///|
pub const ODC_HEADER_SIZE : Int32 = 76

///|
/// Parsed `odc` header fields.
pub(all) struct OdcHeader {
  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 `odc` archive entry.
pub(all) struct OdcEntry {
  name : String
  name_bytes : Bytes
  header : OdcHeader
  data : Bytes
} derive(Eq, Debug)

///|
/// A successfully parsed `odc` archive.
pub(all) struct OdcArchive {
  entries : Array[OdcEntry]
} derive(Eq, Debug)

///|
fn octal_value(byte : Byte) -> UInt64? {
  if byte >= b'0' && byte <= b'7' {
    Some(byte.to_uint64() - b'0'.to_uint64())
  } else {
    None
  }
}

///|
fn parse_octal_field(
  header : BytesView,
  start : Int32,
  width : Int32,
  archive_offset : Int32,
) -> Result[UInt64, CpioError] {
  let mut value = 0UL
  for i = 0; i < width; i = i + 1 {
    let offset = start + i
    let digit = match octal_value(header[offset]) {
      Some(value) => value
      None => return Err(InvalidOctal(archive_offset + offset))
    }
    value = (value << 3) | digit
  }
  Ok(value)
}

///|
fn parse_odc_header(
  header : BytesView,
  archive_offset : Int32,
) -> Result[OdcHeader, CpioError] {
  if header[0:6] != b"070707" {
    return Err(UnsupportedMagic(archive_offset))
  }
  let layout : Array[(Int32, Int32)] = [
    (6, 6),
    (12, 6),
    (18, 6),
    (24, 6),
    (30, 6),
    (36, 6),
    (42, 6),
    (48, 11),
    (59, 6),
    (65, 11),
  ]
  let fields : Array[UInt64] = []
  for (start, width) in layout {
    let value = match parse_octal_field(header, start, width, archive_offset) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    fields.push(value)
  }
  Ok({
    dev: fields[0],
    ino: fields[1],
    mode: fields[2],
    uid: fields[3],
    gid: fields[4],
    nlink: fields[5],
    rdev: fields[6],
    mtime: fields[7],
    name_size: fields[8],
    file_size: fields[9],
  })
}

///|
/// Parses a portable ASCII `odc` cpio archive (`070707`).
///
/// The parser is read-only. `odc` has no padding between the name, data, or
/// following header, so every entry starts immediately after the prior data.
pub fn parse_odc(input : Bytes) -> Result[OdcArchive, CpioError] {
  let entries : Array[OdcEntry] = []
  let mut offset = 0
  while true {
    let header_end = offset + ODC_HEADER_SIZE
    if header_end > input.length() {
      return Err(TruncatedHeader(offset))
    }
    let header = match parse_odc_header(input[offset:header_end], 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))
    }

    if name == "TRAILER!!!" {
      if header.file_size != 0UL {
        return Err(InvalidTrailer(offset))
      }
      return Ok({ entries, })
    }

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