| ... | @@ -48,7 +48,7 @@ pub const Header = struct { | ... | @@ -48,7 +48,7 @@ pub const Header = struct { |
| 48 | /// Return value may point into Header buffer, or might point into the | 48 | /// Return value may point into Header buffer, or might point into the |
| 49 | /// argument buffer. | 49 | /// argument buffer. |
| 50 | /// TODO: check against "../" and other nefarious things | 50 | /// 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: *[std.fs.MAX_PATH_BYTES]u8) ![]const u8 { |
| 52 | const n = name(header); | 52 | const n = name(header); |
| 53 | if (!is_ustar(header)) | 53 | if (!is_ustar(header)) |
| 54 | return n; | 54 | return n; |
| ... | @@ -83,6 +83,44 @@ pub const Header = struct { | ... | @@ -83,6 +83,44 @@ pub const Header = struct { |
| 83 | } | 83 | } |
| 84 | }; | 84 | }; |
| 85 | | 85 | |
| | 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 | |
| 86 | pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !void { | 124 | pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !void { |
| 87 | switch (options.mode_mode) { | 125 | switch (options.mode_mode) { |
| 88 | .ignore => {}, | 126 | .ignore => {}, |
| ... | @@ -95,30 +133,27 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi | ... | @@ -95,30 +133,27 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi |
| 95 | @panic("TODO: unimplemented: tar ModeMode.executable_bit_only"); | 133 | @panic("TODO: unimplemented: tar ModeMode.executable_bit_only"); |
| 96 | }, | 134 | }, |
| 97 | } | 135 | } |
| 98 | var file_name_buffer: [255]u8 = undefined; | 136 | var file_name_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined; |
| 99 | var buffer: [512 * 8]u8 = undefined; | 137 | var file_name_override_len: usize = 0; |
| 100 | var start: usize = 0; | 138 | var buffer: Buffer = .{}; |
| 101 | var end: usize = 0; | | |
| 102 | header: while (true) { | 139 | header: while (true) { |
| 103 | if (buffer.len - start < 1024) { | 140 | const chunk = try buffer.readChunk(reader, 1024); |
| 104 | const dest_end = end - start; | 141 | switch (chunk.len) { |
| 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) { | | |
| 112 | 0 => return, | 142 | 0 => return, |
| 113 | 1...511 => return error.UnexpectedEndOfStream, | 143 | 1...511 => return error.UnexpectedEndOfStream, |
| 114 | else => {}, | 144 | else => {}, |
| 115 | } | 145 | } |
| 116 | const header: Header = .{ .bytes = buffer[start..][0..512] }; | 146 | buffer.advance(512); |
| 117 | start += 512; | 147 | |
| | 148 | const header: Header = .{ .bytes = chunk[0..512] }; |
| 118 | const file_size = try header.fileSize(); | 149 | const file_size = try header.fileSize(); |
| 119 | const rounded_file_size = std.mem.alignForward(u64, file_size, 512); | 150 | const rounded_file_size = std.mem.alignForward(u64, file_size, 512); |
| 120 | const pad_len = @as(usize, @intCast(rounded_file_size - file_size)); | 151 | 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; |
| 122 | switch (header.fileType()) { | 157 | switch (header.fileType()) { |
| 123 | .directory => { | 158 | .directory => { |
| 124 | const file_name = try stripComponents(unstripped_file_name, options.strip_components); | 159 | const file_name = try stripComponents(unstripped_file_name, options.strip_components); |
| ... | @@ -138,36 +173,59 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi | ... | @@ -138,36 +173,59 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi |
| 138 | | 173 | |
| 139 | var file_off: usize = 0; | 174 | var file_off: usize = 0; |
| 140 | while (true) { | 175 | while (true) { |
| 141 | if (buffer.len - start < 1024) { | 176 | const temp = try buffer.readChunk(reader, @intCast(rounded_file_size + 512 - file_off)); |
| 142 | const dest_end = end - start; | 177 | if (temp.len == 0) return error.UnexpectedEndOfStream; |
| 143 | @memcpy(buffer[0..dest_end], buffer[start..end]); | 178 | const slice = temp[0..@as(usize, @intCast(@min(file_size - file_off, temp.len)))]; |
| 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)))]; | | |
| 157 | try file.writeAll(slice); | 179 | try file.writeAll(slice); |
| | 180 | |
| 158 | file_off += slice.len; | 181 | file_off += slice.len; |
| 159 | start += slice.len; | 182 | buffer.advance(slice.len); |
| 160 | if (file_off >= file_size) { | 183 | if (file_off >= file_size) { |
| 161 | start += pad_len; | 184 | buffer.advance(pad_len); |
| 162 | // Guaranteed since we use a buffer divisible by 512. | | |
| 163 | assert(start <= end); | | |
| 164 | continue :header; | 185 | continue :header; |
| 165 | } | 186 | } |
| 166 | } | 187 | } |
| 167 | }, | 188 | }, |
| 168 | .global_extended_header, .extended_header => { | 189 | .extended_header => { |
| 169 | if (start + rounded_file_size > end) return error.TarHeadersTooBig; | 190 | if (file_size == 0) { |
| 170 | start = @as(usize, @intCast(start + rounded_file_size)); | 191 | buffer.advance(@intCast(rounded_file_size)); |
| | 192 | continue; |
| | 193 | } |
| | 194 | |
| | 195 | const chunk_size: usize = @intCast(rounded_file_size + 512); |
| | 196 | var data_off: usize = 0; |
| | 197 | file_name_override_len = while (data_off < file_size) { |
| | 198 | const slice = try buffer.readChunk(reader, chunk_size - data_off); |
| | 199 | if (slice.len == 0) return error.UnexpectedEndOfStream; |
| | 200 | const remaining_size: usize = @intCast(file_size - data_off); |
| | 201 | const attr_info = try parsePaxAttribute(slice[0..@min(remaining_size, slice.len)], remaining_size); |
| | 202 | |
| | 203 | if (std.mem.eql(u8, attr_info.key, "path")) { |
| | 204 | if (attr_info.value_len > file_name_buffer.len) return error.NameTooLong; |
| | 205 | buffer.advance(attr_info.value_off); |
| | 206 | data_off += attr_info.value_off; |
| | 207 | break attr_info.value_len; |
| | 208 | } |
| | 209 | |
| | 210 | try buffer.skip(reader, attr_info.size); |
| | 211 | data_off += attr_info.size; |
| | 212 | } else 0; |
| | 213 | |
| | 214 | var i: usize = 0; |
| | 215 | while (i < file_name_override_len) { |
| | 216 | const slice = try buffer.readChunk(reader, chunk_size - data_off - i); |
| | 217 | if (slice.len == 0) return error.UnexpectedEndOfStream; |
| | 218 | const copy_size: usize = @intCast(@min(file_name_override_len - i, slice.len)); |
| | 219 | @memcpy(file_name_buffer[i .. i + copy_size], slice[0..copy_size]); |
| | 220 | buffer.advance(copy_size); |
| | 221 | i += copy_size; |
| | 222 | } |
| | 223 | |
| | 224 | try buffer.skip(reader, @intCast(rounded_file_size - data_off - file_name_override_len)); |
| | 225 | continue :header; |
| | 226 | }, |
| | 227 | .global_extended_header => { |
| | 228 | buffer.skip(reader, @intCast(rounded_file_size)) catch return error.TarHeadersTooBig; |
| 171 | }, | 229 | }, |
| 172 | .hard_link => return error.TarUnsupportedFileType, | 230 | .hard_link => return error.TarUnsupportedFileType, |
| 173 | .symbolic_link => return error.TarUnsupportedFileType, | 231 | .symbolic_link => return error.TarUnsupportedFileType, |
| ... | @@ -196,5 +254,44 @@ test stripComponents { | ... | @@ -196,5 +254,44 @@ test stripComponents { |
| 196 | try expectEqualStrings("c", try stripComponents("a/b/c", 2)); | 254 | try expectEqualStrings("c", try stripComponents("a/b/c", 2)); |
| 197 | } | 255 | } |
| 198 | | 256 | |
| | 257 | const PaxAttributeInfo = struct { |
| | 258 | size: usize, |
| | 259 | key: []const u8, |
| | 260 | value_off: usize, |
| | 261 | value_len: usize, |
| | 262 | }; |
| | 263 | |
| | 264 | fn parsePaxAttribute(data: []const u8, max_size: usize) !PaxAttributeInfo { |
| | 265 | const pos_space = std.mem.indexOfScalar(u8, data, ' ') orelse return error.InvalidPaxAttribute; |
| | 266 | const pos_equals = std.mem.indexOfScalarPos(u8, data, pos_space, '=') orelse return error.InvalidPaxAttribute; |
| | 267 | const kv_size = try std.fmt.parseInt(usize, data[0..pos_space], 10); |
| | 268 | if (kv_size > max_size) { |
| | 269 | return error.InvalidPaxAttribute; |
| | 270 | } |
| | 271 | return .{ |
| | 272 | .size = kv_size, |
| | 273 | .key = data[pos_space + 1 .. pos_equals], |
| | 274 | .value_off = pos_equals + 1, |
| | 275 | .value_len = kv_size - pos_equals - 2, |
| | 276 | }; |
| | 277 | } |
| | 278 | |
| | 279 | test parsePaxAttribute { |
| | 280 | const expectEqual = std.testing.expectEqual; |
| | 281 | const expectEqualStrings = std.testing.expectEqualStrings; |
| | 282 | const expectError = std.testing.expectError; |
| | 283 | const prefix = "1011 path="; |
| | 284 | const file_name = "0123456789" ** 100; |
| | 285 | const header = prefix ++ file_name ++ "\n"; |
| | 286 | const attr_info = try parsePaxAttribute(header, 1011); |
| | 287 | try expectEqual(@as(usize, 1011), attr_info.size); |
| | 288 | try expectEqualStrings("path", attr_info.key); |
| | 289 | try expectEqual(prefix.len, attr_info.value_off); |
| | 290 | try expectEqual(file_name.len, attr_info.value_len); |
| | 291 | try expectEqual(attr_info, try parsePaxAttribute(header, 1012)); |
| | 292 | try expectError(error.InvalidPaxAttribute, parsePaxAttribute(header, 1010)); |
| | 293 | try expectError(error.InvalidPaxAttribute, parsePaxAttribute("", 0)); |
| | 294 | } |
| | 295 | |
| 199 | const std = @import("std.zig"); | 296 | const std = @import("std.zig"); |
| 200 | const assert = std.debug.assert; | 297 | const assert = std.debug.assert; |