Skip to content

Commit 9c9f6ba

Browse files
committed
[GR-19220] Fix StringIO#write transcodes strings with a different encoding (#2927)
PullRequest: truffleruby/3700
2 parents 498159d + e538efa commit 9c9f6ba

File tree

9 files changed

+98
-5
lines changed

9 files changed

+98
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Bug fixes:
2424
* Fix constants lookup when `BasicObject#instance_eval` method is called with a String (#2810, @andrykonchin).
2525
* Don't trigger the `method_added` event when changing a method's visibility or calling `module_function` (@paracycle, @nirvdrum).
2626
* Fix `rb_time_timespec_new` function to not call `Time.at` method directly (@andrykonchin).
27+
* Fix `StringIO#write` to transcode strings with encodings that don't match the `StringIO`'s `external_encoding`. (#2839, @flavorjones)
2728

2829
Compatibility:
2930

lib/truffle/stringio.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,14 @@ def write(str)
280280
str = String(str)
281281
return 0 if str.empty?
282282

283+
# difference to IO, see https://github.com/ruby/stringio/blob/009896b973/ext/stringio/stringio.c#L1498-L1506
284+
enc = external_encoding
285+
unless enc == Encoding::BINARY or enc == Encoding::US_ASCII
286+
unless !str.ascii_only? && (str.encoding == Encoding::BINARY || str.encoding == Encoding::US_ASCII)
287+
str = Truffle::IOOperations.write_transcoding(str, enc)
288+
end
289+
end
290+
283291
d = @__data__
284292
TruffleRuby.synchronized(d) do
285293
pos = d.pos

spec/ruby/core/io/shared/write.rb

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,58 @@
9797
end
9898
end
9999
end
100+
101+
describe :io_write_transcode, shared: true do
102+
before :each do
103+
@transcode_filename = tmp("io_write_transcode")
104+
end
105+
106+
after :each do
107+
rm_r @transcode_filename
108+
end
109+
110+
it "transcodes the given string when the external encoding is set and neither is BINARY" do
111+
utf8_str = "hello"
112+
113+
File.open(@transcode_filename, "w", external_encoding: Encoding::UTF_16BE) do |file|
114+
file.external_encoding.should == Encoding::UTF_16BE
115+
file.send(@method, utf8_str)
116+
end
117+
118+
result = File.binread(@transcode_filename)
119+
expected = [0, 104, 0, 101, 0, 108, 0, 108, 0, 111] # UTF-16BE bytes for "hello"
120+
121+
result.bytes.should == expected
122+
end
123+
124+
it "transcodes the given string when the external encoding is set and the string encoding is BINARY" do
125+
str = "été".b
126+
127+
File.open(@transcode_filename, "w", external_encoding: Encoding::UTF_16BE) do |file|
128+
file.external_encoding.should == Encoding::UTF_16BE
129+
-> { file.send(@method, str) }.should raise_error(Encoding::UndefinedConversionError)
130+
end
131+
end
132+
end
133+
134+
describe :io_write_no_transcode, shared: true do
135+
before :each do
136+
@transcode_filename = tmp("io_write_no_transcode")
137+
end
138+
139+
after :each do
140+
rm_r @transcode_filename
141+
end
142+
143+
it "does not transcode the given string even when the external encoding is set" do
144+
utf8_str = "hello"
145+
146+
File.open(@transcode_filename, "w", external_encoding: Encoding::UTF_16BE) do |file|
147+
file.external_encoding.should == Encoding::UTF_16BE
148+
file.send(@method, utf8_str)
149+
end
150+
151+
result = File.binread(@transcode_filename)
152+
result.bytes.should == utf8_str.bytes
153+
end
154+
end

spec/ruby/core/io/syswrite_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,5 @@
7878

7979
describe "IO#syswrite" do
8080
it_behaves_like :io_write, :syswrite
81+
it_behaves_like :io_write_no_transcode, :syswrite
8182
end

spec/ruby/core/io/write_nonblock_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050

5151
describe "IO#write_nonblock" do
5252
it_behaves_like :io_write, :write_nonblock
53+
it_behaves_like :io_write_no_transcode, :write_nonblock
5354
end
5455
end
5556

spec/ruby/core/io/write_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@
220220

221221
describe "IO#write" do
222222
it_behaves_like :io_write, :write
223+
it_behaves_like :io_write_transcode, :write
223224

224225
it "accepts multiple arguments" do
225226
IO.pipe do |r, w|

spec/ruby/library/stringio/shared/write.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,27 @@
8888
@io.write "fghi"
8989
@io.string.should == "12fghi"
9090
end
91+
92+
it "transcodes the given string when the external encoding is set and neither is BINARY" do
93+
utf8_str = "hello"
94+
io = StringIO.new.set_encoding(Encoding::UTF_16BE)
95+
io.external_encoding.should == Encoding::UTF_16BE
96+
97+
io.send(@method, utf8_str)
98+
99+
expected = [0, 104, 0, 101, 0, 108, 0, 108, 0, 111] # UTF-16BE bytes for "hello"
100+
io.string.bytes.should == expected
101+
end
102+
103+
it "does not transcode the given string when the external encoding is set and the string encoding is BINARY" do
104+
str = "été".b
105+
io = StringIO.new.set_encoding(Encoding::UTF_16BE)
106+
io.external_encoding.should == Encoding::UTF_16BE
107+
108+
io.send(@method, str)
109+
110+
io.string.bytes.should == str.bytes
111+
end
91112
end
92113

93114
describe :stringio_write_not_writable, shared: true do

src/main/ruby/truffleruby/core/io.rb

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2324,11 +2324,7 @@ def write(*objects)
23242324

23252325
ensure_open_and_writable
23262326

2327-
if external_encoding && external_encoding != string.encoding && external_encoding != Encoding::BINARY
2328-
unless string.ascii_only? && external_encoding.ascii_compatible?
2329-
string = string.encode(external_encoding)
2330-
end
2331-
end
2327+
string = Truffle::IOOperations.write_transcoding(string, external_encoding)
23322328

23332329
count = Truffle::POSIX.write_string self, string, true
23342330
bytes_written += count

src/main/ruby/truffleruby/core/truffle/io_operations.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,15 @@ def self.puts(io, *args)
7272
nil
7373
end
7474

75+
def self.write_transcoding(string, external_encoding)
76+
if external_encoding && external_encoding != string.encoding && external_encoding != Encoding::BINARY &&
77+
!(string.ascii_only? && external_encoding.ascii_compatible?)
78+
string.encode(external_encoding)
79+
else
80+
string
81+
end
82+
end
83+
7584
def self.dup2_with_cloexec(old_fd, new_fd)
7685
if new_fd < 3
7786
# STDIO should not be made close-on-exec. `dup2` clears the close-on-exec bit for the destination FD.

0 commit comments

Comments
 (0)