recover a wrong closed tag

This commit is contained in:
mmacia committed 2026-08-28 22:35:11 +00:00
1 parent fd3a74f6d6
commit 958b9db9af
2 files changed
+99 -7

No files matched your search

+91 -7
View File
@@ -337,15 +337,99 @@ defmodule FeedReader.Parser do
defp flush_entities({_mode, pending}), do: [pending]
defp normalize_attribute_quotes(input) do
Stream.map(input, fn chunk ->
Regex.replace(
~r/(=\")([^\"]*)(\")([^\"]*)(\")(\")(?=\s*(?:\/>|>))/,
chunk,
"\\1\\2"\\4"\\6"
)
end)
Stream.transform(
input,
fn -> <<>> end,
fn chunk, pending ->
{output, pending} = scan_attribute_markup(pending <> chunk, [])
{if(output == [], do: [], else: [IO.iodata_to_binary(Enum.reverse(output))]), pending}
end,
fn
<<>> -> []
pending -> [pending]
end
)
end
defp scan_attribute_markup(<<>>, output), do: {output, <<>>}
defp scan_attribute_markup(data, output) do
case :binary.match(data, "<") do
:nomatch ->
{[data | output], <<>>}
{position, 1} when position > 0 ->
{prefix, rest} = :erlang.split_binary(data, position)
scan_attribute_markup(rest, [prefix | output])
{0, 1} ->
case markup_end(data) do
:incomplete -> {output, data}
{markup, rest} -> scan_attribute_markup(rest, [repair_markup_quotes(markup) | output])
end
end
end
defp markup_end(<<"<![CDATA[", _rest::binary>> = data), do: special_markup_end(data, "]]>")
defp markup_end(<<"<!--", _rest::binary>> = data), do: special_markup_end(data, "-->")
defp markup_end(data) do
case :binary.match(data, ">") do
:nomatch -> :incomplete
{position, 1} -> :erlang.split_binary(data, position + 1)
end
end
defp special_markup_end(data, closing) do
case :binary.match(data, closing) do
:nomatch -> :incomplete
{position, length} -> :erlang.split_binary(data, position + length)
end
end
defp repair_markup_quotes(<<"<![CDATA[", _rest::binary>> = markup), do: markup
defp repair_markup_quotes(<<"<!--", _rest::binary>> = markup), do: markup
defp repair_markup_quotes(<<"<", rest::binary>>),
do: "<" <> repair_tag_quotes(rest, nil, false, [])
defp repair_tag_quotes(<<>>, _quote, _expect, output),
do: IO.iodata_to_binary(Enum.reverse(output))
defp repair_tag_quotes(<<char, rest::binary>>, nil, expect, output)
when char in [32, 9, 10, 13],
do: repair_tag_quotes(rest, nil, expect, [<<char>> | output])
defp repair_tag_quotes(<<char, rest::binary>>, nil, true, output)
when char in [34, 39],
do: repair_tag_quotes(rest, {char, false}, false, [<<char>> | output])
defp repair_tag_quotes(<<char, rest::binary>>, nil, _expect, output),
do: repair_tag_quotes(rest, nil, char == ?=, [<<char>> | output])
defp repair_tag_quotes(<<quote, rest::binary>>, {quote, has_content}, _expect, output) do
cond do
delimiter?(rest) ->
repair_tag_quotes(rest, nil, false, [<<quote>> | output])
empty_attribute_quote?(rest, quote, has_content) ->
<<_quote, after_quote::binary>> = rest
repair_tag_quotes(after_quote, nil, false, [<<quote>> | output])
true ->
repair_tag_quotes(rest, {quote, true}, false, ["&quot;" | output])
end
end
defp repair_tag_quotes(<<char, rest::binary>>, {quote, _has_content}, _expect, output),
do: repair_tag_quotes(rest, {quote, true}, false, [<<char>> | output])
defp delimiter?(<<char, _rest::binary>>) when char in [32, 9, 10, 13, 47, 62, 63], do: true
defp delimiter?(_rest), do: false
defp empty_attribute_quote?(<<quote, rest::binary>>, quote, false), do: delimiter?(rest)
defp empty_attribute_quote?(_rest, _quote, _has_content), do: false
defp event_stream(input, format, opts, chunk_size) do
Stream.resource(
fn -> start_parser(input, format, opts, chunk_size) end,
+8
View File
@@ -91,6 +91,14 @@ defmodule FeedReader.CollectorTest do
FeedReader.parse_binary(xml, max_items: 1)
end
test "does not repair quotes across adjacent XML elements" do
xml =
~s(<rss version="2.0"><channel><title>Example</title><item><source url="">Source</source><category domain="region">News</category></item></channel></rss>)
assert {:ok, %ParseResult{items: [%Item{title: nil}]}} =
FeedReader.parse_binary(xml, max_items: 1, chunk_size: 1)
end
test "rejects invalid chunks and binary arguments" do
assert {:error, :invalid_chunk} =
FeedReader.parse_stream([@xml, 123], max_items: 2)