aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock16
-rw-r--r--modderbaas/Cargo.toml3
-rw-r--r--modderbaas/src/error.rs3
-rw-r--r--modderbaas/src/kvstore.rs173
-rw-r--r--modderbaas/src/world.rs2
5 files changed, 182 insertions, 15 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 0140ae6..86e8fd5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"))
}
}