1use std::fs::File;
2use std::io::{self, BufWriter, Write};
3
4use crate::output_schema::Output;
5
6mod cyclonedx;
7mod debian;
8mod html;
9mod jsonl;
10mod shared;
11mod spdx;
12mod template;
13
14pub(crate) const SPDX_DOCUMENT_NOTICE: &str = "Generated with Provenant and provided on an \"AS IS\" BASIS, WITHOUT WARRANTIES\nOR CONDITIONS OF ANY KIND, either express or implied. No content created from\nProvenant should be considered or used as legal advice. Consult an attorney\nfor legal advice.\nProvenant is a free software code scanning tool.\nVisit https://github.com/mstykow/provenant/ for support and download.\nSPDX License List: 3.27";
15const OUTPUT_BUFFER_SIZE: usize = 1024 * 1024;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum OutputFormat {
19 #[default]
20 Json,
21 JsonPretty,
22 Yaml,
23 JsonLines,
24 Debian,
25 Html,
26 CustomTemplate,
27 SpdxTv,
28 SpdxRdf,
29 CycloneDxJson,
30 CycloneDxXml,
31}
32
33#[derive(Debug, Clone, Default)]
34pub struct OutputWriteConfig {
35 pub format: OutputFormat,
36 pub custom_template: Option<String>,
37 pub scanned_path: Option<String>,
38}
39
40pub trait OutputWriter {
41 fn write(
42 &self,
43 output: &Output,
44 writer: &mut dyn Write,
45 config: &OutputWriteConfig,
46 ) -> io::Result<()>;
47}
48
49pub struct FormatWriter {
50 format: OutputFormat,
51}
52
53pub fn writer_for_format(format: OutputFormat) -> FormatWriter {
54 FormatWriter { format }
55}
56
57impl OutputWriter for FormatWriter {
58 fn write(
59 &self,
60 output: &Output,
61 writer: &mut dyn Write,
62 config: &OutputWriteConfig,
63 ) -> io::Result<()> {
64 match self.format {
65 OutputFormat::Json => {
66 serde_json::to_writer(&mut *writer, output).map_err(shared::io_other)?;
67 writer.write_all(b"\n")
68 }
69 OutputFormat::JsonPretty => {
70 serde_json::to_writer_pretty(&mut *writer, output).map_err(shared::io_other)?;
71 writer.write_all(b"\n")
72 }
73 OutputFormat::Yaml => write_yaml(output, writer),
74 OutputFormat::JsonLines => jsonl::write_json_lines(output, writer),
75 OutputFormat::Debian => debian::write_debian_copyright(output, writer),
76 OutputFormat::Html => html::write_html_report(output, writer),
77 OutputFormat::CustomTemplate => template::write_custom_template(output, writer, config),
78 OutputFormat::SpdxTv => spdx::write_spdx_tag_value(output, writer, config),
79 OutputFormat::SpdxRdf => spdx::write_spdx_rdf_xml(output, writer, config),
80 OutputFormat::CycloneDxJson => cyclonedx::write_cyclonedx_json(output, writer),
81 OutputFormat::CycloneDxXml => cyclonedx::write_cyclonedx_xml(output, writer),
82 }
83 }
84}
85
86pub fn write_output_file(
87 output_file: &str,
88 output: &Output,
89 config: &OutputWriteConfig,
90) -> io::Result<()> {
91 if output_file == "-" {
92 let stdout = io::stdout();
93 let handle = stdout.lock();
94 let mut writer = BufWriter::with_capacity(OUTPUT_BUFFER_SIZE, handle);
95 writer_for_format(config.format).write(output, &mut writer, config)?;
96 return writer.flush();
97 }
98
99 let file = File::create(output_file)?;
100 let mut writer = BufWriter::with_capacity(OUTPUT_BUFFER_SIZE, file);
101 writer_for_format(config.format).write(output, &mut writer, config)?;
102 writer.flush()
103}
104
105fn write_yaml(output: &Output, writer: &mut dyn Write) -> io::Result<()> {
106 yaml_serde::to_writer(&mut *writer, output).map_err(shared::io_other)?;
107 writer.write_all(b"\n")
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use serde_json::Value;
114 use std::fs;
115
116 use crate::models::{
117 Author, Copyright, ExtraData, FileInfo, FileType, GitSha1, Header, Holder,
118 LicenseDetection, LineNumber, Match, MatchScore, Md5Digest, OutputEmail, OutputURL,
119 Package, PackageData, PackageUid, Sha1Digest, Sha256Digest, SystemEnvironment,
120 };
121 use crate::output_schema::OutputFileInfo;
122
123 #[test]
124 fn test_yaml_writer_outputs_yaml() {
125 let output = Output::from(&sample_internal_output());
126 let mut bytes = Vec::new();
127 writer_for_format(OutputFormat::Yaml)
128 .write(&output, &mut bytes, &OutputWriteConfig::default())
129 .expect("yaml write should succeed");
130 let rendered = String::from_utf8(bytes).expect("yaml should be utf-8");
131 assert!(rendered.contains("headers:"));
132 assert!(rendered.contains("files:"));
133 }
134
135 #[test]
136 fn test_json_lines_writer_outputs_parseable_lines() {
137 let output = Output::from(&sample_internal_output());
138 let mut bytes = Vec::new();
139 writer_for_format(OutputFormat::JsonLines)
140 .write(&output, &mut bytes, &OutputWriteConfig::default())
141 .expect("json-lines write should succeed");
142
143 let rendered = String::from_utf8(bytes).expect("json-lines should be utf-8");
144 let lines = rendered.lines().collect::<Vec<_>>();
145 assert!(lines.len() >= 2);
146 for line in lines {
147 serde_json::from_str::<Value>(line).expect("each line should be valid json");
148 }
149 }
150
151 #[test]
152 fn test_debian_writer_outputs_dep5_style_document() {
153 let mut internal = sample_internal_output();
154 internal.files[0].license_expression = Some("mit".to_string());
155 internal.files[0].license_detections[0].matches[0].matched_text = Some(
156 "Permission is hereby granted, free of charge, to any person obtaining a copy"
157 .to_string(),
158 );
159 let output = Output::from(&internal);
160
161 let mut bytes = Vec::new();
162 writer_for_format(OutputFormat::Debian)
163 .write(&output, &mut bytes, &OutputWriteConfig::default())
164 .expect("debian write should succeed");
165
166 let rendered = String::from_utf8(bytes).expect("debian output should be utf-8");
167 assert!(rendered.contains(
168 "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/"
169 ));
170 assert!(rendered.contains("Comment: Generated with Provenant"));
171 assert!(rendered.contains("Files: src/main.rs"));
172 assert!(rendered.contains("Copyright: Example Org"));
173 assert!(rendered.contains("License: mit"));
174 assert!(rendered.contains(" Permission is hereby granted, free of charge"));
175 }
176
177 #[test]
178 fn test_debian_writer_skips_directories_and_deduplicates_license_texts() {
179 let mut internal = sample_internal_output();
180 internal.files.insert(
181 0,
182 FileInfo::new(
183 "src".to_string(),
184 "src".to_string(),
185 String::new(),
186 "src".to_string(),
187 FileType::Directory,
188 None,
189 None,
190 0,
191 None,
192 None,
193 None,
194 None,
195 None,
196 vec![],
197 None,
198 vec![],
199 vec![],
200 vec![],
201 vec![],
202 vec![],
203 vec![],
204 vec![],
205 vec![],
206 vec![],
207 ),
208 );
209 internal.files[1].license_expression = Some("mit".to_string());
210 internal.files[1].license_detections[0].matches[0].matched_text =
211 Some("Same text".to_string());
212 internal.files[1].license_detections[0].matches.push(Match {
213 license_expression: "mit".to_string(),
214 license_expression_spdx: "MIT".to_string(),
215 from_file: Some("src/main.rs".to_string()),
216 start_line: LineNumber::ONE,
217 end_line: LineNumber::ONE,
218 matcher: Some("2-aho".to_string()),
219 score: MatchScore::MAX,
220 matched_length: Some(1),
221 match_coverage: Some(100.0),
222 rule_relevance: Some(100),
223 rule_identifier: Some("mit_rule".to_string()),
224 rule_url: None,
225 matched_text: Some("Same text again".to_string()),
226 referenced_filenames: None,
227 matched_text_diagnostics: None,
228 });
229 let output = Output::from(&internal);
230
231 let mut bytes = Vec::new();
232 writer_for_format(OutputFormat::Debian)
233 .write(&output, &mut bytes, &OutputWriteConfig::default())
234 .expect("debian write should succeed");
235
236 let rendered = String::from_utf8(bytes).expect("debian output should be utf-8");
237 assert!(!rendered.contains("Files: src\n"));
238 assert_eq!(rendered.matches(" Same text").count(), 1);
239 }
240
241 #[test]
242 fn test_file_info_serialization_omits_info_fields_when_unset() {
243 let file = FileInfo::new(
244 "main.rs".to_string(),
245 "main".to_string(),
246 "rs".to_string(),
247 "src/main.rs".to_string(),
248 FileType::File,
249 None,
250 None,
251 42,
252 None,
253 None,
254 None,
255 None,
256 None,
257 vec![],
258 None,
259 vec![],
260 vec![],
261 vec![],
262 vec![],
263 vec![],
264 vec![],
265 vec![],
266 vec![],
267 vec![],
268 );
269
270 let schema_file = OutputFileInfo::from(&file);
271 let value = serde_json::to_value(&schema_file).expect("file info serializes");
272 let object = value.as_object().expect("file info object");
273
274 assert!(!object.contains_key("date"));
275 assert!(!object.contains_key("sha1"));
276 assert!(!object.contains_key("md5"));
277 assert!(!object.contains_key("sha256"));
278 assert!(!object.contains_key("sha1_git"));
279 assert!(!object.contains_key("mime_type"));
280 assert!(!object.contains_key("file_type"));
281 assert!(!object.contains_key("programming_language"));
282 assert!(!object.contains_key("is_binary"));
283 assert!(!object.contains_key("is_text"));
284 assert!(!object.contains_key("is_archive"));
285 assert!(!object.contains_key("is_media"));
286 assert!(!object.contains_key("is_source"));
287 assert!(!object.contains_key("is_script"));
288 assert!(!object.contains_key("files_count"));
289 assert!(!object.contains_key("dirs_count"));
290 assert!(!object.contains_key("size_count"));
291 assert!(!object.contains_key("license_policy"));
292 }
293
294 #[test]
295 fn test_file_info_serialization_keeps_license_policy_when_enabled() {
296 let mut file = FileInfo::new(
297 "main.rs".to_string(),
298 "main".to_string(),
299 "rs".to_string(),
300 "src/main.rs".to_string(),
301 FileType::File,
302 Some("text/plain".to_string()),
303 Some("text".to_string()),
304 42,
305 Some("2026-01-01T00:00:00Z".to_string()),
306 Some(Sha1Digest::from_hex("da39a3ee5e6b4b0d3255bfef95601890afd80709").unwrap()),
307 Some(Md5Digest::from_hex("d41d8cd98f00b204e9800998ecf8427e").unwrap()),
308 Some(
309 Sha256Digest::from_hex(
310 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
311 )
312 .unwrap(),
313 ),
314 Some("Rust".to_string()),
315 vec![],
316 None,
317 vec![],
318 vec![],
319 vec![],
320 vec![],
321 vec![],
322 vec![],
323 vec![],
324 vec![],
325 vec![],
326 );
327 file.license_policy = Some(vec![]);
328 file.sha1_git =
329 Some(GitSha1::from_hex("da39a3ee5e6b4b0d3255bfef95601890afd80709").unwrap());
330 file.is_binary = Some(false);
331 file.is_text = Some(true);
332 file.is_archive = Some(false);
333 file.is_media = Some(false);
334 file.is_source = Some(true);
335 file.is_script = Some(false);
336 file.files_count = Some(0);
337 file.dirs_count = Some(0);
338 file.size_count = Some(0);
339
340 let schema_file = OutputFileInfo::from(&file);
341 let value = serde_json::to_value(&schema_file).expect("file info serializes");
342 let object = value.as_object().expect("file info object");
343
344 assert_eq!(object.get("license_policy"), Some(&serde_json::json!([])));
345 assert_eq!(object.get("file_type"), Some(&serde_json::json!("text")));
346 assert_eq!(object.get("is_binary"), Some(&serde_json::json!(false)));
347 assert_eq!(object.get("is_text"), Some(&serde_json::json!(true)));
348 assert_eq!(object.get("files_count"), Some(&serde_json::json!(0)));
349 assert_eq!(object.get("dirs_count"), Some(&serde_json::json!(0)));
350 assert_eq!(object.get("size_count"), Some(&serde_json::json!(0)));
351 }
352
353 #[test]
354 fn test_json_lines_writer_sorts_files_by_path_for_reproducibility() {
355 let mut internal = sample_internal_output();
356 internal.files.reverse();
357 let output = Output::from(&internal);
358 let mut bytes = Vec::new();
359 writer_for_format(OutputFormat::JsonLines)
360 .write(&output, &mut bytes, &OutputWriteConfig::default())
361 .expect("json-lines write should succeed");
362
363 let rendered = String::from_utf8(bytes).expect("json-lines should be utf-8");
364 let file_lines = rendered
365 .lines()
366 .filter_map(|line| {
367 let value: Value = serde_json::from_str(line).ok()?;
368 let files = value.get("files")?.as_array()?;
369 files.first()?.get("path")?.as_str().map(str::to_string)
370 })
371 .collect::<Vec<_>>();
372
373 let mut sorted = file_lines.clone();
374 sorted.sort();
375 assert_eq!(file_lines, sorted);
376 }
377
378 #[test]
379 fn test_spdx_tag_value_writer_contains_required_fields() {
380 let output = Output::from(&sample_internal_output());
381 let mut bytes = Vec::new();
382 writer_for_format(OutputFormat::SpdxTv)
383 .write(
384 &output,
385 &mut bytes,
386 &OutputWriteConfig {
387 format: OutputFormat::SpdxTv,
388 custom_template: None,
389 scanned_path: Some("scan".to_string()),
390 },
391 )
392 .expect("spdx tv write should succeed");
393
394 let rendered = String::from_utf8(bytes).expect("spdx should be utf-8");
395 assert!(rendered.contains("SPDXVersion: SPDX-2.2"));
396 assert!(rendered.contains("FileName: ./src/main.rs"));
397 }
398
399 #[test]
400 fn test_spdx_rdf_writer_outputs_xml() {
401 let output = Output::from(&sample_internal_output());
402 let mut bytes = Vec::new();
403 writer_for_format(OutputFormat::SpdxRdf)
404 .write(
405 &output,
406 &mut bytes,
407 &OutputWriteConfig {
408 format: OutputFormat::SpdxRdf,
409 custom_template: None,
410 scanned_path: Some("scan".to_string()),
411 },
412 )
413 .expect("spdx rdf write should succeed");
414
415 let rendered = String::from_utf8(bytes).expect("rdf should be utf-8");
416 assert!(rendered.contains("<rdf:RDF"));
417 assert!(rendered.contains("<spdx:SpdxDocument"));
418 assert!(rendered.contains("<spdx:created>2026-01-01T00:00:00Z</spdx:created>"));
419 }
420
421 #[test]
422 fn test_cyclonedx_writers_keep_iso_timestamps_when_headers_use_scancode_format() {
423 let mut internal = sample_internal_output();
424 internal.packages.push(Package::from_package_data(
425 &PackageData {
426 name: Some("demo".to_string()),
427 version: Some("1.0.0".to_string()),
428 ..PackageData::default()
429 },
430 "scan/package.json".to_string(),
431 ));
432 let output = Output::from(&internal);
433
434 let mut json_bytes = Vec::new();
435 writer_for_format(OutputFormat::CycloneDxJson)
436 .write(
437 &output,
438 &mut json_bytes,
439 &OutputWriteConfig {
440 format: OutputFormat::CycloneDxJson,
441 custom_template: None,
442 scanned_path: Some("scan".to_string()),
443 },
444 )
445 .expect("cyclonedx json write should succeed");
446 let json_value: Value =
447 serde_json::from_slice(&json_bytes).expect("cyclonedx json should parse");
448 assert_eq!(
449 json_value["metadata"]["timestamp"].as_str(),
450 Some("2026-01-01T00:00:01Z")
451 );
452
453 let mut xml_bytes = Vec::new();
454 writer_for_format(OutputFormat::CycloneDxXml)
455 .write(
456 &output,
457 &mut xml_bytes,
458 &OutputWriteConfig {
459 format: OutputFormat::CycloneDxXml,
460 custom_template: None,
461 scanned_path: Some("scan".to_string()),
462 },
463 )
464 .expect("cyclonedx xml write should succeed");
465 let xml = String::from_utf8(xml_bytes).expect("cyclonedx xml should be utf-8");
466 assert!(xml.contains("<timestamp>2026-01-01T00:00:01Z</timestamp>"));
467 }
468
469 #[test]
470 fn test_spdx_writers_emit_real_file_and_package_license_info() {
471 let output = Output::from(&sample_internal_output());
472
473 let mut tv_bytes = Vec::new();
474 writer_for_format(OutputFormat::SpdxTv)
475 .write(
476 &output,
477 &mut tv_bytes,
478 &OutputWriteConfig {
479 format: OutputFormat::SpdxTv,
480 custom_template: None,
481 scanned_path: Some("scan".to_string()),
482 },
483 )
484 .expect("spdx tv write should succeed");
485 let tv_rendered = String::from_utf8(tv_bytes).expect("spdx tv should be utf-8");
486 assert!(tv_rendered.contains("PackageLicenseConcluded: NOASSERTION"));
487 assert!(tv_rendered.contains("PackageLicenseInfoFromFiles: MIT"));
488 assert!(tv_rendered.contains("LicenseConcluded: NOASSERTION"));
489 assert!(tv_rendered.contains("LicenseInfoInFile: MIT"));
490 assert!(tv_rendered.contains("PackageCopyrightText: Copyright (c) Example"));
491
492 let mut rdf_bytes = Vec::new();
493 writer_for_format(OutputFormat::SpdxRdf)
494 .write(
495 &output,
496 &mut rdf_bytes,
497 &OutputWriteConfig {
498 format: OutputFormat::SpdxRdf,
499 custom_template: None,
500 scanned_path: Some("scan".to_string()),
501 },
502 )
503 .expect("spdx rdf write should succeed");
504 let rdf_rendered = String::from_utf8(rdf_bytes).expect("spdx rdf should be utf-8");
505 assert!(rdf_rendered.contains(
506 "<spdx:licenseInfoFromFiles rdf:resource=\"http://spdx.org/licenses/MIT\"/>"
507 ));
508 assert!(
509 rdf_rendered.contains(
510 "<spdx:licenseInfoInFile rdf:resource=\"http://spdx.org/licenses/MIT\"/>"
511 )
512 );
513 assert!(rdf_rendered.contains(
514 "<spdx:licenseConcluded rdf:resource=\"http://spdx.org/rdf/terms#noassertion\"/>"
515 ));
516 }
517
518 #[test]
519 fn test_spdx_writers_emit_license_ref_metadata_and_matched_text() {
520 let mut internal = sample_internal_output();
521 internal.files[0].license_detections = vec![LicenseDetection {
522 license_expression: "unknown-license-reference".to_string(),
523 license_expression_spdx: "LicenseRef-scancode-unknown-license-reference".to_string(),
524 matches: vec![Match {
525 license_expression: "unknown-license-reference".to_string(),
526 license_expression_spdx: "LicenseRef-scancode-unknown-license-reference"
527 .to_string(),
528 from_file: Some("src/main.rs".to_string()),
529 start_line: LineNumber::ONE,
530 end_line: LineNumber::new(2).unwrap(),
531 matcher: Some("2-aho".to_string()),
532 score: MatchScore::MAX,
533 matched_length: Some(4),
534 match_coverage: Some(100.0),
535 rule_relevance: Some(100),
536 rule_identifier: Some("unknown-license-reference.RULE".to_string()),
537 rule_url: Some("https://example.com/unknown-license-reference.LICENSE".to_string()),
538 matched_text: Some("Custom license text".to_string()),
539 referenced_filenames: Some(vec!["LICENSE".to_string()]),
540 matched_text_diagnostics: None,
541 }],
542 detection_log: vec![],
543 identifier: Some("unknown-ref-id".to_string()),
544 }];
545 internal.license_references = vec![crate::models::LicenseReference {
546 key: Some("unknown-license-reference".to_string()),
547 language: Some("en".to_string()),
548 name: "Unknown License Reference".to_string(),
549 short_name: "Unknown License Reference".to_string(),
550 owner: None,
551 homepage_url: None,
552 spdx_license_key: "LicenseRef-scancode-unknown-license-reference".to_string(),
553 other_spdx_license_keys: vec![],
554 osi_license_key: None,
555 text_urls: vec![],
556 osi_url: None,
557 faq_url: None,
558 other_urls: vec![],
559 category: None,
560 is_exception: false,
561 is_unknown: true,
562 is_generic: false,
563 notes: None,
564 minimum_coverage: None,
565 standard_notice: None,
566 ignorable_copyrights: vec![],
567 ignorable_holders: vec![],
568 ignorable_authors: vec![],
569 ignorable_urls: vec![],
570 ignorable_emails: vec![],
571 scancode_url: None,
572 licensedb_url: None,
573 spdx_url: None,
574 text: "Unused fallback text".to_string(),
575 }];
576 let output = Output::from(&internal);
577
578 let mut tv_bytes = Vec::new();
579 writer_for_format(OutputFormat::SpdxTv)
580 .write(
581 &output,
582 &mut tv_bytes,
583 &OutputWriteConfig {
584 format: OutputFormat::SpdxTv,
585 custom_template: None,
586 scanned_path: Some("scan".to_string()),
587 },
588 )
589 .expect("spdx tv write should succeed");
590 let tv_rendered = String::from_utf8(tv_bytes).expect("spdx tv should be utf-8");
591 assert!(
592 tv_rendered
593 .contains("LicenseInfoInFile: LicenseRef-scancode-unknown-license-reference")
594 );
595 assert!(tv_rendered.contains(
596 "PackageLicenseInfoFromFiles: LicenseRef-scancode-unknown-license-reference"
597 ));
598 assert!(tv_rendered.contains("LicenseID: LicenseRef-scancode-unknown-license-reference"));
599 assert!(tv_rendered.contains("ExtractedText: <text>Custom license text"));
600 assert!(tv_rendered.contains("LicenseName: Unknown License Reference"));
601 assert!(tv_rendered.contains(
602 "LicenseComment: <text>See details at https://example.com/unknown-license-reference.LICENSE"
603 ));
604
605 let mut rdf_bytes = Vec::new();
606 writer_for_format(OutputFormat::SpdxRdf)
607 .write(
608 &output,
609 &mut rdf_bytes,
610 &OutputWriteConfig {
611 format: OutputFormat::SpdxRdf,
612 custom_template: None,
613 scanned_path: Some("scan".to_string()),
614 },
615 )
616 .expect("spdx rdf write should succeed");
617 let rdf_rendered = String::from_utf8(rdf_bytes).expect("spdx rdf should be utf-8");
618 assert!(rdf_rendered.contains(
619 "<spdx:licenseInfoInFile rdf:resource=\"http://spdx.org/licenses/LicenseRef-scancode-unknown-license-reference\"/>"
620 ));
621 assert!(rdf_rendered.contains(
622 "<spdx:hasExtractedLicensingInfo><spdx:ExtractedLicensingInfo rdf:about=\"#LicenseRef-scancode-unknown-license-reference\">"
623 ));
624 assert!(
625 rdf_rendered.contains("<spdx:extractedText>Custom license text</spdx:extractedText>")
626 );
627 }
628
629 #[test]
630 fn test_cyclonedx_json_writer_outputs_bom() {
631 let output = Output::from(&sample_internal_output());
632 let mut bytes = Vec::new();
633 writer_for_format(OutputFormat::CycloneDxJson)
634 .write(&output, &mut bytes, &OutputWriteConfig::default())
635 .expect("cyclonedx json write should succeed");
636
637 let rendered = String::from_utf8(bytes).expect("cyclonedx json should be utf-8");
638 let value: Value = serde_json::from_str(&rendered).expect("valid json");
639 assert_eq!(value["bomFormat"], "CycloneDX");
640 assert_eq!(value["specVersion"], "1.3");
641 }
642
643 #[test]
644 fn test_json_writer_includes_summary_and_key_file_flags() {
645 let mut internal = sample_internal_output();
646 internal.summary = Some(crate::models::Summary {
647 declared_license_expression: Some("apache-2.0".to_string()),
648 license_clarity_score: Some(crate::models::LicenseClarityScore {
649 score: 100,
650 declared_license: true,
651 identification_precision: true,
652 has_license_text: true,
653 declared_copyrights: true,
654 conflicting_license_categories: false,
655 ambiguous_compound_licensing: false,
656 }),
657 declared_holder: Some("Example Corp.".to_string()),
658 primary_language: Some("Ruby".to_string()),
659 other_license_expressions: vec![crate::models::TallyEntry {
660 value: Some("mit".to_string()),
661 count: 1,
662 }],
663 other_holders: vec![
664 crate::models::TallyEntry {
665 value: None,
666 count: 2,
667 },
668 crate::models::TallyEntry {
669 value: Some("Other Corp.".to_string()),
670 count: 1,
671 },
672 ],
673 other_languages: vec![crate::models::TallyEntry {
674 value: Some("Python".to_string()),
675 count: 2,
676 }],
677 });
678 internal.files[0].is_legal = true;
679 internal.files[0].is_top_level = true;
680 internal.files[0].is_key_file = true;
681 let output = Output::from(&internal);
682
683 let mut bytes = Vec::new();
684 writer_for_format(OutputFormat::Json)
685 .write(&output, &mut bytes, &OutputWriteConfig::default())
686 .expect("json write should succeed");
687
688 let rendered = String::from_utf8(bytes).expect("json should be utf-8");
689 let value: Value = serde_json::from_str(&rendered).expect("valid json");
690
691 assert_eq!(
692 value["summary"]["declared_license_expression"],
693 "apache-2.0"
694 );
695 assert_eq!(value["summary"]["license_clarity_score"]["score"], 100);
696 assert_eq!(value["summary"]["declared_holder"], "Example Corp.");
697 assert_eq!(value["summary"]["primary_language"], "Ruby");
698 assert_eq!(
699 value["summary"]["other_license_expressions"][0]["value"],
700 "mit"
701 );
702 assert!(value["summary"]["other_holders"][0]["value"].is_null());
703 assert_eq!(value["summary"]["other_holders"][1]["value"], "Other Corp.");
704 assert_eq!(value["summary"]["other_languages"][0]["value"], "Python");
705 assert_eq!(value["files"][0]["is_key_file"], true);
706 }
707
708 #[test]
709 fn test_json_and_json_lines_writers_include_top_level_tallies() {
710 let mut internal = sample_internal_output();
711 internal.tallies = Some(crate::models::Tallies {
712 detected_license_expression: vec![crate::models::TallyEntry {
713 value: Some("mit".to_string()),
714 count: 2,
715 }],
716 copyrights: vec![crate::models::TallyEntry {
717 value: Some("Copyright (c) Example Org".to_string()),
718 count: 1,
719 }],
720 holders: vec![crate::models::TallyEntry {
721 value: Some("Example Org".to_string()),
722 count: 1,
723 }],
724 authors: vec![crate::models::TallyEntry {
725 value: Some("Jane Doe".to_string()),
726 count: 1,
727 }],
728 programming_language: vec![crate::models::TallyEntry {
729 value: Some("Rust".to_string()),
730 count: 1,
731 }],
732 });
733 let output = Output::from(&internal);
734
735 let mut json_bytes = Vec::new();
736 writer_for_format(OutputFormat::Json)
737 .write(&output, &mut json_bytes, &OutputWriteConfig::default())
738 .expect("json write should succeed");
739 let json_value: Value =
740 serde_json::from_slice(&json_bytes).expect("json output should parse");
741 assert_eq!(
742 json_value["tallies"]["detected_license_expression"][0]["value"],
743 "mit"
744 );
745 assert_eq!(
746 json_value["tallies"]["programming_language"][0]["value"],
747 "Rust"
748 );
749
750 let mut jsonl_bytes = Vec::new();
751 writer_for_format(OutputFormat::JsonLines)
752 .write(&output, &mut jsonl_bytes, &OutputWriteConfig::default())
753 .expect("json-lines write should succeed");
754 let rendered = String::from_utf8(jsonl_bytes).expect("json-lines should be utf-8");
755 assert!(rendered.lines().any(|line| line.contains("\"tallies\"")));
756 }
757
758 #[test]
759 fn test_json_and_json_lines_writers_include_key_file_tallies() {
760 let mut internal = sample_internal_output();
761 internal.tallies_of_key_files = Some(crate::models::Tallies {
762 detected_license_expression: vec![crate::models::TallyEntry {
763 value: Some("apache-2.0".to_string()),
764 count: 1,
765 }],
766 copyrights: vec![],
767 holders: vec![],
768 authors: vec![],
769 programming_language: vec![crate::models::TallyEntry {
770 value: Some("Markdown".to_string()),
771 count: 1,
772 }],
773 });
774 let output = Output::from(&internal);
775
776 let mut json_bytes = Vec::new();
777 writer_for_format(OutputFormat::Json)
778 .write(&output, &mut json_bytes, &OutputWriteConfig::default())
779 .expect("json write should succeed");
780 let json_value: Value =
781 serde_json::from_slice(&json_bytes).expect("json output should parse");
782 assert_eq!(
783 json_value["tallies_of_key_files"]["detected_license_expression"][0]["value"],
784 "apache-2.0"
785 );
786
787 let mut jsonl_bytes = Vec::new();
788 writer_for_format(OutputFormat::JsonLines)
789 .write(&output, &mut jsonl_bytes, &OutputWriteConfig::default())
790 .expect("json-lines write should succeed");
791 let rendered = String::from_utf8(jsonl_bytes).expect("json-lines should be utf-8");
792 assert!(
793 rendered
794 .lines()
795 .any(|line| line.contains("\"tallies_of_key_files\""))
796 );
797 }
798
799 #[test]
800 fn test_json_and_json_lines_writers_include_file_tallies() {
801 let mut internal = sample_internal_output();
802 internal.files[0].tallies = Some(crate::models::Tallies {
803 detected_license_expression: vec![crate::models::TallyEntry {
804 value: Some("mit".to_string()),
805 count: 1,
806 }],
807 copyrights: vec![crate::models::TallyEntry {
808 value: None,
809 count: 1,
810 }],
811 holders: vec![],
812 authors: vec![],
813 programming_language: vec![crate::models::TallyEntry {
814 value: Some("Rust".to_string()),
815 count: 1,
816 }],
817 });
818 let output = Output::from(&internal);
819
820 let mut json_bytes = Vec::new();
821 writer_for_format(OutputFormat::Json)
822 .write(&output, &mut json_bytes, &OutputWriteConfig::default())
823 .expect("json write should succeed");
824 let json_value: Value =
825 serde_json::from_slice(&json_bytes).expect("json output should parse");
826 assert_eq!(
827 json_value["files"][0]["tallies"]["detected_license_expression"][0]["value"],
828 "mit"
829 );
830
831 let mut jsonl_bytes = Vec::new();
832 writer_for_format(OutputFormat::JsonLines)
833 .write(&output, &mut jsonl_bytes, &OutputWriteConfig::default())
834 .expect("json-lines write should succeed");
835 let rendered = String::from_utf8(jsonl_bytes).expect("json-lines should be utf-8");
836 assert!(rendered.lines().any(|line| line.contains("\"tallies\"")));
837 }
838
839 #[test]
840 fn test_json_and_json_lines_writers_include_facets_and_tallies_by_facet() {
841 let mut internal = sample_internal_output();
842 internal.files[0].facets = vec!["core".to_string(), "docs".to_string()];
843 internal.tallies_by_facet = Some(vec![crate::models::FacetTallies {
844 facet: "core".to_string(),
845 tallies: crate::models::Tallies {
846 detected_license_expression: vec![crate::models::TallyEntry {
847 value: Some("mit".to_string()),
848 count: 1,
849 }],
850 copyrights: vec![],
851 holders: vec![],
852 authors: vec![],
853 programming_language: vec![],
854 },
855 }]);
856 let output = Output::from(&internal);
857
858 let mut json_bytes = Vec::new();
859 writer_for_format(OutputFormat::Json)
860 .write(&output, &mut json_bytes, &OutputWriteConfig::default())
861 .expect("json write should succeed");
862 let json_value: Value =
863 serde_json::from_slice(&json_bytes).expect("json output should parse");
864 assert_eq!(json_value["files"][0]["facets"][0], "core");
865 assert_eq!(json_value["tallies_by_facet"][0]["facet"], "core");
866
867 let mut jsonl_bytes = Vec::new();
868 writer_for_format(OutputFormat::JsonLines)
869 .write(&output, &mut jsonl_bytes, &OutputWriteConfig::default())
870 .expect("json-lines write should succeed");
871 let rendered = String::from_utf8(jsonl_bytes).expect("json-lines should be utf-8");
872 assert!(
873 rendered
874 .lines()
875 .any(|line| line.contains("\"tallies_by_facet\""))
876 );
877 }
878
879 #[test]
880 fn test_json_and_json_lines_writers_include_top_level_license_references() {
881 let mut internal = sample_internal_output();
882 internal.license_references = vec![crate::models::LicenseReference {
883 key: Some("mit".to_string()),
884 language: Some("en".to_string()),
885 name: "MIT License".to_string(),
886 short_name: "MIT".to_string(),
887 owner: Some("Example Owner".to_string()),
888 homepage_url: Some("https://example.com/license".to_string()),
889 spdx_license_key: "MIT".to_string(),
890 other_spdx_license_keys: vec![],
891 osi_license_key: Some("MIT".to_string()),
892 text_urls: vec!["https://example.com/license.txt".to_string()],
893 osi_url: Some("https://opensource.org/licenses/MIT".to_string()),
894 faq_url: None,
895 other_urls: vec![],
896 category: None,
897 is_exception: false,
898 is_unknown: false,
899 is_generic: false,
900 notes: None,
901 minimum_coverage: None,
902 standard_notice: None,
903 ignorable_copyrights: vec![],
904 ignorable_holders: vec![],
905 ignorable_authors: vec![],
906 ignorable_urls: vec![],
907 ignorable_emails: vec![],
908 scancode_url: None,
909 licensedb_url: None,
910 spdx_url: None,
911 text: "MIT text".to_string(),
912 }];
913 internal.license_rule_references = vec![crate::models::LicenseRuleReference {
914 identifier: "license-clue_1.RULE".to_string(),
915 license_expression: "unknown-license-reference".to_string(),
916 is_license_text: false,
917 is_license_notice: false,
918 is_license_reference: false,
919 is_license_tag: false,
920 is_license_clue: true,
921 is_license_intro: false,
922 language: None,
923 rule_url: None,
924 is_required_phrase: false,
925 skip_for_required_phrase_generation: false,
926 replaced_by: vec![],
927 is_continuous: false,
928 is_synthetic: false,
929 is_from_license: false,
930 length: 0,
931 relevance: None,
932 minimum_coverage: None,
933 referenced_filenames: vec![],
934 notes: None,
935 ignorable_copyrights: vec![],
936 ignorable_holders: vec![],
937 ignorable_authors: vec![],
938 ignorable_urls: vec![],
939 ignorable_emails: vec![],
940 text: None,
941 }];
942 let output = Output::from(&internal);
943
944 let mut json_bytes = Vec::new();
945 writer_for_format(OutputFormat::Json)
946 .write(&output, &mut json_bytes, &OutputWriteConfig::default())
947 .expect("json write should succeed");
948 let json_value: Value =
949 serde_json::from_slice(&json_bytes).expect("json output should parse");
950 assert_eq!(
951 json_value["license_references"][0]["spdx_license_key"],
952 "MIT"
953 );
954 assert_eq!(json_value["license_references"][0]["key"], "mit");
955 assert_eq!(json_value["license_references"][0]["language"], "en");
956 assert_eq!(
957 json_value["license_references"][0]["owner"],
958 "Example Owner"
959 );
960 assert_eq!(
961 json_value["license_references"][0]["homepage_url"],
962 "https://example.com/license"
963 );
964 assert_eq!(
965 json_value["license_references"][0]["osi_license_key"],
966 "MIT"
967 );
968 assert_eq!(
969 json_value["license_references"][0]["text_urls"][0],
970 "https://example.com/license.txt"
971 );
972 assert_eq!(
973 json_value["license_rule_references"][0]["identifier"],
974 "license-clue_1.RULE"
975 );
976 assert_eq!(
977 json_value["license_rule_references"][0]["relevance"],
978 Value::Null
979 );
980 assert_eq!(
981 json_value["license_rule_references"][0]["length"],
982 Value::from(0)
983 );
984
985 let mut jsonl_bytes = Vec::new();
986 writer_for_format(OutputFormat::JsonLines)
987 .write(&output, &mut jsonl_bytes, &OutputWriteConfig::default())
988 .expect("json-lines write should succeed");
989 let rendered = String::from_utf8(jsonl_bytes).expect("json-lines should be utf-8");
990 assert!(
991 rendered
992 .lines()
993 .any(|line| line.contains("\"license_references\""))
994 );
995 assert!(
996 rendered
997 .lines()
998 .any(|line| line.contains("\"license_rule_references\""))
999 );
1000 }
1001
1002 #[test]
1003 fn test_json_and_json_lines_writers_include_top_level_license_detections() {
1004 let mut internal = sample_internal_output();
1005 internal.license_detections = vec![crate::models::TopLevelLicenseDetection {
1006 identifier: "mit-id".to_string(),
1007 license_expression: "mit".to_string(),
1008 license_expression_spdx: "MIT".to_string(),
1009 detection_count: 2,
1010 detection_log: vec![],
1011 reference_matches: vec![crate::models::Match {
1012 license_expression: "mit".to_string(),
1013 license_expression_spdx: "MIT".to_string(),
1014 from_file: Some("src/main.rs".to_string()),
1015 start_line: LineNumber::ONE,
1016 end_line: LineNumber::new(3).unwrap(),
1017 matcher: Some("1-hash".to_string()),
1018 score: MatchScore::MAX,
1019 matched_length: Some(10),
1020 match_coverage: Some(100.0),
1021 rule_relevance: Some(100),
1022 rule_identifier: Some("mit.LICENSE".to_string()),
1023 rule_url: None,
1024 matched_text: None,
1025 referenced_filenames: None,
1026 matched_text_diagnostics: None,
1027 }],
1028 }];
1029 let output = Output::from(&internal);
1030
1031 let mut json_bytes = Vec::new();
1032 writer_for_format(OutputFormat::Json)
1033 .write(&output, &mut json_bytes, &OutputWriteConfig::default())
1034 .expect("json write should succeed");
1035 let json_value: Value =
1036 serde_json::from_slice(&json_bytes).expect("json output should parse");
1037 assert_eq!(json_value["license_detections"][0]["identifier"], "mit-id");
1038 assert_eq!(json_value["license_detections"][0]["detection_count"], 2);
1039
1040 let mut jsonl_bytes = Vec::new();
1041 writer_for_format(OutputFormat::JsonLines)
1042 .write(&output, &mut jsonl_bytes, &OutputWriteConfig::default())
1043 .expect("json-lines write should succeed");
1044 let rendered = String::from_utf8(jsonl_bytes).expect("json-lines should be utf-8");
1045 assert!(
1046 rendered
1047 .lines()
1048 .any(|line| line.contains("\"license_detections\""))
1049 );
1050 }
1051
1052 #[test]
1053 fn test_cyclonedx_xml_writer_outputs_xml() {
1054 let output = Output::from(&sample_internal_output());
1055 let mut bytes = Vec::new();
1056 writer_for_format(OutputFormat::CycloneDxXml)
1057 .write(&output, &mut bytes, &OutputWriteConfig::default())
1058 .expect("cyclonedx xml write should succeed");
1059
1060 let rendered = String::from_utf8(bytes).expect("cyclonedx xml should be utf-8");
1061 assert!(rendered.contains("<bom xmlns=\"http://cyclonedx.org/schema/bom/1.3\""));
1062 assert!(rendered.contains("<components>"));
1063 }
1064
1065 #[test]
1066 fn test_cyclonedx_json_includes_component_license_expression() {
1067 let mut internal = sample_internal_output();
1068 internal.packages = vec![crate::models::Package {
1069 package_type: Some(crate::models::PackageType::Maven),
1070 namespace: Some("example".to_string()),
1071 name: Some("gradle-project".to_string()),
1072 version: Some("1.0.0".to_string()),
1073 qualifiers: None,
1074 subpath: None,
1075 primary_language: Some("Java".to_string()),
1076 description: None,
1077 release_date: None,
1078 parties: vec![],
1079 keywords: vec![],
1080 homepage_url: None,
1081 download_url: None,
1082 size: None,
1083 sha1: None,
1084 md5: None,
1085 sha256: None,
1086 sha512: None,
1087 bug_tracking_url: None,
1088 code_view_url: None,
1089 vcs_url: None,
1090 copyright: None,
1091 holder: None,
1092 declared_license_expression: Some("Apache-2.0".to_string()),
1093 declared_license_expression_spdx: Some("Apache-2.0".to_string()),
1094 license_detections: vec![],
1095 other_license_expression: None,
1096 other_license_expression_spdx: None,
1097 other_license_detections: vec![],
1098 extracted_license_statement: Some("Apache-2.0".to_string()),
1099 notice_text: None,
1100 source_packages: vec![],
1101 is_private: false,
1102 is_virtual: false,
1103 extra_data: None,
1104 repository_homepage_url: None,
1105 repository_download_url: None,
1106 api_data_url: None,
1107 datasource_ids: vec![],
1108 purl: Some("pkg:maven/example/gradle-project@1.0.0".to_string()),
1109 package_uid: PackageUid::from_raw(
1110 "pkg:maven/example/gradle-project@1.0.0?uuid=test".to_string(),
1111 ),
1112 datafile_paths: vec![],
1113 }];
1114 let output = Output::from(&internal);
1115
1116 let mut bytes = Vec::new();
1117 writer_for_format(OutputFormat::CycloneDxJson)
1118 .write(&output, &mut bytes, &OutputWriteConfig::default())
1119 .expect("cyclonedx json write should succeed");
1120
1121 let rendered = String::from_utf8(bytes).expect("cyclonedx json should be utf-8");
1122 let value: Value = serde_json::from_str(&rendered).expect("valid json");
1123
1124 assert_eq!(
1125 value["components"][0]["licenses"][0]["expression"],
1126 "Apache-2.0"
1127 );
1128 }
1129
1130 #[test]
1131 fn test_spdx_empty_scan_tag_value_matches_python_sentinel() {
1132 let output = Output {
1133 summary: None,
1134 tallies: None,
1135 tallies_of_key_files: None,
1136 tallies_by_facet: None,
1137 headers: vec![],
1138 packages: vec![],
1139 dependencies: vec![],
1140 license_detections: vec![],
1141 files: vec![],
1142 license_references: vec![],
1143 license_rule_references: vec![],
1144 };
1145 let mut bytes = Vec::new();
1146 writer_for_format(OutputFormat::SpdxTv)
1147 .write(
1148 &output,
1149 &mut bytes,
1150 &OutputWriteConfig {
1151 format: OutputFormat::SpdxTv,
1152 custom_template: None,
1153 scanned_path: Some("scan".to_string()),
1154 },
1155 )
1156 .expect("spdx tv write should succeed");
1157
1158 let rendered = String::from_utf8(bytes).expect("spdx should be utf-8");
1159 assert_eq!(rendered, "# No results for package 'scan'.\n");
1160 }
1161
1162 #[test]
1163 fn test_spdx_empty_scan_rdf_matches_python_sentinel() {
1164 let output = Output {
1165 summary: None,
1166 tallies: None,
1167 tallies_of_key_files: None,
1168 tallies_by_facet: None,
1169 headers: vec![],
1170 packages: vec![],
1171 dependencies: vec![],
1172 license_detections: vec![],
1173 files: vec![],
1174 license_references: vec![],
1175 license_rule_references: vec![],
1176 };
1177 let mut bytes = Vec::new();
1178 writer_for_format(OutputFormat::SpdxRdf)
1179 .write(
1180 &output,
1181 &mut bytes,
1182 &OutputWriteConfig {
1183 format: OutputFormat::SpdxRdf,
1184 custom_template: None,
1185 scanned_path: Some("scan".to_string()),
1186 },
1187 )
1188 .expect("spdx rdf write should succeed");
1189
1190 let rendered = String::from_utf8(bytes).expect("rdf should be utf-8");
1191 assert_eq!(rendered, "<!-- No results for package 'scan'. -->\n");
1192 }
1193
1194 #[test]
1195 fn test_html_writer_outputs_html_document() {
1196 let output = Output::from(&sample_internal_output());
1197 let mut bytes = Vec::new();
1198 writer_for_format(OutputFormat::Html)
1199 .write(&output, &mut bytes, &OutputWriteConfig::default())
1200 .expect("html write should succeed");
1201 let rendered = String::from_utf8(bytes).expect("html should be utf-8");
1202 assert!(rendered.contains("<!doctype html>"));
1203 assert!(rendered.contains("Custom Template"));
1204 }
1205
1206 #[test]
1207 fn test_custom_template_writer_renders_output_context() {
1208 let output = Output::from(&sample_internal_output());
1209 let temp_dir = tempfile::tempdir().expect("tempdir should be created");
1210 let template_path = temp_dir.path().join("template.tera");
1211 fs::write(
1212 &template_path,
1213 "version={{ output.headers[0].output_format_version }} files={{ files | length }}",
1214 )
1215 .expect("template should be written");
1216
1217 let mut bytes = Vec::new();
1218 writer_for_format(OutputFormat::CustomTemplate)
1219 .write(
1220 &output,
1221 &mut bytes,
1222 &OutputWriteConfig {
1223 format: OutputFormat::CustomTemplate,
1224 custom_template: Some(template_path.to_string_lossy().to_string()),
1225 scanned_path: None,
1226 },
1227 )
1228 .expect("custom template write should succeed");
1229
1230 let rendered = String::from_utf8(bytes).expect("template output should be utf-8");
1231 assert!(rendered.contains("version=4.1.0"));
1232 assert!(rendered.contains("files=1"));
1233 }
1234
1235 fn sample_internal_output() -> crate::models::Output {
1236 crate::models::Output {
1237 summary: None,
1238 tallies: None,
1239 tallies_of_key_files: None,
1240 tallies_by_facet: None,
1241 headers: vec![Header {
1242 tool_name: "provenant".to_string(),
1243 tool_version: env!("CARGO_PKG_VERSION").to_string(),
1244 options: serde_json::Map::new(),
1245 notice: crate::models::HEADER_NOTICE.to_string(),
1246 start_timestamp: "2026-01-01T000000.000000".to_string(),
1247 end_timestamp: "2026-01-01T000001.000000".to_string(),
1248 output_format_version: "4.1.0".to_string(),
1249 duration: 1.0,
1250 errors: vec![],
1251 warnings: vec![],
1252 extra_data: ExtraData {
1253 system_environment: SystemEnvironment {
1254 operating_system: "darwin".to_string(),
1255 cpu_architecture: "aarch64".to_string(),
1256 platform: "darwin".to_string(),
1257 platform_version: "26.3.1".to_string(),
1258 rust_version: "1.93.0".to_string(),
1259 },
1260 spdx_license_list_version: "3.27".to_string(),
1261 files_count: 1,
1262 directories_count: 1,
1263 excluded_count: 0,
1264 },
1265 }],
1266 packages: vec![],
1267 dependencies: vec![],
1268 license_detections: vec![],
1269 files: vec![FileInfo::new(
1270 "main.rs".to_string(),
1271 "main".to_string(),
1272 "rs".to_string(),
1273 "src/main.rs".to_string(),
1274 FileType::File,
1275 Some("text/plain".to_string()),
1276 None,
1277 42,
1278 None,
1279 Some(Sha1Digest::from_hex("da39a3ee5e6b4b0d3255bfef95601890afd80709").unwrap()),
1280 Some(Md5Digest::from_hex("d41d8cd98f00b204e9800998ecf8427e").unwrap()),
1281 Some(
1282 Sha256Digest::from_hex(
1283 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
1284 )
1285 .unwrap(),
1286 ),
1287 Some("Rust".to_string()),
1288 vec![PackageData::default()],
1289 None,
1290 vec![LicenseDetection {
1291 license_expression: "mit".to_string(),
1292 license_expression_spdx: "MIT".to_string(),
1293 matches: vec![Match {
1294 license_expression: "mit".to_string(),
1295 license_expression_spdx: "MIT".to_string(),
1296 from_file: None,
1297 start_line: LineNumber::ONE,
1298 end_line: LineNumber::ONE,
1299 matcher: None,
1300 score: MatchScore::MAX,
1301 matched_length: None,
1302 match_coverage: None,
1303 rule_relevance: None,
1304 rule_identifier: Some("mit_rule".to_string()),
1305 rule_url: None,
1306 matched_text: None,
1307 referenced_filenames: None,
1308 matched_text_diagnostics: None,
1309 }],
1310 detection_log: vec![],
1311 identifier: None,
1312 }],
1313 vec![],
1314 vec![Copyright {
1315 copyright: "Copyright (c) Example".to_string(),
1316 start_line: LineNumber::ONE,
1317 end_line: LineNumber::ONE,
1318 }],
1319 vec![Holder {
1320 holder: "Example Org".to_string(),
1321 start_line: LineNumber::ONE,
1322 end_line: LineNumber::ONE,
1323 }],
1324 vec![Author {
1325 author: "Jane Doe".to_string(),
1326 start_line: LineNumber::ONE,
1327 end_line: LineNumber::ONE,
1328 }],
1329 vec![OutputEmail {
1330 email: "jane@example.com".to_string(),
1331 start_line: LineNumber::ONE,
1332 end_line: LineNumber::ONE,
1333 }],
1334 vec![OutputURL {
1335 url: "https://example.com".to_string(),
1336 start_line: LineNumber::ONE,
1337 end_line: LineNumber::ONE,
1338 }],
1339 vec![],
1340 vec![],
1341 )],
1342 license_references: vec![],
1343 license_rule_references: vec![],
1344 }
1345 }
1346}