diff --git a/lib/microwaveprop/radio/adif_import.ex b/lib/microwaveprop/radio/adif_import.ex index 6a53131a..0b7906ec 100644 --- a/lib/microwaveprop/radio/adif_import.ex +++ b/lib/microwaveprop/radio/adif_import.ex @@ -159,20 +159,23 @@ defmodule Microwaveprop.Radio.AdifImport do |> Enum.reject(&(&1 == %{})) end + # Regex.scan with `return: :index` returns BYTE offsets. We must use + # `:binary.part/3` (byte-based) rather than `String.slice/3` (character- + # based); otherwise multi-byte UTF-8 characters earlier in the record + # (e.g. an accented NOTES field) shift every subsequent field and we + # wind up slicing the wrong substring. defp parse_fields(record_string) do ~r/<([^:>]+):(\d+)(?::[^>]*)?>/i |> Regex.scan(record_string, return: :index) |> Enum.reduce(%{}, fn indices, acc -> - [{tag_start, _tag_len}, {name_start, name_len}, {size_start, size_len} | _] = indices - raw_name = record_string |> String.slice(name_start, name_len) |> String.upcase() + [{tag_start, tag_len}, {name_start, name_len}, {size_start, size_len} | _] = indices + raw_name = record_string |> :binary.part(name_start, name_len) |> String.upcase() name = strip_app_prefix(raw_name) - size = record_string |> String.slice(size_start, size_len) |> String.to_integer() + size = record_string |> :binary.part(size_start, size_len) |> String.to_integer() - # Value starts right after the closing > - # Find the > after the tag - tag_text = String.slice(record_string, tag_start, 999) - value_offset = tag_start + (tag_text |> String.split(">", parts: 2) |> List.first() |> byte_size()) + 1 - value = String.slice(record_string, value_offset, size) + # Value starts right after the closing > of the tag. + value_offset = tag_start + tag_len + value = :binary.part(record_string, value_offset, size) # Standard ADIF fields take precedence over app-defined ones if raw_name != name and Map.has_key?(acc, name) do diff --git a/test/microwaveprop/radio/adif_import_test.exs b/test/microwaveprop/radio/adif_import_test.exs index 4a4fea41..805d19e3 100644 --- a/test/microwaveprop/radio/adif_import_test.exs +++ b/test/microwaveprop/radio/adif_import_test.exs @@ -295,5 +295,22 @@ defmodule Microwaveprop.Radio.AdifImportTest do assert {:ok, preview} = AdifImport.preview(adif, @submitter) assert length(preview.valid) == 1 end + + test "parses records containing UTF-8 characters in earlier fields" do + # Regression: Regex.scan with `return: :index` returns byte offsets, + # but String.slice/3 uses character offsets. A multi-byte UTF-8 char + # (é = 2 bytes) in a NOTES field earlier in the record used to shift + # every subsequent field's character-based slice one byte left, + # causing a crash when the size slice landed on a ">" character. + adif = """ + caf\u00e9 QSOW5XDK5TREM00EM1210368.000CW20260328180000 + """ + + assert {:ok, preview} = AdifImport.preview(adif, @submitter) + assert [row] = preview.valid + assert row.attrs["station1"] == "W5XD" + assert row.attrs["station2"] == "K5TR" + assert row.attrs["band"] == "10000" + end end end