main.odin (8477B)
1 /* 2 sonar: find out what is using the disk. 3 4 Usage: 5 sonar [drive-or-image] [flags] -h lists the flags 6 7 Reads the volume's MFT directly and prints the largest files and directories. Reading 8 a live volume needs an elevated prompt. 9 */ 10 package main 11 12 import "core:debug/trace" 13 import "core:flags" 14 import "core:fmt" 15 import "core:os" 16 import "core:time" 17 18 @(require) import "jm:debug" 19 import "ntfs" 20 import "scan" 21 import "walk" 22 23 // Largest N files/folders to print. 24 TOP_N :: 20 25 26 PROGRAM :: "sonar" 27 28 /* 29 What the command line can set. 30 31 Read in UNIX style, so `--flag`, `--flag=value` and `--flag value` all work, and `-h` 32 prints this. A count left at zero means the reader chooses its own, rather than 33 having a second set of defaults to keep in step here. 34 */ 35 Options :: struct { 36 target: string `args:"pos=0" usage:"Drive, device, image or directory to scan. Defaults to your home directory."`, 37 live: bool `usage:"Draw the answer as it is found rather than once at the end."`, 38 buffered: bool `usage:"Read the volume through the system cache rather than around it."`, 39 no_skip: bool `usage:"Read runs of dead MFT records rather than jumping over them."`, 40 min_skip: int `usage:"Shortest run of dead records worth jumping over, in bytes."`, 41 workers: int `usage:"MFT readers to run at once."`, 42 walk_workers: int `usage:"Directory readers to run at once."`, 43 chunk: int `usage:"Bytes per volume read."`, 44 } 45 46 main :: proc() { 47 os.exit(run()) 48 } 49 50 run :: proc() -> int { 51 when ODIN_DEBUG { 52 da: debug.Allocator 53 debug.init(&da, context.allocator) 54 defer debug.destroy(&da) 55 defer debug.report(&da) 56 context.allocator = debug.allocator(&da) 57 } 58 59 defer free_all(context.temp_allocator) 60 61 context.assertion_failure_proc = trace.assertion_failure_proc 62 63 opt: Options 64 if err := flags.parse(&opt, os.args[1:], .Unix); err != nil { 65 // print_errors writes usage for a help request and the reason for anything 66 // else, so the only decision left here is what to exit with. 67 flags.print_errors(Options, err, PROGRAM, .Unix) 68 _, asked_for_help := err.(flags.Help_Request) 69 return 0 if asked_for_help else 1 70 } 71 72 target_str := opt.target 73 if target_str == "" { 74 target_str = os.user_home_dir(context.temp_allocator) or_else "/" 75 } 76 opts := ntfs.Read_Options { 77 chunk_size = opt.chunk, 78 io_mode = .Buffered if opt.buffered else .Unbuffered, 79 // Negative reads every record; zero leaves the reader its own default. 80 min_skip = -1 if opt.no_skip else opt.min_skip, 81 workers = opt.workers, 82 } 83 wcfg := walk.Config { 84 workers = opt.walk_workers, 85 } 86 87 target, engine, resolve_err := scan.resolve(target_str) 88 if resolve_err != nil { 89 fmt.eprintfln("error: cannot scan %s: %v", target_str, resolve_err) 90 return 1 91 } 92 defer scan.target_destroy(&target) 93 94 fmt.printfln( 95 "sonar: %s on %s (%v, %v engine, %v io)", 96 scan.location(target, context.temp_allocator), 97 target.volume, 98 target.fs, 99 engine.engine, 100 opts.io_mode, 101 ) 102 103 if engine.permission_would_help { 104 when ODIN_OS == .Windows { 105 fmt.eprintln("note: this volume reads far faster from an administrator prompt") 106 } else { 107 fmt.eprintfln( 108 "note: this volume reads far faster with permission to open %s directly", 109 target.volume, 110 ) 111 } 112 } 113 114 if engine.engine == .None { 115 fmt.eprintfln("error: nothing here can be scanned") 116 return 1 117 } 118 119 t: scan.Tree 120 if err := scan.tree_init(&t, 8); err != nil { 121 fmt.eprintfln("error: %v", err) 122 return 1 123 } 124 defer scan.tree_destroy(&t) 125 126 // The table outlives the tree, whose names are borrowed from it. 127 f := Fill { 128 target = target, 129 engine = engine.engine, 130 opts = opts, 131 wcfg = wcfg, 132 tree = &t, 133 } 134 defer fill_destroy(&f) 135 136 if opt.live { 137 return run_live(&f) 138 } 139 140 start := time.tick_now() 141 fill_run(&f) 142 if fill_report(&f) { 143 return 1 144 } 145 elapsed := time.tick_since(start) 146 147 if engine.engine == .Mft { 148 print_native(&f.table, elapsed) 149 } 150 fmt.println() 151 fmt.printfln( 152 "tree %d slots, %d used, read in %.0f ms", 153 t.count, 154 t.nodes_done, 155 time.duration_milliseconds(elapsed), 156 ) 157 158 // The same charging the live scan does, run once over a tree nobody is still 159 // filling. A node left uncharged here has an ancestor that never resolved, and 160 // another pass would not change that. 161 roll: Rollup 162 defer rollup_destroy(&roll) 163 rollup_advance(&roll, &t) 164 165 print_largest(&t, roll.totals[:], .files) 166 print_largest(&t, roll.totals[:], .directories) 167 return 0 168 } 169 170 Kind :: enum { 171 files, 172 directories, 173 } 174 175 // The directories that hold the most, largest first. 176 rank :: proc(t: ^scan.Tree, totals: []u64, top: []Sized) -> int { 177 count := 0 178 // Bounded by the totals rather than the tree: a reader may hand out more slots 179 // while this runs, and those have not been charged yet. 180 for i in 0 ..< u32(len(totals)) { 181 f := scan.node_flags(scan.node(t, i)) 182 if .Used not_in f || .Directory not_in f { 183 continue 184 } 185 if totals[i] == 0 { 186 continue 187 } 188 top_n(top, &count, Sized{bytes = totals[i], record = i}) 189 } 190 return count 191 } 192 193 print_largest :: proc(t: ^scan.Tree, totals: []u64, kind: Kind) { 194 top: [TOP_N]Sized 195 count := 0 196 for i in 0 ..< u32(len(totals)) { 197 n := scan.node(t, i) 198 flags := scan.node_flags(n) 199 if .Used not_in flags { 200 continue 201 } 202 bytes: u64 203 switch kind { 204 case .files: 205 if .Directory in flags || .Extra_Name in flags { 206 continue 207 } 208 bytes = n.disk 209 case .directories: 210 if .Directory not_in flags { 211 continue 212 } 213 bytes = totals[i] 214 } 215 if bytes == 0 { 216 continue 217 } 218 top_n(top[:], &count, Sized{bytes = bytes, record = i}) 219 } 220 fmt.println() 221 fmt.printfln("largest %v (%d)", kind, count) 222 for s in top[:count] { 223 fmt.printfln(" %10s %s", human(s.bytes), scan.path(t, s.record, context.temp_allocator)) 224 } 225 } 226 227 // What only the NTFS reader knows, kept because the tree cannot carry it. 228 print_native :: proc(m: ^ntfs.Mft, elapsed: time.Duration) { 229 230 files: u64 231 allocated: u64 232 for e in m.entries { 233 if .In_Use not_in e.flags { 234 continue 235 } 236 allocated += e.allocated 237 if .Directory not_in e.flags { 238 files += 1 239 } 240 } 241 volume_bytes := m.boot.total_sectors * u64(m.boot.bytes_per_sector) 242 fmt.println() 243 fmt.printfln( 244 "volume %s, %d B clusters, %d B records", 245 human(volume_bytes), 246 m.boot.bytes_per_cluster, 247 m.boot.record_size, 248 ) 249 fmt.printfln( 250 "mft %d slots, %d records, %d unreadable, read in %.0f ms", 251 m.stats.records, 252 m.stats.records_read, 253 m.stats.records_bad, 254 time.duration_milliseconds(elapsed), 255 ) 256 fmt.printfln( 257 "in use %d files, %d directories, %d extra hard links", 258 files, 259 m.stats.directories, 260 len(m.links), 261 ) 262 fmt.printfln("attributed %s summed from every file run list", human(allocated)) 263 264 /* 265 $Bitmap counts allocated clusters without consulting a single file, so comparing it 266 against the sum above checks the run list decoding. The two should agree closely; 267 a wide gap means clusters are allocated that no file claims, which is a parsing bug 268 rather than missing disk space. 269 */ 270 on_disk := m.stats.allocated_clusters * u64(m.boot.bytes_per_cluster) 271 if on_disk > 0 { 272 fmt.printfln("on disk %s marked allocated in $Bitmap", human(on_disk)) 273 if on_disk >= allocated { 274 gap := on_disk - allocated 275 fmt.printfln( 276 "unclaimed %s (%.2f%%) allocated but charged to no file", 277 human(gap), 278 100 * f64(gap) / f64(on_disk), 279 ) 280 } else { 281 fmt.printfln( 282 "overcount %s more attributed than $Bitmap reports allocated", 283 human(allocated - on_disk), 284 ) 285 } 286 } 287 if m.stats.resident_files > 0 { 288 fmt.printfln( 289 "resident %d files hold %s inside their MFT records, charged to $MFT", 290 m.stats.resident_files, 291 human(m.stats.resident_bytes), 292 ) 293 } 294 } 295 296 Sized :: struct { 297 bytes: u64, 298 record: u32, 299 } 300 301 // Keep the N largest candidates in descending order without sorting the whole table. 302 // Most candidates fail the first comparison, so the scan stays linear. 303 @(private = "file") 304 top_n :: proc(top: []Sized, count: ^int, cand: Sized) { 305 if count^ == len(top) && cand.bytes <= top[len(top) - 1].bytes { 306 return 307 } 308 i := count^ if count^ < len(top) else len(top) - 1 309 for i > 0 && top[i - 1].bytes < cand.bytes { 310 top[i] = top[i - 1] 311 i -= 1 312 } 313 top[i] = cand 314 if count^ < len(top) { 315 count^ += 1 316 } 317 } 318 319 human :: proc(n: u64) -> string { 320 units := [?]string{"B", "KiB", "MiB", "GiB", "TiB"} 321 v := f64(n) 322 i := 0 323 for v >= 1024 && i < len(units) - 1 { 324 v /= 1024 325 i += 1 326 } 327 if i == 0 { 328 return fmt.tprintf("%d B", n) 329 } 330 return fmt.tprintf("%.1f %s", v, units[i]) 331 }