authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-09-03 15:48:00-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-09-08 20:45:22+02:00
log9db2a97c0d9d807502b4a0cf206230f6ed92b73b
tree5ebdacfb3c5c9265e627b8e8314f1d1d95424047
parent5e36170b5ff1cb7710aa9b1c5540b7d11aa62537

std.fs.path: append variants for resolve and relative

* introduce resolveAppend variants * introduce relativeAppend variants * deprecated resolve in favor of resolveAlloc * deprecated relative in favor of relativeAlloc * flesh out the documentation more * rewrite alloc variants in terms of append variants

1 files changed, 405 insertions(+), 174 deletions(-)

lib/std/fs/path.zig+405-174
......@@ -860,21 +860,84 @@ fn testCompareDiskDesignators(expected_result: bool, kind: DiskDesignatorKind, p
860860 try std.testing.expectEqual(expected_result, compareDiskDesignators(u16, kind, wtf16_buf1[0..w1_len], wtf16_buf2[0..w2_len]));
861861}
862862
863/// On Windows, this calls `resolveWindows` and on POSIX it calls `resolvePosix`.
864pub fn resolve(allocator: Allocator, paths: []const []const u8) Allocator.Error![]u8 {
865 if (native_os == .windows) {
866 return resolveWindows(allocator, paths);
867 } else {
868 return resolvePosix(allocator, paths);
863/// Deprecated in favor of `resolveAlloc`.
864pub const resolve = resolveAlloc;
865/// Deprecated in favor of `resolveAllocWindows`.
866pub const resolveWindows = resolveAllocWindows;
867/// Deprecated in favor of `resolveAllocPosix`.
868pub const resolvePosix = resolveAllocPosix;
869
870/// Deprecated in favor of `relativeAlloc`.
871pub const relative = relativeAlloc;
872/// Deprecated in favor of `relativeAllocPosix`.
873pub const relativePosix = relativeAllocPosix;
874/// Deprecated in favor of `relativeAllocWindows`.
875pub const relativeWindows = relativeAllocWindows;
876
877/// On Windows, calls `resolveAllocWindows`; otherwise calls `resolveAllocPosix`.
878///
879/// See also:
880/// `resolveAppend`
881pub fn resolveAlloc(gpa: Allocator, paths: []const []const u8) Allocator.Error![]u8 {
882 switch (native_os) {
883 .windows => return resolveAllocWindows(gpa, paths),
884 else => return resolveAllocPosix(gpa, paths),
885 }
886}
887
888/// On Windows, calls `resolveAppendWindows`; otherwise calls `resolveAppendPosix`.
889///
890/// See also:
891/// `resolveAlloc`
892pub fn resolveAppend(gpa: Allocator, al: *std.ArrayList(u8), paths: []const []const u8) Allocator.Error!void {
893 switch (native_os) {
894 .windows => return resolveAppendWindows(gpa, al, paths),
895 else => return resolveAppendPosix(gpa, al, paths),
869896 }
870897}
871898
872/// This function is like a series of `cd` statements executed one after another.
873/// It resolves "." and ".." to the best of its ability, but will not convert relative paths to
874/// an absolute path, use Io.Dir.realpath instead.
875/// ".." components may persist in the resolved path if the resolved path is relative or drive-relative.
899/// Simulates a series of relative directory changes on a virtual filesystem that has no symlinks, returning the result
900/// as an allocated slice.
901///
902/// Path separators are canonicalized to '\\' and drives are canonicalized to capital letters.
903///
904/// "." and ".." are resolved but will not convert relative paths to an absolute path. ".." components may persist in
905/// the resolved path if the resolved path is relative or drive-relative.
906///
907/// The result will not have a trailing path separator, except for the following scenarios:
908/// - The resolved path is drive-absolute with no components (e.g. `C:\`).
909/// - The resolved path is a UNC path with only a server name, and the input path contained a trailing separator
910/// (e.g. `\\server\`).
911/// - The resolved path is a UNC path with no components after the share name, and the input path contained a
912/// trailing separator (e.g. `\\server\share\`).
913///
914/// Each drive has its own current working directory, which is only resolved via the paths provided. In the scenario
915/// that the resolved path contains a drive-relative path that can't be resolved using the paths alone, the result will
916/// be a drive-relative path. Similarly, in the scenario that the resolved path contains a rooted path that can't be
917/// resolved using the paths alone, the result will be a rooted path.
918///
919/// This function does not perform any syscalls. Executing this series of path lookups on an actual filesystem may
920/// produce different results due to symlinks.
921///
922/// See also:
923/// * `resolveAlloc`
924/// * `resolveAppendWindows`
925pub fn resolveAllocWindows(gpa: Allocator, paths: []const []const u8) Allocator.Error![]u8 {
926 var buffer: std.ArrayList(u8) = .empty;
927 defer buffer.deinit(gpa);
928 try resolveAppendWindows(gpa, &buffer, paths);
929 try buffer.shrinkToLen(gpa);
930 return buffer.toOwnedSliceAssert();
931}
932
933/// Simulates a series of relative directory changes on a virtual filesystem that has no symlinks, appending the result
934/// to the provided array list.
935///
876936/// Path separators are canonicalized to '\\' and drives are canonicalized to capital letters.
877937///
938/// "." and ".." are resolved but will not convert relative paths to an absolute path. ".." components may persist in
939/// the resolved path if the resolved path is relative or drive-relative.
940///
878941/// The result will not have a trailing path separator, except for the following scenarios:
879942/// - The resolved path is drive-absolute with no components (e.g. `C:\`).
880943/// - The resolved path is a UNC path with only a server name, and the input path contained a trailing separator
......@@ -882,20 +945,22 @@ pub fn resolve(allocator: Allocator, paths: []const []const u8) Allocator.Error!
882945/// - The resolved path is a UNC path with no components after the share name, and the input path contained a
883946/// trailing separator (e.g. `\\server\share\`).
884947///
885/// Each drive has its own current working directory, which is only resolved via the paths provided.
886/// In the scenario that the resolved path contains a drive-relative path that can't be resolved using the paths alone,
887/// the result will be a drive-relative path.
888/// Similarly, in the scenario that the resolved path contains a rooted path that can't be resolved using the paths alone,
889/// the result will be a rooted path.
948/// Each drive has its own current working directory, which is only resolved via the paths provided. In the scenario
949/// that the resolved path contains a drive-relative path that can't be resolved using the paths alone, the result will
950/// be a drive-relative path. Similarly, in the scenario that the resolved path contains a rooted path that can't be
951/// resolved using the paths alone, the result will be a rooted path.
952///
953/// This function does not perform any syscalls. Executing this series of path lookups on an actual filesystem may
954/// produce different results due to symlinks.
890955///
891/// Note: all usage of this function should be audited due to the existence of symlinks.
892/// Without performing actual syscalls, resolving `..` could be incorrect.
893/// This API may break in the future: https://github.com/ziglang/zig/issues/13613
894pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) Allocator.Error![]u8 {
956/// See also:
957/// * `resolveAppend`
958/// * `resolveAllocWindows`
959pub fn resolveAppendWindows(gpa: Allocator, al: *std.ArrayList(u8), paths: []const []const u8) Allocator.Error!void {
895960 // Avoid heap allocation when paths.len is <= @bitSizeOf(usize) * 2
896961 // (we use `* 3` because stackFallback uses 1 usize as a length)
897962 var buf: [3]usize = undefined;
898 var bit_set_allocator_state: std.heap.BufferFirstAllocator = .init(@ptrCast(&buf), allocator);
963 var bit_set_allocator_state: std.heap.BufferFirstAllocator = .init(@ptrCast(&buf), gpa);
899964 const bit_set_allocator = bit_set_allocator_state.allocator();
900965 var relevant_paths: std.bit_set.Dynamic = try .initEmpty(bit_set_allocator, paths.len);
901966 defer relevant_paths.deinit(bit_set_allocator);
......@@ -984,23 +1049,23 @@ pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) Allocator
9841049 break :root last_effective_root_path;
9851050 };
9861051
987 var result: std.ArrayList(u8) = .empty;
988 defer result.deinit(allocator);
1052 const prev_len = al.items.len;
1053 errdefer al.shrinkRetainingCapacity(prev_len);
9891054
9901055 var want_path_sep_between_root_and_component = false;
9911056 switch (effective_root_path.kind) {
9921057 .root_local_device, .local_device => {
993 try result.ensureUnusedCapacity(allocator, 3);
994 result.appendSliceAssumeCapacity("\\\\");
995 result.appendAssumeCapacity(effective_root_path.root[2]); // . or ?
1058 try al.ensureUnusedCapacity(gpa, 3);
1059 al.appendSliceAssumeCapacity("\\\\");
1060 al.appendAssumeCapacity(effective_root_path.root[2]); // . or ?
9961061 want_path_sep_between_root_and_component = true;
9971062 },
9981063 .drive_absolute, .drive_relative => {
999 try result.ensureUnusedCapacity(allocator, effective_root_path.root.len);
1000 result.appendAssumeCapacity(std.ascii.toUpper(effective_root_path.root[0]));
1001 result.appendAssumeCapacity(':');
1064 try al.ensureUnusedCapacity(gpa, effective_root_path.root.len);
1065 al.appendAssumeCapacity(std.ascii.toUpper(effective_root_path.root[0]));
1066 al.appendAssumeCapacity(':');
10021067 if (effective_root_path.kind == .drive_absolute) {
1003 result.appendAssumeCapacity('\\');
1068 al.appendAssumeCapacity('\\');
10041069 }
10051070 },
10061071 .unc_absolute => {
......@@ -1012,30 +1077,30 @@ pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) Allocator
10121077 if (unc.sep_after_share) len += 1;
10131078 break :len len;
10141079 };
1015 try result.ensureUnusedCapacity(allocator, root_len);
1016 result.appendSliceAssumeCapacity("\\\\");
1080 try al.ensureUnusedCapacity(gpa, root_len);
1081 al.appendSliceAssumeCapacity("\\\\");
10171082 if (unc.server.len > 0 or unc.sep_after_server) {
1018 result.appendSliceAssumeCapacity(unc.server);
1083 al.appendSliceAssumeCapacity(unc.server);
10191084 if (unc.sep_after_server)
1020 result.appendAssumeCapacity('\\')
1085 al.appendAssumeCapacity('\\')
10211086 else
10221087 want_path_sep_between_root_and_component = true;
10231088 }
10241089 if (unc.share.len > 0) {
1025 result.appendSliceAssumeCapacity(unc.share);
1090 al.appendSliceAssumeCapacity(unc.share);
10261091 if (unc.sep_after_share)
1027 result.appendAssumeCapacity('\\')
1092 al.appendAssumeCapacity('\\')
10281093 else
10291094 want_path_sep_between_root_and_component = true;
10301095 }
10311096 },
10321097 .rooted => {
1033 try result.append(allocator, '\\');
1098 try al.append(gpa, '\\');
10341099 },
10351100 .relative => {},
10361101 }
10371102
1038 const root_len = result.items.len;
1103 const root_len = al.items.len - prev_len;
10391104 var negative_count: usize = 0;
10401105 for (paths[first_path_i..], first_path_i..) |path, i| {
10411106 if (!relevant_paths.isSet(i)) continue;
......@@ -1047,69 +1112,90 @@ pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) Allocator
10471112 if (mem.eql(u8, component, ".")) {
10481113 continue;
10491114 } else if (mem.eql(u8, component, "..")) {
1050 if (result.items.len == 0 or (result.items.len == root_len and effective_root_path.kind == .drive_relative)) {
1115 if ((al.items.len - prev_len) == 0 or
1116 ((al.items.len - prev_len) == root_len and effective_root_path.kind == .drive_relative))
1117 {
10511118 negative_count += 1;
10521119 continue;
10531120 }
1054 while (true) {
1055 if (result.items.len == root_len) {
1056 break;
1057 }
1058 const end_with_sep = PathType.windows.isSep(u8, result.items[result.items.len - 1]);
1059 result.items.len -= 1;
1121 while ((al.items.len - prev_len) != root_len) {
1122 const end_with_sep = PathType.windows.isSep(u8, al.items[al.items.len - 1]);
1123 al.items.len -= 1;
10601124 if (end_with_sep) break;
10611125 }
1062 } else if (result.items.len == root_len and !want_path_sep_between_root_and_component) {
1063 try result.appendSlice(allocator, component);
1126 } else if ((al.items.len - prev_len) == root_len and !want_path_sep_between_root_and_component) {
1127 try al.appendSlice(gpa, component);
10641128 } else {
1065 try result.ensureUnusedCapacity(allocator, 1 + component.len);
1066 result.appendAssumeCapacity('\\');
1067 result.appendSliceAssumeCapacity(component);
1129 try al.ensureUnusedCapacity(gpa, 1 + component.len);
1130 al.appendAssumeCapacity('\\');
1131 al.appendSliceAssumeCapacity(component);
10681132 }
10691133 }
10701134 }
10711135
1072 if (root_len != 0 and result.items.len == root_len and negative_count == 0) {
1073 return result.toOwnedSlice(allocator);
1074 }
1136 if (root_len != 0 and (al.items.len - prev_len) == root_len and negative_count == 0) return;
10751137
1076 if (result.items.len == root_len) {
1138 if ((al.items.len - prev_len) == root_len) {
10771139 if (negative_count == 0) {
1078 return allocator.dupe(u8, ".");
1140 try al.resize(gpa, prev_len + 1);
1141 al.items[al.items.len - 1] = '.';
1142 return;
10791143 }
10801144
1081 try result.ensureTotalCapacityPrecise(allocator, 3 * negative_count - 1);
1145 try al.ensureTotalCapacityPrecise(gpa, prev_len + 3 * negative_count - 1);
10821146 for (0..negative_count - 1) |_| {
1083 result.appendSliceAssumeCapacity("..\\");
1147 al.appendSliceAssumeCapacity("..\\");
10841148 }
1085 result.appendSliceAssumeCapacity("..");
1149 al.appendSliceAssumeCapacity("..");
10861150 } else {
1087 const dest = try result.addManyAt(allocator, root_len, 3 * negative_count);
1151 const dest = try al.addManyAt(gpa, prev_len + root_len, 3 * negative_count);
10881152 for (0..negative_count) |i| {
10891153 dest[i * 3 ..][0..3].* = "..\\".*;
10901154 }
10911155 }
1092
1093 return result.toOwnedSlice(allocator);
10941156}
10951157
1096/// Simulates a series of relative directory changes on a virtual filesystem
1097/// that has no symlinks.
1158/// Simulates a series of relative directory changes on a virtual filesystem that has no symlinks, returning the result
1159/// as an allocated slice.
10981160///
1099/// "." and ".." are resolved but will not make relative paths absolute. ".."
1100/// components remain in the resolved path when the resolved path is relative
1101/// and there are not previous components to cancel out.
1161/// "." and ".." are resolved but will not make relative paths absolute. ".." components remain in the resolved path
1162/// when the resolved path is relative and there are not previous components to cancel out.
11021163///
11031164/// The result does not have a trailing path separator.
11041165///
1105/// This function does not perform any syscalls. Executing this series of path
1106/// lookups on an actual filesystem may produce different results due to
1107/// symlinks.
1108pub fn resolvePosix(gpa: Allocator, paths: []const []const u8) Allocator.Error![]u8 {
1166/// This function does not perform any syscalls. Executing this series of path lookups on an actual filesystem may
1167/// produce different results due to symlinks.
1168///
1169/// See also:
1170/// * `resolveAlloc`
1171/// * `resolveAppendPosix`
1172pub fn resolveAllocPosix(gpa: Allocator, paths: []const []const u8) Allocator.Error![]u8 {
1173 var buffer: std.ArrayList(u8) = .empty;
1174 defer buffer.deinit(gpa);
1175 try resolveAppendPosix(gpa, &buffer, paths);
1176 try buffer.shrinkToLen(gpa);
1177 return buffer.toOwnedSliceAssert();
1178}
1179
1180/// Simulates a series of relative directory changes on a virtual filesystem that has no symlinks, appending the result
1181/// to the provided array list.
1182///
1183/// "." and ".." are resolved but will not make relative paths absolute. ".." components remain in the resolved path
1184/// when the resolved path is relative and there are not previous components to cancel out.
1185///
1186/// The result does not have a trailing path separator.
1187///
1188/// This function does not perform any syscalls. Executing this series of path lookups on an actual filesystem may
1189/// produce different results due to symlinks.
1190///
1191/// See also:
1192/// * `resolveAppend`
1193/// * `resolveAllocPosix`
1194pub fn resolveAppendPosix(gpa: Allocator, al: *std.ArrayList(u8), paths: []const []const u8) Allocator.Error!void {
11091195 assert(paths.len > 0);
11101196
1111 var result: std.ArrayList(u8) = .empty;
1112 defer result.deinit(gpa);
1197 const prev_len = al.items.len;
1198 errdefer al.shrinkRetainingCapacity(prev_len);
11131199
11141200 var negative_count: usize = 0;
11151201 var is_abs = false;
......@@ -1118,63 +1204,52 @@ pub fn resolvePosix(gpa: Allocator, paths: []const []const u8) Allocator.Error![
11181204 if (isAbsolutePosix(p)) {
11191205 is_abs = true;
11201206 negative_count = 0;
1121 result.clearRetainingCapacity();
1207 al.shrinkRetainingCapacity(prev_len);
11221208 }
11231209 var it = mem.tokenizeScalar(u8, p, '/');
11241210 while (it.next()) |component| {
11251211 if (mem.eql(u8, component, ".")) {
11261212 continue;
11271213 } else if (mem.eql(u8, component, "..")) {
1128 if (result.items.len == 0) {
1214 if ((al.items.len - prev_len) == 0) {
11291215 negative_count += @intFromBool(!is_abs);
11301216 continue;
11311217 }
11321218 while (true) {
1133 const ends_with_slash = result.items[result.items.len - 1] == '/';
1134 result.items.len -= 1;
1135 if (ends_with_slash or result.items.len == 0) break;
1219 const ends_with_slash = al.items[al.items.len - 1] == '/';
1220 al.items.len -= 1;
1221 if (ends_with_slash or (al.items.len - prev_len) == 0) break;
11361222 }
1137 } else if (result.items.len > 0 or is_abs) {
1138 try result.ensureUnusedCapacity(gpa, 1 + component.len);
1139 result.appendAssumeCapacity('/');
1140 result.appendSliceAssumeCapacity(component);
1223 } else if ((al.items.len - prev_len) > 0 or is_abs) {
1224 try al.ensureUnusedCapacity(gpa, 1 + component.len);
1225 al.appendAssumeCapacity('/');
1226 al.appendSliceAssumeCapacity(component);
11411227 } else {
1142 try result.appendSlice(gpa, component);
1228 try al.appendSlice(gpa, component);
11431229 }
11441230 }
11451231 }
11461232
1147 if (result.items.len == 0) {
1233 if ((al.items.len - prev_len) == 0) {
11481234 if (is_abs) {
1149 return gpa.dupe(u8, "/");
1150 }
1151 if (negative_count == 0) {
1152 return gpa.dupe(u8, ".");
1235 try al.resize(gpa, prev_len + 1);
1236 al.items[al.items.len - 1] = '/';
1237 } else if (negative_count == 0) {
1238 try al.resize(gpa, prev_len + 1);
1239 al.items[al.items.len - 1] = '.';
11531240 } else {
1154 const real_result = try gpa.alloc(u8, 3 * negative_count - 1);
1241 try al.resize(gpa, prev_len + 3 * negative_count - 1);
11551242 var count = negative_count - 1;
1156 var i: usize = 0;
1243 var i: usize = prev_len;
11571244 while (count > 0) : (count -= 1) {
1158 real_result[i..][0..3].* = "../".*;
1245 al.items[i..][0..3].* = "../".*;
11591246 i += 3;
11601247 }
1161 real_result[i..][0..2].* = "..".*;
1162 return real_result;
1248 al.items[i..][0..2].* = "..".*;
11631249 }
1164 }
1165
1166 if (negative_count == 0) {
1167 return result.toOwnedSlice(gpa);
1168 } else {
1169 const real_result = try gpa.alloc(u8, 3 * negative_count + result.items.len);
1170 var count = negative_count;
1171 var i: usize = 0;
1172 while (count > 0) : (count -= 1) {
1173 real_result[i..][0..3].* = "../".*;
1174 i += 3;
1175 }
1176 @memcpy(real_result[i..][0..result.items.len], result.items);
1177 return real_result;
1250 } else if (negative_count != 0) {
1251 const dest = try al.addManyAt(gpa, prev_len, 3 * negative_count);
1252 for (0..negative_count) |i| dest[i * 3 ..][0..3].* = "../".*;
11781253 }
11791254}
11801255
......@@ -1192,7 +1267,7 @@ test resolve {
11921267 try testResolvePosix(&[_][]const u8{""}, ".");
11931268}
11941269
1195test resolveWindows {
1270test resolveAllocWindows {
11961271 try testResolveWindows(
11971272 &[_][]const u8{ "Z:\\", "/usr/local", "lib\\zig\\std\\array_list.zig" },
11981273 "Z:\\usr\\local\\lib\\zig\\std\\array_list.zig",
......@@ -1280,7 +1355,7 @@ test resolveWindows {
12801355 try testResolveWindows(&[_][]const u8{ "C:\\", "\\??\\C:\\foo", "bar" }, "C:\\??\\C:\\foo\\bar");
12811356}
12821357
1283test resolvePosix {
1358test resolveAllocPosix {
12841359 try testResolvePosix(&.{ "/a/b", "c" }, "/a/b/c");
12851360 try testResolvePosix(&.{ "/a/b", "c", "//d", "e///" }, "/d/e");
12861361 try testResolvePosix(&.{ "/a/b/c", "..", "../" }, "/a");
......@@ -1299,15 +1374,45 @@ test resolvePosix {
12991374}
13001375
13011376fn testResolveWindows(paths: []const []const u8, expected: []const u8) !void {
1302 const actual = try resolveWindows(testing.allocator, paths);
1303 defer testing.allocator.free(actual);
1304 try testing.expectEqualStrings(expected, actual);
1377 {
1378 const actual = try resolveAllocWindows(testing.allocator, paths);
1379 defer testing.allocator.free(actual);
1380 try testing.expectEqualStrings(expected, actual);
1381 }
1382
1383 {
1384 var buffer: std.ArrayList(u8) = .empty;
1385 defer buffer.deinit(testing.allocator);
1386 try buffer.append(testing.allocator, 99);
1387
1388 try resolveAppendWindows(testing.allocator, &buffer, paths);
1389 // Test that it preserved the previous contents of the array list.
1390 try testing.expectEqual(expected.len + 1, buffer.items.len);
1391 try testing.expectEqual(99, buffer.items[0]);
1392 const result = buffer.items[1..];
1393 try testing.expectEqualStrings(expected, result);
1394 }
13051395}
13061396
13071397fn testResolvePosix(paths: []const []const u8, expected: []const u8) !void {
1308 const actual = try resolvePosix(testing.allocator, paths);
1309 defer testing.allocator.free(actual);
1310 try testing.expectEqualStrings(expected, actual);
1398 {
1399 const actual = try resolveAllocPosix(testing.allocator, paths);
1400 defer testing.allocator.free(actual);
1401 try testing.expectEqualStrings(expected, actual);
1402 }
1403
1404 {
1405 var buffer: std.ArrayList(u8) = .empty;
1406 defer buffer.deinit(testing.allocator);
1407 try buffer.append(testing.allocator, 99);
1408
1409 try resolveAppendPosix(testing.allocator, &buffer, paths);
1410 // Test that it preserved the previous contents of the array list.
1411 try testing.expectEqual(expected.len + 1, buffer.items.len);
1412 try testing.expectEqual(99, buffer.items[0]);
1413 const result = buffer.items[1..];
1414 try testing.expectEqualStrings(expected, result);
1415 }
13111416}
13121417
13131418/// Strip the last component from a file path.
......@@ -1498,17 +1603,18 @@ fn testBasenameWindows(input: []const u8, expected_output: []const u8) !void {
14981603 try testing.expectEqualSlices(u8, expected_output, basenameWindows(input));
14991604}
15001605
1501/// Returns the non-absolute path from `from` to `to`.
1606/// Returns the non-absolute path from `from` to `to` as an allocated slice.
1607///
1608/// Other than memory allocation, this is a pure function; the result solely depends on the input parameters.
15021609///
1503/// Other than memory allocation, this is a pure function; the result solely
1504/// depends on the input parameters.
1610/// If `from` and `to` each resolve to the same path (after calling `resolve` on each), a zero-length string is
1611/// returned.
15051612///
1506/// If `from` and `to` each resolve to the same path (after calling `resolve`
1507/// on each), a zero-length string is returned.
1613/// See `relativePosix` and `relativeWindows` for operating system specific details and for how `environ_map` is used.
15081614///
1509/// See `relativePosix` and `relativeWindows` for operating system specific
1510/// details and for how `environ_map` is used.
1511pub fn relative(
1615/// See also:
1616/// * `relativeAppend`
1617pub fn relativeAlloc(
15121618 gpa: Allocator,
15131619 cwd: []const u8,
15141620 environ_map: ?*const std.process.Environ.Map,
......@@ -1516,30 +1622,59 @@ pub fn relative(
15161622 to: []const u8,
15171623) Allocator.Error![]u8 {
15181624 if (native_os == .windows) {
1519 return relativeWindows(gpa, cwd, environ_map, from, to);
1625 return relativeAllocWindows(gpa, cwd, environ_map, from, to);
1626 } else {
1627 return relativeAllocPosix(gpa, cwd, from, to);
1628 }
1629}
1630
1631/// Appends the non-absolute path from `from` to `to` to the provided array list.
1632///
1633/// Other than modifying the provided array list, this is a pure function; the result solely depends on the input
1634/// parameters and no file system operations are performed.
1635///
1636/// If `from` and `to` each resolve to the same path (after calling `resolveAppend` on each), a zero-length string is
1637/// returned.
1638///
1639/// See `relativeAppendPosix` and `relativeAppendWindows` for operating system specific details and for how
1640/// `environ_map` is used.
1641///
1642/// See also:
1643/// * `relativeAlloc`
1644pub fn relativeAppend(
1645 gpa: Allocator,
1646 al: *std.ArrayList(u8),
1647 cwd: []const u8,
1648 environ_map: ?*const std.process.Environ.Map,
1649 from: []const u8,
1650 to: []const u8,
1651) Allocator.Error!void {
1652 if (native_os == .windows) {
1653 return relativeAppendWindows(gpa, al, cwd, environ_map, from, to);
15201654 } else {
1521 return relativePosix(gpa, cwd, from, to);
1655 return relativeAppendPosix(gpa, al, cwd, from, to);
15221656 }
15231657}
15241658
1525/// Returns the non-absolute path from `from` to `to` according to Windows rules.
1659/// Returns the non-absolute path from `from` to `to` according to Windows rules as an allocated slice.
15261660///
1527/// Other than memory allocation, this is a pure function; the result solely
1528/// depends on the input parameters.
1661/// Other than memory allocation, this is a pure function; the result solely depends on the input parameters.
15291662///
1530/// If `from` and `to` each resolve to the same path (after calling `resolve`
1531/// on each), a zero-length string is returned.
1663/// If `from` and `to` each resolve to the same path (after calling `resolve` on each), a zero-length string is
1664/// returned.
15321665///
1533/// The result is not guaranteed to be relative, as the paths may be on
1534/// different volumes. In that case, the result will be the canonicalized
1535/// absolute path of `to`.
1666/// The result is not guaranteed to be relative, as the paths may be on different volumes. In that case, the result will
1667/// be the canonicalized absolute path of `to`.
15361668///
1537/// Per-drive CWDs are stored in special semi-hidden environment variables of
1538/// the format `=<drive-letter>:`, e.g. `=C:`. This type of CWD is purely a
1539/// shell concept, so there's no guarantee that it'll be set or that it'll even
1540/// be accurate. This is the only reason for the `environ_map` parameter. `null` is
1541/// treated equivalent to the environment variable missing.
1542pub fn relativeWindows(
1669/// Per-drive CWDs are stored in special semi-hidden environment variables of the format `=<drive-letter>:`, e.g. `=C:`.
1670/// This type of CWD is purely a shell concept, so there's no guarantee that it'll be set or that it'll even be
1671/// accurate. This is the only reason for the `environ_map` parameter. `null` is treated equivalent to the environment
1672/// variable missing.
1673///
1674/// See also:
1675/// `relativeAlloc`
1676/// `relativeAppendWindows`
1677pub fn relativeAllocWindows(
15431678 gpa: Allocator,
15441679 cwd: []const u8,
15451680 environ_map: ?*const std.process.Environ.Map,
......@@ -1635,6 +1770,37 @@ pub fn relativeWindows(
16351770 return [_]u8{};
16361771}
16371772
1773/// Appends the non-absolute path from `from` to `to` according to Windows rules to the provided array list.
1774///
1775/// Other than memory allocation, this is a pure function; the result solely depends on the input parameters.
1776///
1777/// If `from` and `to` each resolve to the same path (after calling `resolve` on each), a zero-length string is
1778/// returned.
1779///
1780/// The result is not guaranteed to be relative, as the paths may be on different volumes. In that case, the result will
1781/// be the canonicalized absolute path of `to`.
1782///
1783/// Per-drive CWDs are stored in special semi-hidden environment variables of the format `=<drive-letter>:`, e.g. `=C:`.
1784/// This type of CWD is purely a shell concept, so there's no guarantee that it'll be set or that it'll even be
1785/// accurate. This is the only reason for the `environ_map` parameter. `null` is treated equivalent to the environment
1786/// variable missing.
1787///
1788/// See also:
1789/// `relativeAppend`
1790/// `relativeAllocWindows`
1791pub fn relativeAppendWindows(
1792 gpa: Allocator,
1793 al: *std.ArrayList(u8),
1794 cwd: []const u8,
1795 environ_map: ?*const std.process.Environ.Map,
1796 from: []const u8,
1797 to: []const u8,
1798) Allocator.Error!void {
1799 const result = try relativeAllocWindows(gpa, cwd, environ_map, from, to);
1800 defer gpa.free(result);
1801 try al.appendSlice(gpa, result);
1802}
1803
16381804fn windowsResolveAgainstCwd(
16391805 gpa: Allocator,
16401806 cwd: []const u8,
......@@ -1650,9 +1816,9 @@ fn windowsResolveAgainstCwd(
16501816 .unc_absolute,
16511817 .root_local_device,
16521818 .local_device,
1653 => try resolveWindows(gpa, &.{path}),
1819 => try resolveAllocWindows(gpa, &.{path}),
16541820
1655 .relative => try resolveWindows(gpa, &.{ cwd, path }),
1821 .relative => try resolveAllocWindows(gpa, &.{ cwd, path }),
16561822
16571823 .rooted => blk: {
16581824 const parsed_cwd = parsePathWindows(u8, cwd);
......@@ -1660,13 +1826,13 @@ fn windowsResolveAgainstCwd(
16601826 .drive_absolute => {
16611827 var drive_buf = "_:\\".*;
16621828 drive_buf[0] = cwd[0];
1663 break :blk try resolveWindows(gpa, &.{ &drive_buf, path });
1829 break :blk try resolveAllocWindows(gpa, &.{ &drive_buf, path });
16641830 },
16651831 .unc_absolute => {
1666 break :blk try resolveWindows(gpa, &.{ parsed_cwd.root, path });
1832 break :blk try resolveAllocWindows(gpa, &.{ parsed_cwd.root, path });
16671833 },
16681834 // Effectively a malformed CWD, give up and just return a normalized path
1669 else => break :blk try resolveWindows(gpa, &.{path}),
1835 else => break :blk try resolveAllocWindows(gpa, &.{path}),
16701836 }
16711837 },
16721838 .drive_relative => blk: {
......@@ -1695,41 +1861,76 @@ fn windowsResolveAgainstCwd(
16951861 break :drive_cwd drive_buf;
16961862 };
16971863 defer temp_allocator.free(drive_cwd);
1698 break :blk try resolveWindows(gpa, &.{ drive_cwd, path });
1864 break :blk try resolveAllocWindows(gpa, &.{ drive_cwd, path });
16991865 },
17001866 };
17011867}
17021868
1703/// Returns the non-absolute path from `from` to `to` according to Windows rules.
1869/// Returns the non-absolute path from `from` to `to` according to POSIX rules as an allocated slice.
1870///
1871/// Other than memory allocation, this is a pure function; the result solely depends on the input parameters.
1872///
1873/// If `from` and `to` each resolve to the same path (after calling `resolve` on each), a zero-length string is
1874/// returned.
1875///
1876/// See also:
1877/// * `relativeAlloc`
1878/// * `relativeAppendPosix`
1879pub fn relativeAllocPosix(gpa: Allocator, cwd: []const u8, from: []const u8, to: []const u8) Allocator.Error![]u8 {
1880 var buffer: std.ArrayList(u8) = .empty;
1881 defer buffer.deinit(gpa);
1882 try relativeAppendPosix(gpa, &buffer, cwd, from, to);
1883 try buffer.shrinkToLen(gpa);
1884 return buffer.toOwnedSliceAssert();
1885}
1886
1887/// Appends the non-absolute path from `from` to `to` according to POSIX rules to the provided array list.
17041888///
1705/// Other than memory allocation, this is a pure function; the result solely
1706/// depends on the input parameters.
1889/// Other than modifying the provided array list, this is a pure function; the result solely depends on the input
1890/// parameters and it does no file system operations.
17071891///
1708/// If `from` and `to` each resolve to the same path (after calling `resolve`
1709/// on each), a zero-length string is returned.
1892/// If `from` and `to` each resolve to the same path (after calling `resolveAppend` on each), nothing is appended to the
1893/// array list.
17101894///
1711pub fn relativePosix(allocator: Allocator, cwd: []const u8, from: []const u8, to: []const u8) Allocator.Error![]u8 {
1712 const resolved_from = try resolvePosix(allocator, &[_][]const u8{ cwd, from });
1713 defer allocator.free(resolved_from);
1714 const resolved_to = try resolvePosix(allocator, &[_][]const u8{ cwd, to });
1715 defer allocator.free(resolved_to);
1716
1717 var from_it = mem.tokenizeScalar(u8, resolved_from, '/');
1718 var to_it = mem.tokenizeScalar(u8, resolved_to, '/');
1895/// See also:
1896/// * `relativeAppend`
1897/// * `relativeAllocPosix`
1898pub fn relativeAppendPosix(
1899 gpa: Allocator,
1900 al: *std.ArrayList(u8),
1901 cwd: []const u8,
1902 from: []const u8,
1903 to: []const u8,
1904) Allocator.Error!void {
1905 const orig_len = al.items.len;
1906 errdefer al.items.len = orig_len;
1907
1908 const resolved_from_start = al.items.len;
1909 try resolveAppendPosix(gpa, al, &.{ cwd, from });
1910 const resolved_from_end = al.items.len;
1911
1912 const resolved_to_start = al.items.len;
1913 try resolveAppendPosix(gpa, al, &.{ cwd, to });
1914 const resolved_to_end = al.items.len;
1915
1916 var from_it = mem.tokenizeScalar(u8, al.items[resolved_from_start..resolved_from_end], '/');
1917 var to_it = mem.tokenizeScalar(u8, al.items[resolved_to_start..resolved_to_end], '/');
17191918 while (true) {
1720 const from_component = from_it.next() orelse return allocator.dupe(u8, to_it.rest());
1919 const from_component = from_it.next() orelse {
1920 const result = to_it.rest();
1921 @memmove(al.items[orig_len..][0..result.len], result);
1922 al.shrinkRetainingCapacity(orig_len + result.len);
1923 return;
1924 };
17211925 const to_rest = to_it.rest();
17221926 if (to_it.next()) |to_component| {
17231927 if (mem.eql(u8, from_component, to_component))
17241928 continue;
17251929 }
17261930 var up_count: usize = 1;
1727 while (from_it.next()) |_| {
1728 up_count += 1;
1729 }
1931 while (from_it.next()) |_| up_count += 1;
17301932 const up_index_end = up_count * "../".len;
1731 const result = try allocator.alloc(u8, up_index_end + to_rest.len);
1732 errdefer allocator.free(result);
1933 const result = al.items[orig_len..];
17331934
17341935 var result_index: usize = 0;
17351936 while (result_index < up_index_end) {
......@@ -1737,15 +1938,16 @@ pub fn relativePosix(allocator: Allocator, cwd: []const u8, from: []const u8, to
17371938 result_index += 3;
17381939 }
17391940 if (to_rest.len == 0) {
1740 // shave off the trailing slash
1741 return allocator.realloc(result, result_index - 1);
1941 // Shave off the trailing slash.
1942 al.shrinkRetainingCapacity(orig_len + result_index - 1);
1943 return;
17421944 }
1743
1744 @memcpy(result[result_index..][0..to_rest.len], to_rest);
1745 return result;
1945 @memmove(result[result_index..][0..to_rest.len], to_rest);
1946 al.shrinkRetainingCapacity(orig_len + result_index + to_rest.len);
1947 return;
17461948 }
17471949
1748 return [_]u8{};
1950 al.shrinkRetainingCapacity(orig_len);
17491951}
17501952
17511953test relative {
......@@ -1803,15 +2005,44 @@ test relative {
18032005}
18042006
18052007fn testRelativePosix(from: []const u8, to: []const u8, expected_output: []const u8) !void {
1806 const result = try relativePosix(testing.allocator, ".", from, to);
1807 defer testing.allocator.free(result);
1808 try testing.expectEqualStrings(expected_output, result);
2008 {
2009 const result = try relativePosix(testing.allocator, ".", from, to);
2010 defer testing.allocator.free(result);
2011 try testing.expectEqualStrings(expected_output, result);
2012 }
2013
2014 {
2015 var buffer: std.ArrayList(u8) = .empty;
2016 defer buffer.deinit(testing.allocator);
2017 try buffer.append(testing.allocator, 99);
2018
2019 try relativeAppendPosix(testing.allocator, &buffer, ".", from, to);
2020 // Test that it preserved the previous contents of the array list.
2021 try testing.expectEqual(expected_output.len + 1, buffer.items.len);
2022 try testing.expectEqual(99, buffer.items[0]);
2023 const result = buffer.items[1..];
2024 try testing.expectEqualStrings(expected_output, result);
2025 }
18092026}
18102027
18112028fn testRelativeWindows(from: []const u8, to: []const u8, expected_output: []const u8) !void {
1812 const result = try relativeWindows(testing.allocator, ".", null, from, to);
1813 defer testing.allocator.free(result);
1814 try testing.expectEqualStrings(expected_output, result);
2029 {
2030 const result = try relativeAllocWindows(testing.allocator, ".", null, from, to);
2031 defer testing.allocator.free(result);
2032 try testing.expectEqualStrings(expected_output, result);
2033 }
2034 {
2035 var buffer: std.ArrayList(u8) = .empty;
2036 defer buffer.deinit(testing.allocator);
2037 try buffer.append(testing.allocator, 99);
2038
2039 try relativeAppendWindows(testing.allocator, &buffer, ".", null, from, to);
2040 // Test that it preserved the previous contents of the array list.
2041 try testing.expectEqual(expected_output.len + 1, buffer.items.len);
2042 try testing.expectEqual(99, buffer.items[0]);
2043 const result = buffer.items[1..];
2044 try testing.expectEqualStrings(expected_output, result);
2045 }
18152046}
18162047
18172048/// Searches for a file extension separated by a `.` and returns the string after that `.`.