Skip to content

Fix String.reverse/1 grapheme ordering around invalid utf8 bytes - #15645

Merged
josevalim merged 1 commit into
elixir-lang:mainfrom
AlexGx:ag-string_reverse-fix
Jul 21, 2026
Merged

Fix String.reverse/1 grapheme ordering around invalid utf8 bytes#15645
josevalim merged 1 commit into
elixir-lang:mainfrom
AlexGx:ag-string_reverse-fix

Conversation

@AlexGx

@AlexGx AlexGx commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Before

iex> "ab" <> <<255>> <> "cd"
<<97, 98, 255, 99, 100>>

iex> String.reverse("ab" <> <<255>> <> "cd")
<<98, 97, 255, 100, 99>> # "ba" <> <<255>> <> "dc"

After

iex> String.reverse("ab" <> <<255>> <> "cd")
<<100, 99, 255, 98, 97>> # "dc" <> <<255>> <> "ba"

@AlexGx

AlexGx commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Maybe regression after #11024

Comment thread lib/elixir/lib/string.ex

defp do_reverse({:error, <<byte, rest::bits>>}, acc),
do: :unicode.characters_to_binary(acc) <> <<byte>> <> do_reverse(:unicode_util.gc(rest), [])
do: do_reverse(:unicode_util.gc(rest), []) <> <<byte>> <> :unicode.characters_to_binary(acc)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we move them to the accumulator instead?

Suggested change
do: do_reverse(:unicode_util.gc(rest), []) <> <<byte>> <> :unicode.characters_to_binary(acc)
do: do_reverse(:unicode_util.gc(rest), [<<byte>>, acc])

@AlexGx AlexGx Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[<<byte>>, acc]variant may produce a nested lists. And also this affects hot path (strings without invalid utf8), because needs rewrite to something like:

defp do_reverse([grapheme | rest], acc),
    # extra calls grapheme_to_binary, was: do_reverse(:unicode_util.gc(rest), [grapheme | acc])
    do: do_reverse(:unicode_util.gc(rest), [grapheme_to_binary(grapheme) | acc])

  defp do_reverse([], acc),
    # do: :unicode.characters_to_binary(acc)
    do: IO.iodata_to_binary(acc)

If this is what you mean, I will do benchmark...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nested lists are supported in Unicode characters to binary:

iex>  :unicode.characters_to_binary([[?j], ?o, <<?s>>, [[?é]]])
"josé"

But we should probably do this instead anyway:

Suggested change
do: do_reverse(:unicode_util.gc(rest), []) <> <<byte>> <> :unicode.characters_to_binary(acc)
do: do_reverse(:unicode_util.gc(rest), [<<byte>> | acc])

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Im stuck, please help!

on OTP 29
iex> :unicode.characters_to_binary(["dc", [<<255>>, "b", "a"]])
{:error, "dc", [[<<255>>, "b", "a"]]}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, of course, the operation will fail due to the invalid byte.

@lukaszsamson

Copy link
Copy Markdown
Contributor

Shouldn't this function raise ArgumentError on invalid byte instead of silently processing?

@AlexGx

AlexGx commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@lukaszsamson Then all functions in String module would have to raise, which would be a BC

@lukaszsamson

lukaszsamson commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

String API assumes the inputs have been validated at boundary via String.valid?/1. I see no explicite handling of NULL bytes anywhere

@josevalim

Copy link
Copy Markdown
Member

A null byte is actually valid UTF-8 encoding. About invalid encodings, see this section on the module docs: https://elixir.hexdocs.pm/String.html#module-self-synchronization

@josevalim
josevalim merged commit 7918013 into elixir-lang:main Jul 21, 2026
15 checks passed
@josevalim

Copy link
Copy Markdown
Member

💚 💙 💜 💛 ❤️

@AlexGx
AlexGx deleted the ag-string_reverse-fix branch July 21, 2026 18:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants