Skip to content

Commit 043da82

Browse files
committed
Add std.fs.path.resolveRelative helper fn
`std.fs.path.resolveRelative` function is like `std.fs.path.resolve` however it resolves relative paths **only**, and they are always resolved to wrt ".". If the final path is resolved to "." or beyond, `null` is returned instead to signify this. Also, if an absolute path is passed as an argument, `ResolveRelativeError.AbsolutePath` is thrown. This function is particularly useful for Capsicum-like targets such as WASI. This commit also tweaks `std.cache_hash.CacheHash` to use either `std.fs.path.resolveat` or `std.fs.path.resolveRelative` (the latter used when targeting WASI). This way, we essentially got rid of any mention of CWD, and hence, it is now possible to use `CacheHash` struct in WASI. As a result, all tests in the said module have been enabled for WASI.
1 parent 93b1a8f commit 043da82

File tree

2 files changed

+147
-50
lines changed

2 files changed

+147
-50
lines changed

lib/std/cache_hash.zig

Lines changed: 54 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
const std = @import("std.zig");
1+
const builtin = @import("builtin");
2+
const std = @import("std");
23
const Blake3 = std.crypto.Blake3;
34
const fs = std.fs;
45
const base64 = std.base64;
56
const ArrayList = std.ArrayList;
67
const assert = std.debug.assert;
78
const testing = std.testing;
9+
const tmpDir = testing.tmpDir;
810
const mem = std.mem;
911
const fmt = std.fmt;
1012
const Allocator = std.mem.Allocator;
@@ -53,7 +55,6 @@ pub const CacheHash = struct {
5355
return CacheHash{
5456
.allocator = allocator,
5557
.blake3 = Blake3.init(),
56-
// TODO should claim ownership of `work_dir`?
5758
.work_dir = work_dir,
5859
.manifest_dir = try work_dir.makeOpenPath(manifest_dir_path, .{}),
5960
.manifest_file = null,
@@ -103,7 +104,13 @@ pub const CacheHash = struct {
103104
assert(self.manifest_file == null);
104105

105106
try self.files.ensureCapacity(self.files.items.len + 1);
106-
const resolved_path = try fs.path.resolveat(self.allocator, self.work_dir, &[_][]const u8{file_path});
107+
108+
var resolved_path: []u8 = undefined;
109+
if (builtin.os.tag == .wasi) {
110+
resolved_path = (try fs.path.resolveRelative(self.allocator, &[_][]const u8{file_path})) orelse unreachable;
111+
} else {
112+
resolved_path = try fs.path.resolveat(self.allocator, self.work_dir, &[_][]const u8{file_path});
113+
}
107114

108115
const idx = self.files.items.len;
109116
self.files.addOneAssumeCapacity().* = .{
@@ -325,7 +332,12 @@ pub const CacheHash = struct {
325332
pub fn addFilePostFetch(self: *CacheHash, file_path: []const u8, max_file_size: usize) ![]u8 {
326333
assert(self.manifest_file != null);
327334

328-
const resolved_path = try fs.path.resolveat(self.allocator, self.work_dir, &[_][]const u8{file_path});
335+
var resolved_path: []u8 = undefined;
336+
if (builtin.os.tag == .wasi) {
337+
resolved_path = (try fs.path.resolveRelative(self.allocator, &[_][]const u8{file_path})) orelse unreachable;
338+
} else {
339+
resolved_path = try fs.path.resolveat(self.allocator, self.work_dir, &[_][]const u8{file_path});
340+
}
329341
errdefer self.allocator.free(resolved_path);
330342

331343
const new_ch_file = try self.files.addOne();
@@ -350,7 +362,12 @@ pub const CacheHash = struct {
350362
pub fn addFilePost(self: *CacheHash, file_path: []const u8) !void {
351363
assert(self.manifest_file != null);
352364

353-
const resolved_path = try fs.path.resolveat(self.allocator, self.work_dir, &[_][]const u8{file_path});
365+
var resolved_path: []u8 = undefined;
366+
if (builtin.os.tag == .wasi) {
367+
resolved_path = (try fs.path.resolveRelative(self.allocator, &[_][]const u8{file_path})) orelse unreachable;
368+
} else {
369+
resolved_path = try fs.path.resolveat(self.allocator, self.work_dir, &[_][]const u8{file_path});
370+
}
354371
errdefer self.allocator.free(resolved_path);
355372

356373
const new_ch_file = try self.files.addOne();
@@ -473,16 +490,13 @@ fn isProblematicTimestamp(fs_clock: i128) bool {
473490
}
474491

475492
test "cache file and then recall it" {
476-
if (std.Target.current.os.tag == .wasi) {
477-
// https://github.com/ziglang/zig/issues/5437
478-
return error.SkipZigTest;
479-
}
480-
const cwd = fs.cwd();
493+
var tmp = tmpDir(.{});
494+
defer tmp.cleanup();
481495

482496
const temp_file = "test.txt";
483497
const temp_manifest_dir = "temp_manifest_dir";
484498

485-
try cwd.writeFile(temp_file, "Hello, world!\n");
499+
try tmp.dir.writeFile(temp_file, "Hello, world!\n");
486500

487501
while (isProblematicTimestamp(std.time.nanoTimestamp())) {
488502
std.time.sleep(1);
@@ -492,7 +506,7 @@ test "cache file and then recall it" {
492506
var digest2: [BASE64_DIGEST_LEN]u8 = undefined;
493507

494508
{
495-
var ch = try CacheHash.init(testing.allocator, cwd, temp_manifest_dir);
509+
var ch = try CacheHash.init(testing.allocator, tmp.dir, temp_manifest_dir);
496510
defer ch.release();
497511

498512
ch.add(true);
@@ -506,7 +520,7 @@ test "cache file and then recall it" {
506520
digest1 = ch.final();
507521
}
508522
{
509-
var ch = try CacheHash.init(testing.allocator, cwd, temp_manifest_dir);
523+
var ch = try CacheHash.init(testing.allocator, tmp.dir, temp_manifest_dir);
510524
defer ch.release();
511525

512526
ch.add(true);
@@ -520,8 +534,8 @@ test "cache file and then recall it" {
520534

521535
testing.expectEqual(digest1, digest2);
522536

523-
try cwd.deleteTree(temp_manifest_dir);
524-
try cwd.deleteFile(temp_file);
537+
try tmp.dir.deleteTree(temp_manifest_dir);
538+
try tmp.dir.deleteFile(temp_file);
525539
}
526540

527541
test "give problematic timestamp" {
@@ -537,18 +551,15 @@ test "give nonproblematic timestamp" {
537551
}
538552

539553
test "check that changing a file makes cache fail" {
540-
if (std.Target.current.os.tag == .wasi) {
541-
// https://github.com/ziglang/zig/issues/5437
542-
return error.SkipZigTest;
543-
}
544-
const cwd = fs.cwd();
554+
var tmp = tmpDir(.{});
555+
defer tmp.cleanup();
545556

546557
const temp_file = "cache_hash_change_file_test.txt";
547558
const temp_manifest_dir = "cache_hash_change_file_manifest_dir";
548559
const original_temp_file_contents = "Hello, world!\n";
549560
const updated_temp_file_contents = "Hello, world; but updated!\n";
550561

551-
try cwd.writeFile(temp_file, original_temp_file_contents);
562+
try tmp.dir.writeFile(temp_file, original_temp_file_contents);
552563

553564
while (isProblematicTimestamp(std.time.nanoTimestamp())) {
554565
std.time.sleep(1);
@@ -558,7 +569,7 @@ test "check that changing a file makes cache fail" {
558569
var digest2: [BASE64_DIGEST_LEN]u8 = undefined;
559570

560571
{
561-
var ch = try CacheHash.init(testing.allocator, cwd, temp_manifest_dir);
572+
var ch = try CacheHash.init(testing.allocator, tmp.dir, temp_manifest_dir);
562573
defer ch.release();
563574

564575
ch.add("1234");
@@ -572,14 +583,14 @@ test "check that changing a file makes cache fail" {
572583
digest1 = ch.final();
573584
}
574585

575-
try cwd.writeFile(temp_file, updated_temp_file_contents);
586+
try tmp.dir.writeFile(temp_file, updated_temp_file_contents);
576587

577588
while (isProblematicTimestamp(std.time.nanoTimestamp())) {
578589
std.time.sleep(1);
579590
}
580591

581592
{
582-
var ch = try CacheHash.init(testing.allocator, cwd, temp_manifest_dir);
593+
var ch = try CacheHash.init(testing.allocator, tmp.dir, temp_manifest_dir);
583594
defer ch.release();
584595

585596
ch.add("1234");
@@ -596,24 +607,22 @@ test "check that changing a file makes cache fail" {
596607

597608
testing.expect(!mem.eql(u8, digest1[0..], digest2[0..]));
598609

599-
try cwd.deleteTree(temp_manifest_dir);
600-
try cwd.deleteFile(temp_file);
610+
try tmp.dir.deleteTree(temp_manifest_dir);
611+
try tmp.dir.deleteFile(temp_file);
601612
}
602613

603614
test "no file inputs" {
604-
if (std.Target.current.os.tag == .wasi) {
605-
// https://github.com/ziglang/zig/issues/5437
606-
return error.SkipZigTest;
607-
}
608-
const cwd = fs.cwd();
615+
var tmp = tmpDir(.{});
616+
defer tmp.cleanup();
617+
609618
const temp_manifest_dir = "no_file_inputs_manifest_dir";
610-
defer cwd.deleteTree(temp_manifest_dir) catch unreachable;
619+
defer tmp.dir.deleteTree(temp_manifest_dir) catch unreachable;
611620

612621
var digest1: [BASE64_DIGEST_LEN]u8 = undefined;
613622
var digest2: [BASE64_DIGEST_LEN]u8 = undefined;
614623

615624
{
616-
var ch = try CacheHash.init(testing.allocator, cwd, temp_manifest_dir);
625+
var ch = try CacheHash.init(testing.allocator, tmp.dir, temp_manifest_dir);
617626
defer ch.release();
618627

619628
ch.add("1234");
@@ -624,7 +633,7 @@ test "no file inputs" {
624633
digest1 = ch.final();
625634
}
626635
{
627-
var ch = try CacheHash.init(testing.allocator, cwd, temp_manifest_dir);
636+
var ch = try CacheHash.init(testing.allocator, tmp.dir, temp_manifest_dir);
628637
defer ch.release();
629638

630639
ch.add("1234");
@@ -636,18 +645,15 @@ test "no file inputs" {
636645
}
637646

638647
test "CacheHashes with files added after initial hash work" {
639-
if (std.Target.current.os.tag == .wasi) {
640-
// https://github.com/ziglang/zig/issues/5437
641-
return error.SkipZigTest;
642-
}
643-
const cwd = fs.cwd();
648+
var tmp = tmpDir(.{});
649+
defer tmp.cleanup();
644650

645651
const temp_file1 = "cache_hash_post_file_test1.txt";
646652
const temp_file2 = "cache_hash_post_file_test2.txt";
647653
const temp_manifest_dir = "cache_hash_post_file_manifest_dir";
648654

649-
try cwd.writeFile(temp_file1, "Hello, world!\n");
650-
try cwd.writeFile(temp_file2, "Hello world the second!\n");
655+
try tmp.dir.writeFile(temp_file1, "Hello, world!\n");
656+
try tmp.dir.writeFile(temp_file2, "Hello world the second!\n");
651657

652658
while (isProblematicTimestamp(std.time.nanoTimestamp())) {
653659
std.time.sleep(1);
@@ -658,7 +664,7 @@ test "CacheHashes with files added after initial hash work" {
658664
var digest3: [BASE64_DIGEST_LEN]u8 = undefined;
659665

660666
{
661-
var ch = try CacheHash.init(testing.allocator, cwd, temp_manifest_dir);
667+
var ch = try CacheHash.init(testing.allocator, tmp.dir, temp_manifest_dir);
662668
defer ch.release();
663669

664670
ch.add("1234");
@@ -672,7 +678,7 @@ test "CacheHashes with files added after initial hash work" {
672678
digest1 = ch.final();
673679
}
674680
{
675-
var ch = try CacheHash.init(testing.allocator, cwd, temp_manifest_dir);
681+
var ch = try CacheHash.init(testing.allocator, tmp.dir, temp_manifest_dir);
676682
defer ch.release();
677683

678684
ch.add("1234");
@@ -683,14 +689,14 @@ test "CacheHashes with files added after initial hash work" {
683689
testing.expect(mem.eql(u8, &digest1, &digest2));
684690

685691
// Modify the file added after initial hash
686-
try cwd.writeFile(temp_file2, "Hello world the second, updated\n");
692+
try tmp.dir.writeFile(temp_file2, "Hello world the second, updated\n");
687693

688694
while (isProblematicTimestamp(std.time.nanoTimestamp())) {
689695
std.time.sleep(1);
690696
}
691697

692698
{
693-
var ch = try CacheHash.init(testing.allocator, cwd, temp_manifest_dir);
699+
var ch = try CacheHash.init(testing.allocator, tmp.dir, temp_manifest_dir);
694700
defer ch.release();
695701

696702
ch.add("1234");
@@ -706,7 +712,7 @@ test "CacheHashes with files added after initial hash work" {
706712

707713
testing.expect(!mem.eql(u8, &digest1, &digest3));
708714

709-
try cwd.deleteTree(temp_manifest_dir);
710-
try cwd.deleteFile(temp_file1);
711-
try cwd.deleteFile(temp_file2);
715+
try tmp.dir.deleteTree(temp_manifest_dir);
716+
try tmp.dir.deleteFile(temp_file1);
717+
try tmp.dir.deleteFile(temp_file2);
712718
}

lib/std/fs/path.zig

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -652,10 +652,62 @@ pub fn resolvePosix(allocator: *Allocator, paths: []const []const u8) ![]u8 {
652652
return allocator.shrink(result, result_index);
653653
}
654654

655+
pub const ResolveRelativeError = error{AbsolutePath} || Allocator.Error;
656+
657+
/// This function is like a series of `cd` statements executed one after another.
658+
/// It resolves "." and ".." relative to ".".
659+
/// The result does not have a trailing path separator.
660+
/// It does not dereference symlinks, and always returns a relative path. If the
661+
/// resolution leads to ".", returns null instead.
662+
pub fn resolveRelative(allocator: *Allocator, paths: []const []const u8) ResolveRelativeError!?[]u8 {
663+
var max_size: usize = 0;
664+
for (paths) |p, i| {
665+
if (isAbsolute(p)) {
666+
return ResolveRelativeError.AbsolutePath;
667+
}
668+
max_size += p.len + 1;
669+
}
670+
671+
var result_index: usize = 0;
672+
var result = try allocator.alloc(u8, max_size);
673+
errdefer allocator.free(result);
674+
675+
for (paths) |p, i| {
676+
var it = mem.tokenize(p, "/");
677+
while (it.next()) |component| {
678+
if (mem.eql(u8, component, ".")) {
679+
continue;
680+
} else if (mem.eql(u8, component, "..")) {
681+
while (true) {
682+
if (result_index == 0)
683+
break;
684+
result_index -= 1;
685+
if (result[result_index] == '/')
686+
break;
687+
}
688+
} else {
689+
if (result_index > 0) {
690+
result[result_index] = '/';
691+
result_index += 1;
692+
}
693+
mem.copy(u8, result[result_index..], component);
694+
result_index += component.len;
695+
}
696+
}
697+
}
698+
699+
if (result_index == 0) {
700+
allocator.free(result);
701+
return null;
702+
}
703+
704+
return allocator.shrink(result, result_index);
705+
}
706+
655707
/// Similar to `resolve`, however, resolves the paths with respect to the provided
656708
/// `Dir` handle.
657-
/// This function, unlike `resolve`, will perform one syscall to get the path of
658-
/// the specified `dir` handle.
709+
/// Unlike `resolve`, will perform one syscall to get the path of the
710+
/// specified `dir` handle.
659711
pub fn resolveat(allocator: *Allocator, dir: fs.Dir, paths: []const []const u8) ![]u8 {
660712
var buffer: [fs.MAX_PATH_BYTES]u8 = undefined;
661713
const dir_path = try std.os.realpathat(dir.fd, "", &buffer);
@@ -760,6 +812,45 @@ fn testResolvePosix(paths: []const []const u8, expected: []const u8) !void {
760812
return testing.expect(mem.eql(u8, actual, expected));
761813
}
762814

815+
test "resolveRelative" {
816+
try testResolveRelative(&[_][]const u8{ "a/b", "c" }, "a/b/c");
817+
try testResolveRelative(&[_][]const u8{ "a/b", "./../" }, "a");
818+
try testResolveRelative(&[_][]const u8{ "a/b", "..", ".." }, null);
819+
try testResolveRelative(&[_][]const u8{"./"}, null);
820+
try testResolveRelative(&[_][]const u8{ "..", "../" }, null);
821+
try testResolveRelative(&[_][]const u8{ "./a", "/" }, ResolveRelativeError.AbsolutePath);
822+
}
823+
824+
fn testResolveRelative(paths: []const []const u8, expected: var) !void {
825+
const res = resolveRelative(testing.allocator, paths);
826+
827+
if (@typeInfo(@TypeOf(expected)) == .ErrorSet) {
828+
return testing.expectError(expected, res);
829+
}
830+
831+
const actual = try res;
832+
defer if (actual) |a| testing.allocator.free(a);
833+
834+
return switch (@typeInfo(@TypeOf(expected))) {
835+
.Null => testing.expectEqual(actual, expected),
836+
else => |_| testing.expect(mem.eql(u8, actual.?, expected)),
837+
};
838+
}
839+
840+
test "resolveat" {
841+
if (builtin.os.tag == .wasi) return error.SkipZigTest;
842+
843+
const cwd = fs.cwd();
844+
845+
const a = try resolveat(testing.allocator, cwd, &[_][]const u8{});
846+
defer testing.allocator.free(a);
847+
848+
const b = try resolve(testing.allocator, &[_][]const u8{});
849+
defer testing.allocator.free(b);
850+
851+
testing.expect(mem.eql(u8, a, b));
852+
}
853+
763854
/// If the path is a file in the current directory (no directory component)
764855
/// then returns null
765856
pub fn dirname(path: []const u8) ?[]const u8 {

0 commit comments

Comments
 (0)