commit b612a0a8d351cbd4631354ede35479e696a8d266
parent f58720b55051a177b5cdfd15d835a37e170faeb0
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date: Sun, 20 Sep 2026 09:45:31 -0300
walk: read a directory behind a seam
Enumerating one directory is the only part of walking that differs by platform,
and it was welded to the part that does not: claiming a slot, interning the name,
setting the flags. Nothing could be specialised without duplicating the meaning
of a node alongside it.
`read_dir` is now the whole of the platform's job and `record` the whole of the
tree's, so a second reader cannot quietly disagree about what a node is. No
behaviour changes here: over /usr, /usr/share/doc and /home/jfm the output is
identical line for line apart from the time it reports.
Diffstat:
| A | walk/read.odin | | | 70 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | walk/walk.odin | | | 102 | ++++++++++++++++++++++++++++++++++++------------------------------------------- |
2 files changed, 116 insertions(+), 56 deletions(-)
diff --git a/walk/read.odin b/walk/read.odin
@@ -0,0 +1,70 @@
+package walk
+
+import "core:os"
+import "core:strings"
+
+/*
+Read one directory through core:os.
+
+The portable reader: whatever the platform, core:os describes every entry and this
+records what the tree keeps. It asks for a great deal more than that per entry, which
+is the cost of not knowing which platform it is on.
+*/
+@(private)
+read_dir :: proc(d: Dir, w: ^Worker) -> (nodes, bytes: u64, ok: bool) {
+ f, open_err := os.open(d.path)
+ if open_err != nil {
+ return 0, 0, false
+ }
+ 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) {
+ if _, err := os.read_directory_iterator_error(&it); err != nil {
+ continue
+ }
+ e := Entry {
+ name = info.name,
+ size = u64(max(info.size, 0)),
+ directory = info.type == .Directory,
+ symlink = info.type == .Symlink,
+ }
+ if !e.directory && !e.symlink {
+ // core:os reports the logical length and nothing about allocation, so the
+ // on-disk figure is the best estimate available here: the size rounded up
+ // to a block.
+ e.disk = round_up(e.size, BLOCK_ESTIMATE)
+ }
+ index, descend, recorded := record(w, d.index, e)
+ if !recorded {
+ return nodes, bytes, false
+ }
+ if descend {
+ path, clone_err := strings.clone(info.fullpath, w.allocator)
+ if clone_err != nil {
+ w.err = .Out_Of_Memory
+ return nodes, bytes, false
+ }
+ append(&w.found, Dir{index = index, path = path})
+ }
+ nodes += 1
+ bytes += e.disk
+ }
+ return nodes, bytes, true
+}
+
+// Without filesystem-specific knowledge the allocation unit is a guess, and four
+// kilobytes is the common one. It makes small files cost something rather than
+// nothing, which matters more than being exactly right.
+@(private)
+BLOCK_ESTIMATE :: 4096
+
+@(private)
+round_up :: proc(v, unit: u64) -> u64 {
+ if v == 0 {
+ return 0
+ }
+ return (v + unit - 1) / unit * unit
+}
diff --git a/walk/walk.odin b/walk/walk.odin
@@ -8,11 +8,13 @@ fallback rather than the default, and the yardstick a specialised reader is meas
against.
A directory listing is already the shape of the tree, so it writes straight into it.
+Reading one directory is the only part that differs by platform, and it is the part
+worth specialising: `read_dir` is portable through core:os everywhere but Linux,
+which asks the kernel directly.
*/
package walk
import "core:mem"
-import "core:os"
import "core:strings"
import "../flow"
@@ -137,66 +139,54 @@ walk_dir :: proc(d: Dir, w: ^Worker) -> bool {
if scan.cancelled(w.writer.tree) {
return false
}
- f, open_err := os.open(d.path)
- if open_err != nil {
- return false
- }
- defer os.close(f)
-
- it := os.read_directory_iterator_create(f)
- defer os.read_directory_iterator_destroy(&it)
-
- nodes: u64
- bytes: u64
- for info in os.read_directory_iterator(&it) {
- if _, err := os.read_directory_iterator_error(&it); err != nil {
- continue
- }
- index, claim_err := scan.claim(&w.writer, 1)
- if claim_err != nil {
- w.err = .Out_Of_Memory
- return false
- }
- n := scan.node(w.writer.tree, index)
- n.parent = d.index
- n.name = scan.intern(&w.writer, info.name)
- n.size = u64(max(info.size, 0))
- // A walker reaches a child through its parent, so the link is never in doubt.
- n.flags = {.Used, .Settled}
-
- switch info.type {
- case .Directory:
- n.flags |= {.Directory}
- append(&w.found, Dir{index = index, path = strings.clone(info.fullpath, w.allocator)})
- case .Symlink:
- n.flags |= {.Reparse}
- if w.follow {
- append(&w.found, Dir{index = index, path = strings.clone(info.fullpath, w.allocator)})
- }
- case .Undetermined, .Regular, .Named_Pipe, .Socket, .Character_Device, .Block_Device:
- // core:os reports the logical length and nothing about allocation, so
- // the on-disk figure is the best estimate available here: the size
- // rounded up to a block. A reader that talks to the filesystem directly
- // knows the real answer; this one cannot.
- n.disk = round_up(n.size, BLOCK_ESTIMATE)
- }
- nodes += 1
- bytes += n.disk
- }
+ nodes, bytes, ok := read_dir(d, w)
scan.progress(w.writer.tree, nodes, bytes)
- return true
+ return ok
}
-// Without filesystem-specific knowledge the allocation unit is a guess, and four
-// kilobytes is the common one. It makes small files cost something rather than
-// nothing, which matters more than being exactly right.
+// One entry as a directory read describes it, before it becomes a node. `disk` is
+// whatever the platform could learn about allocation; every reader fills it, since
+// only the reader knows how good its own answer is.
@(private)
-BLOCK_ESTIMATE :: 4096
+Entry :: struct {
+ name: string,
+ size: u64,
+ disk: u64,
+ directory: bool,
+ symlink: bool,
+}
+
+/*
+Record one entry against the directory holding it.
+`descend` marks an entry to walk in turn: a directory, or a symlink when the caller
+asked for those to be followed. Shared so that what a node means cannot drift between
+one platform's reader and another's.
+*/
@(private)
-round_up :: proc(v, unit: u64) -> u64 {
- if v == 0 {
- return 0
+record :: proc(w: ^Worker, parent: u32, e: Entry) -> (index: u32, descend: bool, ok: bool) {
+ slot, err := scan.claim(&w.writer, 1)
+ if err != nil {
+ w.err = .Out_Of_Memory
+ return 0, false, false
+ }
+ index = slot
+ n := scan.node(w.writer.tree, index)
+ n.parent = parent
+ n.name = scan.intern(&w.writer, e.name)
+ n.size = e.size
+ // A walker reaches a child through its parent, so the link is never in doubt.
+ n.flags = {.Used, .Settled}
+
+ switch {
+ case e.directory:
+ n.flags |= {.Directory}
+ descend = true
+ case e.symlink:
+ n.flags |= {.Reparse}
+ descend = w.follow
+ case:
+ n.disk = e.disk
}
- return (v + unit - 1) / unit * unit
+ return index, descend, true
}