#[allow(clippy::indexing_slicing)] pub const fn name_valid_check(name: &str) {
if let NAME_MIN_CHARS..=NAME_MAX_CHARS = name.len() {
if !name.is_ascii() {
panic!("Id name must be ascii");
}
} else {
panic!("Id name length must be within range")
}
let bytes = name.as_bytes();
let mut i = 0;
'check_loop: while i < bytes.len() {
let mut j = 0;
while j < CHAR_MAPPING.len() {
if CHAR_MAPPING[j].1 == bytes[i] {
i += 1;
continue 'check_loop;
}
j += 1;
}
panic!("Invalid char in name");
}
}
pub const NAME_MIN_CHARS: usize = 1;
pub const NAME_MAX_CHARS: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NameBitsValidation {
Valid,
Invalid,
}
pub const CHAR_BIT_LENGTH: u8 = 5;
pub const CHAR_MASK: u8 = (1 << CHAR_BIT_LENGTH) - 1;
pub const NON_NAME_BITS: u8 = u128::BITS as u8 - (CHAR_BIT_LENGTH * NAME_MAX_CHARS as u8);
pub const CHAR_MAPPING: [(u8, u8); 31] = [
(1, b'0'),
(2, b'1'),
(3, b'2'),
(4, b'3'),
(5, b'4'),
(6, b'a'),
(7, b'b'),
(8, b'c'),
(9, b'd'),
(10, b'e'),
(11, b'f'),
(12, b'g'),
(13, b'h'),
(14, b'i'),
(15, b'j'),
(16, b'k'),
(17, b'l'),
(18, b'm'),
(19, b'n'),
(20, b'o'),
(21, b'p'),
(22, b'q'),
(23, b'r'),
(24, b's'),
(25, b't'),
(26, b'u'),
(27, b'v'),
(28, b'w'),
(29, b'x'),
(30, b'y'),
(31, b'z'),
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum NameError {
Empty,
TooLong(usize),
NonAscii,
InvalidChar(u8),
}
impl std::fmt::Display for NameError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Empty => write!(f, "name cannot be empty"),
Self::TooLong(len) => {
write!(f, "name length {len} exceeds maximum of 4 characters")
}
Self::NonAscii => write!(f, "name must contain only ASCII characters"),
Self::InvalidChar(byte) => {
write!(
f,
"invalid character '{}' (0x{byte:02x}) in name; only 0-4 and a-z are allowed",
char::from(*byte)
)
}
}
}
}
impl std::error::Error for NameError {}
#[derive(Clone, Copy)]
pub struct NameStr<'a>(&'a str);
impl<'a> NameStr<'a> {
pub const fn new_const(s: &'static str) -> Self {
name_valid_check(s);
Self(s)
}
pub fn new(s: &'a str) -> Result<Self, NameError> {
if s.is_empty() {
return Err(NameError::Empty);
}
if s.len() > NAME_MAX_CHARS {
return Err(NameError::TooLong(s.len()));
}
if !s.is_ascii() {
return Err(NameError::NonAscii);
}
let bytes = s.as_bytes();
for &byte in bytes {
let mut found = false;
for &(_, valid_char) in &CHAR_MAPPING {
if valid_char == byte {
found = true;
break;
}
}
if !found {
return Err(NameError::InvalidChar(byte));
}
}
Ok(Self(s))
}
pub fn as_str(&self) -> &str {
self.0
}
}
pub fn name_mask(name: NameStr) -> u128 {
let name = name.as_str();
let name_bytes = name.as_bytes();
let mut mask = 0u128;
for &name_char in name_bytes {
let encoding_mapping = CHAR_MAPPING
.iter()
.find(|(_encoded, from_char)| *from_char == name_char);
let (encoded_byte, _) = encoding_mapping.expect("mapping must exist");
debug_assert!(*encoded_byte < 32);
mask <<= CHAR_BIT_LENGTH;
mask |= *encoded_byte as u128;
}
let needed_padding_chars = NAME_MAX_CHARS - name.len();
mask <<= CHAR_BIT_LENGTH * needed_padding_chars as u8;
mask <<= NON_NAME_BITS;
mask
}
pub fn validate_name_bits(id: u128) -> NameBitsValidation {
let name_bits = (id >> NON_NAME_BITS) as u32;
let mut found_char = false;
let mut found_null = false;
for i in (0..NAME_MAX_CHARS).rev() {
let shift = i * CHAR_BIT_LENGTH as usize;
let encoded_byte = (name_bits >> shift) as u8 & CHAR_MASK;
if encoded_byte == 0 {
found_null = true;
continue;
}
if found_null {
return NameBitsValidation::Invalid;
}
found_char = true;
}
if found_char {
NameBitsValidation::Valid
} else {
NameBitsValidation::Invalid
}
}
pub fn name_bits_to_hex(id: u128) -> String {
let name_bits = (id >> NON_NAME_BITS) as u32;
let hex = format!("{:05x}", name_bits);
debug_assert_eq!(hex.len(), 5);
hex
}
pub fn extract_name_string(id: u128) -> Option<String> {
let name_bits = (id >> NON_NAME_BITS) as u32;
let expected_string_len = name_bits.trailing_zeros() / 5;
let mut name_bytes = Vec::with_capacity(expected_string_len as usize);
for i in (0..NAME_MAX_CHARS).rev() {
let shift = i * CHAR_BIT_LENGTH as usize;
let encoded_byte = (name_bits >> shift) as u8 & CHAR_MASK;
if encoded_byte == 0 {
break;
}
let decoded_char = CHAR_MAPPING
.iter()
.find(|(encoded, _)| *encoded == encoded_byte)
.map(|(_, ascii_char)| *ascii_char)
.expect("all non-zero 5-bit values (1-31) have mappings");
name_bytes.push(decoded_char);
}
if name_bytes.is_empty() {
return None;
}
Some(String::from_utf8(name_bytes).expect("name bytes must be valid ASCII"))
}
#[cfg(all(test, not(debug_assertions)))]
mod tests_release {
use super::*;
use proptest::prelude::*;
use proptest::test_runner::TestRunner;
#[test]
fn name_mask_no_panic() {
let mut runner = TestRunner::new(ProptestConfig {
cases: 100_000,
..ProptestConfig::default()
});
runner
.run(&any::<String>(), |name| {
let Ok(name) = NameStr::new(name.as_str()) else {
return Ok(());
};
name_mask(name);
Ok(())
})
.unwrap();
}
#[test]
fn extract_name_string_no_panic() {
let mut runner = TestRunner::new(ProptestConfig {
cases: 100_000,
..ProptestConfig::default()
});
runner
.run(&any::<u128>(), |id| {
let _ = extract_name_string(id);
Ok(())
})
.unwrap();
}
#[test]
fn extract_name_string_no_panic_valid_chars() {
let mut runner = TestRunner::new(ProptestConfig {
cases: 100_000,
..ProptestConfig::default()
});
let strategy = (1u128..=31, 0u128..=31, 0u128..=31, 0u128..=31);
runner
.run(&strategy, |(c0, c1, c2, c3)| {
let id = (c0 << 123) | (c1 << 118) | (c2 << 113) | (c3 << 108);
let result = extract_name_string(id);
assert!(result.is_some());
Ok(())
})
.unwrap();
}
}