Security: - Add SQL identifier validation to DbOptimizer.analyze_table/vacuum_table - Move health/readiness routes to non-rate-limited pipeline Correctness: - Fix monitor reference leak in StreamingPacketsPubSub (track refs, demonitor on unsubscribe) - Persist circuit breaker half-open state transitions - Simplify ShutdownHandler.terminate to avoid pointless Process.send_after Performance: - Batch marker removal into single WebSocket event (remove_markers_batch) - Use Task.Supervisor.start_child for fire-and-forget broadcasts Dead code removal: - Remove copy_insert/regular_batch_insert/rows_to_csv from DbOptimizer - Remove dead packet_pipeline_integration tests - Remove unused telemetry event detach Other: - Fix Callsign.valid? docstring to match permissive behavior - Fix DatabaseMetrics to delegate instead of hardcoding pool stats - Add circuit breaker and encoding test coverage
146 lines
3.6 KiB
Elixir
146 lines
3.6 KiB
Elixir
defmodule Aprsme.Callsign do
|
|
@moduledoc """
|
|
Utilities for callsign normalization, validation, and manipulation.
|
|
"""
|
|
|
|
@doc """
|
|
Normalizes a callsign by trimming whitespace and converting to uppercase.
|
|
"""
|
|
@spec normalize(String.t() | nil) :: String.t()
|
|
def normalize(nil), do: ""
|
|
|
|
def normalize(callsign) when is_binary(callsign) do
|
|
callsign
|
|
|> String.trim()
|
|
|> String.upcase()
|
|
end
|
|
|
|
def normalize(_), do: ""
|
|
|
|
@doc """
|
|
Checks whether a callsign is a non-empty string.
|
|
|
|
APRS uses a wide variety of identifiers beyond standard amateur radio
|
|
callsigns, including tactical callsigns, object names, and item names.
|
|
This function intentionally accepts any non-empty, non-whitespace-only
|
|
string to accommodate that variety.
|
|
|
|
## Examples
|
|
|
|
iex> Aprsme.Callsign.valid?("W5ABC")
|
|
true
|
|
|
|
iex> Aprsme.Callsign.valid?("W5ABC-15")
|
|
true
|
|
|
|
iex> Aprsme.Callsign.valid?("TACTICAL1")
|
|
true
|
|
|
|
iex> Aprsme.Callsign.valid?("")
|
|
false
|
|
|
|
iex> Aprsme.Callsign.valid?(nil)
|
|
false
|
|
"""
|
|
@spec valid?(String.t() | nil) :: boolean()
|
|
def valid?(nil), do: false
|
|
|
|
def valid?(callsign) when is_binary(callsign) do
|
|
trimmed = String.trim(callsign)
|
|
|
|
# Accept any non-empty callsign — APRS uses tactical callsigns,
|
|
# object names, and other non-standard identifiers
|
|
trimmed != ""
|
|
end
|
|
|
|
def valid?(_), do: false
|
|
|
|
@doc """
|
|
Checks if a packet's sender matches the target callsign.
|
|
Both callsigns are normalized before comparison.
|
|
"""
|
|
@spec matches?(String.t() | nil, String.t() | nil) :: boolean()
|
|
def matches?(packet_callsign, target_callsign) do
|
|
normalize(packet_callsign) == normalize(target_callsign)
|
|
end
|
|
|
|
@doc """
|
|
Extracts the base callsign from a full callsign (removes SSID if present).
|
|
|
|
## Examples
|
|
|
|
iex> Aprsme.Callsign.extract_base("W5ABC-15")
|
|
"W5ABC"
|
|
|
|
iex> Aprsme.Callsign.extract_base("W5ABC")
|
|
"W5ABC"
|
|
"""
|
|
@spec extract_base(String.t() | nil) :: String.t()
|
|
def extract_base(nil), do: ""
|
|
|
|
def extract_base(callsign) when is_binary(callsign) do
|
|
# Split on last hyphen to get base callsign
|
|
case String.split(callsign, "-") do
|
|
parts when length(parts) > 1 ->
|
|
parts |> Enum.drop(-1) |> Enum.join("-")
|
|
|
|
_ ->
|
|
callsign
|
|
end
|
|
end
|
|
|
|
def extract_base(_), do: ""
|
|
|
|
@doc """
|
|
Extracts the SSID from a callsign, returning "0" if no SSID is present.
|
|
|
|
## Examples
|
|
|
|
iex> Aprsme.Callsign.extract_ssid("W5ABC-15")
|
|
"15"
|
|
|
|
iex> Aprsme.Callsign.extract_ssid("W5ABC")
|
|
"0"
|
|
"""
|
|
@spec extract_ssid(String.t() | nil) :: String.t()
|
|
def extract_ssid(nil), do: "0"
|
|
|
|
def extract_ssid(callsign) when is_binary(callsign) do
|
|
# Extract whatever comes after the last hyphen as SSID
|
|
case String.split(callsign, "-") do
|
|
parts when length(parts) > 1 -> List.last(parts)
|
|
_ -> "0"
|
|
end
|
|
end
|
|
|
|
def extract_ssid(_), do: "0"
|
|
|
|
@doc """
|
|
Extracts both base callsign and SSID as a tuple.
|
|
|
|
## Examples
|
|
|
|
iex> Aprsme.Callsign.extract_parts("W5ABC-15")
|
|
{"W5ABC", "15"}
|
|
|
|
iex> Aprsme.Callsign.extract_parts("W5ABC")
|
|
{"W5ABC", "0"}
|
|
"""
|
|
@spec extract_parts(String.t() | nil) :: {String.t(), String.t()}
|
|
def extract_parts(nil), do: {"", "0"}
|
|
|
|
def extract_parts(callsign) when is_binary(callsign) do
|
|
# Split on last hyphen to separate base and SSID
|
|
case String.split(callsign, "-") do
|
|
parts when length(parts) > 1 ->
|
|
ssid = List.last(parts)
|
|
base = parts |> Enum.drop(-1) |> Enum.join("-")
|
|
{base, ssid}
|
|
|
|
_ ->
|
|
{callsign, "0"}
|
|
end
|
|
end
|
|
|
|
def extract_parts(_), do: {"", "0"}
|
|
end
|