ntfs_test.odin (17106B)
1 package ntfs 2 3 import "core:testing" 4 import "core:unicode/utf16" 5 6 // ---- synthetic on-disk structures ------------------------------------------------- 7 // 8 // Real MFT records are awkward to ship as fixtures (they carry the volume's file 9 // names), so tests build records byte by byte with the same layout rules the parser 10 // enforces. Everything is allocated from the temp allocator, which the test runner 11 // resets between tests. 12 13 @(private = "file") 14 put16 :: proc(b: []byte, off: int, v: u16) { 15 b[off] = byte(v) 16 b[off + 1] = byte(v >> 8) 17 } 18 19 @(private = "file") 20 put32 :: proc(b: []byte, off: int, v: u32) { 21 put16(b, off, u16(v)) 22 put16(b, off + 2, u16(v >> 16)) 23 } 24 25 @(private = "file") 26 put64 :: proc(b: []byte, off: int, v: u64) { 27 put32(b, off, u32(v)) 28 put32(b, off + 4, u32(v >> 32)) 29 } 30 31 @(private = "file") 32 utf16_of :: proc(s: string) -> []u16 { 33 buf := make([]u16, len(s) * 2 + 1, context.temp_allocator) 34 n := utf16.encode_string(buf, s) 35 return buf[:n] 36 } 37 38 @(private = "file") 39 align8 :: proc(n: int) -> int { 40 return (n + 7) & ~int(7) 41 } 42 43 @(private = "file") 44 resident :: proc(type: Attr_Type, value: []byte, name := "", flags: Attr_Flags = {}) -> []byte { 45 name16 := utf16_of(name) 46 name_ofs := size_of(Resident_Header) 47 value_ofs := align8(name_ofs + len(name16) * 2) 48 length := align8(value_ofs + len(value)) 49 b := make([]byte, length, context.temp_allocator) 50 put32(b, 0, u32(type)) 51 put32(b, 4, u32(length)) 52 b[8] = 0 53 b[9] = u8(len(name16)) 54 put16(b, 10, u16(name_ofs)) 55 put16(b, 12, transmute(u16)flags) 56 put16(b, 14, 1) 57 put32(b, 16, u32(len(value))) 58 put16(b, 20, u16(value_ofs)) 59 for c, i in name16 { 60 put16(b, name_ofs + 2 * i, c) 61 } 62 copy(b[value_ofs:], value) 63 return b 64 } 65 66 @(private = "file") 67 nonresident :: proc( 68 type: Attr_Type, 69 allocated, data_size: u64, 70 runlist: []byte, 71 name := "", 72 flags: Attr_Flags = {}, 73 compressed_size: u64 = 0, 74 lowest_vcn: u64 = 0, 75 ) -> []byte { 76 name16 := utf16_of(name) 77 has_cs := .Compressed in flags || .Sparse in flags 78 hdr_len := size_of(Nonresident_Header) + (8 if has_cs else 0) 79 name_ofs := hdr_len 80 run_ofs := align8(name_ofs + len(name16) * 2) 81 length := align8(run_ofs + len(runlist)) 82 b := make([]byte, length, context.temp_allocator) 83 put32(b, 0, u32(type)) 84 put32(b, 4, u32(length)) 85 b[8] = 1 86 b[9] = u8(len(name16)) 87 put16(b, 10, u16(name_ofs)) 88 put16(b, 12, transmute(u16)flags) 89 put16(b, 14, 2) 90 put64(b, 16, lowest_vcn) 91 clusters := allocated / 4096 92 if clusters > 0 { 93 put64(b, 24, lowest_vcn + clusters - 1) 94 } 95 put16(b, 32, u16(run_ofs)) 96 put64(b, 40, allocated) 97 put64(b, 48, data_size) 98 put64(b, 56, data_size) 99 if has_cs { 100 put64(b, 64, compressed_size) 101 } 102 for c, i in name16 { 103 put16(b, name_ofs + 2 * i, c) 104 } 105 copy(b[run_ofs:], runlist) 106 return b 107 } 108 109 @(private = "file") 110 file_name_value :: proc( 111 parent: File_Ref, 112 name: string, 113 ns: Name_Space, 114 attrs: File_Attributes = {}, 115 ) -> []byte { 116 name16 := utf16_of(name) 117 b := make([]byte, size_of(File_Name_Header) + 2 * len(name16), context.temp_allocator) 118 put64(b, 0, u64(parent)) 119 put32(b, 0x38, transmute(u32)attrs) 120 b[0x40] = u8(len(name16)) 121 b[0x41] = u8(ns) 122 for c, i in name16 { 123 put16(b, size_of(File_Name_Header) + 2 * i, c) 124 } 125 return b 126 } 127 128 @(private = "file") 129 standard_info_value :: proc(attrs: File_Attributes) -> []byte { 130 b := make([]byte, 72, context.temp_allocator) 131 put32(b, 0x20, transmute(u32)attrs) 132 return b 133 } 134 135 // A 1024-byte record with fixups applied the way the file system writes them: the 136 // last two bytes of each 512-byte block are replaced by the sequence number and the 137 // displaced bytes are saved in the update sequence array. 138 @(private = "file") 139 build_record :: proc( 140 attrs: [][]byte, 141 record_number: u32 = 100, 142 sequence: u16 = 7, 143 flags: Record_Flags = {.In_Use}, 144 base: File_Ref = 0, 145 link_count: u16 = 1, 146 ) -> []byte { 147 rec := make([]byte, 1024, context.temp_allocator) 148 put32(rec, 0, RECORD_MAGIC) 149 put16(rec, 4, 0x30) // update sequence array offset 150 put16(rec, 6, 3) // sequence number + one entry per 512-byte block 151 put16(rec, 0x10, sequence) 152 put16(rec, 0x12, link_count) 153 put16(rec, 0x14, 0x38) // first attribute 154 put16(rec, 0x16, transmute(u16)flags) 155 put64(rec, 0x20, u64(base)) 156 put32(rec, 0x2C, record_number) 157 158 off := 0x38 159 for a in attrs { 160 copy(rec[off:], a) 161 off += len(a) 162 } 163 put32(rec, off, u32(Attr_Type.End)) 164 off += 8 165 put32(rec, 0x18, u32(off)) 166 put32(rec, 0x1C, 1024) 167 assert(off <= 510, "test record too large: attributes overlap the first fixup") 168 169 // Give the protected bytes recognisable values so a test can see them restored. 170 put16(rec, 510, 0xABCD) 171 put16(rec, 1022, 0xEF01) 172 usn: u16 = 0x1234 173 put16(rec, 0x30, usn) 174 put16(rec, 0x32, rd16(rec, 510)) 175 put16(rec, 0x34, rd16(rec, 1022)) 176 put16(rec, 510, usn) 177 put16(rec, 1022, usn) 178 return rec 179 } 180 181 @(private = "file") 182 add :: proc(t: ^testing.T, m: ^Mft, rec: []byte) { 183 testing.expect_value(t, apply_fixups(rec), Error.None) 184 testing.expect_value( 185 t, 186 mft_add_record(m, record_header(rec).record_number, rec, &m.sinks[0]), 187 Error.None, 188 ) 189 mft_merge_sinks(m) 190 } 191 192 // ---- boot sector -------------------------------------------------------------------- 193 194 @(test) 195 test_boot_sector :: proc(t: ^testing.T) { 196 b := make([]byte, 512, context.temp_allocator) 197 copy(b[3:], "NTFS ") 198 put16(b, 0x0B, 512) 199 b[0x0D] = 8 200 put64(b, 0x28, 1_000_000) 201 put64(b, 0x30, 786_432) 202 put64(b, 0x38, 2) 203 b[0x40] = 0xF6 // 2^10 bytes per record 204 b[0x44] = 1 // one cluster per index block 205 put64(b, 0x48, 0xDEADBEEF) 206 b[0x1FE] = 0x55 207 b[0x1FF] = 0xAA 208 209 bs, err := parse_boot_sector(b) 210 testing.expect_value(t, err, Error.None) 211 testing.expect_value(t, bs.bytes_per_cluster, u32(4096)) 212 testing.expect_value(t, bs.record_size, u32(1024)) 213 testing.expect_value(t, bs.index_block_size, u32(4096)) 214 testing.expect_value(t, bs.mft_lcn, u64(786_432)) 215 testing.expect_value(t, bs.serial, u64(0xDEADBEEF)) 216 217 // 0xF4 encodes 2^12 sectors per cluster: the 2 MiB clusters Windows 10 allows. 218 b[0x0D] = 0xF4 219 bs, err = parse_boot_sector(b) 220 testing.expect_value(t, err, Error.None) 221 testing.expect_value(t, bs.bytes_per_cluster, u32(2 * 1024 * 1024)) 222 223 copy(b[3:], "MSDOS5.0") 224 _, err = parse_boot_sector(b) 225 testing.expect_value(t, err, Error.Not_Ntfs) 226 } 227 228 // ---- run lists ---------------------------------------------------------------------- 229 230 @(test) 231 test_runlist :: proc(t: ^testing.T) { 232 // 0x21: 1-byte length, 2-byte delta. 24 clusters at LCN 0x5634. 233 // 0x11: 1-byte length, 1-byte delta of -16. 48 clusters at LCN 0x5624. 234 // 0x01: 1-byte length, no delta. 16 sparse clusters. 235 data := []byte{0x21, 0x18, 0x34, 0x56, 0x11, 0x30, 0xF0, 0x01, 0x10, 0x00} 236 runs, err := decode_runlist(data, 0, context.temp_allocator) 237 testing.expect_value(t, err, Error.None) 238 testing.expect_value(t, len(runs), 3) 239 testing.expect_value(t, runs[0], Run{vcn = 0, lcn = 0x5634, length = 0x18}) 240 testing.expect_value(t, runs[1], Run{vcn = 0x18, lcn = 0x5624, length = 0x30}) 241 testing.expect_value(t, runs[2], Run{vcn = 0x48, lcn = 0, length = 0x10, sparse = true}) 242 testing.expect_value(t, runlist_clusters(runs), u64(0x58)) 243 244 _, err = decode_runlist([]byte{0x21, 0x18}, 0, context.temp_allocator) 245 testing.expect_value(t, err, Error.Bad_Runlist) 246 247 // A delta that would take the LCN negative is corruption. 248 _, err = decode_runlist([]byte{0x11, 0x01, 0xFF, 0x00}, 0, context.temp_allocator) 249 testing.expect_value(t, err, Error.Bad_Runlist) 250 } 251 252 // ---- fixups ------------------------------------------------------------------------- 253 254 @(test) 255 test_fixups :: proc(t: ^testing.T) { 256 rec := build_record({}) 257 testing.expect_value(t, rd16(rec, 510), u16(0x1234)) 258 259 torn := make([]byte, 1024, context.temp_allocator) 260 copy(torn, rec) 261 put16(torn, 1022, 0x9999) 262 testing.expect_value(t, apply_fixups(torn), Error.Bad_Record) 263 264 testing.expect_value(t, apply_fixups(rec), Error.None) 265 testing.expect_value(t, rd16(rec, 510), u16(0xABCD)) 266 testing.expect_value(t, rd16(rec, 1022), u16(0xEF01)) 267 268 put32(rec, 0, RECORD_MAGIC_BAD) 269 testing.expect_value(t, apply_fixups(rec), Error.Bad_Record) 270 } 271 272 // ---- records into entries ----------------------------------------------------------- 273 274 @(test) 275 test_resident_file :: proc(t: ^testing.T) { 276 root := make_ref(RECORD_ROOT, 5) 277 rec := build_record( 278 { 279 resident(.Standard_Information, standard_info_value({.Archive})), 280 resident(.File_Name, file_name_value(root, "HELLOW~1.TXT", .Dos)), 281 resident(.File_Name, file_name_value(root, "hello world.txt", .Win32)), 282 resident(.Data, transmute([]byte)string("hello, world!")), 283 }, 284 ) 285 m: Mft 286 testing.expect_value(t, mft_init(&m, 128, 4096, 1), Error.None) 287 defer mft_destroy(&m) 288 add(t, &m, rec) 289 290 e := m.entries[100] 291 testing.expect_value(t, e.name, "hello world.txt") 292 testing.expect_value(t, e.namespace, Name_Space.Win32) 293 testing.expect_value(t, e.parent, u32(RECORD_ROOT)) 294 testing.expect_value(t, e.parent_sequence, u16(5)) 295 testing.expect_value(t, e.sequence, u16(7)) 296 testing.expect_value(t, e.size, u64(13)) 297 testing.expect_value(t, e.allocated, u64(0)) 298 testing.expect(t, .In_Use in e.flags) 299 testing.expect(t, .Directory not_in e.flags) 300 testing.expect(t, .Archive in e.attributes) 301 testing.expect_value(t, len(m.links), 0) 302 testing.expect_value(t, m.stats.in_use, u64(1)) 303 } 304 305 @(test) 306 test_dos_name_first_is_replaced :: proc(t: ^testing.T) { 307 root := make_ref(RECORD_ROOT, 5) 308 rec := build_record( 309 { 310 resident(.File_Name, file_name_value(root, "LONGNA~1.TXT", .Dos)), 311 resident(.File_Name, file_name_value(root, "long name.txt", .Win32)), 312 }, 313 ) 314 m: Mft 315 testing.expect_value(t, mft_init(&m, 128, 4096, 1), Error.None) 316 defer mft_destroy(&m) 317 add(t, &m, rec) 318 testing.expect_value(t, m.entries[100].name, "long name.txt") 319 testing.expect_value(t, len(m.links), 0) 320 } 321 322 @(test) 323 test_nonresident_streams :: proc(t: ^testing.T) { 324 two_clusters := []byte{0x11, 0x02, 0x10, 0x00} 325 one_cluster := []byte{0x11, 0x01, 0x20, 0x00} 326 // 16 allocated clusters followed by 240 sparse ones: a 1 MiB compressed stream 327 // that squeezed into 64 KiB. 328 compressed := []byte{0x11, 0x10, 0x30, 0x01, 0xF0, 0x00} 329 rec := build_record( 330 { 331 resident(.File_Name, file_name_value(make_ref(64, 3), "big.bin", .Win32_And_Dos)), 332 nonresident(.Data, 8192, 5000, two_clusters), 333 nonresident(.Data, 4096, 100, one_cluster, name = "Zone.Identifier"), 334 nonresident( 335 .Data, 336 1 << 20, 337 1 << 20, 338 compressed, 339 name = "packed", 340 flags = {.Compressed}, 341 compressed_size = 65536, 342 ), 343 }, 344 ) 345 m: Mft 346 testing.expect_value(t, mft_init(&m, 128, 4096, 1), Error.None) 347 defer mft_destroy(&m) 348 add(t, &m, rec) 349 350 e := m.entries[100] 351 testing.expect_value(t, e.size, u64(5000)) // unnamed stream only 352 testing.expect_value(t, e.allocated, u64(8192 + 4096 + 65536)) // every stream, measured from run lists 353 testing.expect_value(t, e.parent, u32(64)) 354 testing.expect_value(t, e.parent_sequence, u16(3)) 355 } 356 357 @(test) 358 test_fragmented_extents_sum_their_own_clusters :: proc(t: ^testing.T) { 359 // Both extents repeat the header sizes (8192 allocated) but each maps one cluster. 360 rec := build_record( 361 { 362 resident(.File_Name, file_name_value(make_ref(RECORD_ROOT, 5), "frag.bin", .Win32)), 363 nonresident(.Data, 8192, 8000, []byte{0x11, 0x01, 0x10, 0x00}), 364 nonresident(.Data, 8192, 8000, []byte{0x11, 0x01, 0x40, 0x00}, lowest_vcn = 1), 365 }, 366 ) 367 m: Mft 368 testing.expect_value(t, mft_init(&m, 128, 4096, 1), Error.None) 369 defer mft_destroy(&m) 370 add(t, &m, rec) 371 testing.expect_value(t, m.entries[100].allocated, u64(8192)) 372 testing.expect_value(t, m.entries[100].size, u64(8000)) 373 } 374 375 @(test) 376 test_unallocated_runlist_counts_nothing :: proc(t: ^testing.T) { 377 // $BadClus:$Bad spans the whole volume with sparse runs and no sparse flag. 378 rec := build_record( 379 { 380 resident( 381 .File_Name, 382 file_name_value(make_ref(RECORD_ROOT, 5), "$BadClus", .Win32_And_Dos), 383 ), 384 nonresident(.Data, 0, 0, []byte{0x00}), 385 nonresident( 386 .Data, 387 1 << 40, 388 1 << 40, 389 []byte{0x04, 0x00, 0x00, 0x00, 0x10, 0x00}, 390 name = "$Bad", 391 ), 392 }, 393 record_number = RECORD_BAD_CLUS, 394 ) 395 m: Mft 396 testing.expect_value(t, mft_init(&m, 128, 4096, 1), Error.None) 397 defer mft_destroy(&m) 398 add(t, &m, rec) 399 testing.expect_value(t, m.entries[RECORD_BAD_CLUS].allocated, u64(0)) 400 } 401 402 @(test) 403 test_runlist_allocated_clusters :: proc(t: ^testing.T) { 404 clusters, ok := runlist_allocated_clusters([]byte{0x11, 0x10, 0x30, 0x01, 0xF0, 0x00}) 405 testing.expect(t, ok) 406 testing.expect_value(t, clusters, u64(16)) 407 _, ok = runlist_allocated_clusters([]byte{0x21, 0x18}) 408 testing.expect(t, !ok) 409 } 410 411 @(test) 412 test_extension_record_and_hard_links :: proc(t: ^testing.T) { 413 base := build_record( 414 {resident(.File_Name, file_name_value(make_ref(RECORD_ROOT, 5), "a.txt", .Win32))}, 415 record_number = 100, 416 sequence = 9, 417 link_count = 2, 418 ) 419 ext := build_record( 420 { 421 resident(.File_Name, file_name_value(make_ref(70, 2), "b.txt", .Win32)), 422 nonresident(.Data, 4096, 10, []byte{0x11, 0x01, 0x20, 0x00}), 423 }, 424 record_number = 101, 425 base = make_ref(100, 9), 426 ) 427 m: Mft 428 testing.expect_value(t, mft_init(&m, 128, 4096, 1), Error.None) 429 defer mft_destroy(&m) 430 431 // The extension arrives before its base, as it can on disk. 432 add(t, &m, ext) 433 add(t, &m, base) 434 435 e := m.entries[100] 436 testing.expect_value(t, e.name, "b.txt") // first proper name seen wins 437 testing.expect_value(t, e.parent, u32(70)) 438 testing.expect_value(t, e.allocated, u64(4096)) 439 testing.expect_value(t, e.size, u64(10)) 440 testing.expect_value(t, e.sequence, u16(9)) 441 testing.expect_value(t, e.link_count, u16(2)) 442 testing.expect_value(t, len(m.links), 1) 443 testing.expect_value(t, m.links[0].record, u32(100)) 444 testing.expect_value(t, m.links[0].parent, u32(RECORD_ROOT)) 445 testing.expect_value(t, m.links[0].name, "a.txt") 446 447 // The extension slot itself is not a file. 448 testing.expect_value(t, m.entries[101].name, "") 449 testing.expect_value(t, m.stats.in_use, u64(1)) 450 } 451 452 @(test) 453 test_free_record_is_ignored :: proc(t: ^testing.T) { 454 rec := build_record( 455 {resident(.File_Name, file_name_value(make_ref(RECORD_ROOT, 5), "deleted.txt", .Win32))}, 456 flags = {}, 457 ) 458 m: Mft 459 testing.expect_value(t, mft_init(&m, 128, 4096, 1), Error.None) 460 defer mft_destroy(&m) 461 add(t, &m, rec) 462 testing.expect_value(t, m.entries[100].name, "") 463 testing.expect_value(t, m.stats.in_use, u64(0)) 464 } 465 466 // ---- paths -------------------------------------------------------------------------- 467 468 @(test) 469 test_path :: proc(t: ^testing.T) { 470 m: Mft 471 testing.expect_value(t, mft_init(&m, 128, 4096, 1), Error.None) 472 defer mft_destroy(&m) 473 474 root := make_ref(RECORD_ROOT, 5) 475 add( 476 t, 477 &m, 478 build_record( 479 {resident(.File_Name, file_name_value(root, ".", .Win32))}, 480 record_number = RECORD_ROOT, 481 sequence = 5, 482 flags = {.In_Use, .Directory}, 483 ), 484 ) 485 add( 486 t, 487 &m, 488 build_record( 489 {resident(.File_Name, file_name_value(root, "Windows", .Win32))}, 490 record_number = 64, 491 sequence = 1, 492 flags = {.In_Use, .Directory}, 493 ), 494 ) 495 add( 496 t, 497 &m, 498 build_record( 499 {resident(.File_Name, file_name_value(make_ref(64, 1), "explorer.exe", .Win32))}, 500 record_number = 65, 501 sequence = 1, 502 ), 503 ) 504 // Parent sequence 99 does not match record 64's sequence 1: the directory this 505 // file was in has been deleted and its record reused. 506 add( 507 t, 508 &m, 509 build_record( 510 {resident(.File_Name, file_name_value(make_ref(64, 99), "stale.tmp", .Win32))}, 511 record_number = 66, 512 sequence = 1, 513 ), 514 ) 515 516 testing.expect_value(t, mft_path(&m, 65, context.temp_allocator), `\Windows\explorer.exe`) 517 testing.expect_value(t, mft_path(&m, 64, context.temp_allocator), `\Windows`) 518 testing.expect_value(t, mft_path(&m, RECORD_ROOT, context.temp_allocator), `\`) 519 testing.expect_value(t, mft_path(&m, 66, context.temp_allocator), `<orphan>\stale.tmp`) 520 } 521 522 // ---- bitmap ------------------------------------------------------------------------- 523 524 @(test) 525 test_count_set_bits :: proc(t: ^testing.T) { 526 testing.expect_value(t, count_set_bits(nil), u64(0)) 527 testing.expect_value(t, count_set_bits([]byte{0xFF}), u64(8)) 528 529 // Nine bytes exercises both the eight-byte chunk loop and the trailing bytes. 530 nine := []byte{0xFF, 0x00, 0x0F, 0xF0, 0x01, 0x80, 0xAA, 0x55, 0x03} 531 testing.expect_value(t, count_set_bits(nine), u64(28)) 532 533 all := make([]byte, 64, context.temp_allocator) 534 for i in 0 ..< len(all) { 535 all[i] = 0xFF 536 } 537 testing.expect_value(t, count_set_bits(all), u64(512)) 538 } 539 540 // ---- resident data ------------------------------------------------------------------ 541 542 @(test) 543 test_resident_file_costs_no_clusters :: proc(t: ^testing.T) { 544 rec := build_record( 545 { 546 resident(.File_Name, file_name_value(make_ref(RECORD_ROOT, 5), "tiny.txt", .Win32)), 547 resident(.Data, transmute([]byte)string("hello")), 548 }, 549 ) 550 m: Mft 551 testing.expect_value(t, mft_init(&m, 128, 4096, 1), Error.None) 552 defer mft_destroy(&m) 553 add(t, &m, rec) 554 555 // The bytes live inside the MFT record, so the file itself allocates nothing and 556 // the stats record what the directory totals will therefore miss. 557 testing.expect_value(t, m.entries[100].size, u64(5)) 558 testing.expect_value(t, m.entries[100].allocated, u64(0)) 559 testing.expect_value(t, m.stats.resident_files, u64(1)) 560 testing.expect_value(t, m.stats.resident_bytes, u64(5)) 561 }