Skip to main content

tokmd_format/scan_args/
mod.rs

1//! Deterministic scan argument construction for receipt metadata.
2
3use std::path::{Path, PathBuf};
4
5use crate::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 = normalize_rel_path(&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/// Normalize a relative path for matching:
26/// - converts `\` to `/`
27/// - strips all leading `./` segments
28#[must_use]
29fn normalize_rel_path(path: &str) -> String {
30    let normalized = if path.contains('\\') {
31        path.replace('\\', "/")
32    } else {
33        path.to_string()
34    };
35
36    let mut normalized = normalized.as_str();
37    while let Some(rest) = normalized.strip_prefix("./") {
38        normalized = rest;
39    }
40
41    normalized.to_string()
42}
43
44/// Construct `ScanArgs` with optional path and exclusion redaction.
45#[must_use]
46pub fn scan_args(paths: &[PathBuf], global: &ScanOptions, redact: Option<RedactMode>) -> ScanArgs {
47    let should_redact = matches!(redact, Some(RedactMode::Paths | RedactMode::All));
48    let excluded_redacted = should_redact && !global.excluded.is_empty();
49
50    let mut args = ScanArgs {
51        paths: paths.iter().map(|p| normalize_scan_input(p)).collect(),
52        excluded: if should_redact {
53            global.excluded.iter().map(|p| short_hash(p)).collect()
54        } else {
55            global.excluded.clone()
56        },
57        excluded_redacted,
58        config: global.config,
59        hidden: global.hidden,
60        no_ignore: global.no_ignore,
61        no_ignore_parent: global.no_ignore || global.no_ignore_parent,
62        no_ignore_dot: global.no_ignore || global.no_ignore_dot,
63        no_ignore_vcs: global.no_ignore || global.no_ignore_vcs,
64        treat_doc_strings_as_comments: global.treat_doc_strings_as_comments,
65    };
66
67    if should_redact {
68        args.paths = args.paths.iter().map(|p| redact_path(p)).collect();
69    }
70
71    args
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn normalize_scan_input_strips_repeated_dot_slash() {
80        let normalized = normalize_scan_input(Path::new("././src/lib.rs"));
81        assert_eq!(normalized, "src/lib.rs");
82    }
83
84    #[test]
85    fn normalize_scan_input_keeps_dot_for_empty_relative() {
86        let normalized = normalize_scan_input(Path::new("./"));
87        assert_eq!(normalized, ".");
88    }
89
90    #[test]
91    fn scan_args_paths_mode_redacts_scan_paths_and_exclusions() {
92        let paths = vec![PathBuf::from("src/lib.rs")];
93        let scan_options = ScanOptions {
94            excluded: vec!["target".to_string()],
95            ..Default::default()
96        };
97
98        let args = scan_args(&paths, &scan_options, Some(RedactMode::Paths));
99        assert_ne!(args.paths[0], "src/lib.rs");
100        assert_ne!(args.excluded[0], "target");
101        assert!(args.excluded_redacted);
102    }
103
104    #[test]
105    fn scan_args_no_ignore_enables_sub_flags() {
106        let paths = vec![PathBuf::from(".")];
107        let scan_options = ScanOptions {
108            no_ignore: true,
109            no_ignore_parent: false,
110            no_ignore_dot: false,
111            no_ignore_vcs: false,
112            ..Default::default()
113        };
114
115        let args = scan_args(&paths, &scan_options, None);
116        assert!(args.no_ignore_parent);
117        assert!(args.no_ignore_dot);
118        assert!(args.no_ignore_vcs);
119    }
120}