// digest_binding.mbt — Content-Digest binding (RFC 9530, RFC 9421 §3.2).
//
// An HTTP message signature does not automatically protect the body: it only
// covers the components named in Signature-Input. A body is bound to a
// signature only when the message carries a valid Content-Digest and that
// field is covered by the signature. This module provides pluggable bindings
// rather than a full RFC 9530 implementation.

///|
/// Validates that a message body is bound to the signature via Content-Digest.
pub(open) trait DigestBinding {
  /// Validates the body against the message's Content-Digest headers.
  /// Returns `ContentDigestInvalid` when the digest does not match.
  fn validate(Self, OrderedHeaders, Bytes) -> Result[Unit, HsError]
}

///|
/// A binding that never validates the body. Use when the application has
/// already validated Content-Digest elsewhere or does not need body binding.
pub enum NoDigestBinding {
  NoDigestBinding
}

///|
/// A binding that requires Content-Digest to be present, to match the body,
/// and to be covered by the covered components. Content-Digest values are
/// RFC 9651 Dictionaries (`sha-256=:base64:`); the first recognized digest
/// is checked and must match.
pub struct RequireCoveredContentDigest {
  covered : Array[CoveredComponent]
  limits : Limits
}

///|
/// A binding that delegates validation to a callback.
pub struct CallbackDigestBinding {
  callback : (OrderedHeaders, Bytes) -> Result[Unit, HsError]
}

///|
/// Constructs a `NoDigestBinding`.
pub fn NoDigestBinding::new() -> NoDigestBinding {
  NoDigestBinding
}

///|
/// Constructs a `RequireCoveredContentDigest` over the given covered
/// components.
pub fn RequireCoveredContentDigest::new(
  covered : Array[CoveredComponent],
  limits : Limits,
) -> RequireCoveredContentDigest {
  { covered, limits }
}

///|
/// Constructs a `CallbackDigestBinding`.
pub fn CallbackDigestBinding::new(
  callback : (OrderedHeaders, Bytes) -> Result[Unit, HsError],
) -> CallbackDigestBinding {
  { callback, }
}

///|
/// Implements `DigestBinding` for `NoDigestBinding`.
pub impl DigestBinding for NoDigestBinding with fn validate(
  _self,
  _headers,
  _body,
) {
  Ok(())
}

///|
/// Implements `DigestBinding` for `RequireCoveredContentDigest`.
pub impl DigestBinding for RequireCoveredContentDigest with fn validate(
  self,
  headers,
  body,
) {
  validate_content_digest(self.covered, headers, body, self.limits)
}

///|
/// Implements `DigestBinding` for `CallbackDigestBinding`.
pub impl DigestBinding for CallbackDigestBinding with fn validate(
  self,
  headers,
  body,
) {
  (self.callback)(headers, body)
}

///|
/// Validates that the body matches a Content-Digest header that is covered by
/// `covered`. When `require_covered` is false only the digest validity is
/// checked.
pub fn validate_content_digest(
  covered : Array[CoveredComponent],
  headers : OrderedHeaders,
  body : Bytes,
  limits : Limits,
) -> Result[Unit, HsError] {
  try {
    if limits.max_body_bytes_for_digest >= 0 {
      limits.check_body_size_for_digest(body.length())
    }
    let values = headers.get_all("content-digest")
    if values.is_empty() {
      raise hs_error(
        DigestBinding,
        ContentDigestRequired,
        "Content-Digest header is missing",
      )
    }
    // Content-Digest is an SF Dictionary: sha-256=:base64:, sha-512=:base64:.
    let mut checked = false
    for value in values {
      let dict = match parse_sf_dictionary_string(value, limits) {
        Ok(d) => d
        Err(e) => raise e
      }
      for entry in dict {
        if entry.key == "sha-256" {
          let digest_bytes = match entry.value {
            ItemMember(item) =>
              match item.value {
                SfByteSequence(b) => b
                _ =>
                  raise hs_error(
                    DigestBinding,
                    ContentDigestInvalid,
                    "Content-Digest member must be a byte sequence",
                  )
              }
            InnerListMember(_) =>
              raise hs_error(
                DigestBinding,
                ContentDigestInvalid,
                "Content-Digest member must be a byte sequence",
              )
          }
          checked = true
          if !constant_time_equal(sha256_raw(body), digest_bytes) {
            raise hs_error(
              DigestBinding,
              ContentDigestInvalid,
              "Content-Digest does not match the message body",
            )
          }
        }
        // Unsupported digest algorithms (e.g. sha-512) are ignored per
        // RFC 9530 §3.
      }
    }
    if !checked {
      raise hs_error(
        DigestBinding,
        ContentDigestInvalid,
        "no supported digest algorithm present",
      )
    }
    // The digest must be covered by the signature to bind the body.
    let digest_component = covered_field("content-digest")
    let covered = components_cover(covered, digest_component)
    if !covered {
      raise hs_error(
        DigestBinding,
        ContentDigestNotCovered,
        "Content-Digest is not covered by the signature",
      )
    }
    Ok(())
  } catch {
    e => Err(e)
  }
}