diff options
| author | gingerBill <gingerBill@users.noreply.github.com> | 2025-03-24 11:19:21 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-03-24 11:19:21 +0000 |
| commit | 4a595f9dac935a4fc9a0f7905e5d9ab81cb98fbe (patch) | |
| tree | 220633f7be35fe254b667293fc60e5c20961aa43 | |
| parent | 6fd752f647525d82f9d1541715f3317735bcab25 (diff) | |
| parent | 649376fcfe330090b8c6020e29b0600ca7dd9a3b (diff) | |
Merge pull request #4954 from Feoramund/os2-path
Add new path API for `os2`
| -rw-r--r-- | core/os/os2/path.odin | 415 | ||||
| -rw-r--r-- | core/os/os2/path_linux.odin | 2 | ||||
| -rw-r--r-- | core/os/os2/path_posix.odin | 5 | ||||
| -rw-r--r-- | core/os/os2/path_posixfs.odin | 78 | ||||
| -rw-r--r-- | core/os/os2/path_wasi.odin | 5 | ||||
| -rw-r--r-- | core/os/os2/path_windows.odin | 96 | ||||
| -rw-r--r-- | core/os/os2/process_linux.odin | 5 | ||||
| -rw-r--r-- | core/os/os2/process_posix.odin | 3 | ||||
| -rw-r--r-- | core/os/os2/stat.odin | 3 | ||||
| -rw-r--r-- | core/os/os2/stat_linux.odin | 3 | ||||
| -rw-r--r-- | core/os/os2/stat_posix.odin | 5 | ||||
| -rw-r--r-- | core/os/os2/stat_wasi.odin | 3 | ||||
| -rw-r--r-- | core/os/os2/stat_windows.odin | 68 | ||||
| -rw-r--r-- | tests/core/os/os2/dir.odin | 34 | ||||
| -rw-r--r-- | tests/core/os/os2/file.odin | 6 | ||||
| -rw-r--r-- | tests/core/os/os2/path.odin | 350 |
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) + } + } } |