atproto utils for zig zat.dev
atproto sdk zig
26
fork

Configure Feed

Select the types of activity you want to include in your feed.

migrate to zig 0.16: std.Io, libc replacements, updated websocket dep

- minimum_zig_version bumped to 0.16.0, version 0.3.0-alpha.1
- link_libc on module (required for libc socket/time ops)
- std.crypto.random → io.random(), std.time.timestamp → gettimeofday
- posix.nanosleep/setsockopt → std.c equivalents
- HttpTransport/XrpcClient/DidResolver/HandleResolver thread io param
- websocket dep updated to Io-migrated fork (b6eb671)
- all 203 tests pass

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+135 -132
+1
build.zig
··· 13 13 .root_source_file = b.path("src/root.zig"), 14 14 .target = target, 15 15 .optimize = optimize, 16 + .link_libc = true, 16 17 .imports = &.{ 17 18 .{ .name = "websocket", .module = websocket.module("websocket") }, 18 19 },
+4 -4
build.zig.zon
··· 1 1 .{ 2 2 .name = .zat, 3 - .version = "0.2.18", 3 + .version = "0.3.0-alpha.1", 4 4 .fingerprint = 0x8da9db57ee82fbe4, 5 - .minimum_zig_version = "0.15.0", 5 + .minimum_zig_version = "0.16.0", 6 6 .dependencies = .{ 7 7 .websocket = .{ 8 - .url = "https://github.com/zzstoatzz/websocket.zig/archive/395d0f4f323a5d5eda64b3b76498b64554469ed5.tar.gz", 9 - .hash = "websocket-0.1.0-ZPISdVJ8AwD7U03ARGgHclzlYSd9GeU91_WDXjRyjYdh", 8 + .url = "https://github.com/zzstoatzz/websocket.zig/archive/b6eb671ecdfbc02fbdbf45a384f38fbbceb5fac7.tar.gz", 9 + .hash = "websocket-0.1.0-ZPISdZinAwCEIdIg2GcJ7RIgpR8GddRl2SI2dVgfenT6", 10 10 }, 11 11 .@"atproto-interop-tests" = .{ 12 12 .url = "https://github.com/bluesky-social/atproto-interop-tests/archive/35bb5638ab1e5ce71fb88a0c95953fc557ef1925.tar.gz",
+8 -2
src/internal/crypto/jwt.zig
··· 11 11 const multibase = @import("multibase.zig"); 12 12 const multicodec = @import("multicodec.zig"); 13 13 14 + fn timestamp() i64 { 15 + var tv: std.c.timeval = undefined; 16 + _ = std.c.gettimeofday(&tv, null); 17 + return tv.sec; 18 + } 19 + 14 20 /// JWT signing algorithm 15 21 pub const Algorithm = enum { 16 22 ES256, // P-256 / secp256r1 ··· 140 146 141 147 /// check if the token is expired 142 148 pub fn isExpired(self: *const Jwt) bool { 143 - const now = std.time.timestamp(); 149 + const now = timestamp(); 144 150 return now > self.payload.exp; 145 151 } 146 152 147 153 /// check if the token is expired with clock skew tolerance (in seconds) 148 154 pub fn isExpiredWithSkew(self: *const Jwt, skew_seconds: i64) bool { 149 - const now = std.time.timestamp(); 155 + const now = timestamp(); 150 156 return now > (self.payload.exp + skew_seconds); 151 157 } 152 158
+1 -1
src/internal/crypto/multibase.zig
··· 83 83 } 84 84 85 85 // repeatedly divide by 58 to extract base58 digits 86 - var digits: std.ArrayList(u8) = .{}; 86 + var digits: std.ArrayList(u8) = .empty; 87 87 defer digits.deinit(allocator); 88 88 89 89 var divisor = try std.math.big.int.Managed.initSet(allocator, @as(u64, 58));
+7 -7
src/internal/identity/did_resolver.zig
··· 16 16 /// plc directory url (default: https://plc.directory) 17 17 plc_url: []const u8 = "https://plc.directory", 18 18 19 - pub fn init(allocator: std.mem.Allocator) DidResolver { 20 - return initWithOptions(allocator, .{}); 19 + pub fn init(io: std.Io, allocator: std.mem.Allocator) DidResolver { 20 + return initWithOptions(io, allocator, .{}); 21 21 } 22 22 23 23 pub const Options = struct { 24 24 keep_alive: bool = true, 25 25 }; 26 26 27 - pub fn initWithOptions(allocator: std.mem.Allocator, options: Options) DidResolver { 28 - var transport = HttpTransport.init(allocator); 27 + pub fn initWithOptions(io: std.Io, allocator: std.mem.Allocator, options: Options) DidResolver { 28 + var transport = HttpTransport.init(io, allocator); 29 29 transport.keep_alive = options.keep_alive; 30 30 return .{ 31 31 .allocator = allocator, ··· 113 113 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 114 114 defer arena.deinit(); 115 115 116 - var resolver = DidResolver.init(arena.allocator()); 116 + var resolver = DidResolver.init(std.Options.debug_io, arena.allocator()); 117 117 defer resolver.deinit(); 118 118 119 119 const did = Did.parse("did:plc:z72i7hdynmk6r22z27h6tvur").?; ··· 131 131 test "resolve did:plc - leak check (no arena)" { 132 132 // repro for memory leak report: use testing.allocator directly 133 133 // (no arena) to see if std.http.Client leaks on deinit 134 - var resolver = DidResolver.init(std.testing.allocator); 134 + var resolver = DidResolver.init(std.Options.debug_io, std.testing.allocator); 135 135 defer resolver.deinit(); 136 136 137 137 const did = Did.parse("did:plc:z72i7hdynmk6r22z27h6tvur").?; ··· 146 146 147 147 test "did:web url construction" { 148 148 // test url building without network 149 - var resolver = DidResolver.init(std.testing.allocator); 149 + var resolver = DidResolver.init(std.Options.debug_io, std.testing.allocator); 150 150 defer resolver.deinit(); 151 151 152 152 // simple domain
+5 -5
src/internal/identity/handle_resolver.zig
··· 18 18 transport: HttpTransport, 19 19 doh_endpoint: []const u8, 20 20 21 - pub fn init(allocator: std.mem.Allocator) HandleResolver { 21 + pub fn init(io: std.Io, allocator: std.mem.Allocator) HandleResolver { 22 22 return .{ 23 23 .allocator = allocator, 24 - .transport = HttpTransport.init(allocator), 24 + .transport = HttpTransport.init(io, allocator), 25 25 .doh_endpoint = "https://cloudflare-dns.com/dns-query", 26 26 }; 27 27 } ··· 163 163 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 164 164 defer arena.deinit(); 165 165 166 - var resolver = HandleResolver.init(arena.allocator()); 166 + var resolver = HandleResolver.init(std.Options.debug_io, arena.allocator()); 167 167 defer resolver.deinit(); 168 168 169 169 // resolve a known handle that has .well-known/atproto-did ··· 183 183 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 184 184 defer arena.deinit(); 185 185 186 - var resolver = HandleResolver.init(arena.allocator()); 186 + var resolver = HandleResolver.init(std.Options.debug_io, arena.allocator()); 187 187 defer resolver.deinit(); 188 188 189 189 const handle = Handle.parse("seiso.moe") orelse return error.InvalidHandle; ··· 202 202 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 203 203 defer arena.deinit(); 204 204 205 - var resolver = HandleResolver.init(arena.allocator()); 205 + var resolver = HandleResolver.init(std.Options.debug_io, arena.allocator()); 206 206 defer resolver.deinit(); 207 207 208 208 const handle = Handle.parse("jay.bsky.social") orelse return error.InvalidHandle;
+38 -29
src/internal/oauth.zig
··· 11 11 const Keypair = @import("crypto/keypair.zig").Keypair; 12 12 const jwt = @import("crypto/jwt.zig"); 13 13 14 + fn timestamp() i64 { 15 + var tv: std.c.timeval = undefined; 16 + _ = std.c.gettimeofday(&tv, null); 17 + return tv.sec; 18 + } 19 + 14 20 /// create a signed JWT from header and payload JSON strings. 15 21 /// caller owns returned slice. 16 22 pub fn createJwt(allocator: Allocator, header_json: []const u8, payload_json: []const u8, keypair: *const Keypair) ![]u8 { ··· 35 41 /// ath: optional access token hash (base64url-encoded SHA-256). 36 42 pub fn createDpopProof( 37 43 allocator: Allocator, 44 + io: std.Io, 38 45 keypair: *const Keypair, 39 46 htm: []const u8, 40 47 htu: []const u8, ··· 44 51 const jwk_json = try keypair.jwk(allocator); 45 52 defer allocator.free(jwk_json); 46 53 47 - const jti = try generateJti(allocator); 54 + const jti = try generateJti(allocator, io); 48 55 defer allocator.free(jti); 49 56 50 57 const alg = @tagName(keypair.algorithm()); 51 - const now = std.time.timestamp(); 58 + const now = timestamp(); 52 59 53 60 // header: {"typ":"dpop+jwt","alg":"...","jwk":{...}} 54 61 const header = try std.fmt.allocPrint(allocator, ··· 57 64 defer allocator.free(header); 58 65 59 66 // payload — build with writer for optional fields 60 - var payload_buf: std.ArrayList(u8) = .{}; 61 - defer payload_buf.deinit(allocator); 62 - const writer = payload_buf.writer(allocator); 67 + var aw: std.Io.Writer.Allocating = .init(allocator); 68 + defer aw.deinit(); 63 69 64 - try writer.print( 70 + try aw.writer.print( 65 71 \\{{"jti":"{s}","htm":"{s}","htu":"{s}","iat":{d} 66 72 , .{ jti, htm, htu, now }); 67 73 68 74 if (nonce) |n| { 69 - try writer.print(",\"nonce\":\"{s}\"", .{n}); 75 + try aw.writer.print(",\"nonce\":\"{s}\"", .{n}); 70 76 } 71 77 if (ath) |a| { 72 - try writer.print(",\"ath\":\"{s}\"", .{a}); 78 + try aw.writer.print(",\"ath\":\"{s}\"", .{a}); 73 79 } 74 80 75 - try writer.writeAll("}"); 81 + try aw.writer.writeAll("}"); 76 82 77 - return createJwt(allocator, header, payload_buf.items, keypair); 83 + return createJwt(allocator, header, aw.written(), keypair); 78 84 } 79 85 80 86 /// create a private_key_jwt client assertion for token endpoint auth. 81 87 /// client_id: the OAuth client ID, aud: the token endpoint URL. 82 88 pub fn createClientAssertion( 83 89 allocator: Allocator, 90 + io: std.Io, 84 91 keypair: *const Keypair, 85 92 client_id: []const u8, 86 93 aud: []const u8, 87 94 ) ![]u8 { 88 - const jti = try generateJti(allocator); 95 + const jti = try generateJti(allocator, io); 89 96 defer allocator.free(jti); 90 97 91 98 const kid = try keypair.jwkThumbprint(allocator); 92 99 defer allocator.free(kid); 93 100 94 101 const alg = @tagName(keypair.algorithm()); 95 - const now = std.time.timestamp(); 102 + const now = timestamp(); 96 103 97 104 const header = try std.fmt.allocPrint(allocator, 98 105 \\{{"typ":"JWT","alg":"{s}","kid":"{s}"}} ··· 109 116 110 117 /// generate a random PKCE code verifier (43 chars, base64url-encoded 32 random bytes). 111 118 /// caller owns returned slice. 112 - pub fn generatePkceVerifier(allocator: Allocator) ![]u8 { 119 + pub fn generatePkceVerifier(allocator: Allocator, io: std.Io) ![]u8 { 113 120 var random_bytes: [32]u8 = undefined; 114 - crypto.random.bytes(&random_bytes); 121 + io.random(&random_bytes); 115 122 return jwt.base64UrlEncode(allocator, &random_bytes); 116 123 } 117 124 ··· 125 132 126 133 /// generate a random state parameter (CSRF token). 127 134 /// caller owns returned slice. 128 - pub fn generateState(allocator: Allocator) ![]u8 { 135 + pub fn generateState(allocator: Allocator, io: std.Io) ![]u8 { 129 136 var random_bytes: [16]u8 = undefined; 130 - crypto.random.bytes(&random_bytes); 137 + io.random(&random_bytes); 131 138 return jwt.base64UrlEncode(allocator, &random_bytes); 132 139 } 133 140 ··· 142 149 /// encode key-value pairs as application/x-www-form-urlencoded. 143 150 /// caller owns returned slice. 144 151 pub fn formEncode(allocator: Allocator, params: []const [2][]const u8) ![]u8 { 145 - var buf: std.ArrayList(u8) = .{}; 146 - errdefer buf.deinit(allocator); 147 - const writer = buf.writer(allocator); 152 + var aw: std.Io.Writer.Allocating = .init(allocator); 153 + errdefer aw.deinit(); 148 154 149 155 for (params, 0..) |kv, i| { 150 - if (i > 0) try writer.writeAll("&"); 151 - try percentEncode(writer, kv[0]); 152 - try writer.writeAll("="); 153 - try percentEncode(writer, kv[1]); 156 + if (i > 0) try aw.writer.writeAll("&"); 157 + try percentEncode(&aw.writer, kv[0]); 158 + try aw.writer.writeAll("="); 159 + try percentEncode(&aw.writer, kv[1]); 154 160 } 155 161 156 - return buf.toOwnedSlice(allocator); 162 + return try aw.toOwnedSlice(); 157 163 } 158 164 159 165 /// format a JWKS JSON containing a single public key. ··· 169 175 170 176 // --- helpers --- 171 177 172 - fn generateJti(allocator: Allocator) ![]u8 { 178 + fn generateJti(allocator: Allocator, io: std.Io) ![]u8 { 173 179 var random_bytes: [16]u8 = undefined; 174 - crypto.random.bytes(&random_bytes); 180 + io.random(&random_bytes); 175 181 return jwt.base64UrlEncode(allocator, &random_bytes); 176 182 } 177 183 ··· 204 210 205 211 test "PKCE verifier is 43 chars" { 206 212 const allocator = std.testing.allocator; 207 - const verifier = try generatePkceVerifier(allocator); 213 + const io = std.Options.debug_io; 214 + const verifier = try generatePkceVerifier(allocator, io); 208 215 defer allocator.free(verifier); 209 216 try std.testing.expectEqual(@as(usize, 43), verifier.len); 210 217 } ··· 276 283 277 284 test "DPoP proof structure" { 278 285 const allocator = std.testing.allocator; 286 + const io = std.Options.debug_io; 279 287 280 288 const keypair = try Keypair.fromSecretKey(.p256, .{ 281 289 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, ··· 284 292 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 285 293 }); 286 294 287 - const proof = try createDpopProof(allocator, &keypair, "POST", "https://auth.example.com/token", "server-nonce", null); 295 + const proof = try createDpopProof(allocator, io, &keypair, "POST", "https://auth.example.com/token", "server-nonce", null); 288 296 defer allocator.free(proof); 289 297 290 298 // decode header ··· 319 327 320 328 test "client assertion structure" { 321 329 const allocator = std.testing.allocator; 330 + const io = std.Options.debug_io; 322 331 323 332 const keypair = try Keypair.fromSecretKey(.p256, .{ 324 333 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, ··· 327 336 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 328 337 }); 329 338 330 - const assertion = try createClientAssertion(allocator, &keypair, "https://app.example.com/client-metadata", "https://bsky.social/oauth/token"); 339 + const assertion = try createClientAssertion(allocator, io, &keypair, "https://app.example.com/client-metadata", "https://bsky.social/oauth/token"); 331 340 defer allocator.free(assertion); 332 341 333 342 // decode header
+12 -12
src/internal/repo/car.zig
··· 76 76 const header = cbor.decodeAll(allocator, header_bytes) catch return error.InvalidHeader; 77 77 78 78 // extract roots (array of CID links) 79 - var roots: std.ArrayList(cbor.Cid) = .{}; 79 + var roots: std.ArrayList(cbor.Cid) = .empty; 80 80 if (header.getArray("roots")) |root_values| { 81 81 for (root_values) |root_val| { 82 82 switch (root_val) { ··· 89 89 pos = header_end; 90 90 91 91 // read blocks 92 - var blocks: std.ArrayList(Block) = .{}; 92 + var blocks: std.ArrayList(Block) = .empty; 93 93 var block_index: std.StringHashMapUnmanaged([]const u8) = .empty; 94 94 95 95 while (pos < data.len) { ··· 190 190 /// where each block is: [varint block_len] [CID bytes] [data bytes] 191 191 pub fn write(allocator: Allocator, writer: anytype, c: Car) !void { 192 192 // build header: {"roots": [...CID links...], "version": 1} 193 - var root_values: std.ArrayList(cbor.Value) = .{}; 193 + var root_values: std.ArrayList(cbor.Value) = .empty; 194 194 defer root_values.deinit(allocator); 195 195 for (c.roots) |root| { 196 196 try root_values.append(allocator, .{ .cid = root }); ··· 220 220 221 221 /// write a CAR v1 file to a freshly allocated byte slice 222 222 pub fn writeAlloc(allocator: Allocator, c: Car) ![]u8 { 223 - var list: std.ArrayList(u8) = .{}; 224 - errdefer list.deinit(allocator); 225 - try write(allocator, list.writer(allocator), c); 226 - return try list.toOwnedSlice(allocator); 223 + var aw: std.Io.Writer.Allocating = .init(allocator); 224 + errdefer aw.deinit(); 225 + try write(allocator, &aw.writer, c); 226 + return try aw.toOwnedSlice(); 227 227 } 228 228 229 229 // === tests === ··· 331 331 const header_bytes = try cbor.encodeAlloc(alloc, header_value); 332 332 333 333 // assemble minimal CAR: header only, no blocks 334 - var car_buf: std.ArrayList(u8) = .{}; 335 - defer car_buf.deinit(alloc); 336 - try cbor.writeUvarint(car_buf.writer(alloc), header_bytes.len); 337 - try car_buf.appendSlice(alloc, header_bytes); 334 + var car_aw: std.Io.Writer.Allocating = .init(alloc); 335 + defer car_aw.deinit(); 336 + try cbor.writeUvarint(&car_aw.writer, header_bytes.len); 337 + try car_aw.writer.writeAll(header_bytes); 338 338 339 - const car_file = try read(alloc, car_buf.items); 339 + const car_file = try read(alloc, car_aw.written()); 340 340 try std.testing.expectEqual(@as(usize, 1), car_file.roots.len); 341 341 try std.testing.expectEqual(root_cid.version().?, car_file.roots[0].version().?); 342 342 try std.testing.expectEqual(root_cid.codec().?, car_file.roots[0].codec().?);
+12 -13
src/internal/repo/cbor.zig
··· 201 201 var hash: [Sha256.digest_length]u8 = undefined; 202 202 Sha256.hash(data, &hash, .{}); 203 203 204 - var raw_buf: std.ArrayList(u8) = .{}; 205 - errdefer raw_buf.deinit(allocator); 206 - const writer = raw_buf.writer(allocator); 207 - try writeUvarint(writer, ver); 208 - try writeUvarint(writer, cod); 209 - try writeUvarint(writer, hash_fn_code); 210 - try writeUvarint(writer, Sha256.digest_length); 211 - try writer.writeAll(&hash); 204 + var aw: std.Io.Writer.Allocating = .init(allocator); 205 + errdefer aw.deinit(); 206 + try writeUvarint(&aw.writer, ver); 207 + try writeUvarint(&aw.writer, cod); 208 + try writeUvarint(&aw.writer, hash_fn_code); 209 + try writeUvarint(&aw.writer, Sha256.digest_length); 210 + try aw.writer.writeAll(&hash); 212 211 213 - return .{ .raw = try raw_buf.toOwnedSlice(allocator) }; 212 + return .{ .raw = try aw.toOwnedSlice() }; 214 213 } 215 214 216 215 /// serialize this CID to raw bytes (version varint + codec varint + multihash) ··· 487 486 488 487 /// encode a Value to a freshly allocated byte slice 489 488 pub fn encodeAlloc(allocator: Allocator, value: Value) ![]u8 { 490 - var list: std.ArrayList(u8) = .{}; 491 - errdefer list.deinit(allocator); 492 - try encode(allocator, list.writer(allocator), value); 493 - return try list.toOwnedSlice(allocator); 489 + var aw: std.Io.Writer.Allocating = .init(allocator); 490 + errdefer aw.deinit(); 491 + try encode(allocator, &aw.writer, value); 492 + return try aw.toOwnedSlice(); 494 493 } 495 494 496 495 /// write an unsigned varint (LEB128) — used for CID and CAR serialization
+3 -3
src/internal/repo/mst.zig
··· 86 86 fn init() Node { 87 87 return .{ 88 88 .left = .none, 89 - .entries = .{}, 89 + .entries = .empty, 90 90 }; 91 91 } 92 92 }; ··· 341 341 return .{ .node = merged }; 342 342 } 343 343 344 - pub const MstError = error{PartialTree} || Allocator.Error; 344 + pub const MstError = error{ PartialTree, WriteFailed } || Allocator.Error; 345 345 346 346 /// compute the root CID of the tree 347 347 pub fn rootCid(self: *Mst) MstError!cbor.Cid { ··· 383 383 }; 384 384 385 385 // build entry array with prefix compression 386 - var entry_values: std.ArrayList(cbor.Value) = .{}; 386 + var entry_values: std.ArrayList(cbor.Value) = .empty; 387 387 defer entry_values.deinit(self.allocator); 388 388 389 389 var prev_key: []const u8 = "";
+9 -8
src/internal/repo/repo_verifier.zig
··· 142 142 SignatureVerificationFailed, 143 143 MstRootMismatch, 144 144 OutOfMemory, 145 + WriteFailed, 145 146 }; 146 147 147 148 /// verify a repo end-to-end: resolve identity, fetch repo, verify commit signature, walk and rebuild MST. 148 - pub fn verifyRepo(caller_alloc: Allocator, identifier: []const u8) !VerifyResult { 149 + pub fn verifyRepo(io: std.Io, caller_alloc: Allocator, identifier: []const u8) !VerifyResult { 149 150 var arena = std.heap.ArenaAllocator.init(caller_alloc); 150 151 defer arena.deinit(); 151 152 const allocator = arena.allocator(); ··· 155 156 identifier 156 157 else blk: { 157 158 const handle = Handle.parse(identifier) orelse return error.InvalidIdentifier; 158 - var resolver = HandleResolver.init(allocator); 159 + var resolver = HandleResolver.init(io, allocator); 159 160 defer resolver.deinit(); 160 161 break :blk try resolver.resolve(handle); 161 162 }; ··· 163 164 const did = Did.parse(did_str) orelse return error.InvalidIdentifier; 164 165 165 166 // 2. resolve DID → DID document 166 - var did_resolver = DidResolver.init(allocator); 167 + var did_resolver = DidResolver.init(io, allocator); 167 168 defer did_resolver.deinit(); 168 169 var did_doc = try did_resolver.resolve(did); 169 170 defer did_doc.deinit(); ··· 177 178 const pds_endpoint = did_doc.pdsEndpoint() orelse return error.PdsEndpointNotFound; 178 179 179 180 // 5. fetch repo CAR 180 - const car_bytes = try fetchRepo(allocator, pds_endpoint, did_str); 181 + const car_bytes = try fetchRepo(io, allocator, pds_endpoint, did_str); 181 182 182 183 // 6-10. verify CAR: signature, commit structure, MST 183 184 const commit_result = verifyCommitCar(allocator, car_bytes, public_key, .{ ··· 202 203 } 203 204 204 205 /// fetch a repo CAR from a PDS endpoint 205 - fn fetchRepo(allocator: Allocator, pds_endpoint: []const u8, did_str: []const u8) ![]u8 { 206 - var transport = HttpTransport.init(allocator); 206 + fn fetchRepo(io: std.Io, allocator: Allocator, pds_endpoint: []const u8, did_str: []const u8) ![]u8 { 207 + var transport = HttpTransport.init(io, allocator); 207 208 defer transport.deinit(); 208 209 209 210 // build URL: {pds}/xrpc/com.atproto.sync.getRepo?did={did} ··· 222 223 }; 223 224 224 225 // filter out "sig", keep everything else 225 - var unsigned_entries: std.ArrayList(cbor.Value.MapEntry) = .{}; 226 + var unsigned_entries: std.ArrayList(cbor.Value.MapEntry) = .empty; 226 227 for (entries) |entry| { 227 228 if (!std.mem.eql(u8, entry.key, "sig")) { 228 229 try unsigned_entries.append(allocator, entry); ··· 475 476 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 476 477 defer arena.deinit(); 477 478 478 - const result = verifyRepo(arena.allocator(), "zzstoatzz.io") catch |err| { 479 + const result = verifyRepo(std.Options.debug_io, arena.allocator(), "zzstoatzz.io") catch |err| { 479 480 std.debug.print("network error (expected in CI): {}\n", .{err}); 480 481 return; 481 482 };
+20 -20
src/internal/streaming/firehose.zig
··· 18 18 const mem = std.mem; 19 19 const Allocator = mem.Allocator; 20 20 const posix = std.posix; 21 + const libc = std.c; 21 22 const log = std.log.scoped(.zat); 22 23 23 24 pub const CommitAction = sync.CommitAction; ··· 161 162 } 162 163 163 164 // parse blobs array (array of CID links) 164 - var blobs: std.ArrayList(cbor.Cid) = .{}; 165 + var blobs: std.ArrayList(cbor.Cid) = .empty; 165 166 if (payload.getArray("blobs")) |blob_values| { 166 167 for (blob_values) |blob_val| { 167 168 switch (blob_val) { ··· 180 181 181 182 // parse ops 182 183 const ops_array = payload.getArray("ops"); 183 - var ops: std.ArrayList(RepoOp) = .{}; 184 + var ops: std.ArrayList(RepoOp) = .empty; 184 185 185 186 if (ops_array) |op_values| { 186 187 for (op_values) |op_val| { ··· 257 258 258 259 /// encode a firehose Event into a wire frame: [DAG-CBOR header] [DAG-CBOR payload] 259 260 pub fn encodeFrame(allocator: Allocator, event: Event) ![]u8 { 260 - var list: std.ArrayList(u8) = .{}; 261 - errdefer list.deinit(allocator); 262 - const writer = list.writer(allocator); 261 + var aw: std.Io.Writer.Allocating = .init(allocator); 262 + errdefer aw.deinit(); 263 263 264 264 const tag = switch (event) { 265 265 .commit => "#commit", ··· 273 273 .{ .key = "op", .value = .{ .unsigned = 1 } }, 274 274 .{ .key = "t", .value = .{ .text = tag } }, 275 275 } }; 276 - try cbor.encode(allocator, writer, header); 276 + try cbor.encode(allocator, &aw.writer, header); 277 277 278 278 // encode payload based on event type 279 279 switch (event) { 280 - .commit => |c| try encodeCommitPayload(allocator, writer, c), 281 - .identity => |i| try encodeIdentityPayload(allocator, writer, i), 282 - .account => |a| try encodeAccountPayload(allocator, writer, a), 283 - .info => |inf| try encodeInfoPayload(allocator, writer, inf), 280 + .commit => |commit| try encodeCommitPayload(allocator, &aw.writer, commit), 281 + .identity => |id| try encodeIdentityPayload(allocator, &aw.writer, id), 282 + .account => |acct| try encodeAccountPayload(allocator, &aw.writer, acct), 283 + .info => |inf| try encodeInfoPayload(allocator, &aw.writer, inf), 284 284 } 285 285 286 - return try list.toOwnedSlice(allocator); 286 + return try aw.toOwnedSlice(); 287 287 } 288 288 289 289 fn encodeCommitPayload(allocator: Allocator, writer: anytype, commit: CommitEvent) !void { 290 290 // build ops array and CAR blocks simultaneously 291 - var op_values: std.ArrayList(cbor.Value) = .{}; 291 + var op_values: std.ArrayList(cbor.Value) = .empty; 292 292 defer op_values.deinit(allocator); 293 - var car_blocks: std.ArrayList(car.Block) = .{}; 293 + var car_blocks: std.ArrayList(car.Block) = .empty; 294 294 defer car_blocks.deinit(allocator); 295 - var root_cids: std.ArrayList(cbor.Cid) = .{}; 295 + var root_cids: std.ArrayList(cbor.Cid) = .empty; 296 296 defer root_cids.deinit(allocator); 297 297 298 298 for (commit.ops) |op| { ··· 334 334 const blocks_bytes = try car.writeAlloc(allocator, car_data); 335 335 336 336 // build blobs array 337 - var blob_values: std.ArrayList(cbor.Value) = .{}; 337 + var blob_values: std.ArrayList(cbor.Value) = .empty; 338 338 defer blob_values.deinit(allocator); 339 339 for (commit.blobs) |blob| { 340 340 try blob_values.append(allocator, .{ .cid = blob }); 341 341 } 342 342 343 343 // build payload entries 344 - var entries: std.ArrayList(cbor.Value.MapEntry) = .{}; 344 + var entries: std.ArrayList(cbor.Value.MapEntry) = .empty; 345 345 defer entries.deinit(allocator); 346 346 347 347 try entries.append(allocator, .{ .key = "blocks", .value = .{ .bytes = blocks_bytes } }); ··· 365 365 } 366 366 367 367 fn encodeIdentityPayload(allocator: Allocator, writer: anytype, identity: IdentityEvent) !void { 368 - var entries: std.ArrayList(cbor.Value.MapEntry) = .{}; 368 + var entries: std.ArrayList(cbor.Value.MapEntry) = .empty; 369 369 defer entries.deinit(allocator); 370 370 371 371 try entries.append(allocator, .{ .key = "did", .value = .{ .text = identity.did } }); ··· 379 379 } 380 380 381 381 fn encodeAccountPayload(allocator: Allocator, writer: anytype, account: AccountEvent) !void { 382 - var entries: std.ArrayList(cbor.Value.MapEntry) = .{}; 382 + var entries: std.ArrayList(cbor.Value.MapEntry) = .empty; 383 383 defer entries.deinit(allocator); 384 384 385 385 if (!account.active) { ··· 396 396 } 397 397 398 398 fn encodeInfoPayload(allocator: Allocator, writer: anytype, info: InfoEvent) !void { 399 - var entries: std.ArrayList(cbor.Value.MapEntry) = .{}; 399 + var entries: std.ArrayList(cbor.Value.MapEntry) = .empty; 400 400 defer entries.deinit(allocator); 401 401 402 402 if (info.message) |m| { ··· 456 456 457 457 prev_host_index = effective_index; 458 458 host_index += 1; 459 - posix.nanosleep(backoff, 0); 459 + _ = libc.nanosleep(&.{ .sec = @intCast(backoff), .nsec = 0 }, null); 460 460 backoff = @min(backoff * 2, max_backoff); 461 461 } 462 462 }
+2 -1
src/internal/streaming/jetstream.zig
··· 13 13 const mem = std.mem; 14 14 const json = std.json; 15 15 const posix = std.posix; 16 + const libc = std.c; 16 17 const Allocator = mem.Allocator; 17 18 const log = std.log.scoped(.zat); 18 19 ··· 180 181 181 182 prev_host_index = effective_index; 182 183 host_index += 1; 183 - posix.nanosleep(backoff, 0); 184 + _ = libc.nanosleep(&.{ .sec = @intCast(backoff), .nsec = 0 }, null); 184 185 backoff = @min(backoff * 2, max_backoff); 185 186 } 186 187 }
+9 -23
src/internal/xrpc/transport.zig
··· 1 - //! HTTP Transport - isolates HTTP client for 0.16 migration 2 - //! 3 - //! wraps std.http.Client to provide a single point of change 4 - //! when zig 0.16 moves HTTP to std.Io interface. 1 + //! HTTP Transport - wraps std.http.Client for AT Protocol requests 5 2 //! 6 - //! 0.16 migration plan: 7 - //! - add `io: std.Io` field 8 - //! - add `initWithIo(io: std.Io, allocator: Allocator)` constructor 9 - //! - update fetch() to use io.http or equivalent 3 + //! provides a simple fetch() interface over zig's HTTP client. 4 + //! requires std.Io for networking (zig 0.16+). 10 5 11 6 const std = @import("std"); 12 7 13 8 pub const HttpTransport = struct { 14 9 allocator: std.mem.Allocator, 10 + io: std.Io, 15 11 http_client: std.http.Client, 16 12 keep_alive: bool = true, 17 13 18 - // 0.16: will add 19 - // io: ?std.Io = null, 20 - 21 - pub fn init(allocator: std.mem.Allocator) HttpTransport { 14 + pub fn init(io: std.Io, allocator: std.mem.Allocator) HttpTransport { 22 15 return .{ 23 16 .allocator = allocator, 24 - .http_client = .{ .allocator = allocator }, 17 + .io = io, 18 + .http_client = .{ .allocator = allocator, .io = io }, 25 19 }; 26 20 } 27 - 28 - // 0.16: will add 29 - // pub fn initWithIo(io: std.Io, allocator: std.mem.Allocator) HttpTransport { 30 - // return .{ 31 - // .allocator = allocator, 32 - // .http_client = .{ .allocator = allocator }, 33 - // .io = io, 34 - // }; 35 - // } 36 21 37 22 pub fn deinit(self: *HttpTransport) void { 38 23 self.http_client.deinit(); ··· 109 94 // === tests === 110 95 111 96 test "transport init/deinit" { 112 - var transport = HttpTransport.init(std.testing.allocator); 97 + const io = std.Options.debug_io; 98 + var transport = HttpTransport.init(io, std.testing.allocator); 113 99 defer transport.deinit(); 114 100 }
+4 -4
src/internal/xrpc/xrpc.zig
··· 22 22 /// atproto JWTs are ~1KB; buffer needs room for "Bearer " prefix 23 23 const max_auth_header_len = 2048; 24 24 25 - pub fn init(allocator: std.mem.Allocator, host: []const u8) XrpcClient { 25 + pub fn init(io: std.Io, allocator: std.mem.Allocator, host: []const u8) XrpcClient { 26 26 return .{ 27 27 .allocator = allocator, 28 - .transport = HttpTransport.init(allocator), 28 + .transport = HttpTransport.init(io, allocator), 29 29 .host = host, 30 30 }; 31 31 } ··· 130 130 // === tests === 131 131 132 132 test "build url without params" { 133 - var client = XrpcClient.init(std.testing.allocator, "https://bsky.social"); 133 + var client = XrpcClient.init(std.Options.debug_io, std.testing.allocator, "https://bsky.social"); 134 134 defer client.deinit(); 135 135 136 136 const nsid = Nsid.parse("app.bsky.actor.getProfile").?; ··· 141 141 } 142 142 143 143 test "build url with params" { 144 - var client = XrpcClient.init(std.testing.allocator, "https://bsky.social"); 144 + var client = XrpcClient.init(std.Options.debug_io, std.testing.allocator, "https://bsky.social"); 145 145 defer client.deinit(); 146 146 147 147 var params = std.StringHashMap([]const u8).init(std.testing.allocator);