1pub fn truncate_at_char_boundary(value: &str, max_bytes: usize) -> &str {
11 if value.len() <= max_bytes {
12 return value;
13 }
14 let mut end = max_bytes;
15 while end > 0 && !value.is_char_boundary(end) {
16 end -= 1;
17 }
18 &value[..end]
19}
20
21fn find_value_end(s: &str) -> usize {
22 let chars = s.chars();
23 let mut end = 0;
24 let mut in_quote = None;
25 let mut escaped = false;
26
27 for c in chars {
28 let char_len = c.len_utf8();
29
30 if escaped {
31 escaped = false;
32 end += char_len;
33 continue;
34 }
35
36 if c == '\\' {
37 escaped = true;
38 end += char_len;
39 continue;
40 }
41
42 if let Some(q) = in_quote {
43 if c == q {
44 in_quote = None;
45 }
46 end += char_len;
47 continue;
48 }
49
50 if c == '"' || c == '\'' {
51 in_quote = Some(c);
52 end += char_len;
53 continue;
54 }
55
56 if c.is_whitespace() {
57 break;
58 }
59
60 end += char_len;
61 }
62 end
63}
64
65pub fn mask_sensitive_command(cmd: &str) -> String {
70 let patterns = [
73 ("CARGO_REGISTRY_TOKEN=", "CARGO_REGISTRY_TOKEN=***"),
75 ("GITHUB_TOKEN=", "GITHUB_TOKEN=***"),
76 ("GH_TOKEN=", "GH_TOKEN=***"),
77 ("DATABASE_URL=", "DATABASE_URL=***"),
78 ("DB_PASSWORD=", "DB_PASSWORD=***"),
79 ("API_KEY=", "API_KEY=***"),
80 ("API_SECRET=", "API_SECRET=***"),
81 ("SECRET_KEY=", "SECRET_KEY=***"),
82 ("SECRET=", "SECRET=***"),
83 ("PASSWORD=", "PASSWORD=***"),
84 ("PASS=", "PASS=***"),
85 ("TOKEN=", "TOKEN=***"),
86 ("AUTH_TOKEN=", "AUTH_TOKEN=***"),
87 ("ACCESS_TOKEN=", "ACCESS_TOKEN=***"),
88 ("PRIVATE_KEY=", "PRIVATE_KEY=***"),
89 ("AWS_SECRET_ACCESS_KEY=", "AWS_SECRET_ACCESS_KEY=***"),
90 ("AWS_ACCESS_KEY_ID=", "AWS_ACCESS_KEY_ID=***"),
91 ("STRIPE_SECRET_KEY=", "STRIPE_SECRET_KEY=***"),
92 ("OPENAI_API_KEY=", "OPENAI_API_KEY=***"),
93 ("ANTHROPIC_API_KEY=", "ANTHROPIC_API_KEY=***"),
94 ("_KEY=", "_KEY=***"),
97 ("--token ", "--token ***"),
99 ("--token=", "--token=***"),
100 ("--password ", "--password ***"),
101 ("--password=", "--password=***"),
102 ("--api-key ", "--api-key ***"),
103 ("--api-key=", "--api-key=***"),
104 ("--secret ", "--secret ***"),
105 ("--secret=", "--secret=***"),
106 ];
107
108 let mut result = cmd.to_string();
109 for (pattern, replacement) in patterns {
110 let mut search_start = 0;
113 while search_start < result.len() {
114 let Some(start) = result[search_start..].find(pattern) else {
115 break;
116 };
117 let abs_start = search_start + start;
118 let value_start = abs_start + pattern.len();
119
120 let rest = &result[value_start..];
121 let value_end = value_start + find_value_end(rest);
122
123 let prefix = &result[..abs_start];
125 let suffix = &result[value_end..];
126 result = format!("{}{}{}", prefix, replacement, suffix);
127
128 search_start = abs_start + replacement.len();
130 }
131 }
132
133 result
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn test_mask_sensitive_command() {
142 let cmd = "cargo run --release TOKEN=secret123 GITHUB_TOKEN=abcdef --token mytoken --password=hidden";
143 let masked = mask_sensitive_command(cmd);
144
145 assert!(masked.contains("TOKEN=***"));
146 assert!(!masked.contains("secret123"));
147
148 assert!(masked.contains("GITHUB_TOKEN=***"));
149 assert!(!masked.contains("abcdef"));
150
151 assert!(masked.contains("--token ***"));
152 assert!(!masked.contains("mytoken"));
153
154 assert!(masked.contains("--password=***"));
155 assert!(!masked.contains("hidden"));
156 }
157
158 #[test]
159 fn test_mask_sensitive_command_multiple() {
160 let cmd = "TOKEN=a TOKEN=b";
161 let masked = mask_sensitive_command(cmd);
162 assert_eq!(masked, "TOKEN=*** TOKEN=***");
163 }
164
165 #[test]
166 fn test_mask_sensitive_command_quoted() {
167 let cmd = "cargo run TOKEN=\"my super secret\" --other";
168 let masked = mask_sensitive_command(cmd);
169 assert_eq!(masked, "cargo run TOKEN=*** --other");
170 assert!(!masked.contains("super"));
171 assert!(!masked.contains("secret"));
172 }
173
174 #[test]
175 fn test_truncate_at_char_boundary_short_string_unchanged() {
176 assert_eq!(truncate_at_char_boundary("abc", 10), "abc");
177 assert_eq!(truncate_at_char_boundary("", 10), "");
178 assert_eq!(truncate_at_char_boundary("exactly10!", 10), "exactly10!");
179 }
180
181 #[test]
182 fn test_truncate_at_char_boundary_ascii() {
183 assert_eq!(truncate_at_char_boundary("hello world", 5), "hello");
184 assert_eq!(truncate_at_char_boundary("abcdef", 3), "abc");
185 }
186
187 #[test]
188 fn test_truncate_at_char_boundary_snaps_back_from_mid_codepoint() {
189 assert_eq!(truncate_at_char_boundary("é", 1), "");
192 assert_eq!(truncate_at_char_boundary("é", 2), "é");
194 assert_eq!(truncate_at_char_boundary("café", 4), "caf");
197 assert_eq!(truncate_at_char_boundary("café", 3), "caf");
198 assert_eq!(truncate_at_char_boundary("café", 5), "café");
199 }
200
201 #[test]
202 fn test_truncate_at_char_boundary_never_panics_on_len_zero() {
203 assert_eq!(truncate_at_char_boundary("hello", 0), "");
204 assert_eq!(truncate_at_char_boundary("é", 0), "");
205 }
206}