cyclonedx_rust/
lib.rs

1//! #CycloneDx-Rust
2//!
3//! CycloneDx-Rust is a Crate library for encoding and decoding [CycloneDx](https://cyclonedx.org/) files in both XML and JSON format
4//! to the 1.2 spec
5//!
6//! To encode the CycloneDx you cab=n either build up the structure using the provided <X>::new() methods, passing in the parameters where necessary
7//! or make use of the builder pattern.
8//! The builder patterns are created at build time so intelli-sense may not be available. Howver, each struct, for example:
9//! ```
10//! use cyclonedx_rust::CycloneDX;
11//!
12//! CycloneDX::new(None, None, None, None);
13//! ```
14//! can be built as follows:
15//! ```
16//! use cyclonedx_rust::CycloneDXBuilder;
17//!
18//! CycloneDXBuilder::default()
19//!  .metadata(None)
20//!  .components(None)
21//!  .services(None)
22//!  .dependencies(None)
23//!  .build();
24//! ```
25//!
26//! # Encoding
27//! An example of how to encode a CycloneDX BoM to a file:
28//!
29//! ```
30//! use cyclonedx_rust::{CycloneDX, CycloneDXFormatType};
31//! use std::io::BufWriter;
32//! use std::fs::File;
33//!
34//! let mut buffer = BufWriter::new(File::create("foo.txt").unwrap());
35//! let cyclone_dx = CycloneDX::new(None, None, None, None);
36//! CycloneDX::encode(&mut buffer, cyclone_dx, CycloneDXFormatType::XML);
37//! ```
38//!
39//! # Decoding
40//! An example of how to decode a CycloneDX BoM:
41//!
42//! ```
43//! use cyclonedx_rust::{CycloneDX, CycloneDXFormatType};
44//! use std::fs::File;
45//! use std::io::BufReader;
46//! use std::path::PathBuf;
47//!
48//! let mut test_folder = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
49//! test_folder.push("resources/test/bom-1.2.xml");
50//! let file = File::open(test_folder);
51//! let mut reader = BufReader::new(file.unwrap());
52//!
53//! let result: CycloneDX = CycloneDX::decode(reader, CycloneDXFormatType::XML).unwrap();
54//! ```
55use std::error::Error;
56use std::fmt;
57use std::fmt::Formatter;
58
59use derive_builder::Builder;
60use serde::{Deserialize, Serialize};
61use serde_with::skip_serializing_none;
62use yaserde::ser::Config;
63use yaserde_derive::{YaDeserialize, YaSerialize};
64
65use crate::dependency_type::DependencyTypes;
66use crate::service::Services;
67use component::Component;
68use metadata::Metadata;
69
70mod common;
71pub mod component;
72mod dependency_type;
73pub mod metadata;
74pub mod service;
75
76const XMLNS: &'static str = "http://cyclonedx.org/schema/Bom/1.2";
77const BOM_FORMAT: &'static str = "CycloneDX";
78const SPEC_VERSION: &'static str = "1.2";
79const DEFAULT_VERSION: &'static str = "1";
80
81#[skip_serializing_none]
82#[derive(Default, Builder, Serialize, Deserialize, YaSerialize, YaDeserialize)]
83#[yaserde(rename = "bom")]
84#[serde(rename = "bom", rename_all = "camelCase")]
85#[yaserde(
86    prefix = "ns",
87    default_namespace = "ns",
88    namespace = "ns: http://cyclonedx.org/schema/bom/1.2"
89)]
90pub struct CycloneDX {
91    // JSON only
92    #[yaserde(skip_serializing_if = "json_skip")]
93    bom_format: String,
94    #[yaserde(skip_serializing_if = "json_skip")]
95    spec_version: String,
96
97    #[yaserde(attribute)]
98    version: String,
99
100    #[yaserde(rename = "serialNumber", attribute)]
101    serial_number: String,
102
103    metadata: Option<Metadata>,
104    components: Option<Components>,
105    services: Option<Services>,
106    dependencies: Option<DependencyTypes>,
107}
108
109impl CycloneDX {
110    pub fn new(
111        metadata: Option<Metadata>,
112        components: Option<Components>,
113        services: Option<Services>,
114        dependencies: Option<DependencyTypes>,
115    ) -> Self {
116        CycloneDX {
117            bom_format: BOM_FORMAT.to_string(),
118            spec_version: SPEC_VERSION.to_string(),
119            serial_number: "urn:uuid:".to_owned() + &uuid::Uuid::new_v4().to_string(),
120            version: DEFAULT_VERSION.to_string(),
121            metadata,
122            components,
123            services,
124            dependencies,
125        }
126    }
127
128    pub fn decode<R>(
129        reader: R,
130        format: CycloneDXFormatType,
131    ) -> Result<CycloneDX, CycloneDXDecodeError>
132    where
133        R: std::io::Read,
134    {
135        let result: Result<CycloneDX, String> = match format {
136            CycloneDXFormatType::XML => {
137                let result: Result<CycloneDX, String> = yaserde::de::from_reader(reader);
138                match result {
139                    Ok(response) => Ok(response),
140                    Err(err) => Err(err),
141                }
142            }
143            CycloneDXFormatType::JSON => {
144                unimplemented!();
145                let cyclone_dx: CycloneDX = serde_json::from_reader(reader).unwrap();
146                Ok(cyclone_dx)
147            }
148        };
149
150        if result.is_err() {
151            return Err(CycloneDXDecodeError {});
152        }
153        Ok(result.unwrap())
154    }
155
156    pub fn encode<W>(
157        writer: W,
158        cyclone_dx: CycloneDX,
159        format: CycloneDXFormatType,
160    ) -> Result<(), CycloneDXEncodeError>
161    where
162        W: std::io::Write,
163    {
164        let result = match format {
165            CycloneDXFormatType::XML => {
166                let config: Config = Config {
167                    perform_indent: true,
168                    write_document_declaration: true,
169                    indent_string: None,
170                };
171                match yaserde::ser::serialize_with_writer(&cyclone_dx, writer, &config) {
172                    Ok(_) => Ok(()),
173                    Err(err) => Err(err),
174                }
175            }
176
177            CycloneDXFormatType::JSON => {
178                unimplemented!();
179                match serde_json::to_writer_pretty(writer, &cyclone_dx) {
180                    Ok(_) => Ok(()),
181                    Err(err) => Err(err.to_string()),
182                }
183            }
184        };
185
186        if result.is_err() {
187            return Err(CycloneDXEncodeError {});
188        }
189        Ok(())
190    }
191
192    pub const fn json_skip(&self, _: &str) -> bool {
193        true
194    }
195}
196
197#[derive(PartialEq)]
198pub enum CycloneDXFormatType {
199    XML,
200    JSON,
201}
202
203#[derive(Debug)]
204pub struct CycloneDXEncodeError {}
205impl Error for CycloneDXEncodeError {}
206impl fmt::Display for CycloneDXEncodeError {
207    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
208        write!(f, "Error encoding CycloneDX BOM")
209    }
210}
211
212#[derive(Debug)]
213pub struct CycloneDXDecodeError {}
214impl Error for CycloneDXDecodeError {}
215impl fmt::Display for CycloneDXDecodeError {
216    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
217        write!(f, "Error decoding CycloneDX BOM")
218    }
219}
220
221#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, YaSerialize, YaDeserialize)]
222pub struct Components {
223    pub component: Vec<Component>,
224}
225
226#[cfg(test)]
227mod tests {
228    use std::io::{BufReader, ErrorKind};
229
230    use crate::component::classification::Classification;
231    use crate::CycloneDXFormatType::XML;
232    use crate::{CycloneDX, CycloneDXFormatType};
233    use std::fs::File;
234    use std::path::PathBuf;
235
236    #[test]
237    fn error_if_invalid_writer() {
238        let cyclone_dx = CycloneDX::new(None, None, None, None);
239
240        impl std::io::Write for CycloneDX {
241            fn write(&mut self, _buf: &[u8]) -> Result<usize, std::io::Error> {
242                return Err(std::io::Error::new(ErrorKind::BrokenPipe, ""));
243            }
244
245            fn flush(&mut self) -> Result<(), std::io::Error> {
246                return Err(std::io::Error::new(ErrorKind::BrokenPipe, ""));
247            }
248        }
249
250        // Used to to get access to the dummy Write trait above
251        let writer = Box::new(CycloneDX::new(None, None, None, None));
252        let result = CycloneDX::encode(writer, cyclone_dx, XML);
253
254        assert!(result.is_err());
255    }
256
257    #[test]
258    pub fn can_decode() {
259        let reader = setup("bom-1.2.xml");
260
261        let result: CycloneDX = yaserde::de::from_reader(reader).unwrap();
262
263        assert_eq!(
264            result.serial_number,
265            "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79"
266        );
267
268        validate(result);
269    }
270
271    #[test]
272    pub fn can_decode_using_decoder() {
273        let reader = setup("bom-1.2.xml");
274
275        let cyclone_dx = CycloneDX::decode(reader, CycloneDXFormatType::XML).unwrap();
276
277        validate(cyclone_dx);
278    }
279
280    #[test]
281    pub fn can_recode_xml() {
282        let mut buffer = Vec::new();
283        let cyclone_dx = CycloneDX::new(None, None, None, None);
284        CycloneDX::encode(&mut buffer, cyclone_dx, CycloneDXFormatType::XML);
285        let response = CycloneDX::decode(&buffer[..], CycloneDXFormatType::XML).unwrap();
286
287        assert_eq!(response.version, "1");
288    }
289
290    #[test]
291    pub fn can_encode_basic_xml() {
292        let mut writer = Vec::new();
293        let cyclone_dx = CycloneDX::new(None, None, None, None);
294        CycloneDX::encode(&mut writer, cyclone_dx, CycloneDXFormatType::XML);
295
296        let result = String::from_utf8(writer).unwrap();
297        assert!(!result.contains("CycloneDX"));
298    }
299
300    // #[test]
301    // pub fn can_encode_basic_json() {
302    //     let mut writer = Vec::new();
303    //     let cyclone_dx = CycloneDX::new(None, None, None, None);
304    //     CycloneDX::encode(&mut writer, cyclone_dx, CycloneDXFormatType::JSON);
305    //
306    //     let result = String::from_utf8(writer).unwrap();
307    //     assert!(result.contains("CycloneDX"));
308    // }
309
310    fn validate(cyclone_dx: CycloneDX) {
311        let metadata = cyclone_dx.metadata.as_ref().unwrap();
312        assert_eq!(metadata.time_stamp, "2020-04-07T07:01:00Z");
313
314        let component = cyclone_dx.components.as_ref().unwrap();
315        assert_eq!(component.component.len(), 3);
316        assert_eq!(
317            component.component[0].name.as_ref().unwrap(),
318            "tomcat-catalina"
319        );
320        assert_eq!(
321            component.component[2].component_type,
322            Classification::Framework
323        );
324
325        let services = cyclone_dx.services.as_ref().unwrap();
326        assert_eq!(services.service.len(), 1);
327        assert_eq!(services.service[0].name, "Stock ticker service");
328        assert_eq!(
329            services.service[0].endpoints.as_ref().unwrap().endpoint[0].value,
330            "https://partner.org/api/v1/lookup"
331        );
332    }
333
334    fn setup(file: &str) -> BufReader<File> {
335        let mut test_folder = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
336        test_folder.push("resources/test/".to_owned() + file);
337        let file = File::open(test_folder);
338        let reader = BufReader::new(file.unwrap());
339        reader
340    }
341}