1const builtin = @import("builtin"); 2const std = @import("../std.zig"); 3const debug = std.debug; 4const assert = debug.assert; 5const testing = std.testing; 6const mem = std.mem; 7const fmt = std.fmt; 8const Allocator = mem.Allocator; 9const math = std.math; 10const windows = std.os.windows; 11const fs = std.fs; 12const process = std.process; 13const native_os = builtin.target.os.tag; 14 15pub const sep_windows = '\\'; 16pub const sep_posix = '/'; 17pub const sep = if (native_os == .windows) sep_windows else sep_posix; 18 19pub const sep_str_windows = "\\"; 20pub const sep_str_posix = "/"; 21pub const sep_str = if (native_os == .windows) sep_str_windows else sep_str_posix; 22 23pub const delimiter_windows = ';'; 24pub const delimiter_posix = ':'; 25pub const delimiter = if (native_os == .windows) delimiter_windows else delimiter_posix; 26 27/// Returns if the given byte is a valid path separator 28pub fn isSep(byte: u8) bool { 29 if (native_os == .windows) { 30 return byte == '/' or byte == '\\'; 31 } else { 32 return byte == '/'; 33 } 34} 35 36/// This is different from mem.join in that the separator will not be repeated if 37/// it is found at the end or beginning of a pair of consecutive paths. 38fn joinSepMaybeZ(allocator: Allocator, separator: u8, sepPredicate: fn (u8) bool, paths: []const []const u8, zero: bool) ![]u8 { 39 if (paths.len == 0) return if (zero) try allocator.dupe(u8, &[1]u8{0}) else &[0]u8{}; 40 41 // Find first non-empty path index. 42 const first_path_index = blk: { 43 for (paths) |path, index| { 44 if (path.len == 0) continue else break :blk index; 45 } 46 47 // All paths provided were empty, so return early. 48 return if (zero) try allocator.dupe(u8, &[1]u8{0}) else &[0]u8{}; 49 }; 50 51 // Calculate length needed for resulting joined path buffer. 52 const total_len = blk: { 53 var sum: usize = paths[first_path_index].len; 54 var prev_path = paths[first_path_index]; 55 assert(prev_path.len > 0); 56 var i: usize = first_path_index + 1; 57 while (i < paths.len) : (i += 1) { 58 const this_path = paths[i]; 59 if (this_path.len == 0) continue; 60 const prev_sep = sepPredicate(prev_path[prev_path.len - 1]); 61 const this_sep = sepPredicate(this_path[0]); 62 sum += @boolToInt(!prev_sep and !this_sep); 63 sum += if (prev_sep and this_sep) this_path.len - 1 else this_path.len; 64 prev_path = this_path; 65 } 66 67 if (zero) sum += 1; 68 break :blk sum; 69 }; 70 71 const buf = try allocator.alloc(u8, total_len); 72 errdefer allocator.free(buf); 73 74 mem.copy(u8, buf, paths[first_path_index]); 75 var buf_index: usize = paths[first_path_index].len; 76 var prev_path = paths[first_path_index]; 77 assert(prev_path.len > 0); 78 var i: usize = first_path_index + 1; 79 while (i < paths.len) : (i += 1) { 80 const this_path = paths[i]; 81 if (this_path.len == 0) continue; 82 const prev_sep = sepPredicate(prev_path[prev_path.len - 1]); 83 const this_sep = sepPredicate(this_path[0]); 84 if (!prev_sep and !this_sep) { 85 buf[buf_index] = separator; 86 buf_index += 1; 87 } 88 const adjusted_path = if (prev_sep and this_sep) this_path[1..] else this_path; 89 mem.copy(u8, buf[buf_index..], adjusted_path); 90 buf_index += adjusted_path.len; 91 prev_path = this_path; 92 } 93 94 if (zero) buf[buf.len - 1] = 0; 95 96 // No need for shrink since buf is exactly the correct size. 97 return buf; 98} 99 100/// Naively combines a series of paths with the native path seperator. 101/// Allocates memory for the result, which must be freed by the caller. 102pub fn join(allocator: Allocator, paths: []const []const u8) ![]u8 { 103 return joinSepMaybeZ(allocator, sep, isSep, paths, false); 104} 105 106/// Naively combines a series of paths with the native path seperator and null terminator. 107/// Allocates memory for the result, which must be freed by the caller. 108pub fn joinZ(allocator: Allocator, paths: []const []const u8) ![:0]u8 { 109 const out = try joinSepMaybeZ(allocator, sep, isSep, paths, true); 110 return out[0 .. out.len - 1 :0]; 111} 112 113fn testJoinMaybeZWindows(paths: []const []const u8, expected: []const u8, zero: bool) !void { 114 const windowsIsSep = struct { 115 fn isSep(byte: u8) bool { 116 return byte == '/' or byte == '\\'; 117 } 118 }.isSep; 119 const actual = try joinSepMaybeZ(testing.allocator, sep_windows, windowsIsSep, paths, zero); 120 defer testing.allocator.free(actual); 121 try testing.expectEqualSlices(u8, expected, if (zero) actual[0 .. actual.len - 1 :0] else actual); 122} 123 124fn testJoinMaybeZPosix(paths: []const []const u8, expected: []const u8, zero: bool) !void { 125 const posixIsSep = struct { 126 fn isSep(byte: u8) bool { 127 return byte == '/'; 128 } 129 }.isSep; 130 const actual = try joinSepMaybeZ(testing.allocator, sep_posix, posixIsSep, paths, zero); 131 defer testing.allocator.free(actual); 132 try testing.expectEqualSlices(u8, expected, if (zero) actual[0 .. actual.len - 1 :0] else actual); 133} 134 135test "join" { 136 { 137 const actual: []u8 = try join(testing.allocator, &[_][]const u8{}); 138 defer testing.allocator.free(actual); 139 try testing.expectEqualSlices(u8, "", actual); 140 } 141 { 142 const actual: [:0]u8 = try joinZ(testing.allocator, &[_][]const u8{}); 143 defer testing.allocator.free(actual); 144 try testing.expectEqualSlices(u8, "", actual); 145 } 146 for (&[_]bool{ false, true }) |zero| { 147 try testJoinMaybeZWindows(&[_][]const u8{}, "", zero); 148 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\b", "c" }, "c:\\a\\b\\c", zero); 149 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\b", "c" }, "c:\\a\\b\\c", zero); 150 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\b\\", "c" }, "c:\\a\\b\\c", zero); 151 152 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\", "a", "b\\", "c" }, "c:\\a\\b\\c", zero); 153 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\", "b\\", "c" }, "c:\\a\\b\\c", zero); 154 155 try testJoinMaybeZWindows( 156 &[_][]const u8{ "c:\\home\\andy\\dev\\zig\\build\\lib\\zig\\std", "io.zig" }, 157 "c:\\home\\andy\\dev\\zig\\build\\lib\\zig\\std\\io.zig", 158 zero, 159 ); 160 161 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\", "a", "b/", "c" }, "c:\\a\\b/c", zero); 162 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a/", "b\\", "/c" }, "c:\\a/b\\c", zero); 163 164 try testJoinMaybeZWindows(&[_][]const u8{ "", "c:\\", "", "", "a", "b\\", "c", "" }, "c:\\a\\b\\c", zero); 165 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a/", "", "b\\", "", "/c" }, "c:\\a/b\\c", zero); 166 try testJoinMaybeZWindows(&[_][]const u8{ "", "" }, "", zero); 167 168 try testJoinMaybeZPosix(&[_][]const u8{}, "", zero); 169 try testJoinMaybeZPosix(&[_][]const u8{ "/a/b", "c" }, "/a/b/c", zero); 170 try testJoinMaybeZPosix(&[_][]const u8{ "/a/b/", "c" }, "/a/b/c", zero); 171 172 try testJoinMaybeZPosix(&[_][]const u8{ "/", "a", "b/", "c" }, "/a/b/c", zero); 173 try testJoinMaybeZPosix(&[_][]const u8{ "/a/", "b/", "c" }, "/a/b/c", zero); 174 175 try testJoinMaybeZPosix( 176 &[_][]const u8{ "/home/andy/dev/zig/build/lib/zig/std", "io.zig" }, 177 "/home/andy/dev/zig/build/lib/zig/std/io.zig", 178 zero, 179 ); 180 181 try testJoinMaybeZPosix(&[_][]const u8{ "a", "/c" }, "a/c", zero); 182 try testJoinMaybeZPosix(&[_][]const u8{ "a/", "/c" }, "a/c", zero); 183 184 try testJoinMaybeZPosix(&[_][]const u8{ "", "/", "a", "", "b/", "c", "" }, "/a/b/c", zero); 185 try testJoinMaybeZPosix(&[_][]const u8{ "/a/", "", "", "b/", "c" }, "/a/b/c", zero); 186 try testJoinMaybeZPosix(&[_][]const u8{ "", "" }, "", zero); 187 } 188} 189 190pub fn isAbsoluteZ(path_c: [*:0]const u8) bool { 191 if (native_os == .windows) { 192 return isAbsoluteWindowsZ(path_c); 193 } else { 194 return isAbsolutePosixZ(path_c); 195 } 196} 197 198pub fn isAbsolute(path: []const u8) bool { 199 if (native_os == .windows) { 200 return isAbsoluteWindows(path); 201 } else { 202 return isAbsolutePosix(path); 203 } 204} 205 206fn isAbsoluteWindowsImpl(comptime T: type, path: []const T) bool { 207 if (path.len < 1) 208 return false; 209 210 if (path[0] == '/') 211 return true; 212 213 if (path[0] == '\\') 214 return true; 215 216 if (path.len < 3) 217 return false; 218 219 if (path[1] == ':') { 220 if (path[2] == '/') 221 return true; 222 if (path[2] == '\\') 223 return true; 224 } 225 226 return false; 227} 228 229pub fn isAbsoluteWindows(path: []const u8) bool { 230 return isAbsoluteWindowsImpl(u8, path); 231} 232 233pub fn isAbsoluteWindowsW(path_w: [*:0]const u16) bool { 234 return isAbsoluteWindowsImpl(u16, mem.sliceTo(path_w, 0)); 235} 236 237pub fn isAbsoluteWindowsWTF16(path: []const u16) bool { 238 return isAbsoluteWindowsImpl(u16, path); 239} 240 241pub fn isAbsoluteWindowsZ(path_c: [*:0]const u8) bool { 242 return isAbsoluteWindowsImpl(u8, mem.sliceTo(path_c, 0)); 243} 244 245pub fn isAbsolutePosix(path: []const u8) bool { 246 return path.len > 0 and path[0] == sep_posix; 247} 248 249pub fn isAbsolutePosixZ(path_c: [*:0]const u8) bool { 250 return isAbsolutePosix(mem.sliceTo(path_c, 0)); 251} 252 253test "isAbsoluteWindows" { 254 try testIsAbsoluteWindows("", false); 255 try testIsAbsoluteWindows("/", true); 256 try testIsAbsoluteWindows("//", true); 257 try testIsAbsoluteWindows("//server", true); 258 try testIsAbsoluteWindows("//server/file", true); 259 try testIsAbsoluteWindows("\\\\server\\file", true); 260 try testIsAbsoluteWindows("\\\\server", true); 261 try testIsAbsoluteWindows("\\\\", true); 262 try testIsAbsoluteWindows("c", false); 263 try testIsAbsoluteWindows("c:", false); 264 try testIsAbsoluteWindows("c:\\", true); 265 try testIsAbsoluteWindows("c:/", true); 266 try testIsAbsoluteWindows("c://", true); 267 try testIsAbsoluteWindows("C:/Users/", true); 268 try testIsAbsoluteWindows("C:\\Users\\", true); 269 try testIsAbsoluteWindows("C:cwd/another", false); 270 try testIsAbsoluteWindows("C:cwd\\another", false); 271 try testIsAbsoluteWindows("directory/directory", false); 272 try testIsAbsoluteWindows("directory\\directory", false); 273 try testIsAbsoluteWindows("/usr/local", true); 274} 275 276test "isAbsolutePosix" { 277 try testIsAbsolutePosix("", false); 278 try testIsAbsolutePosix("/home/foo", true); 279 try testIsAbsolutePosix("/home/foo/..", true); 280 try testIsAbsolutePosix("bar/", false); 281 try testIsAbsolutePosix("./baz", false); 282} 283 284fn testIsAbsoluteWindows(path: []const u8, expected_result: bool) !void { 285 try testing.expectEqual(expected_result, isAbsoluteWindows(path)); 286} 287 288fn testIsAbsolutePosix(path: []const u8, expected_result: bool) !void { 289 try testing.expectEqual(expected_result, isAbsolutePosix(path)); 290} 291 292pub const WindowsPath = struct { 293 is_abs: bool, 294 kind: Kind, 295 disk_designator: []const u8, 296 297 pub const Kind = enum { 298 None, 299 Drive, 300 NetworkShare, 301 }; 302}; 303 304pub fn windowsParsePath(path: []const u8) WindowsPath { 305 if (path.len >= 2 and path[1] == ':') { 306 return WindowsPath{ 307 .is_abs = isAbsoluteWindows(path), 308 .kind = WindowsPath.Kind.Drive, 309 .disk_designator = path[0..2], 310 }; 311 } 312 if (path.len >= 1 and (path[0] == '/' or path[0] == '\\') and 313 (path.len == 1 or (path[1] != '/' and path[1] != '\\'))) 314 { 315 return WindowsPath{ 316 .is_abs = true, 317 .kind = WindowsPath.Kind.None, 318 .disk_designator = path[0..0], 319 }; 320 } 321 const relative_path = WindowsPath{ 322 .kind = WindowsPath.Kind.None, 323 .disk_designator = &[_]u8{}, 324 .is_abs = false, 325 }; 326 if (path.len < "//a/b".len) { 327 return relative_path; 328 } 329 330 inline for ("/\\") |this_sep| { 331 const two_sep = [_]u8{ this_sep, this_sep }; 332 if (mem.startsWith(u8, path, &two_sep)) { 333 if (path[2] == this_sep) { 334 return relative_path; 335 } 336 337 var it = mem.tokenize(u8, path, &[_]u8{this_sep}); 338 _ = (it.next() orelse return relative_path); 339 _ = (it.next() orelse return relative_path); 340 return WindowsPath{ 341 .is_abs = isAbsoluteWindows(path), 342 .kind = WindowsPath.Kind.NetworkShare, 343 .disk_designator = path[0..it.index], 344 }; 345 } 346 } 347 return relative_path; 348} 349 350test "windowsParsePath" { 351 { 352 const parsed = windowsParsePath("//a/b"); 353 try testing.expect(parsed.is_abs); 354 try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare); 355 try testing.expect(mem.eql(u8, parsed.disk_designator, "//a/b")); 356 } 357 { 358 const parsed = windowsParsePath("\\\\a\\b"); 359 try testing.expect(parsed.is_abs); 360 try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare); 361 try testing.expect(mem.eql(u8, parsed.disk_designator, "\\\\a\\b")); 362 } 363 { 364 const parsed = windowsParsePath("\\\\a\\"); 365 try testing.expect(!parsed.is_abs); 366 try testing.expect(parsed.kind == WindowsPath.Kind.None); 367 try testing.expect(mem.eql(u8, parsed.disk_designator, "")); 368 } 369 { 370 const parsed = windowsParsePath("/usr/local"); 371 try testing.expect(parsed.is_abs); 372 try testing.expect(parsed.kind == WindowsPath.Kind.None); 373 try testing.expect(mem.eql(u8, parsed.disk_designator, "")); 374 } 375 { 376 const parsed = windowsParsePath("c:../"); 377 try testing.expect(!parsed.is_abs); 378 try testing.expect(parsed.kind == WindowsPath.Kind.Drive); 379 try testing.expect(mem.eql(u8, parsed.disk_designator, "c:")); 380 } 381} 382 383pub fn diskDesignator(path: []const u8) []const u8 { 384 if (native_os == .windows) { 385 return diskDesignatorWindows(path); 386 } else { 387 return ""; 388 } 389} 390 391pub fn diskDesignatorWindows(path: []const u8) []const u8 { 392 return windowsParsePath(path).disk_designator; 393} 394 395fn networkShareServersEql(ns1: []const u8, ns2: []const u8) bool { 396 const sep1 = ns1[0]; 397 const sep2 = ns2[0]; 398 399 var it1 = mem.tokenize(u8, ns1, &[_]u8{sep1}); 400 var it2 = mem.tokenize(u8, ns2, &[_]u8{sep2}); 401 402 // TODO ASCII is wrong, we actually need full unicode support to compare paths. 403 return asciiEqlIgnoreCase(it1.next().?, it2.next().?); 404} 405 406fn compareDiskDesignators(kind: WindowsPath.Kind, p1: []const u8, p2: []const u8) bool { 407 switch (kind) { 408 WindowsPath.Kind.None => { 409 assert(p1.len == 0); 410 assert(p2.len == 0); 411 return true; 412 }, 413 WindowsPath.Kind.Drive => { 414 return asciiUpper(p1[0]) == asciiUpper(p2[0]); 415 }, 416 WindowsPath.Kind.NetworkShare => { 417 const sep1 = p1[0]; 418 const sep2 = p2[0]; 419 420 var it1 = mem.tokenize(u8, p1, &[_]u8{sep1}); 421 var it2 = mem.tokenize(u8, p2, &[_]u8{sep2}); 422 423 // TODO ASCII is wrong, we actually need full unicode support to compare paths. 424 return asciiEqlIgnoreCase(it1.next().?, it2.next().?) and asciiEqlIgnoreCase(it1.next().?, it2.next().?); 425 }, 426 } 427} 428 429fn asciiUpper(byte: u8) u8 { 430 return switch (byte) { 431 'a'...'z' => 'A' + (byte - 'a'), 432 else => byte, 433 }; 434} 435 436fn asciiEqlIgnoreCase(s1: []const u8, s2: []const u8) bool { 437 if (s1.len != s2.len) 438 return false; 439 var i: usize = 0; 440 while (i < s1.len) : (i += 1) { 441 if (asciiUpper(s1[i]) != asciiUpper(s2[i])) 442 return false; 443 } 444 return true; 445} 446 447/// On Windows, this calls `resolveWindows` and on POSIX it calls `resolvePosix`. 448pub fn resolve(allocator: Allocator, paths: []const []const u8) ![]u8 { 449 if (native_os == .windows) { 450 return resolveWindows(allocator, paths); 451 } else { 452 return resolvePosix(allocator, paths); 453 } 454} 455 456/// This function is like a series of `cd` statements executed one after another. 457/// It resolves "." and "..". 458/// The result does not have a trailing path separator. 459/// If all paths are relative it uses the current working directory as a starting point. 460/// Each drive has its own current working directory. 461/// Path separators are canonicalized to '\\' and drives are canonicalized to capital letters. 462/// Note: all usage of this function should be audited due to the existence of symlinks. 463/// Without performing actual syscalls, resolving `..` could be incorrect. 464pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) ![]u8 { 465 if (paths.len == 0) { 466 assert(native_os == .windows); // resolveWindows called on non windows can't use getCwd 467 return process.getCwdAlloc(allocator); 468 } 469 470 // determine which disk designator we will result with, if any 471 var result_drive_buf = "_:".*; 472 var result_disk_designator: []const u8 = ""; 473 var have_drive_kind = WindowsPath.Kind.None; 474 var have_abs_path = false; 475 var first_index: usize = 0; 476 var max_size: usize = 0; 477 for (paths) |p, i| { 478 const parsed = windowsParsePath(p); 479 if (parsed.is_abs) { 480 have_abs_path = true; 481 first_index = i; 482 max_size = result_disk_designator.len; 483 } 484 switch (parsed.kind) { 485 WindowsPath.Kind.Drive => { 486 result_drive_buf[0] = asciiUpper(parsed.disk_designator[0]); 487 result_disk_designator = result_drive_buf[0..]; 488 have_drive_kind = WindowsPath.Kind.Drive; 489 }, 490 WindowsPath.Kind.NetworkShare => { 491 result_disk_designator = parsed.disk_designator; 492 have_drive_kind = WindowsPath.Kind.NetworkShare; 493 }, 494 WindowsPath.Kind.None => {}, 495 } 496 max_size += p.len + 1; 497 } 498 499 // if we will result with a disk designator, loop again to determine 500 // which is the last time the disk designator is absolutely specified, if any 501 // and count up the max bytes for paths related to this disk designator 502 if (have_drive_kind != WindowsPath.Kind.None) { 503 have_abs_path = false; 504 first_index = 0; 505 max_size = result_disk_designator.len; 506 var correct_disk_designator = false; 507 508 for (paths) |p, i| { 509 const parsed = windowsParsePath(p); 510 if (parsed.kind != WindowsPath.Kind.None) { 511 if (parsed.kind == have_drive_kind) { 512 correct_disk_designator = compareDiskDesignators(have_drive_kind, result_disk_designator, parsed.disk_designator); 513 } else { 514 continue; 515 } 516 } 517 if (!correct_disk_designator) { 518 continue; 519 } 520 if (parsed.is_abs) { 521 first_index = i; 522 max_size = result_disk_designator.len; 523 have_abs_path = true; 524 } 525 max_size += p.len + 1; 526 } 527 } 528 529 // Allocate result and fill in the disk designator, calling getCwd if we have to. 530 var result: []u8 = undefined; 531 var result_index: usize = 0; 532 533 if (have_abs_path) { 534 switch (have_drive_kind) { 535 WindowsPath.Kind.Drive => { 536 result = try allocator.alloc(u8, max_size); 537 538 mem.copy(u8, result, result_disk_designator); 539 result_index += result_disk_designator.len; 540 }, 541 WindowsPath.Kind.NetworkShare => { 542 result = try allocator.alloc(u8, max_size); 543 var it = mem.tokenize(u8, paths[first_index], "/\\"); 544 const server_name = it.next().?; 545 const other_name = it.next().?; 546 547 result[result_index] = '\\'; 548 result_index += 1; 549 result[result_index] = '\\'; 550 result_index += 1; 551 mem.copy(u8, result[result_index..], server_name); 552 result_index += server_name.len; 553 result[result_index] = '\\'; 554 result_index += 1; 555 mem.copy(u8, result[result_index..], other_name); 556 result_index += other_name.len; 557 558 result_disk_designator = result[0..result_index]; 559 }, 560 WindowsPath.Kind.None => { 561 assert(native_os == .windows); // resolveWindows called on non windows can't use getCwd 562 const cwd = try process.getCwdAlloc(allocator); 563 defer allocator.free(cwd); 564 const parsed_cwd = windowsParsePath(cwd); 565 result = try allocator.alloc(u8, max_size + parsed_cwd.disk_designator.len + 1); 566 mem.copy(u8, result, parsed_cwd.disk_designator); 567 result_index += parsed_cwd.disk_designator.len; 568 result_disk_designator = result[0..parsed_cwd.disk_designator.len]; 569 if (parsed_cwd.kind == WindowsPath.Kind.Drive) { 570 result[0] = asciiUpper(result[0]); 571 } 572 have_drive_kind = parsed_cwd.kind; 573 }, 574 } 575 } else { 576 assert(native_os == .windows); // resolveWindows called on non windows can't use getCwd 577 // TODO call get cwd for the result_disk_designator instead of the global one 578 const cwd = try process.getCwdAlloc(allocator); 579 defer allocator.free(cwd); 580 581 result = try allocator.alloc(u8, max_size + cwd.len + 1); 582 583 mem.copy(u8, result, cwd); 584 result_index += cwd.len; 585 const parsed_cwd = windowsParsePath(result[0..result_index]); 586 result_disk_designator = parsed_cwd.disk_designator; 587 if (parsed_cwd.kind == WindowsPath.Kind.Drive) { 588 result[0] = asciiUpper(result[0]); 589 // Remove the trailing slash if present, eg. if the cwd is a root 590 // directory. 591 if (cwd.len > 0 and cwd[cwd.len - 1] == sep_windows) { 592 result_index -= 1; 593 } 594 } 595 have_drive_kind = parsed_cwd.kind; 596 } 597 errdefer allocator.free(result); 598 599 // Now we know the disk designator to use, if any, and what kind it is. And our result 600 // is big enough to append all the paths to. 601 var correct_disk_designator = true; 602 for (paths[first_index..]) |p| { 603 const parsed = windowsParsePath(p); 604 605 if (parsed.kind != WindowsPath.Kind.None) { 606 if (parsed.kind == have_drive_kind) { 607 correct_disk_designator = compareDiskDesignators(have_drive_kind, result_disk_designator, parsed.disk_designator); 608 } else { 609 continue; 610 } 611 } 612 if (!correct_disk_designator) { 613 continue; 614 } 615 var it = mem.tokenize(u8, p[parsed.disk_designator.len..], "/\\"); 616 while (it.next()) |component| { 617 if (mem.eql(u8, component, ".")) { 618 continue; 619 } else if (mem.eql(u8, component, "..")) { 620 while (true) { 621 if (result_index == 0 or result_index == result_disk_designator.len) 622 break; 623 result_index -= 1; 624 if (result[result_index] == '\\' or result[result_index] == '/') 625 break; 626 } 627 } else { 628 result[result_index] = sep_windows; 629 result_index += 1; 630 mem.copy(u8, result[result_index..], component); 631 result_index += component.len; 632 } 633 } 634 } 635 636 if (result_index == result_disk_designator.len) { 637 result[result_index] = '\\'; 638 result_index += 1; 639 } 640 641 return allocator.shrink(result, result_index); 642} 643 644/// This function is like a series of `cd` statements executed one after another. 645/// It resolves "." and "..". 646/// The result does not have a trailing path separator. 647/// If all paths are relative it uses the current working directory as a starting point. 648/// Note: all usage of this function should be audited due to the existence of symlinks. 649/// Without performing actual syscalls, resolving `..` could be incorrect. 650pub fn resolvePosix(allocator: Allocator, paths: []const []const u8) ![]u8 { 651 if (paths.len == 0) { 652 assert(native_os != .windows); // resolvePosix called on windows can't use getCwd 653 return process.getCwdAlloc(allocator); 654 } 655 656 var first_index: usize = 0; 657 var have_abs = false; 658 var max_size: usize = 0; 659 for (paths) |p, i| { 660 if (isAbsolutePosix(p)) { 661 first_index = i; 662 have_abs = true; 663 max_size = 0; 664 } 665 max_size += p.len + 1; 666 } 667 668 var result: []u8 = undefined; 669 var result_index: usize = 0; 670 671 if (have_abs) { 672 result = try allocator.alloc(u8, max_size); 673 } else { 674 assert(native_os != .windows); // resolvePosix called on windows can't use getCwd 675 const cwd = try process.getCwdAlloc(allocator); 676 defer allocator.free(cwd); 677 result = try allocator.alloc(u8, max_size + cwd.len + 1); 678 mem.copy(u8, result, cwd); 679 result_index += cwd.len; 680 } 681 errdefer allocator.free(result); 682 683 for (paths[first_index..]) |p| { 684 var it = mem.tokenize(u8, p, "/"); 685 while (it.next()) |component| { 686 if (mem.eql(u8, component, ".")) { 687 continue; 688 } else if (mem.eql(u8, component, "..")) { 689 while (true) { 690 if (result_index == 0) 691 break; 692 result_index -= 1; 693 if (result[result_index] == '/') 694 break; 695 } 696 } else { 697 result[result_index] = '/'; 698 result_index += 1; 699 mem.copy(u8, result[result_index..], component); 700 result_index += component.len; 701 } 702 } 703 } 704 705 if (result_index == 0) { 706 result[0] = '/'; 707 result_index += 1; 708 } 709 710 return allocator.shrink(result, result_index); 711} 712 713test "resolve" { 714 if (native_os == .wasi) return error.SkipZigTest; 715 716 const cwd = try process.getCwdAlloc(testing.allocator); 717 defer testing.allocator.free(cwd); 718 if (native_os == .windows) { 719 if (windowsParsePath(cwd).kind == WindowsPath.Kind.Drive) { 720 cwd[0] = asciiUpper(cwd[0]); 721 } 722 try testResolveWindows(&[_][]const u8{"."}, cwd); 723 } else { 724 try testResolvePosix(&[_][]const u8{ "a/b/c/", "../../.." }, cwd); 725 try testResolvePosix(&[_][]const u8{"."}, cwd); 726 } 727} 728 729test "resolveWindows" { 730 if (builtin.target.cpu.arch == .aarch64) { 731 // TODO https://github.com/ziglang/zig/issues/3288 732 return error.SkipZigTest; 733 } 734 if (native_os == .wasi) return error.SkipZigTest; 735 if (native_os == .windows) { 736 const cwd = try process.getCwdAlloc(testing.allocator); 737 defer testing.allocator.free(cwd); 738 const parsed_cwd = windowsParsePath(cwd); 739 { 740 const expected = try join(testing.allocator, &[_][]const u8{ 741 parsed_cwd.disk_designator, 742 "usr\\local\\lib\\zig\\std\\array_list.zig", 743 }); 744 defer testing.allocator.free(expected); 745 if (parsed_cwd.kind == WindowsPath.Kind.Drive) { 746 expected[0] = asciiUpper(parsed_cwd.disk_designator[0]); 747 } 748 try testResolveWindows(&[_][]const u8{ "/usr/local", "lib\\zig\\std\\array_list.zig" }, expected); 749 } 750 { 751 const expected = try join(testing.allocator, &[_][]const u8{ 752 cwd, 753 "usr\\local\\lib\\zig", 754 }); 755 defer testing.allocator.free(expected); 756 if (parsed_cwd.kind == WindowsPath.Kind.Drive) { 757 expected[0] = asciiUpper(parsed_cwd.disk_designator[0]); 758 } 759 try testResolveWindows(&[_][]const u8{ "usr/local", "lib\\zig" }, expected); 760 } 761 } 762 763 try testResolveWindows(&[_][]const u8{ "c:\\a\\b\\c", "/hi", "ok" }, "C:\\hi\\ok"); 764 try testResolveWindows(&[_][]const u8{ "c:/blah\\blah", "d:/games", "c:../a" }, "C:\\blah\\a"); 765 try testResolveWindows(&[_][]const u8{ "c:/blah\\blah", "d:/games", "C:../a" }, "C:\\blah\\a"); 766 try testResolveWindows(&[_][]const u8{ "c:/ignore", "d:\\a/b\\c/d", "\\e.exe" }, "D:\\e.exe"); 767 try testResolveWindows(&[_][]const u8{ "c:/ignore", "c:/some/file" }, "C:\\some\\file"); 768 try testResolveWindows(&[_][]const u8{ "d:/ignore", "d:some/dir//" }, "D:\\ignore\\some\\dir"); 769 try testResolveWindows(&[_][]const u8{ "//server/share", "..", "relative\\" }, "\\\\server\\share\\relative"); 770 try testResolveWindows(&[_][]const u8{ "c:/", "//" }, "C:\\"); 771 try testResolveWindows(&[_][]const u8{ "c:/", "//dir" }, "C:\\dir"); 772 try testResolveWindows(&[_][]const u8{ "c:/", "//server/share" }, "\\\\server\\share\\"); 773 try testResolveWindows(&[_][]const u8{ "c:/", "//server//share" }, "\\\\server\\share\\"); 774 try testResolveWindows(&[_][]const u8{ "c:/", "///some//dir" }, "C:\\some\\dir"); 775 try testResolveWindows(&[_][]const u8{ "C:\\foo\\tmp.3\\", "..\\tmp.3\\cycles\\root.js" }, "C:\\foo\\tmp.3\\cycles\\root.js"); 776} 777 778test "resolvePosix" { 779 if (native_os == .wasi) return error.SkipZigTest; 780 781 try testResolvePosix(&[_][]const u8{ "/a/b", "c" }, "/a/b/c"); 782 try testResolvePosix(&[_][]const u8{ "/a/b", "c", "//d", "e///" }, "/d/e"); 783 try testResolvePosix(&[_][]const u8{ "/a/b/c", "..", "../" }, "/a"); 784 try testResolvePosix(&[_][]const u8{ "/", "..", ".." }, "/"); 785 try testResolvePosix(&[_][]const u8{"/a/b/c/"}, "/a/b/c"); 786 787 try testResolvePosix(&[_][]const u8{ "/var/lib", "../", "file/" }, "/var/file"); 788 try testResolvePosix(&[_][]const u8{ "/var/lib", "/../", "file/" }, "/file"); 789 try testResolvePosix(&[_][]const u8{ "/some/dir", ".", "/absolute/" }, "/absolute"); 790 try testResolvePosix(&[_][]const u8{ "/foo/tmp.3/", "../tmp.3/cycles/root.js" }, "/foo/tmp.3/cycles/root.js"); 791} 792 793fn testResolveWindows(paths: []const []const u8, expected: []const u8) !void { 794 const actual = try resolveWindows(testing.allocator, paths); 795 defer testing.allocator.free(actual); 796 try testing.expect(mem.eql(u8, actual, expected)); 797} 798 799fn testResolvePosix(paths: []const []const u8, expected: []const u8) !void { 800 const actual = try resolvePosix(testing.allocator, paths); 801 defer testing.allocator.free(actual); 802 try testing.expect(mem.eql(u8, actual, expected)); 803} 804 805/// Strip the last component from a file path. 806/// 807/// If the path is a file in the current directory (no directory component) 808/// then returns null. 809/// 810/// If the path is the root directory, returns null. 811pub fn dirname(path: []const u8) ?[]const u8 { 812 if (native_os == .windows) { 813 return dirnameWindows(path); 814 } else { 815 return dirnamePosix(path); 816 } 817} 818 819pub fn dirnameWindows(path: []const u8) ?[]const u8 { 820 if (path.len == 0) 821 return null; 822 823 const root_slice = diskDesignatorWindows(path); 824 if (path.len == root_slice.len) 825 return null; 826 827 const have_root_slash = path.len > root_slice.len and (path[root_slice.len] == '/' or path[root_slice.len] == '\\'); 828 829 var end_index: usize = path.len - 1; 830 831 while (path[end_index] == '/' or path[end_index] == '\\') { 832 if (end_index == 0) 833 return null; 834 end_index -= 1; 835 } 836 837 while (path[end_index] != '/' and path[end_index] != '\\') { 838 if (end_index == 0) 839 return null; 840 end_index -= 1; 841 } 842 843 if (have_root_slash and end_index == root_slice.len) { 844 end_index += 1; 845 } 846 847 if (end_index == 0) 848 return null; 849 850 return path[0..end_index]; 851} 852 853pub fn dirnamePosix(path: []const u8) ?[]const u8 { 854 if (path.len == 0) 855 return null; 856 857 var end_index: usize = path.len - 1; 858 while (path[end_index] == '/') { 859 if (end_index == 0) 860 return null; 861 end_index -= 1; 862 } 863 864 while (path[end_index] != '/') { 865 if (end_index == 0) 866 return null; 867 end_index -= 1; 868 } 869 870 if (end_index == 0 and path[0] == '/') 871 return path[0..1]; 872 873 if (end_index == 0) 874 return null; 875 876 return path[0..end_index]; 877} 878 879test "dirnamePosix" { 880 try testDirnamePosix("/a/b/c", "/a/b"); 881 try testDirnamePosix("/a/b/c///", "/a/b"); 882 try testDirnamePosix("/a", "/"); 883 try testDirnamePosix("/", null); 884 try testDirnamePosix("//", null); 885 try testDirnamePosix("///", null); 886 try testDirnamePosix("////", null); 887 try testDirnamePosix("", null); 888 try testDirnamePosix("a", null); 889 try testDirnamePosix("a/", null); 890 try testDirnamePosix("a//", null); 891} 892 893test "dirnameWindows" { 894 try testDirnameWindows("c:\\", null); 895 try testDirnameWindows("c:\\foo", "c:\\"); 896 try testDirnameWindows("c:\\foo\\", "c:\\"); 897 try testDirnameWindows("c:\\foo\\bar", "c:\\foo"); 898 try testDirnameWindows("c:\\foo\\bar\\", "c:\\foo"); 899 try testDirnameWindows("c:\\foo\\bar\\baz", "c:\\foo\\bar"); 900 try testDirnameWindows("\\", null); 901 try testDirnameWindows("\\foo", "\\"); 902 try testDirnameWindows("\\foo\\", "\\"); 903 try testDirnameWindows("\\foo\\bar", "\\foo"); 904 try testDirnameWindows("\\foo\\bar\\", "\\foo"); 905 try testDirnameWindows("\\foo\\bar\\baz", "\\foo\\bar"); 906 try testDirnameWindows("c:", null); 907 try testDirnameWindows("c:foo", null); 908 try testDirnameWindows("c:foo\\", null); 909 try testDirnameWindows("c:foo\\bar", "c:foo"); 910 try testDirnameWindows("c:foo\\bar\\", "c:foo"); 911 try testDirnameWindows("c:foo\\bar\\baz", "c:foo\\bar"); 912 try testDirnameWindows("file:stream", null); 913 try testDirnameWindows("dir\\file:stream", "dir"); 914 try testDirnameWindows("\\\\unc\\share", null); 915 try testDirnameWindows("\\\\unc\\share\\foo", "\\\\unc\\share\\"); 916 try testDirnameWindows("\\\\unc\\share\\foo\\", "\\\\unc\\share\\"); 917 try testDirnameWindows("\\\\unc\\share\\foo\\bar", "\\\\unc\\share\\foo"); 918 try testDirnameWindows("\\\\unc\\share\\foo\\bar\\", "\\\\unc\\share\\foo"); 919 try testDirnameWindows("\\\\unc\\share\\foo\\bar\\baz", "\\\\unc\\share\\foo\\bar"); 920 try testDirnameWindows("/a/b/", "/a"); 921 try testDirnameWindows("/a/b", "/a"); 922 try testDirnameWindows("/a", "/"); 923 try testDirnameWindows("", null); 924 try testDirnameWindows("/", null); 925 try testDirnameWindows("////", null); 926 try testDirnameWindows("foo", null); 927} 928 929fn testDirnamePosix(input: []const u8, expected_output: ?[]const u8) !void { 930 if (dirnamePosix(input)) |output| { 931 try testing.expect(mem.eql(u8, output, expected_output.?)); 932 } else { 933 try testing.expect(expected_output == null); 934 } 935} 936 937fn testDirnameWindows(input: []const u8, expected_output: ?[]const u8) !void { 938 if (dirnameWindows(input)) |output| { 939 try testing.expect(mem.eql(u8, output, expected_output.?)); 940 } else { 941 try testing.expect(expected_output == null); 942 } 943} 944 945pub fn basename(path: []const u8) []const u8 { 946 if (native_os == .windows) { 947 return basenameWindows(path); 948 } else { 949 return basenamePosix(path); 950 } 951} 952 953pub fn basenamePosix(path: []const u8) []const u8 { 954 if (path.len == 0) 955 return &[_]u8{}; 956 957 var end_index: usize = path.len - 1; 958 while (path[end_index] == '/') { 959 if (end_index == 0) 960 return &[_]u8{}; 961 end_index -= 1; 962 } 963 var start_index: usize = end_index; 964 end_index += 1; 965 while (path[start_index] != '/') { 966 if (start_index == 0) 967 return path[0..end_index]; 968 start_index -= 1; 969 } 970 971 return path[start_index + 1 .. end_index]; 972} 973 974pub fn basenameWindows(path: []const u8) []const u8 { 975 if (path.len == 0) 976 return &[_]u8{}; 977 978 var end_index: usize = path.len - 1; 979 while (true) { 980 const byte = path[end_index]; 981 if (byte == '/' or byte == '\\') { 982 if (end_index == 0) 983 return &[_]u8{}; 984 end_index -= 1; 985 continue; 986 } 987 if (byte == ':' and end_index == 1) { 988 return &[_]u8{}; 989 } 990 break; 991 } 992 993 var start_index: usize = end_index; 994 end_index += 1; 995 while (path[start_index] != '/' and path[start_index] != '\\' and 996 !(path[start_index] == ':' and start_index == 1)) 997 { 998 if (start_index == 0) 999 return path[0..end_index]; 1000 start_index -= 1; 1001 } 1002 1003 return path[start_index + 1 .. end_index]; 1004} 1005 1006test "basename" { 1007 try testBasename("", ""); 1008 try testBasename("/", ""); 1009 try testBasename("/dir/basename.ext", "basename.ext"); 1010 try testBasename("/basename.ext", "basename.ext"); 1011 try testBasename("basename.ext", "basename.ext"); 1012 try testBasename("basename.ext/", "basename.ext"); 1013 try testBasename("basename.ext//", "basename.ext"); 1014 try testBasename("/aaa/bbb", "bbb"); 1015 try testBasename("/aaa/", "aaa"); 1016 try testBasename("/aaa/b", "b"); 1017 try testBasename("/a/b", "b"); 1018 try testBasename("//a", "a"); 1019 1020 try testBasenamePosix("\\dir\\basename.ext", "\\dir\\basename.ext"); 1021 try testBasenamePosix("\\basename.ext", "\\basename.ext"); 1022 try testBasenamePosix("basename.ext", "basename.ext"); 1023 try testBasenamePosix("basename.ext\\", "basename.ext\\"); 1024 try testBasenamePosix("basename.ext\\\\", "basename.ext\\\\"); 1025 try testBasenamePosix("foo", "foo"); 1026 1027 try testBasenameWindows("\\dir\\basename.ext", "basename.ext"); 1028 try testBasenameWindows("\\basename.ext", "basename.ext"); 1029 try testBasenameWindows("basename.ext", "basename.ext"); 1030 try testBasenameWindows("basename.ext\\", "basename.ext"); 1031 try testBasenameWindows("basename.ext\\\\", "basename.ext"); 1032 try testBasenameWindows("foo", "foo"); 1033 try testBasenameWindows("C:", ""); 1034 try testBasenameWindows("C:.", "."); 1035 try testBasenameWindows("C:\\", ""); 1036 try testBasenameWindows("C:\\dir\\base.ext", "base.ext"); 1037 try testBasenameWindows("C:\\basename.ext", "basename.ext"); 1038 try testBasenameWindows("C:basename.ext", "basename.ext"); 1039 try testBasenameWindows("C:basename.ext\\", "basename.ext"); 1040 try testBasenameWindows("C:basename.ext\\\\", "basename.ext"); 1041 try testBasenameWindows("C:foo", "foo"); 1042 try testBasenameWindows("file:stream", "file:stream"); 1043} 1044 1045fn testBasename(input: []const u8, expected_output: []const u8) !void { 1046 try testing.expectEqualSlices(u8, expected_output, basename(input)); 1047} 1048 1049fn testBasenamePosix(input: []const u8, expected_output: []const u8) !void { 1050 try testing.expectEqualSlices(u8, expected_output, basenamePosix(input)); 1051} 1052 1053fn testBasenameWindows(input: []const u8, expected_output: []const u8) !void { 1054 try testing.expectEqualSlices(u8, expected_output, basenameWindows(input)); 1055} 1056 1057/// Returns the relative path from `from` to `to`. If `from` and `to` each 1058/// resolve to the same path (after calling `resolve` on each), a zero-length 1059/// string is returned. 1060/// On Windows this canonicalizes the drive to a capital letter and paths to `\\`. 1061pub fn relative(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 { 1062 if (native_os == .windows) { 1063 return relativeWindows(allocator, from, to); 1064 } else { 1065 return relativePosix(allocator, from, to); 1066 } 1067} 1068 1069pub fn relativeWindows(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 { 1070 const resolved_from = try resolveWindows(allocator, &[_][]const u8{from}); 1071 defer allocator.free(resolved_from); 1072 1073 var clean_up_resolved_to = true; 1074 const resolved_to = try resolveWindows(allocator, &[_][]const u8{to}); 1075 defer if (clean_up_resolved_to) allocator.free(resolved_to); 1076 1077 const parsed_from = windowsParsePath(resolved_from); 1078 const parsed_to = windowsParsePath(resolved_to); 1079 const result_is_to = x: { 1080 if (parsed_from.kind != parsed_to.kind) { 1081 break :x true; 1082 } else switch (parsed_from.kind) { 1083 WindowsPath.Kind.NetworkShare => { 1084 break :x !networkShareServersEql(parsed_to.disk_designator, parsed_from.disk_designator); 1085 }, 1086 WindowsPath.Kind.Drive => { 1087 break :x asciiUpper(parsed_from.disk_designator[0]) != asciiUpper(parsed_to.disk_designator[0]); 1088 }, 1089 else => unreachable, 1090 } 1091 }; 1092 1093 if (result_is_to) { 1094 clean_up_resolved_to = false; 1095 return resolved_to; 1096 } 1097 1098 var from_it = mem.tokenize(u8, resolved_from, "/\\"); 1099 var to_it = mem.tokenize(u8, resolved_to, "/\\"); 1100 while (true) { 1101 const from_component = from_it.next() orelse return allocator.dupe(u8, to_it.rest()); 1102 const to_rest = to_it.rest(); 1103 if (to_it.next()) |to_component| { 1104 // TODO ASCII is wrong, we actually need full unicode support to compare paths. 1105 if (asciiEqlIgnoreCase(from_component, to_component)) 1106 continue; 1107 } 1108 var up_count: usize = 1; 1109 while (from_it.next()) |_| { 1110 up_count += 1; 1111 } 1112 const up_index_end = up_count * "..\\".len; 1113 const result = try allocator.alloc(u8, up_index_end + to_rest.len); 1114 errdefer allocator.free(result); 1115 1116 var result_index: usize = 0; 1117 while (result_index < up_index_end) { 1118 result[result_index] = '.'; 1119 result_index += 1; 1120 result[result_index] = '.'; 1121 result_index += 1; 1122 result[result_index] = '\\'; 1123 result_index += 1; 1124 } 1125 // shave off the trailing slash 1126 result_index -= 1; 1127 1128 var rest_it = mem.tokenize(u8, to_rest, "/\\"); 1129 while (rest_it.next()) |to_component| { 1130 result[result_index] = '\\'; 1131 result_index += 1; 1132 mem.copy(u8, result[result_index..], to_component); 1133 result_index += to_component.len; 1134 } 1135 1136 return result[0..result_index]; 1137 } 1138 1139 return [_]u8{}; 1140} 1141 1142pub fn relativePosix(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 { 1143 const resolved_from = try resolvePosix(allocator, &[_][]const u8{from}); 1144 defer allocator.free(resolved_from); 1145 1146 const resolved_to = try resolvePosix(allocator, &[_][]const u8{to}); 1147 defer allocator.free(resolved_to); 1148 1149 var from_it = mem.tokenize(u8, resolved_from, "/"); 1150 var to_it = mem.tokenize(u8, resolved_to, "/"); 1151 while (true) { 1152 const from_component = from_it.next() orelse return allocator.dupe(u8, to_it.rest()); 1153 const to_rest = to_it.rest(); 1154 if (to_it.next()) |to_component| { 1155 if (mem.eql(u8, from_component, to_component)) 1156 continue; 1157 } 1158 var up_count: usize = 1; 1159 while (from_it.next()) |_| { 1160 up_count += 1; 1161 } 1162 const up_index_end = up_count * "../".len; 1163 const result = try allocator.alloc(u8, up_index_end + to_rest.len); 1164 errdefer allocator.free(result); 1165 1166 var result_index: usize = 0; 1167 while (result_index < up_index_end) { 1168 result[result_index] = '.'; 1169 result_index += 1; 1170 result[result_index] = '.'; 1171 result_index += 1; 1172 result[result_index] = '/'; 1173 result_index += 1; 1174 } 1175 if (to_rest.len == 0) { 1176 // shave off the trailing slash 1177 return allocator.shrink(result, result_index - 1); 1178 } 1179 1180 mem.copy(u8, result[result_index..], to_rest); 1181 return result; 1182 } 1183 1184 return [_]u8{}; 1185} 1186 1187test "relative" { 1188 if (builtin.target.cpu.arch == .aarch64) { 1189 // TODO https://github.com/ziglang/zig/issues/3288 1190 return error.SkipZigTest; 1191 } 1192 if (native_os == .wasi) return error.SkipZigTest; 1193 1194 try testRelativeWindows("c:/blah\\blah", "d:/games", "D:\\games"); 1195 try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa", ".."); 1196 try testRelativeWindows("c:/aaaa/bbbb", "c:/cccc", "..\\..\\cccc"); 1197 try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa/bbbb", ""); 1198 try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa/cccc", "..\\cccc"); 1199 try testRelativeWindows("c:/aaaa/", "c:/aaaa/cccc", "cccc"); 1200 try testRelativeWindows("c:/", "c:\\aaaa\\bbbb", "aaaa\\bbbb"); 1201 try testRelativeWindows("c:/aaaa/bbbb", "d:\\", "D:\\"); 1202 try testRelativeWindows("c:/AaAa/bbbb", "c:/aaaa/bbbb", ""); 1203 try testRelativeWindows("c:/aaaaa/", "c:/aaaa/cccc", "..\\aaaa\\cccc"); 1204 try testRelativeWindows("C:\\foo\\bar\\baz\\quux", "C:\\", "..\\..\\..\\.."); 1205 try testRelativeWindows("C:\\foo\\test", "C:\\foo\\test\\bar\\package.json", "bar\\package.json"); 1206 try testRelativeWindows("C:\\foo\\bar\\baz-quux", "C:\\foo\\bar\\baz", "..\\baz"); 1207 try testRelativeWindows("C:\\foo\\bar\\baz", "C:\\foo\\bar\\baz-quux", "..\\baz-quux"); 1208 try testRelativeWindows("\\\\foo\\bar", "\\\\foo\\bar\\baz", "baz"); 1209 try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar", ".."); 1210 try testRelativeWindows("\\\\foo\\bar\\baz-quux", "\\\\foo\\bar\\baz", "..\\baz"); 1211 try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz-quux", "..\\baz-quux"); 1212 try testRelativeWindows("C:\\baz-quux", "C:\\baz", "..\\baz"); 1213 try testRelativeWindows("C:\\baz", "C:\\baz-quux", "..\\baz-quux"); 1214 try testRelativeWindows("\\\\foo\\baz-quux", "\\\\foo\\baz", "..\\baz"); 1215 try testRelativeWindows("\\\\foo\\baz", "\\\\foo\\baz-quux", "..\\baz-quux"); 1216 try testRelativeWindows("C:\\baz", "\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz"); 1217 try testRelativeWindows("\\\\foo\\bar\\baz", "C:\\baz", "C:\\baz"); 1218 1219 try testRelativePosix("/var/lib", "/var", ".."); 1220 try testRelativePosix("/var/lib", "/bin", "../../bin"); 1221 try testRelativePosix("/var/lib", "/var/lib", ""); 1222 try testRelativePosix("/var/lib", "/var/apache", "../apache"); 1223 try testRelativePosix("/var/", "/var/lib", "lib"); 1224 try testRelativePosix("/", "/var/lib", "var/lib"); 1225 try testRelativePosix("/foo/test", "/foo/test/bar/package.json", "bar/package.json"); 1226 try testRelativePosix("/Users/a/web/b/test/mails", "/Users/a/web/b", "../.."); 1227 try testRelativePosix("/foo/bar/baz-quux", "/foo/bar/baz", "../baz"); 1228 try testRelativePosix("/foo/bar/baz", "/foo/bar/baz-quux", "../baz-quux"); 1229 try testRelativePosix("/baz-quux", "/baz", "../baz"); 1230 try testRelativePosix("/baz", "/baz-quux", "../baz-quux"); 1231} 1232 1233fn testRelativePosix(from: []const u8, to: []const u8, expected_output: []const u8) !void { 1234 const result = try relativePosix(testing.allocator, from, to); 1235 defer testing.allocator.free(result); 1236 try testing.expectEqualSlices(u8, expected_output, result); 1237} 1238 1239fn testRelativeWindows(from: []const u8, to: []const u8, expected_output: []const u8) !void { 1240 const result = try relativeWindows(testing.allocator, from, to); 1241 defer testing.allocator.free(result); 1242 try testing.expectEqualSlices(u8, expected_output, result); 1243} 1244 1245/// Returns the extension of the file name (if any). 1246/// This function will search for the file extension (separated by a `.`) and will return the text after the `.`. 1247/// Files that end with `.` are considered to have no extension, files that start with `.` 1248/// Examples: 1249/// - `"main.zig"` ⇒ `".zig"` 1250/// - `"src/main.zig"` ⇒ `".zig"` 1251/// - `".gitignore"` ⇒ `""` 1252/// - `"keep."` ⇒ `"."` 1253/// - `"src.keep.me"` ⇒ `".me"` 1254/// - `"/src/keep.me"` ⇒ `".me"` 1255/// - `"/src/keep.me/"` ⇒ `".me"` 1256/// The returned slice is guaranteed to have its pointer within the start and end 1257/// pointer address range of `path`, even if it is length zero. 1258pub fn extension(path: []const u8) []const u8 { 1259 const filename = basename(path); 1260 const index = mem.lastIndexOfScalar(u8, filename, '.') orelse return path[path.len..]; 1261 if (index == 0) return path[path.len..]; 1262 return filename[index..]; 1263} 1264 1265fn testExtension(path: []const u8, expected: []const u8) !void { 1266 try std.testing.expectEqualStrings(expected, extension(path)); 1267} 1268 1269test "extension" { 1270 try testExtension("", ""); 1271 try testExtension(".", ""); 1272 try testExtension("a.", "."); 1273 try testExtension("abc.", "."); 1274 try testExtension(".a", ""); 1275 try testExtension(".file", ""); 1276 try testExtension(".gitignore", ""); 1277 try testExtension("file.ext", ".ext"); 1278 try testExtension("file.ext.", "."); 1279 try testExtension("very-long-file.bruh", ".bruh"); 1280 try testExtension("a.b.c", ".c"); 1281 try testExtension("a.b.c/", ".c"); 1282 1283 try testExtension("/", ""); 1284 try testExtension("/.", ""); 1285 try testExtension("/a.", "."); 1286 try testExtension("/abc.", "."); 1287 try testExtension("/.a", ""); 1288 try testExtension("/.file", ""); 1289 try testExtension("/.gitignore", ""); 1290 try testExtension("/file.ext", ".ext"); 1291 try testExtension("/file.ext.", "."); 1292 try testExtension("/very-long-file.bruh", ".bruh"); 1293 try testExtension("/a.b.c", ".c"); 1294 try testExtension("/a.b.c/", ".c"); 1295 1296 try testExtension("/foo/bar/bam/", ""); 1297 try testExtension("/foo/bar/bam/.", ""); 1298 try testExtension("/foo/bar/bam/a.", "."); 1299 try testExtension("/foo/bar/bam/abc.", "."); 1300 try testExtension("/foo/bar/bam/.a", ""); 1301 try testExtension("/foo/bar/bam/.file", ""); 1302 try testExtension("/foo/bar/bam/.gitignore", ""); 1303 try testExtension("/foo/bar/bam/file.ext", ".ext"); 1304 try testExtension("/foo/bar/bam/file.ext.", "."); 1305 try testExtension("/foo/bar/bam/very-long-file.bruh", ".bruh"); 1306 try testExtension("/foo/bar/bam/a.b.c", ".c"); 1307 try testExtension("/foo/bar/bam/a.b.c/", ".c"); 1308} 1309