diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf6d9d8..0095d43 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Formatting checks run: cargo fmt --all -- --check - name: Clippy checks diff --git a/Cargo.toml b/Cargo.toml index 09a9775..70107e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ear" description = "EAT Attestation Results implementation" -version = "0.3.0" +version = "0.4.0" edition = "2021" repository = "https://github.com/veraison/rust-ear" readme = "README.md" @@ -20,4 +20,4 @@ openssl = "0.10.54" phf = {version = "0.11.1", features = ["macros", "serde"]} serde = {version = "1.0", features = ["derive"]} serde_json = {version = "1.0.93", features = ["raw_value"]} -thiserror = "1.0.40" +thiserror = "2.0.16" diff --git a/src/base64.rs b/src/base64.rs index 45db55d..33e3f8c 100644 --- a/src/base64.rs +++ b/src/base64.rs @@ -1,5 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 +use std::fmt::Display; + use base64::{self, engine::general_purpose, Engine as _}; use serde::{ de::{self, Deserialize, Visitor}, @@ -17,7 +19,7 @@ pub fn decode_str(v: &str) -> Result, Error> { /// a `Vec` encoded as base64 in human readable serialization #[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord)] -pub struct Bytes(Vec); +pub struct Bytes(pub Vec); impl Bytes { pub fn new() -> Self { @@ -100,3 +102,25 @@ impl Visitor<'_> for BytesVisitor { Ok(Bytes::from(v)) } } + +impl Display for Bytes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0 + .iter() + .map(|v| format!("{:02x}", v)) + .collect::>() + .join("") + .fmt(f) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn to_string() { + let bytes = Bytes(vec![222u8, 173u8, 190u8, 239u8]); + assert_eq!(bytes.to_string(), "deadbeef".to_string()); + } +} diff --git a/src/raw.rs b/src/raw.rs index bc9ea41..56b31f8 100644 --- a/src/raw.rs +++ b/src/raw.rs @@ -5,6 +5,9 @@ // - tags are stripped when serializing to JSON // - byte strings are written as base64-encoded strings to JSON (meaning they deserialize as // text strings, losing their original type). + +use std::fmt::Display; + use serde::de::{self, Deserialize, EnumAccess, MapAccess, SeqAccess, Visitor}; use serde::ser::{Serialize, Serializer}; use serde::ser::{SerializeMap as _, SerializeSeq as _, SerializeTupleVariant as _}; @@ -251,6 +254,35 @@ impl<'de> Visitor<'de> for RawValueVisitor { } } +impl Display for RawValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Null => "Null".to_string(), + Self::Integer(v) => v.to_string(), + Self::Float(v) => v.to_string(), + Self::Bool(v) => v.to_string(), + Self::Bytes(b) => b.to_string(), + Self::String(s) => format!(r#""{}""#, s), + Self::Array(vs) => format!( + r#"[{}]"#, + vs.iter() + .map(|v| v.to_string()) + .collect::>() + .join(", ") + ), + Self::Map(vs) => format!( + r#"{{{}}}"#, + vs.iter() + .map(|(k, v)| format!("{0}: {1}", k, v)) + .collect::>() + .join(", ") + ), + Self::Tagged(t, v) => format!("#6.{0}({1})", t, v), + } + .fmt(f) + } +} + #[cfg(test)] mod test { use super::*; @@ -395,4 +427,39 @@ mod test { buf ); } + + #[test] + fn to_string() { + assert_eq!(RawValue::Null.to_string(), "Null".to_string()); + assert_eq!(RawValue::Integer(234).to_string(), "234".to_string()); + assert_eq!(RawValue::Float(1.5).to_string(), "1.5".to_string()); + assert_eq!( + RawValue::String("foo".to_string()).to_string(), + r#""foo""#.to_string() + ); + assert_eq!( + RawValue::Tagged(7, Box::new(RawValue::Bool(true))).to_string(), + "#6.7(true)".to_string() + ); + assert_eq!( + RawValue::Bytes(Bytes(vec![222u8, 173u8, 190u8, 239u8])).to_string(), + "deadbeef".to_string() + ); + assert_eq!( + RawValue::Array(vec![ + RawValue::String("foo".to_string()), + RawValue::String("bar".to_string()) + ]) + .to_string(), + r#"["foo", "bar"]"#.to_string() + ); + assert_eq!( + RawValue::Map(vec![ + (RawValue::String("foo".to_string()), RawValue::Integer(1)), + (RawValue::String("bar".to_string()), RawValue::Integer(2)), + ]) + .to_string(), + r#"{"foo": 1, "bar": 2}"#, + ) + } } diff --git a/src/trust/claim.rs b/src/trust/claim.rs index 85bc438..4441c0d 100644 --- a/src/trust/claim.rs +++ b/src/trust/claim.rs @@ -340,7 +340,7 @@ pub static SOURCED_DATA_CLAIM_MAP: &Map> = &phf_ma /// This is a claim regarding the trustworthiness of one aspect of the attested environment, as /// defined in /// -#[derive(Debug, Clone, Copy)] +#[derive(Clone, Copy)] pub struct TrustClaim { /// Claim value pub value: Option, @@ -464,6 +464,18 @@ impl TrustClaim { } } +impl std::fmt::Debug for TrustClaim { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + r#"TrustClaim{{"{0}({1})": {2}}}"#, + self.tag(), + self.key(), + self.value() + ) + } +} + impl PartialEq for TrustClaim { fn eq(&self, other: &TrustClaim) -> bool { self.value() == other.value() diff --git a/src/trust/tier.rs b/src/trust/tier.rs index 9e9698c..aa12587 100644 --- a/src/trust/tier.rs +++ b/src/trust/tier.rs @@ -1,5 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 +use crate::error::Error; + use serde::{ de::{self, Visitor}, ser::{Serialize, Serializer}, @@ -25,19 +27,9 @@ impl Serialize for TrustTier { S: Serializer, { if serializer.is_human_readable() { - match self { - TrustTier::None => serializer.serialize_str("none"), - TrustTier::Affirming => serializer.serialize_str("affirming"), - TrustTier::Warning => serializer.serialize_str("warning"), - TrustTier::Contraindicated => serializer.serialize_str("contraindicated"), - } + serializer.serialize_str(self.into()) } else { - match self { - TrustTier::None => serializer.serialize_i8(0), - TrustTier::Affirming => serializer.serialize_i8(2), - TrustTier::Warning => serializer.serialize_i8(32), - TrustTier::Contraindicated => serializer.serialize_i8(96), - } + serializer.serialize_i8(self.into()) } } } @@ -64,13 +56,9 @@ impl Visitor<'_> for TrustTierVisitor { where E: de::Error, { - match value { - 0 => Ok(TrustTier::None), - 2 => Ok(TrustTier::Affirming), - 32 => Ok(TrustTier::Warning), - 96 => Ok(TrustTier::Contraindicated), - _ => Err(E::custom(format!("Unexpected TrustTier value: {value}"))), - } + value + .try_into() + .map_err(|_| E::custom(format!("Unexpected TrustTier value: {value}"))) } fn visit_i16(self, value: i16) -> Result @@ -117,12 +105,80 @@ impl Visitor<'_> for TrustTierVisitor { where E: de::Error, { - match value { + value + .try_into() + .map_err(|_| E::custom(format!("Unexpected TrustTier value: {value}"))) + } +} + +impl TryFrom<&str> for TrustTier { + type Error = Error; + + fn try_from(value: &str) -> Result { + match value.to_lowercase().as_str() { "none" => Ok(TrustTier::None), "affirming" => Ok(TrustTier::Affirming), "warning" => Ok(TrustTier::Warning), "contraindicated" => Ok(TrustTier::Contraindicated), - _ => Err(E::custom(format!("Unexpected TrustTier value: {value}"))), + _ => Err(Error::InvalidName(value.to_string())), + } + } +} + +impl TryFrom for TrustTier { + type Error = Error; + + fn try_from(value: i8) -> Result { + match value { + 0i8 => Ok(TrustTier::None), + 2i8 => Ok(TrustTier::Affirming), + 32i8 => Ok(TrustTier::Warning), + 96i8 => Ok(TrustTier::Contraindicated), + _ => Err(Error::InvalidValue(value)), + } + } +} + +impl From for String { + fn from(val: TrustTier) -> String { + match val { + TrustTier::None => "none".to_string(), + TrustTier::Affirming => "affirming".to_string(), + TrustTier::Warning => "warning".to_string(), + TrustTier::Contraindicated => "contraindicated".to_string(), + } + } +} + +impl<'a, 'b> From<&'a TrustTier> for &'b str { + fn from(val: &'a TrustTier) -> &'b str { + match val { + TrustTier::None => "none", + TrustTier::Affirming => "affirming", + TrustTier::Warning => "warning", + TrustTier::Contraindicated => "contraindicated", + } + } +} + +impl From for i8 { + fn from(val: TrustTier) -> i8 { + match val { + TrustTier::None => 0i8, + TrustTier::Affirming => 2i8, + TrustTier::Warning => 32i8, + TrustTier::Contraindicated => 96i8, + } + } +} + +impl From<&TrustTier> for i8 { + fn from(val: &TrustTier) -> i8 { + match val { + TrustTier::None => 0i8, + TrustTier::Affirming => 2i8, + TrustTier::Warning => 32i8, + TrustTier::Contraindicated => 96i8, } } } @@ -158,4 +214,28 @@ mod test { "Semantic(None, \"Unexpected TrustTier value: -2\")" ); } + + #[test] + fn from() { + let tier: TrustTier = 2i8.try_into().unwrap(); + assert_eq!(tier, TrustTier::Affirming); + + let res = TryInto::::try_into(7i8).err().unwrap(); + assert_eq!(res.to_string(), "invalid value: 7".to_string()); + + let tier: TrustTier = "WaRniNg".try_into().unwrap(); + assert_eq!(tier, TrustTier::Warning); + + let res = TryInto::::try_into("bad").err().unwrap(); + assert_eq!(res.to_string(), "invalid name: bad".to_string()); + } + + #[test] + fn into() { + let i: i8 = TrustTier::Affirming.into(); + assert_eq!(i, 2i8); + + let s: String = TrustTier::Warning.into(); + assert_eq!(s, "warning".to_string()); + } }