lib/qr_nbu/encoders/base64url.ex

defmodule QRNBU.Encoders.Base64URL do
  @moduledoc """
  Base64URL encoding for NBU QR code data strings (V002/V003).

  Implements URL-safe Base64 encoding as per RFC 4648 Section 5.

  ## Encoding Rules

  - Uses `-` instead of `+`
  - Uses `_` instead of `/`
  - No padding (`=` characters removed)

  ## Examples

      iex> QRNBU.Encoders.Base64URL.encode("Hello")
      "SGVsbG8"

      iex> QRNBU.Encoders.Base64URL.decode("SGVsbG8")
      {:ok, "Hello"}
  """

  @doc """
  Encodes binary data to Base64URL format.

  ## Parameters

  - `data` - Binary data to encode

  ## Returns

  Base64URL-encoded string without padding.

  ## Examples

      iex> QRNBU.Encoders.Base64URL.encode("test")
      "dGVzdA"

      iex> QRNBU.Encoders.Base64URL.encode(<<0, 1, 2>>)
      "AAEC"
  """
  @spec encode(binary()) :: String.t()
  def encode(data) when is_binary(data) do
    data
    |> Base.url_encode64(padding: false)
  end

  @doc """
  Decodes Base64URL-encoded string back to binary.

  ## Parameters

  - `encoded` - Base64URL-encoded string

  ## Returns

  - `{:ok, decoded_binary}` if decoding succeeds
  - `{:error, reason}` if decoding fails

  ## Examples

      iex> QRNBU.Encoders.Base64URL.decode("dGVzdA")
      {:ok, "test"}

      iex> QRNBU.Encoders.Base64URL.decode("invalid!")
      {:error, "Invalid Base64URL string"}
  """
  @spec decode(String.t()) :: {:ok, binary()} | {:error, String.t()}
  def decode(encoded) when is_binary(encoded) do
    case Base.url_decode64(encoded, padding: false) do
      {:ok, decoded} -> {:ok, decoded}
      :error -> {:error, "Invalid Base64URL string"}
    end
  end

  def decode(_), do: {:error, "Input must be a string"}

  @doc """
  Validates if a string is valid Base64URL format.

  ## Examples

      iex> QRNBU.Encoders.Base64URL.valid?("dGVzdA")
      true

      iex> QRNBU.Encoders.Base64URL.valid?("invalid!")
      false
  """
  @spec valid?(String.t()) :: boolean()
  def valid?(encoded) when is_binary(encoded) do
    # Base64URL uses A-Z, a-z, 0-9, -, _ (no padding)
    # Empty string is valid
    encoded == "" or String.match?(encoded, ~r/^[A-Za-z0-9_-]+$/)
  end

  def valid?(_), do: false
end