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