From 224a7ca0fe3496e4c7bbb1cd4e25e7460c2f1b4b Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 3 Apr 2026 10:26:51 +0800 Subject: [PATCH] Tweak snapshot for workers We'll have two types of contexts: one for pages and one for workers. They'll [probably] both be js.Context, but they'll have distinct FunctionTemplates attached to their global. The Worker template will only contain a small subset of the main Page's types, along with 1 or 2 of its own specific ones. The Snapshot now creates the templates for both, so that the Env contains the function templates for both contexts. Furthermore, having a "merged" view like this ensures that the env.template[N] indices are consistent between the two. However, the snapshot only attaches the Page-specific types to the snapshot context. This allows the Page-context to be created as-is (e.g. efficiently). The worker context will be created lazily, on demand, but from the templates loaded into the env (since, again, the env contains templates for both). --- src/browser/js/Snapshot.zig | 231 ++++++++++++----------- src/browser/js/bridge.zig | 16 +- src/browser/webapi/EventTarget.zig | 3 + src/browser/webapi/Window.zig | 20 +- src/browser/webapi/WorkerGlobalScope.zig | 135 +++++++++++++ src/browser/webapi/encoding/base64.zig | 50 +++++ 6 files changed, 322 insertions(+), 133 deletions(-) create mode 100644 src/browser/webapi/WorkerGlobalScope.zig create mode 100644 src/browser/webapi/encoding/base64.zig diff --git a/src/browser/js/Snapshot.zig b/src/browser/js/Snapshot.zig index abea4ee5..e1b4905c 100644 --- a/src/browser/js/Snapshot.zig +++ b/src/browser/js/Snapshot.zig @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire @@ -25,6 +25,7 @@ const IS_DEBUG = @import("builtin").mode == .Debug; const v8 = js.v8; const JsApis = bridge.JsApis; +const PageJsApis = bridge.PageJsApis; const Snapshot = @This(); @@ -113,6 +114,8 @@ fn isValid(self: Snapshot) bool { } pub fn create() !Snapshot { + comptime validatePrototypeChains(&JsApis); + var external_references = collectExternalReferences(); var params: v8.CreateParams = undefined; @@ -135,7 +138,7 @@ pub fn create() !Snapshot { v8.v8__HandleScope__CONSTRUCT(&handle_scope, isolate); defer v8.v8__HandleScope__DESTRUCT(&handle_scope); - // Create templates (constructors only) FIRST + // Create templates for ALL types (JsApis) var templates: [JsApis.len]*const v8.FunctionTemplate = undefined; inline for (JsApis, 0..) |JsApi, i| { @setEvalBranchQuota(10_000); @@ -144,20 +147,17 @@ pub fn create() !Snapshot { } // Set up prototype chains BEFORE attaching properties - // This must come before attachClass so inheritance is set up first inline for (JsApis, 0..) |JsApi, i| { if (comptime protoIndexLookup(JsApi)) |proto_index| { v8.v8__FunctionTemplate__Inherit(templates[i], templates[proto_index]); } } - // Set up the global template to inherit from Window's template - // This way the global object gets all Window properties through inheritance const context = v8.v8__Context__New(isolate, null, null); v8.v8__Context__Enter(context); defer v8.v8__Context__Exit(context); - // Add templates to context snapshot + // Add ALL templates to context snapshot var last_data_index: usize = 0; inline for (JsApis, 0..) |_, i| { @setEvalBranchQuota(10_000); @@ -166,11 +166,6 @@ pub fn create() !Snapshot { data_start = data_index; last_data_index = data_index; } else { - // This isn't strictly required, but it means we only need to keep - // the first data_index. This is based on the assumption that - // addDataWithContext always increases by 1. If we ever hit this - // error, then that assumption is wrong and we should capture - // all the indexes explicitly in an array. if (data_index != last_data_index + 1) { return error.InvalidDataIndex; } @@ -178,13 +173,12 @@ pub fn create() !Snapshot { } } - // Realize all templates by getting their functions and attaching to global const global_obj = v8.v8__Context__Global(context); - inline for (JsApis, 0..) |JsApi, i| { + // Attach only PAGE types to the default context's global + inline for (PageJsApis, 0..) |JsApi, i| { + // PageJsApis[i] == JsApis[i] because the PageJsApis are position at the start of the list const func = v8.v8__FunctionTemplate__GetFunction(templates[i], context); - - // Attach to global if it has a name if (@hasDecl(JsApi.Meta, "name")) { if (@hasDecl(JsApi.Meta, "constructor_alias")) { const alias = JsApi.Meta.constructor_alias; @@ -192,12 +186,6 @@ pub fn create() !Snapshot { var maybe_result: v8.MaybeBool = undefined; v8.v8__Object__Set(global_obj, context, v8_class_name, func, &maybe_result); - // @TODO: This is wrong. This name should be registered with the - // illegalConstructorCallback. I.e. new Image() is OK, but - // new HTMLImageElement() isn't. - // But we _have_ to register the name, i.e. HTMLImageElement - // has to be registered so, for now, instead of creating another - // template, we just hook it into the constructor. const name = JsApi.Meta.name; const illegal_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len)); var maybe_result2: v8.MaybeBool = undefined; @@ -216,8 +204,7 @@ pub fn create() !Snapshot { } { - // If we want to overwrite the built-in console, we have to - // delete the built-in one. + // Delete built-in console so we can inject our own const console_key = v8.v8__String__NewFromUtf8(isolate, "console", v8.kNormal, 7); var maybe_deleted: v8.MaybeBool = undefined; v8.v8__Object__Delete(global_obj, context, console_key, &maybe_deleted); @@ -226,9 +213,8 @@ pub fn create() !Snapshot { } } - // This shouldn't be necessary, but it is: + // Set prototype chains on function objects // https://groups.google.com/g/v8-users/c/qAQQBmbi--8 - // TODO: see if newer V8 engines have a way around this. inline for (JsApis, 0..) |JsApi, i| { if (comptime protoIndexLookup(JsApi)) |proto_index| { const proto_func = v8.v8__FunctionTemplate__GetFunction(templates[proto_index], context); @@ -243,8 +229,7 @@ pub fn create() !Snapshot { } { - // Custom exception - // TODO: this is an horrible hack, I can't figure out how to do this cleanly. + // DOMException prototype setup const code_str = "DOMException.prototype.__proto__ = Error.prototype"; const code = v8.v8__String__NewFromUtf8(isolate, code_str.ptr, v8.kNormal, @intCast(code_str.len)); const script = v8.v8__Script__Compile(context, code, null) orelse return error.ScriptCompileFailed; @@ -264,20 +249,6 @@ pub fn create() !Snapshot { }; } -// Helper to check if a JsApi has a NamedIndexed handler -fn hasNamedIndexedGetter(comptime JsApi: type) bool { - const declarations = @typeInfo(JsApi).@"struct".decls; - inline for (declarations) |d| { - const value = @field(JsApi, d.name); - const T = @TypeOf(value); - if (T == bridge.NamedIndexed) { - return true; - } - } - return false; -} - -// Count total callbacks needed for external_references array fn countExternalReferences() comptime_int { @setEvalBranchQuota(100_000); @@ -290,23 +261,20 @@ fn countExternalReferences() comptime_int { count += 1; inline for (JsApis) |JsApi| { - // Constructor (only if explicit) if (@hasDecl(JsApi, "constructor")) { count += 1; } - // Callable (htmldda) if (@hasDecl(JsApi, "callable")) { count += 1; } - // All other callbacks const declarations = @typeInfo(JsApi).@"struct".decls; inline for (declarations) |d| { const value = @field(JsApi, d.name); const T = @TypeOf(value); if (T == bridge.Accessor) { - count += 1; // getter + count += 1; if (value.setter != null) { count += 1; } @@ -320,14 +288,13 @@ fn countExternalReferences() comptime_int { count += 1; } } else if (T == bridge.NamedIndexed) { - count += 1; // getter + count += 1; if (value.setter != null) count += 1; if (value.deleter != null) count += 1; } } } - // In debug mode, add unknown property callbacks for types without NamedIndexed if (comptime IS_DEBUG) { inline for (JsApis) |JsApi| { if (!hasNamedIndexedGetter(JsApi)) { @@ -399,7 +366,6 @@ fn collectExternalReferences() [countExternalReferences()]isize { } } - // In debug mode, collect unknown property callbacks for types without NamedIndexed if (comptime IS_DEBUG) { inline for (JsApis) |JsApi| { if (!hasNamedIndexedGetter(JsApi)) { @@ -412,34 +378,8 @@ fn collectExternalReferences() [countExternalReferences()]isize { return references; } -// Even if a struct doesn't have a `constructor` function, we still -// `generateConstructor`, because this is how we create our -// FunctionTemplate. Such classes exist, but they can't be instantiated -// via `new ClassName()` - but they could, for example, be created in -// Zig and returned from a function call, which is why we need the -// FunctionTemplate. -fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *const v8.FunctionTemplate { - const callback = blk: { - if (@hasDecl(JsApi, "constructor")) { - break :blk JsApi.constructor.func; - } - - // Use shared illegal constructor callback - break :blk illegalConstructorCallback; - }; - - const template = v8.v8__FunctionTemplate__New__DEFAULT2(isolate, callback).?; - { - const internal_field_count = comptime countInternalFields(JsApi); - if (internal_field_count > 0) { - const instance_template = v8.v8__FunctionTemplate__InstanceTemplate(template); - v8.v8__ObjectTemplate__SetInternalFieldCount(instance_template, internal_field_count); - } - } - const name_str = if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi); - const class_name = v8.v8__String__NewFromUtf8(isolate, name_str.ptr, v8.kNormal, @intCast(name_str.len)); - v8.v8__FunctionTemplate__SetClassName(template, class_name); - return template; +fn protoIndexLookup(comptime JsApi: type) ?u16 { + return protoIndexLookupFor(&JsApis, JsApi); } pub fn countInternalFields(comptime JsApi: type) u8 { @@ -480,14 +420,109 @@ pub fn countInternalFields(comptime JsApi: type) u8 { return cache_count + 1; } -// Attaches JsApi members to the prototype template (normal case) +// Shared illegal constructor callback for types without explicit constructors +fn illegalConstructorCallback(raw_info: ?*const v8.FunctionCallbackInfo) callconv(.c) void { + const isolate = v8.v8__FunctionCallbackInfo__GetIsolate(raw_info); + log.warn(.js, "Illegal constructor call", .{}); + + const message = v8.v8__String__NewFromUtf8(isolate, "Illegal Constructor", v8.kNormal, 19); + const js_exception = v8.v8__Exception__TypeError(message); + + _ = v8.v8__Isolate__ThrowException(isolate, js_exception); + var return_value: v8.ReturnValue = undefined; + v8.v8__FunctionCallbackInfo__GetReturnValue(raw_info, &return_value); + v8.v8__ReturnValue__Set(return_value, js_exception); +} + +// Helper to check if a JsApi has a NamedIndexed handler (public for reuse) +fn hasNamedIndexedGetter(comptime JsApi: type) bool { + const declarations = @typeInfo(JsApi).@"struct".decls; + inline for (declarations) |d| { + const value = @field(JsApi, d.name); + const T = @TypeOf(value); + if (T == bridge.NamedIndexed) { + return true; + } + } + return false; +} + +// Generic prototype index lookup for a given API list +fn protoIndexLookupFor(comptime ApiList: []const type, comptime JsApi: type) ?u16 { + @setEvalBranchQuota(100_000); + comptime { + const T = JsApi.bridge.type; + if (!@hasField(T, "_proto")) { + return null; + } + const Ptr = std.meta.fieldInfo(T, ._proto).type; + const F = @typeInfo(Ptr).pointer.child; + // Look up in the provided API list + for (ApiList, 0..) |Api, i| { + if (Api == F.JsApi) { + return i; + } + } + @compileError("Prototype " ++ @typeName(F.JsApi) ++ " not found in API list"); + } +} + +// Validates that every type in the API list has its full prototype chain +// contained within that same list. This catches errors where a type is added +// to a snapshot but its prototype dependencies are missing. +// See bridge.AllJsApis for more information. +fn validatePrototypeChains(comptime ApiList: []const type) void { + @setEvalBranchQuota(100_000); + inline for (ApiList) |JsApi| { + const T = JsApi.bridge.type; + if (@hasField(T, "_proto")) { + const Ptr = std.meta.fieldInfo(T, ._proto).type; + const ProtoType = @typeInfo(Ptr).pointer.child; + // Verify the prototype's JsApi is in our list + var found = false; + inline for (ApiList) |Api| { + if (Api == ProtoType.JsApi) { + found = true; + break; + } + } + if (!found) { + @compileError( + @typeName(JsApi) ++ " has prototype " ++ + @typeName(ProtoType.JsApi) ++ " which is not in the API list", + ); + } + } + } +} + +// Generate a constructor template for a JsApi type (public for reuse) +pub fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *const v8.FunctionTemplate { + const callback = blk: { + if (@hasDecl(JsApi, "constructor")) { + break :blk JsApi.constructor.func; + } + break :blk illegalConstructorCallback; + }; + + const template = v8.v8__FunctionTemplate__New__DEFAULT2(isolate, callback).?; + { + const internal_field_count = comptime countInternalFields(JsApi); + if (internal_field_count > 0) { + const instance_template = v8.v8__FunctionTemplate__InstanceTemplate(template); + v8.v8__ObjectTemplate__SetInternalFieldCount(instance_template, internal_field_count); + } + } + const name_str = if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi); + const class_name = v8.v8__String__NewFromUtf8(isolate, name_str.ptr, v8.kNormal, @intCast(name_str.len)); + v8.v8__FunctionTemplate__SetClassName(template, class_name); + return template; +} + +// Attach JsApi members to a template (public for reuse) fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.FunctionTemplate) void { const instance = v8.v8__FunctionTemplate__InstanceTemplate(template); const prototype = v8.v8__FunctionTemplate__PrototypeTemplate(template); - - // Create a signature that validates the receiver is an instance of this template. - // This prevents crashes when JavaScript extracts a getter/method and calls it - // with the wrong `this` (e.g., documentGetter.call(null)). const signature = v8.v8__Signature__New(isolate, template); const declarations = @typeInfo(JsApi).@"struct".decls; @@ -523,7 +558,6 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F } if (value.static) { - // Static accessors: use Template's SetAccessorProperty v8.v8__Template__SetAccessorProperty(@ptrCast(template), js_name, getter_callback, setter_callback, attribute); } else { v8.v8__ObjectTemplate__SetAccessorProperty__Config(prototype, &.{ @@ -535,7 +569,6 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F } }, bridge.Function => { - // For non-static functions, use the signature to validate the receiver const func_signature = if (value.static) null else signature; const function_template = v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.func, @@ -589,7 +622,7 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F bridge.Property => { const js_value = switch (value.value) { .null => js.simpleZigValueToJs(.{ .handle = isolate }, null, true, false), - inline .bool, .int, .float, .string => |v| js.simpleZigValueToJs(.{ .handle = isolate }, v, true, false), + inline .bool, .int, .float, .string => |pv| js.simpleZigValueToJs(.{ .handle = isolate }, pv, true, false), }; const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len)); @@ -599,11 +632,10 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F } if (value.template) { - // apply it both to the type itself (e.g. Node.Elem) v8.v8__Template__Set(@ptrCast(template), js_name, js_value, v8.ReadOnly + v8.DontDelete); } }, - bridge.Constructor => {}, // already handled in generateConstructor + bridge.Constructor => {}, else => {}, } } @@ -636,30 +668,3 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F } } } - -fn protoIndexLookup(comptime JsApi: type) ?bridge.JsApiLookup.BackingInt { - @setEvalBranchQuota(2000); - comptime { - const T = JsApi.bridge.type; - if (!@hasField(T, "_proto")) { - return null; - } - const Ptr = std.meta.fieldInfo(T, ._proto).type; - const F = @typeInfo(Ptr).pointer.child; - return bridge.JsApiLookup.getId(F.JsApi); - } -} - -// Shared illegal constructor callback for types without explicit constructors -fn illegalConstructorCallback(raw_info: ?*const v8.FunctionCallbackInfo) callconv(.c) void { - const isolate = v8.v8__FunctionCallbackInfo__GetIsolate(raw_info); - log.warn(.js, "Illegal constructor call", .{}); - - const message = v8.v8__String__NewFromUtf8(isolate, "Illegal Constructor", v8.kNormal, 19); - const js_exception = v8.v8__Exception__TypeError(message); - - _ = v8.v8__Isolate__ThrowException(isolate, js_exception); - var return_value: v8.ReturnValue = undefined; - v8.v8__FunctionCallbackInfo__GetReturnValue(raw_info, &return_value); - v8.v8__ReturnValue__Set(return_value, js_exception); -} diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 0a51327e..24845d11 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -550,7 +550,7 @@ fn PrototypeType(comptime T: type) ?type { return Struct(std.meta.fieldInfo(T, ._proto).type); } -fn flattenTypes(comptime Types: []const type) [countFlattenedTypes(Types)]type { +pub fn flattenTypes(comptime Types: []const type) [countFlattenedTypes(Types)]type { var index: usize = 0; var flat: [countFlattenedTypes(Types)]type = undefined; for (Types) |T| { @@ -673,7 +673,8 @@ pub const SubType = enum { webassemblymemory, }; -pub const JsApis = flattenTypes(&.{ +/// APIs for Page/Window contexts. Used by Snapshot.zig for Page snapshot creation. +pub const PageJsApis = flattenTypes(&.{ @import("../webapi/AbortController.zig"), @import("../webapi/AbortSignal.zig"), @import("../webapi/CData.zig"), @@ -866,3 +867,14 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/Selection.zig"), @import("../webapi/ImageData.zig"), }); + +/// APIs that exist only in Worker contexts (not in Page/Window). +const WorkerOnlyApis = flattenTypes(&.{ + @import("../webapi/WorkerGlobalScope.zig"), +}); + +/// Master list of ALL JS APIs across all contexts. +/// Used by Env (class IDs, templates), JsApiLookup, and anywhere that needs +/// to know about all possible types. Individual snapshots use their own +/// subsets (PageJsApis, WorkerSnapshot.JsApis). +pub const JsApis = PageJsApis ++ WorkerOnlyApis; diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index 704efeb3..2c2c5ae1 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -34,6 +34,7 @@ pub const Type = union(enum) { generic: void, node: *@import("Node.zig"), window: *@import("Window.zig"), + worker_global_scope: *@import("WorkerGlobalScope.zig"), xhr: *@import("net/XMLHttpRequestEventTarget.zig"), abort_signal: *@import("AbortSignal.zig"), media_query_list: *@import("css/MediaQueryList.zig"), @@ -130,6 +131,7 @@ pub fn format(self: *EventTarget, writer: *std.Io.Writer) !void { .node => |n| n.format(writer), .generic => writer.writeAll(""), .window => writer.writeAll(""), + .worker_global_scope => writer.writeAll(""), .xhr => writer.writeAll(""), .abort_signal => writer.writeAll(""), .media_query_list => writer.writeAll(""), @@ -149,6 +151,7 @@ pub fn toString(self: *EventTarget) []const u8 { .node => return "[object Node]", .generic => return "[object EventTarget]", .window => return "[object Window]", + .worker_global_scope => return "[object WorkerGlobalScope]", .xhr => return "[object XMLHttpRequestEventTarget]", .abort_signal => return "[object AbortSignal]", .media_query_list => return "[object MediaQueryList]", diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index fb3ec8f8..5feabb44 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -429,27 +429,11 @@ pub fn postMessage(self: *Window, message: js.Value.Temp, target_origin: ?[]cons } pub fn btoa(_: *const Window, input: []const u8, page: *Page) ![]const u8 { - const encoded_len = std.base64.standard.Encoder.calcSize(input.len); - const encoded = try page.call_arena.alloc(u8, encoded_len); - return std.base64.standard.Encoder.encode(encoded, input); + return @import("encoding/base64.zig").encode(page.call_arena, input); } pub fn atob(_: *const Window, input: []const u8, page: *Page) ![]const u8 { - const trimmed = std.mem.trim(u8, input, &std.ascii.whitespace); - // Forgiving base64 decode per WHATWG spec: - // https://infra.spec.whatwg.org/#forgiving-base64-decode - // Remove trailing padding to use standard_no_pad decoder - const unpadded = std.mem.trimRight(u8, trimmed, "="); - - // Length % 4 == 1 is invalid (can't represent valid base64) - if (unpadded.len % 4 == 1) { - return error.InvalidCharacterError; - } - - const decoded_len = std.base64.standard_no_pad.Decoder.calcSizeForSlice(unpadded) catch return error.InvalidCharacterError; - const decoded = try page.call_arena.alloc(u8, decoded_len); - std.base64.standard_no_pad.Decoder.decode(decoded, unpadded) catch return error.InvalidCharacterError; - return decoded; + return @import("encoding/base64.zig").decode(page.call_arena, input); } pub fn structuredClone(_: *const Window, value: js.Value) !js.Value { diff --git a/src/browser/webapi/WorkerGlobalScope.zig b/src/browser/webapi/WorkerGlobalScope.zig new file mode 100644 index 00000000..9ad3b0c1 --- /dev/null +++ b/src/browser/webapi/WorkerGlobalScope.zig @@ -0,0 +1,135 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const js = @import("../js/js.zig"); + +const base64 = @import("encoding/base64.zig"); +const Console = @import("Console.zig"); +const Crypto = @import("Crypto.zig"); +const EventTarget = @import("EventTarget.zig"); +const Performance = @import("Performance.zig"); + +const WorkerGlobalScope = @This(); + +_proto: *EventTarget, +_console: Console = .init, +_crypto: Crypto = .init, +_performance: Performance, +_on_error: ?js.Function.Global = null, +_on_rejection_handled: ?js.Function.Global = null, +_on_unhandled_rejection: ?js.Function.Global = null, + +pub fn asEventTarget(self: *WorkerGlobalScope) *EventTarget { + return self._proto; +} + +pub fn getSelf(self: *WorkerGlobalScope) *WorkerGlobalScope { + return self; +} + +pub fn getConsole(self: *WorkerGlobalScope) *Console { + return &self._console; +} + +pub fn getCrypto(self: *WorkerGlobalScope) *Crypto { + return &self._crypto; +} + +pub fn getPerformance(self: *WorkerGlobalScope) *Performance { + return &self._performance; +} + +pub fn getOnError(self: *const WorkerGlobalScope) ?js.Function.Global { + return self._on_error; +} + +pub fn setOnError(self: *WorkerGlobalScope, setter: ?FunctionSetter) void { + self._on_error = getFunctionFromSetter(setter); +} + +pub fn getOnRejectionHandled(self: *const WorkerGlobalScope) ?js.Function.Global { + return self._on_rejection_handled; +} + +pub fn setOnRejectionHandled(self: *WorkerGlobalScope, setter: ?FunctionSetter) void { + self._on_rejection_handled = getFunctionFromSetter(setter); +} + +pub fn getOnUnhandledRejection(self: *const WorkerGlobalScope) ?js.Function.Global { + return self._on_unhandled_rejection; +} + +pub fn setOnUnhandledRejection(self: *WorkerGlobalScope, setter: ?FunctionSetter) void { + self._on_unhandled_rejection = getFunctionFromSetter(setter); +} + +pub fn btoa(_: *const WorkerGlobalScope, input: []const u8, exec: *js.Execution) ![]const u8 { + return base64.encode(exec.call_arena, input); +} + +pub fn atob(_: *const WorkerGlobalScope, input: []const u8, exec: *js.Execution) ![]const u8 { + return base64.decode(exec.call_arena, input); +} + +pub fn structuredClone(_: *const WorkerGlobalScope, value: js.Value) !js.Value { + return value.structuredClone(); +} + +// TODO: importScripts - needs script loading infrastructure +// TODO: location - needs WorkerLocation +// TODO: navigator - needs WorkerNavigator +// TODO: Timer functions - need scheduler integration + +const FunctionSetter = union(enum) { + func: js.Function.Global, + anything: js.Value, +}; + +fn getFunctionFromSetter(setter_: ?FunctionSetter) ?js.Function.Global { + const setter = setter_ orelse return null; + return switch (setter) { + .func => |func| func, + .anything => null, + }; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(WorkerGlobalScope); + + pub const Meta = struct { + pub const name = "WorkerGlobalScope"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const self = bridge.accessor(WorkerGlobalScope.getSelf, null, .{}); + pub const console = bridge.accessor(WorkerGlobalScope.getConsole, null, .{}); + pub const crypto = bridge.accessor(WorkerGlobalScope.getCrypto, null, .{}); + pub const performance = bridge.accessor(WorkerGlobalScope.getPerformance, null, .{}); + + pub const onerror = bridge.accessor(WorkerGlobalScope.getOnError, WorkerGlobalScope.setOnError, .{}); + pub const onrejectionhandled = bridge.accessor(WorkerGlobalScope.getOnRejectionHandled, WorkerGlobalScope.setOnRejectionHandled, .{}); + pub const onunhandledrejection = bridge.accessor(WorkerGlobalScope.getOnUnhandledRejection, WorkerGlobalScope.setOnUnhandledRejection, .{}); + + pub const btoa = bridge.function(WorkerGlobalScope.btoa, .{}); + pub const atob = bridge.function(WorkerGlobalScope.atob, .{ .dom_exception = true }); + pub const structuredClone = bridge.function(WorkerGlobalScope.structuredClone, .{}); + + // Return false since workers don't have secure-context-only APIs + pub const isSecureContext = bridge.property(false, .{ .template = false }); +}; diff --git a/src/browser/webapi/encoding/base64.zig b/src/browser/webapi/encoding/base64.zig new file mode 100644 index 00000000..cdbe98a7 --- /dev/null +++ b/src/browser/webapi/encoding/base64.zig @@ -0,0 +1,50 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! Base64 encoding/decoding helpers for btoa/atob. +//! Used by both Window and WorkerGlobalScope. + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +/// Encodes input to base64 (btoa). +pub fn encode(alloc: Allocator, input: []const u8) ![]const u8 { + const encoded_len = std.base64.standard.Encoder.calcSize(input.len); + const encoded = try alloc.alloc(u8, encoded_len); + return std.base64.standard.Encoder.encode(encoded, input); +} + +/// Decodes base64 input (atob). +/// Implements forgiving base64 decode per WHATWG spec. +pub fn decode(alloc: Allocator, input: []const u8) ![]const u8 { + const trimmed = std.mem.trim(u8, input, &std.ascii.whitespace); + // Forgiving base64 decode per WHATWG spec: + // https://infra.spec.whatwg.org/#forgiving-base64-decode + // Remove trailing padding to use standard_no_pad decoder + const unpadded = std.mem.trimRight(u8, trimmed, "="); + + // Length % 4 == 1 is invalid (can't represent valid base64) + if (unpadded.len % 4 == 1) { + return error.InvalidCharacterError; + } + + const decoded_len = std.base64.standard_no_pad.Decoder.calcSizeForSlice(unpadded) catch return error.InvalidCharacterError; + const decoded = try alloc.alloc(u8, decoded_len); + std.base64.standard_no_pad.Decoder.decode(decoded, unpadded) catch return error.InvalidCharacterError; + return decoded; +}