base94/
lib.rs

1//! # Base94 Encoding Library
2//!
3//! This crate provides functions for encoding and decoding data using Base94 encoding.
4//! Base94 encoding is a method of converting binary data into a text-based format using a larger
5//! character set than Base64. The encoding and decoding functions in this crate allow you to
6//! convert data between its original binary form and a Base94-encoded string representation.
7//!
8//! ## Usage
9//!
10//! To use this library, you can include it as a dependency in your `Cargo.toml`:
11//!
12//! ```toml
13//! [dependencies]
14//! base94 = {current_version}
15//! ```
16//!
17//! Then, you can use the provided functions in your Rust code:
18//!
19//! ```rust
20//! use base94::{encode, decode};
21//!
22//! let data = b"Hello, World!";
23//! let base = 94;
24//! let encoded = encode(data, base);
25//! let decoded = decode(&encoded, base).unwrap();
26//!
27//! assert_eq!(decoded, data);
28//! ```
29//!
30//! ## Supported Bases
31//!
32//! The encoding and decoding functions support various bases within the range of 2 to 94.
33//! The specified base must be consistent between encoding and decoding operations.
34//!
35//! ## Examples
36//!
37//! ```
38//! use base94::{encode, decode};
39//!
40//! let data = b"Example data for encoding.";
41//! let base = 50;
42//!
43//! let encoded = encode(data, base);
44//! let decoded = decode(&encoded, base).unwrap();
45//!
46//! assert_eq!(decoded, data);
47//! ```
48//!
49
50use num::BigUint;
51use num::Integer;
52use num::ToPrimitive;
53use thiserror::Error;
54
55pub static CHARACTERS: &[u8; 94] = include_bytes!("characters.txt");
56
57/// Encodes a slice of bytes into a Base94-encoded string using the specified base.
58///
59/// Base94 encoding is a method of converting binary data into a text-based format using
60/// a larger character set than Base64. The provided base specifies the number of unique
61/// characters used in the encoding, ranging from 2 to 94. The encoded string can later be
62/// decoded back to the original data using the `decode` function.
63///
64/// # Arguments
65///
66/// * `data` - A slice of bytes representing the data to be encoded.
67/// * `base` - The base used for encoding. Must be between 2 and 94 (inclusive).
68///
69/// # Panics
70///
71/// This function panics if the specified base is outside the valid range (2 to 94).
72///
73/// # Returns
74///
75/// A Base94-encoded string representation of the input data.
76///
77/// # Examples
78///
79/// ```
80/// use base94::encode;
81///
82/// let data = b"Hello, World!";
83/// let base = 94;
84/// let encoded = encode(data, base);
85/// println!("Encoded: {}", encoded);
86/// ```
87pub fn encode(data: &[u8], base: u8) -> String {
88    assert!(base <= 94, "Base must be less than or equal to 94");
89    assert!(base >= 2, "Base must be greater than or equal to 2");
90
91    let mut num = BigUint::from_bytes_le(data);
92    let mut out = String::new();
93
94    while num > BigUint::from(0u8) {
95        let (div, rem) = num.div_rem(&BigUint::from(base));
96        num = div;
97        out.push(CHARACTERS[rem.to_usize().unwrap()] as char);
98    }
99
100    out
101}
102
103#[derive(Error, Debug)]
104pub enum DecodeError {
105    #[error("Invalid character '{c}' at position {position}")]
106    InvalidCharacter { c: u8, position: usize },
107}
108
109/// Decodes a Base94-encoded string back to its original byte representation using the specified base.
110///
111/// Base94 encoding is a method of converting binary data into a text-based format using
112/// a larger character set than Base64. The provided base specifies the number of unique
113/// characters used in the encoding, ranging from 2 to 94. The encoded string should have
114/// been generated using the `encode` function with the same base.
115///
116/// # Arguments
117///
118/// * `encoded` - A Base94-encoded string to be decoded.
119/// * `base` - The base used for decoding. Must match the base used for encoding.
120///
121/// # Returns
122///
123/// An optional vector of bytes representing the decoded original data. Returns `None`
124/// if the decoding process encounters invalid characters.
125///
126/// # Examples
127///
128/// ```
129/// use base94::decode;
130///
131/// let encoded = "A@#D9e@D9n9RRb6^";
132/// let base = 94;
133/// let decoded = decode(encoded, base).unwrap();
134/// println!("Decoded: {:?}", decoded);
135/// ```
136pub fn decode(encoded: &str, base: u8) -> Result<Vec<u8>, DecodeError> {
137    let mut num = BigUint::from(0u8);
138    let mut power = BigUint::from(1u8);
139
140    for (i, c) in encoded.chars().enumerate() {
141        let index =
142            CHARACTERS
143                .iter()
144                .position(|&x| x == c as u8)
145                .ok_or(DecodeError::InvalidCharacter {
146                    c: c as u8,
147                    position: i,
148                })?;
149        num += BigUint::from(index) * &power;
150        power *= BigUint::from(base);
151    }
152
153    let out = num.to_bytes_le();
154    Ok(out)
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    const MAX_BASE: u8 = 94;
162
163    #[test]
164    fn test_encode_decode_empty() {
165        for base in 2..=MAX_BASE {
166            let data = [0];
167            let encoded = encode(&data, base);
168            let decoded = decode(&encoded, base).unwrap();
169            assert_eq!(decoded, data);
170        }
171    }
172
173    #[test]
174    fn test_encode_decode_single_byte() {
175        for base in 2..=MAX_BASE {
176            let data = [65]; // ASCII value of 'A'
177            let encoded = encode(&data, base);
178            let decoded = decode(&encoded, base).unwrap();
179            assert_eq!(decoded, data);
180        }
181    }
182
183    #[test]
184    fn test_encode_decode_hello_world() {
185        for base in 2..=MAX_BASE {
186            let data = b"Hello, World!";
187            let encoded = encode(data, base);
188            let decoded = decode(&encoded, base).unwrap();
189            assert_eq!(decoded, data);
190        }
191    }
192
193    #[test]
194    fn test_encode_decode_max_value() {
195        for base in 2..=MAX_BASE {
196            let data = [255, 255, 255]; // Three bytes of value 255
197            let encoded = encode(&data, base);
198            let decoded = decode(&encoded, base).unwrap();
199            assert_eq!(decoded, data);
200        }
201    }
202
203    #[test]
204    fn test_encode_decode_large_data() {
205        for base in 2..=MAX_BASE {
206            let data = vec![42; 1000]; // 1000 bytes of value 42 ('*')
207            let encoded = encode(&data, base);
208            let decoded = decode(&encoded, base).unwrap();
209            assert_eq!(decoded, data);
210        }
211    }
212}