Skip to main content

cargo_context_core/scrub/
paths.rs

1//! Path-based redaction rules.
2//!
3//! Files whose paths match the `redact_whole` globs are replaced with a
4//! single `\[REDACTED FILE: <path>\]` marker when fed through
5//! [`crate::scrub::Scrubber::scrub_file`]. Paths matching the `exclude`
6//! globs bypass *all* scrubbing — useful for test fixtures containing
7//! public sample keys.
8
9use std::path::Path;
10
11use globset::{Glob, GlobSet, GlobSetBuilder};
12use serde::Deserialize;
13
14use crate::error::{Error, Result};
15
16#[derive(Debug, Clone, Default, Deserialize)]
17pub struct PathRulesRaw {
18    #[serde(default)]
19    pub redact_whole: Vec<String>,
20    #[serde(default)]
21    pub exclude: Vec<String>,
22}
23
24#[derive(Debug, Default)]
25pub struct PathRules {
26    redact_whole: Option<GlobSet>,
27    exclude: Option<GlobSet>,
28}
29
30impl PathRules {
31    pub fn from_raw(raw: &PathRulesRaw) -> Result<Self> {
32        let redact_whole = if raw.redact_whole.is_empty() {
33            None
34        } else {
35            Some(build_globset(&raw.redact_whole)?)
36        };
37        let exclude = if raw.exclude.is_empty() {
38            None
39        } else {
40            Some(build_globset(&raw.exclude)?)
41        };
42        Ok(Self {
43            redact_whole,
44            exclude,
45        })
46    }
47
48    pub fn is_redact_whole(&self, path: &Path) -> bool {
49        self.redact_whole
50            .as_ref()
51            .map(|gs| gs.is_match(path))
52            .unwrap_or(false)
53    }
54
55    pub fn is_excluded(&self, path: &Path) -> bool {
56        self.exclude
57            .as_ref()
58            .map(|gs| gs.is_match(path))
59            .unwrap_or(false)
60    }
61}
62
63fn build_globset(patterns: &[String]) -> Result<GlobSet> {
64    let mut b = GlobSetBuilder::new();
65    for p in patterns {
66        let glob = Glob::new(p).map_err(|e| Error::Glob(format!("invalid glob `{p}`: {e}")))?;
67        b.add(glob);
68    }
69    b.build().map_err(|e| Error::Glob(format!("globset: {e}")))
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use std::path::PathBuf;
76
77    fn rules_with(redact: &[&str], exclude: &[&str]) -> PathRules {
78        PathRules::from_raw(&PathRulesRaw {
79            redact_whole: redact.iter().map(|s| s.to_string()).collect(),
80            exclude: exclude.iter().map(|s| s.to_string()).collect(),
81        })
82        .unwrap()
83    }
84
85    #[test]
86    fn matches_env_file() {
87        let r = rules_with(&["**/.env", "**/.env.*"], &[]);
88        assert!(r.is_redact_whole(&PathBuf::from("project/.env")));
89        assert!(r.is_redact_whole(&PathBuf::from("a/b/c/.env.production")));
90        assert!(!r.is_redact_whole(&PathBuf::from("project/env.rs")));
91    }
92
93    #[test]
94    fn matches_pem_file() {
95        let r = rules_with(&["**/*.pem", "**/*.key"], &[]);
96        assert!(r.is_redact_whole(&PathBuf::from("certs/server.pem")));
97        assert!(r.is_redact_whole(&PathBuf::from("id_rsa.key")));
98        assert!(!r.is_redact_whole(&PathBuf::from("rsa.rs")));
99    }
100
101    #[test]
102    fn exclude_overrides_redact_in_caller() {
103        // PathRules reports booleans independently; it's the caller's job to
104        // prefer exclude-over-redact. Verify the two methods don't cross-talk.
105        let r = rules_with(&["**/.env"], &["**/test_fixtures/**/.env"]);
106        let path = PathBuf::from("project/test_fixtures/samples/.env");
107        assert!(r.is_redact_whole(&path));
108        assert!(r.is_excluded(&path));
109    }
110
111    #[test]
112    fn empty_rules_match_nothing() {
113        let r = rules_with(&[], &[]);
114        assert!(!r.is_redact_whole(&PathBuf::from(".env")));
115        assert!(!r.is_excluded(&PathBuf::from(".env")));
116    }
117
118    #[test]
119    fn invalid_glob_errors() {
120        let result = PathRules::from_raw(&PathRulesRaw {
121            redact_whole: vec!["[invalid".into()],
122            exclude: vec![],
123        });
124        assert!(result.is_err(), "malformed glob should error");
125    }
126}