| ... | @@ -1,3 +1,23 @@ | ... | @@ -1,3 +1,23 @@ |
| | 1 | /// Tar archive is single ordinary file which can contain many files (or |
| | 2 | /// directories, symlinks, ...). It's build by series of blocks each size of 512 |
| | 3 | /// bytes. First block of each entry is header which defines type, name, size |
| | 4 | /// permissions and other attributes. Header is followed by series of blocks of |
| | 5 | /// file content, if any that entry has content. Content is padded to the block |
| | 6 | /// size, so next header always starts at block boundary. |
| | 7 | /// |
| | 8 | /// This simple format is extended by GNU and POSIX pax extensions to support |
| | 9 | /// file names longer than 256 bytes and additional attributes. |
| | 10 | /// |
| | 11 | /// This is not comprehensive tar parser. Here we are only file types needed to |
| | 12 | /// support Zig package manager; normal file, directory, symbolic link. And |
| | 13 | /// subset of attributes: name, size, permissions. |
| | 14 | /// |
| | 15 | /// GNU tar reference: https://www.gnu.org/software/tar/manual/html_node/Standard.html |
| | 16 | /// pax reference: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html#tag_20_92_13 |
| | 17 | /// |
| | 18 | const std = @import("std.zig"); |
| | 19 | const assert = std.debug.assert; |
| | 20 | |
| 1 | pub const Options = struct { | 21 | pub const Options = struct { |
| 2 | /// Number of directory levels to skip when extracting files. | 22 | /// Number of directory levels to skip when extracting files. |
| 3 | strip_components: u32 = 0, | 23 | strip_components: u32 = 0, |
| ... | @@ -37,7 +57,7 @@ pub const Options = struct { | ... | @@ -37,7 +57,7 @@ pub const Options = struct { |
| 37 | }, | 57 | }, |
| 38 | unsupported_file_type: struct { | 58 | unsupported_file_type: struct { |
| 39 | file_name: []const u8, | 59 | file_name: []const u8, |
| 40 | file_type: Header.FileType, | 60 | file_type: Header.Kind, |
| 41 | }, | 61 | }, |
| 42 | }; | 62 | }; |
| 43 | | 63 | |
| ... | @@ -63,9 +83,13 @@ pub const Options = struct { | ... | @@ -63,9 +83,13 @@ pub const Options = struct { |
| 63 | }; | 83 | }; |
| 64 | | 84 | |
| 65 | pub const Header = struct { | 85 | pub const Header = struct { |
| 66 | bytes: *const [512]u8, | 86 | const SIZE = 512; |
| | 87 | const MAX_NAME_SIZE = 100 + 1 + 155; // name(100) + separator(1) + prefix(155) |
| | 88 | const LINK_NAME_SIZE = 100; |
| 67 | | 89 | |
| 68 | pub const FileType = enum(u8) { | 90 | bytes: *const [SIZE]u8, |
| | 91 | |
| | 92 | pub const Kind = enum(u8) { |
| 69 | normal_alias = 0, | 93 | normal_alias = 0, |
| 70 | normal = '0', | 94 | normal = '0', |
| 71 | hard_link = '1', | 95 | hard_link = '1', |
| ... | @@ -77,103 +101,424 @@ pub const Header = struct { | ... | @@ -77,103 +101,424 @@ pub const Header = struct { |
| 77 | contiguous = '7', | 101 | contiguous = '7', |
| 78 | global_extended_header = 'g', | 102 | global_extended_header = 'g', |
| 79 | extended_header = 'x', | 103 | extended_header = 'x', |
| | 104 | // Types 'L' and 'K' are used by the GNU format for a meta file |
| | 105 | // used to store the path or link name for the next file. |
| | 106 | gnu_long_name = 'L', |
| | 107 | gnu_long_link = 'K', |
| | 108 | gnu_sparse = 'S', |
| | 109 | solaris_extended_header = 'X', |
| 80 | _, | 110 | _, |
| 81 | }; | 111 | }; |
| 82 | | 112 | |
| 83 | pub fn fileSize(header: Header) !u64 { | | |
| 84 | const raw = header.bytes[124..][0..12]; | | |
| 85 | const ltrimmed = std.mem.trimLeft(u8, raw, "0 "); | | |
| 86 | const rtrimmed = std.mem.trimRight(u8, ltrimmed, " \x00"); | | |
| 87 | if (rtrimmed.len == 0) return 0; | | |
| 88 | return std.fmt.parseInt(u64, rtrimmed, 8); | | |
| 89 | } | | |
| 90 | | | |
| 91 | pub fn is_ustar(header: Header) bool { | | |
| 92 | return std.mem.eql(u8, header.bytes[257..][0..6], "ustar\x00"); | | |
| 93 | } | | |
| 94 | | | |
| 95 | /// Includes prefix concatenated, if any. | 113 | /// Includes prefix concatenated, if any. |
| 96 | /// Return value may point into Header buffer, or might point into the | | |
| 97 | /// argument buffer. | | |
| 98 | /// TODO: check against "../" and other nefarious things | 114 | /// TODO: check against "../" and other nefarious things |
| 99 | pub fn fullFileName(header: Header, buffer: *[std.fs.MAX_PATH_BYTES]u8) ![]const u8 { | 115 | pub fn fullName(header: Header, buffer: *[MAX_NAME_SIZE]u8) ![]const u8 { |
| 100 | const n = name(header); | 116 | const n = name(header); |
| 101 | if (!is_ustar(header)) | | |
| 102 | return n; | | |
| 103 | const p = prefix(header); | 117 | const p = prefix(header); |
| 104 | if (p.len == 0) | 118 | if (!is_ustar(header) or p.len == 0) { |
| 105 | return n; | 119 | @memcpy(buffer[0..n.len], n); |
| | 120 | return buffer[0..n.len]; |
| | 121 | } |
| 106 | @memcpy(buffer[0..p.len], p); | 122 | @memcpy(buffer[0..p.len], p); |
| 107 | buffer[p.len] = '/'; | 123 | buffer[p.len] = '/'; |
| 108 | @memcpy(buffer[p.len + 1 ..][0..n.len], n); | 124 | @memcpy(buffer[p.len + 1 ..][0..n.len], n); |
| 109 | return buffer[0 .. p.len + 1 + n.len]; | 125 | return buffer[0 .. p.len + 1 + n.len]; |
| 110 | } | 126 | } |
| 111 | | 127 | |
| | 128 | pub fn linkName(header: Header, buffer: *[LINK_NAME_SIZE]u8) []const u8 { |
| | 129 | const link_name = header.str(157, 100); |
| | 130 | if (link_name.len == 0) { |
| | 131 | return buffer[0..0]; |
| | 132 | } |
| | 133 | const buf = buffer[0..link_name.len]; |
| | 134 | @memcpy(buf, link_name); |
| | 135 | return buf; |
| | 136 | } |
| | 137 | |
| 112 | pub fn name(header: Header) []const u8 { | 138 | pub fn name(header: Header) []const u8 { |
| 113 | return str(header, 0, 0 + 100); | 139 | return header.str(0, 100); |
| | 140 | } |
| | 141 | |
| | 142 | pub fn mode(header: Header) !u32 { |
| | 143 | return @intCast(try header.numeric(100, 8)); |
| 114 | } | 144 | } |
| 115 | | 145 | |
| 116 | pub fn linkName(header: Header) []const u8 { | 146 | pub fn size(header: Header) !u64 { |
| 117 | return str(header, 157, 157 + 100); | 147 | return header.numeric(124, 12); |
| | 148 | } |
| | 149 | |
| | 150 | pub fn chksum(header: Header) !u64 { |
| | 151 | return header.octal(148, 8); |
| | 152 | } |
| | 153 | |
| | 154 | pub fn is_ustar(header: Header) bool { |
| | 155 | const magic = header.bytes[257..][0..6]; |
| | 156 | return std.mem.eql(u8, magic[0..5], "ustar") and (magic[5] == 0 or magic[5] == ' '); |
| 118 | } | 157 | } |
| 119 | | 158 | |
| 120 | pub fn prefix(header: Header) []const u8 { | 159 | pub fn prefix(header: Header) []const u8 { |
| 121 | return str(header, 345, 345 + 155); | 160 | return header.str(345, 155); |
| 122 | } | 161 | } |
| 123 | | 162 | |
| 124 | pub fn fileType(header: Header) FileType { | 163 | pub fn kind(header: Header) Kind { |
| 125 | const result: FileType = @enumFromInt(header.bytes[156]); | 164 | const result: Kind = @enumFromInt(header.bytes[156]); |
| 126 | if (result == .normal_alias) return .normal; | 165 | if (result == .normal_alias) return .normal; |
| 127 | return result; | 166 | return result; |
| 128 | } | 167 | } |
| 129 | | 168 | |
| 130 | fn str(header: Header, start: usize, end: usize) []const u8 { | 169 | fn str(header: Header, start: usize, len: usize) []const u8 { |
| 131 | var i: usize = start; | 170 | return nullStr(header.bytes[start .. start + len]); |
| 132 | while (i < end) : (i += 1) { | 171 | } |
| 133 | if (header.bytes[i] == 0) break; | 172 | |
| | 173 | fn numeric(header: Header, start: usize, len: usize) !u64 { |
| | 174 | const raw = header.bytes[start..][0..len]; |
| | 175 | // If the leading byte is 0xff (255), all the bytes of the field |
| | 176 | // (including the leading byte) are concatenated in big-endian order, |
| | 177 | // with the result being a negative number expressed in two’s |
| | 178 | // complement form. |
| | 179 | if (raw[0] == 0xff) return error.TarNumericValueNegative; |
| | 180 | // If the leading byte is 0x80 (128), the non-leading bytes of the |
| | 181 | // field are concatenated in big-endian order. |
| | 182 | if (raw[0] == 0x80) { |
| | 183 | if (raw[1] + raw[2] + raw[3] != 0) return error.TarNumericValueTooBig; |
| | 184 | return std.mem.readInt(u64, raw[4..12], .big); |
| 134 | } | 185 | } |
| 135 | return header.bytes[start..i]; | 186 | return try header.octal(start, len); |
| 136 | } | 187 | } |
| 137 | }; | | |
| 138 | | 188 | |
| 139 | const Buffer = struct { | 189 | fn octal(header: Header, start: usize, len: usize) !u64 { |
| 140 | buffer: [512 * 8]u8 = undefined, | 190 | const raw = header.bytes[start..][0..len]; |
| 141 | start: usize = 0, | 191 | // Zero-filled octal number in ASCII. Each numeric field of width w |
| 142 | end: usize = 0, | 192 | // contains w minus 1 digits, and a null |
| | 193 | const ltrimmed = std.mem.trimLeft(u8, raw, "0 "); |
| | 194 | const rtrimmed = std.mem.trimRight(u8, ltrimmed, " \x00"); |
| | 195 | if (rtrimmed.len == 0) return 0; |
| | 196 | return std.fmt.parseInt(u64, rtrimmed, 8) catch return error.TarHeader; |
| | 197 | } |
| 143 | | 198 | |
| 144 | pub fn readChunk(b: *Buffer, reader: anytype, count: usize) ![]const u8 { | 199 | const Chksums = struct { |
| 145 | b.ensureCapacity(1024); | 200 | unsigned: u64, |
| | 201 | signed: i64, |
| | 202 | }; |
| 146 | | 203 | |
| 147 | const ask = @min(b.buffer.len - b.end, count -| (b.end - b.start)); | 204 | // Sum of all bytes in the header block. The chksum field is treated as if |
| 148 | b.end += try reader.readAtLeast(b.buffer[b.end..], ask); | 205 | // it were filled with spaces (ASCII 32). |
| | 206 | fn computeChksum(header: Header) Chksums { |
| | 207 | var cs: Chksums = .{ .signed = 0, .unsigned = 0 }; |
| | 208 | for (header.bytes, 0..) |v, i| { |
| | 209 | const b = if (148 <= i and i < 156) 32 else v; // Treating chksum bytes as spaces. |
| | 210 | cs.unsigned += b; |
| | 211 | cs.signed += @as(i8, @bitCast(b)); |
| | 212 | } |
| | 213 | return cs; |
| | 214 | } |
| 149 | | 215 | |
| 150 | return b.buffer[b.start..b.end]; | 216 | // Checks calculated chksum with value of chksum field. |
| | 217 | // Returns error or valid chksum value. |
| | 218 | // Zero value indicates empty block. |
| | 219 | pub fn checkChksum(header: Header) !u64 { |
| | 220 | const field = try header.chksum(); |
| | 221 | const cs = header.computeChksum(); |
| | 222 | if (field == 0 and cs.unsigned == 256) return 0; |
| | 223 | if (field != cs.unsigned and field != cs.signed) return error.TarHeaderChksum; |
| | 224 | return field; |
| 151 | } | 225 | } |
| | 226 | }; |
| 152 | | 227 | |
| 153 | pub fn advance(b: *Buffer, count: usize) void { | 228 | // Breaks string on first null character. |
| 154 | b.start += count; | 229 | fn nullStr(str: []const u8) []const u8 { |
| 155 | assert(b.start <= b.end); | 230 | for (str, 0..) |c, i| { |
| | 231 | if (c == 0) return str[0..i]; |
| 156 | } | 232 | } |
| | 233 | return str; |
| | 234 | } |
| 157 | | 235 | |
| 158 | pub fn skip(b: *Buffer, reader: anytype, count: usize) !void { | 236 | /// Iterates over files in tar archive. |
| 159 | if (b.start + count > b.end) { | 237 | /// `next` returns each file in `reader` tar archive. |
| 160 | try reader.skipBytes(b.start + count - b.end, .{}); | 238 | pub fn iterator(reader: anytype, diagnostics: ?*Options.Diagnostics) Iterator(@TypeOf(reader)) { |
| 161 | b.start = b.end; | 239 | return .{ |
| 162 | } else { | 240 | .reader = reader, |
| 163 | b.advance(count); | 241 | .diagnostics = diagnostics, |
| | 242 | }; |
| | 243 | } |
| | 244 | |
| | 245 | fn Iterator(comptime ReaderType: type) type { |
| | 246 | return struct { |
| | 247 | reader: ReaderType, |
| | 248 | diagnostics: ?*Options.Diagnostics, |
| | 249 | |
| | 250 | // buffers for heeader and file attributes |
| | 251 | header_buffer: [Header.SIZE]u8 = undefined, |
| | 252 | file_name_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined, |
| | 253 | link_name_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined, |
| | 254 | |
| | 255 | // bytes of padding to the end of the block |
| | 256 | padding: usize = 0, |
| | 257 | // current tar file |
| | 258 | file: File = undefined, |
| | 259 | |
| | 260 | pub const File = struct { |
| | 261 | name: []const u8, // name of file, symlink or directory |
| | 262 | link_name: []const u8, // target name of symlink |
| | 263 | size: u64, // size of the file in bytes |
| | 264 | mode: u32, |
| | 265 | kind: Header.Kind, |
| | 266 | |
| | 267 | reader: ReaderType, |
| | 268 | |
| | 269 | // Writes file content to writer. |
| | 270 | pub fn write(self: File, writer: anytype) !void { |
| | 271 | var buffer: [4096]u8 = undefined; |
| | 272 | |
| | 273 | var n: u64 = 0; |
| | 274 | while (n < self.size) { |
| | 275 | const buf = buffer[0..@min(buffer.len, self.size - n)]; |
| | 276 | try self.reader.readNoEof(buf); |
| | 277 | try writer.writeAll(buf); |
| | 278 | n += buf.len; |
| | 279 | } |
| | 280 | } |
| | 281 | |
| | 282 | // Skips file content. Advances reader. |
| | 283 | pub fn skip(self: File) !void { |
| | 284 | try self.reader.skipBytes(self.size, .{}); |
| | 285 | } |
| | 286 | }; |
| | 287 | |
| | 288 | const Self = @This(); |
| | 289 | |
| | 290 | fn readHeader(self: *Self) !?Header { |
| | 291 | if (self.padding > 0) { |
| | 292 | try self.reader.skipBytes(self.padding, .{}); |
| | 293 | } |
| | 294 | const n = try self.reader.readAll(&self.header_buffer); |
| | 295 | if (n == 0) return null; |
| | 296 | if (n < Header.SIZE) return error.UnexpectedEndOfStream; |
| | 297 | const header = Header{ .bytes = self.header_buffer[0..Header.SIZE] }; |
| | 298 | if (try header.checkChksum() == 0) return null; |
| | 299 | return header; |
| 164 | } | 300 | } |
| 165 | } | | |
| 166 | | 301 | |
| 167 | inline fn ensureCapacity(b: *Buffer, count: usize) void { | 302 | inline fn readString(self: *Self, size: usize, buffer: []u8) ![]const u8 { |
| 168 | if (b.buffer.len - b.start < count) { | 303 | assert(buffer.len >= size); |
| 169 | const dest_end = b.end - b.start; | 304 | const buf = buffer[0..size]; |
| 170 | @memcpy(b.buffer[0..dest_end], b.buffer[b.start..b.end]); | 305 | try self.reader.readNoEof(buf); |
| 171 | b.end = dest_end; | 306 | return nullStr(buf); |
| 172 | b.start = 0; | | |
| 173 | } | 307 | } |
| 174 | } | 308 | |
| | 309 | inline fn initFile(self: *Self) void { |
| | 310 | self.file = File{ |
| | 311 | .name = self.file_name_buffer[0..0], |
| | 312 | .link_name = self.link_name_buffer[0..0], |
| | 313 | .size = 0, |
| | 314 | .kind = .normal, |
| | 315 | .mode = 0, |
| | 316 | .reader = self.reader, |
| | 317 | }; |
| | 318 | } |
| | 319 | |
| | 320 | // Number of padding bytes in the last file block. |
| | 321 | inline fn blockPadding(size: u64) usize { |
| | 322 | const block_rounded = std.mem.alignForward(u64, size, Header.SIZE); // size rounded to te block boundary |
| | 323 | return @intCast(block_rounded - size); |
| | 324 | } |
| | 325 | |
| | 326 | /// Iterates through the tar archive as if it is a series of files. |
| | 327 | /// Internally, the tar format often uses entries (header with optional |
| | 328 | /// content) to add meta data that describes the next file. These |
| | 329 | /// entries should not normally be visible to the outside. As such, this |
| | 330 | /// loop iterates through one or more entries until it collects a all |
| | 331 | /// file attributes. |
| | 332 | pub fn next(self: *Self) !?File { |
| | 333 | self.initFile(); |
| | 334 | |
| | 335 | while (try self.readHeader()) |header| { |
| | 336 | const kind = header.kind(); |
| | 337 | const size: u64 = try header.size(); |
| | 338 | self.padding = blockPadding(size); |
| | 339 | |
| | 340 | switch (kind) { |
| | 341 | // File types to retrun upstream |
| | 342 | .directory, .normal, .symbolic_link => { |
| | 343 | self.file.kind = kind; |
| | 344 | self.file.mode = try header.mode(); |
| | 345 | |
| | 346 | // set file attributes if not already set by prefix/extended headers |
| | 347 | if (self.file.size == 0) { |
| | 348 | self.file.size = size; |
| | 349 | } |
| | 350 | if (self.file.link_name.len == 0) { |
| | 351 | self.file.link_name = header.linkName(self.link_name_buffer[0..Header.LINK_NAME_SIZE]); |
| | 352 | } |
| | 353 | if (self.file.name.len == 0) { |
| | 354 | self.file.name = try header.fullName(self.file_name_buffer[0..Header.MAX_NAME_SIZE]); |
| | 355 | } |
| | 356 | |
| | 357 | self.padding = blockPadding(self.file.size); |
| | 358 | return self.file; |
| | 359 | }, |
| | 360 | // Prefix header types |
| | 361 | .gnu_long_name => { |
| | 362 | self.file.name = try self.readString(@intCast(size), &self.file_name_buffer); |
| | 363 | }, |
| | 364 | .gnu_long_link => { |
| | 365 | self.file.link_name = try self.readString(@intCast(size), &self.link_name_buffer); |
| | 366 | }, |
| | 367 | .extended_header => { |
| | 368 | // Use just attributes from last extended header. |
| | 369 | self.initFile(); |
| | 370 | |
| | 371 | var rdr = paxIterator(self.reader, @intCast(size)); |
| | 372 | while (try rdr.next()) |attr| { |
| | 373 | switch (attr.kind) { |
| | 374 | .path => { |
| | 375 | self.file.name = try attr.value(&self.file_name_buffer); |
| | 376 | }, |
| | 377 | .linkpath => { |
| | 378 | self.file.link_name = try attr.value(&self.link_name_buffer); |
| | 379 | }, |
| | 380 | .size => { |
| | 381 | var buf: [64]u8 = undefined; |
| | 382 | self.file.size = try std.fmt.parseInt(u64, try attr.value(&buf), 10); |
| | 383 | }, |
| | 384 | } |
| | 385 | } |
| | 386 | }, |
| | 387 | // Ignored header type |
| | 388 | .global_extended_header => { |
| | 389 | self.reader.skipBytes(size, .{}) catch return error.TarHeadersTooBig; |
| | 390 | }, |
| | 391 | // All other are unsupported header types |
| | 392 | else => { |
| | 393 | const d = self.diagnostics orelse return error.TarUnsupportedHeader; |
| | 394 | try d.errors.append(d.allocator, .{ .unsupported_file_type = .{ |
| | 395 | .file_name = try d.allocator.dupe(u8, header.name()), |
| | 396 | .file_type = kind, |
| | 397 | } }); |
| | 398 | if (kind == .gnu_sparse) { |
| | 399 | try self.skipGnuSparseExtendedHeaders(header); |
| | 400 | } |
| | 401 | self.reader.skipBytes(size, .{}) catch return error.TarHeadersTooBig; |
| | 402 | }, |
| | 403 | } |
| | 404 | } |
| | 405 | return null; |
| | 406 | } |
| | 407 | |
| | 408 | fn skipGnuSparseExtendedHeaders(self: *Self, header: Header) !void { |
| | 409 | var is_extended = header.bytes[482] > 0; |
| | 410 | while (is_extended) { |
| | 411 | var buf: [Header.SIZE]u8 = undefined; |
| | 412 | const n = try self.reader.readAll(&buf); |
| | 413 | if (n < Header.SIZE) return error.UnexpectedEndOfStream; |
| | 414 | is_extended = buf[504] > 0; |
| | 415 | } |
| | 416 | } |
| | 417 | }; |
| | 418 | } |
| | 419 | |
| | 420 | /// Pax attributes iterator. |
| | 421 | /// Size is length of pax extended header in reader. |
| | 422 | fn paxIterator(reader: anytype, size: usize) PaxIterator(@TypeOf(reader)) { |
| | 423 | return PaxIterator(@TypeOf(reader)){ |
| | 424 | .reader = reader, |
| | 425 | .size = size, |
| | 426 | }; |
| | 427 | } |
| | 428 | |
| | 429 | const PaxAttributeKind = enum { |
| | 430 | path, |
| | 431 | linkpath, |
| | 432 | size, |
| 175 | }; | 433 | }; |
| 176 | | 434 | |
| | 435 | fn PaxIterator(comptime ReaderType: type) type { |
| | 436 | return struct { |
| | 437 | size: usize, // cumulative size of all pax attributes |
| | 438 | reader: ReaderType, |
| | 439 | // scratch buffer used for reading attribute length and keyword |
| | 440 | scratch: [128]u8 = undefined, |
| | 441 | |
| | 442 | const Self = @This(); |
| | 443 | |
| | 444 | const Attribute = struct { |
| | 445 | kind: PaxAttributeKind, |
| | 446 | len: usize, // length of the attribute value |
| | 447 | reader: ReaderType, // reader positioned at value start |
| | 448 | |
| | 449 | // Copies pax attribute value into destination buffer. |
| | 450 | // Must be called with destination buffer of size at least Attribute.len. |
| | 451 | pub fn value(self: Attribute, dst: []u8) ![]const u8 { |
| | 452 | assert(self.len <= dst.len); |
| | 453 | const buf = dst[0..self.len]; |
| | 454 | const n = try self.reader.readAll(buf); |
| | 455 | if (n < self.len) return error.UnexpectedEndOfStream; |
| | 456 | try validateAttributeEnding(self.reader); |
| | 457 | if (hasNull(buf)) return error.PaxNullInValue; |
| | 458 | return buf; |
| | 459 | } |
| | 460 | }; |
| | 461 | |
| | 462 | // Iterates over pax attributes. Returns known only known attributes. |
| | 463 | // Caller has to call value in Attribute, to advance reader across value. |
| | 464 | pub fn next(self: *Self) !?Attribute { |
| | 465 | // Pax extended header consists of one or more attributes, each constructed as follows: |
| | 466 | // "%d %s=%s\n", <length>, <keyword>, <value> |
| | 467 | while (self.size > 0) { |
| | 468 | const length_buf = try self.readUntil(' '); |
| | 469 | const length = try std.fmt.parseInt(usize, length_buf, 10); // record length in bytes |
| | 470 | |
| | 471 | const keyword = try self.readUntil('='); |
| | 472 | if (hasNull(keyword)) return error.PaxNullInKeyword; |
| | 473 | |
| | 474 | // calculate value_len |
| | 475 | const value_start = length_buf.len + keyword.len + 2; // 2 separators |
| | 476 | if (length < value_start + 1 or self.size < length) return error.UnexpectedEndOfStream; |
| | 477 | const value_len = length - value_start - 1; // \n separator at end |
| | 478 | self.size -= length; |
| | 479 | |
| | 480 | const kind: PaxAttributeKind = if (eql(keyword, "path")) |
| | 481 | .path |
| | 482 | else if (eql(keyword, "linkpath")) |
| | 483 | .linkpath |
| | 484 | else if (eql(keyword, "size")) |
| | 485 | .size |
| | 486 | else { |
| | 487 | try self.reader.skipBytes(value_len, .{}); |
| | 488 | try validateAttributeEnding(self.reader); |
| | 489 | continue; |
| | 490 | }; |
| | 491 | return Attribute{ |
| | 492 | .kind = kind, |
| | 493 | .len = value_len, |
| | 494 | .reader = self.reader, |
| | 495 | }; |
| | 496 | } |
| | 497 | |
| | 498 | return null; |
| | 499 | } |
| | 500 | |
| | 501 | inline fn readUntil(self: *Self, delimiter: u8) ![]const u8 { |
| | 502 | var fbs = std.io.fixedBufferStream(&self.scratch); |
| | 503 | try self.reader.streamUntilDelimiter(fbs.writer(), delimiter, null); |
| | 504 | return fbs.getWritten(); |
| | 505 | } |
| | 506 | |
| | 507 | inline fn eql(a: []const u8, b: []const u8) bool { |
| | 508 | return std.mem.eql(u8, a, b); |
| | 509 | } |
| | 510 | |
| | 511 | inline fn hasNull(str: []const u8) bool { |
| | 512 | return (std.mem.indexOfScalar(u8, str, 0)) != null; |
| | 513 | } |
| | 514 | |
| | 515 | // Checks that each record ends with new line. |
| | 516 | inline fn validateAttributeEnding(reader: ReaderType) !void { |
| | 517 | if (try reader.readByte() != '\n') return error.PaxInvalidAttributeEnd; |
| | 518 | } |
| | 519 | }; |
| | 520 | } |
| | 521 | |
| 177 | pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !void { | 522 | pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !void { |
| 178 | switch (options.mode_mode) { | 523 | switch (options.mode_mode) { |
| 179 | .ignore => {}, | 524 | .ignore => {}, |
| ... | @@ -186,39 +531,21 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi | ... | @@ -186,39 +531,21 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi |
| 186 | @panic("TODO: unimplemented: tar ModeMode.executable_bit_only"); | 531 | @panic("TODO: unimplemented: tar ModeMode.executable_bit_only"); |
| 187 | }, | 532 | }, |
| 188 | } | 533 | } |
| 189 | var file_name_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined; | 534 | |
| 190 | var file_name_override_len: usize = 0; | 535 | var iter = iterator(reader, options.diagnostics); |
| 191 | var buffer: Buffer = .{}; | 536 | while (try iter.next()) |file| { |
| 192 | header: while (true) { | 537 | switch (file.kind) { |
| 193 | const chunk = try buffer.readChunk(reader, 1024); | | |
| 194 | switch (chunk.len) { | | |
| 195 | 0 => return, | | |
| 196 | 1...511 => return error.UnexpectedEndOfStream, | | |
| 197 | else => {}, | | |
| 198 | } | | |
| 199 | buffer.advance(512); | | |
| 200 | | | |
| 201 | const header: Header = .{ .bytes = chunk[0..512] }; | | |
| 202 | const file_size = try header.fileSize(); | | |
| 203 | const rounded_file_size = std.mem.alignForward(u64, file_size, 512); | | |
| 204 | const pad_len: usize = @intCast(rounded_file_size - file_size); | | |
| 205 | const unstripped_file_name = if (file_name_override_len > 0) | | |
| 206 | file_name_buffer[0..file_name_override_len] | | |
| 207 | else | | |
| 208 | try header.fullFileName(&file_name_buffer); | | |
| 209 | file_name_override_len = 0; | | |
| 210 | switch (header.fileType()) { | | |
| 211 | .directory => { | 538 | .directory => { |
| 212 | const file_name = try stripComponents(unstripped_file_name, options.strip_components); | 539 | const file_name = try stripComponents(file.name, options.strip_components); |
| 213 | if (file_name.len != 0 and !options.exclude_empty_directories) { | 540 | if (file_name.len != 0 and !options.exclude_empty_directories) { |
| 214 | try dir.makePath(file_name); | 541 | try dir.makePath(file_name); |
| 215 | } | 542 | } |
| 216 | }, | 543 | }, |
| 217 | .normal => { | 544 | .normal => { |
| 218 | if (file_size == 0 and unstripped_file_name.len == 0) return; | 545 | if (file.size == 0 and file.name.len == 0) return; |
| 219 | const file_name = try stripComponents(unstripped_file_name, options.strip_components); | 546 | const file_name = try stripComponents(file.name, options.strip_components); |
| 220 | | 547 | |
| 221 | const file = dir.createFile(file_name, .{}) catch |err| switch (err) { | 548 | const fs_file = dir.createFile(file_name, .{}) catch |err| switch (err) { |
| 222 | error.FileNotFound => again: { | 549 | error.FileNotFound => again: { |
| 223 | const code = code: { | 550 | const code = code: { |
| 224 | if (std.fs.path.dirname(file_name)) |dir_name| { | 551 | if (std.fs.path.dirname(file_name)) |dir_name| { |
| ... | @@ -238,70 +565,19 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi | ... | @@ -238,70 +565,19 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi |
| 238 | }, | 565 | }, |
| 239 | else => |e| return e, | 566 | else => |e| return e, |
| 240 | }; | 567 | }; |
| 241 | defer if (file) |f| f.close(); | 568 | defer if (fs_file) |f| f.close(); |
| 242 | | | |
| 243 | var file_off: usize = 0; | | |
| 244 | while (true) { | | |
| 245 | const temp = try buffer.readChunk(reader, @intCast(rounded_file_size + 512 - file_off)); | | |
| 246 | if (temp.len == 0) return error.UnexpectedEndOfStream; | | |
| 247 | const slice = temp[0..@intCast(@min(file_size - file_off, temp.len))]; | | |
| 248 | if (file) |f| try f.writeAll(slice); | | |
| 249 | | | |
| 250 | file_off += slice.len; | | |
| 251 | buffer.advance(slice.len); | | |
| 252 | if (file_off >= file_size) { | | |
| 253 | buffer.advance(pad_len); | | |
| 254 | continue :header; | | |
| 255 | } | | |
| 256 | } | | |
| 257 | }, | | |
| 258 | .extended_header => { | | |
| 259 | if (file_size == 0) { | | |
| 260 | buffer.advance(@intCast(rounded_file_size)); | | |
| 261 | continue; | | |
| 262 | } | | |
| 263 | | 569 | |
| 264 | const chunk_size: usize = @intCast(rounded_file_size + 512); | 570 | if (fs_file) |f| { |
| 265 | var data_off: usize = 0; | 571 | try file.write(f); |
| 266 | file_name_override_len = while (data_off < file_size) { | 572 | } else { |
| 267 | const slice = try buffer.readChunk(reader, chunk_size - data_off); | 573 | try file.skip(); |
| 268 | if (slice.len == 0) return error.UnexpectedEndOfStream; | | |
| 269 | const remaining_size: usize = @intCast(file_size - data_off); | | |
| 270 | const attr_info = try parsePaxAttribute(slice[0..@min(remaining_size, slice.len)], remaining_size); | | |
| 271 | | | |
| 272 | if (std.mem.eql(u8, attr_info.key, "path")) { | | |
| 273 | if (attr_info.value_len > file_name_buffer.len) return error.NameTooLong; | | |
| 274 | buffer.advance(attr_info.value_off); | | |
| 275 | data_off += attr_info.value_off; | | |
| 276 | break attr_info.value_len; | | |
| 277 | } | | |
| 278 | | | |
| 279 | try buffer.skip(reader, attr_info.size); | | |
| 280 | data_off += attr_info.size; | | |
| 281 | } else 0; | | |
| 282 | | | |
| 283 | var i: usize = 0; | | |
| 284 | while (i < file_name_override_len) { | | |
| 285 | const slice = try buffer.readChunk(reader, chunk_size - data_off - i); | | |
| 286 | if (slice.len == 0) return error.UnexpectedEndOfStream; | | |
| 287 | const copy_size: usize = @intCast(@min(file_name_override_len - i, slice.len)); | | |
| 288 | @memcpy(file_name_buffer[i .. i + copy_size], slice[0..copy_size]); | | |
| 289 | buffer.advance(copy_size); | | |
| 290 | i += copy_size; | | |
| 291 | } | 574 | } |
| 292 | | | |
| 293 | try buffer.skip(reader, @intCast(rounded_file_size - data_off - file_name_override_len)); | | |
| 294 | continue :header; | | |
| 295 | }, | 575 | }, |
| 296 | .global_extended_header => { | | |
| 297 | buffer.skip(reader, @intCast(rounded_file_size)) catch return error.TarHeadersTooBig; | | |
| 298 | }, | | |
| 299 | .hard_link => return error.TarUnsupportedFileType, | | |
| 300 | .symbolic_link => { | 576 | .symbolic_link => { |
| 301 | // The file system path of the symbolic link. | 577 | // The file system path of the symbolic link. |
| 302 | const file_name = try stripComponents(unstripped_file_name, options.strip_components); | 578 | const file_name = try stripComponents(file.name, options.strip_components); |
| 303 | // The data inside the symbolic link. | 579 | // The data inside the symbolic link. |
| 304 | const link_name = header.linkName(); | 580 | const link_name = file.link_name; |
| 305 | | 581 | |
| 306 | dir.symLink(link_name, file_name, .{}) catch |err| again: { | 582 | dir.symLink(link_name, file_name, .{}) catch |err| again: { |
| 307 | const code = code: { | 583 | const code = code: { |
| ... | @@ -323,13 +599,7 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi | ... | @@ -323,13 +599,7 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi |
| 323 | } }); | 599 | } }); |
| 324 | }; | 600 | }; |
| 325 | }, | 601 | }, |
| 326 | else => |file_type| { | 602 | else => unreachable, |
| 327 | const d = options.diagnostics orelse return error.TarUnsupportedFileType; | | |
| 328 | try d.errors.append(d.allocator, .{ .unsupported_file_type = .{ | | |
| 329 | .file_name = try d.allocator.dupe(u8, unstripped_file_name), | | |
| 330 | .file_type = file_type, | | |
| 331 | } }); | | |
| 332 | }, | | |
| 333 | } | 603 | } |
| 334 | } | 604 | } |
| 335 | } | 605 | } |
| ... | @@ -347,51 +617,137 @@ fn stripComponents(path: []const u8, count: u32) ![]const u8 { | ... | @@ -347,51 +617,137 @@ fn stripComponents(path: []const u8, count: u32) ![]const u8 { |
| 347 | return path[i..]; | 617 | return path[i..]; |
| 348 | } | 618 | } |
| 349 | | 619 | |
| 350 | test stripComponents { | 620 | test "tar stripComponents" { |
| 351 | const expectEqualStrings = std.testing.expectEqualStrings; | 621 | const expectEqualStrings = std.testing.expectEqualStrings; |
| 352 | try expectEqualStrings("a/b/c", try stripComponents("a/b/c", 0)); | 622 | try expectEqualStrings("a/b/c", try stripComponents("a/b/c", 0)); |
| 353 | try expectEqualStrings("b/c", try stripComponents("a/b/c", 1)); | 623 | try expectEqualStrings("b/c", try stripComponents("a/b/c", 1)); |
| 354 | try expectEqualStrings("c", try stripComponents("a/b/c", 2)); | 624 | try expectEqualStrings("c", try stripComponents("a/b/c", 2)); |
| 355 | } | 625 | } |
| 356 | | 626 | |
| 357 | const PaxAttributeInfo = struct { | 627 | test "tar PaxIterator" { |
| 358 | size: usize, | 628 | const Attr = struct { |
| 359 | key: []const u8, | 629 | kind: PaxAttributeKind, |
| 360 | value_off: usize, | 630 | value: []const u8 = undefined, |
| 361 | value_len: usize, | 631 | err: ?anyerror = null, |
| 362 | }; | 632 | }; |
| | 633 | const cases = [_]struct { |
| | 634 | data: []const u8, |
| | 635 | attrs: []const Attr, |
| | 636 | err: ?anyerror = null, |
| | 637 | }{ |
| | 638 | .{ // valid but unknown keys |
| | 639 | .data = |
| | 640 | \\30 mtime=1350244992.023960108 |
| | 641 | \\6 k=1 |
| | 642 | \\13 key1=val1 |
| | 643 | \\10 a=name |
| | 644 | \\9 a=name |
| | 645 | \\ |
| | 646 | , |
| | 647 | .attrs = &[_]Attr{}, |
| | 648 | }, |
| | 649 | .{ // mix of known and unknown keys |
| | 650 | .data = |
| | 651 | \\6 k=1 |
| | 652 | \\13 path=name |
| | 653 | \\17 linkpath=link |
| | 654 | \\13 key1=val1 |
| | 655 | \\12 size=123 |
| | 656 | \\13 key2=val2 |
| | 657 | \\ |
| | 658 | , |
| | 659 | .attrs = &[_]Attr{ |
| | 660 | .{ .kind = .path, .value = "name" }, |
| | 661 | .{ .kind = .linkpath, .value = "link" }, |
| | 662 | .{ .kind = .size, .value = "123" }, |
| | 663 | }, |
| | 664 | }, |
| | 665 | .{ // too short size of the second key-value pair |
| | 666 | .data = |
| | 667 | \\13 path=name |
| | 668 | \\10 linkpath=value |
| | 669 | \\ |
| | 670 | , |
| | 671 | .attrs = &[_]Attr{ |
| | 672 | .{ .kind = .path, .value = "name" }, |
| | 673 | }, |
| | 674 | .err = error.UnexpectedEndOfStream, |
| | 675 | }, |
| | 676 | .{ // too long size of the second key-value pair |
| | 677 | .data = |
| | 678 | \\13 path=name |
| | 679 | \\6 k=1 |
| | 680 | \\19 linkpath=value |
| | 681 | \\ |
| | 682 | , |
| | 683 | .attrs = &[_]Attr{ |
| | 684 | .{ .kind = .path, .value = "name" }, |
| | 685 | }, |
| | 686 | .err = error.UnexpectedEndOfStream, |
| | 687 | }, |
| 363 | | 688 | |
| 364 | fn parsePaxAttribute(data: []const u8, max_size: usize) !PaxAttributeInfo { | 689 | .{ // too long size of the second key-value pair |
| 365 | const pos_space = std.mem.indexOfScalar(u8, data, ' ') orelse return error.InvalidPaxAttribute; | 690 | .data = |
| 366 | const pos_equals = std.mem.indexOfScalarPos(u8, data, pos_space, '=') orelse return error.InvalidPaxAttribute; | 691 | \\13 path=name |
| 367 | const kv_size = try std.fmt.parseInt(usize, data[0..pos_space], 10); | 692 | \\19 linkpath=value |
| 368 | if (kv_size > max_size) { | 693 | \\6 k=1 |
| 369 | return error.InvalidPaxAttribute; | 694 | \\ |
| 370 | } | 695 | , |
| 371 | return .{ | 696 | .attrs = &[_]Attr{ |
| 372 | .size = kv_size, | 697 | .{ .kind = .path, .value = "name" }, |
| 373 | .key = data[pos_space + 1 .. pos_equals], | 698 | .{ .kind = .linkpath, .err = error.PaxInvalidAttributeEnd }, |
| 374 | .value_off = pos_equals + 1, | 699 | }, |
| 375 | .value_len = kv_size - pos_equals - 2, | 700 | }, |
| | 701 | .{ // null in keyword is not valid |
| | 702 | .data = "13 path=name\n" ++ "7 k\x00b=1\n", |
| | 703 | .attrs = &[_]Attr{ |
| | 704 | .{ .kind = .path, .value = "name" }, |
| | 705 | }, |
| | 706 | .err = error.PaxNullInKeyword, |
| | 707 | }, |
| | 708 | .{ // null in value is not valid |
| | 709 | .data = "23 path=name\x00with null\n", |
| | 710 | .attrs = &[_]Attr{ |
| | 711 | .{ .kind = .path, .err = error.PaxNullInValue }, |
| | 712 | }, |
| | 713 | }, |
| | 714 | .{ // 1000 characters path |
| | 715 | .data = "1011 path=" ++ "0123456789" ** 100 ++ "\n", |
| | 716 | .attrs = &[_]Attr{ |
| | 717 | .{ .kind = .path, .value = "0123456789" ** 100 }, |
| | 718 | }, |
| | 719 | }, |
| 376 | }; | 720 | }; |
| 377 | } | 721 | var buffer: [1024]u8 = undefined; |
| 378 | | 722 | |
| 379 | test parsePaxAttribute { | 723 | outer: for (cases) |case| { |
| 380 | const expectEqual = std.testing.expectEqual; | 724 | var stream = std.io.fixedBufferStream(case.data); |
| 381 | const expectEqualStrings = std.testing.expectEqualStrings; | 725 | var iter = paxIterator(stream.reader(), case.data.len); |
| 382 | const expectError = std.testing.expectError; | 726 | |
| 383 | const prefix = "1011 path="; | 727 | var i: usize = 0; |
| 384 | const file_name = "0123456789" ** 100; | 728 | while (iter.next() catch |err| { |
| 385 | const header = prefix ++ file_name ++ "\n"; | 729 | if (case.err) |e| { |
| 386 | const attr_info = try parsePaxAttribute(header, 1011); | 730 | try std.testing.expectEqual(e, err); |
| 387 | try expectEqual(@as(usize, 1011), attr_info.size); | 731 | continue; |
| 388 | try expectEqualStrings("path", attr_info.key); | 732 | } |
| 389 | try expectEqual(prefix.len, attr_info.value_off); | 733 | return err; |
| 390 | try expectEqual(file_name.len, attr_info.value_len); | 734 | }) |attr| : (i += 1) { |
| 391 | try expectEqual(attr_info, try parsePaxAttribute(header, 1012)); | 735 | const exp = case.attrs[i]; |
| 392 | try expectError(error.InvalidPaxAttribute, parsePaxAttribute(header, 1010)); | 736 | try std.testing.expectEqual(exp.kind, attr.kind); |
| 393 | try expectError(error.InvalidPaxAttribute, parsePaxAttribute("", 0)); | 737 | const value = attr.value(&buffer) catch |err| { |
| | 738 | if (exp.err) |e| { |
| | 739 | try std.testing.expectEqual(e, err); |
| | 740 | break :outer; |
| | 741 | } |
| | 742 | return err; |
| | 743 | }; |
| | 744 | try std.testing.expectEqualStrings(exp.value, value); |
| | 745 | } |
| | 746 | try std.testing.expectEqual(case.attrs.len, i); |
| | 747 | try std.testing.expect(case.err == null); |
| | 748 | } |
| 394 | } | 749 | } |
| 395 | | 750 | |
| 396 | const std = @import("std.zig"); | 751 | test { |
| 397 | const assert = std.debug.assert; | 752 | _ = @import("tar/test.zig"); |
| | 753 | } |