///|
pub(all) enum ChecksumAlgorithm {
  Sha1
  Sha256
  Sha512
} derive(Eq, Debug)

///|
pub fn ChecksumAlgorithm::to_string(self : ChecksumAlgorithm) -> String {
  match self {
    Sha1 => "sha1"
    Sha256 => "sha256"
    Sha512 => "sha512"
  }
}

///|
fn ChecksumAlgorithm::size(self : ChecksumAlgorithm) -> Int32 {
  match self {
    Sha1 => 20
    Sha256 => 32
    Sha512 => 64
  }
}

///|
fn ChecksumAlgorithm::rank(self : ChecksumAlgorithm) -> Int32 {
  match self {
    Sha1 => 1
    Sha256 => 2
    Sha512 => 3
  }
}

///|
/// An algorithm and digest validated together at the boundary.
pub struct Checksum {
  algorithm : ChecksumAlgorithm
  priv digest : Bytes
} derive(Eq, Debug)

///|
pub fn Checksum::parse_hex(
  algorithm : ChecksumAlgorithm,
  text : String,
) -> Checksum? {
  if text.length() != algorithm.size() * 2 {
    return None
  }
  let bytes = []
  fn digit(c : UInt16) -> Int32? {
    let n = c.to_int()
    if n >= 48 && n <= 57 {
      Some(n - 48)
    } else if n >= 65 && n <= 70 {
      Some(n - 55)
    } else if n >= 97 && n <= 102 {
      Some(n - 87)
    } else {
      None
    }
  }
  for i in 0.. String {
  self.algorithm.to_string() + ":" + @crypto.bytes_to_hex_string(self.digest)
}

///|
pub fn Checksum::matches(self : Checksum, body : Bytes) -> Bool {
  self.digest == self.algorithm.digest(body)
}

///|
fn ChecksumAlgorithm::digest(self : ChecksumAlgorithm, body : Bytes) -> Bytes {
  let actual = match self {
    Sha1 => @crypto.sha1(body)
    Sha256 => @crypto.sha256(body)
    Sha512 => @crypto.sha512(body)
  }
  Bytes::from_array(actual.iter().to_array())
}

///|
pub(all) enum NpmIntegritySource {
  Sri
  Shasum
} derive(Eq, Debug)

///|
/// Only the strongest supported SRI algorithm participates in verification.
pub struct NpmIntegrity {
  source : NpmIntegritySource
  priv checksums : Array[Checksum]
} derive(Eq, Debug)

///|
pub fn NpmIntegrity::parse_sri(text : String) -> NpmIntegrity? {
  if text.is_empty() || text.length() > 8192 {
    return None
  }
  let checksums = []
  let mut strongest = 0
  for
    token in text
    .replace_all(old="\t", new=" ")
    .replace_all(old="\r", new=" ")
    .replace_all(old="\n", new=" ")
    .split(" ") {
    if token.is_empty() {
      continue
    }
    guard token.find("-") is Some(separator) else { return None }
    let algorithm = match token[:separator] {
      "sha1" => Sha1
      "sha256" => Sha256
      "sha512" => Sha512
      _ => continue
    }
    let encoded = token[separator + 1:].to_owned()
    let digest = @base64.decode(encoded) catch { _ => return None }
    // Require canonical padded base64; the decoder also accepts some lax forms.
    if digest.length() != algorithm.size() ||
      @base64.encode(digest[:]) != encoded {
      return None
    }
    if algorithm.rank() > strongest {
      checksums.clear()
      strongest = algorithm.rank()
    }
    if algorithm.rank() == strongest {
      checksums.push(Checksum::{ algorithm, digest, })
    }
  }
  if checksums.is_empty() {
    None
  } else {
    Some({ source: Sri, checksums, })
  }
}

///|
pub fn NpmIntegrity::parse_shasum(text : String) -> NpmIntegrity? {
  Checksum::parse_hex(Sha1, text).map(checksum => {
    source: Shasum,
    checksums: [checksum],
  })
}

///|
pub fn NpmIntegrity::to_string(self : NpmIntegrity) -> String {
  let prefix = match self.source {
    Sri => "SRI "
    Shasum => "shasum "
  }
  prefix + self.checksums.map(Checksum::to_string).join(" ")
}

///|
pub fn NpmIntegrity::verify(
  self : NpmIntegrity,
  body : Bytes,
  at : Provenance,
) -> Unit raise ResourceError {
  // Every candidate has the same strongest algorithm, so hash the body once.
  let digest = self.checksums[0].algorithm.digest(body)
  if !self.checksums.any(checksum => checksum.digest == digest) {
    raise ResourceError(
      IntegrityFailure,
      at,
      "Archive does not match npm " + self.to_string(),
    )
  }
}

///|
/// Runs for fresh responses before cache publication, and for every cache hit.
pub type IntegrityVerifier = (Bytes, Provenance) -> Unit raise ResourceError