old_crypto_rs/helpers.rs
1//! A set of helpers functions.
2//!
3use std::collections::HashSet;
4
5use eyre::Result;
6
7/// Removes all duplicate characters from a string, preserving the first occurrence of each.
8///
9/// This function iterates through the input string and builds a new string containing only
10/// the first occurrence of each character, maintaining the original order. It uses a `HashSet`
11/// to track which characters have already been encountered.
12///
13/// # Algorithm
14///
15/// 1. Create an empty `HashSet` to track seen characters
16/// 2. Create an empty result `String`
17/// 3. For each character in the input:
18/// - If the character hasn't been seen (insert returns true), append it to the result
19/// - If the character has been seen (insert returns false), skip it
20/// 4. Return the result string
21///
22/// # Arguments
23///
24/// * `str` - A string slice to be condensed
25///
26/// # Returns
27///
28/// A new `String` with all duplicate characters removed, preserving the order of first occurrences
29///
30/// # Examples
31///
32/// ```
33/// use old_crypto_rs::helpers::condense;
34///
35/// // Simple duplicates
36/// let result = condense("AAAAA");
37/// assert_eq!(result, "A");
38///
39/// // Mixed duplicates
40/// let result = condense("ARABESQUE");
41/// assert_eq!(result, "ARBESQU");
42///
43/// // Longer string with duplicates
44/// let result = condense("ARABESQUEABCDEFGHIKLMNOPQRSTUVWXYZ");
45/// assert_eq!(result, "ARBESQUCDFGHIKLMNOPTVWXYZ");
46///
47/// // No duplicates
48/// let result = condense("ABCDE");
49/// assert_eq!(result, "ABCDE");
50/// ```
51///
52/// # Performance
53///
54/// Time complexity: O(n) where n is the length of the input string
55/// Space complexity: O(k) where k is the number of unique characters
56///
57/// # See Also
58///
59/// * [`condense_str`] - An optimized version using a bitset for ASCII characters
60///
61pub fn condense(str: &str) -> String {
62 let mut seen = HashSet::new();
63 let mut condensed = String::new();
64
65 for ch in str.chars() {
66 if seen.insert(ch) {
67 condensed.push(ch);
68 }
69 }
70 condensed
71}
72
73/// Efficiently removes all duplicate characters from a string, returning a new String.
74///
75/// This implementation uses a bitset for ASCII characters to achieve O(n) time complexity
76/// and minimal overhead.
77///
78pub fn condense_str(s: &str) -> String {
79 let mut seen_ascii = [false; 256];
80 let mut res = String::with_capacity(s.len());
81
82 for c in s.chars() {
83 if !seen_ascii[c as usize] {
84 seen_ascii[c as usize] = true;
85 res.push(c);
86 }
87 }
88 res
89}
90
91/// insert one character inside the array
92///
93pub fn insert(src: &[u8], obj: u8, ind: usize) -> Vec<u8> {
94 let mut dst = Vec::with_capacity(src.len() + 1);
95 dst.extend_from_slice(&src[..ind]);
96 dst.push(obj);
97 dst.extend_from_slice(&src[ind..]);
98 dst.to_vec()
99}
100
101/// Expands a byte slice by inserting 'X' between consecutive duplicate characters.
102///
103/// This function processes the input in pairs (digrams), inserting the byte `b'X'`
104/// between any two consecutive identical characters. This is commonly used in
105/// classical ciphers like Playfair to ensure all digrams consist of different characters.
106///
107/// # Algorithm
108///
109/// 1. Copy the source slice into a mutable vector
110/// 2. Iterate through the vector in steps of 2 (processing digrams)
111/// 3. If two consecutive characters are identical, insert `b'X'` between them
112/// 4. Continue processing, which causes the inserted 'X' to become part of the next digram
113///
114/// # Arguments
115///
116/// * `src` - A byte slice to be expanded
117///
118/// # Returns
119///
120/// A new `Vec<u8>` with 'X' characters inserted between consecutive duplicates
121///
122/// # Examples
123///
124/// ```
125/// use old_crypto_rs::helpers::expand;
126///
127/// // Simple duplicate
128/// let input = b"AAA";
129/// let result = expand(input);
130/// assert_eq!(result, b"AXAXA");
131///
132/// // No duplicates
133/// let input = b"ARABESQUE";
134/// let result = expand(input);
135/// assert_eq!(result, b"ARABESQUE");
136///
137/// // Multiple consecutive duplicates
138/// let input = b"AAAA";
139/// let result = expand(input);
140/// assert_eq!(result, b"AXAXAXA");
141///
142/// // Duplicates in different positions
143/// let input = b"LANNONCE";
144/// let result = expand(input);
145/// assert_eq!(result, b"LANXNONCE");
146/// ```
147///
148/// # Performance
149///
150/// Time complexity: O(n) where n is the length of the input, though insertions may cause
151/// reallocation in worst case scenarios with many consecutive duplicates.
152/// Space complexity: O(n + k) where k is the number of 'X' characters inserted.
153///
154/// # See Also
155///
156/// * [`insert`] - The helper function used to insert characters
157///
158pub fn expand(src: &[u8]) -> Vec<u8> {
159 let mut res = src.to_vec();
160 let mut i = 0;
161 while i < res.len().saturating_sub(1) {
162 if res[i] == res[i + 1] {
163 res = insert(&res, b'X', i + 1);
164 }
165 i += 2;
166 }
167 res
168}
169
170/// The 26-letter alphabet we know and love in the Western part of the world
171pub const REGULAR_ALPHABET: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
172
173/// Default alphabet containing A-Z plus special characters '/' and '-'.
174/// The '/' character is used as a digit escape marker in encryption.
175pub const SC_ALPHABET: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ/-";
176
177/*
178 # Form an alphabet formed with a keyword, re-shuffle everything to
179 # make it less predictable (i.e. checkerboard effect)
180 #
181 # Shuffle the alphabet a bit to avoid sequential allocation of the
182 # code numbers. This is actually performing a transposition with the word
183 # itself as key.
184 #
185 # Regular rectangle
186 # -----------------
187 # Key is ARABESQUE condensed into ARBESQU (len = 7) (height = 4)
188 # Let word be ARBESQUCDFGHIJKLMNOPTVWXYZ/-
189 #
190 # First passes will generate
191 #
192 # A RBESQUCDFGHIJKLMNOPTVWXYZ/- c=0 0 x 6
193 # AC RBESQUDFGHIJKLMNOPTVWXYZ/- c=6 1 x 6
194 # ACK RBESQUDFGHIJLMNOPTVWXYZ/- c=12 2 x 6
195 # ACKV RBESQUDFGHIJLMNOPTWXYZ/- c=18 3 x 6
196 # ACKVR BESQUDFGHIJLMNOPTWXYZ/- c=0 0 x 5
197 # ACKVRD BESQUFGHIJLMNOPTWXYZ/- c=5 1 x 5
198 # ...
199 # ACKVRDLWBFMXEGNYSHOZQIP/UJT-
200 #
201 # Irregular rectangle
202 # -------------------
203 # Key is SUBWAY condensed info SUBWAY (len = 6) (height = 5)
204 #
205 # S UBWAYCDEFGHIJKLMNOPQRTVXZ/- c=0 0 x 5
206 # SC UBWAYDEFGHIJKLMNOPQRTVXZ/- c=5 1 x 5
207 # SCI UBWAYDEFGHJKLMNOPQRTVXZ/- c=10 2 x 5
208 # SCIO UBWAYDEFGHJKLMNPQRTVXZ/- c=15 3 x 5
209 # SCIOX UBWAYDEFGHJKLMNPQRTVZ/- c=20 4 x 5
210 # SCIOXU BWAYDEFGHJKLMNPQRTVZ/- c=0 0 x 4
211 # ...
212 # SCIOXUDJPZBEKQ/WFLR-AG YHMNTV c=1 1 x 1
213 # SCIOXUDJPZBEKQ/WFLR-AGM YHNTV c=2 2 x 1
214 # SCIOXUDJPZBEKQ/WFLR-AGMT YHNV c=3 3 x 1
215 # SCIOXUDJPZBEKQ/WFLR-AGMTYHNV
216*/
217
218/// Shuffles an alphabet using a keyword to create a mixed alphabet for cipher use.
219///
220/// # Algorithm
221///
222/// 1. Condense the concatenation of `key` and `alphabet` to remove duplicates
223/// 2. Calculate dimensions: `length` = condensed key length, `height` = alphabet length / length
224/// 3. Extract characters in a specific pattern based on these dimensions, working backwards
225/// through columns and forwards through rows
226///
227/// # Arguments
228///
229/// * `key` - The keyword used to shuffle the alphabet (will be condensed to remove duplicates)
230/// * `alphabet` - The alphabet string to be shuffled
231///
232/// # Returns
233///
234/// A new `String` containing the shuffled alphabet
235///
236/// # Examples
237///
238/// Regular rectangle (key length divides alphabet evenly):
239/// ```
240/// use old_crypto_rs::helpers::shuffle;
241///
242/// let key = "ARABESQUE"; // Condenses to "ARBESQU" (length = 7)
243/// let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ/-"; // 28 chars, height = 4
244/// let result = shuffle(key, alphabet);
245/// assert_eq!(result, "ACKVRDLWBFMXEGNYSHOZQIP/UJT-");
246/// ```
247///
248/// Irregular rectangle (key length does not divide alphabet evenly):
249/// ```
250/// use old_crypto_rs::helpers::shuffle;
251///
252/// let key = "SUBWAY"; // Condenses to "SUBWAY" (length = 6)
253/// let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ/-"; // 28 chars, height = 5
254/// let result = shuffle(key, alphabet);
255/// assert_eq!(result, "SCIOXUDJPZBEKQ/WFLR-AGMTYHNV");
256/// ```
257///
258/// # Performance
259///
260/// Time complexity: O(n * m) where n is the key length and m is the height
261/// Space complexity: O(n + m) for the working vector and result string
262///
263pub fn shuffle(key: &str, alphabet: &str) -> String {
264 let mut word = Vec::with_capacity(key.len() + alphabet.len());
265 let mut seen = [false; 256];
266 let mut length = 0;
267 for c in key.chars() {
268 if (c as usize) < 256 && !seen[c as usize] {
269 seen[c as usize] = true;
270 word.push(c as u8);
271 length += 1;
272 }
273 }
274 if length == 0 {
275 return alphabet.to_string();
276 }
277
278 for c in alphabet.chars() {
279 if (c as usize) < 256 && !seen[c as usize] {
280 seen[c as usize] = true;
281 word.push(c as u8);
282 }
283 }
284
285 let mut height = alphabet.len() / length;
286 if alphabet.len() % length != 0 {
287 height += 1;
288 }
289
290 let mut res = String::with_capacity(word.len());
291 for i in (0..length).rev() {
292 for j in 0..=height {
293 if word.len() <= height.saturating_sub(1) {
294 res.push_str(std::str::from_utf8(&word).unwrap());
295 return res;
296 } else {
297 if i * j < word.len() {
298 let c = word.remove(i * j);
299 res.push(c as char);
300 }
301 }
302 }
303 }
304 res
305}
306
307/// Shuffles an alphabet using a keyword to create a mixed alphabet for cipher use.
308///
309/// BTW: the transposition comment about `shuffle` was coming from the Go version...
310/// and is wrong.
311///
312/// The main issue with plain `shuffle()` is that the first letter in the final
313/// alphabet is always the same as the key. This version does not have this problem.
314///
315/// This optimized version performs transposition directly without creating intermediate
316/// objects, making it as fast as the plain shuffle, if not faster.
317///
318/// inlining FTW :)
319///
320pub fn transp_shuffle(key: &str, alphabet: &str) -> Result<String> {
321 // Short-circuit if key is empty
322 //
323 if key.is_empty() {
324 return Ok(alphabet.to_string());
325 }
326
327 // Condense key inline using bitset (same as shuffle())
328 //
329 let mut seen = [false; 256];
330 let mut condensed_key = Vec::with_capacity(key.len());
331 for c in key.chars() {
332 if (c as usize) < 256 && !seen[c as usize] {
333 seen[c as usize] = true;
334 condensed_key.push(c as u8);
335 }
336 }
337
338 // Compute numeric key inline (to_numeric equivalent)
339 //
340 let klen = condensed_key.len();
341 let mut indexed: Vec<(usize, u8)> = condensed_key.iter().enumerate().map(|(i, &b)| (i, b)).collect();
342 indexed.sort_unstable_by_key(|&(_, b)| b);
343
344 let mut tkey = vec![0u8; klen];
345 for (rank, (original_idx, _)) in indexed.into_iter().enumerate() {
346 tkey[original_idx] = rank as u8;
347 }
348
349 // Precompute column positions to avoid repeated searches
350 //
351 let mut col_positions = vec![0usize; klen];
352 for i in 0..klen {
353 col_positions[i] = tkey.iter().position(|&x| x == i as u8).unwrap();
354 }
355
356 // Perform transposition directly into String
357 //
358 let src = alphabet.as_bytes();
359 let mut result = Vec::with_capacity(alphabet.len());
360
361 // Build the result
362 //
363 for &j in &col_positions {
364 let mut curr = j;
365 while curr < src.len() {
366 result.push(src[curr]);
367 curr += klen;
368 }
369 }
370
371 Ok(String::from_utf8(result)?)
372}
373
374/// Converts a string key into a numeric representation based on alphabetical order.
375///
376/// This function is commonly used to derive numeric keys for transposition ciphers.
377/// It assigns a rank to each character in the key based on its Unicode scalar value.
378/// If characters are identical, their original positions determine their relative rank.
379///
380/// # Arguments
381///
382/// * `key` - The string slice to be converted.
383///
384/// # Returns
385///
386/// A `Vec<u8>` where each element corresponds to the alphabetical rank (0-indexed)
387/// of the character at that position in the original string.
388///
389/// # Examples
390///
391/// ```
392/// use old_crypto_rs::helpers::to_numeric;
393///
394/// let key = "ZEBRAS";
395/// let numeric = to_numeric(key);
396/// // A=0, B=1, E=2, R=3, S=4, Z=5
397/// // Z (5), E (2), B (1), R (3), A (0), S (4)
398/// assert_eq!(numeric, vec![5, 2, 1, 3, 0, 4]);
399/// ```
400///
401pub fn to_numeric(key: &str) -> Vec<u8> {
402 let letters = key.as_bytes();
403 let mut indexed: Vec<(usize, u8)> = letters.iter().enumerate().map(|(i, &b)| (i, b)).collect();
404 indexed.sort_by_key(|&(_, b)| b);
405
406 let mut ar = vec![0u8; letters.len()];
407 for (rank, (original_idx, _)) in indexed.into_iter().enumerate() {
408 ar[original_idx] = rank as u8;
409 }
410 ar
411}
412
413/// Inserts a space every `n` characters in a string.
414///
415/// This is typically used to format ciphertext into readable blocks.
416///
417/// # Arguments
418///
419/// * `ct` - The input string slice.
420/// * `n` - The number of characters in each block.
421///
422/// # Returns
423///
424/// A new `String` with spaces inserted every `n` characters.
425///
426/// # Examples
427///
428/// ```
429/// use old_crypto_rs::helpers::by_n;
430///
431/// let input = "ABCDEFGHIJ";
432/// let result = by_n(input, 5);
433/// assert_eq!(result, "ABCDE FGHIJ");
434/// ```
435///
436pub fn by_n(ct: &str, n: usize) -> String {
437 let mut out = String::new();
438 let mut count = 0;
439 for ch in ct.chars() {
440 if count > 0 && count % n == 0 {
441 out.push(' ');
442 }
443 out.push(ch);
444 count += 1;
445 }
446 out
447}
448
449/// Formats a string into blocks of 5 characters separated by spaces.
450///
451/// This function takes a string slice and returns a new `String` where a space
452/// is inserted every 5 characters.
453///
454/// # Arguments
455///
456/// * `input` - A string slice to be formatted
457///
458/// # Returns
459///
460/// A new `String` with spaces inserted every 5 characters.
461pub fn output_as_block(input: &str) -> String {
462 by_n(input, 5)
463}
464
465/// Replaces consecutive identical characters by inserting a fill character between them.
466///
467/// For example, "AA" becomes "AQA" if the fill character is 'Q'. This is often used
468/// in classical ciphers to handle double letters.
469///
470/// >NOTE: this function works regardless of alignment; that is, it does not check if the double
471/// letters are on a 2-character boundary. Therefore, it is useless for `Playfair`, or any
472/// bi-grammatic ciphers.
473///
474/// e.g.
475/// HELLOWORLD -> HE LX LO WO RL DX
476/// but also
477/// CETOOTEST -> CE TO XO TE ST
478///
479/// # Arguments
480///
481/// * `str` - The input string to process.
482/// * `fill` - The character to insert between identical consecutive characters.
483///
484/// # Returns
485///
486/// A new `String` with the fill character inserted where necessary.
487///
488pub fn fix_double(str: &str, fill: char) -> String {
489 let mut fixed = String::with_capacity(str.len());
490 let mut chars = str.chars();
491
492 if let Some(first) = chars.next() {
493 fixed.push(first);
494 let mut prev = first;
495 for ch in chars {
496 if ch == prev {
497 fixed.push(fill);
498 }
499 fixed.push(ch);
500 prev = ch;
501 }
502 }
503
504 fixed
505}
506
507/// Replaces consecutive identical characters by inserting a fill character between them,
508/// but only when the duplicate pair falls on a 2-character boundary.
509///
510/// This function is specifically designed for bi-grammatic ciphers like Playfair, where
511/// doubles must be fixed only if they would form a single digram (2-character block).
512///
513/// Unlike `fix_double`, this function respects alignment. It processes the string in
514/// 2-character chunks and only inserts the fill character when both characters in a
515/// digram are identical.
516///
517/// e.g.
518/// HELLOWORLD -> HE LX LO WO RL DX (LL becomes LX, DD becomes DX)
519/// CETOOTEST -> CE TO OT ES TX (OO is on boundary, so no change needed as they're in different digrams)
520///
521/// # Arguments
522///
523/// * `str` - The input string to process.
524/// * `fill` - The character to insert between identical characters in the same digram.
525///
526/// # Returns
527///
528/// A new `String` with the fill character inserted where necessary, respecting 2-character boundaries.
529///
530pub fn fix_double_aligned(str: &str, fill: char) -> String {
531 let chars: Vec<char> = str.chars().collect();
532 let mut fixed = String::with_capacity(str.len());
533 let mut i = 0;
534
535 while i < chars.len() {
536 if i + 1 < chars.len() && chars[i] == chars[i + 1] {
537 // Double on boundary - insert fill character
538 fixed.push(chars[i]);
539 fixed.push(fill);
540 i += 1; // Move forward by 1, so the second character becomes part of next digram
541 } else if i + 1 < chars.len() {
542 // Normal digram - add both characters
543 fixed.push(chars[i]);
544 fixed.push(chars[i + 1]);
545 i += 2;
546 } else {
547 // Last character (odd length)
548 fixed.push(chars[i]);
549 i += 1;
550 }
551 }
552
553 fixed
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559 use rstest::*;
560
561 #[rstest]
562 #[case("ABCDE", "ABCDE")]
563 #[case("AAAAA", "A")]
564 #[case("ARABESQUE", "ARBESQU")]
565 #[case("ARABESQUEABCDEFGHIKLMNOPQRSTUVWXYZ", "ARBESQUCDFGHIKLMNOPTVWXYZ")]
566 #[case("PLAYFAIRABCDEFGHIKLMNOPQRSTUVWXYZ", "PLAYFIRBCDEGHKMNOQSTUVWXZ")]
567 #[case("PLAYFAIREXMABCDEFGHIKLMNOPQRSTUVWXYZ", "PLAYFIREXMBCDGHKNOQSTUVWZ")]
568 fn test_condense(#[case] in_str: &str, #[case] expected: &str) {
569 assert_eq!(condense(in_str), expected);
570 }
571
572 #[rstest]
573 #[case("AAA", "AXAXA")]
574 #[case("AAAA", "AXAXAXA")]
575 #[case("AAABRAACADAABRA", "AXAXABRAACADAXABRA")]
576 #[case("ARABESQUE", "ARABESQUE")]
577 #[case("LANNONCE", "LANXNONCE")]
578 #[case("PJRJJJJJJS", "PJRJJXJXJXJXJS")]
579 #[case("ABCDEFGHJJKLM", "ABCDEFGHJXJKLM")]
580 fn test_expand_insert(#[case] in_str: &str, #[case] expected: &str) {
581 assert_eq!(expand(in_str.as_bytes()), expected.as_bytes());
582 }
583
584 #[test]
585 fn test_insert() {
586 let a = [0, 1, 2, 3];
587 let b = [0, 1, 42, 2, 3];
588 assert_eq!(insert(&a, 42, 2), b);
589 }
590
591 #[rstest]
592 #[case("ARABESQUE", "ACKVRDLWBFMXEGNYSHOZQIP/UJT-")]
593 #[case("SUBWAY", "SCIOXUDJPZBEKQ/WFLR-AGMTYHNV")]
594 fn test_shuffle(#[case] key: &str, #[case] expected: &str) {
595 let res = shuffle(key, SC_ALPHABET);
596 assert_eq!(res, expected);
597 }
598
599 #[rstest]
600 #[case("ARABESQUE", "AHOVCJQXDKRYFMT/BIPWELSZGNU-")]
601 #[case("SUBWAY", "EKQWCIOU/AGMSYBHNTZDJPV-FLRX")]
602 fn test_transp_shuffle(#[case] key: &str, #[case] expected: &str) {
603 let res = transp_shuffle(key, SC_ALPHABET);
604 assert!(res.is_ok());
605 let res = res.unwrap();
606 assert_eq!(res, expected);
607 }
608
609 #[rstest]
610 #[case("ARABESQUE", vec![0, 6, 1, 2, 3, 7, 5, 8, 4])]
611 #[case("PJRJJJJJJS", vec![7, 0, 8, 1, 2, 3, 4, 5, 6, 9])]
612 #[case("AAABRAACADAABRA", vec![0, 1, 2, 9, 13, 3, 4, 11, 5, 12, 6, 7, 10, 14, 8])]
613 fn test_to_numeric(#[case] key: &str, #[case] expected: Vec<u8>) {
614 assert_eq!(to_numeric(key), expected);
615 }
616
617 #[rstest]
618 #[case(5, "ARABESQUE", "ARABE SQUE")]
619 #[case(4, "PJRJJJJJJS", "PJRJ JJJJ JS")]
620 #[case(5, "AAABRAACADAABRA", "AAABR AACAD AABRA")]
621 fn test_by_n(#[case] n: usize, #[case] in_str: &str, #[case] expected: &str) {
622 assert_eq!(by_n(in_str, n), expected);
623 }
624
625 #[rstest]
626 #[case("ARABESQUE", "ARABE SQUE")]
627 #[case("PJRJJJJJJS", "PJRJJ JJJJS")]
628 #[case("AAABRAACADAABRA", "AAABR AACAD AABRA")]
629 #[case("ABCDE", "ABCDE")]
630 #[case("ABCDEF", "ABCDE F")]
631 fn test_output_as_block(#[case] in_str: &str, #[case] expected: &str) {
632 assert_eq!(output_as_block(in_str), expected);
633 }
634
635 #[rstest]
636 #[case('Q', "ABCDEF", "ABCDEF")]
637 #[case('Q', "AABCDE", "AQABCDE")]
638 #[case('Q', "AAAAA", "AQAQAQAQA")]
639 #[case('Q', "CETOOT", "CETOQOT")]
640 #[case('Q', "CETOOTESTCHIFFREAVEC", "CETOQOTESTCHIFQFREAVEC")]
641 #[case('X', "ABCDEF", "ABCDEF")]
642 #[case('X', "AABCDE", "AXABCDE")]
643 #[case('X', "AAAAA", "AXAXAXAXA")]
644 #[case('X', "CETOOT", "CETOXOT")]
645 #[case('X', "CETOOTESTCHIFFREAVEC", "CETOXOTESTCHIFXFREAVEC")]
646 fn test_fix_double(#[case] fill: char, #[case] in_str: &str, #[case] expected: &str) {
647 assert_eq!(fix_double(in_str, fill), expected);
648 }
649
650 #[rstest]
651 #[case('Q', "ABCDEF", "ABCDEF")]
652 #[case('Q', "AABCDE", "AQABCDE")]
653 #[case('Q', "AAAAA", "AQAQAQAQA")] // AA -> AQ A, AA -> AQ A, AA -> AQ A
654 #[case('Q', "CETOOT", "CETOOT")] // CE TO OT becomes CE TO QO T
655 #[case('Q', "HELLOWORLD", "HELQLOWORLD")] // LL on boundary becomes LQ
656 #[case('Q', "CETOOTEST", "CETOOTEST")] // OO is split across digrams (O|O), so no change
657 #[case('X', "ABCDEF", "ABCDEF")]
658 #[case('X', "AABCDE", "AXABCDE")]
659 #[case('X', "AAAAA", "AXAXAXAXA")]
660 #[case('X', "CETOOT", "CETOOT")]
661 #[case('X', "HELLOWORLD", "HELXLOWORLD")]
662 #[case('X', "CETOOTEST", "CETOOTEST")] // OO is split across digrams
663 fn test_fix_double_aligned(#[case] fill: char, #[case] in_str: &str, #[case] expected: &str) {
664 assert_eq!(fix_double_aligned(in_str, fill), expected);
665 }
666}