diff --git a/README.md b/README.md index e04268d4b2..8cbf31b3e1 100644 --- a/README.md +++ b/README.md @@ -43,14 +43,50 @@ clarity. * Cross-compiling is a primary use case. * In addition to creating executables, creating a C library is a primary use case. You can export an auto-generated .h file. - * Standard library supports Operating System abstractions for: - * `x86_64` `linux` - * `x86_64` `macos` - * Support for all popular operating systems and architectures is planned. * For OS development, Zig supports all architectures that LLVM does. All the standard library that does not depend on an OS is available to you in freestanding mode. +### Support Table + +Freestanding means that you do not directly interact with the OS +or you are writing your own OS. + +Note that if you use libc or other libraries to interact with the OS, +that counts as "freestanding" for the purposes of this table. + +| | freestanding | linux | macosx | windows | other | +|-------------|--------------|---------|---------|---------|---------| +|i386 | OK | planned | OK | OK | planned | +|x86_64 | OK | OK | OK | OK | planned | +|arm | OK | planned | planned | N/A | planned | +|aarch64 | OK | planned | planned | planned | planned | +|avr | OK | planned | planned | N/A | planned | +|bpf | OK | planned | planned | N/A | planned | +|hexagon | OK | planned | planned | N/A | planned | +|mips | OK | planned | planned | N/A | planned | +|msp430 | OK | planned | planned | N/A | planned | +|nios2 | OK | planned | planned | N/A | planned | +|powerpc | OK | planned | planned | N/A | planned | +|r600 | OK | planned | planned | N/A | planned | +|amdgcn | OK | planned | planned | N/A | planned | +|riscv | OK | planned | planned | N/A | planned | +|sparc | OK | planned | planned | N/A | planned | +|s390x | OK | planned | planned | N/A | planned | +|tce | OK | planned | planned | N/A | planned | +|thumb | OK | planned | planned | N/A | planned | +|xcore | OK | planned | planned | N/A | planned | +|nvptx | OK | planned | planned | N/A | planned | +|le | OK | planned | planned | N/A | planned | +|amdil | OK | planned | planned | N/A | planned | +|hsail | OK | planned | planned | N/A | planned | +|spir | OK | planned | planned | N/A | planned | +|kalimba | OK | planned | planned | N/A | planned | +|shave | OK | planned | planned | N/A | planned | +|lanai | OK | planned | planned | N/A | planned | +|wasm | OK | N/A | N/A | N/A | N/A | +|renderscript | OK | N/A | N/A | N/A | N/A | + ## Community * IRC: `#zig` on Freenode. diff --git a/std/build.zig b/std/build.zig index 98cc78e211..526e2c3cf9 100644 --- a/std/build.zig +++ b/std/build.zig @@ -309,7 +309,7 @@ pub const Builder = struct { fn processNixOSEnvVars(self: &Builder) { if (os.getEnv("NIX_CFLAGS_COMPILE")) |nix_cflags_compile| { - var it = mem.split(nix_cflags_compile, ' '); + var it = mem.split(nix_cflags_compile, " "); while (true) { const word = it.next() ?? break; if (mem.eql(u8, word, "-isystem")) { @@ -325,7 +325,7 @@ pub const Builder = struct { } } if (os.getEnv("NIX_LDFLAGS")) |nix_ldflags| { - var it = mem.split(nix_ldflags, ' '); + var it = mem.split(nix_ldflags, " "); while (true) { const word = it.next() ?? break; if (mem.eql(u8, word, "-rpath")) { diff --git a/std/mem.zig b/std/mem.zig index e16e947c25..9aca27ed69 100644 --- a/std/mem.zig +++ b/std/mem.zig @@ -216,20 +216,28 @@ pub fn dupe(allocator: &Allocator, comptime T: type, m: []const T) -> %[]T { /// Linear search for the index of a scalar value inside a slice. pub fn indexOfScalar(comptime T: type, slice: []const T, value: T) -> ?usize { - for (slice) |item, i| { - if (item == value) { + return indexOfScalarPos(T, slice, 0, value); +} + +pub fn indexOfScalarPos(comptime T: type, slice: []const T, start_index: usize, value: T) -> ?usize { + var i: usize = start_index; + while (i < slice.len) : (i += 1) { + if (slice[i] == value) return i; - } } return null; } -// TODO boyer-moore algorithm pub fn indexOf(comptime T: type, haystack: []const T, needle: []const T) -> ?usize { + return indexOfPos(T, haystack, 0, needle); +} + +// TODO boyer-moore algorithm +pub fn indexOfPos(comptime T: type, haystack: []const T, start_index: usize, needle: []const T) -> ?usize { if (needle.len > haystack.len) return null; - var i: usize = 0; + var i: usize = start_index; const end = haystack.len - needle.len; while (i <= end) : (i += 1) { if (eql(T, haystack[i .. i + needle.len], needle)) @@ -303,20 +311,20 @@ pub fn eql_slice_u8(a: []const u8, b: []const u8) -> bool { return eql(u8, a, b); } -/// Returns an iterator that iterates over the slices of ::s that are not -/// the byte ::c. -/// split(" abc def ghi ") +/// Returns an iterator that iterates over the slices of `buffer` that are not +/// any of the bytes in `split_bytes`. +/// split(" abc def ghi ", " ") /// Will return slices for "abc", "def", "ghi", null, in that order. -pub fn split(s: []const u8, c: u8) -> SplitIterator { +pub fn split(buffer: []const u8, split_bytes: []const u8) -> SplitIterator { SplitIterator { .index = 0, - .s = s, - .c = c, + .buffer = buffer, + .split_bytes = split_bytes, } } test "mem.split" { - var it = split(" abc def ghi ", ' '); + var it = split(" abc def ghi ", " "); assert(eql(u8, ??it.next(), "abc")); assert(eql(u8, ??it.next(), "def")); assert(eql(u8, ??it.next(), "ghi")); @@ -328,31 +336,40 @@ pub fn startsWith(comptime T: type, haystack: []const T, needle: []const T) -> b } const SplitIterator = struct { - s: []const u8, - c: u8, + buffer: []const u8, + split_bytes: []const u8, index: usize, pub fn next(self: &SplitIterator) -> ?[]const u8 { // move to beginning of token - while (self.index < self.s.len and self.s[self.index] == self.c) : (self.index += 1) {} + while (self.index < self.buffer.len and self.isSplitByte(self.buffer[self.index])) : (self.index += 1) {} const start = self.index; - if (start == self.s.len) { + if (start == self.buffer.len) { return null; } // move to end of token - while (self.index < self.s.len and self.s[self.index] != self.c) : (self.index += 1) {} + while (self.index < self.buffer.len and !self.isSplitByte(self.buffer[self.index])) : (self.index += 1) {} const end = self.index; - return self.s[start..end]; + return self.buffer[start..end]; } /// Returns a slice of the remaining bytes. Does not affect iterator state. pub fn rest(self: &const SplitIterator) -> []const u8 { // move to beginning of token var index: usize = self.index; - while (index < self.s.len and self.s[index] == self.c) : (index += 1) {} - return self.s[index..]; + while (index < self.buffer.len and self.isSplitByte(self.buffer[index])) : (index += 1) {} + return self.buffer[index..]; + } + + fn isSplitByte(self: &const SplitIterator, byte: u8) -> bool { + for (self.split_bytes) |split_byte| { + if (byte == split_byte) { + return true; + } + } + return false; } }; diff --git a/std/os/index.zig b/std/os/index.zig index ccd7527975..d4681eaa15 100644 --- a/std/os/index.zig +++ b/std/os/index.zig @@ -384,7 +384,7 @@ pub fn posixExecve(argv: []const []const u8, env_map: &const BufMap, // +1 for the null terminating byte const path_buf = %return allocator.alloc(u8, PATH.len + exe_path.len + 2); defer allocator.free(path_buf); - var it = mem.split(PATH, ':'); + var it = mem.split(PATH, ":"); var seen_eacces = false; var err: usize = undefined; while (it.next()) |search_path| { @@ -474,18 +474,41 @@ pub const args = struct { /// Caller must free the returned memory. pub fn getCwd(allocator: &Allocator) -> %[]u8 { - var buf = %return allocator.alloc(u8, 1024); - %defer allocator.free(buf); - while (true) { - const err = posix.getErrno(posix.getcwd(buf.ptr, buf.len)); - if (err == posix.ERANGE) { - buf = %return allocator.realloc(u8, buf, buf.len * 2); - continue; - } else if (err > 0) { - return error.Unexpected; - } + switch (builtin.os) { + Os.windows => { + var buf = %return allocator.alloc(u8, 256); + %defer allocator.free(buf); - return cstr.toSlice(buf.ptr); + while (true) { + const result = windows.GetCurrentDirectoryA(windows.WORD(buf.len), buf.ptr); + + if (result == 0) { + return error.Unexpected; + } + + if (result > buf.len) { + buf = %return allocator.realloc(u8, buf, result); + continue; + } + + return buf[0..result]; + } + }, + else => { + var buf = %return allocator.alloc(u8, 1024); + %defer allocator.free(buf); + while (true) { + const err = posix.getErrno(posix.getcwd(buf.ptr, buf.len)); + if (err == posix.ERANGE) { + buf = %return allocator.realloc(u8, buf, buf.len * 2); + continue; + } else if (err > 0) { + return error.Unexpected; + } + + return cstr.toSlice(buf.ptr); + } + }, } } @@ -1033,3 +1056,16 @@ pub fn posix_setregid(rgid: u32, egid: u32) -> %void { else => error.Unexpected, }; } + +test "std.os" { + _ = @import("child_process.zig"); + _ = @import("darwin_errno.zig"); + _ = @import("darwin.zig"); + _ = @import("get_user_id.zig"); + _ = @import("linux_errno.zig"); + //_ = @import("linux_i386.zig"); + _ = @import("linux_x86_64.zig"); + _ = @import("linux.zig"); + _ = @import("path.zig"); + _ = @import("windows/index.zig"); +} diff --git a/std/os/path.zig b/std/os/path.zig index cfed8d7b2d..eea0df461a 100644 --- a/std/os/path.zig +++ b/std/os/path.zig @@ -11,41 +11,208 @@ const posix = os.posix; const c = @import("../c/index.zig"); const cstr = @import("../cstr.zig"); -pub const sep = switch (builtin.os) { - Os.windows => '\\', - else => '/', -}; -pub const delimiter = switch (builtin.os) { - Os.windows => ';', - else => ':', -}; +pub const sep_windows = '\\'; +pub const sep_posix = '/'; +pub const sep = if (is_windows) sep_windows else sep_posix; + +pub const delimiter_windows = ';'; +pub const delimiter_posix = ':'; +pub const delimiter = if (is_windows) delimiter_windows else delimiter_posix; + +const is_windows = builtin.os == builtin.Os.windows; /// Naively combines a series of paths with the native path seperator. /// Allocates memory for the result, which must be freed by the caller. pub fn join(allocator: &Allocator, paths: ...) -> %[]u8 { - mem.join(allocator, sep, paths) + if (is_windows) { + return joinWindows(allocator, paths); + } else { + return joinPosix(allocator, paths); + } +} + +pub fn joinWindows(allocator: &Allocator, paths: ...) -> %[]u8 { + return mem.join(allocator, sep_windows, paths); +} + +pub fn joinPosix(allocator: &Allocator, paths: ...) -> %[]u8 { + return mem.join(allocator, sep_posix, paths); } test "os.path.join" { - assert(mem.eql(u8, %%join(&debug.global_allocator, "/a/b", "c"), "/a/b/c")); - assert(mem.eql(u8, %%join(&debug.global_allocator, "/a/b/", "c"), "/a/b/c")); + assert(mem.eql(u8, %%joinWindows(&debug.global_allocator, "c:\\a\\b", "c"), "c:\\a\\b\\c")); + assert(mem.eql(u8, %%joinWindows(&debug.global_allocator, "c:\\a\\b\\", "c"), "c:\\a\\b\\c")); - assert(mem.eql(u8, %%join(&debug.global_allocator, "/", "a", "b/", "c"), "/a/b/c")); - assert(mem.eql(u8, %%join(&debug.global_allocator, "/a/", "b/", "c"), "/a/b/c")); + assert(mem.eql(u8, %%joinWindows(&debug.global_allocator, "c:\\", "a", "b\\", "c"), "c:\\a\\b\\c")); + assert(mem.eql(u8, %%joinWindows(&debug.global_allocator, "c:\\a\\", "b\\", "c"), "c:\\a\\b\\c")); - assert(mem.eql(u8, %%join(&debug.global_allocator, "/home/andy/dev/zig/build/lib/zig/std", "io.zig"), + assert(mem.eql(u8, %%joinWindows(&debug.global_allocator, + "c:\\home\\andy\\dev\\zig\\build\\lib\\zig\\std", "io.zig"), + "c:\\home\\andy\\dev\\zig\\build\\lib\\zig\\std\\io.zig")); + + assert(mem.eql(u8, %%joinPosix(&debug.global_allocator, "/a/b", "c"), "/a/b/c")); + assert(mem.eql(u8, %%joinPosix(&debug.global_allocator, "/a/b/", "c"), "/a/b/c")); + + assert(mem.eql(u8, %%joinPosix(&debug.global_allocator, "/", "a", "b/", "c"), "/a/b/c")); + assert(mem.eql(u8, %%joinPosix(&debug.global_allocator, "/a/", "b/", "c"), "/a/b/c")); + + assert(mem.eql(u8, %%joinPosix(&debug.global_allocator, "/home/andy/dev/zig/build/lib/zig/std", "io.zig"), "/home/andy/dev/zig/build/lib/zig/std/io.zig")); } pub fn isAbsolute(path: []const u8) -> bool { - switch (builtin.os) { - Os.windows => @compileError("Unsupported OS"), - else => return path[0] == sep, + if (is_windows) { + return isAbsoluteWindows(path); + } else { + return isAbsolutePosix(path); } } -/// This function is like a series of `cd` statements executed one after another. -/// The result does not have a trailing path separator. +pub fn isAbsoluteWindows(path: []const u8) -> bool { + if (path[0] == '/') + return true; + + if (path[0] == '\\') { + return true; + } + if (path.len < 3) { + return false; + } + if (path[1] == ':') { + if (path[2] == '/') + return true; + if (path[2] == '\\') + return true; + } + return false; +} + +pub fn isAbsolutePosix(path: []const u8) -> bool { + return path[0] == sep_posix; +} + +test "os.path.isAbsoluteWindows" { + testIsAbsoluteWindows("/", true); + testIsAbsoluteWindows("//", true); + testIsAbsoluteWindows("//server", true); + testIsAbsoluteWindows("//server/file", true); + testIsAbsoluteWindows("\\\\server\\file", true); + testIsAbsoluteWindows("\\\\server", true); + testIsAbsoluteWindows("\\\\", true); + testIsAbsoluteWindows("c", false); + testIsAbsoluteWindows("c:", false); + testIsAbsoluteWindows("c:\\", true); + testIsAbsoluteWindows("c:/", true); + testIsAbsoluteWindows("c://", true); + testIsAbsoluteWindows("C:/Users/", true); + testIsAbsoluteWindows("C:\\Users\\", true); + testIsAbsoluteWindows("C:cwd/another", false); + testIsAbsoluteWindows("C:cwd\\another", false); + testIsAbsoluteWindows("directory/directory", false); + testIsAbsoluteWindows("directory\\directory", false); +} + +test "os.path.isAbsolutePosix" { + testIsAbsolutePosix("/home/foo", true); + testIsAbsolutePosix("/home/foo/..", true); + testIsAbsolutePosix("bar/", false); + testIsAbsolutePosix("./baz", false); +} + +fn testIsAbsoluteWindows(path: []const u8, expected_result: bool) { + assert(isAbsoluteWindows(path) == expected_result); +} + +fn testIsAbsolutePosix(path: []const u8, expected_result: bool) { + assert(isAbsolutePosix(path) == expected_result); +} + +pub fn drive(path: []const u8) -> ?[]const u8 { + if (path.len < 2) + return null; + if (path[1] != ':') + return null; + return path[0..2]; +} + +pub fn networkShare(path: []const u8) -> ?[]const u8 { + if (path.len < "//a/b".len) + return null; + + // TODO when I combined these together with `inline for` the compiler crashed + { + const this_sep = '/'; + const two_sep = []u8{this_sep, this_sep}; + if (mem.startsWith(u8, path, two_sep)) { + if (path[2] == this_sep) + return null; + + var it = mem.split(path, []u8{this_sep}); + _ = (it.next() ?? return null); + _ = (it.next() ?? return null); + return path[0..it.index]; + } + } + { + const this_sep = '\\'; + const two_sep = []u8{this_sep, this_sep}; + if (mem.startsWith(u8, path, two_sep)) { + if (path[2] == this_sep) + return null; + + var it = mem.split(path, []u8{this_sep}); + _ = (it.next() ?? return null); + _ = (it.next() ?? return null); + return path[0..it.index]; + } + } + return null; +} + +test "os.path.networkShare" { + assert(mem.eql(u8, ??networkShare("//a/b"), "//a/b")); + assert(mem.eql(u8, ??networkShare("\\\\a\\b"), "\\\\a\\b")); + + assert(networkShare("\\\\a\\") == null); +} + +pub fn diskDesignator(path: []const u8) -> []const u8 { + if (!is_windows) + return ""; + + return drive(path) ?? (networkShare(path) ?? []u8{}); +} + +// TODO ASCII is wrong, we actually need full unicode support to compare paths. +fn networkShareServersEql(ns1: []const u8, ns2: []const u8) -> bool { + const sep1 = ns1[0]; + const sep2 = ns2[0]; + + var it1 = mem.split(ns1, []u8{sep1}); + var it2 = mem.split(ns2, []u8{sep2}); + + return asciiEqlIgnoreCase(??it1.next(), ??it2.next()); +} + +fn asciiUpper(byte: u8) -> u8 { + return switch (byte) { + 'a' ... 'z' => 'A' + (byte - 'a'), + else => byte, + }; +} + +fn asciiEqlIgnoreCase(s1: []const u8, s2: []const u8) -> bool { + if (s1.len != s2.len) + return false; + var i: usize = 0; + while (i < s1.len) : (i += 1) { + if (asciiUpper(s1[i]) != asciiUpper(s2[i])) + return false; + } + return true; +} + +/// Converts the command line arguments into a slice and calls `resolveSlice`. pub fn resolve(allocator: &Allocator, args: ...) -> %[]u8 { var paths: [args.len][]const u8 = undefined; comptime var arg_i = 0; @@ -55,18 +222,178 @@ pub fn resolve(allocator: &Allocator, args: ...) -> %[]u8 { return resolveSlice(allocator, paths); } +/// On Windows, this calls `resolveWindows` and on POSIX it calls `resolvePosix`. pub fn resolveSlice(allocator: &Allocator, paths: []const []const u8) -> %[]u8 { - if (builtin.os == builtin.Os.windows) { - @compileError("TODO implement os.path.resolve for windows"); + if (is_windows) { + return resolveWindows(allocator, paths); + } else { + return resolvePosix(allocator, paths); } - if (paths.len == 0) +} + +/// This function is like a series of `cd` statements executed one after another. +/// It resolves "." and "..". +/// The result does not have a trailing path separator. +/// If all paths are relative it uses the current working directory as a starting point. +/// Each drive has its own current working directory. +/// Path separators are canonicalized to '\\' and drives are canonicalized to capital letters. +pub fn resolveWindows(allocator: &Allocator, paths: []const []const u8) -> %[]u8 { + if (paths.len == 0) { + assert(is_windows); // resolveWindows called on non windows can't use getCwd return os.getCwd(allocator); + } + + // determine which drive we want to result with + var result_drive_upcase: ?u8 = null; + var have_abs = false; + var first_index: usize = 0; + var max_size: usize = 0; + for (paths) |p, i| { + const is_abs = isAbsoluteWindows(p); + if (is_abs) { + have_abs = true; + first_index = i; + max_size = 0; + } + if (drive(p)) |d| { + result_drive_upcase = asciiUpper(d[0]); + } else if (networkShare(p)) |_| { + result_drive_upcase = null; + } + max_size += p.len + 1; + } + + + // if we will result with a drive, loop again to determine + // which is the first time the drive is absolutely specified, if any + // and count up the max bytes for paths related to this drive + if (result_drive_upcase) |res_dr| { + have_abs = false; + first_index = 0; + max_size = "_:".len; + var correct_drive = false; + + for (paths) |p, i| { + if (drive(p)) |dr| { + correct_drive = asciiUpper(dr[0]) == res_dr; + } else if (networkShare(p)) |_| { + continue; + } + if (!correct_drive) { + continue; + } + const is_abs = isAbsoluteWindows(p); + if (is_abs) { + first_index = i; + max_size = "_:".len; + have_abs = true; + } + max_size += p.len + 1; + } + } + + var drive_buf = "_:"; + var result: []u8 = undefined; + var result_index: usize = 0; + var root_slice: []const u8 = undefined; + + if (have_abs) { + result = %return allocator.alloc(u8, max_size); + + if (result_drive_upcase) |res_dr| { + drive_buf[0] = res_dr; + root_slice = drive_buf[0..]; + + mem.copy(u8, result, root_slice); + result_index += root_slice.len; + } else { + // We know it looks like //a/b or \\a\b because of earlier code + var it = mem.split(paths[first_index], "/\\"); + const server_name = ??it.next(); + const other_name = ??it.next(); + + result[result_index] = '\\'; + result_index += 1; + result[result_index] = '\\'; + result_index += 1; + mem.copy(u8, result[result_index..], server_name); + result_index += server_name.len; + result[result_index] = '\\'; + result_index += 1; + mem.copy(u8, result[result_index..], other_name); + result_index += other_name.len; + + root_slice = result[0..result_index]; + } + } else { + assert(is_windows); // resolveWindows called on non windows can't use getCwd + // TODO get cwd for result_drive if applicable + const cwd = %return os.getCwd(allocator); + defer allocator.free(cwd); + result = %return allocator.alloc(u8, max_size + cwd.len + 1); + mem.copy(u8, result, cwd); + result_index += cwd.len; + + root_slice = diskDesignator(result[0..result_index]); + } + %defer allocator.free(result); + + var correct_drive = true; + for (paths[first_index..]) |p, i| { + if (result_drive_upcase) |res_dr| { + if (drive(p)) |dr| { + correct_drive = asciiUpper(dr[0]) == res_dr; + } else if (networkShare(p)) |_| { + continue; + } + if (!correct_drive) { + continue; + } + } + var it = mem.split(p[diskDesignator(p).len..], "/\\"); + while (it.next()) |component| { + if (mem.eql(u8, component, ".")) { + continue; + } else if (mem.eql(u8, component, "..")) { + while (true) { + if (result_index == 0 or result_index == root_slice.len) + break; + result_index -= 1; + if (result[result_index] == '\\' or result[result_index] == '/') + break; + } + } else { + result[result_index] = sep_windows; + result_index += 1; + mem.copy(u8, result[result_index..], component); + result_index += component.len; + } + } + } + + if (result_index == root_slice.len) { + result[result_index] = '\\'; + result_index += 1; + } + + return result[0..result_index]; +} + +/// This function is like a series of `cd` statements executed one after another. +/// It resolves "." and "..". +/// The result does not have a trailing path separator. +/// If all paths are relative it uses the current working directory as a starting point. +pub fn resolvePosix(allocator: &Allocator, paths: []const []const u8) -> %[]u8 { + if (paths.len == 0) { + assert(!is_windows); // resolvePosix called on windows can't use getCwd + return os.getCwd(allocator); + } var first_index: usize = 0; var have_abs = false; var max_size: usize = 0; for (paths) |p, i| { - if (isAbsolute(p)) { + if (isAbsolutePosix(p)) { first_index = i; have_abs = true; max_size = 0; @@ -80,6 +407,7 @@ pub fn resolveSlice(allocator: &Allocator, paths: []const []const u8) -> %[]u8 { if (have_abs) { result = %return allocator.alloc(u8, max_size); } else { + assert(!is_windows); // resolvePosix called on windows can't use getCwd const cwd = %return os.getCwd(allocator); defer allocator.free(cwd); result = %return allocator.alloc(u8, max_size + cwd.len + 1); @@ -89,7 +417,7 @@ pub fn resolveSlice(allocator: &Allocator, paths: []const []const u8) -> %[]u8 { %defer allocator.free(result); for (paths[first_index..]) |p, i| { - var it = mem.split(p, '/'); + var it = mem.split(p, "/"); while (it.next()) |component| { if (mem.eql(u8, component, ".")) { continue; @@ -119,22 +447,95 @@ pub fn resolveSlice(allocator: &Allocator, paths: []const []const u8) -> %[]u8 { } test "os.path.resolve" { - assert(mem.eql(u8, testResolve("/a/b", "c"), "/a/b/c")); - assert(mem.eql(u8, testResolve("/a/b", "c", "//d", "e///"), "/d/e")); - assert(mem.eql(u8, testResolve("/a/b/c", "..", "../"), "/a")); - assert(mem.eql(u8, testResolve("/", "..", ".."), "/")); - assert(mem.eql(u8, testResolve("/a/b/c/"), "/a/b/c")); + const cwd = %%os.getCwd(&debug.global_allocator); + if (is_windows) { + assert(mem.eql(u8, testResolveWindows([][]const u8{"."}), cwd)); + } else { + assert(mem.eql(u8, testResolvePosix([][]const u8{"a/b/c/", "../../.."}), cwd)); + assert(mem.eql(u8, testResolvePosix([][]const u8{"."}), cwd)); + } } -fn testResolve(args: ...) -> []u8 { - return %%resolve(&debug.global_allocator, args); + +test "os.path.resolveWindows" { + assert(mem.eql(u8, testResolveWindows([][]const u8{"c:/blah\\blah", "d:/games", "c:../a"}), "C:\\blah\\a")); + assert(mem.eql(u8, testResolveWindows([][]const u8{"c:/blah\\blah", "d:/games", "C:../a"}), "C:\\blah\\a")); + assert(mem.eql(u8, testResolveWindows([][]const u8{"c:/ignore", "d:\\a/b\\c/d", "\\e.exe"}), "D:\\e.exe")); + assert(mem.eql(u8, testResolveWindows([][]const u8{"c:/ignore", "c:/some/file"}), "C:\\some\\file")); + assert(mem.eql(u8, testResolveWindows([][]const u8{"d:/ignore", "d:some/dir//"}), "D:\\ignore\\some\\dir")); + assert(mem.eql(u8, testResolveWindows([][]const u8{"//server/share", "..", "relative\\"}), "\\\\server\\share\\relative")); + assert(mem.eql(u8, testResolveWindows([][]const u8{"c:/", "//"}), "C:\\")); + assert(mem.eql(u8, testResolveWindows([][]const u8{"c:/", "//dir"}), "C:\\dir")); + assert(mem.eql(u8, testResolveWindows([][]const u8{"c:/", "//server/share"}), "\\\\server\\share\\")); + assert(mem.eql(u8, testResolveWindows([][]const u8{"c:/", "//server//share"}), "\\\\server\\share\\")); + assert(mem.eql(u8, testResolveWindows([][]const u8{"c:/", "///some//dir"}), "C:\\some\\dir")); + assert(mem.eql(u8, testResolveWindows([][]const u8{"C:\\foo\\tmp.3\\", "..\\tmp.3\\cycles\\root.js"}), + "C:\\foo\\tmp.3\\cycles\\root.js")); +} + +test "os.path.resolvePosix" { + assert(mem.eql(u8, testResolvePosix([][]const u8{"/a/b", "c"}), "/a/b/c")); + assert(mem.eql(u8, testResolvePosix([][]const u8{"/a/b", "c", "//d", "e///"}), "/d/e")); + assert(mem.eql(u8, testResolvePosix([][]const u8{"/a/b/c", "..", "../"}), "/a")); + assert(mem.eql(u8, testResolvePosix([][]const u8{"/", "..", ".."}), "/")); + assert(mem.eql(u8, testResolvePosix([][]const u8{"/a/b/c/"}), "/a/b/c")); + + assert(mem.eql(u8, testResolvePosix([][]const u8{"/var/lib", "../", "file/"}), "/var/file")); + assert(mem.eql(u8, testResolvePosix([][]const u8{"/var/lib", "/../", "file/"}), "/file")); + assert(mem.eql(u8, testResolvePosix([][]const u8{"/some/dir", ".", "/absolute/"}), "/absolute")); + assert(mem.eql(u8, testResolvePosix([][]const u8{"/foo/tmp.3/", "../tmp.3/cycles/root.js"}), "/foo/tmp.3/cycles/root.js")); +} + +fn testResolveWindows(paths: []const []const u8) -> []u8 { + return %%resolveWindows(&debug.global_allocator, paths); +} + +fn testResolvePosix(paths: []const []const u8) -> []u8 { + return %%resolvePosix(&debug.global_allocator, paths); } pub fn dirname(path: []const u8) -> []const u8 { - if (builtin.os == builtin.Os.windows) { - @compileError("TODO implement os.path.dirname for windows"); + if (is_windows) { + return dirnameWindows(path); + } else { + return dirnamePosix(path); } +} + +pub fn dirnameWindows(path: []const u8) -> []const u8 { if (path.len == 0) return path[0..0]; + + const root_slice = diskDesignator(path); + if (path.len == root_slice.len) + return path; + + const have_root_slash = path.len > root_slice.len and (path[root_slice.len] == '/' or path[root_slice.len] == '\\'); + + var end_index: usize = path.len - 1; + + while ((path[end_index] == '/' or path[end_index] == '\\') and end_index > root_slice.len) { + if (end_index == 0) + return path[0..0]; + end_index -= 1; + } + + while (path[end_index] != '/' and path[end_index] != '\\' and end_index > root_slice.len) { + if (end_index == 0) + return path[0..0]; + end_index -= 1; + } + + if (have_root_slash and end_index == root_slice.len) { + end_index += 1; + } + + return path[0..end_index]; +} + +pub fn dirnamePosix(path: []const u8) -> []const u8 { + if (path.len == 0) + return path[0..0]; + var end_index: usize = path.len - 1; while (path[end_index] == '/') { if (end_index == 0) @@ -154,25 +555,71 @@ pub fn dirname(path: []const u8) -> []const u8 { return path[0..end_index]; } -test "os.path.dirname" { - testDirname("/a/b/c", "/a/b"); - testDirname("/a/b/c///", "/a/b"); - testDirname("/a", "/"); - testDirname("/", "/"); - testDirname("////", "/"); - testDirname("", ""); - testDirname("a", ""); - testDirname("a/", ""); - testDirname("a//", ""); +test "os.path.dirnamePosix" { + testDirnamePosix("/a/b/c", "/a/b"); + testDirnamePosix("/a/b/c///", "/a/b"); + testDirnamePosix("/a", "/"); + testDirnamePosix("/", "/"); + testDirnamePosix("////", "/"); + testDirnamePosix("", ""); + testDirnamePosix("a", ""); + testDirnamePosix("a/", ""); + testDirnamePosix("a//", ""); } -fn testDirname(input: []const u8, expected_output: []const u8) { - assert(mem.eql(u8, dirname(input), expected_output)); + +test "os.path.dirnameWindows" { + testDirnameWindows("c:\\", "c:\\"); + testDirnameWindows("c:\\foo", "c:\\"); + testDirnameWindows("c:\\foo\\", "c:\\"); + testDirnameWindows("c:\\foo\\bar", "c:\\foo"); + testDirnameWindows("c:\\foo\\bar\\", "c:\\foo"); + testDirnameWindows("c:\\foo\\bar\\baz", "c:\\foo\\bar"); + testDirnameWindows("\\", "\\"); + testDirnameWindows("\\foo", "\\"); + testDirnameWindows("\\foo\\", "\\"); + testDirnameWindows("\\foo\\bar", "\\foo"); + testDirnameWindows("\\foo\\bar\\", "\\foo"); + testDirnameWindows("\\foo\\bar\\baz", "\\foo\\bar"); + testDirnameWindows("c:", "c:"); + testDirnameWindows("c:foo", "c:"); + testDirnameWindows("c:foo\\", "c:"); + testDirnameWindows("c:foo\\bar", "c:foo"); + testDirnameWindows("c:foo\\bar\\", "c:foo"); + testDirnameWindows("c:foo\\bar\\baz", "c:foo\\bar"); + testDirnameWindows("file:stream", ""); + testDirnameWindows("dir\\file:stream", "dir"); + testDirnameWindows("\\\\unc\\share", "\\\\unc\\share"); + testDirnameWindows("\\\\unc\\share\\foo", "\\\\unc\\share\\"); + testDirnameWindows("\\\\unc\\share\\foo\\", "\\\\unc\\share\\"); + testDirnameWindows("\\\\unc\\share\\foo\\bar", "\\\\unc\\share\\foo"); + testDirnameWindows("\\\\unc\\share\\foo\\bar\\", "\\\\unc\\share\\foo"); + testDirnameWindows("\\\\unc\\share\\foo\\bar\\baz", "\\\\unc\\share\\foo\\bar"); + testDirnameWindows("/a/b/", "/a"); + testDirnameWindows("/a/b", "/a"); + testDirnameWindows("/a", "/"); + testDirnameWindows("", ""); + testDirnameWindows("/", "/"); + testDirnameWindows("////", "/"); + testDirnameWindows("foo", ""); +} + +fn testDirnamePosix(input: []const u8, expected_output: []const u8) { + assert(mem.eql(u8, dirnamePosix(input), expected_output)); +} + +fn testDirnameWindows(input: []const u8, expected_output: []const u8) { + assert(mem.eql(u8, dirnameWindows(input), expected_output)); } pub fn basename(path: []const u8) -> []const u8 { - if (builtin.os == builtin.Os.windows) { - @compileError("TODO implement os.path.basename for windows"); + if (is_windows) { + return basenameWindows(path); + } else { + return basenamePosix(path); } +} + +pub fn basenamePosix(path: []const u8) -> []const u8 { if (path.len == 0) return []u8{}; @@ -193,6 +640,38 @@ pub fn basename(path: []const u8) -> []const u8 { return path[start_index + 1..end_index]; } +pub fn basenameWindows(path: []const u8) -> []const u8 { + if (path.len == 0) + return []u8{}; + + var end_index: usize = path.len - 1; + while (true) { + const byte = path[end_index]; + if (byte == '/' or byte == '\\') { + if (end_index == 0) + return []u8{}; + end_index -= 1; + continue; + } + if (byte == ':' and end_index == 1) { + return []u8{}; + } + break; + } + + var start_index: usize = end_index; + end_index += 1; + while (path[start_index] != '/' and path[start_index] != '\\' and + !(path[start_index] == ':' and start_index == 1)) + { + if (start_index == 0) + return path[0..end_index]; + start_index -= 1; + } + + return path[start_index + 1..end_index]; +} + test "os.path.basename" { testBasename("", ""); testBasename("/", ""); @@ -206,26 +685,137 @@ test "os.path.basename" { testBasename("/aaa/b", "b"); testBasename("/a/b", "b"); testBasename("//a", "a"); + + testBasenamePosix("\\dir\\basename.ext", "\\dir\\basename.ext"); + testBasenamePosix("\\basename.ext", "\\basename.ext"); + testBasenamePosix("basename.ext", "basename.ext"); + testBasenamePosix("basename.ext\\", "basename.ext\\"); + testBasenamePosix("basename.ext\\\\", "basename.ext\\\\"); + testBasenamePosix("foo", "foo"); + + testBasenameWindows("\\dir\\basename.ext", "basename.ext"); + testBasenameWindows("\\basename.ext", "basename.ext"); + testBasenameWindows("basename.ext", "basename.ext"); + testBasenameWindows("basename.ext\\", "basename.ext"); + testBasenameWindows("basename.ext\\\\", "basename.ext"); + testBasenameWindows("foo", "foo"); + testBasenameWindows("C:", ""); + testBasenameWindows("C:.", "."); + testBasenameWindows("C:\\", ""); + testBasenameWindows("C:\\dir\\base.ext", "base.ext"); + testBasenameWindows("C:\\basename.ext", "basename.ext"); + testBasenameWindows("C:basename.ext", "basename.ext"); + testBasenameWindows("C:basename.ext\\", "basename.ext"); + testBasenameWindows("C:basename.ext\\\\", "basename.ext"); + testBasenameWindows("C:foo", "foo"); + testBasenameWindows("file:stream", "file:stream"); } + fn testBasename(input: []const u8, expected_output: []const u8) { assert(mem.eql(u8, basename(input), expected_output)); } -/// Returns the relative path from ::from to ::to. If ::from and ::to each -/// resolve to the same path (after calling ::resolve on each), a zero-length +fn testBasenamePosix(input: []const u8, expected_output: []const u8) { + assert(mem.eql(u8, basenamePosix(input), expected_output)); +} + +fn testBasenameWindows(input: []const u8, expected_output: []const u8) { + assert(mem.eql(u8, basenameWindows(input), expected_output)); +} + +/// Returns the relative path from `from` to `to`. If `from` and `to` each +/// resolve to the same path (after calling `resolve` on each), a zero-length /// string is returned. +/// On Windows this canonicalizes the drive to a capital letter and paths to `\\`. pub fn relative(allocator: &Allocator, from: []const u8, to: []const u8) -> %[]u8 { - if (builtin.os == builtin.Os.windows) { - @compileError("TODO implement os.path.relative for windows"); + if (is_windows) { + return relativeWindows(allocator, from, to); + } else { + return relativePosix(allocator, from, to); } - const resolved_from = %return resolve(allocator, from); +} + +pub fn relativeWindows(allocator: &Allocator, from: []const u8, to: []const u8) -> %[]u8 { + const resolved_from = %return resolveWindows(allocator, [][]const u8{from}); defer allocator.free(resolved_from); - const resolved_to = %return resolve(allocator, to); + var clean_up_resolved_to = true; + const resolved_to = %return resolveWindows(allocator, [][]const u8{to}); + defer if (clean_up_resolved_to) allocator.free(resolved_to); + + const result_is_to = if (drive(resolved_to)) |to_drive| { + if (drive(resolved_from)) |from_drive| { + asciiUpper(from_drive[0]) != asciiUpper(to_drive[0]) + } else { + true + } + } else if (networkShare(resolved_to)) |to_ns| { + if (networkShare(resolved_from)) |from_ns| { + !networkShareServersEql(to_ns, from_ns) + } else { + true + } + } else { + unreachable + }; + if (result_is_to) { + clean_up_resolved_to = false; + return resolved_to; + } + + var from_it = mem.split(resolved_from, "/\\"); + var to_it = mem.split(resolved_to, "/\\"); + while (true) { + const from_component = from_it.next() ?? return mem.dupe(allocator, u8, to_it.rest()); + const to_rest = to_it.rest(); + if (to_it.next()) |to_component| { + // TODO ASCII is wrong, we actually need full unicode support to compare paths. + if (asciiEqlIgnoreCase(from_component, to_component)) + continue; + } + var up_count: usize = 1; + while (from_it.next()) |_| { + up_count += 1; + } + const up_index_end = up_count * "..\\".len; + const result = %return allocator.alloc(u8, up_index_end + to_rest.len); + %defer allocator.free(result); + + var result_index: usize = 0; + while (result_index < up_index_end) { + result[result_index] = '.'; + result_index += 1; + result[result_index] = '.'; + result_index += 1; + result[result_index] = '\\'; + result_index += 1; + } + // shave off the trailing slash + result_index -= 1; + + var rest_it = mem.split(to_rest, "/\\"); + while (rest_it.next()) |to_component| { + result[result_index] = '\\'; + result_index += 1; + mem.copy(u8, result[result_index..], to_component); + result_index += to_component.len; + } + + return result[0..result_index]; + } + + return []u8{}; +} + +pub fn relativePosix(allocator: &Allocator, from: []const u8, to: []const u8) -> %[]u8 { + const resolved_from = %return resolvePosix(allocator, [][]const u8{from}); + defer allocator.free(resolved_from); + + const resolved_to = %return resolvePosix(allocator, [][]const u8{to}); defer allocator.free(resolved_to); - var from_it = mem.split(resolved_from, '/'); - var to_it = mem.split(resolved_to, '/'); + var from_it = mem.split(resolved_from, "/"); + var to_it = mem.split(resolved_to, "/"); while (true) { const from_component = from_it.next() ?? return mem.dupe(allocator, u8, to_it.rest()); const to_rest = to_it.rest(); @@ -263,21 +853,52 @@ pub fn relative(allocator: &Allocator, from: []const u8, to: []const u8) -> %[]u } test "os.path.relative" { - testRelative("/var/lib", "/var", ".."); - testRelative("/var/lib", "/bin", "../../bin"); - testRelative("/var/lib", "/var/lib", ""); - testRelative("/var/lib", "/var/apache", "../apache"); - testRelative("/var/", "/var/lib", "lib"); - testRelative("/", "/var/lib", "var/lib"); - testRelative("/foo/test", "/foo/test/bar/package.json", "bar/package.json"); - testRelative("/Users/a/web/b/test/mails", "/Users/a/web/b", "../.."); - testRelative("/foo/bar/baz-quux", "/foo/bar/baz", "../baz"); - testRelative("/foo/bar/baz", "/foo/bar/baz-quux", "../baz-quux"); - testRelative("/baz-quux", "/baz", "../baz"); - testRelative("/baz", "/baz-quux", "../baz-quux"); + testRelativeWindows("c:/blah\\blah", "d:/games", "D:\\games"); + testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa", ".."); + testRelativeWindows("c:/aaaa/bbbb", "c:/cccc", "..\\..\\cccc"); + testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa/bbbb", ""); + testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa/cccc", "..\\cccc"); + testRelativeWindows("c:/aaaa/", "c:/aaaa/cccc", "cccc"); + testRelativeWindows("c:/", "c:\\aaaa\\bbbb", "aaaa\\bbbb"); + testRelativeWindows("c:/aaaa/bbbb", "d:\\", "D:\\"); + testRelativeWindows("c:/AaAa/bbbb", "c:/aaaa/bbbb", ""); + testRelativeWindows("c:/aaaaa/", "c:/aaaa/cccc", "..\\aaaa\\cccc"); + testRelativeWindows("C:\\foo\\bar\\baz\\quux", "C:\\", "..\\..\\..\\.."); + testRelativeWindows("C:\\foo\\test", "C:\\foo\\test\\bar\\package.json", "bar\\package.json"); + testRelativeWindows("C:\\foo\\bar\\baz-quux", "C:\\foo\\bar\\baz", "..\\baz"); + testRelativeWindows("C:\\foo\\bar\\baz", "C:\\foo\\bar\\baz-quux", "..\\baz-quux"); + testRelativeWindows("\\\\foo\\bar", "\\\\foo\\bar\\baz", "baz"); + testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar", ".."); + testRelativeWindows("\\\\foo\\bar\\baz-quux", "\\\\foo\\bar\\baz", "..\\baz"); + testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz-quux", "..\\baz-quux"); + testRelativeWindows("C:\\baz-quux", "C:\\baz", "..\\baz"); + testRelativeWindows("C:\\baz", "C:\\baz-quux", "..\\baz-quux"); + testRelativeWindows("\\\\foo\\baz-quux", "\\\\foo\\baz", "..\\baz"); + testRelativeWindows("\\\\foo\\baz", "\\\\foo\\baz-quux", "..\\baz-quux"); + testRelativeWindows("C:\\baz", "\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz"); + testRelativeWindows("\\\\foo\\bar\\baz", "C:\\baz", "C:\\baz"); + + testRelativePosix("/var/lib", "/var", ".."); + testRelativePosix("/var/lib", "/bin", "../../bin"); + testRelativePosix("/var/lib", "/var/lib", ""); + testRelativePosix("/var/lib", "/var/apache", "../apache"); + testRelativePosix("/var/", "/var/lib", "lib"); + testRelativePosix("/", "/var/lib", "var/lib"); + testRelativePosix("/foo/test", "/foo/test/bar/package.json", "bar/package.json"); + testRelativePosix("/Users/a/web/b/test/mails", "/Users/a/web/b", "../.."); + testRelativePosix("/foo/bar/baz-quux", "/foo/bar/baz", "../baz"); + testRelativePosix("/foo/bar/baz", "/foo/bar/baz-quux", "../baz-quux"); + testRelativePosix("/baz-quux", "/baz", "../baz"); + testRelativePosix("/baz", "/baz-quux", "../baz-quux"); } -fn testRelative(from: []const u8, to: []const u8, expected_output: []const u8) { - const result = %%relative(&debug.global_allocator, from, to); + +fn testRelativePosix(from: []const u8, to: []const u8, expected_output: []const u8) { + const result = %%relativePosix(&debug.global_allocator, from, to); + assert(mem.eql(u8, result, expected_output)); +} + +fn testRelativeWindows(from: []const u8, to: []const u8, expected_output: []const u8) { + const result = %%relativeWindows(&debug.global_allocator, from, to); assert(mem.eql(u8, result, expected_output)); } diff --git a/std/os/windows/index.zig b/std/os/windows/index.zig index 8922de8599..83e96d6777 100644 --- a/std/os/windows/index.zig +++ b/std/os/windows/index.zig @@ -13,6 +13,8 @@ pub extern "kernel32" stdcallcc fn GetCommandLine() -> LPTSTR; pub extern "kernel32" stdcallcc fn GetConsoleMode(in_hConsoleHandle: HANDLE, out_lpMode: &DWORD) -> bool; +pub extern "kernel32" stdcallcc fn GetCurrentDirectoryA(nBufferLength: WORD, lpBuffer: ?LPTSTR) -> DWORD; + /// Retrieves the calling thread's last-error code value. The last-error code is maintained on a per-thread basis. /// Multiple threads do not overwrite each other's last-error code. pub extern "kernel32" stdcallcc fn GetLastError() -> DWORD; @@ -50,7 +52,7 @@ pub extern "user32" stdcallcc fn MessageBoxA(hWnd: ?HANDLE, lpText: ?LPCTSTR, lp pub const PROV_RSA_FULL = 1; pub const UNICODE = false; -pub const LPTSTR = if (unicode) LPWSTR else LPSTR; +pub const LPTSTR = if (UNICODE) LPWSTR else LPSTR; pub const LPWSTR = &WCHAR; pub const LPSTR = &CHAR; pub const CHAR = u8; @@ -59,6 +61,7 @@ pub const SIZE_T = usize; pub const BOOL = bool; pub const BYTE = u8; +pub const WORD = u16; pub const DWORD = u32; pub const FLOAT = f32; pub const HANDLE = &c_void;