imprimatur 0.4.0 → 0.6.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/Cargo.lock +472 -5
- data/Cargo.toml +16 -1
- data/README.md +59 -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 +29 -13
- data/ext/imprimatur_native/core/README.md +13 -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 +190 -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 +155 -0
- data/ext/imprimatur_native/core/src/read/pdf/isolation/system.rs +71 -0
- data/ext/imprimatur_native/core/src/read/pdf/isolation/worker.rs +43 -0
- data/ext/imprimatur_native/core/src/read/pdf/isolation.rs +166 -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 +73 -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 +27 -0
- data/lib/imprimatur/reader.rb +48 -11
- data/lib/imprimatur/spreadsheet.rb +1 -1
- data/lib/imprimatur/version.rb +1 -1
- data/lib/imprimatur.rb +28 -3
- metadata +19 -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,155 @@
|
|
|
1
|
+
//! A versioned, bounded reply and numeric-only worker arguments.
|
|
2
|
+
|
|
3
|
+
use std::ffi::OsString;
|
|
4
|
+
use std::io::Write;
|
|
5
|
+
|
|
6
|
+
use super::ProcessLimits;
|
|
7
|
+
use crate::read::Limits;
|
|
8
|
+
use crate::read::pdf::RenderOptions;
|
|
9
|
+
use crate::{Error, Result};
|
|
10
|
+
|
|
11
|
+
pub(super) const HEADER: &[u8; 6] = b"IMPR\x01\0";
|
|
12
|
+
pub(super) const MAX_ERROR_BYTES: usize = 4096;
|
|
13
|
+
|
|
14
|
+
pub(super) struct Request {
|
|
15
|
+
pub number: u32,
|
|
16
|
+
pub options: RenderOptions,
|
|
17
|
+
pub limits: Limits,
|
|
18
|
+
pub memory: u64,
|
|
19
|
+
pub cpu_seconds: u64,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
impl Request {
|
|
23
|
+
pub fn new(
|
|
24
|
+
number: u32,
|
|
25
|
+
options: &RenderOptions,
|
|
26
|
+
limits: &Limits,
|
|
27
|
+
process: &ProcessLimits,
|
|
28
|
+
) -> Result<Self> {
|
|
29
|
+
options
|
|
30
|
+
.max_output_bytes
|
|
31
|
+
.checked_add(HEADER.len())
|
|
32
|
+
.ok_or(Error::InvalidPdfOption("output limit is too large"))?;
|
|
33
|
+
if process.memory_bytes == 0 || process.memory_bytes >= i64::MAX as u64 {
|
|
34
|
+
return Err(Error::InvalidPdfOption("memory limit must be positive and finite"));
|
|
35
|
+
}
|
|
36
|
+
let cpu_seconds = process
|
|
37
|
+
.wall_time
|
|
38
|
+
.as_secs()
|
|
39
|
+
.checked_add(u64::from(process.wall_time.subsec_nanos() != 0))
|
|
40
|
+
.filter(|&seconds| seconds != 0 && seconds < i64::MAX as u64)
|
|
41
|
+
.ok_or(Error::InvalidPdfOption("worker wall time must be positive and finite"))?;
|
|
42
|
+
Ok(Self {
|
|
43
|
+
number,
|
|
44
|
+
options: options.clone(),
|
|
45
|
+
limits: limits.clone(),
|
|
46
|
+
memory: process.memory_bytes,
|
|
47
|
+
cpu_seconds,
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
pub fn arguments(&self) -> Vec<OsString> {
|
|
52
|
+
[
|
|
53
|
+
u64::from(self.number),
|
|
54
|
+
u64::from(self.options.max_side),
|
|
55
|
+
self.options.max_pixels,
|
|
56
|
+
self.options.max_output_bytes as u64,
|
|
57
|
+
u64::from(self.options.grayscale),
|
|
58
|
+
self.limits.max_input as u64,
|
|
59
|
+
self.limits.max_inflated,
|
|
60
|
+
self.limits.max_depth as u64,
|
|
61
|
+
self.memory,
|
|
62
|
+
self.cpu_seconds,
|
|
63
|
+
]
|
|
64
|
+
.map(|number| OsString::from(number.to_string()))
|
|
65
|
+
.to_vec()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
pub fn parse() -> Result<Self> {
|
|
69
|
+
let invalid = || Error::InvalidPdfOption("invalid render worker arguments");
|
|
70
|
+
let values: Vec<u64> = std::env::args()
|
|
71
|
+
.skip(1)
|
|
72
|
+
.map(|value| value.parse().map_err(|_| invalid()))
|
|
73
|
+
.collect::<Result<_>>()?;
|
|
74
|
+
let [number, side, pixels, output, gray, input, inflated, depth, memory, cpu] =
|
|
75
|
+
values.as_slice()
|
|
76
|
+
else {
|
|
77
|
+
return Err(invalid());
|
|
78
|
+
};
|
|
79
|
+
if *gray > 1
|
|
80
|
+
|| *memory == 0
|
|
81
|
+
|| *memory >= i64::MAX as u64
|
|
82
|
+
|| *cpu == 0
|
|
83
|
+
|| *cpu >= i64::MAX as u64
|
|
84
|
+
{
|
|
85
|
+
return Err(invalid());
|
|
86
|
+
}
|
|
87
|
+
Ok(Self {
|
|
88
|
+
number: u32::try_from(*number).map_err(|_| invalid())?,
|
|
89
|
+
options: RenderOptions {
|
|
90
|
+
max_side: u16::try_from(*side).map_err(|_| invalid())?,
|
|
91
|
+
max_pixels: *pixels,
|
|
92
|
+
max_output_bytes: usize::try_from(*output).map_err(|_| invalid())?,
|
|
93
|
+
grayscale: *gray == 1,
|
|
94
|
+
},
|
|
95
|
+
limits: Limits::default()
|
|
96
|
+
.max_input(usize::try_from(*input).map_err(|_| invalid())?)
|
|
97
|
+
.max_inflated(*inflated)
|
|
98
|
+
.max_depth(usize::try_from(*depth).map_err(|_| invalid())?)
|
|
99
|
+
.timeout(None),
|
|
100
|
+
memory: *memory,
|
|
101
|
+
cpu_seconds: *cpu,
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
pub(super) fn write_reply(mut out: impl Write, result: Result<Vec<u8>>) -> Result<()> {
|
|
107
|
+
let mut header = *HEADER;
|
|
108
|
+
match result {
|
|
109
|
+
Ok(data) => {
|
|
110
|
+
out.write_all(&header)?;
|
|
111
|
+
out.write_all(&data)?;
|
|
112
|
+
}
|
|
113
|
+
Err(error) => {
|
|
114
|
+
header[5] = match &error {
|
|
115
|
+
Error::PasswordRequired => 1,
|
|
116
|
+
Error::UnsupportedDocument(_) => 2,
|
|
117
|
+
Error::LimitExceeded(_) | Error::Cancelled => 3,
|
|
118
|
+
Error::InvalidPdfOption(_) => 4,
|
|
119
|
+
_ => 5,
|
|
120
|
+
};
|
|
121
|
+
let message = error.to_string();
|
|
122
|
+
let mut end = message.len().min(MAX_ERROR_BYTES);
|
|
123
|
+
while !message.is_char_boundary(end) {
|
|
124
|
+
end -= 1;
|
|
125
|
+
}
|
|
126
|
+
out.write_all(&header)?;
|
|
127
|
+
out.write_all(&message.as_bytes()[..end])?;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
out.flush()?;
|
|
131
|
+
Ok(())
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
pub(super) fn decode(mut data: Vec<u8>, maximum: usize) -> Result<Vec<u8>> {
|
|
135
|
+
if data.len() < HEADER.len() || data[..5] != HEADER[..5] {
|
|
136
|
+
return Err(Error::malformed("render worker", "missing or incompatible response"));
|
|
137
|
+
}
|
|
138
|
+
let status = data[5];
|
|
139
|
+
data.drain(..HEADER.len());
|
|
140
|
+
if status == 0 {
|
|
141
|
+
return if data.len() > maximum {
|
|
142
|
+
Err(Error::LimitExceeded("rendered bytes"))
|
|
143
|
+
} else {
|
|
144
|
+
Ok(data)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
let message = String::from_utf8_lossy(&data[..data.len().min(MAX_ERROR_BYTES)]).into_owned();
|
|
148
|
+
Err(match status {
|
|
149
|
+
1 => Error::PasswordRequired,
|
|
150
|
+
2 => Error::UnsupportedDocument(message),
|
|
151
|
+
3 => Error::LimitExceeded("render worker limit"),
|
|
152
|
+
4 => Error::InvalidPdfOption("the render worker rejected the rendering options"),
|
|
153
|
+
_ => Error::malformed("render worker", message),
|
|
154
|
+
})
|
|
155
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
//! OS resource enforcement. macOS uses the supervisor's RSS watchdog.
|
|
2
|
+
|
|
3
|
+
#[cfg(test)]
|
|
4
|
+
mod tests;
|
|
5
|
+
|
|
6
|
+
use std::process::Child;
|
|
7
|
+
|
|
8
|
+
use nix::errno::Errno;
|
|
9
|
+
use nix::sys::resource::{Resource, getrlimit, rlim_t, setrlimit};
|
|
10
|
+
use nix::sys::signal::{Signal, killpg};
|
|
11
|
+
use nix::unistd::Pid;
|
|
12
|
+
|
|
13
|
+
use crate::{Error, Result};
|
|
14
|
+
|
|
15
|
+
pub(super) fn apply_limits(memory: u64, cpu: u64, output: u64) -> Result<()> {
|
|
16
|
+
if memory == 0 {
|
|
17
|
+
return Err(Error::InvalidPdfOption("memory limit must be positive"));
|
|
18
|
+
}
|
|
19
|
+
#[cfg(target_os = "linux")]
|
|
20
|
+
lower(Resource::RLIMIT_AS, memory)?;
|
|
21
|
+
#[cfg(target_os = "freebsd")]
|
|
22
|
+
lower(Resource::RLIMIT_VMEM, memory)?;
|
|
23
|
+
#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "macos")))]
|
|
24
|
+
return Err(Error::UnsupportedDocument("process limits are unsupported on this OS".into()));
|
|
25
|
+
lower(Resource::RLIMIT_CORE, 0)?;
|
|
26
|
+
lower(Resource::RLIMIT_CPU, cpu)?;
|
|
27
|
+
lower(Resource::RLIMIT_FSIZE, output)?;
|
|
28
|
+
Ok(())
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fn lower(resource: Resource, value: u64) -> Result<()> {
|
|
32
|
+
let (soft, hard) = getrlimit(resource).map_err(std::io::Error::from)?;
|
|
33
|
+
let value = bounded_limit(value, soft, hard)?;
|
|
34
|
+
setrlimit(resource, value, value).map_err(std::io::Error::from)?;
|
|
35
|
+
Ok(())
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn bounded_limit(value: u64, soft: rlim_t, hard: rlim_t) -> Result<rlim_t> {
|
|
39
|
+
let value = i128::from(value).min(i128::from(soft)).min(i128::from(hard));
|
|
40
|
+
value.try_into().map_err(|_| Error::InvalidPdfOption("resource limit is out of range"))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#[cfg(target_os = "macos")]
|
|
44
|
+
pub(super) fn check_memory(pid: u32, maximum: u64) -> Result<()> {
|
|
45
|
+
let pid = i32::try_from(pid).map_err(|_| Error::InvalidPdfOption("invalid worker PID"))?;
|
|
46
|
+
let usage = libproc::proc_pid::pidinfo::<libproc::task_info::TaskInfo>(pid, 0)
|
|
47
|
+
.map_err(|error| std::io::Error::other(format!("cannot monitor render worker: {error}")))?;
|
|
48
|
+
if usage.pti_resident_size > maximum {
|
|
49
|
+
return Err(Error::LimitExceeded("render worker memory"));
|
|
50
|
+
}
|
|
51
|
+
Ok(())
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
pub(super) fn terminate(child: &mut Child) {
|
|
55
|
+
if let Ok(pid) = i32::try_from(child.id())
|
|
56
|
+
&& let Err(error) = killpg(Pid::from_raw(pid), Signal::SIGKILL)
|
|
57
|
+
&& error != Errno::ESRCH
|
|
58
|
+
{
|
|
59
|
+
eprintln!("imprimatur: could not kill render worker group: {error}");
|
|
60
|
+
}
|
|
61
|
+
// Also handle a child that failed before joining its new process group.
|
|
62
|
+
if let Err(error) = child.kill()
|
|
63
|
+
&& error.kind() != std::io::ErrorKind::InvalidInput
|
|
64
|
+
&& error.raw_os_error() != Some(Errno::ESRCH as i32)
|
|
65
|
+
{
|
|
66
|
+
eprintln!("imprimatur: could not kill render worker: {error}");
|
|
67
|
+
}
|
|
68
|
+
if let Err(error) = child.wait() {
|
|
69
|
+
eprintln!("imprimatur: could not reap render worker: {error}");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//! The companion worker sets limits before touching the document.
|
|
2
|
+
|
|
3
|
+
use std::io::Read;
|
|
4
|
+
use std::process::ExitCode;
|
|
5
|
+
|
|
6
|
+
use super::{protocol, system};
|
|
7
|
+
use crate::read::pdf::render_page;
|
|
8
|
+
use crate::{Error, Result};
|
|
9
|
+
|
|
10
|
+
/// Internal entry point for the bundled worker executable.
|
|
11
|
+
#[doc(hidden)]
|
|
12
|
+
#[must_use]
|
|
13
|
+
pub fn worker_main() -> ExitCode {
|
|
14
|
+
match protocol::write_reply(std::io::stdout().lock(), run()) {
|
|
15
|
+
Ok(()) => ExitCode::SUCCESS,
|
|
16
|
+
Err(error) => {
|
|
17
|
+
eprintln!("imprimatur render worker: {error}");
|
|
18
|
+
ExitCode::FAILURE
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fn run() -> Result<Vec<u8>> {
|
|
24
|
+
let request = protocol::Request::parse()?;
|
|
25
|
+
let output = request
|
|
26
|
+
.options
|
|
27
|
+
.max_output_bytes
|
|
28
|
+
.max(protocol::MAX_ERROR_BYTES)
|
|
29
|
+
.checked_add(protocol::HEADER.len())
|
|
30
|
+
.and_then(|value| u64::try_from(value).ok())
|
|
31
|
+
.ok_or(Error::InvalidPdfOption("output limit is too large"))?;
|
|
32
|
+
system::apply_limits(request.memory, request.cpu_seconds, output)?;
|
|
33
|
+
let mut bytes = Vec::new();
|
|
34
|
+
let maximum = u64::try_from(request.limits.max_input)
|
|
35
|
+
.ok()
|
|
36
|
+
.and_then(|value| value.checked_add(1))
|
|
37
|
+
.ok_or(Error::InvalidPdfOption("input limit is too large"))?;
|
|
38
|
+
std::io::stdin().lock().take(maximum).read_to_end(&mut bytes)?;
|
|
39
|
+
if bytes.len() > request.limits.max_input {
|
|
40
|
+
return Err(Error::LimitExceeded("input size"));
|
|
41
|
+
}
|
|
42
|
+
render_page(&bytes, request.number, &request.options, &request.limits).map(|image| image.data)
|
|
43
|
+
}
|