use std::borrow::Cow;
use pdfrum_common::{Limits, hex_digit};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CharClass {
Whitespace,
Numeric,
Delimiter,
Regular,
}
#[must_use]
pub const fn class_of(byte: u8) -> CharClass {
match byte {
0x00 | 0x09 | 0x0A | 0x0C | 0x0D | 0x20 | 0x80 | 0xFF => CharClass::Whitespace,
b'0'..=b'9' | b'+' | b'-' | b'.' => CharClass::Numeric,
b'%' | b'(' | b')' | b'/' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' => CharClass::Delimiter,
_ => CharClass::Regular,
}
}
#[must_use]
pub fn is_whitespace(byte: u8) -> bool {
class_of(byte) == CharClass::Whitespace
}
#[must_use]
pub fn is_numeric(byte: u8) -> bool {
class_of(byte) == CharClass::Numeric
}
#[must_use]
pub fn is_delimiter(byte: u8) -> bool {
class_of(byte) == CharClass::Delimiter
}
#[must_use]
pub fn is_line_ending(byte: u8) -> bool {
byte == b'\r' || byte == b'\n'
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Token<'a> {
Number(&'a [u8]),
Name(&'a [u8]),
Keyword(&'a [u8]),
Delim(Delim),
Eof,
}
impl<'a> Token<'a> {
#[must_use]
pub fn bytes(&self) -> &'a [u8] {
match self {
Self::Number(b) | Self::Name(b) | Self::Keyword(b) => b,
Self::Delim(d) => d.as_bytes(),
Self::Eof => b"",
}
}
#[must_use]
pub fn is_number(&self) -> bool {
matches!(self, Self::Number(_))
}
#[must_use]
pub fn is_eof(&self) -> bool {
matches!(self, Self::Eof)
}
#[must_use]
pub fn is_keyword(&self, word: &[u8]) -> bool {
matches!(self, Self::Keyword(b) if *b == word)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Delim {
ArrayOpen,
ArrayClose,
DictOpen,
DictClose,
HexOpen,
HexClose,
StringOpen,
StringClose,
BraceOpen,
BraceClose,
Percent,
}
impl Delim {
#[must_use]
pub fn as_bytes(self) -> &'static [u8] {
match self {
Self::ArrayOpen => b"[",
Self::ArrayClose => b"]",
Self::DictOpen => b"<<",
Self::DictClose => b">>",
Self::HexOpen => b"<",
Self::HexClose => b">",
Self::StringOpen => b"(",
Self::StringClose => b")",
Self::BraceOpen => b"{",
Self::BraceClose => b"}",
Self::Percent => b"%",
}
}
}
#[derive(Debug, Clone)]
pub struct Lexer<'a> {
bytes: &'a [u8],
pos: usize,
}
impl<'a> Lexer<'a> {
#[must_use]
pub fn new(bytes: &'a [u8]) -> Self {
Self { bytes, pos: 0 }
}
#[must_use]
pub fn at(bytes: &'a [u8], pos: usize) -> Self {
Self {
bytes,
pos: pos.min(bytes.len()),
}
}
#[must_use]
pub fn bytes(&self) -> &'a [u8] {
self.bytes
}
#[must_use]
pub fn pos(&self) -> usize {
self.pos
}
pub fn seek(&mut self, pos: usize) {
self.pos = pos.min(self.bytes.len());
}
#[must_use]
pub fn at_eof(&self) -> bool {
self.pos >= self.bytes.len()
}
#[must_use]
pub fn peek_byte(&self) -> Option<u8> {
self.bytes.get(self.pos).copied()
}
fn read_byte(&mut self) -> Option<u8> {
let b = self.bytes.get(self.pos).copied()?;
self.pos += 1;
Some(b)
}
fn unread(&mut self) {
self.pos = self.pos.saturating_sub(1);
}
pub fn skip_to_word(&mut self) {
while let Some(b) = self.peek_byte() {
if is_whitespace(b) {
self.pos += 1;
} else if b == b'%' {
self.skip_comment();
} else {
return;
}
}
}
fn skip_comment(&mut self) {
while let Some(b) = self.peek_byte() {
if is_line_ending(b) {
return;
}
self.pos += 1;
}
}
pub fn to_next_line(&mut self) {
while let Some(b) = self.read_byte() {
if b == b'\n' {
return;
}
if b == b'\r' {
if self.peek_byte() == Some(b'\n') {
self.pos += 1;
}
return;
}
}
}
pub fn skip_eol_marker(&mut self) -> usize {
match self.peek_byte() {
Some(b'\r') => {
self.pos += 1;
if self.peek_byte() == Some(b'\n') {
self.pos += 1;
2
} else {
1
}
}
Some(b'\n') => {
self.pos += 1;
1
}
_ => 0,
}
}
pub fn next_word(&mut self, limits: &Limits) -> Token<'a> {
self.skip_to_word();
let Some(first) = self.read_byte() else {
return Token::Eof;
};
if is_delimiter(first) {
return self.delimiter_token(first, limits);
}
let start = self.pos - 1;
let mut all_numeric = is_numeric(first);
while let Some(b) = self.read_byte() {
if is_whitespace(b) || is_delimiter(b) {
self.unread();
break;
}
all_numeric &= is_numeric(b);
}
let word = truncate(self.bytes.get(start..self.pos).unwrap_or_default(), limits);
if all_numeric {
Token::Number(word)
} else {
Token::Keyword(word)
}
}
fn delimiter_token(&mut self, first: u8, limits: &Limits) -> Token<'a> {
match first {
b'/' => {
let start = self.pos;
while let Some(b) = self.peek_byte() {
if matches!(class_of(b), CharClass::Regular | CharClass::Numeric) {
self.pos += 1;
} else {
break;
}
}
let payload = self.bytes.get(start..self.pos).unwrap_or_default();
let budget = limits.max_word_len.saturating_sub(1);
Token::Name(payload.get(..budget).unwrap_or(payload))
}
b'<' => {
if self.peek_byte() == Some(b'<') {
self.pos += 1;
Token::Delim(Delim::DictOpen)
} else {
Token::Delim(Delim::HexOpen)
}
}
b'>' => {
if self.peek_byte() == Some(b'>') {
self.pos += 1;
Token::Delim(Delim::DictClose)
} else {
Token::Delim(Delim::HexClose)
}
}
b'[' => Token::Delim(Delim::ArrayOpen),
b']' => Token::Delim(Delim::ArrayClose),
b'(' => Token::Delim(Delim::StringOpen),
b')' => Token::Delim(Delim::StringClose),
b'{' => Token::Delim(Delim::BraceOpen),
b'}' => Token::Delim(Delim::BraceClose),
_ => Token::Delim(Delim::Percent),
}
}
pub fn peek_word(&mut self, limits: &Limits) -> Token<'a> {
let saved = self.pos;
let token = self.next_word(limits);
self.pos = saved;
token
}
pub fn read_literal_string(&mut self) -> Cow<'a, [u8]> {
let start = self.pos;
let mut out: Option<Vec<u8>> = None;
let mut depth: u32 = 0;
let mut verbatim_end = start;
while let Some(b) = self.read_byte() {
match b {
b'(' => {
depth += 1;
push(&mut out, verbatim_end, b);
verbatim_end = self.pos;
}
b')' => {
if depth == 0 {
return finish(self.bytes, start, verbatim_end, out);
}
depth -= 1;
push(&mut out, verbatim_end, b);
verbatim_end = self.pos;
}
b'\\' => {
let buf = out.get_or_insert_with(|| {
self.bytes
.get(start..verbatim_end)
.unwrap_or_default()
.to_vec()
});
self.read_escape(buf);
verbatim_end = self.pos;
}
_ => {
push(&mut out, verbatim_end, b);
verbatim_end = self.pos;
}
}
}
finish(self.bytes, start, verbatim_end, out)
}
fn read_escape(&mut self, out: &mut Vec<u8>) {
let Some(b) = self.read_byte() else { return };
match b {
b'n' => out.push(b'\n'),
b'r' => out.push(b'\r'),
b't' => out.push(b'\t'),
b'b' => out.push(0x08),
b'f' => out.push(0x0C),
b'\r' => {
if self.peek_byte() == Some(b'\n') {
self.pos += 1;
}
}
b'\n' => {}
b'0'..=b'7' => {
let mut value: u32 = u32::from(b - b'0');
for _ in 0..2 {
match self.peek_byte() {
Some(d @ b'0'..=b'7') => {
self.pos += 1;
value = value * 8 + u32::from(d - b'0');
}
_ => break,
}
}
out.push(u8::try_from(value & 0xFF).unwrap_or(0));
}
other => out.push(other),
}
}
pub fn read_hex_string(&mut self) -> Vec<u8> {
let mut out = Vec::new();
let mut high: Option<u8> = None;
while let Some(b) = self.read_byte() {
if b == b'>' {
break;
}
let Some(nibble) = hex_digit(b) else { continue };
match high.take() {
None => high = Some(nibble),
Some(h) => out.push((h << 4) | nibble),
}
}
if let Some(h) = high {
out.push(h << 4);
}
out
}
pub fn search_back(&mut self, word: &[u8], window: usize) -> bool {
if word.is_empty() || self.pos + 1 < word.len() {
return false;
}
let limit = self.pos.saturating_sub(window);
let mut candidate = (self.pos + 1).saturating_sub(word.len());
loop {
if self.bytes.get(candidate..candidate + word.len()) == Some(word)
&& is_whole_word(
self.bytes,
candidate,
word.len(),
WordBoundary::WhitespaceOrDelimiter,
)
{
self.pos = candidate;
return true;
}
if candidate == 0 || candidate <= limit {
return false;
}
candidate -= 1;
}
}
}
fn push(out: &mut Option<Vec<u8>>, _verbatim_end: usize, b: u8) {
if let Some(buf) = out {
buf.push(b);
}
}
fn finish(bytes: &[u8], start: usize, verbatim_end: usize, out: Option<Vec<u8>>) -> Cow<'_, [u8]> {
match out {
Some(buf) => Cow::Owned(buf),
None => Cow::Borrowed(bytes.get(start..verbatim_end).unwrap_or_default()),
}
}
fn truncate<'a>(word: &'a [u8], limits: &Limits) -> &'a [u8] {
word.get(..limits.max_word_len).unwrap_or(word)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WordBoundary {
WhitespaceOnly,
WhitespaceOrDelimiter,
}
impl WordBoundary {
#[must_use]
fn accepts(self, b: u8) -> bool {
match self {
Self::WhitespaceOnly => is_whitespace(b),
Self::WhitespaceOrDelimiter => {
!matches!(class_of(b), CharClass::Regular | CharClass::Numeric)
}
}
}
}
#[must_use]
pub fn is_whole_word(bytes: &[u8], pos: usize, len: usize, rule: WordBoundary) -> bool {
let boundary = |b: u8| rule.accepts(b);
if pos > 0 && !bytes.get(pos - 1).copied().is_some_and(boundary) {
return false;
}
match bytes.get(pos + len) {
None => true,
Some(&b) => boundary(b),
}
}
#[must_use]
pub fn find_word(bytes: &[u8], word: &[u8], from: usize, rule: WordBoundary) -> Option<usize> {
if word.is_empty() || from > bytes.len() {
return None;
}
let last = bytes.len().checked_sub(word.len())?;
(from..=last).find(|&i| {
bytes.get(i..i + word.len()) == Some(word) && is_whole_word(bytes, i, word.len(), rule)
})
}
#[must_use]
pub fn atoui(word: &[u8]) -> u32 {
let (negative, digits) = match word.split_first() {
Some((b'-', rest)) => (true, rest),
Some((b'+', rest)) => (false, rest),
_ => (false, word),
};
let mut value: u32 = 0;
for &b in digits {
let Some(d) = (b as char).to_digit(10) else {
break;
};
value = match value.checked_mul(10).and_then(|v| v.checked_add(d)) {
Some(v) => v,
None => return u32::MAX,
};
}
if negative {
(!value).wrapping_add(1)
} else {
value
}
}
#[must_use]
pub fn atoi64(word: &[u8]) -> i64 {
let (negative, digits) = match word.split_first() {
Some((b'-', rest)) => (true, rest),
Some((b'+', rest)) => (false, rest),
_ => (false, word),
};
let mut value: i64 = 0;
for &b in digits {
let Some(d) = (b as char).to_digit(10) else {
break;
};
value = match value
.checked_mul(10)
.and_then(|v| v.checked_add(i64::from(d)))
{
Some(v) => v,
None => return if negative { i64::MIN } else { i64::MAX },
};
}
if negative { -value } else { value }
}
#[cfg(test)]
mod tests {
use super::{
CharClass, Delim, Lexer, Token, WordBoundary, atoi64, atoui, class_of, find_word,
is_whole_word,
};
use pdfrum_common::Limits;
fn limits() -> Limits {
Limits::default()
}
#[test]
fn classifies_the_two_pdfium_quirks() {
assert_eq!(class_of(0x80), CharClass::Whitespace);
assert_eq!(class_of(0xFF), CharClass::Whitespace);
assert_eq!(class_of(0x0B), CharClass::Regular);
assert_eq!(class_of(b'.'), CharClass::Numeric);
assert_eq!(class_of(b'%'), CharClass::Delimiter);
}
#[test]
fn high_bytes_separate_words() {
let bytes = [b'a', 0x80, b'b', 0xFF, b'c'];
let mut lx = Lexer::new(&bytes);
assert_eq!(lx.next_word(&limits()), Token::Keyword(b"a"));
assert_eq!(lx.next_word(&limits()), Token::Keyword(b"b"));
assert_eq!(lx.next_word(&limits()), Token::Keyword(b"c"));
assert!(lx.next_word(&limits()).is_eof());
}
#[test]
fn vertical_tab_stays_inside_a_name() {
let bytes = [b'/', b'a', 0x0B, b'b', b' '];
let mut lx = Lexer::new(&bytes);
assert_eq!(lx.next_word(&limits()), Token::Name(&[b'a', 0x0B, b'b']));
}
#[test]
fn number_tokens_are_shape_not_value() {
let mut lx = Lexer::new(b"--37 1.2.3 +-. 12a");
assert_eq!(lx.next_word(&limits()), Token::Number(b"--37"));
assert_eq!(lx.next_word(&limits()), Token::Number(b"1.2.3"));
assert_eq!(lx.next_word(&limits()), Token::Number(b"+-."));
assert_eq!(lx.next_word(&limits()), Token::Keyword(b"12a"));
}
#[test]
fn delimiters_pair_and_push_back() {
let mut lx = Lexer::new(b"<</a[1]>>><");
assert_eq!(lx.next_word(&limits()), Token::Delim(Delim::DictOpen));
assert_eq!(lx.next_word(&limits()), Token::Name(b"a"));
assert_eq!(lx.next_word(&limits()), Token::Delim(Delim::ArrayOpen));
assert_eq!(lx.next_word(&limits()), Token::Number(b"1"));
assert_eq!(lx.next_word(&limits()), Token::Delim(Delim::ArrayClose));
assert_eq!(lx.next_word(&limits()), Token::Delim(Delim::DictClose));
assert_eq!(lx.next_word(&limits()), Token::Delim(Delim::HexClose));
assert_eq!(lx.next_word(&limits()), Token::Delim(Delim::HexOpen));
}
#[test]
fn a_bare_slash_is_the_empty_name() {
let mut lx = Lexer::new(b"/ /Name/Other");
assert_eq!(lx.next_word(&limits()), Token::Name(b""));
assert_eq!(lx.next_word(&limits()), Token::Name(b"Name"));
assert_eq!(lx.next_word(&limits()), Token::Name(b"Other"));
}
#[test]
fn comments_vanish() {
let mut lx = Lexer::new(b"% one\n%two\n 42");
assert_eq!(lx.next_word(&limits()), Token::Number(b"42"));
}
#[test]
fn two_long_names_collide_one_byte_sooner_than_keywords() {
let name = |tail: u8| {
let mut v = vec![b'/'];
v.extend(std::iter::repeat_n(b'a', 255));
v.push(tail);
v.push(b' ');
v
};
let (x, y) = (name(b'x'), name(b'y'));
assert_eq!(
Lexer::new(&x).next_word(&limits()),
Lexer::new(&y).next_word(&limits())
);
}
#[test]
fn words_truncate_at_the_limit() {
let long = vec![b'a'; 300];
let mut source = long.clone();
source.push(b' ');
source.push(b'z');
let mut lx = Lexer::new(&source);
let token = lx.next_word(&limits());
assert_eq!(token.bytes().len(), 256);
assert_eq!(lx.next_word(&limits()), Token::Keyword(b"z"));
}
#[test]
fn names_truncate_one_byte_sooner_than_keywords() {
let mut source = vec![b'/'];
source.extend(std::iter::repeat_n(b'x', 300));
let mut lx = Lexer::new(&source);
assert_eq!(lx.next_word(&limits()).bytes().len(), 255);
}
#[test]
fn peek_is_position_neutral() {
let mut lx = Lexer::new(b" hello world");
let before = lx.pos();
assert_eq!(lx.peek_word(&limits()), Token::Keyword(b"hello"));
assert_eq!(lx.pos(), before);
assert_eq!(lx.next_word(&limits()), Token::Keyword(b"hello"));
}
#[test]
fn literal_string_escapes() {
let cases: &[(&[u8], &[u8])] = &[
(b"abc)", b"abc"),
(b"a(b)c)", b"a(b)c"),
(b"\\n\\r\\t\\b\\f)", b"\n\r\t\x08\x0C"),
(b"\\101)", b"A"),
(b"\\777)", b"\xFF"),
(b"\\(\\)\\\\)", b"()\\"),
(b"a\\\nb)", b"ab"),
(b"a\\\r\nb)", b"ab"),
(b"a\\\rb)", b"ab"),
(b"\\q)", b"q"),
(b"abc", b"abc"),
];
for (input, expected) in cases {
let mut lx = Lexer::new(input);
assert_eq!(&*lx.read_literal_string(), *expected, "input {input:?}");
}
}
#[test]
fn literal_string_borrows_when_it_can() {
let mut lx = Lexer::new(b"plain)");
assert!(matches!(
lx.read_literal_string(),
std::borrow::Cow::Borrowed(_)
));
}
#[test]
fn hex_string_skips_everything_it_does_not_understand() {
let cases: &[(&[u8], &[u8], usize)] = &[
(b"1A2b>abcd", b"\x1a\x2b", 5),
(b"1A2>abcd", b"\x1a\x20", 4),
(b"z12b>abcd", b"\x12\xb0", 5),
(b"*<&*#$^&@1>abcd", b"\x10", 11),
(b"\x00z12b>", b"\x12\xb0", 6),
(b"12&%^*b>", b"\x12\xb0", 8),
(b"1A2b", b"\x1a\x2b", 4),
(b"1A2", b"\x1a\x20", 3),
(b"", b"", 0),
(b">", b"", 1),
];
for (input, expected, end) in cases {
let mut lx = Lexer::new(input);
assert_eq!(&lx.read_hex_string(), expected, "input {input:?}");
assert_eq!(lx.pos(), *end, "end position for {input:?}");
}
}
#[test]
fn to_next_line_treats_crlf_as_one() {
let mut lx = Lexer::new(b"abc\r\ndef");
lx.to_next_line();
assert_eq!(lx.pos(), 5);
let mut lx = Lexer::new(b"abc\rdef");
lx.to_next_line();
assert_eq!(lx.pos(), 4);
let mut lx = Lexer::new(b"abc\ndef");
lx.to_next_line();
assert_eq!(lx.pos(), 4);
let mut lx = Lexer::new(b"abc");
lx.to_next_line();
assert_eq!(lx.pos(), 3);
}
#[test]
fn eol_markers_count_their_bytes() {
assert_eq!(Lexer::new(b"\r\nx").skip_eol_marker(), 2);
assert_eq!(Lexer::new(b"\rx").skip_eol_marker(), 1);
assert_eq!(Lexer::new(b"\nx").skip_eol_marker(), 1);
assert_eq!(Lexer::new(b"x").skip_eol_marker(), 0);
}
#[test]
fn whole_word_boundaries_differ_by_strictness() {
let bytes = b">>endstream ";
assert!(!is_whole_word(bytes, 2, 9, WordBoundary::WhitespaceOnly));
assert!(is_whole_word(
bytes,
2,
9,
WordBoundary::WhitespaceOrDelimiter
));
}
#[test]
fn find_word_respects_the_keyword_rule() {
let bytes = b"x >>endstream y endstream z";
assert_eq!(
find_word(bytes, b"endstream", 0, WordBoundary::WhitespaceOnly),
Some(16)
);
assert_eq!(
find_word(bytes, b"endstream", 0, WordBoundary::WhitespaceOrDelimiter),
Some(4)
);
assert_eq!(
find_word(bytes, b"nothere", 0, WordBoundary::WhitespaceOnly),
None
);
}
#[test]
fn search_back_finds_the_last_occurrence() {
let bytes = b"startxref 1\nstartxref 2\n";
let mut lx = Lexer::at(bytes, bytes.len());
assert!(lx.search_back(b"startxref", 4096));
assert_eq!(lx.pos(), 12);
}
#[test]
fn search_back_includes_the_byte_under_the_cursor() {
let file = b"%PDF-1.7\nstartxref 1234567";
let start = 9;
let cursor = file.len() - 9;
assert_eq!(file.get(start..start + 9), Some(&b"startxref"[..]));
assert_eq!(start + 8, cursor);
let mut lx = Lexer::at(file, cursor);
assert!(lx.search_back(b"startxref", 4096));
assert_eq!(lx.pos(), start);
}
#[test]
fn search_back_declines_a_word_that_does_not_fit() {
let mut lx = Lexer::at(b"xref", 1);
assert!(!lx.search_back(b"startxref", 4096));
assert_eq!(lx.pos(), 1);
let mut lx = Lexer::at(b"abc", 2);
assert!(lx.search_back(b"abc", 4096));
assert_eq!(lx.pos(), 0);
}
#[test]
fn atoui_saturates_and_negates() {
assert_eq!(atoui(b"0"), 0);
assert_eq!(atoui(b"42"), 42);
assert_eq!(atoui(b"4294967295"), u32::MAX);
assert_eq!(atoui(b"99999999999"), u32::MAX);
assert_eq!(atoui(b"-1"), u32::MAX);
assert_eq!(atoui(b"-2"), u32::MAX - 1);
assert_eq!(atoui(b"12a34"), 12);
assert_eq!(atoui(b""), 0);
}
#[test]
fn atoi64_saturates() {
assert_eq!(atoi64(b"-5"), -5);
assert_eq!(atoi64(b"100940"), 100_940);
assert_eq!(atoi64(b"999999999999999999999"), i64::MAX);
}
#[test]
fn never_panics_on_arbitrary_bytes() {
for seed in 0u8..=255 {
let bytes: Vec<u8> = (0..64u8)
.map(|i| i.wrapping_mul(7).wrapping_add(seed))
.collect();
let mut lx = Lexer::new(&bytes);
for _ in 0..200 {
if lx.next_word(&limits()).is_eof() {
break;
}
}
}
}
}