Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

os/os2: recursive directory walker, expose errors in read_directory, file clone #4877

Merged
merged 1 commit into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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