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::Config(format!("invalid glob `{p}`: {e}")))?;
67        b.add(glob);
68    }
69    b.build()
70        .map_err(|e| Error::Config(format!("globset: {e}")))
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use std::path::PathBuf;
77
78    fn rules_with(redact: &[&str], exclude: &[&str]) -> PathRules {
79        PathRules::from_raw(&PathRulesRaw {
80            redact_whole: redact.iter().map(|s| s.to_string()).collect(),
81            exclude: exclude.iter().map(|s| s.to_string()).collect(),
82        })
83        .unwrap()
84    }
85
86    #[test]
87    fn matches_env_file() {
88        let r = rules_with(&["**/.env", "**/.env.*"], &[]);
89        assert!(r.is_redact_whole(&PathBuf::from("project/.env")));
90        assert!(r.is_redact_whole(&PathBuf::from("a/b/c/.env.production")));
91        assert!(!r.is_redact_whole(&PathBuf::from("project/env.rs")));
92    }
93
94    #[test]
95    fn matches_pem_file() {
96        let r = rules_with(&["**/*.pem", "**/*.key"], &[]);
97        assert!(r.is_redact_whole(&PathBuf::from("certs/server.pem")));
98        assert!(r.is_redact_whole(&PathBuf::from("id_rsa.key")));
99        assert!(!r.is_redact_whole(&PathBuf::from("rsa.rs")));
100    }
101
102    #[test]
103    fn exclude_overrides_redact_in_caller() {
104        // PathRules reports booleans independently; it's the caller's job to
105        // prefer exclude-over-redact. Verify the two methods don't cross-talk.
106        let r = rules_with(&["**/.env"], &["**/test_fixtures/**/.env"]);
107        let path = PathBuf::from("project/test_fixtures/samples/.env");
108        assert!(r.is_redact_whole(&path));
109        assert!(r.is_excluded(&path));
110    }
111
112    #[test]
113    fn empty_rules_match_nothing() {
114        let r = rules_with(&[], &[]);
115        assert!(!r.is_redact_whole(&PathBuf::from(".env")));
116        assert!(!r.is_excluded(&PathBuf::from(".env")));
117    }
118
119    #[test]
120    fn invalid_glob_errors() {
121        let result = PathRules::from_raw(&PathRulesRaw {
122            redact_whole: vec!["[invalid".into()],
123            exclude: vec![],
124        });
125        assert!(result.is_err(), "malformed glob should error");
126    }
127}