bare-x-to-pdf 0.3.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CMakeLists.txt CHANGED
@@ -9,7 +9,11 @@ include(cmake/rust.cmake)
9
9
 
10
10
  bare_target(target)
11
11
 
12
- add_bare_module(simple_converter NAME bare-x-to-pdf-${target})
12
+ if(target STREQUAL "wasi-wasm32")
13
+ add_bare_module(simple_converter)
14
+ else()
15
+ add_bare_module(simple_converter NAME bare-x-to-pdf-${target})
16
+ endif()
13
17
 
14
18
  target_sources(
15
19
  ${simple_converter}
@@ -29,10 +33,26 @@ target_link_libraries(
29
33
  simple_converter_core
30
34
  )
31
35
 
36
+ # Bound the linear memory a hostile document can claim, so it traps instead of
37
+ # growing to the 4 GiB limit of wasm32, and give the recursive readers and
38
+ # Typst the 8 MiB stack they get on native threads rather than the 64 KiB
39
+ # default of wasm-ld. The default cap leaves room for the 256 MB images and
40
+ # 128 MB parts that core/src/limits.rs lets through.
41
+ set(SIMPLE_CONVERTER_WASM_MAX_MEMORY 1073741824 CACHE STRING "Linear memory limit of the WebAssembly addon in bytes, a multiple of 65536")
42
+
43
+ if(target MATCHES "^wasi-")
44
+ target_link_options(
45
+ ${simple_converter}_module
46
+ PRIVATE
47
+ -Wl,--max-memory=${SIMPLE_CONVERTER_WASM_MAX_MEMORY}
48
+ -Wl,-z,stack-size=8388608
49
+ )
50
+ endif()
51
+
32
52
  if(CMAKE_STRIP AND NOT WIN32)
33
53
  add_custom_command(
34
54
  TARGET ${simple_converter}_module
35
55
  POST_BUILD
36
- COMMAND ${CMAKE_STRIP} $<IF:$<PLATFORM_ID:Darwin,iOS>,-x,--strip-unneeded> $<TARGET_FILE:${simple_converter}_module>
56
+ COMMAND ${CMAKE_STRIP} $<IF:$<PLATFORM_ID:Darwin,iOS>,-x,$<IF:$<PLATFORM_ID:WASI>,--strip-all,--strip-unneeded>> $<TARGET_FILE:${simple_converter}_module>
37
57
  )
38
58
  endif()
package/README.md CHANGED
@@ -42,22 +42,58 @@ npm install --os=android --cpu=arm64
42
42
  npm install --os=ios --cpu=arm64
43
43
  ```
44
44
 
45
+ ### WebAssembly
46
+
47
+ On Bare 1.36 and later the converter can also run as a WebAssembly addon, sandboxed through
48
+ WASI. The `.wasm` prebuild ships in the main package, so there is nothing extra to install.
49
+ Import with the `wasi-wasm32` host and a WASI provider. The attributes must be written
50
+ inline in the `require` call. The addon needs a random source and read access to the fonts:
51
+
52
+ ```js
53
+ const converter = require('bare-x-to-pdf', {
54
+ with: { host: 'wasi-wasm32', wasi: './wasi.js' }
55
+ })
56
+ ```
57
+
58
+ ```js
59
+ // wasi.js
60
+ const crypto = require('bare-crypto')
61
+ const { DirectorySync } = require('bare-directory')
62
+ const { HostDirectory } = require('bare-wasi/host')
63
+
64
+ const fonts = require.resolve('bare-x-to-pdf/package').replace(/package\.json$/, 'fonts')
65
+
66
+ module.exports = (url) => ({
67
+ version: 'preview1',
68
+ random: (b) => crypto.randomFillSync(b),
69
+ preopens: { [fonts]: new HostDirectory(DirectorySync.open(fonts)) }
70
+ })
71
+ ```
72
+
73
+ The WebAssembly addon has only `convertSync`. `convert()` falls back to it, so it blocks the
74
+ calling thread; run conversions in a [`bare-thread`](https://github.com/holepunchto/bare-thread).
75
+ A hostile document can crash the addon with a `RuntimeError` without taking down the process,
76
+ but the addon stays unusable in that thread afterwards, so replace the thread.
77
+
45
78
  ## Usage
46
79
 
47
80
  ```js
48
81
  const converter = require('bare-x-to-pdf')
49
82
 
50
- const pdf = converter.convert(docxBytes, { to: 'pdf' })
83
+ const pdf = await converter.convert(docxBytes, { to: 'pdf' })
51
84
  ```
52
85
 
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.
86
+ Input is a `Uint8Array` or `ArrayBuffer` and the result is a `Buffer`. `convert()` runs on
87
+ the libuv thread pool and does not block the JavaScript thread; `convertSync()` runs on the
88
+ calling thread.
55
89
 
56
90
  ## API
57
91
 
58
- ### `convert(input, options)`
92
+ ### `const pdf = await convert(input, options)`
59
93
 
60
- Convert `input` and return a `Buffer`.
94
+ Convert `input` off the JavaScript thread and resolve with a `Buffer`. The input is copied,
95
+ so it may be changed or transferred once the call returns. Invalid options and conversion
96
+ errors reject the promise.
61
97
 
62
98
  - `options.to` — target format, `'pdf'`. Required.
63
99
  - `options.from` — source format; sniffed from the bytes when omitted. Markdown has no
@@ -65,6 +101,11 @@ Convert `input` and return a `Buffer`.
65
101
  URIs, since the input is bytes with no directory to resolve relative paths against.
66
102
  - `options.fontsDir` — a directory of fonts to use instead of the bundled set.
67
103
 
104
+ ### `const pdf = convertSync(input, options)`
105
+
106
+ Convert `input` on the calling thread and return a `Buffer`. Takes the same options as
107
+ `convert()`.
108
+
68
109
  ### `detect(input)`
69
110
 
70
111
  Sniff the format from the bytes and return `'docx'`, `'pptx'`, `'xlsx'`, `'doc'`, `'xls'`,
@@ -82,7 +123,34 @@ Return every supported pair as `{ from, to }`.
82
123
 
83
124
  ### `FONTS_DIR`
84
125
 
85
- Path to the bundled fonts directory.
126
+ Path to the bundled fonts directory, resolved with `require.asset`.
127
+
128
+ ## Limits
129
+
130
+ Documents are often untrusted, so the engine caps what a small file can make it allocate or
131
+ lay out. A document past a hard limit is rejected with an error. One past a soft limit still
132
+ converts, with the value clamped.
133
+
134
+ Rejected:
135
+
136
+ - An archive part larger than 128 MB uncompressed.
137
+ - XML nested more than 512 elements deep.
138
+ - A spreadsheet that would print more than 30,000 cells.
139
+
140
+ Clamped:
141
+
142
+ - Table spans: 1–1,000 columns.
143
+ - Text columns: 1–64.
144
+ - Pages: 72–14,400 pt on each side, with margins scaled to fit.
145
+ - Lengths: NaN and infinity become 0; any other length is held within ±1,000,000 pt.
146
+ - List counters: above 32,767, letter and Roman labels fall back to decimal.
147
+ - Charts: up to 100,000 points per series (in .xls, 32,000 per series and 256,000 per chart).
148
+ - Formulas: nesting depth up to 256.
149
+ - Images: more than 50 megapixels, or ones that cannot be decoded, are drawn as blank
150
+ space.
151
+ - Tables nested more than 12 deep: the inner tables are flattened into paragraphs.
152
+
153
+ Spreadsheet pages with no content are not printed, as in LibreOffice Calc.
86
154
 
87
155
  ## Development
88
156
 
package/binding.c CHANGED
@@ -1,5 +1,5 @@
1
1
  #include <assert.h>
2
- #include <bare.h>
2
+ #include <bare/module.h>
3
3
  #include <js.h>
4
4
  #include <stdbool.h>
5
5
  #include <stdint.h>
@@ -7,12 +7,46 @@
7
7
  #include <string.h>
8
8
  #include <utf.h>
9
9
 
10
+ #ifndef __wasm__
11
+ #include <uv.h>
12
+ #endif
13
+
10
14
  #include "simple_converter.h"
11
15
 
16
+ typedef struct {
17
+ const uint8_t *input;
18
+ size_t input_len;
19
+ uint8_t *owned_input;
20
+ char *from;
21
+ char *to;
22
+ char *fonts_dir;
23
+ sc_buffer_t out;
24
+ sc_buffer_t error;
25
+ int status;
26
+ } simple_converter_job_t;
27
+
28
+ // WebAssembly addons are single threaded and have no event loop, so they
29
+ // export only convertSync() and leave running it off the JavaScript thread to
30
+ // the embedder.
31
+ #ifndef __wasm__
32
+ typedef struct {
33
+ uv_work_t work;
34
+ js_env_t *env;
35
+ js_deferred_t *deferred;
36
+ js_deferred_teardown_t *teardown;
37
+ bool exiting;
38
+ simple_converter_job_t job;
39
+ } simple_converter_req_t;
40
+ #endif
41
+
12
42
  static bool
13
43
  simple_converter__string(js_env_t *env, js_value_t *value, char **result) {
14
44
  int err;
15
45
 
46
+ bool is_string;
47
+ err = js_is_string(env, value, &is_string);
48
+ if (err != 0 || !is_string) return false;
49
+
16
50
  size_t len = 0;
17
51
  err = js_get_value_string_utf8(env, value, NULL, 0, &len);
18
52
  if (err != 0) return false;
@@ -27,14 +61,32 @@ simple_converter__string(js_env_t *env, js_value_t *value, char **result) {
27
61
  }
28
62
 
29
63
  str[len] = '\0';
64
+
65
+ if (strlen(str) != len) {
66
+ free(str);
67
+ return false;
68
+ }
69
+
30
70
  *result = str;
31
71
  return true;
32
72
  }
33
73
 
34
- static js_value_t *
35
- simple_converter_convert(js_env_t *env, js_callback_info_t *info) {
74
+ static void
75
+ simple_converter__job_release(simple_converter_job_t *job) {
76
+ free(job->owned_input);
77
+ free(job->from);
78
+ free(job->to);
79
+ free(job->fonts_dir);
80
+ sc_free(&job->out);
81
+ sc_free(&job->error);
82
+ }
83
+
84
+ static bool
85
+ simple_converter__job_init(js_env_t *env, js_callback_info_t *info, bool copy_input, simple_converter_job_t *job) {
36
86
  int err;
37
87
 
88
+ memset(job, 0, sizeof(*job));
89
+
38
90
  size_t argc = 4;
39
91
  js_value_t *argv[4];
40
92
 
@@ -44,68 +96,221 @@ simple_converter_convert(js_env_t *env, js_callback_info_t *info) {
44
96
  if (argc < 4) {
45
97
  err = js_throw_error(env, NULL, "convert expects (bytes, from, to, fontsDir)");
46
98
  assert(err == 0);
47
- return NULL;
99
+ return false;
48
100
  }
49
101
 
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) {
102
+ bool is_typedarray;
103
+ err = js_is_typedarray(env, argv[0], &is_typedarray);
104
+ assert(err == 0);
105
+
106
+ js_typedarray_type_t type = js_int8array;
107
+ uint8_t *input = NULL;
108
+ size_t input_len = 0;
109
+ if (is_typedarray) {
110
+ err = js_get_typedarray_info(env, argv[0], &type, (void **) &input, &input_len, NULL, NULL);
111
+ }
112
+ if (!is_typedarray || err != 0 || type != js_uint8array) {
54
113
  err = js_throw_error(env, NULL, "input must be a Uint8Array");
55
114
  assert(err == 0);
56
- return NULL;
115
+ return false;
57
116
  }
58
117
 
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);
118
+ bool ok = simple_converter__string(env, argv[1], &job->from) &&
119
+ simple_converter__string(env, argv[2], &job->to) &&
120
+ simple_converter__string(env, argv[3], &job->fonts_dir);
66
121
 
67
122
  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");
123
+ simple_converter__job_release(job);
124
+ err = js_throw_error(env, NULL, "from, to and fontsDir must be strings without NUL characters");
72
125
  assert(err == 0);
73
- return NULL;
126
+ return false;
74
127
  }
75
128
 
76
- sc_buffer_t out = {NULL, 0};
77
- sc_buffer_t error = {NULL, 0};
129
+ job->input = input;
130
+ job->input_len = input_len;
78
131
 
79
- int status = sc_convert(input, input_len, from, to, fonts_dir, &out, &error);
132
+ if (copy_input && input_len > 0) {
133
+ job->owned_input = malloc(input_len);
134
+ if (job->owned_input == NULL) {
135
+ simple_converter__job_release(job);
136
+ err = js_throw_error(env, NULL, "out of memory");
137
+ assert(err == 0);
138
+ return false;
139
+ }
140
+ memcpy(job->owned_input, input, input_len);
141
+ job->input = job->owned_input;
142
+ }
143
+
144
+ return true;
145
+ }
146
+
147
+ static void
148
+ simple_converter__run(simple_converter_job_t *job) {
149
+ job->status = sc_convert(job->input, job->input_len, job->from, job->to, job->fonts_dir, &job->out, &job->error);
150
+ }
151
+
152
+ static void
153
+ simple_converter__finalize_output(js_env_t *env, void *data, void *finalize_hint) {
154
+ sc_buffer_t *buffer = finalize_hint;
155
+ sc_free(buffer);
156
+ free(buffer);
157
+ }
80
158
 
81
- free(from);
82
- free(to);
83
- free(fonts_dir);
159
+ static js_value_t *
160
+ simple_converter__output(js_env_t *env, simple_converter_job_t *job) {
161
+ int err;
84
162
 
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';
163
+ js_value_t *result;
164
+
165
+ sc_buffer_t *owned = job->out.len > 0 ? malloc(sizeof(sc_buffer_t)) : NULL;
166
+
167
+ if (owned != NULL) {
168
+ *owned = job->out;
169
+ err = js_create_external_arraybuffer(env, owned->data, owned->len, simple_converter__finalize_output, owned, &result);
170
+ if (err == 0) {
171
+ job->out.data = NULL;
172
+ job->out.len = 0;
173
+ return result;
90
174
  }
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;
175
+ free(owned);
96
176
  }
97
177
 
98
- js_value_t *result;
99
178
  void *data;
100
- err = js_create_arraybuffer(env, out.len, &data, &result);
179
+ err = js_create_arraybuffer(env, job->out.len, &data, &result);
101
180
  assert(err == 0);
102
181
 
103
- if (out.len > 0) memcpy(data, out.data, out.len);
104
- sc_free(&out);
182
+ if (job->out.len > 0) memcpy(data, job->out.data, job->out.len);
105
183
 
106
184
  return result;
107
185
  }
108
186
 
187
+ static js_value_t *
188
+ simple_converter__error(js_env_t *env, simple_converter_job_t *job) {
189
+ int err;
190
+
191
+ js_value_t *message;
192
+ if (job->error.len > 0) {
193
+ err = js_create_string_utf8(env, (utf8_t *) job->error.data, job->error.len, &message);
194
+ } else {
195
+ err = js_create_string_utf8(env, (utf8_t *) "conversion failed", (size_t) -1, &message);
196
+ }
197
+ assert(err == 0);
198
+
199
+ js_value_t *error;
200
+ err = js_create_error(env, NULL, message, &error);
201
+ assert(err == 0);
202
+
203
+ return error;
204
+ }
205
+
206
+ static js_value_t *
207
+ simple_converter_convert_sync(js_env_t *env, js_callback_info_t *info) {
208
+ int err;
209
+
210
+ simple_converter_job_t job;
211
+ if (!simple_converter__job_init(env, info, false, &job)) return NULL;
212
+
213
+ simple_converter__run(&job);
214
+
215
+ js_value_t *result = NULL;
216
+
217
+ if (job.status == 0) {
218
+ result = simple_converter__output(env, &job);
219
+ } else {
220
+ err = js_throw(env, simple_converter__error(env, &job));
221
+ assert(err == 0);
222
+ }
223
+
224
+ simple_converter__job_release(&job);
225
+
226
+ return result;
227
+ }
228
+
229
+ #ifndef __wasm__
230
+ static void
231
+ simple_converter__on_work(uv_work_t *work) {
232
+ simple_converter_req_t *req = (simple_converter_req_t *) work;
233
+
234
+ simple_converter__run(&req->job);
235
+ }
236
+
237
+ static void
238
+ simple_converter__on_after_work(uv_work_t *work, int status) {
239
+ int err;
240
+
241
+ simple_converter_req_t *req = (simple_converter_req_t *) work;
242
+
243
+ js_env_t *env = req->env;
244
+
245
+ if (!req->exiting) {
246
+ js_handle_scope_t *scope;
247
+ err = js_open_handle_scope(env, &scope);
248
+ assert(err == 0);
249
+
250
+ if (status == 0 && req->job.status == 0) {
251
+ err = js_resolve_deferred(env, req->deferred, simple_converter__output(env, &req->job));
252
+ } else {
253
+ err = js_reject_deferred(env, req->deferred, simple_converter__error(env, &req->job));
254
+ }
255
+ assert(err == 0);
256
+
257
+ err = js_close_handle_scope(env, scope);
258
+ assert(err == 0);
259
+ }
260
+
261
+ err = js_finish_deferred_teardown_callback(req->teardown);
262
+ assert(err == 0);
263
+
264
+ simple_converter__job_release(&req->job);
265
+ free(req);
266
+ }
267
+
268
+ static void
269
+ simple_converter__on_teardown(js_deferred_teardown_t *teardown, void *data) {
270
+ simple_converter_req_t *req = data;
271
+
272
+ req->exiting = true;
273
+
274
+ uv_cancel((uv_req_t *) &req->work);
275
+ }
276
+
277
+ static js_value_t *
278
+ simple_converter_convert(js_env_t *env, js_callback_info_t *info) {
279
+ int err;
280
+
281
+ simple_converter_req_t *req = malloc(sizeof(simple_converter_req_t));
282
+ if (req == NULL) {
283
+ err = js_throw_error(env, NULL, "out of memory");
284
+ assert(err == 0);
285
+ return NULL;
286
+ }
287
+
288
+ if (!simple_converter__job_init(env, info, true, &req->job)) {
289
+ free(req);
290
+ return NULL;
291
+ }
292
+
293
+ req->env = env;
294
+ req->exiting = false;
295
+
296
+ js_value_t *promise;
297
+ err = js_create_promise(env, &req->deferred, &promise);
298
+ assert(err == 0);
299
+
300
+ uv_loop_t *loop;
301
+ err = js_get_env_loop(env, &loop);
302
+ assert(err == 0);
303
+
304
+ err = uv_queue_work(loop, &req->work, simple_converter__on_work, simple_converter__on_after_work);
305
+ assert(err == 0);
306
+
307
+ err = js_add_deferred_teardown_callback(env, simple_converter__on_teardown, req, &req->teardown);
308
+ assert(err == 0);
309
+
310
+ return promise;
311
+ }
312
+ #endif
313
+
109
314
  static js_value_t *
110
315
  simple_converter_exports(js_env_t *env, js_value_t *exports) {
111
316
  int err;
@@ -119,7 +324,10 @@ simple_converter_exports(js_env_t *env, js_value_t *exports) {
119
324
  assert(err == 0); \
120
325
  }
121
326
 
327
+ #ifndef __wasm__
122
328
  V("convert", simple_converter_convert)
329
+ #endif
330
+ V("convertSync", simple_converter_convert_sync)
123
331
  #undef V
124
332
 
125
333
  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)
@@ -43,16 +43,26 @@ elseif(_sys STREQUAL "win32")
43
43
  else()
44
44
  set(_triple x86_64-pc-windows-msvc)
45
45
  endif()
46
+ elseif(_sys STREQUAL "wasi")
47
+ set(_triple wasm32-wasip1)
46
48
  else()
47
49
  message(FATAL_ERROR "simple-converter: no Rust target for '${_sys}/${_arch}'")
48
50
  endif()
49
51
 
50
52
  set(Rust_CARGO_TARGET "${_triple}" CACHE STRING "Rust target triple")
51
53
 
52
- fetch_package("github:corrosion-rs/corrosion#${CORROSION_VERSION}")
54
+ fetch_package("github:corrosion-rs/corrosion#${CORROSION_COMMIT}")
53
55
 
54
56
  corrosion_import_crate(
55
57
  MANIFEST_PATH "${CMAKE_CURRENT_SOURCE_DIR}/core/Cargo.toml"
56
58
  CRATE_TYPES staticlib
57
59
  PROFILE release
58
60
  )
61
+
62
+ # Corrosion 0.6.1 adds the Command Line Tools SDK as a link directory on macOS.
63
+ # The linker searches it before CMAKE_OSX_SYSROOT, so -lSystem resolves against
64
+ # the wrong SDK and fails when the two differ (corrosion-rs/corrosion#686, fixed
65
+ # upstream after 0.6.1). Drop this once the pin moves past that fix.
66
+ if(APPLE)
67
+ set_property(TARGET simple_converter_core-static PROPERTY INTERFACE_LINK_DIRECTORIES "")
68
+ endif()