#![expect(clippy::cast_possible_truncation)]
#[cfg(test)]
mod tests;
use crate::MAX_INDEX_FIELDS;
use std::{
cmp::Ordering,
fmt::{self, Display},
};
use thiserror::Error as ThisError;
pub(super) const MAX_ENTITY_NAME_LEN: usize = 64;
pub(super) const MAX_INDEX_FIELD_NAME_LEN: usize = 64;
pub(super) const MAX_INDEX_NAME_LEN: usize =
MAX_ENTITY_NAME_LEN + (MAX_INDEX_FIELDS * (MAX_INDEX_FIELD_NAME_LEN + 1));
#[derive(Debug, ThisError)]
pub enum IdentityDecodeError {
#[error("invalid size")]
InvalidSize,
#[error("invalid length")]
InvalidLength,
#[error("non-ascii encoding")]
NonAscii,
#[error("non-zero padding")]
NonZeroPadding,
}
#[derive(Debug, ThisError)]
pub enum EntityNameError {
#[error("entity name is empty")]
Empty,
#[error("entity name length {len} exceeds max {max}")]
TooLong { len: usize, max: usize },
#[error("entity name must be ASCII")]
NonAscii,
}
#[derive(Debug, ThisError)]
pub enum IndexNameError {
#[error("index has {len} fields (max {max})")]
TooManyFields { len: usize, max: usize },
#[error("index field name '{field}' exceeds max length {max}")]
FieldTooLong { field: String, max: usize },
#[error("index field name '{field}' must be ASCII")]
FieldNonAscii { field: String },
#[error("index name length {len} exceeds max {max}")]
TooLong { len: usize, max: usize },
}
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct EntityName {
len: u8,
bytes: [u8; MAX_ENTITY_NAME_LEN],
}
impl EntityName {
pub const STORED_SIZE_BYTES: u64 = 1 + (MAX_ENTITY_NAME_LEN as u64);
pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE_BYTES as usize;
pub fn try_from_str(name: &str) -> Result<Self, EntityNameError> {
let bytes = name.as_bytes();
let len = bytes.len();
if len == 0 {
return Err(EntityNameError::Empty);
}
if len > MAX_ENTITY_NAME_LEN {
return Err(EntityNameError::TooLong {
len,
max: MAX_ENTITY_NAME_LEN,
});
}
if !bytes.is_ascii() {
return Err(EntityNameError::NonAscii);
}
let mut out = [0u8; MAX_ENTITY_NAME_LEN];
out[..len].copy_from_slice(bytes);
Ok(Self {
len: len as u8,
bytes: out,
})
}
#[must_use]
pub const fn len(&self) -> usize {
self.len as usize
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.bytes[..self.len()]
}
#[must_use]
pub fn as_str(&self) -> &str {
std::str::from_utf8(self.as_bytes()).expect("EntityName invariant: ASCII-only storage")
}
#[must_use]
pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
let mut out = [0u8; Self::STORED_SIZE_USIZE];
out[0] = self.len;
out[1..].copy_from_slice(&self.bytes);
out
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, IdentityDecodeError> {
if bytes.len() != Self::STORED_SIZE_USIZE {
return Err(IdentityDecodeError::InvalidSize);
}
let len = bytes[0] as usize;
if len == 0 || len > MAX_ENTITY_NAME_LEN {
return Err(IdentityDecodeError::InvalidLength);
}
if !bytes[1..=len].is_ascii() {
return Err(IdentityDecodeError::NonAscii);
}
if bytes[1 + len..].iter().any(|&b| b != 0) {
return Err(IdentityDecodeError::NonZeroPadding);
}
let mut name = [0u8; MAX_ENTITY_NAME_LEN];
name.copy_from_slice(&bytes[1..]);
Ok(Self {
len: len as u8,
bytes: name,
})
}
#[must_use]
pub const fn max_storable() -> Self {
Self {
len: MAX_ENTITY_NAME_LEN as u8,
bytes: [b'z'; MAX_ENTITY_NAME_LEN],
}
}
}
impl Ord for EntityName {
fn cmp(&self, other: &Self) -> Ordering {
self.len.cmp(&other.len).then(self.bytes.cmp(&other.bytes))
}
}
impl PartialOrd for EntityName {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Display for EntityName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl fmt::Debug for EntityName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "EntityName({})", self.as_str())
}
}
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct IndexName {
len: u16,
bytes: [u8; MAX_INDEX_NAME_LEN],
}
impl IndexName {
pub const STORED_SIZE_BYTES: u64 = 2 + (MAX_INDEX_NAME_LEN as u64);
pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE_BYTES as usize;
pub fn try_from_parts(entity: &EntityName, fields: &[&str]) -> Result<Self, IndexNameError> {
if fields.len() > MAX_INDEX_FIELDS {
return Err(IndexNameError::TooManyFields {
len: fields.len(),
max: MAX_INDEX_FIELDS,
});
}
let mut total_len = entity.len();
for field in fields {
let field_len = field.len();
if field_len > MAX_INDEX_FIELD_NAME_LEN {
return Err(IndexNameError::FieldTooLong {
field: (*field).to_string(),
max: MAX_INDEX_FIELD_NAME_LEN,
});
}
if !field.is_ascii() {
return Err(IndexNameError::FieldNonAscii {
field: (*field).to_string(),
});
}
total_len = total_len.saturating_add(1 + field_len);
}
if total_len > MAX_INDEX_NAME_LEN {
return Err(IndexNameError::TooLong {
len: total_len,
max: MAX_INDEX_NAME_LEN,
});
}
let mut out = [0u8; MAX_INDEX_NAME_LEN];
let mut len = 0usize;
Self::push_bytes(&mut out, &mut len, entity.as_bytes());
for field in fields {
Self::push_bytes(&mut out, &mut len, b"|");
Self::push_bytes(&mut out, &mut len, field.as_bytes());
}
Ok(Self {
len: len as u16,
bytes: out,
})
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.bytes[..self.len as usize]
}
#[must_use]
pub fn as_str(&self) -> &str {
std::str::from_utf8(self.as_bytes()).expect("IndexName invariant: ASCII-only storage")
}
#[must_use]
pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
let mut out = [0u8; Self::STORED_SIZE_USIZE];
out[..2].copy_from_slice(&self.len.to_be_bytes());
out[2..].copy_from_slice(&self.bytes);
out
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, IdentityDecodeError> {
if bytes.len() != Self::STORED_SIZE_USIZE {
return Err(IdentityDecodeError::InvalidSize);
}
let len = u16::from_be_bytes([bytes[0], bytes[1]]) as usize;
if len == 0 || len > MAX_INDEX_NAME_LEN {
return Err(IdentityDecodeError::InvalidLength);
}
if !bytes[2..2 + len].is_ascii() {
return Err(IdentityDecodeError::NonAscii);
}
if bytes[2 + len..].iter().any(|&b| b != 0) {
return Err(IdentityDecodeError::NonZeroPadding);
}
let mut name = [0u8; MAX_INDEX_NAME_LEN];
name.copy_from_slice(&bytes[2..]);
Ok(Self {
len: len as u16,
bytes: name,
})
}
fn push_bytes(out: &mut [u8; MAX_INDEX_NAME_LEN], len: &mut usize, bytes: &[u8]) {
let end = *len + bytes.len();
out[*len..end].copy_from_slice(bytes);
*len = end;
}
#[must_use]
pub const fn max_storable() -> Self {
Self {
len: MAX_INDEX_NAME_LEN as u16,
bytes: [b'z'; MAX_INDEX_NAME_LEN],
}
}
}
impl Ord for IndexName {
fn cmp(&self, other: &Self) -> Ordering {
self.to_bytes().cmp(&other.to_bytes())
}
}
impl PartialOrd for IndexName {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl fmt::Debug for IndexName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "IndexName({})", self.as_str())
}
}
impl Display for IndexName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}