aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorgingerBill <gingerBill@users.noreply.github.com>2025-02-28 14:37:47 +0000
committerGitHub <noreply@github.com>2025-02-28 14:37:47 +0000
commitbb429696f89467e5c1e517da850606f9c45fdb92 (patch)
tree46ac6a719135ba5ae88a9021605ffd00f1c84ca6
parent8a8894a9818945773e7dbb8fda031286b794b134 (diff)
parent0e4140a60215fbde090ed8e184d638134c28f7cc (diff)
Merge pull request #4877 from laytan/os2-additions
os/os2: recursive directory walker, expose errors in read_directory, file clone
-rw-r--r--core/os/os2/dir.odin124
-rw-r--r--core/os/os2/dir_linux.odin46
-rw-r--r--core/os/os2/dir_posix.odin65
-rw-r--r--core/os/os2/dir_walker.odin230
-rw-r--r--core/os/os2/dir_wasi.odin60
-rw-r--r--core/os/os2/dir_windows.odin45
-rw-r--r--core/os/os2/file.odin5
-rw-r--r--core/os/os2/file_linux.odin17
-rw-r--r--core/os/os2/file_posix.odin23
-rw-r--r--core/os/os2/file_wasi.odin26
-rw-r--r--core/os/os2/file_windows.odin23
-rw-r--r--core/os/os2/path_wasi.odin6
-rw-r--r--tests/core/os/os2/dir.odin74
-rw-r--r--tests/core/os/os2/file.odin31
14 files changed, 687 insertions, 88 deletions
diff --git a/core/os/os2/dir.odin b/core/os/os2/dir.odin
index a41ef68f9..4a7762ded 100644
--- a/core/os/os2/dir.odin
+++ b/core/os/os2/dir.odin
@@ -20,7 +20,7 @@ read_directory :: proc(f: ^File, n: int, allocator: runtime.Allocator) -> (files
TEMP_ALLOCATOR_GUARD()
- it := read_directory_iterator_create(f) or_return
+ it := read_directory_iterator_create(f)
defer _read_directory_iterator_destroy(&it)
dfi := make([dynamic]File_Info, 0, size, temp_allocator())
@@ -34,9 +34,14 @@ read_directory :: proc(f: ^File, n: int, allocator: runtime.Allocator) -> (files
if n > 0 && index == n {
break
}
+
+ _ = read_directory_iterator_error(&it) or_break
+
append(&dfi, file_info_clone(fi, allocator) or_return)
}
+ _ = read_directory_iterator_error(&it) or_return
+
return slice.clone(dfi[:], allocator)
}
@@ -61,22 +66,129 @@ read_all_directory_by_path :: proc(path: string, allocator: runtime.Allocator) -
Read_Directory_Iterator :: struct {
- f: ^File,
+ f: ^File,
+ err: struct {
+ err: Error,
+ path: [dynamic]byte,
+ },
+ index: int,
impl: Read_Directory_Iterator_Impl,
}
+/*
+Creates a directory iterator with the given directory.
-@(require_results)
-read_directory_iterator_create :: proc(f: ^File) -> (Read_Directory_Iterator, Error) {
- return _read_directory_iterator_create(f)
+For an example on how to use the iterator, see `read_directory_iterator`.
+*/
+read_directory_iterator_create :: proc(f: ^File) -> (it: Read_Directory_Iterator) {
+ read_directory_iterator_init(&it, f)
+ return
+}
+
+/*
+Initialize a directory iterator with the given directory.
+
+This procedure may be called on an existing iterator to reuse it for another directory.
+
+For an example on how to use the iterator, see `read_directory_iterator`.
+*/
+read_directory_iterator_init :: proc(it: ^Read_Directory_Iterator, f: ^File) {
+ it.err.err = nil
+ it.err.path.allocator = file_allocator()
+ clear(&it.err.path)
+
+ it.f = f
+ it.index = 0
+
+ _read_directory_iterator_init(it, f)
}
+/*
+Destroys a directory iterator.
+*/
read_directory_iterator_destroy :: proc(it: ^Read_Directory_Iterator) {
+ if it == nil {
+ return
+ }
+
+ delete(it.err.path)
+
_read_directory_iterator_destroy(it)
}
-// NOTE(bill): `File_Info` does not need to deleted on each iteration. Any copies must be manually copied with `file_info_clone`
+/*
+Retrieve the last error that happened during iteration.
+*/
+@(require_results)
+read_directory_iterator_error :: proc(it: ^Read_Directory_Iterator) -> (path: string, err: Error) {
+ return string(it.err.path[:]), it.err.err
+}
+
+@(private)
+read_directory_iterator_set_error :: proc(it: ^Read_Directory_Iterator, path: string, err: Error) {
+ if err == nil {
+ return
+ }
+
+ resize(&it.err.path, len(path))
+ copy(it.err.path[:], path)
+
+ it.err.err = err
+}
+
+/*
+Returns the next file info entry for the iterator's directory.
+
+The given `File_Info` is reused in subsequent calls so a copy (`file_info_clone`) has to be made to
+extend its lifetime.
+
+Example:
+ package main
+
+ import "core:fmt"
+ import os "core:os/os2"
+
+ main :: proc() {
+ f, oerr := os.open("core")
+ ensure(oerr == nil)
+ defer os.close(f)
+
+ it := os.read_directory_iterator_create(f)
+ defer os.read_directory_iterator_destroy(&it)
+
+ for info in os.read_directory_iterator(&it) {
+ // Optionally break on the first error:
+ // Supports not doing this, and keeping it going with remaining items.
+ // _ = os.read_directory_iterator_error(&it) or_break
+
+ // Handle error as we go:
+ // Again, no need to do this as it will keep going with remaining items.
+ if path, err := os.read_directory_iterator_error(&it); err != nil {
+ fmt.eprintfln("failed reading %s: %s", path, err)
+ continue
+ }
+
+ // Or, do not handle errors during iteration, and just check the error at the end.
+
+
+ fmt.printfln("%#v", info)
+ }
+
+ // Handle error if one happened during iteration at the end:
+ if path, err := os.read_directory_iterator_error(&it); err != nil {
+ fmt.eprintfln("read directory failed at %s: %s", path, err)
+ }
+ }
+*/
@(require_results)
read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info, index: int, ok: bool) {
+ if it.f == nil {
+ return
+ }
+
+ if it.index == 0 && it.err.err != nil {
+ return
+ }
+
return _read_directory_iterator(it)
}
diff --git a/core/os/os2/dir_linux.odin b/core/os/os2/dir_linux.odin
index f7723936b..a868a02c4 100644
--- a/core/os/os2/dir_linux.odin
+++ b/core/os/os2/dir_linux.odin
@@ -8,12 +8,11 @@ Read_Directory_Iterator_Impl :: struct {
dirent_backing: []u8,
dirent_buflen: int,
dirent_off: int,
- index: int,
}
@(require_results)
_read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info, index: int, ok: bool) {
- scan_entries :: proc(dfd: linux.Fd, entries: []u8, offset: ^int) -> (fd: linux.Fd, file_name: string) {
+ scan_entries :: proc(it: ^Read_Directory_Iterator, dfd: linux.Fd, entries: []u8, offset: ^int) -> (fd: linux.Fd, file_name: string) {
for d in linux.dirent_iterate_buf(entries, offset) {
file_name = linux.dirent_name(d)
if file_name == "." || file_name == ".." {
@@ -24,18 +23,21 @@ _read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info
entry_fd, errno := linux.openat(dfd, file_name_cstr, {.NOFOLLOW, .PATH})
if errno == .NONE {
return entry_fd, file_name
+ } else {
+ read_directory_iterator_set_error(it, file_name, _get_platform_error(errno))
}
}
+
return -1, ""
}
- index = it.impl.index
- it.impl.index += 1
+ index = it.index
+ it.index += 1
dfd := linux.Fd(_fd(it.f))
entries := it.impl.dirent_backing[:it.impl.dirent_buflen]
- entry_fd, file_name := scan_entries(dfd, entries, &it.impl.dirent_off)
+ entry_fd, file_name := scan_entries(it, dfd, entries, &it.impl.dirent_off)
for entry_fd == -1 {
if len(it.impl.dirent_backing) == 0 {
@@ -58,44 +60,60 @@ _read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info
it.impl.dirent_buflen = buflen
entries = it.impl.dirent_backing[:buflen]
break loop
- case: // error
+ case:
+ read_directory_iterator_set_error(it, name(it.f), _get_platform_error(errno))
return
}
}
- entry_fd, file_name = scan_entries(dfd, entries, &it.impl.dirent_off)
+ entry_fd, file_name = scan_entries(it, dfd, entries, &it.impl.dirent_off)
}
defer linux.close(entry_fd)
+ // PERF: reuse the fullpath string like on posix and wasi.
file_info_delete(it.impl.prev_fi, file_allocator())
- fi, _ = _fstat_internal(entry_fd, file_allocator())
+
+ err: Error
+ fi, err = _fstat_internal(entry_fd, file_allocator())
it.impl.prev_fi = fi
+ if err != nil {
+ path, _ := _get_full_path(entry_fd, temp_allocator())
+ read_directory_iterator_set_error(it, path, err)
+ }
+
ok = true
return
}
-@(require_results)
-_read_directory_iterator_create :: proc(f: ^File) -> (Read_Directory_Iterator, Error) {
+_read_directory_iterator_init :: proc(it: ^Read_Directory_Iterator, f: ^File) {
+ // NOTE: Allow calling `init` to target a new directory with the same iterator.
+ it.impl.dirent_buflen = 0
+ it.impl.dirent_off = 0
+
if f == nil || f.impl == nil {
- return {}, .Invalid_File
+ read_directory_iterator_set_error(it, "", .Invalid_File)
+ return
}
stat: linux.Stat
errno := linux.fstat(linux.Fd(fd(f)), &stat)
if errno != .NONE {
- return {}, _get_platform_error(errno)
+ read_directory_iterator_set_error(it, name(f), _get_platform_error(errno))
+ return
}
+
if (stat.mode & linux.S_IFMT) != linux.S_IFDIR {
- return {}, .Invalid_Dir
+ read_directory_iterator_set_error(it, name(f), .Invalid_Dir)
+ return
}
- return {f = f}, nil
}
_read_directory_iterator_destroy :: proc(it: ^Read_Directory_Iterator) {
if it == nil {
return
}
+
delete(it.impl.dirent_backing, file_allocator())
file_info_delete(it.impl.prev_fi, file_allocator())
}
diff --git a/core/os/os2/dir_posix.odin b/core/os/os2/dir_posix.odin
index 36cac2597..d9fa16f8d 100644
--- a/core/os/os2/dir_posix.odin
+++ b/core/os/os2/dir_posix.odin
@@ -6,7 +6,6 @@ import "core:sys/posix"
Read_Directory_Iterator_Impl :: struct {
dir: posix.DIR,
- idx: int,
fullpath: [dynamic]byte,
}
@@ -14,14 +13,16 @@ Read_Directory_Iterator_Impl :: struct {
_read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info, index: int, ok: bool) {
fimpl := (^File_Impl)(it.f.impl)
- index = it.impl.idx
- it.impl.idx += 1
+ index = it.index
+ it.index += 1
for {
+ posix.set_errno(nil)
entry := posix.readdir(it.impl.dir)
if entry == nil {
- // NOTE(laytan): would be good to have an `error` field on the `Read_Directory_Iterator`
- // There isn't a way to now know if it failed or if we are at the end.
+ if errno := posix.errno(); errno != nil {
+ read_directory_iterator_set_error(it, name(it.f), _get_platform_error(errno))
+ }
return
}
@@ -31,54 +32,62 @@ _read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info
}
sname := string(cname)
- stat: posix.stat_t
- if posix.fstatat(posix.dirfd(it.impl.dir), cname, &stat, { .SYMLINK_NOFOLLOW }) != .OK {
- // NOTE(laytan): would be good to have an `error` field on the `Read_Directory_Iterator`
- // There isn't a way to now know if it failed or if we are at the end.
- return
- }
-
n := len(fimpl.name)+1
if err := non_zero_resize(&it.impl.fullpath, n+len(sname)); err != nil {
- // Can't really tell caller we had an error, sad.
+ read_directory_iterator_set_error(it, sname, err)
+ ok = true
return
}
copy(it.impl.fullpath[n:], sname)
+ stat: posix.stat_t
+ if posix.fstatat(posix.dirfd(it.impl.dir), cname, &stat, { .SYMLINK_NOFOLLOW }) != .OK {
+ read_directory_iterator_set_error(it, string(it.impl.fullpath[:]), _get_platform_error())
+ ok = true
+ return
+ }
+
fi = internal_stat(stat, string(it.impl.fullpath[:]))
ok = true
return
}
}
-@(require_results)
-_read_directory_iterator_create :: proc(f: ^File) -> (iter: Read_Directory_Iterator, err: Error) {
+_read_directory_iterator_init :: proc(it: ^Read_Directory_Iterator, f: ^File) {
if f == nil || f.impl == nil {
- err = .Invalid_File
+ read_directory_iterator_set_error(it, "", .Invalid_File)
return
}
impl := (^File_Impl)(f.impl)
- iter.f = f
- iter.impl.idx = 0
+ // NOTE: Allow calling `init` to target a new directory with the same iterator.
+ it.impl.fullpath.allocator = file_allocator()
+ clear(&it.impl.fullpath)
+ if err := reserve(&it.impl.fullpath, len(impl.name)+128); err != nil {
+ read_directory_iterator_set_error(it, name(f), err)
+ return
+ }
- iter.impl.fullpath = make([dynamic]byte, 0, len(impl.name)+128, file_allocator()) or_return
- append(&iter.impl.fullpath, impl.name)
- append(&iter.impl.fullpath, "/")
- defer if err != nil { delete(iter.impl.fullpath) }
+ append(&it.impl.fullpath, impl.name)
+ append(&it.impl.fullpath, "/")
// `fdopendir` consumes the file descriptor so we need to `dup` it.
dupfd := posix.dup(impl.fd)
if dupfd == -1 {
- err = _get_platform_error()
+ read_directory_iterator_set_error(it, name(f), _get_platform_error())
return
}
- defer if err != nil { posix.close(dupfd) }
+ defer if it.err.err != nil { posix.close(dupfd) }
+
+ // NOTE: Allow calling `init` to target a new directory with the same iterator.
+ if it.impl.dir != nil {
+ posix.closedir(it.impl.dir)
+ }
- iter.impl.dir = posix.fdopendir(dupfd)
- if iter.impl.dir == nil {
- err = _get_platform_error()
+ it.impl.dir = posix.fdopendir(dupfd)
+ if it.impl.dir == nil {
+ read_directory_iterator_set_error(it, name(f), _get_platform_error())
return
}
@@ -86,7 +95,7 @@ _read_directory_iterator_create :: proc(f: ^File) -> (iter: Read_Directory_Itera
}
_read_directory_iterator_destroy :: proc(it: ^Read_Directory_Iterator) {
- if it == nil || it.impl.dir == nil {
+ if it.impl.dir == nil {
return
}
diff --git a/core/os/os2/dir_walker.odin b/core/os/os2/dir_walker.odin
new file mode 100644
index 000000000..0af751f31
--- /dev/null
+++ b/core/os/os2/dir_walker.odin
@@ -0,0 +1,230 @@
+package os2
+
+import "core:container/queue"
+
+/*
+A recursive directory walker.
+
+Note that none of the fields should be accessed directly.
+*/
+Walker :: struct {
+ todo: queue.Queue(string),
+ skip_dir: bool,
+ err: struct {
+ path: [dynamic]byte,
+ err: Error,
+ },
+ iter: Read_Directory_Iterator,
+}
+
+walker_init_path :: proc(w: ^Walker, path: string) {
+ cloned_path, err := clone_string(path, file_allocator())
+ if err != nil {
+ walker_set_error(w, path, err)
+ return
+ }
+
+ walker_clear(w)
+
+ if _, err = queue.push(&w.todo, cloned_path); err != nil {
+ walker_set_error(w, cloned_path, err)
+ return
+ }
+}
+
+walker_init_file :: proc(w: ^Walker, f: ^File) {
+ handle, err := clone(f)
+ if err != nil {
+ path, _ := clone_string(name(f), file_allocator())
+ walker_set_error(w, path, err)
+ return
+ }
+
+ walker_clear(w)
+
+ read_directory_iterator_init(&w.iter, handle)
+}
+
+/*
+Initializes a walker, either using a path or a file pointer to a directory the walker will start at.
+
+You are allowed to repeatedly call this to reuse it for later walks.
+
+For an example on how to use the walker, see `walker_walk`.
+*/
+walker_init :: proc {
+ walker_init_path,
+ walker_init_file,
+}
+
+@(require_results)
+walker_create_path :: proc(path: string) -> (w: Walker) {
+ walker_init_path(&w, path)
+ return
+}
+
+@(require_results)
+walker_create_file :: proc(f: ^File) -> (w: Walker) {
+ walker_init_file(&w, f)
+ return
+}
+
+/*
+Creates a walker, either using a path or a file pointer to a directory the walker will start at.
+
+For an example on how to use the walker, see `walker_walk`.
+*/
+walker_create :: proc {
+ walker_create_path,
+ walker_create_file,
+}
+
+/*
+Returns the last error that occurred during the walker's operations.
+
+Can be called while iterating, or only at the end to check if anything failed.
+*/
+@(require_results)
+walker_error :: proc(w: ^Walker) -> (path: string, err: Error) {
+ return string(w.err.path[:]), w.err.err
+}
+
+@(private)
+walker_set_error :: proc(w: ^Walker, path: string, err: Error) {
+ if err == nil {
+ return
+ }
+
+ resize(&w.err.path, len(path))
+ copy(w.err.path[:], path)
+
+ w.err.err = err
+}
+
+@(private)
+walker_clear :: proc(w: ^Walker) {
+ w.iter.f = nil
+ w.skip_dir = false
+
+ w.err.path.allocator = file_allocator()
+ clear(&w.err.path)
+
+ w.todo.data.allocator = file_allocator()
+ for path in queue.pop_front_safe(&w.todo) {
+ delete(path, file_allocator())
+ }
+}
+
+walker_destroy :: proc(w: ^Walker) {
+ walker_clear(w)
+ queue.destroy(&w.todo)
+ delete(w.err.path)
+ read_directory_iterator_destroy(&w.iter)
+}
+
+// Marks the current directory to be skipped (not entered into).
+walker_skip_dir :: proc(w: ^Walker) {
+ w.skip_dir = true
+}
+
+/*
+Returns the next file info in the iterator, files are iterated in breadth-first order.
+
+If an error occurred opening a directory, you may get zero'd info struct and
+`walker_error` will return the error.
+
+Example:
+ package main
+
+ import "core:fmt"
+ import "core:strings"
+ import os "core:os/os2"
+
+ main :: proc() {
+ w := os.walker_create("core")
+ defer os.walker_destroy(&w)
+
+ for info in os.walker_walk(&w) {
+ // Optionally break on the first error:
+ // _ = walker_error(&w) or_break
+
+ // Or, handle error as we go:
+ if path, err := os.walker_error(&w); err != nil {
+ fmt.eprintfln("failed walking %s: %s", path, err)
+ continue
+ }
+
+ // Or, do not handle errors during iteration, and just check the error at the end.
+
+
+
+ // Skip a directory:
+ if strings.has_suffix(info.fullpath, ".git") {
+ os.walker_skip_dir(&w)
+ continue
+ }
+
+ fmt.printfln("%#v", info)
+ }
+
+ // Handle error if one happened during iteration at the end:
+ if path, err := os.walker_error(&w); err != nil {
+ fmt.eprintfln("failed walking %s: %v", path, err)
+ }
+ }
+*/
+@(require_results)
+walker_walk :: proc(w: ^Walker) -> (fi: File_Info, ok: bool) {
+ if w.skip_dir {
+ w.skip_dir = false
+ if skip, sok := queue.pop_back_safe(&w.todo); sok {
+ delete(skip, file_allocator())
+ }
+ }
+
+ if w.iter.f == nil {
+ if queue.len(w.todo) == 0 {
+ return
+ }
+
+ next := queue.pop_front(&w.todo)
+
+ handle, err := open(next)
+ if err != nil {
+ walker_set_error(w, next, err)
+ return {}, true
+ }
+
+ read_directory_iterator_init(&w.iter, handle)
+
+ delete(next, file_allocator())
+ }
+
+ info, _, iter_ok := read_directory_iterator(&w.iter)
+
+ if path, err := read_directory_iterator_error(&w.iter); err != nil {
+ walker_set_error(w, path, err)
+ }
+
+ if !iter_ok {
+ close(w.iter.f)
+ w.iter.f = nil
+ return walker_walk(w)
+ }
+
+ if info.type == .Directory {
+ path, err := clone_string(info.fullpath, file_allocator())
+ if err != nil {
+ walker_set_error(w, "", err)
+ return
+ }
+
+ _, err = queue.push_back(&w.todo, path)
+ if err != nil {
+ walker_set_error(w, path, err)
+ return
+ }
+ }
+
+ return info, iter_ok
+}
diff --git a/core/os/os2/dir_wasi.odin b/core/os/os2/dir_wasi.odin
index e4349069a..61c005674 100644
--- a/core/os/os2/dir_wasi.odin
+++ b/core/os/os2/dir_wasi.odin
@@ -1,6 +1,8 @@
#+private
package os2
+import "base:runtime"
+import "core:slice"
import "base:intrinsics"
import "core:sys/wasm/wasi"
@@ -8,7 +10,6 @@ Read_Directory_Iterator_Impl :: struct {
fullpath: [dynamic]byte,
buf: []byte,
off: int,
- idx: int,
}
@(require_results)
@@ -17,8 +18,8 @@ _read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info
buf := it.impl.buf[it.impl.off:]
- index = it.impl.idx
- it.impl.idx += 1
+ index = it.index
+ it.index += 1
for {
if len(buf) < size_of(wasi.dirent_t) {
@@ -28,10 +29,7 @@ _read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info
entry := intrinsics.unaligned_load((^wasi.dirent_t)(raw_data(buf)))
buf = buf[size_of(wasi.dirent_t):]
- if len(buf) < int(entry.d_namlen) {
- // shouldn't be possible.
- return
- }
+ assert(len(buf) < int(entry.d_namlen))
name := string(buf[:entry.d_namlen])
buf = buf[entry.d_namlen:]
@@ -43,7 +41,8 @@ _read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info
n := len(fimpl.name)+1
if alloc_err := non_zero_resize(&it.impl.fullpath, n+len(name)); alloc_err != nil {
- // Can't really tell caller we had an error, sad.
+ read_directory_iterator_set_error(it, name, alloc_err)
+ ok = true
return
}
copy(it.impl.fullpath[n:], name)
@@ -55,6 +54,7 @@ _read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info
ino = entry.d_ino,
filetype = entry.d_type,
}
+ read_directory_iterator_set_error(it, string(it.impl.fullpath[:]), _get_platform_error(err))
}
fi = internal_stat(stat, string(it.impl.fullpath[:]))
@@ -63,27 +63,35 @@ _read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info
}
}
-@(require_results)
-_read_directory_iterator_create :: proc(f: ^File) -> (iter: Read_Directory_Iterator, err: Error) {
+_read_directory_iterator_init :: proc(it: ^Read_Directory_Iterator, f: ^File) {
+ // NOTE: Allow calling `init` to target a new directory with the same iterator.
+ it.impl.off = 0
+
if f == nil || f.impl == nil {
- err = .Invalid_File
+ read_directory_iterator_set_error(it, "", .Invalid_File)
return
}
impl := (^File_Impl)(f.impl)
- iter.f = f
buf: [dynamic]byte
+ // NOTE: Allow calling `init` to target a new directory with the same iterator.
+ if it.impl.buf != nil {
+ buf = slice.into_dynamic(it.impl.buf)
+ }
buf.allocator = file_allocator()
- defer if err != nil { delete(buf) }
- // NOTE: this is very grug.
+ defer if it.err.err != nil { delete(buf) }
+
for {
- non_zero_resize(&buf, 512 if len(buf) == 0 else len(buf)*2) or_return
+ if err := non_zero_resize(&buf, 512 if len(buf) == 0 else len(buf)*2); err != nil {
+ read_directory_iterator_set_error(it, name(f), err)
+ return
+ }
- n, _err := wasi.fd_readdir(__fd(f), buf[:], 0)
- if _err != nil {
- err = _get_platform_error(_err)
+ n, err := wasi.fd_readdir(__fd(f), buf[:], 0)
+ if err != nil {
+ read_directory_iterator_set_error(it, name(f), _get_platform_error(err))
return
}
@@ -94,11 +102,18 @@ _read_directory_iterator_create :: proc(f: ^File) -> (iter: Read_Directory_Itera
assert(n == len(buf))
}
- iter.impl.buf = buf[:]
+ it.impl.buf = buf[:]
+
+ // NOTE: Allow calling `init` to target a new directory with the same iterator.
+ it.impl.fullpath.allocator = file_allocator()
+ clear(&it.impl.fullpath)
+ if err := reserve(&it.impl.fullpath, len(impl.name)+128); err != nil {
+ read_directory_iterator_set_error(it, name(f), err)
+ return
+ }
- iter.impl.fullpath = make([dynamic]byte, 0, len(impl.name)+128, file_allocator()) or_return
- append(&iter.impl.fullpath, impl.name)
- append(&iter.impl.fullpath, "/")
+ append(&it.impl.fullpath, impl.name)
+ append(&it.impl.fullpath, "/")
return
}
@@ -106,5 +121,4 @@ _read_directory_iterator_create :: proc(f: ^File) -> (iter: Read_Directory_Itera
_read_directory_iterator_destroy :: proc(it: ^Read_Directory_Iterator) {
delete(it.impl.buf, file_allocator())
delete(it.impl.fullpath)
- it^ = {}
}
diff --git a/core/os/os2/dir_windows.odin b/core/os/os2/dir_windows.odin
index f71e7e763..d592e8036 100644
--- a/core/os/os2/dir_windows.odin
+++ b/core/os/os2/dir_windows.odin
@@ -44,16 +44,11 @@ Read_Directory_Iterator_Impl :: struct {
path: string,
prev_fi: File_Info,
no_more_files: bool,
- index: int,
}
@(require_results)
_read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info, index: int, ok: bool) {
- if it.f == nil {
- return
- }
-
TEMP_ALLOCATOR_GUARD()
for !it.impl.no_more_files {
@@ -63,19 +58,21 @@ _read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info
fi, err = find_data_to_file_info(it.impl.path, &it.impl.find_data, file_allocator())
if err != nil {
+ read_directory_iterator_set_error(it, it.impl.path, err)
return
}
+
if fi.name != "" {
it.impl.prev_fi = fi
ok = true
- index = it.impl.index
- it.impl.index += 1
+ index = it.index
+ it.index += 1
}
if !win32.FindNextFileW(it.impl.find_handle, &it.impl.find_data) {
e := _get_platform_error()
- if pe, _ := is_platform_error(e); pe == i32(win32.ERROR_NO_MORE_FILES) {
- it.impl.no_more_files = true
+ if pe, _ := is_platform_error(e); pe != i32(win32.ERROR_NO_MORE_FILES) {
+ read_directory_iterator_set_error(it, it.impl.path, e)
}
it.impl.no_more_files = true
}
@@ -86,16 +83,27 @@ _read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info
return
}
-@(require_results)
-_read_directory_iterator_create :: proc(f: ^File) -> (it: Read_Directory_Iterator, err: Error) {
- if f == nil {
+_read_directory_iterator_init :: proc(it: ^Read_Directory_Iterator, f: ^File) {
+ it.impl.no_more_files = false
+
+ if f == nil || f.impl == nil {
+ read_directory_iterator_set_error(it, "", .Invalid_File)
return
}
+
it.f = f
impl := (^File_Impl)(f.impl)
+ // NOTE: Allow calling `init` to target a new directory with the same iterator - reset idx.
+ if it.impl.find_handle != nil {
+ win32.FindClose(it.impl.find_handle)
+ }
+ if it.impl.path != "" {
+ delete(it.impl.path, file_allocator())
+ }
+
if !is_directory(impl.name) {
- err = .Invalid_Dir
+ read_directory_iterator_set_error(it, impl.name, .Invalid_Dir)
return
}
@@ -118,14 +126,19 @@ _read_directory_iterator_create :: proc(f: ^File) -> (it: Read_Directory_Iterato
it.impl.find_handle = win32.FindFirstFileW(raw_data(wpath_search), &it.impl.find_data)
if it.impl.find_handle == win32.INVALID_HANDLE_VALUE {
- err = _get_platform_error()
+ read_directory_iterator_set_error(it, impl.name, _get_platform_error())
return
}
- defer if err != nil {
+ defer if it.err.err != nil {
win32.FindClose(it.impl.find_handle)
}
- it.impl.path = _cleanpath_from_buf(wpath, file_allocator()) or_return
+ err: Error
+ it.impl.path, err = _cleanpath_from_buf(wpath, file_allocator())
+ if err != nil {
+ read_directory_iterator_set_error(it, impl.name, err)
+ }
+
return
}
diff --git a/core/os/os2/file.odin b/core/os/os2/file.odin
index 1a25472a1..28d2bc69b 100644
--- a/core/os/os2/file.odin
+++ b/core/os/os2/file.odin
@@ -123,6 +123,11 @@ new_file :: proc(handle: uintptr, name: string) -> ^File {
}
@(require_results)
+clone :: proc(f: ^File) -> (^File, Error) {
+ return _clone(f)
+}
+
+@(require_results)
fd :: proc(f: ^File) -> uintptr {
return _fd(f)
}
diff --git a/core/os/os2/file_linux.odin b/core/os/os2/file_linux.odin
index 9f6625091..811ee7055 100644
--- a/core/os/os2/file_linux.odin
+++ b/core/os/os2/file_linux.odin
@@ -113,6 +113,23 @@ _new_file :: proc(fd: uintptr, _: string, allocator: runtime.Allocator) -> (f: ^
return &impl.file, nil
}
+_clone :: proc(f: ^File) -> (clone: ^File, err: Error) {
+ if f == nil || f.impl == nil {
+ return
+ }
+
+ fd := (^File_Impl)(f.impl).fd
+
+ clonefd, errno := linux.dup(fd)
+ if errno != nil {
+ err = _get_platform_error(errno)
+ return
+ }
+ defer if err != nil { linux.close(clonefd) }
+
+ return _new_file(uintptr(clonefd), "", file_allocator())
+}
+
@(require_results)
_open_buffered :: proc(name: string, buffer_size: uint, flags := File_Flags{.Read}, perm := 0o777) -> (f: ^File, err: Error) {
diff --git a/core/os/os2/file_posix.odin b/core/os/os2/file_posix.odin
index 184c89368..43d5866b1 100644
--- a/core/os/os2/file_posix.odin
+++ b/core/os/os2/file_posix.odin
@@ -114,6 +114,29 @@ __new_file :: proc(handle: posix.FD, allocator: runtime.Allocator) -> ^File {
return &impl.file
}
+_clone :: proc(f: ^File) -> (clone: ^File, err: Error) {
+ if f == nil || f.impl == nil {
+ err = .Invalid_Pointer
+ return
+ }
+
+ impl := (^File_Impl)(f.impl)
+
+ fd := posix.dup(impl.fd)
+ if fd <= 0 {
+ err = _get_platform_error()
+ return
+ }
+ defer if err != nil { posix.close(fd) }
+
+ clone = __new_file(fd, file_allocator())
+ clone_impl := (^File_Impl)(clone.impl)
+ clone_impl.cname = clone_to_cstring(impl.name, file_allocator()) or_return
+ clone_impl.name = string(clone_impl.cname)
+
+ return
+}
+
_close :: proc(f: ^File_Impl) -> (err: Error) {
if f == nil { return nil }
diff --git a/core/os/os2/file_wasi.odin b/core/os/os2/file_wasi.odin
index 2b722e5dd..0245841e3 100644
--- a/core/os/os2/file_wasi.odin
+++ b/core/os/os2/file_wasi.odin
@@ -223,6 +223,32 @@ _new_file :: proc(handle: uintptr, name: string, allocator: runtime.Allocator) -
return &impl.file, nil
}
+_clone :: proc(f: ^File) -> (clone: ^File, err: Error) {
+ if f == nil || f.impl == nil {
+ return
+ }
+
+ dir_fd, relative, ok := match_preopen(name(f))
+ if !ok {
+ return nil, .Invalid_Path
+ }
+
+ fd, fderr := wasi.path_open(dir_fd, {.SYMLINK_FOLLOW}, relative, {}, {}, {}, {})
+ if fderr != nil {
+ err = _get_platform_error(fderr)
+ return
+ }
+ defer if err != nil { wasi.fd_close(fd) }
+
+ fderr = wasi.fd_renumber((^File_Impl)(f.impl).fd, fd)
+ if fderr != nil {
+ err = _get_platform_error(fderr)
+ return
+ }
+
+ return _new_file(uintptr(fd), name(f), file_allocator())
+}
+
_close :: proc(f: ^File_Impl) -> (err: Error) {
if errno := wasi.fd_close(f.fd); errno != nil {
err = _get_platform_error(errno)
diff --git a/core/os/os2/file_windows.odin b/core/os/os2/file_windows.odin
index f594cc72f..b123330e0 100644
--- a/core/os/os2/file_windows.odin
+++ b/core/os/os2/file_windows.odin
@@ -210,6 +210,29 @@ _new_file_buffered :: proc(handle: uintptr, name: string, buffer_size: uint) ->
return
}
+_clone :: proc(f: ^File) -> (clone: ^File, err: Error) {
+ if f == nil || f.impl == nil {
+ return
+ }
+
+ clonefd: win32.HANDLE
+ process := win32.GetCurrentProcess()
+ if !win32.DuplicateHandle(
+ process,
+ win32.HANDLE(_fd(f)),
+ process,
+ &clonefd,
+ 0,
+ false,
+ win32.DUPLICATE_SAME_ACCESS,
+ ) {
+ err = _get_platform_error()
+ return
+ }
+ defer if err != nil { win32.CloseHandle(clonefd) }
+
+ return _new_file(uintptr(clonefd), name(f), file_allocator())
+}
_fd :: proc(f: ^File) -> uintptr {
if f == nil || f.impl == nil {
diff --git a/core/os/os2/path_wasi.odin b/core/os/os2/path_wasi.odin
index 2f8a3c8c6..1c4fafa17 100644
--- a/core/os/os2/path_wasi.odin
+++ b/core/os/os2/path_wasi.odin
@@ -60,16 +60,20 @@ _remove_all :: proc(path: string) -> (err: Error) {
dir := open(path) or_return
defer close(dir)
- iter := read_directory_iterator_create(dir) or_return
+ iter := read_directory_iterator_create(dir)
defer read_directory_iterator_destroy(&iter)
for fi in read_directory_iterator(&iter) {
+ _ = read_directory_iterator_error(&iter) or_break
+
if fi.type == .Directory {
_remove_all(fi.fullpath) or_return
} else {
remove(fi.fullpath) or_return
}
}
+
+ _ = read_directory_iterator_error(&iter) or_return
}
return remove(path)
diff --git a/tests/core/os/os2/dir.odin b/tests/core/os/os2/dir.odin
index 5bb5c9820..7077e9ae2 100644
--- a/tests/core/os/os2/dir.odin
+++ b/tests/core/os/os2/dir.odin
@@ -5,6 +5,7 @@ import "core:log"
import "core:path/filepath"
import "core:slice"
import "core:testing"
+import "core:strings"
@(test)
test_read_dir :: proc(t: ^testing.T) {
@@ -30,3 +31,76 @@ test_read_dir :: proc(t: ^testing.T) {
testing.expect_value(t, fis[1].name, "sub")
testing.expect_value(t, fis[1].type, os.File_Type.Directory)
}
+
+@(test)
+test_walker :: proc(t: ^testing.T) {
+ path := filepath.join({#directory, "../dir"})
+ defer delete(path)
+
+ w := os.walker_create(path)
+ defer os.walker_destroy(&w)
+
+ test_walker_internal(t, &w)
+}
+
+@(test)
+test_walker_file :: proc(t: ^testing.T) {
+ path := filepath.join({#directory, "../dir"})
+ defer delete(path)
+
+ f, err := os.open(path)
+ testing.expect_value(t, err, nil)
+ defer os.close(f)
+
+ w := os.walker_create(f)
+ defer os.walker_destroy(&w)
+
+ test_walker_internal(t, &w)
+}
+
+test_walker_internal :: proc(t: ^testing.T, w: ^os.Walker) {
+ Seen :: struct {
+ type: os.File_Type,
+ path: string,
+ }
+
+ expected := [?]Seen{
+ {.Regular, filepath.join({"dir", "b.txt"})},
+ {.Directory, filepath.join({"dir", "sub"})},
+ {.Regular, filepath.join({"dir", "sub", ".gitkeep"})},
+ }
+
+ seen: [dynamic]Seen
+ defer delete(seen)
+
+ for info in os.walker_walk(w) {
+
+ errpath, err := os.walker_error(w)
+ testing.expectf(t, err == nil, "walker error for %q: %v", errpath, err)
+
+ append(&seen, Seen{
+ info.type,
+ strings.clone(info.fullpath),
+ })
+ }
+
+ if _, err := os.walker_error(w); err == .Unsupported {
+ log.warn("os2 directory functionality is unsupported, skipping test")
+ return
+ }
+
+ testing.expect_value(t, len(seen), len(expected))
+
+ for expectation in expected {
+ found: bool
+ for entry in seen {
+ if strings.has_suffix(entry.path, expectation.path) {
+ found = true
+ testing.expect_value(t, entry.type, expectation.type)
+ delete(entry.path)
+ }
+ }
+ testing.expectf(t, found, "%q not found in %v", expectation, seen)
+ delete(expectation.path)
+ }
+}
diff --git a/tests/core/os/os2/file.odin b/tests/core/os/os2/file.odin
new file mode 100644
index 000000000..c4df74f4a
--- /dev/null
+++ b/tests/core/os/os2/file.odin
@@ -0,0 +1,31 @@
+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))
+ testing.expect_value(t, err, nil)
+ testing.expect(t, f != nil)
+
+ clone: ^os.File
+ clone, err = os.clone(f)
+ testing.expect_value(t, err, nil)
+ testing.expect(t, clone != nil)
+
+ testing.expect_value(t, os.name(clone), os.name(f))
+ testing.expect(t, os.fd(clone) != os.fd(f))
+
+ os.close(f)
+
+ buf: [128]byte
+ n: int
+ n, err = os.read(clone, buf[:])
+ testing.expect_value(t, err, nil)
+ testing.expect(t, n > 13)
+ testing.expect_value(t, string(buf[:13]), "package tests")
+
+ os.close(clone)
+}