use std::fmt;
const DELIMITER: u8 = b'/';
const NUL: u8 = b'\0';
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyPathError {
EmptySegment,
ContainsDelimiter,
ContainsNul,
DotSegment,
}
impl fmt::Display for KeyPathError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptySegment => write!(f, "key path segment must not be empty"),
Self::ContainsDelimiter => write!(f, "key path segment must not contain '/'"),
Self::ContainsNul => write!(f, "key path segment must not contain NUL"),
Self::DotSegment => write!(f, "key path segment must not be '.' or '..'"),
}
}
}
impl std::error::Error for KeyPathError {}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct KeyPathBuf {
bytes: Vec<u8>,
}
impl KeyPathBuf {
#[must_use]
pub const fn new() -> Self {
Self { bytes: Vec::new() }
}
pub fn with_namespace(ns: impl AsRef<[u8]>) -> Result<Self, KeyPathError> {
validate_segment(ns.as_ref())?;
let mut bytes = Vec::with_capacity(ns.as_ref().len() + 1);
bytes.extend_from_slice(ns.as_ref());
bytes.push(DELIMITER);
Ok(Self { bytes })
}
pub fn push(&mut self, segment: impl AsRef<[u8]>) -> Result<(), KeyPathError> {
validate_segment(segment.as_ref())?;
if !self.bytes.is_empty() && !self.bytes.ends_with(&[DELIMITER]) {
self.bytes.push(DELIMITER);
}
self.bytes.extend_from_slice(segment.as_ref());
Ok(())
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.bytes
}
#[must_use]
pub fn into_bytes(self) -> Vec<u8> {
self.bytes
}
#[must_use]
pub fn into_prefix(mut self) -> KeyPrefixBuf {
if !self.bytes.is_empty() && !self.bytes.ends_with(&[DELIMITER]) {
self.bytes.push(DELIMITER);
}
KeyPrefixBuf { bytes: self.bytes }
}
#[must_use]
pub fn len(&self) -> usize {
self.bytes.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
}
impl AsRef<[u8]> for KeyPathBuf {
fn as_ref(&self) -> &[u8] {
self.as_bytes()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct KeyPrefixBuf {
bytes: Vec<u8>,
}
impl KeyPrefixBuf {
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.bytes
}
#[must_use]
pub fn into_bytes(self) -> Vec<u8> {
self.bytes
}
#[must_use]
pub fn len(&self) -> usize {
self.bytes.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
}
impl AsRef<[u8]> for KeyPrefixBuf {
fn as_ref(&self) -> &[u8] {
self.as_bytes()
}
}
fn validate_segment(segment: &[u8]) -> Result<(), KeyPathError> {
if segment.is_empty() {
return Err(KeyPathError::EmptySegment);
}
if segment.contains(&DELIMITER) {
return Err(KeyPathError::ContainsDelimiter);
}
if segment.contains(&NUL) {
return Err(KeyPathError::ContainsNul);
}
if segment == b"." || segment == b".." {
return Err(KeyPathError::DotSegment);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{KeyPathBuf, KeyPathError};
#[test]
fn builds_namespaced_key_from_segments() {
let mut key = KeyPathBuf::with_namespace(b"o").unwrap();
key.push(b"bucket-a").unwrap();
key.push(b"photos").unwrap();
key.push(b"img.jpg").unwrap();
assert_eq!(key.as_bytes(), b"o/bucket-a/photos/img.jpg");
}
#[test]
fn prefix_is_slash_terminated() {
let mut key = KeyPathBuf::with_namespace(b"o").unwrap();
key.push(b"bucket-a").unwrap();
key.push(b"photos").unwrap();
let prefix = key.into_prefix();
assert_eq!(prefix.as_bytes(), b"o/bucket-a/photos/");
}
#[test]
fn namespace_only_prefix_keeps_single_trailing_slash() {
let prefix = KeyPathBuf::with_namespace(b"o").unwrap().into_prefix();
assert_eq!(prefix.as_bytes(), b"o/");
}
#[test]
fn empty_prefix_scans_everything() {
let prefix = KeyPathBuf::new().into_prefix();
assert!(prefix.is_empty());
}
#[test]
fn rejects_ambiguous_segments() {
assert_eq!(
KeyPathBuf::new().push(b"").unwrap_err(),
KeyPathError::EmptySegment
);
assert_eq!(
KeyPathBuf::new().push(b"a/b").unwrap_err(),
KeyPathError::ContainsDelimiter
);
assert_eq!(
KeyPathBuf::new().push(b"a\0b").unwrap_err(),
KeyPathError::ContainsNul
);
assert_eq!(
KeyPathBuf::new().push(b"..").unwrap_err(),
KeyPathError::DotSegment
);
}
}