| ... | ... | @@ -48,7 +48,7 @@ pub const Header = struct { |
| 48 | 48 | /// Return value may point into Header buffer, or might point into the |
| 49 | 49 | /// argument buffer. |
| 50 | 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 | 52 | const n = name(header); |
| 53 | 53 | if (!is_ustar(header)) |
| 54 | 54 | return n; |
| ... | ... | @@ -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 | 124 | pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !void { |
| 87 | 125 | switch (options.mode_mode) { |
| 88 | 126 | .ignore => {}, |
| ... | ... | @@ -95,30 +133,27 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi |
| 95 | 133 | @panic("TODO: unimplemented: tar ModeMode.executable_bit_only"); |
| 96 | 134 | }, |
| 97 | 135 | } |
| 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: [std.fs.MAX_PATH_BYTES]u8 = undefined; |
| 137 | var file_name_override_len: usize = 0; |
| 138 | var buffer: Buffer = .{}; |
| 102 | 139 | 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) { |
| 112 | 142 | 0 => return, |
| 113 | 143 | 1...511 => return error.UnexpectedEndOfStream, |
| 114 | 144 | else => {}, |
| 115 | 145 | } |
| 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] }; |
| 118 | 149 | const file_size = try header.fileSize(); |
| 119 | 150 | const rounded_file_size = std.mem.alignForward(u64, file_size, 512); |
| 120 | 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 | 157 | switch (header.fileType()) { |
| 123 | 158 | .directory => { |
| 124 | 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 | 173 | |
| 139 | 174 | var file_off: usize = 0; |
| 140 | 175 | 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)))]; |
| 157 | 179 | try file.writeAll(slice); |
| 180 | |
| 158 | 181 | file_off += slice.len; |
| 159 | | start += slice.len; |
| 182 | buffer.advance(slice.len); |
| 160 | 183 | 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); |
| 164 | 185 | continue :header; |
| 165 | 186 | } |
| 166 | 187 | } |
| 167 | 188 | }, |
| 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 | |
| 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 | 230 | .hard_link => return error.TarUnsupportedFileType, |
| 173 | 231 | .symbolic_link => return error.TarUnsupportedFileType, |
| ... | ... | @@ -196,5 +254,44 @@ test stripComponents { |
| 196 | 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 | 296 | const std = @import("std.zig"); |
| 200 | 297 | const assert = std.debug.assert; |