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...@@ -860,21 +860,84 @@ fn testCompareDiskDesignators(expected_result: bool, kind: DiskDesignatorKind, p
860 try std.testing.expectEqual(expected_result, compareDiskDesignators(u16, kind, wtf16_buf1[0..w1_len], wtf16_buf2[0..w2_len]));860 try std.testing.expectEqual(expected_result, compareDiskDesignators(u16, kind, wtf16_buf1[0..w1_len], wtf16_buf2[0..w2_len]));
861}861}
862862
863/// On Windows, this calls `resolveWindows` and on POSIX it calls `resolvePosix`.863/// Deprecated in favor of `resolveAlloc`.
864pub fn resolve(allocator: Allocator, paths: []const []const u8) Allocator.Error![]u8 {864pub const resolve = resolveAlloc;
865 if (native_os == .windows) {865/// Deprecated in favor of `resolveAllocWindows`.
866 return resolveWindows(allocator, paths);866pub const resolveWindows = resolveAllocWindows;
867 } else {867/// Deprecated in favor of `resolveAllocPosix`.
868 return resolvePosix(allocator, paths);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),
869 }896 }
870}897}
871898
872/// This function is like a series of `cd` statements executed one after another.899/// Simulates a series of relative directory changes on a virtual filesystem that has no symlinks, returning the result
873/// It resolves "." and ".." to the best of its ability, but will not convert relative paths to900/// as an allocated slice.
874/// an absolute path, use Io.Dir.realpath instead.901///
875/// ".." components may persist in the resolved path if the resolved path is relative or drive-relative.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///
876/// Path separators are canonicalized to '\\' and drives are canonicalized to capital letters.936/// Path separators are canonicalized to '\\' and drives are canonicalized to capital letters.
877///937///
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///
878/// The result will not have a trailing path separator, except for the following scenarios:941/// The result will not have a trailing path separator, except for the following scenarios:
879/// - The resolved path is drive-absolute with no components (e.g. `C:\`).942/// - The resolved path is drive-absolute with no components (e.g. `C:\`).
880/// - The resolved path is a UNC path with only a server name, and the input path contained a trailing separator943/// - 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!...@@ -882,20 +945,22 @@ pub fn resolve(allocator: Allocator, paths: []const []const u8) Allocator.Error!
882/// - The resolved path is a UNC path with no components after the share name, and the input path contained a945/// - The resolved path is a UNC path with no components after the share name, and the input path contained a
883/// trailing separator (e.g. `\\server\share\`).946/// trailing separator (e.g. `\\server\share\`).
884///947///
885/// Each drive has its own current working directory, which is only resolved via the paths provided.948/// Each drive has its own current working directory, which is only resolved via the paths provided. In the scenario
886/// In the scenario that the resolved path contains a drive-relative path that can't be resolved using the paths alone,949/// that the resolved path contains a drive-relative path that can't be resolved using the paths alone, the result will
887/// the result will be a drive-relative path.950/// be a drive-relative path. Similarly, in the scenario that the resolved path contains a rooted path that can't be
888/// Similarly, in the scenario that the resolved path contains a rooted path that can't be resolved using the paths alone,951/// resolved using the paths alone, the result will be a rooted path.
889/// 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.
890///955///
891/// Note: all usage of this function should be audited due to the existence of symlinks.956/// See also:
892/// Without performing actual syscalls, resolving `..` could be incorrect.957/// * `resolveAppend`
893/// This API may break in the future: https://github.com/ziglang/zig/issues/13613958/// * `resolveAllocWindows`
894pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) Allocator.Error![]u8 {959pub fn resolveAppendWindows(gpa: Allocator, al: *std.ArrayList(u8), paths: []const []const u8) Allocator.Error!void {
895 // Avoid heap allocation when paths.len is <= @bitSizeOf(usize) * 2960 // Avoid heap allocation when paths.len is <= @bitSizeOf(usize) * 2
896 // (we use `* 3` because stackFallback uses 1 usize as a length)961 // (we use `* 3` because stackFallback uses 1 usize as a length)
897 var buf: [3]usize = undefined;962 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);
899 const bit_set_allocator = bit_set_allocator_state.allocator();964 const bit_set_allocator = bit_set_allocator_state.allocator();
900 var relevant_paths: std.bit_set.Dynamic = try .initEmpty(bit_set_allocator, paths.len);965 var relevant_paths: std.bit_set.Dynamic = try .initEmpty(bit_set_allocator, paths.len);
901 defer relevant_paths.deinit(bit_set_allocator);966 defer relevant_paths.deinit(bit_set_allocator);
...@@ -984,23 +1049,23 @@ pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) Allocator...@@ -984,23 +1049,23 @@ pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) Allocator
984 break :root last_effective_root_path;1049 break :root last_effective_root_path;
985 };1050 };
9861051
987 var result: std.ArrayList(u8) = .empty;1052 const prev_len = al.items.len;
988 defer result.deinit(allocator);1053 errdefer al.shrinkRetainingCapacity(prev_len);
9891054
990 var want_path_sep_between_root_and_component = false;1055 var want_path_sep_between_root_and_component = false;
991 switch (effective_root_path.kind) {1056 switch (effective_root_path.kind) {
992 .root_local_device, .local_device => {1057 .root_local_device, .local_device => {
993 try result.ensureUnusedCapacity(allocator, 3);1058 try al.ensureUnusedCapacity(gpa, 3);
994 result.appendSliceAssumeCapacity("\\\\");1059 al.appendSliceAssumeCapacity("\\\\");
995 result.appendAssumeCapacity(effective_root_path.root[2]); // . or ?1060 al.appendAssumeCapacity(effective_root_path.root[2]); // . or ?
996 want_path_sep_between_root_and_component = true;1061 want_path_sep_between_root_and_component = true;
997 },1062 },
998 .drive_absolute, .drive_relative => {1063 .drive_absolute, .drive_relative => {
999 try result.ensureUnusedCapacity(allocator, effective_root_path.root.len);1064 try al.ensureUnusedCapacity(gpa, effective_root_path.root.len);
1000 result.appendAssumeCapacity(std.ascii.toUpper(effective_root_path.root[0]));1065 al.appendAssumeCapacity(std.ascii.toUpper(effective_root_path.root[0]));
1001 result.appendAssumeCapacity(':');1066 al.appendAssumeCapacity(':');
1002 if (effective_root_path.kind == .drive_absolute) {1067 if (effective_root_path.kind == .drive_absolute) {
1003 result.appendAssumeCapacity('\\');1068 al.appendAssumeCapacity('\\');
1004 }1069 }
1005 },1070 },
1006 .unc_absolute => {1071 .unc_absolute => {
...@@ -1012,30 +1077,30 @@ pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) Allocator...@@ -1012,30 +1077,30 @@ pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) Allocator
1012 if (unc.sep_after_share) len += 1;1077 if (unc.sep_after_share) len += 1;
1013 break :len len;1078 break :len len;
1014 };1079 };
1015 try result.ensureUnusedCapacity(allocator, root_len);1080 try al.ensureUnusedCapacity(gpa, root_len);
1016 result.appendSliceAssumeCapacity("\\\\");1081 al.appendSliceAssumeCapacity("\\\\");
1017 if (unc.server.len > 0 or unc.sep_after_server) {1082 if (unc.server.len > 0 or unc.sep_after_server) {
1018 result.appendSliceAssumeCapacity(unc.server);1083 al.appendSliceAssumeCapacity(unc.server);
1019 if (unc.sep_after_server)1084 if (unc.sep_after_server)
1020 result.appendAssumeCapacity('\\')1085 al.appendAssumeCapacity('\\')
1021 else1086 else
1022 want_path_sep_between_root_and_component = true;1087 want_path_sep_between_root_and_component = true;
1023 }1088 }
1024 if (unc.share.len > 0) {1089 if (unc.share.len > 0) {
1025 result.appendSliceAssumeCapacity(unc.share);1090 al.appendSliceAssumeCapacity(unc.share);
1026 if (unc.sep_after_share)1091 if (unc.sep_after_share)
1027 result.appendAssumeCapacity('\\')1092 al.appendAssumeCapacity('\\')
1028 else1093 else
1029 want_path_sep_between_root_and_component = true;1094 want_path_sep_between_root_and_component = true;
1030 }1095 }
1031 },1096 },
1032 .rooted => {1097 .rooted => {
1033 try result.append(allocator, '\\');1098 try al.append(gpa, '\\');
1034 },1099 },
1035 .relative => {},1100 .relative => {},
1036 }1101 }
10371102
1038 const root_len = result.items.len;1103 const root_len = al.items.len - prev_len;
1039 var negative_count: usize = 0;1104 var negative_count: usize = 0;
1040 for (paths[first_path_i..], first_path_i..) |path, i| {1105 for (paths[first_path_i..], first_path_i..) |path, i| {
1041 if (!relevant_paths.isSet(i)) continue;1106 if (!relevant_paths.isSet(i)) continue;
...@@ -1047,69 +1112,90 @@ pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) Allocator...@@ -1047,69 +1112,90 @@ pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) Allocator
1047 if (mem.eql(u8, component, ".")) {1112 if (mem.eql(u8, component, ".")) {
1048 continue;1113 continue;
1049 } else if (mem.eql(u8, component, "..")) {1114 } 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 {
1051 negative_count += 1;1118 negative_count += 1;
1052 continue;1119 continue;
1053 }1120 }
1054 while (true) {1121 while ((al.items.len - prev_len) != root_len) {
1055 if (result.items.len == root_len) {1122 const end_with_sep = PathType.windows.isSep(u8, al.items[al.items.len - 1]);
1056 break;1123 al.items.len -= 1;
1057 }
1058 const end_with_sep = PathType.windows.isSep(u8, result.items[result.items.len - 1]);
1059 result.items.len -= 1;
1060 if (end_with_sep) break;1124 if (end_with_sep) break;
1061 }1125 }
1062 } else if (result.items.len == root_len and !want_path_sep_between_root_and_component) {1126 } else if ((al.items.len - prev_len) == root_len and !want_path_sep_between_root_and_component) {
1063 try result.appendSlice(allocator, component);1127 try al.appendSlice(gpa, component);
1064 } else {1128 } else {
1065 try result.ensureUnusedCapacity(allocator, 1 + component.len);1129 try al.ensureUnusedCapacity(gpa, 1 + component.len);
1066 result.appendAssumeCapacity('\\');1130 al.appendAssumeCapacity('\\');
1067 result.appendSliceAssumeCapacity(component);1131 al.appendSliceAssumeCapacity(component);
1068 }1132 }
1069 }1133 }
1070 }1134 }
10711135
1072 if (root_len != 0 and result.items.len == root_len and negative_count == 0) {1136 if (root_len != 0 and (al.items.len - prev_len) == root_len and negative_count == 0) return;
1073 return result.toOwnedSlice(allocator);
1074 }
10751137
1076 if (result.items.len == root_len) {1138 if ((al.items.len - prev_len) == root_len) {
1077 if (negative_count == 0) {1139 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;
1079 }1143 }
10801144
1081 try result.ensureTotalCapacityPrecise(allocator, 3 * negative_count - 1);1145 try al.ensureTotalCapacityPrecise(gpa, prev_len + 3 * negative_count - 1);
1082 for (0..negative_count - 1) |_| {1146 for (0..negative_count - 1) |_| {
1083 result.appendSliceAssumeCapacity("..\\");1147 al.appendSliceAssumeCapacity("..\\");
1084 }1148 }
1085 result.appendSliceAssumeCapacity("..");1149 al.appendSliceAssumeCapacity("..");
1086 } else {1150 } 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);
1088 for (0..negative_count) |i| {1152 for (0..negative_count) |i| {
1089 dest[i * 3 ..][0..3].* = "..\\".*;1153 dest[i * 3 ..][0..3].* = "..\\".*;
1090 }1154 }
1091 }1155 }
1092
1093 return result.toOwnedSlice(allocator);
1094}1156}
10951157
1096/// Simulates a series of relative directory changes on a virtual filesystem1158/// Simulates a series of relative directory changes on a virtual filesystem that has no symlinks, returning the result
1097/// that has no symlinks.1159/// as an allocated slice.
1098///1160///
1099/// "." and ".." are resolved but will not make relative paths absolute. ".."1161/// "." and ".." are resolved but will not make relative paths absolute. ".." components remain in the resolved path
1100/// components remain in the resolved path when the resolved path is relative1162/// when the resolved path is relative and there are not previous components to cancel out.
1101/// and there are not previous components to cancel out.
1102///1163///
1103/// The result does not have a trailing path separator.1164/// The result does not have a trailing path separator.
1104///1165///
1105/// This function does not perform any syscalls. Executing this series of path1166/// This function does not perform any syscalls. Executing this series of path lookups on an actual filesystem may
1106/// lookups on an actual filesystem may produce different results due to1167/// produce different results due to symlinks.
1107/// symlinks.1168///
1108pub fn resolvePosix(gpa: Allocator, paths: []const []const u8) Allocator.Error![]u8 {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 {
1109 assert(paths.len > 0);1195 assert(paths.len > 0);
11101196
1111 var result: std.ArrayList(u8) = .empty;1197 const prev_len = al.items.len;
1112 defer result.deinit(gpa);1198 errdefer al.shrinkRetainingCapacity(prev_len);
11131199
1114 var negative_count: usize = 0;1200 var negative_count: usize = 0;
1115 var is_abs = false;1201 var is_abs = false;
...@@ -1118,63 +1204,52 @@ pub fn resolvePosix(gpa: Allocator, paths: []const []const u8) Allocator.Error![...@@ -1118,63 +1204,52 @@ pub fn resolvePosix(gpa: Allocator, paths: []const []const u8) Allocator.Error![
1118 if (isAbsolutePosix(p)) {1204 if (isAbsolutePosix(p)) {
1119 is_abs = true;1205 is_abs = true;
1120 negative_count = 0;1206 negative_count = 0;
1121 result.clearRetainingCapacity();1207 al.shrinkRetainingCapacity(prev_len);
1122 }1208 }
1123 var it = mem.tokenizeScalar(u8, p, '/');1209 var it = mem.tokenizeScalar(u8, p, '/');
1124 while (it.next()) |component| {1210 while (it.next()) |component| {
1125 if (mem.eql(u8, component, ".")) {1211 if (mem.eql(u8, component, ".")) {
1126 continue;1212 continue;
1127 } else if (mem.eql(u8, component, "..")) {1213 } else if (mem.eql(u8, component, "..")) {
1128 if (result.items.len == 0) {1214 if ((al.items.len - prev_len) == 0) {
1129 negative_count += @intFromBool(!is_abs);1215 negative_count += @intFromBool(!is_abs);
1130 continue;1216 continue;
1131 }1217 }
1132 while (true) {1218 while (true) {
1133 const ends_with_slash = result.items[result.items.len - 1] == '/';1219 const ends_with_slash = al.items[al.items.len - 1] == '/';
1134 result.items.len -= 1;1220 al.items.len -= 1;
1135 if (ends_with_slash or result.items.len == 0) break;1221 if (ends_with_slash or (al.items.len - prev_len) == 0) break;
1136 }1222 }
1137 } else if (result.items.len > 0 or is_abs) {1223 } else if ((al.items.len - prev_len) > 0 or is_abs) {
1138 try result.ensureUnusedCapacity(gpa, 1 + component.len);1224 try al.ensureUnusedCapacity(gpa, 1 + component.len);
1139 result.appendAssumeCapacity('/');1225 al.appendAssumeCapacity('/');
1140 result.appendSliceAssumeCapacity(component);1226 al.appendSliceAssumeCapacity(component);
1141 } else {1227 } else {
1142 try result.appendSlice(gpa, component);1228 try al.appendSlice(gpa, component);
1143 }1229 }
1144 }1230 }
1145 }1231 }
11461232
1147 if (result.items.len == 0) {1233 if ((al.items.len - prev_len) == 0) {
1148 if (is_abs) {1234 if (is_abs) {
1149 return gpa.dupe(u8, "/");1235 try al.resize(gpa, prev_len + 1);
1150 }1236 al.items[al.items.len - 1] = '/';
1151 if (negative_count == 0) {1237 } else if (negative_count == 0) {
1152 return gpa.dupe(u8, ".");1238 try al.resize(gpa, prev_len + 1);
1239 al.items[al.items.len - 1] = '.';
1153 } else {1240 } else {
1154 const real_result = try gpa.alloc(u8, 3 * negative_count - 1);1241 try al.resize(gpa, prev_len + 3 * negative_count - 1);
1155 var count = negative_count - 1;1242 var count = negative_count - 1;
1156 var i: usize = 0;1243 var i: usize = prev_len;
1157 while (count > 0) : (count -= 1) {1244 while (count > 0) : (count -= 1) {
1158 real_result[i..][0..3].* = "../".*;1245 al.items[i..][0..3].* = "../".*;
1159 i += 3;1246 i += 3;
1160 }1247 }
1161 real_result[i..][0..2].* = "..".*;1248 al.items[i..][0..2].* = "..".*;
1162 return real_result;
1163 }1249 }
1164 }1250 } else if (negative_count != 0) {
11651251 const dest = try al.addManyAt(gpa, prev_len, 3 * negative_count);
1166 if (negative_count == 0) {1252 for (0..negative_count) |i| dest[i * 3 ..][0..3].* = "../".*;
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;
1178 }1253 }
1179}1254}
11801255
...@@ -1192,7 +1267,7 @@ test resolve {...@@ -1192,7 +1267,7 @@ test resolve {
1192 try testResolvePosix(&[_][]const u8{""}, ".");1267 try testResolvePosix(&[_][]const u8{""}, ".");
1193}1268}
11941269
1195test resolveWindows {1270test resolveAllocWindows {
1196 try testResolveWindows(1271 try testResolveWindows(
1197 &[_][]const u8{ "Z:\\", "/usr/local", "lib\\zig\\std\\array_list.zig" },1272 &[_][]const u8{ "Z:\\", "/usr/local", "lib\\zig\\std\\array_list.zig" },
1198 "Z:\\usr\\local\\lib\\zig\\std\\array_list.zig",1273 "Z:\\usr\\local\\lib\\zig\\std\\array_list.zig",
...@@ -1280,7 +1355,7 @@ test resolveWindows {...@@ -1280,7 +1355,7 @@ test resolveWindows {
1280 try testResolveWindows(&[_][]const u8{ "C:\\", "\\??\\C:\\foo", "bar" }, "C:\\??\\C:\\foo\\bar");1355 try testResolveWindows(&[_][]const u8{ "C:\\", "\\??\\C:\\foo", "bar" }, "C:\\??\\C:\\foo\\bar");
1281}1356}
12821357
1283test resolvePosix {1358test resolveAllocPosix {
1284 try testResolvePosix(&.{ "/a/b", "c" }, "/a/b/c");1359 try testResolvePosix(&.{ "/a/b", "c" }, "/a/b/c");
1285 try testResolvePosix(&.{ "/a/b", "c", "//d", "e///" }, "/d/e");1360 try testResolvePosix(&.{ "/a/b", "c", "//d", "e///" }, "/d/e");
1286 try testResolvePosix(&.{ "/a/b/c", "..", "../" }, "/a");1361 try testResolvePosix(&.{ "/a/b/c", "..", "../" }, "/a");
...@@ -1299,15 +1374,45 @@ test resolvePosix {...@@ -1299,15 +1374,45 @@ test resolvePosix {
1299}1374}
13001375
1301fn testResolveWindows(paths: []const []const u8, expected: []const u8) !void {1376fn testResolveWindows(paths: []const []const u8, expected: []const u8) !void {
1302 const actual = try resolveWindows(testing.allocator, paths);1377 {
1303 defer testing.allocator.free(actual);1378 const actual = try resolveAllocWindows(testing.allocator, paths);
1304 try testing.expectEqualStrings(expected, actual);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 }
1305}1395}
13061396
1307fn testResolvePosix(paths: []const []const u8, expected: []const u8) !void {1397fn testResolvePosix(paths: []const []const u8, expected: []const u8) !void {
1308 const actual = try resolvePosix(testing.allocator, paths);1398 {
1309 defer testing.allocator.free(actual);1399 const actual = try resolveAllocPosix(testing.allocator, paths);
1310 try testing.expectEqualStrings(expected, actual);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 }
1311}1416}
13121417
1313/// Strip the last component from a file path.1418/// Strip the last component from a file path.
...@@ -1498,17 +1603,18 @@ fn testBasenameWindows(input: []const u8, expected_output: []const u8) !void {...@@ -1498,17 +1603,18 @@ fn testBasenameWindows(input: []const u8, expected_output: []const u8) !void {
1498 try testing.expectEqualSlices(u8, expected_output, basenameWindows(input));1603 try testing.expectEqualSlices(u8, expected_output, basenameWindows(input));
1499}1604}
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.
1502///1609///
1503/// Other than memory allocation, this is a pure function; the result solely1610/// If `from` and `to` each resolve to the same path (after calling `resolve` on each), a zero-length string is
1504/// depends on the input parameters.1611/// returned.
1505///1612///
1506/// If `from` and `to` each resolve to the same path (after calling `resolve`1613/// See `relativePosix` and `relativeWindows` for operating system specific details and for how `environ_map` is used.
1507/// on each), a zero-length string is returned.
1508///1614///
1509/// See `relativePosix` and `relativeWindows` for operating system specific1615/// See also:
1510/// details and for how `environ_map` is used.1616/// * `relativeAppend`
1511pub fn relative(1617pub fn relativeAlloc(
1512 gpa: Allocator,1618 gpa: Allocator,
1513 cwd: []const u8,1619 cwd: []const u8,
1514 environ_map: ?*const std.process.Environ.Map,1620 environ_map: ?*const std.process.Environ.Map,
...@@ -1516,30 +1622,59 @@ pub fn relative(...@@ -1516,30 +1622,59 @@ pub fn relative(
1516 to: []const u8,1622 to: []const u8,
1517) Allocator.Error![]u8 {1623) Allocator.Error![]u8 {
1518 if (native_os == .windows) {1624 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);
1520 } else {1654 } else {
1521 return relativePosix(gpa, cwd, from, to);1655 return relativeAppendPosix(gpa, al, cwd, from, to);
1522 }1656 }
1523}1657}
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.
1526///1660///
1527/// Other than memory allocation, this is a pure function; the result solely1661/// Other than memory allocation, this is a pure function; the result solely depends on the input parameters.
1528/// depends on the input parameters.
1529///1662///
1530/// If `from` and `to` each resolve to the same path (after calling `resolve`1663/// If `from` and `to` each resolve to the same path (after calling `resolve` on each), a zero-length string is
1531/// on each), a zero-length string is returned.1664/// returned.
1532///1665///
1533/// The result is not guaranteed to be relative, as the paths may be on1666/// The result is not guaranteed to be relative, as the paths may be on different volumes. In that case, the result will
1534/// different volumes. In that case, the result will be the canonicalized1667/// be the canonicalized absolute path of `to`.
1535/// absolute path of `to`.
1536///1668///
1537/// Per-drive CWDs are stored in special semi-hidden environment variables of1669/// Per-drive CWDs are stored in special semi-hidden environment variables of the format `=<drive-letter>:`, e.g. `=C:`.
1538/// the format `=<drive-letter>:`, e.g. `=C:`. This type of CWD is purely a1670/// 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
1539/// shell concept, so there's no guarantee that it'll be set or that it'll even1671/// accurate. This is the only reason for the `environ_map` parameter. `null` is treated equivalent to the environment
1540/// be accurate. This is the only reason for the `environ_map` parameter. `null` is1672/// variable missing.
1541/// treated equivalent to the environment variable missing.1673///
1542pub fn relativeWindows(1674/// See also:
1675/// `relativeAlloc`
1676/// `relativeAppendWindows`
1677pub fn relativeAllocWindows(
1543 gpa: Allocator,1678 gpa: Allocator,
1544 cwd: []const u8,1679 cwd: []const u8,
1545 environ_map: ?*const std.process.Environ.Map,1680 environ_map: ?*const std.process.Environ.Map,
...@@ -1635,6 +1770,37 @@ pub fn relativeWindows(...@@ -1635,6 +1770,37 @@ pub fn relativeWindows(
1635 return [_]u8{};1770 return [_]u8{};
1636}1771}
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
1638fn windowsResolveAgainstCwd(1804fn windowsResolveAgainstCwd(
1639 gpa: Allocator,1805 gpa: Allocator,
1640 cwd: []const u8,1806 cwd: []const u8,
...@@ -1650,9 +1816,9 @@ fn windowsResolveAgainstCwd(...@@ -1650,9 +1816,9 @@ fn windowsResolveAgainstCwd(
1650 .unc_absolute,1816 .unc_absolute,
1651 .root_local_device,1817 .root_local_device,
1652 .local_device,1818 .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
1657 .rooted => blk: {1823 .rooted => blk: {
1658 const parsed_cwd = parsePathWindows(u8, cwd);1824 const parsed_cwd = parsePathWindows(u8, cwd);
...@@ -1660,13 +1826,13 @@ fn windowsResolveAgainstCwd(...@@ -1660,13 +1826,13 @@ fn windowsResolveAgainstCwd(
1660 .drive_absolute => {1826 .drive_absolute => {
1661 var drive_buf = "_:\\".*;1827 var drive_buf = "_:\\".*;
1662 drive_buf[0] = cwd[0];1828 drive_buf[0] = cwd[0];
1663 break :blk try resolveWindows(gpa, &.{ &drive_buf, path });1829 break :blk try resolveAllocWindows(gpa, &.{ &drive_buf, path });
1664 },1830 },
1665 .unc_absolute => {1831 .unc_absolute => {
1666 break :blk try resolveWindows(gpa, &.{ parsed_cwd.root, path });1832 break :blk try resolveAllocWindows(gpa, &.{ parsed_cwd.root, path });
1667 },1833 },
1668 // Effectively a malformed CWD, give up and just return a normalized path1834 // 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}),
1670 }1836 }
1671 },1837 },
1672 .drive_relative => blk: {1838 .drive_relative => blk: {
...@@ -1695,41 +1861,76 @@ fn windowsResolveAgainstCwd(...@@ -1695,41 +1861,76 @@ fn windowsResolveAgainstCwd(
1695 break :drive_cwd drive_buf;1861 break :drive_cwd drive_buf;
1696 };1862 };
1697 defer temp_allocator.free(drive_cwd);1863 defer temp_allocator.free(drive_cwd);
1698 break :blk try resolveWindows(gpa, &.{ drive_cwd, path });1864 break :blk try resolveAllocWindows(gpa, &.{ drive_cwd, path });
1699 },1865 },
1700 };1866 };
1701}1867}
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.
1704///1888///
1705/// Other than memory allocation, this is a pure function; the result solely1889/// Other than modifying the provided array list, this is a pure function; the result solely depends on the input
1706/// depends on the input parameters.1890/// parameters and it does no file system operations.
1707///1891///
1708/// If `from` and `to` each resolve to the same path (after calling `resolve`1892/// If `from` and `to` each resolve to the same path (after calling `resolveAppend` on each), nothing is appended to the
1709/// on each), a zero-length string is returned.1893/// array list.
1710///1894///
1711pub fn relativePosix(allocator: Allocator, cwd: []const u8, from: []const u8, to: []const u8) Allocator.Error![]u8 {1895/// See also:
1712 const resolved_from = try resolvePosix(allocator, &[_][]const u8{ cwd, from });1896/// * `relativeAppend`
1713 defer allocator.free(resolved_from);1897/// * `relativeAllocPosix`
1714 const resolved_to = try resolvePosix(allocator, &[_][]const u8{ cwd, to });1898pub fn relativeAppendPosix(
1715 defer allocator.free(resolved_to);1899 gpa: Allocator,
17161900 al: *std.ArrayList(u8),
1717 var from_it = mem.tokenizeScalar(u8, resolved_from, '/');1901 cwd: []const u8,
1718 var to_it = mem.tokenizeScalar(u8, resolved_to, '/');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], '/');
1719 while (true) {1918 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 };
1721 const to_rest = to_it.rest();1925 const to_rest = to_it.rest();
1722 if (to_it.next()) |to_component| {1926 if (to_it.next()) |to_component| {
1723 if (mem.eql(u8, from_component, to_component))1927 if (mem.eql(u8, from_component, to_component))
1724 continue;1928 continue;
1725 }1929 }
1726 var up_count: usize = 1;1930 var up_count: usize = 1;
1727 while (from_it.next()) |_| {1931 while (from_it.next()) |_| up_count += 1;
1728 up_count += 1;
1729 }
1730 const up_index_end = up_count * "../".len;1932 const up_index_end = up_count * "../".len;
1731 const result = try allocator.alloc(u8, up_index_end + to_rest.len);1933 const result = al.items[orig_len..];
1732 errdefer allocator.free(result);
17331934
1734 var result_index: usize = 0;1935 var result_index: usize = 0;
1735 while (result_index < up_index_end) {1936 while (result_index < up_index_end) {
...@@ -1737,15 +1938,16 @@ pub fn relativePosix(allocator: Allocator, cwd: []const u8, from: []const u8, to...@@ -1737,15 +1938,16 @@ pub fn relativePosix(allocator: Allocator, cwd: []const u8, from: []const u8, to
1737 result_index += 3;1938 result_index += 3;
1738 }1939 }
1739 if (to_rest.len == 0) {1940 if (to_rest.len == 0) {
1740 // shave off the trailing slash1941 // Shave off the trailing slash.
1741 return allocator.realloc(result, result_index - 1);1942 al.shrinkRetainingCapacity(orig_len + result_index - 1);
1943 return;
1742 }1944 }
17431945 @memmove(result[result_index..][0..to_rest.len], to_rest);
1744 @memcpy(result[result_index..][0..to_rest.len], to_rest);1946 al.shrinkRetainingCapacity(orig_len + result_index + to_rest.len);
1745 return result;1947 return;
1746 }1948 }
17471949
1748 return [_]u8{};1950 al.shrinkRetainingCapacity(orig_len);
1749}1951}
17501952
1751test relative {1953test relative {
...@@ -1803,15 +2005,44 @@ test relative {...@@ -1803,15 +2005,44 @@ test relative {
1803}2005}
18042006
1805fn testRelativePosix(from: []const u8, to: []const u8, expected_output: []const u8) !void {2007fn testRelativePosix(from: []const u8, to: []const u8, expected_output: []const u8) !void {
1806 const result = try relativePosix(testing.allocator, ".", from, to);2008 {
1807 defer testing.allocator.free(result);2009 const result = try relativePosix(testing.allocator, ".", from, to);
1808 try testing.expectEqualStrings(expected_output, result);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 }
1809}2026}
18102027
1811fn testRelativeWindows(from: []const u8, to: []const u8, expected_output: []const u8) !void {2028fn testRelativeWindows(from: []const u8, to: []const u8, expected_output: []const u8) !void {
1812 const result = try relativeWindows(testing.allocator, ".", null, from, to);2029 {
1813 defer testing.allocator.free(result);2030 const result = try relativeAllocWindows(testing.allocator, ".", null, from, to);
1814 try testing.expectEqualStrings(expected_output, result);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 }
1815}2046}
18162047
1817/// Searches for a file extension separated by a `.` and returns the string after that `.`.2048/// Searches for a file extension separated by a `.` and returns the string after that `.`.