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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/Cargo.lock +472 -5
  4. data/Cargo.toml +16 -1
  5. data/README.md +59 -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 +29 -13
  9. data/ext/imprimatur_native/core/README.md +13 -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 +190 -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 +155 -0
  35. data/ext/imprimatur_native/core/src/read/pdf/isolation/system.rs +71 -0
  36. data/ext/imprimatur_native/core/src/read/pdf/isolation/worker.rs +43 -0
  37. data/ext/imprimatur_native/core/src/read/pdf/isolation.rs +166 -0
  38. data/ext/imprimatur_native/core/src/read/pdf/render.rs +206 -0
  39. data/ext/imprimatur_native/core/src/read/pdf.rs +16 -8
  40. data/ext/imprimatur_native/core/src/read/xls/strings.rs +2 -1
  41. data/ext/imprimatur_native/core/src/read/xls.rs +1 -8
  42. data/ext/imprimatur_native/core/src/read.rs +6 -5
  43. data/ext/imprimatur_native/core/src/test_support/font.rs +37 -0
  44. data/ext/imprimatur_native/core/src/xlsx/validation.rs +28 -18
  45. data/ext/imprimatur_native/core/src/zip/reader.rs +1 -8
  46. data/ext/imprimatur_native/extconf.rb +18 -0
  47. data/ext/imprimatur_native/ffi/Cargo.toml +2 -2
  48. data/ext/imprimatur_native/ffi/src/document.rs +6 -0
  49. data/ext/imprimatur_native/ffi/src/lib.rs +7 -0
  50. data/ext/imprimatur_native/ffi/src/pdf.rs +73 -0
  51. data/ext/imprimatur_native/ffi/src/read.rs +18 -12
  52. data/lib/imprimatur/document.rb +4 -2
  53. data/lib/imprimatur/native.rb +7 -0
  54. data/lib/imprimatur/pdf_reader.rb +27 -0
  55. data/lib/imprimatur/reader.rb +48 -11
  56. data/lib/imprimatur/spreadsheet.rb +1 -1
  57. data/lib/imprimatur/version.rb +1 -1
  58. data/lib/imprimatur.rb +28 -3
  59. 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
- /// 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,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
+ }