1use 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 #[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 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 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}