Fix ADIF parser crash on records with multi-byte UTF-8 characters
Regex.scan with `return: :index` reports BYTE offsets, but String.slice/3
uses CHARACTER offsets. When a record contained a multi-byte UTF-8
character in an earlier field (e.g. "café" in NOTES), every subsequent
field's slice shifted one byte left, eventually landing on a literal
">" that crashed String.to_integer/1 with ArgumentError.
Switch to :binary.part/3 which is byte-based and matches what Regex.scan
reports. Also simplify the value offset calculation to use the scanned
tag_len directly instead of re-splitting the tag text. Reproduced in a
new regression test.
Prod stack trace: AdifImport.parse_fields/1 crashed on upload with
binary_to_integer(">") -- a FreeDV/N1MM logger that embedded UTF-8
characters in a NOTES field triggered it.
This commit is contained in:
parent
58cb8bb5bc
commit
265669fc3a
2 changed files with 28 additions and 8 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = """
|
||||
<NOTES:6>caf\u00e9 QSO<STATION_CALLSIGN:4>W5XD<CALL:4>K5TR<GRIDSQUARE:4>EM00<MY_GRIDSQUARE:4>EM12<FREQ:9>10368.000<MODE:2>CW<QSO_DATE:8>20260328<TIME_ON:6>180000<EOR>
|
||||
"""
|
||||
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue