Skip to main content

safe_decode/
rot13.rs

1//! ROT13: rotate ASCII letters by thirteen. Nothing else moves.
2
3use alloc::string::String;
4
5/// Rotate every ASCII letter of `s` by thirteen positions, wrapping within its own case.
6///
7/// Non-letters — digits, punctuation, whitespace, and every non-ASCII character — pass
8/// through unchanged. ROT13 is its own inverse, so encoding and decoding are one function.
9///
10/// ```
11/// use safe_decode::rot13;
12/// assert_eq!(rot13("Hello, World!"), "Uryyb, Jbeyq!");
13/// assert_eq!(rot13(&rot13("round trip")), "round trip");
14/// ```
15#[must_use]
16pub fn rot13(s: &str) -> String {
17    s.chars()
18        .map(|c| match c {
19            'A'..='Z' => rotate(c, b'A'),
20            'a'..='z' => rotate(c, b'a'),
21            other => other,
22        })
23        .collect()
24}
25
26/// Rotate one ASCII letter thirteen places within the 26-letter run starting at `base`.
27///
28/// Only reachable from the two matched ASCII ranges above, so `c as u8` is exact and the
29/// arithmetic stays inside the run.
30fn rotate(c: char, base: u8) -> char {
31    let offset = (c as u8).wrapping_sub(base);
32    char::from(base + (offset + 13) % 26)
33}
34
35#[cfg(test)]
36mod tests {
37    use super::rot13;
38
39    #[test]
40    fn rotates_both_cases() {
41        assert_eq!(rot13("Hello, World!"), "Uryyb, Jbeyq!");
42    }
43
44    #[test]
45    fn is_its_own_inverse() {
46        let plain = "The quick brown fox jumps over the lazy dog.";
47        assert_eq!(rot13(&rot13(plain)), plain);
48    }
49
50    #[test]
51    fn wraps_at_the_alphabet_boundary() {
52        // The halves either side of the wrap: A..M shift up, N..Z wrap down.
53        assert_eq!(rot13("ABMNYZ"), "NOZALM");
54        assert_eq!(rot13("abmnyz"), "nozalm");
55    }
56
57    #[test]
58    fn leaves_non_letters_alone() {
59        assert_eq!(rot13("0123456789"), "0123456789");
60        assert_eq!(rot13(" \t\n{}[]|\\/@#"), " \t\n{}[]|\\/@#");
61    }
62
63    #[test]
64    fn leaves_non_ascii_alone() {
65        // Accented Latin, CJK and emoji are not ASCII letters and must not rotate.
66        assert_eq!(rot13("café 日本語 🦀"), "pnsé 日本語 🦀");
67    }
68
69    #[test]
70    fn empty_input_yields_empty_output() {
71        assert_eq!(rot13(""), "");
72    }
73
74    #[test]
75    fn decodes_a_real_userassist_value_name() {
76        // UserAssist stores its value names ROT13'd; this is the shape they arrive in.
77        assert_eq!(
78            rot13(r"{P:\Jvaqbjf\flfgrz32\pzq.rkr"),
79            r"{C:\Windows\system32\cmd.exe"
80        );
81    }
82}