defmodule AprsmeWeb.Live.Shared.ParamUtils do @moduledoc """ Shared parameter parsing, validation, and sanitization utilities. Used across multiple LiveView modules for consistent parameter handling. """ @doc """ Parse float value within specified range with sanitization. """ @spec parse_float_in_range(binary() | any(), float(), float(), float()) :: float() def parse_float_in_range(str, default, min, max) when is_binary(str) do # Sanitize input first sanitized = sanitize_numeric_string(str) case Float.parse(sanitized) do {val, ""} when val >= min and val <= max -> if finite?(val), do: val, else: default {val, _remainder} when val >= min and val <= max -> # Accept even with trailing characters, but validate the number if finite?(val), do: val, else: default _ -> default end end def parse_float_in_range(_, default, _, _), do: default @doc """ Parse integer value within specified range with sanitization. """ @spec parse_int_in_range(binary() | any(), integer(), integer(), integer()) :: integer() def parse_int_in_range(str, default, min, max) when is_binary(str) do # Sanitize input first sanitized = sanitize_numeric_string(str) case Integer.parse(sanitized) do {val, ""} when val >= min and val <= max -> val {val, _remainder} when val >= min and val <= max -> # Accept even with trailing characters val _ -> default end end def parse_int_in_range(_, default, _, _), do: default @doc """ Sanitize numeric strings to prevent injection attacks. """ @spec sanitize_numeric_string(binary() | any()) :: binary() def sanitize_numeric_string(str) when is_binary(str) do # Remove any potentially dangerous characters, keeping only numbers, dots, minus, and e/E for scientific notation str |> String.trim() |> String.replace(~r/[^\d\.\-eE]/, "") # Prevent extremely long inputs |> limit_string_length(20) end def sanitize_numeric_string(_), do: "" @doc """ Limit string length to prevent DoS attacks. """ @spec limit_string_length(binary(), integer()) :: binary() def limit_string_length(str, max_length) when byte_size(str) > max_length do :binary.part(str, 0, max_length) end def limit_string_length(str, _), do: str @doc """ Check if a float is finite (not infinity or NaN). """ @spec finite?(float() | any()) :: boolean() def finite?(float) when is_float(float) do # In Elixir, we can't have infinity or NaN in regular floats # This function is kept for API compatibility true end def finite?(_), do: false @doc """ Safely parse and clamp coordinate values. """ @spec safe_parse_coordinate(binary() | number() | any(), float(), float(), float()) :: float() def safe_parse_coordinate(value, default, min, max) when is_binary(value) do case parse_float_in_range(value, default, min, max) do ^default -> default parsed -> clamp_coordinate(parsed, min, max) end end def safe_parse_coordinate(value, default, min, max) when is_number(value) do if finite_number?(value) do clamp_coordinate(value, min, max) else default end end def safe_parse_coordinate(_, default, _, _), do: default @doc """ Clamp coordinate value within bounds. """ @spec clamp_coordinate(number(), number(), number()) :: float() def clamp_coordinate(value, min, max) when is_number(value) do value |> max(min) |> min(max) end def clamp_coordinate(_, _, _), do: 0.0 @doc """ Clamp zoom level within valid range. """ @spec clamp_zoom(binary() | integer() | float() | any()) :: integer() def clamp_zoom(zoom) when is_binary(zoom) do parse_int_in_range(zoom, 5, 1, 20) end def clamp_zoom(zoom) when is_integer(zoom) do max(1, min(20, zoom)) end def clamp_zoom(zoom) when is_float(zoom) do max(1, min(20, trunc(zoom))) end def clamp_zoom(_), do: 5 @doc """ Check if a number is finite. """ @spec finite_number?(number() | any()) :: boolean() def finite_number?(num) when is_number(num) do # Convert integer to float for check finite?(num / 1.0) end def finite_number?(_), do: false @doc """ Validate callsign format. """ @spec valid_callsign?(binary()) :: boolean() def valid_callsign?(callsign) when is_binary(callsign) do # Basic callsign validation - alphanumeric with optional dash and suffix String.match?(callsign, ~r/^[A-Z0-9]{1,6}(-[A-Z0-9]{1,2})?$/i) end def valid_callsign?(_), do: false @doc """ Sanitize path string for RF path visualization. """ @spec sanitize_path_string(binary() | any()) :: binary() def sanitize_path_string(path) when is_binary(path) do # Remove potentially dangerous characters, keep only alphanumeric, dash, comma, space, asterisk path |> String.trim() |> String.replace(~r/[^A-Za-z0-9\-,\s\*]/, "") |> limit_string_length(200) end def sanitize_path_string(_), do: "" @doc """ Converts various numeric types to float for consistent display. """ @spec to_float(any()) :: float() def to_float(value) do Aprsme.EncodingUtils.to_float(value) || 0.0 end end