///|
pub const BINARY_HEADER_SIZE : Int32 = 26

///|
/// Binary cpio layout. The two variants differ in file type semantics, while
/// old and new little-endian headers share the same physical encoding.
pub(all) enum BinaryVariant {
  Old
  New
} derive(Eq, Debug)

///|
/// 16-bit word byte order used by new binary cpio.
pub(all) enum BinaryByteOrder {
  Little
  Big
} derive(Eq, Debug)

///|
/// Parsed binary cpio header fields.
pub(all) struct BinaryHeader {
  variant : BinaryVariant
  dev : UInt64
  ino : UInt64
  mode : UInt64
  uid : UInt64
  gid : UInt64
  nlink : UInt64
  rdev : UInt64
  mtime : UInt64
  file_size : UInt64
  name_size : UInt64
} derive(Eq, Debug)

///|
/// One binary cpio entry.
pub(all) struct BinaryEntry {
  name : String
  name_bytes : Bytes
  header : BinaryHeader
  data : Bytes
} derive(Eq, Debug)

///|
/// A successfully parsed binary cpio archive.
pub(all) struct BinaryArchive {
  variant : BinaryVariant
  byte_order : BinaryByteOrder
  entries : Array[BinaryEntry]
} derive(Eq, Debug)

///|
fn binary_padding(length : Int32) -> Int32 {
  length % 2
}

///|
fn read_u16(
  input : BytesView,
  offset : Int32,
  byte_order : BinaryByteOrder,
) -> UInt64 {
  let first = input[offset].to_uint64()
  let second = input[offset + 1].to_uint64()
  if byte_order == Little {
    first | (second << 8)
  } else {
    (first << 8) | second
  }
}

///|
fn read_u32(
  input : BytesView,
  offset : Int32,
  byte_order : BinaryByteOrder,
) -> UInt64 {
  let high = read_u16(input, offset, byte_order)
  let low = read_u16(input, offset + 2, byte_order)
  (high << 16) | low
}

///|
fn parse_binary_header(
  header : BytesView,
  variant : BinaryVariant,
  byte_order : BinaryByteOrder,
  archive_offset : Int32,
) -> Result[BinaryHeader, CpioError] {
  if read_u16(header, 0, byte_order) != 0x71C7UL {
    return Err(UnsupportedMagic(archive_offset))
  }
  Ok({
    variant,
    dev: read_u16(header, 2, byte_order),
    ino: read_u16(header, 4, byte_order),
    mode: read_u16(header, 6, byte_order),
    uid: read_u16(header, 8, byte_order),
    gid: read_u16(header, 10, byte_order),
    nlink: read_u16(header, 12, byte_order),
    rdev: read_u16(header, 14, byte_order),
    mtime: read_u32(header, 16, byte_order),
    name_size: read_u16(header, 20, byte_order),
    file_size: read_u32(header, 22, byte_order),
  })
}

///|
fn parse_binary(
  input : Bytes,
  variant : BinaryVariant,
  byte_order : BinaryByteOrder,
) -> Result[BinaryArchive, CpioError] {
  let entries : Array[BinaryEntry] = []
  let mut offset = 0
  while true {
    let header_end = offset + BINARY_HEADER_SIZE
    if header_end > input.length() {
      return Err(TruncatedHeader(offset))
    }
    let header = match
      parse_binary_header(input[offset:header_end], variant, byte_order, offset) {
      Ok(header) => header
      Err(error) => return Err(error)
    }

    let name_start = header_end
    let remaining = (input.length() - name_start).to_uint64()
    if header.name_size == 0UL || header.name_size > remaining {
      return Err(InvalidNameSize(offset))
    }
    let name_size = header.name_size.to_int()
    let name_end = name_start + name_size
    if input[name_end - 1] != b'\x00' {
      return Err(MissingNameTerminator(name_start))
    }
    let name_view = input[name_start:name_end - 1]
    for i = 0; i < name_view.length(); i = i + 1 {
      if name_view[i] == b'\x00' {
        return Err(EmbeddedNul(name_start + i))
      }
    }
    let name_bytes = name_view.to_owned()
    let name = @utf8.decode(name_view) catch {
      _ => return Err(InvalidUtf8Name(name_start))
    }

    let data_start = name_end + binary_padding(name_size)
    if data_start > input.length() {
      return Err(TruncatedName(name_end))
    }
    if name == "TRAILER!!!" {
      if header.file_size != 0UL {
        return Err(InvalidTrailer(offset))
      }
      return Ok({ variant, byte_order, entries, })
    }

    let data_remaining = (input.length() - data_start).to_uint64()
    if header.file_size > data_remaining {
      return Err(TruncatedData(data_start))
    }
    let file_size = header.file_size.to_int()
    let data_end = data_start + file_size
    let next_offset = data_end + binary_padding(file_size)
    if next_offset > input.length() {
      return Err(TruncatedData(data_end))
    }
    let data = input[data_start:data_end].to_owned()
    entries.push({ name, name_bytes, header, data, })
    offset = next_offset
  } nobreak {
    abort("unreachable: parse_binary always returns")
  }
}

///|
/// Parses a PWB old binary cpio archive with PDP little-endian words.
pub fn parse_binary_old(input : Bytes) -> Result[BinaryArchive, CpioError] {
  parse_binary(input, Old, Little)
}

///|
/// Parses a new binary cpio archive with caller-specified word byte order.
///
/// Auto-detection between old and new layouts is intentionally not attempted:
/// the formats use the same magic and can share identical little-endian bytes.
pub fn parse_binary_new(
  input : Bytes,
  byte_order : BinaryByteOrder,
) -> Result[BinaryArchive, CpioError] {
  parse_binary(input, New, byte_order)
}