Skip to main content

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;