mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-08-02 10:47:15 -04:00
webapi: supported HTMLCollection properties can't be deleted
Fixes 2 failing tests in WPT /dom/collections/HTMLCollection-delete.html
("Strict id" and "Strict name"): deleting a supported indexed or named
property of an HTMLCollection must fail (and throw a TypeError in strict
mode), since those properties are backed by the live collection.
HTMLCollection registered no deleter interceptors, so `delete c[0]` and
`delete c.name` fell through to the ordinary [[Delete]] which succeeded
on the (non-own) property.
HTMLCollection now registers an indexed and a named deleter (via the
bridge's indexedReadWrite/namedIndexed deleter callbacks) that return
false for supported properties - which v8 turns into a TypeError in
strict mode - and error.NotHandled for unsupported ones, leaving those
to the ordinary path.
(As originally written, this commit also added indexed setter/deleter
interceptor support to the js bridge; main gained equivalent support
independently, so the rebase keeps main's bridge API.)
Coverage: /dom/collections/HTMLCollection-delete.html 2/4 -> 4/4.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -209,59 +209,6 @@ fn _getIndex(comptime T: type, local: *const Local, func: anytype, idx: u32, inf
|
||||
return handleIndexedReturn(T, F, true, local, ret, info, opts);
|
||||
}
|
||||
|
||||
pub fn setIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, js_value: *const v8.Value, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u32 {
|
||||
const local = &self.local;
|
||||
|
||||
var hs: js.HandleScope = undefined;
|
||||
hs.init(local.isolate);
|
||||
defer hs.deinit();
|
||||
|
||||
const info = PropertyCallbackInfo{ .handle = handle };
|
||||
return _setIndex(T, local, func, idx, .{ .local = &self.local, .handle = js_value }, info, opts) catch |err| {
|
||||
handleError(T, @TypeOf(func), local, err, info);
|
||||
return js.Intercepted.no;
|
||||
};
|
||||
}
|
||||
|
||||
fn _setIndex(comptime T: type, local: *const Local, func: anytype, idx: u32, js_value: js.Value, info: PropertyCallbackInfo, comptime opts: CallOpts) !u32 {
|
||||
const F = @TypeOf(func);
|
||||
var args: ParameterTypes(F) = undefined;
|
||||
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||
@field(args, "1") = idx;
|
||||
@field(args, "2") = try local.jsValueToZig(@TypeOf(@field(args, "2")), js_value);
|
||||
if (@typeInfo(F).@"fn".params.len == 4) {
|
||||
@field(args, "3") = getGlobalArg(@TypeOf(args.@"3"), local.ctx);
|
||||
}
|
||||
const ret = @call(.auto, func, args);
|
||||
return handleIndexedReturn(T, F, false, local, ret, info, opts);
|
||||
}
|
||||
|
||||
pub fn deleteIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u32 {
|
||||
const local = &self.local;
|
||||
|
||||
var hs: js.HandleScope = undefined;
|
||||
hs.init(local.isolate);
|
||||
defer hs.deinit();
|
||||
|
||||
const info = PropertyCallbackInfo{ .handle = handle };
|
||||
return _deleteIndex(T, local, func, idx, info, opts) catch |err| {
|
||||
handleError(T, @TypeOf(func), local, err, info);
|
||||
return js.Intercepted.no;
|
||||
};
|
||||
}
|
||||
|
||||
fn _deleteIndex(comptime T: type, local: *const Local, func: anytype, idx: u32, info: PropertyCallbackInfo, comptime opts: CallOpts) !u32 {
|
||||
const F = @TypeOf(func);
|
||||
var args: ParameterTypes(F) = undefined;
|
||||
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||
@field(args, "1") = idx;
|
||||
if (@typeInfo(F).@"fn".params.len == 3) {
|
||||
@field(args, "2") = getGlobalArg(@TypeOf(args.@"2"), local.ctx);
|
||||
}
|
||||
const ret = @call(.auto, func, args);
|
||||
return handleIndexedReturn(T, F, false, local, ret, info, opts);
|
||||
}
|
||||
|
||||
pub fn getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u32 {
|
||||
const local = &self.local;
|
||||
|
||||
@@ -288,6 +235,59 @@ fn _getNamedIndex(comptime T: type, local: *const Local, func: anytype, name: *c
|
||||
return handleIndexedReturn(T, F, true, local, ret, info, opts);
|
||||
}
|
||||
|
||||
pub fn setIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, js_value: *const v8.Value, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u32 {
|
||||
const local = &self.local;
|
||||
|
||||
var hs: js.HandleScope = undefined;
|
||||
hs.init(local.isolate);
|
||||
defer hs.deinit();
|
||||
|
||||
const info = PropertyCallbackInfo{ .handle = handle };
|
||||
return _setIndex(T, local, func, idx, .{ .local = &self.local, .handle = js_value }, info, opts) catch |err| {
|
||||
handleError(T, @TypeOf(func), local, err, info);
|
||||
return js.Intercepted.no;
|
||||
};
|
||||
}
|
||||
|
||||
fn _setIndex(comptime T: type, local: *const Local, func: anytype, idx: u32, js_value: js.Value, info: PropertyCallbackInfo, comptime opts: CallOpts) !u32 {
|
||||
const F = @TypeOf(func);
|
||||
var args: ParameterTypes(F) = undefined;
|
||||
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||
@field(args, "1") = idx;
|
||||
@field(args, "2") = try local.jsValueToZig(@TypeOf(@field(args, "2")), js_value);
|
||||
if (@typeInfo(F).@"fn".params.len == 4) {
|
||||
@field(args, "3") = getGlobalArg(@TypeOf(args.@"3"), local.ctx);
|
||||
}
|
||||
const ret = @call(.auto, func, args);
|
||||
return handleIndexedReturn(T, F, comptime returnsBool(F), local, ret, info, opts);
|
||||
}
|
||||
|
||||
pub fn deleteIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u32 {
|
||||
const local = &self.local;
|
||||
|
||||
var hs: js.HandleScope = undefined;
|
||||
hs.init(local.isolate);
|
||||
defer hs.deinit();
|
||||
|
||||
const info = PropertyCallbackInfo{ .handle = handle };
|
||||
return _deleteIndex(T, local, func, idx, info, opts) catch |err| {
|
||||
handleError(T, @TypeOf(func), local, err, info);
|
||||
return js.Intercepted.no;
|
||||
};
|
||||
}
|
||||
|
||||
fn _deleteIndex(comptime T: type, local: *const Local, func: anytype, idx: u32, info: PropertyCallbackInfo, comptime opts: CallOpts) !u32 {
|
||||
const F = @TypeOf(func);
|
||||
var args: ParameterTypes(F) = undefined;
|
||||
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||
@field(args, "1") = idx;
|
||||
if (@typeInfo(F).@"fn".params.len == 3) {
|
||||
@field(args, "2") = getGlobalArg(@TypeOf(args.@"2"), local.ctx);
|
||||
}
|
||||
const ret = @call(.auto, func, args);
|
||||
return handleIndexedReturn(T, F, comptime returnsBool(F), local, ret, info, opts);
|
||||
}
|
||||
|
||||
pub fn setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, js_value: *const v8.Value, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u32 {
|
||||
const local = &self.local;
|
||||
|
||||
@@ -312,7 +312,7 @@ fn _setNamedIndex(comptime T: type, local: *const Local, func: anytype, name: *c
|
||||
@field(args, "3") = getGlobalArg(@TypeOf(args.@"3"), local.ctx);
|
||||
}
|
||||
const ret = @call(.auto, func, args);
|
||||
return handleIndexedReturn(T, F, false, local, ret, info, opts);
|
||||
return handleIndexedReturn(T, F, comptime returnsBool(F), local, ret, info, opts);
|
||||
}
|
||||
|
||||
pub fn deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u32 {
|
||||
@@ -338,7 +338,7 @@ fn _deleteNamedIndex(comptime T: type, local: *const Local, func: anytype, name:
|
||||
@field(args, "2") = getGlobalArg(@TypeOf(args.@"2"), local.ctx);
|
||||
}
|
||||
const ret = @call(.auto, func, args);
|
||||
return handleIndexedReturn(T, F, false, local, ret, info, opts);
|
||||
return handleIndexedReturn(T, F, comptime returnsBool(F), local, ret, info, opts);
|
||||
}
|
||||
|
||||
pub fn getEnumerator(self: *Caller, comptime T: type, func: anytype, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u32 {
|
||||
@@ -453,6 +453,18 @@ fn handleIndexedReturn(comptime T: type, comptime F: type, comptime with_value:
|
||||
return js.Intercepted.yes;
|
||||
}
|
||||
|
||||
// Setter/deleter interceptors normally return void: intercepting is enough
|
||||
// to mark the operation successful. When they return a bool instead, it is
|
||||
// forwarded as the v8 return value; false marks the operation as failed,
|
||||
// which makes v8 throw a TypeError in strict mode.
|
||||
fn returnsBool(comptime F: type) bool {
|
||||
const RT = @typeInfo(F).@"fn".return_type.?;
|
||||
return switch (@typeInfo(RT)) {
|
||||
.error_union => |eu| eu.payload == bool,
|
||||
else => RT == bool,
|
||||
};
|
||||
}
|
||||
|
||||
fn isInErrorSet(err: anyerror, comptime T: type) bool {
|
||||
inline for (@typeInfo(T).error_set.?) |e| {
|
||||
if (err == @field(anyerror, e.name)) return true;
|
||||
|
||||
@@ -146,7 +146,7 @@ pub const JsApi = struct {
|
||||
};
|
||||
|
||||
pub const length = bridge.accessor(HTMLCollection.length, null, .{});
|
||||
pub const @"[int]" = bridge.indexed(HTMLCollection.getAtIndex, null, .{ .null_as_undefined = true });
|
||||
pub const @"[int]" = bridge.indexedReadWrite(HTMLCollection.getAtIndex, null, deleteAtIndex, null, null, .{ .null_as_undefined = true });
|
||||
pub const @"[str]" = bridge.namedIndexed(struct {
|
||||
pub fn wrap(self: *HTMLCollection, name: []const u8, frame: *Frame) !?*Element {
|
||||
if (name.len == 0) {
|
||||
@@ -155,7 +155,24 @@ pub const JsApi = struct {
|
||||
|
||||
return self.getByName(name, frame) orelse error.NotHandled;
|
||||
}
|
||||
}.wrap, null, null, null, null, .{ .null_as_undefined = true });
|
||||
}.wrap, null, deleteByName, null, null, .{ .null_as_undefined = true });
|
||||
|
||||
// Supported indexed and named properties can't be deleted (delete returns
|
||||
// false, which throws a TypeError in strict mode); unsupported ones follow
|
||||
// the ordinary [[Delete]] path.
|
||||
fn deleteAtIndex(self: *HTMLCollection, idx: u32, frame: *Frame) !bool {
|
||||
if (idx < self.length(frame)) {
|
||||
return false;
|
||||
}
|
||||
return error.NotHandled;
|
||||
}
|
||||
|
||||
fn deleteByName(self: *HTMLCollection, name: []const u8, frame: *Frame) !bool {
|
||||
if (self.getByName(name, frame) != null) {
|
||||
return false;
|
||||
}
|
||||
return error.NotHandled;
|
||||
}
|
||||
|
||||
pub const item = bridge.function(_item, .{});
|
||||
fn _item(self: *HTMLCollection, index: i32, frame: *Frame) ?*Element {
|
||||
|
||||
Reference in New Issue
Block a user