diff --git a/src/browser/js/Caller.zig b/src/browser/js/Caller.zig index 416d70cc0..db1caa59b 100644 --- a/src/browser/js/Caller.zig +++ b/src/browser/js/Caller.zig @@ -719,6 +719,10 @@ pub const Function = struct { exposed: Exposed = .both, ce_reactions: bool = false, js_name: ?[:0]const u8 = null, + // Web IDL [LegacyUnforgeable]: the accessor is an own property of + // every instance (including subclass instances) instead of living on + // the interface prototype object. + unforgeable: bool = false, pub const Exposed = enum { both, window, worker }; diff --git a/src/browser/js/Snapshot.zig b/src/browser/js/Snapshot.zig index 63ad37b0f..450f415ba 100644 --- a/src/browser/js/Snapshot.zig +++ b/src/browser/js/Snapshot.zig @@ -757,6 +757,52 @@ fn inheritsFromHtmlElement(comptime JsApi: type) bool { // and define_on != null. This is the "flattening" pass, and it defines all of // the functions/accessors on directly on the global instance. Thus, globals have // it defined on both their prototype (first pass) and their own instance (2nd pass). +fn attachAccessorProperty(comptime name: [:0]const u8, value: bridge.Accessor, isolate: *v8.Isolate, template: *const v8.FunctionTemplate, signature: anytype, target: anytype) void { + const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len)); + const getter_signature = if (value.static) null else signature; + const getter_callback = v8.v8__FunctionTemplate__New__Config(isolate, &.{ + .callback = value.getter, + .signature = getter_signature, + }).?; + // WebIDL: getter function's .name should be "get X" + const getter_name_str = "get " ++ name; + const getter_name_v8 = v8.v8__String__NewFromUtf8(isolate, getter_name_str.ptr, v8.kNormal, @intCast(getter_name_str.len)); + v8.v8__FunctionTemplate__SetClassName(getter_callback, getter_name_v8); + + const setter_callback = if (value.setter) |setter| blk: { + const cb = v8.v8__FunctionTemplate__New__Config(isolate, &.{ + .callback = setter, + .signature = getter_signature, + .length = 1, + }).?; + const setter_name_str = "set " ++ name; + const setter_name_v8 = v8.v8__String__NewFromUtf8(isolate, setter_name_str.ptr, v8.kNormal, @intCast(setter_name_str.len)); + v8.v8__FunctionTemplate__SetClassName(cb, setter_name_v8); + break :blk cb; + } else null; + + var attribute: v8.PropertyAttribute = 0; + if (value.setter == null) { + attribute |= v8.ReadOnly; + } + if (value.deletable == false) { + attribute |= v8.DontDelete; + } + + if (value.static) { + v8.v8__Template__SetAccessorProperty(@ptrCast(template), js_name, getter_callback, setter_callback, attribute); + } else { + // Web IDL: attributes on the interface prototype object + // (and mirrored onto [Global] instances) are enumerable. + v8.v8__ObjectTemplate__SetAccessorProperty__Config(target, &.{ + .key = js_name, + .getter = getter_callback, + .setter = setter_callback, + .attribute = attribute, + }); + } +} + fn attachClass(comptime JsApi: type, comptime flatten: bool, isolate: *v8.Isolate, template: *const v8.FunctionTemplate, define_on: ?*const v8.ObjectTemplate) void { const instance = v8.v8__FunctionTemplate__InstanceTemplate(template); const prototype = v8.v8__FunctionTemplate__PrototypeTemplate(template); @@ -792,49 +838,10 @@ fn attachClass(comptime JsApi: type, comptime flatten: bool, isolate: *v8.Isolat continue; } - const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len)); - const getter_signature = if (value.static) null else signature; - const getter_callback = v8.v8__FunctionTemplate__New__Config(isolate, &.{ - .callback = value.getter, - .signature = getter_signature, - }).?; - // WebIDL: getter function's .name should be "get X" - const getter_name_str = "get " ++ name; - const getter_name_v8 = v8.v8__String__NewFromUtf8(isolate, getter_name_str.ptr, v8.kNormal, @intCast(getter_name_str.len)); - v8.v8__FunctionTemplate__SetClassName(getter_callback, getter_name_v8); - - const setter_callback = if (value.setter) |setter| blk: { - const cb = v8.v8__FunctionTemplate__New__Config(isolate, &.{ - .callback = setter, - .signature = getter_signature, - .length = 1, - }).?; - const setter_name_str = "set " ++ name; - const setter_name_v8 = v8.v8__String__NewFromUtf8(isolate, setter_name_str.ptr, v8.kNormal, @intCast(setter_name_str.len)); - v8.v8__FunctionTemplate__SetClassName(cb, setter_name_v8); - break :blk cb; - } else null; - - var attribute: v8.PropertyAttribute = 0; - if (value.setter == null) { - attribute |= v8.ReadOnly; - } - if (value.deletable == false) { - attribute |= v8.DontDelete; - } - - if (value.static) { - v8.v8__Template__SetAccessorProperty(@ptrCast(template), js_name, getter_callback, setter_callback, attribute); - } else { - // Web IDL: attributes on the interface prototype object - // (and mirrored onto [Global] instances) are enumerable. - v8.v8__ObjectTemplate__SetAccessorProperty__Config(define_on orelse prototype, &.{ - .key = js_name, - .getter = getter_callback, - .setter = setter_callback, - .attribute = attribute, - }); - } + // [LegacyUnforgeable] accessors live on the instances, not on + // the interface prototype object. + const target = if (value.unforgeable) instance else (define_on orelse prototype); + attachAccessorProperty(name, value, isolate, template, signature, target); }, bridge.Function => { if (value.wpt_only and wpt_extensions_enabled == false) { @@ -920,6 +927,26 @@ fn attachClass(comptime JsApi: type, comptime flatten: bool, isolate: *v8.Isolat } } + // Web IDL [LegacyUnforgeable]: unforgeable accessors of ancestor + // interfaces are own properties of every subclass instance too. v8's + // Inherit only chains prototype templates, not instance templates, so + // re-install them on this class's instance template. + if (comptime !flatten) { + comptime var T = JsApi.bridge.type; + inline while (comptime @hasField(T, "_proto")) { + const P = @typeInfo(std.meta.fieldInfo(T, ._proto).type).pointer.child; + inline for (@typeInfo(P.JsApi).@"struct".decls) |pd| { + const pvalue = @field(P.JsApi, pd.name); + if (comptime @TypeOf(pvalue) == bridge.Accessor) { + if (comptime (pvalue.unforgeable and !pvalue.static)) { + attachAccessorProperty(pd.name, pvalue, isolate, template, signature, instance); + } + } + } + T = P; + } + } + // The remaining per-class setup targets the class's own instance template; // in [Global] flattening mode the global already has these (or doesn't need // them), so skip it. diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 9ff8a0ee4..d535b7e79 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -230,6 +230,7 @@ pub const Accessor = struct { static: bool = false, deletable: bool = true, wpt_only: bool = false, + unforgeable: bool = false, exposed: Caller.Function.Opts.Exposed = .both, cache: ?Caller.Function.Opts.Caching = null, getter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null, @@ -241,6 +242,7 @@ pub const Accessor = struct { .static = opts.static, .wpt_only = opts.wpt_only, .deletable = opts.deletable, + .unforgeable = opts.unforgeable, .exposed = opts.exposed, }; diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index 3fac87066..173c1a23e 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -481,7 +481,9 @@ pub const JsApi = struct { pub const eventPhase = bridge.accessor(Event.getEventPhase, null, .{}); pub const defaultPrevented = bridge.accessor(Event.getDefaultPrevented, null, .{}); pub const timeStamp = bridge.accessor(Event.getTimeStamp, null, .{}); - pub const isTrusted = bridge.accessor(Event.getIsTrusted, null, .{}); + // [LegacyUnforgeable]: an own, non-configurable accessor of every + // Event (and subclass) instance. + pub const isTrusted = bridge.accessor(Event.getIsTrusted, null, .{ .unforgeable = true, .deletable = false }); pub const preventDefault = bridge.function(Event.preventDefault, .{}); pub const stopPropagation = bridge.function(Event.stopPropagation, .{}); pub const stopImmediatePropagation = bridge.function(Event.stopImmediatePropagation, .{});