1mod run;
7
8pub use run::run;
9
10use clap::{ArgGroup, Args, Parser, Subcommand};
11use serde_json::{Map as JsonMap, Number as JsonNumber, Value as JsonValue};
12use std::ffi::OsString;
13use std::fs;
14#[cfg(test)]
15use std::ops::Deref;
16use std::path::{Path, PathBuf};
17use yaml_serde::Value as YamlValue;
18
19use crate::app::request::{InputMode, OutputTarget, ScanRequest};
20use crate::license_detection::DEFAULT_LICENSEDB_URL_TEMPLATE;
21use crate::output::OutputFormat;
22use crate::scanner::MemoryMode;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum ProcessMode {
26 Parallel(usize),
27 SequentialWithTimeouts,
28 SequentialWithoutTimeouts,
29}
30
31impl Default for ProcessMode {
32 fn default() -> Self {
33 let cpus = std::thread::available_parallelism().map_or(1, |n| n.get());
34 if cpus > 1 {
35 ProcessMode::Parallel(cpus - 1)
36 } else {
37 ProcessMode::Parallel(1)
38 }
39 }
40}
41
42impl ProcessMode {
43 fn default_value() -> Self {
44 let cpus = std::thread::available_parallelism().map_or(1, |n| n.get());
45 if cpus > 1 {
46 ProcessMode::Parallel(cpus - 1)
47 } else {
48 ProcessMode::Parallel(1)
49 }
50 }
51
52 pub fn to_i32(self) -> i32 {
53 match self {
54 ProcessMode::Parallel(n) => n as i32,
55 ProcessMode::SequentialWithTimeouts => 0,
56 ProcessMode::SequentialWithoutTimeouts => -1,
57 }
58 }
59}
60
61impl std::fmt::Display for ProcessMode {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 write!(f, "{}", self.to_i32())
64 }
65}
66
67fn parse_processes(value: &str) -> Result<ProcessMode, String> {
68 let parsed: i32 = value
69 .parse()
70 .map_err(|e| format!("invalid integer for --processes: {e}"))?;
71 if parsed > 0 {
72 Ok(ProcessMode::Parallel(
73 u32::try_from(parsed).unwrap() as usize
74 ))
75 } else if parsed == 0 {
76 Ok(ProcessMode::SequentialWithTimeouts)
77 } else {
78 Ok(ProcessMode::SequentialWithoutTimeouts)
79 }
80}
81
82const PDF_OXIDE_LOG_HELP: &str = "Troubleshooting PDF parser logs:\n Provenant suppresses noisy pdf_oxide logs by default.\n To inspect raw pdf_oxide logs for debugging, rerun with RUST_LOG=pdf_oxide=warn (or =error).";
83const CLI_ABOUT: &str = "Rust scanner for ScanCode-compatible workflows. Not affiliated with, endorsed by, or sponsored by ScanCode Toolkit, AboutCode, or nexB Inc.";
84const CLI_LONG_ABOUT: &str = "Rust scanner for ScanCode-compatible workflows.\n\nNot affiliated with, endorsed by, or sponsored by ScanCode Toolkit, AboutCode, or nexB Inc.";
85
86fn parse_license_policy_arg(value: &str) -> Result<String, String> {
87 let policy_path = Path::new(value);
88 let metadata = fs::metadata(policy_path).map_err(|err| {
89 format!(
90 "Failed to read license policy file {:?}: {err}",
91 policy_path
92 )
93 })?;
94 if !metadata.is_file() {
95 return Err(format!(
96 "License policy path {:?} is not a regular file",
97 policy_path
98 ));
99 }
100
101 let policy_text = fs::read_to_string(policy_path).map_err(|err| {
102 format!(
103 "Failed to read license policy file {:?}: {err}",
104 policy_path
105 )
106 })?;
107 if policy_text.trim().is_empty() {
108 return Err(format!("License policy file {:?} is empty", policy_path));
109 }
110
111 let policy_value: YamlValue = yaml_serde::from_str(&policy_text).map_err(|err| {
112 format!(
113 "Failed to parse license policy file {:?}: {err}",
114 policy_path
115 )
116 })?;
117 let has_license_policies = policy_value
118 .as_mapping()
119 .and_then(|mapping| mapping.get(YamlValue::String("license_policies".to_string())))
120 .is_some();
121 if !has_license_policies {
122 return Err(format!(
123 "License policy file {:?} is missing a 'license_policies' attribute",
124 policy_path
125 ));
126 }
127
128 Ok(value.to_string())
129}
130
131#[derive(Parser, Debug)]
132#[command(
133 author = "The Provenant contributors",
134 version = crate::version::BUILD_VERSION,
135 long_version = crate::version::build_long_version(),
136 after_help = PDF_OXIDE_LOG_HELP,
137 about = CLI_ABOUT,
138 long_about = CLI_LONG_ABOUT,
139 arg_required_else_help = true,
140 subcommand_required = true
141)]
142pub struct Cli {
143 #[command(subcommand)]
144 pub command: Command,
145}
146
147#[derive(Subcommand, Debug, Clone)]
148pub enum Command {
149 Scan(Box<ScanArgs>),
151 Serve(ServeArgs),
153 Compare(CompareArgs),
155 ShowAttribution,
157 ExportLicenseDataset(ExportLicenseDatasetArgs),
159}
160
161#[derive(Args, Debug, Clone)]
162pub struct CompareArgs {
163 #[arg(long = "scancode-json", value_name = "PATH")]
165 pub scancode_json: PathBuf,
166
167 #[arg(long = "provenant-json", value_name = "PATH")]
169 pub provenant_json: PathBuf,
170
171 #[arg(long = "artifact-dir", value_name = "DIR")]
173 pub artifact_dir: Option<PathBuf>,
174}
175
176#[derive(Args, Debug, Clone)]
177pub struct ExportLicenseDatasetArgs {
178 #[arg(value_name = "DIR")]
179 pub dir: String,
180}
181
182#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Default)]
183pub enum CompatibilityMode {
184 #[default]
185 Native,
186 Scancode,
187}
188
189impl CompatibilityMode {
190 fn as_str(self) -> &'static str {
191 match self {
192 Self::Native => "native",
193 Self::Scancode => "scancode",
194 }
195 }
196}
197
198#[derive(Args, Debug, Clone)]
199pub struct ServeArgs {
200 #[arg(long = "bind", value_name = "ADDR", default_value = "127.0.0.1:8080")]
202 pub bind: String,
203}
204
205#[derive(Args, Debug, Clone)]
206#[command(
207 group(
208 ArgGroup::new("output")
209 .required(true)
210 .multiple(true)
211 .args([
212 "output_json",
213 "output_json_pp",
214 "output_json_lines",
215 "output_yaml",
216 "output_debian",
217 "output_html",
218 "output_spdx_tv",
219 "output_spdx_rdf",
220 "output_cyclonedx",
221 "output_cyclonedx_xml",
222 "custom_output"
223 ])
224 ),
225 after_help = PDF_OXIDE_LOG_HELP
226)]
227pub struct ScanArgs {
228 #[arg(required = false)]
230 pub dir_path: Vec<String>,
231
232 #[arg(long = "json", value_name = "FILE", allow_hyphen_values = true)]
234 pub output_json: Option<String>,
235
236 #[arg(long = "json-pp", value_name = "FILE", allow_hyphen_values = true)]
238 pub output_json_pp: Option<String>,
239
240 #[arg(long = "json-lines", value_name = "FILE", allow_hyphen_values = true)]
242 pub output_json_lines: Option<String>,
243
244 #[arg(long = "yaml", value_name = "FILE", allow_hyphen_values = true)]
246 pub output_yaml: Option<String>,
247
248 #[arg(
250 long = "debian",
251 value_name = "FILE",
252 allow_hyphen_values = true,
253 requires_all = ["copyright", "license", "license_text"]
254 )]
255 pub output_debian: Option<String>,
256
257 #[arg(long = "html", value_name = "FILE", allow_hyphen_values = true)]
259 pub output_html: Option<String>,
260
261 #[arg(long = "spdx-tv", value_name = "FILE", allow_hyphen_values = true)]
263 pub output_spdx_tv: Option<String>,
264
265 #[arg(long = "spdx-rdf", value_name = "FILE", allow_hyphen_values = true)]
267 pub output_spdx_rdf: Option<String>,
268
269 #[arg(long = "cyclonedx", value_name = "FILE", allow_hyphen_values = true)]
271 pub output_cyclonedx: Option<String>,
272
273 #[arg(
275 long = "cyclonedx-xml",
276 value_name = "FILE",
277 allow_hyphen_values = true
278 )]
279 pub output_cyclonedx_xml: Option<String>,
280
281 #[arg(
283 long = "custom-output",
284 value_name = "FILE",
285 requires = "custom_template",
286 allow_hyphen_values = true
287 )]
288 pub custom_output: Option<String>,
289
290 #[arg(
292 long = "custom-template",
293 value_name = "FILE",
294 requires = "custom_output"
295 )]
296 pub custom_template: Option<String>,
297
298 #[arg(short, long, default_value = "0")]
300 pub max_depth: usize,
301
302 #[arg(short = 'n', long, default_value_t = ProcessMode::default_value(), value_parser = parse_processes, allow_hyphen_values = true)]
303 pub processes: ProcessMode,
304
305 #[arg(long, default_value_t = 120.0)]
306 pub timeout: f64,
307
308 #[arg(short, long, conflicts_with = "verbose")]
309 pub quiet: bool,
310
311 #[arg(short, long, conflicts_with = "quiet")]
312 pub verbose: bool,
313
314 #[arg(long, conflicts_with = "full_root")]
315 pub strip_root: bool,
316
317 #[arg(long, conflicts_with = "strip_root")]
318 pub full_root: bool,
319
320 #[arg(long = "exclude", visible_alias = "ignore", value_delimiter = ',')]
322 pub exclude: Vec<String>,
323
324 #[arg(long, value_delimiter = ',')]
326 pub include: Vec<String>,
327
328 #[arg(long = "paths-file", value_name = "FILE", allow_hyphen_values = true)]
330 pub paths_file: Vec<String>,
331
332 #[arg(long = "cache-dir", value_name = "PATH")]
333 pub cache_dir: Option<String>,
334
335 #[arg(long = "cache-clear")]
336 pub cache_clear: bool,
337
338 #[arg(long = "incremental")]
339 pub incremental: bool,
340
341 #[arg(
344 long = "max-in-memory",
345 value_name = "INT",
346 default_value_t = MemoryMode::Limit(10000),
347 value_parser = parse_max_in_memory,
348 allow_hyphen_values = true
349 )]
350 pub max_in_memory: MemoryMode,
351
352 #[arg(short = 'i', long)]
354 pub info: bool,
355
356 #[arg(long)]
358 pub from_json: bool,
359
360 #[arg(short = 'p', long)]
362 pub package: bool,
363
364 #[arg(
366 long = "compat-mode",
367 visible_alias = "compat",
368 value_enum,
369 default_value_t = CompatibilityMode::Native
370 )]
371 pub compat_mode: CompatibilityMode,
372
373 #[arg(long = "system-package")]
375 pub system_package: bool,
376
377 #[arg(long = "package-in-compiled")]
379 pub package_in_compiled: bool,
380
381 #[arg(
383 long = "package-only",
384 conflicts_with_all = ["license", "summary", "package", "system_package"]
385 )]
386 pub package_only: bool,
387
388 #[arg(long)]
390 pub no_assemble: bool,
391
392 #[arg(
395 long = "license-dataset-path",
396 value_name = "PATH",
397 requires = "license"
398 )]
399 pub license_dataset_path: Option<String>,
400
401 #[arg(long)]
403 pub reindex: bool,
404
405 #[arg(long = "no-license-index-cache")]
407 pub no_license_index_cache: bool,
408
409 #[arg(long = "license-text", requires = "license")]
411 pub license_text: bool,
412
413 #[arg(long = "license-text-diagnostics", requires = "license_text")]
414 pub license_text_diagnostics: bool,
415
416 #[arg(long = "license-diagnostics", requires = "license")]
417 pub license_diagnostics: bool,
418
419 #[arg(long = "unknown-licenses", requires = "license")]
420 pub unknown_licenses: bool,
421
422 #[arg(long = "no-sequence-matching", requires = "license")]
424 pub no_sequence_matching: bool,
425
426 #[arg(
427 long = "license-score",
428 default_value_t = 0,
429 requires = "license",
430 value_parser = clap::value_parser!(u8).range(0..=100)
431 )]
432 pub license_score: u8,
433
434 #[arg(
435 long = "license-url-template",
436 default_value = DEFAULT_LICENSEDB_URL_TEMPLATE,
437 requires = "license"
438 )]
439 pub license_url_template: String,
440
441 #[arg(long)]
442 pub filter_clues: bool,
443
444 #[arg(
445 long = "ignore-author",
446 value_name = "PATTERN",
447 help = "Ignore a file and all its findings if an author matches the regex PATTERN"
448 )]
449 pub ignore_author: Vec<String>,
450
451 #[arg(
452 long = "ignore-copyright-holder",
453 value_name = "PATTERN",
454 help = "Ignore a file and all its findings if a copyright holder matches the regex PATTERN"
455 )]
456 pub ignore_copyright_holder: Vec<String>,
457
458 #[arg(long)]
459 pub only_findings: bool,
460
461 #[arg(long, requires = "info")]
462 pub mark_source: bool,
463
464 #[arg(long)]
465 pub classify: bool,
466
467 #[arg(long, requires = "classify")]
468 pub summary: bool,
469
470 #[arg(long = "license-clarity-score", requires = "classify")]
471 pub license_clarity_score: bool,
472
473 #[arg(long = "license-references", requires = "license")]
474 pub license_references: bool,
475
476 #[arg(
478 long = "license-policy",
479 value_name = "FILE",
480 value_parser = parse_license_policy_arg
481 )]
482 pub license_policy: Option<String>,
483
484 #[arg(long)]
485 pub tallies: bool,
486
487 #[arg(long = "tallies-key-files", requires_all = ["tallies", "classify"])]
488 pub tallies_key_files: bool,
489
490 #[arg(long = "tallies-with-details")]
491 pub tallies_with_details: bool,
492
493 #[arg(long = "facet", value_name = "<facet>=<pattern>")]
494 pub facet: Vec<String>,
495
496 #[arg(long = "tallies-by-facet", requires_all = ["facet", "tallies"])]
497 pub tallies_by_facet: bool,
498
499 #[arg(long)]
500 pub generated: bool,
501
502 #[arg(short = 'l', long)]
504 pub license: bool,
505
506 #[arg(short = 'c', long)]
507 pub copyright: bool,
508
509 #[arg(short = 'e', long)]
511 pub email: bool,
512
513 #[arg(long, default_value_t = 50, requires = "email")]
515 pub max_email: usize,
516
517 #[arg(short = 'u', long)]
519 pub url: bool,
520
521 #[arg(long, default_value_t = 50, requires = "url")]
523 pub max_url: usize,
524}
525
526impl Cli {
527 pub fn parse() -> Self {
528 <Self as Parser>::parse_from(rewrite_args_for_default_scan(std::env::args_os()))
529 }
530
531 pub fn try_parse_from<I, T>(itr: I) -> Result<Self, clap::Error>
532 where
533 I: IntoIterator<Item = T>,
534 T: Into<OsString>,
535 {
536 <Self as Parser>::try_parse_from(rewrite_args_for_default_scan(itr))
537 }
538
539 pub(crate) fn scan_args(&self) -> Option<&ScanArgs> {
540 match &self.command {
541 Command::Scan(scan_args) => Some(scan_args.as_ref()),
542 Command::Serve(_)
543 | Command::Compare(_)
544 | Command::ShowAttribution
545 | Command::ExportLicenseDataset(_) => None,
546 }
547 }
548}
549
550#[cfg(test)]
551impl Deref for Cli {
552 type Target = ScanArgs;
553
554 fn deref(&self) -> &Self::Target {
555 self.scan_args()
556 .expect("scan arguments are only available for the scan command")
557 }
558}
559
560fn rewrite_args_for_default_scan<I, T>(itr: I) -> Vec<OsString>
561where
562 I: IntoIterator<Item = T>,
563 T: Into<OsString>,
564{
565 let mut args: Vec<OsString> = itr.into_iter().map(Into::into).collect();
566 if args.len() <= 1 {
567 return args;
568 }
569
570 let first = args[1].to_string_lossy();
571 if matches!(
572 first.as_ref(),
573 "scan"
574 | "serve"
575 | "compare"
576 | "show-attribution"
577 | "export-license-dataset"
578 | "help"
579 | "-h"
580 | "--help"
581 | "-V"
582 | "--version"
583 ) {
584 return args;
585 }
586
587 if first.starts_with('-') || Path::new(first.as_ref()).exists() {
588 args.insert(1, OsString::from("scan"));
589 }
590
591 args
592}
593
594fn parse_max_in_memory(value: &str) -> Result<MemoryMode, String> {
595 let parsed = value
596 .parse::<i64>()
597 .map_err(|_| format!("invalid integer value: {value}"))?;
598 if parsed < -1 {
599 return Err("--max-in-memory must be -1, 0, or a positive integer".to_string());
600 }
601 match parsed {
602 -1 => Ok(MemoryMode::StreamUnlimited),
603 0 => Ok(MemoryMode::CollectFirst),
604 n if n > 0 => Ok(MemoryMode::Limit(usize::try_from(n).unwrap_or(usize::MAX))),
605 _ => Ok(MemoryMode::CollectFirst),
606 }
607}
608
609impl ScanArgs {
610 pub(crate) fn output_targets(&self) -> Vec<OutputTarget> {
611 let mut targets = Vec::new();
612
613 if let Some(file) = &self.output_json {
614 targets.push(OutputTarget {
615 format: OutputFormat::Json,
616 file: file.clone(),
617 custom_template: None,
618 });
619 }
620
621 if let Some(file) = &self.output_json_pp {
622 targets.push(OutputTarget {
623 format: OutputFormat::JsonPretty,
624 file: file.clone(),
625 custom_template: None,
626 });
627 }
628
629 if let Some(file) = &self.output_json_lines {
630 targets.push(OutputTarget {
631 format: OutputFormat::JsonLines,
632 file: file.clone(),
633 custom_template: None,
634 });
635 }
636
637 if let Some(file) = &self.output_yaml {
638 targets.push(OutputTarget {
639 format: OutputFormat::Yaml,
640 file: file.clone(),
641 custom_template: None,
642 });
643 }
644
645 if let Some(file) = &self.output_debian {
646 targets.push(OutputTarget {
647 format: OutputFormat::Debian,
648 file: file.clone(),
649 custom_template: None,
650 });
651 }
652
653 if let Some(file) = &self.output_html {
654 targets.push(OutputTarget {
655 format: OutputFormat::Html,
656 file: file.clone(),
657 custom_template: None,
658 });
659 }
660
661 if let Some(file) = &self.output_spdx_tv {
662 targets.push(OutputTarget {
663 format: OutputFormat::SpdxTv,
664 file: file.clone(),
665 custom_template: None,
666 });
667 }
668
669 if let Some(file) = &self.output_spdx_rdf {
670 targets.push(OutputTarget {
671 format: OutputFormat::SpdxRdf,
672 file: file.clone(),
673 custom_template: None,
674 });
675 }
676
677 if let Some(file) = &self.output_cyclonedx {
678 targets.push(OutputTarget {
679 format: OutputFormat::CycloneDxJson,
680 file: file.clone(),
681 custom_template: None,
682 });
683 }
684
685 if let Some(file) = &self.output_cyclonedx_xml {
686 targets.push(OutputTarget {
687 format: OutputFormat::CycloneDxXml,
688 file: file.clone(),
689 custom_template: None,
690 });
691 }
692
693 if let Some(file) = &self.custom_output {
694 targets.push(OutputTarget {
695 format: OutputFormat::CustomTemplate,
696 file: file.clone(),
697 custom_template: self.custom_template.clone(),
698 });
699 }
700
701 targets
702 }
703
704 pub(crate) fn output_header_options(&self) -> JsonMap<String, JsonValue> {
705 let mut options = JsonMap::new();
706 if !self.dir_path.is_empty() {
707 options.insert(
708 "input".to_string(),
709 JsonValue::Array(
710 self.dir_path
711 .iter()
712 .cloned()
713 .map(JsonValue::String)
714 .collect(),
715 ),
716 );
717 }
718
719 let mut flags = Vec::new();
720
721 push_string_option(&mut flags, "--cache-dir", self.cache_dir.as_ref());
722 push_bool_option(&mut flags, "--cache-clear", self.cache_clear);
723 push_bool_option(&mut flags, "--classify", self.classify);
724 push_string_option(&mut flags, "--custom-output", self.custom_output.as_ref());
725 push_string_option(
726 &mut flags,
727 "--custom-template",
728 self.custom_template.as_ref(),
729 );
730 push_bool_option(&mut flags, "--copyright", self.copyright);
731 if self.compat_mode != CompatibilityMode::Native {
732 flags.push((
733 "--compat-mode".to_string(),
734 JsonValue::String(self.compat_mode.as_str().to_string()),
735 ));
736 }
737 push_string_option(&mut flags, "--cyclonedx", self.output_cyclonedx.as_ref());
738 push_string_option(
739 &mut flags,
740 "--cyclonedx-xml",
741 self.output_cyclonedx_xml.as_ref(),
742 );
743 push_string_option(&mut flags, "--debian", self.output_debian.as_ref());
744 push_bool_option(&mut flags, "--email", self.email);
745 push_array_option(&mut flags, "--facet", &self.facet);
746 push_bool_option(&mut flags, "--filter-clues", self.filter_clues);
747 push_bool_option(&mut flags, "--from-json", self.from_json);
748 push_bool_option(&mut flags, "--full-root", self.full_root);
749 push_bool_option(&mut flags, "--generated", self.generated);
750 push_string_option(&mut flags, "--html", self.output_html.as_ref());
751 push_array_option(&mut flags, "--ignore", &self.exclude);
752 push_array_option(&mut flags, "--ignore-author", &self.ignore_author);
753 push_array_option(
754 &mut flags,
755 "--ignore-copyright-holder",
756 &self.ignore_copyright_holder,
757 );
758 push_bool_option(&mut flags, "--incremental", self.incremental);
759 push_array_option(&mut flags, "--include", &self.include);
760 push_bool_option(&mut flags, "--info", self.info);
761 push_string_option(&mut flags, "--json", self.output_json.as_ref());
762 push_string_option(&mut flags, "--json-lines", self.output_json_lines.as_ref());
763 push_string_option(&mut flags, "--json-pp", self.output_json_pp.as_ref());
764 push_bool_option(&mut flags, "--license", self.license);
765 push_bool_option(
766 &mut flags,
767 "--license-clarity-score",
768 self.license_clarity_score,
769 );
770 push_bool_option(
771 &mut flags,
772 "--license-diagnostics",
773 self.license_diagnostics,
774 );
775 push_string_option(
776 &mut flags,
777 "--license-dataset-path",
778 self.license_dataset_path.as_ref(),
779 );
780 push_string_option(&mut flags, "--license-policy", self.license_policy.as_ref());
781 push_bool_option(
782 &mut flags,
783 "--no-license-index-cache",
784 self.no_license_index_cache,
785 );
786 push_bool_option(&mut flags, "--license-references", self.license_references);
787 push_bool_option(&mut flags, "--reindex", self.reindex);
788 push_non_default_u8_option(&mut flags, "--license-score", self.license_score, 0);
789 push_bool_option(&mut flags, "--license-text", self.license_text);
790 push_bool_option(
791 &mut flags,
792 "--license-text-diagnostics",
793 self.license_text_diagnostics,
794 );
795 push_non_default_string_option(
796 &mut flags,
797 "--license-url-template",
798 &self.license_url_template,
799 DEFAULT_LICENSEDB_URL_TEMPLATE,
800 );
801 push_non_default_usize_option(&mut flags, "--max-depth", self.max_depth, 0);
802 match self.max_in_memory {
803 MemoryMode::Limit(10000) => {}
804 MemoryMode::CollectFirst => {
805 flags.push(("--max-in-memory".to_string(), JsonValue::Number(0.into())));
806 }
807 MemoryMode::StreamUnlimited => {
808 flags.push((
809 "--max-in-memory".to_string(),
810 JsonValue::Number((-1i64).into()),
811 ));
812 }
813 MemoryMode::Limit(n) => {
814 flags.push(("--max-in-memory".to_string(), JsonValue::Number(n.into())));
815 }
816 }
817 if self.email {
818 push_non_default_usize_option(&mut flags, "--max-email", self.max_email, 50);
819 }
820 if self.url {
821 push_non_default_usize_option(&mut flags, "--max-url", self.max_url, 50);
822 }
823 push_bool_option(&mut flags, "--mark-source", self.mark_source);
824 push_bool_option(&mut flags, "--no-assemble", self.no_assemble);
825 push_bool_option(&mut flags, "--only-findings", self.only_findings);
826 push_bool_option(&mut flags, "--package", self.package);
827 push_bool_option(
828 &mut flags,
829 "--package-in-compiled",
830 self.package_in_compiled,
831 );
832 push_bool_option(&mut flags, "--package-only", self.package_only);
833 push_array_option(&mut flags, "--paths-file", &self.paths_file);
834 push_non_default_process_mode_option(
835 &mut flags,
836 "--processes",
837 self.processes,
838 ProcessMode::default_value(),
839 );
840 push_bool_option(&mut flags, "--quiet", self.quiet);
841 push_string_option(&mut flags, "--spdx-rdf", self.output_spdx_rdf.as_ref());
842 push_string_option(&mut flags, "--spdx-tv", self.output_spdx_tv.as_ref());
843 push_bool_option(&mut flags, "--strip-root", self.strip_root);
844 push_bool_option(&mut flags, "--summary", self.summary);
845 push_bool_option(&mut flags, "--system-package", self.system_package);
846 push_bool_option(&mut flags, "--tallies", self.tallies);
847 push_bool_option(&mut flags, "--tallies-by-facet", self.tallies_by_facet);
848 push_bool_option(&mut flags, "--tallies-key-files", self.tallies_key_files);
849 push_bool_option(
850 &mut flags,
851 "--tallies-with-details",
852 self.tallies_with_details,
853 );
854 push_non_default_f64_option(&mut flags, "--timeout", self.timeout, 120.0);
855 push_bool_option(&mut flags, "--unknown-licenses", self.unknown_licenses);
856 push_bool_option(
857 &mut flags,
858 "--no-sequence-matching",
859 self.no_sequence_matching,
860 );
861 push_bool_option(&mut flags, "--url", self.url);
862 push_bool_option(&mut flags, "--verbose", self.verbose);
863 push_string_option(&mut flags, "--yaml", self.output_yaml.as_ref());
864
865 flags.sort_by(|left, right| left.0.cmp(&right.0));
866 for (key, value) in flags {
867 options.insert(key, value);
868 }
869
870 options
871 }
872}
873
874impl From<&ScanArgs> for ScanRequest {
875 fn from(cli: &ScanArgs) -> Self {
876 Self {
877 input_paths: cli.dir_path.clone(),
878 input_mode: if cli.from_json {
879 InputMode::FromJson
880 } else {
881 InputMode::Native
882 },
883 output_targets: cli.output_targets(),
884 output_header_options: cli.output_header_options(),
885 progress_mode: if cli.quiet {
886 crate::progress::ProgressMode::Quiet
887 } else if cli.verbose {
888 crate::progress::ProgressMode::Verbose
889 } else {
890 crate::progress::ProgressMode::Default
891 },
892 process_mode: cli.processes,
893 timeout_seconds: cli.timeout,
894 quiet: cli.quiet,
895 verbose: cli.verbose,
896 strip_root: cli.strip_root,
897 full_root: cli.full_root,
898 exclude: cli.exclude.clone(),
899 include: cli.include.clone(),
900 paths_files: cli.paths_file.clone(),
901 respect_process_cache_env: true,
902 cache_dir: cli.cache_dir.clone(),
903 cache_clear: cli.cache_clear,
904 incremental: cli.incremental,
905 max_depth: cli.max_depth,
906 max_in_memory: cli.max_in_memory,
907 info: cli.info,
908 package: cli.package,
909 system_package: cli.system_package,
910 package_in_compiled: cli.package_in_compiled,
911 package_only: cli.package_only,
912 no_assemble: cli.no_assemble,
913 license_dataset_path: cli.license_dataset_path.clone(),
914 reindex: cli.reindex,
915 no_license_index_cache: cli.no_license_index_cache,
916 license_text: cli.license_text,
917 license_text_diagnostics: cli.license_text_diagnostics,
918 license_diagnostics: cli.license_diagnostics,
919 unknown_licenses: cli.unknown_licenses,
920 no_sequence_matching: cli.no_sequence_matching,
921 license_score: cli.license_score,
922 license_url_template: cli.license_url_template.clone(),
923 filter_clues: cli.filter_clues,
924 ignore_author: cli.ignore_author.clone(),
925 ignore_copyright_holder: cli.ignore_copyright_holder.clone(),
926 only_findings: cli.only_findings,
927 mark_source: cli.mark_source,
928 classify: cli.classify,
929 summary: cli.summary,
930 license_clarity_score: cli.license_clarity_score,
931 license_references: cli.license_references,
932 license_policy: cli.license_policy.clone(),
933 tallies: cli.tallies,
934 tallies_key_files: cli.tallies_key_files,
935 tallies_with_details: cli.tallies_with_details,
936 facet: cli.facet.clone(),
937 tallies_by_facet: cli.tallies_by_facet,
938 generated: cli.generated,
939 license: cli.license,
940 copyright: cli.copyright,
941 email: cli.email,
942 max_email: cli.max_email,
943 url: cli.url,
944 max_url: cli.max_url,
945 }
946 }
947}
948
949fn push_bool_option(options: &mut Vec<(String, JsonValue)>, key: &str, enabled: bool) {
950 if enabled {
951 options.push((key.to_string(), JsonValue::Bool(true)));
952 }
953}
954
955fn push_string_option(options: &mut Vec<(String, JsonValue)>, key: &str, value: Option<&String>) {
956 if let Some(value) = value {
957 options.push((key.to_string(), JsonValue::String(value.clone())));
958 }
959}
960
961fn push_non_default_string_option(
962 options: &mut Vec<(String, JsonValue)>,
963 key: &str,
964 value: &str,
965 default: &str,
966) {
967 if value != default {
968 options.push((key.to_string(), JsonValue::String(value.to_string())));
969 }
970}
971
972fn push_array_option(options: &mut Vec<(String, JsonValue)>, key: &str, values: &[String]) {
973 if !values.is_empty() {
974 options.push((
975 key.to_string(),
976 JsonValue::Array(values.iter().cloned().map(JsonValue::String).collect()),
977 ));
978 }
979}
980
981fn push_non_default_usize_option(
982 options: &mut Vec<(String, JsonValue)>,
983 key: &str,
984 value: usize,
985 default: usize,
986) {
987 if value != default {
988 options.push((key.to_string(), JsonValue::Number(value.into())));
989 }
990}
991
992fn push_non_default_u8_option(
993 options: &mut Vec<(String, JsonValue)>,
994 key: &str,
995 value: u8,
996 default: u8,
997) {
998 if value != default {
999 options.push((key.to_string(), JsonValue::Number(value.into())));
1000 }
1001}
1002
1003fn push_non_default_process_mode_option(
1004 options: &mut Vec<(String, JsonValue)>,
1005 key: &str,
1006 value: ProcessMode,
1007 default: ProcessMode,
1008) {
1009 if value != default {
1010 options.push((key.to_string(), JsonValue::Number(value.to_i32().into())));
1011 }
1012}
1013
1014fn push_non_default_f64_option(
1015 options: &mut Vec<(String, JsonValue)>,
1016 key: &str,
1017 value: f64,
1018 default: f64,
1019) {
1020 if (value - default).abs() > f64::EPSILON
1021 && let Some(number) = JsonNumber::from_f64(value)
1022 {
1023 options.push((key.to_string(), JsonValue::Number(number)));
1024 }
1025}
1026
1027#[cfg(test)]
1028mod tests;