diff --git a/src/Package/Fetch.zig b/src/Package/Fetch.zig index 83bc2753433e763b6f7d641fe71b13dc8299e593..bf52c9cef2198760b5c783f9a2f98a25ffc1b192 100644 --- a/src/Package/Fetch.zig +++ b/src/Package/Fetch.zig @@ -1890,16 +1890,19 @@ const UnpackResult = struct { } }; -test "fetch tarball: fail with unable to create file" { +test "tarball with duplicate file names" { + const gpa = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const tarball_name = "package.tar"; try createTestTarball(tmp.dir, tarball_name, false); + const tarball_path = try std.fmt.allocPrint(gpa, "zig-cache/tmp/{s}/{s}", .{ tmp.sub_path, tarball_name }); + defer gpa.free(tarball_path); // Run tarball fetch, expect to fail var fb: TestFetchBuilder = undefined; - var fetch = try fb.build(std.testing.allocator, tmp, tarball_name); + var fetch = try fb.build(gpa, tmp.dir, tarball_path); defer fb.deinit(); try std.testing.expectError(error.FetchFailed, fetch.run()); @@ -1911,16 +1914,19 @@ test "fetch tarball: fail with unable to create file" { ); } -test "fetch tarball: error path are excluded" { +test "tarball with error paths excluded" { + const gpa = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const tarball_name = "package.tar"; try createTestTarball(tmp.dir, tarball_name, true); + const tarball_path = try std.fmt.allocPrint(gpa, "zig-cache/tmp/{s}/{s}", .{ tmp.sub_path, tarball_name }); + defer gpa.free(tarball_path); // Run tarball fetch, should succeed var fb: TestFetchBuilder = undefined; - var fetch = try fb.build(std.testing.allocator, tmp, tarball_name); + var fetch = try fb.build(std.testing.allocator, tmp.dir, tarball_path); defer fb.deinit(); try fetch.run(); @@ -1944,9 +1950,8 @@ const TestFetchBuilder = struct { job_queue: Fetch.JobQueue, fetch: Fetch, - fn build(self: *TestFetchBuilder, allocator: std.mem.Allocator, tmp: std.testing.TmpDir, tarball_name: []const u8) !*Fetch { - const cache_dir = try tmp.dir.makeOpenPath("zig-global-cache", .{}); - const path_or_url = try std.fmt.allocPrint(allocator, "zig-cache/tmp/{s}/{s}", .{ tmp.sub_path, tarball_name }); + fn build(self: *TestFetchBuilder, allocator: std.mem.Allocator, cache_parent_dir: std.fs.Dir, path_or_url: []const u8) !*Fetch { + const cache_dir = try cache_parent_dir.makeOpenPath("zig-global-cache", .{}); try self.thread_pool.init(.{ .allocator = allocator }); self.http_client = .{ .allocator = allocator }; @@ -1993,7 +1998,6 @@ const TestFetchBuilder = struct { } fn deinit(self: *TestFetchBuilder) void { - self.fetch.arena.child_allocator.free(self.fetch.location.path_or_url); self.fetch.deinit(); self.job_queue.deinit(); self.root_prog_node.end(); @@ -2043,8 +2047,9 @@ const TestFetchBuilder = struct { const em = errors.getErrorMessage(errors.getMessages()[0]); try std.testing.expectEqual(1, em.count); - try std.testing.expectEqual(notes_len, em.notes_len); - + if (notes_len > 0) { + try std.testing.expectEqual(notes_len, em.notes_len); + } var al = std.ArrayList(u8).init(std.testing.allocator); defer al.deinit(); try errors.renderToWriter(.{ .ttyconf = .no_color }, al.writer()); @@ -2098,3 +2103,119 @@ fn createTestTarball(dir: fs.Dir, tarball_name: []const u8, with_manifest: bool) try file.writeAll(&[_]u8{0} ** (512 - build_zig_zon.len)); } } + +// Using test cases from: https://github.com/ianprime0509/pathological-packages +// repository. Depends on existence of the FAT32 file system at /tmp/fat32.mnt +// (look at the fat32TmpDir function below how to create it). If that folder is +// not found test will be skipped. Folder is in FAT32 file system because it is +// case insensitive and and does not support symlinks. +test "pathological packages" { + const gpa = std.testing.allocator; + var buf: [128]u8 = undefined; + + const urls: []const []const u8 = &.{ + "https://github.com/ianprime0509/pathological-packages/archive/{s}.tar.gz", + "git+https://github.com/ianprime0509/pathological-packages#{s}", + }; + const branches: []const []const u8 = &.{ + "excluded-case-collisions", + "excluded-symlinks", + "included-case-collisions", + "included-symlinks", + }; + + // Expected fetched package files or error message for each combination of url/branch. + const expected = [_]struct { + files: []const []const u8 = &.{}, + err_msg: []const u8 = "", + }{ + // tar + .{ .files = &.{ "build.zig", "build.zig.zon" } }, + .{ .files = &.{ "build.zig", "build.zig.zon", "main" } }, + .{ .err_msg = + \\error: unable to unpack tarball + \\ note: unable to create file 'main': PathAlreadyExists + \\ note: unable to create file 'subdir/main': PathAlreadyExists + \\ + }, + .{ .err_msg = + \\error: unable to unpack tarball + \\ note: unable to create symlink from 'link' to 'main': AccessDenied + \\ note: unable to create symlink from 'subdir/link' to 'main': AccessDenied + \\ + }, + // git + .{ .files = &.{ "build.zig", "build.zig.zon" } }, + .{ .files = &.{ "build.zig", "build.zig.zon", "main" } }, + .{ .err_msg = + \\error: unable to unpack packfile + \\ note: unable to create file 'main': PathAlreadyExists + \\ note: unable to create file 'subdir/main': PathAlreadyExists + \\ + }, + .{ .err_msg = + \\error: unable to unpack packfile + \\ note: unable to create symlink from 'link' to 'main': AccessDenied + \\ note: unable to create symlink from 'subdir/link' to 'main': AccessDenied + \\ + }, + }; + + var expected_no: usize = 0; + inline for (urls) |url_fmt| { + var tmp = try fat32TmpDir(); + defer tmp.cleanup(); + + for (branches) |branch| { + defer expected_no += 1; + const url = try std.fmt.bufPrint(&buf, url_fmt, .{branch}); + // std.debug.print("fetching url: {s}\n", .{url}); + + var fb: TestFetchBuilder = undefined; + var fetch = try fb.build(gpa, tmp.dir, url); + defer fb.deinit(); + + const ex = expected[expected_no]; + if (ex.err_msg.len > 0) { + try std.testing.expectError(error.FetchFailed, fetch.run()); + try fb.expectFetchErrors(0, ex.err_msg); + } else { + try fetch.run(); + try fb.expectPackageFiles(ex.files); + } + } + } +} + +// Using logic from std.testing.tmpDir() to make temporary directory at specific +// location. +// +// This assumes FAT32 file system in /tmp/fat32.mnt folder, +// created with something like this: +// $ cd /tmp && fallocate -l 1M fat32.fs && mkfs.fat -F32 fat32.fs && mkdir fat32.mnt && sudo mount -o rw,umask=0000 fat32.fs fat32.mnt +// +// To remove that folder: +// $ cd /tmp && sudo umount fat32.mnt && rm -rf fat32.mnt fat32.fs +// +pub fn fat32TmpDir() !std.testing.TmpDir { + const fat32fs_path = "/tmp/fat32.mnt/"; + + const random_bytes_count = 12; + var random_bytes: [random_bytes_count]u8 = undefined; + std.crypto.random.bytes(&random_bytes); + var sub_path: [std.fs.base64_encoder.calcSize(random_bytes_count)]u8 = undefined; + _ = std.fs.base64_encoder.encode(&sub_path, &random_bytes); + + const parent_dir = std.fs.openDirAbsolute(fat32fs_path, .{}) catch |err| switch (err) { + error.FileNotFound => return error.SkipZigTest, + else => return err, + }; + const dir = parent_dir.makeOpenPath(&sub_path, .{}) catch + @panic("unable to make tmp dir for testing: unable to make and open the tmp dir"); + + return .{ + .dir = dir, + .parent_dir = parent_dir, + .sub_path = sub_path, + }; +}