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.

fix: memory safety issues in jwt and json tests

- fix use-after-free in jwt payload parsing (dupe strings)
- fix crypto.sign.ecdsa path for zig 0.15
- fix test token to have correct 64-byte signature
- use arena allocator in extractAt tests to avoid leaks

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

zzstoatzz 240c179c 7fbd203b

+51 -24
+21 -13
src/internal/json.zig
··· 200 200 \\ } 201 201 \\} 202 202 ; 203 - const parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json_str, .{}); 204 - defer parsed.deinit(); 203 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 204 + defer arena.deinit(); 205 + 206 + const parsed = try std.json.parseFromSlice(std.json.Value, arena.allocator(), json_str, .{}); 205 207 206 208 const External = struct { 207 209 uri: []const u8, 208 210 title: []const u8, 209 211 }; 210 212 211 - const ext = try extractAt(External, std.testing.allocator, parsed.value, .{ "embed", "external" }); 213 + const ext = try extractAt(External, arena.allocator(), parsed.value, .{ "embed", "external" }); 212 214 try std.testing.expectEqualStrings("https://tangled.sh", ext.uri); 213 215 try std.testing.expectEqualStrings("Tangled", ext.title); 214 216 } ··· 222 224 \\ } 223 225 \\} 224 226 ; 225 - const parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json_str, .{}); 226 - defer parsed.deinit(); 227 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 228 + defer arena.deinit(); 229 + 230 + const parsed = try std.json.parseFromSlice(std.json.Value, arena.allocator(), json_str, .{}); 227 231 228 232 const User = struct { 229 233 name: []const u8, ··· 231 235 bio: ?[]const u8 = null, 232 236 }; 233 237 234 - const user = try extractAt(User, std.testing.allocator, parsed.value, .{"user"}); 238 + const user = try extractAt(User, arena.allocator(), parsed.value, .{"user"}); 235 239 try std.testing.expectEqualStrings("alice", user.name); 236 240 try std.testing.expectEqual(@as(i64, 30), user.age); 237 241 try std.testing.expect(user.bio == null); ··· 241 245 const json_str = 242 246 \\{"name": "root", "value": 42} 243 247 ; 244 - const parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json_str, .{}); 245 - defer parsed.deinit(); 248 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 249 + defer arena.deinit(); 250 + 251 + const parsed = try std.json.parseFromSlice(std.json.Value, arena.allocator(), json_str, .{}); 246 252 247 253 const Root = struct { 248 254 name: []const u8, 249 255 value: i64, 250 256 }; 251 257 252 - const root = try extractAt(Root, std.testing.allocator, parsed.value, .{}); 258 + const root = try extractAt(Root, arena.allocator(), parsed.value, .{}); 253 259 try std.testing.expectEqualStrings("root", root.name); 254 260 try std.testing.expectEqual(@as(i64, 42), root.value); 255 261 } ··· 258 264 const json_str = 259 265 \\{"exists": {"value": 1}} 260 266 ; 261 - const parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json_str, .{}); 262 - defer parsed.deinit(); 267 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 268 + defer arena.deinit(); 269 + 270 + const parsed = try std.json.parseFromSlice(std.json.Value, arena.allocator(), json_str, .{}); 263 271 264 272 const Thing = struct { value: i64 }; 265 273 266 - const exists = extractAtOptional(Thing, std.testing.allocator, parsed.value, .{"exists"}); 274 + const exists = extractAtOptional(Thing, arena.allocator(), parsed.value, .{"exists"}); 267 275 try std.testing.expect(exists != null); 268 276 try std.testing.expectEqual(@as(i64, 1), exists.?.value); 269 277 270 - const missing = extractAtOptional(Thing, std.testing.allocator, parsed.value, .{"missing"}); 278 + const missing = extractAtOptional(Thing, arena.allocator(), parsed.value, .{"missing"}); 271 279 try std.testing.expect(missing == null); 272 280 }
+30 -11
src/internal/jwt.zig
··· 152 152 153 153 pub fn deinit(self: *Jwt) void { 154 154 self.allocator.free(self.signature); 155 + self.allocator.free(self.payload.iss); 156 + self.allocator.free(self.payload.aud); 157 + if (self.payload.jti) |s| self.allocator.free(s); 158 + if (self.payload.lxm) |s| self.allocator.free(s); 155 159 } 156 160 }; 157 161 ··· 167 171 } 168 172 169 173 fn parseHeader(allocator: std.mem.Allocator, header_json: []const u8) !Header { 170 - _ = allocator; 171 - const parsed = try std.json.parseFromSlice(std.json.Value, std.heap.page_allocator, header_json, .{}); 174 + const parsed = try std.json.parseFromSlice(std.json.Value, allocator, header_json, .{}); 172 175 defer parsed.deinit(); 173 176 174 177 const alg_str = json.getString(parsed.value, "alg") orelse return error.MissingAlgorithm; 175 178 const alg = Algorithm.fromString(alg_str) orelse return error.UnsupportedAlgorithm; 176 - const typ = json.getString(parsed.value, "typ") orelse "JWT"; 177 179 178 180 return .{ 179 181 .alg = alg, 180 - .typ = typ, 182 + .typ = "JWT", // static string, no need to dupe 181 183 }; 182 184 } 183 185 184 186 fn parsePayload(allocator: std.mem.Allocator, payload_json: []const u8) !Payload { 185 - _ = allocator; 186 - const parsed = try std.json.parseFromSlice(std.json.Value, std.heap.page_allocator, payload_json, .{}); 187 + const parsed = try std.json.parseFromSlice(std.json.Value, allocator, payload_json, .{}); 187 188 defer parsed.deinit(); 188 189 189 - const iss = json.getString(parsed.value, "iss") orelse return error.MissingIssuer; 190 - const aud = json.getString(parsed.value, "aud") orelse return error.MissingAudience; 190 + const iss_raw = json.getString(parsed.value, "iss") orelse return error.MissingIssuer; 191 + const aud_raw = json.getString(parsed.value, "aud") orelse return error.MissingAudience; 191 192 const exp = json.getInt(parsed.value, "exp") orelse return error.MissingExpiration; 192 193 194 + // dupe strings so they outlive parsed 195 + const iss = try allocator.dupe(u8, iss_raw); 196 + errdefer allocator.free(iss); 197 + 198 + const aud = try allocator.dupe(u8, aud_raw); 199 + errdefer allocator.free(aud); 200 + 201 + const jti: ?[]const u8 = if (json.getString(parsed.value, "jti")) |s| 202 + try allocator.dupe(u8, s) 203 + else 204 + null; 205 + errdefer if (jti) |s| allocator.free(s); 206 + 207 + const lxm: ?[]const u8 = if (json.getString(parsed.value, "lxm")) |s| 208 + try allocator.dupe(u8, s) 209 + else 210 + null; 211 + 193 212 return .{ 194 213 .iss = iss, 195 214 .aud = aud, 196 215 .exp = exp, 197 216 .iat = json.getInt(parsed.value, "iat"), 198 - .jti = json.getString(parsed.value, "jti"), 199 - .lxm = json.getString(parsed.value, "lxm"), 217 + .jti = jti, 218 + .lxm = lxm, 200 219 }; 201 220 } 202 221 ··· 236 255 // a minimal valid JWT structure (signature won't verify, just testing parsing) 237 256 // header: {"alg":"ES256K","typ":"JWT"} 238 257 // payload: {"iss":"did:plc:test","aud":"did:plc:service","exp":9999999999} 239 - const token = "eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJkaWQ6cGxjOnRlc3QiLCJhdWQiOiJkaWQ6cGxjOnNlcnZpY2UiLCJleHAiOjk5OTk5OTk5OTl9.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; 258 + const token = "eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJkaWQ6cGxjOnRlc3QiLCJhdWQiOiJkaWQ6cGxjOnNlcnZpY2UiLCJleHAiOjk5OTk5OTk5OTl9.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; 240 259 241 260 var jwt = try Jwt.parse(std.testing.allocator, token); 242 261 defer jwt.deinit();