// Typed load commands.
//
// `macho.mbt` frames the command table; this file gives the framing meaning.
// The segment, symbol and string tables are decoded into structs. Every other
// command is left as a `RawCommand`.

///|
/// `LC_SYMTAB`: where the symbol table and its string table live.
///
/// `nlist` entries do not carry their own names; each holds a 32-bit offset
/// into the string table at `stroff`.
pub(all) struct SymtabCommand {
  /// `symoff`: absolute offset of the first `nlist`.
  symoff : Int64
  /// `nsyms`: number of `nlist` entries.
  nsyms : UInt32
  /// `stroff`: absolute offset of the string table.
  stroff : Int64
  /// `strsize`: byte length of the string table.
  strsize : UInt32
}

///|
/// One `nlist_64` entry.
///
/// `name` is resolved against the string table rather than left as an offset,
/// because every caller wants the name and none wants the offset.
pub(all) struct Symbol {
  /// Symbol name, decoded as UTF-8.
  name : String
  /// `n_type`: the `N_*` type bits plus `N_EXT`/`N_PEXT`.
  n_type : UInt32
  /// `n_sect`: 1-based section ordinal, or 0 when not section-relative.
  n_sect : UInt32
  /// `n_desc`: reference flags and, for undefined symbols, the library ordinal.
  n_desc : UInt32
  /// `n_value`: address, or offset, depending on the symbol's type.
  n_value : Int64
}

///|
/// `N_STAB`: bits set mean this is a debugging entry, not a linkable symbol.
pub const N_STAB : UInt32 = 0xE0

///|
/// `N_TYPE`: the mask selecting a symbol's kind.
pub const N_TYPE : UInt32 = 0x0E

///|
/// `N_EXT`: external symbol.
pub const N_EXT : UInt32 = 0x01

///|
/// Whether the entry is a debug entry rather than a linkable symbol.
///
/// `nm` hides these by default; a tool that wants to match `nm` must filter
/// the same way.
pub fn Symbol::is_debug(self : Symbol) -> Bool {
  (self.n_type & N_STAB) != 0U
}

///|
/// Whether the symbol is visible outside its object.
pub fn Symbol::is_external(self : Symbol) -> Bool {
  (self.n_type & N_EXT) != 0U
}

///|
/// The `n_type` discriminator with the `N_EXT` flag removed.
pub fn Symbol::kind(self : Symbol) -> UInt32 {
  self.n_type & N_TYPE
}

///|
/// Load-command numbers this library decodes into typed values.
///
/// The full list is in ``; only the ones that carry data the
/// rest of the library exposes are named here.
pub const LC_SEGMENT : UInt32 = 0x1

///|
/// 64-bit variant of `LC_SEGMENT`.
pub const LC_SEGMENT_64 : UInt32 = 0x19

///|
/// Symbol and string table locations.
pub const LC_SYMTAB : UInt32 = 0x2

///|
/// String table alone, used by object files with no symbols.
pub const LC_DYSYMTAB : UInt32 = 0xB

///|
/// Looks up a command by number and returns its payload region.
///
/// The payload excludes the 8-byte generic header and is checked to lie inside
/// the command's own `cmdsize`, so a decoder can read fields without repeating
/// the framing checks.
fn command_payload(
  slice : MachSlice,
  cmd : RawCommand,
) -> Bounds raise MachscopeError {
  let start = cmd.offset + 8L
  let size = cmd.size.to_int64() - 8L
  slice.bounds.require(start, size, "command payload")
  // `start` moves to the payload but `limit` stays at the slice's end: embedded
  // tables such as a segment's section list are part of the command's
  // `cmdsize`, which the framing already bounded, and capping the view at the
  // command's end here would double-count that check and reject valid tables.
  Bounds::{ bytes: slice.bounds.bytes, start, limit: slice.bounds.limit, }
}

///|
/// Decodes an `LC_SYMTAB` command.
pub fn symtab_command(
  slice : MachSlice,
  cmd : RawCommand,
) -> SymtabCommand raise MachscopeError {
  if cmd.cmd != LC_SYMTAB {
    raise BadValue(field="LC_SYMTAB cmd", value=cmd.cmd.to_int64())
  }
  let body = command_payload(slice, cmd)
  let cur = Cursor::{ pos: body.start, endian: slice.header.kind.endian(), }
  let (symoff, cur) = body.read_u32(cur)
  let (nsyms, cur) = body.read_u32(cur)
  let (stroff, cur) = body.read_u32(cur)
  let (strsize, _) = body.read_u32(cur)
  // `symoff` and `stroff` are file offsets relative to the start of the image,
  // not to the start of the file. In a thin image those are the same number; in
  // a slice of a universal binary they differ by the slice's own offset, so the
  // slice base has to be added or every name lookup lands in the wrong place.
  let image_base = slice.bounds.start
  let symtab = SymtabCommand::{
    symoff: image_base + symoff.to_int64(),
    nsyms,
    stroff: image_base + stroff.to_int64(),
    strsize,
  }
  // Both tables must be readable, and the symbol table must fit: 16 bytes per
  // `nlist_64`. Checking here means `symbols()` can bound its loop by `nsyms`
  // without re-validating.
  slice.bounds.require(
    symtab.symoff,
    symtab.nsyms.to_int64() * 16L,
    "symbol table",
  )
  slice.bounds.require(symtab.stroff, symtab.strsize.to_int64(), "string table")
  symtab
}

///|
/// Finds the first `LC_SYMTAB` in a slice.
pub fn find_symtab(slice : MachSlice) -> SymtabCommand? raise MachscopeError {
  for cmd in slice.commands {
    if cmd.cmd == LC_SYMTAB {
      return Some(symtab_command(slice, cmd))
    }
  }
  None
}

///|
/// Reads the NUL-terminated string at `offset` inside the string table.
///
/// Mach-O string tables are sequences of NUL-terminated UTF-8 strings; an
/// offset that does not land on a terminator before the end of the table means
/// the table is corrupt, which is reported rather than truncated silently.
pub fn string_table_entry(
  slice : MachSlice,
  tab : SymtabCommand,
  offset : UInt32,
) -> String raise MachscopeError {
  let start = tab.stroff + offset.to_int64()
  let limit = tab.stroff + tab.strsize.to_int64()
  if start < tab.stroff || start >= limit {
    raise OutOfBounds(what="symbol name", offset=start, size=1L, limit~)
  }
  let buf = StringBuilder()
  let mut at = start
  while at < limit {
    let b = slice.bounds.bytes.at(at.to_int())
    if b.to_int() == 0 {
      return buf.to_string()
    }
    // Names are UTF-8; each byte below 0x80 is one code point. Bytes with the
    // high bit set begin a multi-byte sequence, which is decoded as Latin-1 so
    // that a non-ASCII name still yields a distinct string rather than being
    // collapsed into replacement characters.
    buf.write_char(b.to_char())
    at = at + 1
  }
  raise OutOfBounds(
    what="unterminated symbol name",
    offset=start,
    size=1L,
    limit~,
  )
}

///|
/// Decodes every `nlist_64` in the symbol table.
///
/// The count comes from `nsyms` and the loop is bounded by it, which is safe
/// because `symtab_command` already checked that all `nsyms` entries are
/// readable.
pub fn symbols(slice : MachSlice) -> Array[Symbol] raise MachscopeError {
  let tab = match find_symtab(slice) {
    Some(t) => t
    None => return []
  }
  let endian = slice.header.kind.endian()
  let out : Array[Symbol] = []
  let mut at = tab.symoff
  for _ in 0..