aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorgingerBill <gingerBill@users.noreply.github.com>2025-03-24 11:19:21 +0000
committerGitHub <noreply@github.com>2025-03-24 11:19:21 +0000
commit4a595f9dac935a4fc9a0f7905e5d9ab81cb98fbe (patch)
tree220633f7be35fe254b667293fc60e5c20961aa43
parent6fd752f647525d82f9d1541715f3317735bcab25 (diff)
parent649376fcfe330090b8c6020e29b0600ca7dd9a3b (diff)
Merge pull request #4954 from Feoramund/os2-path
Add new path API for `os2`
-rw-r--r--core/os/os2/path.odin415
-rw-r--r--core/os/os2/path_linux.odin2
-rw-r--r--core/os/os2/path_posix.odin5
-rw-r--r--core/os/os2/path_posixfs.odin78
-rw-r--r--core/os/os2/path_wasi.odin5
-rw-r--r--core/os/os2/path_windows.odin96
-rw-r--r--core/os/os2/process_linux.odin5
-rw-r--r--core/os/os2/process_posix.odin3
-rw-r--r--core/os/os2/stat.odin3
-rw-r--r--core/os/os2/stat_linux.odin3
-rw-r--r--core/os/os2/stat_posix.odin5
-rw-r--r--core/os/os2/stat_wasi.odin3
-rw-r--r--core/os/os2/stat_windows.odin68
-rw-r--r--tests/core/os/os2/dir.odin34
-rw-r--r--tests/core/os/os2/file.odin6
-rw-r--r--tests/core/os/os2/path.odin350
16 files changed, 995 insertions, 86 deletions
diff --git a/core/os/os2/path.odin b/core/os/os2/path.odin
index 9231307f5..e62ee11bc 100644
--- a/core/os/os2/path.odin
+++ b/core/os/os2/path.odin
@@ -1,13 +1,20 @@
package os2
+import "base:intrinsics"
import "base:runtime"
-import "core:path/filepath"
+import "core:strings"
Path_Separator :: _Path_Separator // OS-Specific
Path_Separator_String :: _Path_Separator_String // OS-Specific
Path_List_Separator :: _Path_List_Separator // OS-Specific
+#assert(_Path_Separator <= rune(0x7F), "The system-specific path separator rune is expected to be within the 7-bit ASCII character set.")
+
+/*
+Return true if `c` is a character used to separate paths into directory and
+file hierarchies on the current system.
+*/
@(require_results)
is_path_separator :: proc(c: byte) -> bool {
return _is_path_separator(c)
@@ -15,22 +22,42 @@ is_path_separator :: proc(c: byte) -> bool {
mkdir :: make_directory
+/*
+Make a new directory.
+
+If `path` is relative, it will be relative to the process's current working directory.
+*/
make_directory :: proc(name: string, perm: int = 0o755) -> Error {
return _mkdir(name, perm)
}
mkdir_all :: make_directory_all
+/*
+Make a new directory, creating new intervening directories when needed.
+
+If `path` is relative, it will be relative to the process's current working directory.
+*/
make_directory_all :: proc(path: string, perm: int = 0o755) -> Error {
return _mkdir_all(path, perm)
}
+/*
+Delete `path` and all files and directories inside of `path` if it is a directory.
+
+If `path` is relative, it will be relative to the process's current working directory.
+*/
remove_all :: proc(path: string) -> Error {
return _remove_all(path)
}
getwd :: get_working_directory
+/*
+Get the working directory of the current process.
+
+*Allocates Using Provided Allocator*
+*/
@(require_results)
get_working_directory :: proc(allocator: runtime.Allocator) -> (dir: string, err: Error) {
return _get_working_directory(allocator)
@@ -38,16 +65,400 @@ get_working_directory :: proc(allocator: runtime.Allocator) -> (dir: string, err
setwd :: set_working_directory
+/*
+Change the working directory of the current process.
+
+*Allocates Using Provided Allocator*
+*/
set_working_directory :: proc(dir: string) -> (err: Error) {
return _set_working_directory(dir)
}
+/*
+Get the path for the currently running executable.
+
+*Allocates Using Provided Allocator*
+*/
+@(require_results)
get_executable_path :: proc(allocator: runtime.Allocator) -> (path: string, err: Error) {
return _get_executable_path(allocator)
}
+/*
+Get the directory for the currently running executable.
+
+*Allocates Using Provided Allocator*
+*/
+@(require_results)
get_executable_directory :: proc(allocator: runtime.Allocator) -> (path: string, err: Error) {
path = _get_executable_path(allocator) or_return
- path, _ = filepath.split(path)
+ path, _ = split_path(path)
+ return
+}
+
+/*
+Compare two paths for exactness without normalization.
+
+This procedure takes into account case-sensitivity on differing systems.
+*/
+@(require_results)
+are_paths_identical :: proc(a, b: string) -> (identical: bool) {
+ return _are_paths_identical(a, b)
+}
+
+/*
+Normalize a path.
+
+*Allocates Using Provided Allocator*
+
+This will remove duplicate separators and unneeded references to the current or
+parent directory.
+*/
+@(require_results)
+clean_path :: proc(path: string, allocator: runtime.Allocator) -> (cleaned: string, err: Error) {
+ if path == "" || path == "." {
+ return strings.clone(".", allocator)
+ }
+
+ TEMP_ALLOCATOR_GUARD()
+
+ // The extra byte is to simplify appending path elements by letting the
+ // loop to end each with a separator. We'll trim the last one when we're done.
+ buffer := make([]u8, len(path) + 1, temp_allocator()) or_return
+
+ // This is the only point where Windows and POSIX differ, as Windows has
+ // alphabet-based volumes for root paths.
+ rooted, start := _clean_path_handle_start(path, buffer)
+
+ head, buffer_i := start, start
+ for i, j := start, start; i <= len(path); i += 1 {
+ if i == len(path) || _is_path_separator(path[i]) {
+ elem := path[j:i]
+ j = i + 1
+
+ switch elem {
+ case "", ".":
+ // Skip duplicate path separators and current directory references.
+ case "..":
+ if !rooted && buffer_i == head {
+ // Only allow accessing further parent directories when the path is relative.
+ buffer[buffer_i] = '.'
+ buffer[buffer_i+1] = '.'
+ buffer[buffer_i+2] = _Path_Separator
+ buffer_i += 3
+ head = buffer_i
+ } else {
+ // Roll back to the last separator or the head of the buffer.
+ back_to := head
+ // `buffer_i` will be equal to 1 + the last set byte, so
+ // skipping two bytes avoids the final separator we just
+ // added.
+ for k := buffer_i-2; k >= head; k -= 1 {
+ if _is_path_separator(buffer[k]) {
+ back_to = k + 1
+ break
+ }
+ }
+ buffer_i = back_to
+ }
+ case:
+ // Copy the path element verbatim and add a separator.
+ intrinsics.mem_copy_non_overlapping(raw_data(buffer[buffer_i:]), raw_data(elem), len(elem))
+ buffer_i += len(elem)
+ buffer[buffer_i] = _Path_Separator
+ buffer_i += 1
+ }
+ }
+ }
+
+ // Trim the final separator.
+ // NOTE: No need to check if the last byte is a separator, as we always add it.
+ if buffer_i > start {
+ buffer_i -= 1
+ }
+
+ if buffer_i == 0 {
+ return strings.clone(".", allocator)
+ }
+
+ compact := make([]u8, buffer_i, allocator) or_return
+ intrinsics.mem_copy_non_overlapping(raw_data(compact), raw_data(buffer), buffer_i)
+ return string(compact), nil
+}
+
+/*
+Return true if `path` is an absolute path as opposed to a relative one.
+*/
+@(require_results)
+is_absolute_path :: proc(path: string) -> bool {
+ return _is_absolute_path(path)
+}
+
+/*
+Get the absolute path to `path` with respect to the process's current directory.
+
+*Allocates Using Provided Allocator*
+*/
+@(require_results)
+get_absolute_path :: proc(path: string, allocator: runtime.Allocator) -> (absolute_path: string, err: Error) {
+ return _get_absolute_path(path, allocator)
+}
+
+/*
+Get the relative path needed to change directories from `base` to `target`.
+
+*Allocates Using Provided Allocator*
+
+The result is such that `join_path(base, get_relative_path(base, target))` is equivalent to `target`.
+
+NOTE: This procedure expects both `base` and `target` to be normalized first,
+which can be done by calling `clean_path` on them if needed.
+
+This procedure will return an `Invalid_Path` error if `base` begins with a
+reference to the parent directory (`".."`). Use `get_working_directory` with
+`join_path` to construct absolute paths for both arguments instead.
+*/
+@(require_results)
+get_relative_path :: proc(base, target: string, allocator: runtime.Allocator) -> (path: string, err: Error) {
+ if _are_paths_identical(base, target) {
+ return strings.clone(".", allocator)
+ }
+ if base == "." {
+ return strings.clone(target, allocator)
+ }
+
+ // This is the first point where Windows and POSIX differ, as Windows has
+ // alphabet-based volumes for root paths.
+ if !_get_relative_path_handle_start(base, target) {
+ return "", .Invalid_Path
+ }
+ if strings.has_prefix(base, "..") && (len(base) == 2 || _is_path_separator(base[2])) {
+ // We could do the work for the user of getting absolute paths for both
+ // arguments, but that could make something costly (repeatedly
+ // normalizing paths) convenient, when it would be better for the user
+ // to store already-finalized paths and operate on those instead.
+ return "", .Invalid_Path
+ }
+
+ // This is the other point where Windows and POSIX differ, as Windows is
+ // case-insensitive.
+ common := _get_common_path_len(base, target)
+
+ // Get the result of splitting `base` and `target` on _Path_Separator,
+ // comparing them up to their most common elements, then count how many
+ // unshared parts are in the split `base`.
+ seps := 0
+ size := 0
+ if len(base)-common > 0 {
+ seps = 1
+ size = 2
+ }
+ // This range skips separators on the ends of the string.
+ for i in common+1..<len(base)-1 {
+ if _is_path_separator(base[i]) {
+ seps += 1
+ size += 3
+ }
+ }
+
+ // Handle the rest of the size calculations.
+ trailing := target[common:]
+ if len(trailing) > 0 {
+ // Account for leading separators on the target after cutting the common part.
+ // (i.e. base == `/home`, target == `/home/a`)
+ if _is_path_separator(trailing[0]) {
+ trailing = trailing[1:]
+ }
+ size += len(trailing)
+ if seps > 0 {
+ size += 1
+ }
+ }
+ if trailing == "." {
+ trailing = ""
+ size -= 2
+ }
+
+ // Build the string.
+ buf := make([]u8, size, allocator) or_return
+ n := 0
+ if seps > 0 {
+ buf[0] = '.'
+ buf[1] = '.'
+ n = 2
+ }
+ for _ in 1..<seps {
+ buf[n] = _Path_Separator
+ buf[n+1] = '.'
+ buf[n+2] = '.'
+ n += 3
+ }
+ if len(trailing) > 0 {
+ if seps > 0 {
+ buf[n] = _Path_Separator
+ n += 1
+ }
+ runtime.mem_copy_non_overlapping(raw_data(buf[n:]), raw_data(trailing), len(trailing))
+ }
+
+ path = string(buf)
+
return
}
+
+/*
+Split a path into a directory hierarchy and a filename.
+
+For example, `split_path("/home/foo/bar.tar.gz")` will return `"/home/foo"` and `"bar.tar.gz"`.
+*/
+@(require_results)
+split_path :: proc(path: string) -> (dir, filename: string) {
+ return _split_path(path)
+}
+
+/*
+Join all `elems` with the system's path separator and normalize the result.
+
+*Allocates Using Provided Allocator*
+
+For example, `join_path({"/home", "foo", "bar.txt"})` will result in `"/home/foo/bar.txt"`.
+*/
+@(require_results)
+join_path :: proc(elems: []string, allocator: runtime.Allocator) -> (joined: string, err: Error) {
+ for e, i in elems {
+ if e != "" {
+ TEMP_ALLOCATOR_GUARD()
+ p := strings.join(elems[i:], Path_Separator_String, temp_allocator()) or_return
+ return clean_path(p, allocator)
+ }
+ }
+ return "", nil
+}
+
+/*
+Split a filename from its extension.
+
+This procedure splits on the last separator.
+
+If the filename begins with a separator, such as `".readme.txt"`, the separator
+will be included in the filename, resulting in `".readme"` and `"txt"`.
+
+For example, `split_filename("foo.tar.gz")` will return `"foo.tar"` and `"gz"`.
+*/
+@(require_results)
+split_filename :: proc(filename: string) -> (base, ext: string) {
+ i := strings.last_index_byte(filename, '.')
+ if i <= 0 {
+ return filename, ""
+ }
+ return filename[:i], filename[i+1:]
+}
+
+/*
+Split a filename from its extension.
+
+This procedure splits on the first separator.
+
+If the filename begins with a separator, such as `".readme.txt.gz"`, the separator
+will be included in the filename, resulting in `".readme"` and `"txt.gz"`.
+
+For example, `split_filename_all("foo.tar.gz")` will return `"foo"` and `"tar.gz"`.
+*/
+@(require_results)
+split_filename_all :: proc(filename: string) -> (base, ext: string) {
+ i := strings.index_byte(filename, '.')
+ if i == 0 {
+ j := strings.index_byte(filename[1:], '.')
+ if j != -1 {
+ j += 1
+ }
+ i = j
+ }
+ if i == -1 {
+ return filename, ""
+ }
+ return filename[:i], filename[i+1:]
+}
+
+/*
+Join `base` and `ext` with the system's filename extension separator.
+
+*Allocates Using Provided Allocator*
+
+For example, `join_filename("foo", "tar.gz")` will result in `"foo.tar.gz"`.
+*/
+@(require_results)
+join_filename :: proc(base: string, ext: string, allocator: runtime.Allocator) -> (joined: string, err: Error) {
+ len_base := len(base)
+ if len_base == 0 {
+ return strings.clone(ext, allocator)
+ } else if len(ext) == 0 {
+ return strings.clone(base, allocator)
+ }
+
+ buf := make([]u8, len_base + 1 + len(ext), allocator) or_return
+ intrinsics.mem_copy_non_overlapping(raw_data(buf), raw_data(base), len_base)
+ buf[len_base] = '.'
+ intrinsics.mem_copy_non_overlapping(raw_data(buf[1+len_base:]), raw_data(ext), len(ext))
+
+ return string(buf), nil
+}
+
+/*
+Split a string that is separated by a system-specific separator, typically used
+for environment variables specifying multiple directories.
+
+*Allocates Using Provided Allocator*
+
+For example, there is the "PATH" environment variable on POSIX systems which
+this procedure can split into separate entries.
+*/
+@(require_results)
+split_path_list :: proc(path: string, allocator: runtime.Allocator) -> (list: []string, err: Error) {
+ if path == "" {
+ return nil, nil
+ }
+
+ start: int
+ quote: bool
+
+ start, quote = 0, false
+ count := 0
+
+ for i := 0; i < len(path); i += 1 {
+ c := path[i]
+ switch {
+ case c == '"':
+ quote = !quote
+ case c == Path_List_Separator && !quote:
+ count += 1
+ }
+ }
+
+ start, quote = 0, false
+ list = make([]string, count + 1, allocator) or_return
+ index := 0
+ for i := 0; i < len(path); i += 1 {
+ c := path[i]
+ switch {
+ case c == '"':
+ quote = !quote
+ case c == Path_List_Separator && !quote:
+ list[index] = path[start:i]
+ index += 1
+ start = i + 1
+ }
+ }
+ assert(index == count)
+ list[index] = path[start:]
+
+ for s0, i in list {
+ s, new := strings.replace_all(s0, `"`, ``, allocator)
+ if !new {
+ s = strings.clone(s, allocator) or_return
+ }
+ list[i] = s
+ }
+
+ return list, nil
+}
diff --git a/core/os/os2/path_linux.odin b/core/os/os2/path_linux.odin
index e3e7f8a7c..410b4cb28 100644
--- a/core/os/os2/path_linux.odin
+++ b/core/os/os2/path_linux.odin
@@ -14,7 +14,7 @@ _Path_List_Separator :: ':'
_OPENDIR_FLAGS : linux.Open_Flags : {.NONBLOCK, .DIRECTORY, .LARGEFILE, .CLOEXEC}
_is_path_separator :: proc(c: byte) -> bool {
- return c == '/'
+ return c == _Path_Separator
}
_mkdir :: proc(path: string, perm: int) -> Error {
diff --git a/core/os/os2/path_posix.odin b/core/os/os2/path_posix.odin
index e6b95c0d4..39bd0a188 100644
--- a/core/os/os2/path_posix.odin
+++ b/core/os/os2/path_posix.odin
@@ -3,7 +3,6 @@
package os2
import "base:runtime"
-import "core:path/filepath"
import "core:sys/posix"
@@ -35,11 +34,11 @@ _mkdir_all :: proc(path: string, perm: int) -> Error {
return .Exist
}
- clean_path := filepath.clean(path, temp_allocator())
+ clean_path := clean_path(path, temp_allocator()) or_return
return internal_mkdir_all(clean_path, perm)
internal_mkdir_all :: proc(path: string, perm: int) -> Error {
- dir, file := filepath.split(path)
+ dir, file := split_path(path)
if file != path && dir != "/" {
if len(dir) > 1 && dir[len(dir) - 1] == '/' {
dir = dir[:len(dir) - 1]
diff --git a/core/os/os2/path_posixfs.odin b/core/os/os2/path_posixfs.odin
new file mode 100644
index 000000000..8f9d43d63
--- /dev/null
+++ b/core/os/os2/path_posixfs.odin
@@ -0,0 +1,78 @@
+#+private
+#+build linux, darwin, netbsd, freebsd, openbsd, wasi
+package os2
+
+// This implementation is for all systems that have POSIX-compliant filesystem paths.
+
+import "base:runtime"
+import "core:strings"
+import "core:sys/posix"
+
+_are_paths_identical :: proc(a, b: string) -> (identical: bool) {
+ return a == b
+}
+
+_clean_path_handle_start :: proc(path: string, buffer: []u8) -> (rooted: bool, start: int) {
+ // Preserve rooted paths.
+ if _is_path_separator(path[0]) {
+ rooted = true
+ buffer[0] = _Path_Separator
+ start = 1
+ }
+ return
+}
+
+_is_absolute_path :: proc(path: string) -> bool {
+ return len(path) > 0 && _is_path_separator(path[0])
+}
+
+_get_absolute_path :: proc(path: string, allocator: runtime.Allocator) -> (absolute_path: string, err: Error) {
+ rel := path
+ if rel == "" {
+ rel = "."
+ }
+ TEMP_ALLOCATOR_GUARD()
+ rel_cstr := strings.clone_to_cstring(rel, temp_allocator())
+ path_ptr := posix.realpath(rel_cstr, nil)
+ if path_ptr == nil {
+ return "", Platform_Error(posix.errno())
+ }
+ defer posix.free(path_ptr)
+
+ path_str := strings.clone(string(path_ptr), allocator)
+ return path_str, nil
+}
+
+_get_relative_path_handle_start :: proc(base, target: string) -> bool {
+ base_rooted := len(base) > 0 && _is_path_separator(base[0])
+ target_rooted := len(target) > 0 && _is_path_separator(target[0])
+ return base_rooted == target_rooted
+}
+
+_get_common_path_len :: proc(base, target: string) -> int {
+ i := 0
+ end := min(len(base), len(target))
+ for j in 0..=end {
+ if j == end || _is_path_separator(base[j]) {
+ if base[i:j] == target[i:j] {
+ i = j
+ } else {
+ break
+ }
+ }
+ }
+ return i
+}
+
+_split_path :: proc(path: string) -> (dir, file: string) {
+ i := len(path) - 1
+ for i >= 0 && !_is_path_separator(path[i]) {
+ i -= 1
+ }
+ if i == 0 {
+ return path[:i+1], path[i+1:]
+ } else if i > 0 {
+ return path[:i], path[i+1:]
+ }
+ return "", path
+}
diff --git a/core/os/os2/path_wasi.odin b/core/os/os2/path_wasi.odin
index 1c4fafa17..7aee8fcc0 100644
--- a/core/os/os2/path_wasi.odin
+++ b/core/os/os2/path_wasi.odin
@@ -3,7 +3,6 @@ package os2
import "base:runtime"
-import "core:path/filepath"
import "core:sync"
import "core:sys/wasm/wasi"
@@ -35,11 +34,11 @@ _mkdir_all :: proc(path: string, perm: int) -> Error {
return .Exist
}
- clean_path := filepath.clean(path, temp_allocator())
+ clean_path := clean_path(path, temp_allocator())
return internal_mkdir_all(clean_path)
internal_mkdir_all :: proc(path: string) -> Error {
- dir, file := filepath.split(path)
+ dir, file := split_path(path)
if file != path && dir != "/" {
if len(dir) > 1 && dir[len(dir) - 1] == '/' {
dir = dir[:len(dir) - 1]
diff --git a/core/os/os2/path_windows.odin b/core/os/os2/path_windows.odin
index 041a4d1e3..c8264cc2d 100644
--- a/core/os/os2/path_windows.odin
+++ b/core/os/os2/path_windows.odin
@@ -1,8 +1,10 @@
#+private
package os2
-import win32 "core:sys/windows"
+import "base:intrinsics"
import "base:runtime"
+import "core:strings"
+import win32 "core:sys/windows"
_Path_Separator :: '\\'
_Path_Separator_String :: "\\"
@@ -217,7 +219,7 @@ _fix_long_path_internal :: proc(path: string) -> string {
return path
}
- if !_is_abs(path) { // relative path
+ if !_is_absolute_path(path) { // relative path
return path
}
@@ -257,3 +259,93 @@ _fix_long_path_internal :: proc(path: string) -> string {
return string(path_buf[:w])
}
+
+_are_paths_identical :: strings.equal_fold
+
+_clean_path_handle_start :: proc(path: string, buffer: []u8) -> (rooted: bool, start: int) {
+ // Preserve rooted paths.
+ start = _volume_name_len(path)
+ if start > 0 {
+ rooted = true
+ if len(path) > start && _is_path_separator(path[start]) {
+ // Take `C:` to `C:\`.
+ start += 1
+ }
+ intrinsics.mem_copy_non_overlapping(raw_data(buffer), raw_data(path), start)
+ }
+ return
+}
+
+_is_absolute_path :: proc(path: string) -> bool {
+ if _is_reserved_name(path) {
+ return true
+ }
+ l := _volume_name_len(path)
+ if l == 0 {
+ return false
+ }
+
+ path := path
+ path = path[l:]
+ if path == "" {
+ return false
+ }
+ return _is_path_separator(path[0])
+}
+
+_get_absolute_path :: proc(path: string, allocator: runtime.Allocator) -> (absolute_path: string, err: Error) {
+ rel := path
+ if rel == "" {
+ rel = "."
+ }
+ TEMP_ALLOCATOR_GUARD()
+ rel_utf16 := win32.utf8_to_utf16(rel, temp_allocator())
+ n := win32.GetFullPathNameW(raw_data(rel_utf16), 0, nil, nil)
+ if n == 0 {
+ return "", Platform_Error(win32.GetLastError())
+ }
+
+ buf := make([]u16, n, temp_allocator()) or_return
+ n = win32.GetFullPathNameW(raw_data(rel_utf16), u32(n), raw_data(buf), nil)
+ if n == 0 {
+ return "", Platform_Error(win32.GetLastError())
+ }
+
+ return win32.utf16_to_utf8(buf, allocator)
+}
+
+_get_relative_path_handle_start :: proc(base, target: string) -> bool {
+ base_root := base[:_volume_name_len(base)]
+ target_root := target[:_volume_name_len(target)]
+ return strings.equal_fold(base_root, target_root)
+}
+
+_get_common_path_len :: proc(base, target: string) -> int {
+ i := 0
+ end := min(len(base), len(target))
+ for j in 0..=end {
+ if j == end || _is_path_separator(base[j]) {
+ if strings.equal_fold(base[i:j], target[i:j]) {
+ i = j
+ } else {
+ break
+ }
+ }
+ }
+ return i
+}
+
+_split_path :: proc(path: string) -> (dir, file: string) {
+ vol_len := _volume_name_len(path)
+
+ i := len(path) - 1
+ for i >= vol_len && !_is_path_separator(path[i]) {
+ i -= 1
+ }
+ if i == vol_len {
+ return path[:i+1], path[i+1:]
+ } else if i > vol_len {
+ return path[:i], path[i+1:]
+ }
+ return "", path
+}
diff --git a/core/os/os2/process_linux.odin b/core/os/os2/process_linux.odin
index 632bde6ba..6d654008b 100644
--- a/core/os/os2/process_linux.odin
+++ b/core/os/os2/process_linux.odin
@@ -10,7 +10,6 @@ import "core:slice"
import "core:strings"
import "core:strconv"
import "core:sys/linux"
-import "core:path/filepath"
PIDFD_UNASSIGNED :: ~uintptr(0)
@@ -205,7 +204,7 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator
info.executable_path = strings.clone(cmdline[:terminator], allocator) or_return
info.fields += {.Executable_Path}
} else if cwd_err == nil {
- info.executable_path = filepath.join({ cwd, cmdline[:terminator] }, allocator) or_return
+ info.executable_path = join_path({ cwd, cmdline[:terminator] }, allocator) or_return
info.fields += {.Executable_Path}
} else {
break cmdline_if
@@ -407,7 +406,7 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) {
executable_name := desc.command[0]
if strings.index_byte(executable_name, '/') < 0 {
path_env := get_env("PATH", temp_allocator())
- path_dirs := filepath.split_list(path_env, temp_allocator()) or_return
+ path_dirs := split_path_list(path_env, temp_allocator()) or_return
exe_builder := strings.builder_make(temp_allocator()) or_return
diff --git a/core/os/os2/process_posix.odin b/core/os/os2/process_posix.odin
index 3fa429cbe..cd451781f 100644
--- a/core/os/os2/process_posix.odin
+++ b/core/os/os2/process_posix.odin
@@ -6,7 +6,6 @@ import "base:runtime"
import "core:time"
import "core:strings"
-import "core:path/filepath"
import kq "core:sys/kqueue"
import "core:sys/posix"
@@ -62,7 +61,7 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) {
exe_name := desc.command[0]
if strings.index_byte(exe_name, '/') < 0 {
path_env := get_env("PATH", temp_allocator())
- path_dirs := filepath.split_list(path_env, temp_allocator())
+ path_dirs := split_path_list(path_env, temp_allocator()) or_return
found: bool
for dir in path_dirs {
diff --git a/core/os/os2/stat.odin b/core/os/os2/stat.odin
index d0a5a659d..7d76902eb 100644
--- a/core/os/os2/stat.odin
+++ b/core/os/os2/stat.odin
@@ -1,7 +1,6 @@
package os2
import "base:runtime"
-import "core:path/filepath"
import "core:strings"
import "core:time"
@@ -25,7 +24,7 @@ File_Info :: struct {
file_info_clone :: proc(fi: File_Info, allocator: runtime.Allocator) -> (cloned: File_Info, err: runtime.Allocator_Error) {
cloned = fi
cloned.fullpath = strings.clone(fi.fullpath, allocator) or_return
- cloned.name = filepath.base(cloned.fullpath)
+ _, cloned.name = split_path(cloned.fullpath)
return
}
diff --git a/core/os/os2/stat_linux.odin b/core/os/os2/stat_linux.odin
index 0433c1a61..7bff08f29 100644
--- a/core/os/os2/stat_linux.odin
+++ b/core/os/os2/stat_linux.odin
@@ -4,7 +4,6 @@ package os2
import "core:time"
import "base:runtime"
import "core:sys/linux"
-import "core:path/filepath"
_fstat :: proc(f: ^File, allocator: runtime.Allocator) -> (File_Info, Error) {
impl := (^File_Impl)(f.impl)
@@ -42,7 +41,7 @@ _fstat_internal :: proc(fd: linux.Fd, allocator: runtime.Allocator) -> (fi: File
creation_time = time.Time{i64(s.ctime.time_sec) * i64(time.Second) + i64(s.ctime.time_nsec)}, // regular stat does not provide this
}
fi.creation_time = fi.modification_time
- fi.name = filepath.base(fi.fullpath)
+ _, fi.name = split_path(fi.fullpath)
return
}
diff --git a/core/os/os2/stat_posix.odin b/core/os/os2/stat_posix.odin
index 88029c1f5..260dc7b52 100644
--- a/core/os/os2/stat_posix.odin
+++ b/core/os/os2/stat_posix.odin
@@ -4,13 +4,12 @@ package os2
import "base:runtime"
-import "core:path/filepath"
import "core:sys/posix"
import "core:time"
internal_stat :: proc(stat: posix.stat_t, fullpath: string) -> (fi: File_Info) {
fi.fullpath = fullpath
- fi.name = filepath.base(fi.fullpath)
+ _, fi.name = split_path(fi.fullpath)
fi.inode = u128(stat.st_ino)
fi.size = i64(stat.st_size)
@@ -104,7 +103,7 @@ _lstat :: proc(name: string, allocator: runtime.Allocator) -> (fi: File_Info, er
// NOTE: This might not be correct when given "/symlink/foo.txt",
// you would want that to resolve "/symlink", but not resolve "foo.txt".
- fullpath := filepath.clean(name, temp_allocator())
+ fullpath := clean_path(name, temp_allocator()) or_return
assert(len(fullpath) > 0)
switch {
case fullpath[0] == '/':
diff --git a/core/os/os2/stat_wasi.odin b/core/os/os2/stat_wasi.odin
index 2992c6267..bf18d8273 100644
--- a/core/os/os2/stat_wasi.odin
+++ b/core/os/os2/stat_wasi.odin
@@ -3,13 +3,12 @@ package os2
import "base:runtime"
-import "core:path/filepath"
import "core:sys/wasm/wasi"
import "core:time"
internal_stat :: proc(stat: wasi.filestat_t, fullpath: string) -> (fi: File_Info) {
fi.fullpath = fullpath
- fi.name = filepath.base(fi.fullpath)
+ _, fi.name = split_path(fi.fullpath)
fi.inode = u128(stat.ino)
fi.size = i64(stat.size)
diff --git a/core/os/os2/stat_windows.odin b/core/os/os2/stat_windows.odin
index 31f5d9e88..7d8dd3843 100644
--- a/core/os/os2/stat_windows.odin
+++ b/core/os/os2/stat_windows.odin
@@ -315,57 +315,37 @@ _is_UNC :: proc(path: string) -> bool {
}
_volume_name_len :: proc(path: string) -> int {
- if ODIN_OS == .Windows {
- if len(path) < 2 {
- return 0
- }
- c := path[0]
- if path[1] == ':' {
- switch c {
- case 'a'..='z', 'A'..='Z':
- return 2
- }
+ if len(path) < 2 {
+ return 0
+ }
+ c := path[0]
+ if path[1] == ':' {
+ switch c {
+ case 'a'..='z', 'A'..='Z':
+ return 2
}
+ }
- // URL: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
- if l := len(path); l >= 5 && _is_path_separator(path[0]) && _is_path_separator(path[1]) &&
- !_is_path_separator(path[2]) && path[2] != '.' {
- for n := 3; n < l-1; n += 1 {
- if _is_path_separator(path[n]) {
- n += 1
- if !_is_path_separator(path[n]) {
- if path[n] == '.' {
- break
- }
+ // URL: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
+ if l := len(path); l >= 5 && _is_path_separator(path[0]) && _is_path_separator(path[1]) &&
+ !_is_path_separator(path[2]) && path[2] != '.' {
+ for n := 3; n < l-1; n += 1 {
+ if _is_path_separator(path[n]) {
+ n += 1
+ if !_is_path_separator(path[n]) {
+ if path[n] == '.' {
+ break
}
- for ; n < l; n += 1 {
- if _is_path_separator(path[n]) {
- break
- }
+ }
+ for ; n < l; n += 1 {
+ if _is_path_separator(path[n]) {
+ break
}
- return n
}
- break
+ return n
}
+ break
}
}
return 0
}
-
-_is_abs :: proc(path: string) -> bool {
- if _is_reserved_name(path) {
- return true
- }
- l := _volume_name_len(path)
- if l == 0 {
- return false
- }
-
- path := path
- path = path[l:]
- if path == "" {
- return false
- }
- return is_path_separator(path[0])
-}
-
diff --git a/tests/core/os/os2/dir.odin b/tests/core/os/os2/dir.odin
index 7077e9ae2..8ef333219 100644
--- a/tests/core/os/os2/dir.odin
+++ b/tests/core/os/os2/dir.odin
@@ -2,27 +2,27 @@ package tests_core_os_os2
import os "core:os/os2"
import "core:log"
-import "core:path/filepath"
import "core:slice"
import "core:testing"
import "core:strings"
@(test)
test_read_dir :: proc(t: ^testing.T) {
- path := filepath.join({#directory, "../dir"})
+ path, err_join := os.join_path({#directory, "../dir"}, context.allocator)
defer delete(path)
- fis, err := os.read_all_directory_by_path(path, context.allocator)
+ fis, err_read := os.read_all_directory_by_path(path, context.allocator)
defer os.file_info_slice_delete(fis, context.allocator)
slice.sort_by_key(fis, proc(fi: os.File_Info) -> string { return fi.name })
- if err == .Unsupported {
+ if err_read == .Unsupported {
log.warn("os2 directory functionality is unsupported, skipping test")
return
}
- testing.expect_value(t, err, nil)
+ testing.expect_value(t, err_join, nil)
+ testing.expect_value(t, err_read, nil)
testing.expect_value(t, len(fis), 2)
testing.expect_value(t, fis[0].name, "b.txt")
@@ -34,8 +34,9 @@ test_read_dir :: proc(t: ^testing.T) {
@(test)
test_walker :: proc(t: ^testing.T) {
- path := filepath.join({#directory, "../dir"})
+ path, err := os.join_path({#directory, "../dir"}, context.allocator)
defer delete(path)
+ testing.expect_value(t, err, nil)
w := os.walker_create(path)
defer os.walker_destroy(&w)
@@ -45,11 +46,12 @@ test_walker :: proc(t: ^testing.T) {
@(test)
test_walker_file :: proc(t: ^testing.T) {
- path := filepath.join({#directory, "../dir"})
+ path, err_join := os.join_path({#directory, "../dir"}, context.allocator)
defer delete(path)
+ testing.expect_value(t, err_join, nil)
- f, err := os.open(path)
- testing.expect_value(t, err, nil)
+ f, err_open := os.open(path)
+ testing.expect_value(t, err_open, nil)
defer os.close(f)
w := os.walker_create(f)
@@ -64,10 +66,18 @@ test_walker_internal :: proc(t: ^testing.T, w: ^os.Walker) {
path: string,
}
+ joined_1, err_joined_1 := os.join_path({"dir", "b.txt"}, context.allocator)
+ joined_2, err_joined_2 := os.join_path({"dir", "sub"}, context.allocator)
+ joined_3, err_joined_3 := os.join_path({"dir", "sub", ".gitkeep"}, context.allocator)
+
+ testing.expect_value(t, err_joined_1, nil)
+ testing.expect_value(t, err_joined_2, nil)
+ testing.expect_value(t, err_joined_3, nil)
+
expected := [?]Seen{
- {.Regular, filepath.join({"dir", "b.txt"})},
- {.Directory, filepath.join({"dir", "sub"})},
- {.Regular, filepath.join({"dir", "sub", ".gitkeep"})},
+ {.Regular, joined_1},
+ {.Directory, joined_2},
+ {.Regular, joined_3},
}
seen: [dynamic]Seen
diff --git a/tests/core/os/os2/file.odin b/tests/core/os/os2/file.odin
index c4df74f4a..0152a2008 100644
--- a/tests/core/os/os2/file.odin
+++ b/tests/core/os/os2/file.odin
@@ -2,11 +2,13 @@ package tests_core_os_os2
import os "core:os/os2"
import "core:testing"
-import "core:path/filepath"
@(test)
test_clone :: proc(t: ^testing.T) {
- f, err := os.open(filepath.join({#directory, "file.odin"}, context.temp_allocator))
+ joined, err := os.join_path({#directory, "file.odin"}, context.temp_allocator)
+ testing.expect_value(t, err, nil)
+ f: ^os.File
+ f, err = os.open(joined)
testing.expect_value(t, err, nil)
testing.expect(t, f != nil)
diff --git a/tests/core/os/os2/path.odin b/tests/core/os/os2/path.odin
index b91f43368..2cf1f1f1c 100644
--- a/tests/core/os/os2/path.odin
+++ b/tests/core/os/os2/path.odin
@@ -2,7 +2,6 @@ package tests_core_os_os2
import os "core:os/os2"
import "core:log"
-import "core:path/filepath"
import "core:testing"
import "core:strings"
@@ -17,6 +16,351 @@ test_executable :: proc(t: ^testing.T) {
testing.expect_value(t, err, nil)
testing.expect(t, len(path) > 0)
- testing.expect(t, filepath.is_abs(path))
- testing.expectf(t, strings.contains(path, filepath.base(os.args[0])), "expected the executable path to contain the base of os.args[0] which is %q", filepath.base(os.args[0]))
+ testing.expect(t, os.is_absolute_path(path))
+ _, filename := os.split_path(os.args[0])
+ testing.expectf(t, strings.contains(path, filename), "expected the executable path to contain the base of os.args[0] which is %q", filename)
+}
+
+posix_to_dos_path :: proc(path: string) -> string {
+ if len(path) == 0 {
+ return path
+ }
+ path := path
+ path, _ = strings.replace_all(path, `/`, `\`, context.temp_allocator)
+ if path[0] == '\\' {
+ path = strings.concatenate({"C:", path}, context.temp_allocator)
+ }
+ return path
+}
+
+@(test)
+test_clean_path :: proc(t: ^testing.T) {
+ Test_Case :: struct{
+ path: string,
+ expected: string,
+ }
+
+ test_cases := [?]Test_Case {
+ {`../../foo/../../`, `../../..`},
+ {`../../foo/..`, `../..`},
+ {`../../foo`, `../../foo`},
+ {`../..`, `../..`},
+ {`.././foo`, `../foo`},
+ {`..`, `..`},
+ {`.`, `.`},
+ {`.foo`, `.foo`},
+ {`/../../foo/../../`, `/`},
+ {`/../`, `/`},
+ {`/..`, `/`},
+ {`/`, `/`},
+ {`//home/foo/bar/../../`, `/home`},
+ {`/a/../..`, `/`},
+ {`/a/../`, `/`},
+ {`/a/あ`, `/a/あ`},
+ {`/a/あ/..`, `/a`},
+ {`/あ/a/..`, `/あ`},
+ {`/あ/a/../あ`, `/あ/あ`},
+ {`/home/../`, `/`},
+ {`/home/..`, `/`},
+ {`/home/foo/../../usr`, `/usr`},
+ {`/home/foo/../..`, `/`},
+ {`/home/foo/../`, `/home`},
+ {``, `.`},
+ {`a/..`, `.`},
+ {`a`, `a`},
+ {`abc//.//../foo`, `foo`},
+ {`foo`, `foo`},
+ {`home/foo/bar/../../`, `home`},
+ }
+
+ when ODIN_OS == .Windows {
+ for &tc in test_cases {
+ tc.path = posix_to_dos_path(tc.path)
+ tc.expected = posix_to_dos_path(tc.expected)
+ }
+ }
+
+ for tc in test_cases {
+ joined, err := os.clean_path(tc.path, context.temp_allocator)
+ testing.expectf(t, joined == tc.expected && err == nil, "expected clean_path(%q) -> %q; got: %q, %v", tc.path, tc.expected, joined, err)
+ }
+}
+
+@(test)
+test_is_absolute_path :: proc(t: ^testing.T) {
+ when ODIN_OS == .Windows {
+ testing.expect(t, os.is_absolute_path(`C:\Windows`))
+ } else {
+ testing.expect(t, os.is_absolute_path("/home"))
+ }
+ testing.expect(t, !os.is_absolute_path("home"))
+}
+
+@(test)
+test_get_relative_path :: proc(t: ^testing.T) {
+ Test_Case :: struct {
+ base, target: string,
+ expected: string,
+ }
+
+ Fail_Case :: struct {
+ base, target: string,
+ }
+
+ test_cases := [?]Test_Case {
+ {"", "foo", "foo"},
+ {".", "foo", "foo"},
+ {"/", "/", "."},
+ {"/", "/home/alice/bert", "home/alice/bert"},
+ {"/a", "/b", "../b"},
+ {"/あ", "/あ/a", "a"},
+ {"/a", "/a/あ", "あ"},
+ {"/あ", "/い", "../い"},
+ {"/a", "/usr", "../usr"},
+ {"/home", "/", ".."},
+ {"/home", "/home/alice/bert", "alice/bert"},
+ {"/home/foo", "/", "../.."},
+ {"/home/foo", "/home", ".."},
+ {"/home/foo", "/home/alice/bert", "../alice/bert"},
+ {"/home/foo", "/home/foo", "."},
+ {"/home/foo", "/home/foo/bar", "bar"},
+ {"/home/foo/bar", "/home", "../.."},
+ {"/home/foo/bar", "/home/alice/bert", "../../alice/bert"},
+ {"/home/foo/bar/bert", "/home/alice/bert", "../../../alice/bert"},
+ {"/www", "/mount", "../mount"},
+ {"foo", ".", ".."},
+ {"foo", "bar", "../bar"},
+ {"foo", "bar", "../bar"},
+ {"foo", "../bar", "../../bar"},
+ {"foo", "foo", "."},
+ {"foo", "foo/bar", "bar"},
+ {"home/foo/bar", "home/alice/bert", "../../alice/bert"},
+ }
+
+ fail_cases := [?]Fail_Case {
+ {"", "/home"},
+ {"/home", ""},
+ {"..", ""},
+ }
+
+ when ODIN_OS == .Windows {
+ for &tc in test_cases {
+ tc.base = posix_to_dos_path(tc.base)
+ tc.target = posix_to_dos_path(tc.target)
+ // Make one part all capitals to test case-insensitivity.
+ tc.target = strings.to_upper(tc.target, context.temp_allocator)
+ tc.expected = posix_to_dos_path(tc.expected)
+ }
+ for &tc in fail_cases {
+ tc.base = posix_to_dos_path(tc.base)
+ tc.target = posix_to_dos_path(tc.target)
+ }
+ }
+
+ for tc in test_cases {
+ result, err := os.get_relative_path(tc.base, tc.target, context.temp_allocator)
+ joined, err2 := os.join_path({tc.base, result}, context.temp_allocator)
+
+ when ODIN_OS == .Windows {
+ passed := strings.equal_fold(result, tc.expected) && err == nil
+ join_guaranteed := strings.equal_fold(joined, tc.target) && err2 == nil
+ } else {
+ passed := result == tc.expected && err == nil
+ join_guaranteed := joined == tc.target && err2 == nil
+ }
+ testing.expectf(t, passed, "expected get_relative_path(%q, %q) -> %q; got %q, %v", tc.base, tc.target, tc.expected, result, err)
+ testing.expectf(t, join_guaranteed, "join_path({{%q, %q}}) guarantee of get_relative_path(%q, %q) failed; got %q, %v instead", tc.base, result, tc.base, tc.target, joined, err2)
+ }
+
+ for tc in fail_cases {
+ result, err := os.get_relative_path(tc.base, tc.target, context.temp_allocator)
+ testing.expectf(t, result == "" && err != nil, "expected get_relative_path(%q, %q) to fail, got %q, %v", tc.base, tc.target, result, err)
+ }
+}
+
+@(test)
+test_split_path :: proc(t: ^testing.T) {
+ Test_Case :: struct {
+ path: string,
+ dir, filename: string,
+ }
+
+ test_cases := [?]Test_Case {
+ { "", "", "" },
+ { "/", "/", "" },
+ { "/a", "/", "a" },
+ { "readme.txt", "", "readme.txt" },
+ { "/readme.txt", "/", "readme.txt" },
+ { "/var/readme.txt", "/var", "readme.txt" },
+ { "/home/foo/bar.tar.gz", "/home/foo", "bar.tar.gz" },
+ }
+
+ when ODIN_OS == .Windows {
+ for &tc in test_cases {
+ tc.path = posix_to_dos_path(tc.path)
+ tc.dir = posix_to_dos_path(tc.dir)
+ tc.filename = posix_to_dos_path(tc.filename)
+ }
+ }
+
+ for tc in test_cases {
+ dir, filename := os.split_path(tc.path)
+ testing.expectf(t, dir == tc.dir && filename == tc.filename, "expected split_path(%q) -> %q, %q; got: %q, %q", tc.path, tc.dir, tc.filename, dir, filename)
+ }
+}
+
+@(test)
+test_join_path :: proc(t: ^testing.T) {
+ Test_Case :: struct {
+ elems: []string,
+ expected: string,
+ }
+
+ test_cases := [?]Test_Case {
+ { {"" }, "" },
+ { {"/" }, "/" },
+ { {"home" }, "home" },
+ { {"home", "" }, "home" },
+ { {"/home", "" }, "/home" },
+ { {"", "home" }, "home" },
+ { {"", "/home" }, "/home" },
+ { {"", "/home", "", "foo" }, "/home/foo" },
+ { {"", "home", "", "", "foo", "" }, "home/foo" },
+ }
+
+ when ODIN_OS == .Windows {
+ for &tc in test_cases {
+ for &elem in tc.elems {
+ elem = posix_to_dos_path(elem)
+ }
+ tc.expected = posix_to_dos_path(tc.expected)
+ }
+ }
+
+ for tc in test_cases {
+ result, err := os.join_path(tc.elems, context.temp_allocator)
+ testing.expectf(t, result == tc.expected && err == nil, "expected join_path(%v) -> %q; got: %q, %v", tc.elems, tc.expected, result, err)
+ }
+}
+
+@(test)
+test_split_filename :: proc(t: ^testing.T) {
+ Test_Case :: struct {
+ filename: string,
+ base, ext: string,
+ }
+
+ test_cases := [?]Test_Case {
+ {"", "", ""},
+ {"a", "a", ""},
+ {".", ".", ""},
+ {".a", ".a", ""},
+ {".foo", ".foo", ""},
+ {".foo.txt", ".foo", "txt"},
+ {"a.b", "a", "b"},
+ {"foo", "foo", ""},
+ {"readme.txt", "readme", "txt"},
+ {"pkg.tar.gz", "pkg.tar", "gz"},
+ // Assert API ignores directory hierarchies:
+ {"dir/FILE.TXT", "dir/FILE", "TXT"},
+ }
+
+ for tc in test_cases {
+ base, ext := os.split_filename(tc.filename)
+ testing.expectf(t, base == tc.base && ext == tc.ext, "expected split_filename(%q) -> %q, %q; got: %q, %q", tc.filename, tc.base, tc.ext, base, ext)
+ }
+}
+
+@(test)
+test_split_filename_all :: proc(t: ^testing.T) {
+ Test_Case :: struct {
+ filename: string,
+ base, ext: string,
+ }
+
+ test_cases := [?]Test_Case {
+ {"", "", ""},
+ {"a", "a", ""},
+ {".", ".", ""},
+ {".a", ".a", ""},
+ {".foo", ".foo", ""},
+ {".foo.txt", ".foo", "txt"},
+ {"a.b", "a", "b"},
+ {"foo", "foo", ""},
+ {"readme.txt", "readme", "txt"},
+ {"pkg.tar.gz", "pkg", "tar.gz"},
+ // Assert API ignores directory hierarchies:
+ {"dir/FILE.TXT", "dir/FILE", "TXT"},
+ }
+
+ for tc in test_cases {
+ base, ext := os.split_filename_all(tc.filename)
+ testing.expectf(t, base == tc.base && ext == tc.ext, "expected split_filename_all(%q) -> %q, %q; got: %q, %q", tc.filename, tc.base, tc.ext, base, ext)
+ }
+}
+
+@(test)
+test_join_filename :: proc(t: ^testing.T) {
+ Test_Case :: struct {
+ base, ext: string,
+ expected: string,
+ }
+
+ test_cases := [?]Test_Case {
+ {"", "", ""},
+ {"", "foo", "foo"},
+ {"foo", "", "foo"},
+ {"readme", "txt", "readme.txt"},
+ {"pkg.tar", "gz", "pkg.tar.gz"},
+ {"pkg", "tar.gz", "pkg.tar.gz"},
+ // Assert API ignores directory hierarchies:
+ {"dir/FILE", "TXT", "dir/FILE.TXT"},
+ }
+
+ for tc in test_cases {
+ result, err := os.join_filename(tc.base, tc.ext, context.temp_allocator)
+ testing.expectf(t, result == tc.expected && err == nil, "expected join_filename(%q, %q) -> %q; got: %q, %v", tc.base, tc.ext, tc.expected, result, err)
+ }
+}
+
+@(test)
+test_split_path_list :: proc(t: ^testing.T) {
+ Test_Case :: struct {
+ path_list: string,
+ expected: []string,
+ }
+
+ when ODIN_OS != .Windows {
+ test_cases := [?]Test_Case {
+ {``, {}},
+ {`/bin:`, {`/bin`, ``}},
+ {`/usr/local/bin`, {`/usr/local/bin`}},
+ {`/usr/local/bin:/usr/bin`, {`/usr/local/bin`, `/usr/bin`}},
+ {`"/extra bin":/bin`, {`/extra bin`, `/bin`}},
+ {`"/extra:bin":/bin`, {`/extra:bin`, `/bin`}},
+ }
+ } else {
+ test_cases := [?]Test_Case {
+ {``, {}},
+ {`C:\bin;`, {`C:\bin`, ``}},
+ {`C:\usr\local\bin`, {`C:\usr\local\bin`}},
+ {`C:\usr\local\bin;C:\usr\bin`, {`C:\usr\local\bin`, `C:\usr\bin`}},
+ {`"C:\extra bin";C:\bin`, {`C:\extra bin`, `C:\bin`}},
+ {`"C:\extra;bin";C:\bin`, {`C:\extra;bin`, `C:\bin`}},
+ }
+ }
+
+ for tc in test_cases {
+ result, err := os.split_path_list(tc.path_list, context.temp_allocator)
+ if testing.expectf(t, len(result) == len(tc.expected), "expected split_path_list(%q) -> %v; got %v, %v", tc.path_list, tc.expected, result, err) {
+ ok := true
+ for entry, i in result {
+ if entry != tc.expected[i] {
+ ok = false
+ break
+ }
+ }
+ testing.expectf(t, ok, "expected split_path_list(%q) -> %v; got %v, %v", tc.path_list, tc.expected, result, err)
+ }
+ }
}