Skip to main content

rune_atbash/
lib.rs

1//! Atbash cipher — reverse-alphabet substitution for Latin letters.
2//!
3//! Atbash maps each letter to its mirror image in the alphabet: A↔Z, B↔Y, C↔X,
4//! and so on. It originated as a Hebrew cipher and is commonly encountered in CTF
5//! challenges, historical puzzles, and cryptography courses. This implementation
6//! handles Latin (ASCII) letters only, preserving case. Non-letter bytes pass
7//! through unchanged.
8//!
9//! Because the mapping is its own inverse (mirroring twice is a no-op), encoding
10//! and decoding are the same operation. The library has zero dependencies.
11//!
12//! # Features
13//!
14//! - [`atbash`] — applies Atbash to a string; non-letter characters are unchanged.
15//! - [`atbash_bytes`] — applies Atbash to a raw byte slice.
16//!
17//! # Quick Start
18//!
19//! ```rust
20//! use rune_atbash::atbash;
21//!
22//! let encoded = atbash("Hello, World!");
23//! assert_eq!(encoded, "Svool, Dliow!");
24//!
25//! let decoded = atbash(&encoded);
26//! assert_eq!(decoded, "Hello, World!");
27//! ```
28//!
29//! # CLI
30//!
31//! ```bash
32//! rune-atbash "Hello, World!"
33//! echo "Hello, World!" | rune-atbash
34//! rune-atbash -f message.txt
35//! ```
36
37/// Applies the Atbash cipher to a UTF-8 string, substituting only ASCII letters.
38///
39/// Each uppercase letter maps to its mirror: `A`→`Z`, `B`→`Y`, …, `Z`→`A`.
40/// Lowercase letters mirror within their own range: `a`→`z`, `b`→`y`, …, `z`→`a`.
41/// Case is preserved. All other bytes pass through unchanged. Applying this
42/// function twice to the same input returns the original string.
43///
44/// # Examples
45///
46/// ```rust
47/// use rune_atbash::atbash;
48///
49/// assert_eq!(atbash("Hello, World!"), "Svool, Dliow!");
50/// assert_eq!(atbash("Svool, Dliow!"), "Hello, World!");
51/// assert_eq!(atbash("abcxyz"), "zyxcba");
52/// assert_eq!(atbash("ABCXYZ"), "ZYXCBA");
53/// assert_eq!(atbash("123 !@#"), "123 !@#");
54/// assert_eq!(atbash(""), "");
55/// ```
56pub fn atbash(text: &str) -> String {
57    String::from_utf8(atbash_bytes(text.as_bytes()))
58        .expect("atbash preserves UTF-8 validity: only ASCII bytes are modified")
59}
60
61/// Applies the Atbash cipher to a raw byte slice, substituting only ASCII letters.
62///
63/// Suitable for binary data where only the letter bytes should be substituted.
64/// Non-letter bytes are passed through unchanged. Applying this function twice
65/// to the same input returns the original bytes.
66///
67/// # Examples
68///
69/// ```rust
70/// use rune_atbash::atbash_bytes;
71///
72/// assert_eq!(atbash_bytes(b"Hello!"), b"Svool!");
73/// assert_eq!(atbash_bytes(b"Svool!"), b"Hello!");
74/// assert_eq!(atbash_bytes(b"\x00\xff"), b"\x00\xff");
75/// assert_eq!(atbash_bytes(b""), b"");
76/// ```
77pub fn atbash_bytes(bytes: &[u8]) -> Vec<u8> {
78    bytes.iter().copied().map(mirror_byte).collect()
79}
80
81fn mirror_byte(byte: u8) -> u8 {
82    match byte {
83        b'a'..=b'z' => b'a' + b'z' - byte,
84        b'A'..=b'Z' => b'A' + b'Z' - byte,
85        _ => byte,
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn atbash_empty() {
95        assert_eq!(atbash(""), "");
96    }
97
98    #[test]
99    fn atbash_lowercase_full_alphabet() {
100        assert_eq!(
101            atbash("abcdefghijklmnopqrstuvwxyz"),
102            "zyxwvutsrqponmlkjihgfedcba"
103        );
104    }
105
106    #[test]
107    fn atbash_uppercase_full_alphabet() {
108        assert_eq!(
109            atbash("ABCDEFGHIJKLMNOPQRSTUVWXYZ"),
110            "ZYXWVUTSRQPONMLKJIHGFEDCBA"
111        );
112    }
113
114    #[test]
115    fn atbash_known_vector() {
116        assert_eq!(atbash("Hello, World!"), "Svool, Dliow!");
117    }
118
119    #[test]
120    fn atbash_case_preserved() {
121        assert_eq!(atbash("aAbBzZ"), "zZyYaA");
122    }
123
124    #[test]
125    fn atbash_non_letters_unchanged() {
126        assert_eq!(atbash("123 !@# \t\n"), "123 !@# \t\n");
127    }
128
129    #[test]
130    fn atbash_idempotent() {
131        let inputs = ["Hello, World!", "The quick brown fox", "abc XYZ 123 !"];
132        for input in inputs {
133            assert_eq!(atbash(&atbash(input)), input);
134        }
135    }
136
137    #[test]
138    fn atbash_bytes_empty() {
139        assert_eq!(atbash_bytes(b""), b"");
140    }
141
142    #[test]
143    fn atbash_bytes_non_ascii_unchanged() {
144        assert_eq!(atbash_bytes(b"\x00\x80\xff"), b"\x00\x80\xff");
145    }
146
147    #[test]
148    fn atbash_bytes_idempotent() {
149        let original = b"Attack at dawn! \x00\xff";
150        assert_eq!(atbash_bytes(&atbash_bytes(original)), original);
151    }
152
153    #[test]
154    fn atbash_roundtrip() {
155        let plaintext = "The quick brown fox jumps over the lazy dog.";
156        assert_eq!(atbash(&atbash(plaintext)), plaintext);
157    }
158
159    #[test]
160    fn atbash_boundary_letters() {
161        assert_eq!(atbash("azAZ"), "zaZA");
162    }
163}