defmodule Aprsme.Callsign do @moduledoc """ Validation and normalization for APRS source identifiers. RF callsigns follow AX.25's shorter shape, while APRS-IS also carries application and gateway identifiers. Validation therefore enforces a conservative transport-safe character set and length without rejecting established APRS-IS identifiers. """ @safe_identifier_regex ~r/^[A-Z0-9]+(?:-[A-Z0-9]+)*$/ @spec valid?(term()) :: boolean() def valid?(callsign) when is_binary(callsign) do normalized = normalize(callsign) byte_size(normalized) in 1..20 and Regex.match?(@safe_identifier_regex, normalized) end def valid?(_callsign), do: false @spec normalize(term()) :: String.t() def normalize(callsign) when is_binary(callsign), do: callsign |> String.trim() |> String.upcase() def normalize(_callsign), do: "" @spec matches?(term(), term()) :: boolean() def matches?(left, right), do: normalize(left) == normalize(right) @spec extract_base(term()) :: String.t() def extract_base(callsign), do: callsign |> extract_parts() |> elem(0) @spec extract_ssid(term()) :: String.t() def extract_ssid(callsign), do: callsign |> extract_parts() |> elem(1) @spec extract_parts(term()) :: {String.t(), String.t()} def extract_parts(callsign) when is_binary(callsign) do normalized = normalize(callsign) case Regex.run(~r/^(.*)-([0-9]+)$/, normalized) do [_, base, ssid] -> {base, ssid} _ -> {normalized, "0"} end end def extract_parts(_callsign), do: {"", "0"} end