mirror of
https://codeberg.org/ziglang/zig.git
synced 2025-12-06 13:54:21 +00:00
std.tar: update to new I/O API
This commit is contained in:
parent
2ac81c76e3
commit
1dcea220a4
9 changed files with 1028 additions and 1045 deletions
|
|
@ -522,7 +522,9 @@ fn serveSourcesTar(ws: *WebServer, request: *std.http.Server.Request) !void {
|
|||
|
||||
var cwd_cache: ?[]const u8 = null;
|
||||
|
||||
var archiver = std.tar.writer(response.writer());
|
||||
var adapter = response.writer().adaptToNewApi();
|
||||
var archiver: std.tar.Writer = .{ .underlying_writer = &adapter.new_interface };
|
||||
var read_buffer: [1024]u8 = undefined;
|
||||
|
||||
for (deduped_paths) |joined_path| {
|
||||
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 {
|
|||
continue;
|
||||
};
|
||||
defer file.close();
|
||||
|
||||
const stat = try file.stat();
|
||||
var file_reader: std.fs.File.Reader = .initSize(file, &read_buffer, stat.size);
|
||||
archiver.prefix = joined_path.root_dir.path orelse try memoizedCwd(arena, &cwd_cache);
|
||||
try archiver.writeFile(joined_path.sub_path, file);
|
||||
try archiver.writeFile(joined_path.sub_path, &file_reader, stat.mtime);
|
||||
}
|
||||
|
||||
// intentionally omitting the pointless trailer
|
||||
//try archiver.finish();
|
||||
// intentionally not calling `archiver.finishPedantically`
|
||||
try adapter.new_interface.flush();
|
||||
try response.end();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -179,6 +179,12 @@ pub fn streamExact(r: *Reader, w: *Writer, n: usize) StreamError!void {
|
|||
while (remaining != 0) remaining -= try r.stream(w, .limited(remaining));
|
||||
}
|
||||
|
||||
/// "Pump" exactly `n` bytes from the reader to the writer.
|
||||
pub fn streamExact64(r: *Reader, w: *Writer, n: u64) StreamError!void {
|
||||
var remaining = n;
|
||||
while (remaining != 0) remaining -= try r.stream(w, .limited64(remaining));
|
||||
}
|
||||
|
||||
/// "Pump" data from the reader to the writer, handling `error.EndOfStream` as
|
||||
/// a success case.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -54,12 +54,20 @@ pub const Md5 = struct {
|
|||
};
|
||||
}
|
||||
|
||||
pub fn hash(b: []const u8, out: *[digest_length]u8, options: Options) void {
|
||||
pub fn hash(data: []const u8, out: *[digest_length]u8, options: Options) void {
|
||||
var d = Md5.init(options);
|
||||
d.update(b);
|
||||
d.update(data);
|
||||
d.final(out);
|
||||
}
|
||||
|
||||
pub fn hashResult(data: []const u8) [digest_length]u8 {
|
||||
var out: [digest_length]u8 = undefined;
|
||||
var d = Md5.init(.{});
|
||||
d.update(data);
|
||||
d.final(&out);
|
||||
return out;
|
||||
}
|
||||
|
||||
pub fn update(d: *Self, b: []const u8) void {
|
||||
var off: usize = 0;
|
||||
|
||||
|
|
|
|||
240
lib/std/tar.zig
240
lib/std/tar.zig
|
|
@ -19,7 +19,7 @@ const std = @import("std");
|
|||
const assert = std.debug.assert;
|
||||
const testing = std.testing;
|
||||
|
||||
pub const writer = @import("tar/writer.zig").writer;
|
||||
pub const Writer = @import("tar/Writer.zig");
|
||||
|
||||
/// Provide this to receive detailed error messages.
|
||||
/// When this is provided, some errors which would otherwise be returned
|
||||
|
|
@ -293,28 +293,6 @@ fn nullStr(str: []const u8) []const u8 {
|
|||
return str;
|
||||
}
|
||||
|
||||
/// Options for iterator.
|
||||
/// Buffers should be provided by the caller.
|
||||
pub const IteratorOptions = struct {
|
||||
/// Use a buffer with length `std.fs.max_path_bytes` to match file system capabilities.
|
||||
file_name_buffer: []u8,
|
||||
/// Use a buffer with length `std.fs.max_path_bytes` to match file system capabilities.
|
||||
link_name_buffer: []u8,
|
||||
/// Collects error messages during unpacking
|
||||
diagnostics: ?*Diagnostics = null,
|
||||
};
|
||||
|
||||
/// Iterates over files in tar archive.
|
||||
/// `next` returns each file in tar archive.
|
||||
pub fn iterator(reader: anytype, options: IteratorOptions) Iterator(@TypeOf(reader)) {
|
||||
return .{
|
||||
.reader = reader,
|
||||
.diagnostics = options.diagnostics,
|
||||
.file_name_buffer = options.file_name_buffer,
|
||||
.link_name_buffer = options.link_name_buffer,
|
||||
};
|
||||
}
|
||||
|
||||
/// Type of the file returned by iterator `next` method.
|
||||
pub const FileKind = enum {
|
||||
directory,
|
||||
|
|
@ -323,9 +301,8 @@ pub const FileKind = enum {
|
|||
};
|
||||
|
||||
/// Iterator over entries in the tar file represented by reader.
|
||||
pub fn Iterator(comptime ReaderType: type) type {
|
||||
return struct {
|
||||
reader: ReaderType,
|
||||
pub const Iterator = struct {
|
||||
reader: *std.Io.Reader,
|
||||
diagnostics: ?*Diagnostics = null,
|
||||
|
||||
// buffers for heeader and file attributes
|
||||
|
|
@ -338,49 +315,41 @@ pub fn Iterator(comptime ReaderType: type) type {
|
|||
// not consumed bytes of file from last next iteration
|
||||
unread_file_bytes: u64 = 0,
|
||||
|
||||
/// Options for iterator.
|
||||
/// Buffers should be provided by the caller.
|
||||
pub const Options = struct {
|
||||
/// Use a buffer with length `std.fs.max_path_bytes` to match file system capabilities.
|
||||
file_name_buffer: []u8,
|
||||
/// Use a buffer with length `std.fs.max_path_bytes` to match file system capabilities.
|
||||
link_name_buffer: []u8,
|
||||
/// Collects error messages during unpacking
|
||||
diagnostics: ?*Diagnostics = null,
|
||||
};
|
||||
|
||||
/// Iterates over files in tar archive.
|
||||
/// `next` returns each file in tar archive.
|
||||
pub fn init(reader: *std.Io.Reader, options: Options) Iterator {
|
||||
return .{
|
||||
.reader = reader,
|
||||
.diagnostics = options.diagnostics,
|
||||
.file_name_buffer = options.file_name_buffer,
|
||||
.link_name_buffer = options.link_name_buffer,
|
||||
};
|
||||
}
|
||||
|
||||
pub const File = struct {
|
||||
name: []const u8, // name of file, symlink or directory
|
||||
link_name: []const u8, // target name of symlink
|
||||
size: u64 = 0, // size of the file in bytes
|
||||
mode: u32 = 0,
|
||||
kind: FileKind = .file,
|
||||
|
||||
unread_bytes: *u64,
|
||||
parent_reader: ReaderType,
|
||||
|
||||
pub const Reader = std.io.GenericReader(File, ReaderType.Error, File.read);
|
||||
|
||||
pub fn reader(self: File) Reader {
|
||||
return .{ .context = self };
|
||||
}
|
||||
|
||||
pub fn read(self: File, dest: []u8) ReaderType.Error!usize {
|
||||
const buf = dest[0..@min(dest.len, self.unread_bytes.*)];
|
||||
const n = try self.parent_reader.read(buf);
|
||||
self.unread_bytes.* -= n;
|
||||
return n;
|
||||
}
|
||||
|
||||
// Writes file content to writer.
|
||||
pub fn writeAll(self: File, out_writer: anytype) !void {
|
||||
var buffer: [4096]u8 = undefined;
|
||||
|
||||
while (self.unread_bytes.* > 0) {
|
||||
const buf = buffer[0..@min(buffer.len, self.unread_bytes.*)];
|
||||
try self.parent_reader.readNoEof(buf);
|
||||
try out_writer.writeAll(buf);
|
||||
self.unread_bytes.* -= buf.len;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const Self = @This();
|
||||
|
||||
fn readHeader(self: *Self) !?Header {
|
||||
fn readHeader(self: *Iterator) !?Header {
|
||||
if (self.padding > 0) {
|
||||
try self.reader.skipBytes(self.padding, .{});
|
||||
try self.reader.discardAll(self.padding);
|
||||
}
|
||||
const n = try self.reader.readAll(&self.header_buffer);
|
||||
const n = try self.reader.readSliceShort(&self.header_buffer);
|
||||
if (n == 0) return null;
|
||||
if (n < Header.SIZE) return error.UnexpectedEndOfStream;
|
||||
const header = Header{ .bytes = self.header_buffer[0..Header.SIZE] };
|
||||
|
|
@ -388,19 +357,17 @@ pub fn Iterator(comptime ReaderType: type) type {
|
|||
return header;
|
||||
}
|
||||
|
||||
fn readString(self: *Self, size: usize, buffer: []u8) ![]const u8 {
|
||||
fn readString(self: *Iterator, size: usize, buffer: []u8) ![]const u8 {
|
||||
if (size > buffer.len) return error.TarInsufficientBuffer;
|
||||
const buf = buffer[0..size];
|
||||
try self.reader.readNoEof(buf);
|
||||
try self.reader.readSliceAll(buf);
|
||||
return nullStr(buf);
|
||||
}
|
||||
|
||||
fn newFile(self: *Self) File {
|
||||
fn newFile(self: *Iterator) File {
|
||||
return .{
|
||||
.name = self.file_name_buffer[0..0],
|
||||
.link_name = self.link_name_buffer[0..0],
|
||||
.parent_reader = self.reader,
|
||||
.unread_bytes = &self.unread_file_bytes,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -416,10 +383,10 @@ pub fn Iterator(comptime ReaderType: type) type {
|
|||
/// entries should not normally be visible to the outside. As such, this
|
||||
/// loop iterates through one or more entries until it collects a all
|
||||
/// file attributes.
|
||||
pub fn next(self: *Self) !?File {
|
||||
pub fn next(self: *Iterator) !?File {
|
||||
if (self.unread_file_bytes > 0) {
|
||||
// If file content was not consumed by caller
|
||||
try self.reader.skipBytes(self.unread_file_bytes, .{});
|
||||
try self.reader.discardAll64(self.unread_file_bytes);
|
||||
self.unread_file_bytes = 0;
|
||||
}
|
||||
var file: File = self.newFile();
|
||||
|
|
@ -466,7 +433,10 @@ pub fn Iterator(comptime ReaderType: type) type {
|
|||
// Use just attributes from last extended header.
|
||||
file = self.newFile();
|
||||
|
||||
var rdr = paxIterator(self.reader, @intCast(size));
|
||||
var rdr: PaxIterator = .{
|
||||
.reader = self.reader,
|
||||
.size = @intCast(size),
|
||||
};
|
||||
while (try rdr.next()) |attr| {
|
||||
switch (attr.kind) {
|
||||
.path => {
|
||||
|
|
@ -484,7 +454,7 @@ pub fn Iterator(comptime ReaderType: type) type {
|
|||
},
|
||||
// Ignored header type
|
||||
.global_extended_header => {
|
||||
self.reader.skipBytes(size, .{}) catch return error.TarHeadersTooBig;
|
||||
self.reader.discardAll64(size) catch return error.TarHeadersTooBig;
|
||||
},
|
||||
// All other are unsupported header types
|
||||
else => {
|
||||
|
|
@ -496,33 +466,27 @@ pub fn Iterator(comptime ReaderType: type) type {
|
|||
if (kind == .gnu_sparse) {
|
||||
try self.skipGnuSparseExtendedHeaders(header);
|
||||
}
|
||||
self.reader.skipBytes(size, .{}) catch return error.TarHeadersTooBig;
|
||||
self.reader.discardAll64(size) catch return error.TarHeadersTooBig;
|
||||
},
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn skipGnuSparseExtendedHeaders(self: *Self, header: Header) !void {
|
||||
pub fn streamRemaining(it: *Iterator, file: File, w: *std.Io.Writer) std.Io.Reader.StreamError!void {
|
||||
try it.reader.streamExact64(w, file.size);
|
||||
it.unread_file_bytes = 0;
|
||||
}
|
||||
|
||||
fn skipGnuSparseExtendedHeaders(self: *Iterator, header: Header) !void {
|
||||
var is_extended = header.bytes[482] > 0;
|
||||
while (is_extended) {
|
||||
var buf: [Header.SIZE]u8 = undefined;
|
||||
const n = try self.reader.readAll(&buf);
|
||||
if (n < Header.SIZE) return error.UnexpectedEndOfStream;
|
||||
try self.reader.readSliceAll(&buf);
|
||||
is_extended = buf[504] > 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Pax attributes iterator.
|
||||
/// Size is length of pax extended header in reader.
|
||||
fn paxIterator(reader: anytype, size: usize) PaxIterator(@TypeOf(reader)) {
|
||||
return PaxIterator(@TypeOf(reader)){
|
||||
.reader = reader,
|
||||
.size = size,
|
||||
};
|
||||
}
|
||||
|
||||
const PaxAttributeKind = enum {
|
||||
path,
|
||||
|
|
@ -533,19 +497,16 @@ const PaxAttributeKind = enum {
|
|||
// maxInt(u64) has 20 chars, base 10 in practice we got 24 chars
|
||||
const pax_max_size_attr_len = 64;
|
||||
|
||||
fn PaxIterator(comptime ReaderType: type) type {
|
||||
return struct {
|
||||
pub const PaxIterator = struct {
|
||||
size: usize, // cumulative size of all pax attributes
|
||||
reader: ReaderType,
|
||||
// scratch buffer used for reading attribute length and keyword
|
||||
scratch: [128]u8 = undefined,
|
||||
reader: *std.Io.Reader,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
const Attribute = struct {
|
||||
kind: PaxAttributeKind,
|
||||
len: usize, // length of the attribute value
|
||||
reader: ReaderType, // reader positioned at value start
|
||||
reader: *std.Io.Reader, // reader positioned at value start
|
||||
|
||||
// Copies pax attribute value into destination buffer.
|
||||
// Must be called with destination buffer of size at least Attribute.len.
|
||||
|
|
@ -553,7 +514,7 @@ fn PaxIterator(comptime ReaderType: type) type {
|
|||
if (self.len > dst.len) return error.TarInsufficientBuffer;
|
||||
// assert(self.len <= dst.len);
|
||||
const buf = dst[0..self.len];
|
||||
const n = try self.reader.readAll(buf);
|
||||
const n = try self.reader.readSliceShort(buf);
|
||||
if (n < self.len) return error.UnexpectedEndOfStream;
|
||||
try validateAttributeEnding(self.reader);
|
||||
if (hasNull(buf)) return error.PaxNullInValue;
|
||||
|
|
@ -567,10 +528,10 @@ fn PaxIterator(comptime ReaderType: type) type {
|
|||
// Pax extended header consists of one or more attributes, each constructed as follows:
|
||||
// "%d %s=%s\n", <length>, <keyword>, <value>
|
||||
while (self.size > 0) {
|
||||
const length_buf = try self.readUntil(' ');
|
||||
const length_buf = try self.reader.takeSentinel(' ');
|
||||
const length = try std.fmt.parseInt(usize, length_buf, 10); // record length in bytes
|
||||
|
||||
const keyword = try self.readUntil('=');
|
||||
const keyword = try self.reader.takeSentinel('=');
|
||||
if (hasNull(keyword)) return error.PaxNullInKeyword;
|
||||
|
||||
// calculate value_len
|
||||
|
|
@ -586,14 +547,14 @@ fn PaxIterator(comptime ReaderType: type) type {
|
|||
else if (eql(keyword, "size"))
|
||||
.size
|
||||
else {
|
||||
try self.reader.skipBytes(value_len, .{});
|
||||
try self.reader.discardAll(value_len);
|
||||
try validateAttributeEnding(self.reader);
|
||||
continue;
|
||||
};
|
||||
if (kind == .size and value_len > pax_max_size_attr_len) {
|
||||
return error.PaxSizeAttrOverflow;
|
||||
}
|
||||
return Attribute{
|
||||
return .{
|
||||
.kind = kind,
|
||||
.len = value_len,
|
||||
.reader = self.reader,
|
||||
|
|
@ -603,12 +564,6 @@ fn PaxIterator(comptime ReaderType: type) type {
|
|||
return null;
|
||||
}
|
||||
|
||||
fn readUntil(self: *Self, delimiter: u8) ![]const u8 {
|
||||
var fbs = std.io.fixedBufferStream(&self.scratch);
|
||||
try self.reader.streamUntilDelimiter(fbs.writer(), delimiter, null);
|
||||
return fbs.getWritten();
|
||||
}
|
||||
|
||||
fn eql(a: []const u8, b: []const u8) bool {
|
||||
return std.mem.eql(u8, a, b);
|
||||
}
|
||||
|
|
@ -618,23 +573,23 @@ fn PaxIterator(comptime ReaderType: type) type {
|
|||
}
|
||||
|
||||
// Checks that each record ends with new line.
|
||||
fn validateAttributeEnding(reader: ReaderType) !void {
|
||||
if (try reader.readByte() != '\n') return error.PaxInvalidAttributeEnd;
|
||||
fn validateAttributeEnding(reader: *std.Io.Reader) !void {
|
||||
if (try reader.takeByte() != '\n') return error.PaxInvalidAttributeEnd;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Saves tar file content to the file systems.
|
||||
pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: PipeOptions) !void {
|
||||
pub fn pipeToFileSystem(dir: std.fs.Dir, reader: *std.Io.Reader, options: PipeOptions) !void {
|
||||
var file_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
|
||||
var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
|
||||
var iter = iterator(reader, .{
|
||||
var file_contents_buffer: [1024]u8 = undefined;
|
||||
var it: Iterator = .init(reader, .{
|
||||
.file_name_buffer = &file_name_buffer,
|
||||
.link_name_buffer = &link_name_buffer,
|
||||
.diagnostics = options.diagnostics,
|
||||
});
|
||||
|
||||
while (try iter.next()) |file| {
|
||||
while (try it.next()) |file| {
|
||||
const file_name = stripComponents(file.name, options.strip_components);
|
||||
if (file_name.len == 0 and file.kind != .directory) {
|
||||
const d = options.diagnostics orelse return error.TarComponentsOutsideStrippedPrefix;
|
||||
|
|
@ -656,7 +611,9 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: PipeOptions)
|
|||
.file => {
|
||||
if (createDirAndFile(dir, file_name, fileMode(file.mode, options))) |fs_file| {
|
||||
defer fs_file.close();
|
||||
try file.writeAll(fs_file);
|
||||
var file_writer = fs_file.writer(&file_contents_buffer);
|
||||
try it.streamRemaining(file, &file_writer.interface);
|
||||
try file_writer.interface.flush();
|
||||
} else |err| {
|
||||
const d = options.diagnostics orelse return err;
|
||||
try d.errors.append(d.allocator, .{ .unable_to_create_file = .{
|
||||
|
|
@ -826,11 +783,14 @@ test PaxIterator {
|
|||
var buffer: [1024]u8 = undefined;
|
||||
|
||||
outer: for (cases) |case| {
|
||||
var stream = std.io.fixedBufferStream(case.data);
|
||||
var iter = paxIterator(stream.reader(), case.data.len);
|
||||
var reader: std.Io.Reader = .fixed(case.data);
|
||||
var it: PaxIterator = .{
|
||||
.size = case.data.len,
|
||||
.reader = &reader,
|
||||
};
|
||||
|
||||
var i: usize = 0;
|
||||
while (iter.next() catch |err| {
|
||||
while (it.next() catch |err| {
|
||||
if (case.err) |e| {
|
||||
try testing.expectEqual(e, err);
|
||||
continue;
|
||||
|
|
@ -853,12 +813,6 @@ test PaxIterator {
|
|||
}
|
||||
}
|
||||
|
||||
test {
|
||||
_ = @import("tar/test.zig");
|
||||
_ = @import("tar/writer.zig");
|
||||
_ = Diagnostics;
|
||||
}
|
||||
|
||||
test "header parse size" {
|
||||
const cases = [_]struct {
|
||||
in: []const u8,
|
||||
|
|
@ -941,7 +895,7 @@ test "create file and symlink" {
|
|||
file.close();
|
||||
}
|
||||
|
||||
test iterator {
|
||||
test Iterator {
|
||||
// Example tar file is created from this tree structure:
|
||||
// $ tree example
|
||||
// example
|
||||
|
|
@ -962,19 +916,19 @@ test iterator {
|
|||
// example/empty/
|
||||
|
||||
const data = @embedFile("tar/testdata/example.tar");
|
||||
var fbs = std.io.fixedBufferStream(data);
|
||||
var reader: std.Io.Reader = .fixed(data);
|
||||
|
||||
// User provided buffers to the iterator
|
||||
var file_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
|
||||
var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
|
||||
// Create iterator
|
||||
var iter = iterator(fbs.reader(), .{
|
||||
var it: Iterator = .init(&reader, .{
|
||||
.file_name_buffer = &file_name_buffer,
|
||||
.link_name_buffer = &link_name_buffer,
|
||||
});
|
||||
// Iterate over files in example.tar
|
||||
var file_no: usize = 0;
|
||||
while (try iter.next()) |file| : (file_no += 1) {
|
||||
while (try it.next()) |file| : (file_no += 1) {
|
||||
switch (file.kind) {
|
||||
.directory => {
|
||||
switch (file_no) {
|
||||
|
|
@ -987,10 +941,10 @@ test iterator {
|
|||
},
|
||||
.file => {
|
||||
try testing.expectEqualStrings("example/a/file", file.name);
|
||||
// Read file content
|
||||
var buf: [16]u8 = undefined;
|
||||
const n = try file.reader().readAll(&buf);
|
||||
try testing.expectEqualStrings("content\n", buf[0..n]);
|
||||
var w: std.Io.Writer = .fixed(&buf);
|
||||
try it.streamRemaining(file, &w);
|
||||
try testing.expectEqualStrings("content\n", w.buffered());
|
||||
},
|
||||
.sym_link => {
|
||||
try testing.expectEqualStrings("example/b/symlink", file.name);
|
||||
|
|
@ -1021,15 +975,14 @@ test pipeToFileSystem {
|
|||
// example/empty/
|
||||
|
||||
const data = @embedFile("tar/testdata/example.tar");
|
||||
var fbs = std.io.fixedBufferStream(data);
|
||||
const reader = fbs.reader();
|
||||
var reader: std.Io.Reader = .fixed(data);
|
||||
|
||||
var tmp = testing.tmpDir(.{ .no_follow = true });
|
||||
defer tmp.cleanup();
|
||||
const dir = tmp.dir;
|
||||
|
||||
// Save tar from `reader` to the file system `dir`
|
||||
pipeToFileSystem(dir, reader, .{
|
||||
// Save tar from reader to the file system `dir`
|
||||
pipeToFileSystem(dir, &reader, .{
|
||||
.mode_mode = .ignore,
|
||||
.strip_components = 1,
|
||||
.exclude_empty_directories = true,
|
||||
|
|
@ -1053,8 +1006,7 @@ test pipeToFileSystem {
|
|||
|
||||
test "pipeToFileSystem root_dir" {
|
||||
const data = @embedFile("tar/testdata/example.tar");
|
||||
var fbs = std.io.fixedBufferStream(data);
|
||||
const reader = fbs.reader();
|
||||
var reader: std.Io.Reader = .fixed(data);
|
||||
|
||||
// with strip_components = 1
|
||||
{
|
||||
|
|
@ -1063,7 +1015,7 @@ test "pipeToFileSystem root_dir" {
|
|||
var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
|
||||
defer diagnostics.deinit();
|
||||
|
||||
pipeToFileSystem(tmp.dir, reader, .{
|
||||
pipeToFileSystem(tmp.dir, &reader, .{
|
||||
.strip_components = 1,
|
||||
.diagnostics = &diagnostics,
|
||||
}) catch |err| {
|
||||
|
|
@ -1079,13 +1031,13 @@ test "pipeToFileSystem root_dir" {
|
|||
|
||||
// with strip_components = 0
|
||||
{
|
||||
fbs.reset();
|
||||
reader = .fixed(data);
|
||||
var tmp = testing.tmpDir(.{ .no_follow = true });
|
||||
defer tmp.cleanup();
|
||||
var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
|
||||
defer diagnostics.deinit();
|
||||
|
||||
pipeToFileSystem(tmp.dir, reader, .{
|
||||
pipeToFileSystem(tmp.dir, &reader, .{
|
||||
.strip_components = 0,
|
||||
.diagnostics = &diagnostics,
|
||||
}) catch |err| {
|
||||
|
|
@ -1102,45 +1054,42 @@ test "pipeToFileSystem root_dir" {
|
|||
|
||||
test "findRoot with single file archive" {
|
||||
const data = @embedFile("tar/testdata/22752.tar");
|
||||
var fbs = std.io.fixedBufferStream(data);
|
||||
const reader = fbs.reader();
|
||||
var reader: std.Io.Reader = .fixed(data);
|
||||
|
||||
var tmp = testing.tmpDir(.{});
|
||||
defer tmp.cleanup();
|
||||
|
||||
var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
|
||||
defer diagnostics.deinit();
|
||||
try pipeToFileSystem(tmp.dir, reader, .{ .diagnostics = &diagnostics });
|
||||
try pipeToFileSystem(tmp.dir, &reader, .{ .diagnostics = &diagnostics });
|
||||
|
||||
try testing.expectEqualStrings("", diagnostics.root_dir);
|
||||
}
|
||||
|
||||
test "findRoot without explicit root dir" {
|
||||
const data = @embedFile("tar/testdata/19820.tar");
|
||||
var fbs = std.io.fixedBufferStream(data);
|
||||
const reader = fbs.reader();
|
||||
var reader: std.Io.Reader = .fixed(data);
|
||||
|
||||
var tmp = testing.tmpDir(.{});
|
||||
defer tmp.cleanup();
|
||||
|
||||
var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
|
||||
defer diagnostics.deinit();
|
||||
try pipeToFileSystem(tmp.dir, reader, .{ .diagnostics = &diagnostics });
|
||||
try pipeToFileSystem(tmp.dir, &reader, .{ .diagnostics = &diagnostics });
|
||||
|
||||
try testing.expectEqualStrings("root", diagnostics.root_dir);
|
||||
}
|
||||
|
||||
test "pipeToFileSystem strip_components" {
|
||||
const data = @embedFile("tar/testdata/example.tar");
|
||||
var fbs = std.io.fixedBufferStream(data);
|
||||
const reader = fbs.reader();
|
||||
var reader: std.Io.Reader = .fixed(data);
|
||||
|
||||
var tmp = testing.tmpDir(.{ .no_follow = true });
|
||||
defer tmp.cleanup();
|
||||
var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
|
||||
defer diagnostics.deinit();
|
||||
|
||||
pipeToFileSystem(tmp.dir, reader, .{
|
||||
pipeToFileSystem(tmp.dir, &reader, .{
|
||||
.strip_components = 3,
|
||||
.diagnostics = &diagnostics,
|
||||
}) catch |err| {
|
||||
|
|
@ -1194,13 +1143,12 @@ test "executable bit" {
|
|||
const data = @embedFile("tar/testdata/example.tar");
|
||||
|
||||
for ([_]PipeOptions.ModeMode{ .ignore, .executable_bit_only }) |opt| {
|
||||
var fbs = std.io.fixedBufferStream(data);
|
||||
const reader = fbs.reader();
|
||||
var reader: std.Io.Reader = .fixed(data);
|
||||
|
||||
var tmp = testing.tmpDir(.{ .no_follow = true });
|
||||
//defer tmp.cleanup();
|
||||
|
||||
pipeToFileSystem(tmp.dir, reader, .{
|
||||
pipeToFileSystem(tmp.dir, &reader, .{
|
||||
.strip_components = 1,
|
||||
.exclude_empty_directories = true,
|
||||
.mode_mode = opt,
|
||||
|
|
@ -1226,3 +1174,9 @@ test "executable bit" {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
_ = @import("tar/test.zig");
|
||||
_ = Writer;
|
||||
_ = Diagnostics;
|
||||
}
|
||||
|
|
|
|||
498
lib/std/tar/Writer.zig
Normal file
498
lib/std/tar/Writer.zig
Normal file
|
|
@ -0,0 +1,498 @@
|
|||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const testing = std.testing;
|
||||
const Writer = @This();
|
||||
|
||||
const block_size = @sizeOf(Header);
|
||||
|
||||
/// Options for writing file/dir/link. If left empty 0o664 is used for
|
||||
/// file mode and current time for mtime.
|
||||
pub const Options = struct {
|
||||
/// File system permission mode.
|
||||
mode: u32 = 0,
|
||||
/// File system modification time.
|
||||
mtime: u64 = 0,
|
||||
};
|
||||
|
||||
underlying_writer: *std.Io.Writer,
|
||||
prefix: []const u8 = "",
|
||||
mtime_now: u64 = 0,
|
||||
|
||||
const Error = error{
|
||||
WriteFailed,
|
||||
OctalOverflow,
|
||||
NameTooLong,
|
||||
};
|
||||
|
||||
/// Sets prefix for all other write* method paths.
|
||||
pub fn setRoot(w: *Writer, root: []const u8) Error!void {
|
||||
if (root.len > 0)
|
||||
try w.writeDir(root, .{});
|
||||
|
||||
w.prefix = root;
|
||||
}
|
||||
|
||||
pub fn writeDir(w: *Writer, sub_path: []const u8, options: Options) Error!void {
|
||||
try w.writeHeader(.directory, sub_path, "", 0, options);
|
||||
}
|
||||
|
||||
pub const WriteFileError = std.Io.Writer.FileError || Error || std.fs.File.GetEndPosError;
|
||||
|
||||
pub fn writeFile(
|
||||
w: *Writer,
|
||||
sub_path: []const u8,
|
||||
file_reader: *std.fs.File.Reader,
|
||||
stat_mtime: i128,
|
||||
) WriteFileError!void {
|
||||
const size = try file_reader.getSize();
|
||||
const mtime: u64 = @intCast(@divFloor(stat_mtime, std.time.ns_per_s));
|
||||
|
||||
var header: Header = .{};
|
||||
try w.setPath(&header, sub_path);
|
||||
try header.setSize(size);
|
||||
try header.setMtime(mtime);
|
||||
try header.updateChecksum();
|
||||
|
||||
try w.underlying_writer.writeAll(@ptrCast((&header)[0..1]));
|
||||
_ = try w.underlying_writer.sendFileAll(file_reader, .unlimited);
|
||||
try w.writePadding(size);
|
||||
}
|
||||
|
||||
pub const WriteFileStreamError = Error || std.Io.Reader.StreamError;
|
||||
|
||||
/// Writes file reading file content from `reader`. Reads exactly `size` bytes
|
||||
/// from `reader`, or returns `error.EndOfStream`.
|
||||
pub fn writeFileStream(
|
||||
w: *Writer,
|
||||
sub_path: []const u8,
|
||||
size: u64,
|
||||
reader: *std.Io.Reader,
|
||||
options: Options,
|
||||
) WriteFileStreamError!void {
|
||||
try w.writeHeader(.regular, sub_path, "", size, options);
|
||||
try reader.streamExact64(w.underlying_writer, size);
|
||||
try w.writePadding(size);
|
||||
}
|
||||
|
||||
/// Writes file using bytes buffer `content` for size and file content.
|
||||
pub fn writeFileBytes(w: *Writer, sub_path: []const u8, content: []const u8, options: Options) Error!void {
|
||||
try w.writeHeader(.regular, sub_path, "", content.len, options);
|
||||
try w.underlying_writer.writeAll(content);
|
||||
try w.writePadding(content.len);
|
||||
}
|
||||
|
||||
pub fn writeLink(w: *Writer, sub_path: []const u8, link_name: []const u8, options: Options) Error!void {
|
||||
try w.writeHeader(.symbolic_link, sub_path, link_name, 0, options);
|
||||
}
|
||||
|
||||
/// Writes fs.Dir.WalkerEntry. Uses `mtime` from file system entry and
|
||||
/// default for entry mode .
|
||||
pub fn writeEntry(w: *Writer, entry: std.fs.Dir.Walker.Entry) Error!void {
|
||||
switch (entry.kind) {
|
||||
.directory => {
|
||||
try w.writeDir(entry.path, .{ .mtime = try entryMtime(entry) });
|
||||
},
|
||||
.file => {
|
||||
var file = try entry.dir.openFile(entry.basename, .{});
|
||||
defer file.close();
|
||||
const stat = try file.stat();
|
||||
try w.writeFile(entry.path, file, stat);
|
||||
},
|
||||
.sym_link => {
|
||||
var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
|
||||
const link_name = try entry.dir.readLink(entry.basename, &link_name_buffer);
|
||||
try w.writeLink(entry.path, link_name, .{ .mtime = try entryMtime(entry) });
|
||||
},
|
||||
else => {
|
||||
return error.UnsupportedWalkerEntryKind;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn writeHeader(
|
||||
w: *Writer,
|
||||
typeflag: Header.FileType,
|
||||
sub_path: []const u8,
|
||||
link_name: []const u8,
|
||||
size: u64,
|
||||
options: Options,
|
||||
) Error!void {
|
||||
var header = Header.init(typeflag);
|
||||
try w.setPath(&header, sub_path);
|
||||
try header.setSize(size);
|
||||
try header.setMtime(if (options.mtime != 0) options.mtime else w.mtimeNow());
|
||||
if (options.mode != 0)
|
||||
try header.setMode(options.mode);
|
||||
if (typeflag == .symbolic_link)
|
||||
header.setLinkname(link_name) catch |err| switch (err) {
|
||||
error.NameTooLong => try w.writeExtendedHeader(.gnu_long_link, &.{link_name}),
|
||||
else => return err,
|
||||
};
|
||||
try header.write(w.underlying_writer);
|
||||
}
|
||||
|
||||
fn mtimeNow(w: *Writer) u64 {
|
||||
if (w.mtime_now == 0)
|
||||
w.mtime_now = @intCast(std.time.timestamp());
|
||||
return w.mtime_now;
|
||||
}
|
||||
|
||||
fn entryMtime(entry: std.fs.Dir.Walker.Entry) !u64 {
|
||||
const stat = try entry.dir.statFile(entry.basename);
|
||||
return @intCast(@divFloor(stat.mtime, std.time.ns_per_s));
|
||||
}
|
||||
|
||||
/// Writes path in posix header, if don't fit (in name+prefix; 100+155
|
||||
/// bytes) writes it in gnu extended header.
|
||||
fn setPath(w: *Writer, header: *Header, sub_path: []const u8) Error!void {
|
||||
header.setPath(w.prefix, sub_path) catch |err| switch (err) {
|
||||
error.NameTooLong => {
|
||||
// write extended header
|
||||
const buffers: []const []const u8 = if (w.prefix.len == 0)
|
||||
&.{sub_path}
|
||||
else
|
||||
&.{ w.prefix, "/", sub_path };
|
||||
try w.writeExtendedHeader(.gnu_long_name, buffers);
|
||||
},
|
||||
else => return err,
|
||||
};
|
||||
}
|
||||
|
||||
/// Writes gnu extended header: gnu_long_name or gnu_long_link.
|
||||
fn writeExtendedHeader(w: *Writer, typeflag: Header.FileType, buffers: []const []const u8) Error!void {
|
||||
var len: usize = 0;
|
||||
for (buffers) |buf| len += buf.len;
|
||||
|
||||
var header: Header = .init(typeflag);
|
||||
try header.setSize(len);
|
||||
try header.write(w.underlying_writer);
|
||||
for (buffers) |buf|
|
||||
try w.underlying_writer.writeAll(buf);
|
||||
try w.writePadding(len);
|
||||
}
|
||||
|
||||
fn writePadding(w: *Writer, bytes: usize) std.Io.Writer.Error!void {
|
||||
const pos = bytes % block_size;
|
||||
if (pos == 0) return;
|
||||
try w.underlying_writer.splatByteAll(0, block_size - pos);
|
||||
}
|
||||
|
||||
/// According to the specification, tar should finish with two zero blocks, but
|
||||
/// "reasonable system must not assume that such a block exists when reading an
|
||||
/// archive". Therefore, the Zig standard library recommends to not call this
|
||||
/// function.
|
||||
pub fn finishPedantically(w: *Writer) std.Io.Writer.Error!void {
|
||||
try w.underlying_writer.splatByteAll(0, block_size * 2);
|
||||
}
|
||||
|
||||
/// A struct that is exactly 512 bytes and matches tar file format. This is
|
||||
/// intended to be used for outputting tar files; for parsing there is
|
||||
/// `std.tar.Header`.
|
||||
pub const Header = extern struct {
|
||||
// This struct was originally copied from
|
||||
// https://github.com/mattnite/tar/blob/main/src/main.zig which is MIT
|
||||
// licensed.
|
||||
//
|
||||
// The name, linkname, magic, uname, and gname are null-terminated character
|
||||
// strings. All other fields are zero-filled octal numbers in ASCII. Each
|
||||
// numeric field of width w contains w minus 1 digits, and a null.
|
||||
// Reference: https://www.gnu.org/software/tar/manual/html_node/Standard.html
|
||||
// POSIX header: byte offset
|
||||
name: [100]u8 = [_]u8{0} ** 100, // 0
|
||||
mode: [7:0]u8 = default_mode.file, // 100
|
||||
uid: [7:0]u8 = [_:0]u8{0} ** 7, // unused 108
|
||||
gid: [7:0]u8 = [_:0]u8{0} ** 7, // unused 116
|
||||
size: [11:0]u8 = [_:0]u8{'0'} ** 11, // 124
|
||||
mtime: [11:0]u8 = [_:0]u8{'0'} ** 11, // 136
|
||||
checksum: [7:0]u8 = [_:0]u8{' '} ** 7, // 148
|
||||
typeflag: FileType = .regular, // 156
|
||||
linkname: [100]u8 = [_]u8{0} ** 100, // 157
|
||||
magic: [6]u8 = [_]u8{ 'u', 's', 't', 'a', 'r', 0 }, // 257
|
||||
version: [2]u8 = [_]u8{ '0', '0' }, // 263
|
||||
uname: [32]u8 = [_]u8{0} ** 32, // unused 265
|
||||
gname: [32]u8 = [_]u8{0} ** 32, // unused 297
|
||||
devmajor: [7:0]u8 = [_:0]u8{0} ** 7, // unused 329
|
||||
devminor: [7:0]u8 = [_:0]u8{0} ** 7, // unused 337
|
||||
prefix: [155]u8 = [_]u8{0} ** 155, // 345
|
||||
pad: [12]u8 = [_]u8{0} ** 12, // unused 500
|
||||
|
||||
pub const FileType = enum(u8) {
|
||||
regular = '0',
|
||||
symbolic_link = '2',
|
||||
directory = '5',
|
||||
gnu_long_name = 'L',
|
||||
gnu_long_link = 'K',
|
||||
};
|
||||
|
||||
const default_mode = struct {
|
||||
const file = [_:0]u8{ '0', '0', '0', '0', '6', '6', '4' }; // 0o664
|
||||
const dir = [_:0]u8{ '0', '0', '0', '0', '7', '7', '5' }; // 0o775
|
||||
const sym_link = [_:0]u8{ '0', '0', '0', '0', '7', '7', '7' }; // 0o777
|
||||
const other = [_:0]u8{ '0', '0', '0', '0', '0', '0', '0' }; // 0o000
|
||||
};
|
||||
|
||||
pub fn init(typeflag: FileType) Header {
|
||||
return .{
|
||||
.typeflag = typeflag,
|
||||
.mode = switch (typeflag) {
|
||||
.directory => default_mode.dir,
|
||||
.symbolic_link => default_mode.sym_link,
|
||||
.regular => default_mode.file,
|
||||
else => default_mode.other,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn setSize(w: *Header, size: u64) error{OctalOverflow}!void {
|
||||
try octal(&w.size, size);
|
||||
}
|
||||
|
||||
fn octal(buf: []u8, value: u64) error{OctalOverflow}!void {
|
||||
var remainder: u64 = value;
|
||||
var pos: usize = buf.len;
|
||||
while (remainder > 0 and pos > 0) {
|
||||
pos -= 1;
|
||||
const c: u8 = @as(u8, @intCast(remainder % 8)) + '0';
|
||||
buf[pos] = c;
|
||||
remainder /= 8;
|
||||
if (pos == 0 and remainder > 0) return error.OctalOverflow;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setMode(w: *Header, mode: u32) error{OctalOverflow}!void {
|
||||
try octal(&w.mode, mode);
|
||||
}
|
||||
|
||||
// Integer number of seconds since January 1, 1970, 00:00 Coordinated Universal Time.
|
||||
// mtime == 0 will use current time
|
||||
pub fn setMtime(w: *Header, mtime: u64) error{OctalOverflow}!void {
|
||||
try octal(&w.mtime, mtime);
|
||||
}
|
||||
|
||||
pub fn updateChecksum(w: *Header) !void {
|
||||
var checksum: usize = ' '; // other 7 w.checksum bytes are initialized to ' '
|
||||
for (std.mem.asBytes(w)) |val|
|
||||
checksum += val;
|
||||
try octal(&w.checksum, checksum);
|
||||
}
|
||||
|
||||
pub fn write(h: *Header, bw: *std.Io.Writer) error{ OctalOverflow, WriteFailed }!void {
|
||||
try h.updateChecksum();
|
||||
try bw.writeAll(std.mem.asBytes(h));
|
||||
}
|
||||
|
||||
pub fn setLinkname(w: *Header, link: []const u8) !void {
|
||||
if (link.len > w.linkname.len) return error.NameTooLong;
|
||||
@memcpy(w.linkname[0..link.len], link);
|
||||
}
|
||||
|
||||
pub fn setPath(w: *Header, prefix: []const u8, sub_path: []const u8) !void {
|
||||
const max_prefix = w.prefix.len;
|
||||
const max_name = w.name.len;
|
||||
const sep = std.fs.path.sep_posix;
|
||||
|
||||
if (prefix.len + sub_path.len > max_name + max_prefix or prefix.len > max_prefix)
|
||||
return error.NameTooLong;
|
||||
|
||||
// both fit into name
|
||||
if (prefix.len > 0 and prefix.len + sub_path.len < max_name) {
|
||||
@memcpy(w.name[0..prefix.len], prefix);
|
||||
w.name[prefix.len] = sep;
|
||||
@memcpy(w.name[prefix.len + 1 ..][0..sub_path.len], sub_path);
|
||||
return;
|
||||
}
|
||||
|
||||
// sub_path fits into name
|
||||
// there is no prefix or prefix fits into prefix
|
||||
if (sub_path.len <= max_name) {
|
||||
@memcpy(w.name[0..sub_path.len], sub_path);
|
||||
@memcpy(w.prefix[0..prefix.len], prefix);
|
||||
return;
|
||||
}
|
||||
|
||||
if (prefix.len > 0) {
|
||||
@memcpy(w.prefix[0..prefix.len], prefix);
|
||||
w.prefix[prefix.len] = sep;
|
||||
}
|
||||
const prefix_pos = if (prefix.len > 0) prefix.len + 1 else 0;
|
||||
|
||||
// add as much to prefix as you can, must split at /
|
||||
const prefix_remaining = max_prefix - prefix_pos;
|
||||
if (std.mem.lastIndexOf(u8, sub_path[0..@min(prefix_remaining, sub_path.len)], &.{'/'})) |sep_pos| {
|
||||
@memcpy(w.prefix[prefix_pos..][0..sep_pos], sub_path[0..sep_pos]);
|
||||
if ((sub_path.len - sep_pos - 1) > max_name) return error.NameTooLong;
|
||||
@memcpy(w.name[0..][0 .. sub_path.len - sep_pos - 1], sub_path[sep_pos + 1 ..]);
|
||||
return;
|
||||
}
|
||||
|
||||
return error.NameTooLong;
|
||||
}
|
||||
|
||||
comptime {
|
||||
assert(@sizeOf(Header) == 512);
|
||||
}
|
||||
|
||||
test "setPath" {
|
||||
const cases = [_]struct {
|
||||
in: []const []const u8,
|
||||
out: []const []const u8,
|
||||
}{
|
||||
.{
|
||||
.in = &.{ "", "123456789" },
|
||||
.out = &.{ "", "123456789" },
|
||||
},
|
||||
// can fit into name
|
||||
.{
|
||||
.in = &.{ "prefix", "sub_path" },
|
||||
.out = &.{ "", "prefix/sub_path" },
|
||||
},
|
||||
// no more both fits into name
|
||||
.{
|
||||
.in = &.{ "prefix", "0123456789/" ** 8 ++ "basename" },
|
||||
.out = &.{ "prefix", "0123456789/" ** 8 ++ "basename" },
|
||||
},
|
||||
// put as much as you can into prefix the rest goes into name
|
||||
.{
|
||||
.in = &.{ "prefix", "0123456789/" ** 10 ++ "basename" },
|
||||
.out = &.{ "prefix/" ++ "0123456789/" ** 9 ++ "0123456789", "basename" },
|
||||
},
|
||||
|
||||
.{
|
||||
.in = &.{ "prefix", "0123456789/" ** 15 ++ "basename" },
|
||||
.out = &.{ "prefix/" ++ "0123456789/" ** 12 ++ "0123456789", "0123456789/0123456789/basename" },
|
||||
},
|
||||
.{
|
||||
.in = &.{ "prefix", "0123456789/" ** 21 ++ "basename" },
|
||||
.out = &.{ "prefix/" ++ "0123456789/" ** 12 ++ "0123456789", "0123456789/" ** 8 ++ "basename" },
|
||||
},
|
||||
.{
|
||||
.in = &.{ "", "012345678/" ** 10 ++ "foo" },
|
||||
.out = &.{ "012345678/" ** 9 ++ "012345678", "foo" },
|
||||
},
|
||||
};
|
||||
|
||||
for (cases) |case| {
|
||||
var header = Header.init(.regular);
|
||||
try header.setPath(case.in[0], case.in[1]);
|
||||
try testing.expectEqualStrings(case.out[0], str(&header.prefix));
|
||||
try testing.expectEqualStrings(case.out[1], str(&header.name));
|
||||
}
|
||||
|
||||
const error_cases = [_]struct {
|
||||
in: []const []const u8,
|
||||
}{
|
||||
// basename can't fit into name (106 characters)
|
||||
.{ .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" } },
|
||||
// cant fit into 255 + sep
|
||||
.{ .in = &.{ "prefix", "0123456789/" ** 22 ++ "basename" } },
|
||||
// can fit but sub_path can't be split (there is no separator)
|
||||
.{ .in = &.{ "prefix", "0123456789" ** 10 ++ "a" } },
|
||||
.{ .in = &.{ "prefix", "0123456789" ** 14 ++ "basename" } },
|
||||
};
|
||||
|
||||
for (error_cases) |case| {
|
||||
var header = Header.init(.regular);
|
||||
try testing.expectError(
|
||||
error.NameTooLong,
|
||||
header.setPath(case.in[0], case.in[1]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Breaks string on first null character.
|
||||
fn str(s: []const u8) []const u8 {
|
||||
for (s, 0..) |c, i| {
|
||||
if (c == 0) return s[0..i];
|
||||
}
|
||||
return s;
|
||||
}
|
||||
};
|
||||
|
||||
test {
|
||||
_ = Header;
|
||||
}
|
||||
|
||||
test "write files" {
|
||||
const files = [_]struct {
|
||||
path: []const u8,
|
||||
content: []const u8,
|
||||
}{
|
||||
.{ .path = "foo", .content = "bar" },
|
||||
.{ .path = "a12345678/" ** 10 ++ "foo", .content = "a" ** 511 },
|
||||
.{ .path = "b12345678/" ** 24 ++ "foo", .content = "b" ** 512 },
|
||||
.{ .path = "c12345678/" ** 25 ++ "foo", .content = "c" ** 513 },
|
||||
.{ .path = "d12345678/" ** 51 ++ "foo", .content = "d" ** 1025 },
|
||||
.{ .path = "e123456789" ** 11, .content = "e" },
|
||||
};
|
||||
|
||||
var file_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
|
||||
var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
|
||||
|
||||
// with root
|
||||
{
|
||||
const root = "root";
|
||||
|
||||
var output: std.Io.Writer.Allocating = .init(testing.allocator);
|
||||
var w: Writer = .{ .underlying_writer = &output.writer };
|
||||
defer output.deinit();
|
||||
try w.setRoot(root);
|
||||
for (files) |file|
|
||||
try w.writeFileBytes(file.path, file.content, .{});
|
||||
|
||||
var input: std.Io.Reader = .fixed(output.getWritten());
|
||||
var it: std.tar.Iterator = .init(&input, .{
|
||||
.file_name_buffer = &file_name_buffer,
|
||||
.link_name_buffer = &link_name_buffer,
|
||||
});
|
||||
|
||||
// first entry is directory with prefix
|
||||
{
|
||||
const actual = (try it.next()).?;
|
||||
try testing.expectEqualStrings(root, actual.name);
|
||||
try testing.expectEqual(std.tar.FileKind.directory, actual.kind);
|
||||
}
|
||||
|
||||
var i: usize = 0;
|
||||
while (try it.next()) |actual| {
|
||||
defer i += 1;
|
||||
const expected = files[i];
|
||||
try testing.expectEqualStrings(root, actual.name[0..root.len]);
|
||||
try testing.expectEqual('/', actual.name[root.len..][0]);
|
||||
try testing.expectEqualStrings(expected.path, actual.name[root.len + 1 ..]);
|
||||
|
||||
var content: std.Io.Writer.Allocating = .init(testing.allocator);
|
||||
defer content.deinit();
|
||||
try it.streamRemaining(actual, &content.writer);
|
||||
try testing.expectEqualSlices(u8, expected.content, content.getWritten());
|
||||
}
|
||||
}
|
||||
// without root
|
||||
{
|
||||
var output: std.Io.Writer.Allocating = .init(testing.allocator);
|
||||
var w: Writer = .{ .underlying_writer = &output.writer };
|
||||
defer output.deinit();
|
||||
for (files) |file| {
|
||||
var content: std.Io.Reader = .fixed(file.content);
|
||||
try w.writeFileStream(file.path, file.content.len, &content, .{});
|
||||
}
|
||||
|
||||
var input: std.Io.Reader = .fixed(output.getWritten());
|
||||
var it: std.tar.Iterator = .init(&input, .{
|
||||
.file_name_buffer = &file_name_buffer,
|
||||
.link_name_buffer = &link_name_buffer,
|
||||
});
|
||||
|
||||
var i: usize = 0;
|
||||
while (try it.next()) |actual| {
|
||||
defer i += 1;
|
||||
const expected = files[i];
|
||||
try testing.expectEqualStrings(expected.path, actual.name);
|
||||
|
||||
var content: std.Io.Writer.Allocating = .init(testing.allocator);
|
||||
defer content.deinit();
|
||||
try it.streamRemaining(actual, &content.writer);
|
||||
try testing.expectEqualSlices(u8, expected.content, content.getWritten());
|
||||
}
|
||||
try w.finishPedantically();
|
||||
}
|
||||
}
|
||||
|
|
@ -18,8 +18,7 @@ const Case = struct {
|
|||
err: ?anyerror = null, // parsing should fail with this error
|
||||
};
|
||||
|
||||
const cases = [_]Case{
|
||||
.{
|
||||
const gnu_case: Case = .{
|
||||
.data = @embedFile("testdata/gnu.tar"),
|
||||
.files = &[_]Case.File{
|
||||
.{
|
||||
|
|
@ -37,12 +36,54 @@ const cases = [_]Case{
|
|||
"e38b27eaccb4391bdec553a7f3ae6b2f",
|
||||
"c65bd2e50a56a2138bf1716f2fd56fe9",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const gnu_multi_headers_case: Case = .{
|
||||
.data = @embedFile("testdata/gnu-multi-hdrs.tar"),
|
||||
.files = &[_]Case.File{
|
||||
.{
|
||||
.name = "GNU2/GNU2/long-path-name",
|
||||
.link_name = "GNU4/GNU4/long-linkpath-name",
|
||||
.kind = .sym_link,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const trailing_slash_case: Case = .{
|
||||
.data = @embedFile("testdata/trailing-slash.tar"),
|
||||
.files = &[_]Case.File{
|
||||
.{
|
||||
.name = "123456789/" ** 30,
|
||||
.kind = .directory,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const writer_big_long_case: Case = .{
|
||||
// Size in gnu extended format, and name in pax attribute.
|
||||
.data = @embedFile("testdata/writer-big-long.tar"),
|
||||
.files = &[_]Case.File{
|
||||
.{
|
||||
.name = "longname/" ** 15 ++ "16gig.txt",
|
||||
.size = 16 * 1024 * 1024 * 1024,
|
||||
.mode = 0o644,
|
||||
.truncated = true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const fuzz1_case: Case = .{
|
||||
.data = @embedFile("testdata/fuzz1.tar"),
|
||||
.err = error.TarInsufficientBuffer,
|
||||
};
|
||||
|
||||
test "run test cases" {
|
||||
try testCase(gnu_case);
|
||||
try testCase(.{
|
||||
.data = @embedFile("testdata/sparse-formats.tar"),
|
||||
.err = error.TarUnsupportedHeader,
|
||||
},
|
||||
.{
|
||||
});
|
||||
try testCase(.{
|
||||
.data = @embedFile("testdata/star.tar"),
|
||||
.files = &[_]Case.File{
|
||||
.{
|
||||
|
|
@ -60,8 +101,8 @@ const cases = [_]Case{
|
|||
"e38b27eaccb4391bdec553a7f3ae6b2f",
|
||||
"c65bd2e50a56a2138bf1716f2fd56fe9",
|
||||
},
|
||||
},
|
||||
.{
|
||||
});
|
||||
try testCase(.{
|
||||
.data = @embedFile("testdata/v7.tar"),
|
||||
.files = &[_]Case.File{
|
||||
.{
|
||||
|
|
@ -79,8 +120,8 @@ const cases = [_]Case{
|
|||
"e38b27eaccb4391bdec553a7f3ae6b2f",
|
||||
"c65bd2e50a56a2138bf1716f2fd56fe9",
|
||||
},
|
||||
},
|
||||
.{
|
||||
});
|
||||
try testCase(.{
|
||||
.data = @embedFile("testdata/pax.tar"),
|
||||
.files = &[_]Case.File{
|
||||
.{
|
||||
|
|
@ -99,13 +140,13 @@ const cases = [_]Case{
|
|||
.chksums = &[_][]const u8{
|
||||
"3c382e8f5b6631aa2db52643912ffd4a",
|
||||
},
|
||||
},
|
||||
.{
|
||||
});
|
||||
try testCase(.{
|
||||
// pax attribute don't end with \n
|
||||
.data = @embedFile("testdata/pax-bad-hdr-file.tar"),
|
||||
.err = error.PaxInvalidAttributeEnd,
|
||||
},
|
||||
.{
|
||||
});
|
||||
try testCase(.{
|
||||
// size is in pax attribute
|
||||
.data = @embedFile("testdata/pax-pos-size-file.tar"),
|
||||
.files = &[_]Case.File{
|
||||
|
|
@ -119,8 +160,8 @@ const cases = [_]Case{
|
|||
.chksums = &[_][]const u8{
|
||||
"0afb597b283fe61b5d4879669a350556",
|
||||
},
|
||||
},
|
||||
.{
|
||||
});
|
||||
try testCase(.{
|
||||
// has pax records which we are not interested in
|
||||
.data = @embedFile("testdata/pax-records.tar"),
|
||||
.files = &[_]Case.File{
|
||||
|
|
@ -128,8 +169,8 @@ const cases = [_]Case{
|
|||
.name = "file",
|
||||
},
|
||||
},
|
||||
},
|
||||
.{
|
||||
});
|
||||
try testCase(.{
|
||||
// has global records which we are ignoring
|
||||
.data = @embedFile("testdata/pax-global-records.tar"),
|
||||
.files = &[_]Case.File{
|
||||
|
|
@ -146,8 +187,8 @@ const cases = [_]Case{
|
|||
.name = "file4",
|
||||
},
|
||||
},
|
||||
},
|
||||
.{
|
||||
});
|
||||
try testCase(.{
|
||||
.data = @embedFile("testdata/nil-uid.tar"),
|
||||
.files = &[_]Case.File{
|
||||
.{
|
||||
|
|
@ -160,8 +201,8 @@ const cases = [_]Case{
|
|||
.chksums = &[_][]const u8{
|
||||
"08d504674115e77a67244beac19668f5",
|
||||
},
|
||||
},
|
||||
.{
|
||||
});
|
||||
try testCase(.{
|
||||
// has xattrs and pax records which we are ignoring
|
||||
.data = @embedFile("testdata/xattrs.tar"),
|
||||
.files = &[_]Case.File{
|
||||
|
|
@ -182,23 +223,14 @@ const cases = [_]Case{
|
|||
"e38b27eaccb4391bdec553a7f3ae6b2f",
|
||||
"c65bd2e50a56a2138bf1716f2fd56fe9",
|
||||
},
|
||||
},
|
||||
.{
|
||||
.data = @embedFile("testdata/gnu-multi-hdrs.tar"),
|
||||
.files = &[_]Case.File{
|
||||
.{
|
||||
.name = "GNU2/GNU2/long-path-name",
|
||||
.link_name = "GNU4/GNU4/long-linkpath-name",
|
||||
.kind = .sym_link,
|
||||
},
|
||||
},
|
||||
},
|
||||
.{
|
||||
});
|
||||
try testCase(gnu_multi_headers_case);
|
||||
try testCase(.{
|
||||
// has gnu type D (directory) and S (sparse) blocks
|
||||
.data = @embedFile("testdata/gnu-incremental.tar"),
|
||||
.err = error.TarUnsupportedHeader,
|
||||
},
|
||||
.{
|
||||
});
|
||||
try testCase(.{
|
||||
// should use values only from last pax header
|
||||
.data = @embedFile("testdata/pax-multi-hdrs.tar"),
|
||||
.files = &[_]Case.File{
|
||||
|
|
@ -208,8 +240,8 @@ const cases = [_]Case{
|
|||
.kind = .sym_link,
|
||||
},
|
||||
},
|
||||
},
|
||||
.{
|
||||
});
|
||||
try testCase(.{
|
||||
.data = @embedFile("testdata/gnu-long-nul.tar"),
|
||||
.files = &[_]Case.File{
|
||||
.{
|
||||
|
|
@ -217,8 +249,8 @@ const cases = [_]Case{
|
|||
.mode = 0o644,
|
||||
},
|
||||
},
|
||||
},
|
||||
.{
|
||||
});
|
||||
try testCase(.{
|
||||
.data = @embedFile("testdata/gnu-utf8.tar"),
|
||||
.files = &[_]Case.File{
|
||||
.{
|
||||
|
|
@ -226,8 +258,8 @@ const cases = [_]Case{
|
|||
.mode = 0o644,
|
||||
},
|
||||
},
|
||||
},
|
||||
.{
|
||||
});
|
||||
try testCase(.{
|
||||
.data = @embedFile("testdata/gnu-not-utf8.tar"),
|
||||
.files = &[_]Case.File{
|
||||
.{
|
||||
|
|
@ -235,33 +267,33 @@ const cases = [_]Case{
|
|||
.mode = 0o644,
|
||||
},
|
||||
},
|
||||
},
|
||||
.{
|
||||
});
|
||||
try testCase(.{
|
||||
// null in pax key
|
||||
.data = @embedFile("testdata/pax-nul-xattrs.tar"),
|
||||
.err = error.PaxNullInKeyword,
|
||||
},
|
||||
.{
|
||||
});
|
||||
try testCase(.{
|
||||
.data = @embedFile("testdata/pax-nul-path.tar"),
|
||||
.err = error.PaxNullInValue,
|
||||
},
|
||||
.{
|
||||
});
|
||||
try testCase(.{
|
||||
.data = @embedFile("testdata/neg-size.tar"),
|
||||
.err = error.TarHeader,
|
||||
},
|
||||
.{
|
||||
});
|
||||
try testCase(.{
|
||||
.data = @embedFile("testdata/issue10968.tar"),
|
||||
.err = error.TarHeader,
|
||||
},
|
||||
.{
|
||||
});
|
||||
try testCase(.{
|
||||
.data = @embedFile("testdata/issue11169.tar"),
|
||||
.err = error.TarHeader,
|
||||
},
|
||||
.{
|
||||
});
|
||||
try testCase(.{
|
||||
.data = @embedFile("testdata/issue12435.tar"),
|
||||
.err = error.TarHeaderChksum,
|
||||
},
|
||||
.{
|
||||
});
|
||||
try testCase(.{
|
||||
// has magic with space at end instead of null
|
||||
.data = @embedFile("testdata/invalid-go17.tar"),
|
||||
.files = &[_]Case.File{
|
||||
|
|
@ -269,8 +301,8 @@ const cases = [_]Case{
|
|||
.name = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
.{
|
||||
});
|
||||
try testCase(.{
|
||||
.data = @embedFile("testdata/ustar-file-devs.tar"),
|
||||
.files = &[_]Case.File{
|
||||
.{
|
||||
|
|
@ -278,17 +310,9 @@ const cases = [_]Case{
|
|||
.mode = 0o644,
|
||||
},
|
||||
},
|
||||
},
|
||||
.{
|
||||
.data = @embedFile("testdata/trailing-slash.tar"),
|
||||
.files = &[_]Case.File{
|
||||
.{
|
||||
.name = "123456789/" ** 30,
|
||||
.kind = .directory,
|
||||
},
|
||||
},
|
||||
},
|
||||
.{
|
||||
});
|
||||
try testCase(trailing_slash_case);
|
||||
try testCase(.{
|
||||
// Has size in gnu extended format. To represent size bigger than 8 GB.
|
||||
.data = @embedFile("testdata/writer-big.tar"),
|
||||
.files = &[_]Case.File{
|
||||
|
|
@ -299,63 +323,29 @@ const cases = [_]Case{
|
|||
.mode = 0o640,
|
||||
},
|
||||
},
|
||||
},
|
||||
.{
|
||||
// Size in gnu extended format, and name in pax attribute.
|
||||
.data = @embedFile("testdata/writer-big-long.tar"),
|
||||
.files = &[_]Case.File{
|
||||
.{
|
||||
.name = "longname/" ** 15 ++ "16gig.txt",
|
||||
.size = 16 * 1024 * 1024 * 1024,
|
||||
.mode = 0o644,
|
||||
.truncated = true,
|
||||
},
|
||||
},
|
||||
},
|
||||
.{
|
||||
.data = @embedFile("testdata/fuzz1.tar"),
|
||||
.err = error.TarInsufficientBuffer,
|
||||
},
|
||||
.{
|
||||
});
|
||||
try testCase(writer_big_long_case);
|
||||
try testCase(fuzz1_case);
|
||||
try testCase(.{
|
||||
.data = @embedFile("testdata/fuzz2.tar"),
|
||||
.err = error.PaxSizeAttrOverflow,
|
||||
},
|
||||
};
|
||||
|
||||
// used in test to calculate file chksum
|
||||
const Md5Writer = struct {
|
||||
h: std.crypto.hash.Md5 = std.crypto.hash.Md5.init(.{}),
|
||||
|
||||
pub fn writeAll(self: *Md5Writer, buf: []const u8) !void {
|
||||
self.h.update(buf);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn writeByte(self: *Md5Writer, byte: u8) !void {
|
||||
self.h.update(&[_]u8{byte});
|
||||
}
|
||||
|
||||
pub fn chksum(self: *Md5Writer) [32]u8 {
|
||||
var s = [_]u8{0} ** 16;
|
||||
self.h.final(&s);
|
||||
return std.fmt.bytesToHex(s, .lower);
|
||||
}
|
||||
};
|
||||
|
||||
test "run test cases" {
|
||||
fn testCase(case: Case) !void {
|
||||
var file_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
|
||||
var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
|
||||
|
||||
for (cases) |case| {
|
||||
var fsb = std.io.fixedBufferStream(case.data);
|
||||
var iter = tar.iterator(fsb.reader(), .{
|
||||
var br: std.io.Reader = .fixed(case.data);
|
||||
var it: tar.Iterator = .init(&br, .{
|
||||
.file_name_buffer = &file_name_buffer,
|
||||
.link_name_buffer = &link_name_buffer,
|
||||
});
|
||||
var i: usize = 0;
|
||||
while (iter.next() catch |err| {
|
||||
while (it.next() catch |err| {
|
||||
if (case.err) |e| {
|
||||
try testing.expectEqual(e, err);
|
||||
continue;
|
||||
return;
|
||||
} else {
|
||||
return err;
|
||||
}
|
||||
|
|
@ -368,30 +358,37 @@ test "run test cases" {
|
|||
try testing.expectEqualStrings(expected.link_name, actual.link_name);
|
||||
|
||||
if (case.chksums.len > i) {
|
||||
var md5writer = Md5Writer{};
|
||||
try actual.writeAll(&md5writer);
|
||||
const chksum = md5writer.chksum();
|
||||
var aw: std.Io.Writer.Allocating = .init(std.testing.allocator);
|
||||
defer aw.deinit();
|
||||
try it.streamRemaining(actual, &aw.writer);
|
||||
const chksum = std.fmt.bytesToHex(std.crypto.hash.Md5.hashResult(aw.getWritten()), .lower);
|
||||
try testing.expectEqualStrings(case.chksums[i], &chksum);
|
||||
} else {
|
||||
if (expected.truncated) {
|
||||
iter.unread_file_bytes = 0;
|
||||
it.unread_file_bytes = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
try testing.expectEqual(case.files.len, i);
|
||||
}
|
||||
}
|
||||
|
||||
test "pax/gnu long names with small buffer" {
|
||||
try testLongNameCase(gnu_multi_headers_case);
|
||||
try testLongNameCase(trailing_slash_case);
|
||||
try testLongNameCase(.{
|
||||
.data = @embedFile("testdata/fuzz1.tar"),
|
||||
.err = error.TarInsufficientBuffer,
|
||||
});
|
||||
}
|
||||
|
||||
fn testLongNameCase(case: Case) !void {
|
||||
// should fail with insufficient buffer error
|
||||
|
||||
var min_file_name_buffer: [256]u8 = undefined;
|
||||
var min_link_name_buffer: [100]u8 = undefined;
|
||||
const long_name_cases = [_]Case{ cases[11], cases[25], cases[28] };
|
||||
|
||||
for (long_name_cases) |case| {
|
||||
var fsb = std.io.fixedBufferStream(case.data);
|
||||
var iter = tar.iterator(fsb.reader(), .{
|
||||
var br: std.io.Reader = .fixed(case.data);
|
||||
var iter: tar.Iterator = .init(&br, .{
|
||||
.file_name_buffer = &min_file_name_buffer,
|
||||
.link_name_buffer = &min_link_name_buffer,
|
||||
});
|
||||
|
|
@ -405,14 +402,13 @@ test "pax/gnu long names with small buffer" {
|
|||
try testing.expect(iter_err != null);
|
||||
try testing.expectEqual(error.TarInsufficientBuffer, iter_err.?);
|
||||
}
|
||||
}
|
||||
|
||||
test "insufficient buffer in Header name filed" {
|
||||
var min_file_name_buffer: [9]u8 = undefined;
|
||||
var min_link_name_buffer: [100]u8 = undefined;
|
||||
|
||||
var fsb = std.io.fixedBufferStream(cases[0].data);
|
||||
var iter = tar.iterator(fsb.reader(), .{
|
||||
var br: std.io.Reader = .fixed(gnu_case.data);
|
||||
var iter: tar.Iterator = .init(&br, .{
|
||||
.file_name_buffer = &min_file_name_buffer,
|
||||
.link_name_buffer = &min_link_name_buffer,
|
||||
});
|
||||
|
|
@ -466,21 +462,21 @@ test "should not overwrite existing file" {
|
|||
// This ensures that file is not overwritten.
|
||||
//
|
||||
const data = @embedFile("testdata/overwrite_file.tar");
|
||||
var fsb = std.io.fixedBufferStream(data);
|
||||
var r: std.io.Reader = .fixed(data);
|
||||
|
||||
// Unpack with strip_components = 1 should fail
|
||||
var root = std.testing.tmpDir(.{});
|
||||
defer root.cleanup();
|
||||
try testing.expectError(
|
||||
error.PathAlreadyExists,
|
||||
tar.pipeToFileSystem(root.dir, fsb.reader(), .{ .mode_mode = .ignore, .strip_components = 1 }),
|
||||
tar.pipeToFileSystem(root.dir, &r, .{ .mode_mode = .ignore, .strip_components = 1 }),
|
||||
);
|
||||
|
||||
// Unpack with strip_components = 0 should pass
|
||||
fsb.reset();
|
||||
r = .fixed(data);
|
||||
var root2 = std.testing.tmpDir(.{});
|
||||
defer root2.cleanup();
|
||||
try tar.pipeToFileSystem(root2.dir, fsb.reader(), .{ .mode_mode = .ignore, .strip_components = 0 });
|
||||
try tar.pipeToFileSystem(root2.dir, &r, .{ .mode_mode = .ignore, .strip_components = 0 });
|
||||
}
|
||||
|
||||
test "case sensitivity" {
|
||||
|
|
@ -494,12 +490,12 @@ test "case sensitivity" {
|
|||
// 18089/alacritty/Darkermatrix.yml
|
||||
//
|
||||
const data = @embedFile("testdata/18089.tar");
|
||||
var fsb = std.io.fixedBufferStream(data);
|
||||
var r: std.io.Reader = .fixed(data);
|
||||
|
||||
var root = std.testing.tmpDir(.{});
|
||||
defer root.cleanup();
|
||||
|
||||
tar.pipeToFileSystem(root.dir, fsb.reader(), .{ .mode_mode = .ignore, .strip_components = 1 }) catch |err| {
|
||||
tar.pipeToFileSystem(root.dir, &r, .{ .mode_mode = .ignore, .strip_components = 1 }) catch |err| {
|
||||
// on case insensitive fs we fail on overwrite existing file
|
||||
try testing.expectEqual(error.PathAlreadyExists, err);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,497 +0,0 @@
|
|||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const testing = std.testing;
|
||||
|
||||
/// Creates tar Writer which will write tar content to the `underlying_writer`.
|
||||
/// Use setRoot to nest all following entries under single root. If file don't
|
||||
/// fit into posix header (name+prefix: 100+155 bytes) gnu extented header will
|
||||
/// be used for long names. Options enables setting file premission mode and
|
||||
/// mtime. Default is to use current time for mtime and 0o664 for file mode.
|
||||
pub fn writer(underlying_writer: anytype) Writer(@TypeOf(underlying_writer)) {
|
||||
return .{ .underlying_writer = underlying_writer };
|
||||
}
|
||||
|
||||
pub fn Writer(comptime WriterType: type) type {
|
||||
return struct {
|
||||
const block_size = @sizeOf(Header);
|
||||
const empty_block: [block_size]u8 = [_]u8{0} ** block_size;
|
||||
|
||||
/// Options for writing file/dir/link. If left empty 0o664 is used for
|
||||
/// file mode and current time for mtime.
|
||||
pub const Options = struct {
|
||||
/// File system permission mode.
|
||||
mode: u32 = 0,
|
||||
/// File system modification time.
|
||||
mtime: u64 = 0,
|
||||
};
|
||||
const Self = @This();
|
||||
|
||||
underlying_writer: WriterType,
|
||||
prefix: []const u8 = "",
|
||||
mtime_now: u64 = 0,
|
||||
|
||||
/// Sets prefix for all other write* method paths.
|
||||
pub fn setRoot(self: *Self, root: []const u8) !void {
|
||||
if (root.len > 0)
|
||||
try self.writeDir(root, .{});
|
||||
|
||||
self.prefix = root;
|
||||
}
|
||||
|
||||
/// Writes directory.
|
||||
pub fn writeDir(self: *Self, sub_path: []const u8, opt: Options) !void {
|
||||
try self.writeHeader(.directory, sub_path, "", 0, opt);
|
||||
}
|
||||
|
||||
/// Writes file system file.
|
||||
pub fn writeFile(self: *Self, sub_path: []const u8, file: std.fs.File) !void {
|
||||
const stat = try file.stat();
|
||||
const mtime: u64 = @intCast(@divFloor(stat.mtime, std.time.ns_per_s));
|
||||
|
||||
var header = Header{};
|
||||
try self.setPath(&header, sub_path);
|
||||
try header.setSize(stat.size);
|
||||
try header.setMtime(mtime);
|
||||
try header.write(self.underlying_writer);
|
||||
|
||||
try self.underlying_writer.writeFile(file);
|
||||
try self.writePadding(stat.size);
|
||||
}
|
||||
|
||||
/// Writes file reading file content from `reader`. Number of bytes in
|
||||
/// reader must be equal to `size`.
|
||||
pub fn writeFileStream(self: *Self, sub_path: []const u8, size: usize, reader: anytype, opt: Options) !void {
|
||||
try self.writeHeader(.regular, sub_path, "", @intCast(size), opt);
|
||||
|
||||
var counting_reader = std.io.countingReader(reader);
|
||||
var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
|
||||
try fifo.pump(counting_reader.reader(), self.underlying_writer);
|
||||
if (counting_reader.bytes_read != size) return error.WrongReaderSize;
|
||||
try self.writePadding(size);
|
||||
}
|
||||
|
||||
/// Writes file using bytes buffer `content` for size and file content.
|
||||
pub fn writeFileBytes(self: *Self, sub_path: []const u8, content: []const u8, opt: Options) !void {
|
||||
try self.writeHeader(.regular, sub_path, "", @intCast(content.len), opt);
|
||||
try self.underlying_writer.writeAll(content);
|
||||
try self.writePadding(content.len);
|
||||
}
|
||||
|
||||
/// Writes symlink.
|
||||
pub fn writeLink(self: *Self, sub_path: []const u8, link_name: []const u8, opt: Options) !void {
|
||||
try self.writeHeader(.symbolic_link, sub_path, link_name, 0, opt);
|
||||
}
|
||||
|
||||
/// Writes fs.Dir.WalkerEntry. Uses `mtime` from file system entry and
|
||||
/// default for entry mode .
|
||||
pub fn writeEntry(self: *Self, entry: std.fs.Dir.Walker.Entry) !void {
|
||||
switch (entry.kind) {
|
||||
.directory => {
|
||||
try self.writeDir(entry.path, .{ .mtime = try entryMtime(entry) });
|
||||
},
|
||||
.file => {
|
||||
var file = try entry.dir.openFile(entry.basename, .{});
|
||||
defer file.close();
|
||||
try self.writeFile(entry.path, file);
|
||||
},
|
||||
.sym_link => {
|
||||
var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
|
||||
const link_name = try entry.dir.readLink(entry.basename, &link_name_buffer);
|
||||
try self.writeLink(entry.path, link_name, .{ .mtime = try entryMtime(entry) });
|
||||
},
|
||||
else => {
|
||||
return error.UnsupportedWalkerEntryKind;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn writeHeader(
|
||||
self: *Self,
|
||||
typeflag: Header.FileType,
|
||||
sub_path: []const u8,
|
||||
link_name: []const u8,
|
||||
size: u64,
|
||||
opt: Options,
|
||||
) !void {
|
||||
var header = Header.init(typeflag);
|
||||
try self.setPath(&header, sub_path);
|
||||
try header.setSize(size);
|
||||
try header.setMtime(if (opt.mtime != 0) opt.mtime else self.mtimeNow());
|
||||
if (opt.mode != 0)
|
||||
try header.setMode(opt.mode);
|
||||
if (typeflag == .symbolic_link)
|
||||
header.setLinkname(link_name) catch |err| switch (err) {
|
||||
error.NameTooLong => try self.writeExtendedHeader(.gnu_long_link, &.{link_name}),
|
||||
else => return err,
|
||||
};
|
||||
try header.write(self.underlying_writer);
|
||||
}
|
||||
|
||||
fn mtimeNow(self: *Self) u64 {
|
||||
if (self.mtime_now == 0)
|
||||
self.mtime_now = @intCast(std.time.timestamp());
|
||||
return self.mtime_now;
|
||||
}
|
||||
|
||||
fn entryMtime(entry: std.fs.Dir.Walker.Entry) !u64 {
|
||||
const stat = try entry.dir.statFile(entry.basename);
|
||||
return @intCast(@divFloor(stat.mtime, std.time.ns_per_s));
|
||||
}
|
||||
|
||||
/// Writes path in posix header, if don't fit (in name+prefix; 100+155
|
||||
/// bytes) writes it in gnu extended header.
|
||||
fn setPath(self: *Self, header: *Header, sub_path: []const u8) !void {
|
||||
header.setPath(self.prefix, sub_path) catch |err| switch (err) {
|
||||
error.NameTooLong => {
|
||||
// write extended header
|
||||
const buffers: []const []const u8 = if (self.prefix.len == 0)
|
||||
&.{sub_path}
|
||||
else
|
||||
&.{ self.prefix, "/", sub_path };
|
||||
try self.writeExtendedHeader(.gnu_long_name, buffers);
|
||||
},
|
||||
else => return err,
|
||||
};
|
||||
}
|
||||
|
||||
/// Writes gnu extended header: gnu_long_name or gnu_long_link.
|
||||
fn writeExtendedHeader(self: *Self, typeflag: Header.FileType, buffers: []const []const u8) !void {
|
||||
var len: usize = 0;
|
||||
for (buffers) |buf|
|
||||
len += buf.len;
|
||||
|
||||
var header = Header.init(typeflag);
|
||||
try header.setSize(len);
|
||||
try header.write(self.underlying_writer);
|
||||
for (buffers) |buf|
|
||||
try self.underlying_writer.writeAll(buf);
|
||||
try self.writePadding(len);
|
||||
}
|
||||
|
||||
fn writePadding(self: *Self, bytes: u64) !void {
|
||||
const pos: usize = @intCast(bytes % block_size);
|
||||
if (pos == 0) return;
|
||||
try self.underlying_writer.writeAll(empty_block[pos..]);
|
||||
}
|
||||
|
||||
/// Tar should finish with two zero blocks, but 'reasonable system must
|
||||
/// not assume that such a block exists when reading an archive' (from
|
||||
/// reference). In practice it is safe to skip this finish.
|
||||
pub fn finish(self: *Self) !void {
|
||||
try self.underlying_writer.writeAll(&empty_block);
|
||||
try self.underlying_writer.writeAll(&empty_block);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// A struct that is exactly 512 bytes and matches tar file format. This is
|
||||
/// intended to be used for outputting tar files; for parsing there is
|
||||
/// `std.tar.Header`.
|
||||
const Header = extern struct {
|
||||
// This struct was originally copied from
|
||||
// https://github.com/mattnite/tar/blob/main/src/main.zig which is MIT
|
||||
// licensed.
|
||||
//
|
||||
// The name, linkname, magic, uname, and gname are null-terminated character
|
||||
// strings. All other fields are zero-filled octal numbers in ASCII. Each
|
||||
// numeric field of width w contains w minus 1 digits, and a null.
|
||||
// Reference: https://www.gnu.org/software/tar/manual/html_node/Standard.html
|
||||
// POSIX header: byte offset
|
||||
name: [100]u8 = [_]u8{0} ** 100, // 0
|
||||
mode: [7:0]u8 = default_mode.file, // 100
|
||||
uid: [7:0]u8 = [_:0]u8{0} ** 7, // unused 108
|
||||
gid: [7:0]u8 = [_:0]u8{0} ** 7, // unused 116
|
||||
size: [11:0]u8 = [_:0]u8{'0'} ** 11, // 124
|
||||
mtime: [11:0]u8 = [_:0]u8{'0'} ** 11, // 136
|
||||
checksum: [7:0]u8 = [_:0]u8{' '} ** 7, // 148
|
||||
typeflag: FileType = .regular, // 156
|
||||
linkname: [100]u8 = [_]u8{0} ** 100, // 157
|
||||
magic: [6]u8 = [_]u8{ 'u', 's', 't', 'a', 'r', 0 }, // 257
|
||||
version: [2]u8 = [_]u8{ '0', '0' }, // 263
|
||||
uname: [32]u8 = [_]u8{0} ** 32, // unused 265
|
||||
gname: [32]u8 = [_]u8{0} ** 32, // unused 297
|
||||
devmajor: [7:0]u8 = [_:0]u8{0} ** 7, // unused 329
|
||||
devminor: [7:0]u8 = [_:0]u8{0} ** 7, // unused 337
|
||||
prefix: [155]u8 = [_]u8{0} ** 155, // 345
|
||||
pad: [12]u8 = [_]u8{0} ** 12, // unused 500
|
||||
|
||||
pub const FileType = enum(u8) {
|
||||
regular = '0',
|
||||
symbolic_link = '2',
|
||||
directory = '5',
|
||||
gnu_long_name = 'L',
|
||||
gnu_long_link = 'K',
|
||||
};
|
||||
|
||||
const default_mode = struct {
|
||||
const file = [_:0]u8{ '0', '0', '0', '0', '6', '6', '4' }; // 0o664
|
||||
const dir = [_:0]u8{ '0', '0', '0', '0', '7', '7', '5' }; // 0o775
|
||||
const sym_link = [_:0]u8{ '0', '0', '0', '0', '7', '7', '7' }; // 0o777
|
||||
const other = [_:0]u8{ '0', '0', '0', '0', '0', '0', '0' }; // 0o000
|
||||
};
|
||||
|
||||
pub fn init(typeflag: FileType) Header {
|
||||
return .{
|
||||
.typeflag = typeflag,
|
||||
.mode = switch (typeflag) {
|
||||
.directory => default_mode.dir,
|
||||
.symbolic_link => default_mode.sym_link,
|
||||
.regular => default_mode.file,
|
||||
else => default_mode.other,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn setSize(self: *Header, size: u64) !void {
|
||||
try octal(&self.size, size);
|
||||
}
|
||||
|
||||
fn octal(buf: []u8, value: u64) !void {
|
||||
var remainder: u64 = value;
|
||||
var pos: usize = buf.len;
|
||||
while (remainder > 0 and pos > 0) {
|
||||
pos -= 1;
|
||||
const c: u8 = @as(u8, @intCast(remainder % 8)) + '0';
|
||||
buf[pos] = c;
|
||||
remainder /= 8;
|
||||
if (pos == 0 and remainder > 0) return error.OctalOverflow;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setMode(self: *Header, mode: u32) !void {
|
||||
try octal(&self.mode, mode);
|
||||
}
|
||||
|
||||
// Integer number of seconds since January 1, 1970, 00:00 Coordinated Universal Time.
|
||||
// mtime == 0 will use current time
|
||||
pub fn setMtime(self: *Header, mtime: u64) !void {
|
||||
try octal(&self.mtime, mtime);
|
||||
}
|
||||
|
||||
pub fn updateChecksum(self: *Header) !void {
|
||||
var checksum: usize = ' '; // other 7 self.checksum bytes are initialized to ' '
|
||||
for (std.mem.asBytes(self)) |val|
|
||||
checksum += val;
|
||||
try octal(&self.checksum, checksum);
|
||||
}
|
||||
|
||||
pub fn write(self: *Header, output_writer: anytype) !void {
|
||||
try self.updateChecksum();
|
||||
try output_writer.writeAll(std.mem.asBytes(self));
|
||||
}
|
||||
|
||||
pub fn setLinkname(self: *Header, link: []const u8) !void {
|
||||
if (link.len > self.linkname.len) return error.NameTooLong;
|
||||
@memcpy(self.linkname[0..link.len], link);
|
||||
}
|
||||
|
||||
pub fn setPath(self: *Header, prefix: []const u8, sub_path: []const u8) !void {
|
||||
const max_prefix = self.prefix.len;
|
||||
const max_name = self.name.len;
|
||||
const sep = std.fs.path.sep_posix;
|
||||
|
||||
if (prefix.len + sub_path.len > max_name + max_prefix or prefix.len > max_prefix)
|
||||
return error.NameTooLong;
|
||||
|
||||
// both fit into name
|
||||
if (prefix.len > 0 and prefix.len + sub_path.len < max_name) {
|
||||
@memcpy(self.name[0..prefix.len], prefix);
|
||||
self.name[prefix.len] = sep;
|
||||
@memcpy(self.name[prefix.len + 1 ..][0..sub_path.len], sub_path);
|
||||
return;
|
||||
}
|
||||
|
||||
// sub_path fits into name
|
||||
// there is no prefix or prefix fits into prefix
|
||||
if (sub_path.len <= max_name) {
|
||||
@memcpy(self.name[0..sub_path.len], sub_path);
|
||||
@memcpy(self.prefix[0..prefix.len], prefix);
|
||||
return;
|
||||
}
|
||||
|
||||
if (prefix.len > 0) {
|
||||
@memcpy(self.prefix[0..prefix.len], prefix);
|
||||
self.prefix[prefix.len] = sep;
|
||||
}
|
||||
const prefix_pos = if (prefix.len > 0) prefix.len + 1 else 0;
|
||||
|
||||
// add as much to prefix as you can, must split at /
|
||||
const prefix_remaining = max_prefix - prefix_pos;
|
||||
if (std.mem.lastIndexOf(u8, sub_path[0..@min(prefix_remaining, sub_path.len)], &.{'/'})) |sep_pos| {
|
||||
@memcpy(self.prefix[prefix_pos..][0..sep_pos], sub_path[0..sep_pos]);
|
||||
if ((sub_path.len - sep_pos - 1) > max_name) return error.NameTooLong;
|
||||
@memcpy(self.name[0..][0 .. sub_path.len - sep_pos - 1], sub_path[sep_pos + 1 ..]);
|
||||
return;
|
||||
}
|
||||
|
||||
return error.NameTooLong;
|
||||
}
|
||||
|
||||
comptime {
|
||||
assert(@sizeOf(Header) == 512);
|
||||
}
|
||||
|
||||
test setPath {
|
||||
const cases = [_]struct {
|
||||
in: []const []const u8,
|
||||
out: []const []const u8,
|
||||
}{
|
||||
.{
|
||||
.in = &.{ "", "123456789" },
|
||||
.out = &.{ "", "123456789" },
|
||||
},
|
||||
// can fit into name
|
||||
.{
|
||||
.in = &.{ "prefix", "sub_path" },
|
||||
.out = &.{ "", "prefix/sub_path" },
|
||||
},
|
||||
// no more both fits into name
|
||||
.{
|
||||
.in = &.{ "prefix", "0123456789/" ** 8 ++ "basename" },
|
||||
.out = &.{ "prefix", "0123456789/" ** 8 ++ "basename" },
|
||||
},
|
||||
// put as much as you can into prefix the rest goes into name
|
||||
.{
|
||||
.in = &.{ "prefix", "0123456789/" ** 10 ++ "basename" },
|
||||
.out = &.{ "prefix/" ++ "0123456789/" ** 9 ++ "0123456789", "basename" },
|
||||
},
|
||||
|
||||
.{
|
||||
.in = &.{ "prefix", "0123456789/" ** 15 ++ "basename" },
|
||||
.out = &.{ "prefix/" ++ "0123456789/" ** 12 ++ "0123456789", "0123456789/0123456789/basename" },
|
||||
},
|
||||
.{
|
||||
.in = &.{ "prefix", "0123456789/" ** 21 ++ "basename" },
|
||||
.out = &.{ "prefix/" ++ "0123456789/" ** 12 ++ "0123456789", "0123456789/" ** 8 ++ "basename" },
|
||||
},
|
||||
.{
|
||||
.in = &.{ "", "012345678/" ** 10 ++ "foo" },
|
||||
.out = &.{ "012345678/" ** 9 ++ "012345678", "foo" },
|
||||
},
|
||||
};
|
||||
|
||||
for (cases) |case| {
|
||||
var header = Header.init(.regular);
|
||||
try header.setPath(case.in[0], case.in[1]);
|
||||
try testing.expectEqualStrings(case.out[0], str(&header.prefix));
|
||||
try testing.expectEqualStrings(case.out[1], str(&header.name));
|
||||
}
|
||||
|
||||
const error_cases = [_]struct {
|
||||
in: []const []const u8,
|
||||
}{
|
||||
// basename can't fit into name (106 characters)
|
||||
.{ .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" } },
|
||||
// cant fit into 255 + sep
|
||||
.{ .in = &.{ "prefix", "0123456789/" ** 22 ++ "basename" } },
|
||||
// can fit but sub_path can't be split (there is no separator)
|
||||
.{ .in = &.{ "prefix", "0123456789" ** 10 ++ "a" } },
|
||||
.{ .in = &.{ "prefix", "0123456789" ** 14 ++ "basename" } },
|
||||
};
|
||||
|
||||
for (error_cases) |case| {
|
||||
var header = Header.init(.regular);
|
||||
try testing.expectError(
|
||||
error.NameTooLong,
|
||||
header.setPath(case.in[0], case.in[1]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Breaks string on first null character.
|
||||
fn str(s: []const u8) []const u8 {
|
||||
for (s, 0..) |c, i| {
|
||||
if (c == 0) return s[0..i];
|
||||
}
|
||||
return s;
|
||||
}
|
||||
};
|
||||
|
||||
test {
|
||||
_ = Header;
|
||||
}
|
||||
|
||||
test "write files" {
|
||||
const files = [_]struct {
|
||||
path: []const u8,
|
||||
content: []const u8,
|
||||
}{
|
||||
.{ .path = "foo", .content = "bar" },
|
||||
.{ .path = "a12345678/" ** 10 ++ "foo", .content = "a" ** 511 },
|
||||
.{ .path = "b12345678/" ** 24 ++ "foo", .content = "b" ** 512 },
|
||||
.{ .path = "c12345678/" ** 25 ++ "foo", .content = "c" ** 513 },
|
||||
.{ .path = "d12345678/" ** 51 ++ "foo", .content = "d" ** 1025 },
|
||||
.{ .path = "e123456789" ** 11, .content = "e" },
|
||||
};
|
||||
|
||||
var file_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
|
||||
var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
|
||||
|
||||
// with root
|
||||
{
|
||||
const root = "root";
|
||||
|
||||
var output = std.ArrayList(u8).init(testing.allocator);
|
||||
defer output.deinit();
|
||||
var wrt = writer(output.writer());
|
||||
try wrt.setRoot(root);
|
||||
for (files) |file|
|
||||
try wrt.writeFileBytes(file.path, file.content, .{});
|
||||
|
||||
var input = std.io.fixedBufferStream(output.items);
|
||||
var iter = std.tar.iterator(
|
||||
input.reader(),
|
||||
.{ .file_name_buffer = &file_name_buffer, .link_name_buffer = &link_name_buffer },
|
||||
);
|
||||
|
||||
// first entry is directory with prefix
|
||||
{
|
||||
const actual = (try iter.next()).?;
|
||||
try testing.expectEqualStrings(root, actual.name);
|
||||
try testing.expectEqual(std.tar.FileKind.directory, actual.kind);
|
||||
}
|
||||
|
||||
var i: usize = 0;
|
||||
while (try iter.next()) |actual| {
|
||||
defer i += 1;
|
||||
const expected = files[i];
|
||||
try testing.expectEqualStrings(root, actual.name[0..root.len]);
|
||||
try testing.expectEqual('/', actual.name[root.len..][0]);
|
||||
try testing.expectEqualStrings(expected.path, actual.name[root.len + 1 ..]);
|
||||
|
||||
var content = std.ArrayList(u8).init(testing.allocator);
|
||||
defer content.deinit();
|
||||
try actual.writeAll(content.writer());
|
||||
try testing.expectEqualSlices(u8, expected.content, content.items);
|
||||
}
|
||||
}
|
||||
// without root
|
||||
{
|
||||
var output = std.ArrayList(u8).init(testing.allocator);
|
||||
defer output.deinit();
|
||||
var wrt = writer(output.writer());
|
||||
for (files) |file| {
|
||||
var content = std.io.fixedBufferStream(file.content);
|
||||
try wrt.writeFileStream(file.path, file.content.len, content.reader(), .{});
|
||||
}
|
||||
|
||||
var input = std.io.fixedBufferStream(output.items);
|
||||
var iter = std.tar.iterator(
|
||||
input.reader(),
|
||||
.{ .file_name_buffer = &file_name_buffer, .link_name_buffer = &link_name_buffer },
|
||||
);
|
||||
|
||||
var i: usize = 0;
|
||||
while (try iter.next()) |actual| {
|
||||
defer i += 1;
|
||||
const expected = files[i];
|
||||
try testing.expectEqualStrings(expected.path, actual.name);
|
||||
|
||||
var content = std.ArrayList(u8).init(testing.allocator);
|
||||
defer content.deinit();
|
||||
try actual.writeAll(content.writer());
|
||||
try testing.expectEqualSlices(u8, expected.content, content.items);
|
||||
}
|
||||
try wrt.finish();
|
||||
}
|
||||
}
|
||||
|
|
@ -4862,6 +4862,9 @@ fn docsCopyFallible(comp: *Compilation) anyerror!void {
|
|||
};
|
||||
defer tar_file.close();
|
||||
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var tar_file_writer = tar_file.writer(&buffer);
|
||||
|
||||
var seen_table: std.AutoArrayHashMapUnmanaged(*Package.Module, []const u8) = .empty;
|
||||
defer seen_table.deinit(comp.gpa);
|
||||
|
||||
|
|
@ -4871,7 +4874,7 @@ fn docsCopyFallible(comp: *Compilation) anyerror!void {
|
|||
var i: usize = 0;
|
||||
while (i < seen_table.count()) : (i += 1) {
|
||||
const mod = seen_table.keys()[i];
|
||||
try comp.docsCopyModule(mod, seen_table.values()[i], tar_file);
|
||||
try comp.docsCopyModule(mod, seen_table.values()[i], &tar_file_writer);
|
||||
|
||||
const deps = mod.deps.values();
|
||||
try seen_table.ensureUnusedCapacity(comp.gpa, deps.len);
|
||||
|
|
@ -4879,24 +4882,29 @@ fn docsCopyFallible(comp: *Compilation) anyerror!void {
|
|||
}
|
||||
}
|
||||
|
||||
fn docsCopyModule(comp: *Compilation, module: *Package.Module, name: []const u8, tar_file: fs.File) !void {
|
||||
fn docsCopyModule(
|
||||
comp: *Compilation,
|
||||
module: *Package.Module,
|
||||
name: []const u8,
|
||||
tar_file_writer: *fs.File.Writer,
|
||||
) !void {
|
||||
const root = module.root;
|
||||
var mod_dir = d: {
|
||||
const root_dir, const sub_path = root.openInfo(comp.dirs);
|
||||
break :d root_dir.openDir(sub_path, .{ .iterate = true });
|
||||
} catch |err| {
|
||||
return comp.lockAndSetMiscFailure(.docs_copy, "unable to open directory '{f}': {s}", .{
|
||||
root.fmt(comp), @errorName(err),
|
||||
});
|
||||
return comp.lockAndSetMiscFailure(.docs_copy, "unable to open directory '{f}': {t}", .{ root.fmt(comp), err });
|
||||
};
|
||||
defer mod_dir.close();
|
||||
|
||||
var walker = try mod_dir.walk(comp.gpa);
|
||||
defer walker.deinit();
|
||||
|
||||
var archiver = std.tar.writer(tar_file.deprecatedWriter().any());
|
||||
var archiver: std.tar.Writer = .{ .underlying_writer = &tar_file_writer.interface };
|
||||
archiver.prefix = name;
|
||||
|
||||
var buffer: [1024]u8 = undefined;
|
||||
|
||||
while (try walker.next()) |entry| {
|
||||
switch (entry.kind) {
|
||||
.file => {
|
||||
|
|
@ -4907,14 +4915,17 @@ fn docsCopyModule(comp: *Compilation, module: *Package.Module, name: []const u8,
|
|||
else => continue,
|
||||
}
|
||||
var file = mod_dir.openFile(entry.path, .{}) catch |err| {
|
||||
return comp.lockAndSetMiscFailure(.docs_copy, "unable to open '{f}{s}': {s}", .{
|
||||
root.fmt(comp), entry.path, @errorName(err),
|
||||
return comp.lockAndSetMiscFailure(.docs_copy, "unable to open {f}{s}: {t}", .{
|
||||
root.fmt(comp), entry.path, err,
|
||||
});
|
||||
};
|
||||
defer file.close();
|
||||
archiver.writeFile(entry.path, file) catch |err| {
|
||||
return comp.lockAndSetMiscFailure(.docs_copy, "unable to archive '{f}{s}': {s}", .{
|
||||
root.fmt(comp), entry.path, @errorName(err),
|
||||
const stat = try file.stat();
|
||||
var file_reader: fs.File.Reader = .initSize(file, &buffer, stat.size);
|
||||
|
||||
archiver.writeFile(entry.path, &file_reader, stat.mtime) catch |err| {
|
||||
return comp.lockAndSetMiscFailure(.docs_copy, "unable to archive {f}{s}: {t}", .{
|
||||
root.fmt(comp), entry.path, err,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
@ -4926,9 +4937,7 @@ fn workerDocsWasm(comp: *Compilation, parent_prog_node: std.Progress.Node) void
|
|||
|
||||
workerDocsWasmFallible(comp, prog_node) catch |err| switch (err) {
|
||||
error.SubCompilationFailed => return, // error reported already
|
||||
else => comp.lockAndSetMiscFailure(.docs_wasm, "unable to build autodocs: {s}", .{
|
||||
@errorName(err),
|
||||
}),
|
||||
else => comp.lockAndSetMiscFailure(.docs_wasm, "unable to build autodocs: {t}", .{err}),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1197,12 +1197,16 @@ fn unpackResource(
|
|||
};
|
||||
|
||||
switch (file_type) {
|
||||
.tar => return try unpackTarball(f, tmp_directory.handle, resource.reader()),
|
||||
.tar => {
|
||||
var adapter = resource.reader().adaptToNewApi();
|
||||
return unpackTarball(f, tmp_directory.handle, &adapter.new_interface);
|
||||
},
|
||||
.@"tar.gz" => {
|
||||
const reader = resource.reader();
|
||||
var br = std.io.bufferedReaderSize(std.crypto.tls.max_ciphertext_record_len, reader);
|
||||
var dcp = std.compress.gzip.decompressor(br.reader());
|
||||
return try unpackTarball(f, tmp_directory.handle, dcp.reader());
|
||||
var adapter = dcp.reader().adaptToNewApi();
|
||||
return try unpackTarball(f, tmp_directory.handle, &adapter.new_interface);
|
||||
},
|
||||
.@"tar.xz" => {
|
||||
const gpa = f.arena.child_allocator;
|
||||
|
|
@ -1215,7 +1219,8 @@ fn unpackResource(
|
|||
));
|
||||
};
|
||||
defer dcp.deinit();
|
||||
return try unpackTarball(f, tmp_directory.handle, dcp.reader());
|
||||
var adapter = dcp.reader().adaptToNewApi();
|
||||
return try unpackTarball(f, tmp_directory.handle, &adapter.new_interface);
|
||||
},
|
||||
.@"tar.zst" => {
|
||||
const window_size = std.compress.zstd.DecompressorOptions.default_window_buffer_len;
|
||||
|
|
@ -1225,7 +1230,8 @@ fn unpackResource(
|
|||
var dcp = std.compress.zstd.decompressor(br.reader(), .{
|
||||
.window_buffer = window_buffer,
|
||||
});
|
||||
return try unpackTarball(f, tmp_directory.handle, dcp.reader());
|
||||
var adapter = dcp.reader().adaptToNewApi();
|
||||
return try unpackTarball(f, tmp_directory.handle, &adapter.new_interface);
|
||||
},
|
||||
.git_pack => return unpackGitPack(f, tmp_directory.handle, &resource.git) catch |err| switch (err) {
|
||||
error.FetchFailed => return error.FetchFailed,
|
||||
|
|
@ -1239,7 +1245,7 @@ fn unpackResource(
|
|||
}
|
||||
}
|
||||
|
||||
fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: anytype) RunError!UnpackResult {
|
||||
fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: *std.Io.Reader) RunError!UnpackResult {
|
||||
const eb = &f.error_bundle;
|
||||
const arena = f.arena.allocator();
|
||||
|
||||
|
|
@ -1250,10 +1256,10 @@ fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: anytype) RunError!UnpackRes
|
|||
.strip_components = 0,
|
||||
.mode_mode = .ignore,
|
||||
.exclude_empty_directories = true,
|
||||
}) catch |err| return f.fail(f.location_tok, try eb.printString(
|
||||
"unable to unpack tarball to temporary directory: {s}",
|
||||
.{@errorName(err)},
|
||||
));
|
||||
}) catch |err| return f.fail(
|
||||
f.location_tok,
|
||||
try eb.printString("unable to unpack tarball to temporary directory: {t}", .{err}),
|
||||
);
|
||||
|
||||
var res: UnpackResult = .{ .root_dir = diagnostics.root_dir };
|
||||
if (diagnostics.errors.items.len > 0) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue