commit 7890196b13854057d3820bd10a6b118af4f03107
parent 3aa3d09131f904760208dd4dd980c51d3018e16e
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date: Fri, 18 Sep 2026 07:53:19 -0400
ntfs: count files whose data lives inside their MFT record
A file small enough to fit in its own record allocates no clusters, so it
contributes nothing to any directory total and looks free to a disk usage
tool. Its real cost is the record, which is charged to $MFT instead. Counting
these files and their bytes makes the size of that blind spot visible rather
than leaving it as an unexplained difference between what the tool reports
and what the volume says is in use.
Diffstat:
2 files changed, 34 insertions(+), 5 deletions(-)
diff --git a/ntfs/mft.odin b/ntfs/mft.odin
@@ -29,11 +29,15 @@ Hard_Link :: struct {
}
Mft_Stats :: struct {
- records: u64, // record slots in $MFT
- records_read: u64, // slots that held a FILE record
- records_bad: u64, // FILE records whose fixups failed
- in_use: u64,
- directories: u64,
+ records: u64, // record slots in $MFT
+ records_read: u64, // slots that held a FILE record
+ records_bad: u64, // FILE records whose fixups failed
+ in_use: u64,
+ directories: u64,
+ // Files whose unnamed $DATA fits inside their MFT record. They occupy no clusters
+ // of their own, so their bytes are charged to $MFT rather than to their directory.
+ resident_files: u64,
+ resident_bytes: u64,
}
Mft :: struct {
@@ -141,6 +145,8 @@ mft_add_record :: proc(m: ^Mft, record_number: u32, rec: []byte) -> Error {
}
} else if len(a.name) == 0 {
e.size = u64(len(a.value))
+ m.stats.resident_files += 1
+ m.stats.resident_bytes += u64(len(a.value))
}
case:
// Directory indexes, bitmaps, reparse data, and EFS streams occupy clusters too.
diff --git a/ntfs/ntfs_test.odin b/ntfs/ntfs_test.odin
@@ -475,3 +475,26 @@ test_path :: proc(t: ^testing.T) {
testing.expect_value(t, mft_path(&m, RECORD_ROOT, context.temp_allocator), `\`)
testing.expect_value(t, mft_path(&m, 66, context.temp_allocator), `<orphan>\stale.tmp`)
}
+
+// ---- resident data ------------------------------------------------------------------
+
+@(test)
+test_resident_file_costs_no_clusters :: proc(t: ^testing.T) {
+ rec := build_record(
+ {
+ resident(.File_Name, file_name_value(make_ref(RECORD_ROOT, 5), "tiny.txt", .Win32)),
+ resident(.Data, transmute([]byte)string("hello")),
+ },
+ )
+ m: Mft
+ testing.expect_value(t, mft_init(&m, 128, 4096), Error.None)
+ defer mft_destroy(&m)
+ add(t, &m, rec)
+
+ // The bytes live inside the MFT record, so the file itself allocates nothing and
+ // the stats record what the directory totals will therefore miss.
+ testing.expect_value(t, m.entries[100].size, u64(5))
+ testing.expect_value(t, m.entries[100].allocated, u64(0))
+ testing.expect_value(t, m.stats.resident_files, u64(1))
+ testing.expect_value(t, m.stats.resident_bytes, u64(5))
+}