sonar

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

commit 564dc3c0f4b56980178e084cb56e54f8b85adbe9
parent 956dcddfe266dbecb479a07c5901848031460c80
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date:   Sun, 20 Sep 2026 14:47:24 -0300

scan: choose a reader from whether the volume opens

`elevated` asked about the process, which is the wrong question and only Windows
could answer it. Off Windows it returned a constant true, justified by a comment
reasoning about what `resolve` had already done. Being in the group that owns a
device grants the read without root, so the honest question is whether this
volume opens, which also covers one that is missing or locked.

`resolve` finds that out and now returns the reader with the target, since it is
the only thing that knew. Choosing stays a separate procedure so the decision
table can still be tested without a disk, but nothing outside needs to call it.

Diffstat:
Mmain.odin | 16+++++++++++-----
Mscan/scan_test.odin | 26+++++++++++++-------------
Mscan/target.odin | 23++++++++++++++---------
Mscan/target_other.odin | 67++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mscan/target_windows.odin | 62+++++++++++++++++++++++++++++++++++++++++---------------------
5 files changed, 125 insertions(+), 69 deletions(-)

diff --git a/main.odin b/main.odin @@ -85,15 +85,14 @@ run :: proc() -> int { } // Resolve once, so the reader is chosen from what the OS says rather than from - // the shape of the string. - resolved, resolve_err := scan.resolve(target) + // the shape of the string, and comes back with it. + resolved, choice, resolve_err := scan.resolve(target) if resolve_err != nil { fmt.eprintfln("error: cannot scan %s: %v", target, resolve_err) return 1 } defer scan.target_destroy(&resolved) - choice := scan.choose(resolved, scan.elevated()) fmt.printfln( "sonar: %s on %s (%v, %v engine, %v io)", scan.location(resolved, context.temp_allocator), @@ -102,8 +101,15 @@ run :: proc() -> int { choice.engine, opts.io_mode, ) - if choice.elevation_would_help { - fmt.eprintln("note: this volume reads far faster from an administrator prompt") + if choice.permission_would_help { + when ODIN_OS == .Windows { + fmt.eprintln("note: this volume reads far faster from an administrator prompt") + } else { + fmt.eprintfln( + "note: this volume reads far faster with permission to open %s directly", + resolved.volume, + ) + } } if choice.engine == .None { fmt.eprintfln("error: nothing here can be scanned") diff --git a/scan/scan_test.odin b/scan/scan_test.odin @@ -8,13 +8,13 @@ test_choose_prefers_the_mft_when_it_can_be_read :: proc(t: ^testing.T) { fs = .Ntfs, volume = `\\.\C:`, } - testing.expect_value(t, choose(ntfs, elevated = true).engine, Engine.Mft) + testing.expect_value(t, choose(ntfs, readable = true).engine, Engine.Mft) - // Without the privilege the reader is unavailable rather than merely slower, so - // the fallback runs and the caller is told relaunching would pay. - fallback := choose(ntfs, elevated = false) + // A volume that would not open leaves the reader unavailable rather than merely + // slower, so the fallback runs and the caller is told what would pay. + fallback := choose(ntfs, readable = false) testing.expect_value(t, fallback.engine, Engine.Walk) - testing.expect(t, fallback.elevation_would_help) + testing.expect(t, fallback.permission_would_help) } @(test) @@ -23,9 +23,9 @@ test_choose_walks_what_it_cannot_read_directly :: proc(t: ^testing.T) { // what makes a target walkable, and `resolve` sets one for everything that has a // directory tree behind it. for fs in ([]Filesystem{.Refs, .Exfat, .Fat32, .Network, .Other}) { - c := choose(Target{fs = fs, volume = `\\.\D:`, mount = `D:\`}, elevated = true) + c := choose(Target{fs = fs, volume = `\\.\D:`, mount = `D:\`}, readable = true) testing.expect_value(t, c.engine, Engine.Walk) - testing.expect(t, !c.elevation_would_help) + testing.expect(t, !c.permission_would_help) } } @@ -38,7 +38,7 @@ test_choose_reads_an_image_without_elevation :: proc(t: ^testing.T) { volume = "disk.img", image = true, } - testing.expect_value(t, choose(image, elevated = false).engine, Engine.Mft) + testing.expect_value(t, choose(image, readable = true).engine, Engine.Mft) } @(test) @@ -46,13 +46,13 @@ test_choose_gives_up_on_a_volume_it_cannot_name :: proc(t: ^testing.T) { // A raw device or an image whose filesystem went unrecognised has no tree to // walk, so offering the walker would report an empty volume as an answer. for v in ([]Target{{volume = "/dev/sda1"}, {volume = "disk.img", image = true}}) { - testing.expect_value(t, choose(v, elevated = true).engine, Engine.None) + testing.expect_value(t, choose(v, readable = true).engine, Engine.None) } } @(test) test_choose_gives_up_on_a_target_with_no_volume :: proc(t: ^testing.T) { - testing.expect_value(t, choose(Target{}, elevated = true).engine, Engine.None) + testing.expect_value(t, choose(Target{}, readable = true).engine, Engine.None) } @(test) @@ -62,7 +62,7 @@ test_resolve_finds_the_volume_behind_a_path :: proc(t: ^testing.T) { } // The system drive is the one target every Windows machine has, and reading its // name needs no privilege. - root, err := resolve("C:") + root, _, err := resolve("C:") testing.expect_value(t, err, Error.None) defer target_destroy(&root) testing.expect_value(t, root.mount, `C:\`) @@ -70,7 +70,7 @@ test_resolve_finds_the_volume_behind_a_path :: proc(t: ^testing.T) { testing.expect_value(t, root.root, "") // A path below the mount point keeps the same volume and records the subtree. - sub, sub_err := resolve(`C:\Windows`) + sub, _, sub_err := resolve(`C:\Windows`) testing.expect_value(t, sub_err, Error.None) defer target_destroy(&sub) testing.expect_value(t, sub.volume, root.volume) @@ -83,6 +83,6 @@ test_resolve_rejects_what_is_not_there :: proc(t: ^testing.T) { when ODIN_OS != .Windows { return } - _, err := resolve(`Q:\no\such\path`) + _, _, err := resolve(`Q:\no\such\path`) testing.expect_value(t, err, Error.Target_Not_Found) } diff --git a/scan/target.odin b/scan/target.odin @@ -70,27 +70,32 @@ Engine :: enum { } Choice :: struct { - engine: Engine, - // The MFT reader is far faster but needs a raw volume handle. When this is set, - // Walk is what will run unless the caller elevates and asks again. - elevation_would_help: bool, + engine: Engine, + // The MFT reader is far faster but needs to open the volume raw. When this is + // set, Walk is what will run unless the caller obtains that and asks again. + permission_would_help: bool, } /* Pick a reader for a target. +`readable` says the volume opened for raw reading, which is the only thing standing +between an NTFS target and the MFT reader. `resolve` finds it out and calls this; it +is a separate procedure so the whole decision table can be tested without a disk, not +because anything else needs to choose. An image needs no special case: it is a file, +so it opens. + The MFT reader takes the whole volume at once, so a subtree costs no more than the root does and is far cheaper than walking it. That makes a subtree target a reason to filter the result, never a reason to reject the reader. */ -choose :: proc(t: Target, elevated: bool) -> Choice { +@(private) +choose :: proc(t: Target, readable: bool) -> Choice { if t.fs == .Ntfs { - // An image is an ordinary file, so reading one needs no more than opening it. - // It is the raw handle onto a mounted volume that has to be granted. - if elevated || t.image { + if readable { return {engine = .Mft} } - return {engine = .Walk, elevation_would_help = true} + return {engine = .Walk, permission_would_help = true} } // Nothing mounted means no directory tree to enumerate. A raw volume and an image // are both bytes until a reader that knows the filesystem inside says otherwise, diff --git a/scan/target_other.odin b/scan/target_other.odin @@ -15,14 +15,25 @@ A directory names a place inside a volume, which the mount table accounts for. Reading a signature is not parsing a filesystem: saying which reader applies is this layer's job, and doing it here keeps that job free of every backend, exactly as asking Windows for a volume's filesystem name does. + +The reader comes back with the target. Choosing one needs to know whether the volume +opens, which is found out here, so leaving the caller to ask separately would only +mean opening it twice. */ -resolve :: proc(input: string, allocator := context.allocator) -> (t: Target, err: Error) { +resolve :: proc( + input: string, + allocator := context.allocator, +) -> ( + t: Target, + c: Choice, + err: Error, +) { info, stat_err := os.stat(input, context.temp_allocator) if stat_err != nil { if stat_err == io.Error.Permission_Denied { - return {}, .Access_Denied + return {}, {}, .Access_Denied } - return {}, .Target_Not_Found + return {}, {}, .Target_Not_Found } path := info.fullpath @@ -36,16 +47,17 @@ resolve :: proc(input: string, allocator := context.allocator) -> (t: Target, er fs, sig_err := signature(path) if sig_err != .None { target_destroy(&t) - return {}, sig_err + return {}, {}, sig_err } t.fs = fs - return t, .None + // Reading the signature opened it, so there is nothing left to find out. + return t, choose(t, true), .None case .Directory: device, mount, fs, found := mounted_at(path, allocator) if !found { target_destroy(&t) - return {}, .Unsupported_Platform + return {}, {}, .Unsupported_Platform } t.volume = device t.mount = mount @@ -55,22 +67,29 @@ resolve :: proc(input: string, allocator := context.allocator) -> (t: Target, er if len(path) > len(mount) { t.root = strings.clone(path[len(mount):], allocator) } - return t, .None + // Reached through its mount point, so nothing so far has touched the device. + // Whether that opens is what decides between reading the filesystem whole and + // walking what is mounted from it. + return t, choose(t, can_open(device)), .None } target_destroy(&t) - return {}, .Target_Not_Found + return {}, {}, .Target_Not_Found } -/* -Whether this process can open a raw volume handle. - -Always true by the time it is asked. Unix grants raw reads through permissions on the -device rather than a process token, and `resolve` only names a filesystem for a -volume whose front it managed to read, so a target that reached here already opened. -A volume that did not fails resolution with Access_Denied instead. -*/ -elevated :: proc() -> bool { +// Whether a volume opens for raw reading. Unix keeps that answer in the device's +// permissions and the groups held rather than in the process, so opening it is the +// only test that covers every reason it might be refused. +@(private = "file") +can_open :: proc(path: string) -> bool { + if path == "" { + return false + } + f, err := os.open(path, {.Read}) + if err != nil { + return false + } + os.close(f) return true } @@ -141,8 +160,10 @@ mounted_at :: proc( if read_err != nil { return "", "", .Unknown, false } + best_device, best_mount, best_type: string text := string(table) + for line in strings.split_lines_iterator(&text) { fields := strings.fields(line, context.temp_allocator) if len(fields) < 3 { @@ -154,13 +175,17 @@ mounted_at :: proc( } best_device, best_mount, best_type = fields[0], point, fields[2] } + if best_mount == "" { return "", "", .Unknown, false } - return strings.clone(best_device, allocator), - strings.clone(best_mount, allocator), - filesystem_from_name(best_type), - true + + device = strings.clone(best_device, allocator) + mount = strings.clone(best_mount, allocator) + fs = filesystem_from_name(best_type) + found = true + + return device, mount, fs, found } } diff --git a/scan/target_windows.odin b/scan/target_windows.odin @@ -11,7 +11,6 @@ foreign kernel32 { // Not bound by core:sys/windows, so declared here. GetVolumePathNameW :: proc(lpszFileName: win.LPCWSTR, lpszVolumePathName: win.LPWSTR, cchBufferLength: win.DWORD) -> win.BOOL --- GetVolumeInformationW :: proc(lpRootPathName: win.LPCWSTR, lpVolumeNameBuffer: win.LPWSTR, nVolumeNameSize: win.DWORD, lpVolumeSerialNumber: ^win.DWORD, lpMaximumComponentLength: ^win.DWORD, lpFileSystemFlags: ^win.DWORD, lpFileSystemNameBuffer: win.LPWSTR, nFileSystemNameSize: win.DWORD) -> win.BOOL --- - GetCurrentProcess :: proc() -> win.HANDLE --- } /* @@ -20,8 +19,19 @@ Resolve what was given into a target backends can dispatch on. A bare drive letter is accepted for convenience, an existing path is asked about, and anything else is taken to be an image file holding a volume. Failing here is better than failing inside a backend, because this is the layer that knows why. + +The reader comes back with the target. Choosing one needs to know whether the volume +opens, which is found out here, so leaving the caller to ask separately would only +mean opening it twice. */ -resolve :: proc(input: string, allocator := context.allocator) -> (t: Target, err: Error) { +resolve :: proc( + input: string, + allocator := context.allocator, +) -> ( + t: Target, + c: Choice, + err: Error, +) { t.allocator = allocator t.input = strings.clone(input, allocator) @@ -34,7 +44,7 @@ resolve :: proc(input: string, allocator := context.allocator) -> (t: Target, er attrs := win.GetFileAttributesW(wpath) if attrs == win.INVALID_FILE_ATTRIBUTES { target_destroy(&t) - return {}, .Target_Not_Found + return {}, {}, .Target_Not_Found } // A file that is not a directory is taken to be an image of a volume. Nothing @@ -42,7 +52,7 @@ resolve :: proc(input: string, allocator := context.allocator) -> (t: Target, er if attrs & win.FILE_ATTRIBUTE_DIRECTORY == 0 { t.image = true t.volume = strings.clone(path, allocator) - return t, .None + return t, choose(t, can_open(t.volume)), .None } // The mount point tells us which volume the path sits on, which is the only way @@ -50,12 +60,12 @@ resolve :: proc(input: string, allocator := context.allocator) -> (t: Target, er mount_buf: [win.MAX_PATH]u16 if !GetVolumePathNameW(wpath, &mount_buf[0], len(mount_buf)) { target_destroy(&t) - return {}, .Target_Not_Found + return {}, {}, .Target_Not_Found } mount, mount_err := win.wstring_to_utf8(win.wstring(&mount_buf[0]), -1, allocator) if mount_err != nil { target_destroy(&t) - return {}, .Out_Of_Memory + return {}, {}, .Out_Of_Memory } t.mount = mount @@ -89,7 +99,9 @@ resolve :: proc(input: string, allocator := context.allocator) -> (t: Target, er if len(path) > len(t.mount) { t.root = strings.clone(path[len(t.mount) - 1:], allocator) } - return t, .None + // The mount point answered everything above; nothing has touched the volume + // itself. Whether that opens is what decides the reader. + return t, choose(t, can_open(t.volume)), .None } @(private) @@ -127,24 +139,32 @@ is_drive_spec :: proc(s: string) -> bool { } /* -Whether this process can open a raw volume handle. +Whether a volume opens for raw reading. + +Elevation is the usual reason a raw handle is refused, but an elevated token still +says nothing about a volume that is missing, or locked by something else. Opening it +answers all of those at once and costs one handle. -Asked before choosing a reader so the choice can be explained: the MFT reader is much -faster but needs this, and without it the caller may want to relaunch rather than -settle for walking directories. +FILE_SHARE_WRITE is required: the volume is mounted and in use, so asking to exclude +writers would fail for a reason that has nothing to do with permission. */ -elevated :: proc() -> bool { - token: win.HANDLE - if !win.OpenProcessToken(GetCurrentProcess(), win.TOKEN_QUERY, &token) { +@(private = "file") +can_open :: proc(path: string) -> bool { + if path == "" { return false } - defer win.CloseHandle(token) - - // TOKEN_ELEVATION is a single DWORD: non-zero when the token is elevated. - value: win.DWORD - size: win.DWORD - if !win.GetTokenInformation(token, .TokenElevation, &value, size_of(value), &size) { + h := win.CreateFileW( + win.utf8_to_wstring(path, context.temp_allocator), + win.GENERIC_READ, + win.FILE_SHARE_READ | win.FILE_SHARE_WRITE | win.FILE_SHARE_DELETE, + nil, + win.OPEN_EXISTING, + 0, + nil, + ) + if h == win.INVALID_HANDLE_VALUE { return false } - return value != 0 + win.CloseHandle(h) + return true }