Fix CSV parsing and page number input validation

- RFC 4180 CSV parser: handles quoted fields with commas and escaped
  quotes ("Smith, John" no longer breaks parsing)
- Safe page number parsing: non-numeric input defaults to page 1
  instead of raising ArgumentError
This commit is contained in:
Graham McIntire 2026-04-02 12:32:59 -05:00
parent 79af4e4959
commit 2b14e836f1
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
2 changed files with 48 additions and 2 deletions

View file

@ -56,7 +56,7 @@ defmodule Microwaveprop.Radio.CsvImport do
end
defp parse_and_import_row(line, submitter_email) do
fields = line |> String.split(",") |> Enum.map(&String.trim/1)
fields = parse_csv_fields(line)
if length(fields) == @expected_columns do
attrs =
@ -174,6 +174,48 @@ defmodule Microwaveprop.Radio.CsvImport do
{:ok, "#{y}-#{m}-#{d}T#{h}:#{mi}:#{s}Z"}
end
# RFC 4180 CSV field parser. Handles quoted fields with commas and escaped quotes.
defp parse_csv_fields(line) do
line
|> String.trim()
|> do_parse_fields([], "")
|> Enum.reverse()
|> Enum.map(&String.trim/1)
end
defp do_parse_fields("", acc, current), do: [current | acc]
defp do_parse_fields(<<"\"", rest::binary>>, acc, "") do
parse_quoted_field(rest, acc, "")
end
defp do_parse_fields(<<",", rest::binary>>, acc, current) do
do_parse_fields(rest, [current | acc], "")
end
defp do_parse_fields(<<c, rest::binary>>, acc, current) do
do_parse_fields(rest, acc, current <> <<c>>)
end
defp parse_quoted_field(<<"\"\"", rest::binary>>, acc, current) do
parse_quoted_field(rest, acc, current <> "\"")
end
defp parse_quoted_field(<<"\"", rest::binary>>, acc, current) do
# End of quoted field — skip to next comma or end
case rest do
<<"," , rest2::binary>> -> do_parse_fields(rest2, [current | acc], "")
"" -> [current | acc]
_ -> do_parse_fields(rest, [current | acc], "")
end
end
defp parse_quoted_field(<<c, rest::binary>>, acc, current) do
parse_quoted_field(rest, acc, current <> <<c>>)
end
defp parse_quoted_field("", acc, current), do: [current | acc]
defp changeset_error_strings(changeset) do
changeset
|> Ecto.Changeset.traverse_errors(fn {message, opts} ->

View file

@ -15,7 +15,11 @@ defmodule MicrowavepropWeb.ContactLive.Index do
@impl true
def handle_params(params, _uri, socket) do
page = params |> Map.get("page", "1") |> String.to_integer() |> max(1)
page =
case Integer.parse(Map.get(params, "page", "1")) do
{n, _} -> max(n, 1)
:error -> 1
end
sort_by = validate_sort_field(Map.get(params, "sort_by", @default_sort_by))
sort_order = validate_sort_order(Map.get(params, "sort_order", @default_sort_order))
search = Map.get(params, "search", "")