sonar

Scan files at memory bandwidth speed.
Log | Files | Refs

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:
Mntfs/volume_other.odin | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
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 }