Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions rust/signed_doc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
225 changes: 158 additions & 67 deletions rust/signed_doc/src/metadata/content_type.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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: {:?}",
Expand All @@ -56,6 +75,25 @@ impl FromStr for ContentType {
}
}

impl TryFrom<u64> for ContentType {
type Error = anyhow::Error;

fn try_from(value: u64) -> Result<Self, Self::Error> {
// 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<D>(deserializer: D) -> Result<Self, D::Error>
where D: serde::Deserializer<'de> {
Expand Down Expand Up @@ -94,61 +132,114 @@ impl minicbor::Decode<'_, ()> for ContentType {
_ctx: &mut (),
) -> Result<Self, minicbor::decode::Error> {
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::<ContentType>().unwrap(),
ContentType::Cbor
);
assert_eq!(
"application/cddl".parse::<ContentType>().unwrap(),
ContentType::Cddl
);
assert_eq!(
"application/json".parse::<ContentType>().unwrap(),
ContentType::Json
);
assert_eq!(
"application/json+schema".parse::<ContentType>().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::<ContentType>().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());
}
}
17 changes: 11 additions & 6 deletions rust/signed_doc/src/validator/rules/content_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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(())
Expand Down
12 changes: 11 additions & 1 deletion rust/signed_doc/src/validator/rules/template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down
Loading