imprimatur 0.6.0 → 0.7.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 +29 -5
- data/Cargo.lock +4 -3
- data/Cargo.toml +2 -1
- data/README.md +18 -7
- data/ext/imprimatur_native/cli/Cargo.toml +1 -1
- data/ext/imprimatur_native/cli/src/cli/markdown.rs +17 -5
- data/ext/imprimatur_native/core/Cargo.toml +3 -2
- data/ext/imprimatur_native/core/README.md +6 -1
- data/ext/imprimatur_native/core/src/read/budget.rs +15 -0
- data/ext/imprimatur_native/core/src/read/content.rs +1 -0
- data/ext/imprimatur_native/core/src/read/json.rs +1 -1
- data/ext/imprimatur_native/core/src/read/markdown.rs +8 -1
- data/ext/imprimatur_native/core/src/read/pdf/bounded.rs +94 -35
- data/ext/imprimatur_native/core/src/read/pdf/document/labels.rs +152 -0
- data/ext/imprimatur_native/core/src/read/pdf/document.rs +1 -0
- data/ext/imprimatur_native/core/src/read/pdf/isolation/protocol.rs +145 -76
- data/ext/imprimatur_native/core/src/read/pdf/isolation/stream.rs +113 -0
- data/ext/imprimatur_native/core/src/read/pdf/isolation/system.rs +27 -0
- data/ext/imprimatur_native/core/src/read/pdf/isolation/worker.rs +49 -18
- data/ext/imprimatur_native/core/src/read/pdf/isolation.rs +117 -28
- data/ext/imprimatur_native/core/src/read/pdf/layout.rs +190 -54
- data/ext/imprimatur_native/core/src/read/pdf.rs +2 -1
- data/ext/imprimatur_native/ffi/Cargo.toml +1 -1
- data/ext/imprimatur_native/ffi/src/lib.rs +2 -2
- data/ext/imprimatur_native/ffi/src/pdf.rs +20 -5
- data/lib/imprimatur/pdf_reader.rb +6 -4
- data/lib/imprimatur/version.rb +1 -1
- metadata +3 -1
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
use std::collections::HashSet;
|
|
2
|
+
|
|
3
|
+
use super::{Dict, Document, Object, Result};
|
|
4
|
+
|
|
5
|
+
struct LabelRange {
|
|
6
|
+
start: u32,
|
|
7
|
+
style: Option<u8>,
|
|
8
|
+
prefix: Option<String>,
|
|
9
|
+
first: i64,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ROMAN: &[(i64, &str)] = &[
|
|
13
|
+
(1000, "M"),
|
|
14
|
+
(900, "CM"),
|
|
15
|
+
(500, "D"),
|
|
16
|
+
(400, "CD"),
|
|
17
|
+
(100, "C"),
|
|
18
|
+
(90, "XC"),
|
|
19
|
+
(50, "L"),
|
|
20
|
+
(40, "XL"),
|
|
21
|
+
(10, "X"),
|
|
22
|
+
(9, "IX"),
|
|
23
|
+
(5, "V"),
|
|
24
|
+
(4, "IV"),
|
|
25
|
+
(1, "I"),
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
impl Document<'_> {
|
|
29
|
+
pub fn page_labels(&self, count: usize) -> Result<Option<Vec<Option<String>>>> {
|
|
30
|
+
let Some(ranges) = self.label_ranges()? else { return Ok(None) };
|
|
31
|
+
Ok(Some(
|
|
32
|
+
(0..count)
|
|
33
|
+
.map(|index| label_for(&ranges, u32::try_from(index).unwrap_or(u32::MAX)))
|
|
34
|
+
.collect(),
|
|
35
|
+
))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn label_ranges(&self) -> Result<Option<Vec<LabelRange>>> {
|
|
39
|
+
let Some(root) = self.trailer().get(b"Root") else { return Ok(None) };
|
|
40
|
+
let root = self.resolve(root)?;
|
|
41
|
+
let Some(catalog) = root.as_dict() else { return Ok(None) };
|
|
42
|
+
let Some(tree) = catalog.get(b"PageLabels") else { return Ok(None) };
|
|
43
|
+
let mut ranges = Vec::new();
|
|
44
|
+
self.walk_labels(tree, 0, &mut HashSet::new(), &mut ranges)?;
|
|
45
|
+
ranges.sort_by_key(|range| range.start);
|
|
46
|
+
ranges.dedup_by_key(|range| range.start);
|
|
47
|
+
Ok(Some(ranges))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn walk_labels(
|
|
51
|
+
&self,
|
|
52
|
+
node: &Object,
|
|
53
|
+
depth: usize,
|
|
54
|
+
visited: &mut HashSet<u32>,
|
|
55
|
+
out: &mut Vec<LabelRange>,
|
|
56
|
+
) -> Result<()> {
|
|
57
|
+
self.budget.check()?;
|
|
58
|
+
self.budget.nest(depth)?;
|
|
59
|
+
if let Some((number, _)) = node.as_reference()
|
|
60
|
+
&& !visited.insert(number)
|
|
61
|
+
{
|
|
62
|
+
return Ok(());
|
|
63
|
+
}
|
|
64
|
+
let node = self.resolve(node)?;
|
|
65
|
+
let Some(dict) = node.as_dict() else { return Ok(()) };
|
|
66
|
+
if let Some(Object::Array(nums)) = self.entry(dict, b"Nums")?.as_deref() {
|
|
67
|
+
let mut nums = nums.iter();
|
|
68
|
+
while let (Some(start), Some(value)) = (nums.next(), nums.next()) {
|
|
69
|
+
let Some(start) =
|
|
70
|
+
self.resolve(start)?.as_integer().and_then(|start| u32::try_from(start).ok())
|
|
71
|
+
else {
|
|
72
|
+
continue;
|
|
73
|
+
};
|
|
74
|
+
let value = self.resolve(value)?;
|
|
75
|
+
if let Some(label) = value.as_dict() {
|
|
76
|
+
out.push(self.label_range(start, label)?);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if let Some(Object::Array(kids)) = self.entry(dict, b"Kids")?.as_deref() {
|
|
81
|
+
for kid in kids {
|
|
82
|
+
self.walk_labels(kid, depth + 1, visited, out)?;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
Ok(())
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fn label_range(&self, start: u32, dict: &Dict) -> Result<LabelRange> {
|
|
89
|
+
let style = self
|
|
90
|
+
.entry(dict, b"S")?
|
|
91
|
+
.as_deref()
|
|
92
|
+
.and_then(Object::as_name)
|
|
93
|
+
.and_then(|name| name.first().copied());
|
|
94
|
+
let prefix = self
|
|
95
|
+
.entry(dict, b"P")?
|
|
96
|
+
.as_deref()
|
|
97
|
+
.and_then(Object::as_bytes)
|
|
98
|
+
.map(|bytes| String::from_utf8_lossy(bytes).into_owned());
|
|
99
|
+
let first = self.entry(dict, b"St")?.as_deref().and_then(Object::as_integer).unwrap_or(1);
|
|
100
|
+
Ok(LabelRange { start, style, prefix, first })
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fn label_for(ranges: &[LabelRange], index: u32) -> Option<String> {
|
|
105
|
+
let range = ranges.iter().rev().find(|range| range.start <= index)?;
|
|
106
|
+
let offset = i64::from(index - range.start);
|
|
107
|
+
let number = range.first.checked_add(offset)?;
|
|
108
|
+
let mut label = range.prefix.clone().unwrap_or_default();
|
|
109
|
+
if let Some(style) = range.style {
|
|
110
|
+
label.push_str(&numeral(style, number));
|
|
111
|
+
}
|
|
112
|
+
Some(label)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fn numeral(style: u8, number: i64) -> String {
|
|
116
|
+
match style {
|
|
117
|
+
b'R' => roman(number, true),
|
|
118
|
+
b'r' => roman(number, false),
|
|
119
|
+
b'A' => alpha(number, true),
|
|
120
|
+
b'a' => alpha(number, false),
|
|
121
|
+
_ => number.to_string(),
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
fn roman(number: i64, upper: bool) -> String {
|
|
126
|
+
if number < 1 {
|
|
127
|
+
return number.to_string();
|
|
128
|
+
}
|
|
129
|
+
let mut remaining = number;
|
|
130
|
+
let mut out = String::new();
|
|
131
|
+
for &(value, symbol) in ROMAN {
|
|
132
|
+
while remaining >= value {
|
|
133
|
+
out.push_str(symbol);
|
|
134
|
+
remaining -= value;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if upper { out } else { out.to_ascii_lowercase() }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fn alpha(number: i64, upper: bool) -> String {
|
|
141
|
+
if number < 1 {
|
|
142
|
+
return String::new();
|
|
143
|
+
}
|
|
144
|
+
let zero_based = number - 1;
|
|
145
|
+
let letter = u8::try_from(zero_based % 26).unwrap_or(0);
|
|
146
|
+
let repeat = usize::try_from(zero_based / 26 + 1).unwrap_or(1);
|
|
147
|
+
let letter = if upper { b'A' + letter } else { b'a' + letter };
|
|
148
|
+
std::iter::repeat_n(char::from(letter), repeat).collect()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#[cfg(test)]
|
|
152
|
+
mod tests;
|
|
@@ -1,105 +1,190 @@
|
|
|
1
|
-
//!
|
|
1
|
+
//! Versioned replies and numeric-only arguments for the trusted PDF worker.
|
|
2
2
|
|
|
3
3
|
use std::ffi::OsString;
|
|
4
4
|
use std::io::Write;
|
|
5
|
+
use std::time::Duration;
|
|
5
6
|
|
|
6
7
|
use super::ProcessLimits;
|
|
7
8
|
use crate::read::Limits;
|
|
8
|
-
use crate::read::pdf::RenderOptions;
|
|
9
|
+
use crate::read::pdf::{ReadOptions, RenderOptions};
|
|
9
10
|
use crate::{Error, Result};
|
|
10
11
|
|
|
11
|
-
pub(super) const HEADER: &[u8; 6] = b"IMPR\
|
|
12
|
+
pub(super) const HEADER: &[u8; 6] = b"IMPR\x02\0";
|
|
12
13
|
pub(super) const MAX_ERROR_BYTES: usize = 4096;
|
|
13
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
|
+
|
|
14
42
|
pub(super) struct Request {
|
|
15
|
-
pub
|
|
16
|
-
pub
|
|
43
|
+
pub restricted: bool,
|
|
44
|
+
pub operation: Operation,
|
|
17
45
|
pub limits: Limits,
|
|
18
46
|
pub memory: u64,
|
|
19
47
|
pub cpu_seconds: u64,
|
|
20
48
|
}
|
|
21
49
|
|
|
22
50
|
impl Request {
|
|
23
|
-
pub fn new(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
.ok_or(Error::InvalidPdfOption("output limit is too large"))?;
|
|
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
|
+
}
|
|
33
60
|
if process.memory_bytes == 0 || process.memory_bytes >= i64::MAX as u64 {
|
|
34
61
|
return Err(Error::InvalidPdfOption("memory limit must be positive and finite"));
|
|
35
62
|
}
|
|
36
|
-
let
|
|
37
|
-
|
|
63
|
+
let wall = limits.timeout.unwrap_or(process.wall_time).min(process.wall_time);
|
|
64
|
+
let cpu_seconds = wall
|
|
38
65
|
.as_secs()
|
|
39
|
-
.checked_add(u64::from(
|
|
66
|
+
.checked_add(u64::from(wall.subsec_nanos() != 0))
|
|
40
67
|
.filter(|&seconds| seconds != 0 && seconds < i64::MAX as u64)
|
|
41
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"))?;
|
|
42
71
|
Ok(Self {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
limits: limits.clone(),
|
|
72
|
+
restricted: false,
|
|
73
|
+
operation,
|
|
74
|
+
limits: limits.clone().timeout(Some(wall)),
|
|
46
75
|
memory: process.memory_bytes,
|
|
47
76
|
cpu_seconds,
|
|
48
77
|
})
|
|
49
78
|
}
|
|
50
79
|
|
|
51
80
|
pub fn arguments(&self) -> Vec<OsString> {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
u64::from(self.
|
|
55
|
-
self.options.max_pixels,
|
|
56
|
-
self.options.max_output_bytes as u64,
|
|
57
|
-
u64::from(self.options.grayscale),
|
|
81
|
+
let timeout = self.limits.timeout.unwrap_or_default();
|
|
82
|
+
let mut values = vec![
|
|
83
|
+
u64::from(self.restricted),
|
|
58
84
|
self.limits.max_input as u64,
|
|
59
85
|
self.limits.max_inflated,
|
|
60
86
|
self.limits.max_depth as u64,
|
|
61
87
|
self.memory,
|
|
62
88
|
self.cpu_seconds,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
.
|
|
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()
|
|
66
111
|
}
|
|
67
112
|
|
|
68
113
|
pub fn parse() -> Result<Self> {
|
|
69
|
-
let invalid = || Error::InvalidPdfOption("invalid
|
|
114
|
+
let invalid = || Error::InvalidPdfOption("invalid PDF worker arguments");
|
|
70
115
|
let values: Vec<u64> = std::env::args()
|
|
71
116
|
.skip(1)
|
|
72
117
|
.map(|value| value.parse().map_err(|_| invalid()))
|
|
73
118
|
.collect::<Result<_>>()?;
|
|
74
|
-
let [
|
|
119
|
+
let [restricted, input, inflated, depth, memory, cpu, timeout, rest @ ..] =
|
|
75
120
|
values.as_slice()
|
|
76
121
|
else {
|
|
77
122
|
return Err(invalid());
|
|
78
123
|
};
|
|
79
|
-
if *
|
|
80
|
-
|| *memory == 0
|
|
81
|
-
|| *memory >= i64::MAX as u64
|
|
82
|
-
|| *cpu == 0
|
|
83
|
-
|| *cpu >= i64::MAX as u64
|
|
84
|
-
{
|
|
124
|
+
if *restricted > 1 {
|
|
85
125
|
return Err(invalid());
|
|
86
126
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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),
|
|
103
188
|
}
|
|
104
189
|
}
|
|
105
190
|
|
|
@@ -111,20 +196,10 @@ pub(super) fn write_reply(mut out: impl Write, result: Result<Vec<u8>>) -> Resul
|
|
|
111
196
|
out.write_all(&data)?;
|
|
112
197
|
}
|
|
113
198
|
Err(error) => {
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
}
|
|
199
|
+
let (kind, message) = failure(&error);
|
|
200
|
+
header[5] = kind;
|
|
126
201
|
out.write_all(&header)?;
|
|
127
|
-
out.write_all(
|
|
202
|
+
out.write_all(message.as_bytes())?;
|
|
128
203
|
}
|
|
129
204
|
}
|
|
130
205
|
out.flush()?;
|
|
@@ -133,23 +208,17 @@ pub(super) fn write_reply(mut out: impl Write, result: Result<Vec<u8>>) -> Resul
|
|
|
133
208
|
|
|
134
209
|
pub(super) fn decode(mut data: Vec<u8>, maximum: usize) -> Result<Vec<u8>> {
|
|
135
210
|
if data.len() < HEADER.len() || data[..5] != HEADER[..5] {
|
|
136
|
-
return Err(Error::malformed("
|
|
211
|
+
return Err(Error::malformed("PDF worker", "missing or incompatible response"));
|
|
137
212
|
}
|
|
138
213
|
let status = data[5];
|
|
139
214
|
data.drain(..HEADER.len());
|
|
140
215
|
if status == 0 {
|
|
141
216
|
return if data.len() > maximum {
|
|
142
|
-
Err(Error::LimitExceeded("
|
|
217
|
+
Err(Error::LimitExceeded("PDF worker output"))
|
|
143
218
|
} else {
|
|
144
219
|
Ok(data)
|
|
145
220
|
};
|
|
146
221
|
}
|
|
147
222
|
let message = String::from_utf8_lossy(&data[..data.len().min(MAX_ERROR_BYTES)]).into_owned();
|
|
148
|
-
Err(
|
|
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
|
-
})
|
|
223
|
+
Err(error(status, message))
|
|
155
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
|
+
}
|
|
@@ -28,6 +28,33 @@ pub(super) fn apply_limits(memory: u64, cpu: u64, output: u64) -> Result<()> {
|
|
|
28
28
|
Ok(())
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/// Replace the bootstrap after setting address-space limits, so the loader
|
|
32
|
+
/// and parser cannot reuse memory mapped before the cap was installed.
|
|
33
|
+
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
|
34
|
+
pub(super) fn reexec(arguments: &[std::ffi::OsString]) -> Result<()> {
|
|
35
|
+
use std::ffi::CString;
|
|
36
|
+
use std::os::unix::ffi::OsStrExt;
|
|
37
|
+
|
|
38
|
+
let string = |value: &[u8]| {
|
|
39
|
+
CString::new(value)
|
|
40
|
+
.map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidInput, error))
|
|
41
|
+
};
|
|
42
|
+
let executable = string(std::env::current_exe()?.as_os_str().as_bytes())?;
|
|
43
|
+
let mut args = vec![executable.clone(), string(b"1")?];
|
|
44
|
+
for argument in arguments.iter().skip(1) {
|
|
45
|
+
args.push(string(argument.as_os_str().as_bytes())?);
|
|
46
|
+
}
|
|
47
|
+
match nix::unistd::execv(&executable, &args) {
|
|
48
|
+
Ok(never) => match never {},
|
|
49
|
+
// A tiny address-space cap can prevent exec from allocating its
|
|
50
|
+
// argument/environment pages and surface as E2BIG rather than ENOMEM.
|
|
51
|
+
Err(Errno::ENOMEM | Errno::E2BIG) => {
|
|
52
|
+
Err(Error::LimitExceeded("PDF worker startup resources"))
|
|
53
|
+
}
|
|
54
|
+
Err(error) => Err(std::io::Error::from(error).into()),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
31
58
|
fn lower(resource: Resource, value: u64) -> Result<()> {
|
|
32
59
|
let (soft, hard) = getrlimit(resource).map_err(std::io::Error::from)?;
|
|
33
60
|
let value = bounded_limit(value, soft, hard)?;
|