Skip to main content

rain_metadata/cli/
build.rs

1use clap::Parser;
2use anyhow::anyhow;
3use itertools::izip;
4use std::path::PathBuf;
5use crate::cli::output::SupportedOutputEncoding;
6use crate::meta::{
7    RainMetaDocumentV1Item, KnownMeta, ContentType, ContentEncoding, ContentLanguage,
8    magic::KnownMagic,
9};
10
11/// command for building rain meta
12#[derive(Parser)]
13pub struct Build {
14    /// Output path. If not specified, the output is written to stdout.
15    #[arg(short, long)]
16    output_path: Option<PathBuf>,
17    /// Output encoding. If not specified, the output is written in binary format.
18    #[arg(short = 'E', long, default_value = "binary")]
19    output_encoding: SupportedOutputEncoding,
20    /// Global magic number. If not specified, the default magic number is used.
21    /// The default magic number is rain-meta-document-v1. Don't change this
22    /// unless you know what you are doing.
23    #[arg(short = 'M', long, default_value = "rain-meta-document-v1")]
24    global_magic: KnownMagic,
25    /// Sequence of input paths. The number of input paths must match the number
26    /// of magic numbers, content types, content encodings and content languages.
27    /// Reading from stdin is not supported but proccess substitution can be used.
28    #[arg(short, long, num_args = 1..)]
29    input_path: Vec<PathBuf>,
30    /// Sequence of magic numbers. The number of magic numbers must match the
31    /// number of input paths, content types, content encodings and content languages.
32    /// Magic numbers are arbitrary byte sequences used to build self-describing
33    /// payloads.
34    #[arg(short, long, num_args = 1..)]
35    magic: Vec<KnownMagic>,
36    /// Sequence of content types. The number of content types must match the
37    /// number of input paths, magic numbers, content encodings and content languages.
38    /// Content type is as per http headers.
39    #[arg(short = 't', long, num_args = 1..)]
40    content_type: Vec<ContentType>,
41    /// Sequence of content encodings. The number of content encodings must match the
42    /// number of input paths, magic numbers, content types and content languages.
43    /// Content encoding is as per http headers.
44    #[arg(short = 'e', long, num_args = 1..)]
45    content_encoding: Vec<ContentEncoding>,
46    /// Sequence of content languages. The number of content languages must match the
47    /// number of input paths, magic numbers, content types and content encodings.
48    /// Content language is as per http headers.
49    #[arg(short = 'l', long, num_args = 1..)]
50    content_language: Vec<ContentLanguage>,
51}
52
53/// Temporary housing for raw data before it is converted into a RainMetaDocumentV1Item.
54#[derive(Clone, Debug)]
55pub struct BuildItem {
56    /// Raw data. Ostensibly this is the content of a file.
57    pub data: Vec<u8>,
58    /// Magic number taken from build options.
59    pub magic: KnownMagic,
60    /// Content type taken from build options.
61    pub content_type: ContentType,
62    /// Content encoding taken from build options.
63    pub content_encoding: ContentEncoding,
64    /// Content language taken from build options.
65    pub content_language: ContentLanguage,
66}
67
68/// Moving from a BuildItem to a RainMetaDocumentV1Item requires normalization
69/// according to the magic number and encoding from the build options.
70impl TryFrom<&BuildItem> for RainMetaDocumentV1Item {
71    type Error = anyhow::Error;
72    fn try_from(item: &BuildItem) -> anyhow::Result<Self> {
73        let normalized = TryInto::<KnownMeta>::try_into(item.magic)?.normalize(&item.data)?;
74        let encoded = item.content_encoding.encode(&normalized);
75        Ok(RainMetaDocumentV1Item {
76            payload: serde_bytes::ByteBuf::from(encoded),
77            magic: item.magic,
78            content_type: item.content_type,
79            content_encoding: item.content_encoding,
80            content_language: item.content_language,
81            schema: None,
82        })
83    }
84}
85
86/// Build a rain meta document from a sequence of BuildItems.
87pub fn build_bytes(magic: KnownMagic, items: Vec<BuildItem>) -> anyhow::Result<Vec<u8>> {
88    let mut metas: Vec<RainMetaDocumentV1Item> = vec![];
89    for item in items {
90        metas.push(RainMetaDocumentV1Item::try_from(&item)?);
91    }
92    Ok(RainMetaDocumentV1Item::cbor_encode_seq(&metas, magic)?)
93}
94
95/// Build a rain meta document from command line options.
96/// Enforces length constraints on the input paths, magic numbers, content types,
97/// content encodings and content languages.
98/// Handles reading input files and writing to files/stdout according to the
99/// build options.
100pub fn build(b: Build) -> anyhow::Result<()> {
101    if b.input_path.len() != b.magic.len() {
102        return Err(anyhow!(
103            "{} inputs does not match {} magic numbers.",
104            b.input_path.len(),
105            b.magic.len()
106        ));
107    }
108
109    if b.input_path.len() != b.content_type.len() {
110        return Err(anyhow!(
111            "{} inputs does not match {} content types.",
112            b.input_path.len(),
113            b.content_type.len()
114        ));
115    }
116
117    if b.input_path.len() != b.content_encoding.len() {
118        return Err(anyhow!(
119            "{} inputs does not match {} content encodings.",
120            b.input_path.len(),
121            b.content_encoding.len()
122        ));
123    }
124
125    if b.input_path.len() != b.content_language.len() {
126        return Err(anyhow!(
127            "{} inputs does not match {} content languages.",
128            b.input_path.len(),
129            b.content_language.len()
130        ));
131    }
132    let mut items: Vec<BuildItem> = vec![];
133    for (input_path, magic, content_type, content_encoding, content_language) in izip!(
134        b.input_path.iter(),
135        b.magic.iter(),
136        b.content_type.iter(),
137        b.content_encoding.iter(),
138        b.content_language.iter()
139    ) {
140        items.push(BuildItem {
141            data: std::fs::read(input_path)?,
142            magic: *magic,
143            content_type: *content_type,
144            content_encoding: *content_encoding,
145            content_language: *content_language,
146        });
147    }
148    crate::cli::output::output(
149        &b.output_path,
150        b.output_encoding,
151        &build_bytes(b.global_magic, items)?,
152    )
153}
154
155#[cfg(all(test, not(target_family = "wasm")))]
156mod tests {
157    use strum::IntoEnumIterator;
158    use crate::meta::{
159        magic::{self, KnownMagic},
160        ContentType, ContentEncoding, ContentLanguage, RainMetaDocumentV1Item,
161    };
162    use super::BuildItem;
163    use super::build_bytes;
164
165    /// Test that the magic number prefix is correct for all known magic numbers
166    /// in isolation from all build items.
167    #[test]
168    fn test_build_empty() -> anyhow::Result<()> {
169        for global_magic in magic::KnownMagic::iter() {
170            let built_bytes = build_bytes(global_magic, vec![])?;
171            assert_eq!(built_bytes, global_magic.to_prefix_bytes());
172        }
173        Ok(())
174    }
175
176    /// We can build a single document item from a single build item.
177    /// Empty ABI documents are used to avoid testing the normalisation and
178    /// encoding process.
179    #[test]
180    fn test_into_meta_document() -> anyhow::Result<()> {
181        let build_item = BuildItem {
182            data: "[]".as_bytes().to_vec(),
183            magic: KnownMagic::SolidityAbiV2,
184            content_type: ContentType::Json,
185            content_encoding: ContentEncoding::None,
186            content_language: ContentLanguage::En,
187        };
188
189        let meta_document = RainMetaDocumentV1Item::try_from(&build_item)?;
190        let expected_meta_document = RainMetaDocumentV1Item {
191            payload: serde_bytes::ByteBuf::from("[]".as_bytes().to_vec()),
192            magic: KnownMagic::SolidityAbiV2,
193            content_type: ContentType::Json,
194            content_encoding: ContentEncoding::None,
195            content_language: ContentLanguage::En,
196            schema: None,
197        };
198        assert_eq!(meta_document, expected_meta_document);
199        Ok(())
200    }
201
202    /// The final CBOR bytes are as expected for a single build item. An empty
203    /// ABI is used to avoid testing the normalisation and encoding process.
204    #[test]
205    fn test_empty_item() -> anyhow::Result<()> {
206        let build_item = BuildItem {
207            data: "[]".as_bytes().to_vec(),
208            magic: KnownMagic::SolidityAbiV2,
209            content_type: ContentType::Json,
210            content_encoding: ContentEncoding::Identity,
211            content_language: ContentLanguage::En,
212        };
213
214        let bytes = super::build_bytes(KnownMagic::RainMetaDocumentV1, vec![build_item.clone()])?;
215
216        // https://github.com/rainprotocol/specs/blob/main/metadata-v1.md#example
217        // 8 byte magic number prefix
218        assert_eq!(
219            &bytes[0..8],
220            KnownMagic::RainMetaDocumentV1.to_prefix_bytes()
221        );
222        // cbor map with 5 keys
223        assert_eq!(bytes[8], 0xa5);
224        // key 0
225        assert_eq!(bytes[9], 0x00);
226        // major type 2 (bytes) length 2
227        assert_eq!(bytes[10], 0b010_00010);
228        // payload
229        assert_eq!(bytes[11..13], "[]".as_bytes()[..]);
230        // key 1
231        assert_eq!(bytes[13], 0x01);
232        // major type 0 (unsigned integer) value 27
233        assert_eq!(bytes[14], 0b000_11011);
234        // magic number
235        assert_eq!(&bytes[15..23], KnownMagic::SolidityAbiV2.to_prefix_bytes());
236        // key 2
237        assert_eq!(bytes[23], 0x02);
238        // text string application/json length 16
239        assert_eq!(bytes[24], 0b011_10000);
240        // the string application/json
241        assert_eq!(&bytes[25..41], "application/json".as_bytes());
242        // key 3
243        assert_eq!(bytes[41], 0x03);
244        // text string identity length 8
245        assert_eq!(bytes[42], 0b011_01000);
246        // the string identity
247        assert_eq!(&bytes[43..51], "identity".as_bytes());
248        // key 4
249        assert_eq!(bytes[51], 0x04);
250        // text string en length 2
251        assert_eq!(bytes[52], 0b011_00010);
252        // the string en
253        assert_eq!(&bytes[53..55], "en".as_bytes());
254
255        assert_eq!(bytes.len(), 55);
256
257        Ok(())
258    }
259
260    #[test]
261    fn test_cbor_encoding_type() -> anyhow::Result<()> {
262        let build_item = BuildItem {
263            data: "[]".as_bytes().to_vec(),
264            magic: KnownMagic::DotrainV1,
265            content_type: ContentType::Cbor,
266            content_encoding: ContentEncoding::Identity,
267            content_language: ContentLanguage::En,
268        };
269
270        let bytes = super::build_bytes(KnownMagic::RainMetaDocumentV1, vec![build_item.clone()])?;
271
272        // https://github.com/rainprotocol/specs/blob/main/metadata-v1.md#example
273        // 8 byte magic number prefix
274        assert_eq!(
275            &bytes[0..8],
276            KnownMagic::RainMetaDocumentV1.to_prefix_bytes()
277        );
278        // cbor map with 5 keys
279        assert_eq!(bytes[8], 0xa5);
280        // key 0
281        assert_eq!(bytes[9], 0x00);
282        // major type 2 (bytes) length 2
283        assert_eq!(bytes[10], 0b010_00010);
284        // payload
285        assert_eq!(bytes[11..13], "[]".as_bytes()[..]);
286        // key 1
287        assert_eq!(bytes[13], 0x01);
288        // major type 0 (unsigned integer) value 27
289        assert_eq!(bytes[14], 0b000_11011);
290        // magic number
291        assert_eq!(&bytes[15..23], KnownMagic::DotrainV1.to_prefix_bytes());
292        // key 2
293        assert_eq!(bytes[23], 0x02);
294        // text string application/cbor length 16
295        assert_eq!(bytes[24], 0b011_10000);
296        // the string application/cbor
297        assert_eq!(&bytes[25..41], "application/cbor".as_bytes());
298        // key 3
299        assert_eq!(bytes[41], 0x03);
300        // text string identity length 8
301        assert_eq!(bytes[42], 0b011_01000);
302        // the string identity
303        assert_eq!(&bytes[43..51], "identity".as_bytes());
304        // key 4
305        assert_eq!(bytes[51], 0x04);
306        // text string en length 2
307        assert_eq!(bytes[52], 0b011_00010);
308        // the string en
309        assert_eq!(&bytes[53..55], "en".as_bytes());
310
311        assert_eq!(bytes.len(), 55);
312
313        Ok(())
314    }
315}