diff --git a/crates/lib/src/rops_file/format/yaml.rs b/crates/lib/src/rops_file/format/yaml.rs index 9bc8fa5..52e57fc 100644 --- a/crates/lib/src/rops_file/format/yaml.rs +++ b/crates/lib/src/rops_file/format/yaml.rs @@ -16,3 +16,94 @@ impl FileFormat for YamlFileFormat { serde_yaml::from_str(str) } } + +mod encrypted_map_to_tree {} + +mod decrypted_map_to_tree { + use indexmap::IndexMap; + use serde_yaml::{Mapping as YamlMap, Value as YamlValue}; + + use crate::*; + + impl TryFrom> for RopsTree { + type Error = DecryptedMapToTreeError; + + fn try_from(rops_file_map: RopsFileMap) -> Result { + return recursive_map_call(rops_file_map.into_inner_map()); + + fn recursive_map_call(yaml_map: YamlMap) -> Result, DecryptedMapToTreeError> { + let mut inner_map = IndexMap::default(); + + for (yaml_key, value_yaml) in yaml_map { + inner_map.insert(validate_key(yaml_key)?, recursive_value_call(value_yaml)?); + } + + return Ok(RopsTree::Map(inner_map)); + + fn validate_key(yaml_value: YamlValue) -> Result { + match yaml_value { + YamlValue::String(string) => Ok(string), + other => Err(DecryptedMapToTreeError::NonStringKey( + serde_yaml::to_string(&other).expect("yaml value not serializable"), + )), + } + } + } + + fn recursive_value_call(yaml_value: YamlValue) -> Result, DecryptedMapToTreeError> { + Ok(match yaml_value { + // SOPS simply throws away tags, so do we for now. + // It can, however, deserialize manually added tags to encrypted documents, + // so we could in theory keep the tags somewhere without breaking SOPS compatability. + YamlValue::Tagged(tagged) => recursive_value_call(tagged.value)?, + YamlValue::Mapping(map) => recursive_map_call(map)?, + YamlValue::Bool(boolean) => RopsTree::Leaf(RopsValue::Boolean(boolean)), + YamlValue::String(string) => RopsTree::Leaf(RopsValue::String(string)), + YamlValue::Number(number) => RopsTree::Leaf(match number.is_f64() { + true => RopsValue::Float(number.as_f64().expect("number not a f64")), + false => RopsValue::Integer( + number + .as_i64() + .ok_or_else(|| DecryptedMapToTreeError::IntegerOutOfRange(number.as_u64().expect("number not an u64")))?, + ), + }), + YamlValue::Sequence(sequence) => { + RopsTree::Sequence(sequence.into_iter().map(recursive_value_call).collect::, _>>()?) + } + YamlValue::Null => RopsTree::Null, + }) + } + } + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn transforms_decrypted_yaml_map() { + assert_eq!( + RopsTree::mock(), + RopsFileMap::::mock().try_into().unwrap() + ) + } + + #[test] + fn dissallows_non_string_keys() { + let file_map = RopsFileMap::from_inner_map(serde_yaml::from_str::("123: 456").unwrap()); + assert!(matches!( + RopsTree::try_from(file_map).unwrap_err(), + DecryptedMapToTreeError::NonStringKey(_) + )) + } + + #[test] + fn dissallows_out_of_range_integers() { + let file_map = RopsFileMap::from_inner_map(serde_yaml::from_str::(&format!("invalid_integer: {}", u64::MAX)).unwrap()); + assert!(matches!( + RopsTree::try_from(file_map).unwrap_err(), + DecryptedMapToTreeError::IntegerOutOfRange(_) + )) + } + } +} diff --git a/crates/lib/src/rops_file/mod.rs b/crates/lib/src/rops_file/mod.rs index dc5fa6c..75b8f64 100644 --- a/crates/lib/src/rops_file/mod.rs +++ b/crates/lib/src/rops_file/mod.rs @@ -11,7 +11,7 @@ mod value; pub use value::*; mod tree; -pub use tree::{RopsTree, RopsTreeBuildError}; +pub use tree::{DecryptedMapToTreeError, EncryptedMapToTreeError, RopsTree}; mod metadata; pub use metadata::*; diff --git a/crates/lib/src/rops_file/tree.rs b/crates/lib/src/rops_file/tree.rs new file mode 100644 index 0000000..db911aa --- /dev/null +++ b/crates/lib/src/rops_file/tree.rs @@ -0,0 +1,54 @@ +use crate::*; + +#[derive(Debug, PartialEq)] +pub enum RopsTree { + Sequence(Vec>), + Map(indexmap::IndexMap>), + Null, + Leaf(S::RopsTreeLeaf), +} + +#[derive(Debug, thiserror::Error)] +pub enum DecryptedMapToTreeError { + #[error("only string keys are supported, found: {0}")] + NonStringKey(String), + #[error("integer out of range, allowed values must fit inside an i64, found: {0}")] + IntegerOutOfRange(u64), +} + +#[derive(Debug, thiserror::Error)] +pub enum EncryptedMapToTreeError {} + +#[cfg(feature = "test-utils")] +mod mock { + use indexmap::indexmap; + + use super::*; + + impl MockTestUtil for RopsTree { + fn mock() -> Self { + Self::Map(indexmap! { + "hello".to_string() => RopsTree::Leaf(RopsValue::String("world!".to_string())), + "nested_map".to_string() => RopsTree::Map(indexmap! { + "null_key".to_string() => RopsTree::Null, + "array".to_string() => RopsTree::Sequence(vec![ + RopsTree::Leaf(RopsValue::String("string".to_string())), + RopsTree::Map(indexmap! { + "nested_map_in_array".to_string() => RopsTree::Map(indexmap!{ + "integer".to_string() => RopsTree::Leaf(RopsValue::Integer(1234)) + }), + }), + RopsTree::Map(indexmap!{ + "float".to_string() => RopsTree::Leaf(RopsValue::Float(1234.56789)) + }), + ]), + } + ), + "booleans".to_string() => RopsTree::Sequence(vec![ + RopsTree::Leaf(RopsValue::Boolean(true)), + RopsTree::Leaf(RopsValue::Boolean(false)) + ]) + }) + } + } +} diff --git a/crates/lib/src/rops_file/tree/core.rs b/crates/lib/src/rops_file/tree/core.rs deleted file mode 100644 index f36f444..0000000 --- a/crates/lib/src/rops_file/tree/core.rs +++ /dev/null @@ -1,9 +0,0 @@ -use crate::*; - -#[derive(Debug, PartialEq)] -pub enum RopsTree { - Sequence(Vec>), - Map(indexmap::IndexMap>), - Null, - Leaf(S::RopsTreeLeaf), -} diff --git a/crates/lib/src/rops_file/tree/error.rs b/crates/lib/src/rops_file/tree/error.rs deleted file mode 100644 index a03aded..0000000 --- a/crates/lib/src/rops_file/tree/error.rs +++ /dev/null @@ -1,7 +0,0 @@ -#[derive(Debug, thiserror::Error)] -pub enum RopsTreeBuildError { - #[error("only string keys are supported, found: {0}")] - NonStringKey(String), - #[error("integer out of range, allowed values must fit inside an i64, found: {0}")] - IntegerOutOfRange(u64), -} diff --git a/crates/lib/src/rops_file/tree/mock.rs b/crates/lib/src/rops_file/tree/mock.rs deleted file mode 100644 index e138cec..0000000 --- a/crates/lib/src/rops_file/tree/mock.rs +++ /dev/null @@ -1,30 +0,0 @@ -use indexmap::indexmap; - -use crate::*; - -impl MockTestUtil for RopsTree { - fn mock() -> Self { - Self::Map(indexmap! { - "hello".to_string() => RopsTree::Leaf(RopsValue::String("world!".to_string())), - "nested_map".to_string() => RopsTree::Map(indexmap! { - "null_key".to_string() => RopsTree::Null, - "array".to_string() => RopsTree::Sequence(vec![ - RopsTree::Leaf(RopsValue::String("string".to_string())), - RopsTree::Map(indexmap! { - "nested_map_in_array".to_string() => RopsTree::Map(indexmap!{ - "integer".to_string() => RopsTree::Leaf(RopsValue::Integer(1234)) - }), - }), - RopsTree::Map(indexmap!{ - "float".to_string() => RopsTree::Leaf(RopsValue::Float(1234.56789)) - }), - ]), - } - ), - "booleans".to_string() => RopsTree::Sequence(vec![ - RopsTree::Leaf(RopsValue::Boolean(true)), - RopsTree::Leaf(RopsValue::Boolean(false)) - ]) - }) - } -} diff --git a/crates/lib/src/rops_file/tree/mod.rs b/crates/lib/src/rops_file/tree/mod.rs deleted file mode 100644 index b30f8c9..0000000 --- a/crates/lib/src/rops_file/tree/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -mod core; -pub use core::RopsTree; - -mod error; -pub use error::RopsTreeBuildError; - -#[cfg(feature = "test-utils")] -mod mock; - -#[cfg(feature = "yaml")] -mod yaml; diff --git a/crates/lib/src/rops_file/tree/yaml.rs b/crates/lib/src/rops_file/tree/yaml.rs deleted file mode 100644 index af9d633..0000000 --- a/crates/lib/src/rops_file/tree/yaml.rs +++ /dev/null @@ -1,86 +0,0 @@ -use indexmap::IndexMap; -use serde_yaml::{Mapping as YamlMap, Value as YamlValue}; - -use crate::*; - -impl TryFrom> for RopsTree { - type Error = RopsTreeBuildError; - - fn try_from(rops_file_map: RopsFileMap) -> Result { - return recursive_map_call(rops_file_map.into_inner_map()); - - fn recursive_map_call(yaml_map: YamlMap) -> Result, RopsTreeBuildError> { - let mut inner_map = IndexMap::default(); - - for (yaml_key, value_yaml) in yaml_map { - inner_map.insert(validate_key(yaml_key)?, recursive_call(value_yaml)?); - } - - return Ok(RopsTree::Map(inner_map)); - - fn validate_key(yaml_value: YamlValue) -> Result { - match yaml_value { - YamlValue::String(string) => Ok(string), - other => Err(RopsTreeBuildError::NonStringKey( - serde_yaml::to_string(&other).expect("yaml value not serializable"), - )), - } - } - } - - fn recursive_call(yaml_value: YamlValue) -> Result, RopsTreeBuildError> { - Ok(match yaml_value { - // SOPS simply throws away tags, so do we for now. - // It can, however, deserialize manually added tags to encrypted documents, - // so we could in theory keep the tags somewhere without breaking SOPS compatability. - YamlValue::Tagged(tagged) => recursive_call(tagged.value)?, - YamlValue::Mapping(map) => recursive_map_call(map)?, - YamlValue::Bool(boolean) => RopsTree::Leaf(RopsValue::Boolean(boolean)), - YamlValue::String(string) => RopsTree::Leaf(RopsValue::String(string)), - YamlValue::Number(number) => RopsTree::Leaf(match number.is_f64() { - true => RopsValue::Float(number.as_f64().expect("number not a f64")), - false => RopsValue::Integer( - number - .as_i64() - .ok_or_else(|| RopsTreeBuildError::IntegerOutOfRange(number.as_u64().expect("number not an u64")))?, - ), - }), - YamlValue::Sequence(sequence) => { - RopsTree::Sequence(sequence.into_iter().map(recursive_call).collect::, _>>()?) - } - YamlValue::Null => RopsTree::Null, - }) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn transforms_decrypted_yaml_map() { - assert_eq!( - RopsTree::mock(), - RopsFileMap::::mock().try_into().unwrap() - ) - } - - #[test] - fn dissallows_non_string_keys() { - let file_map = RopsFileMap::from_inner_map(serde_yaml::from_str::("123: 456").unwrap()); - assert!(matches!( - RopsTree::try_from(file_map).unwrap_err(), - RopsTreeBuildError::NonStringKey(_) - )) - } - - #[test] - fn dissallows_out_of_range_integers() { - let file_map = RopsFileMap::from_inner_map(serde_yaml::from_str::(&format!("invalid_integer: {}", u64::MAX)).unwrap()); - assert!(matches!( - RopsTree::try_from(file_map).unwrap_err(), - RopsTreeBuildError::IntegerOutOfRange(_) - )) - } -}