1 // clang++ -o kt-dump{,.cpp} -Wall -std=c++20 2 3 #include <assert.h> 4 #include <cstdio> 5 #include <filesystem> 6 #include <fstream> 7 #include <iostream> 8 #include <mach-o/fat.h> 9 #include <mach-o/loader.h> 10 #include <optional> 11 #include <set> 12 #include <span> 13 #include <vector> 14 #include <removefile.h> 15 #include <unistd.h> 16 #include <spawn.h> 17 #include <fcntl.h> 18 #include <uuid/uuid.h> 19 20 /* 21 * kt-dump.cpp 22 * 23 * Tool to dump the kalloc type information from a given Mach-O binary. 24 * Usage: 25 * kt-dump [-f <simple|json|struct|stats>] <mach-o> 26 * 27 * The tool will scan the given Mach-O to find the __kalloc_type section. 28 * It will then walk that section using the kalloc_type_view definition 29 * provided below, in order to dump the type names and signatures that 30 * have been compiled into the binary. 31 * 32 * The output "format" can be specified with the -f option. The default 33 * format ("simple") will output the type name and the signature, 34 * enclosed in square brackets. The "json" format will print a JSON 35 * dictionary for each kalloc_type_view entry, including the type name, 36 * its size and the signature. The "struct" output format will use 37 * __builtin_dump_struct to dump a C-like representation of the view. 38 * Finally, if the "stats" output format is chosen, the tool will only 39 * show overall information about the __kalloc_type section. 40 * 41 * The tool supports both MH_KEXT_BUNDLE and kernel cache files. If a 42 * FAT Mach-O is provided, it must contain an arm64 slice. 43 */ 44 45 /* Note: these must be kept in sync with the defs in kalloc.h/zalloc.h */ 46 47 __options_decl(kalloc_type_flags_t, uint32_t, { 48 KT_DEFAULT = 0x0001, 49 KT_PRIV_ACCT = 0x0002, 50 KT_SHARED_ACCT = 0x0004, 51 KT_DATA_ONLY = 0x0008, 52 KT_VM = 0x0010, 53 KT_CHANGED = 0x0020, 54 KT_CHANGED2 = 0x0040, 55 KT_PTR_ARRAY = 0x0080, 56 KT_NOSHARED = 0x2000, 57 KT_SLID = 0x4000, 58 KT_PROCESSED = 0x8000, 59 KT_HASH = 0xffff0000, 60 }); 61 62 __options_decl(kalloc_type_version_t, uint16_t, { 63 KT_V1 = 0x0001, 64 }); 65 66 /* fixme we need to recognize Intel for which this is 20*/ 67 #define KHEAP_NUM_ZONES 22 68 69 struct zone_view { 70 void *zv_zone; 71 void *zv_stats; 72 const char *zv_name; 73 void *zv_next; 74 }; 75 76 struct kalloc_type_view { 77 struct zone_view kt_zv; 78 const char *kt_signature; 79 kalloc_type_flags_t kt_flags; 80 uint32_t kt_size; 81 struct zone *kt_zshared; 82 struct zone *kt_zsig; 83 }; 84 85 struct kalloc_type_var_view { 86 kalloc_type_version_t kt_version; 87 uint16_t kt_size_hdr; 88 /* 89 * Temporary: Needs to be 32bits cause we have many structs that use 90 * IONew/Delete that are larger than 32K. 91 */ 92 uint32_t kt_size_type; 93 struct zone_stats *kt_stats; 94 const char *kt_name; 95 struct zone_view *kt_next; 96 uint16_t kt_heap_start; 97 uint8_t kt_zones[KHEAP_NUM_ZONES]; 98 const char *kt_sig_hdr; 99 const char *kt_sig_type; 100 kalloc_type_flags_t kt_flags; 101 }; 102 103 template <typename T> struct macho_section { 104 section_64 section; 105 std::span<const T> contents; 106 107 macho_section(const section_64 &sec, std::span<uint8_t> data) 108 : section(sec), 109 contents(reinterpret_cast<T *>( 110 data.subspan(sec.offset, sec.size / sizeof(T)).data()), 111 sec.size / sizeof(T)) 112 { 113 } 114 115 size_t 116 elem_size() const 117 { 118 return sizeof(T); 119 } 120 121 size_t 122 elem_count() const 123 { 124 return section.size / elem_size(); 125 } 126 }; 127 128 int 129 printf_with_indent(const char *indent, const char *format, ...) 130 { 131 int n = 0; 132 133 va_list ap; 134 if (*indent) { 135 std::cout << indent; 136 n += strlen(indent); 137 } 138 139 va_start(ap, format); 140 n += vprintf(format, ap); 141 va_end(ap); 142 return n; 143 } 144 145 static inline const char * 146 decode_string(const macho_section<char> &sec_cstring, const char *string) 147 { 148 /* 149 * Compute the offsets into the __cstring section. 150 * This works for both single kexts (MH_KEXT_BUNDLE) and kernel caches. 151 * For the former, the __cstring section addr is the offset of the section 152 * into the slice. For the latter, the __cstring section addr is the virtual 153 * address of the section, and the fields are pointers into such space. 154 */ 155 156 if (string) { 157 uintptr_t string_p = reinterpret_cast<uintptr_t>(string); 158 uint32_t string_off = (uint32_t)string_p; 159 160 return &sec_cstring.contents[string_off - sec_cstring.section.offset]; 161 } 162 163 return nullptr; 164 } 165 166 static enum class out_fmt_type { 167 SIMPLE, 168 JSON, 169 STRUCT, 170 STATS 171 } out_fmt = out_fmt_type::SIMPLE; 172 173 class image { 174 const std::span<uint8_t> slice_contents; 175 size_t slice_mh_offs; 176 177 std::optional<macho_section<kalloc_type_view> > sec_types; 178 std::optional<macho_section<kalloc_type_var_view> > sec_types_var; 179 std::optional<macho_section<char> > sec_cstring; 180 uuid_t img_uuid; 181 182 std::set<std::pair<const char *, const char *> > dedup_entries; 183 std::set<std::tuple<const char *, const char *, const char *> > dedup_entries_var; 184 std::set<const char *> dedup_strings; 185 186 struct { 187 size_t uniq_structs = 0; 188 size_t uniq_structs_var = 0; 189 size_t names_sz = 0; 190 size_t sig_sz = 0; 191 } stats; 192 193 void 194 dump_types(const char *indent) 195 { 196 const char *sep = "\n"; 197 198 if (out_fmt == out_fmt_type::JSON) { 199 std::cout << ",\n" << indent << " \"fixed\": ["; 200 } 201 202 for (auto &ktv : sec_types->contents) { 203 const char *name = decode_string(*sec_cstring, ktv.kt_zv.zv_name); 204 const char *sig = decode_string(*sec_cstring, ktv.kt_signature); 205 206 /* Only output the equal entries (same name/signature) once */ 207 if (!dedup_entries.insert(std::make_tuple(name, sig)).second) { 208 continue; 209 } 210 211 if (ktv.kt_flags & KT_DATA_ONLY) { 212 sig = "data"; 213 } 214 if (dedup_strings.insert(name).second) { 215 stats.names_sz += strlen(name) + 1; 216 } 217 if (dedup_strings.insert(sig).second) { 218 stats.sig_sz += strlen(sig) + 1; 219 } 220 221 stats.uniq_structs++; 222 if (out_fmt != out_fmt_type::STRUCT) { 223 name += strlen("site."); 224 } 225 226 switch (out_fmt) { 227 case out_fmt_type::SIMPLE: 228 std::cout << indent << name << " [" << sig << "]\n"; 229 break; 230 case out_fmt_type::JSON: 231 std::cout << sep << indent 232 << " { \"name\": \"" << name << "\", " 233 << "\"size\": " << ktv.kt_size << ", " 234 << "\"sig\": \"" << sig << '"' 235 << " }"; 236 sep = ",\n"; 237 break; 238 case out_fmt_type::STRUCT: { 239 /* Make a copy and fill in the pointers to the cstring section */ 240 kalloc_type_view printable_view = ktv; 241 printable_view.kt_zv.zv_name = name; 242 printable_view.kt_signature = sig; 243 __builtin_dump_struct(&printable_view, &printf_with_indent, indent); 244 } break; 245 case out_fmt_type::STATS: 246 break; 247 } 248 } 249 250 if (out_fmt == out_fmt_type::JSON) { 251 std::cout << std::endl << indent << " ]"; 252 } 253 } 254 255 void 256 dump_types_var(const char *indent) 257 { 258 const char *sep = "\n"; 259 260 if (out_fmt == out_fmt_type::JSON) { 261 std::cout << ",\n" << indent << " \"var\": ["; 262 } 263 264 for (auto &ktv : sec_types_var->contents) { 265 const char *name = decode_string(*sec_cstring, ktv.kt_name); 266 const char *sig_hdr = decode_string(*sec_cstring, ktv.kt_sig_hdr); 267 const char *sig_type = decode_string(*sec_cstring, ktv.kt_sig_type); 268 269 /* Only output the equal entries (same name/signature) once */ 270 if (!dedup_entries_var.insert(std::make_tuple(name, sig_hdr, sig_type)).second) { 271 continue; 272 } 273 274 if (dedup_strings.insert(name).second) { 275 stats.names_sz += strlen(name) + 1; 276 } 277 if (sig_hdr && dedup_strings.insert(sig_hdr).second) { 278 stats.sig_sz += strlen(sig_hdr) + 1; 279 } 280 if (dedup_strings.insert(sig_type).second) { 281 stats.sig_sz += strlen(sig_type) + 1; 282 } 283 284 if (ktv.kt_flags & KT_DATA_ONLY) { 285 sig_type = "data"; 286 if (ktv.kt_size_hdr) { 287 sig_hdr = "data"; 288 } 289 } 290 stats.uniq_structs_var++; 291 if (out_fmt != out_fmt_type::STRUCT) { 292 name += strlen("site."); 293 } 294 295 switch (out_fmt) { 296 case out_fmt_type::SIMPLE: 297 if (sig_hdr) { 298 std::cout << indent << name 299 << " [" << sig_hdr << ", " << sig_type << "]\n"; 300 } else { 301 std::cout << indent << name 302 << " [, " << sig_type << "]\n"; 303 } 304 break; 305 case out_fmt_type::JSON: 306 std::cout << sep << indent 307 << " { \"name\": \"" << name << "\", "; 308 if (sig_hdr) { 309 std::cout << "\"size_hdr\": " << ktv.kt_size_hdr << ", " 310 << "\"sig_hdr\": \"" << sig_hdr << "\", "; 311 } 312 std::cout << "\"size_type\": " << ktv.kt_size_type << ", " 313 << "\"sig_type\": \"" << sig_type << '"' 314 << " }"; 315 sep = ",\n"; 316 break; 317 case out_fmt_type::STRUCT: { 318 /* Make a copy and fill in the pointers to the cstring section */ 319 kalloc_type_var_view printable_view = ktv; 320 printable_view.kt_name = name; 321 printable_view.kt_sig_hdr = sig_hdr; 322 printable_view.kt_sig_type = sig_type; 323 __builtin_dump_struct(&printable_view, &printf_with_indent, indent); 324 } break; 325 case out_fmt_type::STATS: 326 break; 327 } 328 } 329 330 if (out_fmt == out_fmt_type::JSON) { 331 std::cout << std::endl << indent << " ]"; 332 } 333 } 334 335 const mach_header_64 * 336 mh_hdr() const 337 { 338 return reinterpret_cast<const mach_header_64 *>(slice_contents.data() + slice_mh_offs); 339 } 340 341 public: 342 image(std::span<uint8_t> contents, size_t mh_offs = 0) 343 : slice_contents{contents}, slice_mh_offs{mh_offs} 344 { 345 auto *hdr = mh_hdr(); 346 std::span<uint8_t> commands = contents.subspan(mh_offs + sizeof(*hdr)); 347 348 assert(hdr->magic == MH_MAGIC_64); 349 350 for (size_t i = 0; i < hdr->ncmds; i++) { 351 auto *cmd = reinterpret_cast<const load_command *>(commands.data()); 352 353 commands = commands.subspan(cmd->cmdsize); 354 355 switch (cmd->cmd) { 356 case LC_SEGMENT_64: 357 break; 358 case LC_UUID: 359 uuid_copy(img_uuid, reinterpret_cast<const uuid_command *>(cmd)->uuid); 360 continue; 361 default: 362 continue; 363 } 364 365 auto *seg_cmd = reinterpret_cast<const segment_command_64 *>(cmd); 366 const std::span<section_64> sections((section_64 *)(seg_cmd + 1), seg_cmd->nsects); 367 368 for (auto &sec : sections) { 369 std::string_view segname(sec.segname); 370 std::string_view sectname(sec.sectname); 371 372 if (sectname == "__kalloc_type") { 373 assert(!sec_types && "Multiple __kalloc_type sections?"); 374 sec_types = macho_section<kalloc_type_view>(sec, slice_contents); 375 assert(sec.size % sec_types->elem_size() == 0 && 376 "Check the definition of kalloc_type_view"); 377 } else if (sectname == "__kalloc_var") { 378 assert(!sec_types_var && "Multiple __kalloc_var sections?"); 379 sec_types_var = macho_section<kalloc_type_var_view>(sec, slice_contents); 380 assert(sec.size % sec_types_var->elem_size() == 0 && 381 "Check the definition of kalloc_type_var_view"); 382 } else if (segname == "__TEXT" && sectname == "__cstring") { 383 assert(!sec_cstring && "Multiple __kalloc_var sections?"); 384 sec_cstring = macho_section<char>(sec, slice_contents); 385 } 386 } 387 } 388 } 389 390 ~image() = default; 391 392 std::string 393 uuid() const 394 { 395 uuid_string_t to_str; 396 uuid_unparse_upper(img_uuid, to_str); 397 return std::string{to_str}; 398 } 399 400 const char * 401 slice() const 402 { 403 auto *hdr = mh_hdr(); 404 cpu_type_t cpu; 405 cpu_subtype_t sub; 406 407 if (hdr->magic == MH_CIGAM_64) { 408 cpu = OSSwapInt32(hdr->cputype); 409 sub = OSSwapInt32(hdr->cpusubtype & CPU_SUBTYPE_MASK); 410 } else { 411 cpu = hdr->cputype; 412 sub = hdr->cpusubtype & OSSwapInt32(CPU_SUBTYPE_MASK); 413 } 414 415 if (cpu == CPU_TYPE_ARM64) { 416 if (sub == CPU_SUBTYPE_ARM64E) { 417 return "arm64e"; 418 } 419 return "arm64"; 420 } 421 422 /* other slices unsupported for now */ 423 return nullptr; 424 } 425 426 void 427 dump(const std::string &imgname, const char *indent = "") 428 { 429 if (out_fmt == out_fmt_type::JSON) { 430 std::cout << indent << "{\n" 431 << indent << " \"image\": \"" << imgname << "\",\n" 432 << indent << " \"slice\": \"" << slice() << "\",\n" 433 << indent << " \"uuid\": \"" << uuid() << '"'; 434 } else { 435 std::cout << imgname << " (" << slice() << ", " << uuid() << ")\n"; 436 } 437 438 if (sec_types) { 439 dump_types(indent); 440 } 441 442 if (sec_types_var) { 443 dump_types_var(indent); 444 } 445 446 if (out_fmt == out_fmt_type::JSON) { 447 std::cout << std::endl << indent << "}"; 448 } 449 450 if (out_fmt == out_fmt_type::STATS) { 451 if (auto &sec = *sec_types; sec_types) { 452 auto ucount = stats.uniq_structs; 453 auto usize = ucount * sec.elem_size(); 454 455 std::cout << indent << "__kalloc_type: " << std::endl; 456 std::cout << indent << " total structs: " << sec.elem_count() << std::endl; 457 std::cout << indent << " unique structs: " << ucount << std::endl; 458 std::cout << indent << " total size: " << sec.section.size << std::endl; 459 std::cout << indent << " unique size: " << usize << std::endl; 460 } 461 if (auto &sec = *sec_types_var; sec_types_var) { 462 auto ucount = stats.uniq_structs_var; 463 auto usize = ucount * sec.elem_size(); 464 465 std::cout << indent << "__kalloc_var: " << std::endl; 466 std::cout << indent << " total structs: " << sec.elem_count() << std::endl; 467 std::cout << indent << " unique structs: " << ucount << std::endl; 468 std::cout << indent << " total size: " << sec.section.size << std::endl; 469 std::cout << indent << " unique size: " << usize << std::endl; 470 } 471 std::cout << indent << "names strings: " << stats.names_sz << std::endl; 472 std::cout << indent << "signatures strings: " << stats.sig_sz << std::endl; 473 } 474 475 stats = {}; 476 dedup_entries.clear(); 477 dedup_entries_var.clear(); 478 dedup_strings.clear(); 479 } 480 }; 481 482 static int 483 do_simple_macho(const std::string filename, std::span<uint8_t> contents) 484 { 485 image img{contents}; 486 img.dump(filename); 487 return 0; 488 } 489 490 static int 491 do_fat_macho(const std::string filename, std::span<uint8_t> contents) 492 { 493 fat_header *fhdr = reinterpret_cast<fat_header *>(contents.data()); 494 std::span<fat_arch> fat_archs( 495 reinterpret_cast<fat_arch *>(&contents[sizeof(fat_header)]), 496 OSSwapInt32(fhdr->nfat_arch)); 497 const char *sep = "\n"; 498 499 if (out_fmt == out_fmt_type::JSON) { 500 std::cout << "["; 501 } 502 503 for (auto &arch : fat_archs) { 504 image img{contents.subspan(OSSwapInt32(arch.offset), OSSwapInt32(arch.size))}; 505 506 if (out_fmt == out_fmt_type::JSON) { 507 std::cout << sep; 508 } else { 509 std::cout << std::endl; 510 } 511 img.dump(filename, " "); 512 sep = ",\n"; 513 } 514 515 if (out_fmt == out_fmt_type::JSON) { 516 std::cout << "\n]"; 517 } 518 519 return 0; 520 } 521 522 static int 523 do_fileset(std::span<uint8_t> contents) 524 { 525 auto *hdr = reinterpret_cast<const mach_header_64 *>(contents.data()); 526 std::span<uint8_t> commands = contents.subspan(sizeof(*hdr)); 527 const char *sep = "\n"; 528 529 if (hdr->cputype != CPU_TYPE_ARM64) { 530 std::cerr << "unsupported cpu type"; 531 return 1; 532 } 533 534 if (out_fmt == out_fmt_type::JSON) { 535 std::cout << "["; 536 } 537 538 for (size_t i = 0; i < hdr->ncmds; i++) { 539 auto *cmd = reinterpret_cast<const segment_command_64 *>(commands.data()); 540 541 commands = commands.subspan(cmd->cmdsize); 542 543 if (cmd->cmd != LC_FILESET_ENTRY) { 544 continue; 545 } 546 547 auto *fec = reinterpret_cast<const fileset_entry_command *>(cmd); 548 const char *name = reinterpret_cast<const char *>(cmd) + fec->entry_id.offset; 549 image img{contents, fec->fileoff}; 550 551 if (out_fmt == out_fmt_type::JSON) { 552 std::cout << sep; 553 } else { 554 std::cout << std::endl; 555 } 556 img.dump(name, " "); 557 sep = ",\n"; 558 } 559 560 if (out_fmt == out_fmt_type::JSON) { 561 std::cout << "]"; 562 } 563 564 return 0; 565 } 566 567 void 568 read_file(std::filesystem::path &path, std::vector<uint8_t> &contents) 569 { 570 std::ifstream file(path, std::ifstream::binary); 571 size_t size(std::filesystem::file_size(path)); 572 573 contents.resize(size); 574 file.read(reinterpret_cast<char *>(contents.data()), size); 575 file.close(); 576 } 577 578 enum class file_kind { 579 UNKNOWN, 580 MACHO, 581 FAT_MACHO, 582 FILESET, 583 IMG4, 584 }; 585 586 static file_kind 587 recognize_file(const std::vector<uint8_t> &contents) 588 { 589 const mach_header_64 *hdr; 590 591 if (contents.size() < sizeof(mach_header_64)) { 592 return file_kind::UNKNOWN; 593 } 594 595 hdr = reinterpret_cast<const mach_header_64 *>(contents.data()); 596 if (hdr->magic == MH_MAGIC_64) { 597 switch (hdr->filetype) { 598 case MH_FILESET: 599 return file_kind::FILESET; 600 default: 601 return file_kind::MACHO; 602 } 603 } 604 605 if (hdr->magic == FAT_CIGAM) { 606 return file_kind::FAT_MACHO; 607 } 608 609 if (memcmp("IM4P", contents.data() + 8, 4) == 0) { 610 return file_kind::IMG4; 611 } 612 613 return file_kind::UNKNOWN; 614 } 615 616 static int 617 call_cmd_silent(const char *const *args) 618 { 619 posix_spawn_file_actions_t facts; 620 extern char **environ; 621 pid_t pid; 622 int rc; 623 624 posix_spawn_file_actions_init(&facts); 625 posix_spawn_file_actions_addopen(&facts, 626 STDIN_FILENO, "/dev/null", O_RDONLY, 0777); 627 posix_spawn_file_actions_addopen(&facts, 628 STDOUT_FILENO, "/dev/null", O_WRONLY, 0777); 629 posix_spawn_file_actions_addopen(&facts, 630 STDERR_FILENO, "/dev/null", O_WRONLY, 0777); 631 rc = posix_spawnp(&pid, args[0], &facts, nullptr, 632 (char *const *)args, environ); 633 posix_spawn_file_actions_destroy(&facts); 634 635 if (rc != 0) { 636 return 1; 637 } 638 639 waitpid(pid, &rc, 0); 640 if (!WIFEXITED(rc) || WEXITSTATUS(rc)) { 641 return 1; 642 } 643 644 return 0; 645 } 646 647 static int 648 do_file(const std::filesystem::path &path, std::vector<uint8_t> &contents) 649 { 650 int status = 0; 651 652 switch (recognize_file(contents)) { 653 case file_kind::MACHO: 654 return do_simple_macho(path.filename().string(), contents); 655 case file_kind::FAT_MACHO: 656 return do_fat_macho(path.filename().string(), contents); 657 case file_kind::FILESET: 658 return do_fileset(contents); 659 case file_kind::IMG4: 660 break; 661 case file_kind::UNKNOWN: 662 std::cerr << "Unsupported file type\n"; 663 return 1; 664 } 665 666 char tmp_tpl[] = "/tmp/kt-dump.XXXXXX"; 667 char *tmp_dir = mkdtemp(tmp_tpl); 668 669 if (tmp_dir == NULL) { 670 std::cerr << "Unable to make temporary directory to unpack img4\n"; 671 return 1; 672 } 673 674 std::filesystem::path compressed_kc{tmp_dir}; 675 std::filesystem::path uncompressed_kc{tmp_dir}; 676 677 compressed_kc /= "compressed.kc"; 678 uncompressed_kc /= "uncompressed.kc"; 679 680 static const char *const img4args[] = { 681 "img4utility", 682 "--copyBinary", 683 "--input", 684 path.c_str(), 685 "--output", 686 compressed_kc.c_str(), 687 NULL, 688 }; 689 690 static const char *const ct_args[] = { 691 "compression_tool", 692 "-decode", 693 "-v", 694 "-v", 695 "-v", 696 "-i", 697 compressed_kc.c_str(), 698 "-o", 699 uncompressed_kc.c_str(), 700 NULL, 701 }; 702 703 if (call_cmd_silent(img4args)) { 704 std::cerr << "Unable to unpack img4 image\n"; 705 status = 1; 706 } else if (call_cmd_silent(ct_args)) { 707 std::cerr << "Unable to decompress KC\n"; 708 status = 1; 709 } else { 710 read_file(uncompressed_kc, contents); 711 } 712 713 removefile_state_t s = removefile_state_alloc(); 714 removefile(tmp_dir, s, REMOVEFILE_RECURSIVE); 715 removefile_state_free(s); 716 717 return status ?: do_file(path, contents); 718 } 719 720 int 721 main(int argc, char const *argv[]) 722 { 723 if (argc != 2 && argc != 4) { 724 std::cout << "Usage: " << argv[0] 725 << " [-f <simple|json|struct|stats>] <mach-o>\n"; 726 return 1; 727 } 728 729 std::string path_arg; 730 731 /* Parse command line args */ 732 for (int i = 1; i < argc; i++) { 733 std::string arg(argv[i]); 734 if (arg == "-f") { 735 if (++i == argc) { 736 std::cerr << "Option " << arg << " requires an argument\n"; 737 return 1; 738 } 739 arg = argv[i]; 740 if (arg == "simple") { 741 out_fmt = out_fmt_type::SIMPLE; 742 } else if (arg == "json" || arg == "JSON") { 743 out_fmt = out_fmt_type::JSON; 744 } else if (arg == "struct") { 745 out_fmt = out_fmt_type::STRUCT; 746 } else if (arg == "stats") { 747 out_fmt = out_fmt_type::STATS; 748 } else { 749 std::cerr << "Unknown output format: " << arg << std::endl; 750 return 1; 751 } 752 } else { 753 /* Read the file specified as a positional arg */ 754 path_arg = arg; 755 } 756 } 757 758 if (path_arg.length() == 0) { 759 std::cerr << "no file specified\n"; 760 return 1; 761 } 762 763 std::filesystem::path path(path_arg); 764 std::vector<uint8_t> contents; 765 766 read_file(path, contents); 767 return do_file(path, contents); 768 } 769