Browse Source

Merge pull request #4954 from Feoramund/os2-path

Add new path API for `os2`
gingerBill 4 months ago
parent
commit
4a595f9dac

+ 413 - 2
core/os/os2/path.odin

@@ -1,13 +1,20 @@
 package os2
 package os2
 
 
+import "base:intrinsics"
 import "base:runtime"
 import "base:runtime"
 
 
-import "core:path/filepath"
+import "core:strings"
 
 
 Path_Separator        :: _Path_Separator        // OS-Specific
 Path_Separator        :: _Path_Separator        // OS-Specific
 Path_Separator_String :: _Path_Separator_String // OS-Specific
 Path_Separator_String :: _Path_Separator_String // OS-Specific
 Path_List_Separator   :: _Path_List_Separator   // 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)
 @(require_results)
 is_path_separator :: proc(c: byte) -> bool {
 is_path_separator :: proc(c: byte) -> bool {
 	return _is_path_separator(c)
 	return _is_path_separator(c)
@@ -15,22 +22,42 @@ is_path_separator :: proc(c: byte) -> bool {
 
 
 mkdir :: make_directory
 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 {
 make_directory :: proc(name: string, perm: int = 0o755) -> Error {
 	return _mkdir(name, perm)
 	return _mkdir(name, perm)
 }
 }
 
 
 mkdir_all :: make_directory_all
 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 {
 make_directory_all :: proc(path: string, perm: int = 0o755) -> Error {
 	return _mkdir_all(path, perm)
 	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 {
 remove_all :: proc(path: string) -> Error {
 	return _remove_all(path)
 	return _remove_all(path)
 }
 }
 
 
 getwd :: get_working_directory
 getwd :: get_working_directory
 
 
+/*
+Get the working directory of the current process.
+
+*Allocates Using Provided Allocator*
+*/
 @(require_results)
 @(require_results)
 get_working_directory :: proc(allocator: runtime.Allocator) -> (dir: string, err: Error) {
 get_working_directory :: proc(allocator: runtime.Allocator) -> (dir: string, err: Error) {
 	return _get_working_directory(allocator)
 	return _get_working_directory(allocator)
@@ -38,16 +65,400 @@ get_working_directory :: proc(allocator: runtime.Allocator) -> (dir: string, err
 
 
 setwd :: set_working_directory
 setwd :: set_working_directory
 
 
+/*
+Change the working directory of the current process.
+
+*Allocates Using Provided Allocator*
+*/
 set_working_directory :: proc(dir: string) -> (err: Error) {
 set_working_directory :: proc(dir: string) -> (err: Error) {
 	return _set_working_directory(dir)
 	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) {
 get_executable_path :: proc(allocator: runtime.Allocator) -> (path: string, err: Error) {
 	return _get_executable_path(allocator)
 	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) {
 get_executable_directory :: proc(allocator: runtime.Allocator) -> (path: string, err: Error) {
 	path = _get_executable_path(allocator) or_return
 	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
 	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
+}

+ 1 - 1
core/os/os2/path_linux.odin

@@ -14,7 +14,7 @@ _Path_List_Separator   :: ':'
 _OPENDIR_FLAGS : linux.Open_Flags : {.NONBLOCK, .DIRECTORY, .LARGEFILE, .CLOEXEC}
 _OPENDIR_FLAGS : linux.Open_Flags : {.NONBLOCK, .DIRECTORY, .LARGEFILE, .CLOEXEC}
 
 
 _is_path_separator :: proc(c: byte) -> bool {
 _is_path_separator :: proc(c: byte) -> bool {
-	return c == '/'
+	return c == _Path_Separator
 }
 }
 
 
 _mkdir :: proc(path: string, perm: int) -> Error {
 _mkdir :: proc(path: string, perm: int) -> Error {

+ 2 - 3
core/os/os2/path_posix.odin

@@ -3,7 +3,6 @@
 package os2
 package os2
 
 
 import "base:runtime"
 import "base:runtime"
-import "core:path/filepath"
 
 
 import "core:sys/posix"
 import "core:sys/posix"
 
 
@@ -35,11 +34,11 @@ _mkdir_all :: proc(path: string, perm: int) -> Error {
 		return .Exist
 		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)
 	return internal_mkdir_all(clean_path, perm)
 
 
 	internal_mkdir_all :: proc(path: string, perm: int) -> Error {
 	internal_mkdir_all :: proc(path: string, perm: int) -> Error {
-		dir, file := filepath.split(path)
+		dir, file := split_path(path)
 		if file != path && dir != "/" {
 		if file != path && dir != "/" {
 			if len(dir) > 1 && dir[len(dir) - 1] == '/' {
 			if len(dir) > 1 && dir[len(dir) - 1] == '/' {
 				dir = dir[:len(dir) - 1]
 				dir = dir[:len(dir) - 1]

+ 78 - 0
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
+}

+ 2 - 3
core/os/os2/path_wasi.odin

@@ -3,7 +3,6 @@ package os2
 
 
 import "base:runtime"
 import "base:runtime"
 
 
-import "core:path/filepath"
 import "core:sync"
 import "core:sync"
 import "core:sys/wasm/wasi"
 import "core:sys/wasm/wasi"
 
 
@@ -35,11 +34,11 @@ _mkdir_all :: proc(path: string, perm: int) -> Error {
 		return .Exist
 		return .Exist
 	}
 	}
 
 
-	clean_path := filepath.clean(path, temp_allocator())
+	clean_path := clean_path(path, temp_allocator())
 	return internal_mkdir_all(clean_path)
 	return internal_mkdir_all(clean_path)
 
 
 	internal_mkdir_all :: proc(path: string) -> Error {
 	internal_mkdir_all :: proc(path: string) -> Error {
-		dir, file := filepath.split(path)
+		dir, file := split_path(path)
 		if file != path && dir != "/" {
 		if file != path && dir != "/" {
 			if len(dir) > 1 && dir[len(dir) - 1] == '/' {
 			if len(dir) > 1 && dir[len(dir) - 1] == '/' {
 				dir = dir[:len(dir) - 1]
 				dir = dir[:len(dir) - 1]

+ 94 - 2
core/os/os2/path_windows.odin

@@ -1,8 +1,10 @@
 #+private
 #+private
 package os2
 package os2
 
 
-import win32 "core:sys/windows"
+import "base:intrinsics"
 import "base:runtime"
 import "base:runtime"
+import "core:strings"
+import win32 "core:sys/windows"
 
 
 _Path_Separator        :: '\\'
 _Path_Separator        :: '\\'
 _Path_Separator_String :: "\\"
 _Path_Separator_String :: "\\"
@@ -217,7 +219,7 @@ _fix_long_path_internal :: proc(path: string) -> string {
 		return path
 		return path
 	}
 	}
 
 
-	if !_is_abs(path) { // relative path
+	if !_is_absolute_path(path) { // relative path
 		return path
 		return path
 	}
 	}
 
 
@@ -257,3 +259,93 @@ _fix_long_path_internal :: proc(path: string) -> string {
 
 
 	return string(path_buf[:w])
 	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
+}

+ 2 - 3
core/os/os2/process_linux.odin

@@ -10,7 +10,6 @@ import "core:slice"
 import "core:strings"
 import "core:strings"
 import "core:strconv"
 import "core:strconv"
 import "core:sys/linux"
 import "core:sys/linux"
-import "core:path/filepath"
 
 
 PIDFD_UNASSIGNED  :: ~uintptr(0)
 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.executable_path = strings.clone(cmdline[:terminator], allocator) or_return
 				info.fields += {.Executable_Path}
 				info.fields += {.Executable_Path}
 			} else if cwd_err == nil {
 			} 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}
 				info.fields += {.Executable_Path}
 			} else {
 			} else {
 				break cmdline_if
 				break cmdline_if
@@ -407,7 +406,7 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) {
 	executable_name := desc.command[0]
 	executable_name := desc.command[0]
 	if strings.index_byte(executable_name, '/') < 0 {
 	if strings.index_byte(executable_name, '/') < 0 {
 		path_env := get_env("PATH", temp_allocator())
 		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
 		exe_builder := strings.builder_make(temp_allocator()) or_return
 
 

+ 1 - 2
core/os/os2/process_posix.odin

@@ -6,7 +6,6 @@ import "base:runtime"
 
 
 import "core:time"
 import "core:time"
 import "core:strings"
 import "core:strings"
-import "core:path/filepath"
 
 
 import kq "core:sys/kqueue"
 import kq "core:sys/kqueue"
 import    "core:sys/posix"
 import    "core:sys/posix"
@@ -62,7 +61,7 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) {
 	exe_name    := desc.command[0]
 	exe_name    := desc.command[0]
 	if strings.index_byte(exe_name, '/') < 0 {
 	if strings.index_byte(exe_name, '/') < 0 {
 		path_env  := get_env("PATH", temp_allocator())
 		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
 		found: bool
 		for dir in path_dirs {
 		for dir in path_dirs {

+ 1 - 2
core/os/os2/stat.odin

@@ -1,7 +1,6 @@
 package os2
 package os2
 
 
 import "base:runtime"
 import "base:runtime"
-import "core:path/filepath"
 import "core:strings"
 import "core:strings"
 import "core:time"
 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) {
 file_info_clone :: proc(fi: File_Info, allocator: runtime.Allocator) -> (cloned: File_Info, err: runtime.Allocator_Error) {
 	cloned = fi
 	cloned = fi
 	cloned.fullpath = strings.clone(fi.fullpath, allocator) or_return
 	cloned.fullpath = strings.clone(fi.fullpath, allocator) or_return
-	cloned.name = filepath.base(cloned.fullpath)
+	_, cloned.name = split_path(cloned.fullpath)
 	return
 	return
 }
 }
 
 

+ 1 - 2
core/os/os2/stat_linux.odin

@@ -4,7 +4,6 @@ package os2
 import "core:time"
 import "core:time"
 import "base:runtime"
 import "base:runtime"
 import "core:sys/linux"
 import "core:sys/linux"
-import "core:path/filepath"
 
 
 _fstat :: proc(f: ^File, allocator: runtime.Allocator) -> (File_Info, Error) {
 _fstat :: proc(f: ^File, allocator: runtime.Allocator) -> (File_Info, Error) {
 	impl := (^File_Impl)(f.impl)
 	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
 		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.creation_time = fi.modification_time
-	fi.name = filepath.base(fi.fullpath)
+	_, fi.name = split_path(fi.fullpath)
 	return
 	return
 }
 }
 
 

+ 2 - 3
core/os/os2/stat_posix.odin

@@ -4,13 +4,12 @@ package os2
 
 
 import "base:runtime"
 import "base:runtime"
 
 
-import "core:path/filepath"
 import "core:sys/posix"
 import "core:sys/posix"
 import "core:time"
 import "core:time"
 
 
 internal_stat :: proc(stat: posix.stat_t, fullpath: string) -> (fi: File_Info) {
 internal_stat :: proc(stat: posix.stat_t, fullpath: string) -> (fi: File_Info) {
 	fi.fullpath = fullpath
 	fi.fullpath = fullpath
-	fi.name = filepath.base(fi.fullpath)
+	_, fi.name = split_path(fi.fullpath)
 
 
 	fi.inode = u128(stat.st_ino)
 	fi.inode = u128(stat.st_ino)
 	fi.size = i64(stat.st_size)
 	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",
 	// NOTE: This might not be correct when given "/symlink/foo.txt",
 	// you would want that to resolve "/symlink", but not resolve "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)
 	assert(len(fullpath) > 0)
 	switch {
 	switch {
 	case fullpath[0] == '/':
 	case fullpath[0] == '/':

+ 1 - 2
core/os/os2/stat_wasi.odin

@@ -3,13 +3,12 @@ package os2
 
 
 import "base:runtime"
 import "base:runtime"
 
 
-import "core:path/filepath"
 import "core:sys/wasm/wasi"
 import "core:sys/wasm/wasi"
 import "core:time"
 import "core:time"
 
 
 internal_stat :: proc(stat: wasi.filestat_t, fullpath: string) -> (fi: File_Info) {
 internal_stat :: proc(stat: wasi.filestat_t, fullpath: string) -> (fi: File_Info) {
 	fi.fullpath = fullpath
 	fi.fullpath = fullpath
-	fi.name = filepath.base(fi.fullpath)
+	_, fi.name = split_path(fi.fullpath)
 
 
 	fi.inode = u128(stat.ino)
 	fi.inode = u128(stat.ino)
 	fi.size  = i64(stat.size)
 	fi.size  = i64(stat.size)

+ 24 - 44
core/os/os2/stat_windows.odin

@@ -315,57 +315,37 @@ _is_UNC :: proc(path: string) -> bool {
 }
 }
 
 
 _volume_name_len :: proc(path: string) -> int {
 _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
 	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])
-}
-

+ 22 - 12
tests/core/os/os2/dir.odin

@@ -2,27 +2,27 @@ package tests_core_os_os2
 
 
 import os "core:os/os2"
 import os "core:os/os2"
 import    "core:log"
 import    "core:log"
-import    "core:path/filepath"
 import    "core:slice"
 import    "core:slice"
 import    "core:testing"
 import    "core:testing"
 import    "core:strings"
 import    "core:strings"
 
 
 @(test)
 @(test)
 test_read_dir :: proc(t: ^testing.T) {
 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)
 	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)
 	defer os.file_info_slice_delete(fis, context.allocator)
 
 
 	slice.sort_by_key(fis, proc(fi: os.File_Info) -> string { return fi.name })
 	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")
 		log.warn("os2 directory functionality is unsupported, skipping test")
 		return
 		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, len(fis), 2)
 
 
 	testing.expect_value(t, fis[0].name, "b.txt")
 	testing.expect_value(t, fis[0].name, "b.txt")
@@ -34,8 +34,9 @@ test_read_dir :: proc(t: ^testing.T) {
 
 
 @(test)
 @(test)
 test_walker :: proc(t: ^testing.T) {
 test_walker :: proc(t: ^testing.T) {
-	path := filepath.join({#directory, "../dir"})
+	path, err := os.join_path({#directory, "../dir"}, context.allocator)
 	defer delete(path)
 	defer delete(path)
+	testing.expect_value(t, err, nil)
 
 
 	w := os.walker_create(path)
 	w := os.walker_create(path)
 	defer os.walker_destroy(&w)
 	defer os.walker_destroy(&w)
@@ -45,11 +46,12 @@ test_walker :: proc(t: ^testing.T) {
 
 
 @(test)
 @(test)
 test_walker_file :: proc(t: ^testing.T) {
 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)
 	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)
 	defer os.close(f)
 
 
 	w := os.walker_create(f)
 	w := os.walker_create(f)
@@ -64,10 +66,18 @@ test_walker_internal :: proc(t: ^testing.T, w: ^os.Walker) {
 		path: string,
 		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{
 	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
 	seen: [dynamic]Seen

+ 4 - 2
tests/core/os/os2/file.odin

@@ -2,11 +2,13 @@ package tests_core_os_os2
 
 
 import os "core:os/os2"
 import os "core:os/os2"
 import    "core:testing"
 import    "core:testing"
-import    "core:path/filepath"
 
 
 @(test)
 @(test)
 test_clone :: proc(t: ^testing.T) {
 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_value(t, err, nil)
 	testing.expect(t, f != nil)
 	testing.expect(t, f != nil)
 
 

+ 347 - 3
tests/core/os/os2/path.odin

@@ -2,7 +2,6 @@ package tests_core_os_os2
 
 
 import os "core:os/os2"
 import os "core:os/os2"
 import    "core:log"
 import    "core:log"
-import    "core:path/filepath"
 import    "core:testing"
 import    "core:testing"
 import    "core:strings"
 import    "core:strings"
 
 
@@ -17,6 +16,351 @@ test_executable :: proc(t: ^testing.T) {
 
 
 	testing.expect_value(t, err, nil)
 	testing.expect_value(t, err, nil)
 	testing.expect(t, len(path) > 0)
 	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)
+		}
+	}
 }
 }