bare-x-to-pdf 0.3.0 → 0.4.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.
package/README.md CHANGED
@@ -47,17 +47,20 @@ npm install --os=ios --cpu=arm64
47
47
  ```js
48
48
  const converter = require('bare-x-to-pdf')
49
49
 
50
- const pdf = converter.convert(docxBytes, { to: 'pdf' })
50
+ const pdf = await converter.convert(docxBytes, { to: 'pdf' })
51
51
  ```
52
52
 
53
- Input is a `Uint8Array` or `ArrayBuffer` and the result is a `Buffer`. Calls are
54
- synchronous and CPU bound; run them in a `Bare.Thread` when latency matters.
53
+ Input is a `Uint8Array` or `ArrayBuffer` and the result is a `Buffer`. `convert()` runs on
54
+ the libuv thread pool and does not block the JavaScript thread; `convertSync()` runs on the
55
+ calling thread.
55
56
 
56
57
  ## API
57
58
 
58
- ### `convert(input, options)`
59
+ ### `const pdf = await convert(input, options)`
59
60
 
60
- Convert `input` and return a `Buffer`.
61
+ Convert `input` off the JavaScript thread and resolve with a `Buffer`. The input is copied,
62
+ so it may be changed or transferred once the call returns. Invalid options and conversion
63
+ errors reject the promise.
61
64
 
62
65
  - `options.to` — target format, `'pdf'`. Required.
63
66
  - `options.from` — source format; sniffed from the bytes when omitted. Markdown has no
@@ -65,6 +68,11 @@ Convert `input` and return a `Buffer`.
65
68
  URIs, since the input is bytes with no directory to resolve relative paths against.
66
69
  - `options.fontsDir` — a directory of fonts to use instead of the bundled set.
67
70
 
71
+ ### `const pdf = convertSync(input, options)`
72
+
73
+ Convert `input` on the calling thread and return a `Buffer`. Takes the same options as
74
+ `convert()`.
75
+
68
76
  ### `detect(input)`
69
77
 
70
78
  Sniff the format from the bytes and return `'docx'`, `'pptx'`, `'xlsx'`, `'doc'`, `'xls'`,
@@ -84,6 +92,33 @@ Return every supported pair as `{ from, to }`.
84
92
 
85
93
  Path to the bundled fonts directory.
86
94
 
95
+ ## Limits
96
+
97
+ Documents are often untrusted, so the engine caps what a small file can make it allocate or
98
+ lay out. A document past a hard limit is rejected with an error. One past a soft limit still
99
+ converts, with the value clamped.
100
+
101
+ Rejected:
102
+
103
+ - An archive part larger than 128 MB uncompressed.
104
+ - XML nested more than 512 elements deep.
105
+ - A spreadsheet that would print more than 30,000 cells.
106
+
107
+ Clamped:
108
+
109
+ - Table spans: 1–1,000 columns.
110
+ - Text columns: 1–64.
111
+ - Pages: 72–14,400 pt on each side, with margins scaled to fit.
112
+ - Lengths: NaN and infinity become 0; any other length is held within ±1,000,000 pt.
113
+ - List counters: above 32,767, letter and Roman labels fall back to decimal.
114
+ - Charts: up to 100,000 points per series (in .xls, 32,000 per series and 256,000 per chart).
115
+ - Formulas: nesting depth up to 256.
116
+ - Images: more than 50 megapixels, or ones that cannot be decoded, are drawn as blank
117
+ space.
118
+ - Tables nested more than 12 deep: the inner tables are flattened into paragraphs.
119
+
120
+ Spreadsheet pages with no content are not printed, as in LibreOffice Calc.
121
+
87
122
  ## Development
88
123
 
89
124
  The platform packages in `npm/` are npm workspaces, and each declares its own `os` and `cpu`
package/binding.c CHANGED
@@ -6,13 +6,39 @@
6
6
  #include <stdlib.h>
7
7
  #include <string.h>
8
8
  #include <utf.h>
9
+ #include <uv.h>
9
10
 
10
11
  #include "simple_converter.h"
11
12
 
13
+ typedef struct {
14
+ const uint8_t *input;
15
+ size_t input_len;
16
+ uint8_t *owned_input;
17
+ char *from;
18
+ char *to;
19
+ char *fonts_dir;
20
+ sc_buffer_t out;
21
+ sc_buffer_t error;
22
+ int status;
23
+ } simple_converter_job_t;
24
+
25
+ typedef struct {
26
+ uv_work_t work;
27
+ js_env_t *env;
28
+ js_deferred_t *deferred;
29
+ js_deferred_teardown_t *teardown;
30
+ bool exiting;
31
+ simple_converter_job_t job;
32
+ } simple_converter_req_t;
33
+
12
34
  static bool
13
35
  simple_converter__string(js_env_t *env, js_value_t *value, char **result) {
14
36
  int err;
15
37
 
38
+ bool is_string;
39
+ err = js_is_string(env, value, &is_string);
40
+ if (err != 0 || !is_string) return false;
41
+
16
42
  size_t len = 0;
17
43
  err = js_get_value_string_utf8(env, value, NULL, 0, &len);
18
44
  if (err != 0) return false;
@@ -27,14 +53,32 @@ simple_converter__string(js_env_t *env, js_value_t *value, char **result) {
27
53
  }
28
54
 
29
55
  str[len] = '\0';
56
+
57
+ if (strlen(str) != len) {
58
+ free(str);
59
+ return false;
60
+ }
61
+
30
62
  *result = str;
31
63
  return true;
32
64
  }
33
65
 
34
- static js_value_t *
35
- simple_converter_convert(js_env_t *env, js_callback_info_t *info) {
66
+ static void
67
+ simple_converter__job_release(simple_converter_job_t *job) {
68
+ free(job->owned_input);
69
+ free(job->from);
70
+ free(job->to);
71
+ free(job->fonts_dir);
72
+ sc_free(&job->out);
73
+ sc_free(&job->error);
74
+ }
75
+
76
+ static bool
77
+ simple_converter__job_init(js_env_t *env, js_callback_info_t *info, bool copy_input, simple_converter_job_t *job) {
36
78
  int err;
37
79
 
80
+ memset(job, 0, sizeof(*job));
81
+
38
82
  size_t argc = 4;
39
83
  js_value_t *argv[4];
40
84
 
@@ -44,68 +88,219 @@ simple_converter_convert(js_env_t *env, js_callback_info_t *info) {
44
88
  if (argc < 4) {
45
89
  err = js_throw_error(env, NULL, "convert expects (bytes, from, to, fontsDir)");
46
90
  assert(err == 0);
47
- return NULL;
91
+ return false;
48
92
  }
49
93
 
50
- uint8_t *input;
51
- size_t input_len;
52
- err = js_get_typedarray_info(env, argv[0], NULL, (void **) &input, &input_len, NULL, NULL);
53
- if (err != 0) {
94
+ bool is_typedarray;
95
+ err = js_is_typedarray(env, argv[0], &is_typedarray);
96
+ assert(err == 0);
97
+
98
+ js_typedarray_type_t type = js_int8array;
99
+ uint8_t *input = NULL;
100
+ size_t input_len = 0;
101
+ if (is_typedarray) {
102
+ err = js_get_typedarray_info(env, argv[0], &type, (void **) &input, &input_len, NULL, NULL);
103
+ }
104
+ if (!is_typedarray || err != 0 || type != js_uint8array) {
54
105
  err = js_throw_error(env, NULL, "input must be a Uint8Array");
55
106
  assert(err == 0);
56
- return NULL;
107
+ return false;
57
108
  }
58
109
 
59
- char *from = NULL;
60
- char *to = NULL;
61
- char *fonts_dir = NULL;
62
-
63
- bool ok = simple_converter__string(env, argv[1], &from) &&
64
- simple_converter__string(env, argv[2], &to) &&
65
- simple_converter__string(env, argv[3], &fonts_dir);
110
+ bool ok = simple_converter__string(env, argv[1], &job->from) &&
111
+ simple_converter__string(env, argv[2], &job->to) &&
112
+ simple_converter__string(env, argv[3], &job->fonts_dir);
66
113
 
67
114
  if (!ok) {
68
- free(from);
69
- free(to);
70
- free(fonts_dir);
71
- err = js_throw_error(env, NULL, "from, to and fontsDir must be strings");
115
+ simple_converter__job_release(job);
116
+ err = js_throw_error(env, NULL, "from, to and fontsDir must be strings without NUL characters");
72
117
  assert(err == 0);
73
- return NULL;
118
+ return false;
119
+ }
120
+
121
+ job->input = input;
122
+ job->input_len = input_len;
123
+
124
+ if (copy_input && input_len > 0) {
125
+ job->owned_input = malloc(input_len);
126
+ if (job->owned_input == NULL) {
127
+ simple_converter__job_release(job);
128
+ err = js_throw_error(env, NULL, "out of memory");
129
+ assert(err == 0);
130
+ return false;
131
+ }
132
+ memcpy(job->owned_input, input, input_len);
133
+ job->input = job->owned_input;
74
134
  }
75
135
 
76
- sc_buffer_t out = {NULL, 0};
77
- sc_buffer_t error = {NULL, 0};
136
+ return true;
137
+ }
138
+
139
+ static void
140
+ simple_converter__run(simple_converter_job_t *job) {
141
+ job->status = sc_convert(job->input, job->input_len, job->from, job->to, job->fonts_dir, &job->out, &job->error);
142
+ }
143
+
144
+ static void
145
+ simple_converter__finalize_output(js_env_t *env, void *data, void *finalize_hint) {
146
+ sc_buffer_t *buffer = finalize_hint;
147
+ sc_free(buffer);
148
+ free(buffer);
149
+ }
78
150
 
79
- int status = sc_convert(input, input_len, from, to, fonts_dir, &out, &error);
151
+ static js_value_t *
152
+ simple_converter__output(js_env_t *env, simple_converter_job_t *job) {
153
+ int err;
80
154
 
81
- free(from);
82
- free(to);
83
- free(fonts_dir);
155
+ js_value_t *result;
156
+
157
+ sc_buffer_t *owned = job->out.len > 0 ? malloc(sizeof(sc_buffer_t)) : NULL;
84
158
 
85
- if (status != 0) {
86
- char *message = malloc(error.len + 1);
87
- if (message != NULL) {
88
- memcpy(message, error.data, error.len);
89
- message[error.len] = '\0';
159
+ if (owned != NULL) {
160
+ *owned = job->out;
161
+ err = js_create_external_arraybuffer(env, owned->data, owned->len, simple_converter__finalize_output, owned, &result);
162
+ if (err == 0) {
163
+ job->out.data = NULL;
164
+ job->out.len = 0;
165
+ return result;
90
166
  }
91
- sc_free(&error);
92
- err = js_throw_error(env, NULL, message ? message : "conversion failed");
93
- assert(err == 0);
94
- free(message);
95
- return NULL;
167
+ free(owned);
96
168
  }
97
169
 
98
- js_value_t *result;
99
170
  void *data;
100
- err = js_create_arraybuffer(env, out.len, &data, &result);
171
+ err = js_create_arraybuffer(env, job->out.len, &data, &result);
172
+ assert(err == 0);
173
+
174
+ if (job->out.len > 0) memcpy(data, job->out.data, job->out.len);
175
+
176
+ return result;
177
+ }
178
+
179
+ static js_value_t *
180
+ simple_converter__error(js_env_t *env, simple_converter_job_t *job) {
181
+ int err;
182
+
183
+ js_value_t *message;
184
+ if (job->error.len > 0) {
185
+ err = js_create_string_utf8(env, (utf8_t *) job->error.data, job->error.len, &message);
186
+ } else {
187
+ err = js_create_string_utf8(env, (utf8_t *) "conversion failed", (size_t) -1, &message);
188
+ }
189
+ assert(err == 0);
190
+
191
+ js_value_t *error;
192
+ err = js_create_error(env, NULL, message, &error);
101
193
  assert(err == 0);
102
194
 
103
- if (out.len > 0) memcpy(data, out.data, out.len);
104
- sc_free(&out);
195
+ return error;
196
+ }
197
+
198
+ static js_value_t *
199
+ simple_converter_convert_sync(js_env_t *env, js_callback_info_t *info) {
200
+ int err;
201
+
202
+ simple_converter_job_t job;
203
+ if (!simple_converter__job_init(env, info, false, &job)) return NULL;
204
+
205
+ simple_converter__run(&job);
206
+
207
+ js_value_t *result = NULL;
208
+
209
+ if (job.status == 0) {
210
+ result = simple_converter__output(env, &job);
211
+ } else {
212
+ err = js_throw(env, simple_converter__error(env, &job));
213
+ assert(err == 0);
214
+ }
215
+
216
+ simple_converter__job_release(&job);
105
217
 
106
218
  return result;
107
219
  }
108
220
 
221
+ static void
222
+ simple_converter__on_work(uv_work_t *work) {
223
+ simple_converter_req_t *req = (simple_converter_req_t *) work;
224
+
225
+ simple_converter__run(&req->job);
226
+ }
227
+
228
+ static void
229
+ simple_converter__on_after_work(uv_work_t *work, int status) {
230
+ int err;
231
+
232
+ simple_converter_req_t *req = (simple_converter_req_t *) work;
233
+
234
+ js_env_t *env = req->env;
235
+
236
+ if (!req->exiting) {
237
+ js_handle_scope_t *scope;
238
+ err = js_open_handle_scope(env, &scope);
239
+ assert(err == 0);
240
+
241
+ if (status == 0 && req->job.status == 0) {
242
+ err = js_resolve_deferred(env, req->deferred, simple_converter__output(env, &req->job));
243
+ } else {
244
+ err = js_reject_deferred(env, req->deferred, simple_converter__error(env, &req->job));
245
+ }
246
+ assert(err == 0);
247
+
248
+ err = js_close_handle_scope(env, scope);
249
+ assert(err == 0);
250
+ }
251
+
252
+ err = js_finish_deferred_teardown_callback(req->teardown);
253
+ assert(err == 0);
254
+
255
+ simple_converter__job_release(&req->job);
256
+ free(req);
257
+ }
258
+
259
+ static void
260
+ simple_converter__on_teardown(js_deferred_teardown_t *teardown, void *data) {
261
+ simple_converter_req_t *req = data;
262
+
263
+ req->exiting = true;
264
+
265
+ uv_cancel((uv_req_t *) &req->work);
266
+ }
267
+
268
+ static js_value_t *
269
+ simple_converter_convert(js_env_t *env, js_callback_info_t *info) {
270
+ int err;
271
+
272
+ simple_converter_req_t *req = malloc(sizeof(simple_converter_req_t));
273
+ if (req == NULL) {
274
+ err = js_throw_error(env, NULL, "out of memory");
275
+ assert(err == 0);
276
+ return NULL;
277
+ }
278
+
279
+ if (!simple_converter__job_init(env, info, true, &req->job)) {
280
+ free(req);
281
+ return NULL;
282
+ }
283
+
284
+ req->env = env;
285
+ req->exiting = false;
286
+
287
+ js_value_t *promise;
288
+ err = js_create_promise(env, &req->deferred, &promise);
289
+ assert(err == 0);
290
+
291
+ uv_loop_t *loop;
292
+ err = js_get_env_loop(env, &loop);
293
+ assert(err == 0);
294
+
295
+ err = uv_queue_work(loop, &req->work, simple_converter__on_work, simple_converter__on_after_work);
296
+ assert(err == 0);
297
+
298
+ err = js_add_deferred_teardown_callback(env, simple_converter__on_teardown, req, &req->teardown);
299
+ assert(err == 0);
300
+
301
+ return promise;
302
+ }
303
+
109
304
  static js_value_t *
110
305
  simple_converter_exports(js_env_t *env, js_value_t *exports) {
111
306
  int err;
@@ -120,6 +315,7 @@ simple_converter_exports(js_env_t *env, js_value_t *exports) {
120
315
  }
121
316
 
122
317
  V("convert", simple_converter_convert)
318
+ V("convertSync", simple_converter_convert_sync)
123
319
  #undef V
124
320
 
125
321
  return exports;
package/cmake/rust.cmake CHANGED
@@ -1,6 +1,6 @@
1
1
  include_guard(GLOBAL)
2
2
 
3
- set(CORROSION_VERSION "v0.6.1" CACHE STRING "Corrosion release tag")
3
+ set(CORROSION_COMMIT "1499b14e4906a2890f5cee1547c8848db261753d" CACHE STRING "Corrosion commit (v0.6.1)")
4
4
 
5
5
  bare_platform(_sys)
6
6
  bare_arch(_arch)
@@ -49,7 +49,7 @@ endif()
49
49
 
50
50
  set(Rust_CARGO_TARGET "${_triple}" CACHE STRING "Rust target triple")
51
51
 
52
- fetch_package("github:corrosion-rs/corrosion#${CORROSION_VERSION}")
52
+ fetch_package("github:corrosion-rs/corrosion#${CORROSION_COMMIT}")
53
53
 
54
54
  corrosion_import_crate(
55
55
  MANIFEST_PATH "${CMAKE_CURRENT_SOURCE_DIR}/core/Cargo.toml"
@@ -34,6 +34,8 @@ pub fn read(bytes: &[u8]) -> Result<Document, Error> {
34
34
  chpx_fkps: file.fkps(file.fib.fc_lcb(12))?,
35
35
  papx_fkps: file.fkps(file.fib.fc_lcb(13))?,
36
36
  counters: std::cell::RefCell::new(HashMap::new()),
37
+ extracted: std::cell::Cell::new(0),
38
+ image_bytes: std::cell::Cell::new(0),
37
39
  };
38
40
 
39
41
  let main_end = file.fib.ccp_text;
@@ -369,7 +371,7 @@ impl File {
369
371
  let (fc_clx, lcb_clx) = self.fib.fc_lcb(33);
370
372
  let clx = self
371
373
  .table
372
- .get(fc_clx as usize..(fc_clx + lcb_clx) as usize)
374
+ .get(fc_clx as usize..fc_clx.saturating_add(lcb_clx) as usize)
373
375
  .ok_or_else(|| Error::new("piece table is out of range"))?;
374
376
  let mut pos = 0;
375
377
  while pos < clx.len() {
@@ -380,7 +382,7 @@ impl File {
380
382
  }
381
383
  2 => {
382
384
  let lcb = u32_at(clx, pos + 1) as usize;
383
- let plc = &clx[pos + 5..(pos + 5 + lcb).min(clx.len())];
385
+ let plc = clx.get(pos + 5..pos.saturating_add(5).saturating_add(lcb).min(clx.len())).unwrap_or(&[]);
384
386
  let n = (plc.len().saturating_sub(4)) / 12;
385
387
  let mut pieces = Vec::with_capacity(n);
386
388
  for i in 0..n {
@@ -402,7 +404,7 @@ impl File {
402
404
 
403
405
  fn fonts(&self) -> Vec<String> {
404
406
  let (fc, lcb) = self.fib.fc_lcb(15);
405
- let Some(sttb) = self.table.get(fc as usize..(fc + lcb) as usize) else { return Vec::new() };
407
+ let Some(sttb) = self.table.get(fc as usize..fc.saturating_add(lcb) as usize) else { return Vec::new() };
406
408
  let mut fonts = Vec::new();
407
409
  let mut pos = 0;
408
410
  if u16_at(sttb, 0) == 0xFFFF {
@@ -441,10 +443,10 @@ impl File {
441
443
  let (fc, lcb) = self.fib.fc_lcb(1);
442
444
  let stsh = self
443
445
  .table
444
- .get(fc as usize..(fc + lcb) as usize)
446
+ .get(fc as usize..fc.saturating_add(lcb) as usize)
445
447
  .ok_or_else(|| Error::new("stylesheet is out of range"))?;
446
448
  let cb_stshi = u16_at(stsh, 0) as usize;
447
- let stshi = &stsh[2..(2 + cb_stshi).min(stsh.len())];
449
+ let stshi = stsh.get(2..(2 + cb_stshi).min(stsh.len())).unwrap_or(&[]);
448
450
  let cstd = u16_at(stshi, 0) as usize;
449
451
  let cb_std_base = u16_at(stshi, 2) as usize;
450
452
  let default_ftc = u16_at(stshi, 12) as usize;
@@ -634,7 +636,7 @@ impl File {
634
636
  }
635
637
 
636
638
  fn parse_fkp(page: &[u8]) -> Fkp {
637
- let crun = page[511] as usize;
639
+ let crun = (page.get(511).copied().unwrap_or(0) as usize).min(101);
638
640
  let mut fcs = Vec::with_capacity(crun + 1);
639
641
  for i in 0..=crun {
640
642
  fcs.push(u32_at(page, i * 4));
@@ -643,15 +645,15 @@ fn parse_fkp(page: &[u8]) -> Fkp {
643
645
  let mut entries = Vec::with_capacity(crun);
644
646
  let mut istds = Vec::with_capacity(crun);
645
647
  for i in 0..crun {
646
- let offset_byte = page[base + i];
648
+ let offset_byte = page.get(base + i).copied().unwrap_or(0);
647
649
  if offset_byte == 0 {
648
650
  entries.push(Vec::new());
649
651
  istds.push(0);
650
652
  continue;
651
653
  }
652
654
  let at = offset_byte as usize * 2;
653
- let cb = page[at] as usize;
654
- let grpprl = &page[at + 1..(at + 1 + cb).min(512)];
655
+ let cb = page.get(at).copied().unwrap_or(0) as usize;
656
+ let grpprl = page.get(at + 1..(at + 1 + cb).min(page.len())).unwrap_or(&[]);
655
657
  entries.push(grpprl.to_vec());
656
658
  istds.push(0);
657
659
  }
@@ -659,7 +661,7 @@ fn parse_fkp(page: &[u8]) -> Fkp {
659
661
  }
660
662
 
661
663
  fn parse_papx_fkp(page: &[u8]) -> Fkp {
662
- let cpara = page[511] as usize;
664
+ let cpara = (page.get(511).copied().unwrap_or(0) as usize).min(29);
663
665
  let mut fcs = Vec::with_capacity(cpara + 1);
664
666
  for i in 0..=cpara {
665
667
  fcs.push(u32_at(page, i * 4));
@@ -668,22 +670,22 @@ fn parse_papx_fkp(page: &[u8]) -> Fkp {
668
670
  let mut entries = Vec::with_capacity(cpara);
669
671
  let mut istds = Vec::with_capacity(cpara);
670
672
  for i in 0..cpara {
671
- let bx = page[base + i * 13];
673
+ let bx = page.get(base + i * 13).copied().unwrap_or(0);
672
674
  if bx == 0 {
673
675
  entries.push(Vec::new());
674
676
  istds.push(0);
675
677
  continue;
676
678
  }
677
679
  let at = bx as usize * 2;
678
- let mut cb = page[at] as usize;
680
+ let mut cb = page.get(at).copied().unwrap_or(0) as usize;
679
681
  let mut start = at + 1;
680
682
  if cb == 0 {
681
- cb = page[at + 1] as usize * 2;
683
+ cb = page.get(at + 1).copied().unwrap_or(0) as usize * 2;
682
684
  start = at + 2;
683
685
  } else {
684
686
  cb = cb * 2 - 1;
685
687
  }
686
- let body = &page[start..(start + cb).min(512)];
688
+ let body = page.get(start..(start + cb).min(page.len())).unwrap_or(&[]);
687
689
  let istd = u16_at(body, 0);
688
690
  entries.push(body.get(2..).map(|b| b.to_vec()).unwrap_or_default());
689
691
  istds.push(istd);
@@ -729,6 +731,8 @@ struct Reader<'a> {
729
731
  chpx_fkps: Vec<(u32, u32, Fkp)>,
730
732
  papx_fkps: Vec<(u32, u32, Fkp)>,
731
733
  counters: std::cell::RefCell<HashMap<u32, Vec<i64>>>,
734
+ extracted: std::cell::Cell<usize>,
735
+ image_bytes: std::cell::Cell<usize>,
732
736
  }
733
737
 
734
738
  #[derive(Debug, Clone)]
@@ -745,7 +749,11 @@ impl Reader<'_> {
745
749
 
746
750
  fn text(&self, start: u32, end: u32) -> Vec<Char> {
747
751
  let mut out = Vec::new();
752
+ let budget = self.file.word.len().saturating_mul(4);
748
753
  for piece in self.pieces {
754
+ if self.extracted.get() + out.len() > budget {
755
+ break;
756
+ }
749
757
  if piece.cp_end <= start || piece.cp_start >= end {
750
758
  continue;
751
759
  }
@@ -780,6 +788,7 @@ impl Reader<'_> {
780
788
  }
781
789
  }
782
790
  }
791
+ self.extracted.set(self.extracted.get() + out.len());
783
792
  out
784
793
  }
785
794
 
@@ -1209,9 +1218,9 @@ impl Reader<'_> {
1209
1218
  let values = counters.entry(lsid).or_insert_with(|| vec![0; 9]);
1210
1219
  let ilvl = ilvl.min(8) as usize;
1211
1220
  if values[ilvl] == 0 {
1212
- values[ilvl] = level.start - 1;
1221
+ values[ilvl] = i64::from(level.start).saturating_sub(1);
1213
1222
  }
1214
- values[ilvl] += 1;
1223
+ values[ilvl] = values[ilvl].saturating_add(1);
1215
1224
  for deeper in ilvl + 1..9 {
1216
1225
  values[deeper] = 0;
1217
1226
  }
@@ -1258,6 +1267,10 @@ impl Reader<'_> {
1258
1267
  }
1259
1268
  let body = data.get(at + cb_header..(at + lcb).min(data.len()))?;
1260
1269
  let content = find_blip(body)
1270
+ .filter(|bytes| {
1271
+ self.image_bytes.set(self.image_bytes.get() + bytes.len());
1272
+ self.image_bytes.get() <= crate::limits::MAX_IMAGE_BYTES
1273
+ })
1261
1274
  .and_then(|bytes| ImageFormat::sniff(bytes).map(|format| ImageData { data: bytes.to_vec(), format }))
1262
1275
  .map(DrawingContent::Image)
1263
1276
  .unwrap_or(DrawingContent::Placeholder(None));
@@ -193,7 +193,7 @@ fn build_section<R: Read + std::io::Seek>(
193
193
  ..Section::default()
194
194
  };
195
195
  if let Some(cols) = sect.child("cols") {
196
- section.columns = cols.attr("num").and_then(|n| n.parse().ok()).unwrap_or(1).max(1);
196
+ section.columns = cols.attr("num").and_then(|n| n.parse().ok()).unwrap_or(1).clamp(1, crate::limits::MAX_TEXT_COLUMNS);
197
197
  if let Some(space) = cols.attr("space").and_then(twips) {
198
198
  section.column_gap = space;
199
199
  }
@@ -267,8 +267,7 @@ fn entry<R: Read + std::io::Seek>(
267
267
  ) -> Result<Option<Vec<u8>>, Error> {
268
268
  match archive.by_name(name) {
269
269
  Ok(mut file) => {
270
- let mut data = Vec::new();
271
- file.read_to_end(&mut data)?;
270
+ let data = crate::limits::read_entry(&mut file)?;
272
271
  Ok(Some(data))
273
272
  }
274
273
  Err(zip::result::ZipError::FileNotFound) => Ok(None),
@@ -810,8 +809,7 @@ impl Reader<'_> {
810
809
  .child("gridSpan")
811
810
  .and_then(|s| s.attr("val"))
812
811
  .and_then(|v| v.parse().ok())
813
- .unwrap_or(1)
814
- .max(1);
812
+ .map_or(1, crate::limits::span);
815
813
  cell.width = pr
816
814
  .child("tcW")
817
815
  .filter(|w| w.attr("type") != Some("pct"))
@@ -147,18 +147,18 @@ impl Counters {
147
147
  let values = self
148
148
  .values
149
149
  .entry(key)
150
- .or_insert_with(|| all.iter().map(|l| l.start - 1).collect());
150
+ .or_insert_with(|| all.iter().map(|l| l.start.saturating_sub(1)).collect());
151
151
 
152
152
  let first_use = self.started.insert((num_id.to_string(), ilvl));
153
153
  if first_use {
154
154
  if let Some(start) = instance.overrides.get(&ilvl).and_then(|o| o.start) {
155
- values[ilvl] = start - 1;
155
+ values[ilvl] = start.saturating_sub(1);
156
156
  }
157
157
  }
158
158
 
159
- values[ilvl] += 1;
159
+ values[ilvl] = values[ilvl].saturating_add(1);
160
160
  for deeper in ilvl + 1..values.len() {
161
- values[deeper] = all[deeper].start - 1;
161
+ values[deeper] = all[deeper].start.saturating_sub(1);
162
162
  }
163
163
 
164
164
  let text = if level.format == "bullet" {
@@ -203,6 +203,10 @@ fn bullet_text(text: &str) -> String {
203
203
  }
204
204
 
205
205
  pub fn format_number(value: i64, format: &str) -> String {
206
+ let format = match format {
207
+ "lowerLetter" | "upperLetter" | "lowerRoman" | "upperRoman" if value > crate::limits::MAX_LIST_VALUE => "decimal",
208
+ other => other,
209
+ };
206
210
  match format {
207
211
  "none" => String::new(),
208
212
  "lowerLetter" => letters(value).to_lowercase(),