Merge pull request #4954 from Feoramund/os2-path

Add new path API for `os2`
This commit is contained in:
gingerBill
2025-03-24 11:19:21 +00:00
committed by GitHub
16 changed files with 996 additions and 87 deletions

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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]

View File

@@ -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
}

View File

@@ -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]

View File

@@ -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
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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] == '/':

View File

@@ -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)

View File

@@ -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
}
}
return n
}
break
for ; n < l; n += 1 {
if _is_path_separator(path[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])
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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)
}
}
}