use crate::error::ParserError;
use crate::interface::{Family, Interface, InterfaceOption, Method};
use std::collections::HashMap;
pub struct Parser;
type ParseResult = Result<(HashMap<String, Interface>, Vec<String>, Vec<String>), ParserError>;
impl Default for Parser {
fn default() -> Self {
Self::new()
}
}
impl Parser {
pub fn new() -> Self {
Parser
}
pub fn parse(&self, content: &str) -> ParseResult {
let mut interfaces = HashMap::new();
let mut current_interface: Option<Interface> = None;
let mut comments = Vec::new();
let mut sources = Vec::new();
for (line_number, line) in content.lines().enumerate() {
let line = line.trim();
if line.starts_with('#') {
if interfaces.is_empty() && current_interface.is_none() {
comments.push(line.to_string());
}
continue;
}
if line.starts_with("source") {
sources.push(line.to_string());
continue;
}
if line.is_empty() {
continue;
}
let tokens: Vec<&str> = line.split_whitespace().collect();
if tokens.is_empty() {
continue;
}
match tokens[0] {
"auto" | "mapping" | "iface" => {
if let Some(iface) = current_interface.take() {
interfaces.insert(iface.name.clone(), iface);
}
}
s if s.starts_with("allow-") => {
if let Some(iface) = current_interface.take() {
interfaces.insert(iface.name.clone(), iface);
}
}
_ => {}
}
match tokens[0] {
"auto" => {
for &iface_name in &tokens[1..] {
if let Some(iface) = interfaces.get_mut(iface_name) {
iface.auto = true;
} else {
interfaces.insert(
iface_name.to_string(),
Interface::builder(iface_name).with_auto(true).build(),
);
}
}
}
s if s.starts_with("allow-") => {
let allow_type = s.strip_prefix("allow-").expect("prefix verified above");
for &iface_name in &tokens[1..] {
if let Some(iface) = interfaces.get_mut(iface_name) {
iface.allow.push(allow_type.to_string());
} else {
let mut iface = Interface::builder(iface_name).build();
iface.allow.push(allow_type.to_string());
interfaces.insert(iface_name.to_string(), iface);
}
}
}
"iface" => {
let iface_name = tokens
.get(1)
.ok_or_else(|| ParserError::new("Missing interface name in 'iface' stanza", line_number + 1))?
.to_string();
let existing_iface = interfaces.remove(&iface_name);
let mut builder = if let Some(existing_iface) = existing_iface {
existing_iface.edit()
} else {
Interface::builder(iface_name.clone())
};
let family = match tokens.get(2) {
Some(s) => s.parse::<Family>().ok(),
None => None,
};
let method: Option<Method> = match tokens.len() {
4 if family.is_some() => Some(tokens[3].parse().expect("Method parse is infallible")),
3 if family.is_none() => Some(tokens[2].parse().expect("Method parse is infallible")),
_ => None,
};
if let Some(family) = family {
builder = builder.with_family(family);
}
if let Some(method) = method {
builder = builder.with_method(method);
}
current_interface = Some(builder.build());
}
"mapping" => {
}
_ => {
if let Some(iface) = &mut current_interface {
let mut tokens = line.split_whitespace();
if let Some(option_name) = tokens.next() {
let option_value = tokens.collect::<Vec<&str>>().join(" ");
iface.options.push(InterfaceOption::from_key_value(option_name, &option_value));
}
} else {
}
}
}
}
if let Some(iface) = current_interface {
interfaces.insert(iface.name.clone(), iface);
}
Ok((interfaces, comments, sources))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::interface::{Family, Method};
#[test]
fn test_parse_iface_without_family_and_method() {
let content = r#"
auto eth0
iface eth0
address 10.130.17.36/255.255.255.128
vrf mgmt
"#;
let parser = Parser::new();
let (interfaces, _comments, _sources) = parser.parse(content).unwrap();
assert!(interfaces.contains_key("eth0"));
let iface = &interfaces["eth0"];
assert_eq!(iface.name, "eth0");
assert_eq!(iface.family, None);
assert_eq!(iface.method, None);
assert!(iface.options.contains(&InterfaceOption::Address(
"10.130.17.36/255.255.255.128".to_string()
)));
assert!(iface
.options
.contains(&InterfaceOption::Vrf("mgmt".to_string())));
}
#[test]
fn test_parse_iface_with_family_and_method() {
let content = r#"
iface eth1 inet static
address 192.168.1.10
netmask 255.255.255.0
"#;
let parser = Parser::new();
let (interfaces, _comments, _sources) = parser.parse(content).unwrap();
assert!(interfaces.contains_key("eth1"));
let iface = &interfaces["eth1"];
assert_eq!(iface.name, "eth1");
assert_eq!(iface.family, Some(Family::Inet));
assert_eq!(iface.method, Some(Method::Static));
assert!(iface
.options
.contains(&InterfaceOption::Address("192.168.1.10".to_string())));
assert!(iface
.options
.contains(&InterfaceOption::Netmask("255.255.255.0".to_string())));
}
#[test]
fn test_parse_multiple_interfaces() {
let content = r#"
auto lo
iface lo inet loopback
auto eth0
iface eth0 inet dhcp
auto wlan0
iface wlan0 inet static
address 192.168.0.100
netmask 255.255.255.0
"#;
let parser = Parser::new();
let (interfaces, _comments, _sources) = parser.parse(content).unwrap();
assert_eq!(interfaces.len(), 3);
let lo_iface = &interfaces["lo"];
assert_eq!(lo_iface.name, "lo");
assert!(lo_iface.auto);
assert_eq!(lo_iface.family, Some(Family::Inet));
assert_eq!(lo_iface.method, Some(Method::Loopback));
let eth0_iface = &interfaces["eth0"];
assert_eq!(eth0_iface.name, "eth0");
assert!(eth0_iface.auto);
assert_eq!(eth0_iface.family, Some(Family::Inet));
assert_eq!(eth0_iface.method, Some(Method::Dhcp));
let wlan0_iface = &interfaces["wlan0"];
assert_eq!(wlan0_iface.name, "wlan0");
assert!(wlan0_iface.auto);
assert_eq!(wlan0_iface.family, Some(Family::Inet));
assert_eq!(wlan0_iface.method, Some(Method::Static));
assert!(wlan0_iface
.options
.contains(&InterfaceOption::Address("192.168.0.100".to_string())));
assert!(wlan0_iface
.options
.contains(&InterfaceOption::Netmask("255.255.255.0".to_string())));
}
#[test]
fn test_parse_multiple_interfaces_strange_order() {
let content = r#"
iface lo inet loopback
iface eth0 inet dhcp
auto eth0
auto wlan0
auto lo
iface wlan0 inet static
address 192.168.0.100
netmask 255.255.255.0
"#;
let parser = Parser::new();
let (interfaces, _comments, _sources) = parser.parse(content).unwrap();
assert_eq!(interfaces.len(), 3);
let lo_iface = &interfaces["lo"];
assert_eq!(lo_iface.name, "lo");
assert!(lo_iface.auto);
assert_eq!(lo_iface.family, Some(Family::Inet));
assert_eq!(lo_iface.method, Some(Method::Loopback));
let eth0_iface = &interfaces["eth0"];
assert_eq!(eth0_iface.name, "eth0");
assert!(eth0_iface.auto);
assert_eq!(eth0_iface.family, Some(Family::Inet));
assert_eq!(eth0_iface.method, Some(Method::Dhcp));
let wlan0_iface = &interfaces["wlan0"];
assert_eq!(wlan0_iface.name, "wlan0");
assert!(wlan0_iface.auto);
assert_eq!(wlan0_iface.family, Some(Family::Inet));
assert_eq!(wlan0_iface.method, Some(Method::Static));
assert!(wlan0_iface
.options
.contains(&InterfaceOption::Address("192.168.0.100".to_string())));
assert!(wlan0_iface
.options
.contains(&InterfaceOption::Netmask("255.255.255.0".to_string())));
}
#[test]
fn test_parse_multiple_interfaces_cumulus() {
let content = r#"
auto swp54
iface swp54
bridge-access 199
mstpctl-bpduguard yes
mstpctl-portadminedge yes
mtu 9216
post-down /some/script.sh
post-up /some/script.sh
auto bridge
iface bridge
bridge-ports swp1 swp2 swp3 swp4 swp5 swp6 swp7 swp8 swp9 swp10 swp11 swp12 swp13 swp14 swp15 swp16 swp17 swp18 swp19 swp20 swp21 swp22 swp23 swp24 swp31 swp32 swp33 swp34 swp35 swp36 swp37 swp38 swp39 swp40 swp41 swp42 swp43 swp44 swp45 swp46 swp47 swp48 swp49 swp50 swp51 swp52 swp53 swp54
bridge-pvid 1
bridge-vids 100-154 199
bridge-vlan-aware yes
auto mgmt
iface mgmt
address 127.0.0.1/8
address ::1/128
vrf-table auto
auto vlan101
iface vlan101
mtu 9216
post-up /some/script.sh
vlan-id 101
vlan-raw-device bridge
"#;
let parser = Parser::new();
let (interfaces, _comments, _sources) = parser.parse(content).unwrap();
assert_eq!(interfaces.len(), 4);
let swp54_iface = &interfaces["swp54"];
assert_eq!(swp54_iface.name, "swp54");
assert_eq!(swp54_iface.auto, true);
assert_eq!(swp54_iface.family, None);
assert_eq!(swp54_iface.method, None);
assert!(swp54_iface
.options
.contains(&InterfaceOption::BridgeAccess(199)));
assert!(swp54_iface
.options
.contains(&InterfaceOption::MstpctlBpduguard(true)));
assert!(swp54_iface
.options
.contains(&InterfaceOption::MstpctlPortadminedge(true)));
assert!(swp54_iface
.options
.contains(&InterfaceOption::Mtu(9216)));
assert!(swp54_iface
.options
.contains(&InterfaceOption::PostDown("/some/script.sh".to_string())));
assert!(swp54_iface
.options
.contains(&InterfaceOption::PostUp("/some/script.sh".to_string())));
let bridge_iface = &interfaces["bridge"];
assert_eq!(bridge_iface.name, "bridge");
assert_eq!(bridge_iface.auto, true);
assert_eq!(bridge_iface.family, None);
assert_eq!(bridge_iface.method, None);
assert!(bridge_iface.options.contains(&InterfaceOption::BridgePorts(vec![
"swp1".to_string(), "swp2".to_string(), "swp3".to_string(), "swp4".to_string(),
"swp5".to_string(), "swp6".to_string(), "swp7".to_string(), "swp8".to_string(),
"swp9".to_string(), "swp10".to_string(), "swp11".to_string(), "swp12".to_string(),
"swp13".to_string(), "swp14".to_string(), "swp15".to_string(), "swp16".to_string(),
"swp17".to_string(), "swp18".to_string(), "swp19".to_string(), "swp20".to_string(),
"swp21".to_string(), "swp22".to_string(), "swp23".to_string(), "swp24".to_string(),
"swp31".to_string(), "swp32".to_string(), "swp33".to_string(), "swp34".to_string(),
"swp35".to_string(), "swp36".to_string(), "swp37".to_string(), "swp38".to_string(),
"swp39".to_string(), "swp40".to_string(), "swp41".to_string(), "swp42".to_string(),
"swp43".to_string(), "swp44".to_string(), "swp45".to_string(), "swp46".to_string(),
"swp47".to_string(), "swp48".to_string(), "swp49".to_string(), "swp50".to_string(),
"swp51".to_string(), "swp52".to_string(), "swp53".to_string(), "swp54".to_string(),
])));
assert!(bridge_iface
.options
.contains(&InterfaceOption::BridgePvid(1)));
assert!(bridge_iface
.options
.contains(&InterfaceOption::BridgeVids("100-154 199".to_string())));
assert!(bridge_iface
.options
.contains(&InterfaceOption::BridgeVlanAware(true)));
let mgmt_iface = &interfaces["mgmt"];
assert_eq!(mgmt_iface.name, "mgmt");
assert_eq!(mgmt_iface.auto, true);
assert_eq!(mgmt_iface.family, None);
assert_eq!(mgmt_iface.method, None);
assert!(mgmt_iface
.options
.contains(&InterfaceOption::Address("127.0.0.1/8".to_string())));
assert!(mgmt_iface
.options
.contains(&InterfaceOption::Address("::1/128".to_string())));
assert!(mgmt_iface
.options
.contains(&InterfaceOption::VrfTable("auto".to_string())));
let vlan101_iface = &interfaces["vlan101"];
assert_eq!(vlan101_iface.name, "vlan101");
assert_eq!(vlan101_iface.auto, true);
assert_eq!(vlan101_iface.family, None);
assert_eq!(vlan101_iface.method, None);
assert!(vlan101_iface
.options
.contains(&InterfaceOption::Mtu(9216)));
assert!(vlan101_iface
.options
.contains(&InterfaceOption::PostUp("/some/script.sh".to_string())));
assert!(vlan101_iface
.options
.contains(&InterfaceOption::VlanId(101)));
assert!(vlan101_iface
.options
.contains(&InterfaceOption::VlanRawDevice("bridge".to_string())));
let mut output = String::new();
output.push_str(&format!("{}\n", swp54_iface));
output.push_str(&format!("{}\n", bridge_iface));
output.push_str(&format!("{}\n", mgmt_iface));
output.push_str(&format!("{}\n", vlan101_iface));
let expected_output = r#"
auto swp54
iface swp54
bridge-access 199
mstpctl-bpduguard yes
mstpctl-portadminedge yes
mtu 9216
post-down /some/script.sh
post-up /some/script.sh
auto bridge
iface bridge
bridge-ports swp1 swp2 swp3 swp4 swp5 swp6 swp7 swp8 swp9 swp10 swp11 swp12 swp13 swp14 swp15 swp16 swp17 swp18 swp19 swp20 swp21 swp22 swp23 swp24 swp31 swp32 swp33 swp34 swp35 swp36 swp37 swp38 swp39 swp40 swp41 swp42 swp43 swp44 swp45 swp46 swp47 swp48 swp49 swp50 swp51 swp52 swp53 swp54
bridge-pvid 1
bridge-vids 100-154 199
bridge-vlan-aware yes
auto mgmt
iface mgmt
address 127.0.0.1/8
address ::1/128
vrf-table auto
auto vlan101
iface vlan101
mtu 9216
post-up /some/script.sh
vlan-id 101
vlan-raw-device bridge
"#;
let expected_output = expected_output.trim();
let output = output.trim();
assert_eq!(output, expected_output);
}
#[test]
fn test_parse_comments_and_sources() {
let content = r#"# This is a comment
# Another comment at the top
source /etc/network/interfaces.d/*
source-directory /etc/network/interfaces.d
auto lo
iface lo inet loopback
"#;
let parser = Parser::new();
let (interfaces, comments, sources) = parser.parse(content).unwrap();
assert_eq!(comments.len(), 2);
assert_eq!(comments[0], "# This is a comment");
assert_eq!(comments[1], "# Another comment at the top");
assert_eq!(sources.len(), 2);
assert_eq!(sources[0], "source /etc/network/interfaces.d/*");
assert_eq!(sources[1], "source-directory /etc/network/interfaces.d");
assert_eq!(interfaces.len(), 1);
assert!(interfaces.contains_key("lo"));
}
#[test]
fn test_parse_allow_hotplug() {
let content = r#"
auto eth0
allow-hotplug eth0
iface eth0 inet dhcp
"#;
let parser = Parser::new();
let (interfaces, _comments, _sources) = parser.parse(content).unwrap();
let eth0 = &interfaces["eth0"];
assert!(eth0.auto);
assert_eq!(eth0.allow, vec!["hotplug"]);
assert_eq!(eth0.method, Some(Method::Dhcp));
}
#[test]
fn test_parse_empty_content() {
let parser = Parser::new();
let (interfaces, comments, sources) = parser.parse("").unwrap();
assert!(interfaces.is_empty());
assert!(comments.is_empty());
assert!(sources.is_empty());
}
#[test]
fn test_parse_only_comments() {
let content = "# Just a comment\n# And another";
let parser = Parser::new();
let (interfaces, comments, sources) = parser.parse(content).unwrap();
assert!(interfaces.is_empty());
assert_eq!(comments.len(), 2);
assert!(sources.is_empty());
}
}