| ... | ... | @@ -15,8 +15,7 @@ |
| 15 | 15 | /// GNU tar reference: https://www.gnu.org/software/tar/manual/html_node/Standard.html |
| 16 | 16 | /// pax reference: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html#tag_20_92_13 |
| 17 | 17 | /// |
| 18 | | //const std = @import("std.zig"); |
| 19 | | const std = @import("std"); |
| 18 | const std = @import("std.zig"); |
| 20 | 19 | const assert = std.debug.assert; |
| 21 | 20 | |
| 22 | 21 | pub const Options = struct { |
| ... | ... | @@ -226,6 +225,276 @@ fn nullStr(str: []const u8) []const u8 { |
| 226 | 225 | return str; |
| 227 | 226 | } |
| 228 | 227 | |
| 228 | pub fn tarReader(reader: anytype, diagnostics: ?*Options.Diagnostics) TarReader(@TypeOf(reader)) { |
| 229 | return .{ |
| 230 | .reader = reader, |
| 231 | .diagnostics = diagnostics, |
| 232 | }; |
| 233 | } |
| 234 | |
| 235 | fn TarReader(comptime ReaderType: type) type { |
| 236 | return struct { |
| 237 | reader: ReaderType, |
| 238 | diagnostics: ?*Options.Diagnostics, |
| 239 | |
| 240 | // buffers for heeader and file attributes |
| 241 | header_buffer: [Header.SIZE]u8 = undefined, |
| 242 | file_name_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined, |
| 243 | link_name_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined, |
| 244 | |
| 245 | // bytes of padding to the end of the block |
| 246 | padding: usize = 0, |
| 247 | // current tar file |
| 248 | file: File = undefined, |
| 249 | |
| 250 | pub const File = struct { |
| 251 | name: []const u8, // name of file, symlink or directory |
| 252 | link_name: []const u8, // target name of symlink |
| 253 | size: usize, // size of the file in bytes |
| 254 | mode: u32, |
| 255 | kind: Header.Kind, |
| 256 | |
| 257 | reader: ReaderType, |
| 258 | |
| 259 | // Writes file content to writer. |
| 260 | pub fn write(self: File, writer: anytype) !void { |
| 261 | var buffer: [4096]u8 = undefined; |
| 262 | |
| 263 | var n: usize = 0; |
| 264 | while (n < self.size) { |
| 265 | const buf = buffer[0..@min(buffer.len, self.size - n)]; |
| 266 | try self.reader.readNoEof(buf); |
| 267 | try writer.writeAll(buf); |
| 268 | n += buf.len; |
| 269 | } |
| 270 | } |
| 271 | |
| 272 | // Skips file content. Advances reader. |
| 273 | pub fn skip(self: File) !void { |
| 274 | try self.reader.skipBytes(self.size, .{}); |
| 275 | } |
| 276 | }; |
| 277 | |
| 278 | const Self = @This(); |
| 279 | |
| 280 | fn readHeader(self: *Self) !?Header { |
| 281 | if (self.padding > 0) { |
| 282 | try self.reader.skipBytes(self.padding, .{}); |
| 283 | } |
| 284 | const n = try self.reader.readAll(&self.header_buffer); |
| 285 | if (n == 0) return null; |
| 286 | if (n < Header.SIZE) return error.UnexpectedEndOfStream; |
| 287 | const header = Header{ .bytes = self.header_buffer[0..Header.SIZE] }; |
| 288 | if (try header.checkChksum() == 0) return null; |
| 289 | return header; |
| 290 | } |
| 291 | |
| 292 | inline fn readString(self: *Self, size: usize, buffer: []u8) ![]const u8 { |
| 293 | assert(buffer.len >= size); |
| 294 | const buf = buffer[0..size]; |
| 295 | try self.reader.readNoEof(buf); |
| 296 | return nullStr(buf); |
| 297 | } |
| 298 | |
| 299 | inline fn initFile(self: *Self) void { |
| 300 | self.file = File{ |
| 301 | .name = self.file_name_buffer[0..0], |
| 302 | .link_name = self.link_name_buffer[0..0], |
| 303 | .size = 0, |
| 304 | .kind = .normal, |
| 305 | .mode = 0, |
| 306 | .reader = self.reader, |
| 307 | }; |
| 308 | } |
| 309 | |
| 310 | // Number of padding bytes in the last file block. |
| 311 | inline fn blockPadding(size: usize) usize { |
| 312 | const block_rounded = std.mem.alignForward(usize, size, Header.SIZE); // size rounded to te block boundary |
| 313 | return block_rounded - size; |
| 314 | } |
| 315 | |
| 316 | /// Iterates through the tar archive as if it is a series of files. |
| 317 | /// Internally, the tar format often uses entries (header with optional |
| 318 | /// content) to add meta data that describes the next file. These |
| 319 | /// entries should not normally be visible to the outside. As such, this |
| 320 | /// loop iterates through one or more entries until it collects a all |
| 321 | /// file attributes. |
| 322 | pub fn next(self: *Self) !?File { |
| 323 | self.initFile(); |
| 324 | |
| 325 | while (try self.readHeader()) |header| { |
| 326 | const kind = header.kind(); |
| 327 | const size: usize = @intCast(try header.size()); |
| 328 | self.padding = blockPadding(size); |
| 329 | |
| 330 | switch (kind) { |
| 331 | // File types to retrun upstream |
| 332 | .directory, .normal, .symbolic_link => { |
| 333 | self.file.kind = kind; |
| 334 | self.file.mode = try header.mode(); |
| 335 | |
| 336 | // set file attributes if not already set by prefix/extended headers |
| 337 | if (self.file.size == 0) { |
| 338 | self.file.size = size; |
| 339 | } |
| 340 | if (self.file.link_name.len == 0) { |
| 341 | self.file.link_name = header.linkName(self.link_name_buffer[0..Header.LINK_NAME_SIZE]); |
| 342 | } |
| 343 | if (self.file.name.len == 0) { |
| 344 | self.file.name = try header.fullName(self.file_name_buffer[0..Header.MAX_NAME_SIZE]); |
| 345 | } |
| 346 | |
| 347 | self.padding = blockPadding(self.file.size); |
| 348 | return self.file; |
| 349 | }, |
| 350 | // Prefix header types |
| 351 | .gnu_long_name => { |
| 352 | self.file.name = try self.readString(size, &self.file_name_buffer); |
| 353 | }, |
| 354 | .gnu_long_link => { |
| 355 | self.file.link_name = try self.readString(size, &self.link_name_buffer); |
| 356 | }, |
| 357 | .extended_header => { |
| 358 | // Use just attributes from last extended header. |
| 359 | self.initFile(); |
| 360 | |
| 361 | var rdr = paxReader(self.reader, size); |
| 362 | while (try rdr.next()) |attr| { |
| 363 | switch (attr.kind) { |
| 364 | .path => { |
| 365 | self.file.name = try attr.value(&self.file_name_buffer); |
| 366 | }, |
| 367 | .linkpath => { |
| 368 | self.file.link_name = try attr.value(&self.link_name_buffer); |
| 369 | }, |
| 370 | .size => { |
| 371 | var buf: [64]u8 = undefined; |
| 372 | self.file.size = try std.fmt.parseInt(usize, try attr.value(&buf), 10); |
| 373 | }, |
| 374 | } |
| 375 | } |
| 376 | }, |
| 377 | // Ignored header type |
| 378 | .global_extended_header => { |
| 379 | self.reader.skipBytes(size, .{}) catch return error.TarHeadersTooBig; |
| 380 | }, |
| 381 | // All other are unsupported header types |
| 382 | else => { |
| 383 | const d = self.diagnostics orelse return error.TarUnsupportedHeader; |
| 384 | try d.errors.append(d.allocator, .{ .unsupported_file_type = .{ |
| 385 | .file_name = try d.allocator.dupe(u8, header.name()), |
| 386 | .file_type = kind, |
| 387 | } }); |
| 388 | }, |
| 389 | } |
| 390 | } |
| 391 | return null; |
| 392 | } |
| 393 | }; |
| 394 | } |
| 395 | |
| 396 | // Pax attributes reader. |
| 397 | // Size is length of pax extended header in reader. |
| 398 | fn paxReader(reader: anytype, size: usize) PaxReader(@TypeOf(reader)) { |
| 399 | return PaxReader(@TypeOf(reader)){ |
| 400 | .reader = reader, |
| 401 | .size = size, |
| 402 | }; |
| 403 | } |
| 404 | |
| 405 | const PaxAttributeKind = enum { |
| 406 | path, |
| 407 | linkpath, |
| 408 | size, |
| 409 | }; |
| 410 | |
| 411 | fn PaxReader(comptime ReaderType: type) type { |
| 412 | return struct { |
| 413 | size: usize, // cumulative size of all pax attributes |
| 414 | reader: ReaderType, |
| 415 | // scratch buffer used for reading attribute length and keyword |
| 416 | scratch: [128]u8 = undefined, |
| 417 | |
| 418 | const Self = @This(); |
| 419 | |
| 420 | const Attribute = struct { |
| 421 | kind: PaxAttributeKind, |
| 422 | len: usize, // length of the attribute value |
| 423 | reader: ReaderType, // reader positioned at value start |
| 424 | |
| 425 | // Copies pax attribute value into destination buffer. |
| 426 | // Must be called with destination buffer of size at least Attribute.len. |
| 427 | pub fn value(self: Attribute, dst: []u8) ![]const u8 { |
| 428 | assert(self.len <= dst.len); |
| 429 | const buf = dst[0..self.len]; |
| 430 | const n = try self.reader.readAll(buf); |
| 431 | if (n < self.len) return error.UnexpectedEndOfStream; |
| 432 | try validateAttributeEnding(self.reader); |
| 433 | if (hasNull(buf)) return error.PaxNullInValue; |
| 434 | return buf; |
| 435 | } |
| 436 | }; |
| 437 | |
| 438 | // Iterates over pax attributes. Returns known only known attributes. |
| 439 | // Caller has to call value in Attribute, to advance reader across value. |
| 440 | pub fn next(self: *Self) !?Attribute { |
| 441 | // Pax extended header consists of one or more attributes, each constructed as follows: |
| 442 | // "%d %s=%s\n", <length>, <keyword>, <value> |
| 443 | while (self.size > 0) { |
| 444 | const length_buf = try self.readUntil(' '); |
| 445 | const length = try std.fmt.parseInt(usize, length_buf, 10); // record length in bytes |
| 446 | |
| 447 | const keyword = try self.readUntil('='); |
| 448 | if (hasNull(keyword)) return error.PaxNullInKeyword; |
| 449 | |
| 450 | // calculate value_len |
| 451 | const value_start = length_buf.len + keyword.len + 2; // 2 separators |
| 452 | if (length < value_start + 1 or self.size < length) return error.UnexpectedEndOfStream; |
| 453 | const value_len = length - value_start - 1; // \n separator at end |
| 454 | self.size -= length; |
| 455 | |
| 456 | const kind: PaxAttributeKind = if (eql(keyword, "path")) |
| 457 | .path |
| 458 | else if (eql(keyword, "linkpath")) |
| 459 | .linkpath |
| 460 | else if (eql(keyword, "size")) |
| 461 | .size |
| 462 | else { |
| 463 | try self.reader.skipBytes(value_len, .{}); |
| 464 | try validateAttributeEnding(self.reader); |
| 465 | continue; |
| 466 | }; |
| 467 | return Attribute{ |
| 468 | .kind = kind, |
| 469 | .len = value_len, |
| 470 | .reader = self.reader, |
| 471 | }; |
| 472 | } |
| 473 | |
| 474 | return null; |
| 475 | } |
| 476 | |
| 477 | inline fn readUntil(self: *Self, delimiter: u8) ![]const u8 { |
| 478 | var fbs = std.io.fixedBufferStream(&self.scratch); |
| 479 | try self.reader.streamUntilDelimiter(fbs.writer(), delimiter, null); |
| 480 | return fbs.getWritten(); |
| 481 | } |
| 482 | |
| 483 | inline fn eql(a: []const u8, b: []const u8) bool { |
| 484 | return std.mem.eql(u8, a, b); |
| 485 | } |
| 486 | |
| 487 | inline fn hasNull(str: []const u8) bool { |
| 488 | return (std.mem.indexOfScalar(u8, str, 0)) != null; |
| 489 | } |
| 490 | |
| 491 | // Checks that each record ends with new line. |
| 492 | inline fn validateAttributeEnding(reader: ReaderType) !void { |
| 493 | if (try reader.readByte() != '\n') return error.PaxInvalidAttributeEnd; |
| 494 | } |
| 495 | }; |
| 496 | } |
| 497 | |
| 229 | 498 | pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !void { |
| 230 | 499 | switch (options.mode_mode) { |
| 231 | 500 | .ignore => {}, |
| ... | ... | @@ -639,170 +908,70 @@ test "tar run Go test cases" { |
| 639 | 908 | .{ |
| 640 | 909 | // Size in gnu extended format, and name in pax attribute. |
| 641 | 910 | .path = "writer-big-long.tar", |
| 642 | | .files = &[_]Case.File{ |
| 643 | | .{ |
| 644 | | .name = "longname/" ** 15 ++ "16gig.txt", |
| 645 | | .size = 16 * 1024 * 1024 * 1024, |
| 646 | | .mode = 0o644, |
| 647 | | .truncated = true, |
| 648 | | }, |
| 649 | | }, |
| 650 | | }, |
| 651 | | }; |
| 652 | | |
| 653 | | for (cases) |case| { |
| 654 | | var fs_file = try test_dir.openFile(case.path, .{}); |
| 655 | | defer fs_file.close(); |
| 656 | | |
| 657 | | //var iter = iterator(fs_file.reader(), null); |
| 658 | | var iter = tarReader(fs_file.reader(), null); |
| 659 | | var i: usize = 0; |
| 660 | | while (iter.next() catch |err| { |
| 661 | | if (case.err) |e| { |
| 662 | | try std.testing.expectEqual(e, err); |
| 663 | | continue; |
| 664 | | } else { |
| 665 | | return err; |
| 666 | | } |
| 667 | | }) |actual| : (i += 1) { |
| 668 | | const expected = case.files[i]; |
| 669 | | try std.testing.expectEqualStrings(expected.name, actual.name); |
| 670 | | try std.testing.expectEqual(expected.size, actual.size); |
| 671 | | try std.testing.expectEqual(expected.kind, actual.kind); |
| 672 | | try std.testing.expectEqual(expected.mode, actual.mode); |
| 673 | | try std.testing.expectEqualStrings(expected.link_name, actual.link_name); |
| 674 | | |
| 675 | | if (case.chksums.len > i) { |
| 676 | | var md5writer = Md5Writer{}; |
| 677 | | try actual.write(&md5writer); |
| 678 | | const chksum = md5writer.chksum(); |
| 679 | | try std.testing.expectEqualStrings(case.chksums[i], &chksum); |
| 680 | | } else { |
| 681 | | if (!expected.truncated) try actual.skip(); // skip file content |
| 682 | | } |
| 683 | | } |
| 684 | | try std.testing.expectEqual(case.files.len, i); |
| 685 | | } |
| 686 | | } |
| 687 | | |
| 688 | | // used in test to calculate file chksum |
| 689 | | const Md5Writer = struct { |
| 690 | | h: std.crypto.hash.Md5 = std.crypto.hash.Md5.init(.{}), |
| 691 | | |
| 692 | | pub fn writeAll(self: *Md5Writer, buf: []const u8) !void { |
| 693 | | self.h.update(buf); |
| 694 | | } |
| 695 | | |
| 696 | | pub fn writeByte(self: *Md5Writer, byte: u8) !void { |
| 697 | | self.h.update(&[_]u8{byte}); |
| 698 | | } |
| 699 | | |
| 700 | | pub fn chksum(self: *Md5Writer) [32]u8 { |
| 701 | | var s = [_]u8{0} ** 16; |
| 702 | | self.h.final(&s); |
| 703 | | return std.fmt.bytesToHex(s, .lower); |
| 704 | | } |
| 705 | | }; |
| 706 | | |
| 707 | | fn paxReader(reader: anytype, size: usize) PaxReader(@TypeOf(reader)) { |
| 708 | | return PaxReader(@TypeOf(reader)){ |
| 709 | | .reader = reader, |
| 710 | | .size = size, |
| 711 | | }; |
| 712 | | } |
| 713 | | |
| 714 | | const PaxAttributeKind = enum { |
| 715 | | path, |
| 716 | | linkpath, |
| 717 | | size, |
| 718 | | }; |
| 719 | | |
| 720 | | fn PaxReader(comptime ReaderType: type) type { |
| 721 | | return struct { |
| 722 | | size: usize, // cumulative size of all pax attributes |
| 723 | | reader: ReaderType, |
| 724 | | // scratch buffer used for reading attribute length and keyword |
| 725 | | scratch: [128]u8 = undefined, |
| 726 | | |
| 727 | | const Self = @This(); |
| 728 | | |
| 729 | | const Attribute = struct { |
| 730 | | kind: PaxAttributeKind, |
| 731 | | len: usize, // length of the attribute value |
| 732 | | reader: ReaderType, // reader positioned at value start |
| 733 | | |
| 734 | | // Copies pax attribute value into destination buffer. |
| 735 | | // Must be called with destination buffer of size at least Attribute.len. |
| 736 | | pub fn value(self: Attribute, dst: []u8) ![]const u8 { |
| 737 | | assert(self.len <= dst.len); |
| 738 | | const buf = dst[0..self.len]; |
| 739 | | const n = try self.reader.readAll(buf); |
| 740 | | if (n < self.len) return error.UnexpectedEndOfStream; |
| 741 | | try validateAttributeEnding(self.reader); |
| 742 | | if (hasNull(buf)) return error.PaxNullInValue; |
| 743 | | return buf; |
| 744 | | } |
| 745 | | }; |
| 746 | | |
| 747 | | // Iterates over pax attributes. Returns known only known attributes. |
| 748 | | // Caller has to call value in Attribute, to advance reader across value. |
| 749 | | pub fn next(self: *Self) !?Attribute { |
| 750 | | // Pax extended header consists of one or more attributes, each constructed as follows: |
| 751 | | // "%d %s=%s\n", <length>, <keyword>, <value> |
| 752 | | while (self.size > 0) { |
| 753 | | const length_buf = try self.readUntil(' '); |
| 754 | | const length = try std.fmt.parseInt(usize, length_buf, 10); // record length in bytes |
| 755 | | |
| 756 | | const keyword = try self.readUntil('='); |
| 757 | | if (hasNull(keyword)) return error.PaxNullInKeyword; |
| 911 | .files = &[_]Case.File{ |
| 912 | .{ |
| 913 | .name = "longname/" ** 15 ++ "16gig.txt", |
| 914 | .size = 16 * 1024 * 1024 * 1024, |
| 915 | .mode = 0o644, |
| 916 | .truncated = true, |
| 917 | }, |
| 918 | }, |
| 919 | }, |
| 920 | }; |
| 758 | 921 | |
| 759 | | // calculate value_len |
| 760 | | const value_start = length_buf.len + keyword.len + 2; // 2 separators |
| 761 | | if (length < value_start + 1 or self.size < length) return error.UnexpectedEndOfStream; |
| 762 | | const value_len = length - value_start - 1; // \n separator at end |
| 763 | | self.size -= length; |
| 922 | for (cases) |case| { |
| 923 | var fs_file = try test_dir.openFile(case.path, .{}); |
| 924 | defer fs_file.close(); |
| 764 | 925 | |
| 765 | | const kind: PaxAttributeKind = if (eql(keyword, "path")) |
| 766 | | .path |
| 767 | | else if (eql(keyword, "linkpath")) |
| 768 | | .linkpath |
| 769 | | else if (eql(keyword, "size")) |
| 770 | | .size |
| 771 | | else { |
| 772 | | try self.reader.skipBytes(value_len, .{}); |
| 773 | | try validateAttributeEnding(self.reader); |
| 774 | | continue; |
| 775 | | }; |
| 776 | | return Attribute{ |
| 777 | | .kind = kind, |
| 778 | | .len = value_len, |
| 779 | | .reader = self.reader, |
| 780 | | }; |
| 926 | //var iter = iterator(fs_file.reader(), null); |
| 927 | var iter = tarReader(fs_file.reader(), null); |
| 928 | var i: usize = 0; |
| 929 | while (iter.next() catch |err| { |
| 930 | if (case.err) |e| { |
| 931 | try std.testing.expectEqual(e, err); |
| 932 | continue; |
| 933 | } else { |
| 934 | return err; |
| 781 | 935 | } |
| 936 | }) |actual| : (i += 1) { |
| 937 | const expected = case.files[i]; |
| 938 | try std.testing.expectEqualStrings(expected.name, actual.name); |
| 939 | try std.testing.expectEqual(expected.size, actual.size); |
| 940 | try std.testing.expectEqual(expected.kind, actual.kind); |
| 941 | try std.testing.expectEqual(expected.mode, actual.mode); |
| 942 | try std.testing.expectEqualStrings(expected.link_name, actual.link_name); |
| 782 | 943 | |
| 783 | | return null; |
| 944 | if (case.chksums.len > i) { |
| 945 | var md5writer = Md5Writer{}; |
| 946 | try actual.write(&md5writer); |
| 947 | const chksum = md5writer.chksum(); |
| 948 | try std.testing.expectEqualStrings(case.chksums[i], &chksum); |
| 949 | } else { |
| 950 | if (!expected.truncated) try actual.skip(); // skip file content |
| 951 | } |
| 784 | 952 | } |
| 953 | try std.testing.expectEqual(case.files.len, i); |
| 954 | } |
| 955 | } |
| 785 | 956 | |
| 786 | | inline fn readUntil(self: *Self, delimiter: u8) ![]const u8 { |
| 787 | | var fbs = std.io.fixedBufferStream(&self.scratch); |
| 788 | | try self.reader.streamUntilDelimiter(fbs.writer(), delimiter, null); |
| 789 | | return fbs.getWritten(); |
| 790 | | } |
| 957 | // used in test to calculate file chksum |
| 958 | const Md5Writer = struct { |
| 959 | h: std.crypto.hash.Md5 = std.crypto.hash.Md5.init(.{}), |
| 791 | 960 | |
| 792 | | inline fn eql(a: []const u8, b: []const u8) bool { |
| 793 | | return std.mem.eql(u8, a, b); |
| 794 | | } |
| 961 | pub fn writeAll(self: *Md5Writer, buf: []const u8) !void { |
| 962 | self.h.update(buf); |
| 963 | } |
| 795 | 964 | |
| 796 | | inline fn hasNull(str: []const u8) bool { |
| 797 | | return (std.mem.indexOfScalar(u8, str, 0)) != null; |
| 798 | | } |
| 965 | pub fn writeByte(self: *Md5Writer, byte: u8) !void { |
| 966 | self.h.update(&[_]u8{byte}); |
| 967 | } |
| 799 | 968 | |
| 800 | | // Checks that each record ends with new line. |
| 801 | | inline fn validateAttributeEnding(reader: ReaderType) !void { |
| 802 | | if (try reader.readByte() != '\n') return error.PaxInvalidAttributeEnd; |
| 803 | | } |
| 804 | | }; |
| 805 | | } |
| 969 | pub fn chksum(self: *Md5Writer) [32]u8 { |
| 970 | var s = [_]u8{0} ** 16; |
| 971 | self.h.final(&s); |
| 972 | return std.fmt.bytesToHex(s, .lower); |
| 973 | } |
| 974 | }; |
| 806 | 975 | |
| 807 | 976 | test "tar PaxReader" { |
| 808 | 977 | const Attr = struct { |
| ... | ... | @@ -927,171 +1096,3 @@ test "tar PaxReader" { |
| 927 | 1096 | try std.testing.expect(case.err == null); |
| 928 | 1097 | } |
| 929 | 1098 | } |
| 930 | | |
| 931 | | pub fn tarReader(reader: anytype, diagnostics: ?*Options.Diagnostics) TarReader(@TypeOf(reader)) { |
| 932 | | return .{ |
| 933 | | .reader = reader, |
| 934 | | .diagnostics = diagnostics, |
| 935 | | }; |
| 936 | | } |
| 937 | | |
| 938 | | fn TarReader(comptime ReaderType: type) type { |
| 939 | | return struct { |
| 940 | | reader: ReaderType, |
| 941 | | diagnostics: ?*Options.Diagnostics, |
| 942 | | |
| 943 | | // buffers for heeader and file attributes |
| 944 | | header_buffer: [Header.SIZE]u8 = undefined, |
| 945 | | file_name_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined, |
| 946 | | link_name_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined, |
| 947 | | |
| 948 | | // bytes of padding to the end of the block |
| 949 | | padding: usize = 0, |
| 950 | | // current tar file |
| 951 | | file: File = undefined, |
| 952 | | |
| 953 | | pub const File = struct { |
| 954 | | name: []const u8, // name of file, symlink or directory |
| 955 | | link_name: []const u8, // target name of symlink |
| 956 | | size: usize, // size of the file in bytes |
| 957 | | mode: u32, |
| 958 | | kind: Header.Kind, |
| 959 | | |
| 960 | | reader: ReaderType, |
| 961 | | |
| 962 | | // Writes file content to writer. |
| 963 | | pub fn write(self: File, writer: anytype) !void { |
| 964 | | var buffer: [4096]u8 = undefined; |
| 965 | | |
| 966 | | var n: usize = 0; |
| 967 | | while (n < self.size) { |
| 968 | | const buf = buffer[0..@min(buffer.len, self.size - n)]; |
| 969 | | try self.reader.readNoEof(buf); |
| 970 | | try writer.writeAll(buf); |
| 971 | | n += buf.len; |
| 972 | | } |
| 973 | | } |
| 974 | | |
| 975 | | // Skips file content. Advances reader. |
| 976 | | pub fn skip(self: File) !void { |
| 977 | | try self.reader.skipBytes(self.size, .{}); |
| 978 | | } |
| 979 | | }; |
| 980 | | |
| 981 | | const Self = @This(); |
| 982 | | |
| 983 | | fn readHeader(self: *Self) !?Header { |
| 984 | | if (self.padding > 0) { |
| 985 | | try self.reader.skipBytes(self.padding, .{}); |
| 986 | | } |
| 987 | | const n = try self.reader.readAll(&self.header_buffer); |
| 988 | | if (n == 0) return null; |
| 989 | | if (n < Header.SIZE) return error.UnexpectedEndOfStream; |
| 990 | | const header = Header{ .bytes = self.header_buffer[0..Header.SIZE] }; |
| 991 | | if (try header.checkChksum() == 0) return null; |
| 992 | | return header; |
| 993 | | } |
| 994 | | |
| 995 | | inline fn readString(self: *Self, size: usize, buffer: []u8) ![]const u8 { |
| 996 | | assert(buffer.len >= size); |
| 997 | | const buf = buffer[0..size]; |
| 998 | | try self.reader.readNoEof(buf); |
| 999 | | return nullStr(buf); |
| 1000 | | } |
| 1001 | | |
| 1002 | | inline fn initFile(self: *Self) void { |
| 1003 | | self.file = File{ |
| 1004 | | .name = self.file_name_buffer[0..0], |
| 1005 | | .link_name = self.link_name_buffer[0..0], |
| 1006 | | .size = 0, |
| 1007 | | .kind = .normal, |
| 1008 | | .mode = 0, |
| 1009 | | .reader = self.reader, |
| 1010 | | }; |
| 1011 | | } |
| 1012 | | |
| 1013 | | // Number of padding bytes in the last file block. |
| 1014 | | inline fn blockPadding(size: usize) usize { |
| 1015 | | const block_rounded = std.mem.alignForward(usize, size, Header.SIZE); // size rounded to te block boundary |
| 1016 | | return block_rounded - size; |
| 1017 | | } |
| 1018 | | |
| 1019 | | // Externally, `next` iterates through the tar archive as if it is a |
| 1020 | | // series of files. Internally, the tar format often uses fake "files" |
| 1021 | | // to add meta data that describes the next file. These meta data |
| 1022 | | // "files" should not normally be visible to the outside. As such, this |
| 1023 | | // loop iterates through one or more "header files" until it finds a |
| 1024 | | // "normal file". |
| 1025 | | pub fn next(self: *Self) !?File { |
| 1026 | | self.initFile(); |
| 1027 | | |
| 1028 | | while (try self.readHeader()) |header| { |
| 1029 | | const kind = header.kind(); |
| 1030 | | const size: usize = @intCast(try header.size()); |
| 1031 | | self.padding = blockPadding(size); |
| 1032 | | |
| 1033 | | switch (kind) { |
| 1034 | | // File types to retrun upstream |
| 1035 | | .directory, .normal, .symbolic_link => { |
| 1036 | | self.file.kind = kind; |
| 1037 | | self.file.mode = try header.mode(); |
| 1038 | | |
| 1039 | | // set file attributes if not already set by prefix/extended headers |
| 1040 | | if (self.file.size == 0) { |
| 1041 | | self.file.size = size; |
| 1042 | | } |
| 1043 | | if (self.file.link_name.len == 0) { |
| 1044 | | self.file.link_name = header.linkName(self.link_name_buffer[0..Header.LINK_NAME_SIZE]); |
| 1045 | | } |
| 1046 | | if (self.file.name.len == 0) { |
| 1047 | | self.file.name = try header.fullName(self.file_name_buffer[0..Header.MAX_NAME_SIZE]); |
| 1048 | | } |
| 1049 | | |
| 1050 | | self.padding = blockPadding(self.file.size); |
| 1051 | | return self.file; |
| 1052 | | }, |
| 1053 | | // Prefix header types |
| 1054 | | .gnu_long_name => { |
| 1055 | | self.file.name = try self.readString(size, &self.file_name_buffer); |
| 1056 | | }, |
| 1057 | | .gnu_long_link => { |
| 1058 | | self.file.link_name = try self.readString(size, &self.link_name_buffer); |
| 1059 | | }, |
| 1060 | | .extended_header => { |
| 1061 | | // Use just attributes from last extended header. |
| 1062 | | self.initFile(); |
| 1063 | | |
| 1064 | | var rdr = paxReader(self.reader, size); |
| 1065 | | while (try rdr.next()) |attr| { |
| 1066 | | switch (attr.kind) { |
| 1067 | | .path => { |
| 1068 | | self.file.name = try attr.value(&self.file_name_buffer); |
| 1069 | | }, |
| 1070 | | .linkpath => { |
| 1071 | | self.file.link_name = try attr.value(&self.link_name_buffer); |
| 1072 | | }, |
| 1073 | | .size => { |
| 1074 | | var buf: [64]u8 = undefined; |
| 1075 | | self.file.size = try std.fmt.parseInt(usize, try attr.value(&buf), 10); |
| 1076 | | }, |
| 1077 | | } |
| 1078 | | } |
| 1079 | | }, |
| 1080 | | // Ignored header type |
| 1081 | | .global_extended_header => { |
| 1082 | | self.reader.skipBytes(size, .{}) catch return error.TarHeadersTooBig; |
| 1083 | | }, |
| 1084 | | // All other are unsupported header types |
| 1085 | | else => { |
| 1086 | | const d = self.diagnostics orelse return error.TarUnsupportedHeader; |
| 1087 | | try d.errors.append(d.allocator, .{ .unsupported_file_type = .{ |
| 1088 | | .file_name = try d.allocator.dupe(u8, header.name()), |
| 1089 | | .file_type = kind, |
| 1090 | | } }); |
| 1091 | | }, |
| 1092 | | } |
| 1093 | | } |
| 1094 | | return null; |
| 1095 | | } |
| 1096 | | }; |
| 1097 | | } |