authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-05-07 20:44:07-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-07-01 16:35:29-07:00
logd603121dc355fee44b86a7fdf3666dce6e3ab677
treec4821a2aaa16861003b419182fca53b2c9d2ff52
parent01b580e75d2d57e315545f63cbea51ebdaaa54d4

std.crypto.Certificate.Bundle.macos: rework

- use ArrayList strategically to reduce allocations - use a BufferedReader to avoid unnecessary memcpy of the certs - use for loops - skip certs with invalid magic instead of asserting

10 files changed, 142 insertions(+), 106 deletions(-)

lib/std/crypto/Certificate/Bundle/macos.zig+34-34
......@@ -16,64 +16,64 @@ pub fn rescanMac(cb: *Bundle, gpa: Allocator) RescanMacError!void {
1616 "/Library/Keychains/System.keychain",
1717 };
1818
19 for (keychainPaths) |keychainPath| {
20 const file = try fs.openFileAbsolute(keychainPath, .{});
21 defer file.close();
19 const now_sec = std.time.timestamp();
2220
23 const bytes = try file.readToEndAlloc(gpa, std.math.maxInt(u32));
24 defer gpa.free(bytes);
21 var records: std.ArrayListUnmanaged(u32) = .empty;
22 defer records.deinit(gpa);
2523
26 var stream = std.io.fixedBufferStream(bytes);
27 const reader = stream.reader();
24 var tables: std.ArrayListUnmanaged(u32) = .empty;
25 defer tables.deinit(gpa);
2826
29 const db_header = try reader.readStructEndian(ApplDbHeader, .big);
30 assert(mem.eql(u8, &db_header.signature, "kych"));
27 for (keychainPaths) |keychainPath| {
28 const file = try fs.openFileAbsolute(keychainPath, .{});
29 defer file.close();
3130
32 try stream.seekTo(db_header.schema_offset);
31 var in_buffer: [256]u8 = undefined;
32 comptime assert(in_buffer.len > @sizeOf(ApplDbHeader));
33 comptime assert(in_buffer.len > @sizeOf(ApplDbSchema));
34 comptime assert(in_buffer.len > @sizeOf(TableHeader));
35 comptime assert(in_buffer.len > @sizeOf(X509CertHeader));
36 var file_reader = file.reader();
37 var br = file_reader.interface().buffered(&in_buffer);
3338
34 const db_schema = try reader.readStructEndian(ApplDbSchema, .big);
39 const db_header = try br.takeStructEndian(ApplDbHeader, .big);
40 if (!mem.eql(u8, &db_header.signature, "kych")) continue;
3541
36 var table_list = try gpa.alloc(u32, db_schema.table_count);
37 defer gpa.free(table_list);
42 try file_reader.seekTo(db_header.schema_offset);
43 br = file_reader.interface().buffered(&in_buffer);
3844
39 var table_idx: u32 = 0;
40 while (table_idx < table_list.len) : (table_idx += 1) {
41 table_list[table_idx] = try reader.readInt(u32, .big);
42 }
45 const db_schema = try br.takeStructEndian(ApplDbSchema, .big);
4346
44 const now_sec = std.time.timestamp();
47 try tables.resize(db_schema.table_count);
48 for (tables.items) |*offset| offset.* = try br.takeInt(u32, .big);
4549
46 for (table_list) |table_offset| {
47 try stream.seekTo(db_header.schema_offset + table_offset);
50 for (tables.items) |table_offset| {
51 try file_reader.seekTo(db_header.schema_offset + table_offset);
52 br = file_reader.interface().buffered(&in_buffer);
4853
49 const table_header = try reader.readStructEndian(TableHeader, .big);
54 const table_header = try br.takeStructEndian(TableHeader, .big);
5055
5156 if (@as(std.c.DB_RECORDTYPE, @enumFromInt(table_header.table_id)) != .X509_CERTIFICATE) {
5257 continue;
5358 }
5459
55 var record_list = try gpa.alloc(u32, table_header.record_count);
56 defer gpa.free(record_list);
60 try records.resize(gpa, table_header.record_count);
61 for (records.items) |*offset| offset.* = try br.takeInt(u32, .big);
5762
58 var record_idx: u32 = 0;
59 while (record_idx < record_list.len) : (record_idx += 1) {
60 record_list[record_idx] = try reader.readInt(u32, .big);
61 }
62
63 for (record_list) |record_offset| {
63 for (records.items) |record_offset| {
6464 // An offset of zero means that the record is not present.
6565 // An offset that is not 4-byte-aligned is invalid.
6666 if (record_offset == 0 or record_offset % 4 != 0) continue;
6767
68 try stream.seekTo(db_header.schema_offset + table_offset + record_offset);
69
70 const cert_header = try reader.readStructEndian(X509CertHeader, .big);
68 try file_reader.seekTo(db_header.schema_offset + table_offset + record_offset);
69 br = file_reader.interface().buffered(&in_buffer);
7170
71 const cert_header = try br.takeStructEndian(X509CertHeader, .big);
7272 if (cert_header.cert_size == 0) continue;
7373
74 const cert_start = @as(u32, @intCast(cb.bytes.items.len));
74 const cert_start: u32 = @intCast(cb.bytes.items.len);
7575 const dest_buf = try cb.bytes.addManyAsSlice(gpa, cert_header.cert_size);
76 try reader.readNoEof(dest_buf);
76 try br.readSlice(dest_buf);
7777
7878 try cb.parseCert(gpa, cert_start, now_sec);
7979 }
lib/std/crypto/phc_encoding.zig+5-4
......@@ -188,9 +188,10 @@ pub fn deserialize(comptime HashResult: type, str: []const u8) Error!HashResult
188188///
189189/// `params` can also include any additional parameters.
190190pub fn serialize(params: anytype, str: []u8) Error![]const u8 {
191 var buf = io.fixedBufferStream(str);
192 try serializeTo(params, buf.writer());
193 return buf.getWritten();
191 var bw: std.io.BufferedWriter = undefined;
192 bw.initFixed(str);
193 try serializeTo(params, &bw);
194 return bw.getWritten();
194195}
195196
196197/// Compute the number of bytes required to serialize `params`
......@@ -200,7 +201,7 @@ pub fn calcSize(params: anytype) usize {
200201 return @as(usize, @intCast(buf.bytes_written));
201202}
202203
203fn serializeTo(params: anytype, out: anytype) !void {
204fn serializeTo(params: anytype, out: *std.io.BufferedWriter) !void {
204205 const HashResult = @TypeOf(params);
205206
206207 if (@hasField(HashResult, version_param_name)) {
lib/std/crypto/scrypt.zig+16-11
......@@ -304,30 +304,35 @@ const crypt_format = struct {
304304
305305 /// Serialize parameters into a string in modular crypt format.
306306 pub fn serialize(params: anytype, str: []u8) EncodingError![]const u8 {
307 var buf = io.fixedBufferStream(str);
308 try serializeTo(params, buf.writer());
309 return buf.getWritten();
307 var bw: std.io.BufferedWriter = undefined;
308 bw.initFixed(str);
309 try serializeTo(params, &bw);
310 return bw.getWritten();
310311 }
311312
312313 /// Compute the number of bytes required to serialize `params`
313314 pub fn calcSize(params: anytype) usize {
314 var buf = io.countingWriter(io.null_writer);
315 serializeTo(params, buf.writer()) catch unreachable;
316 return @as(usize, @intCast(buf.bytes_written));
315 var null_writer: std.io.Writer.Null = .{};
316 var trash: [64]u8 = undefined;
317 var bw = null_writer.writer().buffered(&trash);
318 serializeTo(params, &bw) catch |err| switch (err) {
319 error.WriteFailed => unreachable,
320 };
321 return bw.count;
317322 }
318323
319 fn serializeTo(params: anytype, out: anytype) !void {
324 fn serializeTo(params: anytype, out: *std.io.BufferedWriter) !void {
320325 var header: [14]u8 = undefined;
321326 header[0..3].* = prefix.*;
322327 Codec.intEncode(header[3..4], params.ln);
323328 Codec.intEncode(header[4..9], params.r);
324329 Codec.intEncode(header[9..14], params.p);
325 try out.writeAll(&header);
326 try out.writeAll(params.salt);
327 try out.writeAll("$");
330
328331 var buf: [@TypeOf(params.hash).max_encoded_length]u8 = undefined;
329332 const hash_str = try params.hash.toB64(&buf);
330 try out.writeAll(hash_str);
333
334 var vecs: [4][]const u8 = .{ &header, params.salt, "$", hash_str };
335 try out.writeVecAll(&vecs);
331336 }
332337
333338 /// Custom codec that maps 6 bits into 8 like regular Base64, but uses its own alphabet,
lib/std/fs/File.zig+18-2
......@@ -947,6 +947,22 @@ pub const Reader = struct {
947947 };
948948 }
949949
950 pub fn seekTo(r: *Reader, offset: u64) SeekError!void {
951 // TODO if the offset is after the current offset, seek by discarding.
952 if (r.seek_err) |err| return err;
953 switch (r.mode) {
954 .positional, .positional_reading => {
955 r.pos = offset;
956 },
957 .streaming, .streaming_reading => {
958 posix.lseek_SET(r.file.handle, offset) catch |err| {
959 r.seek_err = err;
960 return err;
961 };
962 },
963 }
964 }
965
950966 /// Number of slices to store on the stack, when trying to send as many byte
951967 /// vectors through the underlying read calls as possible.
952968 const max_buffers_len = 16;
......@@ -975,7 +991,7 @@ pub const Reader = struct {
975991 error.WriteFailed => return error.WriteFailed,
976992 error.Unseekable => {
977993 r.mode = .streaming;
978 assert(pos == 0);
994 if (pos != 0) @panic("TODO need to seek here");
979995 return 0;
980996 },
981997 error.Unimplemented => {
......@@ -1011,7 +1027,7 @@ pub const Reader = struct {
10111027 const n = file.pread(dest, pos) catch |err| switch (err) {
10121028 error.Unseekable => {
10131029 r.mode = .streaming_reading;
1014 assert(pos == 0);
1030 if (pos != 0) @panic("TODO need to seek here");
10151031 return 0;
10161032 },
10171033 else => |e| {
lib/std/math.zig+4
......@@ -579,6 +579,10 @@ pub fn addAny(comptime Result: type, a: anytype, b: anytype) ?Result {
579579 return cast(Result, @as(O, a) + @as(O, b));
580580}
581581
582test addAny {
583 return error.Unimplemented;
584}
585
582586/// Returns a - b, or an error on overflow.
583587pub fn sub(comptime T: type, a: T, b: T) (error{Overflow}!T) {
584588 if (T == comptime_int) return a - b;
lib/std/tar.zig+24-23
......@@ -955,13 +955,14 @@ test Iterator {
955955 // example/empty/
956956
957957 const data = @embedFile("tar/testdata/example.tar");
958 var fbs = std.io.fixedBufferStream(data);
958 var br: std.io.BufferedReader = undefined;
959 br.initFixed(data);
959960
960961 // User provided buffers to the iterator
961962 var file_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
962963 var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
963964 // Create iterator
964 var iter: Iterator = .init(fbs.reader(), .{
965 var iter: Iterator = .init(&br, .{
965966 .file_name_buffer = &file_name_buffer,
966967 .link_name_buffer = &link_name_buffer,
967968 });
......@@ -1014,15 +1015,15 @@ test pipeToFileSystem {
10141015 // example/empty/
10151016
10161017 const data = @embedFile("tar/testdata/example.tar");
1017 var fbs = std.io.fixedBufferStream(data);
1018 const reader = fbs.reader();
1018 var br: std.io.BufferedReader = undefined;
1019 br.initFixed(data);
10191020
10201021 var tmp = testing.tmpDir(.{ .no_follow = true });
10211022 defer tmp.cleanup();
10221023 const dir = tmp.dir;
10231024
1024 // Save tar from `reader` to the file system `dir`
1025 pipeToFileSystem(dir, reader, .{
1025 // Save tar from reader to the file system `dir`
1026 pipeToFileSystem(dir, &br, .{
10261027 .mode_mode = .ignore,
10271028 .strip_components = 1,
10281029 .exclude_empty_directories = true,
......@@ -1046,8 +1047,8 @@ test pipeToFileSystem {
10461047
10471048test "pipeToFileSystem root_dir" {
10481049 const data = @embedFile("tar/testdata/example.tar");
1049 var fbs = std.io.fixedBufferStream(data);
1050 const reader = fbs.reader();
1050 var br: std.io.BufferedReader = undefined;
1051 br.initFixed(data);
10511052
10521053 // with strip_components = 1
10531054 {
......@@ -1056,7 +1057,7 @@ test "pipeToFileSystem root_dir" {
10561057 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
10571058 defer diagnostics.deinit();
10581059
1059 pipeToFileSystem(tmp.dir, reader, .{
1060 pipeToFileSystem(tmp.dir, &br, .{
10601061 .strip_components = 1,
10611062 .diagnostics = &diagnostics,
10621063 }) catch |err| {
......@@ -1072,13 +1073,13 @@ test "pipeToFileSystem root_dir" {
10721073
10731074 // with strip_components = 0
10741075 {
1075 fbs.reset();
1076 br.initFixed(data);
10761077 var tmp = testing.tmpDir(.{ .no_follow = true });
10771078 defer tmp.cleanup();
10781079 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
10791080 defer diagnostics.deinit();
10801081
1081 pipeToFileSystem(tmp.dir, reader, .{
1082 pipeToFileSystem(tmp.dir, &br, .{
10821083 .strip_components = 0,
10831084 .diagnostics = &diagnostics,
10841085 }) catch |err| {
......@@ -1095,45 +1096,45 @@ test "pipeToFileSystem root_dir" {
10951096
10961097test "findRoot with single file archive" {
10971098 const data = @embedFile("tar/testdata/22752.tar");
1098 var fbs = std.io.fixedBufferStream(data);
1099 const reader = fbs.reader();
1099 var br: std.io.BufferedReader = undefined;
1100 br.initFixed(data);
11001101
11011102 var tmp = testing.tmpDir(.{});
11021103 defer tmp.cleanup();
11031104
11041105 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
11051106 defer diagnostics.deinit();
1106 try pipeToFileSystem(tmp.dir, reader, .{ .diagnostics = &diagnostics });
1107 try pipeToFileSystem(tmp.dir, &br, .{ .diagnostics = &diagnostics });
11071108
11081109 try testing.expectEqualStrings("", diagnostics.root_dir);
11091110}
11101111
11111112test "findRoot without explicit root dir" {
11121113 const data = @embedFile("tar/testdata/19820.tar");
1113 var fbs = std.io.fixedBufferStream(data);
1114 const reader = fbs.reader();
1114 var br: std.io.BufferedReader = undefined;
1115 br.initFixed(data);
11151116
11161117 var tmp = testing.tmpDir(.{});
11171118 defer tmp.cleanup();
11181119
11191120 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
11201121 defer diagnostics.deinit();
1121 try pipeToFileSystem(tmp.dir, reader, .{ .diagnostics = &diagnostics });
1122 try pipeToFileSystem(tmp.dir, &br, .{ .diagnostics = &diagnostics });
11221123
11231124 try testing.expectEqualStrings("root", diagnostics.root_dir);
11241125}
11251126
11261127test "pipeToFileSystem strip_components" {
11271128 const data = @embedFile("tar/testdata/example.tar");
1128 var fbs = std.io.fixedBufferStream(data);
1129 const reader = fbs.reader();
1129 var br: std.io.BufferedReader = undefined;
1130 br.initFixed(data);
11301131
11311132 var tmp = testing.tmpDir(.{ .no_follow = true });
11321133 defer tmp.cleanup();
11331134 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
11341135 defer diagnostics.deinit();
11351136
1136 pipeToFileSystem(tmp.dir, reader, .{
1137 pipeToFileSystem(tmp.dir, &br, .{
11371138 .strip_components = 3,
11381139 .diagnostics = &diagnostics,
11391140 }) catch |err| {
......@@ -1187,13 +1188,13 @@ test "executable bit" {
11871188 const data = @embedFile("tar/testdata/example.tar");
11881189
11891190 for ([_]PipeOptions.ModeMode{ .ignore, .executable_bit_only }) |opt| {
1190 var fbs = std.io.fixedBufferStream(data);
1191 const reader = fbs.reader();
1191 var br: std.io.BufferedReader = undefined;
1192 br.initFixed(data);
11921193
11931194 var tmp = testing.tmpDir(.{ .no_follow = true });
11941195 //defer tmp.cleanup();
11951196
1196 pipeToFileSystem(tmp.dir, reader, .{
1197 pipeToFileSystem(tmp.dir, &br, .{
11971198 .strip_components = 1,
11981199 .exclude_empty_directories = true,
11991200 .mode_mode = opt,
lib/std/tar/Writer.zig+15-12
......@@ -441,11 +441,12 @@ test "write files" {
441441 for (files) |file|
442442 try wrt.writeFileBytes(file.path, file.content, .{});
443443
444 var input: std.io.FixedBufferStream = .{ .buffer = output.getWritten() };
445 var iter = std.tar.iterator(
446 input.reader(),
447 .{ .file_name_buffer = &file_name_buffer, .link_name_buffer = &link_name_buffer },
448 );
444 var input: std.io.BufferedReader = undefined;
445 input.initFixed(output.getWritten());
446 var iter = std.tar.iterator(&input, .{
447 .file_name_buffer = &file_name_buffer,
448 .link_name_buffer = &link_name_buffer,
449 });
449450
450451 // first entry is directory with prefix
451452 {
......@@ -475,15 +476,17 @@ test "write files" {
475476 var wrt: Writer = .{ .underlying_writer = &output.buffered_writer };
476477 defer output.deinit();
477478 for (files) |file| {
478 var content = std.io.fixedBufferStream(file.content);
479 try wrt.writeFileStream(file.path, file.content.len, content.reader(), .{});
479 var content: std.io.BufferedReader = undefined;
480 content.initFixed(file.content);
481 try wrt.writeFileStream(file.path, file.content.len, &content, .{});
480482 }
481483
482 var input: std.io.FixedBufferStream = .{ .buffer = output.getWritten() };
483 var iter = std.tar.iterator(
484 input.reader(),
485 .{ .file_name_buffer = &file_name_buffer, .link_name_buffer = &link_name_buffer },
486 );
484 var input: std.io.BufferedReader = undefined;
485 input.initFixed(output.getWritten());
486 var iter = std.tar.iterator(&input, .{
487 .file_name_buffer = &file_name_buffer,
488 .link_name_buffer = &link_name_buffer,
489 });
487490
488491 var i: usize = 0;
489492 while (try iter.next()) |actual| {
lib/std/tar/test.zig+17-12
......@@ -346,8 +346,9 @@ test "run test cases" {
346346 var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
347347
348348 for (cases) |case| {
349 var fsb = std.io.fixedBufferStream(case.data);
350 var iter = tar.iterator(fsb.reader(), .{
349 var br: std.io.BufferedReader = undefined;
350 br.initFixed(case.data);
351 var iter = tar.iterator(&br, .{
351352 .file_name_buffer = &file_name_buffer,
352353 .link_name_buffer = &link_name_buffer,
353354 });
......@@ -390,8 +391,9 @@ test "pax/gnu long names with small buffer" {
390391 const long_name_cases = [_]Case{ cases[11], cases[25], cases[28] };
391392
392393 for (long_name_cases) |case| {
393 var fsb = std.io.fixedBufferStream(case.data);
394 var iter = tar.iterator(fsb.reader(), .{
394 var br: std.io.BufferedReader = undefined;
395 br.initFixed(case.data);
396 var iter = tar.iterator(&br, .{
395397 .file_name_buffer = &min_file_name_buffer,
396398 .link_name_buffer = &min_link_name_buffer,
397399 });
......@@ -411,8 +413,9 @@ test "insufficient buffer in Header name filed" {
411413 var min_file_name_buffer: [9]u8 = undefined;
412414 var min_link_name_buffer: [100]u8 = undefined;
413415
414 var fsb = std.io.fixedBufferStream(cases[0].data);
415 var iter = tar.iterator(fsb.reader(), .{
416 var br: std.io.BufferedReader = undefined;
417 br.initFixed(cases[0].data);
418 var iter = tar.iterator(&br, .{
416419 .file_name_buffer = &min_file_name_buffer,
417420 .link_name_buffer = &min_link_name_buffer,
418421 });
......@@ -466,21 +469,22 @@ test "should not overwrite existing file" {
466469 // This ensures that file is not overwritten.
467470 //
468471 const data = @embedFile("testdata/overwrite_file.tar");
469 var fsb = std.io.fixedBufferStream(data);
472 var br: std.io.BufferedReader = undefined;
473 br.initFixed(data);
470474
471475 // Unpack with strip_components = 1 should fail
472476 var root = std.testing.tmpDir(.{});
473477 defer root.cleanup();
474478 try testing.expectError(
475479 error.PathAlreadyExists,
476 tar.pipeToFileSystem(root.dir, fsb.reader(), .{ .mode_mode = .ignore, .strip_components = 1 }),
480 tar.pipeToFileSystem(root.dir, &br, .{ .mode_mode = .ignore, .strip_components = 1 }),
477481 );
478482
479483 // Unpack with strip_components = 0 should pass
480 fsb.reset();
484 br.initFixed(data);
481485 var root2 = std.testing.tmpDir(.{});
482486 defer root2.cleanup();
483 try tar.pipeToFileSystem(root2.dir, fsb.reader(), .{ .mode_mode = .ignore, .strip_components = 0 });
487 try tar.pipeToFileSystem(root2.dir, &br, .{ .mode_mode = .ignore, .strip_components = 0 });
484488}
485489
486490test "case sensitivity" {
......@@ -494,12 +498,13 @@ test "case sensitivity" {
494498 // 18089/alacritty/Darkermatrix.yml
495499 //
496500 const data = @embedFile("testdata/18089.tar");
497 var fsb = std.io.fixedBufferStream(data);
501 var br: std.io.BufferedReader = undefined;
502 br.initFixed(data);
498503
499504 var root = std.testing.tmpDir(.{});
500505 defer root.cleanup();
501506
502 tar.pipeToFileSystem(root.dir, fsb.reader(), .{ .mode_mode = .ignore, .strip_components = 1 }) catch |err| {
507 tar.pipeToFileSystem(root.dir, &br, .{ .mode_mode = .ignore, .strip_components = 1 }) catch |err| {
503508 // on case insensitive fs we fail on overwrite existing file
504509 try testing.expectEqual(error.PathAlreadyExists, err);
505510 return;
lib/std/zig/WindowsSdk.zig+6-6
......@@ -759,14 +759,14 @@ const MsvcLibDir = struct {
759759 while (instances_dir_it.next() catch return error.PathNotFound) |entry| {
760760 if (entry.kind != .directory) continue;
761761
762 var fbs = std.io.fixedBufferStream(&state_subpath_buf);
763 const writer = fbs.writer();
762 var bw: std.io.BufferedWriter = undefined;
763 bw.initFixed(&state_subpath_buf);
764764
765 writer.writeAll(entry.name) catch unreachable;
766 writer.writeByte(std.fs.path.sep) catch unreachable;
767 writer.writeAll("state.json") catch unreachable;
765 bw.writeAll(entry.name) catch unreachable;
766 bw.writeByte(std.fs.path.sep) catch unreachable;
767 bw.writeAll("state.json") catch unreachable;
768768
769 const json_contents = instances_dir.readFileAlloc(allocator, fbs.getWritten(), std.math.maxInt(usize)) catch continue;
769 const json_contents = instances_dir.readFileAlloc(allocator, bw.getWritten(), std.math.maxInt(usize)) catch continue;
770770 defer allocator.free(json_contents);
771771
772772 var parsed = std.json.parseFromSlice(std.json.Value, allocator, json_contents, .{}) catch continue;
lib/std/zig/system/linux.zig+3-2
......@@ -342,8 +342,9 @@ fn testParser(
342342 expected_model: *const Target.Cpu.Model,
343343 input: []const u8,
344344) !void {
345 var fbs = io.fixedBufferStream(input);
346 const result = try parser.parse(arch, fbs.reader());
345 var br: std.io.BufferedReader = undefined;
346 br.initFixed(@constCast(input));
347 const result = try parser.parse(arch, &br);
347348 try testing.expectEqual(expected_model, result.?.model);
348349 try testing.expect(expected_model.features.eql(result.?.features));
349350}