// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// ignore_for_file: constant_identifier_names

import 'dart:io';
import 'dart:typed_data';

import 'package:path/path.dart' as path;

import 'constants.dart' as constants;
import 'dwarf_container.dart';
import 'reader.dart';

int _readMachOUint8(Reader reader) => reader.readByte(signed: false);

int _readMachOUint16(Reader reader) => reader.readBytes(2, signed: false);

int _readMachOUint32(Reader reader) => reader.readBytes(4, signed: false);

int _readMachOUword(Reader reader) =>
    reader.readBytes(reader.wordSize, signed: false);

class StringTable implements DwarfContainerStringTable {
  final Map<int, String> _stringsByOffset;

  StringTable._(this._stringsByOffset);

  static StringTable fromReader(Reader reader) => StringTable._(Map.fromEntries(
      reader.readRepeatedWithOffsets((r) => r.readNullTerminatedString())));

  @override
  String? operator [](int index) {
    // Fast case: Index is for the start of a null terminated string.
    if (_stringsByOffset.containsKey(index)) {
      return _stringsByOffset[index];
    }
    // We can index into null terminated string entries for suffixes of
    // that string, so do a linear search to find the appropriate entry.
    for (final kv in _stringsByOffset.entries) {
      final start = index - kv.key;
      if (start >= 0 && start <= kv.value.length) {
        return kv.value.substring(start);
      }
    }
    return null;
  }

  void writeToStringBuffer(StringBuffer buffer) {
    for (final k in _stringsByOffset.keys) {
      buffer
        ..write(k.toString().padLeft(8, ' '))
        ..write(' => ')
        ..writeln(_stringsByOffset[k]);
    }
  }

  @override
  String toString() {
    final buffer = StringBuffer();
    writeToStringBuffer(buffer);
    return buffer.toString();
  }
}

class Symbol implements DwarfContainerSymbol {
  final int index;
  final int type;
  final int sect;
  final int desc;
  @override
  final int value;
  @override
  late final String name;

  Symbol._(this.index, this.type, this.sect, this.desc, this.value);

  static Symbol fromReader(Reader reader) {
    final index = _readMachOUint32(reader);
    final type = _readMachOUint8(reader);
    final sect = _readMachOUint8(reader);
    final desc = _readMachOUint16(reader);
    final value = _readMachOUword(reader);
    return Symbol._(index, type, sect, desc, value);
  }
}

class SymbolTable {
  final Map<String, Symbol> _symbols;

  SymbolTable._(this._symbols);

  static SymbolTable fromReader(
      Reader reader, int nsyms, StringTable stringTable) {
    final symbols = <String, Symbol>{};
    for (int i = 0; i < nsyms; i++) {
      final symbol = Symbol.fromReader(reader);
      final index = symbol.index;
      final name = stringTable[index];
      if (name == null) {
        throw FormatException('Index $index not found in string table');
      }
      symbol.name = name;
      symbols[name] = symbol;
    }
    return SymbolTable._(symbols);
  }

  Iterable<String> get keys => _symbols.keys;
  Iterable<Symbol> get values => _symbols.values;

  Symbol? operator [](String name) => _symbols[name];

  bool containsKey(String name) => _symbols.containsKey(name);
}

class LoadCommand {
  final int cmd;
  final int cmdsize;

  LoadCommand._(this.cmd, this.cmdsize);

  static const LC_SEGMENT = 0x1;
  static const LC_SYMTAB = 0x2;
  static const LC_SEGMENT_64 = 0x19;
  static const LC_UUID = 0x1b;

  static LoadCommand fromReader(Reader reader) {
    final start = reader.offset; // cmdsize includes size of cmd and cmdsize.
    final cmd = _readMachOUint32(reader);
    final cmdsize = _readMachOUint32(reader);
    assert(reader.remaining >= cmdsize - (reader.offset - start));
    LoadCommand command = LoadCommand._(cmd, cmdsize);
    switch (cmd) {
      case LC_SEGMENT:
      case LC_SEGMENT_64:
        command = SegmentCommand.fromReader(reader, cmd, cmdsize);
        break;
      case LC_SYMTAB:
        command = SymbolTableCommand.fromReader(reader, cmd, cmdsize);
        break;
      case LC_UUID:
        command = UuidCommand.fromReader(reader, cmd, cmdsize);
        break;
      default:
        break;
    }
    reader.seek(start + cmdsize, absolute: true);
    return command;
  }

  void writeToStringBuffer(StringBuffer buffer) {
    buffer
      ..write('Uninterpreted command 0x')
      ..write(cmd.toRadixString(16))
      ..write(' of size ')
      ..writeln(cmdsize);
  }

  @override
  String toString() {
    StringBuffer buffer = StringBuffer();
    writeToStringBuffer(buffer);
    return buffer.toString();
  }
}

class SegmentCommand extends LoadCommand {
  final String segname;
  final int vmaddr;
  final int vmsize;
  final int fileoff;
  final int filesize;
  final int maxprot;
  final int initprot;
  final int nsects;
  final int flags;
  final Map<String, Section> sections;

  SegmentCommand._(
      super.cmd,
      super.cmdsize,
      this.segname,
      this.vmaddr,
      this.vmsize,
      this.fileoff,
      this.filesize,
      this.maxprot,
      this.initprot,
      this.nsects,
      this.flags,
      this.sections)
      : super._();

  static SegmentCommand fromReader(Reader reader, int cmd, int cmdsize) {
    final segname = reader.readFixedLengthNullTerminatedString(16);
    final vmaddr = _readMachOUword(reader);
    final vmsize = _readMachOUword(reader);
    final fileoff = _readMachOUword(reader);
    final filesize = _readMachOUword(reader);
    final maxprot = _readMachOUint32(reader);
    final initprot = _readMachOUint32(reader);
    final nsects = _readMachOUint32(reader);
    final flags = _readMachOUint32(reader);
    final sections = <String, Section>{};
    for (int i = 0; i < nsects; i++) {
      final section = Section.fromReader(reader);
      sections[section.sectname] = section;
    }
    return SegmentCommand._(cmd, cmdsize, segname, vmaddr, vmsize, fileoff,
        filesize, maxprot, initprot, nsects, flags, sections);
  }

  @override
  void writeToStringBuffer(StringBuffer buffer) {
    buffer
      ..write('Segment "')
      ..write(segname)
      ..write('" of size ')
      ..write(filesize)
      ..write(' at offset 0x')
      ..writeln(fileoff.toRadixString(16));
    buffer.writeln('Sections:');
    for (final section in sections.values) {
      section.writeToStringBuffer(buffer);
      buffer.writeln();
    }
  }
}

class Section {
  String sectname;
  String segname;
  int addr;
  int size;
  int offset;
  int align;
  int reloff;
  int nreloc;
  int flags;
  int reserved1;
  int reserved2;
  int? reserved3;

  Section._(
      this.sectname,
      this.segname,
      this.addr,
      this.size,
      this.offset,
      this.align,
      this.reloff,
      this.nreloc,
      this.flags,
      this.reserved1,
      this.reserved2,
      this.reserved3);

  static Section fromReader(Reader reader) {
    final sectname = reader.readFixedLengthNullTerminatedString(16);
    final segname = reader.readFixedLengthNullTerminatedString(16);
    final addr = _readMachOUword(reader);
    final size = _readMachOUword(reader);
    final offset = _readMachOUint32(reader);
    final align = _readMachOUint32(reader);
    final reloff = _readMachOUint32(reader);
    final nreloc = _readMachOUint32(reader);
    final flags = _readMachOUint32(reader);
    final reserved1 = _readMachOUint32(reader);
    final reserved2 = _readMachOUint32(reader);
    final reserved3 = (reader.wordSize == 8) ? _readMachOUint32(reader) : null;
    return Section._(sectname, segname, addr, size, offset, align, reloff,
        nreloc, flags, reserved1, reserved2, reserved3);
  }

  Reader shrink(Reader reader) => reader.shrink(offset, size);

  void writeToStringBuffer(StringBuffer buffer) {
    buffer
      ..write('Section "')
      ..write(sectname)
      ..write('" of size ')
      ..write(size)
      ..write(' at offset 0x')
      ..write(paddedHex(offset, 4));
  }

  @override
  String toString() {
    StringBuffer buffer = StringBuffer();
    writeToStringBuffer(buffer);
    return buffer.toString();
  }
}

class SymbolTableCommand extends LoadCommand {
  final int _symoff;
  final int _nsyms;
  final int _stroff;
  final int _strsize;

  SymbolTableCommand._(super.cmd, super.cmdsize, this._symoff, this._nsyms,
      this._stroff, this._strsize)
      : super._();

  static SymbolTableCommand fromReader(Reader reader, int cmd, int cmdsize) {
    final symoff = _readMachOUint32(reader);
    final nsyms = _readMachOUint32(reader);
    final stroff = _readMachOUint32(reader);
    final strsize = _readMachOUint32(reader);
    return SymbolTableCommand._(cmd, cmdsize, symoff, nsyms, stroff, strsize);
  }

  SymbolTable load(Reader reader) {
    final stringTable =
        StringTable.fromReader(reader.shrink(_stroff, _strsize));
    return SymbolTable.fromReader(reader.shrink(_symoff), _nsyms, stringTable);
  }

  @override
  void writeToStringBuffer(StringBuffer buffer) {
    buffer
      ..write('Symbol table with ')
      ..write(_nsyms)
      ..write(' symbols of size ')
      ..writeln(cmdsize);
  }
}

class UuidCommand extends LoadCommand {
  Uint8List uuid;

  static const kUuidSize = 16;

  UuidCommand._(super.cmd, super.cmdsize, this.uuid) : super._();

  static UuidCommand fromReader(Reader reader, int cmd, int cmdsize) {
    final uuid = Uint8List.sublistView(
        reader.bytes, reader.offset, reader.offset + kUuidSize);
    return UuidCommand._(cmd, cmdsize, uuid);
  }

  String get uuidString => uuid.map((i) => paddedHex(i, 1)).join();

  @override
  void writeToStringBuffer(StringBuffer buffer) {
    buffer
      ..write('UUID: ')
      ..write(uuidString);
  }
}

class MachOHeader {
  final int magic;
  final int cputype;
  final int cpusubtype;
  final int filetype;
  final int ncmds;
  final int sizeofcmds;
  final int flags;
  final int? reserved;
  final int size;

  MachOHeader._(this.magic, this.cputype, this.cpusubtype, this.filetype,
      this.ncmds, this.sizeofcmds, this.flags, this.reserved, this.size);

  static const _MH_MAGIC = 0xfeedface;
  static const _MH_CIGAM = 0xcefaedfe;
  static const _MH_MAGIC_64 = 0xfeedfacf;
  static const _MH_CIGAM_64 = 0xcffaedfe;

  static const _MH_DSYM = 0xa;

  static int? _wordSize(int magic) => (magic == _MH_MAGIC || magic == _MH_CIGAM)
      ? 4
      : (magic == _MH_MAGIC_64 || magic == _MH_CIGAM_64)
          ? 8
          : null;

  static Endian? _endian(int magic) =>
      (magic == _MH_MAGIC || magic == _MH_MAGIC_64)
          ? Endian.host
          : (magic == _MH_CIGAM || magic == _MH_CIGAM_64)
              ? (Endian.host == Endian.big ? Endian.little : Endian.big)
              : null;

  static MachOHeader? fromReader(Reader reader) {
    final start = reader.offset;
    // Initially assume host endianness.
    reader.endian = Endian.host;
    final magic = _readMachOUint32(reader);
    final wordSize = _wordSize(magic);
    final endian = _endian(magic);
    // Not an expected magic value, so not a supported Mach-O file.
    if (wordSize == null || endian == null) return null;
    reader.wordSize = wordSize;
    reader.endian = endian;
    final cputype = _readMachOUint32(reader);
    final cpusubtype = _readMachOUint32(reader);
    final filetype = _readMachOUint32(reader);
    final ncmds = _readMachOUint32(reader);
    final sizeofcmds = _readMachOUint32(reader);
    final flags = _readMachOUint32(reader);
    final reserved = reader.wordSize == 8 ? _readMachOUint32(reader) : null;
    final size = reader.offset - start;
    return MachOHeader._(magic, cputype, cpusubtype, filetype, ncmds,
        sizeofcmds, flags, reserved, size);
  }

  int get wordSize => _wordSize(magic)!;
  Endian get endian => _endian(magic)!;
  bool get isDSYM => filetype == _MH_DSYM;

  void writeToStringBuffer(StringBuffer buffer) {
    buffer
      ..write('Magic: 0x')
      ..writeln(paddedHex(magic, 4));
    buffer
      ..write('Cpu Type: 0x')
      ..writeln(paddedHex(cputype, 4));
    buffer
      ..write('Cpu Subtype: 0x')
      ..writeln(paddedHex(cpusubtype, 4));
    buffer
      ..write('Filetype: 0x')
      ..writeln(paddedHex(filetype, 4));
    buffer
      ..write('Number of commands: ')
      ..writeln(ncmds);
    buffer
      ..write('Size of commands: ')
      ..writeln(sizeofcmds);
    buffer
      ..write('Flags: 0x')
      ..writeln(paddedHex(flags, 4));
    if (reserved != null) {
      buffer
        ..write('Reserved: 0x')
        ..writeln(paddedHex(reserved!, 4));
    }
  }

  @override
  String toString() {
    final buffer = StringBuffer();
    writeToStringBuffer(buffer);
    return buffer.toString();
  }
}

class MachO extends DwarfContainer {
  final MachOHeader _header;
  final List<LoadCommand> _commands;
  final SymbolTable _symbolTable;
  final SegmentCommand? _dwarfSegment;
  final StringTable? _debugStringTable;
  final StringTable? _debugLineStringTable;

  MachO._(this._header, this._commands, this._symbolTable, this._dwarfSegment,
      this._debugStringTable, this._debugLineStringTable);

  static MachO? fromReader(Reader machOReader) {
    // MachO files contain absolute offsets from the start of the file, so
    // make sure we have a reader that a) makes no assumptions about the
    // endianness or word size, since we'll read those in the header and b)
    // has an internal offset of 0 so absolute offsets can be used directly.
    final reader = machOReader.shrink(machOReader.offset);
    final header = MachOHeader.fromReader(reader);
    if (header == null) return null;

    final commandReader = reader.shrink(reader.offset, header.sizeofcmds);
    final commands =
        List.of(commandReader.readRepeated(LoadCommand.fromReader));
    assert(commands.length == header.ncmds);

    // This MachO file can't contain any debugging information.
    if (commands.isEmpty) return null;

    final symbolTable =
        commands.whereType<SymbolTableCommand>().single.load(reader);

    final dwarfSegment = commands
        .whereType<SegmentCommand?>()
        .firstWhere((sc) => sc!.segname == '__DWARF', orElse: () => null);
    StringTable? debugStringTable;
    StringTable? debugLineStringTable;
    if (dwarfSegment != null) {
      final debugStringTableSection = dwarfSegment.sections['__debug_str'];
      if (debugStringTableSection != null) {
        debugStringTable =
            StringTable.fromReader(debugStringTableSection.shrink(reader));
      }

      final debugLineStringTableSection =
          dwarfSegment.sections['__debug_line_str'];
      if (debugLineStringTableSection != null) {
        debugLineStringTable =
            StringTable.fromReader(debugLineStringTableSection.shrink(reader));
      }
    }

    // Set the wordSize and endian of the original reader before returning.
    machOReader.wordSize = reader.wordSize;
    machOReader.endian = reader.endian;

    return MachO._(header, commands, symbolTable, dwarfSegment,
        debugStringTable, debugLineStringTable);
  }

  static String handleDSYM(String fileName) {
    if (!fileName.endsWith('.dSYM')) {
      return fileName;
    }
    final dwarfDir =
        Directory(path.join(fileName, 'Contents', 'Resources', 'DWARF'));
    // The DWARF directory inside the .dSYM should contain a single MachO file.
    final machoFile = dwarfDir.listSync().single as File;
    return machoFile.path;
  }

  static MachO? fromFile(String fileName) =>
      MachO.fromReader(Reader.fromFile(MachO.handleDSYM(fileName)));

  bool get isDSYM => _header.isDSYM;
  bool get hasDwarf => _dwarfSegment != null;

  Reader applyWordSizeAndEndian(Reader reader) =>
      Reader.fromTypedData(reader.bdata,
          wordSize: _header.wordSize, endian: _header.endian);

  @override
  String? get architecture => CpuType.fromCode(_header.cputype)?.dartName;

  @override
  Reader? abbreviationsTableReader(Reader containerReader) =>
      _dwarfSegment?.sections['__debug_abbrev']?.shrink(containerReader);
  @override
  Reader? lineNumberInfoReader(Reader containerReader) =>
      _dwarfSegment?.sections['__debug_line']?.shrink(containerReader);
  @override
  Reader? debugInfoReader(Reader containerReader) =>
      _dwarfSegment?.sections['__debug_info']?.shrink(containerReader);

  @override
  int? get vmStartAddress => _symbolTable[constants.vmSymbolName]?.value;

  @override
  int? get isolateStartAddress =>
      _symbolTable[constants.isolateSymbolName]?.value;

  @override
  String? get buildId =>
      _commands.whereType<UuidCommand>().firstOrNull?.uuidString;

  @override
  DwarfContainerStringTable? get debugStringTable => _debugStringTable;

  @override
  DwarfContainerStringTable? get debugLineStringTable => _debugLineStringTable;

  @override
  Symbol? staticSymbolAt(int address) {
    Symbol? bestSym;
    for (final symbol in _symbolTable.values) {
      if (symbol.value > address) continue;
      // Pick the symbol with a value closest to the given address.
      if (bestSym == null || (bestSym.value < symbol.value)) {
        bestSym = symbol;
      }
    }
    return bestSym;
  }

  @override
  Iterable<Symbol> get staticSymbols => _symbolTable.values;

  @override
  void writeToStringBuffer(StringBuffer buffer) {
    buffer
      ..writeln('----------------------------------------')
      ..writeln('               Header')
      ..writeln('----------------------------------------')
      ..writeln('');
    _header.writeToStringBuffer(buffer);
    buffer
      ..writeln('')
      ..writeln('')
      ..writeln('----------------------------------------')
      ..writeln('            Load commands')
      ..writeln('----------------------------------------')
      ..writeln('');
    for (final command in _commands) {
      command.writeToStringBuffer(buffer);
      buffer.writeln('');
    }
  }
}

class UniversalBinaryArch {
  final int cputype;
  final int cpusubtype;
  final int offset;
  final int size;
  final int align;

  UniversalBinaryArch._(
      this.cputype, this.cpusubtype, this.offset, this.size, this.align);

  static UniversalBinaryArch fromReader(Reader reader) {
    final cputype = _readMachOUint32(reader);
    final cpusubtype = _readMachOUint32(reader);
    final offset = _readMachOUint32(reader);
    final size = _readMachOUint32(reader);
    final align = _readMachOUint32(reader);

    return UniversalBinaryArch._(cputype, cpusubtype, offset, size, align);
  }

  // Given a reader for the entire universal binary, returns a reader
  // for only this architecture's contents.
  Reader shrink(Reader reader) => reader.shrink(offset, size);

  void writeToStringBuffer(StringBuffer buffer) {
    buffer
      ..write('  Cpu Type: 0x')
      ..writeln(paddedHex(cputype, 4));
    buffer
      ..write('  Cpu Subtype: 0x')
      ..writeln(paddedHex(cpusubtype, 4));
    buffer
      ..write('  Offset: 0x')
      ..writeln(paddedHex(offset, 4));
    buffer
      ..write('  Size: ')
      ..writeln(size);
    buffer
      ..write('  Alignment: ')
      ..writeln(align);
  }

  @override
  String toString() {
    final buffer = StringBuffer();
    writeToStringBuffer(buffer);
    return buffer.toString();
  }
}

class UniversalBinaryHeader {
  final int magic;
  final List<UniversalBinaryArch> _arches;

  UniversalBinaryHeader._(this.magic, this._arches);

  static const _FAT_MAGIC = 0xcafebabe;
  static const _FAT_CIGAM = 0xbebafeca;

  static UniversalBinaryHeader? fromReader(Reader originalReader) {
    assert(originalReader.offset == 0);
    // Make sure we have a reader that makes no assumptions about the
    // endianness, since we'll read that in the header.
    final reader =
        Reader.fromTypedData(ByteData.sublistView(originalReader.bdata));
    reader.wordSize = 4;
    reader.endian = Endian.host;
    final magic = _readMachOUint32(reader);
    if (magic == _FAT_CIGAM) {
      reader.endian = Endian.host == Endian.big ? Endian.little : Endian.big;
    } else if (magic != _FAT_MAGIC) {
      // Not a universal binary.
      return null;
    }
    final archCount = _readMachOUint32(reader);
    final arches = <UniversalBinaryArch>[];
    for (int i = 0; i < archCount; i++) {
      arches.add(UniversalBinaryArch.fromReader(reader));
    }
    return UniversalBinaryHeader._(magic, arches);
  }

  void writeToStringBuffer(StringBuffer buffer) {
    buffer
      ..write('Magic: 0x')
      ..writeln(paddedHex(magic, 4));
    buffer
      ..write('Number of architectures: ')
      ..writeln(_arches.length);
    for (int i = 0; i < _arches.length; i++) {
      buffer
        ..write('Arch ')
        ..write(i)
        ..writeln(':');
      _arches[i].writeToStringBuffer(buffer);
    }
  }

  @override
  String toString() {
    final buffer = StringBuffer();
    writeToStringBuffer(buffer);
    return buffer.toString();
  }
}

/// Represents Dart architectures that have valid CPU type values in MachO.
enum CpuType {
  arm(_CPU_ARCH_ARM, "arm"),
  arm64(_CPU_ARCH_ARM | _CPU_ARCH_ABI64, "arm64"),
  i386(_CPU_ARCH_X86, "ia32"),
  x64(_CPU_ARCH_X86 | _CPU_ARCH_ABI64, "x64");

  static const _CPU_ARCH_ABI64 = 0x01000000;
  static const _CPU_ARCH_X86 = 7;
  static const _CPU_ARCH_ARM = 12;

  static const _prefix = 'CPU_ARCH';

  /// The 32-bit MachO encoding for this architecture.
  final int code;

  /// The name of this architecture as reported by Dart, e.g., in
  /// non-symbolic stack traces.
  final String dartName;

  const CpuType(this.code, this.dartName);

  static CpuType? fromCode(int code) {
    for (final value in values) {
      if (value.code == code) return value;
    }
    return null;
  }

  static CpuType? fromDartName(String arch) {
    for (final value in values) {
      if (value.dartName == arch) return value;
    }
    return null;
  }

  /// Whether this CpuType represents a 64-bit architecture.
  bool get is64Bit => code & _CPU_ARCH_ABI64 != 0;

  @override
  String toString() => '${_prefix}_${name.toUpperCase()}';
}

class UniversalBinary {
  final UniversalBinaryHeader _header;
  final Map<CpuType, UniversalBinaryArch> _arches;
  final Map<UniversalBinaryArch, MachO> _contents;

  UniversalBinary._(this._header, this._arches, this._contents);

  static UniversalBinary? fromReader(Reader originalReader) {
    // Universal binary files contain absolute offsets from the start of the
    // file, so make sure we have a reader that has an internal offset of 0 so
    // absolute offsets can be used directly.
    final reader = originalReader.shrink(originalReader.offset);
    final header = UniversalBinaryHeader.fromReader(reader);
    if (header == null) {
      return null;
    }
    final arches = <CpuType, UniversalBinaryArch>{};
    final contents = <UniversalBinaryArch, MachO>{};
    for (final arch in header._arches) {
      final cpuType = CpuType.fromCode(arch.cputype);
      if (cpuType == null) continue;
      final archReader = arch.shrink(reader);
      final macho = MachO.fromReader(archReader);
      // The MachO parser either failed (likely due to a lack of debugging
      // information) or this contains debugging information for something other
      // than a Dart snapshot (since it contains neither a VM or isolate
      // instruction section symbol).
      if ((macho == null) ||
          ((macho.vmStartAddress == null) &&
              (macho.isolateStartAddress == null))) {
        continue;
      }
      if (!arches.containsKey(cpuType)) {
        arches[cpuType] = arch;
      } else if (macho.isDSYM) {
        // Always take a dSYM section above a non-dSYM section. If there are
        // multiple dSYM sections for some reason, the last one read is fine.
        arches[cpuType] = arch;
      } else if (!contents[arches[cpuType]!]!.hasDwarf) {
        // If the old section didn't have DWARF information but the new one
        // does, take it instead.
        arches[cpuType] = arch;
      }
      contents[arch] = macho;
    }
    return UniversalBinary._(header, arches, contents);
  }

  static UniversalBinary? fromFile(String fileName) =>
      UniversalBinary.fromReader(Reader.fromFile(MachO.handleDSYM(fileName)));

  Iterable<CpuType> get architectures => _arches.keys;

  Reader? readerForCpuType(Reader originalReader, CpuType cpuType) {
    final arch = _arches[cpuType];
    if (arch == null) return null;
    final macho = _contents[arch]!;
    // Universal binary files contain absolute offsets from the start of the
    // file, so make sure to feed arch.shrink a reader that has an internal
    // offset of 0.
    return macho.applyWordSizeAndEndian(
        arch.shrink(originalReader.shrink(originalReader.offset)));
  }

  DwarfContainer? containerForCpuType(CpuType cpuType) {
    final arch = _arches[cpuType];
    if (arch == null) return null;
    return _contents[arch];
  }

  void writeToStringBuffer(StringBuffer buffer) {
    buffer
      ..writeln('----------------------------------------')
      ..writeln('       Universal Binary Header')
      ..writeln('----------------------------------------')
      ..writeln('');
    _header.writeToStringBuffer(buffer);
    for (final cpuType in _arches.keys) {
      buffer
        ..writeln('')
        ..writeln('')
        ..writeln('----------------------------------------------------------')
        ..writeln('          Selected Mach-O Contents for $cpuType')
        ..writeln('----------------------------------------------------------')
        ..writeln('');
      _contents[_arches[cpuType]!]!.writeToStringBuffer(buffer);
    }
  }

  @override
  String toString() {
    final buffer = StringBuffer();
    writeToStringBuffer(buffer);
    return buffer.toString();
  }
}

const _magicByteOffset = 0;
const _cpuTypeByteOffset = 4;
const _fileTypeByteOffset = 12;

/// Used by certain Dart tests to create a MachO file that only contains a
/// header for the given architecture.
Uint8List? emptyMachOForArchitecture(String dartName) {
  final cpuType = CpuType.fromDartName(dartName);
  if (cpuType == null) return null;
  // 4 bytes * 7 fields + 4 padding bytes on 64-bit architectures.
  final contents = Uint8List(32);
  // We'll leave most of the fields at 0.
  final byteData = ByteData.sublistView(contents);
  // Use the host endian magic number marker corresponding to the bit size.
  byteData.setUint32(
      _magicByteOffset,
      cpuType.is64Bit ? MachOHeader._MH_MAGIC_64 : MachOHeader._MH_MAGIC,
      Endian.host);
  byteData.setUint32(_cpuTypeByteOffset, cpuType.code, Endian.host);
  // Just to set it to a valid file type, even though there are no contents.
  byteData.setUint32(_fileTypeByteOffset, MachOHeader._MH_DSYM, Endian.host);
  return contents;
}
