rust_sanitize/processor/
json_proc.rs1use crate::error::{Result, SanitizeError};
15use crate::processor::limits::DEFAULT_INPUT_SIZE;
16use crate::processor::{walk_tree, FileTypeProfile, Processor, TreeNode};
17use crate::store::MappingStore;
18use serde_json::Value;
19
20pub struct JsonProcessor;
22
23impl Processor for JsonProcessor {
24 fn name(&self) -> &'static str {
25 "json"
26 }
27
28 fn can_handle(&self, content: &[u8], profile: &FileTypeProfile) -> bool {
29 if profile.processor == "json" {
30 return true;
31 }
32 let trimmed = content.iter().copied().find(|b| !b.is_ascii_whitespace());
34 matches!(trimmed, Some(b'{' | b'['))
35 }
36
37 fn process(
38 &self,
39 content: &[u8],
40 profile: &FileTypeProfile,
41 store: &MappingStore,
42 ) -> Result<Vec<u8>> {
43 let text = crate::processor::check_size_and_decode(content, "JSON", DEFAULT_INPUT_SIZE)?;
45
46 let mut value: Value =
47 serde_json::from_str(text).map_err(|e| SanitizeError::ParseError {
48 format: "JSON".into(),
49 message: format!("JSON parse error: {}", e),
50 })?;
51
52 walk_json(&mut value, "", profile, store, 0)?;
53
54 let compact = profile.options.get("compact").is_some_and(|v| v == "true");
55
56 let output = if compact {
57 serde_json::to_vec(&value)
58 } else {
59 serde_json::to_vec_pretty(&value)
60 }
61 .map_err(|e| {
62 SanitizeError::IoError(std::io::Error::other(format!("JSON serialize error: {e}")))
63 })?;
64
65 Ok(output)
66 }
67}
68
69impl TreeNode for Value {
70 fn for_each_map_entry<F>(&mut self, mut f: F) -> Result<()>
71 where
72 F: FnMut(&str, &mut Self) -> Result<()>,
73 {
74 if let Self::Object(map) = self {
75 let keys: Vec<String> = map.keys().cloned().collect();
76 for key in keys {
77 if let Some(v) = map.get_mut(&key) {
78 f(&key, v)?;
79 }
80 }
81 }
82 Ok(())
83 }
84
85 fn for_each_seq_item<F>(&mut self, mut f: F) -> Result<()>
86 where
87 F: FnMut(&mut Self) -> Result<()>,
88 {
89 if let Self::Array(arr) = self {
90 for item in arr.iter_mut() {
91 f(item)?;
92 }
93 }
94 Ok(())
95 }
96
97 fn as_str_mut(&mut self) -> Option<&mut String> {
98 if let Self::String(s) = self {
99 Some(s)
100 } else {
101 None
102 }
103 }
104
105 fn is_scalar(&self) -> bool {
106 matches!(self, Self::Number(_) | Self::Bool(_))
107 }
108
109 fn scalar_to_string(&self) -> String {
110 self.to_string()
111 }
112
113 fn set_string(&mut self, s: String) {
114 *self = Self::String(s);
115 }
116}
117
118pub(crate) fn walk_json(
120 value: &mut Value,
121 prefix: &str,
122 profile: &FileTypeProfile,
123 store: &MappingStore,
124 depth: usize,
125) -> Result<()> {
126 walk_tree(value, prefix, profile, store, depth, "JSON")
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::category::Category;
133 use crate::generator::HmacGenerator;
134 use crate::processor::profile::FieldRule;
135 use std::sync::Arc;
136
137 fn make_store() -> MappingStore {
138 let gen = Arc::new(HmacGenerator::new([42u8; 32]));
139 MappingStore::new(gen, None)
140 }
141
142 #[test]
143 fn basic_json_replacement() {
144 let store = make_store();
145 let proc = JsonProcessor;
146
147 let content =
148 br#"{"database": {"host": "db.corp.com", "password": "s3cret"}, "port": 5432}"#;
149 let profile = FileTypeProfile::new(
150 "json",
151 vec![
152 FieldRule::new("database.password").with_category(Category::Custom("pw".into())),
153 FieldRule::new("database.host").with_category(Category::Hostname),
154 ],
155 )
156 .with_option("compact", "true");
157
158 let result = proc.process(content, &profile, &store).unwrap();
159 let out: Value = serde_json::from_slice(&result).unwrap();
160
161 assert_ne!(out["database"]["password"].as_str().unwrap(), "s3cret");
162 assert_ne!(out["database"]["host"].as_str().unwrap(), "db.corp.com");
163 assert_eq!(out["port"], 5432);
164 }
165
166 #[test]
167 fn json_array_traversal() {
168 let store = make_store();
169 let proc = JsonProcessor;
170
171 let content = br#"{"users": [{"email": "a@b.com"}, {"email": "c@d.com"}]}"#;
172 let profile = FileTypeProfile::new(
173 "json",
174 vec![FieldRule::new("users.email").with_category(Category::Email)],
175 )
176 .with_option("compact", "true");
177
178 let result = proc.process(content, &profile, &store).unwrap();
179 let out: Value = serde_json::from_slice(&result).unwrap();
180
181 let users = out["users"].as_array().unwrap();
182 assert_ne!(users[0]["email"].as_str().unwrap(), "a@b.com");
183 assert_ne!(users[1]["email"].as_str().unwrap(), "c@d.com");
184 }
185
186 #[test]
187 fn json_glob_suffix_pattern() {
188 let store = make_store();
189 let proc = JsonProcessor;
190
191 let content =
192 br#"{"db": {"password": "pw1"}, "cache": {"password": "pw2"}, "name": "app"}"#;
193 let profile = FileTypeProfile::new(
194 "json",
195 vec![FieldRule::new("*.password").with_category(Category::Custom("pw".into()))],
196 )
197 .with_option("compact", "true");
198
199 let result = proc.process(content, &profile, &store).unwrap();
200 let out: Value = serde_json::from_slice(&result).unwrap();
201
202 assert_ne!(out["db"]["password"].as_str().unwrap(), "pw1");
203 assert_ne!(out["cache"]["password"].as_str().unwrap(), "pw2");
204 assert_eq!(out["name"], "app");
205 }
206}