mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-08-04 11:44:19 -04:00
wpt: improve webcrypto WPT results
Adds a proper QuoteExceededError WebApi. Handles more invalid parameters. The most significant change is that types which return a promise don't return an error, they reject. This is an issue that we run into a lot, and this commit has started to work on a more generic (i.e. in the bridge) solution.
This commit is contained in:
@@ -605,10 +605,29 @@ fn handleError(comptime T: type, comptime F: type, local: *const Local, err: any
|
||||
};
|
||||
};
|
||||
|
||||
if (comptime returnsPromise(F) and @TypeOf(info) == FunctionCallbackInfo) {
|
||||
// An operation that returns a promise must not throw. It rejects.
|
||||
const resolver = js.PromiseResolver.init(&err_local);
|
||||
resolver.rejectValue(.{ .local = &err_local, .handle = js_err }) catch |reject_err| {
|
||||
log.err(.bug, "handleError reject", .{ .err = reject_err });
|
||||
};
|
||||
info.getReturnValue().set(resolver.promise().toValue());
|
||||
return;
|
||||
}
|
||||
|
||||
const js_exception = isolate.throwException(js_err);
|
||||
info.getReturnValue().setValueHandle(js_exception);
|
||||
}
|
||||
|
||||
fn returnsPromise(comptime F: type) bool {
|
||||
const R = @typeInfo(F).@"fn".return_type orelse return false;
|
||||
const payload = switch (@typeInfo(R)) {
|
||||
.error_union => |eu| eu.payload,
|
||||
else => R,
|
||||
};
|
||||
return payload == js.Promise;
|
||||
}
|
||||
|
||||
// Convert a Zig error to a DOMException. If the error is unknown, return null.
|
||||
fn domExceptionToJs(local: *const Local, err: anyerror) ?*const v8.Value {
|
||||
const DOMException = @import("../webapi/DOMException.zig");
|
||||
|
||||
@@ -108,13 +108,19 @@ pub fn rejectError(
|
||||
fn _reject(self: PromiseResolver, value: anytype) !void {
|
||||
const local = self.local;
|
||||
const js_val = try local.zigValueToJs(value, .{});
|
||||
try self.rejectValue(js_val);
|
||||
local.runMicrotasks();
|
||||
}
|
||||
|
||||
/// Rejects with an already-built JS value, without running a microtask
|
||||
/// checkpoint. For use while the calling script is still on the stack, where
|
||||
/// draining the queue would run unrelated reactions mid-script.
|
||||
pub fn rejectValue(self: PromiseResolver, value: js.Value) !void {
|
||||
var out: v8.MaybeBool = undefined;
|
||||
v8.v8__Promise__Resolver__Reject(self.handle, local.handle, js_val.handle, &out);
|
||||
v8.v8__Promise__Resolver__Reject(self.handle, self.local.handle, value.handle, &out);
|
||||
if (!out.has_value or !out.value) {
|
||||
return error.FailedToRejectPromise;
|
||||
}
|
||||
local.runMicrotasks();
|
||||
}
|
||||
|
||||
pub fn persist(self: PromiseResolver) !Global {
|
||||
|
||||
@@ -962,6 +962,7 @@ pub const PageJsApis = flattenTypes(&.{
|
||||
@import("../webapi/DocumentType.zig"),
|
||||
@import("../webapi/ShadowRoot.zig"),
|
||||
@import("../webapi/DOMException.zig"),
|
||||
@import("../webapi/QuotaExceededError.zig"),
|
||||
@import("../webapi/DOMImplementation.zig"),
|
||||
@import("../webapi/DOMTreeWalker.zig"),
|
||||
@import("../webapi/DOMNodeIterator.zig"),
|
||||
@@ -1229,6 +1230,7 @@ const worker_common_apis = [_]type{
|
||||
@import("../webapi/event/PromiseRejectionEvent.zig"),
|
||||
@import("../webapi/event/CloseEvent.zig"),
|
||||
@import("../webapi/DOMException.zig"),
|
||||
@import("../webapi/QuotaExceededError.zig"),
|
||||
@import("../webapi/DOMRectReadOnly.zig"),
|
||||
@import("../webapi/DOMRect.zig"),
|
||||
@import("../webapi/DOMMatrixReadOnly.zig"),
|
||||
|
||||
@@ -20,6 +20,7 @@ const lp = @import("lightpanda");
|
||||
const js = @import("../js/js.zig");
|
||||
|
||||
const SubtleCrypto = @import("SubtleCrypto.zig");
|
||||
const QuotaExceededError = @import("QuotaExceededError.zig");
|
||||
|
||||
const Crypto = @This();
|
||||
_subtle: SubtleCrypto = .{},
|
||||
@@ -28,11 +29,17 @@ pub const init: Crypto = .{};
|
||||
|
||||
// We take a js.Value, because we want to return the same instance, not a new
|
||||
// TypedArray
|
||||
pub fn getRandomValues(_: *const Crypto, js_obj: js.Object) !js.Object {
|
||||
pub fn getRandomValues(_: *const Crypto, js_obj: js.Object, exec: *const js.Execution) !js.Object {
|
||||
const value = js_obj.toValue();
|
||||
if (value.isFloat16Array() or value.isFloat32Array() or value.isFloat64Array() or (value.isArrayBufferView() and !value.isTypedArray())) {
|
||||
// only integer TypedArrays are supported
|
||||
return error.TypeMismatch;
|
||||
}
|
||||
|
||||
var into = try js_obj.toZig(RandomValues);
|
||||
const buf = into.asBuffer();
|
||||
if (buf.len > 65_536) {
|
||||
return error.QuotaExceeded;
|
||||
return QuotaExceededError.throw(js_obj.local, exec);
|
||||
}
|
||||
lp.io.random(buf);
|
||||
return js_obj;
|
||||
|
||||
@@ -50,6 +50,7 @@ pub fn fromError(err: anyerror) ?DOMException {
|
||||
error.InvalidModification => .{ ._code = .invalid_modification_error },
|
||||
error.NamespaceError => .{ ._code = .namespace_error },
|
||||
error.InvalidAccess => .{ ._code = .invalid_access_error },
|
||||
error.TypeMismatch => .{ ._code = .type_mismatch_error },
|
||||
error.SecurityError => .{ ._code = .security_error },
|
||||
error.NetworkError => .{ ._code = .network_error },
|
||||
error.AbortError => .{ ._code = .abort_error },
|
||||
@@ -97,6 +98,7 @@ pub fn getName(self: *const DOMException) []const u8 {
|
||||
.invalid_modification_error => "InvalidModificationError",
|
||||
.namespace_error => "NamespaceError",
|
||||
.invalid_access_error => "InvalidAccessError",
|
||||
.type_mismatch_error => "TypeMismatchError",
|
||||
.security_error => "SecurityError",
|
||||
.network_error => "NetworkError",
|
||||
.abort_error => "AbortError",
|
||||
@@ -133,6 +135,7 @@ pub fn getMessage(self: *const DOMException) []const u8 {
|
||||
.invalid_modification_error => "The object can not be modified in this way",
|
||||
.namespace_error => "The operation is not allowed by Namespaces in XML",
|
||||
.invalid_access_error => "The object does not support the operation or argument",
|
||||
.type_mismatch_error => "The type of an object was incompatible with the expected type of the parameter associated to the object",
|
||||
.security_error => "The operation is insecure",
|
||||
.network_error => "A network error occurred",
|
||||
.abort_error => "The operation was aborted",
|
||||
@@ -178,6 +181,7 @@ const Code = enum(u8) {
|
||||
invalid_modification_error = 13,
|
||||
namespace_error = 14,
|
||||
invalid_access_error = 15,
|
||||
type_mismatch_error = 17,
|
||||
security_error = 18,
|
||||
network_error = 19,
|
||||
abort_error = 20,
|
||||
@@ -216,6 +220,7 @@ const Code = enum(u8) {
|
||||
.{ "InvalidModificationError", .invalid_modification_error },
|
||||
.{ "NamespaceError", .namespace_error },
|
||||
.{ "InvalidAccessError", .invalid_access_error },
|
||||
.{ "TypeMismatchError", .type_mismatch_error },
|
||||
.{ "SecurityError", .security_error },
|
||||
.{ "NetworkError", .network_error },
|
||||
.{ "AbortError", .abort_error },
|
||||
|
||||
88
src/browser/webapi/QuotaExceededError.zig
Normal file
88
src/browser/webapi/QuotaExceededError.zig
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
|
||||
const DOMException = @import("DOMException.zig");
|
||||
|
||||
const Execution = js.Execution;
|
||||
|
||||
const QuotaExceededError = @This();
|
||||
|
||||
pub const Proto = DOMException;
|
||||
|
||||
_proto: *DOMException,
|
||||
_quota: ?f64 = null,
|
||||
_requested: ?f64 = null,
|
||||
|
||||
const Options = struct {
|
||||
quota: ?f64 = null,
|
||||
requested: ?f64 = null,
|
||||
};
|
||||
|
||||
pub fn init(message_: ?[]const u8, options_: ?Options, exec: *const Execution) !*QuotaExceededError {
|
||||
const opts = options_ orelse Options{};
|
||||
if (opts.quota) |quota| {
|
||||
if (quota < 0) {
|
||||
return error.RangeError;
|
||||
}
|
||||
}
|
||||
if (opts.requested) |requested| {
|
||||
if (requested < 0) {
|
||||
return error.RangeError;
|
||||
}
|
||||
}
|
||||
if (opts.quota != null and opts.requested != null and opts.requested.? < opts.quota.?) {
|
||||
return error.RangeError;
|
||||
}
|
||||
|
||||
const message = if (message_) |m| try exec.dupeString(m) else null;
|
||||
return exec._factory.chained(.{
|
||||
DOMException.init(message, "QuotaExceededError"),
|
||||
QuotaExceededError{ ._proto = undefined, ._quota = opts.quota, ._requested = opts.requested },
|
||||
});
|
||||
}
|
||||
|
||||
pub fn throw(local: *const js.Local, exec: *const Execution) error{ TryCatchRethrow, OutOfMemory } {
|
||||
const self = init(null, null, exec) catch return error.OutOfMemory;
|
||||
const js_val = local.zigValueToJs(self, .{}) catch return error.OutOfMemory;
|
||||
_ = local.isolate.throwException(js_val.handle);
|
||||
return error.TryCatchRethrow;
|
||||
}
|
||||
|
||||
pub fn getQuota(self: *const QuotaExceededError) ?f64 {
|
||||
return self._quota;
|
||||
}
|
||||
|
||||
pub fn getRequested(self: *const QuotaExceededError) ?f64 {
|
||||
return self._requested;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(QuotaExceededError);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "QuotaExceededError";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(QuotaExceededError.init, .{});
|
||||
pub const quota = bridge.accessor(QuotaExceededError.getQuota, null, .{});
|
||||
pub const requested = bridge.accessor(QuotaExceededError.getRequested, null, .{});
|
||||
};
|
||||
@@ -524,10 +524,10 @@ pub fn verify(
|
||||
/// or an object (`{name: "SHA-256"}`). The object variant must come first — a
|
||||
/// `[]const u8` coerces *any* JS value to a string, so it has to be the fallback.
|
||||
const DigestInput = union(enum) {
|
||||
obj: struct { name: []const u8 },
|
||||
obj: struct { name: ?[]const u8 = null },
|
||||
str: []const u8,
|
||||
|
||||
fn name(self: DigestInput) []const u8 {
|
||||
fn name(self: DigestInput) ?[]const u8 {
|
||||
return switch (self) {
|
||||
.obj => |o| o.name,
|
||||
.str => |s| s,
|
||||
@@ -539,7 +539,9 @@ const DigestInput = union(enum) {
|
||||
pub fn digest(_: *const SubtleCrypto, algo: DigestInput, data: js.TypedArray(u8), exec: *const Execution) !js.Promise {
|
||||
const local = exec.js.local.?;
|
||||
|
||||
const algo_name = algo.name();
|
||||
const algo_name = algo.name() orelse {
|
||||
return local.rejectPromise(.{ .type_error = "required member name is undefined" });
|
||||
};
|
||||
if (algo_name.len > 10) {
|
||||
return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
|
||||
}
|
||||
|
||||
@@ -37,6 +37,11 @@ pub fn init(
|
||||
exec: *const Execution,
|
||||
) !js.Promise {
|
||||
const local = exec.js.local.?;
|
||||
// The union probe match to get here is pretty simple, so we can end up here
|
||||
// for an unknown/invalid algo.
|
||||
if (!std.ascii.eqlIgnoreCase(params.name, "HMAC")) {
|
||||
return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
|
||||
}
|
||||
const hash_name = switch (params.hash) {
|
||||
.string => |str| str,
|
||||
.object => |obj| obj.name,
|
||||
|
||||
@@ -26,12 +26,13 @@ const CryptoKey = @import("../CryptoKey.zig");
|
||||
pub const Init = union(enum) {
|
||||
/// For RSASSA-PKCS1-v1_5, RSA-PSS, or RSA-OAEP: pass an RsaHashedKeyGenParams object.
|
||||
rsa_hashed_key_gen: RsaHashedKeyGen,
|
||||
/// Must be before `hmac_key_gen`, since we need to ignore this param even
|
||||
/// if it has a 'hash' property, and if it's after, we'll match that instead.
|
||||
ec_key_gen: EcKeyGen,
|
||||
/// For HMAC: pass an HmacKeyGenParams object.
|
||||
hmac_key_gen: HmacKeyGen,
|
||||
/// For AES variants: pass an AesKeyGenParams object.
|
||||
aes_key_gen: AesKeyGen,
|
||||
/// For ECDSA / ECDH: pass an EcKeyGenParams object.
|
||||
ec_key_gen: EcKeyGen,
|
||||
|
||||
/// don't use []const u8 here, we don't want non-strings coerced. Let those
|
||||
/// fall to the invalid case
|
||||
|
||||
Reference in New Issue
Block a user