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

input-output-hk / catalyst-libs / 16219415812

11 Jul 2025 11:55AM UTC coverage: 68.835%. First build
16219415812

Pull #418

github

web-flow
Merge b09bdeee6 into a8b219ddd
Pull Request #418: feat(rust/signed-doc): Apply deterministic CBOR decoding from `cbork-utils`

157 of 182 new or added lines in 7 files covered. (86.26%)

12919 of 18768 relevant lines covered (68.84%)

2337.1 hits per line

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

94.95
/rust/signed_doc/src/metadata/document_refs/mod.rs
1
//! Document references.
2

3
mod doc_locator;
4
mod doc_ref;
5
use std::fmt::Display;
6

7
use catalyst_types::uuid::{CborContext, UuidV7};
8
use cbork_utils::{array::Array, decode_context::DecodeCtx};
9
pub use doc_locator::DocLocator;
10
pub use doc_ref::DocumentRef;
11
use minicbor::{Decode, Encode};
12
use tracing::warn;
13

14
use crate::CompatibilityPolicy;
15

16
/// List of document reference instance.
17
#[derive(Clone, Debug, PartialEq, Hash, Eq)]
18
pub struct DocumentRefs(Vec<DocumentRef>);
19

20
/// Document reference error.
21
#[derive(Debug, Clone, thiserror::Error)]
22
pub enum DocRefError {
23
    /// Invalid string conversion
24
    #[error("Invalid string conversion: {0}")]
25
    StringConversion(String),
26
    /// Cannot decode hex.
27
    #[error("Cannot decode hex: {0}")]
28
    HexDecode(String),
29
}
30

31
impl DocumentRefs {
32
    /// Get a list of document reference instance.
33
    #[must_use]
34
    pub fn doc_refs(&self) -> &Vec<DocumentRef> {
83✔
35
        &self.0
83✔
36
    }
83✔
37
}
38

39
impl From<Vec<DocumentRef>> for DocumentRefs {
40
    fn from(value: Vec<DocumentRef>) -> Self {
54✔
41
        DocumentRefs(value)
54✔
42
    }
54✔
43
}
44

45
impl Display for DocumentRefs {
46
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
7✔
47
        let items = self
7✔
48
            .0
7✔
49
            .iter()
7✔
50
            .map(|inner| format!("{inner}"))
7✔
51
            .collect::<Vec<_>>()
7✔
52
            .join(", ");
7✔
53
        write!(f, "[{items}]")
7✔
54
    }
7✔
55
}
56

57
impl Decode<'_, CompatibilityPolicy> for DocumentRefs {
58
    fn decode(
178✔
59
        d: &mut minicbor::Decoder<'_>, policy: &mut CompatibilityPolicy,
178✔
60
    ) -> Result<Self, minicbor::decode::Error> {
178✔
61
        const CONTEXT: &str = "DocumentRefs decoding";
62

63
        // Old: [id, ver]
64
        // New: [ 1* [id, ver, locator] ]
65
        let outer_arr = Array::decode(d, &mut DecodeCtx::Deterministic)
178✔
66
            .map_err(|e| minicbor::decode::Error::message(format!("{CONTEXT}: {e}")))?;
178✔
67

68
        match outer_arr.as_slice() {
177✔
69
            [first, rest @ ..] => {
176✔
70
                match minicbor::Decoder::new(first).datatype()? {
176✔
71
                    // New structure inner part [id, ver, locator]
72
                    minicbor::data::Type::Array => {
73
                        let mut arr = vec![first];
145✔
74
                        arr.extend(rest);
145✔
75

76
                        let doc_refs = arr
145✔
77
                            .iter()
145✔
78
                            .map(|bytes| minicbor::Decoder::new(bytes).decode())
151✔
79
                            .collect::<Result<_, _>>()?;
145✔
80

81
                        Ok(DocumentRefs(doc_refs))
144✔
82
                    },
83
                    // Old structure (id, ver)
84
                    minicbor::data::Type::Tag => {
85
                        match policy {
26✔
86
                            CompatibilityPolicy::Accept | CompatibilityPolicy::Warn => {
87
                                if matches!(policy, CompatibilityPolicy::Warn) {
25✔
88
                                    warn!("{CONTEXT}: Conversion of document reference, id and version, to list of document reference with doc locator");
1✔
89
                                }
24✔
90
                                if rest.len() != 1 {
25✔
91
                                    return Err(minicbor::decode::Error::message(format!(
23✔
92
                                        "{CONTEXT}: Must have exactly 2 elements inside array for document reference id and document reference version"
23✔
93
                                    )));
23✔
94
                                }
2✔
95

96
                                let id = UuidV7::decode(&mut minicbor::Decoder::new(first), &mut CborContext::Tagged).map_err(|e| {
2✔
NEW
97
                                    e.with_message("Invalid ID UUIDv7")
×
98
                                })?;
2✔
99
                                let ver = rest
2✔
100
                                    .first()
2✔
101
                                    .map(|ver| UuidV7::decode(&mut minicbor::Decoder::new(ver), &mut CborContext::Tagged).map_err(|e| {
2✔
NEW
102
                                        e.with_message("Invalid Ver UUIDv7")
×
103
                                    }))
2✔
104
                                    .transpose()?
2✔
105
                                    .ok_or_else(|| minicbor::decode::Error::message(format!("{CONTEXT}: Missing document reference version after document reference id")))?;
2✔
106

107
                                Ok(DocumentRefs(vec![DocumentRef::new(
2✔
108
                                    id,
2✔
109
                                    ver,
2✔
110
                                    // If old implementation is used, the locator will be empty
2✔
111
                                    DocLocator::default(),
2✔
112
                                )]))
2✔
113
                            },
114
                            CompatibilityPolicy::Fail => {
115
                                Err(minicbor::decode::Error::message(format!(
1✔
116
                                    "{CONTEXT}: Conversion of document reference id and version to list of document reference with doc locator is not allowed"
1✔
117
                                )))
1✔
118
                            },
119
                        }
120
                    },
121
                    other => {
5✔
122
                        Err(minicbor::decode::Error::message(format!(
5✔
123
                            "{CONTEXT}: Expected array of document reference, or tag of version and id, found {other}",
5✔
124
                        )))
5✔
125
                    },
126
                }
127
            },
128
            _ => {
129
                Err(minicbor::decode::Error::message(format!(
1✔
130
                    "{CONTEXT}: Empty array",
1✔
131
                )))
1✔
132
            },
133
        }
134
    }
178✔
135
}
136

137
impl Encode<()> for DocumentRefs {
138
    fn encode<W: minicbor::encode::Write>(
138✔
139
        &self, e: &mut minicbor::Encoder<W>, ctx: &mut (),
138✔
140
    ) -> Result<(), minicbor::encode::Error<W::Error>> {
138✔
141
        const CONTEXT: &str = "DocumentRefs encoding";
142
        if self.0.is_empty() {
138✔
143
            return Err(minicbor::encode::Error::message(format!(
×
144
                "{CONTEXT}: DocumentRefs cannot be empty"
×
145
            )));
×
146
        }
138✔
147
        e.array(
138✔
148
            self.0
138✔
149
                .len()
138✔
150
                .try_into()
138✔
151
                .map_err(|e| minicbor::encode::Error::message(format!("{CONTEXT}, {e}")))?,
138✔
152
        )?;
×
153

154
        for doc_ref in &self.0 {
282✔
155
            doc_ref.encode(e, ctx)?;
144✔
156
        }
157
        Ok(())
138✔
158
    }
138✔
159
}
160

161
mod serde_impl {
162
    //! `serde::Deserialize` and `serde::Serialize` trait implementations
163

164
    use std::str::FromStr;
165

166
    use super::{DocLocator, DocRefError, DocumentRef, DocumentRefs, UuidV7};
167

168
    /// Old structure deserialize as map {id, ver}
169
    #[derive(serde::Deserialize)]
170
    struct OldRef {
171
        /// "id": "uuidv7
172
        id: String,
173
        /// "ver": "uuidv7"
174
        ver: String,
175
    }
176

177
    /// New structure as deserialize as map {id, ver, cid}
178
    #[derive(serde::Deserialize, serde::Serialize)]
179
    struct NewRef {
180
        /// "id": "uuidv7"
181
        id: String,
182
        /// "ver": "uuidv7"
183
        ver: String,
184
        /// "cid": "0x..."
185
        cid: String,
186
    }
187

188
    #[derive(serde::Deserialize)]
189
    #[serde(untagged)]
190
    enum DocRefSerde {
191
        /// Old structure of document reference.
192
        Old(OldRef),
193
        /// New structure of document reference.
194
        New(Vec<NewRef>),
195
    }
196

197
    impl serde::Serialize for DocumentRefs {
198
        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
4✔
199
        where S: serde::Serializer {
4✔
200
            let iter = self.0.iter().map(|v| {
5✔
201
                NewRef {
5✔
202
                    id: v.id().to_string(),
5✔
203
                    ver: v.ver().to_string(),
5✔
204
                    cid: v.doc_locator().to_string(),
5✔
205
                }
5✔
206
            });
5✔
207
            serializer.collect_seq(iter)
4✔
208
        }
4✔
209
    }
210

211
    impl<'de> serde::Deserialize<'de> for DocumentRefs {
212
        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
90✔
213
        where D: serde::Deserializer<'de> {
90✔
214
            let input = DocRefSerde::deserialize(deserializer)?;
90✔
215
            match input {
90✔
216
                DocRefSerde::Old(v) => {
84✔
217
                    let id = UuidV7::from_str(&v.id).map_err(|_| {
84✔
218
                        serde::de::Error::custom(DocRefError::StringConversion(v.id.clone()))
×
219
                    })?;
84✔
220
                    let ver = UuidV7::from_str(&v.ver).map_err(|_| {
84✔
221
                        serde::de::Error::custom(DocRefError::StringConversion(v.ver.clone()))
×
222
                    })?;
84✔
223

224
                    Ok(DocumentRefs(vec![DocumentRef::new(
84✔
225
                        id,
84✔
226
                        ver,
84✔
227
                        DocLocator::default(),
84✔
228
                    )]))
84✔
229
                },
230
                DocRefSerde::New(value) => {
6✔
231
                    let mut dr = vec![];
6✔
232
                    for v in value {
14✔
233
                        let id = UuidV7::from_str(&v.id).map_err(|_| {
8✔
234
                            serde::de::Error::custom(DocRefError::StringConversion(v.id.clone()))
×
235
                        })?;
8✔
236
                        let ver = UuidV7::from_str(&v.ver).map_err(|_| {
8✔
237
                            serde::de::Error::custom(DocRefError::StringConversion(v.ver.clone()))
×
238
                        })?;
8✔
239
                        let cid = &v.cid.strip_prefix("0x").unwrap_or(&v.cid);
8✔
240
                        let locator = hex::decode(cid).map_err(|_| {
8✔
241
                            serde::de::Error::custom(DocRefError::HexDecode(v.cid.clone()))
×
242
                        })?;
8✔
243
                        dr.push(DocumentRef::new(id, ver, locator.into()));
8✔
244
                    }
245
                    Ok(DocumentRefs(dr))
6✔
246
                },
247
            }
248
        }
90✔
249
    }
250
}
251

252
#[cfg(test)]
253
mod tests {
254

255
    use minicbor::{Decoder, Encoder};
256
    use test_case::test_case;
257

258
    use super::*;
259

260
    #[test_case(
5✔
261
        CompatibilityPolicy::Accept,
5✔
262
        {
5✔
263
            Encoder::new(Vec::new())
5✔
264
        } ;
5✔
265
        "Invalid empty CBOR bytes"
5✔
266
    )]
5✔
267
    #[test_case(
268
        CompatibilityPolicy::Accept,
269
        {
270
            let mut e = Encoder::new(Vec::new());
271
            e.array(0).unwrap();
272
            e
273
        } ;
274
        "Invalid empty CBOR array"
275
    )]
276
    #[test_case(
277
        CompatibilityPolicy::Fail,
278
        {
279
            let mut e = Encoder::new(Vec::new());
280
            e.array(2)
281
                .unwrap()
282
                .encode_with(UuidV7::new(), &mut CborContext::Tagged)
283
                .unwrap()
284
                .encode_with(UuidV7::new(), &mut CborContext::Tagged)
285
                .unwrap();
286
            e
287
        } ;
288
        "Valid array of two uuid v7 (old format), fail policy"
289
    )]
290
    #[test_case(
291
        CompatibilityPolicy::Accept,
292
        {
293
            let mut e = Encoder::new(Vec::new());
294
            e.array(2)
295
                .unwrap()
296
                .encode_with(UuidV7::new(), &mut CborContext::Untagged)
297
                .unwrap()
298
                .encode_with(UuidV7::new(), &mut CborContext::Untagged)
299
                .unwrap();
300
            e
301
        } ;
302
        "Invalid untagged uuids v7 (old format)"
303
    )]
304
    #[test_case(
305
        CompatibilityPolicy::Accept,
306
        {
307
            let mut e = Encoder::new(Vec::new());
308
            e.array(1)
309
                .unwrap()
310
                .array(3)
311
                .unwrap()
312
                .encode_with(UuidV7::new(), &mut CborContext::Untagged)
313
                .unwrap()
314
                .encode_with(UuidV7::new(), &mut CborContext::Untagged)
315
                .unwrap()
316
                .encode(DocLocator::default())
317
                .unwrap();
318
            e
319
        } ;
320
        "Invalid untagged uuid uuids v7 (new format)"
321
    )]
322
    fn test_invalid_cbor_decode(mut policy: CompatibilityPolicy, e: Encoder<Vec<u8>>) {
5✔
323
        assert!(
5✔
324
            DocumentRefs::decode(&mut Decoder::new(e.into_writer().as_slice()), &mut policy)
5✔
325
                .is_err()
5✔
326
        );
5✔
327
    }
5✔
328

329
    #[test_case(
4✔
330
        CompatibilityPolicy::Accept,
4✔
331
        |uuid: UuidV7, _: DocLocator| {
4✔
332
            let mut e = Encoder::new(Vec::new());
1✔
333
            e.array(2)
1✔
334
                .unwrap()
1✔
335
                .encode_with(uuid, &mut CborContext::Tagged)
1✔
336
                .unwrap()
1✔
337
                .encode_with(uuid, &mut CborContext::Tagged)
1✔
338
                .unwrap();
1✔
339
            e
1✔
340
        } ;
1✔
341
        "Valid single doc ref (old format)"
4✔
342
    )]
4✔
343
    #[test_case(
344
        CompatibilityPolicy::Warn,
345
        |uuid: UuidV7, _: DocLocator| {
1✔
346
            let mut e = Encoder::new(Vec::new());
1✔
347
            e.array(2)
1✔
348
                .unwrap()
1✔
349
                .encode_with(uuid, &mut CborContext::Tagged)
1✔
350
                .unwrap()
1✔
351
                .encode_with(uuid, &mut CborContext::Tagged)
1✔
352
                .unwrap();
1✔
353
            e
1✔
354
        } ;
1✔
355
        "Valid single doc ref (old format), warn policy"
356
    )]
357
    #[test_case(
358
        CompatibilityPolicy::Accept,
359
        |uuid: UuidV7, doc_loc: DocLocator| {
1✔
360
            let mut e = Encoder::new(Vec::new());
1✔
361
            e.array(1)
1✔
362
                .unwrap()
1✔
363
                .array(3)
1✔
364
                .unwrap()
1✔
365
                .encode_with(uuid, &mut CborContext::Tagged)
1✔
366
                .unwrap()
1✔
367
                .encode_with(uuid, &mut CborContext::Tagged)
1✔
368
                .unwrap()
1✔
369
                .encode(doc_loc)
1✔
370
                .unwrap();
1✔
371
            e
1✔
372
        } ;
1✔
373
        "Array of new doc ref (new format)"
374
    )]
375
    #[test_case(
376
        CompatibilityPolicy::Fail,
377
        |uuid: UuidV7, doc_loc: DocLocator| {
1✔
378
            let mut e = Encoder::new(Vec::new());
1✔
379
            e.array(1)
1✔
380
                .unwrap()
1✔
381
                .array(3)
1✔
382
                .unwrap()
1✔
383
                .encode_with(uuid, &mut CborContext::Tagged)
1✔
384
                .unwrap()
1✔
385
                .encode_with(uuid, &mut CborContext::Tagged)
1✔
386
                .unwrap()
1✔
387
                .encode(doc_loc)
1✔
388
                .unwrap();
1✔
389
            e
1✔
390
        } ;
1✔
391
        "Array of new doc ref (new format), fail policy"
392
    )]
393
    fn test_valid_cbor_decode(
4✔
394
        mut policy: CompatibilityPolicy, e_gen: impl FnOnce(UuidV7, DocLocator) -> Encoder<Vec<u8>>,
4✔
395
    ) {
4✔
396
        let uuid = UuidV7::new();
4✔
397
        let doc_loc = DocLocator::default();
4✔
398
        let e = e_gen(uuid, doc_loc.clone());
4✔
399

4✔
400
        let doc_refs =
4✔
401
            DocumentRefs::decode(&mut Decoder::new(e.into_writer().as_slice()), &mut policy)
4✔
402
                .unwrap();
4✔
403
        assert_eq!(doc_refs.0, vec![DocumentRef::new(uuid, uuid, doc_loc)]);
4✔
404
    }
4✔
405

406
    #[test_case(
2✔
407
        serde_json::json!(
2✔
408
            {
2✔
409
                "id": UuidV7::new(),
2✔
410
                "ver": UuidV7::new(),
2✔
411
            }
2✔
412
        ) ;
2✔
413
        "Document reference type old format"
2✔
414
    )]
2✔
415
    #[test_case(
416
        serde_json::json!(
417
            [
418
                {
419
                    "id": UuidV7::new(),
420
                    "ver": UuidV7::new(),
421
                    "cid": format!("0x{}", hex::encode([1, 2, 3]))
422
                },
423
                {
424
                    "id": UuidV7::new(),
425
                    "ver": UuidV7::new(),
426
                    "cid": format!("0x{}", hex::encode([1, 2, 3]))
427
                }
428
            ]
429
        ) ;
430
        "Document reference type new format"
431
    )]
432
    fn test_json_valid_serde(json: serde_json::Value) {
2✔
433
        let refs: DocumentRefs = serde_json::from_value(json).unwrap();
2✔
434
        let json_from_refs = serde_json::to_value(&refs).unwrap();
2✔
435
        assert_eq!(refs, serde_json::from_value(json_from_refs).unwrap());
2✔
436
    }
2✔
437
}
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