roezio/config: Split write method and add tests
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
This commit is contained in:
parent
47530a5506
commit
6e94a57eb9
1 changed files with 385 additions and 34 deletions
419
src/config.rs
419
src/config.rs
|
@ -19,12 +19,18 @@ use std::cell::LazyCell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{BufRead, BufReader, Read, Write};
|
use std::io::{BufRead, BufReader, BufWriter, Read, Seek, SeekFrom, Write};
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use configparser::ini::Ini;
|
use configparser::ini::Ini;
|
||||||
use jid::Jid;
|
use jid::Jid;
|
||||||
|
use nom::{
|
||||||
|
bytes::complete::{is_not, tag, take_until1},
|
||||||
|
character::complete::space0,
|
||||||
|
sequence::tuple,
|
||||||
|
IResult,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||||
pub(crate) enum ConfigValue {
|
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>>> =
|
pub(crate) const DEFAULT_CONFIG: LazyCell<HashMap<&str, HashMap<&str, ConfigValue>>> =
|
||||||
LazyCell::new(|| HashMap::new());
|
LazyCell::new(|| HashMap::new());
|
||||||
|
|
||||||
|
@ -118,49 +151,146 @@ impl<'a> Config<'a> {
|
||||||
let section: String = section
|
let section: String = section
|
||||||
.map(|jid| jid.to_string())
|
.map(|jid| jid.to_string())
|
||||||
.unwrap_or(String::from("default"));
|
.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()));
|
let _ = self.ini.set(section.as_str(), key, Some(value.to_string()));
|
||||||
Ok(())
|
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
|
/// Write config file with the least modification to the original file, and replace the
|
||||||
/// original.
|
/// 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
|
// 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 in_file = fs::File::open::<&Path>(self.filename.as_ref())?;
|
||||||
let mut file = fs::File::open(tempfile.clone())?;
|
let mut reader = BufReader::new(in_file);
|
||||||
let mut reader = BufReader::new(file);
|
let mut out_file = fs::File::create(&tempfile)?;
|
||||||
let mut line = String::new();
|
let mut writer = BufWriter::new(&mut out_file);
|
||||||
let mut clear_offset: u64 = 0;
|
|
||||||
|
|
||||||
let linestart = format!("{} ", key);
|
let parsed = Config::parse(&mut reader, section.as_str(), key.as_str())?;
|
||||||
loop {
|
reader.seek(SeekFrom::Start(0))?;
|
||||||
let len = reader.read_line(&mut line)?;
|
Config::write(
|
||||||
if line.trim().starts_with(linestart.as_str()) {
|
&mut reader,
|
||||||
// Found offset
|
&mut writer,
|
||||||
break;
|
parsed,
|
||||||
}
|
key.as_str(),
|
||||||
clear_offset = clear_offset + u64::try_from(len)?; // This can fail?
|
value.as_str(),
|
||||||
|
section.as_str(),
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep remainder of the file in memory
|
fs::rename(tempfile, self.filename.clone())?;
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
fs::rename(tempfile, self.filename.clone());
|
|
||||||
|
|
||||||
Ok(())
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue