From 0e4140a60215fbde090ed8e184d638134c28f7cc Mon Sep 17 00:00:00 2001 From: Laytan Laats Date: Mon, 24 Feb 2025 20:07:08 +0100 Subject: [PATCH] os/os2: recursive directory walker, expose errors in read_directory, file clone Adds a directory walker, a method of exposing and retrieving errors from the existing read directory iterator, allows reusing of the existing read directory iterator, and adds a file clone procedure --- core/os/os2/dir.odin | 124 +++++++++++++++++- core/os/os2/dir_linux.odin | 46 ++++--- core/os/os2/dir_posix.odin | 65 +++++----- core/os/os2/dir_walker.odin | 230 ++++++++++++++++++++++++++++++++++ core/os/os2/dir_wasi.odin | 60 +++++---- core/os/os2/dir_windows.odin | 45 ++++--- core/os/os2/file.odin | 5 + core/os/os2/file_linux.odin | 17 +++ core/os/os2/file_posix.odin | 23 ++++ core/os/os2/file_wasi.odin | 26 ++++ core/os/os2/file_windows.odin | 23 ++++ core/os/os2/path_wasi.odin | 6 +- tests/core/os/os2/dir.odin | 74 +++++++++++ tests/core/os/os2/file.odin | 31 +++++ 14 files changed, 687 insertions(+), 88 deletions(-) create mode 100644 core/os/os2/dir_walker.odin create mode 100644 tests/core/os/os2/file.odin diff --git a/core/os/os2/dir.odin b/core/os/os2/dir.odin index a41ef68f9ca..4a7762dedf4 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 f7723936b24..a868a02c440 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 36cac25971c..d9fa16f8dfb 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 00000000000..0af751f3111 --- /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 e4349069a80..61c0056746e 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 f71e7e763bb..d592e8036bf 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 1a25472a15b..28d2bc69be1 100644 --- a/core/os/os2/file.odin +++ b/core/os/os2/file.odin @@ -122,6 +122,11 @@ new_file :: proc(handle: uintptr, name: string) -> ^File { return 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 9f6625091a2..811ee705529 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 184c893683c..43d5866b1a1 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 2b722e5dd10..0245841e3a6 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 f594cc72f8f..b123330e040 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 2f8a3c8c6cb..1c4fafa17a7 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 5bb5c9820bb..7077e9ae23e 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 00000000000..c4df74f4ab3 --- /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) +}