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

input-output-hk / catalyst-libs / 15785561075

20 Jun 2025 06:39PM UTC coverage: 66.403%. First build
15785561075

Pull #373

github

web-flow
Merge 33cb41d16 into 8d6b5c989
Pull Request #373: chore(rust/signed-doc): Cleanup Catalyst Signed Document `Builder`, make it type safe, add special test builder.

309 of 320 new or added lines in 13 files covered. (96.56%)

11906 of 17930 relevant lines covered (66.4%)

2408.99 hits per line

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

86.05
/rust/signed_doc/src/builder.rs
1
//! Catalyst Signed Document Builder.
2
use catalyst_types::catalyst_id::CatalystId;
3

4
use crate::{
5
    signature::{tbs_data, Signature},
6
    CatalystSignedDocument, Content, ContentType, Metadata, Signatures,
7
};
8

9
/// Catalyst Signed Document Builder.
10
/// Its a type sage state machine which iterates type safely during different stages of
11
/// the Catalyst Signed Document build process:
12
/// Setting Metadata -> Setting Content -> Setting Signatures
13
pub type Builder = MetadataBuilder;
14

15
/// Only `metadata` builder part
16
pub struct MetadataBuilder(BuilderInner);
17

18
/// Only `content` builder part
19
pub struct ContentBuilder(BuilderInner);
20

21
/// Only `Signatures` builder part
22
pub struct SignaturesBuilder(BuilderInner);
23

24
/// Inner state of the Catalyst Signed Documents `Builder`
25
#[derive(Default)]
26
pub struct BuilderInner {
27
    /// metadata
28
    metadata: Metadata,
29
    /// content
30
    content: Content,
31
    /// signatures
32
    signatures: Signatures,
33
}
34

35
impl MetadataBuilder {
36
    /// Start building a signed document
37
    #[must_use]
38
    #[allow(clippy::new_without_default)]
39
    pub fn new() -> Self {
31✔
40
        Self(BuilderInner::default())
31✔
41
    }
31✔
42

43
    /// Set document metadata in JSON format
44
    /// Collect problem report if some fields are missing.
45
    ///
46
    /// # Errors
47
    /// - Fails if it is invalid metadata fields JSON object.
48
    pub fn with_json_metadata(mut self, json: serde_json::Value) -> anyhow::Result<ContentBuilder> {
31✔
49
        self.0.metadata = Metadata::from_json(json)?;
31✔
50
        Ok(ContentBuilder(self.0))
31✔
51
    }
31✔
52
}
53

54
impl ContentBuilder {
55
    /// Sets an empty content
56
    pub fn empty_content(self) -> SignaturesBuilder {
1✔
57
        SignaturesBuilder(self.0)
1✔
58
    }
1✔
59

60
    /// Set the provided JSON content, applying already set `content-encoding`.
61
    ///
62
    /// # Errors
63
    ///  - Verifies that `content-type` field is set to JSON
64
    ///  - Cannot serialize provided JSON
65
    ///  - Compression failure
66
    pub fn with_json_content(
30✔
67
        mut self, json: &serde_json::Value,
30✔
68
    ) -> anyhow::Result<SignaturesBuilder> {
30✔
69
        anyhow::ensure!(
30✔
70
            self.0.metadata.content_type()? == ContentType::Json,
30✔
NEW
71
            "Already set metadata field `content-type` is not JSON value"
×
72
        );
73

74
        let content = serde_json::to_vec(&json)?;
30✔
75
        if let Some(encoding) = self.0.metadata.content_encoding() {
30✔
76
            self.0.content = encoding.encode(&content)?.into();
15✔
77
        } else {
15✔
78
            self.0.content = content.into();
15✔
79
        }
15✔
80

81
        Ok(SignaturesBuilder(self.0))
30✔
82
    }
30✔
83
}
84

85
impl SignaturesBuilder {
86
    /// Add a signature to the document
87
    ///
88
    /// # Errors
89
    ///
90
    /// Fails if a `CatalystSignedDocument` cannot be created due to missing metadata or
91
    /// content, due to malformed data, or when the signed document cannot be
92
    /// converted into `coset::CoseSign`.
93
    pub fn add_signature(
17✔
94
        mut self, sign_fn: impl FnOnce(Vec<u8>) -> Vec<u8>, kid: CatalystId,
17✔
95
    ) -> anyhow::Result<Self> {
17✔
96
        if kid.is_id() {
17✔
97
            anyhow::bail!("Provided kid should be in a uri format, kid: {kid}");
×
98
        }
17✔
99
        let data_to_sign = tbs_data(&kid, &self.0.metadata, &self.0.content)?;
17✔
100
        let sign_bytes = sign_fn(data_to_sign);
17✔
101
        self.0.signatures.push(Signature::new(kid, sign_bytes));
17✔
102

17✔
103
        Ok(self)
17✔
104
    }
17✔
105

106
    /// Builds a document from the set `metadata`, `content` and `signatures`.
107
    ///
108
    /// # Errors:
109
    ///  - CBOR encoding/decoding failures
110
    pub fn build(self) -> anyhow::Result<CatalystSignedDocument> {
31✔
111
        let doc = build_document(&self.0.metadata, &self.0.content, &self.0.signatures)?;
31✔
112
        Ok(doc)
31✔
113
    }
31✔
114
}
115

116
/// Build document from the provided `metadata`, `content` and `signatures`, performs all
117
/// the decoding validation and collects a problem report.
118
fn build_document(
114✔
119
    metadata: &Metadata, content: &Content, signatures: &Signatures,
114✔
120
) -> anyhow::Result<CatalystSignedDocument> {
114✔
121
    let mut e = minicbor::Encoder::new(Vec::new());
114✔
122
    // COSE_Sign tag
114✔
123
    // <!https://datatracker.ietf.org/doc/html/rfc8152#page-9>
114✔
124
    e.tag(minicbor::data::Tag::new(98))?;
114✔
125
    e.array(4)?;
114✔
126
    // protected headers (metadata fields)
127
    e.bytes(minicbor::to_vec(metadata)?.as_slice())?;
114✔
128
    // empty unprotected headers
129
    e.map(0)?;
114✔
130
    // content
131
    e.encode(content)?;
114✔
132
    // signatures
133
    e.encode(signatures)?;
114✔
134
    CatalystSignedDocument::try_from(e.into_writer().as_slice())
114✔
135
}
114✔
136

137
impl From<&CatalystSignedDocument> for SignaturesBuilder {
138
    fn from(value: &CatalystSignedDocument) -> Self {
×
NEW
139
        Self(BuilderInner {
×
140
            metadata: value.inner.metadata.clone(),
×
141
            content: value.inner.content.clone(),
×
142
            signatures: value.inner.signatures.clone(),
×
NEW
143
        })
×
NEW
144
    }
×
145
}
146

147
#[cfg(test)]
148
pub(crate) mod tests {
149
    use crate::builder::SignaturesBuilder;
150

151
    /// A test version of the builder, which allows to build a not fully valid catalyst
152
    /// signed document
153
    pub(crate) struct Builder(super::BuilderInner);
154

155
    impl Default for Builder {
NEW
156
        fn default() -> Self {
×
NEW
157
            Self::new()
×
NEW
158
        }
×
159
    }
160

161
    impl Builder {
162
        /// Start building a signed document
163
        #[must_use]
164
        pub(crate) fn new() -> Self {
83✔
165
            Self(super::BuilderInner::default())
83✔
166
        }
83✔
167

168
        /// Add provided `SupportedField` into the `Metadata`.
169
        pub(crate) fn with_metadata_field(
114✔
170
            mut self, field: crate::metadata::SupportedField,
114✔
171
        ) -> Self {
114✔
172
            self.0.metadata.add_field(field);
114✔
173
            self
114✔
174
        }
114✔
175

176
        /// Set the content (COSE payload) to the document builder.
177
        /// It will set the content as its provided, make sure by yourself that
178
        /// `content-type` and `content-encoding` fields are aligned with the
179
        /// provided content bytes.
180
        pub(crate) fn with_content(mut self, content: Vec<u8>) -> Self {
25✔
181
            self.0.content = content.into();
25✔
182
            self
25✔
183
        }
25✔
184

185
        /// Add a signature to the document
186
        pub(crate) fn add_signature(
1✔
187
            mut self, sign_fn: impl FnOnce(Vec<u8>) -> Vec<u8>, kid: super::CatalystId,
1✔
188
        ) -> anyhow::Result<Self> {
1✔
189
            self.0 = SignaturesBuilder(self.0).add_signature(sign_fn, kid)?.0;
1✔
190
            Ok(self)
1✔
191
        }
1✔
192

193
        /// Build a signed document with the collected error report.
194
        /// Could provide an invalid document.
195
        pub(crate) fn build(self) -> super::CatalystSignedDocument {
83✔
196
            super::build_document(&self.0.metadata, &self.0.content, &self.0.signatures).unwrap()
83✔
197
        }
83✔
198
    }
199
}
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