authorgravatar for igor.anic@gmail.comIgor Anić <igor.anic@gmail.com> 2023-12-11 15:48:43+01:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-01-13 19:37:33-07:00
logdbab45cfc6a952aa4ec873d6a33c487cd431bc62
tree34a1166b2faa6a932cc256d41a9a41222451f1a3
parent58e0e509c6dc8fae77e668ef8ee267dfdb619196

tar: replace custom buffered reader with std.io


1 files changed, 366 insertions(+), 442 deletions(-)

lib/std/tar.zig+366-442
......@@ -1,4 +1,3 @@
1const std = @import("std.zig");
21/// Tar archive is single ordinary file which can contain many files (or
32/// directories, symlinks, ...). It's build by series of blocks each size of 512
43/// bytes. First block of each entry is header which defines type, name, size
......@@ -15,7 +14,9 @@ const std = @import("std.zig");
1514///
1615/// GNU tar reference: https://www.gnu.org/software/tar/manual/html_node/Standard.html
1716/// pax reference: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html#tag_20_92_13
18
17///
18//const std = @import("std.zig");
19const std = @import("std");
1920const assert = std.debug.assert;
2021
2122pub const Options = struct {
......@@ -224,338 +225,6 @@ inline fn blockPadding(size: usize) usize {
224225 return block_rounded - size;
225226}
226227
227fn BufferedReader(comptime ReaderType: type) type {
228 return struct {
229 underlying_reader: ReaderType,
230 buffer: [BLOCK_SIZE * 8]u8 = undefined,
231 start: usize = 0,
232 end: usize = 0,
233
234 const Self = @This();
235
236 // Fills buffer from underlying unbuffered reader.
237 fn fillBuffer(self: *Self) !void {
238 self.removeUsed();
239 self.end += try self.underlying_reader.read(self.buffer[self.end..]);
240 }
241
242 // Returns slice of size count or how much fits into buffer.
243 pub fn readSlice(self: *Self, count: usize) ![]const u8 {
244 if (count <= self.end - self.start) {
245 return self.buffer[self.start .. self.start + count];
246 }
247 try self.fillBuffer();
248 const buf = self.buffer[self.start..self.end];
249 if (buf.len == 0) return error.UnexpectedEndOfStream;
250 return buf[0..@min(count, buf.len)];
251 }
252
253 // Returns tar header block, 512 bytes, or null if eof. Before reading
254 // advances buffer for padding of the previous block, to position reader
255 // at the start of new block. After reading advances for block size, to
256 // position reader at the start of the file content.
257 pub fn readHeader(self: *Self, padding: usize) !?[]const u8 {
258 try self.skip(padding);
259 const buf = self.readSlice(BLOCK_SIZE) catch return null;
260 if (buf.len < BLOCK_SIZE) return error.UnexpectedEndOfStream;
261 self.advance(BLOCK_SIZE);
262 return buf[0..BLOCK_SIZE];
263 }
264
265 // Returns byte at current position in buffer.
266 pub fn readByte(self: *@This()) u8 {
267 assert(self.start < self.end);
268 return self.buffer[self.start];
269 }
270
271 // Advances reader for count bytes, assumes that we have that number of
272 // bytes in buffer.
273 pub fn advance(self: *Self, count: usize) void {
274 self.start += count;
275 assert(self.start <= self.end);
276 }
277
278 // Advances reader without assuming that count bytes are in the buffer.
279 pub fn skip(self: *Self, count: usize) !void {
280 if (self.start + count > self.end) {
281 try self.underlying_reader.skipBytes(self.start + count - self.end, .{});
282 self.start = self.end;
283 } else {
284 self.advance(count);
285 }
286 }
287
288 // Removes used part of the buffer.
289 inline fn removeUsed(self: *Self) void {
290 const dest_end = self.end - self.start;
291 if (self.start == 0 or dest_end > self.start) return;
292 @memcpy(self.buffer[0..dest_end], self.buffer[self.start..self.end]);
293 self.end = dest_end;
294 self.start = 0;
295 }
296
297 // Writes count bytes to the writer. Advances reader.
298 pub fn write(self: *Self, writer: anytype, count: usize) !void {
299 var pos: usize = 0;
300 while (pos < count) {
301 const slice = try self.readSlice(count - pos);
302 try writer.writeAll(slice);
303 self.advance(slice.len);
304 pos += slice.len;
305 }
306 }
307
308 // Copies dst.len bytes into dst buffer. Advances reader.
309 pub fn copy(self: *Self, dst: []u8) ![]const u8 {
310 var pos: usize = 0;
311 while (pos < dst.len) {
312 const slice = try self.readSlice(dst.len - pos);
313 @memcpy(dst[pos .. pos + slice.len], slice);
314 self.advance(slice.len);
315 pos += slice.len;
316 }
317 return dst;
318 }
319
320 pub fn paxFileReader(self: *Self, size: usize) PaxFileReader {
321 return .{
322 .size = size,
323 .reader = self,
324 .offset = 0,
325 };
326 }
327
328 const PaxFileReader = struct {
329 size: usize,
330 offset: usize = 0,
331 reader: *Self,
332
333 const PaxKeyKind = enum {
334 path,
335 linkpath,
336 size,
337 };
338
339 const PaxAttribute = struct {
340 key: PaxKeyKind,
341 value_len: usize,
342 parent: *PaxFileReader,
343
344 // Copies pax attribute value into destination buffer.
345 // Must be called with destination buffer of size at least value_len.
346 pub fn value(self: PaxAttribute, dst: []u8) ![]u8 {
347 assert(dst.len >= self.value_len);
348 const buf = dst[0..self.value_len];
349 _ = try self.parent.reader.copy(buf);
350 self.parent.offset += buf.len;
351 try self.parent.checkAttributeEnding();
352 return buf;
353 }
354 };
355
356 // Caller of the next has to call value in PaxAttribute, to advance
357 // reader across value.
358 pub fn next(self: *PaxFileReader) !?PaxAttribute {
359 while (true) {
360 const remaining_size = self.size - self.offset;
361 if (remaining_size == 0) return null;
362
363 const inf = try parsePaxAttribute(
364 try self.reader.readSlice(remaining_size),
365 remaining_size,
366 );
367 const key: PaxKeyKind = if (inf.is("path"))
368 .path
369 else if (inf.is("linkpath"))
370 .linkpath
371 else if (inf.is("size"))
372 .size
373 else {
374 try self.advance(inf.value_off + inf.value_len);
375 try self.checkAttributeEnding();
376 continue;
377 };
378 try self.advance(inf.value_off); // position reader at the start of the value
379 return PaxAttribute{ .key = key, .value_len = inf.value_len, .parent = self };
380 }
381 }
382
383 fn checkAttributeEnding(self: *PaxFileReader) !void {
384 if (self.reader.readByte() != '\n') return error.InvalidPaxAttribute;
385 try self.advance(1);
386 }
387
388 fn advance(self: *PaxFileReader, len: usize) !void {
389 self.offset += len;
390 try self.reader.skip(len);
391 }
392 };
393 };
394}
395
396fn Iterator(comptime BufferedReaderType: type) type {
397 return struct {
398 // scratch buffer for file attributes
399 scratch: struct {
400 // size: two paths (name and link_name) and files size bytes (24 in pax attribute)
401 buffer: [std.fs.MAX_PATH_BYTES * 2 + 24]u8 = undefined,
402 tail: usize = 0,
403
404 name: []const u8 = undefined,
405 link_name: []const u8 = undefined,
406 size: usize = 0,
407
408 // Allocate size of the buffer for some attribute.
409 fn alloc(self: *@This(), size: usize) ![]u8 {
410 const free_size = self.buffer.len - self.tail;
411 if (size > free_size) return error.TarScratchBufferOverflow;
412 const head = self.tail;
413 self.tail += size;
414 assert(self.tail <= self.buffer.len);
415 return self.buffer[head..self.tail];
416 }
417
418 // Reset buffer and all fields.
419 fn reset(self: *@This()) void {
420 self.tail = 0;
421 self.name = self.buffer[0..0];
422 self.link_name = self.buffer[0..0];
423 self.size = 0;
424 }
425
426 fn append(self: *@This(), header: Header) !void {
427 if (self.size == 0) self.size = try header.fileSize();
428 if (self.link_name.len == 0) {
429 const link_name = header.linkName();
430 if (link_name.len > 0) {
431 const buf = try self.alloc(link_name.len);
432 @memcpy(buf, link_name);
433 self.link_name = buf;
434 }
435 }
436 if (self.name.len == 0) {
437 self.name = try header.fullName((try self.alloc(MAX_HEADER_NAME_SIZE))[0..MAX_HEADER_NAME_SIZE]);
438 }
439 }
440 } = .{},
441
442 reader: BufferedReaderType,
443 diagnostics: ?*Options.Diagnostics,
444 padding: usize = 0, // bytes of padding to the end of the block
445
446 const Self = @This();
447
448 pub const File = struct {
449 name: []const u8, // name of file, symlink or directory
450 link_name: []const u8, // target name of symlink
451 size: usize, // size of the file in bytes
452 mode: u32,
453 file_type: Header.FileType,
454
455 reader: *BufferedReaderType,
456
457 // Writes file content to writer.
458 pub fn write(self: File, writer: anytype) !void {
459 try self.reader.write(writer, self.size);
460 }
461
462 // Skips file content. Advances reader.
463 pub fn skip(self: File) !void {
464 try self.reader.skip(self.size);
465 }
466 };
467
468 // Externally, `next` iterates through the tar archive as if it is a
469 // series of files. Internally, the tar format often uses fake "files"
470 // to add meta data that describes the next file. These meta data
471 // "files" should not normally be visible to the outside. As such, this
472 // loop iterates through one or more "header files" until it finds a
473 // "normal file".
474 pub fn next(self: *Self) !?File {
475 self.scratch.reset();
476
477 while (try self.reader.readHeader(self.padding)) |block_bytes| {
478 const header = Header{ .bytes = block_bytes[0..BLOCK_SIZE] };
479 if (try header.checkChksum() == 0) return null; // zero block found
480
481 const file_type = header.fileType();
482 const size: usize = @intCast(try header.fileSize());
483 self.padding = blockPadding(size);
484
485 switch (file_type) {
486 // File types to retrun upstream
487 .directory, .normal, .symbolic_link => {
488 try self.scratch.append(header);
489 const file = File{
490 .file_type = file_type,
491 .name = self.scratch.name,
492 .link_name = self.scratch.link_name,
493 .size = self.scratch.size,
494 .reader = &self.reader,
495 .mode = try header.mode(),
496 };
497 self.padding = blockPadding(file.size);
498 return file;
499 },
500 // Prefix header types
501 .gnu_long_name => {
502 self.scratch.name = nullStr(try self.reader.copy(try self.scratch.alloc(size)));
503 },
504 .gnu_long_link => {
505 self.scratch.link_name = nullStr(try self.reader.copy(try self.scratch.alloc(size)));
506 },
507 .extended_header => {
508 if (size == 0) continue;
509 // Use just attributes from last extended header.
510 self.scratch.reset();
511
512 var rdr = self.reader.paxFileReader(size);
513 while (try rdr.next()) |attr| {
514 switch (attr.key) {
515 .path => {
516 self.scratch.name = try noNull(try attr.value(try self.scratch.alloc(attr.value_len)));
517 },
518 .linkpath => {
519 self.scratch.link_name = try noNull(try attr.value(try self.scratch.alloc(attr.value_len)));
520 },
521 .size => {
522 self.scratch.size = try std.fmt.parseInt(usize, try attr.value(try self.scratch.alloc(attr.value_len)), 10);
523 },
524 }
525 }
526 },
527 // Ignored header type
528 .global_extended_header => {
529 self.reader.skip(size) catch return error.TarHeadersTooBig;
530 },
531 // All other are unsupported header types
532 else => {
533 const d = self.diagnostics orelse return error.TarUnsupportedFileType;
534 try d.errors.append(d.allocator, .{ .unsupported_file_type = .{
535 .file_name = try d.allocator.dupe(u8, header.name()),
536 .file_type = file_type,
537 } });
538 },
539 }
540 }
541 return null;
542 }
543 };
544}
545
546pub fn iterator(underlying_reader: anytype, diagnostics: ?*Options.Diagnostics) Iterator(BufferedReader(@TypeOf(underlying_reader))) {
547 return .{
548 .reader = bufferedReader(underlying_reader),
549 .diagnostics = diagnostics,
550 };
551}
552
553fn bufferedReader(underlying_reader: anytype) BufferedReader(@TypeOf(underlying_reader)) {
554 return BufferedReader(@TypeOf(underlying_reader)){
555 .underlying_reader = underlying_reader,
556 };
557}
558
559228pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !void {
560229 switch (options.mode_mode) {
561230 .ignore => {},
......@@ -569,7 +238,7 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi
569238 },
570239 }
571240
572 var iter = iterator(reader, options.diagnostics);
241 var iter = tarReader(reader, options.diagnostics);
573242
574243 while (try iter.next()) |file| {
575244 switch (file.file_type) {
......@@ -662,82 +331,37 @@ test "tar stripComponents" {
662331 try expectEqualStrings("c", try stripComponents("a/b/c", 2));
663332}
664333
665const PaxAttributeInfo = struct {
666 size: usize,
667 key: []const u8,
668 value_off: usize,
669 value_len: usize,
670
671 inline fn is(self: @This(), key: []const u8) bool {
672 return (std.mem.eql(u8, self.key, key));
673 }
674};
675
676fn parsePaxAttribute(data: []const u8, max_size: usize) !PaxAttributeInfo {
677 const pos_space = std.mem.indexOfScalar(u8, data, ' ') orelse return error.InvalidPaxAttribute;
678 const pos_equals = std.mem.indexOfScalarPos(u8, data, pos_space, '=') orelse return error.InvalidPaxAttribute;
679 const kv_size = try std.fmt.parseInt(usize, data[0..pos_space], 10);
680 if (kv_size > max_size or kv_size < pos_equals + 2) {
681 return error.InvalidPaxAttribute;
682 }
683 const key = data[pos_space + 1 .. pos_equals];
684 return .{
685 .size = kv_size,
686 .key = try noNull(key),
687 .value_off = pos_equals + 1,
688 .value_len = kv_size - pos_equals - 2,
689 };
690}
691
692334fn noNull(str: []const u8) ![]const u8 {
693335 if (std.mem.indexOfScalar(u8, str, 0)) |_| return error.InvalidPaxAttribute;
694336 return str;
695337}
696338
697test "tar parsePaxAttribute" {
698 const expectEqual = std.testing.expectEqual;
699 const expectEqualStrings = std.testing.expectEqualStrings;
700 const expectError = std.testing.expectError;
701 const prefix = "1011 path=";
702 const file_name = "0123456789" ** 100;
703 const header = prefix ++ file_name ++ "\n";
704 const attr_info = try parsePaxAttribute(header, 1011);
705 try expectEqual(@as(usize, 1011), attr_info.size);
706 try expectEqualStrings("path", attr_info.key);
707 try expectEqual(prefix.len, attr_info.value_off);
708 try expectEqual(file_name.len, attr_info.value_len);
709 try expectEqual(attr_info, try parsePaxAttribute(header, 1012));
710 try expectError(error.InvalidPaxAttribute, parsePaxAttribute(header, 1010));
711 try expectError(error.InvalidPaxAttribute, parsePaxAttribute("", 0));
712 try expectError(error.InvalidPaxAttribute, parsePaxAttribute("13 pa\x00th=abc\n", 1024)); // null in key
713}
339test "tar run Go test cases" {
340 const Case = struct {
341 const File = struct {
342 name: []const u8,
343 size: usize = 0,
344 mode: u32 = 0,
345 link_name: []const u8 = &[0]u8{},
346 file_type: Header.FileType = .normal,
347 truncated: bool = false, // when there is no file body, just header, usefull for huge files
348 };
714349
715const TestCase = struct {
716 const File = struct {
717 name: []const u8,
718 size: usize = 0,
719 mode: u32 = 0,
720 link_name: []const u8 = &[0]u8{},
721 file_type: Header.FileType = .normal,
722 truncated: bool = false, // when there is no file body, just header, usefull for huge files
350 path: []const u8, // path to the tar archive file on dis
351 files: []const File = &[_]@This().File{}, // expected files to found in archive
352 chksums: []const []const u8 = &[_][]const u8{}, // chksums of files content
353 err: ?anyerror = null, // parsing should fail with this error
723354 };
724355
725 path: []const u8, // path to the tar archive file on dis
726 files: []const File = &[_]TestCase.File{}, // expected files to found in archive
727 chksums: []const []const u8 = &[_][]const u8{}, // chksums of files content
728 err: ?anyerror = null, // parsing should fail with this error
729};
730
731test "tar run Go test cases" {
732356 const test_dir = if (std.os.getenv("GO_TAR_TESTDATA_PATH")) |path|
733357 try std.fs.openDirAbsolute(path, .{})
734358 else
735359 return error.SkipZigTest;
736360
737 const cases = [_]TestCase{
361 const cases = [_]Case{
738362 .{
739363 .path = "gnu.tar",
740 .files = &[_]TestCase.File{
364 .files = &[_]Case.File{
741365 .{
742366 .name = "small.txt",
743367 .size = 5,
......@@ -760,7 +384,7 @@ test "tar run Go test cases" {
760384 },
761385 .{
762386 .path = "star.tar",
763 .files = &[_]TestCase.File{
387 .files = &[_]Case.File{
764388 .{
765389 .name = "small.txt",
766390 .size = 5,
......@@ -779,7 +403,7 @@ test "tar run Go test cases" {
779403 },
780404 .{
781405 .path = "v7.tar",
782 .files = &[_]TestCase.File{
406 .files = &[_]Case.File{
783407 .{
784408 .name = "small.txt",
785409 .size = 5,
......@@ -798,7 +422,7 @@ test "tar run Go test cases" {
798422 },
799423 .{
800424 .path = "pax.tar",
801 .files = &[_]TestCase.File{
425 .files = &[_]Case.File{
802426 .{
803427 .name = "a/123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100",
804428 .size = 7,
......@@ -824,7 +448,7 @@ test "tar run Go test cases" {
824448 .{
825449 // size is in pax attribute
826450 .path = "pax-pos-size-file.tar",
827 .files = &[_]TestCase.File{
451 .files = &[_]Case.File{
828452 .{
829453 .name = "foo",
830454 .size = 999,
......@@ -839,7 +463,7 @@ test "tar run Go test cases" {
839463 .{
840464 // has pax records which we are not interested in
841465 .path = "pax-records.tar",
842 .files = &[_]TestCase.File{
466 .files = &[_]Case.File{
843467 .{
844468 .name = "file",
845469 },
......@@ -848,7 +472,7 @@ test "tar run Go test cases" {
848472 .{
849473 // has global records which we are ignoring
850474 .path = "pax-global-records.tar",
851 .files = &[_]TestCase.File{
475 .files = &[_]Case.File{
852476 .{
853477 .name = "file1",
854478 },
......@@ -865,7 +489,7 @@ test "tar run Go test cases" {
865489 },
866490 .{
867491 .path = "nil-uid.tar",
868 .files = &[_]TestCase.File{
492 .files = &[_]Case.File{
869493 .{
870494 .name = "P1050238.JPG.log",
871495 .size = 14,
......@@ -880,7 +504,7 @@ test "tar run Go test cases" {
880504 .{
881505 // has xattrs and pax records which we are ignoring
882506 .path = "xattrs.tar",
883 .files = &[_]TestCase.File{
507 .files = &[_]Case.File{
884508 .{
885509 .name = "small.txt",
886510 .size = 5,
......@@ -901,7 +525,7 @@ test "tar run Go test cases" {
901525 },
902526 .{
903527 .path = "gnu-multi-hdrs.tar",
904 .files = &[_]TestCase.File{
528 .files = &[_]Case.File{
905529 .{
906530 .name = "GNU2/GNU2/long-path-name",
907531 .link_name = "GNU4/GNU4/long-linkpath-name",
......@@ -917,7 +541,7 @@ test "tar run Go test cases" {
917541 .{
918542 // should use values only from last pax header
919543 .path = "pax-multi-hdrs.tar",
920 .files = &[_]TestCase.File{
544 .files = &[_]Case.File{
921545 .{
922546 .name = "bar",
923547 .link_name = "PAX4/PAX4/long-linkpath-name",
......@@ -927,7 +551,7 @@ test "tar run Go test cases" {
927551 },
928552 .{
929553 .path = "gnu-long-nul.tar",
930 .files = &[_]TestCase.File{
554 .files = &[_]Case.File{
931555 .{
932556 .name = "0123456789",
933557 .mode = 0o644,
......@@ -936,7 +560,7 @@ test "tar run Go test cases" {
936560 },
937561 .{
938562 .path = "gnu-utf8.tar",
939 .files = &[_]TestCase.File{
563 .files = &[_]Case.File{
940564 .{
941565 .name = "☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹",
942566 .mode = 0o644,
......@@ -945,7 +569,7 @@ test "tar run Go test cases" {
945569 },
946570 .{
947571 .path = "gnu-not-utf8.tar",
948 .files = &[_]TestCase.File{
572 .files = &[_]Case.File{
949573 .{
950574 .name = "hi\x80\x81\x82\x83bye",
951575 .mode = 0o644,
......@@ -980,7 +604,7 @@ test "tar run Go test cases" {
980604 .{
981605 // has magic with space at end instead of null
982606 .path = "invalid-go17.tar",
983 .files = &[_]TestCase.File{
607 .files = &[_]Case.File{
984608 .{
985609 .name = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/foo",
986610 },
......@@ -988,7 +612,7 @@ test "tar run Go test cases" {
988612 },
989613 .{
990614 .path = "ustar-file-devs.tar",
991 .files = &[_]TestCase.File{
615 .files = &[_]Case.File{
992616 .{
993617 .name = "file",
994618 .mode = 0o644,
......@@ -997,7 +621,7 @@ test "tar run Go test cases" {
997621 },
998622 .{
999623 .path = "trailing-slash.tar",
1000 .files = &[_]TestCase.File{
624 .files = &[_]Case.File{
1001625 .{
1002626 .name = "123456789/" ** 30,
1003627 .file_type = .directory,
......@@ -1007,7 +631,7 @@ test "tar run Go test cases" {
1007631 .{
1008632 // Has size in gnu extended format. To represent size bigger than 8 GB.
1009633 .path = "writer-big.tar",
1010 .files = &[_]TestCase.File{
634 .files = &[_]Case.File{
1011635 .{
1012636 .name = "tmp/16gig.txt",
1013637 .size = 16 * 1024 * 1024 * 1024,
......@@ -1019,7 +643,7 @@ test "tar run Go test cases" {
1019643 .{
1020644 // Size in gnu extended format, and name in pax attribute.
1021645 .path = "writer-big-long.tar",
1022 .files = &[_]TestCase.File{
646 .files = &[_]Case.File{
1023647 .{
1024648 .name = "longname/" ** 15 ++ "16gig.txt",
1025649 .size = 16 * 1024 * 1024 * 1024,
......@@ -1034,7 +658,8 @@ test "tar run Go test cases" {
1034658 var fs_file = try test_dir.openFile(case.path, .{});
1035659 defer fs_file.close();
1036660
1037 var iter = iterator(fs_file.reader(), null);
661 //var iter = iterator(fs_file.reader(), null);
662 var iter = tarReader(fs_file.reader(), null);
1038663 var i: usize = 0;
1039664 while (iter.next() catch |err| {
1040665 if (case.err) |e| {
......@@ -1072,6 +697,10 @@ const Md5Writer = struct {
1072697 self.h.update(buf);
1073698 }
1074699
700 pub fn writeByte(self: *Md5Writer, byte: u8) !void {
701 self.h.update(&[_]u8{byte});
702 }
703
1075704 pub fn chksum(self: *Md5Writer) [32]u8 {
1076705 var s = [_]u8{0} ** 16;
1077706 self.h.final(&s);
......@@ -1079,19 +708,113 @@ const Md5Writer = struct {
1079708 }
1080709};
1081710
1082test "tar PaxFileReader" {
1083 const Attribute = struct {
1084 const PaxKeyKind = enum {
1085 path,
1086 linkpath,
1087 size,
711fn paxReader(reader: anytype, size: usize) PaxReader(@TypeOf(reader)) {
712 return PaxReader(@TypeOf(reader)){
713 .reader = reader,
714 .size = size,
715 };
716}
717
718const PaxAttrKind = enum {
719 path,
720 linkpath,
721 size,
722};
723
724fn PaxReader(comptime ReaderType: type) type {
725 return struct {
726 size: usize,
727 reader: ReaderType,
728
729 const Self = @This();
730
731 const Attr = struct {
732 kind: PaxAttrKind,
733 len: usize,
734 reader: ReaderType,
735
736 // Copies pax attribute value into destination buffer.
737 // Must be called with destination buffer of size at least value_len.
738 pub fn value(self: Attr, dst: []u8) ![]const u8 {
739 assert(self.len <= dst.len);
740 const buf = dst[0..self.len];
741 const n = try self.reader.readAll(buf);
742 if (n < self.len) return error.UnexpectedEndOfStream;
743 try checkRecordEnd(self.reader);
744 return noNull(buf);
745 }
1088746 };
1089 key: PaxKeyKind,
1090 value: []const u8,
747
748 // Iterates over pax records. Returns known records. Caller has to call
749 // value in Record, to advance reader across value.
750 pub fn next(self: *Self) !?Attr {
751 var buf: [128]u8 = undefined;
752 var fbs = std.io.fixedBufferStream(&buf);
753
754 // An extended header consists of one or more records, each constructed as follows:
755 // "%d %s=%s\n", <length>, <keyword>, <value>
756 while (self.size > 0) {
757 fbs.reset();
758 // read length
759 try self.reader.streamUntilDelimiter(fbs.writer(), ' ', null);
760 const rec_len = try std.fmt.parseInt(usize, fbs.getWritten(), 10); // record len in bytes
761 var pos = try fbs.getPos() + 1; // bytes used for record len + separator
762 fbs.reset();
763 // read keyword
764 try self.reader.streamUntilDelimiter(fbs.writer(), '=', null);
765 const keyword = fbs.getWritten();
766 pos += try fbs.getPos() + 1; // keyword bytes + separator
767 try checkKeyword(keyword);
768 // get value_len
769 if (rec_len < pos + 1) return error.InvalidPaxAttribute;
770 const value_len = rec_len - pos - 1; // pos = start of value, -1 => without \n record terminator
771
772 self.size -= rec_len;
773 const kind: PaxAttrKind = if (eql(keyword, "path"))
774 .path
775 else if (eql(keyword, "linkpath"))
776 .linkpath
777 else if (eql(keyword, "size"))
778 .size
779 else {
780 try self.reader.skipBytes(value_len, .{});
781 try checkRecordEnd(self.reader);
782 continue;
783 };
784 return Attr{
785 .kind = kind,
786 .len = value_len,
787 .reader = self.reader,
788 };
789 }
790
791 return null;
792 }
793
794 inline fn eql(a: []const u8, b: []const u8) bool {
795 return std.mem.eql(u8, a, b);
796 }
797
798 fn checkKeyword(keyword: []const u8) !void {
799 if (std.mem.indexOfScalar(u8, keyword, 0)) |_| return error.InvalidPaxAttribute;
800 }
801
802 // Checks that each record ends with new line.
803 fn checkRecordEnd(reader: ReaderType) !void {
804 if (try reader.readByte() != '\n') return error.InvalidPaxAttribute;
805 }
806 };
807}
808
809test "tar PaxReader" {
810 const Attr = struct {
811 kind: PaxAttrKind,
812 value: []const u8 = undefined,
813 err: ?anyerror = null,
1091814 };
1092815 const cases = [_]struct {
1093816 data: []const u8,
1094 attrs: []const Attribute,
817 attrs: []const Attr,
1095818 err: ?anyerror = null,
1096819 }{
1097820 .{ // valid but unknown keys
......@@ -1103,7 +826,7 @@ test "tar PaxFileReader" {
1103826 \\9 a=name
1104827 \\
1105828 ,
1106 .attrs = &[_]Attribute{},
829 .attrs = &[_]Attr{},
1107830 },
1108831 .{ // mix of known and unknown keys
1109832 .data =
......@@ -1115,10 +838,10 @@ test "tar PaxFileReader" {
1115838 \\13 key2=val2
1116839 \\
1117840 ,
1118 .attrs = &[_]Attribute{
1119 .{ .key = .path, .value = "name" },
1120 .{ .key = .linkpath, .value = "link" },
1121 .{ .key = .size, .value = "123" },
841 .attrs = &[_]Attr{
842 .{ .kind = .path, .value = "name" },
843 .{ .kind = .linkpath, .value = "link" },
844 .{ .kind = .size, .value = "123" },
1122845 },
1123846 },
1124847 .{ // too short size of the second key-value pair
......@@ -1127,8 +850,8 @@ test "tar PaxFileReader" {
1127850 \\10 linkpath=value
1128851 \\
1129852 ,
1130 .attrs = &[_]Attribute{
1131 .{ .key = .path, .value = "name" },
853 .attrs = &[_]Attr{
854 .{ .kind = .path, .value = "name" },
1132855 },
1133856 .err = error.InvalidPaxAttribute,
1134857 },
......@@ -1136,36 +859,237 @@ test "tar PaxFileReader" {
1136859 .data =
1137860 \\13 path=name
1138861 \\19 linkpath=value
862 \\6 k=1
1139863 \\
1140864 ,
1141 .attrs = &[_]Attribute{
1142 .{ .key = .path, .value = "name" },
865 .attrs = &[_]Attr{
866 .{ .kind = .path, .value = "name" },
867 .{ .kind = .linkpath, .err = error.InvalidPaxAttribute },
868 },
869 },
870 .{ // null in keyword is not valid
871 .data = "13 path=name\n" ++ "7 k\x00b=1\n",
872 .attrs = &[_]Attr{
873 .{ .kind = .path, .value = "name" },
1143874 },
1144875 .err = error.InvalidPaxAttribute,
1145876 },
877 .{ // null in value is not valid
878 .data = "23 path=name\x00with null\n",
879 .attrs = &[_]Attr{
880 .{ .kind = .path, .err = error.InvalidPaxAttribute },
881 },
882 },
883 .{ // 1000 characters path
884 .data = "1011 path=" ++ "0123456789" ** 100 ++ "\n",
885 .attrs = &[_]Attr{
886 .{ .kind = .path, .value = "0123456789" ** 100 },
887 },
888 },
1146889 };
1147890 var buffer: [1024]u8 = undefined;
1148891
1149 for (cases) |case| {
892 outer: for (cases) |case| {
1150893 var stream = std.io.fixedBufferStream(case.data);
1151 var brdr = bufferedReader(stream.reader());
894 var rdr = paxReader(stream.reader(), case.data.len);
1152895
1153 var rdr = brdr.paxFileReader(case.data.len);
1154896 var i: usize = 0;
1155897 while (rdr.next() catch |err| {
1156898 if (case.err) |e| {
1157899 try std.testing.expectEqual(e, err);
1158900 continue;
1159 } else {
1160 return err;
1161901 }
902 return err;
1162903 }) |attr| : (i += 1) {
1163 try std.testing.expectEqualStrings(
1164 case.attrs[i].value,
1165 try attr.value(&buffer),
1166 );
904 const exp = case.attrs[i];
905 try std.testing.expectEqual(exp.kind, attr.kind);
906 const value = attr.value(&buffer) catch |err| {
907 if (exp.err) |e| {
908 try std.testing.expectEqual(e, err);
909 break :outer;
910 }
911 return err;
912 };
913 try std.testing.expectEqualStrings(exp.value, value);
1167914 }
1168915 try std.testing.expectEqual(case.attrs.len, i);
1169916 try std.testing.expect(case.err == null);
1170917 }
1171918}
919
920pub fn tarReader(reader: anytype, diagnostics: ?*Options.Diagnostics) TarReader(@TypeOf(reader)) {
921 return .{
922 .reader = reader,
923 .diagnostics = diagnostics,
924 };
925}
926
927fn TarReader(comptime ReaderType: type) type {
928 return struct {
929 // scratch buffer for file attributes
930 scratch: struct {
931 // size: two paths (name and link_name) and files size bytes (24 in pax attribute)
932 buffer: [std.fs.MAX_PATH_BYTES * 2 + 24]u8 = undefined,
933 tail: usize = 0,
934
935 name: []const u8 = undefined,
936 link_name: []const u8 = undefined,
937 size: usize = 0,
938
939 // Allocate size of the buffer for some attribute.
940 fn alloc(self: *@This(), size: usize) ![]u8 {
941 const free_size = self.buffer.len - self.tail;
942 if (size > free_size) return error.TarScratchBufferOverflow;
943 const head = self.tail;
944 self.tail += size;
945 assert(self.tail <= self.buffer.len);
946 return self.buffer[head..self.tail];
947 }
948
949 // Reset buffer and all fields.
950 fn reset(self: *@This()) void {
951 self.tail = 0;
952 self.name = self.buffer[0..0];
953 self.link_name = self.buffer[0..0];
954 self.size = 0;
955 }
956
957 fn append(self: *@This(), header: Header) !void {
958 if (self.size == 0) self.size = try header.fileSize();
959 if (self.link_name.len == 0) {
960 const link_name = header.linkName();
961 if (link_name.len > 0) {
962 const buf = try self.alloc(link_name.len);
963 @memcpy(buf, link_name);
964 self.link_name = buf;
965 }
966 }
967 if (self.name.len == 0) {
968 self.name = try header.fullName((try self.alloc(MAX_HEADER_NAME_SIZE))[0..MAX_HEADER_NAME_SIZE]);
969 }
970 }
971 } = .{},
972
973 reader: ReaderType,
974 diagnostics: ?*Options.Diagnostics,
975 padding: usize = 0, // bytes of padding to the end of the block
976 header_buffer: [BLOCK_SIZE]u8 = undefined,
977
978 const Self = @This();
979
980 pub const File = struct {
981 name: []const u8, // name of file, symlink or directory
982 link_name: []const u8, // target name of symlink
983 size: usize, // size of the file in bytes
984 mode: u32,
985 file_type: Header.FileType,
986
987 reader: *ReaderType,
988
989 // Writes file content to writer.
990 pub fn write(self: File, writer: anytype) !void {
991 var n = self.size;
992 while (n > 0) : (n -= 1) {
993 const byte: u8 = try self.reader.readByte();
994 try writer.writeByte(byte);
995 }
996 }
997
998 // Skips file content. Advances reader.
999 pub fn skip(self: File) !void {
1000 try self.reader.skipBytes(self.size, .{});
1001 }
1002 };
1003
1004 fn readHeader(self: *Self) !?Header {
1005 if (self.padding > 0) {
1006 try self.reader.skipBytes(self.padding, .{});
1007 }
1008 const n = try self.reader.readAll(&self.header_buffer);
1009 if (n == 0) return null;
1010 if (n < BLOCK_SIZE) return error.UnexpectedEndOfStream;
1011 const header = Header{ .bytes = self.header_buffer[0..BLOCK_SIZE] };
1012 if (try header.checkChksum() == 0) return null;
1013 return header;
1014 }
1015
1016 fn readString(self: *Self, size: usize) ![]const u8 {
1017 const buf = try self.scratch.alloc(size);
1018 try self.reader.readNoEof(buf);
1019 return nullStr(buf);
1020 }
1021
1022 // Externally, `next` iterates through the tar archive as if it is a
1023 // series of files. Internally, the tar format often uses fake "files"
1024 // to add meta data that describes the next file. These meta data
1025 // "files" should not normally be visible to the outside. As such, this
1026 // loop iterates through one or more "header files" until it finds a
1027 // "normal file".
1028 pub fn next(self: *Self) !?File {
1029 self.scratch.reset();
1030
1031 while (try self.readHeader()) |header| {
1032 const file_type = header.fileType();
1033 const size: usize = @intCast(try header.fileSize());
1034 self.padding = blockPadding(size);
1035
1036 switch (file_type) {
1037 // File types to retrun upstream
1038 .directory, .normal, .symbolic_link => {
1039 try self.scratch.append(header);
1040 const file = File{
1041 .file_type = file_type,
1042 .name = self.scratch.name,
1043 .link_name = self.scratch.link_name,
1044 .size = self.scratch.size,
1045 .reader = &self.reader,
1046 .mode = try header.mode(),
1047 };
1048 self.padding = blockPadding(file.size);
1049 return file;
1050 },
1051 // Prefix header types
1052 .gnu_long_name => {
1053 self.scratch.name = try self.readString(size);
1054 },
1055 .gnu_long_link => {
1056 self.scratch.link_name = try self.readString(size);
1057 },
1058 .extended_header => {
1059 if (size == 0) continue;
1060 // Use just attributes from last extended header.
1061 self.scratch.reset();
1062
1063 var rdr = paxReader(self.reader, size);
1064 while (try rdr.next()) |attr| {
1065 switch (attr.kind) {
1066 .path => {
1067 self.scratch.name = try attr.value(try self.scratch.alloc(attr.len));
1068 },
1069 .linkpath => {
1070 self.scratch.link_name = try attr.value(try self.scratch.alloc(attr.len));
1071 },
1072 .size => {
1073 self.scratch.size = try std.fmt.parseInt(usize, try attr.value(try self.scratch.alloc(attr.len)), 10);
1074 },
1075 }
1076 }
1077 },
1078 // Ignored header type
1079 .global_extended_header => {
1080 self.reader.skipBytes(size, .{}) catch return error.TarHeadersTooBig;
1081 },
1082 // All other are unsupported header types
1083 else => {
1084 const d = self.diagnostics orelse return error.TarUnsupportedFileType;
1085 try d.errors.append(d.allocator, .{ .unsupported_file_type = .{
1086 .file_name = try d.allocator.dupe(u8, header.name()),
1087 .file_type = file_type,
1088 } });
1089 },
1090 }
1091 }
1092 return null;
1093 }
1094 };
1095}