quickjs 0.21.0 → 0.22.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/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
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Quickjs
|
|
6
|
+
# Ruby values reach JS as a declaration rather than a property assignment,
|
|
7
|
+
# so the caller picks the binding form and JS's own rules apply unchanged:
|
|
8
|
+
# `const` is read-only and refuses redeclaration, `var` is the only form
|
|
9
|
+
# that lands on `globalThis`. Nothing here invents semantics QuickJS does
|
|
10
|
+
# not already have, which is what keeps a colliding `let` in user code a
|
|
11
|
+
# loud SyntaxError instead of a silent shadow.
|
|
12
|
+
#
|
|
13
|
+
# Defined directly on VM rather than through a prepended module, since none
|
|
14
|
+
# of these wrap an existing method.
|
|
15
|
+
class VM
|
|
16
|
+
# `$` and `_` are ordinary identifier characters in JS. Anything outside
|
|
17
|
+
# this set would be concatenated straight into source we evaluate, so
|
|
18
|
+
# this check is what stops a name from smuggling in arbitrary JS.
|
|
19
|
+
NAME_PATTERN = /\A[A-Za-z_$][A-Za-z0-9_$]*\z/
|
|
20
|
+
|
|
21
|
+
# The keyword is interpolated too, and a Symbol interpolates through
|
|
22
|
+
# Symbol#to_s exactly as a name would. These are ours rather than the
|
|
23
|
+
# caller's, but "ours" is not the property that matters: what reaches the
|
|
24
|
+
# source has to be a String that answers for itself.
|
|
25
|
+
JS_KEYWORDS = { const: "const", let: "let", var: "var" }.freeze
|
|
26
|
+
|
|
27
|
+
# The full spec list. QuickJS refuses some of these itself, but not all:
|
|
28
|
+
# a declaration eval is sloppy mode, so `var static = 1` and the other
|
|
29
|
+
# strict-mode-only words evaluate cleanly there. Rejecting the whole list
|
|
30
|
+
# keeps the answer the same whichever word it was.
|
|
31
|
+
RESERVED_WORDS = %w[
|
|
32
|
+
await break case catch class const continue debugger default delete do
|
|
33
|
+
else enum export extends false finally for function if implements import
|
|
34
|
+
in instanceof interface let new null package private protected public
|
|
35
|
+
return static super switch this throw true try typeof var void while
|
|
36
|
+
with yield
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
private_constant :NAME_PATTERN, :RESERVED_WORDS, :JS_KEYWORDS
|
|
40
|
+
|
|
41
|
+
def define_const(name, value)
|
|
42
|
+
_define_variable(:const, name, value)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def define_let(name, value)
|
|
46
|
+
_define_variable(:let, name, value)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def define_var(name, value)
|
|
50
|
+
_define_variable(:var, name, value)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def _define_variable(kind, name, value)
|
|
56
|
+
# The validated String itself, all the way to the interpolation. Handing
|
|
57
|
+
# back a Symbol and interpolating that put a dispatch between the check
|
|
58
|
+
# and the source: interpolating a Symbol calls Symbol#to_s, so the bytes
|
|
59
|
+
# that were matched against the pattern were thrown away and the name was
|
|
60
|
+
# asked for again afterwards. A plain String interpolates as itself.
|
|
61
|
+
budget = _js_source_budget
|
|
62
|
+
key = _validate_variable_name(name, budget)
|
|
63
|
+
# The per-container check bounds the expanding case; this catches the
|
|
64
|
+
# flat one, a single enormous String or a very wide container, where no
|
|
65
|
+
# inner container ever crosses the line on its own.
|
|
66
|
+
#
|
|
67
|
+
# Depth is left to the stack rather than to a constant. What a walk
|
|
68
|
+
# costs depends on the stack it runs on, and a Ruby thread's is a
|
|
69
|
+
# fraction of the main thread's, so the same 900 levels are fine on one
|
|
70
|
+
# and fatal on the other. SystemStackError is not a StandardError
|
|
71
|
+
# either, so a caller's `rescue => e` would miss it and the thread would
|
|
72
|
+
# go down.
|
|
73
|
+
literal =
|
|
74
|
+
begin
|
|
75
|
+
ValueLiteral._within_budget(ValueLiteral._js_literal(value, nil, budget), budget)
|
|
76
|
+
rescue ::SystemStackError
|
|
77
|
+
raise ::ArgumentError, "value nests too deeply to convert on this thread's stack"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
declared = (@_defined_variables ||= {})
|
|
81
|
+
existing = declared[key]
|
|
82
|
+
# Cleared before any of the branches below can read it. It is one slot on
|
|
83
|
+
# the VM, and the branches that consult it sit in rescues that a probe
|
|
84
|
+
# ahead of _declare can also reach: _refuse_unusable_global evaluates JS
|
|
85
|
+
# and can raise the same classes. Left standing, an outcome from the
|
|
86
|
+
# previous define would answer for a declaration this one never ran.
|
|
87
|
+
@_last_declare_outcome = nil
|
|
88
|
+
|
|
89
|
+
if existing == :interrupted
|
|
90
|
+
raise ::ArgumentError,
|
|
91
|
+
"#{key} was left uninitialized by a declaration this VM interrupted, so the name is " \
|
|
92
|
+
"unusable for the life of the VM"
|
|
93
|
+
elsif existing.nil?
|
|
94
|
+
_refuse_unusable_global(key) if kind == :var
|
|
95
|
+
begin
|
|
96
|
+
_declare(declared, kind, key, literal)
|
|
97
|
+
rescue Quickjs::InterruptedError
|
|
98
|
+
raise
|
|
99
|
+
rescue Quickjs::SyntaxError
|
|
100
|
+
# JavaScript refused the declaration, and a fresh define used to pass
|
|
101
|
+
# that on as the engine's own redeclaration error while the same
|
|
102
|
+
# situation with a record present answered with an ArgumentError
|
|
103
|
+
# saying why. Two classes for one situation, and the README teaches
|
|
104
|
+
# the ArgumentError.
|
|
105
|
+
raise unless _declare_refused?
|
|
106
|
+
|
|
107
|
+
# Which refusal it was depends on the form. A non-configurable global
|
|
108
|
+
# blocks a lexical declaration and does not block a var: measured, a
|
|
109
|
+
# var of that name is accepted and the assignment reaches the global.
|
|
110
|
+
# So for a var the only thing left is a lexical binding the guest
|
|
111
|
+
# declared, and diagnosing the descriptor there would name a cause
|
|
112
|
+
# that cannot be the one.
|
|
113
|
+
_refuse_restricted_global(key, kind) unless kind == :var
|
|
114
|
+
|
|
115
|
+
raise ::ArgumentError,
|
|
116
|
+
"#{key} is already declared in this VM by JavaScript that ran earlier, and " \
|
|
117
|
+
"JavaScript does not allow redeclaring it"
|
|
118
|
+
end
|
|
119
|
+
elsif existing != kind
|
|
120
|
+
# A record can be ahead of a declaration that never ran, so refusing a
|
|
121
|
+
# change of form on the record's word alone would go on refusing a
|
|
122
|
+
# legitimate one for the life of the VM. The same correction the two
|
|
123
|
+
# branches below make: ask, and if the other form is not there, the
|
|
124
|
+
# caller gets the one they asked for.
|
|
125
|
+
#
|
|
126
|
+
# Every cross-form redeclaration is a parse error in JavaScript, so the
|
|
127
|
+
# declaration answers this the same way it answers the const case.
|
|
128
|
+
unless _live_lexical?(key)
|
|
129
|
+
begin
|
|
130
|
+
_refuse_unusable_global(key) if kind == :var
|
|
131
|
+
return _declare(declared, kind, key, literal)
|
|
132
|
+
rescue Quickjs::InterruptedError
|
|
133
|
+
raise
|
|
134
|
+
rescue Quickjs::SyntaxError
|
|
135
|
+
# The other form really is declared, so say so below. Only when the
|
|
136
|
+
# declaration is what refused: this same class arriving from outside
|
|
137
|
+
# while it ran would revert the record for a binding the VM now has,
|
|
138
|
+
# and the next define would meet the bare redeclaration error.
|
|
139
|
+
raise unless _declare_refused?
|
|
140
|
+
|
|
141
|
+
declared[key] = existing
|
|
142
|
+
rescue Quickjs::RuntimeError
|
|
143
|
+
declared[key] = existing if _declare_refused?
|
|
144
|
+
raise
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
raise ::ArgumentError,
|
|
149
|
+
"#{key} is already defined as a #{existing}; it cannot be redefined as a #{kind}"
|
|
150
|
+
elsif kind == :const
|
|
151
|
+
# Same question, and the same reason not to ask it by provoking a parse
|
|
152
|
+
# error: a refusal is not a place to emit a console.error the caller
|
|
153
|
+
# never wrote. If the name reads as something no `globalThis` property
|
|
154
|
+
# could account for, the const is there and that is the answer.
|
|
155
|
+
if _live_lexical?(key)
|
|
156
|
+
raise ::ArgumentError,
|
|
157
|
+
"#{key} is already defined as a const; a const cannot be redefined"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Otherwise the declaration itself is the question. Redeclaring a live
|
|
161
|
+
# const is a parse error, thrown before anything runs, so this asks
|
|
162
|
+
# without running any of the guest's code and without writing anything.
|
|
163
|
+
# Succeeding means the registry was ahead of a declaration that never
|
|
164
|
+
# happened, and the const the caller asked for is now there.
|
|
165
|
+
begin
|
|
166
|
+
return _declare(declared, kind, key, literal)
|
|
167
|
+
rescue Quickjs::InterruptedError
|
|
168
|
+
# _declare already recorded what the VM is actually left holding, and
|
|
169
|
+
# that is more useful to the next caller than being told they
|
|
170
|
+
# redefined something.
|
|
171
|
+
raise
|
|
172
|
+
rescue Quickjs::SyntaxError
|
|
173
|
+
# The redeclaration this was asking about, and only when the
|
|
174
|
+
# declaration is what refused: the same class raised at us from
|
|
175
|
+
# outside says nothing about whether the const was already there, and
|
|
176
|
+
# answering "already defined" for it would report a collision the
|
|
177
|
+
# caller never hit.
|
|
178
|
+
raise unless _declare_refused?
|
|
179
|
+
|
|
180
|
+
declared[key] = :const
|
|
181
|
+
raise ::ArgumentError,
|
|
182
|
+
"#{key} is already defined as a const; a const cannot be redefined"
|
|
183
|
+
rescue Quickjs::RuntimeError
|
|
184
|
+
# Something else went wrong in the VM: out of memory, or a guest that
|
|
185
|
+
# renamed the error class this was looking for. _declare took the
|
|
186
|
+
# record out on the way past and the const may well still be there, so
|
|
187
|
+
# put it back, but let the real error through. Reporting an
|
|
188
|
+
# out-of-memory as a redefinition would tell the caller they made a
|
|
189
|
+
# mistake while the VM is dead.
|
|
190
|
+
declared[key] = :const
|
|
191
|
+
raise
|
|
192
|
+
end
|
|
193
|
+
else
|
|
194
|
+
# Redeclaring a live `let` is a parse error, so re-defining one assigns
|
|
195
|
+
# to it instead. A `var` we declared ourselves can have been swapped for
|
|
196
|
+
# an accessor since, so the same check applies to the assignment.
|
|
197
|
+
_refuse_unusable_global(key) if kind == :var
|
|
198
|
+
|
|
199
|
+
# The bare declaration first, which is what makes the assignment safe.
|
|
200
|
+
# Assigning to a name that has no binding writes `globalThis` instead:
|
|
201
|
+
# sloppy mode invents the property, and strict mode was no answer
|
|
202
|
+
# either, since it only refuses to invent one and will happily write a
|
|
203
|
+
# global that already exists. Either way a `let` ended up on
|
|
204
|
+
# `globalThis`, which is the one thing this form promises does not
|
|
205
|
+
# happen, and it stayed there for every later define.
|
|
206
|
+
#
|
|
207
|
+
# A live binding refuses the declaration, at parse time, with nothing
|
|
208
|
+
# evaluated and nothing changed. An absent one gets the binding it was
|
|
209
|
+
# missing. Both leave something for the assignment to find: a lexical
|
|
210
|
+
# binding for a `let`, which shadows any `globalThis` property of the
|
|
211
|
+
# same name, and the global property itself for a `var`.
|
|
212
|
+
#
|
|
213
|
+
# Provoking that parse error is not free. Rendering a JS exception
|
|
214
|
+
# announces it to `on_log` first, so a question asked internally came
|
|
215
|
+
# out of the VM as a console.error the caller never wrote, once per
|
|
216
|
+
# redefine, on a path that succeeded. So ask a question that cannot
|
|
217
|
+
# throw first, and only fall back to the declaration when its answer is
|
|
218
|
+
# not decisive.
|
|
219
|
+
#
|
|
220
|
+
# Decisive means: the name reads as something, and no `globalThis`
|
|
221
|
+
# property could be what it read. That is a lexical binding, which is
|
|
222
|
+
# what makes the assignment safe. `in` tests for the property without
|
|
223
|
+
# reading it, and it is tested first, so a guest accessor's getter does
|
|
224
|
+
# not run. A `has` trap on a prototype the guest installed does, which
|
|
225
|
+
# is the tier this file already says it cannot defend. Redeclaring a `var` is
|
|
226
|
+
# legal, so that form never provokes anything and skips this.
|
|
227
|
+
if kind == :var
|
|
228
|
+
# Redeclaring a var is legal, so this provokes nothing and needs no
|
|
229
|
+
# asking, and a guest lexical of the same name refusing it is a real
|
|
230
|
+
# collision rather than noise. Swallowing that let the assignment
|
|
231
|
+
# below write the guest's binding and report success for a define_var
|
|
232
|
+
# that never reached globalThis at all. It is not skipped, though: without it the assignment walks
|
|
233
|
+
# the prototype chain, and a setter inherited from Object.prototype
|
|
234
|
+
# takes the value while the guard above, which asks only about own
|
|
235
|
+
# properties, reports a clean global.
|
|
236
|
+
eval_code("var #{key};")
|
|
237
|
+
elsif !_live_lexical?(key)
|
|
238
|
+
begin
|
|
239
|
+
eval_code("#{JS_KEYWORDS.fetch(kind)} #{key};")
|
|
240
|
+
# It just made the binding, so say so. Only _declare recorded this,
|
|
241
|
+
# and the two are not the same moment: a guest can fix a global of
|
|
242
|
+
# this name afterwards, and then a later redefine, unable to tell
|
|
243
|
+
# that the binding is ours, refuses a name this line already gave
|
|
244
|
+
# the caller.
|
|
245
|
+
_confirmed[key] = kind
|
|
246
|
+
rescue Quickjs::SyntaxError
|
|
247
|
+
# Refused because the binding is already there, which is the
|
|
248
|
+
# ordinary case and leaves the assignment something to find. Or
|
|
249
|
+
# because JavaScript will never accept this declaration at all,
|
|
250
|
+
# which leaves it nothing, and then the assignment writes the
|
|
251
|
+
# global instead.
|
|
252
|
+
# A record we confirmed means our own binding is there and this is an
|
|
253
|
+
# ordinary redeclaration of it. Only an unconfirmed one can be sitting
|
|
254
|
+
# over a name JavaScript will never hand over.
|
|
255
|
+
_refuse_restricted_global(key, kind) unless _confirmed[key] == kind
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# `void` so the statement has no completion value. An assignment
|
|
260
|
+
# expression evaluates to what was assigned, and eval_code converts
|
|
261
|
+
# whatever the statement produced back into Ruby, so this used to
|
|
262
|
+
# rebuild the whole value as Ruby objects and drop them.
|
|
263
|
+
#
|
|
264
|
+
# Sloppy, like the declaration above. Strict mode refuses `eval` and
|
|
265
|
+
# `arguments` as assignment targets, which the declaration accepts, so a
|
|
266
|
+
# strict assignment made those two names definable once and broken
|
|
267
|
+
# afterwards.
|
|
268
|
+
_eval_generated("void (#{key} = #{literal});")
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Only here, where a patched String#to_sym can decide nothing except what
|
|
272
|
+
# the caller is handed back.
|
|
273
|
+
key.to_sym
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# memory_usage walks the whole JS heap, and the limit it reports is fixed
|
|
277
|
+
# when the VM is built, so reading it per call made every define cost a
|
|
278
|
+
# heap walk: single-digit microseconds on an empty VM against milliseconds on
|
|
279
|
+
# one holding a few hundred thousand objects, on this machine.
|
|
280
|
+
#
|
|
281
|
+
# JSMemoryUsage.malloc_limit is an int64_t, so a limit at or above 2**63
|
|
282
|
+
# comes back negative. Anything that is not a usable size means no budget,
|
|
283
|
+
# rather than a budget of minus nine quintillion.
|
|
284
|
+
def _js_source_budget
|
|
285
|
+
return @_js_source_budget if defined?(@_js_source_budget)
|
|
286
|
+
|
|
287
|
+
limit = memory_usage[:malloc_limit]
|
|
288
|
+
@_js_source_budget = limit.is_a?(::Integer) && limit.positive? ? limit : nil
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Every evaluation of a generated value literal goes through here, so that
|
|
292
|
+
# the translation below cannot be given to one caller and forgotten on
|
|
293
|
+
# another. It was, once: a fresh name refused with ArgumentError while a
|
|
294
|
+
# redefine of an existing one raised a raw Quickjs::SyntaxError, in the same
|
|
295
|
+
# band, for the same value.
|
|
296
|
+
#
|
|
297
|
+
# The probes and the bare declarations do not come through here, and should
|
|
298
|
+
# not: they are a few lines each, they cannot plausibly overflow a stack the
|
|
299
|
+
# literal did not, and a stack overflow in one of them would not mean the
|
|
300
|
+
# value nests too deeply.
|
|
301
|
+
#
|
|
302
|
+
# The Ruby walk is not the only stack this runs out of. On a Ruby thread,
|
|
303
|
+
# whose stack is a fraction of the main one, QuickJS clamps its own limit to
|
|
304
|
+
# the same headroom and the parser gives out first, where the walk still had
|
|
305
|
+
# room. A caller told to rescue ArgumentError got a Quickjs::SyntaxError
|
|
306
|
+
# instead, and on a worker thread that is fatal.
|
|
307
|
+
#
|
|
308
|
+
# Matching the message is safe in this one place, because the source being
|
|
309
|
+
# parsed is the one this file just built and has no call in it, so a parse
|
|
310
|
+
# error about it comes from the engine and describes our own literal.
|
|
311
|
+
def _eval_generated(source)
|
|
312
|
+
eval_code(source)
|
|
313
|
+
rescue Quickjs::SyntaxError => e
|
|
314
|
+
raise unless e.message.include?("stack overflow")
|
|
315
|
+
|
|
316
|
+
raise ::ArgumentError, "value nests too deeply to convert on this thread's stack"
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# The declaration is its own eval so the caller's source is never rewritten.
|
|
320
|
+
# Prepending to it would shift every line number in a backtrace away from the
|
|
321
|
+
# code the caller actually wrote.
|
|
322
|
+
#
|
|
323
|
+
# The registry entry goes in before the eval rather than after it. Nothing
|
|
324
|
+
# can be relied on to run in between: eval_code is a C call, so an exception
|
|
325
|
+
# raised at the first Ruby checkpoint after it returns lands exactly in the
|
|
326
|
+
# gap between a binding the VM now has and the line that would say so.
|
|
327
|
+
# Timeout and Thread#raise are held off by the mask below, but a raise from a
|
|
328
|
+
# trap handler is not, and a server that does `trap("TERM") { raise Shutdown
|
|
329
|
+
# }` reaches it four times in five. That left the binding live and correct in
|
|
330
|
+
# JS, absent from the registry, and reported as a redeclaration nobody wrote.
|
|
331
|
+
#
|
|
332
|
+
# Recording first inverts which way the window can be wrong, and the two
|
|
333
|
+
# branches that act on an existing record ask the VM rather than believing
|
|
334
|
+
# it, so a record that ran ahead of its declaration corrects itself on the
|
|
335
|
+
# next define instead of standing for the life of the VM.
|
|
336
|
+
def _declare(declared, kind, key, literal)
|
|
337
|
+
# Both records as they stood, so an attempt that did not declare anything
|
|
338
|
+
# can put them back. A failed declaration cannot have removed a binding an
|
|
339
|
+
# earlier one made, and clearing the records as though it had made the
|
|
340
|
+
# next define refuse a name whose binding was live the whole time.
|
|
341
|
+
was_declared = declared[key]
|
|
342
|
+
was_confirmed = _confirmed[key]
|
|
343
|
+
# The entry goes in before the eval: nothing can be relied on to run
|
|
344
|
+
# between the VM having the binding and the line that would say so.
|
|
345
|
+
declared[key] = kind
|
|
346
|
+
# What happened, rather than what was raised. The record standing is the
|
|
347
|
+
# default on purpose: it is written before the eval because nothing can be
|
|
348
|
+
# relied on to run between the VM having the binding and the line that
|
|
349
|
+
# would say so, and an exception that merely arrives during the call, like
|
|
350
|
+
# a trap handler's raise, must not undo that.
|
|
351
|
+
outcome = :stands
|
|
352
|
+
begin
|
|
353
|
+
::Thread.handle_interrupt(::Exception => :never) do
|
|
354
|
+
_eval_generated("#{JS_KEYWORDS.fetch(kind)} #{key} = #{literal};")
|
|
355
|
+
outcome = :declared
|
|
356
|
+
end
|
|
357
|
+
rescue Quickjs::InterruptedError
|
|
358
|
+
outcome = :interrupted if outcome == :stands
|
|
359
|
+
raise
|
|
360
|
+
rescue ::ArgumentError, ::ThreadError, ::NoMemoryError, Quickjs::RuntimeError
|
|
361
|
+
# Raised by the call, and each of them means the declaration did not
|
|
362
|
+
# take: a parse error, the stack overflow the evaluator translates, the
|
|
363
|
+
# VM refusing a second thread, or running out of memory. Listing them is
|
|
364
|
+
# what separates them from an exception that only arrived while the call
|
|
365
|
+
# was running, which says nothing about whether the declaration ran.
|
|
366
|
+
#
|
|
367
|
+
# Only when the outcome is still what it started as. The mask above
|
|
368
|
+
# defers an interrupt until its block exits, which is inside this begin,
|
|
369
|
+
# so an exception of one of these classes delivered from outside arrives
|
|
370
|
+
# having already let the declaration finish. Reading its class alone
|
|
371
|
+
# said the declaration had not taken and rolled the record back for a
|
|
372
|
+
# binding the VM has, which bricks the name: the next define meets the
|
|
373
|
+
# bare redeclaration error that recording before the eval exists to
|
|
374
|
+
# prevent. Reproduced 30/30 with Timeout.timeout(n, ArgumentError); the
|
|
375
|
+
# default Timeout::Error only missed it by not being in this list.
|
|
376
|
+
outcome = :failed if outcome == :stands
|
|
377
|
+
raise
|
|
378
|
+
ensure
|
|
379
|
+
case outcome
|
|
380
|
+
when :declared
|
|
381
|
+
# The declaration ran, so a global lexical binding exists and
|
|
382
|
+
# JavaScript never takes one away. Recorded by form as well as name, so
|
|
383
|
+
# a later define of a different form cannot inherit this one's claim.
|
|
384
|
+
_confirmed[key] = kind
|
|
385
|
+
when :interrupted
|
|
386
|
+
# QuickJS created the binding and the interrupt landed before it was
|
|
387
|
+
# initialized, so a let or const name stays in the temporal dead zone
|
|
388
|
+
# for the life of the VM: reading it, assigning to it and redeclaring
|
|
389
|
+
# it all raise. Nothing here can undo that, so the next define is told
|
|
390
|
+
# why rather than being left to report a redeclaration. A var is left
|
|
391
|
+
# alone, since redeclaring one is legal JS and works.
|
|
392
|
+
declared[key] = kind == :var ? :var : :interrupted
|
|
393
|
+
was_confirmed.nil? ? _confirmed.delete(key) : _confirmed[key] = was_confirmed
|
|
394
|
+
when :failed
|
|
395
|
+
# Nothing was declared, so both records go back to what they were. A
|
|
396
|
+
# failed declaration cannot have removed a binding an earlier one made.
|
|
397
|
+
was_declared.nil? ? declared.delete(key) : declared[key] = was_declared
|
|
398
|
+
was_confirmed.nil? ? _confirmed.delete(key) : _confirmed[key] = was_confirmed
|
|
399
|
+
else
|
|
400
|
+
# Whether it ran is unknown, so the record stands and the confirmation
|
|
401
|
+
# does not. The next define asks the VM rather than believing either.
|
|
402
|
+
was_confirmed.nil? ? _confirmed.delete(key) : _confirmed[key] = was_confirmed
|
|
403
|
+
end
|
|
404
|
+
# Left where the callers can read it, for the same reason the rescue
|
|
405
|
+
# above checks the outcome rather than the class: they wrap this call in
|
|
406
|
+
# rescues that decided what happened from the exception alone.
|
|
407
|
+
@_last_declare_outcome = outcome
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
key.to_sym
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# What the last _declare decided about its own declaration, for the callers
|
|
414
|
+
# that have to tell "the VM refused this" from "something was raised at us
|
|
415
|
+
# while it ran".
|
|
416
|
+
def _declare_refused?
|
|
417
|
+
@_last_declare_outcome == :failed
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def _confirmed
|
|
421
|
+
@_confirmed_variables ||= {}
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Whether the name resolves to a lexical binding: no property of the global
|
|
425
|
+
# object could account for it, and it reads as something. Not decisive in
|
|
426
|
+
# both directions, and does not have to be. A false answer only means the
|
|
427
|
+
# caller pays for the declaration that asks properly.
|
|
428
|
+
def _live_lexical?(key)
|
|
429
|
+
eval_code(<<~JS) == true
|
|
430
|
+
(() => (function () {
|
|
431
|
+
// The real global object, not the `globalThis` property, which is
|
|
432
|
+
// writable and can be pointed at a decoy: this answered about the
|
|
433
|
+
// decoy while the guard below answered about the real one, and a
|
|
434
|
+
// define_let ended up on globalThis. `false` when it is not an object
|
|
435
|
+
// at all, which asks by declaration instead.
|
|
436
|
+
const g = (function () { return this })();
|
|
437
|
+
if (typeof g !== 'object' || g === null) return false;
|
|
438
|
+
return !(#{::JSON.generate(::String.new(key))} in g);
|
|
439
|
+
// The caller's name is read outside this function on purpose. Read
|
|
440
|
+
// inside it, `typeof <name>` resolves anything declared here first, so
|
|
441
|
+
// a caller who named their variable after one of these locals was told
|
|
442
|
+
// it was live on a VM that had never heard of it.
|
|
443
|
+
})() && typeof #{key} !== 'undefined')()
|
|
444
|
+
JS
|
|
445
|
+
rescue Quickjs::InterruptedError
|
|
446
|
+
# The evaluation is over, and saying so is more use than an answer.
|
|
447
|
+
raise
|
|
448
|
+
rescue Quickjs::RuntimeError
|
|
449
|
+
# Not decisive is a safe answer, and the branches that ask fall back to
|
|
450
|
+
# asking by declaration. Anything thrown out of a question the caller did
|
|
451
|
+
# not ask would be theirs to make sense of.
|
|
452
|
+
false
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# A non-configurable property of the global object makes a lexical
|
|
456
|
+
# declaration of that name impossible for the life of the VM: JavaScript
|
|
457
|
+
# refuses `let x;` outright rather than shadowing it. A guest's top-level
|
|
458
|
+
# `var` creates exactly that shape, so this is not exotic.
|
|
459
|
+
#
|
|
460
|
+
# Letting that refusal pass leaves no lexical binding for the assignment to
|
|
461
|
+
# find, and it writes the global instead. A define_let that puts its value
|
|
462
|
+
# on globalThis is the one thing this form promises does not happen, so the
|
|
463
|
+
# caller is told rather than being given a success it did not get.
|
|
464
|
+
#
|
|
465
|
+
# Anything but a clean false refuses, since not being able to ask is not a
|
|
466
|
+
# reason to go ahead.
|
|
467
|
+
def _refuse_restricted_global(key, kind)
|
|
468
|
+
restricted =
|
|
469
|
+
begin
|
|
470
|
+
eval_code(<<~JS)
|
|
471
|
+
(() => {
|
|
472
|
+
const root = (function () { return this })();
|
|
473
|
+
const d = Object.getOwnPropertyDescriptor(root, #{::JSON.generate(::String.new(key))});
|
|
474
|
+
if (!d) return false;
|
|
475
|
+
Object.setPrototypeOf(d, null);
|
|
476
|
+
return !d.configurable;
|
|
477
|
+
})()
|
|
478
|
+
JS
|
|
479
|
+
rescue Quickjs::InterruptedError
|
|
480
|
+
raise
|
|
481
|
+
rescue Quickjs::RuntimeError => err
|
|
482
|
+
# As above: the engine failing is not a fact about this name.
|
|
483
|
+
raise if err.js_name.nil? || err.js_name == "InternalError"
|
|
484
|
+
|
|
485
|
+
true
|
|
486
|
+
end
|
|
487
|
+
return if restricted == false
|
|
488
|
+
|
|
489
|
+
raise ::ArgumentError,
|
|
490
|
+
"cannot define #{kind} #{key}: globalThis.#{key} cannot be configured away, so JavaScript " \
|
|
491
|
+
"will not accept a #{kind} of that name and the value would be written to that global instead"
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# `var` is the only form that lands on `globalThis`, so it is the only one a
|
|
495
|
+
# property already sitting there can intercept. An accessor takes the value
|
|
496
|
+
# in its setter and hands JS back whatever its getter likes; a non-writable
|
|
497
|
+
# data property swallows the assignment silently, because a declaration eval
|
|
498
|
+
# is sloppy mode. Either way the caller was told the define succeeded when it
|
|
499
|
+
# did not happen.
|
|
500
|
+
#
|
|
501
|
+
# This refuses a global that is already unusable. It is not a security
|
|
502
|
+
# boundary, and is deliberately not described as one: it asks the VM through
|
|
503
|
+
# JS, and code that has already run there can replace
|
|
504
|
+
# Object.getOwnPropertyDescriptor as easily as it can install the accessor. A
|
|
505
|
+
# VM that has evaluated untrusted JavaScript owns its own environment, and
|
|
506
|
+
# there is no way to hand a value into it unobserved. Define before running
|
|
507
|
+
# code you do not control.
|
|
508
|
+
def _refuse_unusable_global(key)
|
|
509
|
+
state = eval_code(<<~JS)
|
|
510
|
+
(() => {
|
|
511
|
+
// The real global object, not the `globalThis` property, which is
|
|
512
|
+
// writable and can be pointed at a decoy. That is reachable from Ruby
|
|
513
|
+
// alone with define_var(:globalThis, 1): the lookup below would then
|
|
514
|
+
// answer about the decoy and report a clean global while the value
|
|
515
|
+
// went to whatever the real one still had sitting there. A sloppy
|
|
516
|
+
// function called with no receiver gets the global object itself.
|
|
517
|
+
const root = (function () { return this })();
|
|
518
|
+
if (typeof root !== 'object' || root === null) return 'broken';
|
|
519
|
+
const d = Object.getOwnPropertyDescriptor(root, #{::JSON.generate(::String.new(key))});
|
|
520
|
+
// Extensibility only decides whether a property can be added, so it
|
|
521
|
+
// is asked about only when there is none. A sealed global with the
|
|
522
|
+
// property already on it takes the declaration quite happily, and
|
|
523
|
+
// refusing that told the caller something untrue about their VM.
|
|
524
|
+
if (!d) return Object.isExtensible(root) ? 'absent' : 'sealed';
|
|
525
|
+
// Own keys only, and read without asking anything that can be
|
|
526
|
+
// replaced. Reading `get` off a descriptor walks the prototype chain,
|
|
527
|
+
// so a stray Object.prototype.get made every var look like an
|
|
528
|
+
// accessor; going through hasOwnProperty instead only moved the
|
|
529
|
+
// problem, since that is a method a guest can replace, and `writable`
|
|
530
|
+
// was still being read through the chain. Two gadgets together made
|
|
531
|
+
// an accessor answer `writable` and its setter took the value.
|
|
532
|
+
// Cutting the descriptor loose from its prototype settles all of it.
|
|
533
|
+
Object.setPrototypeOf(d, null);
|
|
534
|
+
if ('get' in d || 'set' in d) return 'accessor';
|
|
535
|
+
return d.writable ? 'writable' : 'readonly';
|
|
536
|
+
})()
|
|
537
|
+
JS
|
|
538
|
+
|
|
539
|
+
case state
|
|
540
|
+
when 'absent', 'writable'
|
|
541
|
+
nil
|
|
542
|
+
when 'accessor'
|
|
543
|
+
raise ::ArgumentError,
|
|
544
|
+
"cannot define var #{key}: globalThis.#{key} is an accessor, so the value would go to its " \
|
|
545
|
+
"setter rather than to the variable"
|
|
546
|
+
when 'readonly'
|
|
547
|
+
raise ::ArgumentError,
|
|
548
|
+
"cannot define var #{key}: globalThis.#{key} is not writable, so the assignment would be discarded"
|
|
549
|
+
when 'sealed'
|
|
550
|
+
raise ::ArgumentError,
|
|
551
|
+
"cannot define var #{key}: this VM's globalThis is not extensible, so the declaration " \
|
|
552
|
+
"would be refused"
|
|
553
|
+
else
|
|
554
|
+
# The probe reads globalThis and Object, so a caller who has already
|
|
555
|
+
# replaced either of those has taken the check away from itself. That
|
|
556
|
+
# is reachable from Ruby alone: define_var(:globalThis, 1) makes every
|
|
557
|
+
# later probe answer for a Number and report a clean global, and
|
|
558
|
+
# define_var(:Object, 1) makes it throw. Refusing an answer we do not
|
|
559
|
+
# recognise turns both into a refusal rather than into a define that
|
|
560
|
+
# reports success it did not have.
|
|
561
|
+
raise ::ArgumentError,
|
|
562
|
+
"cannot define var #{key}: this VM cannot be asked about its globals, so whether the " \
|
|
563
|
+
"assignment would take effect is unknown. Replacing globalThis or Object does this"
|
|
564
|
+
end
|
|
565
|
+
rescue Quickjs::InterruptedError
|
|
566
|
+
raise
|
|
567
|
+
rescue Quickjs::RuntimeError => e
|
|
568
|
+
# Only an error the guest can have thrown becomes an ArgumentError about
|
|
569
|
+
# their globals. The engine names its own failures InternalError, which is
|
|
570
|
+
# out of memory and stack overflow, and names nothing at all once the VM
|
|
571
|
+
# has latched as poisoned or been disposed.
|
|
572
|
+
#
|
|
573
|
+
# Two earlier versions of this got the split wrong. Asking whether the
|
|
574
|
+
# error carried a name missed the first out-of-memory, which carries one.
|
|
575
|
+
# Asking whether it arrived as a subclass missed everything the guest
|
|
576
|
+
# throws that is not one of the seven names the C layer maps, `new Error`
|
|
577
|
+
# among them, which is the most ordinary throw there is.
|
|
578
|
+
#
|
|
579
|
+
# A guest can name its own error InternalError and be re-raised rather
|
|
580
|
+
# than explained. That is the safe direction: the caller sees what was
|
|
581
|
+
# actually thrown.
|
|
582
|
+
raise if e.js_name.nil? || e.js_name == "InternalError"
|
|
583
|
+
|
|
584
|
+
raise ::ArgumentError,
|
|
585
|
+
"cannot define var #{key}: asking this VM about its globals failed with #{e.class}. " \
|
|
586
|
+
"Replacing Object, or any of the methods this asks it for, does this"
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
def _validate_variable_name(name, budget)
|
|
590
|
+
# `===` rather than `name.is_a?`, which the object answers for itself.
|
|
591
|
+
unless ::String === name || ::Symbol === name
|
|
592
|
+
raise ::TypeError, "variable's name should be a Symbol or a String, got #{name.class}"
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
# A copy of the bytes, for the same reason the value path takes one.
|
|
596
|
+
# `to_s` and `to_sym` are both dispatched on the caller's object, so a
|
|
597
|
+
# String subclass could satisfy the pattern below with one and hand a
|
|
598
|
+
# different name to the interpolation with the other. Everything after
|
|
599
|
+
# this line works on a plain String that answers only for itself.
|
|
600
|
+
str = ::String === name ? ::String.new(name) : ::String.new(name.to_s)
|
|
601
|
+
|
|
602
|
+
# Measured against the same budget as a value, since it goes into the same
|
|
603
|
+
# source. Without this the one piece of the generated JavaScript that was
|
|
604
|
+
# not bounded was the name: an oversized value was refused and left the VM
|
|
605
|
+
# healthy, and an oversized name reached the eval and took the VM with it.
|
|
606
|
+
if budget && str.bytesize > budget
|
|
607
|
+
raise ::ArgumentError,
|
|
608
|
+
"the variable's name is longer than #{budget} bytes, which is this VM's memory_limit"
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
unless NAME_PATTERN.match?(str)
|
|
612
|
+
raise ::ArgumentError, "#{str.inspect} is not a valid JavaScript identifier"
|
|
613
|
+
end
|
|
614
|
+
if RESERVED_WORDS.include?(str)
|
|
615
|
+
raise ::ArgumentError, "#{str.inspect} is a reserved word in JavaScript"
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
str
|
|
619
|
+
end
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
# These build the JavaScript source for a value. They are an implementation
|
|
623
|
+
# detail of the define_* methods and not something to call directly, so they
|
|
624
|
+
# live behind a private constant rather than sitting on Quickjs itself.
|
|
625
|
+
module ValueLiteral
|
|
626
|
+
|
|
627
|
+
# Serialized here rather than through the C converter, which falls back to
|
|
628
|
+
# `#inspect` for anything it doesn't recognize and would silently turn a
|
|
629
|
+
# Time into a string. Defining a variable is an input path, so an
|
|
630
|
+
# unsupported value is a caller mistake worth raising on.
|
|
631
|
+
def self._js_literal(value, seen = nil, budget = nil)
|
|
632
|
+
case value
|
|
633
|
+
when nil then "null"
|
|
634
|
+
when true then "true"
|
|
635
|
+
when false then "false"
|
|
636
|
+
# Neither to_json nor to_s: both are dispatched on the caller's object, so
|
|
637
|
+
# a subclass overriding either would decide what goes into the source, and
|
|
638
|
+
# a gem replacing String#to_json process-wide needs no hostile value at all.
|
|
639
|
+
# String.new copies the bytes without asking the object anything, and
|
|
640
|
+
# JSON.generate does the escaping. That is not out of a monkey patch's
|
|
641
|
+
# reach, and nothing in Ruby is: JSON.generate, String#initialize,
|
|
642
|
+
# Array#join and format are all replaceable, and a process that has done
|
|
643
|
+
# that has bigger problems than this method. What holds is narrower and
|
|
644
|
+
# is the part that matters: no method dispatched on the caller's value
|
|
645
|
+
# decides these bytes.
|
|
646
|
+
when ::String then _js_string_literal(value, budget)
|
|
647
|
+
when ::Integer then _js_integer_literal(value, budget)
|
|
648
|
+
when ::Float then _js_float_literal(value)
|
|
649
|
+
when ::Symbol then _js_symbol_literal(value, budget)
|
|
650
|
+
when ::Array, ::Hash then _js_container_literal(value, seen, budget)
|
|
651
|
+
else
|
|
652
|
+
raise ::TypeError, "#{value.class} cannot be converted to a JavaScript value"
|
|
653
|
+
end
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
# Measured before the copy rather than after the literal exists. The
|
|
657
|
+
# per-container check bounds a structure that expands as it nests, but a
|
|
658
|
+
# single enormous String is one value, so nothing checked it until it had
|
|
659
|
+
# been copied once and escaped once. A caller holding 300MB got 900MB.
|
|
660
|
+
#
|
|
661
|
+
# bytesize through an unbound method, since a subclass answering that one
|
|
662
|
+
# low is exactly how a value would get past a cheap check into an
|
|
663
|
+
# expensive copy.
|
|
664
|
+
BYTESIZE = ::String.instance_method(:bytesize)
|
|
665
|
+
|
|
666
|
+
# The same move for the two numeric conversions. format looked like it read
|
|
667
|
+
# the number rather than asking it, but Kernel#format is itself replaceable
|
|
668
|
+
# and fails open when it is: it answers with whatever the patch returns and
|
|
669
|
+
# that goes straight into the source. An unbound Integer#to_s and Float#to_s
|
|
670
|
+
# cannot be redirected after they are taken here, and they write the
|
|
671
|
+
# shortest form that reads back as the same number rather than the widest.
|
|
672
|
+
INTEGER_TO_S = ::Integer.instance_method(:to_s)
|
|
673
|
+
BIT_LENGTH = ::Integer.instance_method(:bit_length)
|
|
674
|
+
FLOAT_TO_S = ::Float.instance_method(:to_s)
|
|
675
|
+
|
|
676
|
+
def self._js_string_literal(value, budget)
|
|
677
|
+
_over_budget!(budget) if budget && BYTESIZE.bind_call(value) > budget
|
|
678
|
+
|
|
679
|
+
::JSON.generate(::String.new(value))
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
# A bignum costs its digits to render, and a decimal digit costs about 3.32
|
|
683
|
+
# bits, so four bits per digit is more than any number needs: anything past
|
|
684
|
+
# four times the budget in bits has more digits than the budget allows. Deliberately loose: this refuses only what is
|
|
685
|
+
# certainly too large, and the finished literal is measured as before.
|
|
686
|
+
def self._js_integer_literal(value, budget)
|
|
687
|
+
_over_budget!(budget) if budget && BIT_LENGTH.bind_call(value) / 4 > budget
|
|
688
|
+
|
|
689
|
+
INTEGER_TO_S.bind_call(value)
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
# `undefined`, `NaN` and `Infinity` are not literals in JavaScript, they are
|
|
693
|
+
# properties of the global object, and QuickJS lets a guest redefine them
|
|
694
|
+
# with defineProperty even though the spec says it should not. Writing them
|
|
695
|
+
# by name handed the guest the value: a const of Value::UNDEFINED came back
|
|
696
|
+
# as whatever the guest had put there, at any depth, and came back to Ruby
|
|
697
|
+
# that way too. That is the one thing const and let are supposed to be safe
|
|
698
|
+
# from, since neither touches globalThis.
|
|
699
|
+
#
|
|
700
|
+
# The declaration forms are refused by QuickJS, which is what made this look
|
|
701
|
+
# settled: `let NaN = 5` is a SyntaxError. defineProperty is not.
|
|
702
|
+
#
|
|
703
|
+
# Operators and numeric literals resolve through no scope chain, so nothing
|
|
704
|
+
# the guest can reach decides what these mean.
|
|
705
|
+
JS_SPELLED_FLOATS = {
|
|
706
|
+
"Infinity" => "(1/0)",
|
|
707
|
+
"-Infinity" => "(-1/0)",
|
|
708
|
+
"NaN" => "(0/0)",
|
|
709
|
+
}.freeze
|
|
710
|
+
|
|
711
|
+
# nan?, infinite? and positive? were each dispatched on the caller's Float
|
|
712
|
+
# to pick between three fixed strings, which is a branch the value decided.
|
|
713
|
+
# They were also unnecessary: Float#to_s already picks those three out.
|
|
714
|
+
def self._js_float_literal(value)
|
|
715
|
+
spelled = FLOAT_TO_S.bind_call(value)
|
|
716
|
+
JS_SPELLED_FLOATS.fetch(spelled, spelled)
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
# `Quickjs::Value::UNDEFINED` and `NAN` are plain Symbols, so they have to
|
|
720
|
+
# be recognized before the generic Symbol case. Every other Symbol becomes
|
|
721
|
+
# a string, matching how the C converter treats Symbols returned from a
|
|
722
|
+
# `define_function` block.
|
|
723
|
+
# Spelled with operators for the same reason as the floats above.
|
|
724
|
+
JS_SPELLED_SYMBOLS = { Value::UNDEFINED => "(void 0)", Value::NAN => "(0/0)" }
|
|
725
|
+
.compare_by_identity.freeze
|
|
726
|
+
|
|
727
|
+
def self._js_symbol_literal(value, budget = nil)
|
|
728
|
+
# Looked up by identity rather than compared with ==, which a Symbol
|
|
729
|
+
# answers for itself and could use to make any Symbol undefined.
|
|
730
|
+
spelled = JS_SPELLED_SYMBOLS[value]
|
|
731
|
+
return spelled if spelled
|
|
732
|
+
|
|
733
|
+
# A Symbol has no bytes to measure until to_s makes some, so the copy is
|
|
734
|
+
# unavoidable. Measuring it before escaping still skips the expensive
|
|
735
|
+
# half, since escaping walks and rewrites every byte.
|
|
736
|
+
copy = ::String.new(value.to_s)
|
|
737
|
+
_over_budget!(budget) if budget && copy.bytesize > budget
|
|
738
|
+
|
|
739
|
+
::JSON.generate(copy)
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
# `seen` is threaded through by identity: a structure that contains itself
|
|
743
|
+
# would otherwise recurse until the stack gives out. Entries are removed on
|
|
744
|
+
# the way out so a value appearing twice as siblings stays legal.
|
|
745
|
+
#
|
|
746
|
+
# compare_by_identity rather than keying on value.object_id, which the
|
|
747
|
+
# value answers for itself. A Hash subclass returning a constant object_id
|
|
748
|
+
# had every nested instance of it called circular; one returning a fresh
|
|
749
|
+
# id each time was never seen twice and walked until the stack guard
|
|
750
|
+
# caught it.
|
|
751
|
+
def self._js_container_literal(value, seen, budget = nil)
|
|
752
|
+
seen ||= {}.compare_by_identity
|
|
753
|
+
if seen.key?(value)
|
|
754
|
+
raise ::ArgumentError, "#{value.class} contains a circular reference and cannot be converted"
|
|
755
|
+
end
|
|
756
|
+
seen[value] = true
|
|
757
|
+
|
|
758
|
+
begin
|
|
759
|
+
# Appended to one String rather than collected and joined. The pieces
|
|
760
|
+
# were retained until the end, and a Ruby String costs about forty
|
|
761
|
+
# bytes of object for the two bytes of budget a small element spends,
|
|
762
|
+
# so a wide container of tiny values reached twenty-seven times
|
|
763
|
+
# memory_limit in host memory before the check fired. Appending keeps
|
|
764
|
+
# one piece alive at a time.
|
|
765
|
+
used = 2
|
|
766
|
+
separator = ""
|
|
767
|
+
if ::Array === value
|
|
768
|
+
out = +"["
|
|
769
|
+
value.each do |v|
|
|
770
|
+
piece = _js_literal(v, seen, budget)
|
|
771
|
+
out << separator << piece
|
|
772
|
+
separator = ","
|
|
773
|
+
used += piece.bytesize + 1
|
|
774
|
+
_over_budget!(budget) if budget && used > budget
|
|
775
|
+
end
|
|
776
|
+
out << "]"
|
|
777
|
+
else
|
|
778
|
+
out = +"{"
|
|
779
|
+
value.each do |k, v|
|
|
780
|
+
# Appended in three pieces rather than interpolated into one. The
|
|
781
|
+
# interpolation built a second full copy of the value's literal
|
|
782
|
+
# beside the one it had just made, so a deeply nested Hash held both
|
|
783
|
+
# at every level and cost about half again what the Array shape
|
|
784
|
+
# costs, which is the opposite of what appending is here for.
|
|
785
|
+
key_piece = _js_object_key(k, budget)
|
|
786
|
+
value_piece = _js_literal(v, seen, budget)
|
|
787
|
+
out << separator << key_piece << ":" << value_piece
|
|
788
|
+
separator = ","
|
|
789
|
+
used += key_piece.bytesize + value_piece.bytesize + 2
|
|
790
|
+
_over_budget!(budget) if budget && used > budget
|
|
791
|
+
end
|
|
792
|
+
out << "}"
|
|
793
|
+
end
|
|
794
|
+
ensure
|
|
795
|
+
seen.delete(value)
|
|
796
|
+
end
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
# A structure that reaches the same object twice is written out twice, so a
|
|
800
|
+
# graph with shared branches expands rather than being shared: each level
|
|
801
|
+
# that references the level below twice doubles the output, and twenty-five
|
|
802
|
+
# of those is a third of a gigabyte from a handful of Ruby objects. YAML
|
|
803
|
+
# aliases and Marshal round-trips produce exactly that shape without anyone
|
|
804
|
+
# meaning to.
|
|
805
|
+
#
|
|
806
|
+
# The bound is the VM's own memory_limit rather than a number invented here.
|
|
807
|
+
# A source larger than the whole JS heap budget cannot be evaluated by that
|
|
808
|
+
# VM under any circumstances, so this refuses work that provably cannot
|
|
809
|
+
# succeed, and the message names the option to change. It is a ceiling and
|
|
810
|
+
# not a prediction: object-heavy literals cost many times their source size
|
|
811
|
+
# once parsed, so a source well under the limit can still exhaust the VM.
|
|
812
|
+
#
|
|
813
|
+
# Counted as each element is produced rather than after a container is
|
|
814
|
+
# built, which is the difference between bounding the source and bounding
|
|
815
|
+
# the host. Checking a finished container still lets `map` and `join`
|
|
816
|
+
# materialise the whole thing first, so a single wide container walks past
|
|
817
|
+
# any budget: three hundred references to one twenty-thousand-element array
|
|
818
|
+
# reached 103MB of Ruby against a 4MB limit that never fired in time.
|
|
819
|
+
#
|
|
820
|
+
# It is still a multiple of the limit rather than the limit. Nesting builds
|
|
821
|
+
# each inner literal in full before the level above can check it, so the
|
|
822
|
+
# doubling shape measured about eight times a 4MB limit and four and a half
|
|
823
|
+
# times a 32MB one, falling as the limit rises, against twice the limit for
|
|
824
|
+
# the wide shape above. Numbers from this machine, and the two that were
|
|
825
|
+
# quoted here before both went stale, so treat them as an order of
|
|
826
|
+
# magnitude rather than a promise. What it is no longer is proportional to
|
|
827
|
+
# the structure: the same value with no check at all reaches a third of a
|
|
828
|
+
# gigabyte at twenty-five levels and ten gigabytes at thirty.
|
|
829
|
+
def self._within_budget(literal, budget)
|
|
830
|
+
return literal if budget.nil? || literal.bytesize <= budget
|
|
831
|
+
|
|
832
|
+
_over_budget!(budget)
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
def self._over_budget!(budget)
|
|
836
|
+
raise ::ArgumentError,
|
|
837
|
+
"value serializes to more than #{budget} bytes of JavaScript, which is this VM's memory_limit; " \
|
|
838
|
+
"it cannot be evaluated. Shared sub-structures are written out once per occurrence, so a value " \
|
|
839
|
+
"with repeated branches expands"
|
|
840
|
+
end
|
|
841
|
+
|
|
842
|
+
# JS object keys are strings regardless, so an Integer key is unambiguous.
|
|
843
|
+
# Anything else would rely on `#to_s` producing something meaningful, which
|
|
844
|
+
# is the silent coercion this converter exists to avoid.
|
|
845
|
+
# `budget` for the same reason the value path takes one: a key is a value
|
|
846
|
+
# the caller supplies, and it was being copied and escaped before anything
|
|
847
|
+
# looked at its size. A 300MB String cost nothing as a value and 900MB as a
|
|
848
|
+
# key.
|
|
849
|
+
def self._js_object_key(key, budget = nil)
|
|
850
|
+
literal = case key
|
|
851
|
+
when ::String
|
|
852
|
+
_over_budget!(budget) if budget && BYTESIZE.bind_call(key) > budget
|
|
853
|
+
::JSON.generate(::String.new(key))
|
|
854
|
+
when ::Integer
|
|
855
|
+
# Read, not asked to describe itself, as on the value path.
|
|
856
|
+
_over_budget!(budget) if budget && BIT_LENGTH.bind_call(key) / 4 > budget
|
|
857
|
+
::JSON.generate(INTEGER_TO_S.bind_call(key))
|
|
858
|
+
when ::Symbol
|
|
859
|
+
# A Symbol has no bytes to copy, so this one asks. What it
|
|
860
|
+
# answers is still escaped here, so the worst a patched
|
|
861
|
+
# Symbol#to_s buys is a different key, not a different
|
|
862
|
+
# meaning.
|
|
863
|
+
copy = ::String.new(key.to_s)
|
|
864
|
+
_over_budget!(budget) if budget && copy.bytesize > budget
|
|
865
|
+
::JSON.generate(copy)
|
|
866
|
+
else
|
|
867
|
+
raise ::TypeError, "#{key.class} cannot be used as a JavaScript object key"
|
|
868
|
+
end
|
|
869
|
+
|
|
870
|
+
# In an object literal `__proto__: v` sets the prototype rather than
|
|
871
|
+
# defining a property, and quoting the key does not opt out of that. A Hash
|
|
872
|
+
# key of that name would otherwise vanish: the entry becomes the object's
|
|
873
|
+
# prototype, `Object.keys` never lists it, and the value round-trips back
|
|
874
|
+
# from JS as if it had never been sent.
|
|
875
|
+
#
|
|
876
|
+
# That is a difference between what Ruby holds and what JS then sees, which
|
|
877
|
+
# is the shape a host-side check can be walked past: a request body with a
|
|
878
|
+
# nested "__proto__" passes inspection in Ruby, where it is an ordinary key
|
|
879
|
+
# with a Hash under it, and arrives in JS as inherited members of the object
|
|
880
|
+
# the host handed over.
|
|
881
|
+
#
|
|
882
|
+
# A computed key is not the special form, so this emits the property that
|
|
883
|
+
# was asked for. Only this one name needs it; nothing else in an object
|
|
884
|
+
# literal means anything other than itself. JS written by hand is untouched:
|
|
885
|
+
# a guest writing `{__proto__: x}` still sets a prototype, as it should.
|
|
886
|
+
literal == '"__proto__"' ? "[#{literal}]" : literal
|
|
887
|
+
end
|
|
888
|
+
end
|
|
889
|
+
private_constant :ValueLiteral
|
|
890
|
+
end
|