Ctrl K
ComunidadGuías…

lib/audio_proxy/cache_key.ex

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
defmodule AudioProxy.CacheKey do
  @moduledoc """
  Variant identity: the object key under which a rendered variant is cached.

  A variant is exactly its normalized processing options plus the source it
  was rendered from, so the key is

      lowercase-hex(SHA-256(normalized-options ‖ "\\n" ‖ canonical-source))

  The newline separates the two halves unambiguously. That is only sound
  because a normalized options string cannot contain one:
  `AudioProxy.Options` rejects control characters in `dl` and `cb`, the only
  two options whose values are opaque, and every other value is drawn from a
  fixed alphabet. Without the separator, `("", "/gain:3")` and `("gain:3", "")`
  would hash identical bytes.

  Equal variants therefore share a key regardless of segment order, and any
  differing option (`cb` included, since it survives normalization) yields a
  different one.

  The digest is flat and length-bounded, which is what an S3 object key wants;
  a human-debuggable prefix was considered and rejected (design).

      iex> AudioProxy.CacheKey.derive!("br:96/f:opus", "s3://masters/a.wav")
      ...> == AudioProxy.CacheKey.derive!("f:opus/br:96", "s3://masters/a.wav")
      true
  """

  alias AudioProxy.OptionError
  alias AudioProxy.Options

  @separator "\n"

  @typedoc "Lowercase hex SHA-256 digest, 64 characters."
  @type t :: String.t()

  @doc """
  Derives the cache key for `options` rendered from `source`.

  `options` may be an options string, a pre-split segment list, or an already
  parsed `t:AudioProxy.Options.t/0`. Every form is validated before it is
  hashed — including the struct, which is not assumed to have come from
  `AudioProxy.Options.parse/1` — so no options that describe an unrenderable
  variant can acquire a cache key.

  `source` is the canonical source identity produced by the source resolver
  (e.g. `"s3://bucket/key"`) — this module does not canonicalize it.
  """
  @spec derive(Options.t() | String.t() | [String.t()], String.t()) ::
          {:ok, t()} | {:error, OptionError.t()}
  def derive(%Options{} = options, source) when is_binary(source) do
    with {:ok, options} <- Options.validate(options) do
      {:ok, digest(Options.normalize(options), source)}
    end
  end

  def derive(options, source) when is_binary(source) do
    with {:ok, normalized} <- Options.normalize_string(options) do
      {:ok, digest(normalized, source)}
    end
  end

  @doc """
  Like `derive/2`, raising `ArgumentError` when the options are invalid.

  For call sites that have already validated their options — tests, and the
  render path once the request has passed the options gate.
  """
  @spec derive!(Options.t() | String.t() | [String.t()], String.t()) :: t()
  def derive!(options, source) do
    case derive(options, source) do
      {:ok, key} -> key
      {:error, error} -> raise ArgumentError, OptionError.message(error)
    end
  end

  defp digest(normalized, source) do
    :sha256
    |> :crypto.hash(normalized <> @separator <> source)
    |> Base.encode16(case: :lower)
  end
end