cargo_context_core/scrub/
paths.rs1use 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 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}