• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

input-output-hk / catalyst-libs / 20020618668

08 Dec 2025 07:49AM UTC coverage: 67.769% (+0.01%) from 67.756%
20020618668

Pull #677

github

web-flow
Merge 0c764ec7c into 9f3f3a36b
Pull Request #677: feat(rust/signed-doc): Update `link_check` to perform recursive parameters check

132 of 132 new or added lines in 2 files covered. (100.0%)

33 existing lines in 2 files now uncovered.

14075 of 20769 relevant lines covered (67.77%)

2576.39 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.65
/rust/signed_doc/src/metadata/mod.rs
1
//! Catalyst Signed Document Metadata.
2
use std::{
3
    collections::HashMap,
4
    fmt::{Display, Formatter},
5
};
6

7
mod chain;
8
mod collaborators;
9
mod content_encoding;
10
mod content_type;
11
pub(crate) mod doc_type;
12
pub(crate) mod document_refs;
13
mod section;
14
mod supported_field;
15

16
use catalyst_types::{catalyst_id::CatalystId, problem_report::ProblemReport, uuid::UuidV7};
17
pub use chain::Chain;
18
pub use content_encoding::ContentEncoding;
19
pub use content_type::ContentType;
20
pub use doc_type::DocType;
21
pub use document_refs::{DocLocator, DocumentRef, DocumentRefs};
22
use minicbor::Decoder;
23
pub use section::Section;
24
use strum::IntoDiscriminant as _;
25

26
use crate::decode_context::CompatibilityPolicy;
27
pub(crate) use crate::metadata::supported_field::{SupportedField, SupportedLabel};
28

29
/// Document Metadata.
30
///
31
/// These values are extracted from the COSE Sign protected header.
32
#[derive(Clone, Debug, PartialEq, Default)]
33
pub struct Metadata(HashMap<SupportedLabel, SupportedField>);
34

35
impl Metadata {
36
    /// Return Document Type `DocType` - a list of `UUIDv4`.
37
    ///
38
    /// # Errors
39
    /// - Missing 'type' field.
40
    pub fn doc_type(&self) -> anyhow::Result<&DocType> {
1,944✔
41
        self.0
1,944✔
42
            .get(&SupportedLabel::Type)
1,944✔
43
            .and_then(SupportedField::try_as_type_ref)
1,944✔
44
            .ok_or(anyhow::anyhow!("Missing 'type' field"))
1,944✔
45
    }
1,944✔
46

47
    /// Return Document ID `UUIDv7`.
48
    ///
49
    /// # Errors
50
    /// - Missing 'id' field.
51
    pub fn doc_id(&self) -> anyhow::Result<UuidV7> {
3,247✔
52
        self.0
3,247✔
53
            .get(&SupportedLabel::Id)
3,247✔
54
            .and_then(SupportedField::try_as_id_ref)
3,247✔
55
            .copied()
3,247✔
56
            .ok_or(anyhow::anyhow!("Missing 'id' field"))
3,247✔
57
    }
3,247✔
58

59
    /// Return Document Version `UUIDv7`.
60
    ///
61
    /// # Errors
62
    /// - Missing 'ver' field.
63
    pub fn doc_ver(&self) -> anyhow::Result<UuidV7> {
3,246✔
64
        self.0
3,246✔
65
            .get(&SupportedLabel::Ver)
3,246✔
66
            .and_then(SupportedField::try_as_ver_ref)
3,246✔
67
            .copied()
3,246✔
68
            .ok_or(anyhow::anyhow!("Missing 'ver' field"))
3,246✔
69
    }
3,246✔
70

71
    /// Returns the Document Content Type, if any.
72
    pub fn content_type(&self) -> Option<ContentType> {
1,000✔
73
        self.0
1,000✔
74
            .get(&SupportedLabel::ContentType)
1,000✔
75
            .and_then(SupportedField::try_as_content_type_ref)
1,000✔
76
            .copied()
1,000✔
77
    }
1,000✔
78

79
    /// Returns the Document Content Encoding, if any.
80
    #[must_use]
81
    pub fn content_encoding(&self) -> Option<ContentEncoding> {
911✔
82
        self.0
911✔
83
            .get(&SupportedLabel::ContentEncoding)
911✔
84
            .and_then(SupportedField::try_as_content_encoding_ref)
911✔
85
            .copied()
911✔
86
    }
911✔
87

88
    /// Return `ref` field.
89
    #[must_use]
90
    pub fn doc_ref(&self) -> Option<&DocumentRefs> {
287✔
91
        self.0
287✔
92
            .get(&SupportedLabel::Ref)
287✔
93
            .and_then(SupportedField::try_as_ref_ref)
287✔
94
    }
287✔
95

96
    /// Return `template` field.
97
    #[must_use]
98
    pub fn template(&self) -> Option<&DocumentRefs> {
253✔
99
        self.0
253✔
100
            .get(&SupportedLabel::Template)
253✔
101
            .and_then(SupportedField::try_as_template_ref)
253✔
102
    }
253✔
103

104
    /// Return `reply` field.
105
    #[must_use]
106
    pub fn reply(&self) -> Option<&DocumentRefs> {
259✔
107
        self.0
259✔
108
            .get(&SupportedLabel::Reply)
259✔
109
            .and_then(SupportedField::try_as_reply_ref)
259✔
110
    }
259✔
111

112
    /// Return `section` field.
113
    #[must_use]
114
    pub fn section(&self) -> Option<&Section> {
122✔
115
        self.0
122✔
116
            .get(&SupportedLabel::Section)
122✔
117
            .and_then(SupportedField::try_as_section_ref)
122✔
118
    }
122✔
119

120
    /// Return `collaborators` field.
121
    #[must_use]
122
    pub fn collaborators(&self) -> &[CatalystId] {
142✔
123
        self.0
142✔
124
            .get(&SupportedLabel::Collaborators)
142✔
125
            .and_then(SupportedField::try_as_collaborators_ref)
142✔
126
            .map_or(&[], |v| &**v)
142✔
127
    }
142✔
128

129
    /// Return `parameters` field.
130
    #[must_use]
131
    pub fn parameters(&self) -> Option<&DocumentRefs> {
356✔
132
        self.0
356✔
133
            .get(&SupportedLabel::Parameters)
356✔
134
            .and_then(SupportedField::try_as_parameters_ref)
356✔
135
    }
356✔
136

137
    /// Return `chain` field.
138
    pub fn chain(&self) -> Option<&Chain> {
238✔
139
        self.0
238✔
140
            .get(&SupportedLabel::Chain)
238✔
141
            .and_then(SupportedField::try_as_chain_ref)
238✔
142
    }
238✔
143

144
    /// Add `SupportedField` into the `Metadata`.
145
    ///
146
    /// # Warning
147
    ///
148
    /// Building metadata by-field with this function doesn't ensure the presence of
149
    /// required fields. Use [`Self::from_fields`] or [`Self::from_json`] if it's
150
    /// important for metadata to be valid.
151
    #[cfg(test)]
152
    pub(crate) fn add_field(
462✔
153
        &mut self,
462✔
154
        field: SupportedField,
462✔
155
    ) {
462✔
156
        self.0.insert(field.discriminant(), field);
462✔
157
    }
462✔
158

159
    /// Build `Metadata` object from the metadata fields, doing all necessary validation.
160
    pub(crate) fn from_fields<E>(
1,471✔
161
        fields: impl Iterator<Item = Result<SupportedField, E>>,
1,471✔
162
        report: &ProblemReport,
1,471✔
163
    ) -> Result<Self, E> {
1,471✔
164
        const REPORT_CONTEXT: &str = "Metadata building";
165

166
        let mut metadata = Metadata(HashMap::new());
1,471✔
167
        for v in fields {
9,024✔
168
            let v = v?;
7,553✔
169
            let k = v.discriminant();
7,553✔
170
            if metadata.0.insert(k, v).is_some() {
7,553✔
171
                report.duplicate_field(
×
172
                    &k.to_string(),
×
173
                    "Duplicate metadata fields are not allowed",
×
174
                    REPORT_CONTEXT,
×
175
                );
×
176
            }
7,553✔
177
        }
178

179
        if metadata.doc_type().is_err() {
1,471✔
180
            report.missing_field("type", REPORT_CONTEXT);
164✔
181
        }
1,307✔
182
        if metadata.doc_id().is_err() {
1,471✔
183
            report.missing_field("id", REPORT_CONTEXT);
105✔
184
        }
1,366✔
185
        if metadata.doc_ver().is_err() {
1,471✔
186
            report.missing_field("ver", REPORT_CONTEXT);
107✔
187
        }
1,364✔
188

189
        Ok(metadata)
1,471✔
190
    }
1,471✔
191

192
    /// Build `Metadata` object from the metadata fields, doing all necessary validation.
193
    ///
194
    /// # Errors
195
    ///   - Json deserialization failure.
196
    ///   - Duplicate fields.
197
    ///   - Missing mandatory fields like `id`, `ver`, `type`.
198
    pub fn from_json(fields: serde_json::Value) -> anyhow::Result<Self> {
595✔
199
        let fields = serde::Deserializer::deserialize_map(fields, MetadataDeserializeVisitor)?;
595✔
200
        let report = ProblemReport::new("Deserializing metadata from json");
595✔
201
        let metadata = Self::from_fields(fields.into_iter().map(anyhow::Result::<_>::Ok), &report)?;
595✔
202
        anyhow::ensure!(!report.is_problematic(), "{report:?}");
595✔
203
        Ok(metadata)
595✔
204
    }
595✔
205

206
    /// Serializes the current `Metadata` object into the JSON object.
207
    ///
208
    /// # Errors
209
    ///   - Json serialization failure.
210
    pub fn to_json(&self) -> anyhow::Result<serde_json::Value> {
1✔
211
        let map = self
1✔
212
            .0
1✔
213
            .iter()
1✔
214
            .map(|(k, v)| Ok((k.to_string(), serde_json::to_value(v)?)))
4✔
215
            .collect::<anyhow::Result<serde_json::Map<_, _>>>()?;
1✔
216
        Ok(serde_json::Value::Object(map))
1✔
217
    }
1✔
218
}
219

220
impl Display for Metadata {
UNCOV
221
    fn fmt(
×
UNCOV
222
        &self,
×
UNCOV
223
        f: &mut Formatter<'_>,
×
UNCOV
224
    ) -> Result<(), std::fmt::Error> {
×
UNCOV
225
        writeln!(f, "Metadata {{")?;
×
UNCOV
226
        writeln!(f, "  type: {:?},", self.doc_type().ok())?;
×
UNCOV
227
        writeln!(f, "  id: {:?},", self.doc_id().ok())?;
×
UNCOV
228
        writeln!(f, "  ver: {:?},", self.doc_ver().ok())?;
×
UNCOV
229
        writeln!(f, "  content_type: {:?},", self.content_type())?;
×
UNCOV
230
        writeln!(f, "  content_encoding: {:?},", self.content_encoding())?;
×
UNCOV
231
        writeln!(f, "  additional_fields: {{")?;
×
UNCOV
232
        writeln!(f, "    ref: {:?}", self.doc_ref())?;
×
UNCOV
233
        writeln!(f, "    template: {:?},", self.template())?;
×
UNCOV
234
        writeln!(f, "    reply: {:?},", self.reply())?;
×
UNCOV
235
        writeln!(f, "    section: {:?},", self.section())?;
×
UNCOV
236
        writeln!(f, "    collaborators: {:?},", self.collaborators())?;
×
UNCOV
237
        writeln!(f, "    parameters: {:?},", self.parameters())?;
×
UNCOV
238
        writeln!(f, "    chain: {:?},", self.chain())?;
×
UNCOV
239
        writeln!(f, "  }},")?;
×
UNCOV
240
        writeln!(f, "}}")
×
UNCOV
241
    }
×
242
}
243

244
impl minicbor::Encode<()> for Metadata {
245
    /// Encode as a CBOR map.
246
    ///
247
    /// Note that to put it in an [RFC 8152] protected header.
248
    /// The header must be then encoded as a binary string.
249
    ///
250
    /// Also note that this won't check the presence of the required fields,
251
    /// so the checks must be done elsewhere.
252
    ///
253
    /// [RFC 8152]: https://datatracker.ietf.org/doc/html/rfc8152#autoid-8
254
    fn encode<W: minicbor::encode::Write>(
858✔
255
        &self,
858✔
256
        e: &mut minicbor::Encoder<W>,
858✔
257
        _ctx: &mut (),
858✔
258
    ) -> Result<(), minicbor::encode::Error<W::Error>> {
858✔
259
        e.map(
858✔
260
            self.0
858✔
261
                .len()
858✔
262
                .try_into()
858✔
263
                .map_err(minicbor::encode::Error::message)?,
858✔
264
        )?;
×
265
        self.0
858✔
266
            .values()
858✔
267
            .try_fold(e, |e, field| e.encode(field))?
3,988✔
268
            .ok()
858✔
269
    }
858✔
270
}
271

272
impl minicbor::Decode<'_, crate::decode_context::DecodeContext> for Metadata {
273
    /// Decode from a CBOR map.
274
    ///
275
    /// Note that this won't decode an [RFC 8152] protected header as is.
276
    /// The header must be first decoded as a binary string.
277
    ///
278
    /// Also note that this won't check the absence of the required fields,
279
    /// so the checks must be done elsewhere.
280
    ///
281
    /// [RFC 8152]: https://datatracker.ietf.org/doc/html/rfc8152#autoid-8
282
    fn decode(
878✔
283
        d: &mut Decoder<'_>,
878✔
284
        ctx: &mut crate::decode_context::DecodeContext,
878✔
285
    ) -> Result<Self, minicbor::decode::Error> {
878✔
286
        let mut map_ctx = match ctx.policy() {
878✔
287
            CompatibilityPolicy::Accept => {
288
                cbork_utils::decode_context::DecodeCtx::non_deterministic()
876✔
289
            },
290
            CompatibilityPolicy::Warn => {
291
                cbork_utils::decode_context::DecodeCtx::non_deterministic_with_handler(|error| {
1✔
292
                    tracing::warn!(
1✔
293
                        error = ?error,
294
                        "Catalyst Signed Document non deterministically encoded metadata field",
×
295
                    );
296
                    Ok(())
1✔
297
                })
1✔
298
            },
299
            CompatibilityPolicy::Fail => cbork_utils::decode_context::DecodeCtx::Deterministic,
1✔
300
        };
301

302
        let report = ctx.report().clone();
878✔
303
        let fields = cbork_utils::map::Map::decode(d, &mut map_ctx)?
878✔
304
            .into_iter()
876✔
305
            .map(|e| {
4,144✔
306
                let mut bytes = e.key_bytes;
4,144✔
307
                bytes.extend(e.value);
4,144✔
308
                Option::<SupportedField>::decode(&mut minicbor::Decoder::new(&bytes), ctx)
4,144✔
309
            })
4,144✔
310
            .filter_map(Result::transpose);
876✔
311

312
        Self::from_fields(fields, &report)
876✔
313
    }
878✔
314
}
315

316
/// Implements [`serde::de::Visitor`], so that [`Metadata`] can be
317
/// deserialized by [`serde::Deserializer::deserialize_map`].
318
struct MetadataDeserializeVisitor;
319

320
impl<'de> serde::de::Visitor<'de> for MetadataDeserializeVisitor {
321
    type Value = Vec<SupportedField>;
322

323
    fn expecting(
×
324
        &self,
×
325
        f: &mut std::fmt::Formatter,
×
326
    ) -> std::fmt::Result {
×
327
        f.write_str("Catalyst Signed Document metadata key-value pairs")
×
328
    }
×
329

330
    fn visit_map<A: serde::de::MapAccess<'de>>(
595✔
331
        self,
595✔
332
        mut d: A,
595✔
333
    ) -> Result<Self::Value, A::Error> {
595✔
334
        let mut res = Vec::with_capacity(d.size_hint().unwrap_or(0));
595✔
335
        while let Some(k) = d.next_key::<SupportedLabel>()? {
4,046✔
336
            let v = d.next_value_seed(k)?;
3,451✔
337
            res.push(v);
3,451✔
338
        }
339
        Ok(res)
595✔
340
    }
595✔
341
}
342

343
#[cfg(test)]
344
mod tests {
345
    use test_case::test_case;
346

347
    use super::*;
348

349
    #[test_case(
350
        serde_json::json!({
351
            "id": "0197f398-9f43-7c23-a576-f765131b81f2",
352
            "ver": "0197f398-9f43-7c23-a576-f765131b81f2",
353
            "type":  "ab7c2428-c353-4331-856e-385b2eb20546",
354
            "content-type": "application/json",
355
        }) ;
356
        "minimally valid JSON"
357
    )]
358
    fn test_json_valid_serde(json: serde_json::Value) {
1✔
359
        let metadata = Metadata::from_json(json).unwrap();
1✔
360
        let json_from_meta = metadata.to_json().unwrap();
1✔
361
        assert_eq!(metadata, Metadata::from_json(json_from_meta).unwrap());
1✔
362
    }
1✔
363
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc