use crate::error::{Base38DecodeError, Result};
const CODES: [char; 38] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I',
'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '-', '.',
];
const RADIX: u64 = CODES.len() as u64;
const BASE38_CHARS_NEEDED_IN_CHUNK: [usize; 3] = [2, 4, 5];
const MAX_BYTES_IN_CHUNK: usize = 3;
const MAX_ENCODED_CHARS_IN_CHUNK: usize = 5;
pub fn encode(bytes: &[u8]) -> String {
let mut qrcode = String::new();
for chunk in bytes.chunks(MAX_BYTES_IN_CHUNK) {
let mut value = chunk
.iter()
.enumerate()
.fold(0u64, |acc, (i, &byte)| acc | ((byte as u64) << (i * 8)));
let chars_needed = BASE38_CHARS_NEEDED_IN_CHUNK[chunk.len() - 1];
for _ in 0..chars_needed {
let remainder = (value % RADIX) as usize;
qrcode.push(CODES[remainder]);
value /= RADIX;
}
}
qrcode
}
pub fn decode(s: &str) -> Result<Vec<u8>> {
let mut decoded_bytes = Vec::new();
let chars: Vec<char> = s.chars().collect();
for chunk in chars.chunks(MAX_ENCODED_CHARS_IN_CHUNK) {
let value = chunk.iter().rev().try_fold(0u64, |acc, &c| {
CODES
.iter()
.position(|&code| code == c)
.map(|val| acc * RADIX + val as u64)
.ok_or(Base38DecodeError::InvalidCharacter(c))
})?;
let bytes_in_chunk = match chunk.len() {
2 => 1,
4 => 2,
5 => 3,
len => return Err(Base38DecodeError::InvalidChunkLength(len).into()),
};
let max_value = 1u64 << (8 * bytes_in_chunk);
if value >= max_value {
return Err(Base38DecodeError::ValueOutOfRange {
value,
digits: chunk.len(),
expected_bytes: bytes_in_chunk,
}
.into());
}
let mut temp_value = value;
for _ in 0..bytes_in_chunk {
decoded_bytes.push((temp_value & 0xFF) as u8);
temp_value >>= 8;
}
}
Ok(decoded_bytes)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::MatterPayloadError;
use crate::error::Base38DecodeError;
#[test]
fn test_round_trip() {
let original_data = b"Hello, Matter!".to_vec();
let encoded = encode(&original_data);
let decoded = decode(&encoded).expect("Decoding failed");
assert_eq!(original_data, decoded);
}
#[test]
fn test_chunk_boundaries() {
let inputs: Vec<Vec<u8>> = vec![
vec![1],
vec![1, 2],
vec![1, 2, 3],
vec![1, 2, 3, 4],
vec![1, 2, 3, 4, 5],
vec![1, 2, 3, 4, 5, 6],
vec![],
];
for input in inputs {
let encoded = encode(&input);
let decoded = decode(&encoded).unwrap();
assert_eq!(input, decoded, "Round trip failed for input: {:?}", input);
}
}
#[test]
fn test_decode_invalid_character() {
let result = decode("ABC@123");
let expected_error = MatterPayloadError::Base38(Base38DecodeError::InvalidCharacter('@'));
assert_eq!(result.unwrap_err(), expected_error);
}
#[test]
fn test_decode_invalid_length() {
let result = decode("ABC");
let expected_error = MatterPayloadError::Base38(Base38DecodeError::InvalidChunkLength(3));
assert_eq!(result.unwrap_err(), expected_error);
}
#[test]
fn test_decode_value_out_of_range() {
let invalid_input = "ZZZZZ";
let result = decode(invalid_input);
assert!(matches!(
result,
Err(MatterPayloadError::Base38(
Base38DecodeError::ValueOutOfRange { .. }
))
));
}
#[test]
fn test_edge_cases() {
let edge_cases = vec![
vec![0x00; 100],
vec![0xFF; 100],
(0..=255).collect(),
];
for case in edge_cases {
let encoded = encode(&case);
let decoded = decode(&encoded).expect("Decoding failed");
assert_eq!(case, decoded, "Edge case failed");
}
}
}