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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/Cargo.lock +473 -5
- data/Cargo.toml +17 -1
- data/README.md +63 -7
- data/ext/imprimatur_native/cli/Cargo.toml +1 -1
- data/ext/imprimatur_native/cli/src/app.rs +3 -1
- data/ext/imprimatur_native/core/Cargo.toml +30 -13
- data/ext/imprimatur_native/core/README.md +18 -1
- data/ext/imprimatur_native/core/src/bin/imprimatur-render-worker.rs +10 -0
- data/ext/imprimatur_native/core/src/bytes.rs +16 -0
- data/ext/imprimatur_native/core/src/doc/picture.rs +3 -7
- data/ext/imprimatur_native/core/src/doc/size.rs +14 -0
- data/ext/imprimatur_native/core/src/doc/symbol.rs +3 -5
- data/ext/imprimatur_native/core/src/doc.rs +24 -0
- data/ext/imprimatur_native/core/src/docx.rs +6 -5
- data/ext/imprimatur_native/core/src/error.rs +6 -0
- data/ext/imprimatur_native/core/src/font.rs +12 -0
- data/ext/imprimatur_native/core/src/lib.rs +6 -0
- data/ext/imprimatur_native/core/src/pdf/layout.rs +13 -5
- data/ext/imprimatur_native/core/src/pdf/objects.rs +1 -1
- data/ext/imprimatur_native/core/src/pdf/truetype/descriptor.rs +1 -0
- data/ext/imprimatur_native/core/src/read/budget.rs +10 -5
- data/ext/imprimatur_native/core/src/read/content.rs +16 -0
- data/ext/imprimatur_native/core/src/read/image/dimensions.rs +2 -2
- data/ext/imprimatur_native/core/src/read/ole.rs +1 -8
- data/ext/imprimatur_native/core/src/read/pdf/bounded.rs +249 -0
- data/ext/imprimatur_native/core/src/read/pdf/content/state.rs +50 -0
- data/ext/imprimatur_native/core/src/read/pdf/content.rs +43 -54
- data/ext/imprimatur_native/core/src/read/pdf/document/pages.rs +140 -0
- data/ext/imprimatur_native/core/src/read/pdf/document.rs +20 -110
- data/ext/imprimatur_native/core/src/read/pdf/filter.rs +25 -13
- data/ext/imprimatur_native/core/src/read/pdf/font.rs +4 -3
- data/ext/imprimatur_native/core/src/read/pdf/isolation/protocol.rs +224 -0
- data/ext/imprimatur_native/core/src/read/pdf/isolation/stream.rs +113 -0
- data/ext/imprimatur_native/core/src/read/pdf/isolation/system.rs +98 -0
- data/ext/imprimatur_native/core/src/read/pdf/isolation/worker.rs +74 -0
- data/ext/imprimatur_native/core/src/read/pdf/isolation.rs +255 -0
- data/ext/imprimatur_native/core/src/read/pdf/render.rs +206 -0
- data/ext/imprimatur_native/core/src/read/pdf.rs +16 -8
- data/ext/imprimatur_native/core/src/read/xls/strings.rs +2 -1
- data/ext/imprimatur_native/core/src/read/xls.rs +1 -8
- data/ext/imprimatur_native/core/src/read.rs +6 -5
- data/ext/imprimatur_native/core/src/test_support/font.rs +37 -0
- data/ext/imprimatur_native/core/src/xlsx/validation.rs +28 -18
- data/ext/imprimatur_native/core/src/zip/reader.rs +1 -8
- data/ext/imprimatur_native/extconf.rb +18 -0
- data/ext/imprimatur_native/ffi/Cargo.toml +2 -2
- data/ext/imprimatur_native/ffi/src/document.rs +6 -0
- data/ext/imprimatur_native/ffi/src/lib.rs +7 -0
- data/ext/imprimatur_native/ffi/src/pdf.rs +88 -0
- data/ext/imprimatur_native/ffi/src/read.rs +18 -12
- data/lib/imprimatur/document.rb +4 -2
- data/lib/imprimatur/native.rb +7 -0
- data/lib/imprimatur/pdf_reader.rb +29 -0
- data/lib/imprimatur/version.rb +1 -1
- data/lib/imprimatur.rb +26 -2
- 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
|
-
|
|
289
|
-
|
|
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
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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<'_
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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("
|
|
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
|
+
}
|