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 {...@@ -41,9 +41,9 @@ pub fn build(b: *Builder) !void {
41 docs_step.dependOn(&docgen_cmd.step);41 docs_step.dependOn(&docgen_cmd.step);
4242
43 const test_cases = b.addTest("src/test.zig");43 const test_cases = b.addTest("src/test.zig");
44 test_cases.main_pkg_path = ".";
44 test_cases.stack_size = stack_size;45 test_cases.stack_size = stack_size;
45 test_cases.setBuildMode(mode);46 test_cases.setBuildMode(mode);
46 test_cases.addPackagePath("test_cases", "test/cases.zig");
47 test_cases.single_threaded = single_threaded;47 test_cases.single_threaded = single_threaded;
4848
49 const fmt_build_zig = b.addFmt(&[_][]const u8{"build.zig"});49 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 {...@@ -462,7 +462,6 @@ pub fn resolve(allocator: Allocator, paths: []const []const u8) ![]u8 {
462/// This function is like a series of `cd` statements executed one after another.462/// This function is like a series of `cd` statements executed one after another.
463/// It resolves "." and "..".463/// It resolves "." and "..".
464/// The result does not have a trailing path separator.464/// 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.
466/// Each drive has its own current working directory.465/// Each drive has its own current working directory.
467/// Path separators are canonicalized to '\\' and drives are canonicalized to capital letters.466/// Path separators are canonicalized to '\\' and drives are canonicalized to capital letters.
468/// Note: all usage of this function should be audited due to the existence of symlinks.467/// 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 {...@@ -572,15 +571,15 @@ pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) ![]u8 {
572 continue;571 continue;
573 }572 }
574 var it = mem.tokenize(u8, p[parsed.disk_designator.len..], "/\\");573 var it = mem.tokenize(u8, p[parsed.disk_designator.len..], "/\\");
575 component: while (it.next()) |component| {574 while (it.next()) |component| {
576 if (mem.eql(u8, component, ".")) {575 if (mem.eql(u8, component, ".")) {
577 continue;576 continue;
578 } else if (mem.eql(u8, component, "..")) {577 } else if (mem.eql(u8, component, "..")) {
578 if (result.items.len == 0) {
579 negative_count += 1;
580 continue;
581 }
579 while (true) {582 while (true) {
580 if (result.items.len == 0) {
581 negative_count += 1;
582 continue :component;
583 }
584 if (result.items.len == disk_designator_len) {583 if (result.items.len == disk_designator_len) {
585 break;584 break;
586 }585 }
...@@ -589,7 +588,7 @@ pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) ![]u8 {...@@ -589,7 +588,7 @@ pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) ![]u8 {
589 else => false,588 else => false,
590 };589 };
591 result.items.len -= 1;590 result.items.len -= 1;
592 if (end_with_sep) break;591 if (end_with_sep or result.items.len == 0) break;
593 }592 }
594 } else if (!have_abs_path and result.items.len == 0) {593 } else if (!have_abs_path and result.items.len == 0) {
595 try result.appendSlice(component);594 try result.appendSlice(component);
...@@ -659,18 +658,18 @@ pub fn resolvePosix(allocator: Allocator, paths: []const []const u8) Allocator.E...@@ -659,18 +658,18 @@ pub fn resolvePosix(allocator: Allocator, paths: []const []const u8) Allocator.E
659 result.clearRetainingCapacity();658 result.clearRetainingCapacity();
660 }659 }
661 var it = mem.tokenize(u8, p, "/");660 var it = mem.tokenize(u8, p, "/");
662 component: while (it.next()) |component| {661 while (it.next()) |component| {
663 if (mem.eql(u8, component, ".")) {662 if (mem.eql(u8, component, ".")) {
664 continue;663 continue;
665 } else if (mem.eql(u8, component, "..")) {664 } else if (mem.eql(u8, component, "..")) {
665 if (result.items.len == 0) {
666 negative_count += @boolToInt(!is_abs);
667 continue;
668 }
666 while (true) {669 while (true) {
667 if (result.items.len == 0) {
668 negative_count += @boolToInt(!is_abs);
669 continue :component;
670 }
671 const ends_with_slash = result.items[result.items.len - 1] == '/';670 const ends_with_slash = result.items[result.items.len - 1] == '/';
672 result.items.len -= 1;671 result.items.len -= 1;
673 if (ends_with_slash) break;672 if (ends_with_slash or result.items.len == 0) break;
674 }673 }
675 } else if (result.items.len > 0 or is_abs) {674 } else if (result.items.len > 0 or is_abs) {
676 try result.ensureUnusedCapacity(1 + component.len);675 try result.ensureUnusedCapacity(1 + component.len);
...@@ -717,10 +716,10 @@ pub fn resolvePosix(allocator: Allocator, paths: []const []const u8) Allocator.E...@@ -717,10 +716,10 @@ pub fn resolvePosix(allocator: Allocator, paths: []const []const u8) Allocator.E
717}716}
718717
719test "resolve" {718test "resolve" {
720 try testResolveWindows(&[_][]const u8{ "a\\b\\c\\", "..\\..\\.." }, "..");719 try testResolveWindows(&[_][]const u8{ "a\\b\\c\\", "..\\..\\.." }, ".");
721 try testResolveWindows(&[_][]const u8{"."}, ".");720 try testResolveWindows(&[_][]const u8{"."}, ".");
722721
723 try testResolvePosix(&[_][]const u8{ "a/b/c/", "../../.." }, "..");722 try testResolvePosix(&[_][]const u8{ "a/b/c/", "../../.." }, ".");
724 try testResolvePosix(&[_][]const u8{"."}, ".");723 try testResolvePosix(&[_][]const u8{"."}, ".");
725}724}
726725
...@@ -753,19 +752,21 @@ test "resolveWindows" {...@@ -753,19 +752,21 @@ test "resolveWindows" {
753}752}
754753
755test "resolvePosix" {754test "resolvePosix" {
756 try testResolvePosix(&[_][]const u8{ "/a/b", "c" }, "/a/b/c");755 try testResolvePosix(&.{ "/a/b", "c" }, "/a/b/c");
757 try testResolvePosix(&[_][]const u8{ "/a/b", "c", "//d", "e///" }, "/d/e");756 try testResolvePosix(&.{ "/a/b", "c", "//d", "e///" }, "/d/e");
758 try testResolvePosix(&[_][]const u8{ "/a/b/c", "..", "../" }, "/a");757 try testResolvePosix(&.{ "/a/b/c", "..", "../" }, "/a");
759 try testResolvePosix(&[_][]const u8{ "/", "..", ".." }, "/");758 try testResolvePosix(&.{ "/", "..", ".." }, "/");
760 try testResolvePosix(&[_][]const u8{"/a/b/c/"}, "/a/b/c");759 try testResolvePosix(&.{"/a/b/c/"}, "/a/b/c");
761760
762 try testResolvePosix(&[_][]const u8{ "/var/lib", "../", "file/" }, "/var/file");761 try testResolvePosix(&.{ "/var/lib", "../", "file/" }, "/var/file");
763 try testResolvePosix(&[_][]const u8{ "/var/lib", "/../", "file/" }, "/file");762 try testResolvePosix(&.{ "/var/lib", "/../", "file/" }, "/file");
764 try testResolvePosix(&[_][]const u8{ "/some/dir", ".", "/absolute/" }, "/absolute");763 try testResolvePosix(&.{ "/some/dir", ".", "/absolute/" }, "/absolute");
765 try testResolvePosix(&[_][]const u8{ "/foo/tmp.3/", "../tmp.3/cycles/root.js" }, "/foo/tmp.3/cycles/root.js");764 try testResolvePosix(&.{ "/foo/tmp.3/", "../tmp.3/cycles/root.js" }, "/foo/tmp.3/cycles/root.js");
766765
767 // Keep relative paths relative.766 // 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");
769}770}
770771
771fn testResolveWindows(paths: []const []const u8, expected: []const u8) !void {772fn testResolveWindows(paths: []const []const u8, expected: []const u8) !void {
src/Module.zig+25-12
...@@ -30,6 +30,7 @@ const Sema = @import("Sema.zig");...@@ -30,6 +30,7 @@ const Sema = @import("Sema.zig");
30const target_util = @import("target.zig");30const target_util = @import("target.zig");
31const build_options = @import("build_options");31const build_options = @import("build_options");
32const Liveness = @import("Liveness.zig");32const Liveness = @import("Liveness.zig");
33const isUpDir = @import("introspect.zig").isUpDir;
3334
34/// General-purpose allocator. Used for both temporary and long-term storage.35/// General-purpose allocator. Used for both temporary and long-term storage.
35gpa: Allocator,36gpa: Allocator,
...@@ -4957,15 +4958,19 @@ pub fn importFile(...@@ -4957,15 +4958,19 @@ pub fn importFile(
4957 const resolved_root_path = try std.fs.path.resolve(gpa, &[_][]const u8{cur_pkg_dir_path});4958 const resolved_root_path = try std.fs.path.resolve(gpa, &[_][]const u8{cur_pkg_dir_path});
4958 defer gpa.free(resolved_root_path);4959 defer gpa.free(resolved_root_path);
49594960
4960 if (!mem.startsWith(u8, resolved_path, resolved_root_path) or4961 const sub_file_path = p: {
4961 // This prevents this check from triggering when the name of the4962 if (mem.startsWith(u8, resolved_path, resolved_root_path)) {
4962 // imported file starts with the root path's directory name.4963 // +1 for the directory separator here.
4963 !std.fs.path.isSep(resolved_path[resolved_root_path.len]))4964 break :p try gpa.dupe(u8, resolved_path[resolved_root_path.len + 1 ..]);
4964 {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 }
4965 return error.ImportOutsidePkgPath;4972 return error.ImportOutsidePkgPath;
4966 }4973 };
4967 // +1 for the directory separator here.
4968 const sub_file_path = try gpa.dupe(u8, resolved_path[resolved_root_path.len + 1 ..]);
4969 errdefer gpa.free(sub_file_path);4974 errdefer gpa.free(sub_file_path);
49704975
4971 log.debug("new importFile. resolved_root_path={s}, resolved_path={s}, sub_file_path={s}, import_string={s}", .{4976 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...@@ -5015,11 +5020,19 @@ pub fn embedFile(mod: *Module, cur_file: *File, rel_file_path: []const u8) !*Emb
5015 const resolved_root_path = try std.fs.path.resolve(gpa, &[_][]const u8{cur_pkg_dir_path});5020 const resolved_root_path = try std.fs.path.resolve(gpa, &[_][]const u8{cur_pkg_dir_path});
5016 defer gpa.free(resolved_root_path);5021 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 }
5019 return error.ImportOutsidePkgPath;5034 return error.ImportOutsidePkgPath;
5020 }5035 };
5021 // +1 for the directory separator here.
5022 const sub_file_path = try gpa.dupe(u8, resolved_path[resolved_root_path.len + 1 ..]);
5023 errdefer gpa.free(sub_file_path);5036 errdefer gpa.free(sub_file_path);
50245037
5025 var file = try cur_file.pkg.root_src_directory.handle.openFile(sub_file_path, .{});5038 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 {...@@ -82,7 +82,12 @@ pub fn findZigLibDir(gpa: mem.Allocator) !Compilation.Directory {
82pub fn findZigLibDirFromSelfExe(82pub fn findZigLibDirFromSelfExe(
83 allocator: mem.Allocator,83 allocator: mem.Allocator,
84 self_exe_path: []const u8,84 self_exe_path: []const u8,
85) error{ OutOfMemory, FileNotFound }!Compilation.Directory {85) error{
86 OutOfMemory,
87 FileNotFound,
88 CurrentWorkingDirectoryUnlinked,
89 Unexpected,
90}!Compilation.Directory {
86 const cwd = fs.cwd();91 const cwd = fs.cwd();
87 var cur_path: []const u8 = self_exe_path;92 var cur_path: []const u8 = self_exe_path;
88 while (fs.path.dirname(cur_path)) |dirname| : (cur_path = dirname) {93 while (fs.path.dirname(cur_path)) |dirname| : (cur_path = dirname) {
...@@ -90,9 +95,11 @@ pub fn findZigLibDirFromSelfExe(...@@ -90,9 +95,11 @@ pub fn findZigLibDirFromSelfExe(
90 defer base_dir.close();95 defer base_dir.close();
9196
92 const sub_directory = testZigInstallPrefix(base_dir) orelse continue;97 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);
93 return Compilation.Directory{100 return Compilation.Directory{
94 .handle = sub_directory.handle,101 .handle = sub_directory.handle,
95 .path = try fs.path.join(allocator, &[_][]const u8{ dirname, sub_directory.path.? }),102 .path = try resolvePath(allocator, p),
96 };103 };
97 }104 }
98 return error.FileNotFound;105 return error.FileNotFound;
...@@ -130,3 +137,46 @@ pub fn resolveGlobalCacheDir(allocator: mem.Allocator) ![]u8 {...@@ -130,3 +137,46 @@ pub fn resolveGlobalCacheDir(allocator: mem.Allocator) ![]u8 {
130 return fs.getAppDataDir(allocator, appname);137 return fs.getAppDataDir(allocator, appname);
131 }138 }
132}139}
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(...@@ -885,24 +885,28 @@ fn buildOutputType(
885 fatal("unexpected end-of-parameter mark: --", .{});885 fatal("unexpected end-of-parameter mark: --", .{});
886 }886 }
887 } else if (mem.eql(u8, arg, "--pkg-begin")) {887 } else if (mem.eql(u8, arg, "--pkg-begin")) {
888 const pkg_name = args_iter.next();888 const opt_pkg_name = args_iter.next();
889 const pkg_path = args_iter.next();889 const opt_pkg_path = args_iter.next();
890 if (pkg_name == null or pkg_path == null) fatal("Expected 2 arguments after {s}", .{arg});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
892 const new_cur_pkg = Package.create(896 const new_cur_pkg = Package.create(
893 gpa,897 gpa,
894 fs.path.dirname(pkg_path.?),898 fs.path.dirname(pkg_path),
895 fs.path.basename(pkg_path.?),899 fs.path.basename(pkg_path),
896 ) catch |err| {900 ) 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) });
898 };902 };
899903
900 if (mem.eql(u8, pkg_name.?, "std") or mem.eql(u8, pkg_name.?, "root") or mem.eql(u8, pkg_name.?, "builtin")) {904 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.? });905 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| {906 } 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 });907 fatal("unable to add package '{s}' -> '{s}': already exists as '{s}", .{ pkg_name, pkg_path, prev.root_src_path });
904 }908 }
905 try cur_pkg.addAndAdopt(gpa, pkg_name.?, new_cur_pkg);909 try cur_pkg.addAndAdopt(gpa, pkg_name, new_cur_pkg);
906 cur_pkg = new_cur_pkg;910 cur_pkg = new_cur_pkg;
907 } else if (mem.eql(u8, arg, "--pkg-end")) {911 } else if (mem.eql(u8, arg, "--pkg-end")) {
908 cur_pkg = cur_pkg.parent orelse912 cur_pkg = cur_pkg.parent orelse
...@@ -2705,11 +2709,16 @@ fn buildOutputType(...@@ -2705,11 +2709,16 @@ fn buildOutputType(
2705 };2709 };
2706 defer emit_implib_resolved.deinit();2710 defer emit_implib_resolved.deinit();
27072711
2708 const main_pkg: ?*Package = if (root_src_file) |src_path| blk: {2712 const main_pkg: ?*Package = if (root_src_file) |unresolved_src_path| blk: {
2709 if (main_pkg_path) |p| {2713 const src_path = try introspect.resolvePath(arena, unresolved_src_path);
2710 const rel_src_path = try fs.path.relative(gpa, p, src_path);2714 if (main_pkg_path) |unresolved_main_pkg_path| {
2711 defer gpa.free(rel_src_path);2715 const p = try introspect.resolvePath(arena, unresolved_main_pkg_path);
2712 break :blk try Package.create(gpa, p, rel_src_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 }
2713 } else {2722 } else {
2714 const root_src_dir_path = fs.path.dirname(src_path);2723 const root_src_dir_path = fs.path.dirname(src_path);
2715 break :blk Package.create(gpa, root_src_dir_path, fs.path.basename(src_path)) catch |err| {2724 break :blk Package.create(gpa, root_src_dir_path, fs.path.basename(src_path)) catch |err| {
...@@ -2745,7 +2754,7 @@ fn buildOutputType(...@@ -2745,7 +2754,7 @@ fn buildOutputType(
27452754
2746 const self_exe_path = try introspect.findZigExePath(arena);2755 const self_exe_path = try introspect.findZigExePath(arena);
2747 var zig_lib_directory: Compilation.Directory = if (override_lib_dir) |unresolved_lib_dir| l: {2756 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);
2749 break :l .{2758 break :l .{
2750 .path = lib_dir,2759 .path = lib_dir,
2751 .handle = fs.cwd().openDir(lib_dir, .{}) catch |err| {2760 .handle = fs.cwd().openDir(lib_dir, .{}) catch |err| {
src/test.zig+1-1
...@@ -60,7 +60,7 @@ test {...@@ -60,7 +60,7 @@ test {
60 ctx.addTestCasesFromDir(dir);60 ctx.addTestCasesFromDir(dir);
61 }61 }
6262
63 try @import("test_cases").addCases(&ctx);63 try @import("../test/cases.zig").addCases(&ctx);
6464
65 try ctx.run();65 try ctx.run();
66}66}