sonar

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

record.odin (10413B)


      1 package ntfs
      2 
      3 import "core:mem"
      4 
      5 RECORD_MAGIC     :: u32(0x454C4946) // "FILE"
      6 RECORD_MAGIC_BAD :: u32(0x44414142) // "BAAD": chkdsk marked the record as damaged
      7 
      8 // Fixups protect every 512 bytes regardless of the physical sector size.
      9 FIXUP_BLOCK :: 512
     10 
     11 Record_Flag  :: enum u16 {
     12 	In_Use     = 0,
     13 	Directory  = 1,
     14 	Extension  = 2, // record lives in $Extend
     15 	View_Index = 3,
     16 }
     17 Record_Flags :: bit_set[Record_Flag;u16]
     18 
     19 Record_Header :: struct #packed {
     20 	magic:           u32,
     21 	usa_offset:      u16, // update sequence array
     22 	usa_count:       u16, // entries in the array, including the sequence number itself
     23 	lsn:             u64, // $LogFile sequence number
     24 	sequence:        u16,
     25 	link_count:      u16, // hard links, i.e. number of $FILE_NAME attributes across all namespaces
     26 	attrs_offset:    u16,
     27 	flags:           Record_Flags,
     28 	bytes_in_use:    u32,
     29 	bytes_allocated: u32,
     30 	base_record:     File_Ref, // 0 for a base record; otherwise this is an extension record
     31 	next_attr_id:    u16,
     32 	_pad:            u16,
     33 	record_number:   u32, // NTFS 3.1+
     34 }
     35 #assert(size_of(Record_Header) == 48)
     36 
     37 Attr_Type :: enum u32 {
     38 	Standard_Information  = 0x10,
     39 	Attribute_List        = 0x20,
     40 	File_Name             = 0x30,
     41 	Object_Id             = 0x40,
     42 	Security_Descriptor   = 0x50,
     43 	Volume_Name           = 0x60,
     44 	Volume_Information    = 0x70,
     45 	Data                  = 0x80,
     46 	Index_Root            = 0x90,
     47 	Index_Allocation      = 0xA0,
     48 	Bitmap                = 0xB0,
     49 	Reparse_Point         = 0xC0,
     50 	EA_Information        = 0xD0,
     51 	EA                    = 0xE0,
     52 	Logged_Utility_Stream = 0x100,
     53 	End                   = 0xFFFFFFFF,
     54 }
     55 
     56 Attr_Flag  :: enum u16 {
     57 	Compressed = 0,
     58 	Encrypted  = 14,
     59 	Sparse     = 15,
     60 }
     61 Attr_Flags :: bit_set[Attr_Flag;u16]
     62 
     63 Attr_Header :: struct #packed {
     64 	type:         Attr_Type,
     65 	length:       u32, // whole attribute record, header included, 8-byte aligned
     66 	non_resident: u8,
     67 	name_length:  u8, // in UTF-16 code units
     68 	name_offset:  u16,
     69 	flags:        Attr_Flags,
     70 	id:           u16,
     71 }
     72 #assert(size_of(Attr_Header) == 16)
     73 
     74 Resident_Header :: struct #packed {
     75 	hdr:          Attr_Header,
     76 	value_length: u32,
     77 	value_offset: u16,
     78 	indexed:      u8,
     79 	_pad:         u8,
     80 }
     81 #assert(size_of(Resident_Header) == 24)
     82 
     83 Nonresident_Header :: struct #packed {
     84 	hdr:              Attr_Header,
     85 	lowest_vcn:       u64,
     86 	highest_vcn:      u64,
     87 	runlist_offset:   u16,
     88 	compression_unit: u8,
     89 	_pad:             [5]u8,
     90 	allocated_size:   u64, // clusters reserved, in bytes; for compressed/sparse this is logical, see compressed_size
     91 	data_size:        u64, // logical length
     92 	initialized_size: u64,
     93 	// compressed_size: u64 follows only when flags contain Compressed or Sparse
     94 }
     95 #assert(size_of(Nonresident_Header) == 64)
     96 
     97 // Decoded view of one attribute record. Slices point into the record buffer.
     98 Attribute :: struct {
     99 	type:             Attr_Type,
    100 	flags:            Attr_Flags,
    101 	id:               u16,
    102 	name:             []u16, // empty for the unnamed attribute
    103 	non_resident:     bool,
    104 	value:            []byte, // resident only
    105 	lowest_vcn:       u64, // non-resident only from here on
    106 	highest_vcn:      u64,
    107 	allocated_size:   u64,
    108 	data_size:        u64,
    109 	initialized_size: u64,
    110 	compressed_size:  u64, // equals allocated_size when the attribute is neither compressed nor sparse
    111 	runlist:          []byte, // raw mapping pairs; decode with decode_runlist
    112 }
    113 
    114 /*
    115 Bytes this attribute extent actually occupies on disk.
    116 
    117 Resident values live inside the record and cost nothing extra. For non-resident
    118 extents the run list is authoritative: it says exactly which clusters are allocated,
    119 which is correct for plain, compressed, and sparse attributes alike, and for the
    120 system files whose headers claim allocation they do not have. Each extent's run list
    121 covers only that extent's VCN range, so summing over every extent of a fragmented
    122 attribute gives the whole. The header sizes are only a fallback for a malformed run
    123 list, and then only on the first extent, since later extents repeat them.
    124 */
    125 attr_disk_size :: proc "contextless" (a: Attribute, bytes_per_cluster: u64) -> u64 {
    126 	if !a.non_resident {
    127 		return 0
    128 	}
    129 	if clusters, ok := runlist_allocated_clusters(a.runlist); ok {
    130 		return clusters * bytes_per_cluster
    131 	}
    132 	if a.lowest_vcn != 0 {
    133 		return 0
    134 	}
    135 	if .Compressed in a.flags || .Sparse in a.flags {
    136 		return a.compressed_size
    137 	}
    138 	return a.allocated_size
    139 }
    140 
    141 record_header :: proc "contextless" (rec: []byte) -> ^Record_Header {
    142 	return (^Record_Header)(raw_data(rec))
    143 }
    144 
    145 /*
    146 Undo the update sequence protection in place.
    147 
    148 NTFS writes a record in 512-byte blocks and cannot guarantee all blocks land together
    149 after a power loss. So it stamps the last two bytes of every block with the same
    150 sequence number and stores the displaced bytes in the update sequence array. Reading
    151 a record means checking every stamp matches, then restoring the original bytes. A
    152 mismatch means a torn write; the record is unreliable.
    153 */
    154 apply_fixups :: proc(rec: []byte) -> Error {
    155 	if len(rec) < size_of(Record_Header) {
    156 		return .Bad_Record
    157 	}
    158 	h := record_header(rec)
    159 	if h.magic != RECORD_MAGIC {
    160 		return .Bad_Record
    161 	}
    162 	usa_ofs := int(h.usa_offset)
    163 	count := int(h.usa_count)
    164 	if count < 2 || usa_ofs + 2 * count > len(rec) || (count - 1) * FIXUP_BLOCK > len(rec) {
    165 		return .Bad_Record
    166 	}
    167 	usn := rd16(rec, usa_ofs)
    168 	for i in 1 ..< count {
    169 		end := i * FIXUP_BLOCK
    170 		if rd16(rec, end - 2) != usn {
    171 			return .Bad_Record
    172 		}
    173 		wr16(rec, end - 2, rd16(rec, usa_ofs + 2 * i))
    174 	}
    175 	return .None
    176 }
    177 
    178 Attr_Iterator :: struct {
    179 	rec:    []byte,
    180 	offset: int,
    181 }
    182 
    183 // Iterate a fixed-up record's attributes. Stops at the end marker or the first
    184 // malformed header, so a corrupt record yields a prefix rather than garbage.
    185 attributes :: proc "contextless" (rec: []byte) -> Attr_Iterator {
    186 	return Attr_Iterator{rec = rec, offset = int(record_header(rec).attrs_offset)}
    187 }
    188 
    189 next_attribute :: proc(it: ^Attr_Iterator) -> (a: Attribute, ok: bool) {
    190 	rec := it.rec
    191 	if it.offset < 0 || it.offset + 4 > len(rec) {
    192 		return
    193 	}
    194 	if Attr_Type(rd32(rec, it.offset)) == .End {
    195 		return
    196 	}
    197 	if it.offset + size_of(Attr_Header) > len(rec) {
    198 		return
    199 	}
    200 	hdr := (^Attr_Header)(raw_data(rec[it.offset:]))^
    201 	length := int(hdr.length)
    202 	if length < size_of(Attr_Header) || length % 8 != 0 || it.offset + length > len(rec) {
    203 		return
    204 	}
    205 	raw := rec[it.offset:it.offset + length]
    206 
    207 	a.type = hdr.type
    208 	a.flags = hdr.flags
    209 	a.id = hdr.id
    210 	if hdr.name_length > 0 {
    211 		no := int(hdr.name_offset)
    212 		nl := int(hdr.name_length) * 2
    213 		if no + nl > length {
    214 			return
    215 		}
    216 		a.name = mem.slice_data_cast([]u16, raw[no:no + nl])
    217 	}
    218 
    219 	if hdr.non_resident == 0 {
    220 		if length < size_of(Resident_Header) {
    221 			return
    222 		}
    223 		r := (^Resident_Header)(raw_data(raw))^
    224 		vo := int(r.value_offset)
    225 		vl := int(r.value_length)
    226 		if vo + vl > length {
    227 			return
    228 		}
    229 		a.value = raw[vo:vo + vl]
    230 	} else {
    231 		if length < size_of(Nonresident_Header) {
    232 			return
    233 		}
    234 		n := (^Nonresident_Header)(raw_data(raw))^
    235 		a.non_resident = true
    236 		a.lowest_vcn = n.lowest_vcn
    237 		a.highest_vcn = n.highest_vcn
    238 		a.allocated_size = n.allocated_size
    239 		a.data_size = n.data_size
    240 		a.initialized_size = n.initialized_size
    241 		a.compressed_size = n.allocated_size
    242 		if (.Compressed in hdr.flags || .Sparse in hdr.flags) &&
    243 		   length >= size_of(Nonresident_Header) + 8 {
    244 			a.compressed_size = rd64(raw, size_of(Nonresident_Header))
    245 		}
    246 		ro := int(n.runlist_offset)
    247 		if ro >= size_of(Nonresident_Header) && ro <= length {
    248 			a.runlist = raw[ro:]
    249 		}
    250 	}
    251 
    252 	it.offset += length
    253 	ok = true
    254 	return
    255 }
    256 
    257 // Windows FILE_ATTRIBUTE_* bits as stored in $STANDARD_INFORMATION and $FILE_NAME.
    258 File_Attribute  :: enum u32 {
    259 	Read_Only             = 0,
    260 	Hidden                = 1,
    261 	System                = 2,
    262 	Directory             = 4,
    263 	Archive               = 5,
    264 	Device                = 6,
    265 	Normal                = 7,
    266 	Temporary             = 8,
    267 	Sparse                = 9,
    268 	Reparse_Point         = 10,
    269 	Compressed            = 11,
    270 	Offline               = 12,
    271 	Not_Content_Indexed   = 13,
    272 	Encrypted             = 14,
    273 	Integrity_Stream      = 15,
    274 	Virtual               = 16,
    275 	No_Scrub              = 17,
    276 	Recall_On_Open        = 18,
    277 	Pinned                = 19,
    278 	Unpinned              = 20,
    279 	Recall_On_Data_Access = 22,
    280 	Directory_Index       = 28, // NTFS-internal: record has an $I30 index
    281 	View_Index            = 29,
    282 }
    283 File_Attributes :: bit_set[File_Attribute;u32]
    284 
    285 Standard_Information :: struct #packed {
    286 	created:         u64, // FILETIME
    287 	modified:        u64,
    288 	mft_modified:    u64,
    289 	accessed:        u64,
    290 	file_attributes: File_Attributes,
    291 	max_versions:    u32,
    292 	version:         u32,
    293 	class_id:        u32,
    294 	// NTFS 3.x adds owner id, security id, quota, and USN; not needed here
    295 }
    296 #assert(size_of(Standard_Information) == 48)
    297 
    298 standard_information :: proc(a: Attribute) -> (si: Standard_Information, ok: bool) {
    299 	if a.type != .Standard_Information ||
    300 	   a.non_resident ||
    301 	   len(a.value) < size_of(Standard_Information) {
    302 		return
    303 	}
    304 	return (^Standard_Information)(raw_data(a.value))^, true
    305 }
    306 
    307 Name_Space :: enum u8 {
    308 	Posix         = 0, // case-sensitive, any Unicode except NUL and '/'
    309 	Win32         = 1,
    310 	Dos           = 2, // 8.3 alias generated alongside a Win32 name
    311 	Win32_And_Dos = 3, // the Win32 name already fits 8.3, so one entry serves both
    312 }
    313 
    314 File_Name_Header :: struct #packed {
    315 	parent:          File_Ref,
    316 	created:         u64,
    317 	modified:        u64,
    318 	mft_modified:    u64,
    319 	accessed:        u64,
    320 	allocated_size:  u64, // only updated when the directory entry is; do not trust for sizing
    321 	data_size:       u64,
    322 	file_attributes: File_Attributes,
    323 	reparse_or_ea:   u32,
    324 	name_length:     u8, // UTF-16 code units
    325 	namespace:       Name_Space,
    326 }
    327 #assert(size_of(File_Name_Header) == 66)
    328 
    329 File_Name :: struct {
    330 	parent:          File_Ref,
    331 	file_attributes: File_Attributes,
    332 	namespace:       Name_Space,
    333 	name:            []u16,
    334 }
    335 
    336 file_name :: proc(a: Attribute) -> (fn: File_Name, ok: bool) {
    337 	if a.type != .File_Name || a.non_resident || len(a.value) < size_of(File_Name_Header) {
    338 		return
    339 	}
    340 	h := (^File_Name_Header)(raw_data(a.value))^
    341 	n := int(h.name_length) * 2
    342 	if size_of(File_Name_Header) + n > len(a.value) {
    343 		return
    344 	}
    345 	fn.parent = h.parent
    346 	fn.file_attributes = h.file_attributes
    347 	fn.namespace = h.namespace
    348 	fn.name = mem.slice_data_cast(
    349 		[]u16,
    350 		a.value[size_of(File_Name_Header):size_of(File_Name_Header) + n],
    351 	)
    352 	return fn, true
    353 }