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::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?
|
||||
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(),
|
||||
)?;
|
||||
}
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue