use std::{
fmt,
hash,
path::Path,
};
#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
const ASCII_CASE_MASK: u8 = 0b0010_0000;
const EXT_SIZE: usize = 8;
const ZEROES: [u8; EXT_SIZE] = [0_u8; EXT_SIZE];
macro_rules! notslash {
() => ( 0..=46 | 48..=91 | 93..=255 );
}
#[cfg(unix)]
macro_rules! path_slice {
($path:ident) => ($path.as_ref().as_os_str().as_bytes());
}
#[cfg(not(unix))]
macro_rules! path_slice {
($path:ident) => ($path.as_ref().to_string_lossy().as_bytes());
}
#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
pub struct Extension([u8; EXT_SIZE]);
impl AsRef<[u8]> for Extension {
#[inline]
fn as_ref(&self) -> &[u8] { self.as_bytes() }
}
impl AsRef<str> for Extension {
#[inline]
fn as_ref(&self) -> &str { self.as_str() }
}
impl fmt::Debug for Extension {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Extension({self})")
}
}
impl fmt::Display for Extension {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
<str as fmt::Display>::fmt(self.as_str(), f)
}
}
impl hash::Hash for Extension {
#[inline]
fn hash<H: hash::Hasher>(&self, state: &mut H) {
state.write_u64(u64::from_be_bytes(self.0));
}
}
impl Extension {
#[inline]
#[must_use]
pub const fn new(src: &str) -> Option<Self> { Self::new_slice(src.as_bytes()) }
#[doc(hidden)]
#[inline]
#[must_use]
pub const fn new_slice(mut src: &[u8]) -> Option<Self> {
if ! src.is_empty() && src.len() <= EXT_SIZE {
let mut dst = ZEROES;
let mut idx = EXT_SIZE;
while let [ rest @ .., n ] = src && let Some(n) = sanitize_byte(*n) {
if idx == 0 { return None; } idx -= 1;
dst[idx] = n;
src = rest;
}
if idx < EXT_SIZE && src.is_empty() { Some(Self(dst)) }
else { None }
}
else { None }
}
#[inline]
#[must_use]
pub fn from_path<P: AsRef<Path>>(src: P) -> Option<Self> {
Self::from_path_slice(path_slice!(src))
}
#[inline]
#[must_use]
pub const fn from_path_slice(mut src: &[u8]) -> Option<Self> {
let mut dst = ZEROES;
let mut idx = EXT_SIZE;
while let [ rest @ .., n ] = src && let Some(n) = sanitize_byte(*n) {
if idx == 0 { return None; }
idx -= 1;
dst[idx] = n;
src = rest;
}
if idx < EXT_SIZE && matches!(src, [ .., notslash!(), b'.' ]) { Some(Self(dst)) }
else { None }
}
}
impl Extension {
#[inline]
#[must_use]
pub const fn as_bytes(&self) -> &[u8] {
let mut out = self.0.as_slice();
while let [ 0, rest @ .. ] = out { out = rest; }
out
}
#[must_use]
pub const fn as_str(&self) -> &str {
let Ok(out) = std::str::from_utf8(self.as_bytes()) else { unreachable!(); };
out
}
#[must_use]
pub const fn eq(a: Self, b: Self) -> bool {
u64::from_be_bytes(a.0) == u64::from_be_bytes(b.0)
}
#[must_use]
pub const fn is_empty(self) -> bool { self.0[EXT_SIZE - 1] == 0 }
#[must_use]
pub const fn len(self) -> usize { self.as_bytes().len() }
}
impl Extension {
#[inline]
#[must_use]
pub fn matches_path<P: AsRef<Path>>(self, path: P) -> bool {
self.matches_path_slice(path_slice!(path))
}
#[inline]
#[must_use]
pub const fn matches_path_slice(self, path: &[u8]) -> bool {
const fn check_byte(us: u8, them: u8) -> bool {
if let Some(them) = sanitize_byte(them) { them == us }
else { false }
}
let mut ext1 = self.as_bytes();
if ext1.len() + 2 <= path.len() {
let (path, mut ext2) = path.split_at(path.len() - ext1.len());
while let [ a, rest1 @ .. ] = ext1 && let [ b, rest2 @ .. ] = ext2 && check_byte(*a, *b) {
ext1 = rest1;
ext2 = rest2;
}
ext1.is_empty() && matches!(path, [ .., notslash!(), b'.' ])
}
else { false }
}
}
#[inline(always)]
#[expect(clippy::inline_always, reason = "Foundational.")]
const fn sanitize_byte(src: u8) -> Option<u8> {
match src {
b'!' | b'#' | b'+' | b'-' | b'0'..=b'9' | b'_' | b'a'..=b'z' => Some(src),
b'A'..=b'Z' => Some(src | ASCII_CASE_MASK),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn t_sanitize_byte() {
for i in u8::MIN..=u8::MAX {
if i.is_ascii_alphanumeric() || matches!(i, b'!' | b'#' | b'+' | b'-' | b'_') {
assert_eq!(
sanitize_byte(i),
Some(i.to_ascii_lowercase()),
);
}
else { assert!(sanitize_byte(i).is_none()); }
}
}
#[test]
fn t_notslash() {
for i in u8::MIN..=u8::MAX {
assert_eq!(
i != b'/' && i != b'\\',
matches!(i, notslash!()),
);
}
}
#[test]
fn t_ext_len() {
const RAW: [&str; EXT_SIZE] = [
"1",
"gz",
"av1",
"html",
"vcard",
"jsonld",
"geojson",
"manifest",
];
for i in RAW {
let Some(ext) = Extension::new(i) else {
panic!("Extension failed: {i:?}");
};
let mut file = format!("file.{i}");
assert!(ext.matches_path(&file));
assert_eq!(Extension::from_path(&file), Some(ext));
file.make_ascii_uppercase();
assert!(ext.matches_path(&file));
assert_eq!(Extension::from_path(&file), Some(ext));
file.push('s');
assert!(! ext.matches_path(&file));
assert_eq!(ext.as_str(), i);
assert_eq!(ext.to_string(), i);
assert_eq!(ext.len(), i.len());
let Some(ext2) = Extension::new(&i.to_ascii_uppercase()) else {
panic!("Extension failed: {:?}", i.to_ascii_uppercase());
};
assert_eq!(ext, ext2);
assert_eq!(ext.as_str(), ext2.as_str());
}
let mut exts = RAW.into_iter().filter_map(Extension::new).collect::<Vec<_>>();
assert!(exts.is_sorted());
exts.push(Extension::new("Z").unwrap());
exts.push(Extension::new("c").unwrap());
exts.push(Extension::new("C").unwrap());
exts.sort();
exts.dedup();
assert_eq!(exts[0].as_str(), "1");
assert_eq!(exts[1].as_str(), "c");
assert_eq!(exts[2].as_str(), "z");
assert_eq!(exts[3].as_str(), "gz");
exts.remove(2);
exts.remove(1);
assert!(exts.iter().map(|e| e.as_str()).eq(RAW.into_iter()));
}
#[test]
fn t_ext_real() {
let raw = std::fs::read_to_string("tests/extensions.txt").expect("Missing extensions.txt.");
let all: Vec<&str> = raw.lines()
.filter_map(|line| {
let line = line.trim();
if line.is_empty() { None }
else { Some(line) }
})
.collect();
assert_eq!(all.len(), 2790);
for ext in all {
assert!(
Extension::new(ext).is_some(),
"Failed for {ext}.",
);
}
}
}