homura-runtime 0.1.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.
@@ -0,0 +1,653 @@
1
+ # backtick_javascript: true
2
+ # Runtime patches to extend Opal's corelib with methods required by
3
+ # real-world gems (Sinatra, Rack, Mustermann, ...) that are missing
4
+ # from upstream opal 1.8.3.rc1. Each patch is kept strictly additive:
5
+ # it only installs a method if the method does not already exist.
6
+ #
7
+ # We prefer to patch Opal here (in the adapter layer) rather than
8
+ # modifying the vendored gems, so that vendored code keeps the same
9
+ # shape as its upstream counterparts. If a patch here turns out to
10
+ # fix something upstream Opal is missing, it should be turned into
11
+ # a PR against github.com/opal/opal.
12
+
13
+ # -----------------------------------------------------------------
14
+ # Module#deprecate_constant
15
+ # -----------------------------------------------------------------
16
+ # CRuby 2.6+ ships `Module#deprecate_constant(*names)` which marks the
17
+ # listed constants so that accessing them prints a deprecation warning.
18
+ # Opal 1.8.3.rc1 does not implement this, so any gem that calls
19
+ # `deprecate_constant :FOO` at module-load time aborts with a
20
+ # method_missing error. Real-world examples that hit this in Phase 2:
21
+ # * rack/multipart/parser.rb -> deprecate_constant :CHARSET
22
+ #
23
+ # Ruby's behaviour is "warn on read, still return the value", which we
24
+ # approximate here as a no-op (Opal has no const-access hook to warn
25
+ # from, and a warning-only behaviour does not affect program output).
26
+ class Module
27
+ unless private_method_defined?(:deprecate_constant) || method_defined?(:deprecate_constant)
28
+ def deprecate_constant(*_names)
29
+ self
30
+ end
31
+ private :deprecate_constant
32
+ end
33
+ end
34
+
35
+ # -----------------------------------------------------------------
36
+ # Encoding::* constants that Opal corelib does not register
37
+ # -----------------------------------------------------------------
38
+ # Opal ships a handful of encodings in opal/corelib/string/encoding.rb
39
+ # (UTF-8, UTF-16{LE,BE}, UTF-32{LE,BE}, ASCII-8BIT, ISO-8859-1, US-ASCII).
40
+ # Real-world gems reference many more legacy encodings that Opal never
41
+ # declares. When such a constant appears at class-load time (e.g. in a
42
+ # constant hash literal inside rack/multipart/parser.rb), Opal raises
43
+ # `NameError: uninitialized constant Encoding::FOO` and aborts the
44
+ # whole require chain.
45
+ #
46
+ # Real transcoding is out of scope for Phase 2 — Workers do not need to
47
+ # convert ISO-2022-JP for hello-world. We install each missing constant
48
+ # as an alias of an encoding Opal already ships so that the constant
49
+ # reference succeeds. If a gem actually calls .encode onto one of these
50
+ # Opal will raise a clear error at the call site, which is what we want.
51
+ # -----------------------------------------------------------------
52
+ # Module#const_defined? / Module#const_get — qualified name support
53
+ # -----------------------------------------------------------------
54
+ # CRuby's `Module#const_defined?` and `Module#const_get` both accept
55
+ # "Foo::Bar::Baz" style qualified names. Opal 1.8.3.rc1 supports
56
+ # qualified names in `const_get` but NOT in `const_defined?`, so any
57
+ # call like `Object.const_defined?('Mustermann::AST::Node::Root')`
58
+ # returns false (or raises NameError) even when the constant exists,
59
+ # and Mustermann's `Node[:root]` factory falls through to `nil`.
60
+ #
61
+ # Patch: split qualified names in `const_defined?` and walk the chain
62
+ # exactly like `const_get` does.
63
+ class Module
64
+ unless instance_method(:const_defined?).source_location.nil?
65
+ # pass — we monkey-patch regardless
66
+ end
67
+
68
+ alias_method :__homura_const_defined_simple, :const_defined?
69
+
70
+ def const_defined?(name, inherit = true)
71
+ name_str = name.to_s
72
+ if name_str.include?('::')
73
+ parts = name_str.split('::')
74
+ parts.shift if parts.first.empty? # leading "::Foo::Bar"
75
+ current = self
76
+ parts.each do |part|
77
+ return false unless current.__homura_const_defined_simple(part, inherit)
78
+ current = current.const_get(part, inherit)
79
+ return false unless current.is_a?(Module)
80
+ end
81
+ true
82
+ else
83
+ __homura_const_defined_simple(name, inherit)
84
+ end
85
+ end
86
+ end
87
+
88
+ # -----------------------------------------------------------------
89
+ # Global defaults that Opal does not initialise
90
+ # -----------------------------------------------------------------
91
+ # CRuby sets `$0` to the program name from argv[0]. Opal leaves it
92
+ # as nil, which breaks gems that call `File.expand_path($0)` at class-
93
+ # body time (sinatra/main.rb: `proc { File.expand_path($0) }`).
94
+ # Install a harmless default string.
95
+ $0 ||= '(homura)'
96
+ $PROGRAM_NAME ||= $0
97
+
98
+ # (Previously this file force-set APP_ENV/RACK_ENV to 'production' to
99
+ # keep Rack::ShowExceptions out of the way — its ERB renderer uses
100
+ # `binding.eval` which lands on `new Function($code)`, forbidden on
101
+ # Workers. The real fix is now in `vendor/rack/show_exceptions.rb`
102
+ # where `#pretty` builds the HTML directly, so we no longer need to
103
+ # hide development mode. Users who want production settings should
104
+ # set `APP_ENV=production` themselves, same as on any Rack server.)
105
+
106
+ # Load Opal's stdlib Forwardable BEFORE patching it, so our overrides
107
+ # are applied last and are not clobbered when a vendored gem requires
108
+ # 'forwardable' transitively.
109
+ require 'forwardable'
110
+
111
+ # -----------------------------------------------------------------
112
+ # (removed) Debug method_missing logger — was used while iterating
113
+ # through Phase 2 init-time issues. Permanent fix for the root cause
114
+ # (Opal `@prototype` collision) landed in vendor/opal-gem/. Keeping
115
+ # method_missing unpatched restores the fast path for real requests.
116
+ # -----------------------------------------------------------------
117
+
118
+ # -----------------------------------------------------------------
119
+ # Forwardable#def_instance_delegator — support for expression accessors
120
+ # -----------------------------------------------------------------
121
+ # CRuby's Forwardable evaluates the `accessor` argument as a Ruby
122
+ # expression when it is neither an ivar (`@foo`) nor a plain method
123
+ # name — so `def_delegator 'self.class', :foo` delegates to the current
124
+ # class's #foo method. Opal's simplified Forwardable (stdlib/forwardable.rb)
125
+ # just treats the accessor literally and calls `__send__('self.class')`,
126
+ # which raises method_missing.
127
+ #
128
+ # Mustermann's AST::Pattern relies on this exact CRuby behaviour:
129
+ #
130
+ # instance_delegate %i[parser compiler ...] => 'self.class'
131
+ #
132
+ # so without this patch the first Mustermann::Pattern#compile call from
133
+ # Sinatra::Base#route aborts the whole require chain. The fix below
134
+ # re-implements def_instance_delegator / def_single_delegator so that
135
+ # non-ivar accessors that look like Ruby expressions go through
136
+ # `instance_eval` / `class_eval` instead of `__send__`.
137
+ # homura patch: Cloudflare Workers disallows `new Function($code)` /
138
+ # `eval($code)` (Workers' "Code generation from strings disallowed" rule).
139
+ # Opal's `instance_eval(String)` compiles to a `new Function($code)` call,
140
+ # so any Forwardable delegation with an *expression* accessor (Mustermann's
141
+ # `instance_delegate %i[parser compiler] => 'self.class'` is the real-world
142
+ # trigger) crashes on Workers at first dispatch.
143
+ #
144
+ # The helper below walks a small subset of dot-separated accessor
145
+ # expressions — `self`, `self.class`, `@ivar`, `@ivar.method`, plain
146
+ # `method_name`, `method_name.other_method` — without going through
147
+ # `instance_eval`. Mustermann (and the Ruby stdlib itself) only uses
148
+ # identifiers from that subset, so we never need the full Ruby parser.
149
+ module ForwardableAccessor
150
+ module_function
151
+
152
+ def resolve(instance, expr)
153
+ expr = expr.to_s
154
+ current = instance
155
+ expr.split('.').each do |part|
156
+ current = if part == 'self'
157
+ instance
158
+ elsif part.start_with?('@')
159
+ instance.instance_variable_get(part)
160
+ else
161
+ current.__send__(part)
162
+ end
163
+ end
164
+ current
165
+ end
166
+ end
167
+
168
+ module Forwardable
169
+ remove_method :def_instance_delegator if method_defined?(:def_instance_delegator)
170
+
171
+ def def_instance_delegator(accessor, method, ali = method)
172
+ accessor_str = accessor.to_s
173
+ if accessor_str.start_with?('@') && !accessor_str.include?('.')
174
+ define_method ali do |*args, &block|
175
+ instance_variable_get(accessor_str).__send__(method, *args, &block)
176
+ end
177
+ elsif accessor_str =~ /\A[A-Za-z_]\w*\z/
178
+ # Plain identifier (method name) — call via __send__ as before.
179
+ define_method ali do |*args, &block|
180
+ __send__(accessor_str).__send__(method, *args, &block)
181
+ end
182
+ else
183
+ # Dot-path expression like 'self.class'. Resolve without eval.
184
+ define_method ali do |*args, &block|
185
+ ForwardableAccessor.resolve(self, accessor_str).__send__(method, *args, &block)
186
+ end
187
+ end
188
+ end
189
+ end
190
+
191
+ module SingleForwardable
192
+ remove_method :def_single_delegator if method_defined?(:def_single_delegator)
193
+
194
+ def def_single_delegator(accessor, method, ali = method)
195
+ accessor_str = accessor.to_s
196
+ if accessor_str.start_with?('@') && !accessor_str.include?('.')
197
+ define_singleton_method ali do |*args, &block|
198
+ instance_variable_get(accessor_str).__send__(method, *args, &block)
199
+ end
200
+ elsif accessor_str =~ /\A[A-Za-z_]\w*\z/
201
+ define_singleton_method ali do |*args, &block|
202
+ __send__(accessor_str).__send__(method, *args, &block)
203
+ end
204
+ else
205
+ define_singleton_method ali do |*args, &block|
206
+ ForwardableAccessor.resolve(self, accessor_str).__send__(method, *args, &block)
207
+ end
208
+ end
209
+ end
210
+ end
211
+
212
+ # -----------------------------------------------------------------
213
+ # URI::DEFAULT_PARSER — CGI-backed stand-in
214
+ # -----------------------------------------------------------------
215
+ # Opal ships a tiny `uri.rb` that does not define RFC2396_PARSER or
216
+ # DEFAULT_PARSER. Multiple gems reference URI::DEFAULT_PARSER at
217
+ # method default-value time (eager on first call) or at constant
218
+ # lookup time:
219
+ # - rack/utils.rb (already handled in vendor patch)
220
+ # - mustermann/ast/translator.rb line 121: def escape(char, parser: URI::DEFAULT_PARSER, ...)
221
+ # - mustermann/pattern.rb line 12: @@uri ||= URI::Parser.new
222
+ #
223
+ # We install a module-shaped URI::DEFAULT_PARSER that wraps CGI so
224
+ # that gems that only call escape / unescape / regexp[:UNSAFE] on it
225
+ # continue to work.
226
+ require 'uri' rescue nil
227
+ require 'cgi'
228
+
229
+ module ::URI
230
+ unless const_defined?(:DEFAULT_PARSER)
231
+ DEFAULT_PARSER = Module.new do
232
+ UNSAFE = Regexp.compile('[^\-_.!~*\'()a-zA-Z0-9;/?:@&=+$,\[\]]').freeze
233
+
234
+ def self.regexp
235
+ { UNSAFE: UNSAFE }
236
+ end
237
+
238
+ def self.escape(s, unsafe = UNSAFE)
239
+ CGI.escape(s.to_s)
240
+ end
241
+
242
+ def self.unescape(s)
243
+ CGI.unescape(s.to_s)
244
+ end
245
+ end
246
+ end
247
+
248
+ # CRuby's URI.decode_www_form_component / encode_www_form_component are used
249
+ # by Rack::Utils#unescape / Rack::Utils#escape. Opal's `uri` stdlib omits
250
+ # them. Back them with CGI so that Rack's query-string parser works for
251
+ # any request with a body / query (Sinatra's `request.body.read` path
252
+ # eventually walks through this code).
253
+ #
254
+ # IMPORTANT (Opal): CGI.unescape maps to JS **decodeURI**, which does NOT
255
+ # decode `%2F` (`/`). RFC 3986 decodeURI reserves those escapes; Rack form
256
+ # bodies need **decodeURIComponent** semantics (same as CGI.unescapeURIComponent).
257
+ # Without this, HTML like `</h1>` survives as literal `<%2Fh1>` in params.
258
+ def self.decode_www_form_component(str, _enc = nil)
259
+ s = str.to_s.tr('+', ' ')
260
+ CGI.unescapeURIComponent(s)
261
+ rescue ::Exception
262
+ str.to_s
263
+ end
264
+
265
+ unless respond_to?(:encode_www_form_component)
266
+ def self.encode_www_form_component(str, _enc = nil)
267
+ CGI.escape(str.to_s)
268
+ end
269
+ end
270
+
271
+ unless const_defined?(:RFC2396_PARSER)
272
+ RFC2396_PARSER = DEFAULT_PARSER
273
+ end
274
+
275
+ unless const_defined?(:Parser)
276
+ # Some gems instantiate URI::Parser.new directly. Return the
277
+ # singleton module which has the same surface area.
278
+ class Parser
279
+ def self.new(*)
280
+ ::URI::DEFAULT_PARSER
281
+ end
282
+ end
283
+ end
284
+
285
+ # Rack::Protection::JsonCsrf#has_vector? does `rescue URI::InvalidURIError`,
286
+ # so the constant needs to exist even if the referrer is actually valid.
287
+ unless const_defined?(:InvalidURIError)
288
+ class InvalidURIError < StandardError; end
289
+ end
290
+
291
+ unless const_defined?(:Error)
292
+ class Error < StandardError; end
293
+ end
294
+
295
+ # Opal's stdlib does not implement URI.parse. Rack::Protection calls
296
+ # `URI.parse(env['HTTP_REFERER']).host`. A tiny JS-URL-backed parser is
297
+ # enough to cover that and any equivalent `host`-only usage.
298
+ class Generic
299
+ attr_reader :host, :scheme, :port, :path, :query, :fragment
300
+
301
+ def initialize(host:, scheme:, port:, path:, query:, fragment:)
302
+ @host = host
303
+ @scheme = scheme
304
+ @port = port
305
+ @path = path
306
+ @query = query
307
+ @fragment = fragment
308
+ end
309
+ end
310
+
311
+ def self.parse(str)
312
+ s = str.to_s
313
+ return Generic.new(host: nil, scheme: nil, port: nil, path: '', query: nil, fragment: nil) if s.empty?
314
+
315
+ js_url = `
316
+ (function() {
317
+ try { return new URL(#{s}); }
318
+ catch (e) {
319
+ try { return new URL(#{s}, "http://__homura.invalid/"); }
320
+ catch (e2) { return null; }
321
+ }
322
+ })()
323
+ `
324
+ raise ::URI::InvalidURIError, "bad URI(is not URI?): #{s}" if `#{js_url} == null`
325
+
326
+ host = `#{js_url}.host` || ''
327
+ host = nil if host == '' || host.include?('__homura.invalid')
328
+ scheme = `#{js_url}.protocol` || ''
329
+ scheme = scheme.sub(/:$/, '')
330
+ scheme = nil if scheme == ''
331
+ port_raw = `#{js_url}.port` || ''
332
+ port = port_raw == '' ? nil : port_raw.to_i
333
+ path = `#{js_url}.pathname` || ''
334
+ query = `#{js_url}.search` || ''
335
+ query = query.sub(/^\?/, '')
336
+ query = nil if query == ''
337
+ frag = `#{js_url}.hash` || ''
338
+ frag = frag.sub(/^#/, '')
339
+ frag = nil if frag == ''
340
+
341
+ Generic.new(host: host, scheme: scheme, port: port, path: path, query: query, fragment: frag)
342
+ end
343
+
344
+ # Net::HTTP.get(URI('https://...')) is the canonical entry point in
345
+ # CRuby Ruby code. CRuby resolves `URI('...')` via Kernel#URI, which
346
+ # is defined in `uri/common.rb` as `URI.parse(arg)`. Opal's stdlib
347
+ # omits that; install it here so vendored gems (and our Net::HTTP
348
+ # shim) can use the idiomatic short form.
349
+ def self.HTTP_class_for(scheme)
350
+ HTTP if scheme == 'http'
351
+ end
352
+ end
353
+
354
+ # Kernel#URI(string) — CRuby alias for URI.parse(string). Phase 6
355
+ # requires it for `Net::HTTP.get(URI('https://...'))` to work.
356
+ module ::Kernel
357
+ def URI(arg)
358
+ return arg if arg.is_a?(::URI::Generic)
359
+ ::URI.parse(arg.to_s)
360
+ end
361
+ module_function :URI
362
+ end
363
+
364
+ # -----------------------------------------------------------------
365
+ # IO.read / IO.binread / File.read / File.binread — Workers have no FS
366
+ # -----------------------------------------------------------------
367
+ # Opal does not implement IO.read / File.read. On Cloudflare Workers
368
+ # there is no writable filesystem anyway, so any code that tries to
369
+ # read a local file will fail. Gems that *optionally* read a file
370
+ # (Sinatra's inline_templates= for example) expect an Errno::ENOENT
371
+ # and rescue it silently; plain method_missing breaks that rescue.
372
+ #
373
+ # Install Errno::ENOENT-raising stubs so callers that rescue the
374
+ # standard not-found exception take the silent path. Callers that
375
+ # do not rescue get a clear, specific error instead of method_missing.
376
+ module ::Kernel
377
+ module_function
378
+ end
379
+
380
+ # -----------------------------------------------------------------
381
+ # SecureRandom — Web Crypto API with graceful fallback
382
+ # -----------------------------------------------------------------
383
+ # Cloudflare Workers forbids async I/O AND random-value generation at
384
+ # module-load time (global scope). Sinatra eagerly generates a session
385
+ # secret at class-body time via SecureRandom.hex(64), which crashes
386
+ # with "Disallowed operation called within global scope" on Workers.
387
+ #
388
+ # We provide a SecureRandom implementation that:
389
+ # 1. Tries `crypto.getRandomValues` via Web Crypto (works inside fetch
390
+ # handlers on Workers and everywhere on Node/browsers).
391
+ # 2. Catches any failure (including the Workers global-scope
392
+ # restriction) and falls back to a deterministic all-zero string.
393
+ #
394
+ # Sinatra's session_secret therefore becomes "000…0" for the duration
395
+ # of the isolate lifetime when no request is in flight. That is the
396
+ # same strength CRuby Sinatra gives you when SecureRandom is unavailable
397
+ # (it falls back to `Kernel.rand`), so we are not reducing security
398
+ # beyond upstream's own fallback path.
399
+ # Ensure our Digest/Zlib/Tempfile/Tilt stubs from vendor/ are available
400
+ # everywhere, even when a gem references `Digest::SHA1` at class body
401
+ # time without explicitly `require 'digest'`-ing first.
402
+ require 'digest'
403
+ require 'digest/sha2'
404
+ require 'zlib'
405
+ require 'tempfile'
406
+ require 'tilt'
407
+
408
+ module ::SecureRandom
409
+ # Raised when neither node:crypto.randomBytes nor Web Crypto
410
+ # getRandomValues is available. We FAIL CLOSED rather than return
411
+ # predictable bytes — silent degradation would let downstream code
412
+ # generate predictable session secrets, JWT signing keys, IVs, etc.
413
+ #
414
+ # NOTE: extends NotImplementedError on purpose. CRuby's SecureRandom
415
+ # raises NotImplementedError when no random device is available,
416
+ # and several gems (most notably Sinatra at line 1988 of base.rb)
417
+ # rescue NotImplementedError to fall back to Kernel.rand for
418
+ # module-load-time secrets. Cloudflare Workers blocks ALL random
419
+ # value generation at module-load (global) scope, so eager
420
+ # session_secret generation must take that fallback path.
421
+ # Request-time calls (where entropy is available) still get real
422
+ # cryptographic randomness; only the module-load case ever falls
423
+ # back, and Sinatra's session secret is the lone caller that
424
+ # actually does that gracefully.
425
+ class EntropyError < ::NotImplementedError; end
426
+
427
+ def self.random_bytes(n = 16)
428
+ n = n.to_i
429
+ n = 16 if n <= 0
430
+ hex_string = secure_hex_bytes(n)
431
+ raise EntropyError, 'no source of cryptographic entropy available (node:crypto AND Web Crypto both unreachable)' if hex_string.nil?
432
+ [hex_string].pack('H*')
433
+ end
434
+
435
+ def self.hex(n = 16)
436
+ n = n.to_i
437
+ n = 16 if n <= 0
438
+ out = secure_hex_bytes(n)
439
+ raise EntropyError, 'no source of cryptographic entropy available (node:crypto AND Web Crypto both unreachable)' if out.nil?
440
+ out
441
+ end
442
+
443
+ def self.uuid
444
+ h = hex(16)
445
+ "#{h[0, 8]}-#{h[8, 4]}-4#{h[13, 3]}-#{h[16, 4]}-#{h[20, 12]}"
446
+ end
447
+
448
+ def self.base64(n = 16)
449
+ require 'base64'
450
+ Base64.strict_encode64(random_bytes(n))
451
+ end
452
+
453
+ def self.urlsafe_base64(n = 16, padding = false)
454
+ s = base64(n).tr('+/', '-_')
455
+ padding ? s : s.delete('=')
456
+ end
457
+
458
+ def self.random_number(n = 0)
459
+ # Not used at class-init time; real implementations welcome.
460
+ 0
461
+ end
462
+
463
+ # Returns a hex string of `n` random bytes, or nil when no entropy
464
+ # source is available. Tries node:crypto.randomBytes first (works
465
+ # on both Cloudflare Workers with `nodejs_compat` and Node.js),
466
+ # falls back to Web Crypto getRandomValues (works at request time
467
+ # on Workers and everywhere on browsers).
468
+ def self.secure_hex_bytes(n)
469
+ # Opal does not always auto-return backtick IIFEs; assign first
470
+ # so the method's last expression is a normal Ruby reference.
471
+ result = `(function(n) {
472
+ try {
473
+ if (typeof globalThis.__nodeCrypto__ !== 'undefined' && globalThis.__nodeCrypto__) {
474
+ return globalThis.__nodeCrypto__.randomBytes(n).toString('hex');
475
+ }
476
+ } catch (e) { /* fall through to Web Crypto */ }
477
+ try {
478
+ if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
479
+ var bytes = new Uint8Array(n);
480
+ crypto.getRandomValues(bytes);
481
+ var out = '';
482
+ for (var i = 0; i < bytes.length; i++) {
483
+ var h = bytes[i].toString(16);
484
+ if (h.length < 2) h = '0' + h;
485
+ out += h;
486
+ }
487
+ return out;
488
+ }
489
+ } catch (e) {
490
+ // Workers blocks getRandomValues at module-load scope; fall through.
491
+ }
492
+ return nil; // Opal nil singleton, not JS null — so .nil? works
493
+ })(#{n})`
494
+ result
495
+ end
496
+ end
497
+
498
+ # -----------------------------------------------------------------
499
+ # Phase 7: Array#pack('H*') / String#unpack1('H*') for hex<->bin.
500
+ # Opal's pack.rb / unpack.rb don't register the 'H' directive
501
+ # ("hex string, high nibble first"). Crypto code (Digest, OpenSSL,
502
+ # jwt) heavily uses these to convert between binary and hex, so we
503
+ # add a minimal handler that intercepts the "H*" / "H<n>" format
504
+ # and falls back to the original implementation for everything else.
505
+ # -----------------------------------------------------------------
506
+
507
+ class ::Array
508
+ alias_method :pack_without_homura_hex, :pack
509
+
510
+ # `H*` consumes the entire hex string. `H<n>` consumes exactly `n`
511
+ # nibbles (= n/2 bytes, rounded down). Matches CRuby semantics so
512
+ # `[hex].pack('H4')` yields the first 2 bytes — Copilot caught
513
+ # this divergence in the initial Phase 7 PR.
514
+ def pack(format)
515
+ fmt = format.to_s
516
+ return pack_without_homura_hex(format) unless fmt == 'H*' || fmt =~ /\AH(\d+)\z/
517
+
518
+ hex = self.first.to_s
519
+ nibble_count = if fmt == 'H*'
520
+ hex.length
521
+ else
522
+ [fmt[1..-1].to_i, hex.length].min
523
+ end
524
+ nibble_count -= 1 if nibble_count.odd? # round down to whole bytes
525
+ out = ''
526
+ i = 0
527
+ while i < nibble_count
528
+ out = out + hex[i, 2].to_i(16).chr
529
+ i += 2
530
+ end
531
+ out
532
+ end
533
+ end
534
+
535
+ class ::String
536
+ alias_method :unpack1_without_homura_hex, :unpack1
537
+
538
+ # `unpack1('H*')` returns one hex pair per byte. CRuby treats the
539
+ # receiver as a raw byte sequence (encoding ASCII-8BIT). Opal stores
540
+ # all Strings as JS Strings (UTF-16 chars) and reports `bytesize` as
541
+ # the UTF-8 encoded byte count, which double-counts chars > 0x7F.
542
+ #
543
+ # For our crypto code, every "binary" String comes from
544
+ # `[hex].pack('H*')` or other functions that pack each byte (0..255)
545
+ # as exactly one JS char. We therefore iterate by char (`length`)
546
+ # and read each char's UTF-16 code unit as the byte value. This
547
+ # matches CRuby's behavior for ASCII-8BIT encoded strings.
548
+ #
549
+ # `H*` produces 2 hex chars per byte. `H<n>` truncates to the first
550
+ # `n` nibbles (rounded down to whole bytes for an odd `n`).
551
+ def unpack1(format)
552
+ fmt = format.to_s
553
+ return unpack1_without_homura_hex(format) unless fmt == 'H*' || fmt =~ /\AH(\d+)\z/
554
+
555
+ requested_nibbles = if fmt == 'H*'
556
+ self.length * 2
557
+ else
558
+ fmt[1..-1].to_i
559
+ end
560
+ out = ''
561
+ i = 0
562
+ n = self.length
563
+ while i < n && out.length < requested_nibbles
564
+ b = `(#{self}.charCodeAt(#{i}) & 0xff)`
565
+ h = b.to_s(16)
566
+ h = '0' + h if h.length == 1
567
+ out = out + h
568
+ i += 1
569
+ end
570
+ out[0, requested_nibbles]
571
+ end
572
+ end
573
+
574
+ class ::IO
575
+ def self.read(*args)
576
+ raise ::Errno::ENOENT, args.first.to_s
577
+ end
578
+
579
+ def self.binread(*args)
580
+ raise ::Errno::ENOENT, args.first.to_s
581
+ end
582
+ end
583
+
584
+ # File inherits from IO in CRuby; in Opal File is its own class.
585
+ # Install the same stubs on File defensively.
586
+ begin
587
+ file_class = ::File
588
+ unless file_class.respond_to?(:read) && !file_class.method(:read).source_location.nil?
589
+ def file_class.read(*args)
590
+ raise ::Errno::ENOENT, args.first.to_s
591
+ end
592
+ def file_class.binread(*args)
593
+ raise ::Errno::ENOENT, args.first.to_s
594
+ end
595
+ end
596
+ unless file_class.respond_to?(:fnmatch)
597
+ def file_class.fnmatch(pattern, path, *)
598
+ # Very small fnmatch: supports `*` and `?` only, good enough for
599
+ # Sinatra's template extension matching.
600
+ regex = '\A'
601
+ i = 0
602
+ p = pattern.to_s
603
+ while i < p.length
604
+ c = p[i]
605
+ case c
606
+ when '*' then regex += '.*'
607
+ when '?' then regex += '.'
608
+ when '.', '(', ')', '[', ']', '+', '^', '$' then regex += "\\#{c}"
609
+ else regex += c
610
+ end
611
+ i += 1
612
+ end
613
+ regex += '\z'
614
+ !!(path.to_s =~ Regexp.new(regex))
615
+ end
616
+ def file_class.fnmatch?(pattern, path, *)
617
+ fnmatch(pattern, path)
618
+ end
619
+ end
620
+ rescue NameError
621
+ # File not available at this load point — ignore.
622
+ end
623
+
624
+ # Phase 13 (upstream Sinatra 4.2.1): Sinatra::IndifferentHash references
625
+ # Gem::Version at class body eval to gate the `except` override. Opal
626
+ # does not bundle RubyGems — pre-require our minimal stub so the
627
+ # reference resolves before upstream Sinatra loads.
628
+ require 'rubygems/version'
629
+
630
+ # Phase 13 originally required `opal-parser` because upstream Sinatra's
631
+ # `set` helper used `class_eval("def ...")` for primitive option values.
632
+ # Phase 15-Pre removes that string-eval path in `vendor/sinatra_upstream/base.rb`
633
+ # (Proc-based getters / predicate) so the Workers bundle no longer needs the
634
+ # full Opal compiler + whitequark parser at runtime.
635
+
636
+ [
637
+ :ISO_2022_JP,
638
+ :SHIFT_JIS, :Shift_JIS, :WINDOWS_31J, :CP932, :SJIS,
639
+ :EUC_JP, :EUC_KR, :EUC_CN, :EUC_TW,
640
+ :BIG5, :GB18030, :GBK, :GB2312,
641
+ :WINDOWS_1250, :WINDOWS_1251, :WINDOWS_1252, :WINDOWS_1253,
642
+ :WINDOWS_1254, :WINDOWS_1255, :WINDOWS_1256, :WINDOWS_1257, :WINDOWS_1258,
643
+ :KOI8_R, :KOI8_U,
644
+ :ISO_8859_2, :ISO_8859_3, :ISO_8859_4, :ISO_8859_5,
645
+ :ISO_8859_6, :ISO_8859_7, :ISO_8859_8, :ISO_8859_9,
646
+ :ISO_8859_10, :ISO_8859_11, :ISO_8859_13, :ISO_8859_14,
647
+ :ISO_8859_15, :ISO_8859_16,
648
+ :MACROMAN
649
+ ].each do |name|
650
+ unless Encoding.const_defined?(name)
651
+ Encoding.const_set(name, Encoding::ASCII_8BIT)
652
+ end
653
+ end