diff --git a/src/config.rs b/src/config.rs index f20bbfb5..58c30dc7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -19,12 +19,18 @@ use std::cell::LazyCell; use std::collections::HashMap; use std::fmt; use std::fs; -use std::io::{BufRead, BufReader, Read, Write}; -use std::path::PathBuf; +use std::io::{BufRead, BufReader, BufWriter, Read, Seek, SeekFrom, Write}; +use std::path::{Path, PathBuf}; use std::str::FromStr; use configparser::ini::Ini; use jid::Jid; +use nom::{ + bytes::complete::{is_not, tag, take_until1}, + character::complete::space0, + sequence::tuple, + IResult, +}; #[derive(Clone, Debug, Hash, Eq, PartialEq)] pub(crate) enum ConfigValue { @@ -78,6 +84,33 @@ impl fmt::Display for ConfigValue { } } +struct ConfigParsed { + section_found: bool, + key_found: bool, + offset: u64, +} + +impl fmt::Debug for ConfigParsed { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ConfigParsed") + .field("section_found", &self.section_found) + .field("key_found", &self.key_found) + .field("offset", &self.offset) + .finish() + } +} + +fn parse_section(i: &str) -> IResult<&str, &str> { + let (i, (_, _, _, section, _)) = + tuple((space0, tag("["), space0, take_until1("]"), space0))(i)?; + Ok((i, section)) +} + +fn parse_assignment(i: &str) -> IResult<&str, &str> { + let (i, (_, key)) = tuple((space0, is_not("= \t\r\n")))(i)?; + Ok((i, key)) +} + pub(crate) const DEFAULT_CONFIG: LazyCell>> = LazyCell::new(|| HashMap::new()); @@ -118,49 +151,146 @@ impl<'a> Config<'a> { let section: String = section .map(|jid| jid.to_string()) .unwrap_or(String::from("default")); - self.write(key.clone(), value.clone(), section.as_str())?; + self.write_to_file(key.clone(), value.clone(), section.as_str())?; let _ = self.ini.set(section.as_str(), key, Some(value.to_string())); Ok(()) } + fn parse( + mut reader: R, + key: &str, + section: &str, + ) -> Result { + let key = key.trim().to_lowercase(); + let section = section.trim().to_lowercase(); + + let mut line = String::new(); + let mut clear_offset: usize = 0; + let mut sections_found: Vec = Vec::new(); + let mut current_section: Option = None; + let mut key_found = false; + + loop { + let len = reader.read_line(&mut line)?; + + // EOF + if len == 0 { + if current_section == Some(section.clone()) { + sections_found.push(clear_offset); + } + break; + } + + if let Ok((_, new_section)) = parse_section(&line.as_ref()) { + // Save offset of the section we're looking for, before replacing `current_section` + // value. + if current_section == Some(section.clone()) { + sections_found.push(clear_offset); + } + + current_section = Some(String::from(new_section)); + } else if let Ok((_, new_key)) = parse_assignment(line.as_ref()) { + if current_section == Some(section.clone()) && key == new_key { + sections_found.push(clear_offset); + key_found = true; + break; + } + } + + clear_offset = clear_offset + len; + line = String::new(); + } + + let offset = { + if sections_found.len() > 0 { + sections_found[sections_found.len() - 1] + } else { + // If the section doesn't exist, we want to use the latest offset we've seen + clear_offset + } + }; + + Ok(ConfigParsed { + section_found: sections_found.len() > 0, + key_found, + offset: u64::try_from(offset)?, + }) + } + + fn write( + reader: &mut R, + writer: &mut W, + parsed: ConfigParsed, + key: &str, + value: &str, + section: &str, + ) -> Result<(), Error> { + let key = key.trim().to_lowercase(); + let value = value.trim(); + let section = section.trim().to_lowercase(); + + // Write up to offset value + let mut buffer: Vec = Vec::new(); + buffer.resize(usize::try_from(parsed.offset)?, 0u8); + reader.read(&mut buffer)?; + writer.write(&buffer)?; + + // Write new section + if !parsed.section_found { + writer.write(&format!("[{}]\n", section).as_bytes())?; + } + + // Write new key. + writer.write(&format!("{} = {}\n", key, value).as_bytes())?; + + // If key was found in original config, skip it + if parsed.key_found { + { + let mut buffer = String::new(); + let _ = reader.read_line(&mut buffer)?; + } + } + + let mut buffer = String::new(); + reader.read_to_string(&mut buffer)?; + writer.write(buffer.as_bytes())?; + + Ok(()) + } + /// Write config file with the least modification to the original file, and replace the /// original. - fn write(&self, key: &str, value: ConfigValue, _section: &str) -> Result<(), Error> { + pub(crate) fn write_to_file( + &self, + key: &str, + value: ConfigValue, + section: &str, + ) -> Result<(), Error> { + let key = key.trim().to_lowercase(); + let value = format!("{}", value); + let section = section.trim().to_lowercase(); + let tempfile = self.filename.with_extension("tmp"); + // Copy the file - let tempfile = self.filename.clone().with_extension("tmp"); - fs::copy(self.filename.clone(), tempfile.clone())?; - { - // Parse the file, get seek offset - let mut file = fs::File::open(tempfile.clone())?; - let mut reader = BufReader::new(file); - let mut line = String::new(); - let mut clear_offset: u64 = 0; + let in_file = fs::File::open::<&Path>(self.filename.as_ref())?; + let mut reader = BufReader::new(in_file); + let mut out_file = fs::File::create(&tempfile)?; + let mut writer = BufWriter::new(&mut out_file); - let linestart = format!("{} ", key); - loop { - let len = reader.read_line(&mut line)?; - if line.trim().starts_with(linestart.as_str()) { - // Found offset - break; - } - clear_offset = clear_offset + u64::try_from(len)?; // This can fail? - } - - // Keep remainder of the file in memory - let mut remainder = String::new(); - reader.read_to_string(&mut remainder); - - let mut file = reader.into_inner(); - - // Clear file after the found offset. If not offset was found, clears after EOF, and - // the remainder is empty. - file.set_len(clear_offset); - file.write(format!("{} = {}\n", key, value).as_bytes()); - file.write(remainder.as_bytes()); + let parsed = Config::parse(&mut reader, section.as_str(), key.as_str())?; + reader.seek(SeekFrom::Start(0))?; + Config::write( + &mut reader, + &mut writer, + parsed, + key.as_str(), + value.as_str(), + section.as_str(), + )?; } - fs::rename(tempfile, self.filename.clone()); + fs::rename(tempfile, self.filename.clone())?; Ok(()) } @@ -233,3 +363,224 @@ impl<'a> Config<'a> { } } } + +#[cfg(test)] +mod tests { + use super::*; + use std::io::BufReader; + + #[test] + fn parse_section_and_key_exist() { + let file = String::from("[section1]\n key1= value1\n"); + let mut reader = BufReader::new(file.as_bytes()); + let parsed = Config::parse(&mut reader, "key1", "section1").unwrap(); + + assert_eq!(parsed.section_found, true); + assert_eq!(parsed.key_found, true); + assert_eq!(parsed.offset, u64::try_from("[section1]\n".len()).unwrap()); + } + + #[test] + fn parse_section_exists_key_doesnt() { + let file = String::from("[section1]\nkey1 = value1\nkey2 = value2\n"); + let mut reader = BufReader::new(file.as_bytes()); + let parsed = Config::parse(&mut reader, "key3", "section1").unwrap(); + + assert_eq!(parsed.section_found, true); + assert_eq!(parsed.key_found, false); + assert_eq!(parsed.offset, u64::try_from(file.len()).unwrap()); + } + + #[test] + fn parse_section_doesnt_exists_key_does() { + let file = String::from("[section2]\nkey1 = value1\nkey2 = value2\n"); + let mut reader = BufReader::new(file.as_bytes()); + let parsed = Config::parse(&mut reader, "key1", "section1").unwrap(); + + assert_eq!(parsed.section_found, false); + assert_eq!(parsed.key_found, false); + assert_eq!(parsed.offset, u64::try_from(file.len()).unwrap()); + } + + #[test] + fn parse_duplicate_section_key_exists() { + let file = String::from( + r#" + [section1] + key1 = value1 + [section2] + key2 = value2 + [section1] + key3 = value3 + "#, + ); + let offset = u64::try_from( + r#" + [section1] + key1 = value1 + [section2] + key2 = value2 + [section1] +"# + .len(), + ) + .unwrap(); + let mut reader = BufReader::new(file.as_bytes()); + let parsed = Config::parse(&mut reader, "key3", "section1").unwrap(); + + assert_eq!(parsed.section_found, true); + assert_eq!(parsed.key_found, true); + assert_eq!(parsed.offset, offset); + } + + #[test] + fn parse_duplicate_section_key_exists2() { + let file = String::from( + r#" + [section1] + key1 = value1 + [section1] + key2 = value2 + "#, + ); + let offset = u64::try_from( + r#" + [section1] + key1 = value1 + [section1] +"# + .len(), + ) + .unwrap(); + let mut reader = BufReader::new(file.as_bytes()); + let parsed = Config::parse(&mut reader, "key2", "section1").unwrap(); + + assert_eq!(parsed.section_found, true); + assert_eq!(parsed.key_found, true); + assert_eq!(parsed.offset, offset); + } + + #[test] + fn parse_offset() { + let file = String::from( + r#" + [section1] + key1 = value1 + [section2] + key2 = value2 + [section3] + key2 = value2 + "#, + ); + let offset = u64::try_from( + r#" + [section1] + key1 = value1 +"# + .len(), + ) + .unwrap(); + let mut reader = BufReader::new(file.as_bytes()); + let parsed = Config::parse(&mut reader, "key3", "section1").unwrap(); + + assert_eq!(parsed.section_found, true); + assert_eq!(parsed.key_found, false); + assert_eq!(parsed.offset, offset); + } + + #[test] + fn write_assignment() { + let before = String::from("[section1]\n"); + let after = String::from("[section1]\nkey1 = value1\n"); + let parsed = ConfigParsed { + section_found: true, + key_found: false, + offset: u64::try_from("[section1]\n".len()).unwrap(), + }; + + let mut reader = before.as_bytes(); + let mut writer: Vec = vec![]; + Config::write( + &mut reader, + &mut writer, + parsed, + "key1", + "value1", + "section1", + ) + .unwrap(); + assert_eq!(writer, after.as_bytes()); + } + + #[test] + fn write_assignment_remainder() { + let before = String::from("[section1]\n[section2]\n"); + let after = String::from("[section1]\nkey1 = value1\n[section2]\n"); + let parsed = ConfigParsed { + section_found: true, + key_found: false, + offset: u64::try_from("[section1]\n".len()).unwrap(), + }; + + let mut reader = before.as_bytes(); + let mut writer: Vec = vec![]; + Config::write( + &mut reader, + &mut writer, + parsed, + "key1", + "value1", + "section1", + ) + .unwrap(); + assert_eq!(writer, after.as_bytes()); + } + + #[test] + fn write_assignment_key_exists() { + let before = String::from("[section1]\nkey1 = value2\n[section3]\nkey3 = value2\n"); + let after = String::from("[section1]\nkey1 = value1\n[section3]\nkey3 = value2\n"); + let parsed = ConfigParsed { + section_found: true, + key_found: true, + offset: u64::try_from("[section1]\n".len()).unwrap(), + }; + + let mut reader = before.as_bytes(); + let mut writer: Vec = vec![]; + Config::write( + &mut reader, + &mut writer, + parsed, + "key1", + "value1", + "section1", + ) + .unwrap(); + assert_eq!(writer, after.as_bytes()); + } + + #[test] + fn write_assignment_no_section() { + let before = String::from("[section3]\nkey3 = value2\n"); + let after = String::from("[section3]\nkey3 = value2\n[section1]\nkey1 = value1\n"); + let parsed = ConfigParsed { + section_found: false, + key_found: false, + offset: u64::try_from("[section3]\nkey3 = value2\n".len()).unwrap(), + }; + + let mut reader = before.as_bytes(); + let mut writer: Vec = vec![]; + Config::write( + &mut reader, + &mut writer, + parsed, + "key1", + "value1", + "section1", + ) + .unwrap(); + assert_eq!(writer, after.as_bytes()); + } +}