sonar

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

commit 17d958e303f8ee131ba092bba12e64b3cafcecf2
parent c853f8bfac1302ef74c28630be418a1e7864dcd4
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date:   Sun, 20 Sep 2026 14:09:12 -0300

style: run odinfmt over the files this branch touched

Formatting only, no behaviour: the binary's output over /usr and /usr/share/doc
is unchanged line for line, and the tests pass as before.

Only the files this branch already changed. The repository is not odinfmt-clean
as a whole, and rewriting the twenty-odd untouched files would bury real changes
in alignment churn. Some of what is here is that older drift rather than this
branch's, since a formatter takes the whole file or none of it.

Diffstat:
Mbuilder.odin | 21++++++++++++---------
Mmain.odin | 68+++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mntfs/volume_other.odin | 9++++++++-
Mntfs/volume_other_test.odin | 8+++++++-
Mscan/scan_test.odin | 11+++++++++--
Mscan/target.odin | 4++--
Mscan/target_other.odin | 9++++++++-
Mscan/tree.odin | 12++++++------
Mscan/tree_test.odin | 35+++++++++++++++++++++++++++++------
Mwalk/read_linux_test.odin | 6+++++-
Mwalk/walk_test.odin | 12++++++++++--
11 files changed, 145 insertions(+), 50 deletions(-)

diff --git a/builder.odin b/builder.odin @@ -20,15 +20,15 @@ readers have finished with, charges it, and publishes. If a pass takes longer th frame the UI simply draws the previous answer. */ Builder :: struct { - table: ^ntfs.Mft, - tree: ^scan.Tree, - out: ^scan.Publisher, - mount: string, - scanning: ^b32, // cleared by the reader thread when the table is finished - started: time.Tick, + table: ^ntfs.Mft, + tree: ^scan.Tree, + out: ^scan.Publisher, + mount: string, + scanning: ^b32, // cleared by the reader thread when the table is finished + started: time.Tick, projection: ntfs.Projection, - roll: Rollup, - done: b32, + roll: Rollup, + done: b32, } builder_run :: proc(b: ^Builder) { @@ -88,7 +88,10 @@ builder_pass :: proc(b: ^Builder, exact: bool) { complete = exact, } for i in 0 ..< count { - s.rows[i] = {node = top[i].record, bytes = top[i].bytes} + s.rows[i] = { + node = top[i].record, + bytes = top[i].bytes, + } } scan.publish(b.out, s) } diff --git a/main.odin b/main.odin @@ -16,8 +16,6 @@ import "core:strconv" import "core:strings" import "core:time" -// Only referenced inside `when ODIN_DEBUG`; @(require) keeps release builds from -// rejecting the import as unused. @(require) import "debug" import "ntfs" import "scan" @@ -26,13 +24,10 @@ import "walk" TOP_N :: 20 main :: proc() { - os.exit(debug_main()) + os.exit(run()) } -// Deferred cleanup must run before os.exit, so the debug wiring lives one frame down. -debug_main :: proc() -> int { - // Assertion failures print a call chain in every build; symbols need -debug. - context.assertion_failure_proc = trace.assertion_failure_proc +run :: proc() -> int { when ODIN_DEBUG { da: debug.Allocator debug.init(&da, context.allocator) @@ -40,16 +35,17 @@ debug_main :: proc() -> int { defer debug.report(&da) context.allocator = debug.allocator(&da) } - return run() -} -run :: proc() -> int { defer free_all(context.temp_allocator) - target := "C:" + // Assertion failures print a call chain in every build; symbols need -debug. + context.assertion_failure_proc = trace.assertion_failure_proc + + target := os.user_home_dir(context.allocator) or_else "/" opts: ntfs.Read_Options wcfg: walk.Config live := false + for arg in os.args[1:] { switch arg { case "-h", "--help", "/?": @@ -148,7 +144,9 @@ run :: proc() -> int { if err != nil { #partial switch err { case .Access_Denied: - fmt.eprintln("error: access denied. Reading a raw volume needs an administrator prompt.") + fmt.eprintln( + "error: access denied. Reading a raw volume needs an administrator prompt.", + ) case .Not_Ntfs: fmt.eprintln("error: not an NTFS volume") case .Open_Failed: @@ -182,7 +180,12 @@ run :: proc() -> int { print_native(&m, elapsed) } fmt.println() - fmt.printfln("tree %d slots, %d used, read in %.0f ms", t.count, t.nodes_done, time.duration_milliseconds(elapsed)) + fmt.printfln( + "tree %d slots, %d used, read in %.0f ms", + t.count, + t.nodes_done, + time.duration_milliseconds(elapsed), + ) totals := make([]u64, t.count) defer delete(totals) @@ -301,9 +304,25 @@ print_native :: proc(m: ^ntfs.Mft, elapsed: time.Duration) { } volume_bytes := m.boot.total_sectors * u64(m.boot.bytes_per_sector) fmt.println() - fmt.printfln("volume %s, %d B clusters, %d B records", human(volume_bytes), m.boot.bytes_per_cluster, m.boot.record_size) - fmt.printfln("mft %d slots, %d records, %d unreadable, read in %.0f ms", m.stats.records, m.stats.records_read, m.stats.records_bad, time.duration_milliseconds(elapsed)) - fmt.printfln("in use %d files, %d directories, %d extra hard links", files, m.stats.directories, len(m.links)) + fmt.printfln( + "volume %s, %d B clusters, %d B records", + human(volume_bytes), + m.boot.bytes_per_cluster, + m.boot.record_size, + ) + fmt.printfln( + "mft %d slots, %d records, %d unreadable, read in %.0f ms", + m.stats.records, + m.stats.records_read, + m.stats.records_bad, + time.duration_milliseconds(elapsed), + ) + fmt.printfln( + "in use %d files, %d directories, %d extra hard links", + files, + m.stats.directories, + len(m.links), + ) fmt.printfln("attributed %s summed from every file run list", human(allocated)) /* @@ -317,13 +336,24 @@ print_native :: proc(m: ^ntfs.Mft, elapsed: time.Duration) { fmt.printfln("on disk %s marked allocated in $Bitmap", human(on_disk)) if on_disk >= allocated { gap := on_disk - allocated - fmt.printfln("unclaimed %s (%.2f%%) allocated but charged to no file", human(gap), 100 * f64(gap) / f64(on_disk)) + fmt.printfln( + "unclaimed %s (%.2f%%) allocated but charged to no file", + human(gap), + 100 * f64(gap) / f64(on_disk), + ) } else { - fmt.printfln("overcount %s more attributed than $Bitmap reports allocated", human(allocated - on_disk)) + fmt.printfln( + "overcount %s more attributed than $Bitmap reports allocated", + human(allocated - on_disk), + ) } } if m.stats.resident_files > 0 { - fmt.printfln("resident %d files hold %s inside their MFT records, charged to $MFT", m.stats.resident_files, human(m.stats.resident_bytes)) + fmt.printfln( + "resident %d files hold %s inside their MFT records, charged to $MFT", + m.stats.resident_files, + human(m.stats.resident_bytes), + ) } } diff --git a/ntfs/volume_other.odin b/ntfs/volume_other.odin @@ -33,7 +33,14 @@ 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) { +volume_open :: proc( + path: string, + mode := IO_Mode.Unbuffered, + allocator := context.allocator, +) -> ( + v: Volume, + err: Error, +) { f, open_err := os.open(path, {.Read}) if open_err != nil { if open_err == io.Error.Permission_Denied { diff --git a/ntfs/volume_other_test.odin b/ntfs/volume_other_test.odin @@ -43,7 +43,13 @@ test_volume_read_at_offsets :: proc(t: ^testing.T) { off, size := span[0], span[1] buf := make([]byte, size, context.temp_allocator) testing.expect_value(t, volume_read_at(&v, buf, u64(off)), Error.None) - testing.expectf(t, slice.equal(buf, content[off:off + size]), "wrong bytes at %d for %d", off, size) + testing.expectf( + t, + slice.equal(buf, content[off:off + size]), + "wrong bytes at %d for %d", + off, + size, + ) } } diff --git a/scan/scan_test.odin b/scan/scan_test.odin @@ -4,7 +4,10 @@ import "core:testing" @(test) test_choose_prefers_the_mft_when_it_can_be_read :: proc(t: ^testing.T) { - ntfs := Target{fs = .Ntfs, volume = `\\.\C:`} + ntfs := Target { + fs = .Ntfs, + volume = `\\.\C:`, + } testing.expect_value(t, choose(ntfs, elevated = true).engine, Engine.Mft) // Without the privilege the reader is unavailable rather than merely slower, so @@ -30,7 +33,11 @@ test_choose_walks_what_it_cannot_read_directly :: proc(t: ^testing.T) { test_choose_reads_an_image_without_elevation :: proc(t: ^testing.T) { // An image is a file like any other: opening it is the whole of the permission // needed, so asking for more would send a reader that cannot work. - image := Target{fs = .Ntfs, volume = "disk.img", image = true} + image := Target { + fs = .Ntfs, + volume = "disk.img", + image = true, + } testing.expect_value(t, choose(image, elevated = false).engine, Engine.Mft) } diff --git a/scan/target.odin b/scan/target.odin @@ -65,12 +65,12 @@ target_destroy :: proc(t: ^Target) { // themselves, which keeps this package free of a dependency on any of them. Engine :: enum { None, // nothing here can be scanned - Mft, // read the NTFS master file table whole + Mft, // read the NTFS master file table whole Walk, // enumerate directories } Choice :: struct { - engine: Engine, + 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, diff --git a/scan/target_other.odin b/scan/target_other.odin @@ -125,7 +125,14 @@ The longest mount point that prefixes the path wins, which is what makes a files mounted inside another resolve to the inner one. Names are returned owned. */ @(private = "file") -mounted_at :: proc(path: string, allocator := context.allocator) -> (device, mount: string, fs: Filesystem, found: bool) { +mounted_at :: proc( + path: string, + allocator := context.allocator, +) -> ( + device, mount: string, + fs: Filesystem, + found: bool, +) { when ODIN_OS != .Linux { // Other unixes report their mounts through getmntinfo rather than a file. return "", "", .Unknown, false diff --git a/scan/tree.odin b/scan/tree.odin @@ -48,12 +48,12 @@ identifier. One that discovers as it goes calls `claim` for a run of slots. Eith way a slot belongs to one worker, so filling it needs no lock. */ Tree :: struct { - blocks: [MAX_BLOCKS]^Block, - count: u32, // slots handed out - committed: u32, // slots backed by an allocated block - mutex: sync.Mutex, // guards block allocation only - arenas: []virtual.Arena, // one per writer; names live here for the tree's life - root: u32, + blocks: [MAX_BLOCKS]^Block, + count: u32, // slots handed out + committed: u32, // slots backed by an allocated block + mutex: sync.Mutex, // guards block allocation only + arenas: []virtual.Arena, // one per writer; names live here for the tree's life + root: u32, // Polled by a UI rather than pushed to it, so sampling costs a reader nothing. nodes_done: u64, bytes_done: u64, diff --git a/scan/tree_test.odin b/scan/tree_test.odin @@ -79,13 +79,28 @@ test_path_walks_to_a_root :: proc(t: ^testing.T) { first, _ := claim(&w, 3) testing.expect_value(t, first, u32(0)) - node(&tree, 0)^ = Node{parent = 0, name = "C:", flags = {.Used, .Directory}} - node(&tree, 1)^ = Node{parent = 0, name = "Windows", flags = {.Used, .Directory}} - node(&tree, 2)^ = Node{parent = 1, name = "notepad.exe", flags = {.Used}} + node(&tree, 0)^ = Node { + parent = 0, + name = "C:", + flags = {.Used, .Directory}, + } + node(&tree, 1)^ = Node { + parent = 0, + name = "Windows", + flags = {.Used, .Directory}, + } + node(&tree, 2)^ = Node { + parent = 1, + name = "notepad.exe", + flags = {.Used}, + } // Written with the separator of the machine showing the path, not the one the // names came from, so this is built rather than spelled. - want := strings.concatenate({"C:", SEPARATOR, "Windows", SEPARATOR, "notepad.exe"}, context.temp_allocator) + want := strings.concatenate( + {"C:", SEPARATOR, "Windows", SEPARATOR, "notepad.exe"}, + context.temp_allocator, + ) testing.expect_value(t, path(&tree, 2, context.temp_allocator), want) testing.expect_value(t, path(&tree, 0, context.temp_allocator), "C:") } @@ -98,8 +113,16 @@ test_path_stops_at_a_cycle :: proc(t: ^testing.T) { defer tree_destroy(&tree) w := writer(&tree) claim(&w, 2) - node(&tree, 0)^ = Node{parent = 1, name = "a", flags = {.Used, .Directory}} - node(&tree, 1)^ = Node{parent = 0, name = "b", flags = {.Used, .Directory}} + node(&tree, 0)^ = Node { + parent = 1, + name = "a", + flags = {.Used, .Directory}, + } + node(&tree, 1)^ = Node { + parent = 0, + name = "b", + flags = {.Used, .Directory}, + } p := path(&tree, 0, context.temp_allocator) testing.expect(t, len(p) > 0, "a cycle produced no path at all") diff --git a/walk/read_linux_test.odin b/walk/read_linux_test.odin @@ -34,7 +34,11 @@ test_a_sparse_file_is_charged_what_it_occupies :: proc(t: ^testing.T) { // The same length, actually written. body := make([]byte, 1 << 20, context.temp_allocator) slice.fill(body, 'x') - testing.expect(t, os.write_entire_file(fmt.tprintf("%s/solid.bin", root), body) == nil, "could not write") + testing.expect( + t, + os.write_entire_file(fmt.tprintf("%s/solid.bin", root), body) == nil, + "could not write", + ) tree: shared.Tree testing.expect_value(t, shared.tree_init(&tree, 1), shared.Error.None) diff --git a/walk/walk_test.odin b/walk/walk_test.odin @@ -34,11 +34,19 @@ fixture :: proc(t: ^testing.T, name: string) -> string { } testing.expect(t, os.make_directory(root) == nil, "could not make the root") testing.expect(t, os.make_directory(fmt.tprintf("%s/sub", root)) == nil, "could not make sub") - testing.expect(t, os.make_directory(fmt.tprintf("%s/sub/deep", root)) == nil, "could not make deep") + testing.expect( + t, + os.make_directory(fmt.tprintf("%s/sub/deep", root)) == nil, + "could not make deep", + ) write(t, fmt.tprintf("%s/a.txt", root), 100) write(t, fmt.tprintf("%s/sub/b.txt", root), 5000) write(t, fmt.tprintf("%s/sub/deep/c.txt", root), 1) - testing.expect(t, os.symlink("sub", fmt.tprintf("%s/link", root)) == nil, "could not make the symlink") + testing.expect( + t, + os.symlink("sub", fmt.tprintf("%s/link", root)) == nil, + "could not make the symlink", + ) return root }