1use crate::{borrow::Cow, format, string::ToString, String, Vec};
4use sha2::{Digest, Sha256};
5
6use crate::types::{
7 Action, ActionOutcome, EnforcementMode, HashAlgo, Outcome, OutcomeCode, Record,
8 RedactionPolicy, Value,
9};
10
11pub fn apply_action(action: &Action, value: &Value) -> ActionOutcome {
13 match action {
14 Action::Drop => ActionOutcome::Drop,
15 Action::ReplaceWith(v) => ActionOutcome::ReplaceWith(v.clone()),
16 Action::Hash(algo) => ActionOutcome::ReplaceWith(Value::String(hash_value(*algo, value))),
17 Action::Mask {
18 visible_prefix,
19 visible_suffix,
20 mask_char,
21 } => ActionOutcome::ReplaceWith(mask_value(
22 value,
23 *visible_prefix,
24 *visible_suffix,
25 *mask_char,
26 )),
27 Action::Truncate { max_bytes } => {
28 ActionOutcome::ReplaceWith(truncate_value(value, *max_bytes))
29 }
30 }
31}
32
33pub fn apply_action_to_str(action: &Action, value: &str) -> Option<String> {
35 match apply_action(action, &Value::String(value.to_string())) {
36 ActionOutcome::Drop => None,
37 ActionOutcome::ReplaceWith(Value::String(s)) => Some(s),
38 ActionOutcome::ReplaceWith(Value::Bytes(bytes)) => Some(match String::from_utf8(bytes) {
39 Ok(s) => s,
40 Err(err) => String::from_utf8_lossy(&err.into_bytes()).into_owned(),
41 }),
42 ActionOutcome::ReplaceWith(Value::I64(v)) => Some(v.to_string()),
43 ActionOutcome::ReplaceWith(Value::U64(v)) => Some(v.to_string()),
44 ActionOutcome::ReplaceWith(Value::Bool(v)) => Some(
45 if v {
46 Cow::Borrowed("true")
47 } else {
48 Cow::Borrowed("false")
49 }
50 .into_owned(),
51 ),
52 }
53}
54
55pub fn enforce_in_place(
60 mode: EnforcementMode,
61 policy: &RedactionPolicy,
62 record: &mut Record,
63) -> Outcome {
64 match mode {
65 EnforcementMode::Block => {
66 for rule in policy.rules() {
67 if record.contains_key(&rule.field) {
68 return Outcome::reject(
69 OutcomeCode::RejectedBlockedField,
70 format!("blocked sensitive field present name={}", rule.field),
71 );
72 }
73 }
74 Outcome::accept(0, "accepted (no blocked fields present)".to_string())
75 }
76 EnforcementMode::Transform => {
77 let mut applied: u32 = 0;
78 for rule in policy.rules() {
79 let Some(existing) = record.get(&rule.field).cloned() else {
80 continue;
81 };
82
83 match apply_action(&rule.action, &existing) {
84 ActionOutcome::Drop => {
85 let _ = record.remove(&rule.field);
86 applied = applied.saturating_add(1);
87 }
88 ActionOutcome::ReplaceWith(v) => {
89 let _ = record.insert(rule.field.clone(), v);
90 applied = applied.saturating_add(1);
91 }
92 }
93 }
94 Outcome::accept(applied, format!("accepted transforms_applied={applied}"))
95 }
96 }
97}
98
99fn hash_value(algo: HashAlgo, value: &Value) -> String {
100 match algo {
101 HashAlgo::Sha256 => {
102 let mut hasher = Sha256::new();
103 match value {
104 Value::String(s) => {
105 hasher.update([b's', 0]);
106 hasher.update(s.as_bytes());
107 }
108 Value::Bytes(b) => {
109 hasher.update([b'b', 0]);
110 hasher.update(b);
111 }
112 Value::I64(v) => {
113 hasher.update([b'i', 0]);
114 hasher.update(v.to_string().as_bytes());
115 }
116 Value::U64(v) => {
117 hasher.update([b'u', 0]);
118 hasher.update(v.to_string().as_bytes());
119 }
120 Value::Bool(v) => {
121 hasher.update([b'o', 0]);
122 hasher.update(if *v {
123 b"true".as_slice()
124 } else {
125 b"false".as_slice()
126 });
127 }
128 }
129 to_lower_hex(&hasher.finalize())
130 }
131 }
132}
133
134fn to_lower_hex(bytes: &[u8]) -> String {
135 const HEX: &[u8; 16] = b"0123456789abcdef";
136 let mut out = String::with_capacity(bytes.len() * 2);
137 for byte in bytes {
138 out.push(HEX[(byte >> 4) as usize] as char);
139 out.push(HEX[(byte & 0x0f) as usize] as char);
140 }
141 out
142}
143
144fn mask_value(
145 value: &Value,
146 visible_prefix: usize,
147 visible_suffix: usize,
148 mask_char: char,
149) -> Value {
150 match value {
151 Value::String(s) => {
152 Value::String(mask_string(s, visible_prefix, visible_suffix, mask_char))
153 }
154 Value::Bytes(bytes) => Value::String(mask_string(
155 &String::from_utf8_lossy(bytes),
156 visible_prefix,
157 visible_suffix,
158 mask_char,
159 )),
160 Value::I64(v) => Value::String(mask_string(
161 &v.to_string(),
162 visible_prefix,
163 visible_suffix,
164 mask_char,
165 )),
166 Value::U64(v) => Value::String(mask_string(
167 &v.to_string(),
168 visible_prefix,
169 visible_suffix,
170 mask_char,
171 )),
172 Value::Bool(v) => Value::String(mask_string(
173 if *v { "true" } else { "false" },
174 visible_prefix,
175 visible_suffix,
176 mask_char,
177 )),
178 }
179}
180
181fn mask_string(s: &str, visible_prefix: usize, visible_suffix: usize, mask_char: char) -> String {
182 let chars: Vec<char> = s.chars().collect();
183 if visible_prefix.saturating_add(visible_suffix) >= chars.len() {
184 return s.to_string();
185 }
186
187 let mut out = String::new();
188 for ch in chars.iter().take(visible_prefix) {
189 out.push(*ch);
190 }
191 for _ in 0..(chars.len() - visible_prefix - visible_suffix) {
192 out.push(mask_char);
193 }
194 for ch in chars.iter().skip(chars.len() - visible_suffix) {
195 out.push(*ch);
196 }
197 out
198}
199
200fn truncate_value(value: &Value, max_bytes: usize) -> Value {
201 match value {
202 Value::String(s) => Value::String(truncate_string(s, max_bytes)),
203 Value::Bytes(bytes) => Value::Bytes(bytes.iter().take(max_bytes).copied().collect()),
204 Value::I64(v) => Value::String(truncate_string(&v.to_string(), max_bytes)),
205 Value::U64(v) => Value::String(truncate_string(&v.to_string(), max_bytes)),
206 Value::Bool(v) => Value::String(truncate_string(
207 if *v { "true" } else { "false" },
208 max_bytes,
209 )),
210 }
211}
212
213fn truncate_string(s: &str, max_bytes: usize) -> String {
214 if s.len() <= max_bytes {
215 return s.to_string();
216 }
217 let mut end = max_bytes;
218 while end > 0 && !s.is_char_boundary(end) {
219 end -= 1;
220 }
221 s[..end].to_string()
222}