Skip to content

Commit

Permalink
os/os2: recursive directory walker, expose errors in read_directory, …
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
laytan committed Feb 24, 2025
1 parent d0d5cf8 commit 0e4140a
Show file tree
Hide file tree
Showing 14 changed files with 687 additions and 88 deletions.
124 changes: 118 additions & 6 deletions core/os/os2/dir.odin
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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)
}

Expand All @@ -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)
}
46 changes: 32 additions & 14 deletions core/os/os2/dir_linux.odin
Original file line number Diff line number Diff line change
Expand Up @@ -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 == ".." {
Expand All @@ -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 {
Expand All @@ -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())
}
65 changes: 37 additions & 28 deletions core/os/os2/dir_posix.odin
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,23 @@ import "core:sys/posix"

Read_Directory_Iterator_Impl :: struct {
dir: posix.DIR,
idx: int,
fullpath: [dynamic]byte,
}

@(require_results)
_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
}

Expand All @@ -31,62 +32,70 @@ _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
}

return
}

_read_directory_iterator_destroy :: proc(it: ^Read_Directory_Iterator) {
if it == nil || it.impl.dir == nil {
if it.impl.dir == nil {
return
}

Expand Down
Loading

0 comments on commit 0e4140a

Please sign in to comment.