use std::fmt;
use std::str::FromStr;
use anyhow::{Context, Result, bail};
use serde::{Serialize, Serializer};
pub(crate) const HASHLINE_MODULUS: u32 = 0x1000;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(crate) struct LineHash(u16);
impl LineHash {
pub(crate) fn new(value: u16) -> Result<Self> {
if u32::from(value) >= HASHLINE_MODULUS {
bail!("line hash {value:#x} is out of range");
}
Ok(Self(value))
}
pub(crate) fn as_u16(self) -> u16 {
self.0
}
}
impl fmt::Display for LineHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:03x}", self.0)
}
}
impl FromStr for LineHash {
type Err = anyhow::Error;
fn from_str(value: &str) -> Result<Self> {
let raw = u16::from_str_radix(value, 16)
.with_context(|| format!("invalid line hash `{value}`"))?;
Self::new(raw)
}
}
impl Serialize for LineHash {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
pub(crate) fn hash_line(text: &str) -> LineHash {
let text = text.strip_suffix('\r').unwrap_or(text);
let mut hasher = xxhash_rust::xxh32::Xxh32::new(0);
for token in text.split_whitespace() {
hasher.update(token.as_bytes());
}
let digest = hasher.digest() % HASHLINE_MODULUS;
LineHash(u16::try_from(digest).unwrap_or_default())
}
pub(crate) fn hash_text(text: &str) -> String {
blake3::hash(text.as_bytes()).to_string()
}
pub(crate) fn hash_bytes(bytes: &[u8]) -> String {
blake3::hash(bytes).to_string()
}
pub(crate) fn is_hex(s: &str, expected_len: Option<usize>) -> bool {
if expected_len.is_some_and(|len| s.len() != len) {
return false;
}
!s.is_empty() && s.bytes().all(|b| b.is_ascii_hexdigit())
}
pub(crate) fn decode_hex<const N: usize>(s: &str) -> Result<[u8; N]> {
if s.len() != N * 2 {
bail!("invalid hex length {}, expected {}", s.len(), N * 2);
}
let mut out = [0u8; N];
for (i, chunk) in s.as_bytes().chunks_exact(2).enumerate() {
let h = match chunk[0] {
b'0'..=b'9' => chunk[0] - b'0',
b'a'..=b'f' => chunk[0] - b'a' + 10,
b'A'..=b'F' => chunk[0] - b'A' + 10,
_ => bail!("invalid hex character"),
};
let l = match chunk[1] {
b'0'..=b'9' => chunk[1] - b'0',
b'a'..=b'f' => chunk[1] - b'a' + 10,
b'A'..=b'F' => chunk[1] - b'A' + 10,
_ => bail!("invalid hex character"),
};
out[i] = (h << 4) | l;
}
Ok(out)
}