imprimatur 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +8 -0
  3. data/Cargo.lock +835 -0
  4. data/Cargo.toml +44 -0
  5. data/LICENSE.txt +29 -0
  6. data/README.md +314 -0
  7. data/ext/imprimatur_native/cli/Cargo.toml +30 -0
  8. data/ext/imprimatur_native/cli/src/app.rs +183 -0
  9. data/ext/imprimatur_native/cli/src/cli/completion.rs +26 -0
  10. data/ext/imprimatur_native/cli/src/cli/grep.rs +359 -0
  11. data/ext/imprimatur_native/cli/src/cli/images.rs +137 -0
  12. data/ext/imprimatur_native/cli/src/cli/limits.rs +49 -0
  13. data/ext/imprimatur_native/cli/src/cli/markdown.rs +128 -0
  14. data/ext/imprimatur_native/cli/src/cli.rs +71 -0
  15. data/ext/imprimatur_native/cli/src/main.rs +15 -0
  16. data/ext/imprimatur_native/core/Cargo.toml +33 -0
  17. data/ext/imprimatur_native/core/LICENSE +29 -0
  18. data/ext/imprimatur_native/core/README.md +105 -0
  19. data/ext/imprimatur_native/core/src/doc/paragraph.rs +112 -0
  20. data/ext/imprimatur_native/core/src/doc/table.rs +41 -0
  21. data/ext/imprimatur_native/core/src/doc.rs +210 -0
  22. data/ext/imprimatur_native/core/src/docx.rs +343 -0
  23. data/ext/imprimatur_native/core/src/error.rs +113 -0
  24. data/ext/imprimatur_native/core/src/lib.rs +38 -0
  25. data/ext/imprimatur_native/core/src/opc.rs +51 -0
  26. data/ext/imprimatur_native/core/src/pdf/font.rs +96 -0
  27. data/ext/imprimatur_native/core/src/pdf/layout/wrap.rs +161 -0
  28. data/ext/imprimatur_native/core/src/pdf/layout.rs +280 -0
  29. data/ext/imprimatur_native/core/src/pdf/metrics.rs +139 -0
  30. data/ext/imprimatur_native/core/src/pdf/objects.rs +141 -0
  31. data/ext/imprimatur_native/core/src/pdf.rs +84 -0
  32. data/ext/imprimatur_native/core/src/read/budget.rs +140 -0
  33. data/ext/imprimatur_native/core/src/read/content.rs +266 -0
  34. data/ext/imprimatur_native/core/src/read/docx/styles.rs +118 -0
  35. data/ext/imprimatur_native/core/src/read/docx.rs +336 -0
  36. data/ext/imprimatur_native/core/src/read/image/dimensions.rs +215 -0
  37. data/ext/imprimatur_native/core/src/read/image.rs +233 -0
  38. data/ext/imprimatur_native/core/src/read/inflate.rs +44 -0
  39. data/ext/imprimatur_native/core/src/read/json.rs +172 -0
  40. data/ext/imprimatur_native/core/src/read/markdown.rs +254 -0
  41. data/ext/imprimatur_native/core/src/read/package.rs +191 -0
  42. data/ext/imprimatur_native/core/src/read/pdf/cmap.rs +216 -0
  43. data/ext/imprimatur_native/core/src/read/pdf/content/geometry.rs +79 -0
  44. data/ext/imprimatur_native/core/src/read/pdf/content/operands.rs +156 -0
  45. data/ext/imprimatur_native/core/src/read/pdf/content.rs +439 -0
  46. data/ext/imprimatur_native/core/src/read/pdf/crypt.rs +256 -0
  47. data/ext/imprimatur_native/core/src/read/pdf/document.rs +420 -0
  48. data/ext/imprimatur_native/core/src/read/pdf/filter.rs +258 -0
  49. data/ext/imprimatur_native/core/src/read/pdf/font.rs +315 -0
  50. data/ext/imprimatur_native/core/src/read/pdf/glyphs.rs +179 -0
  51. data/ext/imprimatur_native/core/src/read/pdf/glyphs.txt +6298 -0
  52. data/ext/imprimatur_native/core/src/read/pdf/images.rs +365 -0
  53. data/ext/imprimatur_native/core/src/read/pdf/layout.rs +451 -0
  54. data/ext/imprimatur_native/core/src/read/pdf/lexer.rs +255 -0
  55. data/ext/imprimatur_native/core/src/read/pdf/object/name.rs +44 -0
  56. data/ext/imprimatur_native/core/src/read/pdf/object.rs +261 -0
  57. data/ext/imprimatur_native/core/src/read/pdf/xref.rs +346 -0
  58. data/ext/imprimatur_native/core/src/read/pdf.rs +53 -0
  59. data/ext/imprimatur_native/core/src/read/span.rs +22 -0
  60. data/ext/imprimatur_native/core/src/read/xlsx/numbers.rs +155 -0
  61. data/ext/imprimatur_native/core/src/read/xlsx.rs +340 -0
  62. data/ext/imprimatur_native/core/src/read.rs +145 -0
  63. data/ext/imprimatur_native/core/src/style.rs +82 -0
  64. data/ext/imprimatur_native/core/src/test_support.rs +163 -0
  65. data/ext/imprimatur_native/core/src/xlsx/cell.rs +112 -0
  66. data/ext/imprimatur_native/core/src/xlsx/comment.rs +256 -0
  67. data/ext/imprimatur_native/core/src/xlsx/date.rs +173 -0
  68. data/ext/imprimatur_native/core/src/xlsx/format.rs +226 -0
  69. data/ext/imprimatur_native/core/src/xlsx/package.rs +268 -0
  70. data/ext/imprimatur_native/core/src/xlsx/range.rs +124 -0
  71. data/ext/imprimatur_native/core/src/xlsx/styles.rs +240 -0
  72. data/ext/imprimatur_native/core/src/xlsx/text.rs +122 -0
  73. data/ext/imprimatur_native/core/src/xlsx/validation.rs +258 -0
  74. data/ext/imprimatur_native/core/src/xlsx/worksheet.rs +350 -0
  75. data/ext/imprimatur_native/core/src/xlsx.rs +120 -0
  76. data/ext/imprimatur_native/core/src/xml/reader.rs +238 -0
  77. data/ext/imprimatur_native/core/src/xml.rs +95 -0
  78. data/ext/imprimatur_native/core/src/zip/reader.rs +144 -0
  79. data/ext/imprimatur_native/core/src/zip.rs +161 -0
  80. data/ext/imprimatur_native/extconf.rb +45 -0
  81. data/ext/imprimatur_native/ffi/Cargo.toml +21 -0
  82. data/ext/imprimatur_native/ffi/src/convert.rs +132 -0
  83. data/ext/imprimatur_native/ffi/src/document.rs +138 -0
  84. data/ext/imprimatur_native/ffi/src/lib.rs +301 -0
  85. data/ext/imprimatur_native/ffi/src/read.rs +182 -0
  86. data/ext/imprimatur_native/ffi/src/style.rs +85 -0
  87. data/ext/imprimatur_native/ffi/src/validation.rs +56 -0
  88. data/lib/imprimatur/document.rb +127 -0
  89. data/lib/imprimatur/error.rb +22 -0
  90. data/lib/imprimatur/formula.rb +13 -0
  91. data/lib/imprimatur/native.rb +17 -0
  92. data/lib/imprimatur/reader.rb +59 -0
  93. data/lib/imprimatur/style.rb +31 -0
  94. data/lib/imprimatur/validation.rb +35 -0
  95. data/lib/imprimatur/version.rb +5 -0
  96. data/lib/imprimatur/xlsx.rb +183 -0
  97. data/lib/imprimatur.rb +111 -0
  98. metadata +157 -0
@@ -0,0 +1,255 @@
1
+ //! PDF tokens (ISO 32000-1, 7.2), for the file's objects and for content
2
+ //! streams alike. Tokens borrow from the input; strings and names are
3
+ //! decoded only when asked.
4
+
5
+ #[cfg(test)]
6
+ mod tests;
7
+
8
+ use std::borrow::Cow;
9
+
10
+ #[derive(Debug, Clone, Copy, PartialEq)]
11
+ pub enum Token<'a> {
12
+ Integer(i64),
13
+ Real(f64),
14
+ /// A name without its slash, `#xx` escapes still in place.
15
+ Name(&'a [u8]),
16
+ /// A literal string's bytes between its parentheses, escapes in place.
17
+ String(&'a [u8]),
18
+ /// A hexadecimal string's digits between `<` and `>`.
19
+ Hex(&'a [u8]),
20
+ ArrayStart,
21
+ ArrayEnd,
22
+ DictStart,
23
+ DictEnd,
24
+ /// `true`, `obj`, `R`, `Tj`...: anything else made of regular characters.
25
+ Keyword(&'a [u8]),
26
+ }
27
+
28
+ pub const fn is_whitespace(byte: u8) -> bool {
29
+ matches!(byte, b'\0' | b'\t' | b'\n' | b'\x0C' | b'\r' | b' ')
30
+ }
31
+
32
+ pub const fn is_delimiter(byte: u8) -> bool {
33
+ matches!(byte, b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%')
34
+ }
35
+
36
+ const fn is_regular(byte: u8) -> bool {
37
+ !is_whitespace(byte) && !is_delimiter(byte)
38
+ }
39
+
40
+ #[derive(Clone)]
41
+ pub struct Lexer<'a> {
42
+ pub pos: usize,
43
+ data: &'a [u8],
44
+ }
45
+
46
+ impl<'a> Lexer<'a> {
47
+ pub const fn new(data: &'a [u8], pos: usize) -> Self {
48
+ Self { pos, data }
49
+ }
50
+
51
+ pub const fn data(&self) -> &'a [u8] {
52
+ self.data
53
+ }
54
+
55
+ /// Skips whitespace and comments.
56
+ pub fn skip_space(&mut self) {
57
+ while let Some(&byte) = self.data.get(self.pos) {
58
+ if is_whitespace(byte) {
59
+ self.pos += 1;
60
+ } else if byte == b'%' {
61
+ while self.data.get(self.pos).is_some_and(|&byte| byte != b'\n' && byte != b'\r') {
62
+ self.pos += 1;
63
+ }
64
+ } else {
65
+ break;
66
+ }
67
+ }
68
+ }
69
+
70
+ /// The next token, `None` at the end. Stray closing delimiters are
71
+ /// skipped: broken files have them, and nothing reads them.
72
+ pub fn next(&mut self) -> Option<Token<'a>> {
73
+ loop {
74
+ self.skip_space();
75
+ let &byte = self.data.get(self.pos)?;
76
+ self.pos += 1;
77
+ return Some(match byte {
78
+ b'[' => Token::ArrayStart,
79
+ b']' => Token::ArrayEnd,
80
+ b'<' if self.data.get(self.pos) == Some(&b'<') => {
81
+ self.pos += 1;
82
+ Token::DictStart
83
+ }
84
+ b'>' if self.data.get(self.pos) == Some(&b'>') => {
85
+ self.pos += 1;
86
+ Token::DictEnd
87
+ }
88
+ b'<' => {
89
+ let start = self.pos;
90
+ let end = self.data[start..]
91
+ .iter()
92
+ .position(|&byte| byte == b'>')
93
+ .map_or(self.data.len(), |at| start + at);
94
+ self.pos = (end + 1).min(self.data.len());
95
+ Token::Hex(&self.data[start..end])
96
+ }
97
+ b'(' => self.literal(),
98
+ b'/' => {
99
+ let start = self.pos;
100
+ self.regular();
101
+ Token::Name(&self.data[start..self.pos])
102
+ }
103
+ b')' | b'>' | b'{' | b'}' => continue,
104
+ _ => {
105
+ let start = self.pos - 1;
106
+ self.regular();
107
+ let word = &self.data[start..self.pos];
108
+ number(word).unwrap_or(Token::Keyword(word))
109
+ }
110
+ });
111
+ }
112
+ }
113
+
114
+ fn regular(&mut self) {
115
+ while self.data.get(self.pos).is_some_and(|&byte| is_regular(byte)) {
116
+ self.pos += 1;
117
+ }
118
+ }
119
+
120
+ /// A literal string: parentheses balance unless escaped.
121
+ fn literal(&mut self) -> Token<'a> {
122
+ let start = self.pos;
123
+ let mut depth = 1_usize;
124
+ while let Some(&byte) = self.data.get(self.pos) {
125
+ self.pos += 1;
126
+ match byte {
127
+ b'\\' => self.pos += 1,
128
+ b'(' => depth += 1,
129
+ b')' => {
130
+ depth -= 1;
131
+ if depth == 0 {
132
+ return Token::String(&self.data[start..self.pos - 1]);
133
+ }
134
+ }
135
+ _ => {}
136
+ }
137
+ }
138
+ self.pos = self.data.len();
139
+ Token::String(&self.data[start..])
140
+ }
141
+ }
142
+
143
+ /// An integer or a real (`+1`, `-.5`, `3.`), or `None` for anything else.
144
+ fn number(word: &[u8]) -> Option<Token<'static>> {
145
+ let first = *word.first()?;
146
+ if !(first.is_ascii_digit() || matches!(first, b'+' | b'-' | b'.')) {
147
+ return None;
148
+ }
149
+ let text = std::str::from_utf8(word).ok()?;
150
+ if let Ok(integer) = text.parse::<i64>() {
151
+ return Some(Token::Integer(integer));
152
+ }
153
+ let digits = text.trim_start_matches(['+', '-']);
154
+ if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit() || byte == b'.') {
155
+ // `--5` and the like: Acrobat reads the number, so do we.
156
+ return digits.trim_start_matches('-').parse::<f64>().ok().map(Token::Real);
157
+ }
158
+ let value = if digits == "." { 0.0 } else { digits.parse::<f64>().ok()? };
159
+ Some(Token::Real(if text.starts_with('-') { -value } else { value }))
160
+ }
161
+
162
+ /// A literal string's bytes with its escapes resolved (ISO 32000-1, 7.3.4.2).
163
+ pub fn literal_bytes(raw: &[u8]) -> Cow<'_, [u8]> {
164
+ if !raw.iter().any(|&byte| byte == b'\\' || byte == b'\r') {
165
+ return Cow::Borrowed(raw);
166
+ }
167
+ let mut out = Vec::with_capacity(raw.len());
168
+ literal_into(raw, &mut out);
169
+ Cow::Owned(out)
170
+ }
171
+
172
+ /// As [`literal_bytes`], appended to `out`.
173
+ pub fn literal_into(raw: &[u8], out: &mut Vec<u8>) {
174
+ let mut bytes = raw.iter().copied().peekable();
175
+ while let Some(byte) = bytes.next() {
176
+ match byte {
177
+ b'\r' => {
178
+ bytes.next_if_eq(&b'\n');
179
+ out.push(b'\n');
180
+ }
181
+ b'\\' => match bytes.next() {
182
+ Some(b'n') => out.push(b'\n'),
183
+ Some(b'r') => out.push(b'\r'),
184
+ Some(b't') => out.push(b'\t'),
185
+ Some(b'b') => out.push(0x08),
186
+ Some(b'f') => out.push(0x0C),
187
+ Some(b'\r') => {
188
+ bytes.next_if_eq(&b'\n');
189
+ }
190
+ Some(digit @ b'0'..=b'7') => {
191
+ let mut value = u32::from(digit - b'0');
192
+ for _ in 0..2 {
193
+ let Some(next) = bytes.next_if(|byte| (b'0'..=b'7').contains(byte)) else {
194
+ break;
195
+ };
196
+ value = value * 8 + u32::from(next - b'0');
197
+ }
198
+ out.push(value.to_le_bytes()[0]);
199
+ }
200
+ Some(b'\n') | None => {}
201
+ Some(other) => out.push(other),
202
+ },
203
+ byte => out.push(byte),
204
+ }
205
+ }
206
+ }
207
+
208
+ /// A hexadecimal string's bytes; whitespace is ignored and a final odd
209
+ /// digit counts as followed by 0.
210
+ pub fn hex_bytes(raw: &[u8]) -> Vec<u8> {
211
+ let mut out = Vec::with_capacity(raw.len() / 2 + 1);
212
+ hex_into(raw, &mut out);
213
+ out
214
+ }
215
+
216
+ /// As [`hex_bytes`], appended to `out`.
217
+ pub fn hex_into(raw: &[u8], out: &mut Vec<u8>) {
218
+ let mut high = None;
219
+ for digit in raw.iter().filter_map(|&byte| char::from(byte).to_digit(16)) {
220
+ let digit = digit.to_le_bytes()[0];
221
+ match high.take() {
222
+ Some(high) => out.push(high << 4 | digit),
223
+ None => high = Some(digit),
224
+ }
225
+ }
226
+ if let Some(high) = high {
227
+ out.push(high << 4);
228
+ }
229
+ }
230
+
231
+ /// A name's bytes with its `#xx` escapes resolved.
232
+ pub fn name_bytes(raw: &[u8]) -> Cow<'_, [u8]> {
233
+ if !raw.contains(&b'#') {
234
+ return Cow::Borrowed(raw);
235
+ }
236
+ let mut out = Vec::with_capacity(raw.len());
237
+ let mut at = 0;
238
+ while at < raw.len() {
239
+ let escaped = raw
240
+ .get(at + 1..at + 3)
241
+ .and_then(|hex| std::str::from_utf8(hex).ok())
242
+ .and_then(|hex| u8::from_str_radix(hex, 16).ok());
243
+ match (raw[at], escaped) {
244
+ (b'#', Some(byte)) => {
245
+ out.push(byte);
246
+ at += 3;
247
+ }
248
+ (byte, _) => {
249
+ out.push(byte);
250
+ at += 1;
251
+ }
252
+ }
253
+ }
254
+ Cow::Owned(out)
255
+ }
@@ -0,0 +1,44 @@
1
+ //! PDF names: `Type`, `Font`, `F1`. Nearly all fit in a few bytes, so they
2
+ //! live inline; a long one goes to the heap.
3
+
4
+ use std::fmt;
5
+ use std::ops::Deref;
6
+
7
+ /// The bytes a name keeps inline: its enum is no larger than a `Vec`.
8
+ const INLINE: usize = 22;
9
+
10
+ #[derive(Clone, PartialEq, Eq)]
11
+ pub enum Name {
12
+ Inline { length: u8, bytes: [u8; INLINE] },
13
+ Heap(Box<[u8]>),
14
+ }
15
+
16
+ impl Name {
17
+ pub fn new(bytes: &[u8]) -> Self {
18
+ u8::try_from(bytes.len()).ok().filter(|&length| usize::from(length) <= INLINE).map_or_else(
19
+ || Self::Heap(bytes.into()),
20
+ |length| {
21
+ let mut inline = [0; INLINE];
22
+ inline[..bytes.len()].copy_from_slice(bytes);
23
+ Self::Inline { length, bytes: inline }
24
+ },
25
+ )
26
+ }
27
+ }
28
+
29
+ impl Deref for Name {
30
+ type Target = [u8];
31
+
32
+ fn deref(&self) -> &[u8] {
33
+ match self {
34
+ Self::Inline { length, bytes } => &bytes[..usize::from(*length)],
35
+ Self::Heap(bytes) => bytes,
36
+ }
37
+ }
38
+ }
39
+
40
+ impl fmt::Debug for Name {
41
+ fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result {
42
+ write!(out, "/{}", String::from_utf8_lossy(self))
43
+ }
44
+ }
@@ -0,0 +1,261 @@
1
+ //! PDF objects (ISO 32000-1, 7.3) and the parser that reads them.
2
+
3
+ mod name;
4
+ #[cfg(test)]
5
+ mod tests;
6
+
7
+ use std::ops::Range;
8
+
9
+ pub use name::Name;
10
+
11
+ use super::lexer::{Lexer, Token, hex_bytes, literal_bytes, name_bytes};
12
+ use crate::{Error, Result};
13
+
14
+ #[derive(Debug, Clone, PartialEq)]
15
+ pub enum Object {
16
+ Null,
17
+ Bool(bool),
18
+ Integer(i64),
19
+ Real(f64),
20
+ Name(Name),
21
+ String(Vec<u8>),
22
+ Array(Vec<Self>),
23
+ Dict(Dict),
24
+ Stream(Stream),
25
+ Ref(u32, u16),
26
+ }
27
+
28
+ /// A dictionary: a handful of keys, so a vector beats a map.
29
+ #[derive(Debug, Clone, Default, PartialEq)]
30
+ pub struct Dict(pub Vec<(Name, Object)>);
31
+
32
+ /// A stream: its dictionary and where its raw (encoded, maybe encrypted)
33
+ /// bytes lie in the file.
34
+ #[derive(Debug, Clone, PartialEq)]
35
+ pub struct Stream {
36
+ pub dict: Dict,
37
+ pub data: Range<usize>,
38
+ }
39
+
40
+ impl Dict {
41
+ pub fn get(&self, key: &[u8]) -> Option<&Object> {
42
+ self.0.iter().find(|(name, _)| **name == *key).map(|(_, value)| value)
43
+ }
44
+
45
+ pub fn has_name(&self, key: &[u8], name: &[u8]) -> bool {
46
+ self.get(key).and_then(Object::as_name) == Some(name)
47
+ }
48
+ }
49
+
50
+ impl Object {
51
+ pub const fn as_integer(&self) -> Option<i64> {
52
+ match *self {
53
+ Self::Integer(value) => Some(value),
54
+ _ => None,
55
+ }
56
+ }
57
+
58
+ #[expect(clippy::cast_precision_loss, reason = "PDF numbers fit an f64 in practice")]
59
+ pub const fn as_number(&self) -> Option<f64> {
60
+ match *self {
61
+ Self::Integer(value) => Some(value as f64),
62
+ Self::Real(value) => Some(value),
63
+ _ => None,
64
+ }
65
+ }
66
+
67
+ pub fn as_name(&self) -> Option<&[u8]> {
68
+ match self {
69
+ Self::Name(name) => Some(name),
70
+ _ => None,
71
+ }
72
+ }
73
+
74
+ pub fn as_bytes(&self) -> Option<&[u8]> {
75
+ match self {
76
+ Self::String(bytes) => Some(bytes),
77
+ _ => None,
78
+ }
79
+ }
80
+
81
+ pub fn as_array(&self) -> Option<&[Self]> {
82
+ match self {
83
+ Self::Array(items) => Some(items),
84
+ _ => None,
85
+ }
86
+ }
87
+
88
+ /// A dictionary, or a stream's.
89
+ pub const fn as_dict(&self) -> Option<&Dict> {
90
+ match self {
91
+ Self::Dict(dict) | Self::Stream(Stream { dict, .. }) => Some(dict),
92
+ _ => None,
93
+ }
94
+ }
95
+
96
+ pub const fn as_reference(&self) -> Option<(u32, u16)> {
97
+ match *self {
98
+ Self::Ref(number, generation) => Some((number, generation)),
99
+ _ => None,
100
+ }
101
+ }
102
+ }
103
+
104
+ /// Reads objects from a lexer, refusing to nest deeper than `max_depth`.
105
+ pub struct Parser<'a> {
106
+ pub lexer: Lexer<'a>,
107
+ max_depth: usize,
108
+ }
109
+
110
+ impl<'a> Parser<'a> {
111
+ pub const fn new(data: &'a [u8], pos: usize, max_depth: usize) -> Self {
112
+ Self { lexer: Lexer::new(data, pos), max_depth }
113
+ }
114
+
115
+ /// The next whole object.
116
+ ///
117
+ /// # Errors
118
+ /// [`Error::Malformed`] at the end of the input or on a stray token, and
119
+ /// [`Error::LimitExceeded`] past the nesting limit.
120
+ pub fn object(&mut self) -> Result<Object> {
121
+ let token = self.lexer.next().ok_or_else(|| malformed("an object is cut short"))?;
122
+ self.value(token, 0)
123
+ }
124
+
125
+ /// The object a token starts. Integers look ahead for `N G R`.
126
+ pub fn value(&mut self, token: Token<'a>, depth: usize) -> Result<Object> {
127
+ if depth > self.max_depth {
128
+ return Err(Error::LimitExceeded("PDF object nesting depth"));
129
+ }
130
+ Ok(match token {
131
+ Token::Integer(number) => {
132
+ let saved = self.lexer.pos;
133
+ if let (Ok(number), Some(Token::Integer(generation)), Some(Token::Keyword(b"R"))) =
134
+ (u32::try_from(number), self.lexer.next(), self.lexer.next())
135
+ {
136
+ Object::Ref(number, u16::try_from(generation).unwrap_or(0))
137
+ } else {
138
+ self.lexer.pos = saved;
139
+ Object::Integer(number)
140
+ }
141
+ }
142
+ Token::Real(number) => Object::Real(number),
143
+ Token::Name(raw) => Object::Name(Name::new(&name_bytes(raw))),
144
+ Token::String(raw) => Object::String(literal_bytes(raw).into_owned()),
145
+ Token::Hex(raw) => Object::String(hex_bytes(raw)),
146
+ Token::ArrayStart => {
147
+ let mut items = Vec::new();
148
+ loop {
149
+ match self.lexer.next() {
150
+ Some(Token::ArrayEnd) | None => break,
151
+ Some(token) => items.push(self.value(token, depth + 1)?),
152
+ }
153
+ }
154
+ Object::Array(items)
155
+ }
156
+ Token::DictStart => Object::Dict(self.dict(depth)?),
157
+ Token::Keyword(b"true") => Object::Bool(true),
158
+ Token::Keyword(b"false") => Object::Bool(false),
159
+ Token::Keyword(_) | Token::ArrayEnd | Token::DictEnd => Object::Null,
160
+ })
161
+ }
162
+
163
+ /// A dictionary's entries, after its `<<`. A key without a value, as
164
+ /// broken files have, is dropped.
165
+ fn dict(&mut self, depth: usize) -> Result<Dict> {
166
+ let mut entries = Vec::new();
167
+ loop {
168
+ match self.lexer.next() {
169
+ Some(Token::DictEnd) | None => return Ok(Dict(entries)),
170
+ Some(Token::Name(raw)) => {
171
+ let saved = self.lexer.pos;
172
+ match self.lexer.next() {
173
+ Some(Token::DictEnd) | None => {
174
+ self.lexer.pos = saved;
175
+ }
176
+ Some(token) => {
177
+ let value = self.value(token, depth + 1)?;
178
+ entries.push((Name::new(&name_bytes(raw)), value));
179
+ }
180
+ }
181
+ }
182
+ Some(_) => {}
183
+ }
184
+ }
185
+ }
186
+
187
+ /// An indirect object at the lexer's position: `N G obj ... endobj`,
188
+ /// with its stream data located when it is a stream. `length` resolves
189
+ /// a `/Length` given by reference.
190
+ pub fn indirect(
191
+ &mut self,
192
+ length: &dyn Fn(&Object) -> Option<usize>,
193
+ ) -> Result<(u32, u16, Object)> {
194
+ let header = (self.lexer.next(), self.lexer.next(), self.lexer.next());
195
+ let (
196
+ Some(Token::Integer(number)),
197
+ Some(Token::Integer(generation)),
198
+ Some(Token::Keyword(b"obj")),
199
+ ) = header
200
+ else {
201
+ return Err(malformed("no object where the cross-reference table points"));
202
+ };
203
+ let number = u32::try_from(number).map_err(|_| malformed("a negative object number"))?;
204
+ let generation = u16::try_from(generation).unwrap_or(0);
205
+ let object = self.object()?;
206
+ let Object::Dict(dict) = object else {
207
+ return Ok((number, generation, object));
208
+ };
209
+ let saved = self.lexer.pos;
210
+ if self.lexer.next() != Some(Token::Keyword(b"stream")) {
211
+ self.lexer.pos = saved;
212
+ return Ok((number, generation, Object::Dict(dict)));
213
+ }
214
+ let data = self.lexer.data();
215
+ let mut start = self.lexer.pos;
216
+ if data.get(start) == Some(&b'\r') {
217
+ start += 1;
218
+ }
219
+ if data.get(start) == Some(&b'\n') {
220
+ start += 1;
221
+ }
222
+ let declared = dict.get(b"Length").and_then(length).filter(|&length| {
223
+ let end = start.saturating_add(length);
224
+ data.get(end..).is_some_and(|rest| {
225
+ let rest = &rest
226
+ [rest.iter().take_while(|&&byte| super::lexer::is_whitespace(byte)).count()..];
227
+ rest.starts_with(b"endstream")
228
+ })
229
+ });
230
+ let end = declared.map_or_else(
231
+ || {
232
+ let found = find(&data[start..], b"endstream").map_or(data.len(), |at| start + at);
233
+ let mut end = found;
234
+ if end > start && data[end - 1] == b'\n' {
235
+ end -= 1;
236
+ }
237
+ if end > start && data[end - 1] == b'\r' {
238
+ end -= 1;
239
+ }
240
+ end
241
+ },
242
+ |length| start + length,
243
+ );
244
+ self.lexer.pos = find(&data[end..], b"endstream").map_or(data.len(), |at| end + at + 9);
245
+ Ok((number, generation, Object::Stream(Stream { dict, data: start..end })))
246
+ }
247
+ }
248
+
249
+ /// A `/Length` given directly: what the cross-reference layer, which
250
+ /// cannot resolve references yet, passes to [`Parser::indirect`].
251
+ pub fn direct_length(length: &Object) -> Option<usize> {
252
+ length.as_integer().and_then(|length| usize::try_from(length).ok())
253
+ }
254
+
255
+ pub fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
256
+ haystack.windows(needle.len()).position(|window| window == needle)
257
+ }
258
+
259
+ pub fn malformed(reason: &str) -> Error {
260
+ Error::malformed("PDF", reason)
261
+ }