Skip to main content

vole_document/adapter/pdf/
cos.rs

1//! Minimal, conservative PDF COS token helpers over the lexical cover.
2//!
3//! These routines never re-parse strings or invent tokens: every decision is
4//! made from the existing [`Span`] partition produced by [`super::lexer`]. They
5//! recognise only the simple token shapes needed to resolve a stream `/Length`
6//! and to read a bare-integer object body. Anything ambiguous returns `None` so
7//! callers fall back to a conservative keyword search rather than guessing.
8//!
9//! The functions are deliberately narrow. A dictionary is identified by byte
10//! range `[dict_lo, dict_hi)`; a `/Length` key is a `Name` span whose bytes are
11//! exactly `/Length`, and its value is either a single `Regular` decimal integer
12//! or the reference shape `int Whitespace int Whitespace R`. Separators between
13//! tokens may be whitespace or comments, exactly as PDF tokenisation treats them.
14
15use super::span::{Span, SpanKind};
16
17/// A resolved stream `/Length` value.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum LengthValue {
20    /// `/Length N`: a direct integer.
21    Direct(u64),
22    /// `/Length N G R`: an indirect reference to object `N`, generation `G`.
23    Indirect { number: u64, generation: u64 },
24}
25
26/// Find the value of key `/Length` within the dictionary byte range
27/// `[dict_lo, dict_hi)`.
28///
29/// Returns `None` if the key is absent or its value is not a simple integer or
30/// `int int R` reference. Only `Regular`/`Name` lexemes are inspected; string
31/// and comment spans can never be mistaken for a key or value.
32pub fn dict_length(input: &[u8], lex: &[Span], dict_lo: u64, dict_hi: u64) -> Option<LengthValue> {
33    let name = find_length_name(input, lex, dict_lo, dict_hi)?;
34    let t0 = next_significant(lex, name + 1, dict_hi)?;
35    let number = integer_of(input, lex[t0])?;
36
37    // The reference shape `int Whitespace int Whitespace R` takes precedence.
38    if let Some(t1) = next_significant(lex, t0 + 1, dict_hi)
39        && let Some(generation) = integer_of(input, lex[t1])
40        && let Some(t2) = next_significant(lex, t1 + 1, dict_hi)
41        && is_regular_r(input, lex[t2])
42    {
43        return Some(LengthValue::Indirect { number, generation });
44    }
45
46    Some(LengthValue::Direct(number))
47}
48
49/// Whether a `/Length` key is present in the dictionary range at all, even if
50/// its value is malformed. Lets callers distinguish "absent" from "present but
51/// unusable" when choosing a [`super::physical::LengthSource`].
52pub fn dict_has_length(input: &[u8], lex: &[Span], dict_lo: u64, dict_hi: u64) -> bool {
53    find_length_name(input, lex, dict_lo, dict_hi).is_some()
54}
55
56/// Classification of a stream dictionary's `/Filter` entry.
57///
58/// The exact-replay candidate only admits a stream whose encoded payload is a
59/// lone DEFLATE/zlib stream, i.e. exactly one `FlateDecode` filter. Anything
60/// else — no key, a different filter, a filter chain, or an unrecognized shape —
61/// is conservative `Other`/`Absent` and is never a replay candidate.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum FilterClass {
64    /// The `/Filter` key is absent.
65    Absent,
66    /// Exactly one filter, `FlateDecode` (bare name or a single-element array).
67    FlateDecode,
68    /// Any other value (different filter, chain, empty array, odd shape).
69    Other,
70}
71
72/// Classify `/Filter` within the dictionary range `[dict_lo, dict_hi)`.
73///
74/// Recognizes only the canonical spelling `/FlateDecode` (not the legal PDF name
75/// abbreviations) and only the two shapes `/Filter /FlateDecode` and
76/// `/Filter [/FlateDecode]`. Only `Name` spans and the array `[`/`]` spans are
77/// inspected, so a `/Filter` spelling inside a string or comment cannot match.
78pub fn dict_filter(input: &[u8], lex: &[Span], dict_lo: u64, dict_hi: u64) -> FilterClass {
79    let Some(name) = find_name_key(input, lex, dict_lo, dict_hi, b"Filter") else {
80        return FilterClass::Absent;
81    };
82
83    let is_flate = |sp: Span| {
84        sp.kind == SpanKind::Name
85            && span_bytes(input, sp).is_some_and(|b| b == b"/FlateDecode".as_slice())
86    };
87
88    let Some(t0) = next_significant(lex, name + 1, dict_hi) else {
89        return FilterClass::Other;
90    };
91    if is_flate(lex[t0]) {
92        return FilterClass::FlateDecode;
93    }
94    if lex[t0].kind != SpanKind::ArrayOpen {
95        return FilterClass::Other;
96    }
97
98    let Some(t1) = next_significant(lex, t0 + 1, dict_hi) else {
99        return FilterClass::Other;
100    };
101    if !is_flate(lex[t1]) {
102        return FilterClass::Other;
103    }
104    let Some(t2) = next_significant(lex, t1 + 1, dict_hi) else {
105        return FilterClass::Other;
106    };
107    if lex[t2].kind == SpanKind::ArrayClose {
108        FilterClass::FlateDecode
109    } else {
110        FilterClass::Other
111    }
112}
113
114/// The value of a key whose value is a name (e.g. `/Type /XRef`).
115///
116/// The key is matched as a `Name` span equal to `/<key>` fully inside
117/// `[dict_lo, dict_hi)`; the value must be a `Name` span. Returns the raw bytes
118/// after the value's leading `/` (the slash is not included), or `None` when the
119/// key is absent or its value is not a simple name. Strings and comments can
120/// never be mistaken for a key or value because only `Name` spans are inspected.
121pub fn dict_name_value<'a>(
122    input: &'a [u8],
123    lex: &[Span],
124    dict_lo: u64,
125    dict_hi: u64,
126    key: &[u8],
127) -> Option<&'a [u8]> {
128    let name = find_name_key(input, lex, dict_lo, dict_hi, key)?;
129    let t = next_significant(lex, name + 1, dict_hi)?;
130    let sp = lex[t];
131    if sp.kind != SpanKind::Name {
132        return None;
133    }
134    span_bytes(input, sp)?.strip_prefix(b"/")
135}
136
137/// The value of a key that is either a direct non-negative integer or an
138/// indirect reference of shape `int int R`.
139///
140/// The key is matched exactly as in [`dict_name_value`]. Returns
141/// [`LengthValue::Direct`] for a bare integer, [`LengthValue::Indirect`] for the
142/// reference shape, and `None` when the key is absent or its value is neither.
143/// Only simple token shapes are considered; nothing is re-parsed from strings.
144pub fn dict_int_or_ref(
145    input: &[u8],
146    lex: &[Span],
147    dict_lo: u64,
148    dict_hi: u64,
149    key: &[u8],
150) -> Option<LengthValue> {
151    let name = find_name_key(input, lex, dict_lo, dict_hi, key)?;
152    let t0 = next_significant(lex, name + 1, dict_hi)?;
153    let number = integer_of(input, lex[t0])?;
154
155    // The reference shape `int Whitespace int Whitespace R` takes precedence.
156    if let Some(t1) = next_significant(lex, t0 + 1, dict_hi)
157        && let Some(generation) = integer_of(input, lex[t1])
158        && let Some(t2) = next_significant(lex, t1 + 1, dict_hi)
159        && is_regular_r(input, lex[t2])
160    {
161        return Some(LengthValue::Indirect { number, generation });
162    }
163
164    Some(LengthValue::Direct(number))
165}
166
167/// Interpret the significant tokens of an object body `[body_lo, body_hi)` as a
168/// single bare non-negative decimal integer.
169///
170/// Leading and trailing whitespace/comments are ignored; if anything other than
171/// exactly one `Regular` decimal integer remains, `None` is returned. This is
172/// used for indirect `/Length` targets such as `5 0 obj 12 endobj`.
173pub fn body_as_u64(input: &[u8], lex: &[Span], body_lo: u64, body_hi: u64) -> Option<u64> {
174    let mut only: Option<usize> = None;
175    for (idx, sp) in lex.iter().enumerate() {
176        if sp.start < body_lo {
177            continue;
178        }
179        if sp.start >= body_hi {
180            break;
181        }
182        // A token that straddles the region boundary makes the shape ambiguous.
183        if !sp
184            .start
185            .checked_add(sp.len)
186            .is_some_and(|end| end <= body_hi)
187        {
188            return None;
189        }
190        if matches!(sp.kind, SpanKind::Whitespace | SpanKind::Comment) {
191            continue;
192        }
193        if only.is_some() {
194            return None;
195        }
196        only = Some(idx);
197    }
198    integer_of(input, lex[only?])
199}
200
201/// Index of a `Name` span equal to `/Length` fully inside `[lo, hi)`.
202fn find_length_name(input: &[u8], lex: &[Span], lo: u64, hi: u64) -> Option<usize> {
203    find_name_key(input, lex, lo, hi, b"Length")
204}
205
206/// Index of a `Name` span equal to `/<key>` fully inside `[lo, hi)`.
207fn find_name_key(input: &[u8], lex: &[Span], lo: u64, hi: u64, key: &[u8]) -> Option<usize> {
208    lex.iter().position(|sp| {
209        sp.kind == SpanKind::Name
210            && sp.start >= lo
211            && sp.start.checked_add(sp.len).is_some_and(|end| end <= hi)
212            && span_bytes(input, *sp)
213                .is_some_and(|b| b.len() == key.len() + 1 && b[0] == b'/' && &b[1..] == key)
214    })
215}
216
217/// Index of the next non-whitespace, non-comment span at or after `from` whose
218/// start is before `hi`.
219fn next_significant(lex: &[Span], from: usize, hi: u64) -> Option<usize> {
220    let mut j = from;
221    while j < lex.len() {
222        let sp = lex[j];
223        if sp.start >= hi {
224            return None;
225        }
226        match sp.kind {
227            SpanKind::Whitespace | SpanKind::Comment => j += 1,
228            _ => return Some(j),
229        }
230    }
231    None
232}
233
234/// Parse a `Regular` span as a non-negative decimal integer.
235fn integer_of(input: &[u8], sp: Span) -> Option<u64> {
236    if sp.kind != SpanKind::Regular {
237        return None;
238    }
239    let bytes = span_bytes(input, sp)?;
240    if bytes.is_empty() {
241        return None;
242    }
243    let mut value: u64 = 0;
244    for &b in bytes {
245        if !b.is_ascii_digit() {
246            return None;
247        }
248        value = value.checked_mul(10)?.checked_add(u64::from(b - b'0'))?;
249    }
250    Some(value)
251}
252
253/// Whether `sp` is the `Regular` keyword `R`.
254fn is_regular_r(input: &[u8], sp: Span) -> bool {
255    sp.kind == SpanKind::Regular && span_bytes(input, sp) == Some(b"R".as_slice())
256}
257
258/// The bytes backing `sp`, or `None` if the offset is out of range.
259fn span_bytes(input: &[u8], sp: Span) -> Option<&[u8]> {
260    let start = usize::try_from(sp.start).ok()?;
261    let end = usize::try_from(sp.start.checked_add(sp.len)?).ok()?;
262    if start > end || end > input.len() {
263        return None;
264    }
265    Some(&input[start..end])
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271    use crate::adapter::pdf::lexer::lex;
272    use crate::limits::Limits;
273
274    fn lexed(input: &[u8]) -> Vec<Span> {
275        lex(input, Limits::DEFAULT)
276            .expect("lex must succeed")
277            .spans
278            .spans
279    }
280
281    #[test]
282    fn direct_length_is_read() {
283        let input = b"<< /Length 42 >>";
284        let spans = lexed(input);
285        assert_eq!(
286            dict_length(input, &spans, 0, input.len() as u64),
287            Some(LengthValue::Direct(42))
288        );
289        assert!(dict_has_length(input, &spans, 0, input.len() as u64));
290    }
291
292    #[test]
293    fn indirect_length_is_read() {
294        let input = b"<< /Length 5 0 R >>";
295        let spans = lexed(input);
296        assert_eq!(
297            dict_length(input, &spans, 0, input.len() as u64),
298            Some(LengthValue::Indirect {
299                number: 5,
300                generation: 0
301            })
302        );
303    }
304
305    #[test]
306    fn absent_key_returns_none() {
307        let input = b"<< /Type /X /N 3 >>";
308        let spans = lexed(input);
309        assert_eq!(dict_length(input, &spans, 0, input.len() as u64), None);
310        assert!(!dict_has_length(input, &spans, 0, input.len() as u64));
311    }
312
313    #[test]
314    fn non_integer_value_returns_none_but_key_is_present() {
315        let input = b"<< /Length /Foo >>";
316        let spans = lexed(input);
317        assert_eq!(dict_length(input, &spans, 0, input.len() as u64), None);
318        assert!(dict_has_length(input, &spans, 0, input.len() as u64));
319    }
320
321    #[test]
322    fn length_name_inside_string_is_not_a_key() {
323        let input = b"<< /X ( /Length 9 ) >>";
324        let spans = lexed(input);
325        assert_eq!(dict_length(input, &spans, 0, input.len() as u64), None);
326        assert!(!dict_has_length(input, &spans, 0, input.len() as u64));
327    }
328
329    #[test]
330    fn dict_name_value_reads_xref_and_objstm_types() {
331        let xref = b"<< /Type /XRef /Length 4 >>";
332        let spans = lexed(xref);
333        assert_eq!(
334            dict_name_value(xref, &spans, 0, xref.len() as u64, b"Type"),
335            Some(b"XRef".as_slice())
336        );
337
338        let objstm = b"<< /Type /ObjStm /N 3 >>";
339        let spans = lexed(objstm);
340        assert_eq!(
341            dict_name_value(objstm, &spans, 0, objstm.len() as u64, b"Type"),
342            Some(b"ObjStm".as_slice())
343        );
344    }
345
346    #[test]
347    fn dict_name_value_absent_key_and_non_name_value_return_none() {
348        let absent = b"<< /Foo /XRef >>";
349        let spans = lexed(absent);
350        assert_eq!(
351            dict_name_value(absent, &spans, 0, absent.len() as u64, b"Type"),
352            None
353        );
354
355        let non_name = b"<< /Type 5 >>";
356        let spans = lexed(non_name);
357        assert_eq!(
358            dict_name_value(non_name, &spans, 0, non_name.len() as u64, b"Type"),
359            None
360        );
361    }
362
363    #[test]
364    fn dict_name_value_inside_string_is_not_a_key() {
365        let input = b"<< /X ( /Type /XRef ) >>";
366        let spans = lexed(input);
367        assert_eq!(
368            dict_name_value(input, &spans, 0, input.len() as u64, b"Type"),
369            None
370        );
371    }
372
373    #[test]
374    fn dict_int_or_ref_reads_direct_and_reference() {
375        let direct = b"<< /Prev 1234 >>";
376        let spans = lexed(direct);
377        assert_eq!(
378            dict_int_or_ref(direct, &spans, 0, direct.len() as u64, b"Prev"),
379            Some(LengthValue::Direct(1234))
380        );
381
382        let reference = b"<< /Prev 7 0 R >>";
383        let spans = lexed(reference);
384        assert_eq!(
385            dict_int_or_ref(reference, &spans, 0, reference.len() as u64, b"Prev"),
386            Some(LengthValue::Indirect {
387                number: 7,
388                generation: 0,
389            })
390        );
391    }
392
393    #[test]
394    fn dict_int_or_ref_rejects_non_integer_and_absent() {
395        let name_value = b"<< /Prev /X >>";
396        let spans = lexed(name_value);
397        assert_eq!(
398            dict_int_or_ref(name_value, &spans, 0, name_value.len() as u64, b"Prev"),
399            None
400        );
401
402        let absent = b"<< /Size 4 >>";
403        let spans = lexed(absent);
404        assert_eq!(
405            dict_int_or_ref(absent, &spans, 0, absent.len() as u64, b"Prev"),
406            None
407        );
408    }
409
410    #[test]
411    fn dict_filter_absent_without_key() {
412        let input = b"<< /Length 1 >>";
413        let spans = lexed(input);
414        assert_eq!(
415            dict_filter(input, &spans, 0, input.len() as u64),
416            FilterClass::Absent
417        );
418    }
419
420    #[test]
421    fn dict_filter_reads_bare_and_single_element_array() {
422        let bare = b"<< /Filter /FlateDecode >>";
423        let spans = lexed(bare);
424        assert_eq!(
425            dict_filter(bare, &spans, 0, bare.len() as u64),
426            FilterClass::FlateDecode
427        );
428
429        let array = b"<< /Filter [ /FlateDecode ] >>";
430        let spans = lexed(array);
431        assert_eq!(
432            dict_filter(array, &spans, 0, array.len() as u64),
433            FilterClass::FlateDecode
434        );
435    }
436
437    #[test]
438    fn dict_filter_rejects_chain_other_filter_and_empty_array() {
439        let chain = b"<< /Filter [ /FlateDecode /ASCIIHexDecode ] >>";
440        let spans = lexed(chain);
441        assert_eq!(
442            dict_filter(chain, &spans, 0, chain.len() as u64),
443            FilterClass::Other
444        );
445
446        let other = b"<< /Filter /LZWDecode >>";
447        let spans = lexed(other);
448        assert_eq!(
449            dict_filter(other, &spans, 0, other.len() as u64),
450            FilterClass::Other
451        );
452
453        let empty = b"<< /Filter [] >>";
454        let spans = lexed(empty);
455        assert_eq!(
456            dict_filter(empty, &spans, 0, empty.len() as u64),
457            FilterClass::Other
458        );
459    }
460
461    #[test]
462    fn dict_filter_ignores_string_spelling_and_non_name_value() {
463        let inside_string = b"<< /X ( /Filter /FlateDecode ) >>";
464        let spans = lexed(inside_string);
465        assert_eq!(
466            dict_filter(inside_string, &spans, 0, inside_string.len() as u64),
467            FilterClass::Absent
468        );
469
470        let non_name = b"<< /Filter 5 >>";
471        let spans = lexed(non_name);
472        assert_eq!(
473            dict_filter(non_name, &spans, 0, non_name.len() as u64),
474            FilterClass::Other
475        );
476    }
477
478    #[test]
479    fn body_as_u64_accepts_single_integer_with_padding() {
480        let input = b"12";
481        let spans = lexed(input);
482        assert_eq!(body_as_u64(input, &spans, 0, 2), Some(12));
483
484        let padded = b"\n 12 \n";
485        let spans = lexed(padded);
486        assert_eq!(
487            body_as_u64(padded, &spans, 0, padded.len() as u64),
488            Some(12)
489        );
490    }
491
492    #[test]
493    fn body_as_u64_rejects_multi_token_body() {
494        let input = b"12 0";
495        let spans = lexed(input);
496        assert_eq!(body_as_u64(input, &spans, 0, input.len() as u64), None);
497
498        let reference = b"12 0 R";
499        let spans = lexed(reference);
500        assert_eq!(
501            body_as_u64(reference, &spans, 0, reference.len() as u64),
502            None
503        );
504    }
505
506    #[test]
507    fn body_as_u64_rejects_non_integer() {
508        let input = b"1.5";
509        let spans = lexed(input);
510        assert_eq!(body_as_u64(input, &spans, 0, input.len() as u64), None);
511    }
512}