From c5aab67c39dc64b9b6c5c01c7d768e88037e4e5a Mon Sep 17 00:00:00 2001 From: Ali Alimohammadi Date: Wed, 7 Jan 2026 11:02:56 -0800 Subject: [PATCH 1/2] feat: add base32 cipher implementation --- DIRECTORY.md | 1 + src/ciphers/base32.rs | 275 ++++++++++++++++++++++++++++++++++++++++++ src/ciphers/mod.rs | 2 + 3 files changed, 278 insertions(+) create mode 100644 src/ciphers/base32.rs diff --git a/DIRECTORY.md b/DIRECTORY.md index 6dd938b44df..51e5182de79 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -38,6 +38,7 @@ * [Affine Cipher](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/affine_cipher.rs) * [Another ROT13](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/another_rot13.rs) * [Baconian Cipher](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/baconian_cipher.rs) + * [Base32](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/base32.rs) * [Base64](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/base64.rs) * [Blake2B](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/blake2b.rs) * [Caesar](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/caesar.rs) diff --git a/src/ciphers/base32.rs b/src/ciphers/base32.rs new file mode 100644 index 00000000000..739e9b19fcf --- /dev/null +++ b/src/ciphers/base32.rs @@ -0,0 +1,275 @@ +//! Base32 encoding and decoding implementation. +//! +//! Base32 is a binary-to-text encoding scheme that represents binary data using 32 ASCII characters +//! (A-Z and 2-7). It's commonly used when case-insensitive encoding is needed or when avoiding +//! characters that might be confused (like 0/O or 1/l). +//! +//! This implementation follows the standard Base32 alphabet as defined in RFC 4648. + +const B32_CHARSET: &[u8; 32] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + +/// Encodes the given bytes into base32. +/// +/// The function converts binary data into base32 format using the standard alphabet. +/// Output is padded with '=' characters to make the length a multiple of 8. +/// +/// # Arguments +/// +/// * `data` - A byte slice to encode +/// +/// # Returns +/// +/// A `Vec` containing the base32-encoded data with padding. +/// +/// # Examples +/// +/// ``` +/// use the_algorithms_rust::ciphers::base32_encode; +/// assert_eq!(base32_encode(b"Hello World!"), b"JBSWY3DPEBLW64TMMQQQ===="); +/// assert_eq!(base32_encode(b"123456"), b"GEZDGNBVGY======"); +/// assert_eq!(base32_encode(b"some long complex string"), b"ONXW2ZJANRXW4ZZAMNXW24DMMV4CA43UOJUW4ZY="); +/// ``` +pub fn base32_encode(data: &[u8]) -> Vec { + if data.is_empty() { + return Vec::new(); + } + + // Convert bytes to binary string representation + use std::fmt::Write; + let mut binary_data = String::with_capacity(data.len() * 8); + for byte in data { + write!(binary_data, "{byte:08b}").unwrap(); + } + + // Pad binary data to be a multiple of 5 bits + let padding_needed = (5 - (binary_data.len() % 5)) % 5; + for _ in 0..padding_needed { + binary_data.push('0'); + } + + // Convert 5-bit chunks to base32 characters + let mut result = Vec::new(); + for chunk in binary_data.as_bytes().chunks(5) { + let chunk_str = std::str::from_utf8(chunk).unwrap(); + let index = usize::from_str_radix(chunk_str, 2).unwrap(); + result.push(B32_CHARSET[index]); + } + + // Pad result to be a multiple of 8 characters + while !result.len().is_multiple_of(8) { + result.push(b'='); + } + + result +} + +/// Decodes base32-encoded data into bytes. +/// +/// The function decodes base32 format back to binary data, removing padding characters. +/// +/// # Arguments +/// +/// * `data` - A byte slice containing base32-encoded data +/// +/// # Returns +/// +/// * `Ok(Vec)` - Successfully decoded bytes +/// * `Err(String)` - Error message if the input is invalid +/// +/// # Errors +/// +/// Returns an error if: +/// - The input contains invalid base32 characters +/// - The input cannot be properly decoded +/// +/// # Examples +/// +/// ``` +/// use the_algorithms_rust::ciphers::base32_decode; +/// assert_eq!(base32_decode(b"JBSWY3DPEBLW64TMMQQQ====").unwrap(), b"Hello World!"); +/// assert_eq!(base32_decode(b"GEZDGNBVGY======").unwrap(), b"123456"); +/// assert_eq!(base32_decode(b"ONXW2ZJANRXW4ZZAMNXW24DMMV4CA43UOJUW4ZY=").unwrap(), b"some long complex string"); +/// ``` +pub fn base32_decode(data: &[u8]) -> Result, String> { + if data.is_empty() { + return Ok(Vec::new()); + } + + // Remove padding and convert to string + let data_str = + std::str::from_utf8(data).map_err(|_| "Invalid UTF-8 in base32 data".to_string())?; + let data_stripped = data_str.trim_end_matches('='); + + // Convert base32 characters to binary string + use std::fmt::Write; + let mut binary_chunks = String::with_capacity(data_stripped.len() * 5); + for ch in data_stripped.chars() { + // Find the index of this character in the charset + let index = B32_CHARSET + .iter() + .position(|&c| c == ch as u8) + .ok_or_else(|| format!("Invalid base32 character: {ch}"))?; + + // Convert index to 5-bit binary string + write!(binary_chunks, "{index:05b}").unwrap(); + } + + // Convert 8-bit chunks back to bytes + let mut result = Vec::new(); + for chunk in binary_chunks.as_bytes().chunks(8) { + if chunk.len() == 8 { + let chunk_str = std::str::from_utf8(chunk).unwrap(); + let byte_value = u8::from_str_radix(chunk_str, 2) + .map_err(|_| "Failed to parse binary chunk".to_string())?; + result.push(byte_value); + } + } + + Ok(result) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_encode_hello_world() { + assert_eq!(base32_encode(b"Hello World!"), b"JBSWY3DPEBLW64TMMQQQ===="); + } + + #[test] + fn test_encode_numbers() { + assert_eq!(base32_encode(b"123456"), b"GEZDGNBVGY======"); + } + + #[test] + fn test_encode_long_string() { + assert_eq!( + base32_encode(b"some long complex string"), + b"ONXW2ZJANRXW4ZZAMNXW24DMMV4CA43UOJUW4ZY=" + ); + } + + #[test] + fn test_encode_empty() { + assert_eq!(base32_encode(b""), b""); + } + + #[test] + fn test_encode_single_char() { + assert_eq!(base32_encode(b"A"), b"IE======"); + } + + #[test] + fn test_decode_hello_world() { + assert_eq!( + base32_decode(b"JBSWY3DPEBLW64TMMQQQ====").unwrap(), + b"Hello World!" + ); + } + + #[test] + fn test_decode_numbers() { + assert_eq!(base32_decode(b"GEZDGNBVGY======").unwrap(), b"123456"); + } + + #[test] + fn test_decode_long_string() { + assert_eq!( + base32_decode(b"ONXW2ZJANRXW4ZZAMNXW24DMMV4CA43UOJUW4ZY=").unwrap(), + b"some long complex string" + ); + } + + #[test] + fn test_decode_empty() { + assert_eq!(base32_decode(b"").unwrap(), b""); + } + + #[test] + fn test_decode_single_char() { + assert_eq!(base32_decode(b"IE======").unwrap(), b"A"); + } + + #[test] + fn test_decode_without_padding() { + assert_eq!( + base32_decode(b"JBSWY3DPEBLW64TMMQQQ").unwrap(), + b"Hello World!" + ); + } + + #[test] + fn test_decode_invalid_character() { + let result = base32_decode(b"INVALID!@#$"); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("Invalid base32 character")); + } + + #[test] + fn test_roundtrip_hello() { + let original = b"Hello"; + let encoded = base32_encode(original); + let decoded = base32_decode(&encoded).unwrap(); + assert_eq!(decoded, original); + } + + #[test] + fn test_roundtrip_various_strings() { + let test_cases = vec![ + b"a" as &[u8], + b"ab", + b"abc", + b"abcd", + b"abcde", + b"The quick brown fox jumps over the lazy dog", + b"1234567890", + b"!@#$%^&*()", + ]; + + for original in test_cases { + let encoded = base32_encode(original); + let decoded = base32_decode(&encoded).unwrap(); + assert_eq!(decoded, original, "Failed for: {:?}", original); + } + } + + #[test] + fn test_all_charset_characters() { + // Test that all characters in the charset can be encoded/decoded + for i in 0..32 { + let data = vec![i * 8]; // Arbitrary byte values + let encoded = base32_encode(&data); + let decoded = base32_decode(&encoded).unwrap(); + assert_eq!(decoded, data); + } + } + + #[test] + fn test_binary_data() { + let binary_data = vec![0x00, 0x01, 0x02, 0xFF, 0xFE, 0xFD]; + let encoded = base32_encode(&binary_data); + let decoded = base32_decode(&encoded).unwrap(); + assert_eq!(decoded, binary_data); + } + + #[test] + fn test_padding_variations() { + // Test different amounts of padding + let test_cases: Vec<(&[u8], &[u8])> = vec![ + (b"f", b"MY======"), + (b"fo", b"MZXQ===="), + (b"foo", b"MZXW6==="), + (b"foob", b"MZXW6YQ="), + (b"fooba", b"MZXW6YTB"), + (b"foobar", b"MZXW6YTBOI======"), + ]; + + for (input, expected) in test_cases { + let encoded = base32_encode(input); + assert_eq!(encoded, expected, "Encoding failed for: {:?}", input); + let decoded = base32_decode(&encoded).unwrap(); + assert_eq!(decoded, input, "Roundtrip failed for: {:?}", input); + } + } +} diff --git a/src/ciphers/mod.rs b/src/ciphers/mod.rs index 985876c7a58..829242c6ba4 100644 --- a/src/ciphers/mod.rs +++ b/src/ciphers/mod.rs @@ -2,6 +2,7 @@ mod aes; mod affine_cipher; mod another_rot13; mod baconian_cipher; +mod base32; mod base64; mod blake2b; mod caesar; @@ -28,6 +29,7 @@ pub use self::aes::{aes_decrypt, aes_encrypt, AesKey}; pub use self::affine_cipher::{affine_decrypt, affine_encrypt, affine_generate_key}; pub use self::another_rot13::another_rot13; pub use self::baconian_cipher::{baconian_decode, baconian_encode}; +pub use self::base32::{base32_decode, base32_encode}; pub use self::base64::{base64_decode, base64_encode}; pub use self::blake2b::blake2b; pub use self::caesar::caesar; From 12e6da1686b78a2ae9802713c88a2f35261b2481 Mon Sep 17 00:00:00 2001 From: Ali Alimohammadi <41567902+AliAlimohammadi@users.noreply.github.com> Date: Wed, 7 Jan 2026 11:09:10 -0800 Subject: [PATCH 2/2] Update base32.rs --- src/ciphers/base32.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ciphers/base32.rs b/src/ciphers/base32.rs index 739e9b19fcf..4a6b4b3da02 100644 --- a/src/ciphers/base32.rs +++ b/src/ciphers/base32.rs @@ -230,7 +230,7 @@ mod tests { for original in test_cases { let encoded = base32_encode(original); let decoded = base32_decode(&encoded).unwrap(); - assert_eq!(decoded, original, "Failed for: {:?}", original); + assert_eq!(decoded, original, "Failed for: {original:?}"); } } @@ -267,9 +267,9 @@ mod tests { for (input, expected) in test_cases { let encoded = base32_encode(input); - assert_eq!(encoded, expected, "Encoding failed for: {:?}", input); + assert_eq!(encoded, expected, "Encoding failed for: {input:?}"); let decoded = base32_decode(&encoded).unwrap(); - assert_eq!(decoded, input, "Roundtrip failed for: {:?}", input); + assert_eq!(decoded, input, "Roundtrip failed for: {input:?}"); } } }