pkvdb.zig 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. const std = @import("std");
  2. pub const superblock_size: u64 = 4096;
  3. pub const data_offset: u64 = 8192;
  4. pub const extent_header_size: usize = 64;
  5. pub const transaction_header_size: usize = 56;
  6. pub const operation_header_size: usize = 16;
  7. pub const group_header_size: usize = 24;
  8. pub const checkpoint_header_size: usize = 56;
  9. pub const checkpoint_entry_size: usize = 48;
  10. pub const manifest_size: usize = 104;
  11. pub const max_key_size: u32 = 1024 * 1024;
  12. pub const max_value_size: u32 = 64 * 1024 * 1024;
  13. pub const max_transaction_size: u32 = 64 * 1024 * 1024;
  14. pub const max_operations: u32 = 65535;
  15. pub const ExtentType = enum(u16) {
  16. journal = 1,
  17. checkpoint_entries = 2,
  18. checkpoint_ordered_index = 3,
  19. manifest = 4,
  20. store_metadata = 5,
  21. _,
  22. };
  23. pub const Opcode = enum(u8) {
  24. put = 1,
  25. delete = 2,
  26. };
  27. pub const Superblock = struct {
  28. flags: u32 = 0,
  29. generation: u64,
  30. uuid: [16]u8,
  31. manifest_offset: u64,
  32. checkpoint_lsn: u64,
  33. known_lsn: u64,
  34. known_file_length: u64,
  35. created_ns: i64,
  36. updated_ns: i64,
  37. };
  38. pub const ExtentHeader = struct {
  39. extent_type: ExtentType,
  40. version: u16 = 1,
  41. flags: u32 = 0,
  42. payload_length: u64,
  43. first_lsn: u64,
  44. last_lsn: u64,
  45. previous_offset: u64 = 0,
  46. payload_crc: u32,
  47. };
  48. pub const TransactionHeader = struct {
  49. frame_type: u8 = 1,
  50. flags: u8 = 0,
  51. total_length: u32,
  52. lsn: u64,
  53. transaction_id: u64,
  54. timestamp_ns: i64,
  55. operation_count: u32,
  56. metadata_length: u32,
  57. payload_crc: u32,
  58. };
  59. pub const OperationHeader = struct {
  60. opcode: Opcode,
  61. flags: u8 = 0,
  62. key_length: u32,
  63. value_length: u32,
  64. extension_length: u32 = 0,
  65. };
  66. pub const CheckpointHeader = struct {
  67. flags: u32 = 0,
  68. lsn: u64,
  69. timestamp_ns: i64,
  70. entry_count: u64,
  71. source_start: u64,
  72. source_end: u64,
  73. };
  74. pub const CheckpointEntry = struct {
  75. hash: u64,
  76. lsn: u64,
  77. key_offset: u64,
  78. value_offset: u64,
  79. key_len: u32,
  80. value_len: u32,
  81. flags: u16 = 0,
  82. };
  83. pub const Manifest = struct {
  84. uuid: [16]u8,
  85. generation: u64,
  86. checkpoint_lsn: u64,
  87. entries_offset: u64,
  88. ordered_offset: u64,
  89. replay_offset: u64,
  90. known_tail: u64,
  91. known_lsn: u64,
  92. history_start_lsn: u64,
  93. flags: u32 = 1,
  94. };
  95. pub fn align8(value: u64) !u64 {
  96. const added = try std.math.add(u64, value, 7);
  97. return added & ~@as(u64, 7);
  98. }
  99. const crc32c_tables = blk: {
  100. @setEvalBranchQuota(10000);
  101. var tables: [8][256]u32 = undefined;
  102. for (&tables[0], 0..) |*entry, index| {
  103. var value: u32 = index;
  104. for (0..8) |_| value = (value >> 1) ^ (@as(u32, 0x82f63b78) & (0 -% (value & 1)));
  105. entry.* = value;
  106. }
  107. for (1..8) |table_index| for (0..256) |index| {
  108. const previous = tables[table_index - 1][index];
  109. tables[table_index][index] = tables[0][@as(u8, @truncate(previous))] ^ (previous >> 8);
  110. };
  111. break :blk tables;
  112. };
  113. pub fn crc32cUpdate(state: u32, bytes: []const u8) u32 {
  114. var crc = state;
  115. var position: usize = 0;
  116. while (bytes.len - position >= 8) : (position += 8) {
  117. crc ^= std.mem.readInt(u32, bytes[position..][0..4], .little);
  118. crc = crc32c_tables[7][@as(u8, @truncate(crc))] ^
  119. crc32c_tables[6][@as(u8, @truncate(crc >> 8))] ^
  120. crc32c_tables[5][@as(u8, @truncate(crc >> 16))] ^
  121. crc32c_tables[4][@as(u8, @truncate(crc >> 24))] ^
  122. crc32c_tables[3][bytes[position + 4]] ^
  123. crc32c_tables[2][bytes[position + 5]] ^
  124. crc32c_tables[1][bytes[position + 6]] ^
  125. crc32c_tables[0][bytes[position + 7]];
  126. }
  127. for (bytes[position..]) |byte| crc = crc32c_tables[0][@as(u8, @truncate(crc)) ^ byte] ^ (crc >> 8);
  128. return crc;
  129. }
  130. pub fn crc32c(bytes: []const u8) u32 {
  131. return ~crc32cUpdate(0xffffffff, bytes);
  132. }
  133. fn put(comptime T: type, dst: []u8, offset: usize, value: T) void {
  134. std.mem.writeInt(T, dst[offset..][0..@sizeOf(T)], value, .little);
  135. }
  136. fn get(comptime T: type, src: []const u8, offset: usize) T {
  137. return std.mem.readInt(T, src[offset..][0..@sizeOf(T)], .little);
  138. }
  139. fn zeroCrc(bytes: []u8, offset: usize) u32 {
  140. const old = get(u32, bytes, offset);
  141. put(u32, bytes, offset, 0);
  142. const crc = crc32c(bytes);
  143. put(u32, bytes, offset, old);
  144. return crc;
  145. }
  146. pub fn encodeSuperblock(sb: Superblock, out: *[4096]u8) void {
  147. @memset(out, 0);
  148. @memcpy(out[0..8], "PKVDB\x00\x00\x00");
  149. put(u16, out, 8, 1);
  150. put(u16, out, 10, 0);
  151. put(u32, out, 12, 112);
  152. put(u32, out, 16, sb.flags);
  153. put(u64, out, 24, sb.generation);
  154. @memcpy(out[32..48], &sb.uuid);
  155. put(u64, out, 48, sb.manifest_offset);
  156. put(u64, out, 56, sb.checkpoint_lsn);
  157. put(u64, out, 64, sb.known_lsn);
  158. put(u64, out, 72, sb.known_file_length);
  159. put(i64, out, 80, sb.created_ns);
  160. put(i64, out, 88, sb.updated_ns);
  161. put(u32, out, 96, 0);
  162. put(u32, out, 96, crc32c(out[0..112]));
  163. }
  164. pub fn decodeSuperblock(bytes: []const u8, file_length: u64) !Superblock {
  165. if (bytes.len < superblock_size) return error.Truncated;
  166. if (!std.mem.eql(u8, bytes[0..8], "PKVDB\x00\x00\x00")) return error.InvalidMagic;
  167. if (get(u16, bytes, 8) != 1) return error.IncompatibleVersion;
  168. if (get(u32, bytes, 12) != 112) return error.InvalidLength;
  169. var header: [112]u8 = undefined;
  170. @memcpy(&header, bytes[0..112]);
  171. const stored_crc = get(u32, &header, 96);
  172. if (zeroCrc(&header, 96) != stored_crc) return error.ChecksumMismatch;
  173. const known_length = get(u64, bytes, 72);
  174. const manifest_offset = get(u64, bytes, 48);
  175. if (known_length < data_offset or known_length > file_length) return error.UnusableRoot;
  176. if (manifest_offset != 0 and (manifest_offset < data_offset or manifest_offset >= known_length or manifest_offset % 8 != 0)) return error.UnusableRoot;
  177. var uuid: [16]u8 = undefined;
  178. @memcpy(&uuid, bytes[32..48]);
  179. return .{
  180. .flags = get(u32, bytes, 16),
  181. .generation = get(u64, bytes, 24),
  182. .uuid = uuid,
  183. .manifest_offset = manifest_offset,
  184. .checkpoint_lsn = get(u64, bytes, 56),
  185. .known_lsn = get(u64, bytes, 64),
  186. .known_file_length = known_length,
  187. .created_ns = get(i64, bytes, 80),
  188. .updated_ns = get(i64, bytes, 88),
  189. };
  190. }
  191. pub fn encodeExtentHeader(header: ExtentHeader, out: *[64]u8) void {
  192. @memset(out, 0);
  193. @memcpy(out[0..4], "PKEX");
  194. put(u16, out, 4, @intFromEnum(header.extent_type));
  195. put(u16, out, 6, header.version);
  196. put(u32, out, 8, header.flags);
  197. put(u32, out, 12, extent_header_size);
  198. put(u64, out, 16, header.payload_length);
  199. put(u64, out, 24, header.first_lsn);
  200. put(u64, out, 32, header.last_lsn);
  201. put(u64, out, 40, header.previous_offset);
  202. put(u32, out, 48, header.payload_crc);
  203. put(u32, out, 52, 0);
  204. put(u32, out, 52, crc32c(out));
  205. }
  206. pub fn decodeExtentHeader(bytes: []const u8) !ExtentHeader {
  207. if (bytes.len < extent_header_size) return error.Truncated;
  208. if (!std.mem.eql(u8, bytes[0..4], "PKEX")) return error.InvalidMagic;
  209. if (get(u32, bytes, 12) != extent_header_size) return error.InvalidLength;
  210. var copy: [64]u8 = undefined;
  211. @memcpy(&copy, bytes[0..64]);
  212. const stored_crc = get(u32, &copy, 52);
  213. if (zeroCrc(&copy, 52) != stored_crc) return error.ChecksumMismatch;
  214. return .{
  215. .extent_type = @enumFromInt(get(u16, bytes, 4)),
  216. .version = get(u16, bytes, 6),
  217. .flags = get(u32, bytes, 8),
  218. .payload_length = get(u64, bytes, 16),
  219. .first_lsn = get(u64, bytes, 24),
  220. .last_lsn = get(u64, bytes, 32),
  221. .previous_offset = get(u64, bytes, 40),
  222. .payload_crc = get(u32, bytes, 48),
  223. };
  224. }
  225. pub fn encodeTransactionHeader(header: TransactionHeader, out: *[56]u8) void {
  226. @memset(out, 0);
  227. @memcpy(out[0..4], "PKTX");
  228. put(u16, out, 4, 1);
  229. out[6] = header.frame_type;
  230. out[7] = header.flags;
  231. put(u32, out, 8, header.total_length);
  232. put(u16, out, 12, transaction_header_size);
  233. put(u64, out, 16, header.lsn);
  234. put(u64, out, 24, header.transaction_id);
  235. put(i64, out, 32, header.timestamp_ns);
  236. put(u32, out, 40, header.operation_count);
  237. put(u32, out, 44, header.metadata_length);
  238. put(u32, out, 48, header.payload_crc);
  239. put(u32, out, 52, 0);
  240. put(u32, out, 52, crc32c(out));
  241. }
  242. pub fn decodeTransactionHeader(bytes: []const u8) !TransactionHeader {
  243. if (bytes.len < transaction_header_size) return error.Truncated;
  244. if (!std.mem.eql(u8, bytes[0..4], "PKTX")) return error.InvalidMagic;
  245. if (get(u16, bytes, 4) != 1) return error.IncompatibleVersion;
  246. if (get(u16, bytes, 12) != transaction_header_size) return error.InvalidLength;
  247. const total = get(u32, bytes, 8);
  248. if (total < transaction_header_size or total > max_transaction_size) return error.InvalidLength;
  249. const count = get(u32, bytes, 40);
  250. if (count > max_operations) return error.InvalidLength;
  251. var copy: [56]u8 = undefined;
  252. @memcpy(&copy, bytes[0..56]);
  253. const stored_crc = get(u32, &copy, 52);
  254. if (zeroCrc(&copy, 52) != stored_crc) return error.ChecksumMismatch;
  255. return .{
  256. .frame_type = bytes[6],
  257. .flags = bytes[7],
  258. .total_length = total,
  259. .lsn = get(u64, bytes, 16),
  260. .transaction_id = get(u64, bytes, 24),
  261. .timestamp_ns = get(i64, bytes, 32),
  262. .operation_count = count,
  263. .metadata_length = get(u32, bytes, 44),
  264. .payload_crc = get(u32, bytes, 48),
  265. };
  266. }
  267. pub fn encodeOperationHeader(header: OperationHeader, out: *[16]u8) void {
  268. @memset(out, 0);
  269. out[0] = @intFromEnum(header.opcode);
  270. out[1] = header.flags;
  271. put(u32, out, 4, header.key_length);
  272. put(u32, out, 8, header.value_length);
  273. put(u32, out, 12, header.extension_length);
  274. }
  275. pub fn decodeOperationHeader(bytes: []const u8) !OperationHeader {
  276. if (bytes.len < operation_header_size) return error.Truncated;
  277. const opcode: Opcode = std.meta.intToEnum(Opcode, bytes[0]) catch return error.InvalidOpcode;
  278. const key_length = get(u32, bytes, 4);
  279. const value_length = get(u32, bytes, 8);
  280. if (key_length > max_key_size or value_length > max_value_size) return error.InvalidLength;
  281. if (opcode == .delete and value_length != 0) return error.InvalidLength;
  282. return .{
  283. .opcode = opcode,
  284. .flags = bytes[1],
  285. .key_length = key_length,
  286. .value_length = value_length,
  287. .extension_length = get(u32, bytes, 12),
  288. };
  289. }
  290. pub fn encodeCheckpointHeader(header: CheckpointHeader, out: *[56]u8) void {
  291. @memset(out, 0);
  292. put(u16, out, 0, 1);
  293. put(u16, out, 2, 1);
  294. put(u32, out, 4, header.flags);
  295. put(u64, out, 8, header.lsn);
  296. put(i64, out, 16, header.timestamp_ns);
  297. put(u64, out, 24, header.entry_count);
  298. put(u32, out, 32, checkpoint_entry_size);
  299. put(u32, out, 36, 1);
  300. put(u64, out, 40, header.source_start);
  301. put(u64, out, 48, header.source_end);
  302. }
  303. pub fn decodeCheckpointHeader(bytes: []const u8) !CheckpointHeader {
  304. if (bytes.len < checkpoint_header_size) return error.Truncated;
  305. return decodeCheckpointHeaderOnly(bytes[0..checkpoint_header_size], bytes.len);
  306. }
  307. pub fn decodeCheckpointHeaderOnly(bytes: []const u8, payload_length: u64) !CheckpointHeader {
  308. if (bytes.len < checkpoint_header_size) return error.Truncated;
  309. if (get(u16, bytes, 0) != 1 or get(u16, bytes, 2) != 1) return error.IncompatibleVersion;
  310. if (get(u32, bytes, 32) != checkpoint_entry_size or get(u32, bytes, 36) != 1) return error.IncompatibleVersion;
  311. const count = get(u64, bytes, 24);
  312. const entries_bytes = try std.math.mul(u64, count, checkpoint_entry_size);
  313. const needed = try std.math.add(u64, checkpoint_header_size, entries_bytes);
  314. if (needed != payload_length) return error.InvalidLength;
  315. return .{
  316. .flags = get(u32, bytes, 4),
  317. .lsn = get(u64, bytes, 8),
  318. .timestamp_ns = get(i64, bytes, 16),
  319. .entry_count = count,
  320. .source_start = get(u64, bytes, 40),
  321. .source_end = get(u64, bytes, 48),
  322. };
  323. }
  324. pub fn encodeCheckpointEntry(entry: CheckpointEntry, out: *[48]u8) void {
  325. @memset(out, 0);
  326. put(u64, out, 0, entry.hash);
  327. put(u64, out, 8, entry.lsn);
  328. put(u64, out, 16, entry.key_offset);
  329. put(u64, out, 24, entry.value_offset);
  330. put(u32, out, 32, entry.key_len);
  331. put(u32, out, 36, entry.value_len);
  332. put(u16, out, 40, entry.flags);
  333. put(u32, out, 44, 0);
  334. put(u32, out, 44, crc32c(out));
  335. }
  336. pub fn decodeCheckpointEntry(bytes: []const u8, file_length: u64) !CheckpointEntry {
  337. if (bytes.len < checkpoint_entry_size) return error.Truncated;
  338. var copy: [48]u8 = undefined;
  339. @memcpy(&copy, bytes[0..48]);
  340. const stored_crc = get(u32, &copy, 44);
  341. if (zeroCrc(&copy, 44) != stored_crc) return error.ChecksumMismatch;
  342. const key_offset = get(u64, bytes, 16);
  343. const value_offset = get(u64, bytes, 24);
  344. const key_len = get(u32, bytes, 32);
  345. const value_len = get(u32, bytes, 36);
  346. if (key_len > max_key_size or value_len > max_value_size) return error.InvalidLength;
  347. if (key_offset < data_offset or value_offset < data_offset) return error.InvalidOffset;
  348. if (try std.math.add(u64, key_offset, key_len) > file_length or try std.math.add(u64, value_offset, value_len) > file_length) return error.InvalidOffset;
  349. return .{
  350. .hash = get(u64, bytes, 0),
  351. .lsn = get(u64, bytes, 8),
  352. .key_offset = key_offset,
  353. .value_offset = value_offset,
  354. .key_len = key_len,
  355. .value_len = value_len,
  356. .flags = get(u16, bytes, 40),
  357. };
  358. }
  359. pub fn encodeManifest(manifest: Manifest, out: *[104]u8) void {
  360. @memset(out, 0);
  361. put(u16, out, 0, 1);
  362. put(u16, out, 2, 0);
  363. put(u32, out, 4, manifest_size);
  364. @memcpy(out[8..24], &manifest.uuid);
  365. put(u64, out, 24, manifest.generation);
  366. put(u64, out, 32, manifest.checkpoint_lsn);
  367. put(u64, out, 40, manifest.entries_offset);
  368. put(u64, out, 48, manifest.ordered_offset);
  369. put(u64, out, 56, manifest.replay_offset);
  370. put(u64, out, 64, manifest.known_tail);
  371. put(u64, out, 72, manifest.known_lsn);
  372. put(u64, out, 80, manifest.history_start_lsn);
  373. put(u32, out, 88, 1);
  374. put(u32, out, 92, manifest.flags);
  375. put(u32, out, 96, 0);
  376. put(u32, out, 96, crc32c(out));
  377. }
  378. pub fn decodeManifest(bytes: []const u8) !Manifest {
  379. if (bytes.len != manifest_size) return error.InvalidLength;
  380. if (get(u16, bytes, 0) != 1 or get(u32, bytes, 4) != manifest_size) return error.IncompatibleVersion;
  381. var copy: [104]u8 = undefined;
  382. @memcpy(&copy, bytes);
  383. const stored_crc = get(u32, &copy, 96);
  384. if (zeroCrc(&copy, 96) != stored_crc) return error.ChecksumMismatch;
  385. if (get(u32, bytes, 88) != 1) return error.IncompatibleVersion;
  386. var uuid: [16]u8 = undefined;
  387. @memcpy(&uuid, bytes[8..24]);
  388. return .{
  389. .uuid = uuid,
  390. .generation = get(u64, bytes, 24),
  391. .checkpoint_lsn = get(u64, bytes, 32),
  392. .entries_offset = get(u64, bytes, 40),
  393. .ordered_offset = get(u64, bytes, 48),
  394. .replay_offset = get(u64, bytes, 56),
  395. .known_tail = get(u64, bytes, 64),
  396. .known_lsn = get(u64, bytes, 72),
  397. .history_start_lsn = get(u64, bytes, 80),
  398. .flags = get(u32, bytes, 92),
  399. };
  400. }
  401. test "crc32c golden" {
  402. try std.testing.expectEqual(@as(u32, 0xe3069283), crc32c("123456789"));
  403. }
  404. test "superblock golden round trip" {
  405. const sb = Superblock{ .generation = 7, .uuid = [_]u8{3} ** 16, .manifest_offset = 8192, .checkpoint_lsn = 4, .known_lsn = 6, .known_file_length = 9000, .created_ns = -2, .updated_ns = 8 };
  406. var bytes: [4096]u8 = undefined;
  407. encodeSuperblock(sb, &bytes);
  408. const decoded = try decodeSuperblock(&bytes, 9000);
  409. try std.testing.expectEqual(sb.generation, decoded.generation);
  410. try std.testing.expectEqual(sb.uuid, decoded.uuid);
  411. try std.testing.expectEqual(sb.created_ns, decoded.created_ns);
  412. try std.testing.expectEqual(@as(u8, 'P'), bytes[0]);
  413. try std.testing.expectEqual(@as(u8, 1), bytes[8]);
  414. }
  415. test "codec truncation and checksums" {
  416. var extent_bytes: [64]u8 = undefined;
  417. encodeExtentHeader(.{ .extent_type = .journal, .payload_length = 9, .first_lsn = 1, .last_lsn = 1, .payload_crc = 2 }, &extent_bytes);
  418. try std.testing.expectError(error.Truncated, decodeExtentHeader(extent_bytes[0..63]));
  419. extent_bytes[20] ^= 1;
  420. try std.testing.expectError(error.ChecksumMismatch, decodeExtentHeader(&extent_bytes));
  421. }
  422. test "checkpoint length overflow" {
  423. var bytes: [56]u8 = [_]u8{0} ** 56;
  424. put(u16, &bytes, 0, 1);
  425. put(u16, &bytes, 2, 1);
  426. put(u64, &bytes, 24, std.math.maxInt(u64));
  427. put(u32, &bytes, 32, checkpoint_entry_size);
  428. put(u32, &bytes, 36, 1);
  429. try std.testing.expectError(error.Overflow, decodeCheckpointHeader(&bytes));
  430. }
  431. test "unknown extent type remains decodable" {
  432. var bytes: [64]u8 = undefined;
  433. encodeExtentHeader(.{ .extent_type = @enumFromInt(99), .version = 7, .payload_length = 0, .first_lsn = 0, .last_lsn = 0, .payload_crc = crc32c("") }, &bytes);
  434. const decoded = try decodeExtentHeader(&bytes);
  435. try std.testing.expectEqual(@as(u16, 99), @intFromEnum(decoded.extent_type));
  436. try std.testing.expectEqual(@as(u16, 7), decoded.version);
  437. }
  438. test "every truncated fixed header is rejected" {
  439. var extent: [64]u8 = undefined;
  440. encodeExtentHeader(.{ .extent_type = .journal, .payload_length = 0, .first_lsn = 1, .last_lsn = 1, .payload_crc = crc32c("") }, &extent);
  441. for (0..extent.len) |length| try std.testing.expectError(error.Truncated, decodeExtentHeader(extent[0..length]));
  442. var transaction: [56]u8 = undefined;
  443. encodeTransactionHeader(.{ .total_length = 56, .lsn = 1, .transaction_id = 2, .timestamp_ns = 3, .operation_count = 0, .metadata_length = 0, .payload_crc = crc32c("") }, &transaction);
  444. for (0..transaction.len) |length| try std.testing.expectError(error.Truncated, decodeTransactionHeader(transaction[0..length]));
  445. }
  446. test "operation validation rejects oversized and invalid delete" {
  447. var bytes: [16]u8 = undefined;
  448. encodeOperationHeader(.{ .opcode = .delete, .key_length = 1, .value_length = 1 }, &bytes);
  449. try std.testing.expectError(error.InvalidLength, decodeOperationHeader(&bytes));
  450. encodeOperationHeader(.{ .opcode = .put, .key_length = max_key_size + 1, .value_length = 0 }, &bytes);
  451. try std.testing.expectError(error.InvalidLength, decodeOperationHeader(&bytes));
  452. bytes[0] = 99;
  453. try std.testing.expectError(error.InvalidOpcode, decodeOperationHeader(&bytes));
  454. }