Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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(
(vec![0x00], ContentType::Plain);
"text/plain; charset=utf-8"
)]
#[test_case(
(vec![0x18, 0x32], ContentType::Json);
"application/json"
)]
#[test_case(
(vec![0x18, 0x3C], ContentType::Cbor);
"application/cbor"
)]
#[test_case(
(vec![0x19, 0x4E, 0x20], ContentType::Css);
"text/css; charset=utf-8"
)]
fn cbor_coap_decoding_test((coap_code_bytes, variant): (Vec<u8>, ContentType)) {
let mut decoder = Decoder::new(coap_code_bytes.as_slice());
assert_eq!(ContentType::decode(&mut decoder, &mut ()).unwrap(), variant);
}

#[test_case(
vec![0x13];
"application/ace+cbor"
)]
fn cbor_unsupported_coap_decoding_test(coap_code_bytes: Vec<u8>) {
let mut decoder = Decoder::new(coap_code_bytes.as_slice());
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