From eec06238d05e17627f333d67293d18138fefa117 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sun, 14 Jun 2026 10:17:39 +0800 Subject: [PATCH] nits: just personalizing the code as a means to understand it. --- src/browser/structured_data.zig | 65 +++++++++++++++++++++------------ src/network/Robots.zig | 15 +++++--- 2 files changed, 50 insertions(+), 30 deletions(-) diff --git a/src/browser/structured_data.zig b/src/browser/structured_data.zig index 21e063b86..55043cfb6 100644 --- a/src/browser/structured_data.zig +++ b/src/browser/structured_data.zig @@ -223,12 +223,6 @@ pub fn collectStructuredData( }; } -/// Registered link relations we surface from the HTTP `Link:` response header -/// (RFC 8288). `service-doc`/`service-desc` come from RFC 8631; `api` is in the -/// IANA Link Relations registry. These let an agent discover a site's developer -/// docs / API description without scraping. -const header_link_rels = [_][]const u8{ "service-doc", "service-desc", "api" }; - /// Parse every `Link:` response header retained on the frame, surfacing only the /// registered, agent-useful relations. Relative targets are resolved against the /// frame's base URL, mirroring `collectLink`. @@ -237,8 +231,14 @@ fn collectLinkHeaders( frame: *Frame, out: *std.ArrayList(LinkRel), ) !void { + // Registered link relations we surface from the HTTP `Link:` response header + // (RFC 8288). + const header_link_rels = [_][]const u8{ "service-doc", "service-desc", "api" }; + for (frame._http_headers.items) |header| { - if (!std.ascii.eqlIgnoreCase(header.name, "link")) continue; + if (!std.ascii.eqlIgnoreCase(header.name, "link")) { + continue; + } var it: LinkHeaderIterator = .{ .value = header.value }; while (it.next()) |entry| { @@ -248,17 +248,21 @@ fn collectLinkHeaders( var rel_it = std.mem.tokenizeAny(u8, rel_value, " \t"); while (rel_it.next()) |rel_token| { const known = for (header_link_rels) |candidate| { - if (std.ascii.eqlIgnoreCase(rel_token, candidate)) break candidate; + if (std.ascii.eqlIgnoreCase(rel_token, candidate)) { + break candidate; + } } else continue; const href = URL.resolve(arena, frame.base(), entry.target, .{ .encoding = frame.charset }) catch entry.target; - // Drop duplicate (rel, href) pairs — a target can legitimately - // carry the same relation across repeated headers. - const dup = for (out.items) |existing| { - if (std.mem.eql(u8, existing.rel, known) and std.mem.eql(u8, existing.href, href)) break true; - } else false; - if (!dup) try out.append(arena, .{ .rel = known, .href = href }); + // Drop duplicate (rel, href) pairs (it happens) + for (out.items) |existing| { + if (std.mem.eql(u8, existing.rel, known) and std.mem.eql(u8, existing.href, href)) { + break; + } + } else { + try out.append(arena, .{ .rel = known, .href = href }); + } } } } @@ -274,25 +278,30 @@ const LinkHeaderEntry = struct { /// skipping commas that fall inside the `<...>` target or a `"..."` quoted /// parameter value (RFC 8288 §3). const LinkHeaderIterator = struct { - value: []const u8, i: usize = 0, + value: []const u8, fn next(self: *LinkHeaderIterator) ?LinkHeaderEntry { const v = self.value; - // Loop (rather than recurse) over malformed segments so stack usage is - // independent of the attacker-controlled header length. while (true) { // Skip separators and whitespace between link-values. The comma that // ended the previous link-value is consumed here. - while (self.i < v.len and (std.ascii.isWhitespace(v[self.i]) or v[self.i] == ',')) : (self.i += 1) {} - if (self.i >= v.len) return null; + for (v[self.i..]) |c| { + if (std.ascii.isWhitespace(c) == false and c != ',') { + break; + } + self.i += 1; + } else { + return null; + } // A link-value must begin with the angle-bracketed target. if (v[self.i] != '<') { self.skipToNextComma(); continue; } + const target_start = self.i + 1; const gt = std.mem.indexOfScalarPos(u8, v, target_start, '>') orelse { self.i = v.len; @@ -316,13 +325,18 @@ const LinkHeaderIterator = struct { var in_quotes = false; while (self.i < v.len) : (self.i += 1) { const c = v[self.i]; + + if (c == ',' and in_quotes == false) { + return; + } + if (c == '\\' and in_quotes) { // Skip the escaped byte; guard a trailing backslash. - if (self.i + 1 < v.len) self.i += 1; + if (self.i + 1 < v.len) { + self.i += 1; + } } else if (c == '"') { in_quotes = !in_quotes; - } else if (c == ',' and !in_quotes) { - return; } } } @@ -334,8 +348,11 @@ fn extractRel(params: []const u8) ?[]const u8 { var it = std.mem.splitScalar(u8, params, ';'); while (it.next()) |raw_param| { const param = std.mem.trim(u8, raw_param, &std.ascii.whitespace); - const eq = std.mem.indexOfScalar(u8, param, '=') orelse continue; - if (!std.ascii.eqlIgnoreCase(std.mem.trim(u8, param[0..eq], &std.ascii.whitespace), "rel")) continue; + const eq = std.mem.indexOfScalarPos(u8, param, 0, '=') orelse continue; + const name = std.mem.trim(u8, param[0..eq], &std.ascii.whitespace); + if (std.ascii.eqlIgnoreCase(name, "rel") == false) { + continue; + } var val = std.mem.trim(u8, param[eq + 1 ..], &std.ascii.whitespace); if (val.len >= 2 and val[0] == '"' and val[val.len - 1] == '"') { diff --git a/src/network/Robots.zig b/src/network/Robots.zig index 423a308cf..73038a460 100644 --- a/src/network/Robots.zig +++ b/src/network/Robots.zig @@ -87,6 +87,9 @@ pub const ContentSignal = struct { pub const Robots = @This(); pub const empty: Robots = .{ .rules = &.{}, .content_signals = &.{} }; +// Think twice before deleting/freeing any entries from the map. Readers, e.g. +// get and getContentSignals, receive values from the map, and if another thread +// was to delete / free those values while in use, UAF. pub const RobotStore = struct { const RobotsEntry = union(enum) { present: Robots, @@ -165,10 +168,7 @@ pub const RobotStore = struct { try self.map.put(self.allocator, duped, .{ .present = robots }); } - /// Advisory `Content-Signal` preferences declared for `url`'s host, or null - /// when no robots.txt has been fetched/stored for it. The returned slice is - /// owned by the store; callers must read it synchronously (and dupe to keep - /// it) and must never free it. + // The returned slice is owned by the store pub fn getContentSignals(self: *RobotStore, url: []const u8) ?[]const ContentSignal { self.mutex.lock(); defer self.mutex.unlock(); @@ -237,14 +237,17 @@ fn appendContentSignals( var iter = std.mem.splitScalar(u8, value, ','); while (iter.next()) |raw_pair| { const pair = std.mem.trim(u8, raw_pair, &std.ascii.whitespace); - const eq = std.mem.indexOfScalar(u8, pair, '=') orelse continue; + const eq = std.mem.indexOfScalarPos(u8, pair, 0, '=') orelse continue; const name_raw = std.mem.trim(u8, pair[0..eq], &std.ascii.whitespace); const value_raw = std.mem.trim(u8, pair[eq + 1 ..], &std.ascii.whitespace); - if (name_raw.len == 0) continue; + if (name_raw.len == 0) { + continue; + } const name = try std.ascii.allocLowerString(allocator, name_raw); errdefer allocator.free(name); + const val = try std.ascii.allocLowerString(allocator, value_raw); errdefer allocator.free(val);