1use rand::{Rng, RngExt};
2
3const GARBLE_CHARS: &[u8] =
4 b"!@#$%^&*()_+-=[]{}|;:,.<>?`~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
5
6pub fn garble_text<R: Rng>(length: usize, rng: &mut R) -> String {
7 (0..length)
8 .map(|_| GARBLE_CHARS[rng.random_range(0..GARBLE_CHARS.len())] as char)
9 .collect()
10}
11
12pub fn garble_frame<R: Rng>(from_len: usize, to_len: usize, progress: f64, rng: &mut R) -> String {
13 let interpolated = from_len as f64 + (to_len as f64 - from_len as f64) * progress.clamp(0.0, 1.0);
14 let length = interpolated.round().max(1.0) as usize;
15
16 garble_text(length, rng)
17}
18
19pub fn decode_frame<R: Rng>(target: &str, revealed_count: usize, rng: &mut R) -> String {
20 let chars: Vec<char> = target.chars().collect();
21 let revealed = revealed_count.min(chars.len());
22 let prefix: String = chars[..revealed].iter().collect();
23 let remaining = chars.len().saturating_sub(revealed);
24
25 if remaining == 0 {
26 return prefix;
27 }
28
29 let suffix = garble_text(remaining, rng);
30 format!("{prefix}{suffix}")
31}
32
33pub fn encode_frame<R: Rng>(target: &str, garbled_count: usize, rng: &mut R) -> String {
34 let chars: Vec<char> = target.chars().collect();
35 let garbled = garbled_count.min(chars.len());
36 let suffix: String = chars[garbled..].iter().collect();
37
38 if garbled == 0 {
39 return suffix;
40 }
41
42 let prefix = garble_text(garbled, rng);
43 format!("{prefix}{suffix}")
44}
45
46pub fn is_fully_revealed(target: &str, revealed_count: usize) -> bool {
47 revealed_count >= target.chars().count()
48}
49
50pub fn is_fully_garbled(target: &str, garbled_count: usize) -> bool {
51 garbled_count >= target.chars().count()
52}
53
54pub fn decode_text<R: Rng>(target: &str, tick: usize, garble_ticks: usize, rng: &mut R) -> String {
58 if tick < garble_ticks {
59 garble_frame(1, target.len(), tick as f64 / garble_ticks as f64, rng)
60 } else {
61 decode_frame(target, tick - garble_ticks, rng)
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68 use rand::SeedableRng;
69 use rand::rngs::SmallRng;
70
71 fn test_rng() -> SmallRng {
72 SmallRng::seed_from_u64(42)
73 }
74
75 #[test]
76 fn garble_text_produces_correct_length() {
77 let mut rng = test_rng();
78
79 assert_eq!(garble_text(10, &mut rng).len(), 10);
80 assert_eq!(garble_text(0, &mut rng).len(), 0);
81 assert_eq!(garble_text(100, &mut rng).len(), 100);
82 }
83
84 #[test]
85 fn garble_text_uses_only_valid_chars() {
86 let mut rng = test_rng();
87 let text = garble_text(200, &mut rng);
88
89 for ch in text.chars() {
90 assert!(GARBLE_CHARS.contains(&(ch as u8)), "unexpected char: {ch}");
91 }
92 }
93
94 #[test]
95 fn garble_frame_interpolates_length() {
96 let mut rng = test_rng();
97
98 let start = garble_frame(5, 20, 0.0, &mut rng);
99 let middle = garble_frame(5, 20, 0.5, &mut rng);
100 let end = garble_frame(5, 20, 1.0, &mut rng);
101
102 assert_eq!(start.len(), 5);
103 assert_eq!(middle.len(), 13); assert_eq!(end.len(), 20);
105 }
106
107 #[test]
108 fn garble_frame_clamps_progress() {
109 let mut rng = test_rng();
110
111 let below = garble_frame(5, 20, -1.0, &mut rng);
112 let above = garble_frame(5, 20, 2.0, &mut rng);
113
114 assert_eq!(below.len(), 5);
115 assert_eq!(above.len(), 20);
116 }
117
118 #[test]
119 fn decode_frame_reveals_prefix_progressively() {
120 let mut rng = test_rng();
121 let target = "hello world";
122
123 let frame0 = decode_frame(target, 0, &mut rng);
124 let frame5 = decode_frame(target, 5, &mut rng);
125 let frame_full = decode_frame(target, 11, &mut rng);
126
127 assert_eq!(frame0.chars().count(), target.chars().count());
128 assert!(!frame0.starts_with('h'));
129
130 assert!(frame5.starts_with("hello"));
131 assert_eq!(frame5.chars().count(), target.chars().count());
132
133 assert_eq!(frame_full, "hello world");
134 }
135
136 #[test]
137 fn decode_frame_handles_beyond_target_length() {
138 let mut rng = test_rng();
139 let target = "hi";
140
141 let frame = decode_frame(target, 100, &mut rng);
142
143 assert_eq!(frame, "hi");
144 }
145
146 #[test]
147 fn is_fully_revealed_checks_char_count() {
148 assert!(!is_fully_revealed("hello", 4));
149 assert!(is_fully_revealed("hello", 5));
150 assert!(is_fully_revealed("hello", 6));
151 }
152
153 #[test]
154 fn encode_frame_garbles_prefix_progressively() {
155 let mut rng = test_rng();
156 let target = "hello world";
157
158 let frame0 = encode_frame(target, 0, &mut rng);
159 let frame5 = encode_frame(target, 5, &mut rng);
160 let frame_full = encode_frame(target, 11, &mut rng);
161
162 assert_eq!(frame0, "hello world");
163
164 assert!(frame5.ends_with(" world"));
165 assert_eq!(frame5.chars().count(), target.chars().count());
166
167 assert_eq!(frame_full.chars().count(), target.chars().count());
168 assert!(!frame_full.contains("hello"));
169 }
170
171 #[test]
172 fn encode_frame_handles_beyond_target_length() {
173 let mut rng = test_rng();
174 let target = "hi";
175
176 let frame = encode_frame(target, 100, &mut rng);
177
178 assert_eq!(frame.chars().count(), 2);
179 }
180
181 #[test]
182 fn is_fully_garbled_checks_char_count() {
183 assert!(!is_fully_garbled("hello", 4));
184 assert!(is_fully_garbled("hello", 5));
185 assert!(is_fully_garbled("hello", 6));
186 }
187
188 #[test]
189 fn decode_preserves_multibyte_target() {
190 let mut rng = test_rng();
191 let target = "café";
192
193 let frame = decode_frame(target, 3, &mut rng);
194
195 assert!(frame.starts_with("caf"));
196 assert_eq!(frame.chars().count(), 4);
197 }
198
199 #[test]
200 fn decode_multibyte_char_count_stable_but_byte_len_varies() {
201 let mut rng = test_rng();
202 let target = "café résumé";
203 let target_chars = target.chars().count();
204
205 for revealed in 0..=target_chars {
206 let frame = decode_frame(target, revealed, &mut rng);
207 assert_eq!(
208 frame.chars().count(),
209 target_chars,
210 "char count must equal target at revealed={revealed}"
211 );
212 }
213
214 let partial = decode_frame(target, 2, &mut test_rng());
215 assert_ne!(
216 partial.len(),
217 target.len(),
218 "byte length of partially-decoded multi-byte string should differ from target — \
219 centering must use chars().count(), not len()"
220 );
221 }
222
223 #[test]
224 fn decode_text_garbles_then_reveals() {
225 let mut rng = test_rng();
226 let target = "hello";
227 let garble_ticks = 5;
228
229 let early = decode_text(target, 2, garble_ticks, &mut rng);
230 assert!(!early.is_empty());
231 assert_ne!(early, "hello");
232
233 let revealed = decode_text(target, garble_ticks + 10, garble_ticks, &mut rng);
234 assert_eq!(revealed, "hello");
235 }
236}