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