Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
80 changes: 63 additions & 17 deletions rust/signed_doc/src/metadata/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Catalyst Signed Document Metadata.
use std::{
collections::BTreeMap,
collections::{btree_map, BTreeMap},
error::Error,
fmt::{Display, Formatter},
};

Expand Down Expand Up @@ -555,14 +556,27 @@ impl MetadataDecodeContext {
report: &mut self.report,
}
}
}

/// First in a map [`SupportedField`] decoding context.
fn first_field_context(&mut self) -> supported_field::DecodeContext {
supported_field::DecodeContext {
prev_key: None,
metadata_context: self,
}
/// An error that's been reported, but doesn't affect the further decoding.
/// [`minicbor::Decoder`] should be assumed to be in a correct state and advanced towards
/// the next item.
///
/// The wrapped error can be returned up the call stack.
#[derive(Debug, thiserror::Error)]
#[error(transparent)]
pub struct TransientDecodeError(pub minicbor::decode::Error);

/// Creates a [`TransientDecodeError`] and wraps it in a
/// [`minicbor::decode::Error::custom`].
fn custom_transient_decode_error(
message: &str, position: Option<usize>,
) -> minicbor::decode::Error {
let mut inner = minicbor::decode::Error::message(message);
if let Some(pos) = position {
inner = inner.at(pos);
}
minicbor::decode::Error::custom(inner)
}

impl minicbor::Decode<'_, MetadataDecodeContext> for Metadata {
Expand All @@ -578,20 +592,52 @@ impl minicbor::Decode<'_, MetadataDecodeContext> for Metadata {
fn decode(
d: &mut Decoder<'_>, ctx: &mut MetadataDecodeContext,
) -> Result<Self, minicbor::decode::Error> {
const REPORT_CONTEXT: &str = "Metadata decoding";

let Some(len) = d.map()? else {
return Err(minicbor::decode::Error::message(
"Indefinite map is not supported",
));
};
let mut field_ctx = ctx.first_field_context();

// This performs duplication, ordering and length mismatch checks.
(0..len)
.map(|_| {
d.decode_with(&mut field_ctx)
.map(|field: SupportedField| (field.discriminant(), field))
})
.collect::<Result<_, _>>()
.map(Self)

// TODO: verify key order.
// TODO: use helpers from <https://github.com/input-output-hk/catalyst-libs/pull/360> once it's merged.

let mut metadata_map = BTreeMap::new();
let mut first_err = None;

// This will return an error on the end of input.
for _ in 0..len {
let entry_pos = d.position();
match d.decode_with::<_, SupportedField>(ctx) {
Ok(field) => {
let label = field.discriminant();
let entry = metadata_map.entry(label);
if let btree_map::Entry::Vacant(entry) = entry {
entry.insert(field);
} else {
ctx.report.duplicate_field(
&label.to_string(),
"Duplicate metadata fields are not allowed",
REPORT_CONTEXT,
);
first_err.get_or_insert(custom_transient_decode_error(
"Duplicate fields",
Some(entry_pos),
));
}
},
Err(err)
if err
.source()
.is_some_and(<dyn std::error::Error>::is::<TransientDecodeError>) =>
{
first_err.get_or_insert(err);
},
Err(err) => return Err(err),
}
}

first_err.map_or(Ok(Self(metadata_map)), Err)
}
}
74 changes: 34 additions & 40 deletions rust/signed_doc/src/metadata/supported_field.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
//! Catalyst Signed Document unified metadata field.

use std::fmt::{self, Display};
#[cfg(test)]
use std::convert::Infallible;
use std::{
cmp,
ops::{Deref, DerefMut},
};
use std::{cmp, convert::Infallible};

use catalyst_types::uuid::UuidV7;
use strum::{EnumDiscriminants, EnumTryAs, IntoDiscriminant as _};

use crate::{
metadata::{MetadataDecodeContext, MetadataEncodeContext},
metadata::{custom_transient_decode_error, MetadataDecodeContext, MetadataEncodeContext},
ContentEncoding, ContentType, DocType, DocumentRef, Section,
};

Expand Down Expand Up @@ -72,6 +69,15 @@ impl Label<'_> {
}
}

impl Display for Label<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Label::U8(u) => write!(f, "{u}"),
Label::Str(s) => f.write_str(s),
}
}
}

/// Catalyst Signed Document metadata field.
/// Fields are assigned discriminants based on deterministic ordering (see [RFC 8949
/// section 4.2.1]).
Expand Down Expand Up @@ -154,50 +160,39 @@ impl SupportedLabel {
}
}

/// [`SupportedField`] decoding context for the [`minicbor::Decode`] implementation.
pub(crate) struct DecodeContext<'a> {
/// Key of the previously decoded field. Used to check for duplicates and invalid
/// ordering.
pub prev_key: Option<SupportedLabel>,
/// Used by some values' decoding implementations.
pub metadata_context: &'a mut MetadataDecodeContext,
}

impl Deref for DecodeContext<'_> {
type Target = MetadataDecodeContext;

fn deref(&self) -> &Self::Target {
self.metadata_context
impl Display for SupportedLabel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.to_cose(), f)
}
}

impl DerefMut for DecodeContext<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.metadata_context
}
}

impl minicbor::Decode<'_, DecodeContext<'_>> for SupportedField {
impl minicbor::Decode<'_, MetadataDecodeContext> for SupportedField {
#[allow(clippy::todo, reason = "Not migrated to `minicbor` yet.")]
fn decode(
d: &mut minicbor::Decoder<'_>, ctx: &mut DecodeContext,
d: &mut minicbor::Decoder<'_>, ctx: &mut MetadataDecodeContext,
) -> Result<Self, minicbor::decode::Error> {
const REPORT_CONTEXT: &str = "Metadata field decoding";

let label_pos = d.position();
let label = Label::decode(d, &mut ())?;
let Some(key) = SupportedLabel::from_cose(label) else {
return Err(minicbor::decode::Error::message("Not a supported key").at(label_pos))?;
let value_start = d.position();
d.skip()?;
let value_end = d.position();
// Since the high level type isn't know, the value CBOR is tokenized and reported as
// such.
let value = minicbor::decode::Tokenizer::new(
d.input().get(value_start..value_end).unwrap_or_default(),
)
.to_string();
ctx.report
.unknown_field(&label.to_string(), &value, REPORT_CONTEXT);
return Err(custom_transient_decode_error(
"Not a supported key",
Some(label_pos),
));
};

match ctx.prev_key.map(|prev_key| prev_key.cmp(&key)) {
Some(cmp::Ordering::Equal) => {
return Err(minicbor::decode::Error::message("Duplicate keys").at(label_pos));
},
Some(cmp::Ordering::Greater) => {
return Err(minicbor::decode::Error::message("Invalid key ordering").at(label_pos));
},
_ => (),
}

let field = match key {
SupportedLabel::ContentType => todo!(),
SupportedLabel::Id => d.decode_with(&mut ctx.uuid_context).map(Self::Id),
Expand All @@ -212,7 +207,6 @@ impl minicbor::Decode<'_, DecodeContext<'_>> for SupportedField {
SupportedLabel::ContentEncoding => todo!(),
}?;

ctx.prev_key = Some(key);
Ok(field)
}
}
Expand Down