lib/choir/src/composition/module/serialization/test.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const fixture = @import("../../fixture/root.zig");
2 const std = @import("std");
3 const serialization = @import("root.zig");
4 const model = @import("../root.zig");
5 const source_model = @import("../../source/root.zig");
6 const abi = @import("../../root.zig").abi;
7 const artifact = @import("../../../backends/root.zig").artifact;
8 const product = @import("../../../product/root.zig");
9
10 const format = serialization.format;
11 const serialize = serialization.serialize;
12 const deserialize = serialization.deserialize;
13
14 test {
15 std.testing.refAllDecls(serialization);
16 _ = serialization.reference;
17 _ = serialization.codec;
18 }
19
20 fn testModule(allocator: std.mem.Allocator) !model.CompositionModule {
21 const records = try fixture.Records.init(allocator);
22 defer records.deinit();
23 var partitioned = try source_model.PartitionedModule.init(allocator, "hybrid.persisted");
24 defer partitioned.deinit();
25 try partitioned.addBoundary(.{
26 .id = .{ .value = 1 },
27 .name = "lhs",
28 .element_type = .f32,
29 .dimensions = &.{8},
30 .byte_size = 32,
31 .access = .read_write,
32 .ownership = .borrowed,
33 .alias = .disjoint,
34 .provenance = .{ .path = "persisted.chic", .symbol = "lhs", .line = 9, .column = 2 },
35 });
36 try partitioned.addBoundary(.{
37 .id = .{ .value = 2 },
38 .name = "rhs",
39 .element_type = .f32,
40 .dimensions = &.{8},
41 .byte_size = 32,
42 .access = .read_write,
43 .ownership = .borrowed,
44 .alias = .{ .boundary = .{ .value = 1 } },
45 .provenance = .{ .path = "persisted.chic", .symbol = "rhs", .line = 10, .column = 2 },
46 });
47 try partitioned.addBoundary(.{
48 .id = .{ .value = 3 },
49 .name = "result",
50 .element_type = .key,
51 .dimensions = &.{4},
52 .byte_size = 32,
53 .access = .write,
54 .ownership = .produced,
55 .alias = .disjoint,
56 .provenance = .{ .path = "persisted.chic", .symbol = "result", .line = 11, .column = 3 },
57 });
58 try partitioned.addPartition(.{
59 .id = .{ .value = 10 },
60 .name = "host",
61 .pipeline = .choir,
62 .outputs = &.{.{ .value = 1 }},
63 .product = .{ .producer = "composition-fixture", .source = "101", .stage = "hybrid.host.input", .variant = "default" },
64 .effect = .unknown,
65 .provenance = .{ .path = "persisted.chic", .symbol = "host", .line = 7, .column = 3 },
66 });
67 try partitioned.addPartition(.{
68 .id = .{ .value = 11 },
69 .name = "kernel",
70 .pipeline = .accy,
71 .inputs = &.{ .{ .value = 1 }, .{ .value = 2 } },
72 .outputs = &.{.{ .value = 3 }},
73 .product = .{ .producer = "composition-fixture", .source = "103", .stage = "hybrid.kernel.input", .variant = "default" },
74 .effect = .none,
75 .provenance = .{ .path = "persisted.chic", .symbol = "kernel", .line = 11, .column = 5 },
76 });
77 try partitioned.addCallSite(.{
78 .id = .{ .value = 30 },
79 .name = "host/kernel",
80 .caller = .{ .value = 10 },
81 .callee = .{ .value = 11 },
82 .inputs = &.{ .{ .value = 1 }, .{ .value = 2 } },
83 .outputs = &.{.{ .value = 3 }},
84 .provenance = .{ .path = "persisted.chic", .symbol = "kernel", .line = 12, .column = 9 },
85 });
86
87 var host_artifact = try artifact.Artifact.init(allocator, .{
88 .kind = .machine_code,
89 .producer = "choir-x64",
90 .target = .{
91 .architecture = .x86_64,
92 .triple = "x86_64-unknown-linux-gnu",
93 .vendor = "unknown",
94 .os = "linux",
95 .environment = "gnu",
96 .cpu = "x86-64-v3",
97 .features = &.{ "sse2", "+avx2" },
98 },
99 .abi = .{
100 .name = "sysv",
101 .calling_convention = "c",
102 .object_format = "elf",
103 .pointer_width_bits = 64,
104 .endianness = .little,
105 },
106 });
107 defer host_artifact.deinit();
108 try host_artifact.options.add(.{ .key = "checked", .value = .{ .flag = true } });
109 try host_artifact.options.add(.{ .key = "bias", .value = .{ .signed = -17 } });
110 try host_artifact.options.add(.{ .key = "seed", .value = .{ .unsigned = 0xfedcba9876543210 } });
111 try host_artifact.options.add(.{ .key = "mode", .value = .{ .text = "durable" } });
112 try host_artifact.options.add(.{ .key = "opaque", .value = .{ .bytes = &.{ 0, 255, 7 } } });
113 try host_artifact.linkage.addRequired(.{ .name = abi.invoke_symbol, .kind = .runtime, .binding = .external });
114 try host_artifact.linkage.addRequired(.{ .name = "weak_data", .kind = .data, .binding = .weak });
115 try host_artifact.linkage.addProvided(.{ .name = "host_entry", .kind = .function, .binding = .external });
116 try host_artifact.linkage.addProvided(.{ .name = "local_data", .kind = .data, .binding = .local });
117 try host_artifact.linkage.addRelocation(.{
118 .offset = 2,
119 .symbol = abi.invoke_symbol,
120 .kind = .call,
121 .addend = -4,
122 .width_bits = 64,
123 });
124 try host_artifact.payload.addDebugRecord(.{ .name = ".debug_line", .kind = .dwarf_section, .bytes = &.{ 1, 0, 2, 0 } });
125 try host_artifact.payload.addBuffer(.{ .name = "host_entry", .format = .machine_code, .bytes = &.{ 0x48, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xd0, 0xc3 }, .alignment = 32 });
126 try host_artifact.payload.addTextDump(.{ .name = "lowered", .contents = "func @host_entry" });
127 try host_artifact.verification.replace(host_artifact.allocator, .{ .state = .failed, .stage = "machine-verify", .message = "preserved diagnostic" });
128
129 var kernel_artifact = try artifact.Artifact.init(allocator, .{
130 .kind = .object_file,
131 .producer = "accy-cpu",
132 .target = .{ .architecture = .x86_64 },
133 });
134 defer kernel_artifact.deinit();
135 try kernel_artifact.linkage.addProvided(.{ .name = "kernel_entry", .kind = .function, .binding = .external });
136 try kernel_artifact.payload.addBuffer(.{ .name = "kernel_entry", .format = .object_file, .bytes = &.{ 0x7f, 'E', 'L', 'F' }, .alignment = 8 });
137 try kernel_artifact.verification.replace(kernel_artifact.allocator, .{ .state = .skipped });
138
139 var module = try model.CompositionModule.init(allocator, &partitioned);
140 errdefer module.deinit();
141 try module.addFragment(.{
142 .id = .{ .value = 20 },
143 .partition = .{ .value = 10 },
144 .pipeline = .choir,
145 .pipeline_input = try records.key("hybrid.host.input", "101", "201"),
146
147 .artifacts = &.{host_artifact},
148 .exports = &.{.{ .name = "host", .symbol = "host_entry", .abi_version = abi.version }},
149 .imports = &.{.{ .name = "invoke", .symbol = abi.invoke_symbol, .abi_version = abi.version, .kind = .runtime }},
150 .provenance = .{ .path = "persisted.chic", .symbol = "host_entry", .line = 20, .column = 4 },
151 });
152 try module.addFragment(.{
153 .id = .{ .value = 21 },
154 .partition = .{ .value = 11 },
155 .pipeline = .accy,
156 .pipeline_input = try records.key("hybrid.kernel.input", "103", "203"),
157
158 .artifacts = &.{kernel_artifact},
159 .exports = &.{.{ .name = "kernel", .symbol = "kernel_entry", .abi_version = abi.version }},
160 .provenance = .{ .path = "persisted.chic", .symbol = "kernel_entry", .line = 24, .column = 8 },
161 });
162 try module.addFragment(.{
163 .id = .{ .value = 22 },
164 .partition = .{ .value = 11 },
165 .pipeline = .accy,
166 .pipeline_input = try records.key("hybrid.kernel.input", "103", "203"),
167
168 .artifacts = &.{kernel_artifact},
169 .exports = &.{.{ .name = "kernel", .symbol = "kernel_entry", .abi_version = abi.version }},
170 .provenance = .{ .path = "persisted.chic", .symbol = "kernel_entry", .line = 24, .column = 8 },
171 });
172 try module.addVariant(.{
173 .id = .{ .value = 1 },
174 .choice = .{
175 .target = product.productRef("composition-fixture", "301", "hybrid.target", "default"),
176 .policy = product.productRef("composition-fixture", "302", "hybrid.policy", "default"),
177 },
178 .fragments = &.{ .{ .value = 20 }, .{ .value = 21 } },
179 .call_sites = &.{.{
180 .id = .{ .value = 30 },
181 .name = "host/kernel",
182 .caller = .{ .value = 20 },
183 .caller_export = "host",
184 .runtime_import = "invoke",
185 .callee = .{ .value = 21 },
186 .callee_export = "kernel",
187 .inputs = &.{ .{ .value = 1 }, .{ .value = 2 } },
188 .outputs = &.{.{ .value = 3 }},
189 .abi_version = abi.version,
190 .provenance = .{ .path = "persisted.chic", .symbol = "kernel", .line = 12, .column = 9 },
191 }},
192 });
193 try module.addVariant(.{
194 .id = .{ .value = 2 },
195 .choice = .{
196 .target = product.productRef("composition-fixture", "303", "hybrid.target", "default"),
197 .policy = product.productRef("composition-fixture", "302", "hybrid.policy", "default"),
198 },
199 .fragments = &.{ .{ .value = 20 }, .{ .value = 22 } },
200 .call_sites = &.{.{
201 .id = .{ .value = 30 },
202 .name = "host/kernel",
203 .caller = .{ .value = 20 },
204 .caller_export = "host",
205 .runtime_import = "invoke",
206 .callee = .{ .value = 22 },
207 .callee_export = "kernel",
208 .inputs = &.{ .{ .value = 1 }, .{ .value = 2 } },
209 .outputs = &.{.{ .value = 3 }},
210 .abi_version = abi.version,
211 .provenance = .{ .path = "persisted.chic", .symbol = "kernel", .line = 12, .column = 9 },
212 }},
213 });
214 try module.verify();
215 return module;
216 }
217
218 fn expectProductEqual(expected: product.ProductKey, actual: product.ProductKey) !void {
219 try std.testing.expect(expected.ref.eql(actual.ref));
220 try std.testing.expectEqualSlices(u8, expected.record.bytes(), actual.record.bytes());
221 }
222
223 fn expectProvenanceEqual(expected: source_model.Provenance, actual: source_model.Provenance) !void {
224 try std.testing.expectEqualStrings(expected.path, actual.path);
225 try std.testing.expectEqualStrings(expected.symbol, actual.symbol);
226 try std.testing.expectEqual(expected.line, actual.line);
227 try std.testing.expectEqual(expected.column, actual.column);
228 }
229
230 fn expectBoundaryIdsEqual(expected: []const source_model.BoundaryId, actual: []const source_model.BoundaryId) !void {
231 try std.testing.expectEqual(expected.len, actual.len);
232 for (expected, actual) |expected_id, actual_id| try std.testing.expectEqual(expected_id.value, actual_id.value);
233 }
234
235 fn expectModuleEqual(expected: *const model.CompositionModule, actual: *const model.CompositionModule) !void {
236 try std.testing.expectEqualStrings(expected.source_module.name, actual.source_module.name);
237 try std.testing.expectEqual(expected.source_module.boundaries.items.len, actual.source_module.boundaries.items.len);
238 for (expected.source_module.boundaries.items, actual.source_module.boundaries.items) |expected_boundary, actual_boundary| {
239 try std.testing.expectEqual(expected_boundary.id.value, actual_boundary.id.value);
240 try std.testing.expectEqualStrings(expected_boundary.name, actual_boundary.name);
241 try std.testing.expectEqual(expected_boundary.element_type, actual_boundary.element_type);
242 try std.testing.expectEqualSlices(u64, expected_boundary.dimensions, actual_boundary.dimensions);
243 try std.testing.expectEqual(expected_boundary.byte_size, actual_boundary.byte_size);
244 try std.testing.expectEqual(expected_boundary.access, actual_boundary.access);
245 try std.testing.expectEqual(expected_boundary.ownership, actual_boundary.ownership);
246 try std.testing.expect(expected_boundary.alias.eql(actual_boundary.alias));
247 try expectProvenanceEqual(expected_boundary.provenance, actual_boundary.provenance);
248 }
249
250 try std.testing.expectEqual(expected.source_module.partitions.items.len, actual.source_module.partitions.items.len);
251 for (expected.source_module.partitions.items, actual.source_module.partitions.items) |expected_partition, actual_partition| {
252 try std.testing.expectEqual(expected_partition.id.value, actual_partition.id.value);
253 try std.testing.expectEqualStrings(expected_partition.name, actual_partition.name);
254 try std.testing.expectEqual(expected_partition.pipeline, actual_partition.pipeline);
255 try expectBoundaryIdsEqual(expected_partition.inputs, actual_partition.inputs);
256 try expectBoundaryIdsEqual(expected_partition.outputs, actual_partition.outputs);
257 try std.testing.expect(expected_partition.product.eql(actual_partition.product));
258 try std.testing.expectEqual(expected_partition.effect, actual_partition.effect);
259 try expectProvenanceEqual(expected_partition.provenance, actual_partition.provenance);
260 }
261
262 try std.testing.expectEqual(expected.source_module.call_sites.items.len, actual.source_module.call_sites.items.len);
263 for (expected.source_module.call_sites.items, actual.source_module.call_sites.items) |expected_call, actual_call| {
264 try std.testing.expectEqual(expected_call.id.value, actual_call.id.value);
265 try std.testing.expectEqualStrings(expected_call.name, actual_call.name);
266 try std.testing.expectEqual(expected_call.caller.value, actual_call.caller.value);
267 try std.testing.expectEqual(expected_call.callee.value, actual_call.callee.value);
268 try expectBoundaryIdsEqual(expected_call.inputs, actual_call.inputs);
269 try expectBoundaryIdsEqual(expected_call.outputs, actual_call.outputs);
270 try expectProvenanceEqual(expected_call.provenance, actual_call.provenance);
271 }
272
273 try std.testing.expectEqual(expected.fragments.items.len, actual.fragments.items.len);
274 for (expected.fragments.items, actual.fragments.items) |expected_fragment, actual_fragment| {
275 try std.testing.expectEqual(expected_fragment.id.value, actual_fragment.id.value);
276 try std.testing.expectEqual(expected_fragment.partition.value, actual_fragment.partition.value);
277 try std.testing.expectEqual(expected_fragment.pipeline, actual_fragment.pipeline);
278 try expectProductEqual(expected_fragment.pipeline_input, actual_fragment.pipeline_input);
279 try std.testing.expectEqual(expected_fragment.artifacts.len, actual_fragment.artifacts.len);
280 for (expected_fragment.artifacts, actual_fragment.artifacts) |expected_artifact, actual_artifact| {
281 try std.testing.expect(expected_artifact.eql(actual_artifact));
282 }
283 try std.testing.expectEqual(expected_fragment.exports.len, actual_fragment.exports.len);
284 for (expected_fragment.exports, actual_fragment.exports) |expected_export, actual_export| {
285 try std.testing.expectEqualStrings(expected_export.name, actual_export.name);
286 try std.testing.expectEqualStrings(expected_export.symbol, actual_export.symbol);
287 try std.testing.expectEqual(expected_export.abi_version, actual_export.abi_version);
288 }
289 try std.testing.expectEqual(expected_fragment.imports.len, actual_fragment.imports.len);
290 for (expected_fragment.imports, actual_fragment.imports) |expected_import, actual_import| {
291 try std.testing.expectEqualStrings(expected_import.name, actual_import.name);
292 try std.testing.expectEqualStrings(expected_import.symbol, actual_import.symbol);
293 try std.testing.expectEqual(expected_import.abi_version, actual_import.abi_version);
294 try std.testing.expectEqual(expected_import.kind, actual_import.kind);
295 }
296 try expectProvenanceEqual(expected_fragment.provenance, actual_fragment.provenance);
297 }
298
299 try std.testing.expectEqual(expected.variants.items.len, actual.variants.items.len);
300 for (expected.variants.items, actual.variants.items) |expected_variant, actual_variant| {
301 try std.testing.expectEqual(expected_variant.id.value, actual_variant.id.value);
302 try std.testing.expect(expected_variant.choice.target.eql(actual_variant.choice.target));
303 try std.testing.expect(expected_variant.choice.policy.eql(actual_variant.choice.policy));
304 try std.testing.expectEqual(expected_variant.fragments.len, actual_variant.fragments.len);
305 for (expected_variant.fragments, actual_variant.fragments) |expected_fragment, actual_fragment| {
306 try std.testing.expectEqual(expected_fragment.value, actual_fragment.value);
307 }
308 try std.testing.expectEqual(expected_variant.call_sites.len, actual_variant.call_sites.len);
309 for (expected_variant.call_sites, actual_variant.call_sites) |expected_call, actual_call| {
310 try std.testing.expectEqual(expected_call.id.value, actual_call.id.value);
311 try std.testing.expectEqualStrings(expected_call.name, actual_call.name);
312 try std.testing.expectEqual(expected_call.caller.value, actual_call.caller.value);
313 try std.testing.expectEqualStrings(expected_call.caller_export, actual_call.caller_export);
314 try std.testing.expectEqualStrings(expected_call.runtime_import, actual_call.runtime_import);
315 try std.testing.expectEqual(expected_call.callee.value, actual_call.callee.value);
316 try std.testing.expectEqualStrings(expected_call.callee_export, actual_call.callee_export);
317 try expectBoundaryIdsEqual(expected_call.inputs, actual_call.inputs);
318 try expectBoundaryIdsEqual(expected_call.outputs, actual_call.outputs);
319 try std.testing.expectEqual(expected_call.abi_version, actual_call.abi_version);
320 try expectProvenanceEqual(expected_call.provenance, actual_call.provenance);
321 }
322 }
323 }
324
325 test "composition module binary serialization round-trips every owned field" {
326 const testing = std.testing;
327 var original = try testModule(testing.allocator);
328 var original_owned = true;
329 defer if (original_owned) original.deinit();
330 const encoded = try serialize(testing.allocator, &original);
331 defer testing.allocator.free(encoded);
332 try testing.expectEqual(@as(usize, 2885), encoded.len);
333 try testing.expectEqual(@as(u32, 3643165904), format.checksum(encoded));
334
335 var decoded = try deserialize(testing.allocator, encoded);
336 defer decoded.deinit();
337 try expectModuleEqual(&original, &decoded);
338 try testing.expectEqual(@as(usize, 2), decoded.variants.items.len);
339 const first_variant = decoded.selectVariant(.{
340 .target = product.productRef("composition-fixture", "301", "hybrid.target", "default"),
341 .policy = product.productRef("composition-fixture", "302", "hybrid.policy", "default"),
342 }) orelse return error.TestExpectedResult;
343 const second_variant = decoded.selectVariant(.{
344 .target = product.productRef("composition-fixture", "303", "hybrid.target", "default"),
345 .policy = product.productRef("composition-fixture", "302", "hybrid.policy", "default"),
346 }) orelse return error.TestExpectedResult;
347 try testing.expectEqualSlices(
348 source_model.FragmentId,
349 &.{ .{ .value = 20 }, .{ .value = 21 } },
350 first_variant.fragments,
351 );
352 try testing.expectEqualSlices(
353 source_model.FragmentId,
354 &.{ .{ .value = 20 }, .{ .value = 22 } },
355 second_variant.fragments,
356 );
357 try testing.expectEqual(source_model.Effect.unknown, decoded.source_module.partitions.items[0].effect);
358 const reencoded = try serialize(testing.allocator, &decoded);
359 defer testing.allocator.free(reencoded);
360 try testing.expectEqualSlices(u8, encoded, reencoded);
361
362 original.deinit();
363 original_owned = false;
364 @memset(encoded, 0);
365 const independent = try serialize(testing.allocator, &decoded);
366 defer testing.allocator.free(independent);
367 try testing.expectEqualSlices(u8, reencoded, independent);
368 try testing.expectEqualStrings("preserved diagnostic", decoded.fragments.items[0].artifacts[0].verification.message.?);
369 }
370
371 test "composition module round trip keeps two compilers apart at one product ref" {
372 const testing = std.testing;
373 const records = try fixture.Records.init(testing.allocator);
374 defer records.deinit();
375 const left = try records.importUnder("hybrid.target", "301", "image", "toolchain-a");
376 const right = try records.importUnder("hybrid.target", "301", "image", "toolchain-b");
377 try testing.expectEqualSlices(u8, left.record.bytes(), right.record.bytes());
378 try testing.expect(left.ref.eql(right.ref));
379 try testing.expect(!left.eql(right));
380
381 var manifests = serialization.reference.Manifests{};
382 var writer = format.Writer.init(testing.allocator);
383 defer writer.deinit();
384 try writer.writeBlob(left.record.bytes());
385 try writer.writeInt(u32, @intCast(try manifests.intern(left.record.compilerManifest())));
386 try writer.writeBlob(right.record.bytes());
387 try writer.writeInt(u32, @intCast(try manifests.intern(right.record.compilerManifest())));
388 try testing.expectEqual(@as(usize, 2), manifests.count);
389
390 var table = format.Writer.init(testing.allocator);
391 defer table.deinit();
392 try serialization.reference.writeManifests(&table, &manifests);
393 var table_reader = try format.Reader.init(table.bytes.items);
394 const restored = try serialization.reference.readManifests(&table_reader);
395 try testing.expect(table_reader.atEnd());
396
397 const owner = try serialization.reference.createStore(testing.allocator, 2);
398 defer owner.release();
399 var reader = try format.Reader.init(writer.bytes.items);
400 const first = try serialization.reference.readRecord(owner, &reader, &restored);
401 const second = try serialization.reference.readRecord(owner, &reader, &restored);
402 try testing.expect(reader.atEnd());
403 try testing.expect(first.ref.eql(second.ref));
404 try testing.expectEqualSlices(u8, first.record.bytes(), second.record.bytes());
405 try testing.expect(!first.eql(second));
406 try testing.expectEqualStrings("toolchain-a", first.record.compilerManifest());
407 try testing.expectEqualStrings("toolchain-b", second.record.compilerManifest());
408 }
409
410 test "composition module refuses the record shape this format replaced" {
411 const testing = std.testing;
412 var original = try testModule(testing.allocator);
413 defer original.deinit();
414 const encoded = try serialize(testing.allocator, &original);
415 defer testing.allocator.free(encoded);
416 try testing.expectEqual(@as(u32, 6), format.version);
417 std.mem.writeInt(u32, encoded[format.magic.len..][0..4], 5, .little);
418 try testing.expectError(error.UnsupportedVersion, deserialize(testing.allocator, encoded));
419 }
420
421 test "composition module binary deserialization rejects malformed inputs" {
422 const testing = std.testing;
423 var module = try testModule(testing.allocator);
424 defer module.deinit();
425 const encoded = try serialize(testing.allocator, &module);
426 defer testing.allocator.free(encoded);
427
428 const invalid_magic = try testing.allocator.dupe(u8, encoded);
429 defer testing.allocator.free(invalid_magic);
430 invalid_magic[0] ^= 0xff;
431 try testing.expectError(error.InvalidMagic, deserialize(testing.allocator, invalid_magic));
432
433 const unsupported_version = try testing.allocator.dupe(u8, encoded);
434 defer testing.allocator.free(unsupported_version);
435 std.mem.writeInt(u32, unsupported_version[format.magic.len..][0..4], format.version + 1, .little);
436 try testing.expectError(error.UnsupportedVersion, deserialize(testing.allocator, unsupported_version));
437
438 const invalid_length = try testing.allocator.dupe(u8, encoded);
439 defer testing.allocator.free(invalid_length);
440 const length_offset = format.magic.len + @sizeOf(u32);
441 std.mem.writeInt(u64, invalid_length[length_offset..][0..@sizeOf(u64)], std.math.maxInt(u64), .little);
442 var length_allocator = testing.FailingAllocator.init(testing.allocator, .{ .fail_index = 0 });
443 try testing.expectError(error.LimitExceeded, deserialize(length_allocator.allocator(), invalid_length));
444
445 const source_name_offset = format.header_size;
446 const boundary_count_offset = source_name_offset + @sizeOf(u32) + module.source_module.name.len;
447 const first_boundary = module.source_module.boundaries.items[0];
448 const first_element_type_offset = boundary_count_offset + @sizeOf(u32) + @sizeOf(u64) + @sizeOf(u32) + first_boundary.name.len;
449 const invalid_tag = try testing.allocator.dupe(u8, encoded);
450 defer testing.allocator.free(invalid_tag);
451 invalid_tag[first_element_type_offset] = 0xff;
452 refreshChecksum(invalid_tag);
453 try testing.expectError(error.InvalidTag, deserialize(testing.allocator, invalid_tag));
454
455 try testing.expectError(error.Truncated, deserialize(testing.allocator, encoded[0 .. encoded.len - 1]));
456
457 const trailing = try testing.allocator.alloc(u8, encoded.len + 1);
458 defer testing.allocator.free(trailing);
459 @memcpy(trailing[0..encoded.len], encoded);
460 trailing[encoded.len] = 0;
461 try testing.expectError(error.TrailingBytes, deserialize(testing.allocator, trailing));
462
463 const count_bomb = try testing.allocator.dupe(u8, encoded);
464 defer testing.allocator.free(count_bomb);
465 const first_dimensions_count_offset = first_element_type_offset + @sizeOf(u8);
466 std.mem.writeInt(u32, count_bomb[first_dimensions_count_offset..][0..4], std.math.maxInt(u32), .little);
467 refreshChecksum(count_bomb);
468 try testing.expectError(error.LimitExceeded, deserialize(testing.allocator, count_bomb));
469
470 const artifact_offset = std.mem.indexOf(u8, encoded, artifact.serialization.magic).?;
471 const artifact_payload_offset = artifact_offset + artifact.serialization.magic.len + @sizeOf(u32);
472
473 const invalid_bool = try testing.allocator.dupe(u8, encoded);
474 defer testing.allocator.free(invalid_bool);
475 invalid_bool[artifact_payload_offset + @sizeOf(u8)] = 2;
476 refreshChecksum(invalid_bool);
477 try testing.expectError(error.InvalidValue, deserialize(testing.allocator, invalid_bool));
478
479 const invalid_string = try testing.allocator.dupe(u8, encoded);
480 defer testing.allocator.free(invalid_string);
481 const producer_length_offset = artifact_payload_offset + @sizeOf(u8) + @sizeOf(u8);
482 std.mem.writeInt(u32, invalid_string[producer_length_offset..][0..@sizeOf(u32)], artifact.serialization.format.max_string_bytes + 1, .little);
483 refreshChecksum(invalid_string);
484 try testing.expectError(error.LimitExceeded, deserialize(testing.allocator, invalid_string));
485
486 const invalid_blob = try testing.allocator.dupe(u8, encoded);
487 defer testing.allocator.free(invalid_blob);
488 const executable = module.fragments.items[0].artifacts[0].payload.buffers.items[0].bytes;
489 const executable_offset = std.mem.indexOf(u8, invalid_blob, executable).?;
490 const executable_length_offset = executable_offset - @sizeOf(u32);
491 std.mem.writeInt(u32, invalid_blob[executable_length_offset..][0..@sizeOf(u32)], artifact.serialization.format.max_blob_bytes + 1, .little);
492 refreshChecksum(invalid_blob);
493 try testing.expectError(error.LimitExceeded, deserialize(testing.allocator, invalid_blob));
494
495 const executable_corruption = try testing.allocator.dupe(u8, encoded);
496 defer testing.allocator.free(executable_corruption);
497 const corruption_offset = std.mem.indexOf(u8, executable_corruption, executable).?;
498 executable_corruption[corruption_offset + executable.len / 2] ^= 0x80;
499 var checksum_allocator = testing.FailingAllocator.init(testing.allocator, .{ .fail_index = 0 });
500 try testing.expectError(error.InvalidChecksum, deserialize(checksum_allocator.allocator(), executable_corruption));
501 }
502
503 fn refreshChecksum(bytes: []u8) void {
504 const payload = bytes[format.header_size .. bytes.len - format.checksum_size];
505 std.mem.writeInt(u32, bytes[bytes.len - format.checksum_size ..][0..format.checksum_size], format.checksum(payload), .little);
506 }
507
508 test "composition module binary serialization survives every allocation failure" {
509 const testing = std.testing;
510 var module = try testModule(testing.allocator);
511 defer module.deinit();
512 const encoded = try serialize(testing.allocator, &module);
513 defer testing.allocator.free(encoded);
514
515 const Harness = struct {
516 fn serializeRun(allocator: std.mem.Allocator, value: *const model.CompositionModule) !void {
517 const bytes = try serialize(allocator, value);
518 defer allocator.free(bytes);
519 }
520
521 fn deserializeRun(allocator: std.mem.Allocator, bytes: []const u8) !void {
522 var decoded = try deserialize(allocator, bytes);
523 defer decoded.deinit();
524 }
525 };
526
527 try testing.checkAllAllocationFailures(testing.allocator, Harness.serializeRun, .{&module});
528 try testing.checkAllAllocationFailures(testing.allocator, Harness.deserializeRun, .{encoded});
529 }