target.odin (3474B)
1 package scan 2 3 import "core:mem" 4 import "core:strings" 5 6 /* 7 What the user asked to scan, resolved once so no backend has to re-derive it. 8 9 Deciding which volume a path lives on and what filesystem that volume runs is work 10 for the OS, not string matching, and every backend would otherwise repeat it. Doing 11 it here also puts the failures that are knowable up front, a path that does not 12 exist or a share with no volume behind it, in the one place that can explain them. 13 */ 14 Target :: struct { 15 input: string, // exactly what was given 16 volume: string, // device path to open, e.g. \\.\C: 17 mount: string, // where that volume is mounted, e.g. C:\ 18 root: string, // subtree within the volume; "" means all of it 19 fs: Filesystem, 20 image: bool, // a file holding a volume rather than a mounted one 21 allocator: mem.Allocator, 22 } 23 24 Filesystem :: enum { 25 Unknown, // nothing could be learned about it 26 Ntfs, 27 Refs, 28 Exfat, 29 Fat32, 30 Network, 31 Other, // named, but no reader here knows it: walking is all that is left 32 } 33 34 /* 35 The top of the target, named as a path: where the volume is mounted, or the volume 36 itself when nothing has it mounted. No trailing separator, so a subtree joins on. 37 */ 38 top :: proc(t: Target) -> string { 39 if t.mount == "" { 40 return t.volume 41 } 42 if len(t.mount) > 1 { 43 return strings.trim_right(t.mount, `\/`) 44 } 45 return t.mount 46 } 47 48 // The full path of what will be scanned: the top, and the subtree below it. 49 location :: proc(t: Target, allocator := context.allocator) -> string { 50 if t.root == "" { 51 return strings.clone(top(t), allocator) 52 } 53 return strings.concatenate({top(t), t.root}, allocator) 54 } 55 56 target_destroy :: proc(t: ^Target) { 57 delete(t.input, t.allocator) 58 delete(t.volume, t.allocator) 59 delete(t.mount, t.allocator) 60 delete(t.root, t.allocator) 61 t^ = {} 62 } 63 64 // Which reader to use. Named here so choosing needs no knowledge of the backends 65 // themselves, which keeps this package free of a dependency on any of them. 66 Engine :: enum { 67 None, // nothing here can be scanned 68 Mft, // read the NTFS master file table whole 69 Walk, // enumerate directories 70 } 71 72 Choice :: struct { 73 engine: Engine, 74 // The MFT reader is far faster but needs to open the volume raw. When this is 75 // set, Walk is what will run unless the caller obtains that and asks again. 76 permission_would_help: bool, 77 } 78 79 /* 80 Pick a reader for a target. 81 82 `readable` says the volume opened for raw reading, which is the only thing standing 83 between an NTFS target and the MFT reader. `resolve` finds it out and calls this; it 84 is a separate procedure so the whole decision table can be tested without a disk, not 85 because anything else needs to choose. An image needs no special case: it is a file, 86 so it opens. 87 88 The MFT reader takes the whole volume at once, so a subtree costs no more than the 89 root does and is far cheaper than walking it. That makes a subtree target a reason to 90 filter the result, never a reason to reject the reader. 91 */ 92 @(private) 93 choose :: proc(t: Target, readable: bool) -> Choice { 94 if t.fs == .Ntfs { 95 if readable { 96 return {engine = .Mft} 97 } 98 return {engine = .Walk, permission_would_help = true} 99 } 100 // Nothing mounted means no directory tree to enumerate. A raw volume and an image 101 // are both bytes until a reader that knows the filesystem inside says otherwise, 102 // so an unrecognised one has nothing to offer rather than something to walk. 103 if t.mount == "" { 104 return {engine = .None} 105 } 106 return {engine = .Walk} 107 }