Skip to content

Commit d8e7e00

Browse files
committed
std.tar: add support for file path in pax attributes
Handles .extended_header type to parse PAX attributes and check if they override the path of the next file. Increases maximum file path length from 255 to 1014. Fixes #15342
1 parent ff61c42 commit d8e7e00

File tree

1 file changed

+123
-40
lines changed

1 file changed

+123
-40
lines changed

lib/std/tar.zig

Lines changed: 123 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ pub const Header = struct {
4848
/// Return value may point into Header buffer, or might point into the
4949
/// argument buffer.
5050
/// TODO: check against "../" and other nefarious things
51-
pub fn fullFileName(header: Header, buffer: *[255]u8) ![]const u8 {
51+
pub fn fullFileName(header: Header, buffer: *[1014]u8) ![]const u8 {
5252
const n = name(header);
5353
if (!is_ustar(header))
5454
return n;
@@ -83,6 +83,44 @@ pub const Header = struct {
8383
}
8484
};
8585

86+
const Buffer = struct {
87+
buffer: [512 * 8]u8 = undefined,
88+
start: usize = 0,
89+
end: usize = 0,
90+
91+
pub fn readChunk(b: *Buffer, reader: anytype, count: usize) ![]const u8 {
92+
b.ensureCapacity(1024);
93+
94+
const ask = @min(b.buffer.len - b.end, count -| (b.end - b.start));
95+
b.end += try reader.readAtLeast(b.buffer[b.end..], ask);
96+
97+
return b.buffer[b.start..b.end];
98+
}
99+
100+
pub fn advance(b: *Buffer, count: usize) void {
101+
b.start += count;
102+
assert(b.start <= b.end);
103+
}
104+
105+
pub fn skip(b: *Buffer, reader: anytype, count: usize) !void {
106+
if (b.start + count > b.end) {
107+
try reader.skipBytes(b.start + count - b.end, .{});
108+
b.start = b.end;
109+
} else {
110+
b.advance(count);
111+
}
112+
}
113+
114+
inline fn ensureCapacity(b: *Buffer, count: usize) void {
115+
if (b.buffer.len - b.start < count) {
116+
const dest_end = b.end - b.start;
117+
@memcpy(b.buffer[0..dest_end], b.buffer[b.start..b.end]);
118+
b.end = dest_end;
119+
b.start = 0;
120+
}
121+
}
122+
};
123+
86124
pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !void {
87125
switch (options.mode_mode) {
88126
.ignore => {},
@@ -95,30 +133,27 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi
95133
@panic("TODO: unimplemented: tar ModeMode.executable_bit_only");
96134
},
97135
}
98-
var file_name_buffer: [255]u8 = undefined;
99-
var buffer: [512 * 8]u8 = undefined;
100-
var start: usize = 0;
101-
var end: usize = 0;
136+
var file_name_buffer: [1014]u8 = undefined;
137+
var file_name_override_len: usize = 0;
138+
var buffer: Buffer = .{};
102139
header: while (true) {
103-
if (buffer.len - start < 1024) {
104-
const dest_end = end - start;
105-
@memcpy(buffer[0..dest_end], buffer[start..end]);
106-
end = dest_end;
107-
start = 0;
108-
}
109-
const ask_header = @min(buffer.len - end, 1024 -| (end - start));
110-
end += try reader.readAtLeast(buffer[end..], ask_header);
111-
switch (end - start) {
140+
const chunk = try buffer.readChunk(reader, 1024);
141+
switch (chunk.len) {
112142
0 => return,
113143
1...511 => return error.UnexpectedEndOfStream,
114144
else => {},
115145
}
116-
const header: Header = .{ .bytes = buffer[start..][0..512] };
117-
start += 512;
146+
buffer.advance(512);
147+
148+
const header: Header = .{ .bytes = chunk[0..512] };
118149
const file_size = try header.fileSize();
119150
const rounded_file_size = std.mem.alignForward(u64, file_size, 512);
120151
const pad_len = @as(usize, @intCast(rounded_file_size - file_size));
121-
const unstripped_file_name = try header.fullFileName(&file_name_buffer);
152+
const unstripped_file_name = if (file_name_override_len > 0)
153+
file_name_buffer[0..file_name_override_len]
154+
else
155+
try header.fullFileName(&file_name_buffer);
156+
file_name_override_len = 0;
122157
switch (header.fileType()) {
123158
.directory => {
124159
const file_name = try stripComponents(unstripped_file_name, options.strip_components);
@@ -138,36 +173,49 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi
138173

139174
var file_off: usize = 0;
140175
while (true) {
141-
if (buffer.len - start < 1024) {
142-
const dest_end = end - start;
143-
@memcpy(buffer[0..dest_end], buffer[start..end]);
144-
end = dest_end;
145-
start = 0;
146-
}
147-
// Ask for the rounded up file size + 512 for the next header.
148-
// TODO: https://github.com/ziglang/zig/issues/14039
149-
const ask = @as(usize, @intCast(@min(
150-
buffer.len - end,
151-
rounded_file_size + 512 - file_off -| (end - start),
152-
)));
153-
end += try reader.readAtLeast(buffer[end..], ask);
154-
if (end - start < ask) return error.UnexpectedEndOfStream;
155-
// TODO: https://github.com/ziglang/zig/issues/14039
156-
const slice = buffer[start..@as(usize, @intCast(@min(file_size - file_off + start, end)))];
176+
const temp = try buffer.readChunk(reader, @intCast(rounded_file_size + 512 - file_off));
177+
if (temp.len == 0) return error.UnexpectedEndOfStream;
178+
const slice = temp[0..@as(usize, @intCast(@min(file_size - file_off, temp.len)))];
157179
try file.writeAll(slice);
180+
158181
file_off += slice.len;
159-
start += slice.len;
182+
buffer.advance(slice.len);
160183
if (file_off >= file_size) {
161-
start += pad_len;
162-
// Guaranteed since we use a buffer divisible by 512.
163-
assert(start <= end);
184+
buffer.advance(pad_len);
164185
continue :header;
165186
}
166187
}
167188
},
168-
.global_extended_header, .extended_header => {
169-
if (start + rounded_file_size > end) return error.TarHeadersTooBig;
170-
start = @as(usize, @intCast(start + rounded_file_size));
189+
.extended_header => {
190+
if (file_size == 0) {
191+
buffer.advance(@intCast(rounded_file_size));
192+
continue;
193+
}
194+
var data_off: usize = 0;
195+
while (true) {
196+
const temp = try buffer.readChunk(reader, @intCast(rounded_file_size + 512 - data_off));
197+
if (temp.len == 0) return error.UnexpectedEndOfStream;
198+
const slice = temp[0..@as(usize, @intCast(@min(file_size - data_off, temp.len)))];
199+
200+
const attribute = try parsePaxAttribute(slice, @intCast(file_size - data_off));
201+
if (std.mem.eql(u8, attribute.key, "path")) {
202+
const path_value = attribute.value orelse return error.NameTooLong;
203+
@memcpy(file_name_buffer[0..path_value.len], path_value);
204+
file_name_override_len = path_value.len;
205+
try buffer.skip(reader, @intCast(rounded_file_size - data_off));
206+
continue :header;
207+
}
208+
209+
try buffer.skip(reader, attribute.size);
210+
data_off += attribute.size;
211+
if (data_off >= file_size) {
212+
try buffer.skip(reader, pad_len);
213+
continue :header;
214+
}
215+
}
216+
},
217+
.global_extended_header => {
218+
buffer.skip(reader, @intCast(rounded_file_size)) catch return error.TarHeadersTooBig;
171219
},
172220
.hard_link => return error.TarUnsupportedFileType,
173221
.symbolic_link => return error.TarUnsupportedFileType,
@@ -196,5 +244,40 @@ test stripComponents {
196244
try expectEqualStrings("c", try stripComponents("a/b/c", 2));
197245
}
198246

247+
const PaxAttribute = struct {
248+
size: usize,
249+
key: []const u8,
250+
value: ?[]const u8,
251+
};
252+
253+
fn parsePaxAttribute(data: []const u8, max_size: usize) !PaxAttribute {
254+
const pos_space = std.mem.indexOfScalar(u8, data, ' ') orelse return error.InvalidPaxAttribute;
255+
const pos_equals = std.mem.indexOfScalarPos(u8, data, pos_space, '=') orelse return error.InvalidPaxAttribute;
256+
const kv_size = try std.fmt.parseInt(u64, data[0..pos_space], 10);
257+
if (kv_size > max_size) {
258+
return error.InvalidPaxAttribute;
259+
}
260+
return .{
261+
.size = kv_size,
262+
.key = data[pos_space + 1 .. pos_equals],
263+
.value = if (kv_size - 1 > data.len) null else data[pos_equals + 1 .. kv_size - 1],
264+
};
265+
}
266+
267+
test parsePaxAttribute {
268+
const expectEqual = std.testing.expectEqual;
269+
const expectEqualStrings = std.testing.expectEqualStrings;
270+
const expectError = std.testing.expectError;
271+
const file_name = "0123456789" ** 100;
272+
const header = "1011 path=" ++ file_name ++ "\n";
273+
const attribute = try parsePaxAttribute(header, 1011);
274+
try expectEqual(@as(u64, 1011), attribute.size);
275+
try expectEqualStrings("path", attribute.key);
276+
try expectEqualStrings(file_name, attribute.value.?);
277+
try expectEqual(attribute, try parsePaxAttribute(header, 1012));
278+
try expectError(error.InvalidPaxAttribute, parsePaxAttribute(header, 1010));
279+
try expectError(error.InvalidPaxAttribute, parsePaxAttribute("", 0));
280+
}
281+
199282
const std = @import("std.zig");
200283
const assert = std.debug.assert;

0 commit comments

Comments
 (0)