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 #[arg(long = "allow-privileged-inputs")]
206 pub allow_privileged_inputs: bool,
207}
208
209#[derive(Args, Debug, Clone)]
210#[command(
211 group(
212 ArgGroup::new("output")
213 .required(true)
214 .multiple(true)
215 .args([
216 "output_json",
217 "output_json_pp",
218 "output_json_lines",
219 "output_yaml",
220 "output_debian",
221 "output_html",
222 "output_spdx_tv",
223 "output_spdx_rdf",
224 "output_cyclonedx",
225 "output_cyclonedx_xml",
226 "custom_output"
227 ])
228 ),
229 after_help = PDF_OXIDE_LOG_HELP
230)]
231pub struct ScanArgs {
232 #[arg(required = false)]
234 pub dir_path: Vec<String>,
235
236 #[arg(long = "json", value_name = "FILE", allow_hyphen_values = true)]
238 pub output_json: Option<String>,
239
240 #[arg(long = "json-pp", value_name = "FILE", allow_hyphen_values = true)]
242 pub output_json_pp: Option<String>,
243
244 #[arg(long = "json-lines", value_name = "FILE", allow_hyphen_values = true)]
246 pub output_json_lines: Option<String>,
247
248 #[arg(long = "yaml", value_name = "FILE", allow_hyphen_values = true)]
250 pub output_yaml: Option<String>,
251
252 #[arg(
254 long = "debian",
255 value_name = "FILE",
256 allow_hyphen_values = true,
257 requires_all = ["copyright", "license", "license_text"]
258 )]
259 pub output_debian: Option<String>,
260
261 #[arg(long = "html", value_name = "FILE", allow_hyphen_values = true)]
263 pub output_html: Option<String>,
264
265 #[arg(long = "spdx-tv", value_name = "FILE", allow_hyphen_values = true)]
267 pub output_spdx_tv: Option<String>,
268
269 #[arg(long = "spdx-rdf", value_name = "FILE", allow_hyphen_values = true)]
271 pub output_spdx_rdf: Option<String>,
272
273 #[arg(long = "cyclonedx", value_name = "FILE", allow_hyphen_values = true)]
275 pub output_cyclonedx: Option<String>,
276
277 #[arg(
279 long = "cyclonedx-xml",
280 value_name = "FILE",
281 allow_hyphen_values = true
282 )]
283 pub output_cyclonedx_xml: Option<String>,
284
285 #[arg(
287 long = "custom-output",
288 value_name = "FILE",
289 requires = "custom_template",
290 allow_hyphen_values = true
291 )]
292 pub custom_output: Option<String>,
293
294 #[arg(
296 long = "custom-template",
297 value_name = "FILE",
298 requires = "custom_output"
299 )]
300 pub custom_template: Option<String>,
301
302 #[arg(short, long, default_value = "0")]
304 pub max_depth: usize,
305
306 #[arg(short = 'n', long, default_value_t = ProcessMode::default_value(), value_parser = parse_processes, allow_hyphen_values = true)]
307 pub processes: ProcessMode,
308
309 #[arg(long, default_value_t = 120.0)]
310 pub timeout: f64,
311
312 #[arg(short, long, conflicts_with = "verbose")]
313 pub quiet: bool,
314
315 #[arg(short, long, conflicts_with = "quiet")]
316 pub verbose: bool,
317
318 #[arg(long, conflicts_with = "full_root")]
319 pub strip_root: bool,
320
321 #[arg(long, conflicts_with = "strip_root")]
322 pub full_root: bool,
323
324 #[arg(long = "exclude", visible_alias = "ignore", value_delimiter = ',')]
326 pub exclude: Vec<String>,
327
328 #[arg(long, value_delimiter = ',')]
330 pub include: Vec<String>,
331
332 #[arg(long = "paths-file", value_name = "FILE", allow_hyphen_values = true)]
334 pub paths_file: Vec<String>,
335
336 #[arg(long = "cache-dir", value_name = "PATH")]
337 pub cache_dir: Option<String>,
338
339 #[arg(long = "cache-clear")]
340 pub cache_clear: bool,
341
342 #[arg(long = "incremental")]
343 pub incremental: bool,
344
345 #[arg(long = "cache-trust-mtime", requires = "incremental")]
351 pub cache_trust_mtime: bool,
352
353 #[arg(
360 long = "max-in-memory",
361 value_name = "INT",
362 default_value_t = MemoryMode::Limit(10000),
363 value_parser = parse_max_in_memory,
364 allow_hyphen_values = true
365 )]
366 pub max_in_memory: MemoryMode,
367
368 #[arg(short = 'i', long)]
370 pub info: bool,
371
372 #[arg(long)]
374 pub from_json: bool,
375
376 #[arg(short = 'p', long)]
378 pub package: bool,
379
380 #[arg(
382 long = "compat-mode",
383 visible_alias = "compat",
384 value_enum,
385 default_value_t = CompatibilityMode::Native
386 )]
387 pub compat_mode: CompatibilityMode,
388
389 #[arg(long = "system-package")]
391 pub system_package: bool,
392
393 #[arg(long = "package-in-compiled")]
395 pub package_in_compiled: bool,
396
397 #[arg(
399 long = "package-only",
400 conflicts_with_all = ["license", "summary", "package", "system_package"]
401 )]
402 pub package_only: bool,
403
404 #[arg(long)]
406 pub no_assemble: bool,
407
408 #[arg(
411 long = "license-dataset-path",
412 value_name = "PATH",
413 requires = "license"
414 )]
415 pub license_dataset_path: Option<String>,
416
417 #[arg(long)]
419 pub reindex: bool,
420
421 #[arg(long = "no-license-index-cache")]
423 pub no_license_index_cache: bool,
424
425 #[arg(long = "license-text", requires = "license")]
427 pub license_text: bool,
428
429 #[arg(long = "license-text-diagnostics", requires = "license_text")]
430 pub license_text_diagnostics: bool,
431
432 #[arg(long = "license-diagnostics", requires = "license")]
433 pub license_diagnostics: bool,
434
435 #[arg(long = "unknown-licenses", requires = "license")]
436 pub unknown_licenses: bool,
437
438 #[arg(long = "no-sequence-matching", requires = "license")]
440 pub no_sequence_matching: bool,
441
442 #[arg(
443 long = "license-score",
444 default_value_t = 0,
445 requires = "license",
446 value_parser = clap::value_parser!(u8).range(0..=100)
447 )]
448 pub license_score: u8,
449
450 #[arg(
451 long = "license-url-template",
452 default_value = DEFAULT_LICENSEDB_URL_TEMPLATE,
453 requires = "license"
454 )]
455 pub license_url_template: String,
456
457 #[arg(long)]
458 pub filter_clues: bool,
459
460 #[arg(
461 long = "ignore-author",
462 value_name = "PATTERN",
463 help = "Ignore a file and all its findings if an author matches the regex PATTERN"
464 )]
465 pub ignore_author: Vec<String>,
466
467 #[arg(
468 long = "ignore-copyright-holder",
469 value_name = "PATTERN",
470 help = "Ignore a file and all its findings if a copyright holder matches the regex PATTERN"
471 )]
472 pub ignore_copyright_holder: Vec<String>,
473
474 #[arg(long)]
475 pub only_findings: bool,
476
477 #[arg(long, requires = "info")]
478 pub mark_source: bool,
479
480 #[arg(long)]
481 pub classify: bool,
482
483 #[arg(long, requires = "classify")]
484 pub summary: bool,
485
486 #[arg(long = "license-clarity-score", requires = "classify")]
487 pub license_clarity_score: bool,
488
489 #[arg(long = "license-references", requires = "license")]
490 pub license_references: bool,
491
492 #[arg(
494 long = "license-policy",
495 value_name = "FILE",
496 value_parser = parse_license_policy_arg
497 )]
498 pub license_policy: Option<String>,
499
500 #[arg(long)]
501 pub tallies: bool,
502
503 #[arg(long = "tallies-key-files", requires_all = ["tallies", "classify"])]
504 pub tallies_key_files: bool,
505
506 #[arg(long = "tallies-with-details")]
507 pub tallies_with_details: bool,
508
509 #[arg(long = "facet", value_name = "<facet>=<pattern>")]
510 pub facet: Vec<String>,
511
512 #[arg(long = "tallies-by-facet", requires_all = ["facet", "tallies"])]
513 pub tallies_by_facet: bool,
514
515 #[arg(long)]
516 pub generated: bool,
517
518 #[arg(short = 'l', long)]
520 pub license: bool,
521
522 #[arg(short = 'c', long)]
523 pub copyright: bool,
524
525 #[arg(short = 'e', long)]
527 pub email: bool,
528
529 #[arg(long, default_value_t = 50, requires = "email")]
531 pub max_email: usize,
532
533 #[arg(short = 'u', long)]
535 pub url: bool,
536
537 #[arg(long, default_value_t = 50, requires = "url")]
539 pub max_url: usize,
540}
541
542impl Cli {
543 pub fn parse() -> Self {
544 <Self as Parser>::parse_from(rewrite_args_for_default_scan(std::env::args_os()))
545 }
546
547 pub fn try_parse_from<I, T>(itr: I) -> Result<Self, clap::Error>
548 where
549 I: IntoIterator<Item = T>,
550 T: Into<OsString>,
551 {
552 <Self as Parser>::try_parse_from(rewrite_args_for_default_scan(itr))
553 }
554
555 pub(crate) fn scan_args(&self) -> Option<&ScanArgs> {
556 match &self.command {
557 Command::Scan(scan_args) => Some(scan_args.as_ref()),
558 Command::Serve(_)
559 | Command::Compare(_)
560 | Command::ShowAttribution
561 | Command::ExportLicenseDataset(_) => None,
562 }
563 }
564}
565
566#[cfg(test)]
567impl Deref for Cli {
568 type Target = ScanArgs;
569
570 fn deref(&self) -> &Self::Target {
571 self.scan_args()
572 .expect("scan arguments are only available for the scan command")
573 }
574}
575
576fn rewrite_args_for_default_scan<I, T>(itr: I) -> Vec<OsString>
577where
578 I: IntoIterator<Item = T>,
579 T: Into<OsString>,
580{
581 let mut args: Vec<OsString> = itr.into_iter().map(Into::into).collect();
582 if args.len() <= 1 {
583 return args;
584 }
585
586 let first = args[1].to_string_lossy();
587 if matches!(
588 first.as_ref(),
589 "scan"
590 | "serve"
591 | "compare"
592 | "show-attribution"
593 | "export-license-dataset"
594 | "help"
595 | "-h"
596 | "--help"
597 | "-V"
598 | "--version"
599 ) {
600 return args;
601 }
602
603 if first.starts_with('-') || Path::new(first.as_ref()).exists() {
604 args.insert(1, OsString::from("scan"));
605 }
606
607 args
608}
609
610fn parse_max_in_memory(value: &str) -> Result<MemoryMode, String> {
611 let parsed = value
612 .parse::<i64>()
613 .map_err(|_| format!("invalid integer value: {value}"))?;
614 if parsed < -1 {
615 return Err("--max-in-memory must be -1, 0, or a positive integer".to_string());
616 }
617 match parsed {
618 -1 => Ok(MemoryMode::StreamUnlimited),
619 0 => Ok(MemoryMode::CollectFirst),
620 n if n > 0 => Ok(MemoryMode::Limit(usize::try_from(n).unwrap_or(usize::MAX))),
621 _ => Ok(MemoryMode::CollectFirst),
622 }
623}
624
625impl ScanArgs {
626 pub(crate) fn output_targets(&self) -> Vec<OutputTarget> {
627 let mut targets = Vec::new();
628
629 if let Some(file) = &self.output_json {
630 targets.push(OutputTarget {
631 format: OutputFormat::Json,
632 file: file.clone(),
633 custom_template: None,
634 });
635 }
636
637 if let Some(file) = &self.output_json_pp {
638 targets.push(OutputTarget {
639 format: OutputFormat::JsonPretty,
640 file: file.clone(),
641 custom_template: None,
642 });
643 }
644
645 if let Some(file) = &self.output_json_lines {
646 targets.push(OutputTarget {
647 format: OutputFormat::JsonLines,
648 file: file.clone(),
649 custom_template: None,
650 });
651 }
652
653 if let Some(file) = &self.output_yaml {
654 targets.push(OutputTarget {
655 format: OutputFormat::Yaml,
656 file: file.clone(),
657 custom_template: None,
658 });
659 }
660
661 if let Some(file) = &self.output_debian {
662 targets.push(OutputTarget {
663 format: OutputFormat::Debian,
664 file: file.clone(),
665 custom_template: None,
666 });
667 }
668
669 if let Some(file) = &self.output_html {
670 targets.push(OutputTarget {
671 format: OutputFormat::Html,
672 file: file.clone(),
673 custom_template: None,
674 });
675 }
676
677 if let Some(file) = &self.output_spdx_tv {
678 targets.push(OutputTarget {
679 format: OutputFormat::SpdxTv,
680 file: file.clone(),
681 custom_template: None,
682 });
683 }
684
685 if let Some(file) = &self.output_spdx_rdf {
686 targets.push(OutputTarget {
687 format: OutputFormat::SpdxRdf,
688 file: file.clone(),
689 custom_template: None,
690 });
691 }
692
693 if let Some(file) = &self.output_cyclonedx {
694 targets.push(OutputTarget {
695 format: OutputFormat::CycloneDxJson,
696 file: file.clone(),
697 custom_template: None,
698 });
699 }
700
701 if let Some(file) = &self.output_cyclonedx_xml {
702 targets.push(OutputTarget {
703 format: OutputFormat::CycloneDxXml,
704 file: file.clone(),
705 custom_template: None,
706 });
707 }
708
709 if let Some(file) = &self.custom_output {
710 targets.push(OutputTarget {
711 format: OutputFormat::CustomTemplate,
712 file: file.clone(),
713 custom_template: self.custom_template.clone(),
714 });
715 }
716
717 targets
718 }
719
720 pub(crate) fn output_header_options(&self) -> JsonMap<String, JsonValue> {
721 let mut options = JsonMap::new();
722 if !self.dir_path.is_empty() {
723 options.insert(
724 "input".to_string(),
725 JsonValue::Array(
726 self.dir_path
727 .iter()
728 .cloned()
729 .map(JsonValue::String)
730 .collect(),
731 ),
732 );
733 }
734
735 let mut flags = Vec::new();
736
737 push_string_option(&mut flags, "--cache-dir", self.cache_dir.as_ref());
738 push_bool_option(&mut flags, "--cache-clear", self.cache_clear);
739 push_bool_option(&mut flags, "--cache-trust-mtime", self.cache_trust_mtime);
740 push_bool_option(&mut flags, "--classify", self.classify);
741 push_string_option(&mut flags, "--custom-output", self.custom_output.as_ref());
742 push_string_option(
743 &mut flags,
744 "--custom-template",
745 self.custom_template.as_ref(),
746 );
747 push_bool_option(&mut flags, "--copyright", self.copyright);
748 if self.compat_mode != CompatibilityMode::Native {
749 flags.push((
750 "--compat-mode".to_string(),
751 JsonValue::String(self.compat_mode.as_str().to_string()),
752 ));
753 }
754 push_string_option(&mut flags, "--cyclonedx", self.output_cyclonedx.as_ref());
755 push_string_option(
756 &mut flags,
757 "--cyclonedx-xml",
758 self.output_cyclonedx_xml.as_ref(),
759 );
760 push_string_option(&mut flags, "--debian", self.output_debian.as_ref());
761 push_bool_option(&mut flags, "--email", self.email);
762 push_array_option(&mut flags, "--facet", &self.facet);
763 push_bool_option(&mut flags, "--filter-clues", self.filter_clues);
764 push_bool_option(&mut flags, "--from-json", self.from_json);
765 push_bool_option(&mut flags, "--full-root", self.full_root);
766 push_bool_option(&mut flags, "--generated", self.generated);
767 push_string_option(&mut flags, "--html", self.output_html.as_ref());
768 push_array_option(&mut flags, "--ignore", &self.exclude);
769 push_array_option(&mut flags, "--ignore-author", &self.ignore_author);
770 push_array_option(
771 &mut flags,
772 "--ignore-copyright-holder",
773 &self.ignore_copyright_holder,
774 );
775 push_bool_option(&mut flags, "--incremental", self.incremental);
776 push_array_option(&mut flags, "--include", &self.include);
777 push_bool_option(&mut flags, "--info", self.info);
778 push_string_option(&mut flags, "--json", self.output_json.as_ref());
779 push_string_option(&mut flags, "--json-lines", self.output_json_lines.as_ref());
780 push_string_option(&mut flags, "--json-pp", self.output_json_pp.as_ref());
781 push_bool_option(&mut flags, "--license", self.license);
782 push_bool_option(
783 &mut flags,
784 "--license-clarity-score",
785 self.license_clarity_score,
786 );
787 push_bool_option(
788 &mut flags,
789 "--license-diagnostics",
790 self.license_diagnostics,
791 );
792 push_string_option(
793 &mut flags,
794 "--license-dataset-path",
795 self.license_dataset_path.as_ref(),
796 );
797 push_string_option(&mut flags, "--license-policy", self.license_policy.as_ref());
798 push_bool_option(
799 &mut flags,
800 "--no-license-index-cache",
801 self.no_license_index_cache,
802 );
803 push_bool_option(&mut flags, "--license-references", self.license_references);
804 push_bool_option(&mut flags, "--reindex", self.reindex);
805 push_non_default_u8_option(&mut flags, "--license-score", self.license_score, 0);
806 push_bool_option(&mut flags, "--license-text", self.license_text);
807 push_bool_option(
808 &mut flags,
809 "--license-text-diagnostics",
810 self.license_text_diagnostics,
811 );
812 push_non_default_string_option(
813 &mut flags,
814 "--license-url-template",
815 &self.license_url_template,
816 DEFAULT_LICENSEDB_URL_TEMPLATE,
817 );
818 push_non_default_usize_option(&mut flags, "--max-depth", self.max_depth, 0);
819 match self.max_in_memory {
820 MemoryMode::Limit(10000) => {}
821 MemoryMode::CollectFirst => {
822 flags.push(("--max-in-memory".to_string(), JsonValue::Number(0.into())));
823 }
824 MemoryMode::StreamUnlimited => {
825 flags.push((
826 "--max-in-memory".to_string(),
827 JsonValue::Number((-1i64).into()),
828 ));
829 }
830 MemoryMode::Limit(n) => {
831 flags.push(("--max-in-memory".to_string(), JsonValue::Number(n.into())));
832 }
833 }
834 if self.email {
835 push_non_default_usize_option(&mut flags, "--max-email", self.max_email, 50);
836 }
837 if self.url {
838 push_non_default_usize_option(&mut flags, "--max-url", self.max_url, 50);
839 }
840 push_bool_option(&mut flags, "--mark-source", self.mark_source);
841 push_bool_option(&mut flags, "--no-assemble", self.no_assemble);
842 push_bool_option(&mut flags, "--only-findings", self.only_findings);
843 push_bool_option(&mut flags, "--package", self.package);
844 push_bool_option(
845 &mut flags,
846 "--package-in-compiled",
847 self.package_in_compiled,
848 );
849 push_bool_option(&mut flags, "--package-only", self.package_only);
850 push_array_option(&mut flags, "--paths-file", &self.paths_file);
851 push_non_default_process_mode_option(
852 &mut flags,
853 "--processes",
854 self.processes,
855 ProcessMode::default_value(),
856 );
857 push_bool_option(&mut flags, "--quiet", self.quiet);
858 push_string_option(&mut flags, "--spdx-rdf", self.output_spdx_rdf.as_ref());
859 push_string_option(&mut flags, "--spdx-tv", self.output_spdx_tv.as_ref());
860 push_bool_option(&mut flags, "--strip-root", self.strip_root);
861 push_bool_option(&mut flags, "--summary", self.summary);
862 push_bool_option(&mut flags, "--system-package", self.system_package);
863 push_bool_option(&mut flags, "--tallies", self.tallies);
864 push_bool_option(&mut flags, "--tallies-by-facet", self.tallies_by_facet);
865 push_bool_option(&mut flags, "--tallies-key-files", self.tallies_key_files);
866 push_bool_option(
867 &mut flags,
868 "--tallies-with-details",
869 self.tallies_with_details,
870 );
871 push_non_default_f64_option(&mut flags, "--timeout", self.timeout, 120.0);
872 push_bool_option(&mut flags, "--unknown-licenses", self.unknown_licenses);
873 push_bool_option(
874 &mut flags,
875 "--no-sequence-matching",
876 self.no_sequence_matching,
877 );
878 push_bool_option(&mut flags, "--url", self.url);
879 push_bool_option(&mut flags, "--verbose", self.verbose);
880 push_string_option(&mut flags, "--yaml", self.output_yaml.as_ref());
881
882 flags.sort_by(|left, right| left.0.cmp(&right.0));
883 for (key, value) in flags {
884 options.insert(key, value);
885 }
886
887 options
888 }
889}
890
891impl From<&ScanArgs> for ScanRequest {
892 fn from(cli: &ScanArgs) -> Self {
893 Self {
894 input_paths: cli.dir_path.clone(),
895 input_mode: if cli.from_json {
896 InputMode::FromJson
897 } else {
898 InputMode::Native
899 },
900 output_targets: cli.output_targets(),
901 output_header_options: cli.output_header_options(),
902 progress_mode: if cli.quiet {
903 crate::progress::ProgressMode::Quiet
904 } else if cli.verbose {
905 crate::progress::ProgressMode::Verbose
906 } else {
907 crate::progress::ProgressMode::Default
908 },
909 process_mode: cli.processes,
910 timeout_seconds: cli.timeout,
911 quiet: cli.quiet,
912 verbose: cli.verbose,
913 strip_root: cli.strip_root,
914 full_root: cli.full_root,
915 exclude: cli.exclude.clone(),
916 include: cli.include.clone(),
917 paths_files: cli.paths_file.clone(),
918 respect_process_cache_env: true,
919 cache_dir: cli.cache_dir.clone(),
920 cache_clear: cli.cache_clear,
921 incremental: cli.incremental,
922 cache_trust_mtime: cli.cache_trust_mtime,
923 max_depth: cli.max_depth,
924 max_in_memory: cli.max_in_memory,
925 info: cli.info,
926 package: cli.package,
927 system_package: cli.system_package,
928 package_in_compiled: cli.package_in_compiled,
929 package_only: cli.package_only,
930 no_assemble: cli.no_assemble,
931 license_dataset_path: cli.license_dataset_path.clone(),
932 reindex: cli.reindex,
933 no_license_index_cache: cli.no_license_index_cache,
934 license_text: cli.license_text,
935 license_text_diagnostics: cli.license_text_diagnostics,
936 license_diagnostics: cli.license_diagnostics,
937 unknown_licenses: cli.unknown_licenses,
938 no_sequence_matching: cli.no_sequence_matching,
939 license_score: cli.license_score,
940 license_url_template: cli.license_url_template.clone(),
941 filter_clues: cli.filter_clues,
942 ignore_author: cli.ignore_author.clone(),
943 ignore_copyright_holder: cli.ignore_copyright_holder.clone(),
944 only_findings: cli.only_findings,
945 mark_source: cli.mark_source,
946 classify: cli.classify,
947 summary: cli.summary,
948 license_clarity_score: cli.license_clarity_score,
949 license_references: cli.license_references,
950 license_policy: cli.license_policy.clone(),
951 tallies: cli.tallies,
952 tallies_key_files: cli.tallies_key_files,
953 tallies_with_details: cli.tallies_with_details,
954 facet: cli.facet.clone(),
955 tallies_by_facet: cli.tallies_by_facet,
956 generated: cli.generated,
957 license: cli.license,
958 copyright: cli.copyright,
959 email: cli.email,
960 max_email: cli.max_email,
961 url: cli.url,
962 max_url: cli.max_url,
963 scan_bounds: crate::app::request::ScanBounds::default(),
964 }
965 }
966}
967
968fn push_bool_option(options: &mut Vec<(String, JsonValue)>, key: &str, enabled: bool) {
969 if enabled {
970 options.push((key.to_string(), JsonValue::Bool(true)));
971 }
972}
973
974fn push_string_option(options: &mut Vec<(String, JsonValue)>, key: &str, value: Option<&String>) {
975 if let Some(value) = value {
976 options.push((key.to_string(), JsonValue::String(value.clone())));
977 }
978}
979
980fn push_non_default_string_option(
981 options: &mut Vec<(String, JsonValue)>,
982 key: &str,
983 value: &str,
984 default: &str,
985) {
986 if value != default {
987 options.push((key.to_string(), JsonValue::String(value.to_string())));
988 }
989}
990
991fn push_array_option(options: &mut Vec<(String, JsonValue)>, key: &str, values: &[String]) {
992 if !values.is_empty() {
993 options.push((
994 key.to_string(),
995 JsonValue::Array(values.iter().cloned().map(JsonValue::String).collect()),
996 ));
997 }
998}
999
1000fn push_non_default_usize_option(
1001 options: &mut Vec<(String, JsonValue)>,
1002 key: &str,
1003 value: usize,
1004 default: usize,
1005) {
1006 if value != default {
1007 options.push((key.to_string(), JsonValue::Number(value.into())));
1008 }
1009}
1010
1011fn push_non_default_u8_option(
1012 options: &mut Vec<(String, JsonValue)>,
1013 key: &str,
1014 value: u8,
1015 default: u8,
1016) {
1017 if value != default {
1018 options.push((key.to_string(), JsonValue::Number(value.into())));
1019 }
1020}
1021
1022fn push_non_default_process_mode_option(
1023 options: &mut Vec<(String, JsonValue)>,
1024 key: &str,
1025 value: ProcessMode,
1026 default: ProcessMode,
1027) {
1028 if value != default {
1029 options.push((key.to_string(), JsonValue::Number(value.to_i32().into())));
1030 }
1031}
1032
1033fn push_non_default_f64_option(
1034 options: &mut Vec<(String, JsonValue)>,
1035 key: &str,
1036 value: f64,
1037 default: f64,
1038) {
1039 if (value - default).abs() > f64::EPSILON
1040 && let Some(number) = JsonNumber::from_f64(value)
1041 {
1042 options.push((key.to_string(), JsonValue::Number(number)));
1043 }
1044}
1045
1046#[cfg(test)]
1047mod tests;