Skip to main content

vole_document/adapter/pdf/
span.rs

1//! Byte-span vocabulary for the PDF lexical cover.
2//!
3//! A [`SpanSet`] is a *partition* of an input: an ordered list of spans whose
4//! lengths sum to the declared length, with no gap and no overlap. The cover is
5//! the authority for later structural passes: every source byte belongs to
6//! exactly one span, so interpretation is always anchored to raw offsets and no
7//! byte can be silently lost.
8
9use crate::error::{Error, Result};
10
11/// The lexical class of a byte span.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum SpanKind {
14    /// A maximal run of PDF whitespace bytes.
15    Whitespace,
16    /// A `%` comment, including the `%` and excluding the terminating EOL.
17    Comment,
18    /// A `(` ... `)` literal string (possibly with escaped/nested parens).
19    LiteralString,
20    /// A `<` ... `>` hexadecimal string.
21    HexString,
22    /// A `<<` dictionary opener.
23    DictOpen,
24    /// A `>>` dictionary closer.
25    DictClose,
26    /// A `[` array opener.
27    ArrayOpen,
28    /// A `]` array closer.
29    ArrayClose,
30    /// A `{` brace opener.
31    BraceOpen,
32    /// A `}` brace closer.
33    BraceClose,
34    /// A `/` name, including the leading slash.
35    Name,
36    /// A maximal run of regular bytes (keywords, numbers, references, ...).
37    Regular,
38}
39
40/// One lexical span: a half-open byte range `[start, start + len)` with a class.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub struct Span {
43    /// Offset of the first byte of the span.
44    pub start: u64,
45    /// Number of bytes in the span (always > 0 for a valid cover).
46    pub len: u64,
47    /// The lexical class of the span.
48    pub kind: SpanKind,
49}
50
51/// An ordered cover of an input by lexical spans.
52#[derive(Debug, Clone, PartialEq, Eq, Default)]
53pub struct SpanSet {
54    /// Spans in ascending, non-overlapping order.
55    pub spans: Vec<Span>,
56}
57
58impl SpanSet {
59    /// Sum of all span lengths. Saturates rather than panicking on absurd input.
60    pub fn total_len(&self) -> u64 {
61        self.spans
62            .iter()
63            .fold(0u64, |acc, s| acc.saturating_add(s.len))
64    }
65
66    /// Require a contiguous cover of exactly `[0, declared_len)`.
67    ///
68    /// Returns [`crate::ErrorClass::CoverageViolation`] for a gap, overlap,
69    /// wrong total, or length overflow, and
70    /// [`crate::ErrorClass::InvalidPdfStructure`] for a degenerate zero-length
71    /// span. This is the internal self-check that turns a scanner bug into a
72    /// loud classified failure instead of silent byte loss.
73    pub fn validate(&self, declared_len: u64) -> Result<()> {
74        let mut cursor: u64 = 0;
75        for (i, span) in self.spans.iter().enumerate() {
76            if span.len == 0 {
77                return Err(Error::invalid_pdf_structure(format!(
78                    "span {i} has zero length at offset {}",
79                    span.start
80                )));
81            }
82            if span.start != cursor {
83                let why = if span.start < cursor {
84                    "overlap"
85                } else {
86                    "gap"
87                };
88                return Err(Error::coverage_violation(format!(
89                    "span {i} {why}: expected start {cursor}, found {}",
90                    span.start
91                )));
92            }
93            cursor = cursor.checked_add(span.len).ok_or_else(|| {
94                Error::coverage_violation("span lengths overflow the address space")
95            })?;
96        }
97        if cursor != declared_len {
98            return Err(Error::coverage_violation(format!(
99                "cover ends at {cursor}, declared length is {declared_len}"
100            )));
101        }
102        Ok(())
103    }
104
105    /// The span containing `offset`, if any. Binary search over sorted starts.
106    pub fn span_at(&self, offset: u64) -> Option<Span> {
107        let idx = self.spans.partition_point(|s| s.start <= offset);
108        if idx == 0 {
109            return None;
110        }
111        let span = self.spans[idx - 1];
112        if offset < span.start.saturating_add(span.len) {
113            Some(span)
114        } else {
115            None
116        }
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use crate::error::ErrorClass;
124
125    fn s(start: u64, len: u64, kind: SpanKind) -> Span {
126        Span { start, len, kind }
127    }
128
129    #[test]
130    fn empty_set_validates_zero() {
131        let set = SpanSet::default();
132        assert_eq!(set.total_len(), 0);
133        assert!(set.validate(0).is_ok());
134        assert_eq!(set.span_at(0), None);
135    }
136
137    #[test]
138    fn contiguous_set_validates_and_totals() {
139        let set = SpanSet {
140            spans: vec![s(0, 3, SpanKind::Whitespace), s(3, 2, SpanKind::Regular)],
141        };
142        assert_eq!(set.total_len(), 5);
143        assert!(set.validate(5).is_ok());
144    }
145
146    #[test]
147    fn gap_is_coverage_violation() {
148        let set = SpanSet {
149            spans: vec![s(0, 1, SpanKind::Regular), s(2, 1, SpanKind::Regular)],
150        };
151        let e = set.validate(3).unwrap_err();
152        assert_eq!(e.class(), ErrorClass::CoverageViolation);
153    }
154
155    #[test]
156    fn overlap_is_coverage_violation() {
157        let set = SpanSet {
158            spans: vec![s(0, 2, SpanKind::Regular), s(1, 2, SpanKind::Regular)],
159        };
160        let e = set.validate(3).unwrap_err();
161        assert_eq!(e.class(), ErrorClass::CoverageViolation);
162    }
163
164    #[test]
165    fn short_cover_is_coverage_violation() {
166        let set = SpanSet {
167            spans: vec![s(0, 2, SpanKind::Regular)],
168        };
169        let e = set.validate(3).unwrap_err();
170        assert_eq!(e.class(), ErrorClass::CoverageViolation);
171    }
172
173    #[test]
174    fn zero_length_span_is_invalid_structure() {
175        let set = SpanSet {
176            spans: vec![s(0, 0, SpanKind::Regular)],
177        };
178        let e = set.validate(0).unwrap_err();
179        assert_eq!(e.class(), ErrorClass::InvalidPdfStructure);
180    }
181
182    #[test]
183    fn span_at_finds_the_right_span() {
184        let set = SpanSet {
185            spans: vec![
186                s(0, 2, SpanKind::Regular),
187                s(2, 3, SpanKind::Whitespace),
188                s(5, 1, SpanKind::Name),
189            ],
190        };
191        assert_eq!(set.span_at(0), Some(s(0, 2, SpanKind::Regular)));
192        assert_eq!(set.span_at(1), Some(s(0, 2, SpanKind::Regular)));
193        assert_eq!(set.span_at(2), Some(s(2, 3, SpanKind::Whitespace)));
194        assert_eq!(set.span_at(4), Some(s(2, 3, SpanKind::Whitespace)));
195        assert_eq!(set.span_at(5), Some(s(5, 1, SpanKind::Name)));
196        assert_eq!(set.span_at(6), None);
197        assert_eq!(set.span_at(u64::MAX), None);
198    }
199}