From d2003c7c9ada697296e429afc502efd7cdb4c975 Mon Sep 17 00:00:00 2001 From: Tenith01 Date: Tue, 17 Mar 2026 14:12:13 +0530 Subject: [PATCH 1/3] fix: stub navigator.permissions, storage, deviceMemory to unblock Turnstile --- src/browser/js/bridge.zig | 3 + src/browser/tests/navigator/permissions.html | 37 ++++++++++++ src/browser/webapi/Navigator.zig | 22 +++++++ src/browser/webapi/Permissions.zig | 61 ++++++++++++++++++++ src/browser/webapi/StorageManager.zig | 56 ++++++++++++++++++ 5 files changed, 179 insertions(+) create mode 100644 src/browser/tests/navigator/permissions.html create mode 100644 src/browser/webapi/Permissions.zig create mode 100644 src/browser/webapi/StorageManager.zig diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index cf6e999e..1b035837 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -725,6 +725,9 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/collections.zig"), @import("../webapi/Console.zig"), @import("../webapi/Crypto.zig"), + @import("../webapi/Permissions.zig"), + @import("../webapi/Permissions.zig").PermissionStatus, + @import("../webapi/StorageManager.zig"), @import("../webapi/CSS.zig"), @import("../webapi/css/CSSRule.zig"), @import("../webapi/css/CSSRuleList.zig"), diff --git a/src/browser/tests/navigator/permissions.html b/src/browser/tests/navigator/permissions.html new file mode 100644 index 00000000..befecf8b --- /dev/null +++ b/src/browser/tests/navigator/permissions.html @@ -0,0 +1,37 @@ + + + + + + + + + + diff --git a/src/browser/webapi/Navigator.zig b/src/browser/webapi/Navigator.zig index 5e69db07..36d32f1e 100644 --- a/src/browser/webapi/Navigator.zig +++ b/src/browser/webapi/Navigator.zig @@ -21,10 +21,14 @@ const builtin = @import("builtin"); const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); const PluginArray = @import("PluginArray.zig"); +const Permissions = @import("Permissions.zig"); +const StorageManager = @import("StorageManager.zig"); const Navigator = @This(); _pad: bool = false, _plugins: PluginArray = .{}, +_permissions: Permissions = Permissions.init, +_storage: StorageManager = StorageManager.init, pub const init: Navigator = .{}; @@ -55,6 +59,20 @@ pub fn getPlugins(self: *Navigator) *PluginArray { return &self._plugins; } +pub fn getPermissions(self: *Navigator) *Permissions { + return &self._permissions; +} + +pub fn getStorage(self: *Navigator) *StorageManager { + return &self._storage; +} + +pub fn getBattery(_: *const Navigator, page: *Page) !js.Promise { + // Battery API is not supported in headless mode. + // Return a rejected Promise — callers must .catch() this. + return js.Promise.reject(error.NotSupportedError, page); +} + pub fn registerProtocolHandler(_: *const Navigator, scheme: []const u8, url: [:0]const u8, page: *const Page) !void { try validateProtocolHandlerScheme(scheme); try validateProtocolHandlerURL(url, page); @@ -144,6 +162,7 @@ pub const JsApi = struct { pub const onLine = bridge.property(true, .{ .template = false }); pub const cookieEnabled = bridge.property(true, .{ .template = false }); pub const hardwareConcurrency = bridge.property(4, .{ .template = false }); + pub const deviceMemory = bridge.property(@as(f64, 8.0), .{ .template = false }); pub const maxTouchPoints = bridge.property(0, .{ .template = false }); pub const vendor = bridge.property("", .{ .template = false }); pub const product = bridge.property("Gecko", .{ .template = false }); @@ -156,4 +175,7 @@ pub const JsApi = struct { // Methods pub const javaEnabled = bridge.function(Navigator.javaEnabled, .{}); + pub const getBattery = bridge.function(Navigator.getBattery, .{}); + pub const permissions = bridge.accessor(Navigator.getPermissions, null, .{}); + pub const storage = bridge.accessor(Navigator.getStorage, null, .{}); }; diff --git a/src/browser/webapi/Permissions.zig b/src/browser/webapi/Permissions.zig new file mode 100644 index 00000000..5f5954b3 --- /dev/null +++ b/src/browser/webapi/Permissions.zig @@ -0,0 +1,61 @@ +// src/browser/webapi/Permissions.zig +// +// Minimal Permissions API stub. +// https://www.w3.org/TR/permissions/ +// +// Turnstile probes: navigator.permissions.query({ name: 'notifications' }) +// It expects a Promise resolving to { state: 'granted' | 'denied' | 'prompt' } + +const std = @import("std"); +const js = @import("../js/js.zig"); +const Page = @import("../Page.zig"); + +const Permissions = @This(); + +// Padding to avoid zero-size struct pointer collisions +_pad: bool = false, + +pub const init: Permissions = .{}; + +const QueryDescriptor = struct { + name: []const u8, +}; + +const PermissionStatus = struct { + state: []const u8, + + pub const JsApi = struct { + pub const bridge = js.Bridge(PermissionStatus); + pub const Meta = struct { + pub const name = "PermissionStatus"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + pub const empty_with_no_proto = true; + }; + pub const state = bridge.accessor(getState, null, .{}); + }; + + fn getState(self: *const PermissionStatus) []const u8 { + return self.state; + } +}; + +// query() returns a Promise. +// We always report 'prompt' (the default safe value — neither granted nor denied). +pub fn query(_: *const Permissions, _: QueryDescriptor, page: *Page) !js.Promise { + const status = try page._factory.create(PermissionStatus{ .state = "prompt" }); + return js.Promise.resolve(status, page); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(Permissions); + + pub const Meta = struct { + pub const name = "Permissions"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + pub const empty_with_no_proto = true; + }; + + pub const query = bridge.function(Permissions.query, .{ .dom_exception = true }); +}; diff --git a/src/browser/webapi/StorageManager.zig b/src/browser/webapi/StorageManager.zig new file mode 100644 index 00000000..280614a0 --- /dev/null +++ b/src/browser/webapi/StorageManager.zig @@ -0,0 +1,56 @@ +// src/browser/webapi/StorageManager.zig +// Minimal stub for navigator.storage +// https://storage.spec.whatwg.org/#storagemanager + +const js = @import("../js/js.zig"); +const Page = @import("../Page.zig"); + +const StorageManager = @This(); +_pad: bool = false, + +pub const init: StorageManager = .{}; + +const StorageEstimate = struct { + quota: u64, + usage: u64, + + pub const JsApi = struct { + pub const bridge = js.Bridge(StorageEstimate); + pub const Meta = struct { + pub const name = "StorageEstimate"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + pub const empty_with_no_proto = true; + }; + pub const quota = bridge.accessor(getQuota, null, .{}); + pub const usage = bridge.accessor(getUsage, null, .{}); + }; + + fn getQuota(self: *const StorageEstimate) u64 { + return self.quota; + } + fn getUsage(self: *const StorageEstimate) u64 { + return self.usage; + } +}; + +// Returns a resolved Promise with plausible stub values. +// quota = 1GB, usage = 0 (headless browser has no real storage) +pub fn estimate(_: *const StorageManager, page: *Page) !js.Promise { + const est = try page._factory.create(StorageEstimate{ + .quota = 1024 * 1024 * 1024, // 1 GiB + .usage = 0, + }); + return js.Promise.resolve(est, page); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(StorageManager); + pub const Meta = struct { + pub const name = "StorageManager"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + pub const empty_with_no_proto = true; + }; + pub const estimate = bridge.function(StorageManager.estimate, .{}); +}; From a88c21cdb5532c388fdda35cd5db2fbacacddad3 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 19 Mar 2026 09:41:13 +0800 Subject: [PATCH 2/3] Fix Navigator Additions Follow up to https://github.com/lightpanda-io/browser/pull/1884 Fixes build, uses arena/finalizer for PermissionStatus. Fixes tests. A few other small cleanups. --- src/browser/js/Local.zig | 6 ++ src/browser/js/PromiseResolver.zig | 9 ++- src/browser/js/bridge.zig | 1 - .../{window => navigator}/navigator.html | 41 +++++++++++ src/browser/tests/navigator/permissions.html | 37 ---------- src/browser/webapi/Navigator.zig | 11 ++- src/browser/webapi/Permissions.zig | 73 ++++++++++++++----- src/browser/webapi/StorageManager.zig | 63 ++++++++++------ 8 files changed, 155 insertions(+), 86 deletions(-) rename src/browser/tests/{window => navigator}/navigator.html (56%) delete mode 100644 src/browser/tests/navigator/permissions.html diff --git a/src/browser/js/Local.zig b/src/browser/js/Local.zig index a45b35df..f913dafd 100644 --- a/src/browser/js/Local.zig +++ b/src/browser/js/Local.zig @@ -1212,6 +1212,12 @@ pub fn rejectPromise(self: *const Local, value: anytype) !js.Promise { return resolver.promise(); } +pub fn rejectErrorPromise(self: *const Local, value: js.PromiseResolver.RejectError) !js.Promise { + var resolver = js.PromiseResolver.init(self); + resolver.rejectError("Local.rejectPromise", value); + return resolver.promise(); +} + pub fn resolvePromise(self: *const Local, value: anytype) !js.Promise { var resolver = js.PromiseResolver.init(self); resolver.resolve("Local.resolvePromise", value); diff --git a/src/browser/js/PromiseResolver.zig b/src/browser/js/PromiseResolver.zig index 67f04311..6386569a 100644 --- a/src/browser/js/PromiseResolver.zig +++ b/src/browser/js/PromiseResolver.zig @@ -18,7 +18,9 @@ const js = @import("js.zig"); const v8 = js.v8; + const log = @import("../../log.zig"); +const DOMException = @import("../webapi/DOMException.zig"); const PromiseResolver = @This(); @@ -63,14 +65,19 @@ pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype }; } -const RejectError = union(enum) { +pub const RejectError = union(enum) { generic: []const u8, type_error: []const u8, + dom_exception: anyerror, }; pub fn rejectError(self: PromiseResolver, comptime source: []const u8, err: RejectError) void { const handle = switch (err) { .type_error => |str| self.local.isolate.createTypeError(str), .generic => |str| self.local.isolate.createError(str), + .dom_exception => |exception| { + self.reject(source, DOMException.fromError(exception)); + return; + }, }; self._reject(js.Value{ .handle = handle, .local = self.local }) catch |reject_err| { log.err(.bug, "rejectError", .{ .source = source, .err = reject_err, .persistent = false }); diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 1b035837..dd9a81b4 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -726,7 +726,6 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/Console.zig"), @import("../webapi/Crypto.zig"), @import("../webapi/Permissions.zig"), - @import("../webapi/Permissions.zig").PermissionStatus, @import("../webapi/StorageManager.zig"), @import("../webapi/CSS.zig"), @import("../webapi/css/CSSRule.zig"), diff --git a/src/browser/tests/window/navigator.html b/src/browser/tests/navigator/navigator.html similarity index 56% rename from src/browser/tests/window/navigator.html rename to src/browser/tests/navigator/navigator.html index df9f436a..b82c6e38 100644 --- a/src/browser/tests/window/navigator.html +++ b/src/browser/tests/navigator/navigator.html @@ -27,3 +27,44 @@ testing.expectEqual(false, navigator.javaEnabled()); testing.expectEqual(false, navigator.webdriver); + + + + + + + + + diff --git a/src/browser/tests/navigator/permissions.html b/src/browser/tests/navigator/permissions.html deleted file mode 100644 index befecf8b..00000000 --- a/src/browser/tests/navigator/permissions.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - diff --git a/src/browser/webapi/Navigator.zig b/src/browser/webapi/Navigator.zig index 36d32f1e..83b883b1 100644 --- a/src/browser/webapi/Navigator.zig +++ b/src/browser/webapi/Navigator.zig @@ -27,8 +27,8 @@ const StorageManager = @import("StorageManager.zig"); const Navigator = @This(); _pad: bool = false, _plugins: PluginArray = .{}, -_permissions: Permissions = Permissions.init, -_storage: StorageManager = StorageManager.init, +_permissions: Permissions = .{}, +_storage: StorageManager = .{}, pub const init: Navigator = .{}; @@ -70,7 +70,7 @@ pub fn getStorage(self: *Navigator) *StorageManager { pub fn getBattery(_: *const Navigator, page: *Page) !js.Promise { // Battery API is not supported in headless mode. // Return a rejected Promise — callers must .catch() this. - return js.Promise.reject(error.NotSupportedError, page); + return page.js.local.?.rejectErrorPromise(.{ .dom_exception = error.NotSupported }); } pub fn registerProtocolHandler(_: *const Navigator, scheme: []const u8, url: [:0]const u8, page: *const Page) !void { @@ -179,3 +179,8 @@ pub const JsApi = struct { pub const permissions = bridge.accessor(Navigator.getPermissions, null, .{}); pub const storage = bridge.accessor(Navigator.getStorage, null, .{}); }; + +const testing = @import("../../testing.zig"); +test "WebApi: Navigator" { + try testing.htmlRunner("navigator", .{}); +} diff --git a/src/browser/webapi/Permissions.zig b/src/browser/webapi/Permissions.zig index 5f5954b3..ee197d3f 100644 --- a/src/browser/webapi/Permissions.zig +++ b/src/browser/webapi/Permissions.zig @@ -1,28 +1,70 @@ -// src/browser/webapi/Permissions.zig +// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) // -// Minimal Permissions API stub. -// https://www.w3.org/TR/permissions/ +// Francis Bouvier +// Pierre Tachoire // -// Turnstile probes: navigator.permissions.query({ name: 'notifications' }) -// It expects a Promise resolving to { state: 'granted' | 'denied' | 'prompt' } +// 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 std = @import("std"); const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); +const Session = @import("../Session.zig"); + +const Allocator = std.mem.Allocator; + +pub fn registerTypes() []const type { + return &.{ Permissions, PermissionStatus }; +} const Permissions = @This(); // Padding to avoid zero-size struct pointer collisions _pad: bool = false, -pub const init: Permissions = .{}; - const QueryDescriptor = struct { name: []const u8, }; +// We always report 'prompt' (the default safe value — neither granted nor denied). +pub fn query(_: *const Permissions, qd: QueryDescriptor, page: *Page) !js.Promise { + const arena = try page.getArena(.{ .debug = "PermissionStatus" }); + errdefer page.releaseArena(arena); + + const status = try arena.create(PermissionStatus); + status.* = .{ + ._arena = arena, + ._state = "prompt", + ._name = try arena.dupe(u8, qd.name), + }; + return page.js.local.?.resolvePromise(status); +} const PermissionStatus = struct { - state: []const u8, + _arena: Allocator, + _name: []const u8, + _state: []const u8, + + pub fn deinit(self: *PermissionStatus, _: bool, session: *Session) void { + session.releaseArena(self._arena); + } + + fn getName(self: *const PermissionStatus) []const u8 { + return self._name; + } + + fn getState(self: *const PermissionStatus) []const u8 { + return self._state; + } pub const JsApi = struct { pub const bridge = js.Bridge(PermissionStatus); @@ -30,23 +72,14 @@ const PermissionStatus = struct { pub const name = "PermissionStatus"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const empty_with_no_proto = true; + pub const weak = true; + pub const finalizer = bridge.finalizer(PermissionStatus.deinit); }; + pub const name = bridge.accessor(getName, null, .{}); pub const state = bridge.accessor(getState, null, .{}); }; - - fn getState(self: *const PermissionStatus) []const u8 { - return self.state; - } }; -// query() returns a Promise. -// We always report 'prompt' (the default safe value — neither granted nor denied). -pub fn query(_: *const Permissions, _: QueryDescriptor, page: *Page) !js.Promise { - const status = try page._factory.create(PermissionStatus{ .state = "prompt" }); - return js.Promise.resolve(status, page); -} - pub const JsApi = struct { pub const bridge = js.Bridge(Permissions); diff --git a/src/browser/webapi/StorageManager.zig b/src/browser/webapi/StorageManager.zig index 280614a0..e7b95cc4 100644 --- a/src/browser/webapi/StorageManager.zig +++ b/src/browser/webapi/StorageManager.zig @@ -1,18 +1,51 @@ -// src/browser/webapi/StorageManager.zig -// Minimal stub for navigator.storage -// https://storage.spec.whatwg.org/#storagemanager +// 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 Page = @import("../Page.zig"); +pub fn registerTypes() []const type { + return &.{ StorageManager, StorageEstimate }; +} + const StorageManager = @This(); + _pad: bool = false, -pub const init: StorageManager = .{}; +pub fn estimate(_: *const StorageManager, page: *Page) !js.Promise { + const est = try page._factory.create(StorageEstimate{ + ._usage = 0, + ._quota = 1024 * 1024 * 1024, // 1 GiB + }); + return page.js.local.?.resolvePromise(est); +} const StorageEstimate = struct { - quota: u64, - usage: u64, + _quota: u64, + _usage: u64, + + fn getUsage(self: *const StorageEstimate) u64 { + return self._usage; + } + + fn getQuota(self: *const StorageEstimate) u64 { + return self._quota; + } pub const JsApi = struct { pub const bridge = js.Bridge(StorageEstimate); @@ -20,30 +53,12 @@ const StorageEstimate = struct { pub const name = "StorageEstimate"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const empty_with_no_proto = true; }; pub const quota = bridge.accessor(getQuota, null, .{}); pub const usage = bridge.accessor(getUsage, null, .{}); }; - - fn getQuota(self: *const StorageEstimate) u64 { - return self.quota; - } - fn getUsage(self: *const StorageEstimate) u64 { - return self.usage; - } }; -// Returns a resolved Promise with plausible stub values. -// quota = 1GB, usage = 0 (headless browser has no real storage) -pub fn estimate(_: *const StorageManager, page: *Page) !js.Promise { - const est = try page._factory.create(StorageEstimate{ - .quota = 1024 * 1024 * 1024, // 1 GiB - .usage = 0, - }); - return js.Promise.resolve(est, page); -} - pub const JsApi = struct { pub const bridge = js.Bridge(StorageManager); pub const Meta = struct { From 1fa87442b84dcb2356cd10fa19eb05676d464196 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 19 Mar 2026 20:11:03 +0800 Subject: [PATCH 3/3] log not_implemented on navigator.getBattery --- src/browser/webapi/Navigator.zig | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/browser/webapi/Navigator.zig b/src/browser/webapi/Navigator.zig index 83b883b1..218110c9 100644 --- a/src/browser/webapi/Navigator.zig +++ b/src/browser/webapi/Navigator.zig @@ -18,8 +18,12 @@ const std = @import("std"); const builtin = @import("builtin"); + +const log = @import("../../log.zig"); + const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); + const PluginArray = @import("PluginArray.zig"); const Permissions = @import("Permissions.zig"); const StorageManager = @import("StorageManager.zig"); @@ -68,8 +72,7 @@ pub fn getStorage(self: *Navigator) *StorageManager { } pub fn getBattery(_: *const Navigator, page: *Page) !js.Promise { - // Battery API is not supported in headless mode. - // Return a rejected Promise — callers must .catch() this. + log.info(.not_implemented, "navigator.getBattery", .{}); return page.js.local.?.rejectErrorPromise(.{ .dom_exception = error.NotSupported }); }