sonar

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

mft.odin (11973B)


      1 package ntfs
      2 
      3 import "core:mem"
      4 import "core:mem/virtual"
      5 import "core:strings"
      6 import "core:sync"
      7 import "core:unicode/utf16"
      8 import "core:unicode/utf8"
      9 
     10 // One row per MFT record. Indexed by record number in Mft.entries.
     11 Entry :: struct {
     12 	parent:          u32, // record number of the directory holding `name`
     13 	parent_sequence: u16, // sequence the parent had when this name was written; mismatch means orphan
     14 	sequence:        u16,
     15 	link_count:      u16,
     16 	namespace:       Name_Space, // of `name`
     17 	flags:           Record_Flags, // .In_Use and .Directory are the ones that matter
     18 	attributes:      File_Attributes, // from $STANDARD_INFORMATION; .Reparse_Point marks junctions and cloud placeholders
     19 	name:            string, // UTF-8; empty when the record has no $FILE_NAME (free records, extension records)
     20 	size:            u64, // logical length of the unnamed $DATA stream
     21 	allocated:       u64, // bytes on disk across every non-resident attribute: data, alternate streams, indexes
     22 }
     23 
     24 // A file with several names. The first Win32 or POSIX name becomes Entry.name; the
     25 // rest land here so they can be shown but never double-counted.
     26 Hard_Link :: struct {
     27 	record: u32,
     28 	parent: u32,
     29 	name:   string,
     30 }
     31 
     32 Mft_Stats :: struct {
     33 	records:            u64, // record slots in $MFT
     34 	records_read:       u64, // slots that held a FILE record
     35 	records_bad:        u64, // FILE records whose fixups failed
     36 	in_use:             u64,
     37 	directories:        u64,
     38 	// Files whose unnamed $DATA fits inside their MFT record. They occupy no clusters
     39 	// of their own, so their bytes are charged to $MFT rather than to their directory.
     40 	resident_files:     u64,
     41 	resident_bytes:     u64,
     42 	// Clusters $Bitmap reports as allocated; 0 when it could not be read. This is the
     43 	// file system's own total, independent of the per-file sums.
     44 	allocated_clusters: u64,
     45 	// What the read plan decided, from $MFT's own $BITMAP of live record slots.
     46 	planned_bytes:      u64,
     47 	skipped_bytes:      u64,
     48 	extents:            u64,
     49 	workers:            u64, // threads the scan actually ran on
     50 	// Phase timings, nanoseconds.
     51 	io_ns:              i64,
     52 	parse_ns:           i64,
     53 	bitmap_ns:          i64,
     54 }
     55 
     56 /*
     57 A record that belongs to a base record elsewhere in the table.
     58 
     59 Its attributes are credited to the base entry, which another worker may own, so it
     60 cannot be folded in during a parallel pass. The bytes are copied because the read
     61 buffer is reused for the next chunk.
     62 */
     63 Deferred :: struct {
     64 	record: u32,
     65 	bytes:  []byte,
     66 }
     67 
     68 /*
     69 Where one worker puts what it produces.
     70 
     71 Entries are written straight into the table at their own record numbers, which no
     72 two workers share, so only these need merging afterwards. The arena outlives the run
     73 because every `Entry.name` points into it.
     74 */
     75 Sink :: struct {
     76 	names:    virtual.Arena,
     77 	links:    [dynamic]Hard_Link,
     78 	deferred: [dynamic]Deferred,
     79 	stats:    Mft_Stats,
     80 }
     81 
     82 Mft :: struct {
     83 	entries:           []Entry,
     84 	links:             [dynamic]Hard_Link,
     85 	sinks:             []Sink, // one per worker; kept alive for the names they hold
     86 	boot:              Boot_Sector,
     87 	bytes_per_cluster: u64,
     88 	stats:             Mft_Stats,
     89 	allocator:         mem.Allocator,
     90 	ready:             b32, // the table exists and may be read while it fills
     91 	complete:          b32, // nothing further will be added to it
     92 }
     93 
     94 mft_init :: proc(
     95 	m: ^Mft,
     96 	record_count: int,
     97 	bytes_per_cluster: u64,
     98 	sinks := 1,
     99 	allocator := context.allocator,
    100 ) -> Error {
    101 	m.allocator = allocator
    102 	m.bytes_per_cluster = bytes_per_cluster
    103 	entries, err := make([]Entry, record_count, allocator)
    104 	if err != nil {
    105 		return .Out_Of_Memory
    106 	}
    107 	m.entries = entries
    108 	m.links = make([dynamic]Hard_Link, allocator)
    109 	m.sinks = make([]Sink, max(sinks, 1), allocator)
    110 	for &s in m.sinks {
    111 		if virtual.arena_init_growing(&s.names) != nil {
    112 			mft_destroy(m)
    113 			return .Out_Of_Memory
    114 		}
    115 		s.links.allocator = allocator
    116 		s.deferred.allocator = allocator
    117 	}
    118 	m.stats.records = u64(record_count)
    119 	return .None
    120 }
    121 
    122 mft_destroy :: proc(m: ^Mft) {
    123 	delete(m.entries, m.allocator)
    124 	delete(m.links)
    125 	for &s in m.sinks {
    126 		delete(s.links)
    127 		delete(s.deferred)
    128 		virtual.arena_destroy(&s.names)
    129 	}
    130 	delete(m.sinks, m.allocator)
    131 	m^ = {}
    132 }
    133 
    134 // Say the table is sized and safe to watch. Until then there is nothing to read.
    135 mft_set_ready :: proc(m: ^Mft) {
    136 	sync.atomic_store(&m.ready, true)
    137 }
    138 
    139 mft_ready :: proc(m: ^Mft) -> bool {
    140 	return bool(sync.atomic_load(&m.ready))
    141 }
    142 
    143 // Say the table is finished. Hard links are merged in at the very end, so anything
    144 // reading them concurrently has to wait for this.
    145 mft_set_complete :: proc(m: ^Mft) {
    146 	sync.atomic_store(&m.complete, true)
    147 }
    148 
    149 mft_complete :: proc(m: ^Mft) -> bool {
    150 	return bool(sync.atomic_load(&m.complete))
    151 }
    152 
    153 /*
    154 Whether an entry is finished and safe for another thread to read.
    155 
    156 Folding writes an entry's flags last, so a reader that sees them set is looking at a
    157 record whose name and sizes are already in place. Without that order a watcher could
    158 catch a half-written name, which is a pointer and a length written separately.
    159 */
    160 entry_published :: proc(e: ^Entry) -> bool {
    161 	return .In_Use in transmute(Record_Flags)sync.atomic_load((^u16)(&e.flags))
    162 }
    163 
    164 /*
    165 An entry's parent, and whether that answer can still change.
    166 
    167 A parent record that has not been published yet may turn out to hold this name, so
    168 the link is not final until it appears. One that has been published and does not
    169 match never will be, which is just as final as a valid answer and lets the node be
    170 charged instead of waiting forever.
    171 
    172 The returned index is the entry's own when it has no usable parent.
    173 */
    174 entry_parent_settled :: proc(m: ^Mft, i: u32) -> (parent: u32, settled: bool) {
    175 	e := &m.entries[i]
    176 	if int(e.parent) >= len(m.entries) || e.parent == i {
    177 		return i, true
    178 	}
    179 	p := &m.entries[e.parent]
    180 	if !entry_published(p) {
    181 		return i, false
    182 	}
    183 	if .Directory in p.flags && p.sequence == e.parent_sequence {
    184 		return e.parent, true
    185 	}
    186 	return i, true
    187 }
    188 
    189 // Fold each worker's private results into the table. Only these needed merging;
    190 // entries were written into slots no other worker could reach.
    191 mft_merge_sinks :: proc(m: ^Mft) {
    192 	for &s in m.sinks {
    193 		m.stats.records_read += s.stats.records_read
    194 		m.stats.records_bad += s.stats.records_bad
    195 		m.stats.in_use += s.stats.in_use
    196 		m.stats.directories += s.stats.directories
    197 		m.stats.resident_files += s.stats.resident_files
    198 		m.stats.resident_bytes += s.stats.resident_bytes
    199 		m.stats.io_ns += s.stats.io_ns
    200 		m.stats.parse_ns += s.stats.parse_ns
    201 		for l in s.links {
    202 			append(&m.links, l)
    203 		}
    204 		clear(&s.links)
    205 	}
    206 }
    207 
    208 /*
    209 Fold one fixed-up FILE record into the table. Records may arrive in any order.
    210 
    211 An extension record carries attributes that did not fit in its base record (a heavily
    212 fragmented file, or one with many hard links). Its header names the base record, so
    213 its attributes are credited to the base entry. This makes the $ATTRIBUTE_LIST
    214 attribute unnecessary for our purposes: by visiting every record we see every
    215 attribute anyway.
    216 
    217 Records not in use are skipped entirely. Their contents are stale and may point at
    218 records that have since been reused.
    219 */
    220 mft_add_record :: proc(m: ^Mft, record_number: u32, rec: []byte, sink: ^Sink) -> Error {
    221 	if len(rec) < size_of(Record_Header) {
    222 		return .Bad_Record
    223 	}
    224 	h := record_header(rec)
    225 	if h.magic != RECORD_MAGIC {
    226 		return .Bad_Record
    227 	}
    228 	if .In_Use not_in h.flags {
    229 		return .None
    230 	}
    231 
    232 	owner := record_number
    233 	if h.base_record != 0 {
    234 		owner = u32(ref_record(h.base_record))
    235 		if int(owner) >= len(m.entries) {
    236 			return .None
    237 		}
    238 	}
    239 	if int(owner) >= len(m.entries) {
    240 		return .Bad_Record
    241 	}
    242 	e := &m.entries[owner]
    243 	publish := owner == record_number
    244 	if publish {
    245 		e.sequence = h.sequence
    246 		e.link_count = h.link_count
    247 		sink.stats.in_use += 1
    248 		if .Directory in h.flags {
    249 			sink.stats.directories += 1
    250 		}
    251 	}
    252 
    253 	it := attributes(rec)
    254 	for {
    255 		a, ok := next_attribute(&it)
    256 		if !ok {
    257 			break
    258 		}
    259 		#partial switch a.type {
    260 		case .Standard_Information:
    261 			if si, si_ok := standard_information(a); si_ok {
    262 				e.attributes = si.file_attributes
    263 			}
    264 		case .File_Name:
    265 			if fn, fn_ok := file_name(a); fn_ok {
    266 				add_name(m, owner, fn, sink)
    267 			}
    268 		case .Data:
    269 			if a.non_resident {
    270 				// Every extent contributes its own clusters, but only the first carries the
    271 				// logical size; later extents repeat it.
    272 				e.allocated += attr_disk_size(a, m.bytes_per_cluster)
    273 				if a.lowest_vcn == 0 && len(a.name) == 0 {
    274 					e.size = a.data_size
    275 				}
    276 			} else if len(a.name) == 0 {
    277 				e.size = u64(len(a.value))
    278 				sink.stats.resident_files += 1
    279 				sink.stats.resident_bytes += u64(len(a.value))
    280 			}
    281 		case:
    282 			// Directory indexes, bitmaps, reparse data, and EFS streams occupy clusters too.
    283 			e.allocated += attr_disk_size(a, m.bytes_per_cluster)
    284 		}
    285 	}
    286 	// Last, and atomically: this is what tells a watching thread the entry is whole.
    287 	if publish {
    288 		sync.atomic_store((^u16)(&e.flags), transmute(u16)h.flags)
    289 	}
    290 	return .None
    291 }
    292 
    293 /*
    294 Choose which $FILE_NAME becomes the entry's name.
    295 
    296 A file usually has one Win32 name and, when that name does not fit 8.3, a DOS alias
    297 pointing at the same parent. The alias is not a separate link, so it is skipped
    298 whenever a proper name exists. Any further Win32 or POSIX name is a real hard link.
    299 */
    300 @(private)
    301 add_name :: proc(m: ^Mft, owner: u32, fn: File_Name, sink: ^Sink) {
    302 	e := &m.entries[owner]
    303 	parent := u32(ref_record(fn.parent))
    304 	parent_seq := ref_sequence(fn.parent)
    305 
    306 	if e.name == "" {
    307 		e.name = intern_utf16(sink, fn.name)
    308 		e.parent = parent
    309 		e.parent_sequence = parent_seq
    310 		e.namespace = fn.namespace
    311 		return
    312 	}
    313 	if fn.namespace == .Dos {
    314 		return
    315 	}
    316 	if e.namespace == .Dos {
    317 		// The alias arrived first; the real name replaces it.
    318 		e.name = intern_utf16(sink, fn.name)
    319 		e.parent = parent
    320 		e.parent_sequence = parent_seq
    321 		e.namespace = fn.namespace
    322 		return
    323 	}
    324 	name := intern_utf16(sink, fn.name)
    325 	if name == e.name && parent == e.parent {
    326 		return
    327 	}
    328 	append(&sink.links, Hard_Link{record = owner, parent = parent, name = name})
    329 }
    330 
    331 // Copy a UTF-16LE name into the arena as UTF-8. Names are at most 255 code units.
    332 @(private)
    333 intern_utf16 :: proc(sink: ^Sink, name: []u16) -> string {
    334 	runes: [256]rune
    335 	n := utf16.decode(runes[:], name)
    336 	total := 0
    337 	for r in runes[:n] {
    338 		total += utf8.rune_size(r)
    339 	}
    340 	buf, err := make([]byte, total, virtual.arena_allocator(&sink.names))
    341 	if err != nil {
    342 		return ""
    343 	}
    344 	off := 0
    345 	for r in runes[:n] {
    346 		enc, size := utf8.encode_rune(r)
    347 		copy(buf[off:], enc[:size])
    348 		off += size
    349 	}
    350 	return string(buf)
    351 }
    352 
    353 // True when the entry's parent reference still points at the record it was written for.
    354 entry_parent_valid :: proc(m: ^Mft, e: Entry) -> bool {
    355 	if int(e.parent) >= len(m.entries) {
    356 		return false
    357 	}
    358 	p := m.entries[e.parent]
    359 	return .In_Use in p.flags && .Directory in p.flags && p.sequence == e.parent_sequence
    360 }
    361 
    362 /*
    363 Reconstruct the path of a record by following parent references up to the root.
    364 The result starts with a backslash and omits the drive letter, e.g.
    365 `\Windows\explorer.exe`. Records whose chain does not reach the root (files inside a
    366 deleted directory, or a stale parent reference) yield a path rooted at the highest
    367 reachable ancestor, prefixed with `<orphan>`.
    368 */
    369 mft_path :: proc(m: ^Mft, record: u32, allocator := context.allocator) -> string {
    370 	chain: [64]u32
    371 	depth := 0
    372 	r := record
    373 	orphan := false
    374 	for depth < len(chain) {
    375 		if int(r) >= len(m.entries) || r == RECORD_ROOT {
    376 			break
    377 		}
    378 		e := m.entries[r]
    379 		chain[depth] = r
    380 		depth += 1
    381 		if !entry_parent_valid(m, e) || e.parent == r {
    382 			orphan = true
    383 			break
    384 		}
    385 		r = e.parent
    386 	}
    387 	if depth == len(chain) {
    388 		orphan = true
    389 	}
    390 
    391 	sb := strings.builder_make(allocator)
    392 	if orphan {
    393 		strings.write_string(&sb, "<orphan>")
    394 	}
    395 	if depth == 0 {
    396 		strings.write_string(&sb, `\`)
    397 	}
    398 	for i := depth - 1; i >= 0; i -= 1 {
    399 		strings.write_string(&sb, `\`)
    400 		strings.write_string(&sb, m.entries[chain[i]].name)
    401 	}
    402 	return strings.to_string(sb)
    403 }