Creating a BEP3 compatible percent encoder (URI encoder)

Eureka!

defmodule App.BittorrentUrlEncoder do
  @moduledoc """
  URL encoding for Bittorrent Info hash v1. Designed to be compatible with qBittorrent's percent encoding.
  """

  import Bitwise

  @doc """
  Encodes `string` as a Bittorrent-flavored percent-encoded string.

  ## Example

      iex> encode("a88fda5954e89178c372716a6a78b8180ed4dad3")
      "%a8%8f%daYT%e8%91x%c3rqjjx%b8%18%0e%d4%da%d3"

  """
  @spec encode(binary()) :: binary()
  def encode(hex_string) when is_binary(hex_string) do
    hex_string
    |> Base.decode16!(case: :lower) # Decode from hex to raw bytes
    |> encode_bytes()
  end

  defp encode_bytes(<<>>), do: ""

  defp encode_bytes(<<byte, rest::binary>>) do
    percent_encode(byte) <> encode_bytes(rest)
  end

  defp percent_encode(byte) when byte in ?0..?9 or byte in ?a..?z or byte in ?A..?Z or byte in ~c"~_-.!" do
    <<byte>>
  end

  defp percent_encode(byte) do
    "%" <> <<hex(bsr(byte, 4)), hex(band(byte, 15))>>
  end

  defp hex(n) when n <= 9, do: n + ?0
  defp hex(n), do: n + ?a - 10
end

The secret sauce was changing ?A to ?a in the hex/1 function

-defp hex(n), do: n + ?A - 10
+defp hex(n), do: n + ?a - 10