Skip to content

Commit 4788714

Browse files
committed
tar: add writer to public interface
1 parent 8722929 commit 4788714

File tree

2 files changed

+102
-85
lines changed

2 files changed

+102
-85
lines changed

lib/std/tar.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const assert = std.debug.assert;
2020
const testing = std.testing;
2121

2222
pub const output = @import("tar/output.zig");
23+
pub const writer = @import("tar/writer.zig").writer;
2324

2425
/// Provide this to receive detailed error messages.
2526
/// When this is provided, some errors which would otherwise be returned

lib/std/tar/writer.zig

Lines changed: 101 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,6 @@ pub fn writer(underlying_writer: anytype, prefix: []const u8) !Writer(@TypeOf(un
1313

1414
pub fn Writer(comptime WriterType: type) type {
1515
return struct {
16-
const default_mode = struct {
17-
const file = 0o664;
18-
const dir = 0o775;
19-
const sym_link = 0o777;
20-
};
2116
const block_size = @sizeOf(Header);
2217
const zero: [@sizeOf(Header)]u8 = .{0} ** @sizeOf(Header);
2318
pub const Options = struct {
@@ -28,7 +23,9 @@ pub fn Writer(comptime WriterType: type) type {
2823

2924
underlying_writer: WriterType,
3025
prefix: []const u8 = "",
26+
3127
block_buffer: [block_size]u8 = undefined,
28+
mtime_now: u64 = 0,
3229

3330
/// Tar will be written to the `underlying_writer`. All paths will be
3431
/// prefixed with `perfix` if it is not empty string.
@@ -37,33 +34,31 @@ pub fn Writer(comptime WriterType: type) type {
3734
.underlying_writer = underlying_writer,
3835
};
3936
if (prefix.len > 0) {
40-
try self.directory(prefix, .{});
37+
try self.addDir(prefix, .{});
4138
self.prefix = prefix;
4239
}
4340
return self;
4441
}
4542

4643
/// Writes directory. If options are omitted `default_mode.dir` is used
4744
/// for mode and current time for mtime.
48-
pub fn directory(self: *Self, sub_path: []const u8, opt: Options) !void {
49-
var header = Header.init();
50-
header.typeflag = .directory;
45+
pub fn addDir(self: *Self, sub_path: []const u8, opt: Options) !void {
46+
var header = Header.init(.directory);
5147
try self.setPath(&header, sub_path);
52-
try header.setMode(if (opt.mode == 0) default_mode.dir else opt.mode);
53-
try header.setMtime(opt.mtime);
48+
try self.setMtime(&header, opt.mtime);
49+
try header.setMode(opt.mode);
5450
try header.write(self.underlying_writer);
5551
}
5652

5753
/// Writes file. File content is read from `reader`. Number of bytes in
5854
/// reader must be equal to `size`. If options are omitted
5955
/// `default_mode.file` is used for mode and current time for mtime.
60-
pub fn file(self: *Self, sub_path: []const u8, size: usize, reader: anytype, opt: Options) !void {
61-
var header = Header.init();
62-
header.typeflag = .regular;
56+
pub fn addFile(self: *Self, sub_path: []const u8, size: usize, reader: anytype, opt: Options) !void {
57+
var header = Header.init(.regular);
6358
try self.setPath(&header, sub_path);
59+
try self.setMtime(&header, opt.mtime);
6460
try header.setSize(size);
65-
try header.setMode(if (opt.mode == 0) default_mode.file else opt.mode);
66-
try header.setMtime(opt.mtime);
61+
try header.setMode(opt.mode);
6762
try header.write(self.underlying_writer);
6863

6964
var written: usize = 0;
@@ -77,6 +72,58 @@ pub fn Writer(comptime WriterType: type) type {
7772
}
7873
}
7974

75+
fn setMtime(self: *Self, header: *Header, mtime: u64) !void {
76+
const mt = blk: {
77+
if (mtime == 0) {
78+
// use time now
79+
if (self.mtime_now == 0)
80+
self.mtime_now = @intCast(std.time.timestamp());
81+
break :blk self.mtime_now;
82+
}
83+
break :blk mtime;
84+
};
85+
try header.setMtime(mt);
86+
}
87+
88+
/// Writes symlink. If options are omitted `default_mode.sym_link` is
89+
/// used for mode and current time for mtime.
90+
pub fn addLink(self: *Self, sub_path: []const u8, link_name: []const u8, opt: Options) !void {
91+
var header = Header.init(.symbolic_link);
92+
try self.setPath(&header, sub_path);
93+
try self.setMtime(&header, opt.mtime);
94+
try header.setMode(opt.mode);
95+
header.setLinkname(link_name) catch |err| switch (err) {
96+
error.NameTooLong => try self.writeExtendedHeader(.gnu_long_link, &.{link_name}),
97+
else => return err,
98+
};
99+
try header.write(self.underlying_writer);
100+
}
101+
102+
/// Writes fs.Dir.WalkerEntry. Uses mtime from file system entry and
103+
/// defaults from `default_mode` for mode.
104+
pub fn addEntry(self: *Self, entry: std.fs.Dir.Walker.WalkerEntry) !void {
105+
const stat = try entry.dir.statFile(entry.basename);
106+
const mtime: u64 = @intCast(@divFloor(stat.mtime, std.time.ns_per_s));
107+
switch (entry.kind) {
108+
.directory => {
109+
try self.addDir(entry.path, .{ .mtime = mtime });
110+
},
111+
.file => {
112+
var file = try entry.dir.openFile(entry.basename, .{});
113+
defer file.close();
114+
try self.addFile(entry.path, stat.size, file.reader(), .{ .mtime = mtime });
115+
},
116+
.sym_link => {
117+
var link_name_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined;
118+
const link_name = try entry.dir.readLink(entry.basename, &link_name_buffer);
119+
try self.addLink(entry.path, link_name, .{ .mtime = mtime });
120+
},
121+
else => {
122+
return error.UnsupportedWalkerEntryKind;
123+
},
124+
}
125+
}
126+
80127
/// Writes path in posix header, if don't fit (in name+prefix; 100+155
81128
/// bytes) writes it in gnu extended header.
82129
fn setPath(self: *Self, header: *Header, sub_path: []const u8) !void {
@@ -93,70 +140,27 @@ pub fn Writer(comptime WriterType: type) type {
93140
};
94141
}
95142

96-
fn writeExtendedHeader(self: *Self, kind: Header.FileType, buffers: []const []const u8) !void {
143+
fn writeExtendedHeader(self: *Self, typeflag: Header.FileType, buffers: []const []const u8) !void {
97144
var len: usize = 0;
98145
for (buffers) |buf|
99146
len += buf.len;
100147

101-
var header = Header.init();
102-
header.typeflag = kind;
148+
var header = Header.init(typeflag);
103149
try header.setSize(len);
104150
try header.write(self.underlying_writer);
105151
for (buffers) |buf|
106152
try self.underlying_writer.writeAll(buf);
107-
try self.addPadding(len);
153+
try self.writePadding(len);
108154
}
109155

110-
fn addPadding(self: *Self, bytes: usize) !void {
156+
fn writePadding(self: *Self, bytes: usize) !void {
111157
const remainder = bytes % block_size;
112158
if (remainder == 0) return;
113159
const padding_bytes = block_size - remainder;
114160
@memset(self.block_buffer[0..padding_bytes], 0);
115161
try self.underlying_writer.writeAll(self.block_buffer[0..padding_bytes]);
116162
}
117163

118-
/// Writes symlink. If options are omitted `default_mode.sym_link` is
119-
/// used for mode and current time for mtime.
120-
pub fn symLink(self: *Self, sub_path: []const u8, link_name: []const u8, opt: Options) !void {
121-
var header = Header.init();
122-
header.typeflag = .symbolic_link;
123-
try self.setPath(&header, sub_path);
124-
header.setLinkname(link_name) catch |err| switch (err) {
125-
error.NameTooLong => try self.writeExtendedHeader(.gnu_long_link, &.{link_name}),
126-
else => return err,
127-
};
128-
try header.setMode(if (opt.mode == 0) default_mode.sym_link else opt.mode);
129-
try header.setMtime(opt.mtime);
130-
try header.write(self.underlying_writer);
131-
}
132-
133-
/// Writes fs.Dir.WalkerEntry. Uses mtime from file system entry and
134-
/// defaults from `default_mode` for mode.
135-
pub fn walkerEntry(self: *Self, entry: std.fs.Dir.Walker.WalkerEntry) !void {
136-
const stat = try entry.dir.statFile(entry.basename);
137-
const mtime: u64 = @intCast(@divFloor(stat.mtime, std.time.ns_per_s));
138-
switch (entry.kind) {
139-
.directory => {
140-
try self.directory(entry.path, .{ .mtime = mtime });
141-
},
142-
.file => {
143-
var f = try entry.dir.openFile(entry.basename, .{});
144-
defer f.close();
145-
try self.file(entry.path, stat.size, f.reader(), .{ .mtime = mtime });
146-
},
147-
.sym_link => {
148-
var f = try entry.dir.openFile(entry.basename, .{});
149-
defer f.close();
150-
var link_name_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined;
151-
const link_name = try entry.dir.readLink(entry.basename, &link_name_buffer);
152-
try self.symLink(entry.path, link_name, .{ .mtime = mtime });
153-
},
154-
else => {
155-
return error.UnsupportedWalkerEntryKind;
156-
},
157-
}
158-
}
159-
160164
/// Tar should finish with two zero blocks, but 'reasonable system must
161165
/// not assume that such a block exists when reading an archive' (from
162166
/// reference). In practice it is safe to skip this finish.
@@ -167,6 +171,12 @@ pub fn Writer(comptime WriterType: type) type {
167171
};
168172
}
169173

174+
const default_mode = struct {
175+
const file = 0o664;
176+
const dir = 0o775;
177+
const sym_link = 0o777;
178+
};
179+
170180
/// A struct that is exactly 512 bytes and matches tar file format. This is
171181
/// intended to be used for outputting tar files; for parsing there is
172182
/// `std.tar.Header`.
@@ -206,28 +216,34 @@ const Header = extern struct {
206216
gnu_long_link = 'K',
207217
};
208218

209-
pub fn init() Header {
210-
var ret = std.mem.zeroes(Header);
211-
ret.magic = [_]u8{ 'u', 's', 't', 'a', 'r', 0 };
212-
ret.version = [_]u8{ '0', '0' };
213-
return ret;
219+
pub fn init(typeflag: FileType) Header {
220+
var header = std.mem.zeroes(Header);
221+
header.magic = [_]u8{ 'u', 's', 't', 'a', 'r', 0 };
222+
header.version = [_]u8{ '0', '0' };
223+
header.typeflag = typeflag;
224+
return header;
214225
}
215226

216227
pub fn setSize(self: *Header, size: u64) !void {
217228
_ = try std.fmt.bufPrint(&self.size, "{o:0>11}", .{size});
218229
}
219230

220231
pub fn setMode(self: *Header, mode: u32) !void {
221-
_ = try std.fmt.bufPrint(&self.mode, "{o:0>7}", .{mode});
232+
const m: u32 = if (mode == 0)
233+
switch (self.typeflag) {
234+
.directory => default_mode.dir,
235+
.symbolic_link => default_mode.sym_link,
236+
else => default_mode.file,
237+
}
238+
else
239+
mode;
240+
_ = try std.fmt.bufPrint(&self.mode, "{o:0>7}", .{m});
222241
}
223242

224243
// Integer number of seconds since January 1, 1970, 00:00 Coordinated Universal Time.
244+
// mtime == 0 will use current time
225245
pub fn setMtime(self: *Header, mtime: u64) !void {
226-
_ = try std.fmt.bufPrint(
227-
&self.mtime,
228-
"{o:0>11}",
229-
.{if (mtime == 0) @as(u64, @intCast(std.time.timestamp())) else mtime},
230-
);
246+
_ = try std.fmt.bufPrint(&self.mtime, "{o:0>11}", .{mtime});
231247
}
232248

233249
pub fn updateChecksum(self: *Header) !void {
@@ -339,7 +355,7 @@ const Header = extern struct {
339355
};
340356

341357
for (cases) |case| {
342-
var header = Header.init();
358+
var header = Header.init(.regular);
343359
try header.setPath(case.in[0], case.in[1]);
344360
try testing.expectEqualStrings(case.out[0], str(&header.prefix));
345361
try testing.expectEqualStrings(case.out[1], str(&header.name));
@@ -358,7 +374,7 @@ const Header = extern struct {
358374
};
359375

360376
for (error_cases) |case| {
361-
var header = Header.init();
377+
var header = Header.init(.regular);
362378
try testing.expectError(
363379
error.NameTooLong,
364380
header.setPath(case.in[0], case.in[1]),
@@ -404,7 +420,7 @@ test "write files" {
404420
var wrt = try writer(output.writer(), prefix);
405421
for (files) |file| {
406422
var content = std.io.fixedBufferStream(file.content);
407-
try wrt.file(file.path, file.content.len, content.reader(), .{});
423+
try wrt.addFile(file.path, file.content.len, content.reader(), .{});
408424
}
409425

410426
var input = std.io.fixedBufferStream(output.items);
@@ -441,7 +457,7 @@ test "write files" {
441457
var wrt = try writer(output.writer(), "");
442458
for (files) |file| {
443459
var content = std.io.fixedBufferStream(file.content);
444-
try wrt.file(file.path, file.content.len, content.reader(), .{});
460+
try wrt.addFile(file.path, file.content.len, content.reader(), .{});
445461
}
446462

447463
var input = std.io.fixedBufferStream(output.items);
@@ -487,20 +503,20 @@ pub fn main() !void {
487503
var wrt = try writer(cmp.writer(), in_dir_name);
488504

489505
const excluded = [_][]const u8{
490-
"zig-cache/",
491-
"zig-out/",
506+
"zig-cache",
507+
"zig-out",
492508
".git/",
493509
"build/",
494510
"build2/",
495511
};
496512

497513
var walker = try in_dir.walk(gpa);
498514
defer walker.deinit();
499-
while (try walker.next()) |entry| {
515+
outer: while (try walker.next()) |entry| {
500516
for (excluded) |ex|
501-
if (std.mem.indexOf(u8, entry.path, ex)) |_| continue;
517+
if (std.mem.indexOf(u8, entry.path, ex)) |_| continue :outer;
502518

503-
try wrt.walkerEntry(entry);
519+
try wrt.addEntry(entry);
504520
}
505521
try cmp.finish();
506522
}
@@ -594,7 +610,7 @@ test "sourcesTar" {
594610
},
595611
else => continue,
596612
}
597-
try w.walkerEntry(entry);
613+
try w.addEntry(entry);
598614
}
599615

600616
{
@@ -603,7 +619,7 @@ test "sourcesTar" {
603619
// this source file corresponds to the user's host system.
604620
const builtin_zig = @embedFile("builtin");
605621
var stm = std.io.fixedBufferStream(builtin_zig);
606-
try w.file("builtin.zig", builtin_zig.len, stm.reader(), .{});
622+
try w.addFile("builtin.zig", builtin_zig.len, stm.reader(), .{});
607623
}
608624
}
609625
}

0 commit comments

Comments
 (0)