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 }