Skip to main content

tokmd_scan_args/
lib.rs

1//! Deterministic scan argument construction for receipt metadata.
2
3use std::path::{Path, PathBuf};
4
5use tokmd_redact::{redact_path, short_hash};
6use tokmd_settings::ScanOptions;
7use tokmd_types::{RedactMode, ScanArgs};
8
9/// Normalize a path to forward slashes and strip leading `./` segments.
10#[must_use]
11pub fn normalize_scan_input(p: &Path) -> String {
12    let mut normalized = tokmd_path::normalize_slashes(&p.display().to_string());
13
14    while let Some(stripped) = normalized.strip_prefix("./") {
15        normalized = stripped.to_string();
16    }
17
18    if normalized.is_empty() {
19        ".".to_string()
20    } else {
21        normalized
22    }
23}
24
25/// Construct `ScanArgs` with optional path and exclusion redaction.
26#[must_use]
27pub fn scan_args(paths: &[PathBuf], global: &ScanOptions, redact: Option<RedactMode>) -> ScanArgs {
28    let should_redact = matches!(redact, Some(RedactMode::Paths | RedactMode::All));
29    let excluded_redacted = should_redact && !global.excluded.is_empty();
30
31    let mut args = ScanArgs {
32        paths: paths.iter().map(|p| normalize_scan_input(p)).collect(),
33        excluded: if should_redact {
34            global.excluded.iter().map(|p| short_hash(p)).collect()
35        } else {
36            global.excluded.clone()
37        },
38        excluded_redacted,
39        config: global.config,
40        hidden: global.hidden,
41        no_ignore: global.no_ignore,
42        no_ignore_parent: global.no_ignore || global.no_ignore_parent,
43        no_ignore_dot: global.no_ignore || global.no_ignore_dot,
44        no_ignore_vcs: global.no_ignore || global.no_ignore_vcs,
45        treat_doc_strings_as_comments: global.treat_doc_strings_as_comments,
46    };
47
48    if should_redact {
49        args.paths = args.paths.iter().map(|p| redact_path(p)).collect();
50    }
51
52    args
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn normalize_scan_input_strips_repeated_dot_slash() {
61        let normalized = normalize_scan_input(Path::new("././src/lib.rs"));
62        assert_eq!(normalized, "src/lib.rs");
63    }
64
65    #[test]
66    fn normalize_scan_input_keeps_dot_for_empty_relative() {
67        let normalized = normalize_scan_input(Path::new("./"));
68        assert_eq!(normalized, ".");
69    }
70
71    #[test]
72    fn scan_args_paths_mode_redacts_scan_paths_and_exclusions() {
73        let paths = vec![PathBuf::from("src/lib.rs")];
74        let scan_options = ScanOptions {
75            excluded: vec!["target".to_string()],
76            ..Default::default()
77        };
78
79        let args = scan_args(&paths, &scan_options, Some(RedactMode::Paths));
80        assert_ne!(args.paths[0], "src/lib.rs");
81        assert_ne!(args.excluded[0], "target");
82        assert!(args.excluded_redacted);
83    }
84
85    #[test]
86    fn scan_args_no_ignore_enables_sub_flags() {
87        let paths = vec![PathBuf::from(".")];
88        let scan_options = ScanOptions {
89            no_ignore: true,
90            no_ignore_parent: false,
91            no_ignore_dot: false,
92            no_ignore_vcs: false,
93            ..Default::default()
94        };
95
96        let args = scan_args(&paths, &scan_options, None);
97        assert!(args.no_ignore_parent);
98        assert!(args.no_ignore_dot);
99        assert!(args.no_ignore_vcs);
100    }
101}