///|
priv suberror InvalidArchive {
  InvalidArchive(ParseError)
}

///|
fn[T] reject(
  code : String,
  offset : Int,
  message : String,
) -> T raise InvalidArchive {
  raise InvalidArchive({ code, offset, message, })
}

///|
// Header numbers use ASCII digits, with spaces permitted only at the ends.
fn number_field(
  data : Bytes,
  start : Int,
  width : Int,
  radix : Int,
  optional : Bool,
  limit : Int64,
) -> Int64? raise InvalidArchive {
  let mut first = start
  let mut last = start + width
  while first < last && data[first] == b' ' {
    first += 1
  }
  while last > first && data[last - 1] == b' ' {
    last -= 1
  }
  if first == last {
    if optional {
      return None
    }
    reject("AR004", start, "missing numeric field")
  }
  let mut value = 0L
  for i = first; i < last; i = i + 1 {
    let digit = data[i].to_int() - 48
    if digit < 0 || digit >= radix {
      reject("AR004", i, "invalid digit in archive header")
    }
    if value > (limit - digit.to_int64()) / radix.to_int64() {
      reject("AR005", start, "numeric field exceeds supported range")
    }
    value = value * radix.to_int64() + digit.to_int64()
  }
  Some(value)
}

///|
fn parse_records(
  data : Bytes,
  max_members : Int,
  max_name_bytes : Int,
) -> Archive raise InvalidArchive {
  if data.length() >= 8 && data[:8] == b"!\n"[:] {
    reject("AR007", 0, "thin archives are not supported")
  }
  if data.length() < 8 || data[:8] != b"!\n"[:] {
    reject("AR001", 0, "expected ar archive magic")
  }
  let records : Array[Member] = []
  let pending_names : Array[(Int, Int)] = []
  let wanted_names : Map[Int, Unit] = Map([])
  let mut long_table : (Int, Int)? = None
  let mut offset = 8
  while offset < data.length() {
    if records.length() >= max_members {
      reject("AR008", offset, "archive member limit exceeded")
    }
    if data.length() - offset < 60 {
      reject("AR002", offset, "truncated member header")
    }
    if data[offset + 58] != b'`' || data[offset + 59] != b'\n' {
      reject("AR003", offset + 58, "invalid member header trailer")
    }
    let mut name_end = offset + 16
    while name_end > offset && data[name_end - 1] == b' ' {
      name_end -= 1
    }
    let raw_name = data[offset:name_end].to_owned()
    if raw_name.is_empty() {
      reject("AR006", offset, "empty member name")
    }
    let mut kind = member_kind(raw_name)
    if kind == Regular && raw_name.has_prefix(b"/"[:]) {
      for i = 1; i < raw_name.length(); i = i + 1 {
        if raw_name[i] < b'0' || raw_name[i] > b'9' {
          reject("AR006", offset + i, "invalid GNU name reference")
        }
      }
      let reference = number_field(
          data,
          offset + 1,
          raw_name.length() - 1,
          10,
          false,
          2147483647L,
        )
        .unwrap()
        .to_int()
      pending_names.push((records.length(), reference))
      wanted_names[reference] = ()
    }
    let mut name = if kind == Regular && raw_name.has_suffix(b"/"[:]) {
      raw_name[:raw_name.length() - 1].to_owned()
    } else {
      raw_name
    }
    if name.is_empty() {
      reject("AR006", offset, "empty member name")
    }
    for byte in name {
      if byte == b'\x00' {
        reject("AR006", offset, "NUL inside member name")
      }
    }
    let timestamp = number_field(
      data,
      offset + 16,
      12,
      10,
      true,
      0x7fffffffffffffffL,
    )
    let uid = number_field(data, offset + 28, 6, 10, true, 0x7fffffffffffffffL)
    let gid = number_field(data, offset + 34, 6, 10, true, 0x7fffffffffffffffL)
    let mode = number_field(data, offset + 40, 8, 8, true, 0x7fffffffffffffffL)
    let stored_size = number_field(
        data,
        offset + 48,
        10,
        10,
        false,
        2147483647L,
      )
      .unwrap()
      .to_int()
    let mut data_offset = offset + 60
    if stored_size > data.length() - data_offset {
      reject("AR002", data_offset, "truncated member payload")
    }
    let end = data_offset + stored_size
    let padding = stored_size % 2
    if padding == 1 && (end == data.length() || data[end] != b'\n') {
      reject("AR003", end, "missing or invalid alignment byte")
    }
    if kind == NameTable {
      if long_table is Some(_) {
        reject("AR006", offset, "more than one GNU name table")
      }
      long_table = Some((data_offset, stored_size))
    }
    let mut size = stored_size
    if raw_name.has_prefix(b"#1/"[:]) {
      let name_size = embedded_name_size(
        data,
        offset + 3,
        raw_name.length() - 3,
      )
      if name_size == 0 || name_size > stored_size {
        reject("AR006", offset, "BSD name length is outside its record")
      }
      name = bsd_name(data, data_offset, name_size)
      data_offset += name_size
      size -= name_size
    }
    kind = member_kind(name)
    records.push({
      name,
      kind,
      header_offset: offset,
      data_offset,
      size,
      stored_size,
      metadata: { timestamp, uid, gid, mode, },
    })
    offset = end + padding
  }
  let long_names = match long_table {
    Some((start, size)) => Some(gnu_names(data, start, size, wanted_names))
    None => None
  }
  for (index, reference) in pending_names {
    let entry = records[index]
    match long_names {
      None =>
        reject(
          "AR006",
          entry.header_offset,
          "GNU name reference has no name table",
        )
      Some(names) =>
        match names.get(reference) {
          None =>
            reject(
              "AR006",
              entry.header_offset,
              "GNU name reference is not an entry boundary",
            )
          Some(name) =>
            records[index] = { ..entry, name, kind: member_kind(name), }
        }
    }
  }
  let mut total_name_bytes = 0
  for entry in records {
    if entry.name.length() > max_name_bytes - total_name_bytes {
      reject(
        "AR008",
        entry.header_offset,
        "expanded member-name byte limit exceeded",
      )
    }
    total_name_bytes += entry.name.length()
  }
  { data, records, indexes: [], }
}

///|
/// Read a regular ar archive. Limits include special records and the entire input.
/// Input bytes are immutable. Errors carry offsets into that input.
pub fn parse(
  input : Bytes,
  max_members? : Int = 100000,
  max_bytes? : Int = 67108864,
  max_name_bytes? : Int = 67108864,
  max_symbols? : Int = 1000000,
  max_symbol_name_bytes? : Int = 67108864,
) -> Result[Archive, ParseError] {
  if max_members < 0 ||
    max_bytes < 0 ||
    max_name_bytes < 0 ||
    max_symbols < 0 ||
    max_symbol_name_bytes < 0 {
    return Err({
      code: "AR008",
      offset: 0,
      message: "limits must be nonnegative",
    })
  }
  if input.length() > max_bytes {
    return Err({
      code: "AR008",
      offset: 0,
      message: "archive byte limit exceeded",
    })
  }
  try {
    let archive = parse_records(input, max_members, max_name_bytes)
    let indexes = parse_symbol_indexes(
      archive, max_symbols, max_symbol_name_bytes,
    )
    Ok({ ..archive, indexes, })
  } catch {
    InvalidArchive(error) => Err(error)
  }
}