diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index 25fb35586c5..4adbdb0c061 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -22,6 +22,7 @@ brotli = "7.0.0" ed25519-dalek = { version = "2.1.1", features = ["rand_core", "pem"] } hex = "0.4.3" strum = { version = "0.27.1", features = ["derive"] } +strum_macros = { version = "0.27.1" } clap = { version = "4.5.23", features = ["derive", "env"] } jsonschema = "0.28.3" jsonpath-rust = "0.7.5" diff --git a/rust/signed_doc/src/metadata/content_type.rs b/rust/signed_doc/src/metadata/content_type.rs index c2b4eba6679..40f10274c59 100644 --- a/rust/signed_doc/src/metadata/content_type.rs +++ b/rust/signed_doc/src/metadata/content_type.rs @@ -1,37 +1,48 @@ //! Document Payload Content Type. -use std::{ - fmt::{Display, Formatter}, - str::FromStr, -}; +use std::{str::FromStr, string::ToString}; use strum::VariantArray; /// Payload Content Type. -#[derive(Debug, Copy, Clone, PartialEq, Eq, VariantArray)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, VariantArray, strum_macros::Display)] pub enum ContentType { /// `application/cbor` + #[strum(to_string = "application/cbor")] Cbor, /// `application/cddl` + #[strum(to_string = "application/cddl")] Cddl, /// `application/json` + #[strum(to_string = "application/json")] Json, /// `application/json+schema` + #[strum(to_string = "application/json+schema")] JsonSchema, -} - -impl Display for ContentType { - fn fmt( - &self, - f: &mut Formatter<'_>, - ) -> Result<(), std::fmt::Error> { - match self { - Self::Cbor => write!(f, "application/cbor"), - Self::Cddl => write!(f, "application/cddl"), - Self::Json => write!(f, "application/json"), - Self::JsonSchema => write!(f, "application/json+schema"), - } - } + /// `text/css; charset=utf-8` + #[strum(to_string = "text/css; charset=utf-8")] + Css, + /// `text/css; charset=utf-8; template=handlebars` + #[strum(to_string = "text/css; charset=utf-8; template=handlebars")] + CssHandlebars, + /// `text/html; charset=utf-8` + #[strum(to_string = "text/html; charset=utf-8")] + Html, + /// `text/html; charset=utf-8; template=handlebars` + #[strum(to_string = "text/html; charset=utf-8; template=handlebars")] + HtmlHandlebars, + /// `text/markdown; charset=utf-8` + #[strum(to_string = "text/markdown; charset=utf-8")] + Markdown, + /// `text/markdown; charset=utf-8; template=handlebars` + #[strum(to_string = "text/markdown; charset=utf-8; template=handlebars")] + MarkdownHandlebars, + /// `text/plain; charset=utf-8` + #[strum(to_string = "text/plain; charset=utf-8")] + Plain, + /// `text/plain; charset=utf-8; template=handlebars` + #[strum(to_string = "text/plain; charset=utf-8; template=handlebars")] + PlainHandlebars, } impl FromStr for ContentType { @@ -43,6 +54,14 @@ impl FromStr for ContentType { "application/cddl" => Ok(Self::Cddl), "application/json" => Ok(Self::Json), "application/json+schema" => Ok(Self::JsonSchema), + "text/css; charset=utf-8" => Ok(Self::Css), + "text/css; charset=utf-8; template=handlebars" => Ok(Self::CssHandlebars), + "text/html; charset=utf-8" => Ok(Self::Html), + "text/html; charset=utf-8; template=handlebars" => Ok(Self::HtmlHandlebars), + "text/markdown; charset=utf-8" => Ok(Self::Markdown), + "text/markdown; charset=utf-8; template=handlebars" => Ok(Self::MarkdownHandlebars), + "text/plain; charset=utf-8" => Ok(Self::Plain), + "text/plain; charset=utf-8; template=handlebars" => Ok(Self::PlainHandlebars), _ => { anyhow::bail!( "Unsupported Content Type: {s:?}, Supported only: {:?}", @@ -56,6 +75,25 @@ impl FromStr for ContentType { } } +impl TryFrom for ContentType { + type Error = anyhow::Error; + + fn try_from(value: u64) -> Result { + // https://www.iana.org/assignments/core-parameters/core-parameters.xhtml#content-formats + match value { + 0 => Ok(Self::Plain), + 50 => Ok(Self::Json), + 60 => Ok(Self::Cbor), + 20000 => Ok(Self::Css), + _ => { + anyhow::bail!( + "Unsupported CoAP Content-Format: {value}, Supported only: 0, 50, 60, 20000", + ) + }, + } + } +} + impl<'de> serde::Deserialize<'de> for ContentType { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de> { @@ -94,61 +132,114 @@ impl minicbor::Decode<'_, ()> for ContentType { _ctx: &mut (), ) -> Result { let p = d.position(); - match d.int() { - // CoAP Content Format JSON - Ok(val) if val == minicbor::data::Int::from(50_u8) => Ok(Self::Json), - // CoAP Content Format CBOR - Ok(val) if val == minicbor::data::Int::from(60_u8) => Ok(Self::Cbor), - Ok(val) => { - Err(minicbor::decode::Error::message(format!( - "unsupported CoAP Content Formats value: {val}" - ))) - }, - Err(_) => { - d.set_position(p); - d.str()?.parse().map_err(minicbor::decode::Error::message) - }, + if let Ok(val) = d.int() { + let val: u64 = val.try_into().map_err(minicbor::decode::Error::custom)?; + Self::try_from(val).map_err(minicbor::decode::Error::message) + } else { + d.set_position(p); + d.str()?.parse().map_err(minicbor::decode::Error::message) } } } #[cfg(test)] mod tests { + use minicbor::{Decode, Decoder, Encoder}; + use test_case::test_case; + use super::*; - #[test] - fn content_type_string_test() { - assert_eq!( - ContentType::from_str("application/cbor").unwrap(), - ContentType::Cbor - ); - assert_eq!( - ContentType::from_str("application/cddl").unwrap(), - ContentType::Cddl - ); - assert_eq!( - ContentType::from_str("application/json").unwrap(), - ContentType::Json - ); - assert_eq!( - ContentType::from_str("application/json+schema").unwrap(), - ContentType::JsonSchema - ); - assert_eq!( - "application/cbor".parse::().unwrap(), - ContentType::Cbor - ); - assert_eq!( - "application/cddl".parse::().unwrap(), - ContentType::Cddl - ); - assert_eq!( - "application/json".parse::().unwrap(), - ContentType::Json - ); - assert_eq!( - "application/json+schema".parse::().unwrap(), - ContentType::JsonSchema - ); + #[test_case( + ("application/cbor", ContentType::Cbor); + "application/cbor" + )] + #[test_case( + ("application/cddl", ContentType::Cddl); + "application/cddl" + )] + #[test_case( + ("application/json", ContentType::Json); + "application/json" + )] + #[test_case( + ("application/json+schema", ContentType::JsonSchema); + "application/json+schema" + )] + #[test_case( + ("text/css; charset=utf-8", ContentType::Css); + "text/css; charset=utf-8" + )] + #[test_case( + ("text/css; charset=utf-8; template=handlebars", ContentType::CssHandlebars); + "text/css; charset=utf-8; template=handlebars" + )] + #[test_case( + ("text/html; charset=utf-8", ContentType::Html); + "text/html; charset=utf-8" + )] + #[test_case( + ("text/html; charset=utf-8; template=handlebars", ContentType::HtmlHandlebars); + "text/html; charset=utf-8; template=handlebars" + )] + #[test_case( + ("text/markdown; charset=utf-8", ContentType::Markdown); + "text/markdown; charset=utf-8" + )] + #[test_case( + ("text/markdown; charset=utf-8; template=handlebars", ContentType::MarkdownHandlebars); + "text/markdown; charset=utf-8; template=handlebars" + )] + #[test_case( + ("text/plain; charset=utf-8", ContentType::Plain); + "text/plain; charset=utf-8" + )] + #[test_case( + ("text/plain; charset=utf-8; template=handlebars", ContentType::PlainHandlebars); + "text/plain; charset=utf-8; template=handlebars" + )] + fn content_type_string_test((raw_str, variant): (&str, ContentType)) { + // from str + assert_eq!(ContentType::from_str(raw_str).unwrap(), variant); + + // parsing + assert_eq!(raw_str.parse::().unwrap(), variant); + + // decoding from cbor + let mut e = Encoder::new(vec![]); + e.str(raw_str).unwrap(); + let bytes = e.into_writer().clone(); + let mut decoder = Decoder::new(bytes.as_slice()); + + assert_eq!(ContentType::decode(&mut decoder, &mut ()).unwrap(), variant); + } + + #[test_case( + (&[0x00], ContentType::Plain); + "text/plain; charset=utf-8" + )] + #[test_case( + (&[0x18, 0x32], ContentType::Json); + "application/json" + )] + #[test_case( + (&[0x18, 0x3C], ContentType::Cbor); + "application/cbor" + )] + #[test_case( + (&[0x19, 0x4E, 0x20], ContentType::Css); + "text/css; charset=utf-8" + )] + fn cbor_coap_decoding_test((coap_code_bytes, variant): (&[u8], ContentType)) { + let mut decoder = Decoder::new(coap_code_bytes); + assert_eq!(ContentType::decode(&mut decoder, &mut ()).unwrap(), variant); + } + + #[test_case( + &[0x13]; + "application/ace+cbor" + )] + fn cbor_unsupported_coap_decoding_test(coap_code_bytes: &[u8]) { + let mut decoder = Decoder::new(coap_code_bytes); + assert!(ContentType::decode(&mut decoder, &mut ()).is_err()); } } diff --git a/rust/signed_doc/src/validator/rules/content_type.rs b/rust/signed_doc/src/validator/rules/content_type.rs index 765593adba2..313d220cebc 100644 --- a/rust/signed_doc/src/validator/rules/content_type.rs +++ b/rust/signed_doc/src/validator/rules/content_type.rs @@ -63,10 +63,6 @@ impl ContentTypeRule { anyhow::bail!("Invalid {} content: {e}", self.exp) } }, - ContentType::Cddl => { - // TODO: not implemented yet - anyhow::bail!("`application/cddl` is valid but unavailable yet") - }, ContentType::Cbor => { let mut decoder = minicbor::Decoder::new(content); @@ -76,9 +72,18 @@ impl ContentTypeRule { anyhow::bail!("Unused bytes remain in the input after decoding") } }, - ContentType::JsonSchema => { + ContentType::Cddl + | ContentType::JsonSchema + | ContentType::Css + | ContentType::CssHandlebars + | ContentType::Html + | ContentType::HtmlHandlebars + | ContentType::Markdown + | ContentType::MarkdownHandlebars + | ContentType::Plain + | ContentType::PlainHandlebars => { // TODO: not implemented yet - anyhow::bail!("`application/json+schema` is valid but unavailable yet") + anyhow::bail!("`{}` is valid but unavailable yet", self.exp) }, } Ok(()) diff --git a/rust/signed_doc/src/validator/rules/template.rs b/rust/signed_doc/src/validator/rules/template.rs index 06c14a256d1..7f792b5c4eb 100644 --- a/rust/signed_doc/src/validator/rules/template.rs +++ b/rust/signed_doc/src/validator/rules/template.rs @@ -65,7 +65,17 @@ impl ContentRule { }; match template_content_type { ContentType::Json => templated_json_schema_check(doc, &template_doc), - ContentType::Cddl | ContentType::Cbor | ContentType::JsonSchema => { + ContentType::Cddl + | ContentType::Cbor + | ContentType::JsonSchema + | ContentType::Css + | ContentType::CssHandlebars + | ContentType::Html + | ContentType::HtmlHandlebars + | ContentType::Markdown + | ContentType::MarkdownHandlebars + | ContentType::Plain + | ContentType::PlainHandlebars => { // TODO: not implemented yet true },