imprimatur 0.5.0 → 0.6.1

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/Cargo.lock +473 -5
  4. data/Cargo.toml +17 -1
  5. data/README.md +63 -7
  6. data/ext/imprimatur_native/cli/Cargo.toml +1 -1
  7. data/ext/imprimatur_native/cli/src/app.rs +3 -1
  8. data/ext/imprimatur_native/core/Cargo.toml +30 -13
  9. data/ext/imprimatur_native/core/README.md +18 -1
  10. data/ext/imprimatur_native/core/src/bin/imprimatur-render-worker.rs +10 -0
  11. data/ext/imprimatur_native/core/src/bytes.rs +16 -0
  12. data/ext/imprimatur_native/core/src/doc/picture.rs +3 -7
  13. data/ext/imprimatur_native/core/src/doc/size.rs +14 -0
  14. data/ext/imprimatur_native/core/src/doc/symbol.rs +3 -5
  15. data/ext/imprimatur_native/core/src/doc.rs +24 -0
  16. data/ext/imprimatur_native/core/src/docx.rs +6 -5
  17. data/ext/imprimatur_native/core/src/error.rs +6 -0
  18. data/ext/imprimatur_native/core/src/font.rs +12 -0
  19. data/ext/imprimatur_native/core/src/lib.rs +6 -0
  20. data/ext/imprimatur_native/core/src/pdf/layout.rs +13 -5
  21. data/ext/imprimatur_native/core/src/pdf/objects.rs +1 -1
  22. data/ext/imprimatur_native/core/src/pdf/truetype/descriptor.rs +1 -0
  23. data/ext/imprimatur_native/core/src/read/budget.rs +10 -5
  24. data/ext/imprimatur_native/core/src/read/content.rs +16 -0
  25. data/ext/imprimatur_native/core/src/read/image/dimensions.rs +2 -2
  26. data/ext/imprimatur_native/core/src/read/ole.rs +1 -8
  27. data/ext/imprimatur_native/core/src/read/pdf/bounded.rs +249 -0
  28. data/ext/imprimatur_native/core/src/read/pdf/content/state.rs +50 -0
  29. data/ext/imprimatur_native/core/src/read/pdf/content.rs +43 -54
  30. data/ext/imprimatur_native/core/src/read/pdf/document/pages.rs +140 -0
  31. data/ext/imprimatur_native/core/src/read/pdf/document.rs +20 -110
  32. data/ext/imprimatur_native/core/src/read/pdf/filter.rs +25 -13
  33. data/ext/imprimatur_native/core/src/read/pdf/font.rs +4 -3
  34. data/ext/imprimatur_native/core/src/read/pdf/isolation/protocol.rs +224 -0
  35. data/ext/imprimatur_native/core/src/read/pdf/isolation/stream.rs +113 -0
  36. data/ext/imprimatur_native/core/src/read/pdf/isolation/system.rs +98 -0
  37. data/ext/imprimatur_native/core/src/read/pdf/isolation/worker.rs +74 -0
  38. data/ext/imprimatur_native/core/src/read/pdf/isolation.rs +255 -0
  39. data/ext/imprimatur_native/core/src/read/pdf/render.rs +206 -0
  40. data/ext/imprimatur_native/core/src/read/pdf.rs +16 -8
  41. data/ext/imprimatur_native/core/src/read/xls/strings.rs +2 -1
  42. data/ext/imprimatur_native/core/src/read/xls.rs +1 -8
  43. data/ext/imprimatur_native/core/src/read.rs +6 -5
  44. data/ext/imprimatur_native/core/src/test_support/font.rs +37 -0
  45. data/ext/imprimatur_native/core/src/xlsx/validation.rs +28 -18
  46. data/ext/imprimatur_native/core/src/zip/reader.rs +1 -8
  47. data/ext/imprimatur_native/extconf.rb +18 -0
  48. data/ext/imprimatur_native/ffi/Cargo.toml +2 -2
  49. data/ext/imprimatur_native/ffi/src/document.rs +6 -0
  50. data/ext/imprimatur_native/ffi/src/lib.rs +7 -0
  51. data/ext/imprimatur_native/ffi/src/pdf.rs +88 -0
  52. data/ext/imprimatur_native/ffi/src/read.rs +18 -12
  53. data/lib/imprimatur/document.rb +4 -2
  54. data/lib/imprimatur/native.rb +7 -0
  55. data/lib/imprimatur/pdf_reader.rb +29 -0
  56. data/lib/imprimatur/version.rb +1 -1
  57. data/lib/imprimatur.rb +26 -2
  58. metadata +20 -4
@@ -1,6 +1,8 @@
1
1
  //! A PDF file's objects on demand: loaded once and shared, decrypted, and
2
2
  //! their streams decoded; then the pages, with what they inherit.
3
3
 
4
+ mod pages;
5
+
4
6
  use std::borrow::Cow;
5
7
  use std::cell::RefCell;
6
8
  use std::collections::HashMap;
@@ -67,6 +69,7 @@ pub struct Document<'a> {
67
69
  objects: RefCell<HashMap<u32, Rc<Object>>>,
68
70
  streams: RefCell<HashMap<u32, Rc<ObjectStream>>>,
69
71
  loading: RefCell<Vec<u32>>,
72
+ strict: bool,
70
73
  }
71
74
 
72
75
  impl<'a> Document<'a> {
@@ -81,6 +84,7 @@ impl<'a> Document<'a> {
81
84
  objects: RefCell::default(),
82
85
  streams: RefCell::default(),
83
86
  loading: RefCell::default(),
87
+ strict: false,
84
88
  };
85
89
  if let Some(encrypt) = document.xref.trailer.get(b"Encrypt").cloned() {
86
90
  let encrypt = document.resolve(&encrypt)?;
@@ -100,6 +104,11 @@ impl<'a> Document<'a> {
100
104
  self.budget
101
105
  }
102
106
 
107
+ pub const fn strict(mut self) -> Self {
108
+ self.strict = true;
109
+ self
110
+ }
111
+
103
112
  pub const fn trailer(&self) -> &Dict {
104
113
  &self.xref.trailer
105
114
  }
@@ -221,6 +230,9 @@ impl<'a> Document<'a> {
221
230
  pub fn decode(&self, stream: &Stream, number: Option<u32>) -> Result<Vec<u8>> {
222
231
  let raw = self.raw(stream, number)?;
223
232
  let filters = self.filters(&stream.dict)?;
233
+ if self.strict {
234
+ return filter::decode_strict(&raw, &filter_params(&filters), self.budget);
235
+ }
224
236
  decode_with(&raw, &filters, self.budget)
225
237
  }
226
238
 
@@ -285,107 +297,8 @@ impl<'a> Document<'a> {
285
297
  }
286
298
  }
287
299
 
288
- /// The pages in order. When the page tree is broken, every object of
289
- /// type `/Page`, by object number.
290
- pub fn pages(&self) -> Result<Vec<Page>> {
291
- let mut pages = Vec::new();
292
- if let Some(root) = self.trailer().get(b"Root").cloned() {
293
- let root = self.resolve(&root)?;
294
- if let Some(tree) = root.as_dict().and_then(|root| root.get(b"Pages")).cloned() {
295
- let inherited =
296
- Inherited { resources: None, media_box: [0.0, 0.0, 612.0, 792.0], rotate: 0 };
297
- let mut visited = Vec::new();
298
- self.walk(&tree, &inherited, 0, &mut visited, &mut pages)?;
299
- }
300
- }
301
- if pages.is_empty() {
302
- let mut numbers: Vec<u32> = self.xref.entries.keys().copied().collect();
303
- numbers.sort_unstable();
304
- for number in numbers {
305
- let object = self.get(number)?;
306
- if let Some(dict) = object.as_dict()
307
- && dict.has_name(b"Type", b"Page")
308
- {
309
- let inherited = Inherited {
310
- resources: None,
311
- media_box: [0.0, 0.0, 612.0, 792.0],
312
- rotate: 0,
313
- };
314
- pages.push(self.page(dict, &inherited)?);
315
- }
316
- }
317
- }
318
- Ok(pages)
319
- }
320
-
321
- fn walk(
322
- &self,
323
- node: &Object,
324
- inherited: &Inherited,
325
- depth: usize,
326
- visited: &mut Vec<u32>,
327
- pages: &mut Vec<Page>,
328
- ) -> Result<()> {
329
- self.budget.nest(depth)?;
330
- if let Some((number, _)) = node.as_reference() {
331
- if visited.contains(&number) {
332
- return Ok(());
333
- }
334
- visited.push(number);
335
- }
336
- let node = self.resolve(node)?;
337
- let Some(dict) = node.as_dict() else { return Ok(()) };
338
- let inherited = self.inherit(dict, inherited)?;
339
- match self.entry(dict, b"Kids")?.as_deref() {
340
- Some(Object::Array(kids)) if !dict.has_name(b"Type", b"Page") => {
341
- for kid in kids {
342
- self.walk(kid, &inherited, depth + 1, visited, pages)?;
343
- }
344
- }
345
- _ => pages.push(self.page(dict, &inherited)?),
346
- }
347
- Ok(())
348
- }
349
-
350
- fn inherit(&self, dict: &Dict, inherited: &Inherited) -> Result<Inherited> {
351
- let resources = match dict.get(b"Resources") {
352
- Some(object) => Some(self.resolve(object)?.into_shared()),
353
- None => inherited.resources.clone(),
354
- };
355
- let media_box = match self.entry(dict, b"MediaBox")?.as_deref().and_then(Object::as_array) {
356
- Some(corners) if corners.len() == 4 => {
357
- let mut media_box = [0.0; 4];
358
- for (slot, corner) in media_box.iter_mut().zip(corners) {
359
- *slot = self.resolve(corner)?.as_number().unwrap_or(0.0);
360
- }
361
- media_box
362
- }
363
- _ => inherited.media_box,
364
- };
365
- let rotate = self
366
- .entry(dict, b"Rotate")?
367
- .and_then(|rotate| rotate.as_integer())
368
- .unwrap_or(inherited.rotate);
369
- Ok(Inherited { resources, media_box, rotate })
370
- }
371
-
372
- fn page(&self, dict: &Dict, inherited: &Inherited) -> Result<Page> {
373
- let inherited = self.inherit(dict, inherited)?;
374
- let contents = match dict.get(b"Contents") {
375
- Some(reference @ Object::Ref(..)) => match &*self.resolve(reference)? {
376
- Object::Array(parts) => parts.clone(),
377
- _ => vec![reference.clone()],
378
- },
379
- Some(Object::Array(parts)) => parts.clone(),
380
- Some(other) => vec![other.clone()],
381
- None => Vec::new(),
382
- };
383
- Ok(Page {
384
- resources: inherited.resources,
385
- contents,
386
- media_box: inherited.media_box,
387
- rotate: inherited.rotate,
388
- })
300
+ pub const fn encrypted(&self) -> bool {
301
+ self.crypt.is_some()
389
302
  }
390
303
  }
391
304
 
@@ -394,17 +307,14 @@ pub type Filter = (Name, Option<Rc<Object>>);
394
307
 
395
308
  /// `raw` through `filters`, in order.
396
309
  pub fn decode_with(raw: &[u8], filters: &[Filter], budget: &Budget<'_>) -> Result<Vec<u8>> {
397
- let filters: Vec<(&[u8], Option<&Dict>)> = filters
398
- .iter()
399
- .map(|(name, params)| (&**name, params.as_deref().and_then(Object::as_dict)))
400
- .collect();
401
- filter::decode(raw, &filters, budget)
310
+ filter::decode(raw, &filter_params(filters), budget)
402
311
  }
403
312
 
404
- struct Inherited {
405
- resources: Option<Rc<Object>>,
406
- media_box: [f64; 4],
407
- rotate: i64,
313
+ fn filter_params(filters: &[Filter]) -> Vec<(&[u8], Option<&Dict>)> {
314
+ filters
315
+ .iter()
316
+ .map(|(name, params)| (&**name, params.as_deref().and_then(Object::as_dict)))
317
+ .collect()
408
318
  }
409
319
 
410
320
  /// Decrypts every string in an object freshly read from the file.
@@ -13,12 +13,29 @@ pub fn decode(
13
13
  data: &[u8],
14
14
  filters: &[(&[u8], Option<&Dict>)],
15
15
  budget: &Budget<'_>,
16
+ ) -> Result<Vec<u8>> {
17
+ decode_stream(data, filters, budget, false)
18
+ }
19
+
20
+ pub fn decode_strict(
21
+ data: &[u8],
22
+ filters: &[(&[u8], Option<&Dict>)],
23
+ budget: &Budget<'_>,
24
+ ) -> Result<Vec<u8>> {
25
+ decode_stream(data, filters, budget, true)
26
+ }
27
+
28
+ fn decode_stream(
29
+ data: &[u8],
30
+ filters: &[(&[u8], Option<&Dict>)],
31
+ budget: &Budget<'_>,
32
+ strict: bool,
16
33
  ) -> Result<Vec<u8>> {
17
34
  let mut current: Option<Vec<u8>> = None;
18
35
  for &(name, params) in filters {
19
36
  let input = current.as_deref().unwrap_or(data);
20
37
  let output = match name {
21
- b"FlateDecode" | b"Fl" => predict(inflate(input, budget)?, params)?,
38
+ b"FlateDecode" | b"Fl" => predict(inflate(input, budget, strict)?, params)?,
22
39
  b"LZWDecode" | b"LZW" => {
23
40
  let early = params
24
41
  .and_then(|params| params.get(b"EarlyChange"))
@@ -48,25 +65,20 @@ pub fn decode(
48
65
 
49
66
  /// zlib, or raw deflate when the header is missing. A truncated or
50
67
  /// corrupt stream yields what inflated before the damage, as viewers show.
51
- fn inflate(data: &[u8], budget: &Budget<'_>) -> Result<Vec<u8>> {
68
+ fn inflate(data: &[u8], budget: &Budget<'_>, strict: bool) -> Result<Vec<u8>> {
52
69
  let zlib = data.len() > 2
53
70
  && data[0] & 0x0F == 8
54
71
  && (u16::from(data[0]) << 8 | u16::from(data[1])).is_multiple_of(31);
55
72
  let expected = data.len().saturating_mul(4);
56
- let (output, complete) =
57
- crate::read::inflate::inflate(data, zlib, expected, budget.inflatable())?;
58
- if complete || !output.is_empty() {
59
- return Ok(output);
60
- }
61
- if zlib {
62
- // A header that only looked like zlib's: try the bytes as raw deflate.
63
- let (output, _) =
64
- crate::read::inflate::inflate(data, false, expected, budget.inflatable())?;
65
- if !output.is_empty() {
73
+ // Retry apparent zlib data as raw deflate.
74
+ for header in std::iter::once(zlib).chain(zlib.then_some(false)) {
75
+ let (output, complete) =
76
+ crate::read::inflate::inflate(data, header, expected, budget.inflatable())?;
77
+ if complete || (!strict && !output.is_empty()) {
66
78
  return Ok(output);
67
79
  }
68
80
  }
69
- Err(malformed("a stream does not inflate"))
81
+ Err(malformed("an incomplete or corrupt Flate stream"))
70
82
  }
71
83
 
72
84
  /// Undoes a PNG (10 and up) or TIFF (2) predictor.
@@ -125,8 +125,8 @@ impl Font {
125
125
  &self,
126
126
  bytes: &[u8],
127
127
  scratch: &mut String,
128
- mut glyph: impl FnMut(&str, f64, bool),
129
- ) {
128
+ mut glyph: impl FnMut(&str, f64, bool) -> Result<()>,
129
+ ) -> Result<()> {
130
130
  let mut at = 0;
131
131
  while at < bytes.len() {
132
132
  let length = match &self.codes {
@@ -171,8 +171,9 @@ impl Font {
171
171
  .unwrap_or(*default)
172
172
  }
173
173
  };
174
- glyph(scratch, width * self.scale, length == 1 && code == 32);
174
+ glyph(scratch, width * self.scale, length == 1 && code == 32)?;
175
175
  }
176
+ Ok(())
176
177
  }
177
178
  }
178
179
 
@@ -0,0 +1,224 @@
1
+ //! Versioned replies and numeric-only arguments for the trusted PDF worker.
2
+
3
+ use std::ffi::OsString;
4
+ use std::io::Write;
5
+ use std::time::Duration;
6
+
7
+ use super::ProcessLimits;
8
+ use crate::read::Limits;
9
+ use crate::read::pdf::{ReadOptions, RenderOptions};
10
+ use crate::{Error, Result};
11
+
12
+ pub(super) const HEADER: &[u8; 6] = b"IMPR\x02\0";
13
+ pub(super) const MAX_ERROR_BYTES: usize = 4096;
14
+
15
+ pub(super) enum Operation {
16
+ Info,
17
+ Pages(ReadOptions),
18
+ Render(u32, RenderOptions),
19
+ }
20
+
21
+ impl Operation {
22
+ pub fn maximum(&self) -> Result<usize> {
23
+ let invalid = || Error::InvalidPdfOption("worker output limit is too large");
24
+ let maximum = match self {
25
+ Self::Info => MAX_ERROR_BYTES,
26
+ // JSON can escape each text byte as six ASCII bytes. Reasons and
27
+ // record metadata are separately bounded for every selected page.
28
+ Self::Pages(options) => options
29
+ .max_output_bytes
30
+ .checked_mul(6)
31
+ .and_then(|text| {
32
+ options.max_pages.checked_mul(MAX_ERROR_BYTES * 6 + 512)?.checked_add(text)
33
+ })
34
+ .and_then(|bytes| bytes.checked_add(MAX_ERROR_BYTES * 6 + 512))
35
+ .ok_or_else(invalid)?,
36
+ Self::Render(_, options) => options.max_output_bytes,
37
+ };
38
+ maximum.max(MAX_ERROR_BYTES).checked_add(HEADER.len()).ok_or_else(invalid)
39
+ }
40
+ }
41
+
42
+ pub(super) struct Request {
43
+ pub restricted: bool,
44
+ pub operation: Operation,
45
+ pub limits: Limits,
46
+ pub memory: u64,
47
+ pub cpu_seconds: u64,
48
+ }
49
+
50
+ impl Request {
51
+ pub fn new(operation: Operation, limits: &Limits, process: &ProcessLimits) -> Result<Self> {
52
+ operation.maximum()?;
53
+ match &operation {
54
+ Operation::Pages(options) => options.validate()?,
55
+ Operation::Render(0, _) => {
56
+ return Err(Error::InvalidPdfOption("page number must be positive"));
57
+ }
58
+ _ => {}
59
+ }
60
+ if process.memory_bytes == 0 || process.memory_bytes >= i64::MAX as u64 {
61
+ return Err(Error::InvalidPdfOption("memory limit must be positive and finite"));
62
+ }
63
+ let wall = limits.timeout.unwrap_or(process.wall_time).min(process.wall_time);
64
+ let cpu_seconds = wall
65
+ .as_secs()
66
+ .checked_add(u64::from(wall.subsec_nanos() != 0))
67
+ .filter(|&seconds| seconds != 0 && seconds < i64::MAX as u64)
68
+ .ok_or(Error::InvalidPdfOption("worker wall time must be positive and finite"))?;
69
+ u64::try_from(wall.as_nanos())
70
+ .map_err(|_| Error::InvalidPdfOption("worker wall time is too large"))?;
71
+ Ok(Self {
72
+ restricted: false,
73
+ operation,
74
+ limits: limits.clone().timeout(Some(wall)),
75
+ memory: process.memory_bytes,
76
+ cpu_seconds,
77
+ })
78
+ }
79
+
80
+ pub fn arguments(&self) -> Vec<OsString> {
81
+ let timeout = self.limits.timeout.unwrap_or_default();
82
+ let mut values = vec![
83
+ u64::from(self.restricted),
84
+ self.limits.max_input as u64,
85
+ self.limits.max_inflated,
86
+ self.limits.max_depth as u64,
87
+ self.memory,
88
+ self.cpu_seconds,
89
+ u64::try_from(timeout.as_nanos()).unwrap_or(u64::MAX),
90
+ ];
91
+ match &self.operation {
92
+ Operation::Info => values.push(0),
93
+ Operation::Pages(options) => values.extend([
94
+ 1,
95
+ u64::from(options.first_page),
96
+ u64::from(options.last_page),
97
+ options.max_pages as u64,
98
+ options.max_page_bytes as u64,
99
+ options.max_output_bytes as u64,
100
+ ]),
101
+ Operation::Render(number, options) => values.extend([
102
+ 2,
103
+ u64::from(*number),
104
+ u64::from(options.max_side),
105
+ options.max_pixels,
106
+ options.max_output_bytes as u64,
107
+ u64::from(options.grayscale),
108
+ ]),
109
+ }
110
+ values.into_iter().map(|number| OsString::from(number.to_string())).collect()
111
+ }
112
+
113
+ pub fn parse() -> Result<Self> {
114
+ let invalid = || Error::InvalidPdfOption("invalid PDF worker arguments");
115
+ let values: Vec<u64> = std::env::args()
116
+ .skip(1)
117
+ .map(|value| value.parse().map_err(|_| invalid()))
118
+ .collect::<Result<_>>()?;
119
+ let [restricted, input, inflated, depth, memory, cpu, timeout, rest @ ..] =
120
+ values.as_slice()
121
+ else {
122
+ return Err(invalid());
123
+ };
124
+ if *restricted > 1 {
125
+ return Err(invalid());
126
+ }
127
+ let length = |value| usize::try_from(value).map_err(|_| invalid());
128
+ let operation = match rest {
129
+ [0] => Operation::Info,
130
+ [1, first, last, count, page, output] => Operation::Pages(ReadOptions {
131
+ first_page: u32::try_from(*first).map_err(|_| invalid())?,
132
+ last_page: u32::try_from(*last).map_err(|_| invalid())?,
133
+ max_pages: length(*count)?,
134
+ max_page_bytes: length(*page)?,
135
+ max_output_bytes: length(*output)?,
136
+ }),
137
+ [2, number, side, pixels, output, gray] if *gray <= 1 => Operation::Render(
138
+ u32::try_from(*number).map_err(|_| invalid())?,
139
+ RenderOptions {
140
+ max_side: u16::try_from(*side).map_err(|_| invalid())?,
141
+ max_pixels: *pixels,
142
+ max_output_bytes: length(*output)?,
143
+ grayscale: *gray == 1,
144
+ },
145
+ ),
146
+ _ => return Err(invalid()),
147
+ };
148
+ let limits = Limits::default()
149
+ .max_input(length(*input)?)
150
+ .max_inflated(*inflated)
151
+ .max_depth(length(*depth)?)
152
+ .timeout(Some(Duration::from_nanos(*timeout)));
153
+ let process =
154
+ ProcessLimits { memory_bytes: *memory, wall_time: Duration::from_nanos(*timeout) };
155
+ let mut request = Self::new(operation, &limits, &process)?;
156
+ request.restricted = *restricted == 1;
157
+ if request.cpu_seconds != *cpu {
158
+ return Err(invalid());
159
+ }
160
+ Ok(request)
161
+ }
162
+ }
163
+
164
+ pub(super) fn failure(error: &Error) -> (u8, String) {
165
+ let kind = match error {
166
+ Error::PasswordRequired => 1,
167
+ Error::UnsupportedDocument(_) => 2,
168
+ Error::LimitExceeded(_) | Error::Cancelled => 3,
169
+ Error::InvalidPdfOption(_) => 4,
170
+ _ => 5,
171
+ };
172
+ let mut message = error.to_string();
173
+ let mut end = message.len().min(MAX_ERROR_BYTES);
174
+ while !message.is_char_boundary(end) {
175
+ end -= 1;
176
+ }
177
+ message.truncate(end);
178
+ (kind, message)
179
+ }
180
+
181
+ pub(super) fn error(kind: u8, message: String) -> Error {
182
+ match kind {
183
+ 1 => Error::PasswordRequired,
184
+ 2 => Error::UnsupportedDocument(message),
185
+ 3 => Error::LimitExceeded("PDF worker limit"),
186
+ 4 => Error::InvalidPdfOption("the PDF worker rejected the options"),
187
+ _ => Error::malformed("PDF worker", message),
188
+ }
189
+ }
190
+
191
+ pub(super) fn write_reply(mut out: impl Write, result: Result<Vec<u8>>) -> Result<()> {
192
+ let mut header = *HEADER;
193
+ match result {
194
+ Ok(data) => {
195
+ out.write_all(&header)?;
196
+ out.write_all(&data)?;
197
+ }
198
+ Err(error) => {
199
+ let (kind, message) = failure(&error);
200
+ header[5] = kind;
201
+ out.write_all(&header)?;
202
+ out.write_all(message.as_bytes())?;
203
+ }
204
+ }
205
+ out.flush()?;
206
+ Ok(())
207
+ }
208
+
209
+ pub(super) fn decode(mut data: Vec<u8>, maximum: usize) -> Result<Vec<u8>> {
210
+ if data.len() < HEADER.len() || data[..5] != HEADER[..5] {
211
+ return Err(Error::malformed("PDF worker", "missing or incompatible response"));
212
+ }
213
+ let status = data[5];
214
+ data.drain(..HEADER.len());
215
+ if status == 0 {
216
+ return if data.len() > maximum {
217
+ Err(Error::LimitExceeded("PDF worker output"))
218
+ } else {
219
+ Ok(data)
220
+ };
221
+ }
222
+ let message = String::from_utf8_lossy(&data[..data.len().min(MAX_ERROR_BYTES)]).into_owned();
223
+ Err(error(status, message))
224
+ }
@@ -0,0 +1,113 @@
1
+ //! Flushed page records survive a worker timeout or resource termination.
2
+
3
+ use std::io::Write;
4
+
5
+ use serde::{Deserialize, Serialize};
6
+
7
+ use super::{Output, protocol};
8
+ use crate::read::pdf::{Info, PageResult, Pages, ReadOptions, Status, bounded};
9
+ use crate::{Error, Result};
10
+
11
+ #[derive(Serialize, Deserialize)]
12
+ pub(super) enum Frame {
13
+ Info(Info),
14
+ Page(PageResult),
15
+ End,
16
+ Error { kind: u8, message: String },
17
+ }
18
+
19
+ pub(super) fn write(out: &mut impl Write, frame: &Frame) -> Result<()> {
20
+ serde_json::to_writer(&mut *out, frame)
21
+ .map_err(|error| Error::malformed("PDF worker", error.to_string()))?;
22
+ out.write_all(b"\n")?;
23
+ out.flush()?;
24
+ Ok(())
25
+ }
26
+
27
+ pub(super) fn decode(output: Output, options: &ReadOptions) -> Result<Pages> {
28
+ let mut failure = output.failure;
29
+ if output.data.len() < protocol::HEADER.len() {
30
+ return Err(failure.unwrap_or_else(|| malformed("missing response")));
31
+ }
32
+ let bytes =
33
+ protocol::decode(output.data, protocol::Operation::Pages(options.clone()).maximum()?)?;
34
+ let mut info = None;
35
+ let mut pages = Vec::new();
36
+ let mut text_bytes = 0_usize;
37
+ let mut ended = false;
38
+ for line in bytes.split_inclusive(|&byte| byte == b'\n') {
39
+ if ended {
40
+ return Err(malformed("records after the end of the response"));
41
+ }
42
+ if !line.ends_with(b"\n") && failure.is_some() {
43
+ break; // A killed worker may leave one incomplete final record.
44
+ }
45
+ let frame: Frame =
46
+ serde_json::from_slice(line).map_err(|error| malformed(&error.to_string()))?;
47
+ match frame {
48
+ Frame::Info(value) if info.is_none() && pages.is_empty() => info = Some(value),
49
+ Frame::Page(page) => {
50
+ let metadata = info.ok_or_else(|| malformed("page before metadata"))?;
51
+ validate(&page, pages.len(), metadata, options)?;
52
+ text_bytes = text_bytes
53
+ .checked_add(page.text.len())
54
+ .filter(|&bytes| bytes <= options.max_output_bytes)
55
+ .ok_or_else(|| malformed("text exceeds the requested limit"))?;
56
+ pages.push(page);
57
+ }
58
+ Frame::End => ended = true,
59
+ Frame::Error { kind, message } => {
60
+ failure = Some(protocol::error(kind, message));
61
+ ended = true;
62
+ }
63
+ Frame::Info(_) => return Err(malformed("duplicate metadata")),
64
+ }
65
+ }
66
+ let info =
67
+ info.ok_or_else(|| failure.take().unwrap_or_else(|| malformed("missing metadata")))?;
68
+ let count = selected(info, options);
69
+ if pages.len() == count && ended {
70
+ return Ok(Pages { info, pages });
71
+ }
72
+ let error = failure.ok_or_else(|| malformed("incomplete response"))?;
73
+ let reason = error.to_string();
74
+ let timed_out = matches!(
75
+ error,
76
+ Error::LimitExceeded("time" | "render worker wall time" | "PDF worker limit")
77
+ );
78
+ let first_missing = pages.len();
79
+ for index in first_missing..count {
80
+ let number =
81
+ options.first_page + u32::try_from(index).map_err(|_| malformed("page index"))?;
82
+ let status =
83
+ if timed_out || index > first_missing { Status::Skipped } else { Status::Failed };
84
+ pages.push(bounded::unread(number, status, reason.clone()));
85
+ }
86
+ Ok(Pages { info, pages })
87
+ }
88
+
89
+ fn selected(info: Info, options: &ReadOptions) -> usize {
90
+ let count = options
91
+ .last_page
92
+ .min(info.page_count)
93
+ .checked_sub(options.first_page)
94
+ .map_or(0, |difference| u64::from(difference) + 1);
95
+ usize::try_from(count).unwrap_or(usize::MAX).min(options.max_pages)
96
+ }
97
+
98
+ fn validate(page: &PageResult, index: usize, info: Info, options: &ReadOptions) -> Result<()> {
99
+ let expected = u64::from(options.first_page) + index as u64;
100
+ if index >= selected(info, options)
101
+ || u64::from(page.number) != expected
102
+ || page.text.len() > options.max_page_bytes
103
+ || page.reason.as_ref().is_some_and(|reason| reason.len() > protocol::MAX_ERROR_BYTES)
104
+ || (matches!(page.status, Status::Failed | Status::Skipped) && !page.text.is_empty())
105
+ {
106
+ return Err(malformed("invalid page record"));
107
+ }
108
+ Ok(())
109
+ }
110
+
111
+ fn malformed(reason: &str) -> Error {
112
+ Error::malformed("PDF worker", reason)
113
+ }