///|
/// Byte-level primitives and shared ZIP format constants: the little-endian
/// cursor, fixed-width field readers, the end-of-central scan, and
/// cancellation-aware owned copies.

///|
pub const LOCAL_HEADER_SIG : UInt = 0x04034b50

///|
pub const CENTRAL_HEADER_SIG : UInt = 0x02014b50

///|
pub const END_OF_CENTRAL_SIG : UInt = 0x06054b50

///|
pub const DATA_DESCRIPTOR_SIG : UInt = 0x08074b50

///|
pub const ZIP64_EOCD_SIG : UInt = 0x06064b50

///|
pub const ZIP64_LOCATOR_SIG : UInt = 0x07064b50

///|
pub const FLAG_DATA_DESCRIPTOR : Int = 0x0008

///|
pub const FLAG_UTF8 : Int = 0x0800

///|
pub const FLAG_ENCRYPTED : Int = 0x0001

///|
pub const FLAG_DEFLATE_STRENGTH : Int = 0x0006

///|
pub const ZIP64_EXTRA_ID : Int = 0x0001

///|
pub const MAX_U32 : UInt = 0xFFFFFFFF

///|
priv struct Cursor {
  bytes : BytesView
  mut pos : Int
}

///|
fn Cursor::Cursor(bytes : BytesView, pos : Int) -> Cursor {
  { bytes, pos, }
}

///|
fn Cursor::read_u8(self : Cursor) -> Int raise ZipError {
  if self.pos < 0 || self.pos >= self.bytes.length() {
    raise ZipError(Truncated, "zip: truncated header")
  }
  let value = self.bytes[self.pos].to_int()
  self.pos = self.pos + 1
  value
}

///|
fn Cursor::read_u16(self : Cursor) -> Int raise ZipError {
  let b0 = self.read_u8()
  let b1 = self.read_u8()
  b0 | (b1 << 8)
}

///|
fn Cursor::read_u32_raw(self : Cursor) -> UInt raise ZipError {
  let b0 = self.read_u8()
  let b1 = self.read_u8()
  let b2 = self.read_u8()
  let b3 = self.read_u8()
  b0.reinterpret_as_uint() |
  (b1.reinterpret_as_uint() << 8) |
  (b2.reinterpret_as_uint() << 16) |
  (b3.reinterpret_as_uint() << 24)
}

///|
fn Cursor::read_u64_raw(self : Cursor) -> UInt64 raise ZipError {
  let lo = self.read_u32_raw()
  let hi = self.read_u32_raw()
  (UInt64::extend_uint(hi) << 32) | UInt64::extend_uint(lo)
}

///|
fn Cursor::read_bytes(self : Cursor, len : Int) -> BytesView raise ZipError {
  if len < 0 || self.pos < 0 || len > self.bytes.length() - self.pos {
    raise ZipError(Truncated, "zip: truncated field")
  }
  let start = self.pos
  self.pos = self.pos + len
  self.bytes[start:self.pos]
}

///|
fn Cursor::skip(self : Cursor, len : Int) -> Unit raise ZipError {
  ignore(self.read_bytes(len))
}

///|
fn read_u16_le_at(bytes : BytesView, offset : Int) -> Int raise ZipError {
  if offset < 0 || 2 > bytes.length() - offset {
    raise ZipError(Truncated, "zip: truncated field")
  }
  let b0 = bytes[offset].to_int()
  let b1 = bytes[offset + 1].to_int()
  b0 | (b1 << 8)
}

///|
fn read_u32_le_at_raw(bytes : BytesView, offset : Int) -> UInt raise ZipError {
  if offset < 0 || 4 > bytes.length() - offset {
    raise ZipError(Truncated, "zip: truncated field")
  }
  bytes[offset].to_uint() |
  (bytes[offset + 1].to_uint() << 8) |
  (bytes[offset + 2].to_uint() << 16) |
  (bytes[offset + 3].to_uint() << 24)
}

///|
fn read_u64_le_at(bytes : BytesView, offset : Int) -> UInt64 raise ZipError {
  if offset < 0 || 8 > bytes.length() - offset {
    raise ZipError(Truncated, "zip: truncated field")
  }
  let mut result : UInt64 = 0
  for i in 0..<8 {
    let b = bytes[offset + i].to_uint()
    result = result | (UInt64::extend_uint(b) << (i * 8))
  }
  result
}

///|
/// Locate the end-of-central-directory record by scanning the conventional
/// last (65535 + 22) bytes for the signature whose comment length reaches the
/// end of the input. Returns its offset, or `None`.
fn find_end_of_central(bytes : BytesView) -> Int? raise ZipError {
  let len = bytes.length()
  if len < 22 {
    return None
  }
  let max_scan = 22 + 0xFFFF
  let min_offset = (len - max_scan).max(0)
  for i in (len - 22)>=..min_offset {
    if read_u32_le_at_raw(bytes, i) == END_OF_CENTRAL_SIG &&
      read_u16_le_at(bytes, i + 20) == len - i - 22 {
      return Some(i)
    }
  } nobreak {
    None
  }
}

///|
fn decode_name(bytes : BytesView) -> String raise ZipError {
  @encoding/utf8.decode(bytes) catch {
    _ => raise ZipError(InvalidUtf8, "zip: entry name is not UTF-8")
  }
}

///|
fn ensure_flags_supported(flags : Int) -> Unit raise ZipError {
  if (flags & FLAG_ENCRYPTED) != 0 {
    raise ZipError(
      UnsupportedFeature,
      "zip: encrypted entries are not supported",
    )
  }
  let supported = FLAG_DATA_DESCRIPTOR | FLAG_UTF8 | FLAG_DEFLATE_STRENGTH
  let unsupported = flags & supported.lnot()
  if unsupported != 0 {
    raise ZipError(
      UnsupportedFeature,
      "zip: unsupported entry flags: 0x\{unsupported.to_string(radix=16)}",
    )
  }
}

///|
fn method_from_code(method_id : Int) -> Compression raise ZipError {
  match method_id {
    0 => Store
    8 => Deflate
    _ =>
      raise ZipError(
        UnsupportedCompression(method_id),
        "zip: unsupported compression method \{method_id}",
      )
  }
}

///|
/// Copies `bytes` into owned storage, polling `cancelled` every 64 KiB so a
/// large source record cannot hide behind a monolithic runtime copy.
fn copy_cancellable(
  bytes : BytesView,
  cancelled : () -> Bool,
) -> Bytes raise ZipError {
  check_cancelled(cancelled)
  let output : UninitializedArray[Byte] = UninitializedArray::make(
    bytes.length(),
  )
  for index in 0.. Bytes = "%identity"