use std::borrow::Cow;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::mem::MaybeUninit;
const STACK_KEY_LEN: usize = 256;
#[inline]
fn normalize_ascii_string(mut key: String) -> String {
if let Some(first_uppercase) = key.bytes().position(|byte| byte.is_ascii_uppercase()) {
key[first_uppercase..].make_ascii_lowercase();
}
key
}
#[inline]
fn normalized_ascii_cow(key: &str) -> Cow<'_, str> {
key.bytes()
.position(|byte| byte.is_ascii_uppercase())
.map_or(Cow::Borrowed(key), |first_uppercase| {
let mut normalized = key.as_bytes().to_vec();
normalized[first_uppercase..].make_ascii_lowercase();
unsafe { Cow::Owned(String::from_utf8_unchecked(normalized)) }
})
}
#[inline]
fn with_normalized_ascii_key<T>(key: &str, f: impl FnOnce(&str) -> T) -> T {
match key.bytes().position(|byte| byte.is_ascii_uppercase()) {
Some(first_uppercase) => {
let bytes = key.as_bytes();
if bytes.len() <= STACK_KEY_LEN {
let len = bytes.len();
let mut buffer = [MaybeUninit::<u8>::uninit(); STACK_KEY_LEN];
let normalized = unsafe {
std::ptr::copy_nonoverlapping(
bytes.as_ptr(),
buffer.as_mut_ptr().cast::<u8>(),
len,
);
std::slice::from_raw_parts_mut(buffer.as_mut_ptr().cast::<u8>(), len)
};
normalized[first_uppercase..].make_ascii_lowercase();
f(unsafe { std::str::from_utf8_unchecked(normalized) })
} else {
let mut normalized = bytes.to_vec();
normalized[first_uppercase..].make_ascii_lowercase();
let normalized = unsafe { String::from_utf8_unchecked(normalized) };
f(&normalized)
}
}
None => f(key),
}
}
#[derive(Debug, Clone)]
pub struct AsciiKey<'a>(Cow<'a, str>);
impl<'a> AsciiKey<'a> {
#[inline]
#[must_use]
pub fn new(key: &'a str) -> Self {
Self(normalized_ascii_cow(key))
}
#[inline]
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_ref()
}
#[inline]
#[must_use]
pub const fn is_borrowed(&self) -> bool {
matches!(self.0, Cow::Borrowed(_))
}
}
impl AsRef<str> for AsciiKey<'_> {
#[inline]
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl Hash for AsciiKey<'_> {
#[inline]
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_str().hash(state);
}
}
impl PartialEq for AsciiKey<'_> {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.as_str() == other.as_str()
}
}
impl Eq for AsciiKey<'_> {}
#[derive(Debug)]
pub struct AsciiMap<V> {
inner: HashMap<String, V>,
}
impl<V> AsciiMap<V> {
#[inline]
#[must_use]
pub fn new() -> Self {
Self {
inner: HashMap::new(),
}
}
#[inline]
pub fn insert(&mut self, key: String, value: V) -> Option<V> {
self.inner.insert(normalize_ascii_string(key), value)
}
#[inline]
#[must_use]
pub fn get(&self, key: &str) -> Option<&V> {
with_normalized_ascii_key(key, |key| self.inner.get(key))
}
#[inline]
pub fn iter(&self) -> impl Iterator<Item = (&str, &V)> {
self.inner.iter().map(|(key, value)| (key.as_str(), value))
}
#[inline]
pub fn values(&self) -> impl Iterator<Item = &V> {
self.inner.values()
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.inner.len()
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
}
impl<V> Default for AsciiMap<V> {
#[inline]
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_key_borrows_normalized_input() {
let key = AsciiKey::new("get:001");
assert!(key.is_borrowed());
assert_eq!(key.as_str(), "get:001");
}
#[test]
fn ascii_key_allocates_mixed_case_input() {
let key = AsciiKey::new("GeT:001");
assert!(!key.is_borrowed());
assert_eq!(key.as_str(), "get:001");
}
#[test]
fn insert_and_get_are_case_insensitive() {
let mut map = AsciiMap::new();
map.insert("GET".to_string(), 1);
map.insert("SET".to_string(), 2);
assert_eq!(map.get("GET"), Some(&1));
assert_eq!(map.get("get"), Some(&1));
assert_eq!(map.get("Get"), Some(&1));
assert_eq!(map.get("SET"), Some(&2));
assert_eq!(map.get("set"), Some(&2));
assert_eq!(map.get("UNKNOWN"), None);
}
#[test]
fn insert_overwrites_with_different_case() {
let mut map = AsciiMap::new();
map.insert("GET".to_string(), 1);
let old = map.insert("get".to_string(), 2);
assert_eq!(old, Some(1));
assert_eq!(map.get("GET"), Some(&2));
assert_eq!(map.len(), 1);
}
#[test]
fn iter_returns_normalized_keys() {
let mut map = AsciiMap::new();
map.insert("OriginalCase".to_string(), 1);
let keys: Vec<_> = map.iter().map(|(key, _)| key).collect();
assert_eq!(keys, ["originalcase"]);
}
#[test]
fn values_returns_values() {
let mut map = AsciiMap::new();
map.insert("A".to_string(), 1);
map.insert("B".to_string(), 2);
let values: Vec<_> = map.values().copied().collect();
assert!(values.contains(&1));
assert!(values.contains(&2));
}
#[test]
fn long_lookup_uses_heap_fallback() {
let key = "A".repeat(STACK_KEY_LEN + 1);
let lookup = "a".repeat(STACK_KEY_LEN + 1);
let mut map = AsciiMap::new();
map.insert(key, 1);
assert_eq!(map.get(&lookup), Some(&1));
}
#[test]
fn oversized_lookup_is_rejected() {
let mut map = AsciiMap::new();
map.insert("GET".to_string(), 1);
assert_eq!(map.get(&"a".repeat(1 << 20)), None);
assert_eq!(map.get("get"), Some(&1));
}
#[test]
fn empty_map_rejects_any_lookup() {
let map: AsciiMap<i32> = AsciiMap::new();
assert_eq!(map.get(""), None);
assert_eq!(map.get("anything"), None);
}
}