Pārlūkot izejas kodu

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
Laytan Laats 6 mēneši atpakaļ
vecāks
revīzija
0e4140a602

+ 118 - 6
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)
 }

+ 32 - 14
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())
 }

+ 37 - 28
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
 	}
 

+ 230 - 0
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
+}

+ 37 - 23
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^ = {}
 }

+ 29 - 16
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
 }
 

+ 5 - 0
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)

+ 17 - 0
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) {

+ 23 - 0
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 }
 

+ 26 - 0
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)

+ 23 - 0
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 {

+ 5 - 1
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)

+ 74 - 0
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)
+	}
+} 

+ 31 - 0
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)
+}