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}