roezio/config: Split write method and add tests

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
This commit is contained in:
Maxime “pep” Buquet 2022-08-28 14:56:33 +02:00
parent 47530a5506
commit 6e94a57eb9

View file

@ -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<HashMap<&str, HashMap<&str, ConfigValue>>> =
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<R: Read + BufRead>(
mut reader: R,
key: &str,
section: &str,
) -> Result<ConfigParsed, Error> {
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<usize> = Vec::new();
let mut current_section: Option<String> = 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<R: Read + BufRead, W: 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<u8> = 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<u8> = 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<u8> = 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<u8> = 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<u8> = vec![];
Config::write(
&mut reader,
&mut writer,
parsed,
"key1",
"value1",
"section1",
)
.unwrap();
assert_eq!(writer, after.as_bytes());
}
}