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.

at codex/xrpc-errors-retry 502 lines 20 kB view raw
1const std = @import("std"); 2const cbor = @import("cbor.zig"); 3const readArg = cbor.readArg; 4const Arg = cbor.Arg; 5const readText = cbor.readText; 6const readBytes = cbor.readBytes; 7const readUint = cbor.readUint; 8const readInt = cbor.readInt; 9const readBool = cbor.readBool; 10const readNull = cbor.readNull; 11const readMapHeader = cbor.readMapHeader; 12const readArrayHeader = cbor.readArrayHeader; 13const readCidLink = cbor.readCidLink; 14 15// --------------------------------------------------------------------------- 16// Inline values 0-23 (major type 0 = unsigned) 17// --------------------------------------------------------------------------- 18 19test "readArg: inline value 0" { 20 const data = [_]u8{0x00}; // major 0, additional 0 21 const arg = try readArg(&data, 0); 22 try std.testing.expectEqual(@as(u3, 0), arg.major); 23 try std.testing.expectEqual(@as(u64, 0), arg.val); 24 try std.testing.expectEqual(@as(usize, 1), arg.end); 25} 26 27test "readArg: inline value 1" { 28 const data = [_]u8{0x01}; 29 const arg = try readArg(&data, 0); 30 try std.testing.expectEqual(@as(u3, 0), arg.major); 31 try std.testing.expectEqual(@as(u64, 1), arg.val); 32 try std.testing.expectEqual(@as(usize, 1), arg.end); 33} 34 35test "readArg: inline value 23" { 36 const data = [_]u8{0x17}; // major 0, additional 23 37 const arg = try readArg(&data, 0); 38 try std.testing.expectEqual(@as(u3, 0), arg.major); 39 try std.testing.expectEqual(@as(u64, 23), arg.val); 40 try std.testing.expectEqual(@as(usize, 1), arg.end); 41} 42 43// --------------------------------------------------------------------------- 44// 1-byte value (additional info = 24) 45// --------------------------------------------------------------------------- 46 47test "readArg: 1-byte value 24" { 48 const data = [_]u8{ 0x18, 24 }; // major 0, additional 24, payload 24 49 const arg = try readArg(&data, 0); 50 try std.testing.expectEqual(@as(u3, 0), arg.major); 51 try std.testing.expectEqual(@as(u64, 24), arg.val); 52 try std.testing.expectEqual(@as(usize, 2), arg.end); 53} 54 55test "readArg: 1-byte value 255" { 56 const data = [_]u8{ 0x18, 0xff }; // major 0, additional 24, payload 255 57 const arg = try readArg(&data, 0); 58 try std.testing.expectEqual(@as(u3, 0), arg.major); 59 try std.testing.expectEqual(@as(u64, 255), arg.val); 60 try std.testing.expectEqual(@as(usize, 2), arg.end); 61} 62 63// --------------------------------------------------------------------------- 64// 2-byte value (additional info = 25) 65// --------------------------------------------------------------------------- 66 67test "readArg: 2-byte value 256" { 68 const data = [_]u8{ 0x19, 0x01, 0x00 }; // major 0, additional 25, payload 256 big-endian 69 const arg = try readArg(&data, 0); 70 try std.testing.expectEqual(@as(u3, 0), arg.major); 71 try std.testing.expectEqual(@as(u64, 256), arg.val); 72 try std.testing.expectEqual(@as(usize, 3), arg.end); 73} 74 75// --------------------------------------------------------------------------- 76// 4-byte value (additional info = 26) 77// --------------------------------------------------------------------------- 78 79test "readArg: 4-byte value 65536" { 80 const data = [_]u8{ 0x1a, 0x00, 0x01, 0x00, 0x00 }; // major 0, additional 26, payload 65536 81 const arg = try readArg(&data, 0); 82 try std.testing.expectEqual(@as(u3, 0), arg.major); 83 try std.testing.expectEqual(@as(u64, 65536), arg.val); 84 try std.testing.expectEqual(@as(usize, 5), arg.end); 85} 86 87// --------------------------------------------------------------------------- 88// 8-byte value (additional info = 27) 89// --------------------------------------------------------------------------- 90 91test "readArg: 8-byte value 0x100000000" { 92 // major 0, additional 27, payload 0x00_00_00_01_00_00_00_00 93 const data = [_]u8{ 0x1b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00 }; 94 const arg = try readArg(&data, 0); 95 try std.testing.expectEqual(@as(u3, 0), arg.major); 96 try std.testing.expectEqual(@as(u64, 0x100000000), arg.val); 97 try std.testing.expectEqual(@as(usize, 9), arg.end); 98} 99 100// --------------------------------------------------------------------------- 101// Reject non-minimal encodings 102// --------------------------------------------------------------------------- 103 104test "readArg: reject non-minimal 0 encoded as 1-byte" { 105 // Value 0 encoded with additional=24 payload=0x00 (should be inline 0) 106 const data = [_]u8{ 0x18, 0x00 }; 107 try std.testing.expectError(error.NonMinimalEncoding, readArg(&data, 0)); 108} 109 110test "readArg: reject non-minimal 255 encoded as 2-byte" { 111 // Value 255 encoded with additional=25 payload=0x00ff (should be 1-byte) 112 const data = [_]u8{ 0x19, 0x00, 0xff }; 113 try std.testing.expectError(error.NonMinimalEncoding, readArg(&data, 0)); 114} 115 116// --------------------------------------------------------------------------- 117// Reject truncated data 118// --------------------------------------------------------------------------- 119 120test "readArg: reject truncated 2-byte" { 121 // additional=25 needs 2 payload bytes, but only 1 provided 122 const data = [_]u8{ 0x19, 0x01 }; 123 try std.testing.expectError(error.UnexpectedEof, readArg(&data, 0)); 124} 125 126// --------------------------------------------------------------------------- 127// Reject reserved additional info (28-30) 128// --------------------------------------------------------------------------- 129 130test "readArg: reject reserved additional info 28" { 131 const data = [_]u8{0x1c}; // major 0, additional 28 132 try std.testing.expectError(error.ReservedAdditionalInfo, readArg(&data, 0)); 133} 134 135// --------------------------------------------------------------------------- 136// Reject indefinite length (additional info = 31) 137// --------------------------------------------------------------------------- 138 139test "readArg: reject indefinite length 31" { 140 const data = [_]u8{0x5f}; // major 2 (byte string), additional 31 141 try std.testing.expectError(error.IndefiniteLength, readArg(&data, 0)); 142} 143 144// --------------------------------------------------------------------------- 145// Non-zero start position 146// --------------------------------------------------------------------------- 147 148test "readArg: non-zero start position" { 149 // prefix byte 0xAA, then a valid CBOR unsigned 24 at position 1 150 const data = [_]u8{ 0xaa, 0x18, 24 }; 151 const arg = try readArg(&data, 1); 152 try std.testing.expectEqual(@as(u3, 0), arg.major); 153 try std.testing.expectEqual(@as(u64, 24), arg.val); 154 try std.testing.expectEqual(@as(usize, 3), arg.end); 155} 156 157test "readArg: non-zero start position with different major type" { 158 // At position 2: 0x63 = major 3 (text string), additional 3 (inline length 3) 159 const data = [_]u8{ 0x00, 0x00, 0x63, 0x66, 0x6f, 0x6f }; 160 const arg = try readArg(&data, 2); 161 try std.testing.expectEqual(@as(u3, 3), arg.major); 162 try std.testing.expectEqual(@as(u64, 3), arg.val); 163 try std.testing.expectEqual(@as(usize, 3), arg.end); 164} 165 166// --------------------------------------------------------------------------- 167// EOF at start position 168// --------------------------------------------------------------------------- 169 170test "readArg: empty data returns UnexpectedEof" { 171 const data = [_]u8{}; 172 try std.testing.expectError(error.UnexpectedEof, readArg(&data, 0)); 173} 174 175test "readArg: pos beyond data returns UnexpectedEof" { 176 const data = [_]u8{0x00}; 177 try std.testing.expectError(error.UnexpectedEof, readArg(&data, 1)); 178} 179 180// =========================================================================== 181// readText 182// =========================================================================== 183 184test "readText: short string 'hello'" { 185 // 0x65 = major 3 (text), length 5 186 const data = [_]u8{ 0x65, 'h', 'e', 'l', 'l', 'o' }; 187 const result = try readText(&data, 0); 188 try std.testing.expectEqualStrings("hello", result.val); 189 try std.testing.expectEqual(@as(usize, 6), result.end); 190} 191 192test "readText: empty string" { 193 const data = [_]u8{0x60}; // major 3, length 0 194 const result = try readText(&data, 0); 195 try std.testing.expectEqual(@as(usize, 0), result.val.len); 196 try std.testing.expectEqual(@as(usize, 1), result.end); 197} 198 199test "readText: reject non-text major" { 200 const data = [_]u8{ 0x45, 'h', 'e', 'l', 'l', 'o' }; // major 2 (bytes), length 5 201 try std.testing.expectError(error.WrongType, readText(&data, 0)); 202} 203 204test "readText: reject invalid UTF-8" { 205 // 0x62 = major 3, length 2; 0xff 0xfe is invalid UTF-8 206 const data = [_]u8{ 0x62, 0xff, 0xfe }; 207 try std.testing.expectError(error.InvalidUtf8, readText(&data, 0)); 208} 209 210// =========================================================================== 211// readBytes 212// =========================================================================== 213 214test "readBytes: 3-byte string" { 215 const data = [_]u8{ 0x43, 0x01, 0x02, 0x03 }; // major 2, length 3 216 const result = try readBytes(&data, 0); 217 try std.testing.expectEqual(@as(usize, 3), result.val.len); 218 try std.testing.expectEqual(@as(u8, 0x01), result.val[0]); 219 try std.testing.expectEqual(@as(u8, 0x02), result.val[1]); 220 try std.testing.expectEqual(@as(u8, 0x03), result.val[2]); 221 try std.testing.expectEqual(@as(usize, 4), result.end); 222} 223 224test "readBytes: empty bytes" { 225 const data = [_]u8{0x40}; // major 2, length 0 226 const result = try readBytes(&data, 0); 227 try std.testing.expectEqual(@as(usize, 0), result.val.len); 228 try std.testing.expectEqual(@as(usize, 1), result.end); 229} 230 231// =========================================================================== 232// readUint 233// =========================================================================== 234 235test "readUint: value 1000" { 236 // 0x19 = major 0, additional 25 (2-byte), 0x03e8 = 1000 237 const data = [_]u8{ 0x19, 0x03, 0xe8 }; 238 const result = try readUint(&data, 0); 239 try std.testing.expectEqual(@as(u64, 1000), result.val); 240 try std.testing.expectEqual(@as(usize, 3), result.end); 241} 242 243test "readUint: reject negative" { 244 const data = [_]u8{0x20}; // major 1, value 0 => -1 245 try std.testing.expectError(error.WrongType, readUint(&data, 0)); 246} 247 248// =========================================================================== 249// readInt 250// =========================================================================== 251 252test "readInt: positive 42" { 253 // 0x18 = major 0, additional 24 (1-byte), 42 254 const data = [_]u8{ 0x18, 42 }; 255 const result = try readInt(&data, 0); 256 try std.testing.expectEqual(@as(i64, 42), result.val); 257 try std.testing.expectEqual(@as(usize, 2), result.end); 258} 259 260test "readInt: negative -10" { 261 // major 1, value 9 => -1 - 9 = -10 262 // 0x29 = 0b001_01001 = major 1, additional 9 263 const data = [_]u8{0x29}; 264 const result = try readInt(&data, 0); 265 try std.testing.expectEqual(@as(i64, -10), result.val); 266 try std.testing.expectEqual(@as(usize, 1), result.end); 267} 268 269test "readInt: reject non-integer" { 270 const data = [_]u8{0x60}; // major 3 (text), length 0 271 try std.testing.expectError(error.WrongType, readInt(&data, 0)); 272} 273 274// =========================================================================== 275// readBool 276// =========================================================================== 277 278test "readBool: true" { 279 const data = [_]u8{0xf5}; 280 const result = try readBool(&data, 0); 281 try std.testing.expectEqual(true, result.val); 282 try std.testing.expectEqual(@as(usize, 1), result.end); 283} 284 285test "readBool: false" { 286 const data = [_]u8{0xf4}; 287 const result = try readBool(&data, 0); 288 try std.testing.expectEqual(false, result.val); 289 try std.testing.expectEqual(@as(usize, 1), result.end); 290} 291 292test "readBool: reject non-bool" { 293 const data = [_]u8{0xf6}; // null 294 try std.testing.expectError(error.WrongType, readBool(&data, 0)); 295} 296 297// =========================================================================== 298// readNull 299// =========================================================================== 300 301test "readNull: null" { 302 const data = [_]u8{0xf6}; 303 const result = try readNull(&data, 0); 304 try std.testing.expectEqual(@as(usize, 1), result); 305} 306 307test "readNull: reject non-null" { 308 const data = [_]u8{0xf5}; // true 309 try std.testing.expectError(error.WrongType, readNull(&data, 0)); 310} 311 312// =========================================================================== 313// readMapHeader 314// =========================================================================== 315 316test "readMapHeader: count 2" { 317 const data = [_]u8{0xa2}; // major 5, length 2 318 const result = try readMapHeader(&data, 0); 319 try std.testing.expectEqual(@as(u64, 2), result.val); 320 try std.testing.expectEqual(@as(usize, 1), result.end); 321} 322 323test "readMapHeader: reject non-map" { 324 const data = [_]u8{0x82}; // major 4 (array), length 2 325 try std.testing.expectError(error.WrongType, readMapHeader(&data, 0)); 326} 327 328// =========================================================================== 329// readArrayHeader 330// =========================================================================== 331 332test "readArrayHeader: count 3" { 333 const data = [_]u8{0x83}; // major 4, length 3 334 const result = try readArrayHeader(&data, 0); 335 try std.testing.expectEqual(@as(u64, 3), result.val); 336 try std.testing.expectEqual(@as(usize, 1), result.end); 337} 338 339test "readArrayHeader: reject non-array" { 340 const data = [_]u8{0xa3}; // major 5 (map), length 3 341 try std.testing.expectError(error.WrongType, readArrayHeader(&data, 0)); 342} 343 344// =========================================================================== 345// readCidLink 346// =========================================================================== 347 348test "readCidLink: valid CID (tag(42) + bytes with 0x00 prefix + 36-byte CIDv1)" { 349 // tag(42): 0xd8 0x2a 350 // bytes(37): 0x58 0x25 (37 = 1 prefix + 36 CID) 351 // 0x00 prefix 352 // 36-byte CID: 0x01 0x71 0x12 0x20 ++ [0xaa] ** 32 353 const cid_raw = [_]u8{ 0x01, 0x71, 0x12, 0x20 } ++ [_]u8{0xaa} ** 32; 354 const data = [_]u8{ 0xd8, 0x2a, 0x58, 0x25, 0x00 } ++ cid_raw; 355 const result = try readCidLink(&data, 0); 356 try std.testing.expectEqual(@as(usize, 36), result.val.len); 357 try std.testing.expectEqual(@as(u8, 0x01), result.val[0]); 358 try std.testing.expectEqual(@as(u8, 0x71), result.val[1]); 359 try std.testing.expectEqual(@as(u8, 0x12), result.val[2]); 360 try std.testing.expectEqual(@as(u8, 0x20), result.val[3]); 361 try std.testing.expectEqual(@as(u8, 0xaa), result.val[4]); 362 try std.testing.expectEqual(@as(usize, 4 + 37), result.end); // 4 header bytes (2 tag + 2 bytes hdr) + 37 payload bytes 363} 364 365test "readCidLink: reject non-tag" { 366 const data = [_]u8{ 0x43, 0x01, 0x02, 0x03 }; // major 2 (bytes), not a tag 367 try std.testing.expectError(error.WrongType, readCidLink(&data, 0)); 368} 369 370// =========================================================================== 371// skipValue 372// =========================================================================== 373 374const skipValue = cbor.skipValue; 375const peekType = cbor.peekType; 376const peekTypeAt = cbor.peekTypeAt; 377const encodeAlloc = cbor.encodeAlloc; 378const Value = cbor.Value; 379 380test "skipValue: unsigned integer (1 byte)" { 381 // 0x05 = major 0, value 5 382 const data = [_]u8{0x05}; 383 const end = try skipValue(&data, 0); 384 try std.testing.expectEqual(@as(usize, 1), end); 385} 386 387test "skipValue: text string (header + payload)" { 388 // 0x65 = major 3, length 5 + "hello" 389 const data = [_]u8{ 0x65, 'h', 'e', 'l', 'l', 'o' }; 390 const end = try skipValue(&data, 0); 391 try std.testing.expectEqual(@as(usize, 6), end); 392} 393 394test "skipValue: nested map {\"a\": [1, 2]}" { 395 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 396 defer arena.deinit(); 397 const alloc = arena.allocator(); 398 399 // Build {"a": [1, 2]} using the encoder 400 const val = Value{ .map = &.{ 401 .{ .key = "a", .value = .{ .array = &.{ 402 .{ .unsigned = 1 }, 403 .{ .unsigned = 2 }, 404 } } }, 405 } }; 406 const encoded = try encodeAlloc(alloc, val); 407 const end = try skipValue(encoded, 0); 408 try std.testing.expectEqual(encoded.len, end); 409} 410 411test "skipValue: CID link (tag 42 + byte string)" { 412 // tag(42): 0xd8 0x2a 413 // bytes(37): 0x58 0x25 (37 = 1 prefix + 36 CID) 414 // 0x00 prefix + 36-byte CID 415 const cid_raw = [_]u8{ 0x01, 0x71, 0x12, 0x20 } ++ [_]u8{0xaa} ** 32; 416 const data = [_]u8{ 0xd8, 0x2a, 0x58, 0x25, 0x00 } ++ cid_raw; 417 const end = try skipValue(&data, 0); 418 try std.testing.expectEqual(data.len, end); 419} 420 421test "skipValue: first of two concatenated values" { 422 // Two values: unsigned 5 (0x05) followed by unsigned 10 (0x0a) 423 const data = [_]u8{ 0x05, 0x0a }; 424 const end = try skipValue(&data, 0); 425 try std.testing.expectEqual(@as(usize, 1), end); 426 // The second value starts at position 1 427 try std.testing.expectEqual(@as(u8, 0x0a), data[end]); 428} 429 430test "skipValue: complex record (encoded map)" { 431 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 432 defer arena.deinit(); 433 const alloc = arena.allocator(); 434 435 // Encode a realistic map with multiple types 436 const val = Value{ .map = &.{ 437 .{ .key = "$type", .value = .{ .text = "app.bsky.feed.post" } }, 438 .{ .key = "createdAt", .value = .{ .text = "2024-01-01T00:00:00Z" } }, 439 .{ .key = "text", .value = .{ .text = "hello world" } }, 440 } }; 441 const encoded = try encodeAlloc(alloc, val); 442 const end = try skipValue(encoded, 0); 443 try std.testing.expectEqual(encoded.len, end); 444} 445 446// =========================================================================== 447// peekType 448// =========================================================================== 449 450test "peekType: find $type when present" { 451 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 452 defer arena.deinit(); 453 const alloc = arena.allocator(); 454 455 const val = Value{ .map = &.{ 456 .{ .key = "$type", .value = .{ .text = "app.bsky.feed.post" } }, 457 .{ .key = "text", .value = .{ .text = "hello" } }, 458 } }; 459 const encoded = try encodeAlloc(alloc, val); 460 const result = try peekType(encoded); 461 try std.testing.expect(result != null); 462 try std.testing.expectEqualStrings("app.bsky.feed.post", result.?); 463} 464 465test "peekType: find $type when not first key (DAG-CBOR sort order)" { 466 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 467 defer arena.deinit(); 468 const alloc = arena.allocator(); 469 470 // DAG-CBOR sorts by length then lex. Keys "ab" (2 bytes) sorts before 471 // "$type" (5 bytes), so "$type" won't be first. 472 const val = Value{ .map = &.{ 473 .{ .key = "ab", .value = .{ .unsigned = 42 } }, 474 .{ .key = "$type", .value = .{ .text = "app.bsky.graph.follow" } }, 475 .{ .key = "zzzzzz", .value = .{ .boolean = true } }, 476 } }; 477 const encoded = try encodeAlloc(alloc, val); 478 const result = try peekType(encoded); 479 try std.testing.expect(result != null); 480 try std.testing.expectEqualStrings("app.bsky.graph.follow", result.?); 481} 482 483test "peekType: return null when no $type field" { 484 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 485 defer arena.deinit(); 486 const alloc = arena.allocator(); 487 488 const val = Value{ .map = &.{ 489 .{ .key = "text", .value = .{ .text = "hello" } }, 490 .{ .key = "count", .value = .{ .unsigned = 5 } }, 491 } }; 492 const encoded = try encodeAlloc(alloc, val); 493 const result = try peekType(encoded); 494 try std.testing.expect(result == null); 495} 496 497test "peekType: return null for non-map input" { 498 // An unsigned integer, not a map 499 const data = [_]u8{0x05}; 500 const result = try peekType(&data); 501 try std.testing.expect(result == null); 502}