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

input-output-hk / catalyst-libs / 19403595944

16 Nov 2025 09:31AM UTC coverage: 68.206% (-0.09%) from 68.298%
19403595944

push

github

web-flow
feat(rust/signed-doc): CatalystSignedDocument has CIDv1 (#621)

* feat(rust/signed-doc): CatalystSignedDocument has CIDv1

* feat(rust/signed-doc): Cid newtype

* break(rust/signed-doc): DocLocator(Cid) must be valid

* Breaking change that removes compatibility with old versions

* fix(rust/signed-doc): fix unit tests to support DocLocator

* removes support for old version

* fix(rust/signed-doc): fix integration tests

* chore(rust/signed-doc): fmt fix

* chore(rust/signed-doc): fix spelling

* chore(rust/signed-doc): fix doc comment

* chore(rust/signed-doc): Add `doc_ref` method for `CatalystSignedDoc`. (#640)

* update API

* wip

* wip

* wip

* fix test

* wip

* wip

* fix fmt

* fix(rust/signed-doc): remove stale example

---------

Co-authored-by: Joaquín Rosales <joaquin.rosales@iohk.io>

---------

Co-authored-by: Alex Pozhylenkov <leshiy12345678@gmail.com>

525 of 561 new or added lines in 12 files covered. (93.58%)

10 existing lines in 5 files now uncovered.

13841 of 20293 relevant lines covered (68.21%)

2725.97 hits per line

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

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

3
pub(crate) mod doc_locator;
4
mod doc_ref;
5
use std::{fmt::Display, ops::Deref};
6

7
use cbork_utils::{array::Array, decode_context::DecodeCtx};
8
pub use doc_locator::DocLocator;
9
pub use doc_ref::DocumentRef;
10
use minicbor::{Decode, Encode};
11

12
use crate::CompatibilityPolicy;
13

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

18
impl Deref for DocumentRefs {
19
    type Target = Vec<DocumentRef>;
20

21
    fn deref(&self) -> &Self::Target {
413✔
22
        &self.0
413✔
23
    }
413✔
24
}
25

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

48
/// Document reference error.
49
#[derive(Debug, Clone, thiserror::Error)]
50
pub enum DocRefError {
51
    /// Invalid string conversion
52
    #[error("Invalid string conversion: {0}")]
53
    StringConversion(String),
54
    /// Cannot decode hex.
55
    #[error("Cannot decode hex: {0}")]
56
    HexDecode(String),
57
}
58

59
impl From<Vec<DocumentRef>> for DocumentRefs {
60
    fn from(value: Vec<DocumentRef>) -> Self {
75✔
61
        DocumentRefs(value)
75✔
62
    }
75✔
63
}
64

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

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

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

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

100
                        let doc_refs = arr
423✔
101
                            .iter()
423✔
102
                            .map(|bytes| minicbor::Decoder::new(bytes).decode())
431✔
103
                            .collect::<Result<_, _>>()?;
423✔
104

105
                        Ok(DocumentRefs(doc_refs))
422✔
106
                    },
107
                    // Old structure (id, ver) - no longer supported as DocLocator requires a valid
108
                    // CID
109
                    minicbor::data::Type::Tag => {
110
                        Err(minicbor::decode::Error::message(format!(
30✔
111
                            "{CONTEXT}: Legacy document reference format (id, ver) without CID is no longer supported. \
30✔
112
                             DocLocator now requires a valid Content Identifier (CID)."
30✔
113
                        )))
30✔
114
                    },
115
                    other => {
5✔
116
                        Err(minicbor::decode::Error::message(format!(
5✔
117
                            "{CONTEXT}: Expected array of document reference, or tag of version and id, found {other}",
5✔
118
                        )))
5✔
119
                    },
120
                }
121
            },
122
            _ => {
123
                Err(minicbor::decode::Error::message(format!(
1✔
124
                    "{CONTEXT}: Empty array",
1✔
125
                )))
1✔
126
            },
127
        }
128
    }
460✔
129
}
130

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

150
        for doc_ref in &self.0 {
848✔
151
            doc_ref.encode(e, ctx)?;
428✔
152
        }
153
        Ok(())
420✔
154
    }
420✔
155
}
156

157
mod serde_impl {
158
    //! `serde::Deserialize` and `serde::Serialize` trait implementations
159

160
    use super::{DocumentRef, DocumentRefs};
161

162
    /// A struct to support deserializing for both the old and new version of `ref`.
163
    #[derive(serde::Deserialize)]
164
    #[serde(untagged)]
165
    enum DocRefSerde {
166
        /// Old structure of document reference.
167
        Old(DocumentRef),
168
        /// New structure of document reference.
169
        New(Vec<DocumentRef>),
170
    }
171

172
    impl serde::Serialize for DocumentRefs {
173
        fn serialize<S>(
1✔
174
            &self,
1✔
175
            serializer: S,
1✔
176
        ) -> Result<S::Ok, S::Error>
1✔
177
        where
1✔
178
            S: serde::Serializer,
1✔
179
        {
180
            self.0.serialize(serializer)
1✔
181
        }
1✔
182
    }
183

184
    impl<'de> serde::Deserialize<'de> for DocumentRefs {
185
        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
334✔
186
        where D: serde::Deserializer<'de> {
334✔
187
            match DocRefSerde::deserialize(deserializer)? {
334✔
UNCOV
188
                DocRefSerde::Old(v) => Ok(DocumentRefs(vec![v])),
×
189
                DocRefSerde::New(v) => Ok(DocumentRefs(v)),
334✔
190
            }
191
        }
334✔
192
    }
193
}
194

195
#[cfg(test)]
196
pub(crate) mod tests {
197

198
    use catalyst_types::uuid::{CborContext, UuidV4, UuidV7};
199
    use minicbor::{Decoder, Encoder};
200
    use test_case::test_case;
201

202
    use super::*;
203
    use crate::{ContentType, builder::Builder};
204

205
    pub(crate) fn create_dummy_doc_ref() -> DocumentRef {
42✔
206
        let id = UuidV7::new();
42✔
207
        let ver = UuidV7::new();
42✔
208
        let doc = Builder::new()
42✔
209
            .with_json_metadata(serde_json::json!({
42✔
210
                "id": id.to_string(),
42✔
211
                "ver": ver.to_string(),
42✔
212
                "type": UuidV4::new().to_string(),
42✔
213
                "content-type": ContentType::Json,
42✔
214
            }))
215
            .expect("Should create metadata")
42✔
216
            .with_json_content(&serde_json::json!({"test": "content"}))
42✔
217
            .expect("Should set content")
42✔
218
            .build()
42✔
219
            .expect("Should build document");
42✔
220

221
        doc.doc_ref().expect("Should generate DocumentRef")
42✔
222
    }
42✔
223

224
    #[test_case(
225
        CompatibilityPolicy::Accept,
226
        {
227
            Encoder::new(Vec::new())
228
        } ;
229
        "Invalid empty CBOR bytes"
230
    )]
231
    #[test_case(
232
        CompatibilityPolicy::Accept,
233
        {
234
            let mut e = Encoder::new(Vec::new());
235
            e.array(0).unwrap();
236
            e
237
        } ;
238
        "Invalid empty CBOR array"
239
    )]
240
    #[test_case(
241
        CompatibilityPolicy::Fail,
242
        {
243
            let mut e = Encoder::new(Vec::new());
244
            e.array(2)
245
                .unwrap()
246
                .encode_with(UuidV7::new(), &mut CborContext::Tagged)
247
                .unwrap()
248
                .encode_with(UuidV7::new(), &mut CborContext::Tagged)
249
                .unwrap();
250
            e
251
        } ;
252
        "Valid array of two uuid v7 (old format), fail policy"
253
    )]
254
    #[test_case(
255
        CompatibilityPolicy::Accept,
256
        {
257
            let mut e = Encoder::new(Vec::new());
258
            e.array(2)
259
                .unwrap()
260
                .encode_with(UuidV7::new(), &mut CborContext::Untagged)
261
                .unwrap()
262
                .encode_with(UuidV7::new(), &mut CborContext::Untagged)
263
                .unwrap();
264
            e
265
        } ;
266
        "Invalid untagged uuids v7 (old format)"
267
    )]
268
    #[test_case(
269
        CompatibilityPolicy::Accept,
270
        {
271
            let doc_ref = create_dummy_doc_ref();
272
            let mut e = Encoder::new(Vec::new());
273
            e.array(1)
274
                .unwrap()
275
                .array(3)
276
                .unwrap()
277
                .encode_with(*doc_ref.id(), &mut CborContext::Untagged)
278
                .unwrap()
279
                .encode_with(*doc_ref.ver(), &mut CborContext::Untagged)
280
                .unwrap()
281
                .encode(doc_ref.doc_locator().clone())
282
                .unwrap();
283
            e
284
        } ;
285
        "Invalid untagged uuid uuids v7 (new format)"
286
    )]
287
    fn test_invalid_cbor_decode(
5✔
288
        mut policy: CompatibilityPolicy,
5✔
289
        e: Encoder<Vec<u8>>,
5✔
290
    ) {
5✔
291
        assert!(
5✔
292
            DocumentRefs::decode(&mut Decoder::new(e.into_writer().as_slice()), &mut policy)
5✔
293
                .is_err()
5✔
294
        );
295
    }
5✔
296

297
    #[test_case(
298
        CompatibilityPolicy::Accept,
299
        |id, ver, doc_loc| {
1✔
300
            let mut e = Encoder::new(Vec::new());
1✔
301
            e.array(1)
1✔
302
                .unwrap()
1✔
303
                .array(3)
1✔
304
                .unwrap()
1✔
305
                .encode_with(id, &mut CborContext::Tagged)
1✔
306
                .unwrap()
1✔
307
                .encode_with(ver, &mut CborContext::Tagged)
1✔
308
                .unwrap()
1✔
309
                .encode(doc_loc)
1✔
310
                .unwrap();
1✔
311
            e
1✔
312
        } ;
1✔
313
        "Array of new doc ref (new format)"
314
    )]
315
    #[test_case(
316
        CompatibilityPolicy::Fail,
317
        |id, ver, doc_loc| {
1✔
318
            let mut e = Encoder::new(Vec::new());
1✔
319
            e.array(1)
1✔
320
                .unwrap()
1✔
321
                .array(3)
1✔
322
                .unwrap()
1✔
323
                .encode_with(id, &mut CborContext::Tagged)
1✔
324
                .unwrap()
1✔
325
                .encode_with(ver, &mut CborContext::Tagged)
1✔
326
                .unwrap()
1✔
327
                .encode(doc_loc)
1✔
328
                .unwrap();
1✔
329
            e
1✔
330
        } ;
1✔
331
        "Array of new doc ref (new format), fail policy"
332
    )]
333
    fn test_valid_cbor_decode(
2✔
334
        mut policy: CompatibilityPolicy,
2✔
335
        e_gen: impl FnOnce(UuidV7, UuidV7, DocLocator) -> Encoder<Vec<u8>>,
2✔
336
    ) {
2✔
337
        let doc_ref = create_dummy_doc_ref();
2✔
338
        let e = e_gen(*doc_ref.id(), *doc_ref.ver(), doc_ref.doc_locator().clone());
2✔
339

340
        let doc_refs =
2✔
341
            DocumentRefs::decode(&mut Decoder::new(e.into_writer().as_slice()), &mut policy)
2✔
342
                .unwrap();
2✔
343
        assert_eq!(doc_refs.0, vec![doc_ref]);
2✔
344
    }
2✔
345

346
    #[test]
347
    fn test_json_valid_serde() {
1✔
348
        let doc_ref1 = create_dummy_doc_ref();
1✔
349
        let doc_ref2 = create_dummy_doc_ref();
1✔
350

351
        let refs = DocumentRefs(vec![doc_ref1, doc_ref2]);
1✔
352

353
        let json = serde_json::to_value(&refs).unwrap();
1✔
354
        let refs_from_json: DocumentRefs = serde_json::from_value(json).unwrap();
1✔
355

356
        assert_eq!(refs, refs_from_json);
1✔
357
    }
1✔
358
}
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