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