Merge pull request #4877 from laytan/os2-additions

os/os2: recursive directory walker, expose errors in read_directory, file clone
This commit is contained in:
gingerBill
2025-02-28 14:37:47 +00:00
committed by GitHub
14 changed files with 687 additions and 88 deletions
+118 -6
View File
@@ -20,7 +20,7 @@ read_directory :: proc(f: ^File, n: int, allocator: runtime.Allocator) -> (files
TEMP_ALLOCATOR_GUARD() TEMP_ALLOCATOR_GUARD()
it := read_directory_iterator_create(f) or_return it := read_directory_iterator_create(f)
defer _read_directory_iterator_destroy(&it) defer _read_directory_iterator_destroy(&it)
dfi := make([dynamic]File_Info, 0, size, temp_allocator()) 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 { if n > 0 && index == n {
break break
} }
_ = read_directory_iterator_error(&it) or_break
append(&dfi, file_info_clone(fi, allocator) or_return) append(&dfi, file_info_clone(fi, allocator) or_return)
} }
_ = read_directory_iterator_error(&it) or_return
return slice.clone(dfi[:], allocator) return slice.clone(dfi[:], allocator)
} }
@@ -61,22 +66,129 @@ read_all_directory_by_path :: proc(path: string, allocator: runtime.Allocator) -
Read_Directory_Iterator :: struct { Read_Directory_Iterator :: struct {
f: ^File, f: ^File,
err: struct {
err: Error,
path: [dynamic]byte,
},
index: int,
impl: Read_Directory_Iterator_Impl, impl: Read_Directory_Iterator_Impl,
} }
/*
Creates a directory iterator with the given directory.
@(require_results) For an example on how to use the iterator, see `read_directory_iterator`.
read_directory_iterator_create :: proc(f: ^File) -> (Read_Directory_Iterator, Error) { */
return _read_directory_iterator_create(f) 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) { read_directory_iterator_destroy :: proc(it: ^Read_Directory_Iterator) {
if it == nil {
return
}
delete(it.err.path)
_read_directory_iterator_destroy(it) _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) @(require_results)
read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info, index: int, ok: bool) { 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) return _read_directory_iterator(it)
} }
+32 -14
View File
@@ -8,12 +8,11 @@ Read_Directory_Iterator_Impl :: struct {
dirent_backing: []u8, dirent_backing: []u8,
dirent_buflen: int, dirent_buflen: int,
dirent_off: int, dirent_off: int,
index: int,
} }
@(require_results) @(require_results)
_read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info, index: int, ok: bool) { _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) { for d in linux.dirent_iterate_buf(entries, offset) {
file_name = linux.dirent_name(d) file_name = linux.dirent_name(d)
if file_name == "." || file_name == ".." { 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}) entry_fd, errno := linux.openat(dfd, file_name_cstr, {.NOFOLLOW, .PATH})
if errno == .NONE { if errno == .NONE {
return entry_fd, file_name return entry_fd, file_name
} else {
read_directory_iterator_set_error(it, file_name, _get_platform_error(errno))
} }
} }
return -1, "" return -1, ""
} }
index = it.impl.index index = it.index
it.impl.index += 1 it.index += 1
dfd := linux.Fd(_fd(it.f)) dfd := linux.Fd(_fd(it.f))
entries := it.impl.dirent_backing[:it.impl.dirent_buflen] 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 { for entry_fd == -1 {
if len(it.impl.dirent_backing) == 0 { 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 it.impl.dirent_buflen = buflen
entries = it.impl.dirent_backing[:buflen] entries = it.impl.dirent_backing[:buflen]
break loop break loop
case: // error case:
read_directory_iterator_set_error(it, name(it.f), _get_platform_error(errno))
return 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) defer linux.close(entry_fd)
// PERF: reuse the fullpath string like on posix and wasi.
file_info_delete(it.impl.prev_fi, file_allocator()) 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 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 ok = true
return return
} }
@(require_results) _read_directory_iterator_init :: proc(it: ^Read_Directory_Iterator, f: ^File) {
_read_directory_iterator_create :: proc(f: ^File) -> (Read_Directory_Iterator, Error) { // 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 { if f == nil || f.impl == nil {
return {}, .Invalid_File read_directory_iterator_set_error(it, "", .Invalid_File)
return
} }
stat: linux.Stat stat: linux.Stat
errno := linux.fstat(linux.Fd(fd(f)), &stat) errno := linux.fstat(linux.Fd(fd(f)), &stat)
if errno != .NONE { 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 { 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) { _read_directory_iterator_destroy :: proc(it: ^Read_Directory_Iterator) {
if it == nil { if it == nil {
return return
} }
delete(it.impl.dirent_backing, file_allocator()) delete(it.impl.dirent_backing, file_allocator())
file_info_delete(it.impl.prev_fi, file_allocator()) file_info_delete(it.impl.prev_fi, file_allocator())
} }
+37 -28
View File
@@ -6,7 +6,6 @@ import "core:sys/posix"
Read_Directory_Iterator_Impl :: struct { Read_Directory_Iterator_Impl :: struct {
dir: posix.DIR, dir: posix.DIR,
idx: int,
fullpath: [dynamic]byte, 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) { _read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info, index: int, ok: bool) {
fimpl := (^File_Impl)(it.f.impl) fimpl := (^File_Impl)(it.f.impl)
index = it.impl.idx index = it.index
it.impl.idx += 1 it.index += 1
for { for {
posix.set_errno(nil)
entry := posix.readdir(it.impl.dir) entry := posix.readdir(it.impl.dir)
if entry == nil { if entry == nil {
// NOTE(laytan): would be good to have an `error` field on the `Read_Directory_Iterator` if errno := posix.errno(); errno != nil {
// There isn't a way to now know if it failed or if we are at the end. read_directory_iterator_set_error(it, name(it.f), _get_platform_error(errno))
}
return return
} }
@@ -31,54 +32,62 @@ _read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info
} }
sname := string(cname) 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 n := len(fimpl.name)+1
if err := non_zero_resize(&it.impl.fullpath, n+len(sname)); err != nil { 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 return
} }
copy(it.impl.fullpath[n:], sname) 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[:])) fi = internal_stat(stat, string(it.impl.fullpath[:]))
ok = true ok = true
return return
} }
} }
@(require_results) _read_directory_iterator_init :: proc(it: ^Read_Directory_Iterator, f: ^File) {
_read_directory_iterator_create :: proc(f: ^File) -> (iter: Read_Directory_Iterator, err: Error) {
if f == nil || f.impl == nil { if f == nil || f.impl == nil {
err = .Invalid_File read_directory_iterator_set_error(it, "", .Invalid_File)
return return
} }
impl := (^File_Impl)(f.impl) impl := (^File_Impl)(f.impl)
iter.f = f // NOTE: Allow calling `init` to target a new directory with the same iterator.
iter.impl.idx = 0 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(&it.impl.fullpath, impl.name)
append(&iter.impl.fullpath, impl.name) append(&it.impl.fullpath, "/")
append(&iter.impl.fullpath, "/")
defer if err != nil { delete(iter.impl.fullpath) }
// `fdopendir` consumes the file descriptor so we need to `dup` it. // `fdopendir` consumes the file descriptor so we need to `dup` it.
dupfd := posix.dup(impl.fd) dupfd := posix.dup(impl.fd)
if dupfd == -1 { if dupfd == -1 {
err = _get_platform_error() read_directory_iterator_set_error(it, name(f), _get_platform_error())
return return
} }
defer if err != nil { posix.close(dupfd) } defer if it.err.err != nil { posix.close(dupfd) }
iter.impl.dir = posix.fdopendir(dupfd) // NOTE: Allow calling `init` to target a new directory with the same iterator.
if iter.impl.dir == nil { if it.impl.dir != nil {
err = _get_platform_error() posix.closedir(it.impl.dir)
}
it.impl.dir = posix.fdopendir(dupfd)
if it.impl.dir == nil {
read_directory_iterator_set_error(it, name(f), _get_platform_error())
return 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) { _read_directory_iterator_destroy :: proc(it: ^Read_Directory_Iterator) {
if it == nil || it.impl.dir == nil { if it.impl.dir == nil {
return return
} }
+230
View File
@@ -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
}
+37 -23
View File
@@ -1,6 +1,8 @@
#+private #+private
package os2 package os2
import "base:runtime"
import "core:slice"
import "base:intrinsics" import "base:intrinsics"
import "core:sys/wasm/wasi" import "core:sys/wasm/wasi"
@@ -8,7 +10,6 @@ Read_Directory_Iterator_Impl :: struct {
fullpath: [dynamic]byte, fullpath: [dynamic]byte,
buf: []byte, buf: []byte,
off: int, off: int,
idx: int,
} }
@(require_results) @(require_results)
@@ -17,8 +18,8 @@ _read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info
buf := it.impl.buf[it.impl.off:] buf := it.impl.buf[it.impl.off:]
index = it.impl.idx index = it.index
it.impl.idx += 1 it.index += 1
for { for {
if len(buf) < size_of(wasi.dirent_t) { 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))) entry := intrinsics.unaligned_load((^wasi.dirent_t)(raw_data(buf)))
buf = buf[size_of(wasi.dirent_t):] buf = buf[size_of(wasi.dirent_t):]
if len(buf) < int(entry.d_namlen) { assert(len(buf) < int(entry.d_namlen))
// shouldn't be possible.
return
}
name := string(buf[:entry.d_namlen]) name := string(buf[:entry.d_namlen])
buf = 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 n := len(fimpl.name)+1
if alloc_err := non_zero_resize(&it.impl.fullpath, n+len(name)); alloc_err != nil { 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 return
} }
copy(it.impl.fullpath[n:], name) 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, ino = entry.d_ino,
filetype = entry.d_type, 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[:])) 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_init :: proc(it: ^Read_Directory_Iterator, f: ^File) {
_read_directory_iterator_create :: proc(f: ^File) -> (iter: Read_Directory_Iterator, err: Error) { // NOTE: Allow calling `init` to target a new directory with the same iterator.
it.impl.off = 0
if f == nil || f.impl == nil { if f == nil || f.impl == nil {
err = .Invalid_File read_directory_iterator_set_error(it, "", .Invalid_File)
return return
} }
impl := (^File_Impl)(f.impl) impl := (^File_Impl)(f.impl)
iter.f = f
buf: [dynamic]byte 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() buf.allocator = file_allocator()
defer if err != nil { delete(buf) }
// NOTE: this is very grug. defer if it.err.err != nil { delete(buf) }
for { 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) n, err := wasi.fd_readdir(__fd(f), buf[:], 0)
if _err != nil { if err != nil {
err = _get_platform_error(_err) read_directory_iterator_set_error(it, name(f), _get_platform_error(err))
return return
} }
@@ -94,11 +102,18 @@ _read_directory_iterator_create :: proc(f: ^File) -> (iter: Read_Directory_Itera
assert(n == len(buf)) assert(n == len(buf))
} }
iter.impl.buf = buf[:] it.impl.buf = buf[:]
iter.impl.fullpath = make([dynamic]byte, 0, len(impl.name)+128, file_allocator()) or_return // NOTE: Allow calling `init` to target a new directory with the same iterator.
append(&iter.impl.fullpath, impl.name) it.impl.fullpath.allocator = file_allocator()
append(&iter.impl.fullpath, "/") 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
}
append(&it.impl.fullpath, impl.name)
append(&it.impl.fullpath, "/")
return 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) { _read_directory_iterator_destroy :: proc(it: ^Read_Directory_Iterator) {
delete(it.impl.buf, file_allocator()) delete(it.impl.buf, file_allocator())
delete(it.impl.fullpath) delete(it.impl.fullpath)
it^ = {}
} }
+29 -16
View File
@@ -44,16 +44,11 @@ Read_Directory_Iterator_Impl :: struct {
path: string, path: string,
prev_fi: File_Info, prev_fi: File_Info,
no_more_files: bool, no_more_files: bool,
index: int,
} }
@(require_results) @(require_results)
_read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info, index: int, ok: bool) { _read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info, index: int, ok: bool) {
if it.f == nil {
return
}
TEMP_ALLOCATOR_GUARD() TEMP_ALLOCATOR_GUARD()
for !it.impl.no_more_files { 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()) fi, err = find_data_to_file_info(it.impl.path, &it.impl.find_data, file_allocator())
if err != nil { if err != nil {
read_directory_iterator_set_error(it, it.impl.path, err)
return return
} }
if fi.name != "" { if fi.name != "" {
it.impl.prev_fi = fi it.impl.prev_fi = fi
ok = true ok = true
index = it.impl.index index = it.index
it.impl.index += 1 it.index += 1
} }
if !win32.FindNextFileW(it.impl.find_handle, &it.impl.find_data) { if !win32.FindNextFileW(it.impl.find_handle, &it.impl.find_data) {
e := _get_platform_error() e := _get_platform_error()
if pe, _ := is_platform_error(e); pe == i32(win32.ERROR_NO_MORE_FILES) { if pe, _ := is_platform_error(e); pe != i32(win32.ERROR_NO_MORE_FILES) {
it.impl.no_more_files = true read_directory_iterator_set_error(it, it.impl.path, e)
} }
it.impl.no_more_files = true it.impl.no_more_files = true
} }
@@ -86,16 +83,27 @@ _read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info
return return
} }
@(require_results) _read_directory_iterator_init :: proc(it: ^Read_Directory_Iterator, f: ^File) {
_read_directory_iterator_create :: proc(f: ^File) -> (it: Read_Directory_Iterator, err: Error) { it.impl.no_more_files = false
if f == nil {
if f == nil || f.impl == nil {
read_directory_iterator_set_error(it, "", .Invalid_File)
return return
} }
it.f = f it.f = f
impl := (^File_Impl)(f.impl) 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) { if !is_directory(impl.name) {
err = .Invalid_Dir read_directory_iterator_set_error(it, impl.name, .Invalid_Dir)
return 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) it.impl.find_handle = win32.FindFirstFileW(raw_data(wpath_search), &it.impl.find_data)
if it.impl.find_handle == win32.INVALID_HANDLE_VALUE { 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 return
} }
defer if err != nil { defer if it.err.err != nil {
win32.FindClose(it.impl.find_handle) 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 return
} }
+5
View File
@@ -122,6 +122,11 @@ new_file :: proc(handle: uintptr, name: string) -> ^File {
return file return file
} }
@(require_results)
clone :: proc(f: ^File) -> (^File, Error) {
return _clone(f)
}
@(require_results) @(require_results)
fd :: proc(f: ^File) -> uintptr { fd :: proc(f: ^File) -> uintptr {
return _fd(f) return _fd(f)
+17
View File
@@ -113,6 +113,23 @@ _new_file :: proc(fd: uintptr, _: string, allocator: runtime.Allocator) -> (f: ^
return &impl.file, nil 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) @(require_results)
_open_buffered :: proc(name: string, buffer_size: uint, flags := File_Flags{.Read}, perm := 0o777) -> (f: ^File, err: Error) { _open_buffered :: proc(name: string, buffer_size: uint, flags := File_Flags{.Read}, perm := 0o777) -> (f: ^File, err: Error) {
+23
View File
@@ -114,6 +114,29 @@ __new_file :: proc(handle: posix.FD, allocator: runtime.Allocator) -> ^File {
return &impl.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) { _close :: proc(f: ^File_Impl) -> (err: Error) {
if f == nil { return nil } if f == nil { return nil }
+26
View File
@@ -223,6 +223,32 @@ _new_file :: proc(handle: uintptr, name: string, allocator: runtime.Allocator) -
return &impl.file, nil 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) { _close :: proc(f: ^File_Impl) -> (err: Error) {
if errno := wasi.fd_close(f.fd); errno != nil { if errno := wasi.fd_close(f.fd); errno != nil {
err = _get_platform_error(errno) err = _get_platform_error(errno)
+23
View File
@@ -210,6 +210,29 @@ _new_file_buffered :: proc(handle: uintptr, name: string, buffer_size: uint) ->
return 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 { _fd :: proc(f: ^File) -> uintptr {
if f == nil || f.impl == nil { if f == nil || f.impl == nil {
+5 -1
View File
@@ -60,16 +60,20 @@ _remove_all :: proc(path: string) -> (err: Error) {
dir := open(path) or_return dir := open(path) or_return
defer close(dir) defer close(dir)
iter := read_directory_iterator_create(dir) or_return iter := read_directory_iterator_create(dir)
defer read_directory_iterator_destroy(&iter) defer read_directory_iterator_destroy(&iter)
for fi in read_directory_iterator(&iter) { for fi in read_directory_iterator(&iter) {
_ = read_directory_iterator_error(&iter) or_break
if fi.type == .Directory { if fi.type == .Directory {
_remove_all(fi.fullpath) or_return _remove_all(fi.fullpath) or_return
} else { } else {
remove(fi.fullpath) or_return remove(fi.fullpath) or_return
} }
} }
_ = read_directory_iterator_error(&iter) or_return
} }
return remove(path) return remove(path)
+74
View File
@@ -5,6 +5,7 @@ import "core:log"
import "core:path/filepath" import "core:path/filepath"
import "core:slice" import "core:slice"
import "core:testing" import "core:testing"
import "core:strings"
@(test) @(test)
test_read_dir :: proc(t: ^testing.T) { 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].name, "sub")
testing.expect_value(t, fis[1].type, os.File_Type.Directory) 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)
}
}
+31
View File
@@ -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)
}