use super::Tag;
use crate::base::{Bytes, BytesCow, HasReplacementsError, Range};
use encoding_rs::Encoding;
use std::fmt;
#[derive(PartialEq, Eq, Copy, Clone, Default, Hash)]
pub struct LocalNameHash(u64);
const EMPTY_HASH: u64 = !0;
impl LocalNameHash {
#[inline]
#[must_use]
pub const fn new() -> Self {
Self(0)
}
#[inline]
#[must_use]
pub const fn is_empty(&self) -> bool {
self.0 == EMPTY_HASH
}
#[inline]
pub fn update(&mut self, ch: u8) {
let h = self.0;
self.0 = if h >> (64 - 5) == 0 {
match ch {
b'a'..=b'z' | b'A'..=b'Z' => (h << 5) | ((u64::from(ch) & 0x1F) + 5),
b'1'..=b'6' => (h << 5) | ((u64::from(ch) & 0x0F) - 1),
_ => EMPTY_HASH,
}
} else {
EMPTY_HASH
};
}
}
impl fmt::Debug for LocalNameHash {
#[cold]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_empty() {
return f.write_str("N/A");
}
let mut reverse_buf = [0u8; 12];
let mut pos = 11;
let mut h = self.0;
loop {
reverse_buf[pos] = match (h & 31) as u8 {
v @ 6.. => v + (b'a' - 6),
v => v + b'1',
};
h >>= 5;
if h == 0 || pos == 0 {
break;
}
pos -= 1;
}
std::str::from_utf8(&reverse_buf[pos..])
.unwrap_or_default()
.fmt(f)
}
}
impl From<&str> for LocalNameHash {
#[inline]
fn from(string: &str) -> Self {
let mut hash = Self::new();
for ch in string.bytes() {
hash.update(ch);
}
hash
}
}
impl PartialEq<Tag> for LocalNameHash {
#[inline]
fn eq(&self, tag: &Tag) -> bool {
self.0 == *tag as u64
}
}
#[derive(Clone, Debug, Eq, Hash)]
pub enum LocalName<'i> {
Hash(LocalNameHash),
Bytes(BytesCow<'i>),
}
impl<'i> LocalName<'i> {
#[inline]
#[must_use]
pub(crate) fn new(input: Bytes<'i>, range: Range, hash: LocalNameHash) -> Self {
if hash.is_empty() {
LocalName::Bytes(input.slice(range).into())
} else {
LocalName::Hash(hash)
}
}
#[inline]
#[must_use]
pub fn into_owned(self) -> LocalName<'static> {
match self {
LocalName::Bytes(b) => LocalName::Bytes(b.into_owned()),
LocalName::Hash(h) => LocalName::Hash(h),
}
}
#[inline]
pub fn from_str_without_replacements<'s>(
string: &'s str,
encoding: &'static Encoding,
) -> Result<LocalName<'s>, HasReplacementsError> {
let hash = LocalNameHash::from(string);
if hash.is_empty() {
BytesCow::from_str_without_replacements(string, encoding).map(LocalName::Bytes)
} else {
Ok(LocalName::Hash(hash))
}
}
}
impl PartialEq<Tag> for LocalName<'_> {
#[inline]
fn eq(&self, tag: &Tag) -> bool {
match self {
LocalName::Hash(h) => h == tag,
LocalName::Bytes(_) => false,
}
}
}
impl PartialEq<LocalName<'_>> for LocalName<'_> {
#[inline]
fn eq(&self, other: &LocalName<'_>) -> bool {
use LocalName::{Bytes, Hash};
match (self, other) {
(Hash(s), Hash(o)) => {
debug_assert!(!s.is_empty());
s == o
}
(Bytes(s), Bytes(o)) => s.eq_ignore_ascii_case(o),
_ => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_str() {
assert_eq!(LocalNameHash::from("div"), LocalNameHash(9691u64));
}
#[test]
fn hash_invalidation_for_non_ascii_chars() {
assert!(LocalNameHash::from("div@&").is_empty());
}
#[test]
fn hash_invalidation_for_long_values() {
assert!(LocalNameHash::from("aaaaaaaaaaaaaa").is_empty());
}
}