Skip to content

Commit

Permalink
add comment preservation and source support
Browse files Browse the repository at this point in the history
  • Loading branch information
wiggels committed Nov 14, 2024
1 parent 141e804 commit 534bf5b
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 21 deletions.
109 changes: 98 additions & 11 deletions src/network_interfaces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use std::fmt;
/// let mut net_ifaces = NetworkInterfaces::load("/path/to/interfaces").unwrap();
///
/// // Modify an interface
/// if let Some(iface) = net_ifaces.interfaces.get_mut("eth0") {
/// if let Some(iface) = net_ifaces.get_interface_mut("eth0") {
/// iface.method = Some("static".to_string());
/// iface.options.push(("address".to_string(), "192.168.1.100".to_string()));
/// }
Expand All @@ -35,14 +35,35 @@ use std::fmt;
#[derive(Debug)]
pub struct NetworkInterfaces {
/// A mapping of interface names to their configurations.
pub interfaces: HashMap<String, Interface>,
interfaces: HashMap<String, Interface>,
/// The path to the interfaces file.
path: Option<PathBuf>,
/// The last modified time of the interfaces file.
last_modified: Option<SystemTime>,
/// Comments from the original file
comments: Vec<String>,
/// Source directives from the original file
sources: Vec<String>,
}

impl NetworkInterfaces {
/// Creates a new `NetworkInterfaces` instance.
fn new(
interfaces: HashMap<String, Interface>,
comments: Vec<String>,
sources: Vec<String>,
path: Option<PathBuf>,
last_modified: Option<SystemTime>,
) -> Self {
NetworkInterfaces {
interfaces,
comments,
sources,
path,
last_modified,
}
}

/// Loads the `interfaces(5)` file into memory.
///
/// # Arguments
Expand All @@ -63,13 +84,15 @@ impl NetworkInterfaces {

let content = fs::read_to_string(&path_buf)?;
let parser = Parser::new();
let interfaces = parser.parse(&content)?;
let (interfaces, comments, sources) = parser.parse(&content)?;

Ok(NetworkInterfaces {
Ok(NetworkInterfaces::new(
interfaces,
path: Some(path_buf),
last_modified: Some(last_modified),
})
comments,
sources,
Some(path_buf),
Some(last_modified),
))
}

/// Retrieves a reference to an interface by name.
Expand All @@ -85,6 +108,19 @@ impl NetworkInterfaces {
self.interfaces.get(name)
}

/// Retrieves a mutable reference to an interface by name.
///
/// # Arguments
///
/// * `name` - The name of the interface.
///
/// # Returns
///
/// An `Option` containing a mutable reference to the `Interface` if found.
pub fn get_interface_mut(&mut self, name: &str) -> Option<&mut Interface> {
self.interfaces.get_mut(name)
}

/// Adds or updates an interface in the collection.
///
/// # Arguments
Expand All @@ -103,6 +139,16 @@ impl NetworkInterfaces {
self.interfaces.remove(name);
}

/// Returns the number of interfaces.
pub fn len(&self) -> usize {
self.interfaces.len()
}

/// Checks if the collection is empty.
pub fn is_empty(&self) -> bool {
self.interfaces.is_empty()
}

/// Saves changes back to the `interfaces(5)` file.
///
/// # Errors
Expand Down Expand Up @@ -130,10 +176,24 @@ impl NetworkInterfaces {

let mut file = fs::File::create(&path)?;

// Write comments at the top
for comment in &self.comments {
writeln!(file, "{}", comment)?;
}

// Write source directives
for source in &self.sources {
writeln!(file, "{}", source)?;
}

// Collect interfaces into a vector and sort them by name
let mut interfaces: Vec<&Interface> = self.interfaces.values().collect();
interfaces.sort_by(|a, b| a.name.cmp(&b.name));

// Write interfaces to file
for iface in self.interfaces.values() {
for iface in interfaces {
writeln!(file)?;
write!(file, "{}", iface)?;
writeln!(file)?; // Blank line between interfaces
}

// Update last_modified
Expand All @@ -157,6 +217,8 @@ impl NetworkInterfaces {
};
let reloaded = NetworkInterfaces::load(&path)?;
self.interfaces = reloaded.interfaces;
self.comments = reloaded.comments;
self.sources = reloaded.sources;
self.last_modified = reloaded.last_modified;
Ok(())
}
Expand All @@ -165,9 +227,34 @@ impl NetworkInterfaces {
// Implement Display for NetworkInterfaces to allow easy printing
impl fmt::Display for NetworkInterfaces {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for iface in self.interfaces.values() {
write!(f, "{}\n", iface)?;
// Print comments at the top if any
for comment in &self.comments {
writeln!(f, "{}", comment)?;
}

// Print source directives if any
for source in &self.sources {
writeln!(f, "{}", source)?;
}

// Collect interfaces into a vector and sort them by name
let mut interfaces: Vec<&Interface> = self.interfaces.values().collect();
interfaces.sort_by(|a, b| a.name.cmp(&b.name));

// Print interfaces
for iface in interfaces {
writeln!(f)?;
write!(f, "{}", iface)?;
}
Ok(())
}
}

// Implement methods to access interfaces directly if needed
impl NetworkInterfaces {
/// Returns an iterator over the interfaces.
pub fn iter(&self) -> impl Iterator<Item = (&String, &Interface)> {
self.interfaces.iter()
}
}

36 changes: 26 additions & 10 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,37 @@ impl Parser {
///
/// # Returns
///
/// A `Result` containing a `HashMap<String, Interface>` if successful,
/// A `Result` containing a tuple `(interfaces, comments, sources)` if successful,
/// or a `ParserError` if parsing fails.
pub fn parse(
&self,
content: &str,
) -> Result<HashMap<String, Interface>, ParserError> {
) -> Result<(HashMap<String, Interface>, Vec<String>, Vec<String>), ParserError> {
let mut interfaces = HashMap::new();
let mut lines = content.lines().enumerate().peekable();
let mut current_interface: Option<Interface> = None;
let mut comments = Vec::new();
let mut sources = Vec::new();

while let Some((line_number, line)) = lines.next() {
let line = line.trim();

// Skip empty lines and comments
if line.is_empty() || line.starts_with('#') {
// Collect comments at the top
if line.starts_with('#') {
if interfaces.is_empty() && current_interface.is_none() {
comments.push(line.to_string());
}
continue;
}

// Collect source directives
if line.starts_with("source") {
sources.push(line.to_string());
continue;
}

// Skip empty lines
if line.is_empty() {
continue;
}

Expand Down Expand Up @@ -156,7 +172,7 @@ impl Parser {
interfaces.insert(iface.name.clone(), iface);
}

Ok(interfaces)
Ok((interfaces, comments, sources))
}
}

Expand All @@ -174,7 +190,7 @@ iface eth0
vrf mgmt
"#;
let parser = Parser::new();
let interfaces = parser.parse(content).unwrap();
let (interfaces, _comments, _sources) = parser.parse(content).unwrap();
assert!(interfaces.contains_key("eth0"));
let iface = &interfaces["eth0"];
assert_eq!(iface.name, "eth0");
Expand All @@ -192,7 +208,7 @@ iface eth1 inet static
netmask 255.255.255.0
"#;
let parser = Parser::new();
let interfaces = parser.parse(content).unwrap();
let (interfaces, _comments, _sources) = parser.parse(content).unwrap();
assert!(interfaces.contains_key("eth1"));
let iface = &interfaces["eth1"];
assert_eq!(iface.name, "eth1");
Expand All @@ -217,7 +233,7 @@ iface wlan0 inet static
netmask 255.255.255.0
"#;
let parser = Parser::new();
let interfaces = parser.parse(content).unwrap();
let (interfaces, _comments, _sources) = parser.parse(content).unwrap();

assert_eq!(interfaces.len(), 3);

Expand Down Expand Up @@ -259,7 +275,7 @@ iface wlan0 inet static
netmask 255.255.255.0
"#;
let parser = Parser::new();
let interfaces = parser.parse(content).unwrap();
let (interfaces, _comments, _sources) = parser.parse(content).unwrap();

assert_eq!(interfaces.len(), 3);

Expand Down Expand Up @@ -320,7 +336,7 @@ iface vlan101
vlan-raw-device bridge
"#;
let parser = Parser::new();
let interfaces = parser.parse(content).unwrap();
let (interfaces, _comments, _sources) = parser.parse(content).unwrap();

assert_eq!(interfaces.len(), 4);

Expand Down

0 comments on commit 534bf5b

Please sign in to comment.