authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2022-11-25 17:12:50-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2022-11-28 01:23:39-05:00
log3ae4931dc1a3b1b338d2fd3a49a5a79b445bebf6
tree8c6f031412192a2024f17506a8f9d10e0fd13343
parent7411be3c9e6d169108456f03b3cbb9b476ee7498

CLI: more careful resolution of paths

In general, we prefer compiler code to use relative paths based on open directory handles because this is the most portable. However, sometimes absolute paths are used, and sometimes relative paths are used that go up a directory. The recent improvements in 81d2135ca6ebd71b8c121a19957c8fbf7f87125b regressed the use case when an absolute path is used for the zig lib directory mixed with a relative path used for the root source file. This could happen when, for example, running the standard library tests, like this: stage3/bin/zig test ../lib/std/std.zig This happened because the zig lib dir was inferred to be an absolute directory based on the zig executable directory, while the root source file was detected as a relative path. There was no common prefix and so it was not determined that the std.zig file was inside the lib directory. This commit adds a function for resolving paths that preserves relative path names while allowing absolute paths, and converting relative upwards paths (e.g. "../foo") to absolute paths. This restores the previous functionality while remaining compatible with systems such as WASI that cannot deal with absolute paths.

6 files changed, 131 insertions(+), 58 deletions(-)

build.zig+1-1
......@@ -41,9 +41,9 @@ pub fn build(b: *Builder) !void {
4141 docs_step.dependOn(&docgen_cmd.step);
4242
4343 const test_cases = b.addTest("src/test.zig");
44 test_cases.main_pkg_path = ".";
4445 test_cases.stack_size = stack_size;
4546 test_cases.setBuildMode(mode);
46 test_cases.addPackagePath("test_cases", "test/cases.zig");
4747 test_cases.single_threaded = single_threaded;
4848
4949 const fmt_build_zig = b.addFmt(&[_][]const u8{"build.zig"});
lib/std/fs/path.zig+26-25
......@@ -462,7 +462,6 @@ pub fn resolve(allocator: Allocator, paths: []const []const u8) ![]u8 {
462462/// This function is like a series of `cd` statements executed one after another.
463463/// It resolves "." and "..".
464464/// The result does not have a trailing path separator.
465/// If all paths are relative it uses the current working directory as a starting point.
466465/// Each drive has its own current working directory.
467466/// Path separators are canonicalized to '\\' and drives are canonicalized to capital letters.
468467/// Note: all usage of this function should be audited due to the existence of symlinks.
......@@ -572,15 +571,15 @@ pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) ![]u8 {
572571 continue;
573572 }
574573 var it = mem.tokenize(u8, p[parsed.disk_designator.len..], "/\\");
575 component: while (it.next()) |component| {
574 while (it.next()) |component| {
576575 if (mem.eql(u8, component, ".")) {
577576 continue;
578577 } else if (mem.eql(u8, component, "..")) {
578 if (result.items.len == 0) {
579 negative_count += 1;
580 continue;
581 }
579582 while (true) {
580 if (result.items.len == 0) {
581 negative_count += 1;
582 continue :component;
583 }
584583 if (result.items.len == disk_designator_len) {
585584 break;
586585 }
......@@ -589,7 +588,7 @@ pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) ![]u8 {
589588 else => false,
590589 };
591590 result.items.len -= 1;
592 if (end_with_sep) break;
591 if (end_with_sep or result.items.len == 0) break;
593592 }
594593 } else if (!have_abs_path and result.items.len == 0) {
595594 try result.appendSlice(component);
......@@ -659,18 +658,18 @@ pub fn resolvePosix(allocator: Allocator, paths: []const []const u8) Allocator.E
659658 result.clearRetainingCapacity();
660659 }
661660 var it = mem.tokenize(u8, p, "/");
662 component: while (it.next()) |component| {
661 while (it.next()) |component| {
663662 if (mem.eql(u8, component, ".")) {
664663 continue;
665664 } else if (mem.eql(u8, component, "..")) {
665 if (result.items.len == 0) {
666 negative_count += @boolToInt(!is_abs);
667 continue;
668 }
666669 while (true) {
667 if (result.items.len == 0) {
668 negative_count += @boolToInt(!is_abs);
669 continue :component;
670 }
671670 const ends_with_slash = result.items[result.items.len - 1] == '/';
672671 result.items.len -= 1;
673 if (ends_with_slash) break;
672 if (ends_with_slash or result.items.len == 0) break;
674673 }
675674 } else if (result.items.len > 0 or is_abs) {
676675 try result.ensureUnusedCapacity(1 + component.len);
......@@ -717,10 +716,10 @@ pub fn resolvePosix(allocator: Allocator, paths: []const []const u8) Allocator.E
717716}
718717
719718test "resolve" {
720 try testResolveWindows(&[_][]const u8{ "a\\b\\c\\", "..\\..\\.." }, "..");
719 try testResolveWindows(&[_][]const u8{ "a\\b\\c\\", "..\\..\\.." }, ".");
721720 try testResolveWindows(&[_][]const u8{"."}, ".");
722721
723 try testResolvePosix(&[_][]const u8{ "a/b/c/", "../../.." }, "..");
722 try testResolvePosix(&[_][]const u8{ "a/b/c/", "../../.." }, ".");
724723 try testResolvePosix(&[_][]const u8{"."}, ".");
725724}
726725
......@@ -753,19 +752,21 @@ test "resolveWindows" {
753752}
754753
755754test "resolvePosix" {
756 try testResolvePosix(&[_][]const u8{ "/a/b", "c" }, "/a/b/c");
757 try testResolvePosix(&[_][]const u8{ "/a/b", "c", "//d", "e///" }, "/d/e");
758 try testResolvePosix(&[_][]const u8{ "/a/b/c", "..", "../" }, "/a");
759 try testResolvePosix(&[_][]const u8{ "/", "..", ".." }, "/");
760 try testResolvePosix(&[_][]const u8{"/a/b/c/"}, "/a/b/c");
755 try testResolvePosix(&.{ "/a/b", "c" }, "/a/b/c");
756 try testResolvePosix(&.{ "/a/b", "c", "//d", "e///" }, "/d/e");
757 try testResolvePosix(&.{ "/a/b/c", "..", "../" }, "/a");
758 try testResolvePosix(&.{ "/", "..", ".." }, "/");
759 try testResolvePosix(&.{"/a/b/c/"}, "/a/b/c");
761760
762 try testResolvePosix(&[_][]const u8{ "/var/lib", "../", "file/" }, "/var/file");
763 try testResolvePosix(&[_][]const u8{ "/var/lib", "/../", "file/" }, "/file");
764 try testResolvePosix(&[_][]const u8{ "/some/dir", ".", "/absolute/" }, "/absolute");
765 try testResolvePosix(&[_][]const u8{ "/foo/tmp.3/", "../tmp.3/cycles/root.js" }, "/foo/tmp.3/cycles/root.js");
761 try testResolvePosix(&.{ "/var/lib", "../", "file/" }, "/var/file");
762 try testResolvePosix(&.{ "/var/lib", "/../", "file/" }, "/file");
763 try testResolvePosix(&.{ "/some/dir", ".", "/absolute/" }, "/absolute");
764 try testResolvePosix(&.{ "/foo/tmp.3/", "../tmp.3/cycles/root.js" }, "/foo/tmp.3/cycles/root.js");
766765
767766 // Keep relative paths relative.
768 try testResolvePosix(&[_][]const u8{"a/b"}, "a/b");
767 try testResolvePosix(&.{"a/b"}, "a/b");
768 try testResolvePosix(&.{"."}, ".");
769 try testResolvePosix(&.{ ".", "src/test.zig", "..", "../test/cases.zig" }, "test/cases.zig");
769770}
770771
771772fn testResolveWindows(paths: []const []const u8, expected: []const u8) !void {
src/Module.zig+25-12
......@@ -30,6 +30,7 @@ const Sema = @import("Sema.zig");
3030const target_util = @import("target.zig");
3131const build_options = @import("build_options");
3232const Liveness = @import("Liveness.zig");
33const isUpDir = @import("introspect.zig").isUpDir;
3334
3435/// General-purpose allocator. Used for both temporary and long-term storage.
3536gpa: Allocator,
......@@ -4957,15 +4958,19 @@ pub fn importFile(
49574958 const resolved_root_path = try std.fs.path.resolve(gpa, &[_][]const u8{cur_pkg_dir_path});
49584959 defer gpa.free(resolved_root_path);
49594960
4960 if (!mem.startsWith(u8, resolved_path, resolved_root_path) or
4961 // This prevents this check from triggering when the name of the
4962 // imported file starts with the root path's directory name.
4963 !std.fs.path.isSep(resolved_path[resolved_root_path.len]))
4964 {
4961 const sub_file_path = p: {
4962 if (mem.startsWith(u8, resolved_path, resolved_root_path)) {
4963 // +1 for the directory separator here.
4964 break :p try gpa.dupe(u8, resolved_path[resolved_root_path.len + 1 ..]);
4965 }
4966 if (mem.eql(u8, resolved_root_path, ".") and
4967 !isUpDir(resolved_path) and
4968 !std.fs.path.isAbsolute(resolved_path))
4969 {
4970 break :p try gpa.dupe(u8, resolved_path);
4971 }
49654972 return error.ImportOutsidePkgPath;
4966 }
4967 // +1 for the directory separator here.
4968 const sub_file_path = try gpa.dupe(u8, resolved_path[resolved_root_path.len + 1 ..]);
4973 };
49694974 errdefer gpa.free(sub_file_path);
49704975
49714976 log.debug("new importFile. resolved_root_path={s}, resolved_path={s}, sub_file_path={s}, import_string={s}", .{
......@@ -5015,11 +5020,19 @@ pub fn embedFile(mod: *Module, cur_file: *File, rel_file_path: []const u8) !*Emb
50155020 const resolved_root_path = try std.fs.path.resolve(gpa, &[_][]const u8{cur_pkg_dir_path});
50165021 defer gpa.free(resolved_root_path);
50175022
5018 if (!mem.startsWith(u8, resolved_path, resolved_root_path)) {
5023 const sub_file_path = p: {
5024 if (mem.startsWith(u8, resolved_path, resolved_root_path)) {
5025 // +1 for the directory separator here.
5026 break :p try gpa.dupe(u8, resolved_path[resolved_root_path.len + 1 ..]);
5027 }
5028 if (mem.eql(u8, resolved_root_path, ".") and
5029 !isUpDir(resolved_path) and
5030 !std.fs.path.isAbsolute(resolved_path))
5031 {
5032 break :p try gpa.dupe(u8, resolved_path);
5033 }
50195034 return error.ImportOutsidePkgPath;
5020 }
5021 // +1 for the directory separator here.
5022 const sub_file_path = try gpa.dupe(u8, resolved_path[resolved_root_path.len + 1 ..]);
5035 };
50235036 errdefer gpa.free(sub_file_path);
50245037
50255038 var file = try cur_file.pkg.root_src_directory.handle.openFile(sub_file_path, .{});
src/introspect.zig+52-2
......@@ -82,7 +82,12 @@ pub fn findZigLibDir(gpa: mem.Allocator) !Compilation.Directory {
8282pub fn findZigLibDirFromSelfExe(
8383 allocator: mem.Allocator,
8484 self_exe_path: []const u8,
85) error{ OutOfMemory, FileNotFound }!Compilation.Directory {
85) error{
86 OutOfMemory,
87 FileNotFound,
88 CurrentWorkingDirectoryUnlinked,
89 Unexpected,
90}!Compilation.Directory {
8691 const cwd = fs.cwd();
8792 var cur_path: []const u8 = self_exe_path;
8893 while (fs.path.dirname(cur_path)) |dirname| : (cur_path = dirname) {
......@@ -90,9 +95,11 @@ pub fn findZigLibDirFromSelfExe(
9095 defer base_dir.close();
9196
9297 const sub_directory = testZigInstallPrefix(base_dir) orelse continue;
98 const p = try fs.path.join(allocator, &[_][]const u8{ dirname, sub_directory.path.? });
99 defer allocator.free(p);
93100 return Compilation.Directory{
94101 .handle = sub_directory.handle,
95 .path = try fs.path.join(allocator, &[_][]const u8{ dirname, sub_directory.path.? }),
102 .path = try resolvePath(allocator, p),
96103 };
97104 }
98105 return error.FileNotFound;
......@@ -130,3 +137,46 @@ pub fn resolveGlobalCacheDir(allocator: mem.Allocator) ![]u8 {
130137 return fs.getAppDataDir(allocator, appname);
131138 }
132139}
140
141/// Similar to std.fs.path.resolve, with a few important differences:
142/// * If the input is an absolute path, check it against the cwd and try to
143/// convert it to a relative path.
144/// * If the resulting path would start with a relative up-dir ("../"), instead
145/// return an absolute path based on the cwd.
146/// * When targeting WASI, fail with an error message if an absolute path is
147/// used.
148pub fn resolvePath(
149 ally: mem.Allocator,
150 p: []const u8,
151) error{
152 OutOfMemory,
153 CurrentWorkingDirectoryUnlinked,
154 Unexpected,
155}![]u8 {
156 if (fs.path.isAbsolute(p)) {
157 const cwd_path = try std.process.getCwdAlloc(ally);
158 defer ally.free(cwd_path);
159 const relative = try fs.path.relative(ally, cwd_path, p);
160 if (isUpDir(relative)) {
161 ally.free(relative);
162 return ally.dupe(u8, p);
163 } else {
164 return relative;
165 }
166 } else {
167 const resolved = try fs.path.resolve(ally, &.{p});
168 if (isUpDir(resolved)) {
169 ally.free(resolved);
170 const cwd_path = try std.process.getCwdAlloc(ally);
171 defer ally.free(cwd_path);
172 return fs.path.resolve(ally, &.{ cwd_path, p });
173 } else {
174 return resolved;
175 }
176 }
177}
178
179/// TODO move this to std.fs.path
180pub fn isUpDir(p: []const u8) bool {
181 return mem.startsWith(u8, p, "..") and (p.len == 2 or p[2] == fs.path.sep);
182}
src/main.zig+26-17
......@@ -885,24 +885,28 @@ fn buildOutputType(
885885 fatal("unexpected end-of-parameter mark: --", .{});
886886 }
887887 } else if (mem.eql(u8, arg, "--pkg-begin")) {
888 const pkg_name = args_iter.next();
889 const pkg_path = args_iter.next();
890 if (pkg_name == null or pkg_path == null) fatal("Expected 2 arguments after {s}", .{arg});
888 const opt_pkg_name = args_iter.next();
889 const opt_pkg_path = args_iter.next();
890 if (opt_pkg_name == null or opt_pkg_path == null)
891 fatal("Expected 2 arguments after {s}", .{arg});
892
893 const pkg_name = opt_pkg_name.?;
894 const pkg_path = try introspect.resolvePath(arena, opt_pkg_path.?);
891895
892896 const new_cur_pkg = Package.create(
893897 gpa,
894 fs.path.dirname(pkg_path.?),
895 fs.path.basename(pkg_path.?),
898 fs.path.dirname(pkg_path),
899 fs.path.basename(pkg_path),
896900 ) catch |err| {
897 fatal("Failed to add package at path {s}: {s}", .{ pkg_path.?, @errorName(err) });
901 fatal("Failed to add package at path {s}: {s}", .{ pkg_path, @errorName(err) });
898902 };
899903
900 if (mem.eql(u8, pkg_name.?, "std") or mem.eql(u8, pkg_name.?, "root") or mem.eql(u8, pkg_name.?, "builtin")) {
901 fatal("unable to add package '{s}' -> '{s}': conflicts with builtin package", .{ pkg_name.?, pkg_path.? });
902 } else if (cur_pkg.table.get(pkg_name.?)) |prev| {
903 fatal("unable to add package '{s}' -> '{s}': already exists as '{s}", .{ pkg_name.?, pkg_path.?, prev.root_src_path });
904 if (mem.eql(u8, pkg_name, "std") or mem.eql(u8, pkg_name, "root") or mem.eql(u8, pkg_name, "builtin")) {
905 fatal("unable to add package '{s}' -> '{s}': conflicts with builtin package", .{ pkg_name, pkg_path });
906 } else if (cur_pkg.table.get(pkg_name)) |prev| {
907 fatal("unable to add package '{s}' -> '{s}': already exists as '{s}", .{ pkg_name, pkg_path, prev.root_src_path });
904908 }
905 try cur_pkg.addAndAdopt(gpa, pkg_name.?, new_cur_pkg);
909 try cur_pkg.addAndAdopt(gpa, pkg_name, new_cur_pkg);
906910 cur_pkg = new_cur_pkg;
907911 } else if (mem.eql(u8, arg, "--pkg-end")) {
908912 cur_pkg = cur_pkg.parent orelse
......@@ -2705,11 +2709,16 @@ fn buildOutputType(
27052709 };
27062710 defer emit_implib_resolved.deinit();
27072711
2708 const main_pkg: ?*Package = if (root_src_file) |src_path| blk: {
2709 if (main_pkg_path) |p| {
2710 const rel_src_path = try fs.path.relative(gpa, p, src_path);
2711 defer gpa.free(rel_src_path);
2712 break :blk try Package.create(gpa, p, rel_src_path);
2712 const main_pkg: ?*Package = if (root_src_file) |unresolved_src_path| blk: {
2713 const src_path = try introspect.resolvePath(arena, unresolved_src_path);
2714 if (main_pkg_path) |unresolved_main_pkg_path| {
2715 const p = try introspect.resolvePath(arena, unresolved_main_pkg_path);
2716 if (p.len == 0) {
2717 break :blk try Package.create(gpa, null, src_path);
2718 } else {
2719 const rel_src_path = try fs.path.relative(arena, p, src_path);
2720 break :blk try Package.create(gpa, p, rel_src_path);
2721 }
27132722 } else {
27142723 const root_src_dir_path = fs.path.dirname(src_path);
27152724 break :blk Package.create(gpa, root_src_dir_path, fs.path.basename(src_path)) catch |err| {
......@@ -2745,7 +2754,7 @@ fn buildOutputType(
27452754
27462755 const self_exe_path = try introspect.findZigExePath(arena);
27472756 var zig_lib_directory: Compilation.Directory = if (override_lib_dir) |unresolved_lib_dir| l: {
2748 const lib_dir = try fs.path.resolve(arena, &.{unresolved_lib_dir});
2757 const lib_dir = try introspect.resolvePath(arena, unresolved_lib_dir);
27492758 break :l .{
27502759 .path = lib_dir,
27512760 .handle = fs.cwd().openDir(lib_dir, .{}) catch |err| {
src/test.zig+1-1
......@@ -60,7 +60,7 @@ test {
6060 ctx.addTestCasesFromDir(dir);
6161 }
6262
63 try @import("test_cases").addCases(&ctx);
63 try @import("../test/cases.zig").addCases(&ctx);
6464
6565 try ctx.run();
6666}