///|
pub const NEWC_HEADER_SIZE : Int32 = 110

///|
/// `newc` archive variants.
pub(all) enum NewcFormat {
  Newc
  NewcCrc
} derive(Eq, Debug)

///|
/// Errors reported while parsing a `newc` cpio archive.
///
/// Numeric payloads are byte offsets into the original input.
pub(all) enum CpioError {
  TruncatedHeader(Int32)
  UnsupportedMagic(Int32)
  InvalidHex(Int32)
  InvalidOctal(Int32)
  InvalidNameSize(Int32)
  TruncatedName(Int32)
  MissingNameTerminator(Int32)
  EmbeddedNul(Int32)
  InvalidUtf8Name(Int32)
  TruncatedData(Int32)
  ChecksumMismatch(Int32, UInt64, UInt64)
  MixedFormats(Int32)
  InvalidTrailer(Int32)
} derive(Eq, Debug)

///|
/// Parsed `newc` header fields.
pub(all) struct Header {
  format : NewcFormat
  ino : UInt64
  mode : UInt64
  uid : UInt64
  gid : UInt64
  nlink : UInt64
  mtime : UInt64
  file_size : UInt64
  dev_major : UInt64
  dev_minor : UInt64
  rdev_major : UInt64
  rdev_minor : UInt64
  name_size : UInt64
  check : UInt64
} derive(Eq, Debug)

///|
/// One archive entry. `name_bytes` preserves the exact bytes from the archive,
/// while `name` is the validated UTF-8 representation.
pub(all) struct Entry {
  name : String
  name_bytes : Bytes
  header : Header
  data : Bytes
} derive(Eq, Debug)

///|
/// A successfully parsed archive.
pub(all) struct Archive {
  format : NewcFormat
  entries : Array[Entry]
} derive(Eq, Debug)

///|
fn hex_value(byte : Byte) -> UInt64? {
  if byte >= b'0' && byte <= b'9' {
    Some(byte.to_uint64() - b'0'.to_uint64())
  } else if byte >= b'a' && byte <= b'f' {
    Some(byte.to_uint64() - b'a'.to_uint64() + 10)
  } else if byte >= b'A' && byte <= b'F' {
    Some(byte.to_uint64() - b'A'.to_uint64() + 10)
  } else {
    None
  }
}

///|
fn parse_hex_field(
  header : BytesView,
  field_index : Int32,
  field_offset : Int32,
) -> Result[UInt64, CpioError] {
  let mut value = 0UL
  for i = 0; i < 8; i = i + 1 {
    let byte_offset = 6 + field_index * 8 + i
    let digit = match hex_value(header[byte_offset]) {
      Some(value) => value
      None => return Err(InvalidHex(field_offset + byte_offset))
    }
    value = (value << 4) | digit
  }
  Ok(value)
}

///|
fn parse_header(
  header : BytesView,
  archive_offset : Int32,
) -> Result[Header, CpioError] {
  let format = if header[0:6] == b"070701" {
    NewcFormat::Newc
  } else if header[0:6] == b"070702" {
    NewcFormat::NewcCrc
  } else {
    return Err(UnsupportedMagic(archive_offset))
  }
  let fields : Array[UInt64] = []
  for index = 0; index < 13; index = index + 1 {
    let value = match parse_hex_field(header, index, archive_offset) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    fields.push(value)
  }
  Ok({
    format,
    ino: fields[0],
    mode: fields[1],
    uid: fields[2],
    gid: fields[3],
    nlink: fields[4],
    mtime: fields[5],
    file_size: fields[6],
    dev_major: fields[7],
    dev_minor: fields[8],
    rdev_major: fields[9],
    rdev_minor: fields[10],
    name_size: fields[11],
    check: fields[12],
  })
}

///|
fn add_checksum_byte(sum : UInt64, byte : Byte) -> UInt64 {
  (sum + byte.to_uint64()) & 0xFFFFFFFFUL
}

///|
/// Computes the byte-sum value stored in a `070702` newc `check` field.
///
/// Despite the format's "CRC" name, the field is not a cyclic redundancy
/// check. It is the low 32 bits of the unsigned sum of all data bytes.
pub fn newc_checksum(data : BytesView) -> UInt64 {
  let mut sum = 0UL
  for byte in data {
    sum = add_checksum_byte(sum, byte)
  }
  sum
}

///|
fn padding_for(length : Int32) -> Int32 {
  (4 - length % 4) % 4
}

///|
fn newc_name_padding(name_size : Int32) -> Int32 {
  padding_for(NEWC_HEADER_SIZE + name_size)
}

///|
/// Parses a SVR4 `newc` cpio archive (`070701`).
///
/// The parser is read-only and does not extract to the filesystem. Every
/// length and field is checked before conversion to an index, so malformed
/// archives return `Err` instead of panicking.
pub fn parse_newc(input : Bytes) -> Result[Archive, CpioError] {
  let entries : Array[Entry] = []
  let mut format = NewcFormat::Newc
  let mut has_format = false
  let mut offset = 0
  while true {
    let header_end = offset + NEWC_HEADER_SIZE
    if header_end > input.length() {
      return Err(TruncatedHeader(offset))
    }
    let header = match parse_header(input[offset:header_end], offset) {
      Ok(header) => header
      Err(error) => return Err(error)
    }
    if has_format {
      if header.format != format {
        return Err(MixedFormats(offset))
      }
    } else {
      format = header.format
      has_format = true
    }

    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 + newc_name_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))
      }
      if header.format == NewcCrc && header.check != 0UL {
        return Err(ChecksumMismatch(offset, header.check, 0UL))
      }
      return Ok({ format, 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 + padding_for(file_size)
    if next_offset > input.length() {
      return Err(TruncatedData(data_end))
    }
    let data = input[data_start:data_end].to_owned()
    if header.format == NewcCrc {
      let actual = newc_checksum(data[:])
      if header.check != actual {
        return Err(ChecksumMismatch(data_start, header.check, actual))
      }
    }
    entries.push({ name, name_bytes, header, data, })
    offset = next_offset
  } nobreak {
    abort("unreachable: parse_newc always returns")
  }
}