| ... | ... | @@ -441,7 +441,7 @@ fn runResource( |
| 441 | 441 | const rand_int = std.crypto.random.int(u64); |
| 442 | 442 | const tmp_dir_sub_path = "tmp" ++ s ++ Manifest.hex64(rand_int); |
| 443 | 443 | |
| 444 | | { |
| 444 | const package_sub_path = blk: { |
| 445 | 445 | const tmp_directory_path = try cache_root.join(arena, &.{tmp_dir_sub_path}); |
| 446 | 446 | var tmp_directory: Cache.Directory = .{ |
| 447 | 447 | .path = tmp_directory_path, |
| ... | ... | @@ -461,37 +461,50 @@ fn runResource( |
| 461 | 461 | }; |
| 462 | 462 | defer tmp_directory.handle.close(); |
| 463 | 463 | |
| 464 | | try unpackResource(f, resource, uri_path, tmp_directory); |
| 464 | // Unpack resource into tmp_directory. A non-null return value means |
| 465 | // that the package contents are inside a `pkg_dir` sub-directory. |
| 466 | const pkg_dir = try unpackResource(f, resource, uri_path, tmp_directory); |
| 467 | |
| 468 | var pkg_path: Cache.Path = .{ |
| 469 | .root_dir = tmp_directory, |
| 470 | .sub_path = if (pkg_dir) |pkg_dir_name| pkg_dir_name else "", |
| 471 | }; |
| 472 | |
| 473 | // Apply btrfs workaround if needed. Reopen tmp_directory. |
| 474 | if (native_os == .linux and f.job_queue.work_around_btrfs_bug) { |
| 475 | // https://github.com/ziglang/zig/issues/17095 |
| 476 | pkg_path.root_dir.handle.close(); |
| 477 | pkg_path.root_dir.handle = cache_root.handle.makeOpenPath(tmp_dir_sub_path, .{ |
| 478 | .iterate = true, |
| 479 | }) catch @panic("btrfs workaround failed"); |
| 480 | } |
| 465 | 481 | |
| 466 | 482 | // Load, parse, and validate the unpacked build.zig.zon file. It is allowed |
| 467 | 483 | // for the file to be missing, in which case this fetched package is |
| 468 | 484 | // considered to be a "naked" package. |
| 469 | | try loadManifest(f, .{ .root_dir = tmp_directory }); |
| 470 | | |
| 471 | | // Apply the manifest's inclusion rules to the temporary directory by |
| 472 | | // deleting excluded files. If any error occurred for files that were |
| 473 | | // ultimately excluded, those errors should be ignored, such as failure to |
| 474 | | // create symlinks that weren't supposed to be included anyway. |
| 475 | | |
| 476 | | // Empty directories have already been omitted by `unpackResource`. |
| 485 | try loadManifest(f, pkg_path); |
| 477 | 486 | |
| 478 | 487 | const filter: Filter = .{ |
| 479 | 488 | .include_paths = if (f.manifest) |m| m.paths else .{}, |
| 480 | 489 | }; |
| 481 | 490 | |
| 491 | // TODO: |
| 492 | // If any error occurred for files that were ultimately excluded, those |
| 493 | // errors should be ignored, such as failure to create symlinks that |
| 494 | // weren't supposed to be included anyway. |
| 495 | |
| 496 | // Apply the manifest's inclusion rules to the temporary directory by |
| 497 | // deleting excluded files. |
| 498 | // Empty directories have already been omitted by `unpackResource`. |
| 482 | 499 | // Compute the package hash based on the remaining files in the temporary |
| 483 | 500 | // directory. |
| 501 | f.actual_hash = try computeHash(f, pkg_path, filter); |
| 484 | 502 | |
| 485 | | if (native_os == .linux and f.job_queue.work_around_btrfs_bug) { |
| 486 | | // https://github.com/ziglang/zig/issues/17095 |
| 487 | | tmp_directory.handle.close(); |
| 488 | | tmp_directory.handle = cache_root.handle.makeOpenPath(tmp_dir_sub_path, .{ |
| 489 | | .iterate = true, |
| 490 | | }) catch @panic("btrfs workaround failed"); |
| 491 | | } |
| 492 | | |
| 493 | | f.actual_hash = try computeHash(f, tmp_directory, filter); |
| 494 | | } |
| 503 | break :blk if (pkg_dir) |pkg_dir_name| |
| 504 | try fs.path.join(arena, &.{ tmp_dir_sub_path, pkg_dir_name }) |
| 505 | else |
| 506 | tmp_dir_sub_path; |
| 507 | }; |
| 495 | 508 | |
| 496 | 509 | // Rename the temporary directory into the global zig package cache |
| 497 | 510 | // directory. If the hash already exists, delete the temporary directory |
| ... | ... | @@ -503,7 +516,7 @@ fn runResource( |
| 503 | 516 | .root_dir = cache_root, |
| 504 | 517 | .sub_path = try arena.dupe(u8, "p" ++ s ++ Manifest.hexDigest(f.actual_hash)), |
| 505 | 518 | }; |
| 506 | | renameTmpIntoCache(cache_root.handle, tmp_dir_sub_path, f.package_root.sub_path) catch |err| { |
| 519 | renameTmpIntoCache(cache_root.handle, package_sub_path, f.package_root.sub_path) catch |err| { |
| 507 | 520 | const src = try cache_root.join(arena, &.{tmp_dir_sub_path}); |
| 508 | 521 | const dest = try cache_root.join(arena, &.{f.package_root.sub_path}); |
| 509 | 522 | try eb.addRootErrorMessage(.{ .msg = try eb.printString( |
| ... | ... | @@ -512,6 +525,10 @@ fn runResource( |
| 512 | 525 | ) }); |
| 513 | 526 | return error.FetchFailed; |
| 514 | 527 | }; |
| 528 | // Remove temporary directory root if not already renamed to global cache. |
| 529 | if (!std.mem.eql(u8, package_sub_path, tmp_dir_sub_path)) { |
| 530 | cache_root.handle.deleteDir(tmp_dir_sub_path) catch {}; |
| 531 | } |
| 515 | 532 | |
| 516 | 533 | // Validate the computed hash against the expected hash. If invalid, this |
| 517 | 534 | // job is done. |
| ... | ... | @@ -867,9 +884,9 @@ const FileType = enum { |
| 867 | 884 | try std.testing.expectEqual(@as(?FileType, .@"tar.xz"), fromContentDisposition("ATTACHMENT; filename=\"stuff.tar.xz\"")); |
| 868 | 885 | try std.testing.expectEqual(@as(?FileType, .@"tar.xz"), fromContentDisposition("attachment; FileName=\"stuff.tar.xz\"")); |
| 869 | 886 | try std.testing.expectEqual(@as(?FileType, .@"tar.gz"), fromContentDisposition("attachment; FileName*=UTF-8\'\'xyz%2Fstuff.tar.gz")); |
| 887 | try std.testing.expectEqual(@as(?FileType, .tar), fromContentDisposition("attachment; FileName=\"stuff.tar\"")); |
| 870 | 888 | |
| 871 | 889 | try std.testing.expect(fromContentDisposition("attachment FileName=\"stuff.tar.gz\"") == null); |
| 872 | | try std.testing.expect(fromContentDisposition("attachment; FileName=\"stuff.tar\"") == null); |
| 873 | 890 | try std.testing.expect(fromContentDisposition("attachment; FileName\"stuff.gz\"") == null); |
| 874 | 891 | try std.testing.expect(fromContentDisposition("attachment; size=42") == null); |
| 875 | 892 | try std.testing.expect(fromContentDisposition("inline; size=42") == null); |
| ... | ... | @@ -1027,12 +1044,16 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re |
| 1027 | 1044 | )); |
| 1028 | 1045 | } |
| 1029 | 1046 | |
| 1047 | /// A `null` return value indicates the `tmp_directory` is populated directly |
| 1048 | /// with the package contents. |
| 1049 | /// A non-null return value means that the package contents are inside a |
| 1050 | /// sub-directory indicated by the named path. |
| 1030 | 1051 | fn unpackResource( |
| 1031 | 1052 | f: *Fetch, |
| 1032 | 1053 | resource: *Resource, |
| 1033 | 1054 | uri_path: []const u8, |
| 1034 | 1055 | tmp_directory: Cache.Directory, |
| 1035 | | ) RunError!void { |
| 1056 | ) RunError!?[]const u8 { |
| 1036 | 1057 | const eb = &f.error_bundle; |
| 1037 | 1058 | const file_type = switch (resource.*) { |
| 1038 | 1059 | .file => FileType.fromPath(uri_path) orelse |
| ... | ... | @@ -1093,21 +1114,24 @@ fn unpackResource( |
| 1093 | 1114 | |
| 1094 | 1115 | .git => .git_pack, |
| 1095 | 1116 | |
| 1096 | | .dir => |dir| return f.recursiveDirectoryCopy(dir, tmp_directory.handle) catch |err| { |
| 1097 | | return f.fail(f.location_tok, try eb.printString( |
| 1098 | | "unable to copy directory '{s}': {s}", |
| 1099 | | .{ uri_path, @errorName(err) }, |
| 1100 | | )); |
| 1117 | .dir => |dir| { |
| 1118 | f.recursiveDirectoryCopy(dir, tmp_directory.handle) catch |err| { |
| 1119 | return f.fail(f.location_tok, try eb.printString( |
| 1120 | "unable to copy directory '{s}': {s}", |
| 1121 | .{ uri_path, @errorName(err) }, |
| 1122 | )); |
| 1123 | }; |
| 1124 | return null; |
| 1101 | 1125 | }, |
| 1102 | 1126 | }; |
| 1103 | 1127 | |
| 1104 | 1128 | switch (file_type) { |
| 1105 | | .tar => try unpackTarball(f, tmp_directory.handle, resource.reader()), |
| 1129 | .tar => return try unpackTarball(f, tmp_directory.handle, resource.reader()), |
| 1106 | 1130 | .@"tar.gz" => { |
| 1107 | 1131 | const reader = resource.reader(); |
| 1108 | 1132 | var br = std.io.bufferedReaderSize(std.crypto.tls.max_ciphertext_record_len, reader); |
| 1109 | 1133 | var dcp = std.compress.gzip.decompressor(br.reader()); |
| 1110 | | try unpackTarball(f, tmp_directory.handle, dcp.reader()); |
| 1134 | return try unpackTarball(f, tmp_directory.handle, dcp.reader()); |
| 1111 | 1135 | }, |
| 1112 | 1136 | .@"tar.xz" => { |
| 1113 | 1137 | const gpa = f.arena.child_allocator; |
| ... | ... | @@ -1120,7 +1144,7 @@ fn unpackResource( |
| 1120 | 1144 | )); |
| 1121 | 1145 | }; |
| 1122 | 1146 | defer dcp.deinit(); |
| 1123 | | try unpackTarball(f, tmp_directory.handle, dcp.reader()); |
| 1147 | return try unpackTarball(f, tmp_directory.handle, dcp.reader()); |
| 1124 | 1148 | }, |
| 1125 | 1149 | .@"tar.zst" => { |
| 1126 | 1150 | const window_size = std.compress.zstd.DecompressorOptions.default_window_buffer_len; |
| ... | ... | @@ -1130,21 +1154,25 @@ fn unpackResource( |
| 1130 | 1154 | var dcp = std.compress.zstd.decompressor(br.reader(), .{ |
| 1131 | 1155 | .window_buffer = window_buffer, |
| 1132 | 1156 | }); |
| 1133 | | return unpackTarball(f, tmp_directory.handle, dcp.reader()); |
| 1157 | return try unpackTarball(f, tmp_directory.handle, dcp.reader()); |
| 1134 | 1158 | }, |
| 1135 | | .git_pack => unpackGitPack(f, tmp_directory.handle, resource) catch |err| switch (err) { |
| 1136 | | error.FetchFailed => return error.FetchFailed, |
| 1137 | | error.OutOfMemory => return error.OutOfMemory, |
| 1138 | | else => |e| return f.fail(f.location_tok, try eb.printString( |
| 1139 | | "unable to unpack git files: {s}", |
| 1140 | | .{@errorName(e)}, |
| 1141 | | )), |
| 1159 | .git_pack => { |
| 1160 | unpackGitPack(f, tmp_directory.handle, resource) catch |err| switch (err) { |
| 1161 | error.FetchFailed => return error.FetchFailed, |
| 1162 | error.OutOfMemory => return error.OutOfMemory, |
| 1163 | else => |e| return f.fail(f.location_tok, try eb.printString( |
| 1164 | "unable to unpack git files: {s}", |
| 1165 | .{@errorName(e)}, |
| 1166 | )), |
| 1167 | }; |
| 1168 | return null; |
| 1142 | 1169 | }, |
| 1143 | 1170 | } |
| 1144 | 1171 | } |
| 1145 | 1172 | |
| 1146 | | fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: anytype) RunError!void { |
| 1173 | fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: anytype) RunError!?[]const u8 { |
| 1147 | 1174 | const eb = &f.error_bundle; |
| 1175 | const arena = f.arena.allocator(); |
| 1148 | 1176 | const gpa = f.arena.child_allocator; |
| 1149 | 1177 | |
| 1150 | 1178 | var diagnostics: std.tar.Diagnostics = .{ .allocator = gpa }; |
| ... | ... | @@ -1152,7 +1180,7 @@ fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: anytype) RunError!void { |
| 1152 | 1180 | |
| 1153 | 1181 | std.tar.pipeToFileSystem(out_dir, reader, .{ |
| 1154 | 1182 | .diagnostics = &diagnostics, |
| 1155 | | .strip_components = 1, |
| 1183 | .strip_components = 0, |
| 1156 | 1184 | // https://github.com/ziglang/zig/issues/17463 |
| 1157 | 1185 | .mode_mode = .ignore, |
| 1158 | 1186 | .exclude_empty_directories = true, |
| ... | ... | @@ -1196,6 +1224,11 @@ fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: anytype) RunError!void { |
| 1196 | 1224 | } |
| 1197 | 1225 | return error.FetchFailed; |
| 1198 | 1226 | } |
| 1227 | |
| 1228 | return if (diagnostics.root_dir) |root_dir| |
| 1229 | return try arena.dupe(u8, root_dir) |
| 1230 | else |
| 1231 | null; |
| 1199 | 1232 | } |
| 1200 | 1233 | |
| 1201 | 1234 | fn unpackGitPack(f: *Fetch, out_dir: fs.Dir, resource: *Resource) anyerror!void { |
| ... | ... | @@ -1341,7 +1374,7 @@ pub fn renameTmpIntoCache( |
| 1341 | 1374 | /// function. |
| 1342 | 1375 | fn computeHash( |
| 1343 | 1376 | f: *Fetch, |
| 1344 | | tmp_directory: Cache.Directory, |
| 1377 | pkg_path: Cache.Path, |
| 1345 | 1378 | filter: Filter, |
| 1346 | 1379 | ) RunError!Manifest.Digest { |
| 1347 | 1380 | // All the path name strings need to be in memory for sorting. |
| ... | ... | @@ -1349,6 +1382,7 @@ fn computeHash( |
| 1349 | 1382 | const gpa = f.arena.child_allocator; |
| 1350 | 1383 | const eb = &f.error_bundle; |
| 1351 | 1384 | const thread_pool = f.job_queue.thread_pool; |
| 1385 | const root_dir = pkg_path.root_dir.handle; |
| 1352 | 1386 | |
| 1353 | 1387 | // Collect all files, recursively, then sort. |
| 1354 | 1388 | var all_files = std.ArrayList(*HashedFile).init(gpa); |
| ... | ... | @@ -1362,7 +1396,7 @@ fn computeHash( |
| 1362 | 1396 | var sus_dirs: std.StringArrayHashMapUnmanaged(void) = .{}; |
| 1363 | 1397 | defer sus_dirs.deinit(gpa); |
| 1364 | 1398 | |
| 1365 | | var walker = try tmp_directory.handle.walk(gpa); |
| 1399 | var walker = try root_dir.walk(gpa); |
| 1366 | 1400 | defer walker.deinit(); |
| 1367 | 1401 | |
| 1368 | 1402 | { |
| ... | ... | @@ -1376,13 +1410,14 @@ fn computeHash( |
| 1376 | 1410 | while (walker.next() catch |err| { |
| 1377 | 1411 | try eb.addRootErrorMessage(.{ .msg = try eb.printString( |
| 1378 | 1412 | "unable to walk temporary directory '{}': {s}", |
| 1379 | | .{ tmp_directory, @errorName(err) }, |
| 1413 | .{ pkg_path, @errorName(err) }, |
| 1380 | 1414 | ) }); |
| 1381 | 1415 | return error.FetchFailed; |
| 1382 | 1416 | }) |entry| { |
| 1383 | 1417 | if (entry.kind == .directory) continue; |
| 1384 | 1418 | |
| 1385 | | if (!filter.includePath(entry.path)) { |
| 1419 | const entry_pkg_path = stripRoot(entry.path, pkg_path.sub_path); |
| 1420 | if (!filter.includePath(entry_pkg_path)) { |
| 1386 | 1421 | // Delete instead of including in hash calculation. |
| 1387 | 1422 | const fs_path = try arena.dupe(u8, entry.path); |
| 1388 | 1423 | |
| ... | ... | @@ -1397,7 +1432,7 @@ fn computeHash( |
| 1397 | 1432 | }; |
| 1398 | 1433 | wait_group.start(); |
| 1399 | 1434 | try thread_pool.spawn(workerDeleteFile, .{ |
| 1400 | | tmp_directory.handle, deleted_file, &wait_group, |
| 1435 | root_dir, deleted_file, &wait_group, |
| 1401 | 1436 | }); |
| 1402 | 1437 | try deleted_files.append(deleted_file); |
| 1403 | 1438 | continue; |
| ... | ... | @@ -1420,14 +1455,14 @@ fn computeHash( |
| 1420 | 1455 | const hashed_file = try arena.create(HashedFile); |
| 1421 | 1456 | hashed_file.* = .{ |
| 1422 | 1457 | .fs_path = fs_path, |
| 1423 | | .normalized_path = try normalizePathAlloc(arena, fs_path), |
| 1458 | .normalized_path = try normalizePathAlloc(arena, entry_pkg_path), |
| 1424 | 1459 | .kind = kind, |
| 1425 | 1460 | .hash = undefined, // to be populated by the worker |
| 1426 | 1461 | .failure = undefined, // to be populated by the worker |
| 1427 | 1462 | }; |
| 1428 | 1463 | wait_group.start(); |
| 1429 | 1464 | try thread_pool.spawn(workerHashFile, .{ |
| 1430 | | tmp_directory.handle, hashed_file, &wait_group, |
| 1465 | root_dir, hashed_file, &wait_group, |
| 1431 | 1466 | }); |
| 1432 | 1467 | try all_files.append(hashed_file); |
| 1433 | 1468 | } |
| ... | ... | @@ -1446,7 +1481,7 @@ fn computeHash( |
| 1446 | 1481 | var i: usize = 0; |
| 1447 | 1482 | while (i < sus_dirs.count()) : (i += 1) { |
| 1448 | 1483 | const sus_dir = sus_dirs.keys()[i]; |
| 1449 | | tmp_directory.handle.deleteDir(sus_dir) catch |err| switch (err) { |
| 1484 | root_dir.deleteDir(sus_dir) catch |err| switch (err) { |
| 1450 | 1485 | error.DirNotEmpty => continue, |
| 1451 | 1486 | error.FileNotFound => continue, |
| 1452 | 1487 | else => |e| { |
| ... | ... | @@ -1610,11 +1645,22 @@ const HashedFile = struct { |
| 1610 | 1645 | } |
| 1611 | 1646 | }; |
| 1612 | 1647 | |
| 1648 | /// Strips root directory name from file system path. |
| 1649 | fn stripRoot(fs_path: []const u8, root_dir: []const u8) []const u8 { |
| 1650 | if (root_dir.len == 0 or fs_path.len <= root_dir.len) return fs_path; |
| 1651 | |
| 1652 | if (std.mem.eql(u8, fs_path[0..root_dir.len], root_dir) and fs_path[root_dir.len] == fs.path.sep) { |
| 1653 | return fs_path[root_dir.len + 1 ..]; |
| 1654 | } |
| 1655 | |
| 1656 | return fs_path; |
| 1657 | } |
| 1658 | |
| 1613 | 1659 | /// Make a file system path identical independently of operating system path inconsistencies. |
| 1614 | 1660 | /// This converts backslashes into forward slashes. |
| 1615 | | fn normalizePathAlloc(arena: Allocator, fs_path: []const u8) ![]const u8 { |
| 1616 | | if (fs.path.sep == canonical_sep) return fs_path; |
| 1617 | | const normalized = try arena.dupe(u8, fs_path); |
| 1661 | fn normalizePathAlloc(arena: Allocator, pkg_path: []const u8) ![]const u8 { |
| 1662 | const normalized = try arena.dupe(u8, pkg_path); |
| 1663 | if (fs.path.sep == canonical_sep) return normalized; |
| 1618 | 1664 | normalizePath(normalized); |
| 1619 | 1665 | return normalized; |
| 1620 | 1666 | } |