11//! Catalyst Signed Document Builder.
2- use catalyst_types:: { catalyst_id:: CatalystId , problem_report :: ProblemReport } ;
2+ use catalyst_types:: catalyst_id:: CatalystId ;
33
44use crate :: {
55 signature:: { tbs_data, Signature } ,
6- CatalystSignedDocument , Content , Metadata , Signatures ,
6+ CatalystSignedDocument , Content , ContentType , Metadata , Signatures ,
77} ;
88
99/// Catalyst Signed Document Builder.
10- #[ derive( Debug ) ]
11- pub struct 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 {
1227 /// metadata
1328 metadata : Metadata ,
1429 /// content
@@ -17,46 +32,57 @@ pub struct Builder {
1732 signatures : Signatures ,
1833}
1934
20- impl Default for Builder {
21- fn default ( ) -> Self {
22- Self :: new ( )
23- }
24- }
25-
26- impl Builder {
35+ impl MetadataBuilder {
2736 /// Start building a signed document
2837 #[ must_use]
38+ #[ allow( clippy:: new_without_default) ]
2939 pub fn new ( ) -> Self {
30- Self {
31- metadata : Metadata :: default ( ) ,
32- content : Content :: default ( ) ,
33- signatures : Signatures :: default ( ) ,
34- }
40+ Self ( BuilderInner :: default ( ) )
3541 }
3642
3743 /// Set document metadata in JSON format
3844 /// Collect problem report if some fields are missing.
3945 ///
4046 /// # Errors
4147 /// - Fails if it is invalid metadata fields JSON object.
42- pub fn with_json_metadata ( mut self , json : serde_json:: Value ) -> anyhow:: Result < Self > {
43- self . metadata = Metadata :: from_json ( json, & ProblemReport :: new ( "" ) ) ;
44- Ok ( self )
48+ pub fn with_json_metadata ( mut self , json : serde_json:: Value ) -> anyhow:: Result < ContentBuilder > {
49+ self . 0 . metadata = Metadata :: from_json ( json) ?;
50+ Ok ( ContentBuilder ( self . 0 ) )
51+ }
52+ }
53+
54+ impl ContentBuilder {
55+ /// Sets an empty content
56+ pub fn empty_content ( self ) -> SignaturesBuilder {
57+ SignaturesBuilder ( self . 0 )
4558 }
4659
47- /// Set decoded (original) document content bytes
60+ /// Set the provided JSON content, applying already set `content-encoding`.
4861 ///
4962 /// # Errors
63+ /// - Verifies that `content-type` field is set to JSON
64+ /// - Cannot serialize provided JSON
5065 /// - Compression failure
51- pub fn with_decoded_content ( mut self , decoded : Vec < u8 > ) -> anyhow:: Result < Self > {
52- if let Some ( encoding) = self . metadata . content_encoding ( ) {
53- self . content = encoding. encode ( & decoded) ?. into ( ) ;
66+ pub fn with_json_content (
67+ mut self , json : & serde_json:: Value ,
68+ ) -> anyhow:: Result < SignaturesBuilder > {
69+ anyhow:: ensure!(
70+ self . 0 . metadata. content_type( ) ? == ContentType :: Json ,
71+ "Already set metadata field `content-type` is not JSON value"
72+ ) ;
73+
74+ let content = serde_json:: to_vec ( & json) ?;
75+ if let Some ( encoding) = self . 0 . metadata . content_encoding ( ) {
76+ self . 0 . content = encoding. encode ( & content) ?. into ( ) ;
5477 } else {
55- self . content = decoded . into ( ) ;
78+ self . 0 . content = content . into ( ) ;
5679 }
57- Ok ( self )
80+
81+ Ok ( SignaturesBuilder ( self . 0 ) )
5882 }
83+ }
5984
85+ impl SignaturesBuilder {
6086 /// Add a signature to the document
6187 ///
6288 /// # Errors
@@ -70,49 +96,104 @@ impl Builder {
7096 if kid. is_id ( ) {
7197 anyhow:: bail!( "Provided kid should be in a uri format, kid: {kid}" ) ;
7298 }
73- let data_to_sign = tbs_data ( & kid, & self . metadata , & self . content ) ?;
99+ let data_to_sign = tbs_data ( & kid, & self . 0 . metadata , & self . 0 . content ) ?;
74100 let sign_bytes = sign_fn ( data_to_sign) ;
75- self . signatures . push ( Signature :: new ( kid, sign_bytes) ) ;
101+ self . 0 . signatures . push ( Signature :: new ( kid, sign_bytes) ) ;
76102
77103 Ok ( self )
78104 }
79105
80- /// Build a signed document with the collected error report.
81- /// Could provide an invalid document.
106+ /// Builds a document from the set `metadata`, `content` and `signatures`.
82107 ///
83- /// # Panics
84- /// Should not panic
85- #[ must_use]
86- #[ allow(
87- clippy:: unwrap_used,
88- reason = "At this point all the data MUST be correctly encodable, and the final prepared bytes MUST be correctly decodable as a CatalystSignedDocument object."
89- ) ]
90- pub fn build ( self ) -> CatalystSignedDocument {
91- let mut e = minicbor:: Encoder :: new ( Vec :: new ( ) ) ;
92- // COSE_Sign tag
93- // <!https://datatracker.ietf.org/doc/html/rfc8152#page-9>
94- e. tag ( minicbor:: data:: Tag :: new ( 98 ) ) . unwrap ( ) ;
95- e. array ( 4 ) . unwrap ( ) ;
96- // protected headers (metadata fields)
97- e. bytes ( minicbor:: to_vec ( & self . metadata ) . unwrap ( ) . as_slice ( ) )
98- . unwrap ( ) ;
99- // empty unprotected headers
100- e. map ( 0 ) . unwrap ( ) ;
101- // content
102- e. encode ( & self . content ) . unwrap ( ) ;
103- // signatures
104- e. encode ( self . signatures ) . unwrap ( ) ;
105-
106- CatalystSignedDocument :: try_from ( e. into_writer ( ) . as_slice ( ) ) . unwrap ( )
108+ /// # Errors:
109+ /// - CBOR encoding/decoding failures
110+ pub fn build ( self ) -> anyhow:: Result < CatalystSignedDocument > {
111+ let doc = build_document ( & self . 0 . metadata , & self . 0 . content , & self . 0 . signatures ) ?;
112+ Ok ( doc)
107113 }
108114}
109115
110- impl From < & CatalystSignedDocument > for Builder {
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 (
119+ metadata : & Metadata , content : & Content , signatures : & Signatures ,
120+ ) -> anyhow:: Result < CatalystSignedDocument > {
121+ let mut e = minicbor:: Encoder :: new ( Vec :: new ( ) ) ;
122+ // COSE_Sign tag
123+ // <!https://datatracker.ietf.org/doc/html/rfc8152#page-9>
124+ e. tag ( minicbor:: data:: Tag :: new ( 98 ) ) ?;
125+ e. array ( 4 ) ?;
126+ // protected headers (metadata fields)
127+ e. bytes ( minicbor:: to_vec ( metadata) ?. as_slice ( ) ) ?;
128+ // empty unprotected headers
129+ e. map ( 0 ) ?;
130+ // content
131+ e. encode ( content) ?;
132+ // signatures
133+ e. encode ( signatures) ?;
134+ CatalystSignedDocument :: try_from ( e. into_writer ( ) . as_slice ( ) )
135+ }
136+
137+ impl From < & CatalystSignedDocument > for SignaturesBuilder {
111138 fn from ( value : & CatalystSignedDocument ) -> Self {
112- Self {
139+ Self ( BuilderInner {
113140 metadata : value. inner . metadata . clone ( ) ,
114141 content : value. inner . content . clone ( ) ,
115142 signatures : value. inner . signatures . clone ( ) ,
143+ } )
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 {
156+ fn default ( ) -> Self {
157+ Self :: new ( )
158+ }
159+ }
160+
161+ impl Builder {
162+ /// Start building a signed document
163+ #[ must_use]
164+ pub ( crate ) fn new ( ) -> Self {
165+ Self ( super :: BuilderInner :: default ( ) )
166+ }
167+
168+ /// Add provided `SupportedField` into the `Metadata`.
169+ pub ( crate ) fn with_metadata_field (
170+ mut self , field : crate :: metadata:: SupportedField ,
171+ ) -> Self {
172+ self . 0 . metadata . add_field ( field) ;
173+ self
174+ }
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 {
181+ self . 0 . content = content. into ( ) ;
182+ self
183+ }
184+
185+ /// Add a signature to the document
186+ pub ( crate ) fn add_signature (
187+ mut self , sign_fn : impl FnOnce ( Vec < u8 > ) -> Vec < u8 > , kid : super :: CatalystId ,
188+ ) -> anyhow:: Result < Self > {
189+ self . 0 = SignaturesBuilder ( self . 0 ) . add_signature ( sign_fn, kid) ?. 0 ;
190+ Ok ( self )
191+ }
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 {
196+ super :: build_document ( & self . 0 . metadata , & self . 0 . content , & self . 0 . signatures ) . unwrap ( )
116197 }
117198 }
118199}
0 commit comments