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

input-output-hk / catalyst-libs / 16646523141

31 Jul 2025 10:28AM UTC coverage: 67.341% (+3.8%) from 63.544%
16646523141

push

github

web-flow
feat(rust/signed-doc): Implement new Catalyst Signed Doc (#338)

* chore: add new line to open pr

Signed-off-by: bkioshn <bkioshn@gmail.com>

* chore: revert

Signed-off-by: bkioshn <bkioshn@gmail.com>

* feat(rust/signed-doc): add new type `DocType` (#339)

* feat(signed-doc): add new type DocType

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): add conversion policy

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): doc type

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): doc type error

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): seperate test

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): format

Signed-off-by: bkioshn <bkioshn@gmail.com>

---------

Signed-off-by: bkioshn <bkioshn@gmail.com>

* feat(rust/signed-doc): Add initial decoding tests for the Catalyst Signed Documents (#349)

* wip

* wip

* fix fmt

* fix spelling

* fix clippy

* fix(rust/signed-doc): Apply new `DocType` (#347)

* feat(signed-doc): add new type DocType

Signed-off-by: bkioshn <bkioshn@gmail.com>

* wip: apply doctype

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): add more function to DocType

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): map old doctype to new doctype

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): add eq to uuidv4

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): fix validator

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): minor fixes

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(catalyst-types): add hash to uuidv4

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): decoding test

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): doctype

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): minor fixes

Signed-off-by: bkioshn <bkioshn@gmail.com>

* chore(sign-doc): fix comment

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(catalyst-types): add froms... (continued)

2453 of 2675 new or added lines in 38 files covered. (91.7%)

19 existing lines in 7 files now uncovered.

11312 of 16798 relevant lines covered (67.34%)

2525.16 hits per line

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

90.73
/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
impl DocumentRefs {
21
    /// Returns true if provided `cbor` bytes is a valid old format.
22
    /// ```cddl
23
    /// old_format = [id, ver]
24
    /// ```
25
    /// Returns false if provided `cbor` bytes is a valid new format.
26
    /// ```cddl
27
    /// new_format = [ +[id, ver, cid] ]
28
    /// ```
29
    pub(crate) fn is_deprecated_cbor(cbor: &[u8]) -> Result<bool, minicbor::decode::Error> {
10✔
30
        let mut d = minicbor::Decoder::new(cbor);
10✔
31
        d.array()?;
10✔
32
        match d.datatype()? {
10✔
33
            // new_format = [ +[id, ver, cid] ]
34
            minicbor::data::Type::Array => Ok(false),
3✔
35
            // old_format = [id, ver]
36
            minicbor::data::Type::Tag => Ok(true),
7✔
NEW
37
            ty => Err(minicbor::decode::Error::type_mismatch(ty)),
×
38
        }
39
    }
10✔
40
}
41

42
/// Document reference error.
43
#[derive(Debug, Clone, thiserror::Error)]
44
pub enum DocRefError {
45
    /// Invalid string conversion
46
    #[error("Invalid string conversion: {0}")]
47
    StringConversion(String),
48
    /// Cannot decode hex.
49
    #[error("Cannot decode hex: {0}")]
50
    HexDecode(String),
51
}
52

53
impl DocumentRefs {
54
    /// Get a list of document reference instance.
55
    #[must_use]
56
    pub fn doc_refs(&self) -> &Vec<DocumentRef> {
30✔
57
        &self.0
30✔
58
    }
30✔
59
}
60

61
impl From<Vec<DocumentRef>> for DocumentRefs {
62
    fn from(value: Vec<DocumentRef>) -> Self {
49✔
63
        DocumentRefs(value)
49✔
64
    }
49✔
65
}
66

67
impl Display for DocumentRefs {
68
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
7✔
69
        let items = self
7✔
70
            .0
7✔
71
            .iter()
7✔
72
            .map(|inner| format!("{inner}"))
7✔
73
            .collect::<Vec<_>>()
7✔
74
            .join(", ");
7✔
75
        write!(f, "[{items}]")
7✔
76
    }
7✔
77
}
78

79
impl Decode<'_, CompatibilityPolicy> for DocumentRefs {
80
    fn decode(
171✔
81
        d: &mut minicbor::Decoder<'_>, policy: &mut CompatibilityPolicy,
171✔
82
    ) -> Result<Self, minicbor::decode::Error> {
171✔
83
        const CONTEXT: &str = "DocumentRefs decoding";
84

85
        // Old: [id, ver]
86
        // New: [ 1* [id, ver, locator] ]
87
        let outer_arr = Array::decode(d, &mut DecodeCtx::Deterministic)
171✔
88
            .map_err(|e| minicbor::decode::Error::message(format!("{CONTEXT}: {e}")))?;
171✔
89

90
        match outer_arr.as_slice() {
170✔
91
            [first, rest @ ..] => {
169✔
92
                match minicbor::Decoder::new(first).datatype()? {
169✔
93
                    // New structure inner part [id, ver, locator]
94
                    minicbor::data::Type::Array => {
95
                        let mut arr = vec![first];
131✔
96
                        arr.extend(rest);
131✔
97

98
                        let doc_refs = arr
131✔
99
                            .iter()
131✔
100
                            .map(|bytes| minicbor::Decoder::new(bytes).decode())
137✔
101
                            .collect::<Result<_, _>>()?;
131✔
102

103
                        Ok(DocumentRefs(doc_refs))
130✔
104
                    },
105
                    // Old structure (id, ver)
106
                    minicbor::data::Type::Tag => {
107
                        match policy {
33✔
108
                            CompatibilityPolicy::Accept | CompatibilityPolicy::Warn => {
109
                                if matches!(policy, CompatibilityPolicy::Warn) {
32✔
110
                                    warn!("{CONTEXT}: Conversion of document reference, id and version, to list of document reference with doc locator");
1✔
111
                                }
31✔
112
                                if rest.len() != 1 {
32✔
113
                                    return Err(minicbor::decode::Error::message(format!(
23✔
114
                                        "{CONTEXT}: Must have exactly 2 elements inside array for document reference id and document reference version, found {}",
23✔
115
                                        rest.len().overflowing_add(1).0
23✔
116
                                    )));
23✔
117
                                }
9✔
118

119
                                let id = UuidV7::decode(&mut minicbor::Decoder::new(first), &mut CborContext::Tagged).map_err(|e| {
9✔
NEW
120
                                    e.with_message("Invalid ID UUIDv7")
×
NEW
121
                                })?;
×
122
                                let ver = rest
9✔
123
                                    .first()
9✔
124
                                    .map(|ver| UuidV7::decode(&mut minicbor::Decoder::new(ver), &mut CborContext::Tagged).map_err(|e| {
9✔
NEW
125
                                        e.with_message("Invalid Ver UUIDv7")
×
NEW
126
                                    }))
×
127
                                    .transpose()?
9✔
128
                                    .ok_or_else(|| minicbor::decode::Error::message(format!("{CONTEXT}: Missing document reference version after document reference id")))?;
9✔
129

130
                                Ok(DocumentRefs(vec![DocumentRef::new(
9✔
131
                                    id,
9✔
132
                                    ver,
9✔
133
                                    // If old implementation is used, the locator will be empty
9✔
134
                                    DocLocator::default(),
9✔
135
                                )]))
9✔
136
                            },
137
                            CompatibilityPolicy::Fail => {
138
                                Err(minicbor::decode::Error::message(format!(
1✔
139
                                    "{CONTEXT}: Conversion of document reference id and version to list of document reference with doc locator is not allowed"
1✔
140
                                )))
1✔
141
                            },
142
                        }
143
                    },
144
                    other => {
5✔
145
                        Err(minicbor::decode::Error::message(format!(
5✔
146
                            "{CONTEXT}: Expected array of document reference, or tag of version and id, found {other}",
5✔
147
                        )))
5✔
148
                    },
149
                }
150
            },
151
            _ => {
152
                Err(minicbor::decode::Error::message(format!(
1✔
153
                    "{CONTEXT}: Empty array",
1✔
154
                )))
1✔
155
            },
156
        }
157
    }
171✔
158
}
159

160
impl Encode<()> for DocumentRefs {
161
    fn encode<W: minicbor::encode::Write>(
124✔
162
        &self, e: &mut minicbor::Encoder<W>, ctx: &mut (),
124✔
163
    ) -> Result<(), minicbor::encode::Error<W::Error>> {
124✔
164
        const CONTEXT: &str = "DocumentRefs encoding";
165
        if self.0.is_empty() {
124✔
NEW
166
            return Err(minicbor::encode::Error::message(format!(
×
NEW
167
                "{CONTEXT}: DocumentRefs cannot be empty"
×
NEW
168
            )));
×
169
        }
124✔
170
        e.array(
124✔
171
            self.0
124✔
172
                .len()
124✔
173
                .try_into()
124✔
174
                .map_err(|e| minicbor::encode::Error::message(format!("{CONTEXT}, {e}")))?,
124✔
NEW
175
        )?;
×
176

177
        for doc_ref in &self.0 {
254✔
178
            doc_ref.encode(e, ctx)?;
130✔
179
        }
180
        Ok(())
124✔
181
    }
124✔
182
}
183

184
mod serde_impl {
185
    //! `serde::Deserialize` and `serde::Serialize` trait implementations
186

187
    use std::str::FromStr;
188

189
    use super::{DocLocator, DocRefError, DocumentRef, DocumentRefs, UuidV7};
190

191
    /// Old structure deserialize as map {id, ver}
192
    #[derive(serde::Deserialize)]
193
    struct OldRef {
194
        /// "id": "uuidv7
195
        id: String,
196
        /// "ver": "uuidv7"
197
        ver: String,
198
    }
199

200
    /// New structure as deserialize as map {id, ver, cid}
201
    #[derive(serde::Deserialize, serde::Serialize)]
202
    struct NewRef {
203
        /// "id": "uuidv7"
204
        id: String,
205
        /// "ver": "uuidv7"
206
        ver: String,
207
        /// "cid": "0x..."
208
        cid: String,
209
    }
210

211
    #[derive(serde::Deserialize)]
212
    #[serde(untagged)]
213
    enum DocRefSerde {
214
        /// Old structure of document reference.
215
        Old(OldRef),
216
        /// New structure of document reference.
217
        New(Vec<NewRef>),
218
    }
219

220
    impl serde::Serialize for DocumentRefs {
221
        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
4✔
222
        where S: serde::Serializer {
4✔
223
            let iter = self.0.iter().map(|v| {
5✔
224
                NewRef {
5✔
225
                    id: v.id().to_string(),
5✔
226
                    ver: v.ver().to_string(),
5✔
227
                    cid: v.doc_locator().to_string(),
5✔
228
                }
5✔
229
            });
5✔
230
            serializer.collect_seq(iter)
4✔
231
        }
4✔
232
    }
233

234
    impl<'de> serde::Deserialize<'de> for DocumentRefs {
235
        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
76✔
236
        where D: serde::Deserializer<'de> {
76✔
237
            let input = DocRefSerde::deserialize(deserializer)?;
76✔
238
            match input {
76✔
239
                DocRefSerde::Old(v) => {
70✔
240
                    let id = UuidV7::from_str(&v.id).map_err(|_| {
70✔
NEW
241
                        serde::de::Error::custom(DocRefError::StringConversion(v.id.clone()))
×
NEW
242
                    })?;
×
243
                    let ver = UuidV7::from_str(&v.ver).map_err(|_| {
70✔
NEW
244
                        serde::de::Error::custom(DocRefError::StringConversion(v.ver.clone()))
×
NEW
245
                    })?;
×
246

247
                    Ok(DocumentRefs(vec![DocumentRef::new(
70✔
248
                        id,
70✔
249
                        ver,
70✔
250
                        DocLocator::default(),
70✔
251
                    )]))
70✔
252
                },
253
                DocRefSerde::New(value) => {
6✔
254
                    let mut dr = vec![];
6✔
255
                    for v in value {
14✔
256
                        let id = UuidV7::from_str(&v.id).map_err(|_| {
8✔
NEW
257
                            serde::de::Error::custom(DocRefError::StringConversion(v.id.clone()))
×
NEW
258
                        })?;
×
259
                        let ver = UuidV7::from_str(&v.ver).map_err(|_| {
8✔
NEW
260
                            serde::de::Error::custom(DocRefError::StringConversion(v.ver.clone()))
×
NEW
261
                        })?;
×
262
                        let cid = &v.cid.strip_prefix("0x").unwrap_or(&v.cid);
8✔
263
                        let locator = hex::decode(cid).map_err(|_| {
8✔
NEW
264
                            serde::de::Error::custom(DocRefError::HexDecode(v.cid.clone()))
×
NEW
265
                        })?;
×
266
                        dr.push(DocumentRef::new(id, ver, locator.into()));
8✔
267
                    }
268
                    Ok(DocumentRefs(dr))
6✔
269
                },
270
            }
271
        }
76✔
272
    }
273
}
274

275
#[cfg(test)]
276
mod tests {
277

278
    use minicbor::{Decoder, Encoder};
279
    use test_case::test_case;
280

281
    use super::*;
282

283
    #[test_case(
284
        CompatibilityPolicy::Accept,
285
        {
286
            Encoder::new(Vec::new())
287
        } ;
288
        "Invalid empty CBOR bytes"
289
    )]
290
    #[test_case(
291
        CompatibilityPolicy::Accept,
292
        {
293
            let mut e = Encoder::new(Vec::new());
294
            e.array(0).unwrap();
295
            e
296
        } ;
297
        "Invalid empty CBOR array"
298
    )]
299
    #[test_case(
300
        CompatibilityPolicy::Fail,
301
        {
302
            let mut e = Encoder::new(Vec::new());
303
            e.array(2)
304
                .unwrap()
305
                .encode_with(UuidV7::new(), &mut CborContext::Tagged)
306
                .unwrap()
307
                .encode_with(UuidV7::new(), &mut CborContext::Tagged)
308
                .unwrap();
309
            e
310
        } ;
311
        "Valid array of two uuid v7 (old format), fail policy"
312
    )]
313
    #[test_case(
314
        CompatibilityPolicy::Accept,
315
        {
316
            let mut e = Encoder::new(Vec::new());
317
            e.array(2)
318
                .unwrap()
319
                .encode_with(UuidV7::new(), &mut CborContext::Untagged)
320
                .unwrap()
321
                .encode_with(UuidV7::new(), &mut CborContext::Untagged)
322
                .unwrap();
323
            e
324
        } ;
325
        "Invalid untagged uuids v7 (old format)"
326
    )]
327
    #[test_case(
328
        CompatibilityPolicy::Accept,
329
        {
330
            let mut e = Encoder::new(Vec::new());
331
            e.array(1)
332
                .unwrap()
333
                .array(3)
334
                .unwrap()
335
                .encode_with(UuidV7::new(), &mut CborContext::Untagged)
336
                .unwrap()
337
                .encode_with(UuidV7::new(), &mut CborContext::Untagged)
338
                .unwrap()
339
                .encode(DocLocator::default())
340
                .unwrap();
341
            e
342
        } ;
343
        "Invalid untagged uuid uuids v7 (new format)"
344
    )]
345
    fn test_invalid_cbor_decode(mut policy: CompatibilityPolicy, e: Encoder<Vec<u8>>) {
5✔
346
        assert!(
5✔
347
            DocumentRefs::decode(&mut Decoder::new(e.into_writer().as_slice()), &mut policy)
5✔
348
                .is_err()
5✔
349
        );
350
    }
5✔
351

352
    #[test_case(
353
        CompatibilityPolicy::Accept,
354
        |uuid: UuidV7, _: DocLocator| {
1✔
355
            let mut e = Encoder::new(Vec::new());
1✔
356
            e.array(2)
1✔
357
                .unwrap()
1✔
358
                .encode_with(uuid, &mut CborContext::Tagged)
1✔
359
                .unwrap()
1✔
360
                .encode_with(uuid, &mut CborContext::Tagged)
1✔
361
                .unwrap();
1✔
362
            e
1✔
363
        } ;
1✔
364
        "Valid single doc ref (old format)"
365
    )]
366
    #[test_case(
367
        CompatibilityPolicy::Warn,
368
        |uuid: UuidV7, _: DocLocator| {
1✔
369
            let mut e = Encoder::new(Vec::new());
1✔
370
            e.array(2)
1✔
371
                .unwrap()
1✔
372
                .encode_with(uuid, &mut CborContext::Tagged)
1✔
373
                .unwrap()
1✔
374
                .encode_with(uuid, &mut CborContext::Tagged)
1✔
375
                .unwrap();
1✔
376
            e
1✔
377
        } ;
1✔
378
        "Valid single doc ref (old format), warn policy"
379
    )]
380
    #[test_case(
381
        CompatibilityPolicy::Accept,
382
        |uuid: UuidV7, doc_loc: DocLocator| {
1✔
383
            let mut e = Encoder::new(Vec::new());
1✔
384
            e.array(1)
1✔
385
                .unwrap()
1✔
386
                .array(3)
1✔
387
                .unwrap()
1✔
388
                .encode_with(uuid, &mut CborContext::Tagged)
1✔
389
                .unwrap()
1✔
390
                .encode_with(uuid, &mut CborContext::Tagged)
1✔
391
                .unwrap()
1✔
392
                .encode(doc_loc)
1✔
393
                .unwrap();
1✔
394
            e
1✔
395
        } ;
1✔
396
        "Array of new doc ref (new format)"
397
    )]
398
    #[test_case(
399
        CompatibilityPolicy::Fail,
400
        |uuid: UuidV7, doc_loc: DocLocator| {
1✔
401
            let mut e = Encoder::new(Vec::new());
1✔
402
            e.array(1)
1✔
403
                .unwrap()
1✔
404
                .array(3)
1✔
405
                .unwrap()
1✔
406
                .encode_with(uuid, &mut CborContext::Tagged)
1✔
407
                .unwrap()
1✔
408
                .encode_with(uuid, &mut CborContext::Tagged)
1✔
409
                .unwrap()
1✔
410
                .encode(doc_loc)
1✔
411
                .unwrap();
1✔
412
            e
1✔
413
        } ;
1✔
414
        "Array of new doc ref (new format), fail policy"
415
    )]
416
    fn test_valid_cbor_decode(
4✔
417
        mut policy: CompatibilityPolicy, e_gen: impl FnOnce(UuidV7, DocLocator) -> Encoder<Vec<u8>>,
4✔
418
    ) {
4✔
419
        let uuid = UuidV7::new();
4✔
420
        let doc_loc = DocLocator::default();
4✔
421
        let e = e_gen(uuid, doc_loc.clone());
4✔
422

423
        let doc_refs =
4✔
424
            DocumentRefs::decode(&mut Decoder::new(e.into_writer().as_slice()), &mut policy)
4✔
425
                .unwrap();
4✔
426
        assert_eq!(doc_refs.0, vec![DocumentRef::new(uuid, uuid, doc_loc)]);
4✔
427
    }
4✔
428

429
    #[test_case(
430
        serde_json::json!(
431
            {
432
                "id": UuidV7::new(),
433
                "ver": UuidV7::new(),
434
            }
435
        ) ;
436
        "Document reference type old format"
437
    )]
438
    #[test_case(
439
        serde_json::json!(
440
            [
441
                {
442
                    "id": UuidV7::new(),
443
                    "ver": UuidV7::new(),
444
                    "cid": format!("0x{}", hex::encode([1, 2, 3]))
445
                },
446
                {
447
                    "id": UuidV7::new(),
448
                    "ver": UuidV7::new(),
449
                    "cid": format!("0x{}", hex::encode([1, 2, 3]))
450
                }
451
            ]
452
        ) ;
453
        "Document reference type new format"
454
    )]
455
    fn test_json_valid_serde(json: serde_json::Value) {
2✔
456
        let refs: DocumentRefs = serde_json::from_value(json).unwrap();
2✔
457
        let json_from_refs = serde_json::to_value(&refs).unwrap();
2✔
458
        assert_eq!(refs, serde_json::from_value(json_from_refs).unwrap());
2✔
459
    }
2✔
460
}
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