Something went wrong. Try again.
Identities for entities did.bot
agent llm did
Something went wrong. Try again.
6.5 kB · 178 lines
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179//! Base32 and base64, in the two exact dialects atproto uses.//!//! Both are here rather than taken from a crate because both are needed in//! their *strict* form, and a general-purpose codec is lenient by default: it//! accepts padding where the protocol forbids it, and it accepts a final//! character whose unused low bits are set, which decodes fine and re-encodes//! to a different string. Either would break the one property this whole//! crate exists to hold — that a value has exactly one encoding — and neither//! is something a caller can switch off after the fact.//!//! The two dialects://!//! * base32, lowercase, no padding, RFC 4648 alphabet. This is multibase `b`,//! which is the only string form atproto permits for a CID.//! * base64, standard alphabet (`+` and `/`, not the URL-safe pair), no//! padding. This is the `$bytes` representation in the JSON form of the//! data model.
/// Why a string is not valid in one of the two alphabets.#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]pub enum DecodeError { /// A character outside the alphabet, including `=`: padding is refused /// rather than tolerated, because the protocol specifies unpadded and a /// padded string is a second encoding of the same bytes. #[error("invalid character {0:?}")] InvalidCharacter(char), /// A length that no whole number of bytes can produce, such as a single /// leftover base64 character. The value is how many bits were left over. #[error("truncated: {0} leftover bits encode no whole byte")] Truncated(u32), /// The final character carries bits that the decoded bytes do not use. /// Decoding would silently discard them and re-encoding would produce a /// different string, so it is refused. #[error("the final character has {0} non-zero unused bits")] UnusedBits(u32),}
/// The lowercase RFC 4648 base32 alphabet, which is multibase `b`.const BASE32: &[u8; 32] = b"abcdefghijklmnopqrstuvwxyz234567";
/// The standard RFC 4648 base64 alphabet: `+` and `/`, not the URL-safe pair.const BASE64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
/// Decodes a bit-packed string in `alphabet`, `bits` bits per character.////// One routine covers both bases because they differ only in the alphabet and/// the group size. The two strictness rules — no padding, no set bits beyond/// the last whole byte — are the same rules in both.fn decode(text: &str, alphabet: &[u8], bits: u32) -> Result<Vec<u8>, DecodeError> { let mut out = Vec::with_capacity(text.len() * bits as usize / 8); let mut buffer: u32 = 0; let mut held: u32 = 0; for ch in text.chars() { let value = alphabet .iter() .position(|c| char::from(*c) == ch) .ok_or(DecodeError::InvalidCharacter(ch))?; buffer = (buffer << bits) | value as u32; held += bits; if held >= 8 { held -= 8; out.push((buffer >> held) as u8); } } // What is left in `buffer` is the tail of the last character. It must be // zero, and there must not be enough of it to have been a whole byte. if held >= bits { return Err(DecodeError::Truncated(held)); } let leftover = buffer & ((1 << held) - 1); if leftover != 0 { return Err(DecodeError::UnusedBits(leftover.count_ones())); } Ok(out)}
/// Encodes bytes bit-packed into `alphabet`, `bits` bits per character.fn encode(bytes: &[u8], alphabet: &[u8], bits: u32) -> String { let mut out = String::with_capacity(bytes.len() * 8 / bits as usize + 1); let mut buffer: u32 = 0; let mut held: u32 = 0; for byte in bytes { buffer = (buffer << 8) | u32::from(*byte); held += 8; while held >= bits { held -= bits; out.push(alphabet[((buffer >> held) & ((1 << bits) - 1)) as usize] as char); } } if held > 0 { let index = (buffer << (bits - held)) & ((1 << bits) - 1); out.push(alphabet[index as usize] as char); } out}
/// Decodes lowercase unpadded base32.pub fn base32_decode(text: &str) -> Result<Vec<u8>, DecodeError> { decode(text, BASE32, 5)}
/// Encodes lowercase unpadded base32.pub fn base32_encode(bytes: &[u8]) -> String { encode(bytes, BASE32, 5)}
/// Decodes standard unpadded base64.pub fn base64_decode(text: &str) -> Result<Vec<u8>, DecodeError> { decode(text, BASE64, 6)}
/// Encodes standard unpadded base64.pub fn base64_encode(bytes: &[u8]) -> String { encode(bytes, BASE64, 6)}
#[cfg(test)]mod tests { use super::*;
#[test] fn base64_round_trips_every_tail_length() { for len in 0..8usize { let bytes: Vec<u8> = (0..len) .map(|i| (i as u8).wrapping_mul(37).wrapping_add(9)) .collect(); let text = base64_encode(&bytes); assert!(!text.contains('='), "{text:?} is padded"); assert_eq!(base64_decode(&text), Ok(bytes)); } }
#[test] fn base32_round_trips_every_tail_length() { for len in 0..8usize { let bytes: Vec<u8> = (0..len) .map(|i| (i as u8).wrapping_mul(53).wrapping_add(3)) .collect(); let text = base32_encode(&bytes); assert_eq!(base32_decode(&text), Ok(bytes)); } }
#[test] fn padding_is_a_character_outside_the_alphabet() { assert_eq!( base64_decode("YWJjZA=="), Err(DecodeError::InvalidCharacter('=')) ); }
#[test] fn a_set_bit_beyond_the_last_byte_is_refused() { // "QQ" decodes to one byte and leaves four bits; "QR" sets one of // them. A lenient decoder returns the same byte for both, which is // two strings for one value. assert_eq!(base64_decode("QQ"), Ok(vec![0x41])); assert_eq!(base64_decode("QR"), Err(DecodeError::UnusedBits(1))); }
#[test] fn a_lone_trailing_character_encodes_no_byte() { assert_eq!(base64_decode("QQQQQ"), Err(DecodeError::Truncated(6))); }
#[test] fn the_url_safe_alphabet_is_not_the_one_atproto_uses() { assert_eq!(base64_decode("-_"), Err(DecodeError::InvalidCharacter('-'))); }
#[test] fn uppercase_base32_is_a_different_multibase() { assert_eq!( base32_decode("MFRGG"), Err(DecodeError::InvalidCharacter('M')) ); }}