use std::borrow::Cow;
use pdfrum_common::hex_digit;
use crate::string::decode_text;
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Name(Cow<'static, [u8]>);
impl Name {
#[must_use]
pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
Self(Cow::Owned(bytes.into()))
}
#[must_use]
pub const fn from_static(bytes: &'static [u8]) -> Self {
Self(Cow::Borrowed(bytes))
}
#[must_use]
pub fn decode(raw: &[u8]) -> Self {
Self(Cow::Owned(name_decode(raw)))
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
#[must_use]
pub fn as_str(&self) -> Option<&str> {
std::str::from_utf8(&self.0).ok()
}
#[must_use]
pub fn as_text(&self) -> Cow<'_, str> {
decode_text(&self.0)
}
#[must_use]
pub fn encode(&self) -> Vec<u8> {
let mut out = vec![b'/'];
out.extend_from_slice(&name_encode(&self.0));
out
}
}
impl std::fmt::Debug for Name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", String::from_utf8_lossy(&self.encode()))
}
}
impl AsRef<[u8]> for Name {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl From<&[u8]> for Name {
fn from(bytes: &[u8]) -> Self {
Self::new(bytes.to_vec())
}
}
impl From<&str> for Name {
fn from(s: &str) -> Self {
Self::new(s.as_bytes().to_vec())
}
}
const fn is_pdf_whitespace(b: u8) -> bool {
matches!(b, 0x00 | 0x09 | 0x0A | 0x0C | 0x0D | 0x20)
}
const fn is_pdf_delimiter(b: u8) -> bool {
matches!(
b,
b'%' | b'(' | b')' | b'/' | b'<' | b'>' | b'[' | b']' | b'{' | b'}'
)
}
#[must_use]
pub fn name_decode(raw: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(raw.len());
let mut i = 0;
while let Some(&b) = raw.get(i) {
if let (b'#', Some(&hi), Some(&lo)) = (b, raw.get(i + 1), raw.get(i + 2)) {
let digit = |b: u8| hex_digit(b).unwrap_or(0);
out.push(digit(hi).wrapping_mul(16).wrapping_add(digit(lo)));
i += 3;
} else {
out.push(b);
i += 1;
}
}
out
}
#[must_use]
pub fn name_encode(bytes: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(bytes.len());
for b in bytes {
if *b >= 0x80 || is_pdf_whitespace(*b) || *b == b'#' || is_pdf_delimiter(*b) {
out.push(b'#');
out.extend_from_slice(&hex_pair(*b));
} else {
out.push(*b);
}
}
out
}
pub(crate) const fn hex_pair(b: u8) -> [u8; 2] {
const fn digit(nibble: u8) -> u8 {
match nibble {
0..=9 => b'0' + nibble,
_ => b'A' + nibble - 10,
}
}
[digit(b >> 4), digit(b & 0x0F)]
}
#[macro_export]
macro_rules! names {
($($(#[$meta:meta])* $konst:ident = $spelling:literal;)*) => {
$(
$(#[$meta])*
pub const $konst: &$crate::Name =
&$crate::Name::from_static($spelling.as_bytes());
)*
};
}
#[cfg(test)]
mod tests {
use super::{Name, name_decode, name_encode};
#[test]
fn name_decode_needs_a_full_escape() {
assert_eq!(name_decode(b""), b"");
assert_eq!(name_decode(b"A"), b"A");
assert_eq!(name_decode(b"#"), b"#");
assert_eq!(name_decode(b"#4"), b"#4");
assert_eq!(name_decode(b"#41"), b"A");
assert_eq!(name_decode(b"#411"), b"A1");
}
#[test]
fn name_decode_treats_non_hex_as_zero() {
assert_eq!(name_decode(b"#zz9"), b"\x009");
assert_eq!(name_decode(b"#4z9"), b"\x409");
}
#[test]
fn name_encode_escapes_the_grammar_bytes() {
assert_eq!(name_encode(b""), b"");
assert_eq!(name_encode(b"A"), b"A");
assert_eq!(name_encode(b"#"), b"#23");
assert_eq!(name_encode(b" "), b"#20");
assert_eq!(
name_encode(b"!@#$%^&*()<>[]"),
b"!@#23$#25^&*#28#29#3C#3E#5B#5D"
);
assert_eq!(name_encode(b"\xc2"), b"#C2");
assert_eq!(name_encode(b"f\xc2\xa5"), b"f#C2#A5");
}
#[test]
fn spellings_unify_after_decoding() {
assert_eq!(Name::decode(b"A#42"), Name::decode(b"AB"));
assert_eq!(Name::decode(b"Lengt#68").as_str(), Some("Length"));
}
#[test]
fn static_and_owned_names_compare_equal() {
assert_eq!(Name::from_static(b"Type"), Name::from("Type"));
}
#[test]
fn encode_round_trips_through_decode() {
for name in [&b"Length"[..], b"a b", b"#", b"\xFF\x00", b"(x)"] {
let encoded = name_encode(name);
assert_eq!(name_decode(&encoded), name, "round trip of {name:?}");
}
}
#[test]
fn name_encode_includes_the_slash() {
assert_eq!(Name::from("Length").encode(), b"/Length");
assert_eq!(Name::from("a b").encode(), b"/a#20b");
}
}