diff --git a/build.zig b/build.zig index b063c25..69d7845 100644 --- a/build.zig +++ b/build.zig @@ -1,9 +1,14 @@ const std = @import("std"); +const version = @import("build.zig.zon").version; + pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); + const build_options = b.addOptions(); + build_options.addOption([]const u8, "version", version); + const websocket = b.dependency("websocket", .{ .target = target, .optimize = optimize, @@ -16,6 +21,7 @@ pub fn build(b: *std.Build) void { .link_libc = true, .imports = &.{ .{ .name = "websocket", .module = websocket.module("websocket") }, + .{ .name = "build_options", .module = build_options.createModule() }, }, }); diff --git a/src/internal/identity/did_resolver.zig b/src/internal/identity/did_resolver.zig index f05575c..ca5a816 100644 --- a/src/internal/identity/did_resolver.zig +++ b/src/internal/identity/did_resolver.zig @@ -138,26 +138,6 @@ fn percentDecodeAlloc(allocator: std.mem.Allocator, input: []const u8) ![]u8 { // === tests === test "resolve did:plc - integration" { - // use arena for http client internals that may leak - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var resolver = DidResolver.init(std.Options.debug_io, arena.allocator()); - defer resolver.deinit(); - - const did = Did.parse("did:plc:z72i7hdynmk6r22z27h6tvur").?; - var doc = resolver.resolve(did) catch { - return; // network error, expected in CI - }; - defer doc.deinit(); - - try std.testing.expectEqualStrings("did:plc:z72i7hdynmk6r22z27h6tvur", doc.id); - try std.testing.expect(doc.handle() != null); -} - -test "resolve did:plc - leak check (no arena)" { - // repro for memory leak report: use testing.allocator directly - // (no arena) to see if std.http.Client leaks on deinit var resolver = DidResolver.init(std.Options.debug_io, std.testing.allocator); defer resolver.deinit(); @@ -168,6 +148,7 @@ test "resolve did:plc - leak check (no arena)" { defer doc.deinit(); try std.testing.expectEqualStrings("did:plc:z72i7hdynmk6r22z27h6tvur", doc.id); + try std.testing.expect(doc.handle() != null); } test "did:web loopback host is rejected before fetch" { diff --git a/src/internal/xrpc/transport.zig b/src/internal/xrpc/transport.zig index cbd9e01..3b58528 100644 --- a/src/internal/xrpc/transport.zig +++ b/src/internal/xrpc/transport.zig @@ -4,6 +4,7 @@ //! requires std.Io for networking (zig 0.16+). const std = @import("std"); +const build_options = @import("build_options"); /// Sent unless a caller overrides `user_agent`. std.http's default is /// `zig/ (std.http)`, which tells a PDS operator nothing about who is @@ -11,7 +12,7 @@ const std = @import("std"); /// scale should say what it is: operators seeing unexplained load have no way /// to ask about it otherwise, and every other atproto client on the network /// identifies itself. -pub const default_user_agent = "zat/0.3.30 (+https://tangled.org/zat.dev/zat)"; +pub const default_user_agent = "zat/" ++ build_options.version ++ " (+https://tangled.org/zat.dev/zat)"; pub const HttpTransport = struct { allocator: std.mem.Allocator, @@ -83,11 +84,10 @@ pub const HttpTransport = struct { } if (options.extra_headers) |hdrs| { + if (extra_count + hdrs.len > extra_buf.len) return error.TooManyHeaders; for (hdrs) |h| { - if (extra_count < extra_buf.len) { - extra_buf[extra_count] = h; - extra_count += 1; - } + extra_buf[extra_count] = h; + extra_count += 1; } } @@ -617,6 +617,7 @@ test "requests identify the client, and applications can name themselves" { defer default_transport.deinit(); try testing.expectEqualStrings(default_user_agent, default_transport.user_agent); try testing.expect(std.mem.indexOf(u8, default_transport.user_agent, "http") != null); + try testing.expect(std.mem.startsWith(u8, default_transport.user_agent, "zat/" ++ build_options.version ++ " ")); var named = HttpTransport.initWithUserAgent(io, testing.allocator, "stream/1.2.3 (+https://example.invalid)"); defer named.deinit(); @@ -628,6 +629,23 @@ test "requests identify the client, and applications can name themselves" { try testing.expectEqualStrings(default_user_agent, empty.user_agent); } +test "extra headers past the buffer are an error, not silently dropped" { + const testing = std.testing; + var threaded: std.Io.Threaded = .init(testing.allocator, .{}); + defer threaded.deinit(); + + var transport = HttpTransport.init(threaded.io(), testing.allocator); + defer transport.deinit(); + + const header: std.http.Header = .{ .name = "x-zat-test", .value = "1" }; + const too_many = [_]std.http.Header{header} ** 8; + try testing.expectError(error.TooManyHeaders, transport.fetch(.{ + .url = "http://127.0.0.1:1/never-reached", + .accept = "application/json", + .extra_headers = &too_many, + })); +} + // A loopback origin for transport tests. Each canned response is served on its // own accepted connection; `serve` runs on a thread so the client can drive the // exchange from the test body.