Co-authored-by: Wolfgang Bumiller <w.bumil...@proxmox.com> Signed-off-by: Stefan Hanreich <s.hanre...@proxmox.com> --- proxmox-ve-config/src/firewall/parse.rs | 44 +++++ proxmox-ve-config/src/firewall/types/alias.rs | 160 ++++++++++++++++++ proxmox-ve-config/src/firewall/types/mod.rs | 2 + 3 files changed, 206 insertions(+) create mode 100644 proxmox-ve-config/src/firewall/types/alias.rs
diff --git a/proxmox-ve-config/src/firewall/parse.rs b/proxmox-ve-config/src/firewall/parse.rs index a75daee..8e30006 100644 --- a/proxmox-ve-config/src/firewall/parse.rs +++ b/proxmox-ve-config/src/firewall/parse.rs @@ -1,5 +1,49 @@ use anyhow::{bail, format_err, Error}; +/// Parses out a "name" which can be alphanumeric and include dashes. +/// +/// Returns `None` if the name part would be empty. +/// +/// Returns a tuple with the name and the remainder (not trimmed). +pub fn match_name(line: &str) -> Option<(&str, &str)> { + let end = line + .as_bytes() + .iter() + .position(|&b| !(b.is_ascii_alphanumeric() || b == b'-')); + + let (name, rest) = match end { + Some(end) => line.split_at(end), + None => (line, ""), + }; + + if name.is_empty() { + None + } else { + Some((name, rest)) + } +} + +/// Parses up to the next whitespace character or end of the string. +/// +/// Returns `None` if the non-whitespace part would be empty. +/// +/// Returns a tuple containing the parsed section and the *trimmed* remainder. +pub fn match_non_whitespace(line: &str) -> Option<(&str, &str)> { + let (text, rest) = line + .as_bytes() + .iter() + .position(|&b| b.is_ascii_whitespace()) + .map(|pos| { + let (a, b) = line.split_at(pos); + (a, b.trim_start()) + }) + .unwrap_or((line, "")); + if text.is_empty() { + None + } else { + Some((text, rest)) + } +} pub fn parse_bool(value: &str) -> Result<bool, Error> { Ok( if value == "0" diff --git a/proxmox-ve-config/src/firewall/types/alias.rs b/proxmox-ve-config/src/firewall/types/alias.rs new file mode 100644 index 0000000..43c6486 --- /dev/null +++ b/proxmox-ve-config/src/firewall/types/alias.rs @@ -0,0 +1,160 @@ +use std::fmt::Display; +use std::str::FromStr; + +use anyhow::{bail, format_err, Error}; +use serde_with::DeserializeFromStr; + +use crate::firewall::parse::{match_name, match_non_whitespace}; +use crate::firewall::types::address::Cidr; + +#[derive(Debug, Clone)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub enum AliasScope { + Datacenter, + Guest, +} + +impl FromStr for AliasScope { + type Err = Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + Ok(match s { + "dc" => AliasScope::Datacenter, + "guest" => AliasScope::Guest, + _ => bail!("invalid scope for alias: {s}"), + }) + } +} + +impl Display for AliasScope { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + AliasScope::Datacenter => "dc", + AliasScope::Guest => "guest", + }) + } +} + +#[derive(Debug, Clone, DeserializeFromStr)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub struct AliasName { + scope: AliasScope, + name: String, +} + +impl Display for AliasName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}/{}", self.scope, self.name)) + } +} + +impl FromStr for AliasName { + type Err = Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s.split_once('/') { + Some((prefix, name)) if !name.is_empty() => Ok(Self { + scope: prefix.parse()?, + name: name.to_string(), + }), + _ => { + bail!("Invalid Alias name!") + } + } + } +} + +impl AliasName { + pub fn new(scope: AliasScope, name: impl Into<String>) -> Self { + Self { + scope, + name: name.into(), + } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn scope(&self) -> &AliasScope { + &self.scope + } +} + +#[derive(Debug)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub struct Alias { + name: String, + address: Cidr, + comment: Option<String>, +} + +impl Alias { + pub fn new( + name: impl Into<String>, + address: impl Into<Cidr>, + comment: impl Into<Option<String>>, + ) -> Self { + Self { + name: name.into(), + address: address.into(), + comment: comment.into(), + } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn address(&self) -> &Cidr { + &self.address + } + + pub fn comment(&self) -> Option<&str> { + self.comment.as_deref() + } +} + +impl FromStr for Alias { + type Err = Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let (name, line) = + match_name(s.trim_start()).ok_or_else(|| format_err!("expected an alias name"))?; + + let (address, line) = match_non_whitespace(line.trim_start()) + .ok_or_else(|| format_err!("expected a value for alias {name:?}"))?; + + let address: Cidr = address.parse()?; + + let line = line.trim_start(); + + let comment = match line.strip_prefix('#') { + Some(comment) => Some(comment.trim().to_string()), + None if !line.is_empty() => bail!("trailing characters in alias: {line:?}"), + None => None, + }; + + Ok(Alias { + name: name.to_string(), + address, + comment, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_alias_name() { + for name in ["dc/proxmox_123", "guest/proxmox-123"] { + name.parse::<AliasName>().expect("valid alias name"); + } + + for name in ["proxmox/proxmox_123", "guests/proxmox-123", "dc/", "/name"] { + name.parse::<AliasName>().expect_err("invalid alias name"); + } + } +} diff --git a/proxmox-ve-config/src/firewall/types/mod.rs b/proxmox-ve-config/src/firewall/types/mod.rs index 8bf31b8..69b69f4 100644 --- a/proxmox-ve-config/src/firewall/types/mod.rs +++ b/proxmox-ve-config/src/firewall/types/mod.rs @@ -1,5 +1,7 @@ pub mod address; +pub mod alias; pub mod log; pub mod port; pub use address::Cidr; +pub use alias::Alias; -- 2.39.2 _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel