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