structured_pades/source.rs
1//! Honest physical and decoded coordinates, without fabricated file offsets.
2
3use core::num::NonZeroUsize;
4use structured_asn1::Span;
5
6/// The domain in which a byte range has meaning.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Source {
9 /// Original unmodified PDF file bytes.
10 File,
11 /// Logical bytes of one decoded PDF value, such as an object stream or
12 /// signature string. Its original physical enclosure and the revision
13 /// that selected the decoding remain available independently.
14 Decoded {
15 /// Original physical encoded data, not the logical object's span.
16 stream: Span,
17 /// Revision whose stream-control objects selected this decoding.
18 revision: usize,
19 /// Zero identifies the final decoded value. A positive ordinal
20 /// identifies an intermediate filter input, preserving its distinct
21 /// logical offsets without claiming a physical byte projection.
22 stage: usize,
23 },
24}
25
26/// A range paired with its actual coordinate domain.
27#[derive(Clone, Copy, PartialEq, Eq)]
28pub struct Location {
29 // A real revision is an index into a nonempty owning collection, so its
30 // index plus one is representable. The zero niche denotes physical bytes.
31 revision: Option<NonZeroUsize>,
32 stream: Span,
33 stage: usize,
34 span: Span,
35}
36
37impl core::fmt::Debug for Location {
38 fn fmt(&self, output: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
39 output
40 .debug_struct("Location")
41 .field("source", &self.source())
42 .field("span", &self.span)
43 .finish_non_exhaustive()
44 }
45}
46
47impl Location {
48 /// Checks range arithmetic before admitting a source location.
49 #[must_use]
50 pub fn new(source: Source, start: u64, length: u64) -> Option<Self> {
51 start.checked_add(length)?;
52 let (revision, stream, stage) = match source {
53 Source::File => (
54 None,
55 Span {
56 start: 0,
57 length: 0,
58 },
59 0,
60 ),
61 Source::Decoded {
62 stream,
63 revision,
64 stage,
65 } => {
66 stream.start.checked_add(stream.length)?;
67 (
68 Some(NonZeroUsize::new(revision.checked_add(1)?)?),
69 stream,
70 stage,
71 )
72 }
73 };
74 Some(Self {
75 revision,
76 stream,
77 stage,
78 span: Span { start, length },
79 })
80 }
81
82 /// Exact original coordinate domain.
83 #[must_use]
84 pub fn source(self) -> Source {
85 match self.revision {
86 None => Source::File,
87 Some(revision) => Source::Decoded {
88 stream: self.stream,
89 revision: revision.get() - 1,
90 stage: self.stage,
91 },
92 }
93 }
94
95 /// Range in [`Self::source`], never implicitly a physical file span.
96 #[must_use]
97 pub fn span(self) -> Span {
98 self.span
99 }
100
101 /// Physical enclosing range. For a decoded value this deliberately returns
102 /// the encoded stream, not a guessed one-to-one projection.
103 #[must_use]
104 pub fn enclosing(self) -> Span {
105 if self.revision.is_some() {
106 self.stream
107 } else {
108 self.span
109 }
110 }
111
112 /// End in this source domain; construction already checked overflow.
113 #[must_use]
114 pub fn end(self) -> u64 {
115 self.span.start + self.span.length
116 }
117
118 /// Whether a point belongs to this half-open range.
119 #[must_use]
120 pub fn contains(self, offset: u64) -> bool {
121 offset >= self.span.start && offset < self.end()
122 }
123}
124
125#[cfg(test)]
126mod tests;