mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 01:25:53 -04:00
Merge branch 'main' into agent
This commit is contained in:
@@ -28,6 +28,9 @@ const ArenaPool = @This();
|
||||
|
||||
const IS_DEBUG = builtin.mode == .Debug;
|
||||
|
||||
// In Debug, disable pooling to better catch UAF.
|
||||
const SAFETY = IS_DEBUG == true and builtin.is_test == false;
|
||||
|
||||
pub const BucketSize = enum { tiny, small, medium, large };
|
||||
|
||||
const Bucket = struct {
|
||||
@@ -187,7 +190,8 @@ pub fn release(self: *ArenaPool, allocator: Allocator) void {
|
||||
}
|
||||
}
|
||||
|
||||
if (bucket.free_list_len >= bucket.free_list_max) {
|
||||
if ((comptime SAFETY) or bucket.free_list_len >= bucket.free_list_max) {
|
||||
// In Debug, we never pool. It can mask UAF bugs.
|
||||
arena.deinit();
|
||||
self.entry_pool.destroy(entry);
|
||||
return;
|
||||
@@ -200,12 +204,14 @@ pub fn release(self: *ArenaPool, allocator: Allocator) void {
|
||||
|
||||
pub fn reset(_: *const ArenaPool, allocator: Allocator, retain: usize) void {
|
||||
const arena: *ArenaAllocator = @ptrCast(@alignCast(allocator.ptr));
|
||||
_ = arena.reset(.{ .retain_with_limit = retain });
|
||||
// In Debug, free_all, it's less likely to hide things
|
||||
_ = arena.reset(if (comptime SAFETY) .free_all else .{ .retain_with_limit = retain });
|
||||
}
|
||||
|
||||
pub fn resetRetain(_: *const ArenaPool, allocator: Allocator) void {
|
||||
const arena: *ArenaAllocator = @ptrCast(@alignCast(allocator.ptr));
|
||||
_ = arena.reset(.retain_capacity);
|
||||
// In Debug, free_all, it's less likely to hide things
|
||||
_ = arena.reset(if (comptime SAFETY) .free_all else .retain_capacity);
|
||||
}
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
@@ -2592,10 +2592,10 @@ pub fn createElementNS(self: *Frame, namespace: Element.Namespace, name: []const
|
||||
.{ ._proto = undefined },
|
||||
),
|
||||
asUint("marquee") => return self.createHtmlElementT(
|
||||
Element.Html.Generic,
|
||||
Element.Html.Marquee,
|
||||
namespace,
|
||||
attribute_iterator,
|
||||
.{ ._proto = undefined, ._tag_name = comptime .wrap("marquee"), ._tag = .marquee },
|
||||
.{ ._proto = undefined },
|
||||
),
|
||||
asUint("address") => return self.createHtmlElementT(
|
||||
Element.Html.Generic,
|
||||
|
||||
@@ -38,6 +38,8 @@ const JsApis = bridge.JsApis;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const IS_DEBUG = builtin.mode == .Debug;
|
||||
|
||||
const MAX_CONTEXTS = if (lp.build_config.wpt_extensions) 8192 else 128;
|
||||
|
||||
fn initClassIds() void {
|
||||
inline for (JsApis, 0..) |JsApi, i| {
|
||||
JsApi.Meta.class_id = i;
|
||||
@@ -63,8 +65,7 @@ platform: *const Platform,
|
||||
// the global isolate
|
||||
isolate: js.Isolate,
|
||||
|
||||
contexts: [64]*Context,
|
||||
context_count: usize,
|
||||
contexts: std.ArrayList(*Context),
|
||||
|
||||
// just kept around because we need to free it on deinit
|
||||
isolate_params: *v8.CreateParams,
|
||||
@@ -184,8 +185,7 @@ pub fn init(app: *App, opts: InitOpts) !Env {
|
||||
.app = app,
|
||||
.context_id = 0,
|
||||
.allocator = allocator,
|
||||
.contexts = undefined,
|
||||
.context_count = 0,
|
||||
.contexts = .empty,
|
||||
.isolate = isolate,
|
||||
.platform = &app.platform,
|
||||
.templates = templates,
|
||||
@@ -199,11 +199,12 @@ pub fn init(app: *App, opts: InitOpts) !Env {
|
||||
|
||||
pub fn deinit(self: *Env) void {
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(self.context_count == 0);
|
||||
std.debug.assert(self.contexts.items.len == 0);
|
||||
}
|
||||
for (self.contexts[0..self.context_count]) |ctx| {
|
||||
for (self.contexts.items) |ctx| {
|
||||
ctx.deinit();
|
||||
}
|
||||
self.contexts.deinit(self.allocator);
|
||||
|
||||
const app = self.app;
|
||||
const allocator = app.allocator;
|
||||
@@ -338,22 +339,18 @@ fn _createContext(self: *Env, global: anytype, params: ContextParams) !*Context
|
||||
// a v8 context, we can get our context out
|
||||
v8.v8__Context__SetAlignedPointerInEmbedderData(v8_context, 1, @ptrCast(context));
|
||||
|
||||
const count = self.context_count;
|
||||
if (count >= self.contexts.len) {
|
||||
if (self.contexts.items.len >= MAX_CONTEXTS) {
|
||||
return error.TooManyContexts;
|
||||
}
|
||||
self.contexts[count] = context;
|
||||
self.context_count = count + 1;
|
||||
try self.contexts.append(self.allocator, context);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
pub fn destroyContext(self: *Env, context: *Context) void {
|
||||
for (self.contexts[0..self.context_count], 0..) |ctx, i| {
|
||||
for (self.contexts.items, 0..) |ctx, i| {
|
||||
if (ctx == context) {
|
||||
// Swap with last element and decrement count
|
||||
self.context_count -= 1;
|
||||
self.contexts[i] = self.contexts[self.context_count];
|
||||
_ = self.contexts.swapRemove(i);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -387,9 +384,11 @@ pub fn runMicrotasks(self: *Env) void {
|
||||
self.microtask_queues_are_running = true;
|
||||
defer self.microtask_queues_are_running = false;
|
||||
|
||||
// Re-read len/items each iteration: a checkpoint can run JS that creates
|
||||
// a new context (e.g. an iframe), appending to (and reallocating) the list.
|
||||
var i: usize = 0;
|
||||
while (i < self.context_count) : (i += 1) {
|
||||
const ctx = self.contexts[i];
|
||||
while (i < self.contexts.items.len) : (i += 1) {
|
||||
const ctx = self.contexts.items[i];
|
||||
v8.v8__MicrotaskQueue__PerformCheckpoint(ctx.microtask_queue, v8_isolate);
|
||||
}
|
||||
}
|
||||
@@ -400,7 +399,11 @@ pub fn runMacrotasks(self: *Env) !void {
|
||||
return;
|
||||
}
|
||||
|
||||
for (self.contexts[0..self.context_count]) |ctx| {
|
||||
// Re-read len/items each iteration: scheduler.run() can create a new context
|
||||
// (e.g. an iframe), appending to (and reallocating) the list.
|
||||
var i: usize = 0;
|
||||
while (i < self.contexts.items.len) : (i += 1) {
|
||||
const ctx = self.contexts.items[i];
|
||||
if (comptime builtin.is_test == false) {
|
||||
// I hate this comptime check as much as you do. But we have tests
|
||||
// which rely on short execution before shutdown. In real world, it's
|
||||
@@ -420,7 +423,7 @@ pub fn runMacrotasks(self: *Env) !void {
|
||||
|
||||
pub fn msToNextMacrotask(self: *Env) ?u64 {
|
||||
var next_task: u64 = std.math.maxInt(u64);
|
||||
for (self.contexts[0..self.context_count]) |ctx| {
|
||||
for (self.contexts.items) |ctx| {
|
||||
const candidate = ctx.scheduler.msToNextHigh() orelse continue;
|
||||
next_task = @min(candidate, next_task);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ const v8 = js.v8;
|
||||
const log = lp.log;
|
||||
const CallOpts = Caller.CallOpts;
|
||||
const FinalizerCallback = js.FinalizerCallback;
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
// Where js.Context has a lifetime tied to the frame, and holds the
|
||||
// v8::Global<v8::Context>, this has a much shorter lifetime and holds a
|
||||
@@ -1271,15 +1272,35 @@ fn resolveT(comptime T: type, value: *T) Resolved {
|
||||
const finalizer_ptr_id = identity_finalizer.finalizer_ptr_id;
|
||||
const fc = page.finalizer_callbacks.get(finalizer_ptr_id) orelse return;
|
||||
|
||||
const identity_count = fc.identity_count;
|
||||
if (identity_count == 1) {
|
||||
{
|
||||
// Unlink this identity from the FC's intrusive list
|
||||
var prev: ?*FinalizerCallback.Identity = null;
|
||||
var node = fc.identities;
|
||||
while (node) |n| {
|
||||
if (n == identity_finalizer) {
|
||||
if (prev) |p| {
|
||||
p.next = n.next;
|
||||
} else {
|
||||
fc.identities = n.next;
|
||||
}
|
||||
fc.identity_count -= 1;
|
||||
break;
|
||||
}
|
||||
prev = n;
|
||||
node = n.next;
|
||||
} else {
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fc.identity_count == 0) {
|
||||
// Last identity - clean up the FC.
|
||||
// Remove from map before releaseRef to prevent address reuse issues.
|
||||
_ = page.finalizer_callbacks.remove(finalizer_ptr_id);
|
||||
FT.releaseRef(@ptrFromInt(finalizer_ptr_id), page);
|
||||
page.releaseArena(fc.arena);
|
||||
} else {
|
||||
fc.identity_count = identity_count - 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -866,6 +866,7 @@ pub const PageJsApis = flattenTypes(&.{
|
||||
@import("../webapi/element/html/LI.zig"),
|
||||
@import("../webapi/element/html/Link.zig"),
|
||||
@import("../webapi/element/html/Map.zig"),
|
||||
@import("../webapi/element/html/Marquee.zig"),
|
||||
@import("../webapi/element/html/Media.zig"),
|
||||
@import("../webapi/element/html/Meta.zig"),
|
||||
@import("../webapi/element/html/Meter.zig"),
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<head>
|
||||
<script src="../testing.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Regression test for a SIGSEGV triggered by re-entrant document.close().
|
||||
|
||||
Sibling to reentrant_document_write.html: that test covers a written
|
||||
<script> calling document.write() re-entrantly. This one covers the
|
||||
document.close() path, which the streaming parser's `feeding` guard did
|
||||
NOT protect.
|
||||
|
||||
A script run by the script-created parser executes synchronously inside
|
||||
html5ever's feed() (we're popping that <script>). If that script calls
|
||||
document.close(), the close ran html5ever_streaming_parser_finish() on the
|
||||
handle still mid-feed on the stack. html5ever's streaming parser is not
|
||||
re-entrant: finishing it there walked an empty open-elements stack and
|
||||
panicked with "no current element", then segfaulted.
|
||||
|
||||
The fix defers the finish: a re-entrant close flags the document, and the
|
||||
write() driving the feed loop completes the close once the loop unwinds and
|
||||
any markup the same script queued has drained. A custom element in the
|
||||
written markup ensures a reaction is queued in the pop scope that crashed. -->
|
||||
<script id="reentrant_close">
|
||||
window.nestedCloseRan = false;
|
||||
window.connectedRan = false;
|
||||
|
||||
class ReentrantClose extends HTMLElement {
|
||||
connectedCallback() {
|
||||
window.connectedRan = true;
|
||||
}
|
||||
}
|
||||
customElements.define('reentrant-close', ReentrantClose);
|
||||
|
||||
// onload runs after the initial parse, so document.write() goes through the
|
||||
// script-created parser (the path that crashed).
|
||||
testing.onload(function () {
|
||||
document.open();
|
||||
document.write(
|
||||
'<reentrant-close></reentrant-close>' +
|
||||
'<scr' + 'ipt>' +
|
||||
'window.nestedCloseRan = true;' +
|
||||
'document.write("<p>nested</p>");' +
|
||||
'document.close();' +
|
||||
'</scr' + 'ipt>'
|
||||
);
|
||||
|
||||
// Reaching here without crashing is the whole point of the test.
|
||||
testing.expectTrue(window.nestedCloseRan);
|
||||
testing.expectTrue(window.connectedRan);
|
||||
|
||||
// The re-entrant close deferred the parser finish until the feed loop
|
||||
// unwound; by now the queued <p>nested</p> has been parsed.
|
||||
testing.expectEqual('nested', document.querySelector('p').textContent);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
47
src/browser/tests/element/html/font.html
Normal file
47
src/browser/tests/element/html/font.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<script id="font_constructor">
|
||||
{
|
||||
testing.expectEqual("function", typeof HTMLFontElement)
|
||||
|
||||
const f = document.createElement('font')
|
||||
testing.expectEqual("[object HTMLFontElement]", f.toString())
|
||||
testing.expectEqual("FONT", f.tagName)
|
||||
testing.expectEqual(true, f instanceof HTMLFontElement)
|
||||
testing.expectEqual(true, f instanceof HTMLElement)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="font_string_attributes">
|
||||
{
|
||||
const f = document.createElement('font')
|
||||
|
||||
testing.expectEqual("", f.color)
|
||||
testing.expectEqual("", f.face)
|
||||
testing.expectEqual("", f.size)
|
||||
|
||||
f.color = "red"
|
||||
f.face = "serif"
|
||||
f.size = "7"
|
||||
testing.expectEqual("red", f.color)
|
||||
testing.expectEqual("serif", f.face)
|
||||
testing.expectEqual("7", f.size)
|
||||
testing.expectEqual("red", f.getAttribute("color"))
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="font_color_legacy_null_to_empty_string">
|
||||
{
|
||||
// color is `[LegacyNullToEmptyString] DOMString`: null coerces to "", unlike
|
||||
// face/size which stringify null to "null".
|
||||
const f = document.createElement('font')
|
||||
|
||||
f.color = null
|
||||
testing.expectEqual("", f.color)
|
||||
testing.expectEqual("", f.getAttribute("color"))
|
||||
|
||||
f.face = null
|
||||
testing.expectEqual("null", f.face)
|
||||
}
|
||||
</script>
|
||||
@@ -52,5 +52,33 @@
|
||||
|
||||
const textarea = document.createElement('textarea');
|
||||
testing.expectEqual(0, textarea.tabIndex);
|
||||
|
||||
// tabIndex follows the HTML "rules for parsing integers": skip leading
|
||||
// ASCII whitespace, take an optional sign and the leading run of digits,
|
||||
// ignoring any trailing junk.
|
||||
d3.setAttribute('tabindex', ' 7');
|
||||
testing.expectEqual(7, d3.tabIndex);
|
||||
|
||||
d3.setAttribute('tabindex', '\t\n\f\r9');
|
||||
testing.expectEqual(9, d3.tabIndex);
|
||||
|
||||
d3.setAttribute('tabindex', '7%');
|
||||
testing.expectEqual(7, d3.tabIndex);
|
||||
|
||||
d3.setAttribute('tabindex', '1.5');
|
||||
testing.expectEqual(1, d3.tabIndex);
|
||||
|
||||
d3.setAttribute('tabindex', '-3');
|
||||
testing.expectEqual(-3, d3.tabIndex);
|
||||
|
||||
d3.setAttribute('tabindex', '+4');
|
||||
testing.expectEqual(4, d3.tabIndex);
|
||||
|
||||
// Non-numeric values fall back to the element's default (-1 here).
|
||||
d3.setAttribute('tabindex', 'foo');
|
||||
testing.expectEqual(-1, d3.tabIndex);
|
||||
|
||||
d3.setAttribute('tabindex', '');
|
||||
testing.expectEqual(-1, d3.tabIndex);
|
||||
}
|
||||
</script>
|
||||
|
||||
100
src/browser/tests/element/html/marquee.html
Normal file
100
src/browser/tests/element/html/marquee.html
Normal file
@@ -0,0 +1,100 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<script id="marquee_constructor">
|
||||
{
|
||||
testing.expectEqual("function", typeof HTMLMarqueeElement)
|
||||
|
||||
const m = document.createElement('marquee')
|
||||
testing.expectEqual("[object HTMLMarqueeElement]", m.toString())
|
||||
testing.expectEqual("MARQUEE", m.tagName)
|
||||
testing.expectEqual(true, m instanceof HTMLMarqueeElement)
|
||||
testing.expectEqual(true, m instanceof HTMLElement)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="marquee_string_attributes">
|
||||
{
|
||||
const m = document.createElement('marquee')
|
||||
|
||||
// Plain string reflects.
|
||||
testing.expectEqual("", m.bgColor)
|
||||
m.bgColor = "red"
|
||||
testing.expectEqual("red", m.bgColor)
|
||||
testing.expectEqual("red", m.getAttribute("bgcolor"))
|
||||
|
||||
m.height = "100"
|
||||
m.width = "200"
|
||||
testing.expectEqual("100", m.height)
|
||||
testing.expectEqual("200", m.width)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="marquee_enumerated_attributes">
|
||||
{
|
||||
const m = document.createElement('marquee')
|
||||
|
||||
// Limited-to-known-values with a default (missing/invalid -> default).
|
||||
testing.expectEqual("scroll", m.behavior)
|
||||
testing.expectEqual("left", m.direction)
|
||||
|
||||
m.setAttribute("behavior", "ALTERNATE")
|
||||
testing.expectEqual("alternate", m.behavior)
|
||||
m.setAttribute("behavior", "nonsense")
|
||||
testing.expectEqual("scroll", m.behavior)
|
||||
|
||||
m.setAttribute("direction", "Up")
|
||||
testing.expectEqual("up", m.direction)
|
||||
m.setAttribute("direction", "sideways")
|
||||
testing.expectEqual("left", m.direction)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="marquee_boolean_attribute">
|
||||
{
|
||||
const m = document.createElement('marquee')
|
||||
|
||||
testing.expectEqual(false, m.trueSpeed)
|
||||
m.trueSpeed = true
|
||||
testing.expectEqual(true, m.trueSpeed)
|
||||
testing.expectEqual("", m.getAttribute("truespeed"))
|
||||
m.trueSpeed = false
|
||||
testing.expectEqual(false, m.trueSpeed)
|
||||
testing.expectEqual(false, m.hasAttribute("truespeed"))
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="marquee_unsigned_long_attributes">
|
||||
{
|
||||
const m = document.createElement('marquee')
|
||||
|
||||
// Defaults when unset.
|
||||
testing.expectEqual(0, m.hspace)
|
||||
testing.expectEqual(0, m.vspace)
|
||||
testing.expectEqual(6, m.scrollAmount)
|
||||
testing.expectEqual(85, m.scrollDelay)
|
||||
|
||||
// Rules for parsing non-negative integers: ASCII whitespace prefix and
|
||||
// trailing junk are allowed; VT (\v) is not whitespace; negatives and
|
||||
// out-of-range values fall back to the default.
|
||||
m.setAttribute("hspace", " \t7px")
|
||||
testing.expectEqual(7, m.hspace)
|
||||
m.setAttribute("hspace", "\v7")
|
||||
testing.expectEqual(0, m.hspace)
|
||||
m.setAttribute("hspace", "-1")
|
||||
testing.expectEqual(0, m.hspace)
|
||||
m.setAttribute("scrollAmount", "2147483648")
|
||||
testing.expectEqual(6, m.scrollAmount)
|
||||
m.setAttribute("scrollAmount", "2147483647")
|
||||
testing.expectEqual(2147483647, m.scrollAmount)
|
||||
|
||||
// IDL setter: in-range values are written through, values above the
|
||||
// signed-long max are coerced to the default.
|
||||
m.hspace = 42
|
||||
testing.expectEqual(42, m.hspace)
|
||||
testing.expectEqual("42", m.getAttribute("hspace"))
|
||||
m.scrollAmount = 4294967295
|
||||
testing.expectEqual(6, m.scrollAmount)
|
||||
testing.expectEqual("6", m.getAttribute("scrollamount"))
|
||||
}
|
||||
</script>
|
||||
36
src/browser/tests/element/html/param.html
Normal file
36
src/browser/tests/element/html/param.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<param id="p1" name="n0" value="v0">
|
||||
|
||||
<script id="name">
|
||||
{
|
||||
const p = document.getElementById('p1');
|
||||
testing.expectEqual('n0', p.name);
|
||||
|
||||
p.name = 'n1';
|
||||
testing.expectEqual('n1', p.getAttribute('name'));
|
||||
|
||||
p.setAttribute('name', 'n2');
|
||||
testing.expectEqual('n2', p.name);
|
||||
|
||||
const p2 = document.createElement('param');
|
||||
testing.expectEqual('', p2.name);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="value">
|
||||
{
|
||||
const p = document.getElementById('p1');
|
||||
testing.expectEqual('v0', p.value);
|
||||
|
||||
p.value = 'v1';
|
||||
testing.expectEqual('v1', p.getAttribute('value'));
|
||||
|
||||
p.setAttribute('value', 'v2');
|
||||
testing.expectEqual('v2', p.value);
|
||||
|
||||
const p2 = document.createElement('param');
|
||||
testing.expectEqual('', p2.value);
|
||||
}
|
||||
</script>
|
||||
@@ -49,7 +49,7 @@
|
||||
<script id="type">
|
||||
{
|
||||
const style = document.createElement('style');
|
||||
testing.expectEqual('text/css', style.type);
|
||||
testing.expectEqual('', style.type);
|
||||
|
||||
style.type = 'text/plain';
|
||||
testing.expectEqual('text/plain', style.type);
|
||||
|
||||
@@ -176,7 +176,7 @@ const ValueWriter = struct {
|
||||
stack: ?[]const u8 = null,
|
||||
|
||||
pub fn format(self: ValueWriter, writer: *std.io.Writer) !void {
|
||||
for (self.values, 1..) |value, i| {
|
||||
for (self.valuesToLog(), 1..) |value, i| {
|
||||
try writer.print("\n arg({d}): {f}", .{ i, value });
|
||||
}
|
||||
if (self.stack) |s| {
|
||||
@@ -186,7 +186,7 @@ const ValueWriter = struct {
|
||||
|
||||
pub fn logFmt(self: ValueWriter, _: []const u8, writer: anytype) !void {
|
||||
var buf: [32]u8 = undefined;
|
||||
for (self.values, 0..) |value, i| {
|
||||
for (self.valuesToLog(), 0..) |value, i| {
|
||||
const name = try std.fmt.bufPrint(&buf, "param.{d}", .{i});
|
||||
try writer.write(name, value);
|
||||
}
|
||||
@@ -194,11 +194,20 @@ const ValueWriter = struct {
|
||||
|
||||
pub fn jsonStringify(self: ValueWriter, writer: *std.json.Stringify) !void {
|
||||
try writer.beginArray();
|
||||
for (self.values) |value| {
|
||||
for (self.valuesToLog()) |value| {
|
||||
try writer.write(value);
|
||||
}
|
||||
return writer.endArray();
|
||||
}
|
||||
|
||||
fn valuesToLog(self: ValueWriter) []js.Value {
|
||||
if (lp.build_config.wpt_extensions) {
|
||||
// A few WPT tests print HUGE arrays, it's at best, annoying when
|
||||
// running it locally
|
||||
return self.values[0..@min(self.values.len, 100)];
|
||||
}
|
||||
return self.values;
|
||||
}
|
||||
};
|
||||
|
||||
pub const JsApi = struct {
|
||||
|
||||
@@ -63,6 +63,7 @@ _implementation: ?*DOMImplementation = null,
|
||||
_fonts: ?*FontFaceSet = null,
|
||||
_write_insertion_point: ?*Node = null,
|
||||
_script_created_parser: ?Parser.Streaming = null,
|
||||
_close_requested: bool = false,
|
||||
_adopted_style_sheets: ?js.Object.Global = null,
|
||||
_selection: Selection = .{ ._rc = .init(1) },
|
||||
|
||||
@@ -764,9 +765,22 @@ fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool
|
||||
log.warn(.dom, "flush after parser panic", .{ .err = flush_err });
|
||||
};
|
||||
self._script_created_parser = null;
|
||||
self._close_requested = false;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (self._close_requested) {
|
||||
// document.close was executed during a document.write. We couldn't
|
||||
// execute that during the write, but we can now.
|
||||
if (self._script_created_parser) |*parser| {
|
||||
if (parser.feeding == false) {
|
||||
try self.finishScriptCreatedParser(frame);
|
||||
}
|
||||
} else {
|
||||
self._close_requested = false;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -891,10 +905,23 @@ pub fn close(self: *Document, call_frame: *Frame) !void {
|
||||
return error.InvalidStateError;
|
||||
}
|
||||
|
||||
if (self._script_created_parser == null) {
|
||||
if (self._script_created_parser) |*parser| {
|
||||
if (parser.feeding) {
|
||||
// we're currently in a document.write, we cannot close. We flag
|
||||
// the close and process it at the next safe spot.
|
||||
self._close_requested = true;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
try self.finishScriptCreatedParser(frame);
|
||||
}
|
||||
|
||||
fn finishScriptCreatedParser(self: *Document, frame: *Frame) !void {
|
||||
self._close_requested = false;
|
||||
|
||||
// done() finishes html5ever's handle and runs the final flushPendingText.
|
||||
// Even if flushPendingText errors, the handle is already finished and we
|
||||
// must not retain the Streaming — defer so the error path also drops it.
|
||||
|
||||
@@ -233,6 +233,7 @@ pub fn getTagNameLower(self: *const Element) []const u8 {
|
||||
.li => "li",
|
||||
.link => "link",
|
||||
.map => "map",
|
||||
.marquee => "marquee",
|
||||
.media => |m| switch (m._type) {
|
||||
.audio => "audio",
|
||||
.video => "video",
|
||||
@@ -313,6 +314,7 @@ pub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 {
|
||||
.li => "LI",
|
||||
.link => "LINK",
|
||||
.map => "MAP",
|
||||
.marquee => "MARQUEE",
|
||||
.meta => "META",
|
||||
.media => |m| switch (m._type) {
|
||||
.audio => "AUDIO",
|
||||
@@ -1507,6 +1509,7 @@ pub fn getTag(self: *const Element) Tag {
|
||||
.legend => .legend,
|
||||
.li => .li,
|
||||
.map => .map,
|
||||
.marquee => .marquee,
|
||||
.ul => .ul,
|
||||
.ol => .ol,
|
||||
.object => .object,
|
||||
|
||||
@@ -722,7 +722,7 @@ fn getDefaultDisplay(element: *const Element) []const u8 {
|
||||
.html => |html| {
|
||||
return switch (html._type) {
|
||||
.anchor, .br, .span, .label, .time, .font, .mod, .quote => "inline",
|
||||
.body, .div, .dl, .p, .heading, .form, .button, .canvas, .details, .dialog, .embed, .head, .html, .hr, .iframe, .img, .input, .li, .link, .meta, .ol, .option, .script, .select, .slot, .style, .template, .textarea, .title, .ul, .media, .area, .base, .datalist, .directory, .fieldset, .frameset, .legend, .map, .meter, .object, .optgroup, .output, .param, .picture, .pre, .progress, .source, .table, .table_caption, .table_cell, .table_col, .table_row, .table_section, .track => "block",
|
||||
.body, .div, .dl, .p, .heading, .form, .button, .canvas, .details, .dialog, .embed, .head, .html, .hr, .iframe, .img, .input, .li, .link, .meta, .ol, .option, .script, .select, .slot, .style, .template, .textarea, .title, .ul, .media, .area, .base, .datalist, .directory, .fieldset, .frameset, .legend, .map, .marquee, .meter, .object, .optgroup, .output, .param, .picture, .pre, .progress, .source, .table, .table_caption, .table_cell, .table_col, .table_row, .table_section, .track => "block",
|
||||
.generic, .custom, .unknown, .data => blk: {
|
||||
const tag = element.getTagNameLower();
|
||||
if (isInlineTag(tag)) break :blk "inline";
|
||||
|
||||
@@ -61,6 +61,7 @@ pub const Legend = @import("html/Legend.zig");
|
||||
pub const LI = @import("html/LI.zig");
|
||||
pub const Link = @import("html/Link.zig");
|
||||
pub const Map = @import("html/Map.zig");
|
||||
pub const Marquee = @import("html/Marquee.zig");
|
||||
pub const Media = @import("html/Media.zig");
|
||||
pub const Meta = @import("html/Meta.zig");
|
||||
pub const Meter = @import("html/Meter.zig");
|
||||
@@ -151,6 +152,7 @@ pub const Type = union(enum) {
|
||||
li: *LI,
|
||||
link: *Link,
|
||||
map: *Map,
|
||||
marquee: *Marquee,
|
||||
media: *Media,
|
||||
meta: *Meta,
|
||||
meter: *Meter,
|
||||
@@ -342,14 +344,12 @@ pub fn setHidden(self: *HtmlElement, hidden: bool, frame: *Frame) !void {
|
||||
}
|
||||
|
||||
pub fn getTabIndex(self: *HtmlElement) i32 {
|
||||
const attr = self.asElement().getAttributeSafe(comptime .wrap("tabindex")) orelse {
|
||||
// Per spec, interactive/focusable elements default to 0 when tabindex is absent
|
||||
return switch (self._type) {
|
||||
.anchor, .area, .button, .input, .select, .textarea, .iframe => 0,
|
||||
else => -1,
|
||||
};
|
||||
const default: i32 = switch (self._type) {
|
||||
.anchor, .area, .button, .input, .select, .textarea, .iframe => 0,
|
||||
else => -1,
|
||||
};
|
||||
return std.fmt.parseInt(i32, attr, 10) catch -1;
|
||||
const attr = self.asElement().getAttributeSafe(comptime .wrap("tabindex")) orelse return default;
|
||||
return parseInteger(attr) orelse default;
|
||||
}
|
||||
|
||||
pub fn setTabIndex(self: *HtmlElement, value: i32, frame: *Frame) !void {
|
||||
@@ -359,13 +359,41 @@ pub fn setTabIndex(self: *HtmlElement, value: i32, frame: *Frame) !void {
|
||||
}
|
||||
|
||||
pub fn getDir(self: *HtmlElement) []const u8 {
|
||||
return self.asElement().getAttributeSafe(comptime .wrap("dir")) orelse "";
|
||||
return reflectEnumerated(self.asElement().getAttributeSafe(comptime .wrap("dir")), &.{ "ltr", "rtl", "auto" }, "", "").?;
|
||||
}
|
||||
|
||||
pub fn setDir(self: *HtmlElement, value: []const u8, frame: *Frame) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("dir"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub fn getAccessKey(self: *HtmlElement) []const u8 {
|
||||
return self.asElement().getAttributeSafe(comptime .wrap("accesskey")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setAccessKey(self: *HtmlElement, value: []const u8, frame: *Frame) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("accesskey"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub fn getAutofocus(self: *HtmlElement) bool {
|
||||
return self.asElement().getAttributeSafe(comptime .wrap("autofocus")) != null;
|
||||
}
|
||||
|
||||
pub fn setAutofocus(self: *HtmlElement, autofocus: bool, frame: *Frame) !void {
|
||||
if (autofocus) {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("autofocus"), .wrap(""), frame);
|
||||
} else {
|
||||
try self.asElement().removeAttribute(comptime .wrap("autofocus"), frame);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getNonce(self: *HtmlElement) []const u8 {
|
||||
return self.asElement().getAttributeSafe(comptime .wrap("nonce")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setNonce(self: *HtmlElement, value: []const u8, frame: *Frame) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("nonce"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub fn getLang(self: *HtmlElement) []const u8 {
|
||||
return self.asElement().getAttributeSafe(comptime .wrap("lang")) orelse "";
|
||||
}
|
||||
@@ -1223,6 +1251,57 @@ pub fn getOnWheel(self: *HtmlElement, frame: *Frame) !?js.Function.Global {
|
||||
return self.getAttributeFunction(.onwheel, frame);
|
||||
}
|
||||
|
||||
// HTML integer parsing is lax
|
||||
pub fn parseInteger(input: []const u8) ?i32 {
|
||||
var normalized = std.mem.trimStart(u8, input, "\t\n\r\x0c ");
|
||||
if (normalized.len == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var negative = false;
|
||||
if (normalized[0] == '-') {
|
||||
negative = true;
|
||||
normalized = normalized[1..];
|
||||
} else if (normalized[0] == '+') {
|
||||
normalized = normalized[1..];
|
||||
}
|
||||
|
||||
if (normalized.len == 0 or std.ascii.isDigit(normalized[0]) == false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var i: usize = 0;
|
||||
var value: i64 = 0;
|
||||
while (i < normalized.len and std.ascii.isDigit(normalized[i])) : (i += 1) {
|
||||
value = value * 10 + (normalized[i] - '0');
|
||||
if (value > 2147483648) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (negative) {
|
||||
value = -value;
|
||||
}
|
||||
|
||||
if (value < -2147483648 or value > 2147483647) {
|
||||
return null;
|
||||
}
|
||||
return @intCast(value);
|
||||
}
|
||||
|
||||
pub fn reflectEnumerated(
|
||||
value: ?[]const u8,
|
||||
keywords: []const []const u8,
|
||||
missing: ?[]const u8,
|
||||
invalid: ?[]const u8,
|
||||
) ?[]const u8 {
|
||||
const v = value orelse return missing;
|
||||
for (keywords) |keyword| {
|
||||
if (std.ascii.eqlIgnoreCase(v, keyword)) return keyword;
|
||||
}
|
||||
return invalid;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(HtmlElement);
|
||||
|
||||
@@ -1243,10 +1322,13 @@ pub const JsApi = struct {
|
||||
pub const insertAdjacentHTML = bridge.function(HtmlElement.insertAdjacentHTML, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const click = bridge.function(HtmlElement.click, .{});
|
||||
|
||||
pub const accessKey = bridge.accessor(HtmlElement.getAccessKey, HtmlElement.setAccessKey, .{ .ce_reactions = true });
|
||||
pub const autofocus = bridge.accessor(HtmlElement.getAutofocus, HtmlElement.setAutofocus, .{ .ce_reactions = true });
|
||||
pub const dir = bridge.accessor(HtmlElement.getDir, HtmlElement.setDir, .{ .ce_reactions = true });
|
||||
pub const hidden = bridge.accessor(HtmlElement.getHidden, HtmlElement.setHidden, .{ .ce_reactions = true });
|
||||
pub const isContentEditable = bridge.accessor(HtmlElement.getIsContentEditable, null, .{});
|
||||
pub const lang = bridge.accessor(HtmlElement.getLang, HtmlElement.setLang, .{ .ce_reactions = true });
|
||||
pub const nonce = bridge.accessor(HtmlElement.getNonce, HtmlElement.setNonce, .{ .ce_reactions = true });
|
||||
pub const tabIndex = bridge.accessor(HtmlElement.getTabIndex, HtmlElement.setTabIndex, .{ .ce_reactions = true });
|
||||
pub const title = bridge.accessor(HtmlElement.getTitle, HtmlElement.setTitle, .{ .ce_reactions = true });
|
||||
|
||||
|
||||
@@ -17,6 +17,14 @@ pub fn asNode(self: *Base) *Node {
|
||||
return self.asElement().asNode();
|
||||
}
|
||||
|
||||
pub fn getTarget(self: *Base) []const u8 {
|
||||
return self.asElement().getAttributeSafe(comptime .wrap("target")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setTarget(self: *Base, value: []const u8, frame: *Frame) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("target"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub fn getHref(self: *Base, frame: *Frame) ![]const u8 {
|
||||
const element = self.asElement();
|
||||
const href = element.getAttributeSafe(comptime .wrap("href")) orelse return "";
|
||||
@@ -65,6 +73,7 @@ pub const JsApi = struct {
|
||||
};
|
||||
|
||||
pub const href = bridge.accessor(Base.getHref, Base.setHref, .{ .ce_reactions = true });
|
||||
pub const target = bridge.accessor(Base.getTarget, Base.setTarget, .{ .ce_reactions = true });
|
||||
};
|
||||
|
||||
const testing = @import("../../../../testing.zig");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
const HtmlElement = @import("../Html.zig");
|
||||
@@ -14,6 +15,18 @@ pub fn asNode(self: *Directory) *Node {
|
||||
return self.asElement().asNode();
|
||||
}
|
||||
|
||||
pub fn getCompact(self: *Directory) bool {
|
||||
return self.asElement().getAttributeSafe(comptime .wrap("compact")) != null;
|
||||
}
|
||||
|
||||
pub fn setCompact(self: *Directory, compact: bool, frame: *Frame) !void {
|
||||
if (compact) {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("compact"), .wrap(""), frame);
|
||||
} else {
|
||||
try self.asElement().removeAttribute(comptime .wrap("compact"), frame);
|
||||
}
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Directory);
|
||||
|
||||
@@ -22,4 +35,6 @@ pub const JsApi = struct {
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const compact = bridge.accessor(Directory.getCompact, Directory.setCompact, .{ .ce_reactions = true });
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
const HtmlElement = @import("../Html.zig");
|
||||
@@ -14,6 +15,33 @@ pub fn asNode(self: *Font) *Node {
|
||||
return self.asElement().asNode();
|
||||
}
|
||||
|
||||
pub fn getColor(self: *Font) []const u8 {
|
||||
return self.asElement().getAttributeSafe(comptime .wrap("color")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setColor(self: *Font, value: js.Value, frame: *Frame) !void {
|
||||
// color is `[LegacyNullToEmptyString] DOMString`: a JS null becomes "",
|
||||
// not the string "null".
|
||||
const str: []const u8 = if (value.isNull()) "" else try value.toZig([]const u8);
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("color"), .wrap(str), frame);
|
||||
}
|
||||
|
||||
pub fn getFace(self: *Font) []const u8 {
|
||||
return self.asElement().getAttributeSafe(comptime .wrap("face")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setFace(self: *Font, value: []const u8, frame: *Frame) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("face"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub fn getSize(self: *Font) []const u8 {
|
||||
return self.asElement().getAttributeSafe(comptime .wrap("size")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setSize(self: *Font, value: []const u8, frame: *Frame) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("size"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Font);
|
||||
|
||||
@@ -22,4 +50,13 @@ pub const JsApi = struct {
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const color = bridge.accessor(Font.getColor, Font.setColor, .{ .ce_reactions = true });
|
||||
pub const face = bridge.accessor(Font.getFace, Font.setFace, .{ .ce_reactions = true });
|
||||
pub const size = bridge.accessor(Font.getSize, Font.setSize, .{ .ce_reactions = true });
|
||||
};
|
||||
|
||||
const testing = @import("../../../../testing.zig");
|
||||
test "WebApi: HTML.Font" {
|
||||
try testing.htmlRunner("element/html/font.html", .{});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
const HtmlElement = @import("../Html.zig");
|
||||
@@ -14,6 +15,22 @@ pub fn asNode(self: *FrameSet) *Node {
|
||||
return self.asElement().asNode();
|
||||
}
|
||||
|
||||
pub fn getCols(self: *FrameSet) []const u8 {
|
||||
return self.asElement().getAttributeSafe(comptime .wrap("cols")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setCols(self: *FrameSet, value: []const u8, frame: *Frame) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("cols"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub fn getRows(self: *FrameSet) []const u8 {
|
||||
return self.asElement().getAttributeSafe(comptime .wrap("rows")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setRows(self: *FrameSet, value: []const u8, frame: *Frame) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("rows"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(FrameSet);
|
||||
|
||||
@@ -22,6 +39,9 @@ pub const JsApi = struct {
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const cols = bridge.accessor(FrameSet.getCols, FrameSet.setCols, .{ .ce_reactions = true });
|
||||
pub const rows = bridge.accessor(FrameSet.getRows, FrameSet.setRows, .{ .ce_reactions = true });
|
||||
};
|
||||
|
||||
const testing = @import("../../../../testing.zig");
|
||||
|
||||
@@ -70,13 +70,50 @@ pub fn setRel(self: *Link, value: []const u8, frame: *Frame) !void {
|
||||
}
|
||||
|
||||
pub fn getAs(self: *const Link) []const u8 {
|
||||
return self.asConstElement().getAttributeSafe(comptime .wrap("as")) orelse "";
|
||||
const valid_as = [_][]const u8{
|
||||
"fetch",
|
||||
"audio",
|
||||
"document",
|
||||
"embed",
|
||||
"font",
|
||||
"image",
|
||||
"manifest",
|
||||
"object",
|
||||
"report",
|
||||
"script",
|
||||
"sharedworker",
|
||||
"style",
|
||||
"track",
|
||||
"video",
|
||||
"worker",
|
||||
"xslt",
|
||||
};
|
||||
return HtmlElement.reflectEnumerated(self.asConstElement().getAttributeSafe(comptime .wrap("as")), &valid_as, "", "").?;
|
||||
}
|
||||
|
||||
pub fn setAs(self: *Link, value: []const u8, frame: *Frame) !void {
|
||||
return self.asElement().setAttributeSafe(comptime .wrap("as"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub fn getReferrerPolicy(self: *const Link) []const u8 {
|
||||
const valid_referrer_policy = [_][]const u8{
|
||||
"",
|
||||
"no-referrer",
|
||||
"no-referrer-when-downgrade",
|
||||
"same-origin",
|
||||
"origin",
|
||||
"strict-origin",
|
||||
"origin-when-cross-origin",
|
||||
"strict-origin-when-cross-origin",
|
||||
"unsafe-url",
|
||||
};
|
||||
return HtmlElement.reflectEnumerated(self.asConstElement().getAttributeSafe(.wrap("referrerpolicy")), &valid_referrer_policy, "", "").?;
|
||||
}
|
||||
|
||||
pub fn setReferrerPolicy(self: *Link, value: []const u8, frame: *Frame) !void {
|
||||
return self.asElement().setAttributeSafe(.wrap("referrerpolicy"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub fn getMedia(self: *Link) []const u8 {
|
||||
return self.asElement().getAttributeSafe(comptime .wrap("media")) orelse return "";
|
||||
}
|
||||
@@ -86,15 +123,68 @@ pub fn setMedia(self: *Link, value: []const u8, frame: *Frame) !void {
|
||||
}
|
||||
|
||||
pub fn getCrossOrigin(self: *const Link) ?[]const u8 {
|
||||
return self.asConstElement().getAttributeSafe(comptime .wrap("crossorigin"));
|
||||
const valid_cross_origin = [_][]const u8{
|
||||
"anonymous", "use-credentials",
|
||||
};
|
||||
return HtmlElement.reflectEnumerated(self.asConstElement().getAttributeSafe(comptime .wrap("crossorigin")), &valid_cross_origin, null, "anonymous");
|
||||
}
|
||||
|
||||
pub fn setCrossOrigin(self: *Link, value: []const u8, frame: *Frame) !void {
|
||||
var normalized: []const u8 = "anonymous";
|
||||
if (std.ascii.eqlIgnoreCase(value, "use-credentials")) {
|
||||
normalized = "use-credentials";
|
||||
pub fn setCrossOrigin(self: *Link, value: ?[]const u8, frame: *Frame) !void {
|
||||
// Nullable reflection: a null (or undefined) value removes the attribute;
|
||||
// otherwise the content attribute mirrors the value verbatim and the
|
||||
// getter canonicalizes it.
|
||||
if (value) |v| {
|
||||
return self.asElement().setAttributeSafe(comptime .wrap("crossorigin"), .wrap(v), frame);
|
||||
}
|
||||
return self.asElement().setAttributeSafe(comptime .wrap("crossorigin"), .wrap(normalized), frame);
|
||||
return self.asElement().removeAttribute(comptime .wrap("crossorigin"), frame);
|
||||
}
|
||||
|
||||
pub fn getCharset(self: *const Link) []const u8 {
|
||||
return self.asConstElement().getAttributeSafe(comptime .wrap("charset")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setCharset(self: *Link, value: []const u8, frame: *Frame) !void {
|
||||
return self.asElement().setAttributeSafe(comptime .wrap("charset"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub fn getHreflang(self: *const Link) []const u8 {
|
||||
return self.asConstElement().getAttributeSafe(comptime .wrap("hreflang")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setHreflang(self: *Link, value: []const u8, frame: *Frame) !void {
|
||||
return self.asElement().setAttributeSafe(comptime .wrap("hreflang"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub fn getIntegrity(self: *const Link) []const u8 {
|
||||
return self.asConstElement().getAttributeSafe(comptime .wrap("integrity")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setIntegrity(self: *Link, value: []const u8, frame: *Frame) !void {
|
||||
return self.asElement().setAttributeSafe(comptime .wrap("integrity"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub fn getType(self: *const Link) []const u8 {
|
||||
return self.asConstElement().getAttributeSafe(comptime .wrap("type")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setType(self: *Link, value: []const u8, frame: *Frame) !void {
|
||||
return self.asElement().setAttributeSafe(comptime .wrap("type"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub fn getRev(self: *const Link) []const u8 {
|
||||
return self.asConstElement().getAttributeSafe(comptime .wrap("rev")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setRev(self: *Link, value: []const u8, frame: *Frame) !void {
|
||||
return self.asElement().setAttributeSafe(comptime .wrap("rev"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub fn getTarget(self: *const Link) []const u8 {
|
||||
return self.asConstElement().getAttributeSafe(comptime .wrap("target")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setTarget(self: *Link, value: []const u8, frame: *Frame) !void {
|
||||
return self.asElement().setAttributeSafe(comptime .wrap("target"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub fn linkAddedCallback(self: *Link, frame: *Frame) !void {
|
||||
@@ -145,6 +235,13 @@ pub const JsApi = struct {
|
||||
pub const media = bridge.accessor(Link.getMedia, Link.setMedia, .{ .ce_reactions = true });
|
||||
pub const href = bridge.accessor(Link.getHref, Link.setHref, .{ .ce_reactions = true });
|
||||
pub const crossOrigin = bridge.accessor(Link.getCrossOrigin, Link.setCrossOrigin, .{ .ce_reactions = true });
|
||||
pub const referrerPolicy = bridge.accessor(Link.getReferrerPolicy, Link.setReferrerPolicy, .{ .ce_reactions = true });
|
||||
pub const charset = bridge.accessor(Link.getCharset, Link.setCharset, .{ .ce_reactions = true });
|
||||
pub const hreflang = bridge.accessor(Link.getHreflang, Link.setHreflang, .{ .ce_reactions = true });
|
||||
pub const integrity = bridge.accessor(Link.getIntegrity, Link.setIntegrity, .{ .ce_reactions = true });
|
||||
pub const @"type" = bridge.accessor(Link.getType, Link.setType, .{ .ce_reactions = true });
|
||||
pub const rev = bridge.accessor(Link.getRev, Link.setRev, .{ .ce_reactions = true });
|
||||
pub const target = bridge.accessor(Link.getTarget, Link.setTarget, .{ .ce_reactions = true });
|
||||
pub const relList = bridge.accessor(_getRelList, null, .{ .null_as_undefined = true });
|
||||
|
||||
fn _getRelList(self: *Link, frame: *Frame) !?*@import("../../collections.zig").DOMTokenList {
|
||||
|
||||
154
src/browser/webapi/element/html/Marquee.zig
Normal file
154
src/browser/webapi/element/html/Marquee.zig
Normal file
@@ -0,0 +1,154 @@
|
||||
const std = @import("std");
|
||||
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
const HtmlElement = @import("../Html.zig");
|
||||
|
||||
const Marquee = @This();
|
||||
|
||||
_proto: *HtmlElement,
|
||||
|
||||
pub fn asElement(self: *Marquee) *Element {
|
||||
return self._proto._proto;
|
||||
}
|
||||
pub fn asNode(self: *Marquee) *Node {
|
||||
return self.asElement().asNode();
|
||||
}
|
||||
|
||||
pub fn getBehavior(self: *Marquee) []const u8 {
|
||||
const valid_behavior = [_][]const u8{
|
||||
"scroll", "slide", "alternate",
|
||||
};
|
||||
return HtmlElement.reflectEnumerated(self.asElement().getAttributeSafe(comptime .wrap("behavior")), &valid_behavior, "scroll", "scroll").?;
|
||||
}
|
||||
|
||||
pub fn setBehavior(self: *Marquee, value: []const u8, frame: *Frame) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("behavior"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub fn getDirection(self: *Marquee) []const u8 {
|
||||
const valid_direction = [_][]const u8{
|
||||
"up", "right", "down", "left",
|
||||
};
|
||||
return HtmlElement.reflectEnumerated(self.asElement().getAttributeSafe(comptime .wrap("direction")), &valid_direction, "left", "left").?;
|
||||
}
|
||||
|
||||
pub fn setDirection(self: *Marquee, value: []const u8, frame: *Frame) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("direction"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub fn getBgColor(self: *Marquee) []const u8 {
|
||||
return self.asElement().getAttributeSafe(comptime .wrap("bgcolor")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setBgColor(self: *Marquee, value: []const u8, frame: *Frame) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("bgcolor"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub fn getHeight(self: *Marquee) []const u8 {
|
||||
return self.asElement().getAttributeSafe(comptime .wrap("height")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setHeight(self: *Marquee, value: []const u8, frame: *Frame) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("height"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub fn getWidth(self: *Marquee) []const u8 {
|
||||
return self.asElement().getAttributeSafe(comptime .wrap("width")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setWidth(self: *Marquee, value: []const u8, frame: *Frame) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("width"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub fn getHspace(self: *Marquee) u32 {
|
||||
return getU32(self, "hspace", 0);
|
||||
}
|
||||
|
||||
pub fn setHspace(self: *Marquee, value: u32, frame: *Frame) !void {
|
||||
try setU32(self, "hspace", value, 0, frame);
|
||||
}
|
||||
|
||||
pub fn getVspace(self: *Marquee) u32 {
|
||||
return getU32(self, "vspace", 0);
|
||||
}
|
||||
|
||||
pub fn setVspace(self: *Marquee, value: u32, frame: *Frame) !void {
|
||||
try setU32(self, "vspace", value, 0, frame);
|
||||
}
|
||||
|
||||
pub fn getScrollAmount(self: *Marquee) u32 {
|
||||
return getU32(self, "scrollamount", 6);
|
||||
}
|
||||
|
||||
pub fn setScrollAmount(self: *Marquee, value: u32, frame: *Frame) !void {
|
||||
try setU32(self, "scrollamount", value, 6, frame);
|
||||
}
|
||||
|
||||
pub fn getScrollDelay(self: *Marquee) u32 {
|
||||
return getU32(self, "scrolldelay", 85);
|
||||
}
|
||||
|
||||
pub fn setScrollDelay(self: *Marquee, value: u32, frame: *Frame) !void {
|
||||
try setU32(self, "scrolldelay", value, 85, frame);
|
||||
}
|
||||
|
||||
pub fn getTrueSpeed(self: *Marquee) bool {
|
||||
return self.asElement().getAttributeSafe(comptime .wrap("truespeed")) != null;
|
||||
}
|
||||
|
||||
pub fn setTrueSpeed(self: *Marquee, truespeed: bool, frame: *Frame) !void {
|
||||
if (truespeed) {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("truespeed"), .wrap(""), frame);
|
||||
} else {
|
||||
try self.asElement().removeAttribute(comptime .wrap("truespeed"), frame);
|
||||
}
|
||||
}
|
||||
|
||||
// Reflects an `unsigned long` content attribute: parses with the "rules for
|
||||
// parsing non-negative integers" (the lax integer parser, then rejecting a
|
||||
// negative result), so a valid value lands in [0, 2147483647].
|
||||
fn getU32(self: *Marquee, comptime attr: []const u8, default: u32) u32 {
|
||||
const value = self.asElement().getAttributeSafe(comptime .wrap(attr)) orelse return default;
|
||||
const parsed = HtmlElement.parseInteger(value) orelse return default;
|
||||
|
||||
if (parsed < 0) {
|
||||
return default;
|
||||
}
|
||||
return @intCast(parsed);
|
||||
}
|
||||
|
||||
fn setU32(self: *Marquee, comptime attr: []const u8, value: u32, default: u32, frame: *Frame) !void {
|
||||
const written = if (value > 2147483647) default else value;
|
||||
var buf: [10]u8 = undefined;
|
||||
const str = std.fmt.bufPrint(&buf, "{d}", .{written}) catch unreachable;
|
||||
try self.asElement().setAttributeSafe(comptime .wrap(attr), .wrap(str), frame);
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Marquee);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "HTMLMarqueeElement";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const behavior = bridge.accessor(Marquee.getBehavior, Marquee.setBehavior, .{ .ce_reactions = true });
|
||||
pub const bgColor = bridge.accessor(Marquee.getBgColor, Marquee.setBgColor, .{ .ce_reactions = true });
|
||||
pub const direction = bridge.accessor(Marquee.getDirection, Marquee.setDirection, .{ .ce_reactions = true });
|
||||
pub const height = bridge.accessor(Marquee.getHeight, Marquee.setHeight, .{ .ce_reactions = true });
|
||||
pub const hspace = bridge.accessor(Marquee.getHspace, Marquee.setHspace, .{ .ce_reactions = true });
|
||||
pub const scrollAmount = bridge.accessor(Marquee.getScrollAmount, Marquee.setScrollAmount, .{ .ce_reactions = true });
|
||||
pub const scrollDelay = bridge.accessor(Marquee.getScrollDelay, Marquee.setScrollDelay, .{ .ce_reactions = true });
|
||||
pub const trueSpeed = bridge.accessor(Marquee.getTrueSpeed, Marquee.setTrueSpeed, .{ .ce_reactions = true });
|
||||
pub const vspace = bridge.accessor(Marquee.getVspace, Marquee.setVspace, .{ .ce_reactions = true });
|
||||
pub const width = bridge.accessor(Marquee.getWidth, Marquee.setWidth, .{ .ce_reactions = true });
|
||||
};
|
||||
|
||||
const testing = @import("../../../../testing.zig");
|
||||
test "WebApi: HTML.Marquee" {
|
||||
try testing.htmlRunner("element/html/marquee.html", .{});
|
||||
}
|
||||
@@ -68,6 +68,14 @@ pub fn setMedia(self: *Meta, value: []const u8, frame: *Frame) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("media"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub fn getScheme(self: *Meta) []const u8 {
|
||||
return self.asElement().getAttributeSafe(comptime .wrap("scheme")) orelse return "";
|
||||
}
|
||||
|
||||
pub fn setScheme(self: *Meta, value: []const u8, frame: *Frame) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("scheme"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(MetaElement);
|
||||
|
||||
@@ -81,4 +89,5 @@ pub const JsApi = struct {
|
||||
pub const httpEquiv = bridge.accessor(MetaElement.getHttpEquiv, MetaElement.setHttpEquiv, .{ .ce_reactions = true });
|
||||
pub const content = bridge.accessor(MetaElement.getContent, MetaElement.setContent, .{ .ce_reactions = true });
|
||||
pub const media = bridge.accessor(MetaElement.getMedia, MetaElement.setMedia, .{ .ce_reactions = true });
|
||||
pub const scheme = bridge.accessor(MetaElement.getScheme, MetaElement.setScheme, .{ .ce_reactions = true });
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
const HtmlElement = @import("../Html.zig");
|
||||
@@ -14,6 +15,22 @@ pub fn asNode(self: *Param) *Node {
|
||||
return self.asElement().asNode();
|
||||
}
|
||||
|
||||
pub fn getName(self: *Param) []const u8 {
|
||||
return self.asElement().getAttributeSafe(comptime .wrap("name")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setName(self: *Param, value: []const u8, frame: *Frame) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("name"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub fn getValue(self: *Param) []const u8 {
|
||||
return self.asElement().getAttributeSafe(comptime .wrap("value")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setValue(self: *Param, value: []const u8, frame: *Frame) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("value"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Param);
|
||||
|
||||
@@ -22,4 +39,12 @@ pub const JsApi = struct {
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const name = bridge.accessor(Param.getName, Param.setName, .{ .ce_reactions = true });
|
||||
pub const value = bridge.accessor(Param.getValue, Param.setValue, .{ .ce_reactions = true });
|
||||
};
|
||||
|
||||
const testing = @import("../../../../testing.zig");
|
||||
test "WebApi: HTML.Param" {
|
||||
try testing.htmlRunner("element/html/param.html", .{});
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ pub fn setMedia(self: *Style, value: []const u8, frame: *Frame) !void {
|
||||
}
|
||||
|
||||
pub fn getType(self: *const Style) []const u8 {
|
||||
return self.asConstElement().getAttributeSafe(comptime .wrap("type")) orelse "text/css";
|
||||
return self.asConstElement().getAttributeSafe(comptime .wrap("type")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setType(self: *Style, value: []const u8, frame: *Frame) !void {
|
||||
|
||||
@@ -535,7 +535,7 @@ pub fn frameCreated(bc: *CDP.BrowserContext, frame: *Frame) !void {
|
||||
const in_commit = bc.session.pendingPage() != null;
|
||||
|
||||
if (!in_commit) {
|
||||
_ = bc.cdp.frame_arena.reset(.{ .retain_with_limit = 1024 * 512 });
|
||||
bc.cdp.browser.arena_pool.reset(bc.frame_arena, 1024 * 512);
|
||||
}
|
||||
|
||||
for (bc.isolated_worlds.items) |isolated_world| {
|
||||
|
||||
134
src/network/CurlDebugAllocator.zig
Normal file
134
src/network/CurlDebugAllocator.zig
Normal file
@@ -0,0 +1,134 @@
|
||||
// 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 std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const libcurl = @import("../sys/libcurl.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const CurlDebugAllocator = @This();
|
||||
|
||||
// C11 requires malloc to return memory aligned to max_align_t (16 bytes on x86_64).
|
||||
// We match this guarantee since libcurl expects malloc-compatible alignment.
|
||||
const alignment = 16;
|
||||
|
||||
var instance: ?CurlDebugAllocator = null;
|
||||
|
||||
allocator: Allocator,
|
||||
|
||||
pub fn init(allocator: Allocator) void {
|
||||
lp.assert(instance == null, "Initialization of curl must happen only once", .{});
|
||||
instance = .{ .allocator = allocator };
|
||||
}
|
||||
|
||||
pub fn interface() libcurl.CurlAllocator {
|
||||
return .{
|
||||
.free = free,
|
||||
.strdup = strdup,
|
||||
.malloc = malloc,
|
||||
.calloc = calloc,
|
||||
.realloc = realloc,
|
||||
};
|
||||
}
|
||||
|
||||
fn _allocBlock(size: usize) ?*Block {
|
||||
const slice = instance.?.allocator.alignedAlloc(u8, .fromByteUnits(alignment), Block.fullsize(size)) catch return null;
|
||||
const block: *Block = @ptrCast(@alignCast(slice.ptr));
|
||||
block.size = size;
|
||||
return block;
|
||||
}
|
||||
|
||||
fn _freeBlock(header: *Block) void {
|
||||
instance.?.allocator.free(header.slice());
|
||||
}
|
||||
|
||||
fn malloc(size: usize) ?*anyopaque {
|
||||
const block = _allocBlock(size) orelse return null;
|
||||
return @ptrCast(block.data());
|
||||
}
|
||||
|
||||
fn calloc(nmemb: usize, size: usize) ?*anyopaque {
|
||||
const total = nmemb * size;
|
||||
const block = _allocBlock(total) orelse return null;
|
||||
const ptr = block.data();
|
||||
@memset(ptr[0..total], 0); // for historical reasons, calloc zeroes memory, but malloc does not.
|
||||
return @ptrCast(ptr);
|
||||
}
|
||||
|
||||
fn realloc(ptr: ?*anyopaque, size: usize) ?*anyopaque {
|
||||
const p = ptr orelse return malloc(size);
|
||||
const block = Block.fromPtr(p);
|
||||
|
||||
const old_size = block.size;
|
||||
if (size == old_size) return ptr;
|
||||
|
||||
if (instance.?.allocator.resize(block.slice(), alignment + size)) {
|
||||
block.size = size;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
const copy_size = @min(old_size, size);
|
||||
const new_block = _allocBlock(size) orelse return null;
|
||||
@memcpy(new_block.data()[0..copy_size], block.data()[0..copy_size]);
|
||||
_freeBlock(block);
|
||||
return @ptrCast(new_block.data());
|
||||
}
|
||||
|
||||
fn free(ptr: ?*anyopaque) void {
|
||||
const p = ptr orelse return;
|
||||
_freeBlock(Block.fromPtr(p));
|
||||
}
|
||||
|
||||
fn strdup(str: [*:0]const u8) ?[*:0]u8 {
|
||||
const len = std.mem.len(str);
|
||||
const header = _allocBlock(len + 1) orelse return null;
|
||||
const ptr = header.data();
|
||||
@memcpy(ptr[0..len], str[0..len]);
|
||||
ptr[len] = 0;
|
||||
return ptr[0..len :0];
|
||||
}
|
||||
|
||||
const Block = extern struct {
|
||||
size: usize = 0,
|
||||
_padding: [alignment - @sizeOf(usize)]u8 = .{0} ** (alignment - @sizeOf(usize)),
|
||||
|
||||
inline fn fullsize(bytes: usize) usize {
|
||||
return alignment + bytes;
|
||||
}
|
||||
|
||||
inline fn fromPtr(ptr: *anyopaque) *Block {
|
||||
const raw: [*]u8 = @ptrCast(ptr);
|
||||
return @ptrCast(@alignCast(raw - @sizeOf(Block)));
|
||||
}
|
||||
|
||||
inline fn data(self: *Block) [*]u8 {
|
||||
const ptr: [*]u8 = @ptrCast(self);
|
||||
return ptr + @sizeOf(Block);
|
||||
}
|
||||
|
||||
inline fn slice(self: *Block) []align(alignment) u8 {
|
||||
const base: [*]align(alignment) u8 = @ptrCast(@alignCast(self));
|
||||
return base[0 .. alignment + self.size];
|
||||
}
|
||||
};
|
||||
|
||||
comptime {
|
||||
std.debug.assert(@sizeOf(Block) == alignment);
|
||||
}
|
||||
@@ -30,6 +30,7 @@ const http = @import("http.zig");
|
||||
const IpFilter = @import("IpFilter.zig");
|
||||
const RobotStore = @import("Robots.zig").RobotStore;
|
||||
const WebBotAuth = @import("WebBotAuth.zig");
|
||||
const CurlDebugAllocator = @import("CurlDebugAllocator.zig");
|
||||
|
||||
const Cache = @import("cache/Cache.zig");
|
||||
const FsCache = @import("cache/FsCache.zig");
|
||||
@@ -39,6 +40,7 @@ const net = std.net;
|
||||
const posix = std.posix;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const DoublyLinkedList = std.DoublyLinkedList;
|
||||
const IS_DEBUG = builtin.mode == .Debug;
|
||||
|
||||
const Network = @This();
|
||||
|
||||
@@ -152,120 +154,16 @@ const TickCallback = struct {
|
||||
fun: *const fn (*anyopaque) void,
|
||||
};
|
||||
|
||||
const ZigToCurlAllocator = struct {
|
||||
// C11 requires malloc to return memory aligned to max_align_t (16 bytes on x86_64).
|
||||
// We match this guarantee since libcurl expects malloc-compatible alignment.
|
||||
const alignment = 16;
|
||||
|
||||
const Block = extern struct {
|
||||
size: usize = 0,
|
||||
_padding: [alignment - @sizeOf(usize)]u8 = .{0} ** (alignment - @sizeOf(usize)),
|
||||
|
||||
inline fn fullsize(bytes: usize) usize {
|
||||
return alignment + bytes;
|
||||
}
|
||||
|
||||
inline fn fromPtr(ptr: *anyopaque) *Block {
|
||||
const raw: [*]u8 = @ptrCast(ptr);
|
||||
return @ptrCast(@alignCast(raw - @sizeOf(Block)));
|
||||
}
|
||||
|
||||
inline fn data(self: *Block) [*]u8 {
|
||||
const ptr: [*]u8 = @ptrCast(self);
|
||||
return ptr + @sizeOf(Block);
|
||||
}
|
||||
|
||||
inline fn slice(self: *Block) []align(alignment) u8 {
|
||||
const base: [*]align(alignment) u8 = @ptrCast(@alignCast(self));
|
||||
return base[0 .. alignment + self.size];
|
||||
}
|
||||
};
|
||||
|
||||
comptime {
|
||||
std.debug.assert(@sizeOf(Block) == alignment);
|
||||
}
|
||||
|
||||
var instance: ?ZigToCurlAllocator = null;
|
||||
|
||||
allocator: Allocator,
|
||||
|
||||
pub fn init(allocator: Allocator) void {
|
||||
lp.assert(instance == null, "Initialization of curl must happen only once", .{});
|
||||
instance = .{ .allocator = allocator };
|
||||
}
|
||||
|
||||
pub fn interface() libcurl.CurlAllocator {
|
||||
return .{
|
||||
.free = free,
|
||||
.strdup = strdup,
|
||||
.malloc = malloc,
|
||||
.calloc = calloc,
|
||||
.realloc = realloc,
|
||||
};
|
||||
}
|
||||
|
||||
fn _allocBlock(size: usize) ?*Block {
|
||||
const slice = instance.?.allocator.alignedAlloc(u8, .fromByteUnits(alignment), Block.fullsize(size)) catch return null;
|
||||
const block: *Block = @ptrCast(@alignCast(slice.ptr));
|
||||
block.size = size;
|
||||
return block;
|
||||
}
|
||||
|
||||
fn _freeBlock(header: *Block) void {
|
||||
instance.?.allocator.free(header.slice());
|
||||
}
|
||||
|
||||
fn malloc(size: usize) ?*anyopaque {
|
||||
const block = _allocBlock(size) orelse return null;
|
||||
return @ptrCast(block.data());
|
||||
}
|
||||
|
||||
fn calloc(nmemb: usize, size: usize) ?*anyopaque {
|
||||
const total = nmemb * size;
|
||||
const block = _allocBlock(total) orelse return null;
|
||||
const ptr = block.data();
|
||||
@memset(ptr[0..total], 0); // for historical reasons, calloc zeroes memory, but malloc does not.
|
||||
return @ptrCast(ptr);
|
||||
}
|
||||
|
||||
fn realloc(ptr: ?*anyopaque, size: usize) ?*anyopaque {
|
||||
const p = ptr orelse return malloc(size);
|
||||
const block = Block.fromPtr(p);
|
||||
|
||||
const old_size = block.size;
|
||||
if (size == old_size) return ptr;
|
||||
|
||||
if (instance.?.allocator.resize(block.slice(), alignment + size)) {
|
||||
block.size = size;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
const copy_size = @min(old_size, size);
|
||||
const new_block = _allocBlock(size) orelse return null;
|
||||
@memcpy(new_block.data()[0..copy_size], block.data()[0..copy_size]);
|
||||
_freeBlock(block);
|
||||
return @ptrCast(new_block.data());
|
||||
}
|
||||
|
||||
fn free(ptr: ?*anyopaque) void {
|
||||
const p = ptr orelse return;
|
||||
_freeBlock(Block.fromPtr(p));
|
||||
}
|
||||
|
||||
fn strdup(str: [*:0]const u8) ?[*:0]u8 {
|
||||
const len = std.mem.len(str);
|
||||
const header = _allocBlock(len + 1) orelse return null;
|
||||
const ptr = header.data();
|
||||
@memcpy(ptr[0..len], str[0..len]);
|
||||
ptr[len] = 0;
|
||||
return ptr[0..len :0];
|
||||
}
|
||||
};
|
||||
|
||||
fn globalInit(allocator: Allocator) void {
|
||||
ZigToCurlAllocator.init(allocator);
|
||||
// Only route curl's own allocations through our allocator in Debug, so the
|
||||
// leak detector sees them. In Release it'd just wrap c_allocator (curl's
|
||||
// default malloc anyway) at the cost of a per-allocation header.
|
||||
const curl_allocator = comptime if (IS_DEBUG) CurlDebugAllocator.interface() else null;
|
||||
if (comptime IS_DEBUG) {
|
||||
CurlDebugAllocator.init(allocator);
|
||||
}
|
||||
|
||||
libcurl.curl_global_init(.{ .ssl = true }, ZigToCurlAllocator.interface()) catch |err| {
|
||||
libcurl.curl_global_init(.{ .ssl = true }, curl_allocator) catch |err| {
|
||||
lp.assert(false, "curl global init", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user