authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-02-26 12:23:22-08:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2024-02-26 12:23:22-08:00
log81aa74e7e1acc07872fafcd399f047a4652e3ec6
tree7e5916fb58ad248284cfd35725201ef805b08858
parentd51aa9748f9e4e3616328a207a8047ff37d81f8b
parent65e5c46d6122120b2837c8c74c43801f78aaf60f
signaturebadge-check Signed by PGP key B5690EEEBB952194

Merge pull request #19081 from ianic/tar_case_sensitive

std.tar don't overwrite files on unpack

5 files changed, 151 insertions(+), 38 deletions(-)

lib/std/tar.zig+36-38
...@@ -550,31 +550,15 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi...@@ -550,31 +550,15 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi
550 const file_name = stripComponents(file.name, options.strip_components);550 const file_name = stripComponents(file.name, options.strip_components);
551 if (file_name.len == 0) return error.BadFileName;551 if (file_name.len == 0) return error.BadFileName;
552552
553 const fs_file = dir.createFile(file_name, .{}) catch |err| switch (err) {553 if (createDirAndFile(dir, file_name)) |fs_file| {
554 error.FileNotFound => again: {554 defer fs_file.close();
555 const code = code: {555 try file.write(fs_file);
556 if (std.fs.path.dirname(file_name)) |dir_name| {556 } else |err| {
557 dir.makePath(dir_name) catch |code| break :code code;557 const d = options.diagnostics orelse return err;
558 break :again dir.createFile(file_name, .{}) catch |code| {558 try d.errors.append(d.allocator, .{ .unable_to_create_file = .{
559 break :code code;559 .code = err,
560 };560 .file_name = try d.allocator.dupe(u8, file_name),
561 }561 } });
562 break :code err;
563 };
564 const d = options.diagnostics orelse return error.UnableToCreateFile;
565 try d.errors.append(d.allocator, .{ .unable_to_create_file = .{
566 .code = code,
567 .file_name = try d.allocator.dupe(u8, file_name),
568 } });
569 break :again null;
570 },
571 else => |e| return e,
572 };
573 defer if (fs_file) |f| f.close();
574
575 if (fs_file) |f| {
576 try file.write(f);
577 } else {
578 try file.skip();562 try file.skip();
579 }563 }
580 },564 },
...@@ -585,21 +569,10 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi...@@ -585,21 +569,10 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi
585 // The data inside the symbolic link.569 // The data inside the symbolic link.
586 const link_name = file.link_name;570 const link_name = file.link_name;
587571
588 dir.symLink(link_name, file_name, .{}) catch |err| again: {572 createDirAndSymlink(dir, link_name, file_name) catch |err| {
589 const code = code: {
590 if (err == error.FileNotFound) {
591 if (std.fs.path.dirname(file_name)) |dir_name| {
592 dir.makePath(dir_name) catch |code| break :code code;
593 break :again dir.symLink(link_name, file_name, .{}) catch |code| {
594 break :code code;
595 };
596 }
597 }
598 break :code err;
599 };
600 const d = options.diagnostics orelse return error.UnableToCreateSymLink;573 const d = options.diagnostics orelse return error.UnableToCreateSymLink;
601 try d.errors.append(d.allocator, .{ .unable_to_create_sym_link = .{574 try d.errors.append(d.allocator, .{ .unable_to_create_sym_link = .{
602 .code = code,575 .code = err,
603 .file_name = try d.allocator.dupe(u8, file_name),576 .file_name = try d.allocator.dupe(u8, file_name),
604 .link_name = try d.allocator.dupe(u8, link_name),577 .link_name = try d.allocator.dupe(u8, link_name),
605 } });578 } });
...@@ -610,6 +583,31 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi...@@ -610,6 +583,31 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi
610 }583 }
611}584}
612585
586fn createDirAndFile(dir: std.fs.Dir, file_name: []const u8) !std.fs.File {
587 const fs_file = dir.createFile(file_name, .{ .exclusive = true }) catch |err| {
588 if (err == error.FileNotFound) {
589 if (std.fs.path.dirname(file_name)) |dir_name| {
590 try dir.makePath(dir_name);
591 return try dir.createFile(file_name, .{ .exclusive = true });
592 }
593 }
594 return err;
595 };
596 return fs_file;
597}
598
599fn createDirAndSymlink(dir: std.fs.Dir, link_name: []const u8, file_name: []const u8) !void {
600 dir.symLink(link_name, file_name, .{}) catch |err| {
601 if (err == error.FileNotFound) {
602 if (std.fs.path.dirname(file_name)) |dir_name| {
603 try dir.makePath(dir_name);
604 try dir.symLink(link_name, file_name, .{});
605 }
606 }
607 return err;
608 };
609}
610
613fn stripComponents(path: []const u8, count: u32) []const u8 {611fn stripComponents(path: []const u8, count: u32) []const u8 {
614 var i: usize = 0;612 var i: usize = 0;
615 var c = count;613 var c = count;
lib/std/tar/test.zig+115
...@@ -373,3 +373,118 @@ const Md5Writer = struct {...@@ -373,3 +373,118 @@ const Md5Writer = struct {
373 return std.fmt.bytesToHex(s, .lower);373 return std.fmt.bytesToHex(s, .lower);
374 }374 }
375};375};
376
377test "tar should not overwrite existing file" {
378 // Starting from this folder structure:
379 // $ tree root
380 // root
381 // ├── a
382 // │   └── b
383 // │   └── c
384 // │   └── file.txt
385 // └── d
386 // └── b
387 // └── c
388 // └── file.txt
389 //
390 // Packed with command:
391 // $ cd root; tar cf overwrite_file.tar *
392 // Resulting tar has following structure:
393 // $ tar tvf overwrite_file.tar
394 // size path
395 // 0 a/
396 // 0 a/b/
397 // 0 a/b/c/
398 // 2 a/b/c/file.txt
399 // 0 d/
400 // 0 d/b/
401 // 0 d/b/c/
402 // 2 d/b/c/file.txt
403 //
404 // Note that there is no root folder in archive.
405 //
406 // With strip_components = 1 resulting unpacked folder was:
407 // root
408 // └── b
409 // └── c
410 // └── file.txt
411 //
412 // a/b/c/file.txt is overwritten with d/b/c/file.txt !!!
413 // This ensures that file is not overwritten.
414 //
415 const data = @embedFile("testdata/overwrite_file.tar");
416 var fsb = std.io.fixedBufferStream(data);
417
418 // Unpack with strip_components = 1 should fail
419 var root = std.testing.tmpDir(.{});
420 defer root.cleanup();
421 try testing.expectError(
422 error.PathAlreadyExists,
423 tar.pipeToFileSystem(root.dir, fsb.reader(), .{ .mode_mode = .ignore, .strip_components = 1 }),
424 );
425
426 // Unpack with strip_components = 0 should pass
427 fsb.reset();
428 var root2 = std.testing.tmpDir(.{});
429 defer root2.cleanup();
430 try tar.pipeToFileSystem(root2.dir, fsb.reader(), .{ .mode_mode = .ignore, .strip_components = 0 });
431}
432
433test "tar case sensitivity" {
434 // Mimicking issue #18089, this tar contains, same file name in two case
435 // sensitive name version. Should fail on case insensitive file systems.
436 //
437 // $ tar tvf 18089.tar
438 // 18089/
439 // 18089/alacritty/
440 // 18089/alacritty/darkermatrix.yml
441 // 18089/alacritty/Darkermatrix.yml
442 //
443 const data = @embedFile("testdata/18089.tar");
444 var fsb = std.io.fixedBufferStream(data);
445
446 var root = std.testing.tmpDir(.{});
447 defer root.cleanup();
448
449 tar.pipeToFileSystem(root.dir, fsb.reader(), .{ .mode_mode = .ignore, .strip_components = 1 }) catch |err| {
450 // on case insensitive fs we fail on overwrite existing file
451 try testing.expectEqual(error.PathAlreadyExists, err);
452 return;
453 };
454
455 // on case sensitive os both files are created
456 try testing.expect((try root.dir.statFile("alacritty/darkermatrix.yml")).kind == .file);
457 try testing.expect((try root.dir.statFile("alacritty/Darkermatrix.yml")).kind == .file);
458}
459
460test "tar pipeToFileSystem" {
461 // $ tar tvf
462 // pipe_to_file_system_test/
463 // pipe_to_file_system_test/b/
464 // pipe_to_file_system_test/b/symlink -> ../a/file
465 // pipe_to_file_system_test/a/
466 // pipe_to_file_system_test/a/file
467 // pipe_to_file_system_test/empty/
468 const data = @embedFile("testdata/pipe_to_file_system_test.tar");
469 var fsb = std.io.fixedBufferStream(data);
470
471 var root = std.testing.tmpDir(.{ .no_follow = true });
472 defer root.cleanup();
473
474 tar.pipeToFileSystem(root.dir, fsb.reader(), .{
475 .mode_mode = .ignore,
476 .strip_components = 1,
477 .exclude_empty_directories = true,
478 }) catch |err| {
479 // Skip on platform which don't support symlinks
480 if (err == error.UnableToCreateSymLink) return error.SkipZigTest;
481 return err;
482 };
483
484 try testing.expectError(error.FileNotFound, root.dir.statFile("empty"));
485 try testing.expect((try root.dir.statFile("a/file")).kind == .file);
486 // TODO is there better way to test symlink
487 try testing.expect((try root.dir.statFile("b/symlink")).kind == .file); // statFile follows symlink
488 var buf: [8]u8 = undefined;
489 _ = try root.dir.readLink("b/symlink", &buf);
490}
lib/std/tar/testdata/18089.tar created
Binary files /dev/null and b/lib/std/tar/testdata/18089.tar differ
lib/std/tar/testdata/overwrite_file.tar created
Binary files /dev/null and b/lib/std/tar/testdata/overwrite_file.tar differ
lib/std/tar/testdata/pipe_to_file_system_test.tar created
Binary files /dev/null and b/lib/std/tar/testdata/pipe_to_file_system_test.tar differ