Skip to content

Transcode non-ASCII-compatible strings before dumping - #820

Open
Halvanhelv wants to merge 1 commit into
ruby:masterfrom
Halvanhelv:dump-non-ascii-compatible-encodings
Open

Transcode non-ASCII-compatible strings before dumping#820
Halvanhelv wants to merge 1 commit into
ruby:masterfrom
Halvanhelv:dump-non-ascii-compatible-encodings

Conversation

@Halvanhelv

Copy link
Copy Markdown

Fixes #819.

Psych.dump raises Encoding::CompatibilityError for any string in a non-ASCII-compatible encoding (UTF-16LE/BE, UTF-32LE/BE). An empty string is enough:

Psych.dump("".encode("UTF-16LE"))
# Encoding::CompatibilityError: incompatible encoding regexp match
# (US-ASCII regexp with UTF-16LE string)

This is asymmetric with the load side, which already accepts UTF-16 and has tests for it (test_transcode_utf16le / test_transcode_utf16be).

Cause

visit_String picks the scalar style by matching against US-ASCII regexp literals — yaml_tree.rb:303, :313 and :316. A string whose encoding is not ASCII-compatible cannot be matched against an ASCII regexp at all, so the first of them raises regardless of the string's content. The binary? guard above them only covers ASCII_8BIT, so UTF-16/32 falls through.

The crashing set maps exactly onto Encoding#ascii_compatible? — every other non-UTF-8 encoding (ISO-8859-1, EUC-JP, Windows-1252, Shift_JIS) already transcodes and round-trips fine, so this looks like an unhandled case rather than a deliberate restriction.

Fix

Transcode to UTF-8 at the top of visit_String when the encoding is not ASCII-compatible, so every check below operates on a matchable string.

Placing it before binary? rather than patching the individual regexps means all three checks are covered at once. It is safe there: binary? tests for ASCII_8BIT, which neither the original nor the transcoded string is, so that branch is unaffected; reassigning o is the pattern the method already uses (the binary branch does o = [o].pack('m0')); and alias/anchor registration happens in accept against the original object, so object identity tracking is untouched. UTF-8 input skips encode entirely.

Strings with invalid byte sequences still raise from encode, which matches the existing behaviour for ascii-compatible encodings — psych already raises ArgumentError: invalid byte sequence for those. Using :invalid => :replace would silently corrupt data.

Result

UTF-16/32 strings now produce byte-identical output to their UTF-8 equivalents, and are not tagged !binary:

input dumped
"multi\nline" --- |-\n multi\n line\n
"<<" --- !!str '<<'\n
"yes" --- 'yes'\n
"0123" --- '0123'\n

Those exercise lines 303, 313 and 316 respectively, so the style logic is genuinely reached rather than bypassed.

Tests

Added test_dump_non_ascii_compatible_encoding, asserting that all four encodings dump identically to UTF-8 across seven content shapes (empty, plain, embedded newline, non-ASCII, and the yes / << / 0123 special-cased forms). It errors without the fix and passes with it.

Full suite before: 678 tests, 1706 assertions, 0 failures, 0 errors.
Full suite after: 679 tests, 1734 assertions, 0 failures, 0 errors.

`visit_String` matches the string against US-ASCII regexp literals to
decide the scalar style. A string whose encoding is not ASCII-compatible
cannot be matched against an ASCII regexp, so `Psych.dump` raised
`Encoding::CompatibilityError` for any UTF-16/UTF-32 string, empty ones
included. The `binary?` guard above those checks only covers ASCII_8BIT,
so these encodings fell through to the regexp.

Transcode to UTF-8 up front when the encoding is not ASCII-compatible, so
every check below operates on a matchable string. Such strings now dump
exactly as their UTF-8 equivalents do, matching how the other non-UTF-8
encodings already behave, and matching `Psych.load`, which already
accepts UTF-16 input.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Psych.dump raises Encoding::CompatibilityError for UTF-16/UTF-32 strings

1 participant