diff options
-rw-r--r-- | Cargo.lock | 16 | ||||
-rw-r--r-- | modderbaas/Cargo.toml | 3 | ||||
-rw-r--r-- | modderbaas/src/error.rs | 3 | ||||
-rw-r--r-- | modderbaas/src/kvstore.rs | 173 | ||||
-rw-r--r-- | modderbaas/src/world.rs | 2 |
5 files changed, 182 insertions, 15 deletions
@@ -355,6 +355,15 @@ dependencies = [ ] [[package]] +name = "indoc" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a75aeaaef0ce18b58056d306c27b07436fbb34b8816c53094b76dd81803136" +dependencies = [ + "unindent", +] + +[[package]] name = "instant" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -473,6 +482,7 @@ name = "modderbaas" version = "0.1.0" dependencies = [ "dirs", + "indoc", "itertools", "log", "nix", @@ -1188,6 +1198,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] +name = "unindent" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7" + +[[package]] name = "untrusted" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/modderbaas/Cargo.toml b/modderbaas/Cargo.toml index c07ba82..2c2918c 100644 --- a/modderbaas/Cargo.toml +++ b/modderbaas/Cargo.toml @@ -23,3 +23,6 @@ zip = "0.5.13" [target.'cfg(unix)'.dependencies] nix = "0.23.0" + +[dev-dependencies] +indoc = "1.0.3" diff --git a/modderbaas/src/error.rs b/modderbaas/src/error.rs index 670496c..df78c2f 100644 --- a/modderbaas/src/error.rs +++ b/modderbaas/src/error.rs @@ -44,6 +44,9 @@ pub enum Error { /// ContentDB returned more than one fitting mod for the query. #[error("the mod ID '{0}' does not point to a single mod")] AmbiguousModId(String), + /// The Key-Value store has malformed content. + #[error("syntax error in the metadata")] + MalformedKVStore, /// Wrapper for HTTP errors. #[error("underlying HTTP error")] diff --git a/modderbaas/src/kvstore.rs b/modderbaas/src/kvstore.rs index 006bb94..ee02e2f 100644 --- a/modderbaas/src/kvstore.rs +++ b/modderbaas/src/kvstore.rs @@ -7,21 +7,47 @@ //! side. use std::{collections::HashMap, fs, io::Write, path::Path}; -use super::error::Result; +use super::error::{Error, Result}; /// Read the given file as a KVStore. pub fn read<P: AsRef<Path>>(path: P) -> Result<HashMap<String, String>> { - read_inner(path.as_ref()) + let content = fs::read_to_string(path)?; + parse_kv(&content) } -fn read_inner(path: &Path) -> Result<HashMap<String, String>> { - let content = fs::read_to_string(path)?; +/// Parse the given data as a KVStore. +pub fn parse_kv(content: &str) -> Result<HashMap<String, String>> { + let mut result = HashMap::new(); + let mut current_pair: Option<(String, String)> = None; + for line in content.lines() { + if let Some((current_key, mut current_value)) = current_pair.take() { + if line == r#"""""# { + result.insert(current_key, current_value.trim().into()); + } else { + current_value.push_str(line); + current_value.push('\n'); + current_pair = Some((current_key, current_value)); + } + } else if line.is_empty() { + // Skip empty lines outside of long strings + } else if let [key, value] = line + .splitn(2, '=') + .map(str::trim) + .collect::<Vec<_>>() + .as_slice() + { + let (key, value): (String, String) = (String::from(*key), String::from(*value)); + if value == r#"""""# { + current_pair = Some((key, String::new())); + } else { + result.insert(key, value); + } + } else { + return Err(Error::MalformedKVStore); + } + } - Ok(content - .lines() - .map(|line| line.splitn(2, '=').map(str::trim).collect::<Vec<_>>()) - .map(|v| (v[0].into(), v[1].into())) - .collect()) + Ok(result) } /// Write the given KVStore back to the file. @@ -32,18 +58,137 @@ fn read_inner(path: &Path) -> Result<HashMap<String, String>> { /// 2. All remaining options are saved in alphabetical order. /// /// Note that this function will **override** existing files! -pub fn write<P: AsRef<Path>>(data: &HashMap<String, String>, path: P) -> Result<()> { - write_inner(data, path.as_ref()) +pub fn save<P: AsRef<Path>>(data: &HashMap<String, String>, path: P) -> Result<()> { + let output = fs::File::create(path)?; + write(data, output) } -fn write_inner(data: &HashMap<String, String>, path: &Path) -> Result<()> { +/// Write the given KVStore to a [`Write`]r. +/// +/// See [`save`] for information about the key ordering. +pub fn write<W: Write>(data: &HashMap<String, String>, mut writer: W) -> Result<()> { let mut items = data.iter().collect::<Vec<_>>(); items.sort_by_key(|i| (if i.0.starts_with("load_mod_") { 1 } else { 0 }, i.0)); - let mut output = fs::File::create(path)?; for (key, value) in items { - writeln!(output, "{} = {}", key, value)?; + if !value.contains('\n') { + writeln!(writer, "{} = {}", key, value)?; + } else { + writeln!(writer, r#"{0} = """{1}{2}{1}""""#, key, '\n', value)?; + } } Ok(()) } + +#[cfg(test)] +mod test { + use super::*; + + use indoc::indoc; + + #[test] + fn test_read() { + let text = indoc! {r#" + name = climate_api + title = Climate API + author = TestificateMods + release = 10001 + optional_depends = player_monoids, playerphysics, pova + description = """ + A powerful engine for weather presets and visual effects. + Use the regional climate to set up different effects for different regions. + Control where your effects are activated based on temperature, humidity, wind, + position, light level or a completely custom activator. + Climate API provides temperature and humidity values on a block-per-block basis + that follow the seasons, day / night cycle and random changes. + Make it rain, change the sky or poison the player - it's up to you. + + Climate API requires additional weather packs in order to function. + Try regional_weather for the best experience. + """ + "#}; + let kvstore = parse_kv(text).unwrap(); + let wanted_store = [ + ("name", "climate_api"), + ("title", "Climate API"), + ("author", "TestificateMods"), + ("release", "10001"), + ("optional_depends", "player_monoids, playerphysics, pova"), + ( + "description", + indoc! {" + A powerful engine for weather presets and visual effects. + Use the regional climate to set up different effects for different regions. + Control where your effects are activated based on temperature, humidity, wind, + position, light level or a completely custom activator. + Climate API provides temperature and humidity values on a block-per-block basis + that follow the seasons, day / night cycle and random changes. + Make it rain, change the sky or poison the player - it's up to you. + + Climate API requires additional weather packs in order to function. + Try regional_weather for the best experience.\ + "}, + ), + ] + .iter() + .map(|&(k, v)| (k.into(), v.into())) + .collect::<HashMap<String, String>>(); + assert_eq!(kvstore, wanted_store); + } + + #[test] + fn test_write() { + let mut output = Vec::new(); + let store = [ + ("name", "climate_api"), + ("title", "Climate API"), + ("author", "TestificateMods"), + ("release", "10001"), + ("optional_depends", "player_monoids, playerphysics, pova"), + ( + "description", + indoc! {" + A powerful engine for weather presets and visual effects. + Use the regional climate to set up different effects for different regions. + Control where your effects are activated based on temperature, humidity, wind, + position, light level or a completely custom activator. + Climate API provides temperature and humidity values on a block-per-block basis + that follow the seasons, day / night cycle and random changes. + Make it rain, change the sky or poison the player - it's up to you. + + Climate API requires additional weather packs in order to function. + Try regional_weather for the best experience.\ + "}, + ), + ] + .iter() + .map(|&(k, v)| (k.into(), v.into())) + .collect::<HashMap<String, String>>(); + + write(&store, &mut output).unwrap(); + + let output = std::str::from_utf8(&output).unwrap(); + let expected = indoc! {r#" + author = TestificateMods + description = """ + A powerful engine for weather presets and visual effects. + Use the regional climate to set up different effects for different regions. + Control where your effects are activated based on temperature, humidity, wind, + position, light level or a completely custom activator. + Climate API provides temperature and humidity values on a block-per-block basis + that follow the seasons, day / night cycle and random changes. + Make it rain, change the sky or poison the player - it's up to you. + + Climate API requires additional weather packs in order to function. + Try regional_weather for the best experience. + """ + name = climate_api + optional_depends = player_monoids, playerphysics, pova + release = 10001 + title = Climate API + "#}; + + assert_eq!(output, expected); + } +} diff --git a/modderbaas/src/world.rs b/modderbaas/src/world.rs index 5dce6d0..d51496a 100644 --- a/modderbaas/src/world.rs +++ b/modderbaas/src/world.rs @@ -86,7 +86,7 @@ impl World { conf.entry(key) .and_modify(|e| *e = "true".into()) .or_insert_with(|| "true".into()); - kvstore::write(&conf, &self.path.join("world.mt")) + kvstore::save(&conf, &self.path.join("world.mt")) } } |