const LONGEST_BACKEND_SUFFIX: usize = "__eventcount_checkpoint".len();
const MAX_NAME_BYTES: usize = 255;
pub const MAX_ENCODED_PARTITION_BYTES: usize = MAX_NAME_BYTES - LONGEST_BACKEND_SUFFIX;
const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum PartitionNameError {
#[error("a partition name holds at least one byte")]
Empty,
#[error("the encoded partition name takes {actual} bytes, and storage holds {limit}")]
TooLong {
actual: usize,
limit: usize,
},
#[error("`{0}` names an internal namespace that a caller does not address")]
Reserved(String),
#[error("the physical name `{0}` is not a canonical encoding")]
NotCanonical(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EncodedPartitionName(String);
impl EncodedPartitionName {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl std::fmt::Display for EncodedPartitionName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PartitionRef {
logical: String,
encoded: EncodedPartitionName,
}
impl PartitionRef {
pub fn new(logical: &str) -> Result<Self, PartitionNameError> {
Ok(Self {
logical: logical.to_owned(),
encoded: encode(logical)?,
})
}
#[must_use]
pub fn logical(&self) -> &str {
&self.logical
}
#[must_use]
pub fn storage_key(&self) -> &str {
self.encoded.as_str()
}
}
impl std::fmt::Display for PartitionRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.logical)
}
}
pub fn encode(logical: &str) -> Result<EncodedPartitionName, PartitionNameError> {
if logical.starts_with(crate::REPAIR_STAGE_PREFIX)
|| logical.starts_with(crate::REWRITE_STAGE_PREFIX)
{
return Err(PartitionNameError::Reserved(logical.to_owned()));
}
encode_any_namespace(logical)
}
pub(crate) fn encode_any_namespace(
logical: &str,
) -> Result<EncodedPartitionName, PartitionNameError> {
if logical.is_empty() {
return Err(PartitionNameError::Empty);
}
let mut encoded = String::with_capacity(logical.len());
for byte in logical.bytes() {
match byte {
b'a'..=b'z' | b'0'..=b'9' | b'-' => encoded.push(char::from(byte)),
b'_' => encoded.push_str("__"),
other => {
encoded.push('_');
encoded.push(char::from(HEX_DIGITS[usize::from(other >> 4)]));
encoded.push(char::from(HEX_DIGITS[usize::from(other & 0x0f)]));
}
}
}
if encoded.len() > MAX_ENCODED_PARTITION_BYTES {
return Err(PartitionNameError::TooLong {
actual: encoded.len(),
limit: MAX_ENCODED_PARTITION_BYTES,
});
}
Ok(EncodedPartitionName(encoded))
}
pub fn decode(physical: &str) -> Result<String, PartitionNameError> {
let not_canonical = || PartitionNameError::NotCanonical(physical.to_owned());
if physical.is_empty() {
return Err(PartitionNameError::Empty);
}
let bytes = physical.as_bytes();
let mut logical = Vec::with_capacity(bytes.len());
let mut index = 0;
while index < bytes.len() {
match bytes[index] {
b'_' => {
let next = *bytes.get(index + 1).ok_or_else(not_canonical)?;
if next == b'_' {
logical.push(b'_');
index += 2;
} else {
let low = *bytes.get(index + 2).ok_or_else(not_canonical)?;
let high = hex_value(next).ok_or_else(not_canonical)?;
let low = hex_value(low).ok_or_else(not_canonical)?;
logical.push((high << 4) | low);
index += 3;
}
}
byte => {
logical.push(byte);
index += 1;
}
}
}
let logical = String::from_utf8(logical).map_err(|_| not_canonical())?;
let round_trip = encode_any_namespace(&logical).map_err(|_| not_canonical())?;
if round_trip.as_str() != physical {
return Err(not_canonical());
}
Ok(logical)
}
const fn hex_value(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::{MAX_ENCODED_PARTITION_BYTES, PartitionNameError, decode, encode};
#[test]
fn every_logical_name_survives_the_round_trip() {
for logical in [
"conv-web:cafe",
"conv-web_cafe",
"conv-a",
"conv-A",
"conv-app:persona-1:standup",
"conv-proj/alpha",
"conv-a..b",
"conv-café",
"conv-_",
"conv-__",
"mem-persona-1",
"audit-immutable",
] {
let encoded = encode(logical).expect("the name encodes");
assert_eq!(
decode(encoded.as_str()).expect("the name decodes"),
logical,
"round trip for {logical}"
);
}
}
#[test]
fn a_colon_and_an_underscore_take_different_names() {
let colon = encode("conv-web:cafe").expect("encodes");
let underscore = encode("conv-web_cafe").expect("encodes");
assert_eq!(colon.as_str(), "conv-web_3acafe");
assert_eq!(underscore.as_str(), "conv-web__cafe");
assert_ne!(colon, underscore);
}
#[test]
fn the_physical_alphabet_is_lowercase() {
let encoded = encode("conv-Alpha").expect("encodes");
assert_eq!(encoded.as_str(), "conv-_41lpha");
assert!(
!encoded.as_str().bytes().any(|b| b.is_ascii_uppercase()),
"no uppercase reaches storage: {encoded}"
);
}
#[test]
fn the_conversation_prefix_stays_readable() {
assert!(
encode("conv-web:cafe")
.expect("encodes")
.as_str()
.starts_with("conv-")
);
}
#[test]
fn a_noncanonical_spelling_is_refused() {
for physical in [
"conv-_61", "conv-_3A", "conv-_3", "conv-_", "conv-web:x", "conv-_2d", ] {
assert!(
matches!(
decode(physical),
Err(PartitionNameError::NotCanonical(_) | PartitionNameError::Empty)
),
"{physical} is not a canonical name"
);
}
}
#[test]
fn invalid_utf8_is_refused() {
assert!(matches!(
decode("conv-_ff"),
Err(PartitionNameError::NotCanonical(_))
));
}
#[test]
fn the_length_limit_binds_at_the_adapter() {
let at_limit = "a".repeat(MAX_ENCODED_PARTITION_BYTES);
assert_eq!(
encode(&at_limit)
.expect("a name at the limit encodes")
.as_str()
.len(),
MAX_ENCODED_PARTITION_BYTES
);
let over = "a".repeat(MAX_ENCODED_PARTITION_BYTES + 1);
assert!(matches!(
encode(&over),
Err(PartitionNameError::TooLong { .. })
));
let escaped = ":".repeat(MAX_ENCODED_PARTITION_BYTES / 3 + 1);
assert!(matches!(
encode(&escaped),
Err(PartitionNameError::TooLong { .. })
));
}
#[test]
fn the_internal_namespace_is_refused_at_the_boundary() {
assert!(matches!(
encode("state-repair-stage-beef"),
Err(PartitionNameError::Reserved(_))
));
}
#[test]
fn an_empty_name_is_refused() {
assert!(matches!(encode(""), Err(PartitionNameError::Empty)));
assert!(matches!(decode(""), Err(PartitionNameError::Empty)));
}
}