///|
/// Read a ZIP32/JAR without extracting files. Validate all entry paths and sizes
/// before decoding. Stored and deflated file contents are read on demand.
pub fn EntryTree::zip(
  data : Bytes,
  origin : Location,
  limits~ : Limits = Limits::new(),
) -> EntryTree raise ResourceError {
  check_limits(limits, origin)
  if data.length() > limits.max_archive_bytes {
    raise ResourceError(
      ResourceLimit,
      Provenance::new(origin),
      "Archive exceeds byte limit",
    )
  }
  archive_validate_bounds(data, origin, limits)
  let archive = @zipc.Archive::from_bytes(data) catch {
    @zipc.ZipError(message) => raise archive_error(origin, message)
  }
  let members = archive.to_array()
  let paths = members.filter_map(member_ => {
    if member_.is_dir() {
      None
    } else {
      Some(EntryPath::parse(member_.path()).unwrap())
    }
  })
  paths.sort()
  let read = fn(path : EntryPath) -> Bytes raise ResourceError {
    guard archive.find(path.to_string()) is Some(member_) &&
      member_.kind() is @zipc.File(file) else {
      raise ResourceError(
        MissingResource,
        Provenance::new(origin.archive_entry(path)),
        "Expected a file",
      )
    }
    let bytes = match file.compression() {
      @zipc.Stored => file.data
      @zipc.Deflate =>
        inflate_bounded(
          file.data,
          file.decompressed_size(),
          origin.archive_entry(path),
        )
      _ =>
        raise archive_error(
          origin.archive_entry(path),
          "Unsupported compression method",
        )
    }
    if bytes.length() != file.decompressed_size() ||
      @checksum.crc32(bytes).reinterpret_as_int() != file.decompressed_crc32() {
      raise ResourceError(
        IntegrityFailure,
        Provenance::new(origin.archive_entry(path)),
        "ZIP size or CRC-32 mismatch",
      )
    }
    bytes
  }
  {
    origin,
    provenance: Provenance::new(origin),
    paths,
    members: Set(members.map(member_ => member_.path())),
    read_entry: read,
    limits,
    locate_entry: path => origin.archive_entry(path),
    digest: Some(ContentDigest::sha256(data)),
  }
}

///|
// Use the streaming decoder to enforce the declared size while inflating, even
// when a corrupt entry lies about its uncompressed length.
fn inflate_bounded(
  data : Bytes,
  expected : Int32,
  origin : Location,
) -> Bytes raise ResourceError {
  let inflater = @flate.Inflater::new()
  let buffer : FixedArray[Byte] = FixedArray::make(8192, b'\x00')
  let output : Array[Byte] = []
  let mut offset = 0
  while true {
    let (status, consumed, written) = inflater.step(
      data[offset:],
      buffer.mut_view(),
    ) catch {
      _ => raise archive_error(origin, "Invalid deflated ZIP entry")
    }
    if written > expected - output.length() {
      raise ResourceError(
        IntegrityFailure,
        Provenance::new(origin),
        "ZIP entry exceeds declared size",
      )
    }
    for i in 0.. Unit raise ResourceError {
  fn range(start : Int32, size : Int32) -> Unit raise ResourceError {
    if start < 0 ||
      size < 0 ||
      size > data.length() ||
      start > data.length() - size {
      raise archive_error(
        origin, "Truncated ZIP entry or invalid archive offset",
      )
    }
  }
  fn u16(at : Int32) -> Int32 {
    data[at].to_int() | (data[at + 1].to_int() << 8)
  }
  fn u32(at : Int32) -> Int32 raise ResourceError {
    if data[at + 3].to_int() >= 128 {
      raise archive_error(
        origin, "ZIP64 or oversized ZIP32 archives are not supported",
      )
    }
    data[at].to_int() |
    (data[at + 1].to_int() << 8) |
    (data[at + 2].to_int() << 16) |
    (data[at + 3].to_int() << 24)
  }
  if data.length() < 22 || !@zipc.bytes_has_magic(data) {
    raise archive_error(origin, "Not a ZIP archive")
  }
  let mut end : Int32? = None
  let mut at = data.length() - 22
  let limit = if at > 65535 { at - 65535 } else { 0 }
  while at >= limit {
    if data[at] == 0x50 &&
      data[at + 1] == 0x4b &&
      data[at + 2] == 0x05 &&
      data[at + 3] == 0x06 {
      end = Some(at)
      break
    }
    at -= 1
  }
  guard end is Some(end) else {
    raise archive_error(origin, "Missing ZIP end of central directory")
  }
  if u16(end + 4) != 0 || u16(end + 6) != 0 || u16(end + 8) != u16(end + 10) {
    raise archive_error(origin, "Multi-disk ZIP archives are not supported")
  }
  range(end + 22, u16(end + 20))
  let count = u16(end + 10)
  if count > limits.max_entries {
    raise ResourceError(
      ResourceLimit,
      Provenance::new(origin),
      "Too many ZIP entries",
    )
  }
  let seen : Set[String] = Set([])
  let mut total = 0
  let size = u32(end + 12)
  let directory = u32(end + 16)
  range(directory, size)
  if directory + size > end {
    raise archive_error(origin, "Invalid ZIP central directory range")
  }
  let mut offset = directory
  for _ in 0.. limits.max_entry_bytes ||
      expanded > limits.max_total_bytes - total {
      raise ResourceError(
        ResourceLimit,
        Provenance::new(origin),
        "ZIP entries exceed byte limit",
      )
    }
    total += expanded
    let entry_size = 46 + u16(offset + 28) + u16(offset + 30) + u16(offset + 32)
    range(offset, entry_size)
    let name = @utf8.decode(
      data[offset + 46:offset + 46 + u16(offset + 28)].to_owned(),
    ) catch {
      _ => raise archive_error(origin, "Invalid UTF-8 ZIP entry path")
    }
    let checked = if name.has_suffix("/") {
      name[:name.length() - 1].to_owned()
    } else {
      name
    }
    if !valid_path(checked) {
      raise ResourceError(
        UnsafePath,
        Provenance::new(origin),
        "Unsafe ZIP entry `" + name + "`",
      )
    }
    if seen.contains(name) {
      raise ResourceError(
        DuplicateEntry,
        Provenance::new(origin),
        "Duplicate ZIP entry `" + name + "`",
      )
    }
    seen.add(name)
    let local_offset = u32(offset + 42)
    range(local_offset, 30)
    if u32(local_offset) != 0x04034b50 {
      raise archive_error(origin, "Invalid ZIP local file header")
    }
    let header_size = 30 + u16(local_offset + 26) + u16(local_offset + 28)
    range(local_offset, header_size)
    range(local_offset + header_size, compressed)
    offset += entry_size
  }
  if offset > directory + size {
    raise archive_error(origin, "ZIP entries exceed the central directory")
  }
}

///|
fn archive_error(origin : Location, message : String) -> ResourceError {
  ResourceError(InvalidArchive, Provenance::new(origin), message)
}