// Copyright 2018-2026 the Deno authors. MIT license.

//! `SubtleCrypto.digest()` — algorithm parsing, BufferSource coercion and
//! the dispatch onto the `aws_lc_rs` / `sha3` backends. All of the logic
//! that used to live in the `digest(algorithm, data)` method body in
//! `ext/crypto/00_crypto.js` is now here.

use std::borrow::Cow;

use aws_lc_rs::digest;
use deno_core::v8;
use deno_core::webidl::ContextFn;
use deno_core::webidl::WebIdlConverter;
use deno_core::webidl::WebIdlError;
use deno_core::webidl::WebIdlErrorKind;
use deno_error::JsErrorBox;
use serde::Deserialize;

use crate::CryptoError;
use crate::key::CryptoHash;

/// Parameter dictionary for the SHA-3-family extendable-output digest
/// algorithms (cSHAKE / TurboSHAKE) used by [`DigestAlgorithm::Xof`].
/// Carries the `outputLength`-and-optional-trim parameters validated in
/// [`run_xof`] rather than at converter time so the eventual error is a
/// `DOMExceptionOperationError`, not a `WebIdlError`.
#[derive(Deserialize)]
#[serde(rename_all = "camelCase", tag = "name")]
pub enum SubtleDigestXof {
  #[serde(rename = "cSHAKE128", rename_all = "camelCase")]
  CShake128 {
    output_length: u32,
    #[serde(with = "serde_bytes", default)]
    function_name: Option<Vec<u8>>,
    #[serde(with = "serde_bytes", default)]
    customization: Option<Vec<u8>>,
  },
  #[serde(rename = "cSHAKE256", rename_all = "camelCase")]
  CShake256 {
    output_length: u32,
    #[serde(with = "serde_bytes", default)]
    function_name: Option<Vec<u8>>,
    #[serde(with = "serde_bytes", default)]
    customization: Option<Vec<u8>>,
  },
  #[serde(rename = "TurboSHAKE128", rename_all = "camelCase")]
  TurboShake128 {
    output_length: u32,
    domain_separation: Option<u8>,
  },
  #[serde(rename = "TurboSHAKE256", rename_all = "camelCase")]
  TurboShake256 {
    output_length: u32,
    domain_separation: Option<u8>,
  },
  #[serde(rename = "KT128", rename_all = "camelCase")]
  Kt128 {
    output_length: u32,
    #[serde(with = "serde_bytes", default)]
    customization: Option<Vec<u8>>,
  },
  #[serde(rename = "KT256", rename_all = "camelCase")]
  Kt256 {
    output_length: u32,
    #[serde(with = "serde_bytes", default)]
    customization: Option<Vec<u8>>,
  },
  #[serde(rename = "KangarooTwelve", rename_all = "camelCase")]
  KangarooTwelve {
    output_length: u32,
    #[serde(with = "serde_bytes", default)]
    customization: Option<Vec<u8>>,
  },
}

/// The `WebIdlConverter` for `AlgorithmIdentifier`-restricted-to-digest,
/// canonicalized into the variant the dispatch code needs. Mirrors what
/// `normalizeAlgorithm(algorithm, "digest")` produced in JS.
///
/// Unrecognized algorithm names are kept as [`DigestAlgorithm::Unknown`] so
/// the dispatch in [`run`] can throw the WebCrypto-spec-mandated
/// `NotSupportedError` `DOMException` (not the WebIDL `TypeError` that a
/// converter-level error would produce). The WPT `digest.https.any.html`
/// "AES-GCM/RSA-OAEP/PBKDF2/AES-KW with empty/short/medium/long" subtests
/// hardcode that error name.
pub enum DigestAlgorithm {
  Sha(CryptoHash),
  /// cSHAKE / TurboSHAKE — variable-length output with extra dictionary
  /// parameters. The validation that the JS body performed against the
  /// raw dictionary (multiple-of-8 outputLength, non-zero TurboSHAKE
  /// outputLength, domainSeparation range) is deferred until [`run`] so
  /// the `WebIdlError` path stays clean.
  Xof(SubtleDigestXof),
  /// Algorithm identifier whose `name` resolved (it was a string or had a
  /// `.name` DOMString) but isn't in the digest registry. Holds the input
  /// spelling so the eventual `NotSupportedError` carries it.
  Unknown(String),
}

impl<'a> WebIdlConverter<'a> for DigestAlgorithm {
  type Options = ();

  fn convert<'b>(
    scope: &mut v8::PinScope<'a, '_>,
    value: v8::Local<'a, v8::Value>,
    prefix: Cow<'static, str>,
    context: ContextFn<'b>,
    _options: &Self::Options,
  ) -> Result<Self, WebIdlError> {
    // 1. Resolve the AlgorithmIdentifier union: a `DOMString` is treated
    //    as `{ name: <string> }`; otherwise the input must be an object
    //    carrying a `.name` field. The original dictionary (if any) is
    //    kept around so the XOF arms can pluck `outputLength`,
    //    `functionName`, `customization` and `domainSeparation` off it.
    //
    //    A missing `name` member -> `TypeError` (WebIDL required-member
    //    semantics), exercised by the WPT `digest({}, ...)` "empty
    //    algorithm object" subtest.
    let (name_str, maybe_obj) =
      extract_name_and_obj(scope, value, prefix.clone(), context.borrowed())?;

    // 2. Canonical (case-insensitive) name lookup -- the WebCrypto
    //    registry treats algorithm identifiers as case-insensitive but
    //    every other call site relies on the canonical spelling. If the
    //    name isn't in the digest registry, defer the `NotSupportedError`
    //    to [`run`] so the right error class is thrown.
    let Some(canonical) = canonical_digest_name(&name_str) else {
      return Ok(Self::Unknown(name_str));
    };

    // 3. Build the dispatch variant. SHA / SHA3 do not consult the dict;
    //    XOF variants pull their extra parameters out.
    match canonical {
      "SHA-1" => Ok(Self::Sha(CryptoHash::Sha1)),
      "SHA-256" => Ok(Self::Sha(CryptoHash::Sha256)),
      "SHA-384" => Ok(Self::Sha(CryptoHash::Sha384)),
      "SHA-512" => Ok(Self::Sha(CryptoHash::Sha512)),
      "SHA3-256" => Ok(Self::Sha(CryptoHash::Sha3_256)),
      "SHA3-384" => Ok(Self::Sha(CryptoHash::Sha3_384)),
      "SHA3-512" => Ok(Self::Sha(CryptoHash::Sha3_512)),
      "cSHAKE128" | "cSHAKE256" | "TurboSHAKE128" | "TurboSHAKE256"
      | "KT128" | "KT256" | "KangarooTwelve" => {
        let obj = maybe_obj.ok_or_else(|| {
          WebIdlError::other(
            prefix.clone(),
            context.borrowed(),
            JsErrorBox::type_error(format!(
              "'{canonical}' requires a parameter dictionary"
            )),
          )
        })?;
        let xof = parse_xof_dict(
          scope,
          obj,
          canonical,
          prefix.clone(),
          context.borrowed(),
        )?;
        Ok(Self::Xof(xof))
      }
      _ => unreachable!("canonical_digest_name returned an unknown variant"),
    }
  }
}

fn canonical_digest_name(name: &str) -> Option<&'static str> {
  const NAMES: &[&str] = &[
    "SHA-1",
    "SHA-256",
    "SHA-384",
    "SHA-512",
    "SHA3-256",
    "SHA3-384",
    "SHA3-512",
    "cSHAKE128",
    "cSHAKE256",
    "TurboSHAKE128",
    "TurboSHAKE256",
    "KT128",
    "KT256",
    "KangarooTwelve",
  ];
  NAMES
    .iter()
    .copied()
    .find(|canon| canon.eq_ignore_ascii_case(name))
}

fn extract_name_and_obj<'a, 'b>(
  scope: &mut v8::PinScope<'a, '_>,
  value: v8::Local<'a, v8::Value>,
  prefix: Cow<'static, str>,
  context: ContextFn<'b>,
) -> Result<(String, Option<v8::Local<'a, v8::Object>>), WebIdlError> {
  if value.is_string() {
    let s = value.to_rust_string_lossy(scope);
    return Ok((s, None));
  }
  if let Ok(obj) = v8::Local::<v8::Object>::try_from(value) {
    let name_key = v8_str(scope, "name");
    let name_val = obj
      .get(scope, name_key.into())
      .unwrap_or_else(|| v8::undefined(scope).into());
    // WebIDL `Algorithm` dictionary requires `name` -- WPT
    // `digest({}, ...)` "empty algorithm object" checks the resulting
    // error name is `TypeError`, not `NotSupportedError`.
    if name_val.is_undefined() {
      return Err(WebIdlError::other(
        prefix,
        context,
        JsErrorBox::type_error("required member 'name' is undefined"),
      ));
    }
    let s = name_val
      .to_string(scope)
      .ok_or_else(|| {
        WebIdlError::other(
          prefix.clone(),
          context.borrowed(),
          JsErrorBox::type_error(
            "algorithm.name is not convertible to DOMString",
          ),
        )
      })?
      .to_rust_string_lossy(scope);
    return Ok((s, Some(obj)));
  }
  Err(WebIdlError::new(
    prefix,
    context,
    WebIdlErrorKind::ConvertToConverterType("AlgorithmIdentifier"),
  ))
}

fn parse_xof_dict<'a, 'b>(
  scope: &mut v8::PinScope<'a, '_>,
  obj: v8::Local<'a, v8::Object>,
  canonical: &'static str,
  prefix: Cow<'static, str>,
  context: ContextFn<'b>,
) -> Result<SubtleDigestXof, WebIdlError> {
  let output_length =
    read_required_u32(scope, obj, "outputLength", prefix.clone(), &context)?;
  match canonical {
    "cSHAKE128" => Ok(SubtleDigestXof::CShake128 {
      output_length,
      function_name: read_optional_buffer(
        scope,
        obj,
        "functionName",
        prefix.clone(),
        &context,
      )?,
      customization: read_optional_buffer(
        scope,
        obj,
        "customization",
        prefix.clone(),
        &context,
      )?,
    }),
    "cSHAKE256" => Ok(SubtleDigestXof::CShake256 {
      output_length,
      function_name: read_optional_buffer(
        scope,
        obj,
        "functionName",
        prefix.clone(),
        &context,
      )?,
      customization: read_optional_buffer(
        scope,
        obj,
        "customization",
        prefix.clone(),
        &context,
      )?,
    }),
    "TurboSHAKE128" => Ok(SubtleDigestXof::TurboShake128 {
      output_length,
      domain_separation: read_optional_u8(
        scope,
        obj,
        "domainSeparation",
        prefix.clone(),
        &context,
      )?,
    }),
    "TurboSHAKE256" => Ok(SubtleDigestXof::TurboShake256 {
      output_length,
      domain_separation: read_optional_u8(
        scope,
        obj,
        "domainSeparation",
        prefix.clone(),
        &context,
      )?,
    }),
    "KT128" => Ok(SubtleDigestXof::Kt128 {
      output_length,
      customization: read_optional_buffer(
        scope,
        obj,
        "customization",
        prefix.clone(),
        &context,
      )?,
    }),
    "KT256" => Ok(SubtleDigestXof::Kt256 {
      output_length,
      customization: read_optional_buffer(
        scope,
        obj,
        "customization",
        prefix.clone(),
        &context,
      )?,
    }),
    "KangarooTwelve" => Ok(SubtleDigestXof::KangarooTwelve {
      output_length,
      customization: read_optional_buffer(
        scope,
        obj,
        "customization",
        prefix.clone(),
        &context,
      )?,
    }),
    _ => unreachable!(),
  }
}

fn read_required_u32<'a, 'b>(
  scope: &mut v8::PinScope<'a, '_>,
  obj: v8::Local<'a, v8::Object>,
  key: &'static str,
  prefix: Cow<'static, str>,
  context: &ContextFn<'b>,
) -> Result<u32, WebIdlError> {
  let key_v8 = v8_str(scope, key);
  let val = obj
    .get(scope, key_v8.into())
    .unwrap_or_else(|| v8::undefined(scope).into());
  if val.is_undefined() {
    return Err(WebIdlError::other(
      prefix,
      context.borrowed(),
      JsErrorBox::type_error(format!("required dictionary member '{key}'")),
    ));
  }
  val.uint32_value(scope).ok_or_else(|| {
    WebIdlError::other(
      prefix,
      context.borrowed(),
      JsErrorBox::type_error(format!("'{key}' must be convertible to u32")),
    )
  })
}

fn read_optional_u8<'a, 'b>(
  scope: &mut v8::PinScope<'a, '_>,
  obj: v8::Local<'a, v8::Object>,
  key: &'static str,
  prefix: Cow<'static, str>,
  context: &ContextFn<'b>,
) -> Result<Option<u8>, WebIdlError> {
  let key_v8 = v8_str(scope, key);
  let val = obj
    .get(scope, key_v8.into())
    .unwrap_or_else(|| v8::undefined(scope).into());
  if val.is_undefined() || val.is_null() {
    return Ok(None);
  }
  // Read the full u32 and reject values outside `[0, 0xFF]` so a stray
  // `0x101` does not wrap to `0x01` and slip past the caller's
  // [1, 0x7F] domain-separation range check (TurboSHAKE edge).
  let u = val.uint32_value(scope).unwrap_or(0);
  if u > u8::MAX as u32 {
    return Err(WebIdlError::other(
      prefix,
      context.borrowed(),
      JsErrorBox::type_error(format!("'{key}' must be in the range [0, 0xFF]")),
    ));
  }
  Ok(Some(u as u8))
}

fn read_optional_buffer<'a, 'b>(
  scope: &mut v8::PinScope<'a, '_>,
  obj: v8::Local<'a, v8::Object>,
  key: &'static str,
  prefix: Cow<'static, str>,
  context: &ContextFn<'b>,
) -> Result<Option<Vec<u8>>, WebIdlError> {
  let key_v8 = v8_str(scope, key);
  let val = obj
    .get(scope, key_v8.into())
    .unwrap_or_else(|| v8::undefined(scope).into());
  if val.is_undefined() || val.is_null() {
    return Ok(None);
  }
  // SAB and SAB-backed views are rejected to match the JS
  // `webidl.converters.BufferSource` contract (cSHAKE
  // `functionName`/`customization`), and non-BufferSource values raise
  // `TypeError` instead of silently materializing as an empty `Vec`.
  if let Ok(view) = v8::Local::<v8::ArrayBufferView>::try_from(val) {
    if let Some(ab) = view.buffer(scope) {
      let ab_val: v8::Local<v8::Value> = ab.into();
      if ab_val.is_shared_array_buffer() {
        return Err(WebIdlError::other(
          prefix,
          context.borrowed(),
          JsErrorBox::type_error(format!(
            "'{key}' is a view on a SharedArrayBuffer, which is not allowed"
          )),
        ));
      }
    }
    return Ok(Some(value_to_byte_vec(scope, val)));
  }
  if val.is_shared_array_buffer() {
    return Err(WebIdlError::other(
      prefix,
      context.borrowed(),
      JsErrorBox::type_error(format!(
        "'{key}' is a SharedArrayBuffer, which is not allowed"
      )),
    ));
  }
  if v8::Local::<v8::ArrayBuffer>::try_from(val).is_ok() {
    return Ok(Some(value_to_byte_vec(scope, val)));
  }
  Err(WebIdlError::other(
    prefix,
    context.borrowed(),
    JsErrorBox::type_error(format!("'{key}' is not a BufferSource")),
  ))
}

fn value_to_byte_vec<'a>(
  scope: &mut v8::PinScope<'a, '_>,
  value: v8::Local<'a, v8::Value>,
) -> Vec<u8> {
  if let Ok(view) = v8::Local::<v8::ArrayBufferView>::try_from(value) {
    let byte_offset = view.byte_offset();
    let byte_length = view.byte_length();
    if byte_length == 0 {
      return Vec::new();
    }
    let ab = view.buffer(scope).unwrap();
    // SAFETY: V8 guarantees byte_offset + byte_length is within the
    // backing store, and a non-detached buffer has a non-null data ptr.
    unsafe {
      let base = ab.data().unwrap().as_ptr() as *const u8;
      std::slice::from_raw_parts(base.add(byte_offset), byte_length).to_vec()
    }
  } else if let Ok(ab) = v8::Local::<v8::ArrayBuffer>::try_from(value) {
    let byte_length = ab.byte_length();
    if byte_length == 0 {
      return Vec::new();
    }
    // SAFETY: as above.
    unsafe {
      let base = ab.data().unwrap().as_ptr() as *const u8;
      std::slice::from_raw_parts(base, byte_length).to_vec()
    }
  } else {
    Vec::new()
  }
}

/// `WebIdlConverter` matching the WebCrypto `BufferSource` union
/// (`ArrayBufferView` or `ArrayBuffer`). Always materializes the bytes
/// into an owned `Vec<u8>` so the data is safe to hold across `.await`.
///
/// Rejects `SharedArrayBuffer` and `ArrayBufferView`s whose backing buffer is
/// a `SharedArrayBuffer`, mirroring the WebIDL `BufferSource` converter --
/// the WebCrypto spec uses `BufferSource` without `[AllowShared]`, so the
/// SAB-backed `subtle.digest('SHA-256', new Uint8Array(new SharedArrayBuffer))`
/// call required by the `crypto-subtle-cross-realm` node compat test must
/// reject with a `TypeError`.
pub struct BufferSource(pub Vec<u8>);

impl<'a> WebIdlConverter<'a> for BufferSource {
  type Options = ();

  fn convert<'b>(
    scope: &mut v8::PinScope<'a, '_>,
    value: v8::Local<'a, v8::Value>,
    prefix: Cow<'static, str>,
    context: ContextFn<'b>,
    _options: &Self::Options,
  ) -> Result<Self, WebIdlError> {
    if let Ok(view) = v8::Local::<v8::ArrayBufferView>::try_from(value) {
      if let Some(ab) = view.buffer(scope) {
        let ab_val: v8::Local<v8::Value> = ab.into();
        if ab_val.is_shared_array_buffer() {
          return Err(WebIdlError::other(
            prefix,
            context,
            JsErrorBox::type_error(
              "is a view on a SharedArrayBuffer, which is not allowed",
            ),
          ));
        }
      }
      return Ok(BufferSource(value_to_byte_vec(scope, value)));
    }
    if value.is_shared_array_buffer() {
      return Err(WebIdlError::other(
        prefix,
        context,
        JsErrorBox::type_error("is not an ArrayBuffer or a view on one"),
      ));
    }
    if v8::Local::<v8::ArrayBuffer>::try_from(value).is_ok() {
      return Ok(BufferSource(value_to_byte_vec(scope, value)));
    }
    Err(WebIdlError::new(
      prefix,
      context,
      WebIdlErrorKind::ConvertToConverterType("BufferSource"),
    ))
  }
}

fn v8_str<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  s: &str,
) -> v8::Local<'s, v8::String> {
  v8::String::new_from_one_byte(scope, s.as_bytes(), v8::NewStringType::Normal)
    .unwrap()
}

/// Execute the `SubtleCrypto.digest()` body for the already-normalized
/// algorithm + copied data. Mirrors the JS dispatch in
/// `00_crypto.js: SubtleCrypto.prototype.digest`.
pub fn run(
  algorithm: DigestAlgorithm,
  data: Vec<u8>,
) -> Result<Vec<u8>, CryptoError> {
  match algorithm {
    DigestAlgorithm::Sha(hash) => {
      Ok(digest::digest(hash.into(), &data).as_ref().to_vec())
    }
    DigestAlgorithm::Xof(xof) => run_xof(xof, &data),
    DigestAlgorithm::Unknown(name) => {
      Err(CryptoError::UnsupportedDigestAlgorithm(name))
    }
  }
}

fn run_xof(
  algorithm: SubtleDigestXof,
  data: &[u8],
) -> Result<Vec<u8>, CryptoError> {
  use sha3::digest::ExtendableOutput;
  use sha3::digest::Update;
  use sha3::digest::XofReader;
  use sha3::digest::core_api::CoreWrapper;

  // Validate `outputLength` and `domainSeparation` per the JS body. The
  // JS code threw `OperationError` DOMExceptions for these; the
  // `InvalidXofParameters` variant maps to the same class.
  let output_length = match &algorithm {
    SubtleDigestXof::CShake128 { output_length, .. }
    | SubtleDigestXof::CShake256 { output_length, .. }
    | SubtleDigestXof::TurboShake128 { output_length, .. }
    | SubtleDigestXof::TurboShake256 { output_length, .. }
    | SubtleDigestXof::Kt128 { output_length, .. }
    | SubtleDigestXof::Kt256 { output_length, .. }
    | SubtleDigestXof::KangarooTwelve { output_length, .. } => *output_length,
  };
  if !output_length.is_multiple_of(8) {
    return Err(CryptoError::InvalidXofParameters);
  }
  let is_turbo = matches!(
    algorithm,
    SubtleDigestXof::TurboShake128 { .. }
      | SubtleDigestXof::TurboShake256 { .. }
  );
  let is_kangaroo = matches!(
    algorithm,
    SubtleDigestXof::Kt128 { .. }
      | SubtleDigestXof::Kt256 { .. }
      | SubtleDigestXof::KangarooTwelve { .. }
  );
  if (is_turbo || is_kangaroo) && output_length == 0 {
    return Err(CryptoError::InvalidXofParameters);
  }
  if let SubtleDigestXof::TurboShake128 {
    domain_separation, ..
  }
  | SubtleDigestXof::TurboShake256 {
    domain_separation, ..
  } = &algorithm
    && let Some(d) = domain_separation
    && !(0x01..=0x7F).contains(d)
  {
    return Err(CryptoError::InvalidXofParameters);
  }

  let out_len = (output_length / 8) as usize;
  let mut out = vec![0u8; out_len];

  match algorithm {
    SubtleDigestXof::CShake128 {
      function_name,
      customization,
      ..
    } => {
      let core = sha3::CShake128Core::new_with_function_name(
        function_name.as_deref().unwrap_or(&[]),
        customization.as_deref().unwrap_or(&[]),
      );
      let mut h: sha3::CShake128 = CoreWrapper::from_core(core);
      h.update(data);
      h.finalize_xof().read(&mut out);
    }
    SubtleDigestXof::CShake256 {
      function_name,
      customization,
      ..
    } => {
      let core = sha3::CShake256Core::new_with_function_name(
        function_name.as_deref().unwrap_or(&[]),
        customization.as_deref().unwrap_or(&[]),
      );
      let mut h: sha3::CShake256 = CoreWrapper::from_core(core);
      h.update(data);
      h.finalize_xof().read(&mut out);
    }
    SubtleDigestXof::TurboShake128 {
      domain_separation, ..
    } => {
      let d = domain_separation.unwrap_or(0x1F);
      let core = sha3::TurboShake128Core::new(d);
      let mut h: sha3::TurboShake128 = CoreWrapper::from_core(core);
      h.update(data);
      h.finalize_xof().read(&mut out);
    }
    SubtleDigestXof::TurboShake256 {
      domain_separation, ..
    } => {
      let d = domain_separation.unwrap_or(0x1F);
      let core = sha3::TurboShake256Core::new(d);
      let mut h: sha3::TurboShake256 = CoreWrapper::from_core(core);
      h.update(data);
      h.finalize_xof().read(&mut out);
    }
    SubtleDigestXof::Kt128 { customization, .. }
    | SubtleDigestXof::KangarooTwelve { customization, .. } => {
      kangaroo_twelve_128(
        data,
        customization.as_deref().unwrap_or(&[]),
        &mut out,
      );
    }
    SubtleDigestXof::Kt256 { customization, .. } => {
      kangaroo_twelve_256(
        data,
        customization.as_deref().unwrap_or(&[]),
        &mut out,
      );
    }
  }

  Ok(out)
}

const KT_CHUNK_SIZE: usize = 8192;

fn encode_len(mut len: usize) -> Vec<u8> {
  if len == 0 {
    return vec![0];
  }
  let mut bytes = Vec::new();
  while len > 0 {
    bytes.push((len & 0xff) as u8);
    len >>= 8;
  }
  bytes.reverse();
  bytes.push(bytes.len() as u8);
  bytes
}

fn kangaroo_twelve_128(data: &[u8], customization: &[u8], out: &mut [u8]) {
  use tiny_keccak::Hasher;

  let mut h = tiny_keccak::KangarooTwelve::new(customization);
  h.update(data);
  h.finalize(out);
}

fn kangaroo_twelve_256(data: &[u8], customization: &[u8], out: &mut [u8]) {
  kangaroo_twelve_turbo::<136>(data, customization, out);
}

fn kangaroo_twelve_turbo<const RATE: usize>(
  data: &[u8],
  customization: &[u8],
  out: &mut [u8],
) {
  let mut input = Vec::with_capacity(data.len() + customization.len() + 9);
  input.extend_from_slice(data);
  input.extend_from_slice(customization);
  input.extend_from_slice(&encode_len(customization.len()));

  if input.len() <= KT_CHUNK_SIZE {
    let mut h = TurboShakeNode::<RATE>::new(0x07);
    h.update(&input);
    h.squeeze(out);
    return;
  }

  let cv_len = if RATE == 168 { 32 } else { 64 };
  let mut h = TurboShakeNode::<RATE>::new(0x06);
  h.update(&input[..KT_CHUNK_SIZE]);
  h.update(&[0x03, 0, 0, 0, 0, 0, 0, 0]);

  let mut chunks = 0usize;
  let mut tail = &input[KT_CHUNK_SIZE..];
  while !tail.is_empty() {
    let take = tail.len().min(KT_CHUNK_SIZE);
    let mut inner = TurboShakeNode::<RATE>::new(0x0b);
    inner.update(&tail[..take]);
    let mut cv = vec![0u8; cv_len];
    inner.squeeze(&mut cv);
    h.update(&cv);
    chunks += 1;
    tail = &tail[take..];
  }

  h.update(&encode_len(chunks));
  h.update(&[0xff, 0xff]);
  h.squeeze(out);
}

struct TurboShakeNode<const RATE: usize> {
  state: [u64; 25],
  offset: usize,
  delim: u8,
  squeezing: bool,
}

impl<const RATE: usize> TurboShakeNode<RATE> {
  fn new(delim: u8) -> Self {
    Self {
      state: [0; 25],
      offset: 0,
      delim,
      squeezing: false,
    }
  }

  fn update(&mut self, mut input: &[u8]) {
    debug_assert!(!self.squeezing);
    while !input.is_empty() {
      let take = input.len().min(RATE - self.offset);
      xor_into_state(&mut self.state, self.offset, &input[..take]);
      self.offset += take;
      input = &input[take..];
      if self.offset == RATE {
        tiny_keccak::keccakp(&mut self.state);
        self.offset = 0;
      }
    }
  }

  fn squeeze(&mut self, mut out: &mut [u8]) {
    if !self.squeezing {
      xor_into_state(&mut self.state, self.offset, &[self.delim]);
      xor_into_state(&mut self.state, RATE - 1, &[0x80]);
      tiny_keccak::keccakp(&mut self.state);
      self.offset = 0;
      self.squeezing = true;
    }

    while !out.is_empty() {
      if self.offset == RATE {
        tiny_keccak::keccakp(&mut self.state);
        self.offset = 0;
      }
      let take = out.len().min(RATE - self.offset);
      copy_from_state(&self.state, self.offset, &mut out[..take]);
      self.offset += take;
      out = &mut out[take..];
    }
  }
}

fn xor_into_state(state: &mut [u64; 25], offset: usize, bytes: &[u8]) {
  for (i, byte) in bytes.iter().copied().enumerate() {
    let pos = offset + i;
    state[pos / 8] ^= (byte as u64) << ((pos % 8) * 8);
  }
}

fn copy_from_state(state: &[u64; 25], offset: usize, out: &mut [u8]) {
  for (i, byte) in out.iter_mut().enumerate() {
    let pos = offset + i;
    *byte = (state[pos / 8] >> ((pos % 8) * 8)) as u8;
  }
}
