commit c8e5117b684a722c6ce7c10d5954dad3b3f81bee
parent 5097ba0a660e13247abb1fa0c29c2d56aa6e34a1
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date: Sun, 20 Sep 2026 08:32:19 -0300
ntfs: read volumes on unix
Everything above the volume layer was already portable; only the four procedures
that touch a handle were not, so the reader could be compiled off Windows but
never run. Positional reads through core:os supply them.
Reading /dev/nvme0n1p3, a 491 GiB NTFS volume, takes 1411 ms for 2,021,620
records with none unreadable, against 1357 ms for the same work on Windows. Run
lists account for 470,901,706,752 bytes and $Bitmap marks 470,901,846,016, a gap
of 0.00003%.
Reads go through the page cache. O_DIRECT would want per-platform handling for no
throughput the Windows sweep could measure.
Diffstat:
1 file changed, 69 insertions(+), 10 deletions(-)
diff --git a/ntfs/volume_other.odin b/ntfs/volume_other.odin
@@ -1,27 +1,86 @@
#+build !windows
package ntfs
-// Raw volume access is only implemented for Windows. The parsing code above this
-// layer is portable, so an image file reader for other platforms can slot in here.
+import "core:io"
+import "core:mem"
+import "core:os"
+import "core:strings"
+
+/*
+Read-only handle to a volume: a block device such as /dev/nvme0n1p3, or a file
+holding an image of one.
+
+The path and mode are kept so `volume_clone` can open another handle to the same
+place. Reads are positional and carry their own offset, so handles never contend over
+one; a handle each still keeps a worker's lifetime and errors its own.
+
+Reads pass through the page cache. Bypassing it would need O_DIRECT here and
+F_NOCACHE on Darwin, and sweeping the Windows equivalent found the two
+indistinguishable at about 1.9 GB/s, so the mode is carried for a clone to match
+rather than acted on.
+*/
Volume :: struct {
- _: int,
+ file: ^os.File,
+ name: string, // device or image path, owned
+ mode: IO_Mode,
+ allocator: mem.Allocator,
}
+/*
+Open a volume or an image by path.
+
+Reading a block device is a matter of permissions on the device rather than of
+privilege as it is on Windows: membership of the group owning it is what grants it,
+and a group added to a live session only takes effect on the next login.
+*/
volume_open :: proc(path: string, mode := IO_Mode.Unbuffered, allocator := context.allocator) -> (v: Volume, err: Error) {
- _, _, _ = path, mode, allocator
- return {}, .Unsupported_Platform
+ f, open_err := os.open(path, {.Read})
+ if open_err != nil {
+ if open_err == io.Error.Permission_Denied {
+ return {}, .Access_Denied
+ }
+ return {}, .Open_Failed
+ }
+ name, clone_err := strings.clone(path, allocator)
+ if clone_err != nil {
+ os.close(f)
+ return {}, .Out_Of_Memory
+ }
+ return Volume{file = f, name = name, mode = mode, allocator = allocator}, .None
}
+// Another handle to the same volume, for a thread that reads independently.
volume_clone :: proc(v: ^Volume, allocator := context.allocator) -> (Volume, Error) {
- _, _ = v, allocator
- return {}, .Unsupported_Platform
+ return volume_open(v.name, v.mode, allocator)
}
volume_close :: proc(v: ^Volume) {
- _ = v
+ if v.file != nil {
+ os.close(v.file)
+ }
+ if v.name != "" {
+ delete(v.name, v.allocator)
+ }
+ v^ = {}
}
+// Fill `buf` from `offset`. One positional read is capped by the kernel and stops
+// short of a large buffer, so this repeats until the buffer is full or the volume
+// ends under it.
volume_read_at :: proc(v: ^Volume, buf: []byte, offset: u64) -> Error {
- _, _, _ = v, buf, offset
- return .Unsupported_Platform
+ done := 0
+ for done < len(buf) {
+ n, read_err := os.read_at(v.file, buf[done:], i64(offset) + i64(done))
+ if read_err != nil {
+ if read_err == io.Error.EOF {
+ return .Short_Read
+ }
+ return .Read_Failed
+ }
+ if n == 0 {
+ return .Short_Read
+ }
+ done += n
+ }
+ return .None
}