1pub const TOKEN_PREFIXES: &[&str] = &["hnry_session_", "hnry_activity_", "hnry_lifecycle_", "hnry_plugin_"];
5pub const REDACTED: &str = "hnry_session_<redacted>";
6
7pub fn text(text: &str) -> Option<String> {
8 let bytes = text.as_bytes();
9 let mut out: Option<String> = None;
10 let mut copied = 0;
11 let mut i = 0;
12 while i < bytes.len() {
13 if let Some(prefix) = TOKEN_PREFIXES.iter().find(|prefix| {
14 bytes
15 .get(i..i + prefix.len())
16 .is_some_and(|s| s.eq_ignore_ascii_case(prefix.as_bytes()))
17 }) {
18 let digits = bytes[i + prefix.len()..]
19 .iter()
20 .take_while(|b| b.is_ascii_hexdigit())
21 .count();
22 if digits >= 8 {
23 let end = i + prefix.len() + digits;
24 let buffer = out.get_or_insert_with(|| String::with_capacity(text.len()));
25 buffer.push_str(&text[copied..i]);
26 buffer.push_str(prefix);
27 buffer.push_str("<redacted>");
28 copied = end;
29 i = end;
30 continue;
31 }
32 }
33 i += 1;
34 }
35 out.map(|mut buffer| {
36 buffer.push_str(&text[copied..]);
37 buffer
38 })
39}
40
41pub fn value(value: &mut serde_json::Value) -> bool {
42 use serde_json::Value;
43 match value {
44 Value::String(s) => {
45 if let Some(redacted) = text(s) {
46 *s = redacted;
47 true
48 } else {
49 false
50 }
51 }
52 Value::Array(items) => {
53 let mut changed = false;
54 for item in items {
55 changed |= self::value(item);
56 }
57 changed
58 }
59 Value::Object(map) => {
60 let mut changed = false;
61 let keys: Vec<_> = map
62 .keys()
63 .filter_map(|key| text(key).map(|r| (key.clone(), r)))
64 .collect();
65 for (key, replacement) in keys {
66 if let Some(mut entry) = map.remove(&key) {
67 self::value(&mut entry);
68 map.insert(replacement, entry);
69 changed = true;
70 }
71 }
72 for entry in map.values_mut() {
73 changed |= self::value(entry);
74 }
75 changed
76 }
77 _ => false,
78 }
79}
80
81pub struct Assembly {
82 text: String,
83 cap: usize,
84 truncated: bool,
85}
86pub struct Assembled {
87 pub text: String,
88 pub truncated: bool,
89}
90
91impl Assembly {
92 pub fn new(cap: usize) -> Self {
93 Self {
94 text: String::new(),
95 cap,
96 truncated: false,
97 }
98 }
99 pub fn push(&mut self, chunk: &str) {
100 let remaining = self.cap.saturating_sub(self.text.len());
101 let mut end = remaining.min(chunk.len());
102 while !chunk.is_char_boundary(end) {
103 end -= 1;
104 }
105 self.text.push_str(&chunk[..end]);
106 self.truncated |= end < chunk.len();
107 }
108 pub fn finish(mut self, cap: usize) -> Assembled {
109 if self.truncated {
112 let lower = self.text.to_ascii_lowercase();
113 for i in 0..lower.len() {
114 if !self.text.is_char_boundary(i) {
115 continue;
116 }
117 let tail = &lower[i..];
118 if TOKEN_PREFIXES
119 .iter()
120 .any(|p| p.starts_with(tail) || tail.starts_with(p))
121 {
122 self.text.truncate(i);
123 break;
124 }
125 }
126 }
127 let mut text = text(&self.text).unwrap_or(self.text);
128 if text.len() > cap {
129 let mut end = cap;
130 while !text.is_char_boundary(end) {
131 end -= 1;
132 }
133 text.truncate(end);
134 self.truncated = true;
135 }
136 Assembled {
137 text,
138 truncated: self.truncated,
139 }
140 }
141}