#![warn(missing_docs)]
mod bootstring;
mod decode;
mod encode;
pub use decode::decode;
pub use encode::{encode, is_xid_identifier};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DecodeError {
NotEncoded,
InvalidDigit(char),
UnexpectedEnd,
InvalidCodepoint(u32),
Overflow,
}
impl std::fmt::Display for DecodeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DecodeError::NotEncoded => write!(f, "input is not a namecode-encoded string"),
DecodeError::InvalidDigit(c) => write!(f, "invalid digit in encoded portion: '{}'", c),
DecodeError::UnexpectedEnd => write!(f, "encoded data ended unexpectedly"),
DecodeError::InvalidCodepoint(cp) => write!(f, "invalid Unicode codepoint: {}", cp),
DecodeError::Overflow => write!(f, "overflow during decoding"),
}
}
}
impl std::error::Error for DecodeError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_valid_xid_ascii() {
assert_eq!(encode("foo"), "foo");
assert_eq!(encode("bar123"), "bar123");
assert_eq!(encode("_private"), "_private");
assert_eq!(encode("CamelCase"), "CamelCase");
}
#[test]
fn test_encode_valid_xid_unicode() {
assert_eq!(encode("café"), "café");
assert_eq!(encode("名前"), "名前");
assert_eq!(encode("привет"), "привет");
}
#[test]
fn test_encode_empty() {
assert_eq!(encode(""), "");
}
#[test]
fn test_encode_with_space() {
let encoded = encode("hello world");
assert!(encoded.starts_with("_N_"));
}
#[test]
fn test_encode_with_hyphen() {
let encoded = encode("foo-bar");
assert!(encoded.starts_with("_N_"));
}
#[test]
fn test_encode_with_multiple_non_basic() {
let encoded = encode("a b-c");
assert!(encoded.starts_with("_N_"));
}
#[test]
fn test_encode_starts_with_digit() {
let encoded = encode("123foo");
assert!(encoded.starts_with("_N_"));
}
#[test]
fn test_encode_just_underscore() {
assert_eq!(encode("_"), "_");
}
#[test]
fn test_encode_prefix_collision() {
let encoded = encode("_N_test");
assert!(encoded.starts_with("_N_"));
assert_ne!(encoded, "_N_test");
}
#[test]
fn test_encode_double_underscore_passthrough() {
assert_eq!(encode("foo__bar"), "foo__bar");
}
#[test]
fn test_roundtrip_simple() {
let cases = vec![
"hello world",
"foo-bar",
"a b-c",
"test@example",
"with\ttab",
"new\nline",
];
for original in cases {
let encoded = encode(original);
let decoded = decode(&encoded)
.unwrap_or_else(|e| panic!("decode failed for {}: {:?}", original, e));
assert_eq!(
decoded, original,
"roundtrip failed for: {} (encoded: {})",
original, encoded
);
}
}
#[test]
fn test_roundtrip_unicode_non_xid() {
let cases = vec!["hello→world", "price: $100", "50% off"];
for original in cases {
let encoded = encode(original);
let decoded = decode(&encoded)
.unwrap_or_else(|e| panic!("decode failed for {}: {:?}", original, e));
assert_eq!(decoded, original);
}
}
#[test]
fn test_idempotent_valid_xid() {
let s = "foo";
assert_eq!(encode(&encode(s)), encode(s));
}
#[test]
fn test_idempotent_encoded() {
let s = "hello world";
let once = encode(s);
let twice = encode(&once);
assert_eq!(once, twice);
}
#[test]
fn test_identity_encode_decode() {
let original = "hello world";
let encoded = encode(original);
let decoded = decode(&encoded).unwrap();
let re_encoded = encode(&decoded);
assert_eq!(re_encoded, encoded);
}
#[test]
fn test_decode_not_encoded() {
assert_eq!(decode("foo"), Err(DecodeError::NotEncoded));
assert_eq!(decode("hello world"), Err(DecodeError::NotEncoded));
}
#[test]
fn test_decode_error_display() {
assert_eq!(
DecodeError::NotEncoded.to_string(),
"input is not a namecode-encoded string"
);
assert_eq!(
DecodeError::InvalidDigit('X').to_string(),
"invalid digit in encoded portion: 'X'"
);
assert_eq!(
DecodeError::UnexpectedEnd.to_string(),
"encoded data ended unexpectedly"
);
assert_eq!(
DecodeError::InvalidCodepoint(0xFFFFFFFF).to_string(),
"invalid Unicode codepoint: 4294967295"
);
assert_eq!(
DecodeError::Overflow.to_string(),
"overflow during decoding"
);
}
#[test]
fn test_decode_invalid_digit() {
let result = decode("_N_abc__6");
assert!(
matches!(result, Err(DecodeError::InvalidDigit(_))),
"expected InvalidDigit, got {:?}",
result
);
}
#[test]
fn test_decode_unexpected_end() {
let result = decode("_N_abc__z"); assert!(matches!(result, Err(DecodeError::UnexpectedEnd)));
}
#[test]
fn test_is_xid_identifier() {
assert!(is_xid_identifier("foo"));
assert!(is_xid_identifier("_foo"));
assert!(is_xid_identifier("foo123"));
assert!(is_xid_identifier("café"));
assert!(is_xid_identifier("名前"));
assert!(is_xid_identifier("_"));
assert!(!is_xid_identifier(""));
assert!(!is_xid_identifier("123"));
assert!(!is_xid_identifier("foo bar"));
assert!(!is_xid_identifier("foo-bar"));
}
#[test]
fn test_all_non_basic() {
let original = " "; let encoded = encode(original);
assert!(encoded.starts_with("_N_"));
let decoded = decode(&encoded).expect("decode failed");
assert_eq!(decoded, original);
}
#[test]
fn test_single_char() {
assert_eq!(encode("a"), "a");
let encoded = encode(" ");
assert!(encoded.starts_with("_N_"));
assert_eq!(decode(&encoded).unwrap(), " ");
}
#[test]
fn test_very_long_basic() {
let long = "a".repeat(1000);
assert_eq!(encode(&long), long);
}
#[test]
fn test_passthrough_double_underscore() {
let original = "foo__bar";
let encoded = encode(original);
assert_eq!(encoded, original);
assert!(decode(&encoded).is_err());
}
#[test]
fn test_roundtrip_prefix_collision() {
let original = "_N_test";
let encoded = encode(original);
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_multiple_underscores_passthrough() {
let cases = vec!["a__b", "a___b", "a____b", "__a", "___a", "____a"];
for original in cases {
let encoded = encode(original);
assert_eq!(encoded, original, "should passthrough for: {}", original);
}
}
#[test]
fn test_just_underscores_passthrough() {
let cases = vec!["__", "___", "____"];
for original in cases {
let encoded = encode(original);
assert_eq!(encoded, original, "should passthrough: {}", original);
}
}
#[test]
fn test_single_underscore_passthrough() {
assert_eq!(encode("_"), "_");
}
}
#[cfg(test)]
mod spec_vectors {
use super::*;
#[test]
fn passthrough() {
let cases: &[(&str, &str)] = &[
("foo", "foo"),
("_private", "_private"),
("café", "café"),
("名前", "名前"),
("CamelCase", "CamelCase"),
];
for &(input, expected) in cases {
assert_eq!(encode(input), expected, "passthrough: {:?}", input);
}
}
#[test]
fn encoding() {
let cases: &[(&str, &str)] = &[
("hello world", "_N_helloworld__fa0b"),
("foo-bar", "_N_foobar__da1d"),
("a b c", "_N_abc__ba0bb0b"),
("123", "_N_123"),
(" ", "_N___a0ba0ba0b"),
];
for &(input, expected) in cases {
let encoded = encode(input);
assert_eq!(encoded, expected, "encode: {:?}", input);
if encoded.starts_with("_N_") {
assert_eq!(decode(&encoded).unwrap(), input, "roundtrip: {:?}", input);
}
}
}
#[test]
fn edge_cases() {
let cases: &[(&str, &str)] = &[
("", ""),
(" ", "_N___a0b"),
("a", "a"),
("_", "_"),
("_a", "_a"),
("__", "__"),
("___", "___"),
("foo__bar", "foo__bar"),
("_N_test", "_N__N_test"),
("__ _x", "_N__x__ba3la0ba3l"),
];
for &(input, expected) in cases {
let encoded = encode(input);
assert_eq!(encoded, expected, "edge case: {:?}", input);
if encoded.starts_with("_N_") {
assert_eq!(decode(&encoded).unwrap(), input, "roundtrip: {:?}", input);
}
}
}
#[test]
fn decision_tree_examples() {
assert_eq!(encode("foo"), "foo");
assert_eq!(encode("cafe"), "cafe");
assert_eq!(encode("café"), "café");
assert_eq!(encode("名前"), "名前");
assert_eq!(encode("foo__bar"), "foo__bar");
assert_eq!(encode("hello world"), "_N_helloworld__fa0b");
assert_eq!(encode("foo-bar"), "_N_foobar__da1d");
assert_eq!(encode("123foo"), "_N_123foo");
assert_eq!(encode("_N_test"), "_N__N_test");
assert_eq!(encode("_"), "_");
assert_eq!(encode(""), "");
}
#[test]
fn collision_handling() {
assert_eq!(encode("_N_test"), "_N__N_test");
assert_eq!(decode("_N__N_test").unwrap(), "_N_test");
assert_eq!(encode("foo__bar"), "foo__bar");
assert_eq!(encode("__"), "__");
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn prop_roundtrip(s in ".*") {
if !s.is_empty() {
let encoded = encode(&s);
if encoded.starts_with("_N_") {
let decoded = decode(&encoded).unwrap_or_else(|e| {
panic!("decode failed for input '{}' with encoding '{}': {:?}", &s, &encoded, e)
});
prop_assert_eq!(&decoded, &s, "roundtrip failed for: {}", &s);
} else {
prop_assert_eq!(&encoded, &s, "passthrough failed for: {}", &s);
}
}
}
#[test]
fn prop_idempotent(s in ".*") {
let once = encode(&s);
let twice = encode(&once);
prop_assert_eq!(&once, &twice, "idempotency failed for: {}", &s);
}
#[test]
fn prop_identity(s in ".*") {
if !s.is_empty() {
let encoded = encode(&s);
if encoded.starts_with("_N_") {
let decoded = decode(&encoded).unwrap();
let re_encoded = encode(&decoded);
prop_assert_eq!(&re_encoded, &encoded, "identity failed for: {}", &s);
}
}
}
#[test]
fn prop_valid_output(s in ".+") {
let encoded = encode(&s);
prop_assert!(
is_xid_identifier(&encoded),
"encode('{}') = '{}' is not a valid XID identifier",
&s, &encoded
);
}
#[test]
fn prop_xid_passthrough(s in "[a-zA-Z][a-zA-Z0-9_]*") {
if !s.starts_with("_N_") && is_xid_identifier(&s) {
let encoded = encode(&s);
prop_assert_eq!(&encoded, &s, "XID passthrough failed for: {}", &s);
}
}
#[test]
fn prop_roundtrip_mixed(s in "[a-zA-Z0-9 \\-\\.,!@#$%^&*()]{1,50}") {
if s.chars().any(|c| !unicode_ident::is_xid_continue(c)) {
let encoded = encode(&s);
prop_assert!(encoded.starts_with("_N_"), "expected encoding for: {}", &s);
let decoded =
decode(&encoded).unwrap_or_else(|e| panic!("decode failed for {}: {:?}", &s, e));
prop_assert_eq!(&decoded, &s);
}
}
#[test]
fn prop_roundtrip_unicode(s in "[a-z ]{1,10}") {
if !s.is_empty() && s.contains(' ') {
let encoded = encode(&s);
prop_assert!(encoded.starts_with("_N_"));
let decoded =
decode(&encoded).unwrap_or_else(|e| panic!("decode failed for {}: {:?}", &s, e));
prop_assert_eq!(&decoded, &s);
}
}
}
}
#[cfg(kani)]
mod kani_proofs {
use super::*;
use crate::bootstring::{
adapt_bias, decode_digit, encode_digit, threshold, BASE, T_MAX, T_MIN,
};
#[kani::proof]
fn verify_encode_digit_valid_range() {
let digit: u32 = kani::any();
let result = encode_digit(digit);
if digit < 32 {
assert!(
result.is_some(),
"encode_digit should return Some for digit < 32"
);
let c = result.unwrap();
assert!(
('a'..='z').contains(&c) || ('0'..='5').contains(&c),
"encoded digit should be a-z or 0-5"
);
} else {
assert!(
result.is_none(),
"encode_digit should return None for digit >= 32"
);
}
}
#[kani::proof]
fn verify_decode_digit_valid_range() {
let c: char = kani::any();
let result = decode_digit(c);
match c {
'a'..='z' => {
assert!(result.is_some());
assert!(result.unwrap() < 26);
}
'A'..='Z' => {
assert!(result.is_some());
assert!(result.unwrap() < 26);
}
'0'..='5' => {
assert!(result.is_some());
let d = result.unwrap();
assert!(d >= 26 && d < 32);
}
_ => {
assert!(result.is_none());
}
}
}
#[kani::proof]
fn verify_digit_roundtrip() {
let digit: u32 = kani::any();
kani::assume(digit < 32);
let encoded = encode_digit(digit);
assert!(encoded.is_some());
let decoded = decode_digit(encoded.unwrap());
assert!(decoded.is_some());
assert_eq!(
decoded.unwrap(),
digit,
"digit roundtrip should be identity"
);
}
#[kani::proof]
fn verify_threshold_bounds() {
let k: u32 = kani::any();
let bias: u32 = kani::any();
kani::assume(k <= 10000);
kani::assume(bias <= 10000);
let t = threshold(k, bias);
assert!(t >= T_MIN, "threshold should be >= T_MIN");
assert!(t <= T_MAX, "threshold should be <= T_MAX");
}
#[kani::proof]
fn verify_adapt_bias_no_overflow() {
let delta: u32 = kani::any();
let num_points: u32 = kani::any();
let first_time: bool = kani::any();
kani::assume(delta <= 1_000_000);
kani::assume(num_points > 0 && num_points <= 10000);
let bias = adapt_bias(delta, num_points, first_time);
assert!(bias < 1_000_000, "bias should be bounded");
}
#[kani::proof]
fn verify_is_xid_empty() {
assert!(!is_xid_identifier(""));
}
#[kani::proof]
fn verify_is_xid_single_underscore() {
assert!(is_xid_identifier("_"));
}
#[kani::proof]
fn verify_encode_empty() {
let result = encode("");
assert!(result.is_empty());
}
#[kani::proof]
fn verify_decode_requires_prefix() {
let result = decode("abc");
assert!(matches!(result, Err(DecodeError::NotEncoded)));
}
#[kani::proof]
fn verify_idempotent_ascii() {
let input = "a b";
let once = encode(input);
let twice = encode(&once);
assert_eq!(once, twice, "encode should be idempotent");
}
#[kani::proof]
fn verify_roundtrip_simple() {
let input = "hello world";
let encoded = encode(input);
let decoded = decode(&encoded);
assert!(decoded.is_ok());
assert_eq!(decoded.unwrap(), input);
}
#[kani::proof]
fn verify_roundtrip_hyphen() {
let input = "foo-bar";
let encoded = encode(input);
let decoded = decode(&encoded);
assert!(decoded.is_ok());
assert_eq!(decoded.unwrap(), input);
}
#[kani::proof]
fn verify_double_underscore_passthrough() {
let input = "a__b";
let encoded = encode(input);
assert_eq!(encoded, input, "a__b should pass through");
}
#[kani::proof]
fn verify_roundtrip_prefix_collision() {
let input = "_N_x";
let encoded = encode(input);
assert_ne!(encoded, input);
let decoded = decode(&encoded);
assert!(decoded.is_ok());
assert_eq!(decoded.unwrap(), input);
}
#[kani::proof]
fn verify_xid_passthrough() {
let input = "validIdentifier";
let encoded = encode(input);
assert_eq!(encoded, input, "valid XID should pass through");
}
#[kani::proof]
fn verify_encode_produces_valid_xid() {
let input = "test with spaces";
let encoded = encode(input);
assert!(
is_xid_identifier(&encoded),
"encode should produce valid XID"
);
}
#[kani::proof]
fn verify_encode_single_ascii_no_panic() {
let byte: u8 = kani::any();
kani::assume(byte < 128);
let s = String::from(byte as char);
let _ = encode(&s); }
#[kani::proof]
fn verify_encode_two_ascii_no_panic() {
let b1: u8 = kani::any();
let b2: u8 = kani::any();
kani::assume(b1 < 128 && b2 < 128);
let mut s = String::new();
s.push(b1 as char);
s.push(b2 as char);
let _ = encode(&s); }
#[kani::proof]
fn verify_roundtrip_single_printable() {
let byte: u8 = kani::any();
kani::assume(byte >= 32 && byte < 127);
let input = String::from(byte as char);
let encoded = encode(&input);
if encoded.starts_with("_N_") {
let decoded = decode(&encoded);
assert!(decoded.is_ok());
assert_eq!(decoded.unwrap(), input);
} else {
assert_eq!(encoded, input);
}
}
}