quickjs 0.21.0 → 0.22.0.rc1
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/README.md +237 -7
- data/ext/quickjsrb/quickjsrb.c +3113 -556
- data/ext/quickjsrb/quickjsrb.h +401 -6
- data/ext/quickjsrb/quickjsrb_crypto_subtle.c +584 -76
- data/ext/quickjsrb/quickjsrb_file.c +253 -48
- data/lib/quickjs/crypto_key.rb +13 -0
- data/lib/quickjs/importable.rb +80 -0
- data/lib/quickjs/modules.rb +98 -0
- data/lib/quickjs/polyfills.rb +15 -1
- data/lib/quickjs/variables.rb +890 -0
- data/lib/quickjs/version.rb +1 -1
- data/lib/quickjs.rb +3 -0
- data/polyfills/package-lock.json +2 -2
- data/polyfills/package.json +1 -1
- data/sig/quickjs.rbs +20 -3
- metadata +4 -2
- data/ext/quickjsrb/quickjs/repl.c +0 -2057
|
@@ -3,10 +3,25 @@
|
|
|
3
3
|
|
|
4
4
|
static VALUE r_find_alive_rb_file(JSContext *ctx, JSValue j_handle)
|
|
5
5
|
{
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
// Initialised and checked like its siblings. Unreachable today, since the
|
|
7
|
+
// handle is the factory closure's own number and the bridges are never on
|
|
8
|
+
// the global, but it feeds a table lookup and would read a garbage key.
|
|
9
|
+
int64_t handle = 0;
|
|
10
|
+
if (JS_ToInt64(ctx, &handle, j_handle) < 0)
|
|
11
|
+
{
|
|
12
|
+
quickjsrb_drain_pending(ctx);
|
|
13
|
+
return Qnil;
|
|
14
|
+
}
|
|
8
15
|
VMData *data = JS_GetContextOpaque(ctx);
|
|
9
|
-
|
|
16
|
+
VALUE r_file = rb_hash_aref(data->alive_objects, LL2NUM(handle));
|
|
17
|
+
// Only what this subsystem parked, the way both sibling readers check now.
|
|
18
|
+
// The table is shared with bridged exceptions and CryptoKeys, and the bridges
|
|
19
|
+
// below call File methods on whatever they are handed. Unreachable today,
|
|
20
|
+
// since the handle is the factory closure's own number, but the check is what
|
|
21
|
+
// makes that an invariant rather than an argument about reachability.
|
|
22
|
+
if (!rb_obj_is_kind_of(r_file, rb_cFile))
|
|
23
|
+
return Qnil;
|
|
24
|
+
return r_file;
|
|
10
25
|
}
|
|
11
26
|
|
|
12
27
|
static JSValue js_ruby_file_name(JSContext *ctx, JSValueConst _this, int argc, JSValueConst *argv)
|
|
@@ -99,8 +114,41 @@ static JSValue js_ruby_file_array_buffer(JSContext *ctx, JSValueConst _this, int
|
|
|
99
114
|
return promise;
|
|
100
115
|
}
|
|
101
116
|
|
|
117
|
+
// Hands the guest back the throw it made, rather than an error of our own,
|
|
118
|
+
// which would tell it less than it already knew.
|
|
119
|
+
//
|
|
120
|
+
// Deliberately without unparking: the throw is going back into JS, and it is
|
|
121
|
+
// the handle that lets it come out the other side as the host exception it
|
|
122
|
+
// started as. Taking the entry here would leave the JS error carrying a handle
|
|
123
|
+
// that resolves to nothing, and a caller that expected ArgumentError would get
|
|
124
|
+
// a generic Quickjs::RuntimeError instead. A throw the guest then catches and
|
|
125
|
+
// keeps stays parked, which is what any caught bridged error does: #114.
|
|
126
|
+
static JSValue j_rethrow_the_guests_own(JSContext *ctx)
|
|
127
|
+
{
|
|
128
|
+
// JS_Throw clears the uncatchable flag, so a deadline that fired inside the
|
|
129
|
+
// guest's valueOf would come back catchable and a script that caught it and
|
|
130
|
+
// returned would finish past its budget. Thrown the way js_poll_interrupts
|
|
131
|
+
// throws it instead, which is what it was before this touched it.
|
|
132
|
+
if (eval_budget_lapsed_now(ctx))
|
|
133
|
+
{
|
|
134
|
+
// Drained, not freed. This branch is the one that does not hand the throw
|
|
135
|
+
// back, so the reasoning quickjsrb_drain_pending gives for unparking
|
|
136
|
+
// applies here and not to the return below: nothing will carry the guest's
|
|
137
|
+
// throw out now, and a bridge it reached on the way has already parked the
|
|
138
|
+
// host exception, so freeing the JS error is the one path that leaves that
|
|
139
|
+
// entry with nothing able to take it.
|
|
140
|
+
quickjsrb_drain_pending(ctx);
|
|
141
|
+
JS_ThrowInternalError(ctx, "interrupted");
|
|
142
|
+
JS_SetUncatchableException(ctx, TRUE);
|
|
143
|
+
return JS_EXCEPTION;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return JS_Throw(ctx, JS_GetException(ctx));
|
|
147
|
+
}
|
|
148
|
+
|
|
102
149
|
static JSValue js_ruby_file_slice(JSContext *ctx, JSValueConst _this, int argc, JSValueConst *argv)
|
|
103
150
|
{
|
|
151
|
+
VMData *data = JS_GetContextOpaque(ctx);
|
|
104
152
|
VALUE r_file = r_find_alive_rb_file(ctx, argv[0]);
|
|
105
153
|
if (NIL_P(r_file))
|
|
106
154
|
return JS_UNDEFINED;
|
|
@@ -111,8 +159,12 @@ static JSValue js_ruby_file_slice(JSContext *ctx, JSValueConst _this, int argc,
|
|
|
111
159
|
long start = 0;
|
|
112
160
|
if (argc > 1 && !JS_IsUndefined(argv[1]))
|
|
113
161
|
{
|
|
114
|
-
|
|
115
|
-
|
|
162
|
+
// The conversion runs the guest's valueOf, which can reach a bridge: an
|
|
163
|
+
// unchecked failure both parks the host exception and leaves s
|
|
164
|
+
// uninitialized to be used as an offset.
|
|
165
|
+
int64_t s = 0;
|
|
166
|
+
if (JS_ToInt64(ctx, &s, argv[1]) < 0)
|
|
167
|
+
return j_rethrow_the_guests_own(ctx);
|
|
116
168
|
start = (long)s;
|
|
117
169
|
if (start < 0)
|
|
118
170
|
start = file_size + start;
|
|
@@ -125,8 +177,9 @@ static JSValue js_ruby_file_slice(JSContext *ctx, JSValueConst _this, int argc,
|
|
|
125
177
|
long end = file_size;
|
|
126
178
|
if (argc > 2 && !JS_IsUndefined(argv[2]))
|
|
127
179
|
{
|
|
128
|
-
int64_t e;
|
|
129
|
-
JS_ToInt64(ctx, &e, argv[2])
|
|
180
|
+
int64_t e = 0;
|
|
181
|
+
if (JS_ToInt64(ctx, &e, argv[2]) < 0)
|
|
182
|
+
return j_rethrow_the_guests_own(ctx);
|
|
130
183
|
end = (long)e;
|
|
131
184
|
if (end < 0)
|
|
132
185
|
end = file_size + end;
|
|
@@ -136,10 +189,6 @@ static JSValue js_ruby_file_slice(JSContext *ctx, JSValueConst _this, int argc,
|
|
|
136
189
|
end = file_size;
|
|
137
190
|
}
|
|
138
191
|
|
|
139
|
-
const char *content_type = "";
|
|
140
|
-
if (argc > 3 && JS_IsString(argv[3]))
|
|
141
|
-
content_type = JS_ToCString(ctx, argv[3]);
|
|
142
|
-
|
|
143
192
|
long len = end > start ? end - start : 0;
|
|
144
193
|
|
|
145
194
|
rb_funcall(r_file, rb_intern("rewind"), 0);
|
|
@@ -148,30 +197,91 @@ static JSValue js_ruby_file_slice(JSContext *ctx, JSValueConst _this, int argc,
|
|
|
148
197
|
VALUE r_bytes = rb_funcall(r_file, rb_intern("read"), 1, LONG2NUM(len));
|
|
149
198
|
rb_funcall(r_bytes, rb_intern("force_encoding"), 1, rb_str_new_cstr("BINARY"));
|
|
150
199
|
|
|
200
|
+
// Built after every call that can raise, and while the C string is still
|
|
201
|
+
// owned, so nothing here is holding a JS value when a Ruby raise unwinds and
|
|
202
|
+
// nothing is left to free on the paths that bail out below.
|
|
203
|
+
JSValue j_type_str;
|
|
204
|
+
if (argc > 3 && JS_IsString(argv[3]))
|
|
205
|
+
{
|
|
206
|
+
const char *content_type = JS_ToCString(ctx, argv[3]);
|
|
207
|
+
if (content_type == NULL)
|
|
208
|
+
// It threw on the way, and returning a Blob with that left pending would
|
|
209
|
+
// attribute it to whatever asks next.
|
|
210
|
+
return j_rethrow_the_guests_own(ctx);
|
|
211
|
+
j_type_str = JS_NewString(ctx, content_type);
|
|
212
|
+
JS_FreeCString(ctx, content_type);
|
|
213
|
+
}
|
|
214
|
+
else
|
|
215
|
+
{
|
|
216
|
+
j_type_str = JS_NewString(ctx, "");
|
|
217
|
+
}
|
|
218
|
+
if (JS_IsException(j_type_str))
|
|
219
|
+
return j_type_str;
|
|
220
|
+
|
|
221
|
+
// Built through JS_NewTypedArray and the constructor captured at init, not
|
|
222
|
+
// by calling globalThis.Uint8Array and globalThis.Blob: those are names the
|
|
223
|
+
// guest can delete or replace with a shim, and a slice of a host File is not
|
|
224
|
+
// something it gets to answer for. Every step is checked, so a sentinel is
|
|
225
|
+
// never stored into j_parts and handed on as an ordinary element.
|
|
151
226
|
JSValue j_buf = JS_NewArrayBufferCopy(ctx, (const uint8_t *)RSTRING_PTR(r_bytes), RSTRING_LEN(r_bytes));
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
227
|
+
if (JS_IsException(j_buf))
|
|
228
|
+
{
|
|
229
|
+
JS_FreeValue(ctx, j_type_str);
|
|
230
|
+
return j_buf;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
JSValue j_ta_args[3] = {j_buf, JS_NewInt32(ctx, 0), JS_NewInt64(ctx, RSTRING_LEN(r_bytes))};
|
|
234
|
+
JSValue j_uint8 = JS_NewTypedArray(ctx, 3, (JSValueConst *)j_ta_args, JS_TYPED_ARRAY_UINT8);
|
|
235
|
+
JS_FreeValue(ctx, j_buf);
|
|
236
|
+
if (JS_IsException(j_uint8))
|
|
237
|
+
{
|
|
238
|
+
JS_FreeValue(ctx, j_type_str);
|
|
239
|
+
return j_uint8;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (JS_IsUndefined(data->j_blob_ctor))
|
|
243
|
+
{
|
|
244
|
+
JS_FreeValue(ctx, j_uint8);
|
|
245
|
+
JS_FreeValue(ctx, j_type_str);
|
|
246
|
+
return JS_ThrowInternalError(ctx, "Blob is not available to build a slice");
|
|
247
|
+
}
|
|
155
248
|
|
|
156
249
|
JSValue j_parts = JS_NewArray(ctx);
|
|
157
|
-
|
|
250
|
+
if (JS_IsException(j_parts))
|
|
251
|
+
{
|
|
252
|
+
JS_FreeValue(ctx, j_uint8);
|
|
253
|
+
JS_FreeValue(ctx, j_type_str);
|
|
254
|
+
return j_parts;
|
|
255
|
+
}
|
|
256
|
+
if (JS_DefinePropertyValueUint32(ctx, j_parts, 0, j_uint8, JS_PROP_C_W_E) < 0)
|
|
257
|
+
{
|
|
258
|
+
// The define freed the bytes on its way out, so carrying on would build the
|
|
259
|
+
// Blob from an empty array and hand the guest a zero-length slice of a file
|
|
260
|
+
// that is not empty, with a throw left pending behind it.
|
|
261
|
+
JS_FreeValue(ctx, j_parts);
|
|
262
|
+
JS_FreeValue(ctx, j_type_str);
|
|
263
|
+
return JS_EXCEPTION;
|
|
264
|
+
}
|
|
158
265
|
|
|
159
266
|
JSValue j_opts = JS_NewObject(ctx);
|
|
160
|
-
|
|
267
|
+
if (JS_IsException(j_opts))
|
|
268
|
+
{
|
|
269
|
+
JS_FreeValue(ctx, j_parts);
|
|
270
|
+
JS_FreeValue(ctx, j_type_str);
|
|
271
|
+
return j_opts;
|
|
272
|
+
}
|
|
273
|
+
if (JS_DefinePropertyValueStr(ctx, j_opts, "type", j_type_str, JS_PROP_C_W_E) < 0)
|
|
274
|
+
{
|
|
275
|
+
JS_FreeValue(ctx, j_parts);
|
|
276
|
+
JS_FreeValue(ctx, j_opts);
|
|
277
|
+
return JS_EXCEPTION;
|
|
278
|
+
}
|
|
161
279
|
|
|
162
|
-
JSValue j_blob_ctor = JS_GetPropertyStr(ctx, j_global, "Blob");
|
|
163
280
|
JSValueConst blob_args[2] = {j_parts, j_opts};
|
|
164
|
-
JSValue j_blob = JS_CallConstructor(ctx, j_blob_ctor, 2, blob_args);
|
|
281
|
+
JSValue j_blob = JS_CallConstructor(ctx, data->j_blob_ctor, 2, blob_args);
|
|
165
282
|
|
|
166
|
-
if (argc > 3 && JS_IsString(argv[3]))
|
|
167
|
-
JS_FreeCString(ctx, content_type);
|
|
168
|
-
|
|
169
|
-
JS_FreeValue(ctx, j_buf);
|
|
170
|
-
JS_FreeValue(ctx, j_uint8_ctor);
|
|
171
283
|
JS_FreeValue(ctx, j_parts);
|
|
172
284
|
JS_FreeValue(ctx, j_opts);
|
|
173
|
-
JS_FreeValue(ctx, j_blob_ctor);
|
|
174
|
-
JS_FreeValue(ctx, j_global);
|
|
175
285
|
|
|
176
286
|
return j_blob;
|
|
177
287
|
}
|
|
@@ -179,13 +289,28 @@ static JSValue js_ruby_file_slice(JSContext *ctx, JSValueConst _this, int argc,
|
|
|
179
289
|
void quickjsrb_init_file_proxy(VMData *data)
|
|
180
290
|
{
|
|
181
291
|
const char *factory_src =
|
|
182
|
-
|
|
292
|
+
// Everything the closure needs is bound once, here, while this is the only
|
|
293
|
+
// code that has run: Object, Proxy, Reflect and Symbol are the guest's to
|
|
294
|
+
// replace by the time a File actually crosses, and this body runs per
|
|
295
|
+
// crossing. With `globalThis.Proxy = null` every crossing used to fail,
|
|
296
|
+
// and a replaced Object.defineProperty published the handle as an
|
|
297
|
+
// ordinary enumerable property.
|
|
298
|
+
//
|
|
299
|
+
// The handler object has a null prototype for a sharper reason: QuickJS
|
|
300
|
+
// resolves a missing trap with an ordinary property get, which walks the
|
|
301
|
+
// handler's prototype chain. With Object.prototype in it, a guest writing
|
|
302
|
+
// Object.prototype.getOwnPropertyDescriptor supplies the trap for the
|
|
303
|
+
// one read this branch made load-bearing, and the invariant check on the
|
|
304
|
+
// way back compares descriptor flags without comparing the value, so a
|
|
305
|
+
// handle of the guest's choosing comes back for a host File.
|
|
306
|
+
"(function(FileProto, ObjectCreate, DefineProperty, ProxyCtor, ReflectGet, ToStringTag,\n"
|
|
307
|
+
" getName, getSize, getType, getLastModified, getText, getArrayBuffer, getSlice) {\n"
|
|
183
308
|
" return function(handle) {\n"
|
|
184
|
-
" var target =
|
|
185
|
-
"
|
|
186
|
-
"
|
|
187
|
-
"
|
|
188
|
-
"
|
|
309
|
+
" var target = ObjectCreate(FileProto);\n"
|
|
310
|
+
" DefineProperty(target, 'rb_object_id', { value: handle, enumerable: false });\n"
|
|
311
|
+
" var handler = ObjectCreate(null);\n"
|
|
312
|
+
" handler.getPrototypeOf = function() { return FileProto; };\n"
|
|
313
|
+
" handler.get = function(target, prop, receiver) {\n"
|
|
189
314
|
" if (prop === 'name') return getName(handle);\n"
|
|
190
315
|
" if (prop === 'size') return getSize(handle);\n"
|
|
191
316
|
" if (prop === 'type') return getType(handle);\n"
|
|
@@ -193,39 +318,112 @@ void quickjsrb_init_file_proxy(VMData *data)
|
|
|
193
318
|
" if (prop === 'text') return function() { return getText(handle); };\n"
|
|
194
319
|
" if (prop === 'arrayBuffer') return function() { return getArrayBuffer(handle); };\n"
|
|
195
320
|
" if (prop === 'slice') return function(start, end, contentType) { return getSlice(handle, start, end, contentType); };\n"
|
|
196
|
-
" if (prop ===
|
|
321
|
+
" if (prop === ToStringTag) return 'File';\n"
|
|
197
322
|
" if (prop === 'toString') return function() { return '[object File]'; };\n"
|
|
198
|
-
" return
|
|
199
|
-
" }
|
|
200
|
-
"
|
|
323
|
+
" return ReflectGet(target, prop, receiver);\n"
|
|
324
|
+
" };\n"
|
|
325
|
+
" return new ProxyCtor(target, handler);\n"
|
|
201
326
|
" };\n"
|
|
202
327
|
"})";
|
|
203
328
|
JSValue j_factory_fn = JS_Eval(data->context, factory_src, strlen(factory_src), "<file-proxy>", JS_EVAL_TYPE_GLOBAL);
|
|
204
329
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
330
|
+
// The prototype is taken here too, not read from globalThis.File when a proxy
|
|
331
|
+
// is built: the closure below runs per crossing, which is after guest code,
|
|
332
|
+
// and a replaced File otherwise decides what a host File is an instance of.
|
|
333
|
+
// The handle keeps working either way, so what this closes is the shape of
|
|
334
|
+
// the value rather than the bridge.
|
|
335
|
+
JSValue j_global_for_proto = JS_GetGlobalObject(data->context);
|
|
336
|
+
JSValue j_file_class = JS_GetPropertyStr(data->context, j_global_for_proto, "File");
|
|
337
|
+
JS_FreeValue(data->context, j_global_for_proto);
|
|
338
|
+
JSValue j_file_proto = JS_GetPropertyStr(data->context, j_file_class, "prototype");
|
|
339
|
+
JS_FreeValue(data->context, j_file_class);
|
|
340
|
+
if (JS_IsException(j_file_proto))
|
|
341
|
+
{
|
|
342
|
+
// Checked like the Blob capture below. Left as the sentinel it would go
|
|
343
|
+
// into the factory call, come back out as j_file_proxy_creator, and every
|
|
344
|
+
// File crossing would call into it with a throw left pending for whatever
|
|
345
|
+
// asked next.
|
|
346
|
+
JS_FreeValue(data->context, JS_GetException(data->context));
|
|
347
|
+
j_file_proto = JS_UNDEFINED;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// The intrinsics the closure uses, taken now rather than resolved per
|
|
351
|
+
// crossing. Evaluated rather than read off the global one by one, so a value
|
|
352
|
+
// that is already missing cannot leave a sentinel in the argument list.
|
|
353
|
+
static const char intrinsics_src[] =
|
|
354
|
+
"[Object.create, Object.defineProperty, Proxy, Reflect.get, Symbol.toStringTag]";
|
|
355
|
+
JSValue j_intrinsics = JS_Eval(data->context, intrinsics_src, sizeof(intrinsics_src) - 1,
|
|
356
|
+
"<file-proxy-intrinsics>", JS_EVAL_TYPE_GLOBAL);
|
|
357
|
+
if (JS_IsException(j_intrinsics))
|
|
358
|
+
{
|
|
359
|
+
JS_FreeValue(data->context, JS_GetException(data->context));
|
|
360
|
+
JS_FreeValue(data->context, j_intrinsics);
|
|
361
|
+
JS_FreeValue(data->context, j_file_proto);
|
|
362
|
+
JS_FreeValue(data->context, j_factory_fn);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
213
365
|
|
|
214
|
-
|
|
366
|
+
JSValue j_helpers[13];
|
|
367
|
+
j_helpers[0] = j_file_proto;
|
|
368
|
+
for (int i = 0; i < 5; i++)
|
|
369
|
+
j_helpers[1 + i] = JS_GetPropertyUint32(data->context, j_intrinsics, (uint32_t)i);
|
|
370
|
+
JS_FreeValue(data->context, j_intrinsics);
|
|
371
|
+
j_helpers[6] = quickjsrb_new_ruby_bridge(data->context, js_ruby_file_name, "__rb_file_name", 1);
|
|
372
|
+
j_helpers[7] = quickjsrb_new_ruby_bridge(data->context, js_ruby_file_size, "__rb_file_size", 1);
|
|
373
|
+
j_helpers[8] = quickjsrb_new_ruby_bridge(data->context, js_ruby_file_type, "__rb_file_type", 1);
|
|
374
|
+
j_helpers[9] = quickjsrb_new_ruby_bridge(data->context, js_ruby_file_last_modified, "__rb_file_last_modified", 1);
|
|
375
|
+
j_helpers[10] = quickjsrb_new_ruby_bridge(data->context, js_ruby_file_text, "__rb_file_text", 1);
|
|
376
|
+
j_helpers[11] = quickjsrb_new_ruby_bridge(data->context, js_ruby_file_array_buffer, "__rb_file_array_buffer", 1);
|
|
377
|
+
j_helpers[12] = quickjsrb_new_ruby_bridge(data->context, js_ruby_file_slice, "__rb_file_slice", 4);
|
|
378
|
+
|
|
379
|
+
// Freed before it is replaced, for the reason the Blob capture below gives:
|
|
380
|
+
// initialize is private but reachable through send, and a second pass would
|
|
381
|
+
// otherwise leak this closure and the seven bridges it holds.
|
|
382
|
+
if (!JS_IsUndefined(data->j_file_proxy_creator))
|
|
383
|
+
JS_FreeValue(data->context, data->j_file_proxy_creator);
|
|
384
|
+
data->j_file_proxy_creator = JS_Call(data->context, j_factory_fn, JS_UNDEFINED, 13, j_helpers);
|
|
385
|
+
|
|
386
|
+
// Taken here, before a line of guest code has run, because slice builds its
|
|
387
|
+
// result with it and globalThis.Blob is the guest's to replace by then.
|
|
388
|
+
//
|
|
389
|
+
// Once per VM, like the Proxy probe: initialize is private but reachable
|
|
390
|
+
// through send, and a second pass would overwrite this reference without
|
|
391
|
+
// freeing the first, which outlives JS_FreeRuntime along with everything it
|
|
392
|
+
// holds.
|
|
393
|
+
if (!JS_IsUndefined(data->j_blob_ctor))
|
|
394
|
+
JS_FreeValue(data->context, data->j_blob_ctor);
|
|
395
|
+
JSValue j_global = JS_GetGlobalObject(data->context);
|
|
396
|
+
data->j_blob_ctor = JS_GetPropertyStr(data->context, j_global, "Blob");
|
|
397
|
+
JS_FreeValue(data->context, j_global);
|
|
398
|
+
if (JS_IsException(data->j_blob_ctor))
|
|
399
|
+
{
|
|
400
|
+
JS_FreeValue(data->context, JS_GetException(data->context));
|
|
401
|
+
data->j_blob_ctor = JS_UNDEFINED;
|
|
402
|
+
}
|
|
215
403
|
|
|
216
404
|
JS_FreeValue(data->context, j_factory_fn);
|
|
217
|
-
for (int i = 0; i <
|
|
405
|
+
for (int i = 0; i < 13; i++)
|
|
218
406
|
JS_FreeValue(data->context, j_helpers[i]);
|
|
219
407
|
}
|
|
220
408
|
|
|
221
409
|
JSValue quickjsrb_file_to_js(JSContext *ctx, VALUE r_file)
|
|
222
410
|
{
|
|
223
411
|
VMData *data = JS_GetContextOpaque(ctx);
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
412
|
+
// Taken in the same raise-safe stretch as the draw, so the rollback below
|
|
413
|
+
// does not dispatch from inside a JSCFunction on its way out.
|
|
414
|
+
VALUE r_file_object_id = rb_obj_id(r_file);
|
|
415
|
+
bool registered_here = false;
|
|
416
|
+
VALUE r_object_id = alive_objects_register(data, r_file, ®istered_here);
|
|
417
|
+
if (NIL_P(r_object_id))
|
|
418
|
+
return JS_ThrowInternalError(ctx, "quickjs: could not publish a handle for a host File");
|
|
419
|
+
JSValue j_handle = JS_NewInt64(ctx, NUM2LL(r_object_id));
|
|
227
420
|
JSValue j_proxy = JS_Call(ctx, data->j_file_proxy_creator, JS_UNDEFINED, 1, &j_handle);
|
|
228
421
|
JS_FreeValue(ctx, j_handle);
|
|
422
|
+
if (JS_IsException(j_proxy) && registered_here)
|
|
423
|
+
// The third writer, taught the rollback the other two already had: the
|
|
424
|
+
// creator can fail, and a row left behind anchors this File and its
|
|
425
|
+
// descriptor for the life of the VM with nothing in JS able to reach it.
|
|
426
|
+
alive_objects_unregister(data, r_object_id, r_file_object_id);
|
|
229
427
|
return j_proxy;
|
|
230
428
|
}
|
|
231
429
|
|
|
@@ -281,7 +479,12 @@ VALUE quickjsrb_try_convert_js_file(JSContext *ctx, JSValue j_val)
|
|
|
281
479
|
JSValue j_file_ctor = JS_GetPropertyStr(ctx, j_global, "File");
|
|
282
480
|
if (!JS_IsUndefined(j_file_ctor) && !JS_IsException(j_file_ctor))
|
|
283
481
|
{
|
|
482
|
+
// instanceof runs Symbol.hasInstance, which is the guest's to define and
|
|
483
|
+
// can reach a bridge: a failure here leaves that throw pending and the host
|
|
484
|
+
// exception it parked with nothing able to take it back out.
|
|
284
485
|
int is_file = JS_IsInstanceOf(ctx, j_val, j_file_ctor);
|
|
486
|
+
if (is_file < 0)
|
|
487
|
+
quickjsrb_drain_pending(ctx);
|
|
285
488
|
JS_FreeValue(ctx, j_file_ctor);
|
|
286
489
|
if (is_file > 0)
|
|
287
490
|
{
|
|
@@ -320,6 +523,8 @@ VALUE quickjsrb_try_convert_js_file(JSContext *ctx, JSValue j_val)
|
|
|
320
523
|
}
|
|
321
524
|
|
|
322
525
|
int is_blob = JS_IsInstanceOf(ctx, j_val, j_blob_ctor);
|
|
526
|
+
if (is_blob < 0)
|
|
527
|
+
quickjsrb_drain_pending(ctx);
|
|
323
528
|
JS_FreeValue(ctx, j_blob_ctor);
|
|
324
529
|
if (is_blob <= 0)
|
|
325
530
|
return Qnil;
|
data/lib/quickjs/crypto_key.rb
CHANGED
|
@@ -11,5 +11,18 @@ module Quickjs
|
|
|
11
11
|
@usages = usages
|
|
12
12
|
@key_data = key_data
|
|
13
13
|
end
|
|
14
|
+
|
|
15
|
+
# The bytes stay out of the string form. to_js_value has no case for this
|
|
16
|
+
# class, so anything that hands a key back to JS, a pass-through
|
|
17
|
+
# define_function or a logged value, converts it by calling inspect: a key
|
|
18
|
+
# generated with extractable: false would otherwise reach the guest in full
|
|
19
|
+
# through a function that exportKey refuses one call away.
|
|
20
|
+
def inspect
|
|
21
|
+
format(
|
|
22
|
+
'#<%s type=%p extractable=%p algorithm=%p usages=%p key_data=[FILTERED]>',
|
|
23
|
+
self.class, @type, @extractable, @algorithm, @usages
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
alias to_s inspect
|
|
14
27
|
end
|
|
15
28
|
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quickjs
|
|
4
|
+
# An ES module compiled to bytecode once and importable into any number of
|
|
5
|
+
# VMs without reparsing. The module-map counterpart of `Runnable`, which does
|
|
6
|
+
# the same for classic scripts.
|
|
7
|
+
#
|
|
8
|
+
# Unlike `Quickjs.register_module`, nothing about this is process-global:
|
|
9
|
+
# hold the object, drop it when you're done. That matters for a library that
|
|
10
|
+
# ships JS of its own, since a global registry would make unrelated gems
|
|
11
|
+
# coordinate on a name none of their VMs ever share.
|
|
12
|
+
class Importable
|
|
13
|
+
# The name QuickJS keys its module map by, the same thing a module_loader
|
|
14
|
+
# calls `as:`. Not the `filename:` that was passed in: that is only a
|
|
15
|
+
# prefix of this. Useful for pointing a loader at this module by a
|
|
16
|
+
# friendlier specifier.
|
|
17
|
+
attr_reader :canonical_name
|
|
18
|
+
|
|
19
|
+
def initialize(bytecode, canonical_name)
|
|
20
|
+
@bytecode = bytecode
|
|
21
|
+
@canonical_name = canonical_name
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def inspect
|
|
25
|
+
"#<#{self.class} #{@canonical_name} (#{@bytecode.bytesize} bytes)>"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# Deliberately not public. Handing out the blob would invite persisting it,
|
|
31
|
+
# and the bytecode format is tied to the QuickJS build with only a one-byte
|
|
32
|
+
# version tag to catch a mismatch.
|
|
33
|
+
def bytecode
|
|
34
|
+
@bytecode
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Compiles `source` as an ES module and returns an `Importable`.
|
|
39
|
+
#
|
|
40
|
+
# The module's name is generated rather than taken from the caller, because
|
|
41
|
+
# it is baked into the bytecode and becomes the module's identity in every VM
|
|
42
|
+
# that imports it. Generating it means two independently built Importables
|
|
43
|
+
# can never collide, which is the whole point when the JS ships inside a gem.
|
|
44
|
+
# `filename:` prefixes the generated name so stack traces stay readable; it is
|
|
45
|
+
# a label, not an identity.
|
|
46
|
+
#
|
|
47
|
+
# Compiles on a disposable VM: compilation installs a stub module loader to
|
|
48
|
+
# satisfy QuickJS's compile-time import resolution, and those stubs land in
|
|
49
|
+
# the compiling context's module map. The default timeout is generous because
|
|
50
|
+
# a VM's `timeout_msec` is a budget for its own JS, not for parsing.
|
|
51
|
+
def self.compile_module(source, filename: nil, **opts)
|
|
52
|
+
raise ::TypeError, "source must be a String, got #{source.class}" unless source.is_a?(String)
|
|
53
|
+
unless filename.nil? || filename.is_a?(String)
|
|
54
|
+
raise ::TypeError, "filename: must be a String, got #{filename.class}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
name = "#{filename || 'module'}-#{SecureRandom.hex(8)}"
|
|
58
|
+
vm = VM.new(**{timeout_msec: 60_000, features: []}.merge(opts))
|
|
59
|
+
Importable.new(vm.send(:_compile_module_to_bytecode, source, name), name)
|
|
60
|
+
ensure
|
|
61
|
+
vm&.dispose!
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
module ImportableSupport
|
|
65
|
+
# `from:` an Importable reads the bytecode into this VM and then imports it
|
|
66
|
+
# by name, so it takes the same path a preloaded module does: resolution
|
|
67
|
+
# finds it in the module map and `module_loader` is never asked about it.
|
|
68
|
+
# Reading is idempotent, so importing the same Importable repeatedly costs
|
|
69
|
+
# nothing after the first.
|
|
70
|
+
def import(imported, **opts)
|
|
71
|
+
importable = opts[:from]
|
|
72
|
+
return super unless importable.is_a?(Importable)
|
|
73
|
+
|
|
74
|
+
send(:_preload_module_bytecode, importable.send(:bytecode), importable.canonical_name)
|
|
75
|
+
super(imported, **opts.except(:from), filename: importable.canonical_name)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
VM.prepend(ImportableSupport) unless VM.ancestors.include?(ImportableSupport)
|
|
80
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quickjs
|
|
4
|
+
# Process-wide registry of ES modules; entries are lazily compiled to
|
|
5
|
+
# bytecode on first use and the bytecode is reused across VMs.
|
|
6
|
+
@_modules = {}
|
|
7
|
+
|
|
8
|
+
# `name` is the module's canonical name: the string JS code writes in its
|
|
9
|
+
# `import` statement, and the name QuickJS keys its module map by. It is
|
|
10
|
+
# baked into the compiled bytecode, so registering under it is what keeps
|
|
11
|
+
# the registry key and the module's identity from drifting apart.
|
|
12
|
+
#
|
|
13
|
+
# `source:` accepts either a `String` (eager) or a `Proc` returning one
|
|
14
|
+
# (lazy). The lazy form lets a gem register a module at require time
|
|
15
|
+
# without paying the file-read cost unless a VM actually preloads it.
|
|
16
|
+
#
|
|
17
|
+
# Registering only makes the module available; a VM picks it up with
|
|
18
|
+
# `VM.new(preload_modules: [name])`. Reading a module into a VM costs
|
|
19
|
+
# real time whether or not it ends up imported, so preloading is a
|
|
20
|
+
# per-VM decision rather than something registration forces on every VM.
|
|
21
|
+
def self.register_module(name, source:)
|
|
22
|
+
raise ::TypeError, "name must be a String, got #{name.class}" unless name.is_a?(String)
|
|
23
|
+
raise ::TypeError, "source: must be a String or Proc, got #{source.class}" unless source.is_a?(String) || source.is_a?(Proc)
|
|
24
|
+
|
|
25
|
+
@_modules[-name] = {source: source, bytecode: nil, mutex: Mutex.new}
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self._module_registered?(name)
|
|
30
|
+
@_modules.key?(name)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self._unregister_module(name)
|
|
34
|
+
@_modules.delete(name)
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self._preload_modules(vm, names)
|
|
39
|
+
names.each do |name|
|
|
40
|
+
raise ::TypeError, "preload_modules: entries must be Strings, got #{name.class}" unless name.is_a?(String)
|
|
41
|
+
|
|
42
|
+
entry = @_modules[name]
|
|
43
|
+
unless entry
|
|
44
|
+
raise ::ArgumentError,
|
|
45
|
+
"#{name.inspect} is not a registered module; call Quickjs.register_module(#{name.inspect}, source: ...) first"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Same locking as registered polyfills: `||=` alone isn't atomic
|
|
49
|
+
# because compilation yields the GVL, so threads racing to construct
|
|
50
|
+
# VMs could double-compile, or a loser could read entry[:source]
|
|
51
|
+
# after the winner cleared it. A compile failure leaves bytecode nil
|
|
52
|
+
# and source intact so a later VM can retry. The unlocked first read
|
|
53
|
+
# keeps the hot path off the lock once the bytecode exists.
|
|
54
|
+
bytecode = entry[:bytecode] || entry[:mutex].synchronize {
|
|
55
|
+
entry[:bytecode] ||= begin
|
|
56
|
+
compiled = _compile_registered_module(entry, name)
|
|
57
|
+
entry[:source] = nil # let the Proc's captured scope be GC'd
|
|
58
|
+
compiled
|
|
59
|
+
end
|
|
60
|
+
}
|
|
61
|
+
vm.send(:_preload_module_bytecode, bytecode, name)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Compiled on a disposable VM: compilation installs a stub module loader
|
|
66
|
+
# to satisfy QuickJS's compile-time import resolution, and those stubs
|
|
67
|
+
# land in the compiling context's module map, so it must not be a VM
|
|
68
|
+
# anyone goes on to use. The generous timeout covers parsing large
|
|
69
|
+
# bundles, since the user's per-VM `timeout_msec` is a budget for their
|
|
70
|
+
# own JS. `features: []` keeps registered polyfills off the temp VM.
|
|
71
|
+
def self._compile_registered_module(entry, name)
|
|
72
|
+
source = entry[:source]
|
|
73
|
+
source = source.call if source.is_a?(Proc)
|
|
74
|
+
unless source.is_a?(String)
|
|
75
|
+
raise ::TypeError, "source: Proc for #{name.inspect} must return a String, got #{source.class}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
vm = VM.new(timeout_msec: 60_000, features: [])
|
|
79
|
+
vm.send(:_compile_module_to_bytecode, source, name)
|
|
80
|
+
ensure
|
|
81
|
+
vm&.dispose!
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
module ModulePreloader
|
|
85
|
+
def initialize(preload_modules: [], **opts)
|
|
86
|
+
super(**opts)
|
|
87
|
+
unless preload_modules.is_a?(Array)
|
|
88
|
+
raise ::TypeError, "preload_modules: must be an Array, got #{preload_modules.class}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
Quickjs._preload_modules(self, preload_modules)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Prepended after PolyfillLoader so polyfills are installed first: a
|
|
96
|
+
# module body can reach for a polyfilled global when it evaluates.
|
|
97
|
+
VM.prepend(ModulePreloader) unless VM.ancestors.include?(ModulePreloader)
|
|
98
|
+
end
|
data/lib/quickjs/polyfills.rb
CHANGED
|
@@ -12,6 +12,12 @@ module Quickjs
|
|
|
12
12
|
# VM with the same feature: compilation is locked per entry, so that
|
|
13
13
|
# recursion raises ThreadError instead of deadlocking silently.
|
|
14
14
|
#
|
|
15
|
+
# The polyfill's top level must settle synchronously — no top-level
|
|
16
|
+
# await. `VM.new(features:)` promises a usable polyfill on return, but
|
|
17
|
+
# loads never drain the job queue, so nothing past the first await
|
|
18
|
+
# would have run by then; a pending load raises Quickjs::NoAwaitError
|
|
19
|
+
# instead of handing back a silently half-applied VM.
|
|
20
|
+
#
|
|
15
21
|
# Re-registering a name replaces the entry wholesale, lock included: a
|
|
16
22
|
# VM construction already compiling under the old entry finishes
|
|
17
23
|
# against it (and loads the old bytecode) while the first construction
|
|
@@ -57,7 +63,15 @@ module Quickjs
|
|
|
57
63
|
compiled
|
|
58
64
|
end
|
|
59
65
|
}
|
|
60
|
-
|
|
66
|
+
begin
|
|
67
|
+
vm.send(:_load_polyfill_bytecode, bytecode)
|
|
68
|
+
rescue Quickjs::RuntimeError => e
|
|
69
|
+
# The C layer only sees bytecode; name the registration so a VM
|
|
70
|
+
# with several polyfill features points at the one that failed.
|
|
71
|
+
# `raise e, msg` clones — class, js_name, and any JS backtrace
|
|
72
|
+
# already set on the original all survive.
|
|
73
|
+
raise e, "#{feature}: #{e.message}"
|
|
74
|
+
end
|
|
61
75
|
end
|
|
62
76
|
end
|
|
63
77
|
|