draugr~ single ELF file analysis tool |
| git clone https://github.com/djnnvx/draugr.git |
| log | files | refs | archive | README |
analysis_test.odin (22949B)
1 package analysis 2 3 import "core:fmt" 4 import "core:os" 5 import "core:strings" 6 import "core:testing" 7 8 import "../elf" 9 10 // Layout constants - built by doubling so the widths are provable, not counted 11 12 SP1 :: " " 13 SP2 :: SP1 + SP1 14 SP3 :: SP2 + SP1 15 SP4 :: SP2 + SP2 16 SP5 :: SP4 + SP1 17 SP6 :: SP4 + SP2 18 SP7 :: SP4 + SP3 19 SP8 :: SP4 + SP4 20 SP9 :: SP8 + SP1 21 SP10 :: SP8 + SP2 22 SP12 :: SP8 + SP4 23 SP13 :: SP8 + SP5 24 SP15 :: SP8 + SP7 25 SP16 :: SP8 + SP8 26 SP17 :: SP16 + SP1 27 SP32 :: SP16 + SP16 28 SP38 :: SP32 + SP6 29 SP47 :: SP32 + SP15 30 31 DASH1 :: "-" 32 DASH2 :: DASH1 + DASH1 33 DASH4 :: DASH2 + DASH2 34 DASH8 :: DASH4 + DASH4 35 DASH16 :: DASH8 + DASH8 36 DASH25 :: DASH16 + DASH8 + DASH1 37 DASH32 :: DASH16 + DASH16 38 DASH40 :: DASH32 + DASH8 39 DASH42 :: DASH40 + DASH2 40 DASH49 :: DASH32 + DASH16 + DASH1 41 DASH60 :: DASH32 + DASH16 + DASH8 + DASH4 42 DASH69 :: DASH60 + DASH8 + DASH1 43 44 HASH1 :: "#" 45 HASH2 :: HASH1 + HASH1 46 HASH4 :: HASH2 + HASH2 47 HASH8 :: HASH4 + HASH4 48 HASH16 :: HASH8 + HASH8 49 HASH25 :: HASH16 + HASH8 + HASH1 50 HASH50 :: HASH25 + HASH25 51 52 TOLERANCE :: 1e-9 53 54 tmp_path :: proc(name: string) -> string { 55 dir, err := os.temp_directory(context.allocator) 56 if err != nil { 57 return fmt.aprintf("draugr_test_%s.out", name) 58 } 59 defer delete(dir) 60 return fmt.aprintf("%s/draugr_test_%s.out", dir, name) 61 } 62 63 drop :: proc(path: string) { 64 os.remove(path) 65 delete(path) 66 } 67 68 read_all :: proc(t: ^testing.T, path: string) -> string { 69 data, err := os.read_entire_file_from_path(path, context.allocator) 70 testing.expectf(t, err == nil, "cannot read back %s: %v", path, err) 71 return string(data) 72 } 73 74 expect_near :: proc(t: ^testing.T, got, want: f64, loc := #caller_location) { 75 testing.expectf(t, abs(got - want) < TOLERANCE, "expected %.12f, got %.12f", want, got, loc = loc) 76 } 77 78 new_info :: proc() -> elf.ELF_Info { 79 info: elf.ELF_Info 80 info.program_hdrs = make([dynamic]elf.Program_Header) 81 info.section_hdrs = make([dynamic]elf.Section_Header) 82 info.symbols = make([dynamic]elf.Symbol) 83 info.dyn_symbols = make([dynamic]elf.Symbol) 84 return info 85 } 86 87 // offset 1 -> ".text", offset 7 -> ".data" 88 SHSTRTAB := [?]u8{0, '.', 't', 'e', 'x', 't', 0, '.', 'd', 'a', 't', 'a', 0} 89 90 @(test) 91 test_open_out_empty_path_returns_stdout :: proc(t: ^testing.T) { 92 f, err := open_out("") 93 94 testing.expect_value(t, err, "") 95 testing.expect(t, f == os.stdout, "empty out_path must return os.stdout") 96 } 97 98 @(test) 99 test_close_out_leaves_stdout_open :: proc(t: ^testing.T) { 100 f, err := open_out("") 101 testing.expect_value(t, err, "") 102 103 close_out(f, "") 104 105 fi, ferr := os.fstat(os.stdout, context.allocator) 106 testing.expectf(t, ferr == nil, "close_out closed stdout: %v", ferr) 107 if ferr == nil { 108 os.file_info_delete(fi, context.allocator) 109 } 110 } 111 112 @(test) 113 test_open_out_unwritable_path_errors :: proc(t: ^testing.T) { 114 f, err := open_out("/draugr_no_such_dir_4c1f/out.txt") 115 116 testing.expect(t, f == nil, "failed open must return a nil handle") 117 testing.expect(t, err != "", "failed open must return an error string") 118 testing.expect(t, strings.contains(err, "cannot create"), "error must describe the failure") 119 } 120 121 @(test) 122 test_open_out_creates_file_and_close_out_persists_it :: proc(t: ^testing.T) { 123 path := tmp_path("open_close") 124 defer drop(path) 125 126 f, err := open_out(path) 127 testing.expect_value(t, err, "") 128 testing.expect(t, f != nil, "real out_path must return a handle") 129 testing.expect(t, f != os.stdout, "real out_path must not return stdout") 130 131 fmt.fprintf(f, "draugr") 132 close_out(f, path) 133 134 testing.expect(t, os.exists(path), "close_out must leave the file on disk") 135 136 got := read_all(t, path) 137 defer delete(got) 138 testing.expect_value(t, got, "draugr") 139 } 140 141 @(test) 142 test_hexdump_empty_data :: proc(t: ^testing.T) { 143 path := tmp_path("hexdump_empty") 144 defer drop(path) 145 146 testing.expect_value(t, hexdump([]u8{}, 0, path), "") 147 148 got := read_all(t, path) 149 defer delete(got) 150 testing.expect_value(t, got, "") 151 } 152 153 @(test) 154 test_hexdump_full_line :: proc(t: ^testing.T) { 155 path := tmp_path("hexdump_full_line") 156 defer drop(path) 157 158 data := []u8{ 159 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 160 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 161 } 162 testing.expect_value(t, hexdump(data, 0, path), "") 163 164 got := read_all(t, path) 165 defer delete(got) 166 testing.expect_value(t, got, 167 "00000000 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|\n") 168 } 169 170 @(test) 171 test_hexdump_printable_ascii_column :: proc(t: ^testing.T) { 172 path := tmp_path("hexdump_printable") 173 defer drop(path) 174 175 msg := "Hello, World!!!!" 176 testing.expect_value(t, hexdump(transmute([]u8)msg, 0, path), "") 177 178 got := read_all(t, path) 179 defer delete(got) 180 testing.expect_value(t, got, 181 "00000000 48 65 6c 6c 6f 2c 20 57 6f 72 6c 64 21 21 21 21 |Hello, World!!!!|\n") 182 } 183 184 @(test) 185 test_hexdump_short_trailing_line_is_padded :: proc(t: ^testing.T) { 186 path := tmp_path("hexdump_short") 187 defer drop(path) 188 189 data := []u8{'A', 'B', 'C', 'D'} 190 testing.expect_value(t, hexdump(data, 0, path), "") 191 192 got := read_all(t, path) 193 defer delete(got) 194 testing.expect_value(t, got, "00000000 41 42 43 44" + SP38 + "|ABCD" + SP12 + "|\n") 195 } 196 197 @(test) 198 test_hexdump_non_zero_offset :: proc(t: ^testing.T) { 199 path := tmp_path("hexdump_offset") 200 defer drop(path) 201 202 data := []u8{0xff} 203 testing.expect_value(t, hexdump(data, 0x1000, path), "") 204 205 got := read_all(t, path) 206 defer delete(got) 207 testing.expect_value(t, got, "00001000 ff" + SP47 + "|." + SP15 + "|\n") 208 } 209 210 @(test) 211 test_hexdump_offset_advances_per_line :: proc(t: ^testing.T) { 212 path := tmp_path("hexdump_multiline") 213 defer drop(path) 214 215 data := make([]u8, 20) 216 defer delete(data) 217 for i in 0 ..< 16 { 218 data[i] = 'A' 219 } 220 for i in 16 ..< 20 { 221 data[i] = 'B' 222 } 223 224 testing.expect_value(t, hexdump(data, 0x20, path), "") 225 226 got := read_all(t, path) 227 defer delete(got) 228 testing.expect_value(t, got, 229 "00000020 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 |AAAAAAAAAAAAAAAA|\n" + 230 "00000030 42 42 42 42" + SP38 + "|BBBB" + SP12 + "|\n") 231 } 232 233 @(test) 234 test_hexdump_non_printable_bytes_are_dots :: proc(t: ^testing.T) { 235 path := tmp_path("hexdump_nonprintable") 236 defer drop(path) 237 238 data := []u8{0x00, 0x0a, 0x1f, 0x7f, 0x80, 0xff} 239 testing.expect_value(t, hexdump(data, 0, path), "") 240 241 got := read_all(t, path) 242 defer delete(got) 243 testing.expect_value(t, got, "00000000 00 0a 1f 7f 80 ff" + SP32 + "|......" + SP10 + "|\n") 244 } 245 246 @(test) 247 test_hexdump_printable_boundaries_are_inclusive :: proc(t: ^testing.T) { 248 path := tmp_path("hexdump_boundaries") 249 defer drop(path) 250 251 data := []u8{31, 32, 126, 127} 252 testing.expect_value(t, hexdump(data, 0, path), "") 253 254 got := read_all(t, path) 255 defer delete(got) 256 testing.expect_value(t, got, "00000000 1f 20 7e 7f" + SP38 + "|. ~." + SP12 + "|\n") 257 } 258 259 @(test) 260 test_calculate_entropy_empty_is_zero :: proc(t: ^testing.T) { 261 expect_near(t, calculate_entropy([]u8{}), 0.0) 262 } 263 264 @(test) 265 test_calculate_entropy_single_value_is_zero :: proc(t: ^testing.T) { 266 expect_near(t, calculate_entropy([]u8{0x41, 0x41, 0x41, 0x41, 0x41}), 0.0) 267 } 268 269 @(test) 270 test_calculate_entropy_two_values_is_one_bit :: proc(t: ^testing.T) { 271 expect_near(t, calculate_entropy([]u8{0x00, 0xff}), 1.0) 272 expect_near(t, calculate_entropy([]u8{0x00, 0x00, 0xff, 0xff}), 1.0) 273 } 274 275 @(test) 276 test_calculate_entropy_four_values_is_two_bits :: proc(t: ^testing.T) { 277 data := []u8{0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x02, 0x03} 278 expect_near(t, calculate_entropy(data), 2.0) 279 } 280 281 // Pins the logarithm base: with the natural log this would be 5.545. 282 @(test) 283 test_calculate_entropy_all_256_values_is_eight_bits :: proc(t: ^testing.T) { 284 data := make([]u8, 256) 285 defer delete(data) 286 for i in 0 ..< 256 { 287 data[i] = u8(i) 288 } 289 290 expect_near(t, calculate_entropy(data), 8.0) 291 } 292 293 @(test) 294 test_calculate_entropy_lopsided_is_between_zero_and_one :: proc(t: ^testing.T) { 295 data := []u8{0, 0, 0, 0, 0, 0, 0, 1} 296 297 entropy := calculate_entropy(data) 298 testing.expectf(t, entropy > 0.0, "7:1 split should be positive, got %.12f", entropy) 299 testing.expectf(t, entropy < 1.0, "7:1 split should be under 1 bit, got %.12f", entropy) 300 expect_near(t, entropy, 0.543564443) 301 } 302 303 ENTROPY_HEAD :: "Entropy Analysis (block size: 256 bytes)\n" + 304 "Offset" + SP7 + "Size" + SP3 + "Entropy\n" + 305 DASH40 + "\n" 306 307 @(test) 308 test_entropy_output_empty_data :: proc(t: ^testing.T) { 309 path := tmp_path("entropy_empty") 310 defer drop(path) 311 312 testing.expect_value(t, entropy_output([]u8{}, path), "") 313 314 got := read_all(t, path) 315 defer delete(got) 316 testing.expect_value(t, got, 317 ENTROPY_HEAD + 318 "\nOverall entropy: 0.0000 bits/byte\n" + 319 "Classification: Low entropy (text/code)\n") 320 } 321 322 @(test) 323 test_entropy_output_block_table_and_low_class :: proc(t: ^testing.T) { 324 path := tmp_path("entropy_blocks") 325 defer drop(path) 326 327 data := make([]u8, 600) 328 defer delete(data) 329 330 testing.expect_value(t, entropy_output(data, path), "") 331 332 got := read_all(t, path) 333 defer delete(got) 334 testing.expect_value(t, got, 335 ENTROPY_HEAD + 336 "0x00000000" + SP3 + "256" + SP4 + "0.0000\n" + 337 "0x00000100" + SP3 + "256" + SP4 + "0.0000\n" + 338 "0x00000200" + SP3 + "88" + SP5 + "0.0000\n" + 339 "\nOverall entropy: 0.0000 bits/byte\n" + 340 "Classification: Low entropy (text/code)\n") 341 } 342 343 @(test) 344 test_entropy_output_medium_class :: proc(t: ^testing.T) { 345 path := tmp_path("entropy_medium") 346 defer drop(path) 347 348 data := make([]u8, 32) 349 defer delete(data) 350 for i in 0 ..< 32 { 351 data[i] = u8(i) 352 } 353 354 testing.expect_value(t, entropy_output(data, path), "") 355 356 got := read_all(t, path) 357 defer delete(got) 358 testing.expect_value(t, got, 359 ENTROPY_HEAD + 360 "0x00000000" + SP3 + "32" + SP5 + "5.0000\n" + 361 "\nOverall entropy: 5.0000 bits/byte\n" + 362 "Classification: Medium entropy (mixed)\n") 363 } 364 365 @(test) 366 test_entropy_output_high_class :: proc(t: ^testing.T) { 367 path := tmp_path("entropy_high") 368 defer drop(path) 369 370 data := make([]u8, 256) 371 defer delete(data) 372 for i in 0 ..< 256 { 373 data[i] = u8(i) 374 } 375 376 testing.expect_value(t, entropy_output(data, path), "") 377 378 got := read_all(t, path) 379 defer delete(got) 380 testing.expect_value(t, got, 381 ENTROPY_HEAD + 382 "0x00000000" + SP3 + "256" + SP4 + "8.0000\n" + 383 "\nOverall entropy: 8.0000 bits/byte\n" + 384 "Classification: High entropy (compressed/encrypted)\n") 385 } 386 387 // Brackets the 4.0 threshold from below without sitting on it. 388 @(test) 389 test_entropy_output_three_bits_is_low :: proc(t: ^testing.T) { 390 path := tmp_path("entropy_three_bits") 391 defer drop(path) 392 393 data := make([]u8, 8) 394 defer delete(data) 395 for i in 0 ..< 8 { 396 data[i] = u8(i) 397 } 398 399 testing.expect_value(t, entropy_output(data, path), "") 400 expect_near(t, calculate_entropy(data), 3.0) 401 402 got := read_all(t, path) 403 defer delete(got) 404 testing.expect(t, strings.contains(got, "Classification: Low entropy (text/code)\n"), 405 "3.0 bits must classify as low") 406 } 407 408 // Brackets the 7.0 threshold from below without sitting on it. 409 @(test) 410 test_entropy_output_six_bits_is_medium :: proc(t: ^testing.T) { 411 path := tmp_path("entropy_six_bits") 412 defer drop(path) 413 414 data := make([]u8, 64) 415 defer delete(data) 416 for i in 0 ..< 64 { 417 data[i] = u8(i) 418 } 419 420 testing.expect_value(t, entropy_output(data, path), "") 421 expect_near(t, calculate_entropy(data), 6.0) 422 423 got := read_all(t, path) 424 defer delete(got) 425 testing.expect(t, strings.contains(got, "Classification: Medium entropy (mixed)\n"), 426 "6.0 bits must classify as medium") 427 } 428 429 @(test) 430 test_entropy_output_reports_open_failure :: proc(t: ^testing.T) { 431 err := entropy_output([]u8{1, 2, 3}, "/draugr_no_such_dir_4c1f/entropy.txt") 432 433 testing.expect(t, err != "", "unwritable out_path must surface an error") 434 } 435 436 @(test) 437 test_code_output_no_sections :: proc(t: ^testing.T) { 438 path := tmp_path("code_none") 439 defer drop(path) 440 441 info := new_info() 442 defer elf.destroy(&info) 443 444 testing.expect_value(t, code_output([]u8{}, &info, path), "") 445 446 got := read_all(t, path) 447 defer delete(got) 448 testing.expect(t, strings.contains(got, "no section headers"), "must fall back to segments") 449 testing.expect(t, strings.contains(got, "no PT_GNU_EH_FRAME"), "must report missing eh_frame_hdr") 450 } 451 452 @(test) 453 test_code_output_lists_only_executable_sections :: proc(t: ^testing.T) { 454 path := tmp_path("code_exec") 455 defer drop(path) 456 457 info := new_info() 458 defer elf.destroy(&info) 459 info.shstrtab = SHSTRTAB[:] 460 461 append(&info.section_hdrs, elf.Section_Header{type = elf.SHT_NULL}) 462 append(&info.section_hdrs, elf.Section_Header{ 463 name = 1, type = elf.SHT_PROGBITS, flags = elf.SHF_EXECINSTR, 464 addr = 0x401000, offset = 0, size = 4, 465 }) 466 append(&info.section_hdrs, elf.Section_Header{ 467 name = 7, type = elf.SHT_PROGBITS, flags = 0, 468 addr = 0x402000, offset = 4, size = 4, 469 }) 470 471 data := []u8{0x90, 0x90, 0xc3, 0x00, 0xde, 0xad, 0xbe, 0xef} 472 testing.expect_value(t, code_output(data, &info, path), "") 473 474 got := read_all(t, path) 475 defer delete(got) 476 testing.expect(t, strings.contains(got, ".text"), ".text must be listed") 477 testing.expect(t, !strings.contains(got, ".data"), "non-executable sections must be skipped") 478 } 479 480 @(test) 481 test_code_output_skips_nobits_and_out_of_file_sections :: proc(t: ^testing.T) { 482 path := tmp_path("code_nobits") 483 defer drop(path) 484 485 info := new_info() 486 defer elf.destroy(&info) 487 info.shstrtab = SHSTRTAB[:] 488 489 append(&info.section_hdrs, elf.Section_Header{ 490 name = 1, type = elf.SHT_NOBITS, flags = elf.SHF_EXECINSTR, size = 100, 491 }) 492 append(&info.section_hdrs, elf.Section_Header{ 493 name = 7, type = elf.SHT_PROGBITS, flags = elf.SHF_EXECINSTR, 494 offset = 1000, size = 4, 495 }) 496 497 data := []u8{1, 2, 3, 4} 498 testing.expect_value(t, code_output(data, &info, path), "") 499 500 got := read_all(t, path) 501 defer delete(got) 502 testing.expect(t, !strings.contains(got, ".text"), "SHT_NOBITS must be skipped") 503 testing.expect(t, strings.contains(got, ".data"), "out-of-file section is still listed") 504 testing.expect(t, strings.contains(got, "0.00"), "unreadable section entropy is zero") 505 } 506 507 @(test) 508 test_code_output_flags_entry_outside_pt_load :: proc(t: ^testing.T) { 509 path := tmp_path("code_entry_out") 510 defer drop(path) 511 512 info := new_info() 513 defer elf.destroy(&info) 514 info.header.entry = 0xdeadbeef 515 append(&info.program_hdrs, elf.Program_Header{ 516 type = elf.PT_LOAD, vaddr = 0x400000, memsz = 0x1000, flags = elf.PF_R | elf.PF_X, 517 }) 518 519 testing.expect_value(t, code_output([]u8{}, &info, path), "") 520 521 got := read_all(t, path) 522 defer delete(got) 523 testing.expect(t, strings.contains(got, "outside every PT_LOAD"), "must flag unmapped entry") 524 } 525 526 @(test) 527 test_code_output_flags_entry_in_non_executable_segment :: proc(t: ^testing.T) { 528 path := tmp_path("code_entry_nx") 529 defer drop(path) 530 531 info := new_info() 532 defer elf.destroy(&info) 533 info.header.entry = 0x400100 534 append(&info.program_hdrs, elf.Program_Header{ 535 type = elf.PT_LOAD, vaddr = 0x400000, memsz = 0x1000, flags = elf.PF_R | elf.PF_W, 536 }) 537 538 testing.expect_value(t, code_output([]u8{}, &info, path), "") 539 540 got := read_all(t, path) 541 defer delete(got) 542 testing.expect(t, strings.contains(got, "non-executable segment"), "must flag NX entry") 543 } 544 545 MAP_HEAD :: "ELF Memory Map\n" + 546 "==============\n\n" + 547 "File Layout:\n" + 548 DASH42 + "\n" + 549 "0x00000000: ELF Header (64 bytes)\n" 550 551 MAP_SEG_HEAD :: "Segments:\n" + 552 "Type" + SP10 + "Offset" + SP5 + "VirtAddr" + SP3 + 553 "FileSize" + SP3 + "MemSize" + SP4 + "Flags\n" + 554 DASH69 + "\n" 555 556 MAP_SEC_HEAD :: "Sections:\n" + 557 "Name" + SP13 + "Type" + SP7 + "Addr" + SP7 + "Offset" + SP5 + "Size\n" + 558 DASH60 + "\n" 559 560 @(test) 561 test_map_output_no_segments_no_sections :: proc(t: ^testing.T) { 562 path := tmp_path("map_empty") 563 defer drop(path) 564 565 info := new_info() 566 defer elf.destroy(&info) 567 568 data := make([]u8, 32) 569 defer delete(data) 570 571 testing.expect_value(t, map_output(data, &info, path), "") 572 573 got := read_all(t, path) 574 defer delete(got) 575 testing.expect_value(t, got, 576 MAP_HEAD + 577 "0x00000020: EOF (file size: 32 bytes)\n\n" + 578 MAP_SEG_HEAD + "\n" + 579 MAP_SEC_HEAD) 580 } 581 582 @(test) 583 test_map_output_empty_data_no_segments :: proc(t: ^testing.T) { 584 path := tmp_path("map_empty_data") 585 defer drop(path) 586 587 info := new_info() 588 defer elf.destroy(&info) 589 590 testing.expect_value(t, map_output([]u8{}, &info, path), "") 591 592 got := read_all(t, path) 593 defer delete(got) 594 testing.expect(t, strings.contains(got, "0x00000000: EOF (file size: 0 bytes)\n"), 595 "empty file must still report an EOF marker") 596 } 597 598 @(test) 599 test_map_output_header_table_offsets :: proc(t: ^testing.T) { 600 path := tmp_path("map_headers") 601 defer drop(path) 602 603 info := new_info() 604 defer elf.destroy(&info) 605 info.header.phoff = 64 606 info.header.phnum = 2 607 info.header.phentsize = 56 608 info.header.shoff = 1000 609 info.header.shnum = 3 610 info.header.shentsize = 64 611 for _ in 0 ..< 2 { append(&info.program_hdrs, elf.Program_Header{}) } 612 for _ in 0 ..< 3 { append(&info.section_hdrs, elf.Section_Header{}) } 613 614 data := make([]u8, 2048) 615 defer delete(data) 616 617 testing.expect_value(t, map_output(data, &info, path), "") 618 619 got := read_all(t, path) 620 defer delete(got) 621 testing.expect(t, strings.contains(got, 622 "0x00000040: Program Headers\n" + 623 "0x000000b0: Program Headers End\n" + 624 "0x000003e8: Section Headers\n" + 625 "0x000004a8: Section Headers End\n"), 626 "header table extents must be derived from phoff/phnum/phentsize and shoff/shnum/shentsize") 627 } 628 629 @(test) 630 test_map_output_segment_row_and_bar :: proc(t: ^testing.T) { 631 path := tmp_path("map_segments") 632 defer drop(path) 633 634 info := new_info() 635 defer elf.destroy(&info) 636 append(&info.program_hdrs, elf.Program_Header{ 637 type = elf.PT_LOAD, 638 flags = elf.PF_R | elf.PF_X, 639 offset = 0, 640 vaddr = 0x400000, 641 filesz = 50, 642 memsz = 50, 643 }) 644 append(&info.program_hdrs, elf.Program_Header{ 645 type = elf.PT_LOAD, 646 flags = elf.PF_R | elf.PF_W, 647 offset = 50, 648 vaddr = 0x600000, 649 filesz = 50, 650 memsz = 50, 651 }) 652 653 data := make([]u8, 100) 654 defer delete(data) 655 656 testing.expect_value(t, map_output(data, &info, path), "") 657 658 got := read_all(t, path) 659 defer delete(got) 660 testing.expect(t, strings.contains(got, 661 "LOAD" + SP10 + "0x00000000 0x00400000 0x00000032 0x00000032 R X\n" + 662 " [|" + HASH25 + DASH25 + "|]\n" + 663 "LOAD" + SP10 + "0x00000032 0x00600000 0x00000032 0x00000032 RW \n" + 664 " [|" + DASH25 + HASH25 + "|]\n"), 665 "segment rows and coverage bars must match the file layout") 666 } 667 668 @(test) 669 test_map_output_bar_saturates_on_tiny_file :: proc(t: ^testing.T) { 670 path := tmp_path("map_tiny") 671 defer drop(path) 672 673 info := new_info() 674 defer elf.destroy(&info) 675 append(&info.program_hdrs, elf.Program_Header{type = elf.PT_LOAD, filesz = 1, memsz = 1}) 676 677 data := []u8{0x7f} 678 testing.expect_value(t, map_output(data, &info, path), "") 679 680 got := read_all(t, path) 681 defer delete(got) 682 testing.expect(t, strings.contains(got, " [|" + HASH50 + "|]\n"), 683 "a segment covering the whole file must fill the bar") 684 } 685 686 @(test) 687 test_map_output_skips_bar_for_pt_null :: proc(t: ^testing.T) { 688 path := tmp_path("map_pt_null") 689 defer drop(path) 690 691 info := new_info() 692 defer elf.destroy(&info) 693 append(&info.program_hdrs, elf.Program_Header{type = elf.PT_NULL, filesz = 8, memsz = 8}) 694 695 data := make([]u8, 16) 696 defer delete(data) 697 698 testing.expect_value(t, map_output(data, &info, path), "") 699 700 got := read_all(t, path) 701 defer delete(got) 702 testing.expect(t, strings.contains(got, "NULL"), "PT_NULL row must still be listed") 703 testing.expect(t, !strings.contains(got, " [|"), "PT_NULL must not draw a bar") 704 } 705 706 @(test) 707 test_map_output_empty_data_draws_no_bar :: proc(t: ^testing.T) { 708 path := tmp_path("map_empty_bar") 709 defer drop(path) 710 711 info := new_info() 712 defer elf.destroy(&info) 713 append(&info.program_hdrs, elf.Program_Header{type = elf.PT_LOAD, filesz = 1, memsz = 1}) 714 715 testing.expect_value(t, map_output([]u8{}, &info, path), "") 716 717 got := read_all(t, path) 718 defer delete(got) 719 testing.expect(t, !strings.contains(got, " [|"), "empty data must not draw a bar") 720 } 721 722 @(test) 723 test_map_output_section_rows :: proc(t: ^testing.T) { 724 path := tmp_path("map_sections") 725 defer drop(path) 726 727 info := new_info() 728 defer elf.destroy(&info) 729 info.shstrtab = SHSTRTAB[:] 730 append(&info.section_hdrs, elf.Section_Header{ 731 name = 1, 732 type = elf.SHT_PROGBITS, 733 addr = 0x401000, 734 offset = 0, 735 size = 4, 736 }) 737 738 data := make([]u8, 8) 739 defer delete(data) 740 741 testing.expect_value(t, map_output(data, &info, path), "") 742 743 got := read_all(t, path) 744 defer delete(got) 745 testing.expect(t, strings.contains(got, 746 ".text" + SP12 + "PROGBITS" + SP3 + "0x00401000 0x00000000 0x4\n"), 747 "section row must render name, type, addr, offset and size") 748 } 749 750 @(test) 751 test_map_output_reports_open_failure :: proc(t: ^testing.T) { 752 info := new_info() 753 defer elf.destroy(&info) 754 755 err := map_output([]u8{}, &info, "/draugr_no_such_dir_4c1f/map.txt") 756 757 testing.expect(t, err != "", "unwritable out_path must surface an error") 758 } 759 760 @(test) 761 test_bin_diff_missing_reference :: proc(t: ^testing.T) { 762 info := new_info() 763 defer elf.destroy(&info) 764 765 err := bin_diff_output([]u8{}, &info, "/draugr_no_such_file_4c1f", "") 766 testing.expect(t, strings.contains(err, "failed to read"), "missing reference must surface an error") 767 } 768 769 @(test) 770 test_bin_diff_non_elf_reference :: proc(t: ^testing.T) { 771 path := tmp_path("bindiff_notelf") 772 defer drop(path) 773 _ = os.write_entire_file(path, []u8{'n', 'o', 'p', 'e'}) 774 775 info := new_info() 776 defer elf.destroy(&info) 777 778 err := bin_diff_output([]u8{}, &info, path, "") 779 testing.expect(t, strings.contains(err, "not an ELF"), "non-ELF reference must surface an error") 780 } 781 782 @(test) 783 test_bin_diff_reports_section_and_dependency_changes :: proc(t: ^testing.T) { 784 ref_path := tmp_path("bindiff_ref") 785 defer drop(ref_path) 786 out_path := tmp_path("bindiff_out") 787 defer drop(out_path) 788 789 ref := elf.build_dynamic_elf(true, true, elf.DEFAULT_DYN_ENTRIES[:]) 790 defer delete(ref) 791 _ = os.write_entire_file(ref_path, ref) 792 793 entries := [][2]u64{ 794 {elf.DT_NEEDED, elf.DYN_OFF_NEEDED_1}, 795 {elf.DT_STRTAB, elf.DYN_BASE + 0x200}, 796 {elf.DT_STRSZ, u64(len(elf.DYN_STR))}, 797 {elf.DT_NULL, 0}, 798 } 799 mine := elf.build_dynamic_elf(true, true, entries[:]) 800 defer delete(mine) 801 802 info, _ := elf.load(mine) 803 defer elf.destroy(&info) 804 805 testing.expect_value(t, bin_diff_output(mine, &info, ref_path, out_path), "") 806 807 got := read_all(t, out_path) 808 defer delete(got) 809 testing.expect(t, strings.contains(got, "+ libc.so.6"), "reference-only dependency must show as added") 810 testing.expect(t, strings.contains(got, "/opt/runpath"), "runpath change must be reported") 811 } 812 813 @(test) 814 test_bin_diff_identical_files_report_no_changes :: proc(t: ^testing.T) { 815 ref_path := tmp_path("bindiff_same_ref") 816 defer drop(ref_path) 817 out_path := tmp_path("bindiff_same_out") 818 defer drop(out_path) 819 820 img := elf.build_dynamic_elf(true, true, elf.DEFAULT_DYN_ENTRIES[:]) 821 defer delete(img) 822 _ = os.write_entire_file(ref_path, img) 823 824 info, _ := elf.load(img) 825 defer elf.destroy(&info) 826 827 testing.expect_value(t, bin_diff_output(img, &info, ref_path, out_path), "") 828 829 got := read_all(t, out_path) 830 defer delete(got) 831 testing.expect(t, strings.contains(got, "unchanged"), "identical dependencies must report unchanged") 832 testing.expect(t, !strings.contains(got, "->"), "identical files must show no deltas") 833 }