base122-rs 0.1.0

High-performance Base122 encoding/decoding library with bitwise operations
Documentation
//! # Base122 Encoding Library
//!
//! A high-performance Base122 encoding/decoding library for Rust, based on the original
//! [kevinAlbs Base122 algorithm](https://github.com/kevinAlbs/Base122).
//!
//! Base122 is a binary-to-text encoding that is approximately 14% more space-efficient 
//! than Base64, making it ideal for data URIs and other space-constrained applications.
//!
//! ## Algorithm Overview
//!
//! Base122 uses a bitwise approach:
//! - Extracts 7-bit chunks from input data
//! - Maps safe characters directly to single bytes
//! - Encodes "dangerous characters" using UTF-8 multi-byte sequences
//! - Achieves ~87% compression efficiency (7 bits input per 8 bits output)
//!
//! ## Dangerous Characters
//!
//! Six characters are considered "dangerous" for transmission and are specially encoded:
//! - `\0` (null) - can truncate strings
//! - `\n` (newline) - breaks single-line formats  
//! - `\r` (carriage return) - breaks single-line formats
//! - `"` (double quote) - conflicts with JSON/HTML attributes
//! - `&` (ampersand) - conflicts with HTML entities
//! - `\` (backslash) - conflicts with escape sequences
//!
//! ## Performance
//!
//! - **Theoretical efficiency**: 87.5% (7 bits input / 8 bits output)
//! - **Actual efficiency**: ~87% for large data
//! - **vs Base64**: ~14% smaller output
//! - **Dependencies**: Zero - pure Rust implementation
//!
//! ## Examples
//!
//! ```rust
//! use base122::{encode, decode};
//!
//! // Basic encoding/decoding
//! let data = b"Hello, World!";
//! let encoded = encode(data);
//! let decoded = decode(&encoded).unwrap();
//! assert_eq!(data, &decoded[..]);
//!
//! // Binary data with dangerous characters
//! let binary_data = vec![0, 10, 13, 34, 38, 92, 65, 66, 67];
//! let encoded = encode(&binary_data);
//! let decoded = decode(&encoded).unwrap();
//! assert_eq!(binary_data, decoded);
//! ```

#![deny(missing_docs)]
#![deny(unsafe_code)]

/// The six "dangerous" characters that require special UTF-8 encoding.
/// 
/// These characters can cause issues in transmission or parsing and are
/// encoded using 2-byte UTF-8 sequences instead of single bytes.
const ILLEGALS: [u8; 6] = [
    0,   // null - can truncate strings
    10,  // newline - breaks single-line transmission
    13,  // carriage return - breaks single-line transmission
    34,  // double quote - breaks JSON/HTML attributes
    38,  // ampersand - conflicts with HTML entities
    92,  // backslash - conflicts with escape sequences
];

/// Marker value used in UTF-8 encoding to indicate shortened sequences.
const SHORTENED: u8 = 0b111;

/// Encodes binary data using the Base122 algorithm.
///
/// This function implements the kevinAlbs Base122 algorithm using bitwise operations
/// for maximum efficiency. It extracts 7-bit chunks from the input data and encodes
/// them as either single bytes (safe characters) or UTF-8 multi-byte sequences 
/// (dangerous characters).
///
/// # Algorithm Details
///
/// 1. **Bit Extraction**: Extracts exactly 7 bits at a time from input data,
///    handling byte boundaries correctly
/// 2. **Dangerous Character Detection**: Checks if the 7-bit value matches any
///    of the six dangerous characters
/// 3. **Encoding Strategy**:
///    - Safe characters: Direct single-byte output
///    - Dangerous characters: 2-byte UTF-8 encoding that includes the next 7-bit chunk
/// 4. **UTF-8 Format**: Uses `110xxxxx 10yyyyyy` format for dangerous characters
///
/// # Performance
///
/// - **Time Complexity**: O(n) where n is input length
/// - **Space Complexity**: O(m) where m is output length (~1.14n)
/// - **Efficiency**: ~87% compression ratio
///
/// # Arguments
///
/// * `data` - Input byte slice to encode
///
/// # Returns
///
/// A `String` containing the Base122-encoded data as valid UTF-8.
/// Returns an empty string if input is empty.
///
/// # Examples
///
/// ```rust
/// use base122::encode;
///
/// // Simple text
/// let encoded = encode(b"Hello");
/// assert!(!encoded.is_empty());
///
/// // Binary data with dangerous characters
/// let binary = vec![0, 10, 13, 255]; // null, newline, CR, high byte
/// let encoded = encode(&binary);
/// assert!(!encoded.is_empty());
/// ```
pub fn encode(data: &[u8]) -> String {
    if data.is_empty() {
        return String::new();
    }
    
    let mut cur_index = 0;
    let mut cur_bit = 0; 
    let mut result = Vec::new();
    
    // Core bit extraction function - extracts exactly 7 bits from input stream
    let mut get7 = || -> Option<u8> {
        if cur_index >= data.len() {
            return None;
        }
        
        // Extract bits from current byte
        let first_byte = data[cur_index];
        let first_part = ((0b11111110 >> cur_bit) & first_byte) << cur_bit;
        let first_part = first_part >> 1; // Align to 7-bit boundary
        
        // Update bit position
        cur_bit += 7;
        if cur_bit < 8 {
            return Some(first_part);
        }
        
        // Need bits from next byte
        cur_bit -= 8;
        cur_index += 1;
        
        if cur_index >= data.len() {
            return Some(first_part);
        }
        
        // Extract and combine bits from next byte
        let second_byte = data[cur_index] as u16;
        let mut second_part = ((0xFF00u16 >> cur_bit) & second_byte) & 0xFF;
        if cur_bit < 8 {
            second_part >>= 8 - cur_bit;
        }
        let second_part = second_part as u8;
        
        Some(first_part | second_part)
    };
    
    // Main encoding loop
    while let Some(bits) = get7() {
        
        // Check if this is a dangerous character
        if let Some(illegal_index) = ILLEGALS.iter().position(|&x| x == bits) {
            // Dangerous character: encode as UTF-8 multi-byte sequence
            let next_bits = get7();
            
            // UTF-8 two-byte format: 110xxxxx 10yyyyyy
            let mut b1 = 0b11000010; // First byte prefix
            let mut b2 = 0b10000000; // Second byte prefix
            
            if next_bits.is_none() {
                // Last 7 bits are dangerous - use shortened marker
                b1 |= (SHORTENED & 0b111) << 2;
                let final_bits = bits;
                
                // Encode the 7 bits across the UTF-8 sequence
                let first_bit = if (final_bits & 0b01000000) > 0 { 1 } else { 0 };
                b1 |= first_bit;
                b2 |= final_bits & 0b00111111;
            } else {
                let next_bits = next_bits.unwrap();
                b1 |= ((illegal_index as u8) & 0b111) << 2;
                
                // Encode the next 7 bits across the UTF-8 sequence  
                let first_bit = if (next_bits & 0b01000000) > 0 { 1 } else { 0 };
                b1 |= first_bit;
                b2 |= next_bits & 0b00111111;
            }
            
            result.push(b1);
            result.push(b2);
        } else {
            // Safe character: direct single-byte output
            result.push(bits);
        }
    }
    
    // Convert result to UTF-8 string (always valid due to our encoding)
    String::from_utf8(result).unwrap_or_else(|_| String::new())
}

/// Decodes Base122-encoded data back to the original binary data.
///
/// This function reverses the Base122 encoding process by parsing the UTF-8
/// input and reconstructing the original bit stream using the `push7` accumulator
/// pattern from the original kevinAlbs implementation.
///
/// # Algorithm Details
///
/// 1. **Character Processing**: Iterates through UTF-8 characters in the input
/// 2. **UTF-8 Decoding**: Detects and processes multi-byte UTF-8 sequences
/// 3. **Bit Accumulation**: Uses a 7-bit accumulator to reconstruct bytes
/// 4. **Dangerous Character Handling**: Extracts illegal character indices and data
///
/// # Arguments
///
/// * `encoded` - Base122-encoded string to decode
///
/// # Returns
///
/// * `Ok(Vec<u8>)` - Successfully decoded binary data
/// * `Err(String)` - Error message if decoding fails
///
/// # Errors
///
/// This function returns an error if:
/// - The input contains invalid UTF-8 characters
/// - Multi-byte UTF-8 sequences are malformed
/// - The encoded data is corrupted
///
/// # Examples
///
/// ```rust
/// use base122::{encode, decode};
///
/// let original = b"Test data with\0dangerous\ncharacters";
/// let encoded = encode(original);
/// let decoded = decode(&encoded).unwrap();
/// assert_eq!(original, &decoded[..]);
/// ```
pub fn decode(encoded: &str) -> Result<Vec<u8>, String> {
    if encoded.is_empty() {
        return Ok(Vec::new());
    }
    
    let mut decoded = Vec::new();
    let mut cur_byte = 0u8;
    let mut bit_of_byte = 0;
    
    // Bit accumulator function - pushes 7 bits into the output stream
    let mut push7 = |byte: u8| {
        let byte = byte << 1; // Shift to make room for alignment
        
        // Accumulate bits into current output byte
        cur_byte |= byte >> bit_of_byte;
        bit_of_byte += 7;
        
        if bit_of_byte >= 8 {
            // Current byte is complete
            decoded.push(cur_byte);
            bit_of_byte -= 8;
            
            // Carry remaining bits to next byte
            cur_byte = byte << (7 - bit_of_byte);
        }
    };
    
    let chars: Vec<char> = encoded.chars().collect();
    let mut i = 0;
    
    while i < chars.len() {
        let c = chars[i] as u32;
        
        if c > 127 {
            // Multi-byte UTF-8 character (dangerous character encoding)
            let illegal_index = (c >> 8) & 7; // Extract illegal character index
            
            // Check for shortened sequence marker
            if illegal_index != SHORTENED as u32 {
                push7(ILLEGALS[illegal_index as usize]);
            }
            
            // Always push the remaining 7 bits
            push7((c & 127) as u8);
        } else {
            // Single-byte character (safe character)
            push7(c as u8);
        }
        i += 1;
    }
    
    Ok(decoded)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_empty_input() {
        assert_eq!(encode(&[]), "");
        assert_eq!(decode("").unwrap(), Vec::<u8>::new());
    }

    #[test]
    fn test_single_byte() {
        let data = [0x42];
        let encoded = encode(&data);
        let decoded = decode(&encoded).unwrap();
        assert_eq!(decoded, data);
    }

    #[test]
    fn test_dangerous_characters() {
        // Test each dangerous character individually
        for &dangerous_char in &ILLEGALS {
            let data = [dangerous_char];
            let encoded = encode(&data);
            let decoded = decode(&encoded).unwrap();
            assert_eq!(decoded, data, "Failed for dangerous character: {}", dangerous_char);
        }
    }

    #[test]
    fn test_mixed_data() {
        let data = b"Hello\nWorld\0Test\"Data&More\\Path";
        let encoded = encode(data);
        let decoded = decode(&encoded).unwrap();
        assert_eq!(decoded, data);
    }

    #[test]
    fn test_binary_data() {
        let data: Vec<u8> = (0..=255).collect();
        let encoded = encode(&data);
        let decoded = decode(&encoded).unwrap();
        assert_eq!(decoded, data);
    }

    #[test]
    fn test_round_trip_various_sizes() {
        let test_cases = vec![
            vec![],
            vec![0],
            vec![65], // 'A'
            b"Hello".to_vec(),
            b"Hello, World!".to_vec(),
            (0u8..100).collect(),
        ];

        for data in test_cases {
            let encoded = encode(&data);
            let decoded = decode(&encoded).unwrap();
            assert_eq!(decoded, data, "Round-trip failed for: {:?}", data);
        }
    }

    #[test]
    fn test_efficiency() {
        // Test that efficiency is within expected bounds
        let large_data: Vec<u8> = (0..1000).map(|i| (i % 256) as u8).collect();
        let encoded = encode(&large_data);
        
        let input_bits = large_data.len() * 8;
        let output_bits = encoded.as_bytes().len() * 8;
        let efficiency = input_bits as f64 / output_bits as f64;
        
        // Should be close to theoretical maximum of 87.5%
        assert!(efficiency > 0.85, "Efficiency too low: {:.1}%", efficiency * 100.0);
        assert!(efficiency <= 0.875, "Efficiency impossibly high: {:.1}%", efficiency * 100.0);
    }

    #[test]
    fn test_vs_base64_efficiency() {
        let test_data = b"The quick brown fox jumps over the lazy dog. This is a test of Base122 efficiency vs Base64.";
        let base122_encoded = encode(test_data);
        let base64_size = (test_data.len() * 4 + 2) / 3; // Base64 theoretical size
        
        // Base122 should be more efficient than Base64
        assert!(base122_encoded.as_bytes().len() < base64_size, 
               "Base122 ({} bytes) should be smaller than Base64 ({} bytes)", 
               base122_encoded.as_bytes().len(), base64_size);
    }

    #[test]
    fn test_decode_invalid_input() {
        // Test with invalid UTF-8 would be caught by Rust's string handling
        // Our decode function handles all valid UTF-8 strings gracefully
        assert!(decode("valid ascii").is_ok());
    }
}