authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-07-23 10:02:52+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2025-07-23 10:02:52+02:00
logbc8e1a74c514e487842c03ab32d7f6e49f42c529
treef985dec0dbf247c590b2ee87c38184b3b3c555a3
parent2365392e0e2830b147d82ff055bbed3099b21171
parent4fcb479de95775d056f378fdf8a0b9570bc315b7
signaturebadge-check Signed by PGP key B5690EEEBB952194

Merge pull request #24523 from ziglang/fifno

std.tar: update to new I/O API

21 files changed, 1046 insertions(+), 1463 deletions(-)

CMakeLists.txt-9
......@@ -390,15 +390,6 @@ set(ZIG_STAGE2_SOURCES
390390 lib/std/Io.zig
391391 lib/std/Io/Reader.zig
392392 lib/std/Io/Writer.zig
393 lib/std/Io/buffered_atomic_file.zig
394 lib/std/Io/buffered_writer.zig
395 lib/std/Io/change_detection_stream.zig
396 lib/std/Io/counting_reader.zig
397 lib/std/Io/counting_writer.zig
398 lib/std/Io/find_byte_writer.zig
399 lib/std/Io/fixed_buffer_stream.zig
400 lib/std/Io/limited_reader.zig
401 lib/std/Io/seekable_stream.zig
402393 lib/std/Progress.zig
403394 lib/std/Random.zig
404395 lib/std/Target.zig
lib/compiler/std-docs.zig+15-3
......@@ -60,7 +60,9 @@ pub fn main() !void {
6060 const should_open_browser = force_open_browser orelse (listen_port == 0);
6161
6262 const address = std.net.Address.parseIp("127.0.0.1", listen_port) catch unreachable;
63 var http_server = try address.listen(.{});
63 var http_server = try address.listen(.{
64 .reuse_address = true,
65 });
6466 const port = http_server.listen_address.in.getPort();
6567 const url_with_newline = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/\n", .{port});
6668 std.fs.File.stdout().writeAll(url_with_newline) catch {};
......@@ -189,7 +191,11 @@ fn serveSourcesTar(request: *std.http.Server.Request, context: *Context) !void {
189191 var walker = try std_dir.walk(gpa);
190192 defer walker.deinit();
191193
192 var archiver = std.tar.writer(response.writer());
194 var adapter_buffer: [500]u8 = undefined;
195 var response_writer = response.writer().adaptToNewApi();
196 response_writer.new_interface.buffer = &adapter_buffer;
197
198 var archiver: std.tar.Writer = .{ .underlying_writer = &response_writer.new_interface };
193199 archiver.prefix = "std";
194200
195201 while (try walker.next()) |entry| {
......@@ -204,7 +210,13 @@ fn serveSourcesTar(request: *std.http.Server.Request, context: *Context) !void {
204210 }
205211 var file = try entry.dir.openFile(entry.basename, .{});
206212 defer file.close();
207 try archiver.writeFile(entry.path, file);
213 const stat = try file.stat();
214 var file_reader: std.fs.File.Reader = .{
215 .file = file,
216 .interface = std.fs.File.Reader.initInterface(&.{}),
217 .size = stat.size,
218 };
219 try archiver.writeFile(entry.path, &file_reader, stat.mtime);
208220 }
209221
210222 {
lib/docs/wasm/main.zig+3-3
......@@ -772,10 +772,10 @@ export fn decl_type_html(decl_index: Decl.Index) String {
772772const Oom = error{OutOfMemory};
773773
774774fn unpackInner(tar_bytes: []u8) !void {
775 var fbs = std.io.fixedBufferStream(tar_bytes);
775 var reader: std.Io.Reader = .fixed(tar_bytes);
776776 var file_name_buffer: [1024]u8 = undefined;
777777 var link_name_buffer: [1024]u8 = undefined;
778 var it = std.tar.iterator(fbs.reader(), .{
778 var it: std.tar.Iterator = .init(&reader, .{
779779 .file_name_buffer = &file_name_buffer,
780780 .link_name_buffer = &link_name_buffer,
781781 });
......@@ -796,7 +796,7 @@ fn unpackInner(tar_bytes: []u8) !void {
796796 {
797797 gop.value_ptr.* = file;
798798 }
799 const file_bytes = tar_bytes[fbs.pos..][0..@intCast(tar_file.size)];
799 const file_bytes = tar_bytes[reader.seek..][0..@intCast(tar_file.size)];
800800 assert(file == try Walk.add_file(file_name, file_bytes));
801801 }
802802 } else {
lib/std/Build/Fuzz/WebServer.zig+8-5
......@@ -522,7 +522,9 @@ fn serveSourcesTar(ws: *WebServer, request: *std.http.Server.Request) !void {
522522
523523 var cwd_cache: ?[]const u8 = null;
524524
525 var archiver = std.tar.writer(response.writer());
525 var adapter = response.writer().adaptToNewApi();
526 var archiver: std.tar.Writer = .{ .underlying_writer = &adapter.new_interface };
527 var read_buffer: [1024]u8 = undefined;
526528
527529 for (deduped_paths) |joined_path| {
528530 var file = joined_path.root_dir.handle.openFile(joined_path.sub_path, .{}) catch |err| {
......@@ -530,13 +532,14 @@ fn serveSourcesTar(ws: *WebServer, request: *std.http.Server.Request) !void {
530532 continue;
531533 };
532534 defer file.close();
533
535 const stat = try file.stat();
536 var file_reader: std.fs.File.Reader = .initSize(file, &read_buffer, stat.size);
534537 archiver.prefix = joined_path.root_dir.path orelse try memoizedCwd(arena, &cwd_cache);
535 try archiver.writeFile(joined_path.sub_path, file);
538 try archiver.writeFile(joined_path.sub_path, &file_reader, stat.mtime);
536539 }
537540
538 // intentionally omitting the pointless trailer
539 //try archiver.finish();
541 // intentionally not calling `archiver.finishPedantically`
542 try adapter.new_interface.flush();
540543 try response.end();
541544}
542545
lib/std/Io.zig+36-39
......@@ -314,11 +314,11 @@ pub fn GenericReader(
314314 }
315315
316316 /// Helper for bridging to the new `Reader` API while upgrading.
317 pub fn adaptToNewApi(self: *const Self) Adapter {
317 pub fn adaptToNewApi(self: *const Self, buffer: []u8) Adapter {
318318 return .{
319319 .derp_reader = self.*,
320320 .new_interface = .{
321 .buffer = &.{},
321 .buffer = buffer,
322322 .vtable = &.{ .stream = Adapter.stream },
323323 .seek = 0,
324324 .end = 0,
......@@ -334,10 +334,12 @@ pub fn GenericReader(
334334 fn stream(r: *Reader, w: *Writer, limit: Limit) Reader.StreamError!usize {
335335 const a: *@This() = @alignCast(@fieldParentPtr("new_interface", r));
336336 const buf = limit.slice(try w.writableSliceGreedy(1));
337 return a.derp_reader.read(buf) catch |err| {
337 const n = a.derp_reader.read(buf) catch |err| {
338338 a.err = err;
339339 return error.ReadFailed;
340340 };
341 w.advance(n);
342 return n;
341343 }
342344 };
343345 };
......@@ -419,9 +421,14 @@ pub fn GenericWriter(
419421 new_interface: Writer,
420422 err: ?Error = null,
421423
422 fn drain(w: *Writer, data: []const []const u8, splat: usize) Writer.Error!usize {
424 fn drain(w: *std.io.Writer, data: []const []const u8, splat: usize) std.io.Writer.Error!usize {
423425 _ = splat;
424426 const a: *@This() = @alignCast(@fieldParentPtr("new_interface", w));
427 const buffered = w.buffered();
428 if (buffered.len != 0) return w.consume(a.derp_writer.write(buffered) catch |err| {
429 a.err = err;
430 return error.WriteFailed;
431 });
425432 return a.derp_writer.write(data[0]) catch |err| {
426433 a.err = err;
427434 return error.WriteFailed;
......@@ -435,54 +442,46 @@ pub fn GenericWriter(
435442pub const AnyReader = @import("Io/DeprecatedReader.zig");
436443/// Deprecated in favor of `Writer`.
437444pub const AnyWriter = @import("Io/DeprecatedWriter.zig");
438
445/// Deprecated in favor of `File.Reader` and `File.Writer`.
439446pub const SeekableStream = @import("Io/seekable_stream.zig").SeekableStream;
440
447/// Deprecated in favor of `Writer`.
441448pub const BufferedWriter = @import("Io/buffered_writer.zig").BufferedWriter;
449/// Deprecated in favor of `Writer`.
442450pub const bufferedWriter = @import("Io/buffered_writer.zig").bufferedWriter;
443
451/// Deprecated in favor of `Reader`.
444452pub const BufferedReader = @import("Io/buffered_reader.zig").BufferedReader;
453/// Deprecated in favor of `Reader`.
445454pub const bufferedReader = @import("Io/buffered_reader.zig").bufferedReader;
455/// Deprecated in favor of `Reader`.
446456pub const bufferedReaderSize = @import("Io/buffered_reader.zig").bufferedReaderSize;
447
457/// Deprecated in favor of `Reader`.
448458pub const FixedBufferStream = @import("Io/fixed_buffer_stream.zig").FixedBufferStream;
459/// Deprecated in favor of `Reader`.
449460pub const fixedBufferStream = @import("Io/fixed_buffer_stream.zig").fixedBufferStream;
450
451pub const CWriter = @import("Io/c_writer.zig").CWriter;
452pub const cWriter = @import("Io/c_writer.zig").cWriter;
453
461/// Deprecated in favor of `Reader.Limited`.
454462pub const LimitedReader = @import("Io/limited_reader.zig").LimitedReader;
463/// Deprecated in favor of `Reader.Limited`.
455464pub const limitedReader = @import("Io/limited_reader.zig").limitedReader;
456
465/// Deprecated with no replacement; inefficient pattern
457466pub const CountingWriter = @import("Io/counting_writer.zig").CountingWriter;
467/// Deprecated with no replacement; inefficient pattern
458468pub const countingWriter = @import("Io/counting_writer.zig").countingWriter;
469/// Deprecated with no replacement; inefficient pattern
459470pub const CountingReader = @import("Io/counting_reader.zig").CountingReader;
471/// Deprecated with no replacement; inefficient pattern
460472pub const countingReader = @import("Io/counting_reader.zig").countingReader;
461473
462pub const MultiWriter = @import("Io/multi_writer.zig").MultiWriter;
463pub const multiWriter = @import("Io/multi_writer.zig").multiWriter;
464
465474pub const BitReader = @import("Io/bit_reader.zig").BitReader;
466475pub const bitReader = @import("Io/bit_reader.zig").bitReader;
467476
468477pub const BitWriter = @import("Io/bit_writer.zig").BitWriter;
469478pub const bitWriter = @import("Io/bit_writer.zig").bitWriter;
470479
471pub const ChangeDetectionStream = @import("Io/change_detection_stream.zig").ChangeDetectionStream;
472pub const changeDetectionStream = @import("Io/change_detection_stream.zig").changeDetectionStream;
473
474pub const FindByteWriter = @import("Io/find_byte_writer.zig").FindByteWriter;
475pub const findByteWriter = @import("Io/find_byte_writer.zig").findByteWriter;
476
477pub const BufferedAtomicFile = @import("Io/buffered_atomic_file.zig").BufferedAtomicFile;
478
479pub const StreamSource = @import("Io/stream_source.zig").StreamSource;
480
481480pub const tty = @import("Io/tty.zig");
482481
483/// A Writer that doesn't write to anything.
482/// Deprecated in favor of `Writer.Discarding`.
484483pub const null_writer: NullWriter = .{ .context = {} };
485
484/// Deprecated in favor of `Writer.Discarding`.
486485pub const NullWriter = GenericWriter(void, error{}, dummyWrite);
487486fn dummyWrite(context: void, data: []const u8) error{}!usize {
488487 _ = context;
......@@ -898,16 +897,14 @@ test {
898897 _ = Reader;
899898 _ = Reader.Limited;
900899 _ = Writer;
901 _ = @import("Io/bit_reader.zig");
902 _ = @import("Io/bit_writer.zig");
903 _ = @import("Io/buffered_atomic_file.zig");
904 _ = @import("Io/buffered_reader.zig");
905 _ = @import("Io/buffered_writer.zig");
906 _ = @import("Io/c_writer.zig");
907 _ = @import("Io/counting_writer.zig");
908 _ = @import("Io/counting_reader.zig");
909 _ = @import("Io/fixed_buffer_stream.zig");
910 _ = @import("Io/seekable_stream.zig");
911 _ = @import("Io/stream_source.zig");
900 _ = BitReader;
901 _ = BitWriter;
902 _ = BufferedReader;
903 _ = BufferedWriter;
904 _ = CountingWriter;
905 _ = CountingReader;
906 _ = FixedBufferStream;
907 _ = SeekableStream;
908 _ = tty;
912909 _ = @import("Io/test.zig");
913910}
lib/std/Io/DeprecatedReader.zig+3-1
......@@ -393,10 +393,12 @@ pub const Adapter = struct {
393393 fn stream(r: *std.io.Reader, w: *std.io.Writer, limit: std.io.Limit) std.io.Reader.StreamError!usize {
394394 const a: *@This() = @alignCast(@fieldParentPtr("new_interface", r));
395395 const buf = limit.slice(try w.writableSliceGreedy(1));
396 return a.derp_reader.read(buf) catch |err| {
396 const n = a.derp_reader.read(buf) catch |err| {
397397 a.err = err;
398398 return error.ReadFailed;
399399 };
400 w.advance(n);
401 return n;
400402 }
401403};
402404
lib/std/Io/DeprecatedWriter.zig+6-1
......@@ -100,7 +100,12 @@ pub const Adapter = struct {
100100
101101 fn drain(w: *std.io.Writer, data: []const []const u8, splat: usize) std.io.Writer.Error!usize {
102102 _ = splat;
103 const a: *@This() = @fieldParentPtr("new_interface", w);
103 const a: *@This() = @alignCast(@fieldParentPtr("new_interface", w));
104 const buffered = w.buffered();
105 if (buffered.len != 0) return w.consume(a.derp_writer.write(buffered) catch |err| {
106 a.err = err;
107 return error.WriteFailed;
108 });
104109 return a.derp_writer.write(data[0]) catch |err| {
105110 a.err = err;
106111 return error.WriteFailed;
lib/std/Io/Reader.zig+6
......@@ -179,6 +179,12 @@ pub fn streamExact(r: *Reader, w: *Writer, n: usize) StreamError!void {
179179 while (remaining != 0) remaining -= try r.stream(w, .limited(remaining));
180180}
181181
182/// "Pump" exactly `n` bytes from the reader to the writer.
183pub fn streamExact64(r: *Reader, w: *Writer, n: u64) StreamError!void {
184 var remaining = n;
185 while (remaining != 0) remaining -= try r.stream(w, .limited64(remaining));
186}
187
182188/// "Pump" data from the reader to the writer, handling `error.EndOfStream` as
183189/// a success case.
184190///
lib/std/Io/buffered_atomic_file.zig deleted-55
......@@ -1,55 +0,0 @@
1const std = @import("../std.zig");
2const mem = std.mem;
3const fs = std.fs;
4const File = std.fs.File;
5
6pub const BufferedAtomicFile = struct {
7 atomic_file: fs.AtomicFile,
8 file_writer: File.Writer,
9 buffered_writer: BufferedWriter,
10 allocator: mem.Allocator,
11
12 pub const buffer_size = 4096;
13 pub const BufferedWriter = std.io.BufferedWriter(buffer_size, File.Writer);
14 pub const Writer = std.io.GenericWriter(*BufferedWriter, BufferedWriter.Error, BufferedWriter.write);
15
16 /// TODO when https://github.com/ziglang/zig/issues/2761 is solved
17 /// this API will not need an allocator
18 pub fn create(
19 allocator: mem.Allocator,
20 dir: fs.Dir,
21 dest_path: []const u8,
22 atomic_file_options: fs.Dir.AtomicFileOptions,
23 ) !*BufferedAtomicFile {
24 var self = try allocator.create(BufferedAtomicFile);
25 self.* = BufferedAtomicFile{
26 .atomic_file = undefined,
27 .file_writer = undefined,
28 .buffered_writer = undefined,
29 .allocator = allocator,
30 };
31 errdefer allocator.destroy(self);
32
33 self.atomic_file = try dir.atomicFile(dest_path, atomic_file_options);
34 errdefer self.atomic_file.deinit();
35
36 self.file_writer = self.atomic_file.file.deprecatedWriter();
37 self.buffered_writer = .{ .unbuffered_writer = self.file_writer };
38 return self;
39 }
40
41 /// always call destroy, even after successful finish()
42 pub fn destroy(self: *BufferedAtomicFile) void {
43 self.atomic_file.deinit();
44 self.allocator.destroy(self);
45 }
46
47 pub fn finish(self: *BufferedAtomicFile) !void {
48 try self.buffered_writer.flush();
49 try self.atomic_file.finish();
50 }
51
52 pub fn writer(self: *BufferedAtomicFile) Writer {
53 return .{ .context = &self.buffered_writer };
54 }
55};
lib/std/Io/c_writer.zig deleted-44
......@@ -1,44 +0,0 @@
1const std = @import("../std.zig");
2const builtin = @import("builtin");
3const io = std.io;
4const testing = std.testing;
5
6pub const CWriter = io.GenericWriter(*std.c.FILE, std.fs.File.WriteError, cWriterWrite);
7
8pub fn cWriter(c_file: *std.c.FILE) CWriter {
9 return .{ .context = c_file };
10}
11
12fn cWriterWrite(c_file: *std.c.FILE, bytes: []const u8) std.fs.File.WriteError!usize {
13 const amt_written = std.c.fwrite(bytes.ptr, 1, bytes.len, c_file);
14 if (amt_written >= 0) return amt_written;
15 switch (@as(std.c.E, @enumFromInt(std.c._errno().*))) {
16 .SUCCESS => unreachable,
17 .INVAL => unreachable,
18 .FAULT => unreachable,
19 .AGAIN => unreachable, // this is a blocking API
20 .BADF => unreachable, // always a race condition
21 .DESTADDRREQ => unreachable, // connect was never called
22 .DQUOT => return error.DiskQuota,
23 .FBIG => return error.FileTooBig,
24 .IO => return error.InputOutput,
25 .NOSPC => return error.NoSpaceLeft,
26 .PERM => return error.PermissionDenied,
27 .PIPE => return error.BrokenPipe,
28 else => |err| return std.posix.unexpectedErrno(err),
29 }
30}
31
32test cWriter {
33 if (!builtin.link_libc or builtin.os.tag == .wasi) return error.SkipZigTest;
34
35 const filename = "tmp_io_test_file.txt";
36 const out_file = std.c.fopen(filename, "w") orelse return error.UnableToOpenTestFile;
37 defer {
38 _ = std.c.fclose(out_file);
39 std.fs.cwd().deleteFileZ(filename) catch {};
40 }
41
42 const writer = cWriter(out_file);
43 try writer.print("hi: {}\n", .{@as(i32, 123)});
44}
lib/std/Io/change_detection_stream.zig deleted-55
......@@ -1,55 +0,0 @@
1const std = @import("../std.zig");
2const io = std.io;
3const mem = std.mem;
4const assert = std.debug.assert;
5
6/// Used to detect if the data written to a stream differs from a source buffer
7pub fn ChangeDetectionStream(comptime WriterType: type) type {
8 return struct {
9 const Self = @This();
10 pub const Error = WriterType.Error;
11 pub const Writer = io.GenericWriter(*Self, Error, write);
12
13 anything_changed: bool,
14 underlying_writer: WriterType,
15 source_index: usize,
16 source: []const u8,
17
18 pub fn writer(self: *Self) Writer {
19 return .{ .context = self };
20 }
21
22 fn write(self: *Self, bytes: []const u8) Error!usize {
23 if (!self.anything_changed) {
24 const end = self.source_index + bytes.len;
25 if (end > self.source.len) {
26 self.anything_changed = true;
27 } else {
28 const src_slice = self.source[self.source_index..end];
29 self.source_index += bytes.len;
30 if (!mem.eql(u8, bytes, src_slice)) {
31 self.anything_changed = true;
32 }
33 }
34 }
35
36 return self.underlying_writer.write(bytes);
37 }
38
39 pub fn changeDetected(self: *Self) bool {
40 return self.anything_changed or (self.source_index != self.source.len);
41 }
42 };
43}
44
45pub fn changeDetectionStream(
46 source: []const u8,
47 underlying_writer: anytype,
48) ChangeDetectionStream(@TypeOf(underlying_writer)) {
49 return ChangeDetectionStream(@TypeOf(underlying_writer)){
50 .anything_changed = false,
51 .underlying_writer = underlying_writer,
52 .source_index = 0,
53 .source = source,
54 };
55}
lib/std/Io/find_byte_writer.zig deleted-40
......@@ -1,40 +0,0 @@
1const std = @import("../std.zig");
2const io = std.io;
3const assert = std.debug.assert;
4
5/// A Writer that returns whether the given character has been written to it.
6/// The contents are not written to anything.
7pub fn FindByteWriter(comptime UnderlyingWriter: type) type {
8 return struct {
9 const Self = @This();
10 pub const Error = UnderlyingWriter.Error;
11 pub const Writer = io.GenericWriter(*Self, Error, write);
12
13 underlying_writer: UnderlyingWriter,
14 byte_found: bool,
15 byte: u8,
16
17 pub fn writer(self: *Self) Writer {
18 return .{ .context = self };
19 }
20
21 fn write(self: *Self, bytes: []const u8) Error!usize {
22 if (!self.byte_found) {
23 self.byte_found = blk: {
24 for (bytes) |b|
25 if (b == self.byte) break :blk true;
26 break :blk false;
27 };
28 }
29 return self.underlying_writer.write(bytes);
30 }
31 };
32}
33
34pub fn findByteWriter(byte: u8, underlying_writer: anytype) FindByteWriter(@TypeOf(underlying_writer)) {
35 return FindByteWriter(@TypeOf(underlying_writer)){
36 .underlying_writer = underlying_writer,
37 .byte = byte,
38 .byte_found = false,
39 };
40}
lib/std/Io/multi_writer.zig deleted-53
......@@ -1,53 +0,0 @@
1const std = @import("../std.zig");
2const io = std.io;
3
4/// Takes a tuple of streams, and constructs a new stream that writes to all of them
5pub fn MultiWriter(comptime Writers: type) type {
6 comptime var ErrSet = error{};
7 inline for (@typeInfo(Writers).@"struct".fields) |field| {
8 const StreamType = field.type;
9 ErrSet = ErrSet || StreamType.Error;
10 }
11
12 return struct {
13 const Self = @This();
14
15 streams: Writers,
16
17 pub const Error = ErrSet;
18 pub const Writer = io.GenericWriter(*Self, Error, write);
19
20 pub fn writer(self: *Self) Writer {
21 return .{ .context = self };
22 }
23
24 pub fn write(self: *Self, bytes: []const u8) Error!usize {
25 inline for (self.streams) |stream|
26 try stream.writeAll(bytes);
27 return bytes.len;
28 }
29 };
30}
31
32pub fn multiWriter(streams: anytype) MultiWriter(@TypeOf(streams)) {
33 return .{ .streams = streams };
34}
35
36const testing = std.testing;
37
38test "MultiWriter" {
39 var tmp = testing.tmpDir(.{});
40 defer tmp.cleanup();
41 var f = try tmp.dir.createFile("t.txt", .{});
42
43 var buf1: [255]u8 = undefined;
44 var fbs1 = io.fixedBufferStream(&buf1);
45 var buf2: [255]u8 = undefined;
46 var stream = multiWriter(.{ fbs1.writer(), f.writer() });
47
48 try stream.writer().print("HI", .{});
49 f.close();
50
51 try testing.expectEqualSlices(u8, "HI", fbs1.getWritten());
52 try testing.expectEqualSlices(u8, "HI", try tmp.dir.readFile("t.txt", &buf2));
53}
lib/std/Io/stream_source.zig deleted-127
......@@ -1,127 +0,0 @@
1const std = @import("../std.zig");
2const builtin = @import("builtin");
3const io = std.io;
4
5/// Provides `io.GenericReader`, `io.GenericWriter`, and `io.SeekableStream` for in-memory buffers as
6/// well as files.
7/// For memory sources, if the supplied byte buffer is const, then `io.GenericWriter` is not available.
8/// The error set of the stream functions is the error set of the corresponding file functions.
9pub const StreamSource = union(enum) {
10 // TODO: expose UEFI files to std.os in a way that allows this to be true
11 const has_file = (builtin.os.tag != .freestanding and builtin.os.tag != .uefi);
12
13 /// The stream access is redirected to this buffer.
14 buffer: io.FixedBufferStream([]u8),
15
16 /// The stream access is redirected to this buffer.
17 /// Writing to the source will always yield `error.AccessDenied`.
18 const_buffer: io.FixedBufferStream([]const u8),
19
20 /// The stream access is redirected to this file.
21 /// On freestanding, this must never be initialized!
22 file: if (has_file) std.fs.File else void,
23
24 pub const ReadError = io.FixedBufferStream([]u8).ReadError || (if (has_file) std.fs.File.ReadError else error{});
25 pub const WriteError = error{AccessDenied} || io.FixedBufferStream([]u8).WriteError || (if (has_file) std.fs.File.WriteError else error{});
26 pub const SeekError = io.FixedBufferStream([]u8).SeekError || (if (has_file) std.fs.File.SeekError else error{});
27 pub const GetSeekPosError = io.FixedBufferStream([]u8).GetSeekPosError || (if (has_file) std.fs.File.GetSeekPosError else error{});
28
29 pub const Reader = io.GenericReader(*StreamSource, ReadError, read);
30 pub const Writer = io.GenericWriter(*StreamSource, WriteError, write);
31 pub const SeekableStream = io.SeekableStream(
32 *StreamSource,
33 SeekError,
34 GetSeekPosError,
35 seekTo,
36 seekBy,
37 getPos,
38 getEndPos,
39 );
40
41 pub fn read(self: *StreamSource, dest: []u8) ReadError!usize {
42 switch (self.*) {
43 .buffer => |*x| return x.read(dest),
44 .const_buffer => |*x| return x.read(dest),
45 .file => |x| if (!has_file) unreachable else return x.read(dest),
46 }
47 }
48
49 pub fn write(self: *StreamSource, bytes: []const u8) WriteError!usize {
50 switch (self.*) {
51 .buffer => |*x| return x.write(bytes),
52 .const_buffer => return error.AccessDenied,
53 .file => |x| if (!has_file) unreachable else return x.write(bytes),
54 }
55 }
56
57 pub fn seekTo(self: *StreamSource, pos: u64) SeekError!void {
58 switch (self.*) {
59 .buffer => |*x| return x.seekTo(pos),
60 .const_buffer => |*x| return x.seekTo(pos),
61 .file => |x| if (!has_file) unreachable else return x.seekTo(pos),
62 }
63 }
64
65 pub fn seekBy(self: *StreamSource, amt: i64) SeekError!void {
66 switch (self.*) {
67 .buffer => |*x| return x.seekBy(amt),
68 .const_buffer => |*x| return x.seekBy(amt),
69 .file => |x| if (!has_file) unreachable else return x.seekBy(amt),
70 }
71 }
72
73 pub fn getEndPos(self: *StreamSource) GetSeekPosError!u64 {
74 switch (self.*) {
75 .buffer => |*x| return x.getEndPos(),
76 .const_buffer => |*x| return x.getEndPos(),
77 .file => |x| if (!has_file) unreachable else return x.getEndPos(),
78 }
79 }
80
81 pub fn getPos(self: *StreamSource) GetSeekPosError!u64 {
82 switch (self.*) {
83 .buffer => |*x| return x.getPos(),
84 .const_buffer => |*x| return x.getPos(),
85 .file => |x| if (!has_file) unreachable else return x.getPos(),
86 }
87 }
88
89 pub fn reader(self: *StreamSource) Reader {
90 return .{ .context = self };
91 }
92
93 pub fn writer(self: *StreamSource) Writer {
94 return .{ .context = self };
95 }
96
97 pub fn seekableStream(self: *StreamSource) SeekableStream {
98 return .{ .context = self };
99 }
100};
101
102test "refs" {
103 std.testing.refAllDecls(StreamSource);
104}
105
106test "mutable buffer" {
107 var buffer: [64]u8 = undefined;
108 var source = StreamSource{ .buffer = std.io.fixedBufferStream(&buffer) };
109
110 var writer = source.writer();
111
112 try writer.writeAll("Hello, World!");
113
114 try std.testing.expectEqualStrings("Hello, World!", source.buffer.getWritten());
115}
116
117test "const buffer" {
118 const buffer: [64]u8 = "Hello, World!".* ++ ([1]u8{0xAA} ** 51);
119 var source = StreamSource{ .const_buffer = std.io.fixedBufferStream(&buffer) };
120
121 var reader = source.reader();
122
123 var dst_buffer: [13]u8 = undefined;
124 try reader.readNoEof(&dst_buffer);
125
126 try std.testing.expectEqualStrings("Hello, World!", &dst_buffer);
127}
lib/std/crypto/md5.zig+10-2
......@@ -54,12 +54,20 @@ pub const Md5 = struct {
5454 };
5555 }
5656
57 pub fn hash(b: []const u8, out: *[digest_length]u8, options: Options) void {
57 pub fn hash(data: []const u8, out: *[digest_length]u8, options: Options) void {
5858 var d = Md5.init(options);
59 d.update(b);
59 d.update(data);
6060 d.final(out);
6161 }
6262
63 pub fn hashResult(data: []const u8) [digest_length]u8 {
64 var out: [digest_length]u8 = undefined;
65 var d = Md5.init(.{});
66 d.update(data);
67 d.final(&out);
68 return out;
69 }
70
6371 pub fn update(d: *Self, b: []const u8) void {
6472 var off: usize = 0;
6573
lib/std/tar.zig+283-329
......@@ -19,7 +19,7 @@ const std = @import("std");
1919const assert = std.debug.assert;
2020const testing = std.testing;
2121
22pub const writer = @import("tar/writer.zig").writer;
22pub const Writer = @import("tar/Writer.zig");
2323
2424/// Provide this to receive detailed error messages.
2525/// When this is provided, some errors which would otherwise be returned
......@@ -293,28 +293,6 @@ fn nullStr(str: []const u8) []const u8 {
293293 return str;
294294}
295295
296/// Options for iterator.
297/// Buffers should be provided by the caller.
298pub const IteratorOptions = struct {
299 /// Use a buffer with length `std.fs.max_path_bytes` to match file system capabilities.
300 file_name_buffer: []u8,
301 /// Use a buffer with length `std.fs.max_path_bytes` to match file system capabilities.
302 link_name_buffer: []u8,
303 /// Collects error messages during unpacking
304 diagnostics: ?*Diagnostics = null,
305};
306
307/// Iterates over files in tar archive.
308/// `next` returns each file in tar archive.
309pub fn iterator(reader: anytype, options: IteratorOptions) Iterator(@TypeOf(reader)) {
310 return .{
311 .reader = reader,
312 .diagnostics = options.diagnostics,
313 .file_name_buffer = options.file_name_buffer,
314 .link_name_buffer = options.link_name_buffer,
315 };
316}
317
318296/// Type of the file returned by iterator `next` method.
319297pub const FileKind = enum {
320298 directory,
......@@ -323,206 +301,192 @@ pub const FileKind = enum {
323301};
324302
325303/// Iterator over entries in the tar file represented by reader.
326pub fn Iterator(comptime ReaderType: type) type {
327 return struct {
328 reader: ReaderType,
329 diagnostics: ?*Diagnostics = null,
330
331 // buffers for heeader and file attributes
332 header_buffer: [Header.SIZE]u8 = undefined,
333 file_name_buffer: []u8,
334 link_name_buffer: []u8,
335
336 // bytes of padding to the end of the block
337 padding: usize = 0,
338 // not consumed bytes of file from last next iteration
339 unread_file_bytes: u64 = 0,
340
341 pub const File = struct {
342 name: []const u8, // name of file, symlink or directory
343 link_name: []const u8, // target name of symlink
344 size: u64 = 0, // size of the file in bytes
345 mode: u32 = 0,
346 kind: FileKind = .file,
347
348 unread_bytes: *u64,
349 parent_reader: ReaderType,
350
351 pub const Reader = std.io.GenericReader(File, ReaderType.Error, File.read);
304pub const Iterator = struct {
305 reader: *std.Io.Reader,
306 diagnostics: ?*Diagnostics = null,
352307
353 pub fn reader(self: File) Reader {
354 return .{ .context = self };
355 }
308 // buffers for heeader and file attributes
309 header_buffer: [Header.SIZE]u8 = undefined,
310 file_name_buffer: []u8,
311 link_name_buffer: []u8,
356312
357 pub fn read(self: File, dest: []u8) ReaderType.Error!usize {
358 const buf = dest[0..@min(dest.len, self.unread_bytes.*)];
359 const n = try self.parent_reader.read(buf);
360 self.unread_bytes.* -= n;
361 return n;
362 }
313 // bytes of padding to the end of the block
314 padding: usize = 0,
315 // not consumed bytes of file from last next iteration
316 unread_file_bytes: u64 = 0,
363317
364 // Writes file content to writer.
365 pub fn writeAll(self: File, out_writer: anytype) !void {
366 var buffer: [4096]u8 = undefined;
318 /// Options for iterator.
319 /// Buffers should be provided by the caller.
320 pub const Options = struct {
321 /// Use a buffer with length `std.fs.max_path_bytes` to match file system capabilities.
322 file_name_buffer: []u8,
323 /// Use a buffer with length `std.fs.max_path_bytes` to match file system capabilities.
324 link_name_buffer: []u8,
325 /// Collects error messages during unpacking
326 diagnostics: ?*Diagnostics = null,
327 };
367328
368 while (self.unread_bytes.* > 0) {
369 const buf = buffer[0..@min(buffer.len, self.unread_bytes.*)];
370 try self.parent_reader.readNoEof(buf);
371 try out_writer.writeAll(buf);
372 self.unread_bytes.* -= buf.len;
373 }
374 }
329 /// Iterates over files in tar archive.
330 /// `next` returns each file in tar archive.
331 pub fn init(reader: *std.Io.Reader, options: Options) Iterator {
332 return .{
333 .reader = reader,
334 .diagnostics = options.diagnostics,
335 .file_name_buffer = options.file_name_buffer,
336 .link_name_buffer = options.link_name_buffer,
375337 };
338 }
376339
377 const Self = @This();
378
379 fn readHeader(self: *Self) !?Header {
380 if (self.padding > 0) {
381 try self.reader.skipBytes(self.padding, .{});
382 }
383 const n = try self.reader.readAll(&self.header_buffer);
384 if (n == 0) return null;
385 if (n < Header.SIZE) return error.UnexpectedEndOfStream;
386 const header = Header{ .bytes = self.header_buffer[0..Header.SIZE] };
387 if (try header.checkChksum() == 0) return null;
388 return header;
389 }
340 pub const File = struct {
341 name: []const u8, // name of file, symlink or directory
342 link_name: []const u8, // target name of symlink
343 size: u64 = 0, // size of the file in bytes
344 mode: u32 = 0,
345 kind: FileKind = .file,
346 };
390347
391 fn readString(self: *Self, size: usize, buffer: []u8) ![]const u8 {
392 if (size > buffer.len) return error.TarInsufficientBuffer;
393 const buf = buffer[0..size];
394 try self.reader.readNoEof(buf);
395 return nullStr(buf);
348 fn readHeader(self: *Iterator) !?Header {
349 if (self.padding > 0) {
350 try self.reader.discardAll(self.padding);
396351 }
352 const n = try self.reader.readSliceShort(&self.header_buffer);
353 if (n == 0) return null;
354 if (n < Header.SIZE) return error.UnexpectedEndOfStream;
355 const header = Header{ .bytes = self.header_buffer[0..Header.SIZE] };
356 if (try header.checkChksum() == 0) return null;
357 return header;
358 }
397359
398 fn newFile(self: *Self) File {
399 return .{
400 .name = self.file_name_buffer[0..0],
401 .link_name = self.link_name_buffer[0..0],
402 .parent_reader = self.reader,
403 .unread_bytes = &self.unread_file_bytes,
404 };
405 }
360 fn readString(self: *Iterator, size: usize, buffer: []u8) ![]const u8 {
361 if (size > buffer.len) return error.TarInsufficientBuffer;
362 const buf = buffer[0..size];
363 try self.reader.readSliceAll(buf);
364 return nullStr(buf);
365 }
406366
407 // Number of padding bytes in the last file block.
408 fn blockPadding(size: u64) usize {
409 const block_rounded = std.mem.alignForward(u64, size, Header.SIZE); // size rounded to te block boundary
410 return @intCast(block_rounded - size);
411 }
367 fn newFile(self: *Iterator) File {
368 return .{
369 .name = self.file_name_buffer[0..0],
370 .link_name = self.link_name_buffer[0..0],
371 };
372 }
412373
413 /// Iterates through the tar archive as if it is a series of files.
414 /// Internally, the tar format often uses entries (header with optional
415 /// content) to add meta data that describes the next file. These
416 /// entries should not normally be visible to the outside. As such, this
417 /// loop iterates through one or more entries until it collects a all
418 /// file attributes.
419 pub fn next(self: *Self) !?File {
420 if (self.unread_file_bytes > 0) {
421 // If file content was not consumed by caller
422 try self.reader.skipBytes(self.unread_file_bytes, .{});
423 self.unread_file_bytes = 0;
424 }
425 var file: File = self.newFile();
426
427 while (try self.readHeader()) |header| {
428 const kind = header.kind();
429 const size: u64 = try header.size();
430 self.padding = blockPadding(size);
431
432 switch (kind) {
433 // File types to return upstream
434 .directory, .normal, .symbolic_link => {
435 file.kind = switch (kind) {
436 .directory => .directory,
437 .normal => .file,
438 .symbolic_link => .sym_link,
439 else => unreachable,
440 };
441 file.mode = try header.mode();
442
443 // set file attributes if not already set by prefix/extended headers
444 if (file.size == 0) {
445 file.size = size;
446 }
447 if (file.link_name.len == 0) {
448 file.link_name = try header.linkName(self.link_name_buffer);
449 }
450 if (file.name.len == 0) {
451 file.name = try header.fullName(self.file_name_buffer);
452 }
374 // Number of padding bytes in the last file block.
375 fn blockPadding(size: u64) usize {
376 const block_rounded = std.mem.alignForward(u64, size, Header.SIZE); // size rounded to te block boundary
377 return @intCast(block_rounded - size);
378 }
453379
454 self.padding = blockPadding(file.size);
455 self.unread_file_bytes = file.size;
456 return file;
457 },
458 // Prefix header types
459 .gnu_long_name => {
460 file.name = try self.readString(@intCast(size), self.file_name_buffer);
461 },
462 .gnu_long_link => {
463 file.link_name = try self.readString(@intCast(size), self.link_name_buffer);
464 },
465 .extended_header => {
466 // Use just attributes from last extended header.
467 file = self.newFile();
468
469 var rdr = paxIterator(self.reader, @intCast(size));
470 while (try rdr.next()) |attr| {
471 switch (attr.kind) {
472 .path => {
473 file.name = try attr.value(self.file_name_buffer);
474 },
475 .linkpath => {
476 file.link_name = try attr.value(self.link_name_buffer);
477 },
478 .size => {
479 var buf: [pax_max_size_attr_len]u8 = undefined;
480 file.size = try std.fmt.parseInt(u64, try attr.value(&buf), 10);
481 },
482 }
483 }
484 },
485 // Ignored header type
486 .global_extended_header => {
487 self.reader.skipBytes(size, .{}) catch return error.TarHeadersTooBig;
488 },
489 // All other are unsupported header types
490 else => {
491 const d = self.diagnostics orelse return error.TarUnsupportedHeader;
492 try d.errors.append(d.allocator, .{ .unsupported_file_type = .{
493 .file_name = try d.allocator.dupe(u8, header.name()),
494 .file_type = kind,
495 } });
496 if (kind == .gnu_sparse) {
497 try self.skipGnuSparseExtendedHeaders(header);
380 /// Iterates through the tar archive as if it is a series of files.
381 /// Internally, the tar format often uses entries (header with optional
382 /// content) to add meta data that describes the next file. These
383 /// entries should not normally be visible to the outside. As such, this
384 /// loop iterates through one or more entries until it collects a all
385 /// file attributes.
386 pub fn next(self: *Iterator) !?File {
387 if (self.unread_file_bytes > 0) {
388 // If file content was not consumed by caller
389 try self.reader.discardAll64(self.unread_file_bytes);
390 self.unread_file_bytes = 0;
391 }
392 var file: File = self.newFile();
393
394 while (try self.readHeader()) |header| {
395 const kind = header.kind();
396 const size: u64 = try header.size();
397 self.padding = blockPadding(size);
398
399 switch (kind) {
400 // File types to return upstream
401 .directory, .normal, .symbolic_link => {
402 file.kind = switch (kind) {
403 .directory => .directory,
404 .normal => .file,
405 .symbolic_link => .sym_link,
406 else => unreachable,
407 };
408 file.mode = try header.mode();
409
410 // set file attributes if not already set by prefix/extended headers
411 if (file.size == 0) {
412 file.size = size;
413 }
414 if (file.link_name.len == 0) {
415 file.link_name = try header.linkName(self.link_name_buffer);
416 }
417 if (file.name.len == 0) {
418 file.name = try header.fullName(self.file_name_buffer);
419 }
420
421 self.padding = blockPadding(file.size);
422 self.unread_file_bytes = file.size;
423 return file;
424 },
425 // Prefix header types
426 .gnu_long_name => {
427 file.name = try self.readString(@intCast(size), self.file_name_buffer);
428 },
429 .gnu_long_link => {
430 file.link_name = try self.readString(@intCast(size), self.link_name_buffer);
431 },
432 .extended_header => {
433 // Use just attributes from last extended header.
434 file = self.newFile();
435
436 var rdr: PaxIterator = .{
437 .reader = self.reader,
438 .size = @intCast(size),
439 };
440 while (try rdr.next()) |attr| {
441 switch (attr.kind) {
442 .path => {
443 file.name = try attr.value(self.file_name_buffer);
444 },
445 .linkpath => {
446 file.link_name = try attr.value(self.link_name_buffer);
447 },
448 .size => {
449 var buf: [pax_max_size_attr_len]u8 = undefined;
450 file.size = try std.fmt.parseInt(u64, try attr.value(&buf), 10);
451 },
498452 }
499 self.reader.skipBytes(size, .{}) catch return error.TarHeadersTooBig;
500 },
501 }
453 }
454 },
455 // Ignored header type
456 .global_extended_header => {
457 self.reader.discardAll64(size) catch return error.TarHeadersTooBig;
458 },
459 // All other are unsupported header types
460 else => {
461 const d = self.diagnostics orelse return error.TarUnsupportedHeader;
462 try d.errors.append(d.allocator, .{ .unsupported_file_type = .{
463 .file_name = try d.allocator.dupe(u8, header.name()),
464 .file_type = kind,
465 } });
466 if (kind == .gnu_sparse) {
467 try self.skipGnuSparseExtendedHeaders(header);
468 }
469 self.reader.discardAll64(size) catch return error.TarHeadersTooBig;
470 },
502471 }
503 return null;
504472 }
473 return null;
474 }
505475
506 fn skipGnuSparseExtendedHeaders(self: *Self, header: Header) !void {
507 var is_extended = header.bytes[482] > 0;
508 while (is_extended) {
509 var buf: [Header.SIZE]u8 = undefined;
510 const n = try self.reader.readAll(&buf);
511 if (n < Header.SIZE) return error.UnexpectedEndOfStream;
512 is_extended = buf[504] > 0;
513 }
514 }
515 };
516}
476 pub fn streamRemaining(it: *Iterator, file: File, w: *std.Io.Writer) std.Io.Reader.StreamError!void {
477 try it.reader.streamExact64(w, file.size);
478 it.unread_file_bytes = 0;
479 }
517480
518/// Pax attributes iterator.
519/// Size is length of pax extended header in reader.
520fn paxIterator(reader: anytype, size: usize) PaxIterator(@TypeOf(reader)) {
521 return PaxIterator(@TypeOf(reader)){
522 .reader = reader,
523 .size = size,
524 };
525}
481 fn skipGnuSparseExtendedHeaders(self: *Iterator, header: Header) !void {
482 var is_extended = header.bytes[482] > 0;
483 while (is_extended) {
484 var buf: [Header.SIZE]u8 = undefined;
485 try self.reader.readSliceAll(&buf);
486 is_extended = buf[504] > 0;
487 }
488 }
489};
526490
527491const PaxAttributeKind = enum {
528492 path,
......@@ -533,108 +497,99 @@ const PaxAttributeKind = enum {
533497// maxInt(u64) has 20 chars, base 10 in practice we got 24 chars
534498const pax_max_size_attr_len = 64;
535499
536fn PaxIterator(comptime ReaderType: type) type {
537 return struct {
538 size: usize, // cumulative size of all pax attributes
539 reader: ReaderType,
540 // scratch buffer used for reading attribute length and keyword
541 scratch: [128]u8 = undefined,
542
543 const Self = @This();
544
545 const Attribute = struct {
546 kind: PaxAttributeKind,
547 len: usize, // length of the attribute value
548 reader: ReaderType, // reader positioned at value start
549
550 // Copies pax attribute value into destination buffer.
551 // Must be called with destination buffer of size at least Attribute.len.
552 pub fn value(self: Attribute, dst: []u8) ![]const u8 {
553 if (self.len > dst.len) return error.TarInsufficientBuffer;
554 // assert(self.len <= dst.len);
555 const buf = dst[0..self.len];
556 const n = try self.reader.readAll(buf);
557 if (n < self.len) return error.UnexpectedEndOfStream;
558 try validateAttributeEnding(self.reader);
559 if (hasNull(buf)) return error.PaxNullInValue;
560 return buf;
561 }
562 };
500pub const PaxIterator = struct {
501 size: usize, // cumulative size of all pax attributes
502 reader: *std.Io.Reader,
563503
564 // Iterates over pax attributes. Returns known only known attributes.
565 // Caller has to call value in Attribute, to advance reader across value.
566 pub fn next(self: *Self) !?Attribute {
567 // Pax extended header consists of one or more attributes, each constructed as follows:
568 // "%d %s=%s\n", <length>, <keyword>, <value>
569 while (self.size > 0) {
570 const length_buf = try self.readUntil(' ');
571 const length = try std.fmt.parseInt(usize, length_buf, 10); // record length in bytes
572
573 const keyword = try self.readUntil('=');
574 if (hasNull(keyword)) return error.PaxNullInKeyword;
575
576 // calculate value_len
577 const value_start = length_buf.len + keyword.len + 2; // 2 separators
578 if (length < value_start + 1 or self.size < length) return error.UnexpectedEndOfStream;
579 const value_len = length - value_start - 1; // \n separator at end
580 self.size -= length;
581
582 const kind: PaxAttributeKind = if (eql(keyword, "path"))
583 .path
584 else if (eql(keyword, "linkpath"))
585 .linkpath
586 else if (eql(keyword, "size"))
587 .size
588 else {
589 try self.reader.skipBytes(value_len, .{});
590 try validateAttributeEnding(self.reader);
591 continue;
592 };
593 if (kind == .size and value_len > pax_max_size_attr_len) {
594 return error.PaxSizeAttrOverflow;
595 }
596 return Attribute{
597 .kind = kind,
598 .len = value_len,
599 .reader = self.reader,
600 };
601 }
504 const Self = @This();
602505
603 return null;
506 const Attribute = struct {
507 kind: PaxAttributeKind,
508 len: usize, // length of the attribute value
509 reader: *std.Io.Reader, // reader positioned at value start
510
511 // Copies pax attribute value into destination buffer.
512 // Must be called with destination buffer of size at least Attribute.len.
513 pub fn value(self: Attribute, dst: []u8) ![]const u8 {
514 if (self.len > dst.len) return error.TarInsufficientBuffer;
515 // assert(self.len <= dst.len);
516 const buf = dst[0..self.len];
517 const n = try self.reader.readSliceShort(buf);
518 if (n < self.len) return error.UnexpectedEndOfStream;
519 try validateAttributeEnding(self.reader);
520 if (hasNull(buf)) return error.PaxNullInValue;
521 return buf;
604522 }
523 };
605524
606 fn readUntil(self: *Self, delimiter: u8) ![]const u8 {
607 var fbs = std.io.fixedBufferStream(&self.scratch);
608 try self.reader.streamUntilDelimiter(fbs.writer(), delimiter, null);
609 return fbs.getWritten();
525 // Iterates over pax attributes. Returns known only known attributes.
526 // Caller has to call value in Attribute, to advance reader across value.
527 pub fn next(self: *Self) !?Attribute {
528 // Pax extended header consists of one or more attributes, each constructed as follows:
529 // "%d %s=%s\n", <length>, <keyword>, <value>
530 while (self.size > 0) {
531 const length_buf = try self.reader.takeSentinel(' ');
532 const length = try std.fmt.parseInt(usize, length_buf, 10); // record length in bytes
533
534 const keyword = try self.reader.takeSentinel('=');
535 if (hasNull(keyword)) return error.PaxNullInKeyword;
536
537 // calculate value_len
538 const value_start = length_buf.len + keyword.len + 2; // 2 separators
539 if (length < value_start + 1 or self.size < length) return error.UnexpectedEndOfStream;
540 const value_len = length - value_start - 1; // \n separator at end
541 self.size -= length;
542
543 const kind: PaxAttributeKind = if (eql(keyword, "path"))
544 .path
545 else if (eql(keyword, "linkpath"))
546 .linkpath
547 else if (eql(keyword, "size"))
548 .size
549 else {
550 try self.reader.discardAll(value_len);
551 try validateAttributeEnding(self.reader);
552 continue;
553 };
554 if (kind == .size and value_len > pax_max_size_attr_len) {
555 return error.PaxSizeAttrOverflow;
556 }
557 return .{
558 .kind = kind,
559 .len = value_len,
560 .reader = self.reader,
561 };
610562 }
611563
612 fn eql(a: []const u8, b: []const u8) bool {
613 return std.mem.eql(u8, a, b);
614 }
564 return null;
565 }
615566
616 fn hasNull(str: []const u8) bool {
617 return (std.mem.indexOfScalar(u8, str, 0)) != null;
618 }
567 fn eql(a: []const u8, b: []const u8) bool {
568 return std.mem.eql(u8, a, b);
569 }
619570
620 // Checks that each record ends with new line.
621 fn validateAttributeEnding(reader: ReaderType) !void {
622 if (try reader.readByte() != '\n') return error.PaxInvalidAttributeEnd;
623 }
624 };
625}
571 fn hasNull(str: []const u8) bool {
572 return (std.mem.indexOfScalar(u8, str, 0)) != null;
573 }
574
575 // Checks that each record ends with new line.
576 fn validateAttributeEnding(reader: *std.Io.Reader) !void {
577 if (try reader.takeByte() != '\n') return error.PaxInvalidAttributeEnd;
578 }
579};
626580
627581/// Saves tar file content to the file systems.
628pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: PipeOptions) !void {
582pub fn pipeToFileSystem(dir: std.fs.Dir, reader: *std.Io.Reader, options: PipeOptions) !void {
629583 var file_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
630584 var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
631 var iter = iterator(reader, .{
585 var file_contents_buffer: [1024]u8 = undefined;
586 var it: Iterator = .init(reader, .{
632587 .file_name_buffer = &file_name_buffer,
633588 .link_name_buffer = &link_name_buffer,
634589 .diagnostics = options.diagnostics,
635590 });
636591
637 while (try iter.next()) |file| {
592 while (try it.next()) |file| {
638593 const file_name = stripComponents(file.name, options.strip_components);
639594 if (file_name.len == 0 and file.kind != .directory) {
640595 const d = options.diagnostics orelse return error.TarComponentsOutsideStrippedPrefix;
......@@ -656,7 +611,9 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: PipeOptions)
656611 .file => {
657612 if (createDirAndFile(dir, file_name, fileMode(file.mode, options))) |fs_file| {
658613 defer fs_file.close();
659 try file.writeAll(fs_file);
614 var file_writer = fs_file.writer(&file_contents_buffer);
615 try it.streamRemaining(file, &file_writer.interface);
616 try file_writer.interface.flush();
660617 } else |err| {
661618 const d = options.diagnostics orelse return err;
662619 try d.errors.append(d.allocator, .{ .unable_to_create_file = .{
......@@ -826,11 +783,14 @@ test PaxIterator {
826783 var buffer: [1024]u8 = undefined;
827784
828785 outer: for (cases) |case| {
829 var stream = std.io.fixedBufferStream(case.data);
830 var iter = paxIterator(stream.reader(), case.data.len);
786 var reader: std.Io.Reader = .fixed(case.data);
787 var it: PaxIterator = .{
788 .size = case.data.len,
789 .reader = &reader,
790 };
831791
832792 var i: usize = 0;
833 while (iter.next() catch |err| {
793 while (it.next() catch |err| {
834794 if (case.err) |e| {
835795 try testing.expectEqual(e, err);
836796 continue;
......@@ -853,12 +813,6 @@ test PaxIterator {
853813 }
854814}
855815
856test {
857 _ = @import("tar/test.zig");
858 _ = @import("tar/writer.zig");
859 _ = Diagnostics;
860}
861
862816test "header parse size" {
863817 const cases = [_]struct {
864818 in: []const u8,
......@@ -941,7 +895,7 @@ test "create file and symlink" {
941895 file.close();
942896}
943897
944test iterator {
898test Iterator {
945899 // Example tar file is created from this tree structure:
946900 // $ tree example
947901 // example
......@@ -962,19 +916,19 @@ test iterator {
962916 // example/empty/
963917
964918 const data = @embedFile("tar/testdata/example.tar");
965 var fbs = std.io.fixedBufferStream(data);
919 var reader: std.Io.Reader = .fixed(data);
966920
967921 // User provided buffers to the iterator
968922 var file_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
969923 var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
970924 // Create iterator
971 var iter = iterator(fbs.reader(), .{
925 var it: Iterator = .init(&reader, .{
972926 .file_name_buffer = &file_name_buffer,
973927 .link_name_buffer = &link_name_buffer,
974928 });
975929 // Iterate over files in example.tar
976930 var file_no: usize = 0;
977 while (try iter.next()) |file| : (file_no += 1) {
931 while (try it.next()) |file| : (file_no += 1) {
978932 switch (file.kind) {
979933 .directory => {
980934 switch (file_no) {
......@@ -987,10 +941,10 @@ test iterator {
987941 },
988942 .file => {
989943 try testing.expectEqualStrings("example/a/file", file.name);
990 // Read file content
991944 var buf: [16]u8 = undefined;
992 const n = try file.reader().readAll(&buf);
993 try testing.expectEqualStrings("content\n", buf[0..n]);
945 var w: std.Io.Writer = .fixed(&buf);
946 try it.streamRemaining(file, &w);
947 try testing.expectEqualStrings("content\n", w.buffered());
994948 },
995949 .sym_link => {
996950 try testing.expectEqualStrings("example/b/symlink", file.name);
......@@ -1021,15 +975,14 @@ test pipeToFileSystem {
1021975 // example/empty/
1022976
1023977 const data = @embedFile("tar/testdata/example.tar");
1024 var fbs = std.io.fixedBufferStream(data);
1025 const reader = fbs.reader();
978 var reader: std.Io.Reader = .fixed(data);
1026979
1027980 var tmp = testing.tmpDir(.{ .no_follow = true });
1028981 defer tmp.cleanup();
1029982 const dir = tmp.dir;
1030983
1031 // Save tar from `reader` to the file system `dir`
1032 pipeToFileSystem(dir, reader, .{
984 // Save tar from reader to the file system `dir`
985 pipeToFileSystem(dir, &reader, .{
1033986 .mode_mode = .ignore,
1034987 .strip_components = 1,
1035988 .exclude_empty_directories = true,
......@@ -1053,8 +1006,7 @@ test pipeToFileSystem {
10531006
10541007test "pipeToFileSystem root_dir" {
10551008 const data = @embedFile("tar/testdata/example.tar");
1056 var fbs = std.io.fixedBufferStream(data);
1057 const reader = fbs.reader();
1009 var reader: std.Io.Reader = .fixed(data);
10581010
10591011 // with strip_components = 1
10601012 {
......@@ -1063,7 +1015,7 @@ test "pipeToFileSystem root_dir" {
10631015 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
10641016 defer diagnostics.deinit();
10651017
1066 pipeToFileSystem(tmp.dir, reader, .{
1018 pipeToFileSystem(tmp.dir, &reader, .{
10671019 .strip_components = 1,
10681020 .diagnostics = &diagnostics,
10691021 }) catch |err| {
......@@ -1079,13 +1031,13 @@ test "pipeToFileSystem root_dir" {
10791031
10801032 // with strip_components = 0
10811033 {
1082 fbs.reset();
1034 reader = .fixed(data);
10831035 var tmp = testing.tmpDir(.{ .no_follow = true });
10841036 defer tmp.cleanup();
10851037 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
10861038 defer diagnostics.deinit();
10871039
1088 pipeToFileSystem(tmp.dir, reader, .{
1040 pipeToFileSystem(tmp.dir, &reader, .{
10891041 .strip_components = 0,
10901042 .diagnostics = &diagnostics,
10911043 }) catch |err| {
......@@ -1102,45 +1054,42 @@ test "pipeToFileSystem root_dir" {
11021054
11031055test "findRoot with single file archive" {
11041056 const data = @embedFile("tar/testdata/22752.tar");
1105 var fbs = std.io.fixedBufferStream(data);
1106 const reader = fbs.reader();
1057 var reader: std.Io.Reader = .fixed(data);
11071058
11081059 var tmp = testing.tmpDir(.{});
11091060 defer tmp.cleanup();
11101061
11111062 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
11121063 defer diagnostics.deinit();
1113 try pipeToFileSystem(tmp.dir, reader, .{ .diagnostics = &diagnostics });
1064 try pipeToFileSystem(tmp.dir, &reader, .{ .diagnostics = &diagnostics });
11141065
11151066 try testing.expectEqualStrings("", diagnostics.root_dir);
11161067}
11171068
11181069test "findRoot without explicit root dir" {
11191070 const data = @embedFile("tar/testdata/19820.tar");
1120 var fbs = std.io.fixedBufferStream(data);
1121 const reader = fbs.reader();
1071 var reader: std.Io.Reader = .fixed(data);
11221072
11231073 var tmp = testing.tmpDir(.{});
11241074 defer tmp.cleanup();
11251075
11261076 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
11271077 defer diagnostics.deinit();
1128 try pipeToFileSystem(tmp.dir, reader, .{ .diagnostics = &diagnostics });
1078 try pipeToFileSystem(tmp.dir, &reader, .{ .diagnostics = &diagnostics });
11291079
11301080 try testing.expectEqualStrings("root", diagnostics.root_dir);
11311081}
11321082
11331083test "pipeToFileSystem strip_components" {
11341084 const data = @embedFile("tar/testdata/example.tar");
1135 var fbs = std.io.fixedBufferStream(data);
1136 const reader = fbs.reader();
1085 var reader: std.Io.Reader = .fixed(data);
11371086
11381087 var tmp = testing.tmpDir(.{ .no_follow = true });
11391088 defer tmp.cleanup();
11401089 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
11411090 defer diagnostics.deinit();
11421091
1143 pipeToFileSystem(tmp.dir, reader, .{
1092 pipeToFileSystem(tmp.dir, &reader, .{
11441093 .strip_components = 3,
11451094 .diagnostics = &diagnostics,
11461095 }) catch |err| {
......@@ -1194,13 +1143,12 @@ test "executable bit" {
11941143 const data = @embedFile("tar/testdata/example.tar");
11951144
11961145 for ([_]PipeOptions.ModeMode{ .ignore, .executable_bit_only }) |opt| {
1197 var fbs = std.io.fixedBufferStream(data);
1198 const reader = fbs.reader();
1146 var reader: std.Io.Reader = .fixed(data);
11991147
12001148 var tmp = testing.tmpDir(.{ .no_follow = true });
12011149 //defer tmp.cleanup();
12021150
1203 pipeToFileSystem(tmp.dir, reader, .{
1151 pipeToFileSystem(tmp.dir, &reader, .{
12041152 .strip_components = 1,
12051153 .exclude_empty_directories = true,
12061154 .mode_mode = opt,
......@@ -1226,3 +1174,9 @@ test "executable bit" {
12261174 }
12271175 }
12281176}
1177
1178test {
1179 _ = @import("tar/test.zig");
1180 _ = Writer;
1181 _ = Diagnostics;
1182}
lib/std/tar/Writer.zig created+462
......@@ -0,0 +1,462 @@
1const std = @import("std");
2const assert = std.debug.assert;
3const testing = std.testing;
4const Writer = @This();
5
6const block_size = @sizeOf(Header);
7
8/// Options for writing file/dir/link. If left empty 0o664 is used for
9/// file mode and current time for mtime.
10pub const Options = struct {
11 /// File system permission mode.
12 mode: u32 = 0,
13 /// File system modification time.
14 mtime: u64 = 0,
15};
16
17underlying_writer: *std.Io.Writer,
18prefix: []const u8 = "",
19mtime_now: u64 = 0,
20
21const Error = error{
22 WriteFailed,
23 OctalOverflow,
24 NameTooLong,
25};
26
27/// Sets prefix for all other write* method paths.
28pub fn setRoot(w: *Writer, root: []const u8) Error!void {
29 if (root.len > 0)
30 try w.writeDir(root, .{});
31
32 w.prefix = root;
33}
34
35pub fn writeDir(w: *Writer, sub_path: []const u8, options: Options) Error!void {
36 try w.writeHeader(.directory, sub_path, "", 0, options);
37}
38
39pub const WriteFileError = std.Io.Writer.FileError || Error || std.fs.File.GetEndPosError;
40
41pub fn writeFile(
42 w: *Writer,
43 sub_path: []const u8,
44 file_reader: *std.fs.File.Reader,
45 stat_mtime: i128,
46) WriteFileError!void {
47 const size = try file_reader.getSize();
48 const mtime: u64 = @intCast(@divFloor(stat_mtime, std.time.ns_per_s));
49
50 var header: Header = .{};
51 try w.setPath(&header, sub_path);
52 try header.setSize(size);
53 try header.setMtime(mtime);
54 try header.updateChecksum();
55
56 try w.underlying_writer.writeAll(@ptrCast((&header)[0..1]));
57 _ = try w.underlying_writer.sendFileAll(file_reader, .unlimited);
58 try w.writePadding64(size);
59}
60
61pub const WriteFileStreamError = Error || std.Io.Reader.StreamError;
62
63/// Writes file reading file content from `reader`. Reads exactly `size` bytes
64/// from `reader`, or returns `error.EndOfStream`.
65pub fn writeFileStream(
66 w: *Writer,
67 sub_path: []const u8,
68 size: u64,
69 reader: *std.Io.Reader,
70 options: Options,
71) WriteFileStreamError!void {
72 try w.writeHeader(.regular, sub_path, "", size, options);
73 try reader.streamExact64(w.underlying_writer, size);
74 try w.writePadding64(size);
75}
76
77/// Writes file using bytes buffer `content` for size and file content.
78pub fn writeFileBytes(w: *Writer, sub_path: []const u8, content: []const u8, options: Options) Error!void {
79 try w.writeHeader(.regular, sub_path, "", content.len, options);
80 try w.underlying_writer.writeAll(content);
81 try w.writePadding(content.len);
82}
83
84pub fn writeLink(w: *Writer, sub_path: []const u8, link_name: []const u8, options: Options) Error!void {
85 try w.writeHeader(.symbolic_link, sub_path, link_name, 0, options);
86}
87
88fn writeHeader(
89 w: *Writer,
90 typeflag: Header.FileType,
91 sub_path: []const u8,
92 link_name: []const u8,
93 size: u64,
94 options: Options,
95) Error!void {
96 var header = Header.init(typeflag);
97 try w.setPath(&header, sub_path);
98 try header.setSize(size);
99 try header.setMtime(options.mtime);
100 if (options.mode != 0)
101 try header.setMode(options.mode);
102 if (typeflag == .symbolic_link)
103 header.setLinkname(link_name) catch |err| switch (err) {
104 error.NameTooLong => try w.writeExtendedHeader(.gnu_long_link, &.{link_name}),
105 else => return err,
106 };
107 try header.write(w.underlying_writer);
108}
109
110/// Writes path in posix header, if don't fit (in name+prefix; 100+155
111/// bytes) writes it in gnu extended header.
112fn setPath(w: *Writer, header: *Header, sub_path: []const u8) Error!void {
113 header.setPath(w.prefix, sub_path) catch |err| switch (err) {
114 error.NameTooLong => {
115 // write extended header
116 const buffers: []const []const u8 = if (w.prefix.len == 0)
117 &.{sub_path}
118 else
119 &.{ w.prefix, "/", sub_path };
120 try w.writeExtendedHeader(.gnu_long_name, buffers);
121 },
122 else => return err,
123 };
124}
125
126/// Writes gnu extended header: gnu_long_name or gnu_long_link.
127fn writeExtendedHeader(w: *Writer, typeflag: Header.FileType, buffers: []const []const u8) Error!void {
128 var len: usize = 0;
129 for (buffers) |buf| len += buf.len;
130
131 var header: Header = .init(typeflag);
132 try header.setSize(len);
133 try header.write(w.underlying_writer);
134 for (buffers) |buf|
135 try w.underlying_writer.writeAll(buf);
136 try w.writePadding(len);
137}
138
139fn writePadding(w: *Writer, bytes: usize) std.Io.Writer.Error!void {
140 return writePaddingPos(w, bytes % block_size);
141}
142
143fn writePadding64(w: *Writer, bytes: u64) std.Io.Writer.Error!void {
144 return writePaddingPos(w, @intCast(bytes % block_size));
145}
146
147fn writePaddingPos(w: *Writer, pos: usize) std.Io.Writer.Error!void {
148 if (pos == 0) return;
149 try w.underlying_writer.splatByteAll(0, block_size - pos);
150}
151
152/// According to the specification, tar should finish with two zero blocks, but
153/// "reasonable system must not assume that such a block exists when reading an
154/// archive". Therefore, the Zig standard library recommends to not call this
155/// function.
156pub fn finishPedantically(w: *Writer) std.Io.Writer.Error!void {
157 try w.underlying_writer.splatByteAll(0, block_size * 2);
158}
159
160/// A struct that is exactly 512 bytes and matches tar file format. This is
161/// intended to be used for outputting tar files; for parsing there is
162/// `std.tar.Header`.
163pub const Header = extern struct {
164 // This struct was originally copied from
165 // https://github.com/mattnite/tar/blob/main/src/main.zig which is MIT
166 // licensed.
167 //
168 // The name, linkname, magic, uname, and gname are null-terminated character
169 // strings. All other fields are zero-filled octal numbers in ASCII. Each
170 // numeric field of width w contains w minus 1 digits, and a null.
171 // Reference: https://www.gnu.org/software/tar/manual/html_node/Standard.html
172 // POSIX header: byte offset
173 name: [100]u8 = [_]u8{0} ** 100, // 0
174 mode: [7:0]u8 = default_mode.file, // 100
175 uid: [7:0]u8 = [_:0]u8{0} ** 7, // unused 108
176 gid: [7:0]u8 = [_:0]u8{0} ** 7, // unused 116
177 size: [11:0]u8 = [_:0]u8{'0'} ** 11, // 124
178 mtime: [11:0]u8 = [_:0]u8{'0'} ** 11, // 136
179 checksum: [7:0]u8 = [_:0]u8{' '} ** 7, // 148
180 typeflag: FileType = .regular, // 156
181 linkname: [100]u8 = [_]u8{0} ** 100, // 157
182 magic: [6]u8 = [_]u8{ 'u', 's', 't', 'a', 'r', 0 }, // 257
183 version: [2]u8 = [_]u8{ '0', '0' }, // 263
184 uname: [32]u8 = [_]u8{0} ** 32, // unused 265
185 gname: [32]u8 = [_]u8{0} ** 32, // unused 297
186 devmajor: [7:0]u8 = [_:0]u8{0} ** 7, // unused 329
187 devminor: [7:0]u8 = [_:0]u8{0} ** 7, // unused 337
188 prefix: [155]u8 = [_]u8{0} ** 155, // 345
189 pad: [12]u8 = [_]u8{0} ** 12, // unused 500
190
191 pub const FileType = enum(u8) {
192 regular = '0',
193 symbolic_link = '2',
194 directory = '5',
195 gnu_long_name = 'L',
196 gnu_long_link = 'K',
197 };
198
199 const default_mode = struct {
200 const file = [_:0]u8{ '0', '0', '0', '0', '6', '6', '4' }; // 0o664
201 const dir = [_:0]u8{ '0', '0', '0', '0', '7', '7', '5' }; // 0o775
202 const sym_link = [_:0]u8{ '0', '0', '0', '0', '7', '7', '7' }; // 0o777
203 const other = [_:0]u8{ '0', '0', '0', '0', '0', '0', '0' }; // 0o000
204 };
205
206 pub fn init(typeflag: FileType) Header {
207 return .{
208 .typeflag = typeflag,
209 .mode = switch (typeflag) {
210 .directory => default_mode.dir,
211 .symbolic_link => default_mode.sym_link,
212 .regular => default_mode.file,
213 else => default_mode.other,
214 },
215 };
216 }
217
218 pub fn setSize(w: *Header, size: u64) error{OctalOverflow}!void {
219 try octal(&w.size, size);
220 }
221
222 fn octal(buf: []u8, value: u64) error{OctalOverflow}!void {
223 var remainder: u64 = value;
224 var pos: usize = buf.len;
225 while (remainder > 0 and pos > 0) {
226 pos -= 1;
227 const c: u8 = @as(u8, @intCast(remainder % 8)) + '0';
228 buf[pos] = c;
229 remainder /= 8;
230 if (pos == 0 and remainder > 0) return error.OctalOverflow;
231 }
232 }
233
234 pub fn setMode(w: *Header, mode: u32) error{OctalOverflow}!void {
235 try octal(&w.mode, mode);
236 }
237
238 // Integer number of seconds since January 1, 1970, 00:00 Coordinated Universal Time.
239 // mtime == 0 will use current time
240 pub fn setMtime(w: *Header, mtime: u64) error{OctalOverflow}!void {
241 try octal(&w.mtime, mtime);
242 }
243
244 pub fn updateChecksum(w: *Header) !void {
245 var checksum: usize = ' '; // other 7 w.checksum bytes are initialized to ' '
246 for (std.mem.asBytes(w)) |val|
247 checksum += val;
248 try octal(&w.checksum, checksum);
249 }
250
251 pub fn write(h: *Header, bw: *std.Io.Writer) error{ OctalOverflow, WriteFailed }!void {
252 try h.updateChecksum();
253 try bw.writeAll(std.mem.asBytes(h));
254 }
255
256 pub fn setLinkname(w: *Header, link: []const u8) !void {
257 if (link.len > w.linkname.len) return error.NameTooLong;
258 @memcpy(w.linkname[0..link.len], link);
259 }
260
261 pub fn setPath(w: *Header, prefix: []const u8, sub_path: []const u8) !void {
262 const max_prefix = w.prefix.len;
263 const max_name = w.name.len;
264 const sep = std.fs.path.sep_posix;
265
266 if (prefix.len + sub_path.len > max_name + max_prefix or prefix.len > max_prefix)
267 return error.NameTooLong;
268
269 // both fit into name
270 if (prefix.len > 0 and prefix.len + sub_path.len < max_name) {
271 @memcpy(w.name[0..prefix.len], prefix);
272 w.name[prefix.len] = sep;
273 @memcpy(w.name[prefix.len + 1 ..][0..sub_path.len], sub_path);
274 return;
275 }
276
277 // sub_path fits into name
278 // there is no prefix or prefix fits into prefix
279 if (sub_path.len <= max_name) {
280 @memcpy(w.name[0..sub_path.len], sub_path);
281 @memcpy(w.prefix[0..prefix.len], prefix);
282 return;
283 }
284
285 if (prefix.len > 0) {
286 @memcpy(w.prefix[0..prefix.len], prefix);
287 w.prefix[prefix.len] = sep;
288 }
289 const prefix_pos = if (prefix.len > 0) prefix.len + 1 else 0;
290
291 // add as much to prefix as you can, must split at /
292 const prefix_remaining = max_prefix - prefix_pos;
293 if (std.mem.lastIndexOf(u8, sub_path[0..@min(prefix_remaining, sub_path.len)], &.{'/'})) |sep_pos| {
294 @memcpy(w.prefix[prefix_pos..][0..sep_pos], sub_path[0..sep_pos]);
295 if ((sub_path.len - sep_pos - 1) > max_name) return error.NameTooLong;
296 @memcpy(w.name[0..][0 .. sub_path.len - sep_pos - 1], sub_path[sep_pos + 1 ..]);
297 return;
298 }
299
300 return error.NameTooLong;
301 }
302
303 comptime {
304 assert(@sizeOf(Header) == 512);
305 }
306
307 test "setPath" {
308 const cases = [_]struct {
309 in: []const []const u8,
310 out: []const []const u8,
311 }{
312 .{
313 .in = &.{ "", "123456789" },
314 .out = &.{ "", "123456789" },
315 },
316 // can fit into name
317 .{
318 .in = &.{ "prefix", "sub_path" },
319 .out = &.{ "", "prefix/sub_path" },
320 },
321 // no more both fits into name
322 .{
323 .in = &.{ "prefix", "0123456789/" ** 8 ++ "basename" },
324 .out = &.{ "prefix", "0123456789/" ** 8 ++ "basename" },
325 },
326 // put as much as you can into prefix the rest goes into name
327 .{
328 .in = &.{ "prefix", "0123456789/" ** 10 ++ "basename" },
329 .out = &.{ "prefix/" ++ "0123456789/" ** 9 ++ "0123456789", "basename" },
330 },
331
332 .{
333 .in = &.{ "prefix", "0123456789/" ** 15 ++ "basename" },
334 .out = &.{ "prefix/" ++ "0123456789/" ** 12 ++ "0123456789", "0123456789/0123456789/basename" },
335 },
336 .{
337 .in = &.{ "prefix", "0123456789/" ** 21 ++ "basename" },
338 .out = &.{ "prefix/" ++ "0123456789/" ** 12 ++ "0123456789", "0123456789/" ** 8 ++ "basename" },
339 },
340 .{
341 .in = &.{ "", "012345678/" ** 10 ++ "foo" },
342 .out = &.{ "012345678/" ** 9 ++ "012345678", "foo" },
343 },
344 };
345
346 for (cases) |case| {
347 var header = Header.init(.regular);
348 try header.setPath(case.in[0], case.in[1]);
349 try testing.expectEqualStrings(case.out[0], std.mem.sliceTo(&header.prefix, 0));
350 try testing.expectEqualStrings(case.out[1], std.mem.sliceTo(&header.name, 0));
351 }
352
353 const error_cases = [_]struct {
354 in: []const []const u8,
355 }{
356 // basename can't fit into name (106 characters)
357 .{ .in = &.{ "zig", "test/cases/compile_errors/regression_test_2980_base_type_u32_is_not_type_checked_properly_when_assigning_a_value_within_a_struct.zig" } },
358 // cant fit into 255 + sep
359 .{ .in = &.{ "prefix", "0123456789/" ** 22 ++ "basename" } },
360 // can fit but sub_path can't be split (there is no separator)
361 .{ .in = &.{ "prefix", "0123456789" ** 10 ++ "a" } },
362 .{ .in = &.{ "prefix", "0123456789" ** 14 ++ "basename" } },
363 };
364
365 for (error_cases) |case| {
366 var header = Header.init(.regular);
367 try testing.expectError(
368 error.NameTooLong,
369 header.setPath(case.in[0], case.in[1]),
370 );
371 }
372 }
373};
374
375test {
376 _ = Header;
377}
378
379test "write files" {
380 const files = [_]struct {
381 path: []const u8,
382 content: []const u8,
383 }{
384 .{ .path = "foo", .content = "bar" },
385 .{ .path = "a12345678/" ** 10 ++ "foo", .content = "a" ** 511 },
386 .{ .path = "b12345678/" ** 24 ++ "foo", .content = "b" ** 512 },
387 .{ .path = "c12345678/" ** 25 ++ "foo", .content = "c" ** 513 },
388 .{ .path = "d12345678/" ** 51 ++ "foo", .content = "d" ** 1025 },
389 .{ .path = "e123456789" ** 11, .content = "e" },
390 };
391
392 var file_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
393 var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
394
395 // with root
396 {
397 const root = "root";
398
399 var output: std.Io.Writer.Allocating = .init(testing.allocator);
400 var w: Writer = .{ .underlying_writer = &output.writer };
401 defer output.deinit();
402 try w.setRoot(root);
403 for (files) |file|
404 try w.writeFileBytes(file.path, file.content, .{});
405
406 var input: std.Io.Reader = .fixed(output.getWritten());
407 var it: std.tar.Iterator = .init(&input, .{
408 .file_name_buffer = &file_name_buffer,
409 .link_name_buffer = &link_name_buffer,
410 });
411
412 // first entry is directory with prefix
413 {
414 const actual = (try it.next()).?;
415 try testing.expectEqualStrings(root, actual.name);
416 try testing.expectEqual(std.tar.FileKind.directory, actual.kind);
417 }
418
419 var i: usize = 0;
420 while (try it.next()) |actual| {
421 defer i += 1;
422 const expected = files[i];
423 try testing.expectEqualStrings(root, actual.name[0..root.len]);
424 try testing.expectEqual('/', actual.name[root.len..][0]);
425 try testing.expectEqualStrings(expected.path, actual.name[root.len + 1 ..]);
426
427 var content: std.Io.Writer.Allocating = .init(testing.allocator);
428 defer content.deinit();
429 try it.streamRemaining(actual, &content.writer);
430 try testing.expectEqualSlices(u8, expected.content, content.getWritten());
431 }
432 }
433 // without root
434 {
435 var output: std.Io.Writer.Allocating = .init(testing.allocator);
436 var w: Writer = .{ .underlying_writer = &output.writer };
437 defer output.deinit();
438 for (files) |file| {
439 var content: std.Io.Reader = .fixed(file.content);
440 try w.writeFileStream(file.path, file.content.len, &content, .{});
441 }
442
443 var input: std.Io.Reader = .fixed(output.getWritten());
444 var it: std.tar.Iterator = .init(&input, .{
445 .file_name_buffer = &file_name_buffer,
446 .link_name_buffer = &link_name_buffer,
447 });
448
449 var i: usize = 0;
450 while (try it.next()) |actual| {
451 defer i += 1;
452 const expected = files[i];
453 try testing.expectEqualStrings(expected.path, actual.name);
454
455 var content: std.Io.Writer.Allocating = .init(testing.allocator);
456 defer content.deinit();
457 try it.streamRemaining(actual, &content.writer);
458 try testing.expectEqualSlices(u8, expected.content, content.getWritten());
459 }
460 try w.finishPedantically();
461 }
462}
lib/std/tar/test.zig+173-177
......@@ -18,31 +18,72 @@ const Case = struct {
1818 err: ?anyerror = null, // parsing should fail with this error
1919};
2020
21const cases = [_]Case{
22 .{
23 .data = @embedFile("testdata/gnu.tar"),
24 .files = &[_]Case.File{
25 .{
26 .name = "small.txt",
27 .size = 5,
28 .mode = 0o640,
29 },
30 .{
31 .name = "small2.txt",
32 .size = 11,
33 .mode = 0o640,
34 },
21const gnu_case: Case = .{
22 .data = @embedFile("testdata/gnu.tar"),
23 .files = &[_]Case.File{
24 .{
25 .name = "small.txt",
26 .size = 5,
27 .mode = 0o640,
3528 },
36 .chksums = &[_][]const u8{
37 "e38b27eaccb4391bdec553a7f3ae6b2f",
38 "c65bd2e50a56a2138bf1716f2fd56fe9",
29 .{
30 .name = "small2.txt",
31 .size = 11,
32 .mode = 0o640,
33 },
34 },
35 .chksums = &[_][]const u8{
36 "e38b27eaccb4391bdec553a7f3ae6b2f",
37 "c65bd2e50a56a2138bf1716f2fd56fe9",
38 },
39};
40
41const gnu_multi_headers_case: Case = .{
42 .data = @embedFile("testdata/gnu-multi-hdrs.tar"),
43 .files = &[_]Case.File{
44 .{
45 .name = "GNU2/GNU2/long-path-name",
46 .link_name = "GNU4/GNU4/long-linkpath-name",
47 .kind = .sym_link,
3948 },
4049 },
41 .{
50};
51
52const trailing_slash_case: Case = .{
53 .data = @embedFile("testdata/trailing-slash.tar"),
54 .files = &[_]Case.File{
55 .{
56 .name = "123456789/" ** 30,
57 .kind = .directory,
58 },
59 },
60};
61
62const writer_big_long_case: Case = .{
63 // Size in gnu extended format, and name in pax attribute.
64 .data = @embedFile("testdata/writer-big-long.tar"),
65 .files = &[_]Case.File{
66 .{
67 .name = "longname/" ** 15 ++ "16gig.txt",
68 .size = 16 * 1024 * 1024 * 1024,
69 .mode = 0o644,
70 .truncated = true,
71 },
72 },
73};
74
75const fuzz1_case: Case = .{
76 .data = @embedFile("testdata/fuzz1.tar"),
77 .err = error.TarInsufficientBuffer,
78};
79
80test "run test cases" {
81 try testCase(gnu_case);
82 try testCase(.{
4283 .data = @embedFile("testdata/sparse-formats.tar"),
4384 .err = error.TarUnsupportedHeader,
44 },
45 .{
85 });
86 try testCase(.{
4687 .data = @embedFile("testdata/star.tar"),
4788 .files = &[_]Case.File{
4889 .{
......@@ -60,8 +101,8 @@ const cases = [_]Case{
60101 "e38b27eaccb4391bdec553a7f3ae6b2f",
61102 "c65bd2e50a56a2138bf1716f2fd56fe9",
62103 },
63 },
64 .{
104 });
105 try testCase(.{
65106 .data = @embedFile("testdata/v7.tar"),
66107 .files = &[_]Case.File{
67108 .{
......@@ -79,8 +120,8 @@ const cases = [_]Case{
79120 "e38b27eaccb4391bdec553a7f3ae6b2f",
80121 "c65bd2e50a56a2138bf1716f2fd56fe9",
81122 },
82 },
83 .{
123 });
124 try testCase(.{
84125 .data = @embedFile("testdata/pax.tar"),
85126 .files = &[_]Case.File{
86127 .{
......@@ -99,13 +140,13 @@ const cases = [_]Case{
99140 .chksums = &[_][]const u8{
100141 "3c382e8f5b6631aa2db52643912ffd4a",
101142 },
102 },
103 .{
143 });
144 try testCase(.{
104145 // pax attribute don't end with \n
105146 .data = @embedFile("testdata/pax-bad-hdr-file.tar"),
106147 .err = error.PaxInvalidAttributeEnd,
107 },
108 .{
148 });
149 try testCase(.{
109150 // size is in pax attribute
110151 .data = @embedFile("testdata/pax-pos-size-file.tar"),
111152 .files = &[_]Case.File{
......@@ -119,8 +160,8 @@ const cases = [_]Case{
119160 .chksums = &[_][]const u8{
120161 "0afb597b283fe61b5d4879669a350556",
121162 },
122 },
123 .{
163 });
164 try testCase(.{
124165 // has pax records which we are not interested in
125166 .data = @embedFile("testdata/pax-records.tar"),
126167 .files = &[_]Case.File{
......@@ -128,8 +169,8 @@ const cases = [_]Case{
128169 .name = "file",
129170 },
130171 },
131 },
132 .{
172 });
173 try testCase(.{
133174 // has global records which we are ignoring
134175 .data = @embedFile("testdata/pax-global-records.tar"),
135176 .files = &[_]Case.File{
......@@ -146,8 +187,8 @@ const cases = [_]Case{
146187 .name = "file4",
147188 },
148189 },
149 },
150 .{
190 });
191 try testCase(.{
151192 .data = @embedFile("testdata/nil-uid.tar"),
152193 .files = &[_]Case.File{
153194 .{
......@@ -160,8 +201,8 @@ const cases = [_]Case{
160201 .chksums = &[_][]const u8{
161202 "08d504674115e77a67244beac19668f5",
162203 },
163 },
164 .{
204 });
205 try testCase(.{
165206 // has xattrs and pax records which we are ignoring
166207 .data = @embedFile("testdata/xattrs.tar"),
167208 .files = &[_]Case.File{
......@@ -182,23 +223,14 @@ const cases = [_]Case{
182223 "e38b27eaccb4391bdec553a7f3ae6b2f",
183224 "c65bd2e50a56a2138bf1716f2fd56fe9",
184225 },
185 },
186 .{
187 .data = @embedFile("testdata/gnu-multi-hdrs.tar"),
188 .files = &[_]Case.File{
189 .{
190 .name = "GNU2/GNU2/long-path-name",
191 .link_name = "GNU4/GNU4/long-linkpath-name",
192 .kind = .sym_link,
193 },
194 },
195 },
196 .{
226 });
227 try testCase(gnu_multi_headers_case);
228 try testCase(.{
197229 // has gnu type D (directory) and S (sparse) blocks
198230 .data = @embedFile("testdata/gnu-incremental.tar"),
199231 .err = error.TarUnsupportedHeader,
200 },
201 .{
232 });
233 try testCase(.{
202234 // should use values only from last pax header
203235 .data = @embedFile("testdata/pax-multi-hdrs.tar"),
204236 .files = &[_]Case.File{
......@@ -208,8 +240,8 @@ const cases = [_]Case{
208240 .kind = .sym_link,
209241 },
210242 },
211 },
212 .{
243 });
244 try testCase(.{
213245 .data = @embedFile("testdata/gnu-long-nul.tar"),
214246 .files = &[_]Case.File{
215247 .{
......@@ -217,8 +249,8 @@ const cases = [_]Case{
217249 .mode = 0o644,
218250 },
219251 },
220 },
221 .{
252 });
253 try testCase(.{
222254 .data = @embedFile("testdata/gnu-utf8.tar"),
223255 .files = &[_]Case.File{
224256 .{
......@@ -226,8 +258,8 @@ const cases = [_]Case{
226258 .mode = 0o644,
227259 },
228260 },
229 },
230 .{
261 });
262 try testCase(.{
231263 .data = @embedFile("testdata/gnu-not-utf8.tar"),
232264 .files = &[_]Case.File{
233265 .{
......@@ -235,33 +267,33 @@ const cases = [_]Case{
235267 .mode = 0o644,
236268 },
237269 },
238 },
239 .{
270 });
271 try testCase(.{
240272 // null in pax key
241273 .data = @embedFile("testdata/pax-nul-xattrs.tar"),
242274 .err = error.PaxNullInKeyword,
243 },
244 .{
275 });
276 try testCase(.{
245277 .data = @embedFile("testdata/pax-nul-path.tar"),
246278 .err = error.PaxNullInValue,
247 },
248 .{
279 });
280 try testCase(.{
249281 .data = @embedFile("testdata/neg-size.tar"),
250282 .err = error.TarHeader,
251 },
252 .{
283 });
284 try testCase(.{
253285 .data = @embedFile("testdata/issue10968.tar"),
254286 .err = error.TarHeader,
255 },
256 .{
287 });
288 try testCase(.{
257289 .data = @embedFile("testdata/issue11169.tar"),
258290 .err = error.TarHeader,
259 },
260 .{
291 });
292 try testCase(.{
261293 .data = @embedFile("testdata/issue12435.tar"),
262294 .err = error.TarHeaderChksum,
263 },
264 .{
295 });
296 try testCase(.{
265297 // has magic with space at end instead of null
266298 .data = @embedFile("testdata/invalid-go17.tar"),
267299 .files = &[_]Case.File{
......@@ -269,8 +301,8 @@ const cases = [_]Case{
269301 .name = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/foo",
270302 },
271303 },
272 },
273 .{
304 });
305 try testCase(.{
274306 .data = @embedFile("testdata/ustar-file-devs.tar"),
275307 .files = &[_]Case.File{
276308 .{
......@@ -278,17 +310,9 @@ const cases = [_]Case{
278310 .mode = 0o644,
279311 },
280312 },
281 },
282 .{
283 .data = @embedFile("testdata/trailing-slash.tar"),
284 .files = &[_]Case.File{
285 .{
286 .name = "123456789/" ** 30,
287 .kind = .directory,
288 },
289 },
290 },
291 .{
313 });
314 try testCase(trailing_slash_case);
315 try testCase(.{
292316 // Has size in gnu extended format. To represent size bigger than 8 GB.
293317 .data = @embedFile("testdata/writer-big.tar"),
294318 .files = &[_]Case.File{
......@@ -299,120 +323,92 @@ const cases = [_]Case{
299323 .mode = 0o640,
300324 },
301325 },
302 },
303 .{
304 // Size in gnu extended format, and name in pax attribute.
305 .data = @embedFile("testdata/writer-big-long.tar"),
306 .files = &[_]Case.File{
307 .{
308 .name = "longname/" ** 15 ++ "16gig.txt",
309 .size = 16 * 1024 * 1024 * 1024,
310 .mode = 0o644,
311 .truncated = true,
312 },
313 },
314 },
315 .{
316 .data = @embedFile("testdata/fuzz1.tar"),
317 .err = error.TarInsufficientBuffer,
318 },
319 .{
326 });
327 try testCase(writer_big_long_case);
328 try testCase(fuzz1_case);
329 try testCase(.{
320330 .data = @embedFile("testdata/fuzz2.tar"),
321331 .err = error.PaxSizeAttrOverflow,
322 },
323};
324
325// used in test to calculate file chksum
326const Md5Writer = struct {
327 h: std.crypto.hash.Md5 = std.crypto.hash.Md5.init(.{}),
328
329 pub fn writeAll(self: *Md5Writer, buf: []const u8) !void {
330 self.h.update(buf);
331 }
332
333 pub fn writeByte(self: *Md5Writer, byte: u8) !void {
334 self.h.update(&[_]u8{byte});
335 }
336
337 pub fn chksum(self: *Md5Writer) [32]u8 {
338 var s = [_]u8{0} ** 16;
339 self.h.final(&s);
340 return std.fmt.bytesToHex(s, .lower);
341 }
342};
332 });
333}
343334
344test "run test cases" {
335fn testCase(case: Case) !void {
345336 var file_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
346337 var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
347338
348 for (cases) |case| {
349 var fsb = std.io.fixedBufferStream(case.data);
350 var iter = tar.iterator(fsb.reader(), .{
351 .file_name_buffer = &file_name_buffer,
352 .link_name_buffer = &link_name_buffer,
353 });
354 var i: usize = 0;
355 while (iter.next() catch |err| {
356 if (case.err) |e| {
357 try testing.expectEqual(e, err);
358 continue;
359 } else {
360 return err;
361 }
362 }) |actual| : (i += 1) {
363 const expected = case.files[i];
364 try testing.expectEqualStrings(expected.name, actual.name);
365 try testing.expectEqual(expected.size, actual.size);
366 try testing.expectEqual(expected.kind, actual.kind);
367 try testing.expectEqual(expected.mode, actual.mode);
368 try testing.expectEqualStrings(expected.link_name, actual.link_name);
339 var br: std.io.Reader = .fixed(case.data);
340 var it: tar.Iterator = .init(&br, .{
341 .file_name_buffer = &file_name_buffer,
342 .link_name_buffer = &link_name_buffer,
343 });
344 var i: usize = 0;
345 while (it.next() catch |err| {
346 if (case.err) |e| {
347 try testing.expectEqual(e, err);
348 return;
349 } else {
350 return err;
351 }
352 }) |actual| : (i += 1) {
353 const expected = case.files[i];
354 try testing.expectEqualStrings(expected.name, actual.name);
355 try testing.expectEqual(expected.size, actual.size);
356 try testing.expectEqual(expected.kind, actual.kind);
357 try testing.expectEqual(expected.mode, actual.mode);
358 try testing.expectEqualStrings(expected.link_name, actual.link_name);
369359
370 if (case.chksums.len > i) {
371 var md5writer = Md5Writer{};
372 try actual.writeAll(&md5writer);
373 const chksum = md5writer.chksum();
374 try testing.expectEqualStrings(case.chksums[i], &chksum);
375 } else {
376 if (expected.truncated) {
377 iter.unread_file_bytes = 0;
378 }
360 if (case.chksums.len > i) {
361 var aw: std.Io.Writer.Allocating = .init(std.testing.allocator);
362 defer aw.deinit();
363 try it.streamRemaining(actual, &aw.writer);
364 const chksum = std.fmt.bytesToHex(std.crypto.hash.Md5.hashResult(aw.getWritten()), .lower);
365 try testing.expectEqualStrings(case.chksums[i], &chksum);
366 } else {
367 if (expected.truncated) {
368 it.unread_file_bytes = 0;
379369 }
380370 }
381 try testing.expectEqual(case.files.len, i);
382371 }
372 try testing.expectEqual(case.files.len, i);
383373}
384374
385375test "pax/gnu long names with small buffer" {
376 try testLongNameCase(gnu_multi_headers_case);
377 try testLongNameCase(trailing_slash_case);
378 try testLongNameCase(.{
379 .data = @embedFile("testdata/fuzz1.tar"),
380 .err = error.TarInsufficientBuffer,
381 });
382}
383
384fn testLongNameCase(case: Case) !void {
386385 // should fail with insufficient buffer error
387386
388387 var min_file_name_buffer: [256]u8 = undefined;
389388 var min_link_name_buffer: [100]u8 = undefined;
390 const long_name_cases = [_]Case{ cases[11], cases[25], cases[28] };
391389
392 for (long_name_cases) |case| {
393 var fsb = std.io.fixedBufferStream(case.data);
394 var iter = tar.iterator(fsb.reader(), .{
395 .file_name_buffer = &min_file_name_buffer,
396 .link_name_buffer = &min_link_name_buffer,
397 });
390 var br: std.io.Reader = .fixed(case.data);
391 var iter: tar.Iterator = .init(&br, .{
392 .file_name_buffer = &min_file_name_buffer,
393 .link_name_buffer = &min_link_name_buffer,
394 });
398395
399 var iter_err: ?anyerror = null;
400 while (iter.next() catch |err| brk: {
401 iter_err = err;
402 break :brk null;
403 }) |_| {}
396 var iter_err: ?anyerror = null;
397 while (iter.next() catch |err| brk: {
398 iter_err = err;
399 break :brk null;
400 }) |_| {}
404401
405 try testing.expect(iter_err != null);
406 try testing.expectEqual(error.TarInsufficientBuffer, iter_err.?);
407 }
402 try testing.expect(iter_err != null);
403 try testing.expectEqual(error.TarInsufficientBuffer, iter_err.?);
408404}
409405
410406test "insufficient buffer in Header name filed" {
411407 var min_file_name_buffer: [9]u8 = undefined;
412408 var min_link_name_buffer: [100]u8 = undefined;
413409
414 var fsb = std.io.fixedBufferStream(cases[0].data);
415 var iter = tar.iterator(fsb.reader(), .{
410 var br: std.io.Reader = .fixed(gnu_case.data);
411 var iter: tar.Iterator = .init(&br, .{
416412 .file_name_buffer = &min_file_name_buffer,
417413 .link_name_buffer = &min_link_name_buffer,
418414 });
......@@ -466,21 +462,21 @@ test "should not overwrite existing file" {
466462 // This ensures that file is not overwritten.
467463 //
468464 const data = @embedFile("testdata/overwrite_file.tar");
469 var fsb = std.io.fixedBufferStream(data);
465 var r: std.io.Reader = .fixed(data);
470466
471467 // Unpack with strip_components = 1 should fail
472468 var root = std.testing.tmpDir(.{});
473469 defer root.cleanup();
474470 try testing.expectError(
475471 error.PathAlreadyExists,
476 tar.pipeToFileSystem(root.dir, fsb.reader(), .{ .mode_mode = .ignore, .strip_components = 1 }),
472 tar.pipeToFileSystem(root.dir, &r, .{ .mode_mode = .ignore, .strip_components = 1 }),
477473 );
478474
479475 // Unpack with strip_components = 0 should pass
480 fsb.reset();
476 r = .fixed(data);
481477 var root2 = std.testing.tmpDir(.{});
482478 defer root2.cleanup();
483 try tar.pipeToFileSystem(root2.dir, fsb.reader(), .{ .mode_mode = .ignore, .strip_components = 0 });
479 try tar.pipeToFileSystem(root2.dir, &r, .{ .mode_mode = .ignore, .strip_components = 0 });
484480}
485481
486482test "case sensitivity" {
......@@ -494,12 +490,12 @@ test "case sensitivity" {
494490 // 18089/alacritty/Darkermatrix.yml
495491 //
496492 const data = @embedFile("testdata/18089.tar");
497 var fsb = std.io.fixedBufferStream(data);
493 var r: std.io.Reader = .fixed(data);
498494
499495 var root = std.testing.tmpDir(.{});
500496 defer root.cleanup();
501497
502 tar.pipeToFileSystem(root.dir, fsb.reader(), .{ .mode_mode = .ignore, .strip_components = 1 }) catch |err| {
498 tar.pipeToFileSystem(root.dir, &r, .{ .mode_mode = .ignore, .strip_components = 1 }) catch |err| {
503499 // on case insensitive fs we fail on overwrite existing file
504500 try testing.expectEqual(error.PathAlreadyExists, err);
505501 return;
lib/std/tar/writer.zig deleted-497
......@@ -1,497 +0,0 @@
1const std = @import("std");
2const assert = std.debug.assert;
3const testing = std.testing;
4
5/// Creates tar Writer which will write tar content to the `underlying_writer`.
6/// Use setRoot to nest all following entries under single root. If file don't
7/// fit into posix header (name+prefix: 100+155 bytes) gnu extented header will
8/// be used for long names. Options enables setting file premission mode and
9/// mtime. Default is to use current time for mtime and 0o664 for file mode.
10pub fn writer(underlying_writer: anytype) Writer(@TypeOf(underlying_writer)) {
11 return .{ .underlying_writer = underlying_writer };
12}
13
14pub fn Writer(comptime WriterType: type) type {
15 return struct {
16 const block_size = @sizeOf(Header);
17 const empty_block: [block_size]u8 = [_]u8{0} ** block_size;
18
19 /// Options for writing file/dir/link. If left empty 0o664 is used for
20 /// file mode and current time for mtime.
21 pub const Options = struct {
22 /// File system permission mode.
23 mode: u32 = 0,
24 /// File system modification time.
25 mtime: u64 = 0,
26 };
27 const Self = @This();
28
29 underlying_writer: WriterType,
30 prefix: []const u8 = "",
31 mtime_now: u64 = 0,
32
33 /// Sets prefix for all other write* method paths.
34 pub fn setRoot(self: *Self, root: []const u8) !void {
35 if (root.len > 0)
36 try self.writeDir(root, .{});
37
38 self.prefix = root;
39 }
40
41 /// Writes directory.
42 pub fn writeDir(self: *Self, sub_path: []const u8, opt: Options) !void {
43 try self.writeHeader(.directory, sub_path, "", 0, opt);
44 }
45
46 /// Writes file system file.
47 pub fn writeFile(self: *Self, sub_path: []const u8, file: std.fs.File) !void {
48 const stat = try file.stat();
49 const mtime: u64 = @intCast(@divFloor(stat.mtime, std.time.ns_per_s));
50
51 var header = Header{};
52 try self.setPath(&header, sub_path);
53 try header.setSize(stat.size);
54 try header.setMtime(mtime);
55 try header.write(self.underlying_writer);
56
57 try self.underlying_writer.writeFile(file);
58 try self.writePadding(stat.size);
59 }
60
61 /// Writes file reading file content from `reader`. Number of bytes in
62 /// reader must be equal to `size`.
63 pub fn writeFileStream(self: *Self, sub_path: []const u8, size: usize, reader: anytype, opt: Options) !void {
64 try self.writeHeader(.regular, sub_path, "", @intCast(size), opt);
65
66 var counting_reader = std.io.countingReader(reader);
67 var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
68 try fifo.pump(counting_reader.reader(), self.underlying_writer);
69 if (counting_reader.bytes_read != size) return error.WrongReaderSize;
70 try self.writePadding(size);
71 }
72
73 /// Writes file using bytes buffer `content` for size and file content.
74 pub fn writeFileBytes(self: *Self, sub_path: []const u8, content: []const u8, opt: Options) !void {
75 try self.writeHeader(.regular, sub_path, "", @intCast(content.len), opt);
76 try self.underlying_writer.writeAll(content);
77 try self.writePadding(content.len);
78 }
79
80 /// Writes symlink.
81 pub fn writeLink(self: *Self, sub_path: []const u8, link_name: []const u8, opt: Options) !void {
82 try self.writeHeader(.symbolic_link, sub_path, link_name, 0, opt);
83 }
84
85 /// Writes fs.Dir.WalkerEntry. Uses `mtime` from file system entry and
86 /// default for entry mode .
87 pub fn writeEntry(self: *Self, entry: std.fs.Dir.Walker.Entry) !void {
88 switch (entry.kind) {
89 .directory => {
90 try self.writeDir(entry.path, .{ .mtime = try entryMtime(entry) });
91 },
92 .file => {
93 var file = try entry.dir.openFile(entry.basename, .{});
94 defer file.close();
95 try self.writeFile(entry.path, file);
96 },
97 .sym_link => {
98 var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
99 const link_name = try entry.dir.readLink(entry.basename, &link_name_buffer);
100 try self.writeLink(entry.path, link_name, .{ .mtime = try entryMtime(entry) });
101 },
102 else => {
103 return error.UnsupportedWalkerEntryKind;
104 },
105 }
106 }
107
108 fn writeHeader(
109 self: *Self,
110 typeflag: Header.FileType,
111 sub_path: []const u8,
112 link_name: []const u8,
113 size: u64,
114 opt: Options,
115 ) !void {
116 var header = Header.init(typeflag);
117 try self.setPath(&header, sub_path);
118 try header.setSize(size);
119 try header.setMtime(if (opt.mtime != 0) opt.mtime else self.mtimeNow());
120 if (opt.mode != 0)
121 try header.setMode(opt.mode);
122 if (typeflag == .symbolic_link)
123 header.setLinkname(link_name) catch |err| switch (err) {
124 error.NameTooLong => try self.writeExtendedHeader(.gnu_long_link, &.{link_name}),
125 else => return err,
126 };
127 try header.write(self.underlying_writer);
128 }
129
130 fn mtimeNow(self: *Self) u64 {
131 if (self.mtime_now == 0)
132 self.mtime_now = @intCast(std.time.timestamp());
133 return self.mtime_now;
134 }
135
136 fn entryMtime(entry: std.fs.Dir.Walker.Entry) !u64 {
137 const stat = try entry.dir.statFile(entry.basename);
138 return @intCast(@divFloor(stat.mtime, std.time.ns_per_s));
139 }
140
141 /// Writes path in posix header, if don't fit (in name+prefix; 100+155
142 /// bytes) writes it in gnu extended header.
143 fn setPath(self: *Self, header: *Header, sub_path: []const u8) !void {
144 header.setPath(self.prefix, sub_path) catch |err| switch (err) {
145 error.NameTooLong => {
146 // write extended header
147 const buffers: []const []const u8 = if (self.prefix.len == 0)
148 &.{sub_path}
149 else
150 &.{ self.prefix, "/", sub_path };
151 try self.writeExtendedHeader(.gnu_long_name, buffers);
152 },
153 else => return err,
154 };
155 }
156
157 /// Writes gnu extended header: gnu_long_name or gnu_long_link.
158 fn writeExtendedHeader(self: *Self, typeflag: Header.FileType, buffers: []const []const u8) !void {
159 var len: usize = 0;
160 for (buffers) |buf|
161 len += buf.len;
162
163 var header = Header.init(typeflag);
164 try header.setSize(len);
165 try header.write(self.underlying_writer);
166 for (buffers) |buf|
167 try self.underlying_writer.writeAll(buf);
168 try self.writePadding(len);
169 }
170
171 fn writePadding(self: *Self, bytes: u64) !void {
172 const pos: usize = @intCast(bytes % block_size);
173 if (pos == 0) return;
174 try self.underlying_writer.writeAll(empty_block[pos..]);
175 }
176
177 /// Tar should finish with two zero blocks, but 'reasonable system must
178 /// not assume that such a block exists when reading an archive' (from
179 /// reference). In practice it is safe to skip this finish.
180 pub fn finish(self: *Self) !void {
181 try self.underlying_writer.writeAll(&empty_block);
182 try self.underlying_writer.writeAll(&empty_block);
183 }
184 };
185}
186
187/// A struct that is exactly 512 bytes and matches tar file format. This is
188/// intended to be used for outputting tar files; for parsing there is
189/// `std.tar.Header`.
190const Header = extern struct {
191 // This struct was originally copied from
192 // https://github.com/mattnite/tar/blob/main/src/main.zig which is MIT
193 // licensed.
194 //
195 // The name, linkname, magic, uname, and gname are null-terminated character
196 // strings. All other fields are zero-filled octal numbers in ASCII. Each
197 // numeric field of width w contains w minus 1 digits, and a null.
198 // Reference: https://www.gnu.org/software/tar/manual/html_node/Standard.html
199 // POSIX header: byte offset
200 name: [100]u8 = [_]u8{0} ** 100, // 0
201 mode: [7:0]u8 = default_mode.file, // 100
202 uid: [7:0]u8 = [_:0]u8{0} ** 7, // unused 108
203 gid: [7:0]u8 = [_:0]u8{0} ** 7, // unused 116
204 size: [11:0]u8 = [_:0]u8{'0'} ** 11, // 124
205 mtime: [11:0]u8 = [_:0]u8{'0'} ** 11, // 136
206 checksum: [7:0]u8 = [_:0]u8{' '} ** 7, // 148
207 typeflag: FileType = .regular, // 156
208 linkname: [100]u8 = [_]u8{0} ** 100, // 157
209 magic: [6]u8 = [_]u8{ 'u', 's', 't', 'a', 'r', 0 }, // 257
210 version: [2]u8 = [_]u8{ '0', '0' }, // 263
211 uname: [32]u8 = [_]u8{0} ** 32, // unused 265
212 gname: [32]u8 = [_]u8{0} ** 32, // unused 297
213 devmajor: [7:0]u8 = [_:0]u8{0} ** 7, // unused 329
214 devminor: [7:0]u8 = [_:0]u8{0} ** 7, // unused 337
215 prefix: [155]u8 = [_]u8{0} ** 155, // 345
216 pad: [12]u8 = [_]u8{0} ** 12, // unused 500
217
218 pub const FileType = enum(u8) {
219 regular = '0',
220 symbolic_link = '2',
221 directory = '5',
222 gnu_long_name = 'L',
223 gnu_long_link = 'K',
224 };
225
226 const default_mode = struct {
227 const file = [_:0]u8{ '0', '0', '0', '0', '6', '6', '4' }; // 0o664
228 const dir = [_:0]u8{ '0', '0', '0', '0', '7', '7', '5' }; // 0o775
229 const sym_link = [_:0]u8{ '0', '0', '0', '0', '7', '7', '7' }; // 0o777
230 const other = [_:0]u8{ '0', '0', '0', '0', '0', '0', '0' }; // 0o000
231 };
232
233 pub fn init(typeflag: FileType) Header {
234 return .{
235 .typeflag = typeflag,
236 .mode = switch (typeflag) {
237 .directory => default_mode.dir,
238 .symbolic_link => default_mode.sym_link,
239 .regular => default_mode.file,
240 else => default_mode.other,
241 },
242 };
243 }
244
245 pub fn setSize(self: *Header, size: u64) !void {
246 try octal(&self.size, size);
247 }
248
249 fn octal(buf: []u8, value: u64) !void {
250 var remainder: u64 = value;
251 var pos: usize = buf.len;
252 while (remainder > 0 and pos > 0) {
253 pos -= 1;
254 const c: u8 = @as(u8, @intCast(remainder % 8)) + '0';
255 buf[pos] = c;
256 remainder /= 8;
257 if (pos == 0 and remainder > 0) return error.OctalOverflow;
258 }
259 }
260
261 pub fn setMode(self: *Header, mode: u32) !void {
262 try octal(&self.mode, mode);
263 }
264
265 // Integer number of seconds since January 1, 1970, 00:00 Coordinated Universal Time.
266 // mtime == 0 will use current time
267 pub fn setMtime(self: *Header, mtime: u64) !void {
268 try octal(&self.mtime, mtime);
269 }
270
271 pub fn updateChecksum(self: *Header) !void {
272 var checksum: usize = ' '; // other 7 self.checksum bytes are initialized to ' '
273 for (std.mem.asBytes(self)) |val|
274 checksum += val;
275 try octal(&self.checksum, checksum);
276 }
277
278 pub fn write(self: *Header, output_writer: anytype) !void {
279 try self.updateChecksum();
280 try output_writer.writeAll(std.mem.asBytes(self));
281 }
282
283 pub fn setLinkname(self: *Header, link: []const u8) !void {
284 if (link.len > self.linkname.len) return error.NameTooLong;
285 @memcpy(self.linkname[0..link.len], link);
286 }
287
288 pub fn setPath(self: *Header, prefix: []const u8, sub_path: []const u8) !void {
289 const max_prefix = self.prefix.len;
290 const max_name = self.name.len;
291 const sep = std.fs.path.sep_posix;
292
293 if (prefix.len + sub_path.len > max_name + max_prefix or prefix.len > max_prefix)
294 return error.NameTooLong;
295
296 // both fit into name
297 if (prefix.len > 0 and prefix.len + sub_path.len < max_name) {
298 @memcpy(self.name[0..prefix.len], prefix);
299 self.name[prefix.len] = sep;
300 @memcpy(self.name[prefix.len + 1 ..][0..sub_path.len], sub_path);
301 return;
302 }
303
304 // sub_path fits into name
305 // there is no prefix or prefix fits into prefix
306 if (sub_path.len <= max_name) {
307 @memcpy(self.name[0..sub_path.len], sub_path);
308 @memcpy(self.prefix[0..prefix.len], prefix);
309 return;
310 }
311
312 if (prefix.len > 0) {
313 @memcpy(self.prefix[0..prefix.len], prefix);
314 self.prefix[prefix.len] = sep;
315 }
316 const prefix_pos = if (prefix.len > 0) prefix.len + 1 else 0;
317
318 // add as much to prefix as you can, must split at /
319 const prefix_remaining = max_prefix - prefix_pos;
320 if (std.mem.lastIndexOf(u8, sub_path[0..@min(prefix_remaining, sub_path.len)], &.{'/'})) |sep_pos| {
321 @memcpy(self.prefix[prefix_pos..][0..sep_pos], sub_path[0..sep_pos]);
322 if ((sub_path.len - sep_pos - 1) > max_name) return error.NameTooLong;
323 @memcpy(self.name[0..][0 .. sub_path.len - sep_pos - 1], sub_path[sep_pos + 1 ..]);
324 return;
325 }
326
327 return error.NameTooLong;
328 }
329
330 comptime {
331 assert(@sizeOf(Header) == 512);
332 }
333
334 test setPath {
335 const cases = [_]struct {
336 in: []const []const u8,
337 out: []const []const u8,
338 }{
339 .{
340 .in = &.{ "", "123456789" },
341 .out = &.{ "", "123456789" },
342 },
343 // can fit into name
344 .{
345 .in = &.{ "prefix", "sub_path" },
346 .out = &.{ "", "prefix/sub_path" },
347 },
348 // no more both fits into name
349 .{
350 .in = &.{ "prefix", "0123456789/" ** 8 ++ "basename" },
351 .out = &.{ "prefix", "0123456789/" ** 8 ++ "basename" },
352 },
353 // put as much as you can into prefix the rest goes into name
354 .{
355 .in = &.{ "prefix", "0123456789/" ** 10 ++ "basename" },
356 .out = &.{ "prefix/" ++ "0123456789/" ** 9 ++ "0123456789", "basename" },
357 },
358
359 .{
360 .in = &.{ "prefix", "0123456789/" ** 15 ++ "basename" },
361 .out = &.{ "prefix/" ++ "0123456789/" ** 12 ++ "0123456789", "0123456789/0123456789/basename" },
362 },
363 .{
364 .in = &.{ "prefix", "0123456789/" ** 21 ++ "basename" },
365 .out = &.{ "prefix/" ++ "0123456789/" ** 12 ++ "0123456789", "0123456789/" ** 8 ++ "basename" },
366 },
367 .{
368 .in = &.{ "", "012345678/" ** 10 ++ "foo" },
369 .out = &.{ "012345678/" ** 9 ++ "012345678", "foo" },
370 },
371 };
372
373 for (cases) |case| {
374 var header = Header.init(.regular);
375 try header.setPath(case.in[0], case.in[1]);
376 try testing.expectEqualStrings(case.out[0], str(&header.prefix));
377 try testing.expectEqualStrings(case.out[1], str(&header.name));
378 }
379
380 const error_cases = [_]struct {
381 in: []const []const u8,
382 }{
383 // basename can't fit into name (106 characters)
384 .{ .in = &.{ "zig", "test/cases/compile_errors/regression_test_2980_base_type_u32_is_not_type_checked_properly_when_assigning_a_value_within_a_struct.zig" } },
385 // cant fit into 255 + sep
386 .{ .in = &.{ "prefix", "0123456789/" ** 22 ++ "basename" } },
387 // can fit but sub_path can't be split (there is no separator)
388 .{ .in = &.{ "prefix", "0123456789" ** 10 ++ "a" } },
389 .{ .in = &.{ "prefix", "0123456789" ** 14 ++ "basename" } },
390 };
391
392 for (error_cases) |case| {
393 var header = Header.init(.regular);
394 try testing.expectError(
395 error.NameTooLong,
396 header.setPath(case.in[0], case.in[1]),
397 );
398 }
399 }
400
401 // Breaks string on first null character.
402 fn str(s: []const u8) []const u8 {
403 for (s, 0..) |c, i| {
404 if (c == 0) return s[0..i];
405 }
406 return s;
407 }
408};
409
410test {
411 _ = Header;
412}
413
414test "write files" {
415 const files = [_]struct {
416 path: []const u8,
417 content: []const u8,
418 }{
419 .{ .path = "foo", .content = "bar" },
420 .{ .path = "a12345678/" ** 10 ++ "foo", .content = "a" ** 511 },
421 .{ .path = "b12345678/" ** 24 ++ "foo", .content = "b" ** 512 },
422 .{ .path = "c12345678/" ** 25 ++ "foo", .content = "c" ** 513 },
423 .{ .path = "d12345678/" ** 51 ++ "foo", .content = "d" ** 1025 },
424 .{ .path = "e123456789" ** 11, .content = "e" },
425 };
426
427 var file_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
428 var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
429
430 // with root
431 {
432 const root = "root";
433
434 var output = std.ArrayList(u8).init(testing.allocator);
435 defer output.deinit();
436 var wrt = writer(output.writer());
437 try wrt.setRoot(root);
438 for (files) |file|
439 try wrt.writeFileBytes(file.path, file.content, .{});
440
441 var input = std.io.fixedBufferStream(output.items);
442 var iter = std.tar.iterator(
443 input.reader(),
444 .{ .file_name_buffer = &file_name_buffer, .link_name_buffer = &link_name_buffer },
445 );
446
447 // first entry is directory with prefix
448 {
449 const actual = (try iter.next()).?;
450 try testing.expectEqualStrings(root, actual.name);
451 try testing.expectEqual(std.tar.FileKind.directory, actual.kind);
452 }
453
454 var i: usize = 0;
455 while (try iter.next()) |actual| {
456 defer i += 1;
457 const expected = files[i];
458 try testing.expectEqualStrings(root, actual.name[0..root.len]);
459 try testing.expectEqual('/', actual.name[root.len..][0]);
460 try testing.expectEqualStrings(expected.path, actual.name[root.len + 1 ..]);
461
462 var content = std.ArrayList(u8).init(testing.allocator);
463 defer content.deinit();
464 try actual.writeAll(content.writer());
465 try testing.expectEqualSlices(u8, expected.content, content.items);
466 }
467 }
468 // without root
469 {
470 var output = std.ArrayList(u8).init(testing.allocator);
471 defer output.deinit();
472 var wrt = writer(output.writer());
473 for (files) |file| {
474 var content = std.io.fixedBufferStream(file.content);
475 try wrt.writeFileStream(file.path, file.content.len, content.reader(), .{});
476 }
477
478 var input = std.io.fixedBufferStream(output.items);
479 var iter = std.tar.iterator(
480 input.reader(),
481 .{ .file_name_buffer = &file_name_buffer, .link_name_buffer = &link_name_buffer },
482 );
483
484 var i: usize = 0;
485 while (try iter.next()) |actual| {
486 defer i += 1;
487 const expected = files[i];
488 try testing.expectEqualStrings(expected.path, actual.name);
489
490 var content = std.ArrayList(u8).init(testing.allocator);
491 defer content.deinit();
492 try actual.writeAll(content.writer());
493 try testing.expectEqualSlices(u8, expected.content, content.items);
494 }
495 try wrt.finish();
496 }
497}
src/Compilation.zig+23-14
......@@ -4862,6 +4862,9 @@ fn docsCopyFallible(comp: *Compilation) anyerror!void {
48624862 };
48634863 defer tar_file.close();
48644864
4865 var buffer: [1024]u8 = undefined;
4866 var tar_file_writer = tar_file.writer(&buffer);
4867
48654868 var seen_table: std.AutoArrayHashMapUnmanaged(*Package.Module, []const u8) = .empty;
48664869 defer seen_table.deinit(comp.gpa);
48674870
......@@ -4871,7 +4874,7 @@ fn docsCopyFallible(comp: *Compilation) anyerror!void {
48714874 var i: usize = 0;
48724875 while (i < seen_table.count()) : (i += 1) {
48734876 const mod = seen_table.keys()[i];
4874 try comp.docsCopyModule(mod, seen_table.values()[i], tar_file);
4877 try comp.docsCopyModule(mod, seen_table.values()[i], &tar_file_writer);
48754878
48764879 const deps = mod.deps.values();
48774880 try seen_table.ensureUnusedCapacity(comp.gpa, deps.len);
......@@ -4879,24 +4882,29 @@ fn docsCopyFallible(comp: *Compilation) anyerror!void {
48794882 }
48804883}
48814884
4882fn docsCopyModule(comp: *Compilation, module: *Package.Module, name: []const u8, tar_file: fs.File) !void {
4885fn docsCopyModule(
4886 comp: *Compilation,
4887 module: *Package.Module,
4888 name: []const u8,
4889 tar_file_writer: *fs.File.Writer,
4890) !void {
48834891 const root = module.root;
48844892 var mod_dir = d: {
48854893 const root_dir, const sub_path = root.openInfo(comp.dirs);
48864894 break :d root_dir.openDir(sub_path, .{ .iterate = true });
48874895 } catch |err| {
4888 return comp.lockAndSetMiscFailure(.docs_copy, "unable to open directory '{f}': {s}", .{
4889 root.fmt(comp), @errorName(err),
4890 });
4896 return comp.lockAndSetMiscFailure(.docs_copy, "unable to open directory '{f}': {t}", .{ root.fmt(comp), err });
48914897 };
48924898 defer mod_dir.close();
48934899
48944900 var walker = try mod_dir.walk(comp.gpa);
48954901 defer walker.deinit();
48964902
4897 var archiver = std.tar.writer(tar_file.deprecatedWriter().any());
4903 var archiver: std.tar.Writer = .{ .underlying_writer = &tar_file_writer.interface };
48984904 archiver.prefix = name;
48994905
4906 var buffer: [1024]u8 = undefined;
4907
49004908 while (try walker.next()) |entry| {
49014909 switch (entry.kind) {
49024910 .file => {
......@@ -4907,14 +4915,17 @@ fn docsCopyModule(comp: *Compilation, module: *Package.Module, name: []const u8,
49074915 else => continue,
49084916 }
49094917 var file = mod_dir.openFile(entry.path, .{}) catch |err| {
4910 return comp.lockAndSetMiscFailure(.docs_copy, "unable to open '{f}{s}': {s}", .{
4911 root.fmt(comp), entry.path, @errorName(err),
4918 return comp.lockAndSetMiscFailure(.docs_copy, "unable to open {f}{s}: {t}", .{
4919 root.fmt(comp), entry.path, err,
49124920 });
49134921 };
49144922 defer file.close();
4915 archiver.writeFile(entry.path, file) catch |err| {
4916 return comp.lockAndSetMiscFailure(.docs_copy, "unable to archive '{f}{s}': {s}", .{
4917 root.fmt(comp), entry.path, @errorName(err),
4923 const stat = try file.stat();
4924 var file_reader: fs.File.Reader = .initSize(file, &buffer, stat.size);
4925
4926 archiver.writeFile(entry.path, &file_reader, stat.mtime) catch |err| {
4927 return comp.lockAndSetMiscFailure(.docs_copy, "unable to archive {f}{s}: {t}", .{
4928 root.fmt(comp), entry.path, err,
49184929 });
49194930 };
49204931 }
......@@ -4926,9 +4937,7 @@ fn workerDocsWasm(comp: *Compilation, parent_prog_node: std.Progress.Node) void
49264937
49274938 workerDocsWasmFallible(comp, prog_node) catch |err| switch (err) {
49284939 error.SubCompilationFailed => return, // error reported already
4929 else => comp.lockAndSetMiscFailure(.docs_wasm, "unable to build autodocs: {s}", .{
4930 @errorName(err),
4931 }),
4940 else => comp.lockAndSetMiscFailure(.docs_wasm, "unable to build autodocs: {t}", .{err}),
49324941 };
49334942}
49344943
src/Package/Fetch.zig+18-9
......@@ -1197,12 +1197,17 @@ fn unpackResource(
11971197 };
11981198
11991199 switch (file_type) {
1200 .tar => return try unpackTarball(f, tmp_directory.handle, resource.reader()),
1200 .tar => {
1201 var adapter = resource.reader().adaptToNewApi();
1202 return unpackTarball(f, tmp_directory.handle, &adapter.new_interface);
1203 },
12011204 .@"tar.gz" => {
12021205 const reader = resource.reader();
12031206 var br = std.io.bufferedReaderSize(std.crypto.tls.max_ciphertext_record_len, reader);
12041207 var dcp = std.compress.gzip.decompressor(br.reader());
1205 return try unpackTarball(f, tmp_directory.handle, dcp.reader());
1208 var adapter_buffer: [1024]u8 = undefined;
1209 var adapter = dcp.reader().adaptToNewApi(&adapter_buffer);
1210 return try unpackTarball(f, tmp_directory.handle, &adapter.new_interface);
12061211 },
12071212 .@"tar.xz" => {
12081213 const gpa = f.arena.child_allocator;
......@@ -1215,7 +1220,9 @@ fn unpackResource(
12151220 ));
12161221 };
12171222 defer dcp.deinit();
1218 return try unpackTarball(f, tmp_directory.handle, dcp.reader());
1223 var adapter_buffer: [1024]u8 = undefined;
1224 var adapter = dcp.reader().adaptToNewApi(&adapter_buffer);
1225 return try unpackTarball(f, tmp_directory.handle, &adapter.new_interface);
12191226 },
12201227 .@"tar.zst" => {
12211228 const window_size = std.compress.zstd.DecompressorOptions.default_window_buffer_len;
......@@ -1225,7 +1232,9 @@ fn unpackResource(
12251232 var dcp = std.compress.zstd.decompressor(br.reader(), .{
12261233 .window_buffer = window_buffer,
12271234 });
1228 return try unpackTarball(f, tmp_directory.handle, dcp.reader());
1235 var adapter_buffer: [1024]u8 = undefined;
1236 var adapter = dcp.reader().adaptToNewApi(&adapter_buffer);
1237 return try unpackTarball(f, tmp_directory.handle, &adapter.new_interface);
12291238 },
12301239 .git_pack => return unpackGitPack(f, tmp_directory.handle, &resource.git) catch |err| switch (err) {
12311240 error.FetchFailed => return error.FetchFailed,
......@@ -1239,7 +1248,7 @@ fn unpackResource(
12391248 }
12401249}
12411250
1242fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: anytype) RunError!UnpackResult {
1251fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: *std.Io.Reader) RunError!UnpackResult {
12431252 const eb = &f.error_bundle;
12441253 const arena = f.arena.allocator();
12451254
......@@ -1250,10 +1259,10 @@ fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: anytype) RunError!UnpackRes
12501259 .strip_components = 0,
12511260 .mode_mode = .ignore,
12521261 .exclude_empty_directories = true,
1253 }) catch |err| return f.fail(f.location_tok, try eb.printString(
1254 "unable to unpack tarball to temporary directory: {s}",
1255 .{@errorName(err)},
1256 ));
1262 }) catch |err| return f.fail(
1263 f.location_tok,
1264 try eb.printString("unable to unpack tarball to temporary directory: {t}", .{err}),
1265 );
12571266
12581267 var res: UnpackResult = .{ .root_dir = diagnostics.root_dir };
12591268 if (diagnostics.errors.items.len > 0) {