boba/lib.rs
1#![warn(clippy::all)]
2#![warn(clippy::pedantic)]
3#![allow(clippy::cast_possible_truncation)]
4#![warn(clippy::cargo)]
5#![allow(unknown_lints)]
6#![warn(missing_copy_implementations)]
7#![warn(missing_docs)]
8#![warn(missing_debug_implementations)]
9#![warn(rust_2018_idioms)]
10#![warn(rust_2021_compatibility)]
11#![warn(rust_2024_compatibility)]
12#![warn(trivial_casts, trivial_numeric_casts)]
13#![warn(unused_qualifications)]
14#![warn(variant_size_differences)]
15#![forbid(unsafe_code)]
16// Enable feature callouts in generated documentation:
17// https://doc.rust-lang.org/beta/unstable-book/language-features/doc-cfg.html
18//
19// This approach is borrowed from tokio.
20#![cfg_attr(docsrs, feature(doc_cfg))]
21#![cfg_attr(docsrs, feature(doc_alias))]
22
23//! This crate provides an implementation of a Bubble Babble encoder and
24//! decoder.
25//!
26//! The Bubble Babble encoding uses alternation of consonants and vowels to
27//! encode binary data to pseudowords that can be pronounced more easily than
28//! arbitrary lists of hexadecimal digits.
29//!
30//! Bubble Babble is part of the Digest libraries in [Perl][perl-bubblebabble]
31//! and [Ruby][ruby-bubblebabble].
32//!
33//! [perl-bubblebabble]: https://metacpan.org/pod/Digest::BubbleBabble
34//! [ruby-bubblebabble]: https://ruby-doc.org/stdlib-3.1.1/libdoc/digest/rdoc/Digest.html#method-c-bubblebabble
35//!
36//! # Usage
37//!
38//! You can encode binary data by calling [`encode`](encode()):
39//!
40//! ```
41//! let encoded = boba::encode("Pineapple");
42//! assert_eq!(encoded, "xigak-nyryk-humil-bosek-sonax");
43//! ```
44//!
45//! Decoding binary data is done by calling [`decode`](decode()):
46//!
47//! ```
48//! # use boba::DecodeError;
49//! # fn example() -> Result<(), DecodeError> {
50//! let decoded = boba::decode("xexax")?;
51//! assert_eq!(decoded, vec![]);
52//! # Ok(())
53//! # }
54//! # example().unwrap();
55//! ```
56//!
57//! Decoding data is fallible and can return [`DecodeError`]. For example, all
58//! Bubble Babble–encoded data has an ASCII alphabet, so attempting to decode an
59//! emoji will fail.
60//!
61//! ```
62//! # use boba::DecodeError;
63//! let decoded = boba::decode("x🦀x");
64//! // The `DecodeError` contains the offset of the first invalid byte.
65//! assert_eq!(decoded, Err(DecodeError::InvalidByte(1)));
66//! ```
67//!
68//! # Crate Features
69//!
70//! Boba is `no_std` compatible with a required dependency on the [`alloc`]
71//! crate.
72
73#![no_std]
74#![doc(html_root_url = "https://docs.rs/boba/6.0.0")]
75
76extern crate alloc;
77
78use alloc::string::String;
79use alloc::vec::Vec;
80use core::fmt;
81
82mod decode;
83mod encode;
84
85/// Decoding errors from [`boba::decode`](decode()).
86///
87/// `decode` will return a `DecodeError` if:
88///
89/// - The input is not an ASCII string.
90/// - The input contains an ASCII character outside of the Bubble Babble
91/// encoding alphabet.
92/// - The input does not start with a leading `x`.
93/// - The input does not end with a trailing `x`.
94/// - The decoded result does not checksum properly.
95///
96/// # Examples
97///
98/// ```
99/// # use boba::DecodeError;
100/// assert_eq!(boba::decode("x💎🦀x"), Err(DecodeError::InvalidByte(1)));
101/// assert_eq!(boba::decode("x789x"), Err(DecodeError::InvalidByte(1)));
102/// assert_eq!(boba::decode("yx"), Err(DecodeError::MalformedHeader));
103/// assert_eq!(boba::decode("xy"), Err(DecodeError::MalformedTrailer));
104/// assert_eq!(boba::decode(""), Err(DecodeError::Corrupted));
105/// assert_eq!(boba::decode("z"), Err(DecodeError::Corrupted));
106/// assert_eq!(boba::decode("xx"), Err(DecodeError::Corrupted));
107/// ```
108#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
109pub enum DecodeError {
110 /// Checksum mismatch when decoding input.
111 ChecksumMismatch,
112 /// Corrupted input caused a decoding failure.
113 Corrupted,
114 /// Expected to process a consonant from the encoding alphabet, but got
115 /// something else.
116 ExpectedConsonant,
117 /// Expected to process a vowel from the encoding alphabet, but got
118 /// something else.
119 ExpectedVowel,
120 /// Input contained a byte not in the encoding alphabet at this position.
121 InvalidByte(usize),
122 /// Input was missing a leading `x` header.
123 MalformedHeader,
124 /// Input was missing a final `x` trailer.
125 MalformedTrailer,
126}
127
128impl core::error::Error for DecodeError {}
129
130impl fmt::Display for DecodeError {
131 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132 match self {
133 Self::ChecksumMismatch => f.write_str("Checksum mismatch"),
134 Self::Corrupted => f.write_str("Corrupted input"),
135 Self::ExpectedConsonant => f.write_str("Expected consonant, got something else"),
136 Self::ExpectedVowel => f.write_str("Expected vowel, got something else"),
137 Self::InvalidByte(pos) => write!(
138 f,
139 "Encountered byte outside of encoding alphabet at position {pos}"
140 ),
141 Self::MalformedHeader => f.write_str("Missing required 'x' header"),
142 Self::MalformedTrailer => f.write_str("Missing required 'x' trailer"),
143 }
144 }
145}
146
147/// Encode a byte slice with the Bubble Babble encoding to a [`String`].
148///
149/// # Examples
150///
151/// ```
152/// assert_eq!(boba::encode([]), "xexax");
153/// assert_eq!(boba::encode("1234567890"), "xesef-disof-gytuf-katof-movif-baxux");
154/// assert_eq!(boba::encode("Pineapple"), "xigak-nyryk-humil-bosek-sonax");
155/// ```
156#[must_use]
157pub fn encode<T: AsRef<[u8]>>(data: T) -> String {
158 encode::inner(data.as_ref())
159}
160
161/// Decode Bubble Babble-encoded byte slice to a [`Vec<u8>`](Vec).
162///
163/// # Examples
164///
165/// ```
166/// # use boba::DecodeError;
167/// # fn example() -> Result<(), DecodeError> {
168/// assert_eq!(boba::decode("xexax")?, vec![]);
169/// assert_eq!(boba::decode("xesef-disof-gytuf-katof-movif-baxux")?, b"1234567890");
170/// assert_eq!(boba::decode("xigak-nyryk-humil-bosek-sonax")?, b"Pineapple");
171/// # Ok(())
172/// # }
173/// # example().unwrap();
174/// ```
175///
176/// # Errors
177///
178/// Decoding is fallible and might return [`DecodeError`] if:
179///
180/// - The input is not an ASCII string.
181/// - The input contains an ASCII character outside of the Bubble Babble
182/// encoding alphabet.
183/// - The input does not start with a leading `x`.
184/// - The input does not end with a trailing `x`.
185/// - The decoded result does not checksum properly.
186///
187/// ```
188/// # use boba::DecodeError;
189/// assert_eq!(boba::decode("x💎🦀x"), Err(DecodeError::InvalidByte(1)));
190/// assert_eq!(boba::decode("x789x"), Err(DecodeError::InvalidByte(1)));
191/// assert_eq!(boba::decode("yx"), Err(DecodeError::MalformedHeader));
192/// assert_eq!(boba::decode("xy"), Err(DecodeError::MalformedTrailer));
193/// assert_eq!(boba::decode(""), Err(DecodeError::Corrupted));
194/// assert_eq!(boba::decode("z"), Err(DecodeError::Corrupted));
195/// assert_eq!(boba::decode("xx"), Err(DecodeError::Corrupted));
196/// ```
197pub fn decode<T: AsRef<[u8]>>(encoded: T) -> Result<Vec<u8>, DecodeError> {
198 decode::inner(encoded.as_ref())
199}
200
201#[cfg(test)]
202#[allow(clippy::non_ascii_literal)]
203mod tests {
204 use alloc::string::String;
205 use alloc::vec;
206 use core::fmt::Write as _;
207
208 use crate::{decode, encode, DecodeError};
209
210 #[test]
211 fn encoder() {
212 assert_eq!(encode([]), "xexax");
213 assert_eq!(encode("1234567890"), "xesef-disof-gytuf-katof-movif-baxux");
214 assert_eq!(encode("Pineapple"), "xigak-nyryk-humil-bosek-sonax");
215
216 assert_eq!(
217 encode("💎🦀❤️✨💪"),
218 "xusan-zugom-vesin-zenom-bumun-tanav-zyvam-zomon-sapaz-bulin-dypux"
219 );
220
221 assert_eq!(encode("xyz!x6"), "xival-neved-cavuf-kexyx");
222 }
223
224 #[test]
225 fn decoder() {
226 assert_eq!(decode("xexax"), Ok(vec![]));
227 assert_eq!(
228 decode("xesef-disof-gytuf-katof-movif-baxux"),
229 Ok(b"1234567890".to_vec())
230 );
231 assert_eq!(
232 decode("xigak-nyryk-humil-bosek-sonax"),
233 Ok(b"Pineapple".to_vec())
234 );
235
236 assert_eq!(
237 decode("xusan-zugom-vesin-zenom-bumun-tanav-zyvam-zomon-sapaz-bulin-dypux"),
238 Ok(String::from("💎🦀❤️✨💪").into_bytes())
239 );
240
241 assert_eq!(decode("xival-neved-cavuf-kexyx"), Ok(b"xyz!x6".to_vec()));
242 }
243
244 #[test]
245 fn decode_error_sub_dash() {
246 assert_eq!(
247 decode("xesefxdisofxgytufxkatofxmovifxbaxux"),
248 Err(DecodeError::ChecksumMismatch)
249 );
250 }
251
252 #[test]
253 fn decode_sub_vowel_to_consonant() {
254 assert_eq!(
255 decode("xssef-disof-gytuf-katof-movif-baxux"),
256 Err(DecodeError::ExpectedVowel),
257 );
258 }
259
260 #[test]
261 fn decode_sub_consonant_to_vowel() {
262 assert_eq!(
263 decode("xeeef-disof-gytuf-katof-movif-baxux"),
264 Err(DecodeError::ExpectedConsonant)
265 );
266 }
267
268 #[test]
269 fn decode_error() {
270 assert_eq!(decode(""), Err(DecodeError::Corrupted));
271 assert_eq!(decode("z"), Err(DecodeError::Corrupted));
272 assert_eq!(decode("xy"), Err(DecodeError::MalformedTrailer));
273 assert_eq!(decode("yx"), Err(DecodeError::MalformedHeader));
274 assert_eq!(decode("xx"), Err(DecodeError::Corrupted));
275 assert_eq!(decode("x💎🦀x"), Err(DecodeError::InvalidByte(1)));
276 assert_eq!(decode("x789x"), Err(DecodeError::InvalidByte(1)));
277 }
278
279 #[test]
280 fn decode_error_bad_alphabet() {
281 assert_eq!(
282 decode("xigak-nyryk-/umil-bosek-sonax"),
283 Err(DecodeError::InvalidByte(12))
284 );
285 assert_eq!(decode(b"x\xFFx"), Err(DecodeError::InvalidByte(1)));
286 assert_eq!(
287 decode("xigak-nyryk-Humil-bosek-sonax"),
288 Err(DecodeError::InvalidByte(12))
289 );
290 assert_eq!(
291 decode("XIGAK-NYRYK-HUMIL-BOSEK-SONAX"),
292 Err(DecodeError::Corrupted)
293 );
294 assert_eq!(
295 decode("xIGAK-NYRYK-HUMIL-BOSEK-SONAX"),
296 Err(DecodeError::MalformedTrailer)
297 );
298 assert_eq!(
299 decode("xIGAK-NYRYK-HUMIL-BOSEK-SONAx"),
300 Err(DecodeError::InvalidByte(1))
301 );
302 }
303
304 #[test]
305 fn error_display_is_not_empty() {
306 let test_cases = [
307 DecodeError::ChecksumMismatch,
308 DecodeError::Corrupted,
309 DecodeError::ExpectedConsonant,
310 DecodeError::ExpectedVowel,
311 DecodeError::InvalidByte(0),
312 DecodeError::InvalidByte(123),
313 DecodeError::MalformedHeader,
314 DecodeError::MalformedTrailer,
315 ];
316 for tc in test_cases {
317 let mut buf = String::new();
318 write!(&mut buf, "{tc}").unwrap();
319 assert!(!buf.is_empty());
320 }
321 }
322
323 #[test]
324 fn test_inner_triggers_decode_3_tuple_corrupted() {
325 // This encoded input is designed to trigger a DecodeError::Corrupted.
326 // It consists of a header and trailer ('x') framing the 6-byte chunk
327 // "abab-b". Within the chunk, the first three-tuple decodes to a value
328 // where the computed 'high' component is 5 (>= 4), which violates the
329 // valid range and causes an error.
330 let encoded = b"xabab-bx";
331 let result = decode(encoded);
332 assert_eq!(result, Err(DecodeError::Corrupted));
333 }
334}
335
336// Ensure code blocks in `README.md` compile.
337//
338// This module and macro declaration should be kept at the end of the file, in
339// order to not interfere with code coverage.
340#[cfg(doctest)]
341#[doc = include_str!("../README.md")]
342mod readme {}