Skip to content

Commit 773fd3f

Browse files
committed
[GR-18163] Fix Kernel#sprintf: take into account encoding of %c argument
PullRequest: truffleruby/3451
2 parents f9ae7a9 + 2aa077c commit 773fd3f

File tree

20 files changed

+338
-192
lines changed

20 files changed

+338
-192
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ Compatibility:
2626
* Always terminate native strings with enough `\0` bytes (#2704, @eregon).
2727
* Support `#dup` and `#clone` on foreign strings (@eregon).
2828
* Fix `Regexp.new` to coerce non-String arguments (#2705, @andrykonchin).
29+
* Fix `Kernel#sprintf` formatting for `%c` when used non-ASCII encoding (#2369, @andrykonchin).
30+
* Fix `Kernel#sprintf` argument casting for `%c` (@andrykonchin).
2931

3032
Performance:
3133

lib/truffle/stringio.rb

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,7 @@ def print(*args)
8787
def printf(*args)
8888
check_writable
8989

90-
if args.size > 1
91-
write(args.shift % args)
92-
else
93-
write(args.first)
94-
end
90+
write(sprintf(*args))
9591

9692
nil
9793
end

spec/ruby/core/kernel/shared/sprintf.rb

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -292,18 +292,65 @@ def obj.to_i; 10; end
292292
it "raises ArgumentError if argument is a string of several characters" do
293293
-> {
294294
@method.call("%c", "abc")
295-
}.should raise_error(ArgumentError)
295+
}.should raise_error(ArgumentError, /%c requires a character/)
296296
end
297297

298298
it "raises ArgumentError if argument is an empty string" do
299299
-> {
300300
@method.call("%c", "")
301-
}.should raise_error(ArgumentError)
301+
}.should raise_error(ArgumentError, /%c requires a character/)
302+
end
303+
304+
it "raises TypeError if argument is not String or Integer and cannot be converted to them" do
305+
-> {
306+
@method.call("%c", [])
307+
}.should raise_error(TypeError, /no implicit conversion of Array into Integer/)
302308
end
303309

304-
it "supports Unicode characters" do
305-
@method.call("%c", 1286).should == "Ԇ"
306-
@method.call("%c", "ش").should == "ش"
310+
it "raises TypeError if argument is nil" do
311+
-> {
312+
@method.call("%c", nil)
313+
}.should raise_error(TypeError, /no implicit conversion from nil to integer/)
314+
end
315+
316+
it "tries to convert argument to String with to_str" do
317+
obj = BasicObject.new
318+
def obj.to_str
319+
"a"
320+
end
321+
322+
@method.call("%c", obj).should == "a"
323+
end
324+
325+
it "tries to convert argument to Integer with to_int" do
326+
obj = BasicObject.new
327+
def obj.to_int
328+
90
329+
end
330+
331+
@method.call("%c", obj).should == "Z"
332+
end
333+
334+
it "raises TypeError if converting to String with to_str returns non-String" do
335+
obj = BasicObject.new
336+
def obj.to_str
337+
:foo
338+
end
339+
340+
-> {
341+
@method.call("%c", obj)
342+
}.should raise_error(TypeError, /can't convert BasicObject to String/)
343+
end
344+
345+
it "raises TypeError if converting to Integer with to_int returns non-Integer" do
346+
obj = BasicObject.new
347+
def obj.to_str
348+
:foo
349+
end
350+
351+
-> {
352+
@method.call("%c", obj)
353+
}.should raise_error(TypeError, /can't convert BasicObject to String/)
307354
end
308355
end
309356

@@ -362,11 +409,11 @@ def obj.to_str
362409
@method.call("%4.6s", "abcdefg").should == "abcdef"
363410
end
364411

365-
it "formats nli with width" do
412+
it "formats nil with width" do
366413
@method.call("%6s", nil).should == " "
367414
end
368415

369-
it "formats nli with precision" do
416+
it "formats nil with precision" do
370417
@method.call("%.6s", nil).should == ""
371418
end
372419

@@ -927,4 +974,8 @@ def obj.to_str; end
927974
}
928975
end
929976
end
977+
978+
it "does not raise error when passed more arguments than needed" do
979+
sprintf("%s %d %c", "string", 2, "c", []).should == "string 2 c"
980+
end
930981
end

spec/ruby/core/kernel/shared/sprintf_encoding.rb

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# Keep encoding-related specs in a separate shared example to be able to skip them in IO/File/StringIO specs.
2+
# It's difficult to check result's encoding in the test after writing to a file/io buffer.
13
describe :kernel_sprintf_encoding, shared: true do
24
it "can produce a string with valid encoding" do
35
string = @method.call("good day %{valid}", valid: "e")
@@ -25,12 +27,41 @@
2527
result.encoding.should equal(Encoding::UTF_8)
2628
end
2729

28-
it "raises Encoding::CompatibilityError if both encodings are ASCII compatible and there ano not ASCII characters" do
30+
it "raises Encoding::CompatibilityError if both encodings are ASCII compatible and there are not ASCII characters" do
2931
string = "Ä %s".encode('windows-1252')
3032
argument = "Ђ".encode('windows-1251')
3133

3234
-> {
3335
@method.call(string, argument)
3436
}.should raise_error(Encoding::CompatibilityError)
3537
end
38+
39+
describe "%c" do
40+
it "supports Unicode characters" do
41+
result = @method.call("%c", 1286)
42+
result.should == "Ԇ"
43+
result.bytes.should == [212, 134]
44+
45+
result = @method.call("%c", "ش")
46+
result.should == "ش"
47+
result.bytes.should == [216, 180]
48+
end
49+
50+
it "raises error when a codepoint isn't representable in an encoding of a format string" do
51+
format = "%c".encode("ASCII")
52+
53+
-> {
54+
@method.call(format, 1286)
55+
}.should raise_error(RangeError, /out of char range/)
56+
end
57+
58+
it "uses the encoding of the format string to interpret codepoints" do
59+
format = "%c".force_encoding("euc-jp")
60+
result = @method.call(format, 9415601)
61+
62+
result.encoding.should == Encoding::EUC_JP
63+
result.should == "é".encode(Encoding::EUC_JP)
64+
result.bytes.should == [143, 171, 177]
65+
end
66+
end
3667
end

spec/ruby/library/stringio/open_spec.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,14 @@
167167
io.should equal(ret)
168168
end
169169

170-
it "sets the mode to read-write" do
170+
it "sets the mode to read-write (r+)" do
171171
io = StringIO.open("example")
172172
io.closed_read?.should be_false
173173
io.closed_write?.should be_false
174+
175+
io = StringIO.new("example")
176+
io.printf("%d", 123)
177+
io.string.should == "123mple"
174178
end
175179

176180
it "tries to convert the passed Object to a String using #to_str" do
@@ -195,10 +199,14 @@
195199
io.should equal(ret)
196200
end
197201

198-
it "sets the mode to read-write" do
202+
it "sets the mode to read-write (r+)" do
199203
io = StringIO.open
200204
io.closed_read?.should be_false
201205
io.closed_write?.should be_false
206+
207+
io = StringIO.new("example")
208+
io.printf("%d", 123)
209+
io.string.should == "123mple"
202210
end
203211

204212
it "uses an empty String as the StringIO backend" do

spec/ruby/library/stringio/printf_spec.rb

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@
44

55
describe "StringIO#printf" do
66
before :each do
7-
@io = StringIO.new('example')
7+
@io = StringIO.new()
88
end
99

1010
it "returns nil" do
1111
@io.printf("%d %04x", 123, 123).should be_nil
1212
end
1313

1414
it "pads self with \\000 when the current position is after the end" do
15-
@io.pos = 10
15+
@io.pos = 3
1616
@io.printf("%d", 123)
17-
@io.string.should == "example\000\000\000123"
17+
@io.string.should == "\000\000\000123"
1818
end
1919

2020
it "performs format conversion" do
@@ -39,6 +39,27 @@
3939
end
4040
end
4141

42+
describe "StringIO#printf when in read-write mode" do
43+
before :each do
44+
@io = StringIO.new("example", "r+")
45+
end
46+
47+
it "starts from the beginning" do
48+
@io.printf("%s", "abcdefghijk")
49+
@io.string.should == "abcdefghijk"
50+
end
51+
52+
it "does not truncate existing string" do
53+
@io.printf("%s", "abc")
54+
@io.string.should == "abcmple"
55+
end
56+
57+
it "correctly updates self's position" do
58+
@io.printf("%s", "abc")
59+
@io.pos.should eql(3)
60+
end
61+
end
62+
4263
describe "StringIO#printf when in append mode" do
4364
before :each do
4465
@io = StringIO.new("example", "a")

spec/tags/core/kernel/printf_tags.txt

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,6 @@
1-
fails:Kernel.printf formatting io is specified other formats c raises ArgumentError if argument is an empty string
2-
fails:Kernel.printf formatting io is specified other formats c supports Unicode characters
31
fails:Kernel.printf formatting io is specified other formats s does not try to convert with to_str
42
fails:Kernel.printf formatting io is specified flags # applies to format o does nothing for negative argument
53
fails:Kernel.printf formatting io is specified flags # applies to formats bBxX does nothing for zero argument
6-
fails:Kernel.printf formatting io is specified flags # applies to formats aAeEfgG forces a decimal point to be added, even if no digits follow
7-
fails:Kernel.printf formatting io is specified flags # applies to gG does not remove trailing zeros
8-
fails:Kernel.printf formatting io is specified flags - left-justifies the result of conversion if width is specified
9-
fails:Kernel.printf formatting io is specified flags 0 (zero) applies to numeric formats bBdiouxXaAeEfgG and width is specified uses radix-1 when displays negative argument as a two's complement
10-
fails:Kernel.printf formatting io is specified flags * left-justifies the result if width is negative
11-
fails:Kernel.printf formatting io is specified flags * left-justifies the result if specified with $ argument is negative
12-
fails:Kernel.printf formatting io is specified precision string formats determines the maximum number of characters to be copied from the string
13-
fails:Kernel.printf formatting io is specified reference by name %{name} style supports flags, width and precision
14-
fails:Kernel.printf formatting io is not specified other formats c raises ArgumentError if argument is an empty string
15-
fails:Kernel.printf formatting io is not specified other formats c supports Unicode characters
164
fails:Kernel.printf formatting io is not specified other formats s does not try to convert with to_str
175
fails:Kernel.printf formatting io is not specified flags # applies to format o does nothing for negative argument
18-
fails:Kernel.printf formatting io is not specified flags # applies to formats bBxX does nothing for zero argument
19-
fails:Kernel.printf formatting io is not specified flags # applies to formats aAeEfgG forces a decimal point to be added, even if no digits follow
20-
fails:Kernel.printf formatting io is not specified flags # applies to gG does not remove trailing zeros
21-
fails:Kernel.printf formatting io is not specified flags - left-justifies the result of conversion if width is specified
22-
fails:Kernel.printf formatting io is not specified flags 0 (zero) applies to numeric formats bBdiouxXaAeEfgG and width is specified uses radix-1 when displays negative argument as a two's complement
23-
fails:Kernel.printf formatting io is not specified flags * left-justifies the result if width is negative
24-
fails:Kernel.printf formatting io is not specified flags * left-justifies the result if specified with $ argument is negative
25-
fails:Kernel.printf formatting io is not specified precision string formats determines the maximum number of characters to be copied from the string
26-
fails:Kernel.printf formatting io is not specified reference by name %{name} style supports flags, width and precision
27-
fails:Kernel.printf formatting io is specified other formats % alone raises an ArgumentError
28-
fails:Kernel.printf formatting io is not specified other formats % alone raises an ArgumentError
6+
fails:Kernel.printf formatting io is not specified flags # applies to formats bBxX does nothing for zero argument
Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,10 @@
1-
fails:Kernel#sprintf returns a String in the same encoding as the format String if compatible
21
fails:Kernel#sprintf returns a String in the argument's encoding if format encoding is more restrictive
3-
fails:Kernel#sprintf raises Encoding::CompatibilityError if both encodings are ASCII compatible and there ano not ASCII characters
4-
fails:Kernel#sprintf other formats c raises ArgumentError if argument is an empty string
5-
fails:Kernel#sprintf other formats c supports Unicode characters
2+
fails:Kernel#sprintf raises Encoding::CompatibilityError if both encodings are ASCII compatible and there are not ASCII characters
63
fails:Kernel#sprintf other formats s does not try to convert with to_str
74
fails:Kernel#sprintf flags # applies to format o does nothing for negative argument
85
fails:Kernel#sprintf flags # applies to formats bBxX does nothing for zero argument
9-
fails:Kernel.sprintf returns a String in the same encoding as the format String if compatible
106
fails:Kernel.sprintf returns a String in the argument's encoding if format encoding is more restrictive
11-
fails:Kernel.sprintf raises Encoding::CompatibilityError if both encodings are ASCII compatible and there ano not ASCII characters
12-
fails:Kernel.sprintf other formats c raises ArgumentError if argument is an empty string
13-
fails:Kernel.sprintf other formats c supports Unicode characters
7+
fails:Kernel.sprintf raises Encoding::CompatibilityError if both encodings are ASCII compatible and there are not ASCII characters
148
fails:Kernel.sprintf other formats s does not try to convert with to_str
159
fails:Kernel.sprintf flags # applies to format o does nothing for negative argument
1610
fails:Kernel.sprintf flags # applies to formats bBxX does nothing for zero argument

spec/tags/core/string/modulo_tags.txt

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,7 @@
1-
fails:String#% output's encoding is the same as the format string if passed value is encoding-compatible
21
fails:String#% output's encoding negotiates a compatible encoding if necessary
32
fails:String#% output's encoding raises if a compatible encoding can't be found
4-
fails:String#% raises an error if single % appears at the end
5-
fails:String#% returns a String in the same encoding as the format String if compatible
63
fails:String#% returns a String in the argument's encoding if format encoding is more restrictive
7-
fails:String#% raises Encoding::CompatibilityError if both encodings are ASCII compatible and there ano not ASCII characters
8-
fails:String#% other formats c raises ArgumentError if argument is an empty string
9-
fails:String#% other formats c supports Unicode characters
4+
fails:String#% raises Encoding::CompatibilityError if both encodings are ASCII compatible and there are not ASCII characters
105
fails:String#% other formats s does not try to convert with to_str
11-
fails:String#% other formats % alone raises an ArgumentError
126
fails:String#% flags # applies to format o does nothing for negative argument
13-
fails:String#% flags # applies to formats bBxX does nothing for zero argument
14-
fails:String#% flags # applies to formats aAeEfgG forces a decimal point to be added, even if no digits follow
15-
fails:String#% flags # applies to gG does not remove trailing zeros
16-
fails:String#% flags - left-justifies the result of conversion if width is specified
17-
fails:String#% flags 0 (zero) applies to numeric formats bBdiouxXaAeEfgG and width is specified uses radix-1 when displays negative argument as a two's complement
18-
fails:String#% flags * left-justifies the result if width is negative
19-
fails:String#% flags * left-justifies the result if specified with $ argument is negative
20-
fails:String#% precision string formats determines the maximum number of characters to be copied from the string
21-
fails:String#% reference by name %{name} style supports flags, width and precision
7+
fails:String#% flags # applies to formats bBxX does nothing for zero argument
Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,3 @@
1-
fails:StringIO#printf formatting other formats c raises ArgumentError if argument is an empty string
2-
fails:StringIO#printf formatting other formats c supports Unicode characters
31
fails:StringIO#printf formatting other formats s does not try to convert with to_str
4-
fails:StringIO#printf formatting other formats % is escaped by %
52
fails:StringIO#printf formatting flags # applies to format o does nothing for negative argument
63
fails:StringIO#printf formatting flags # applies to formats bBxX does nothing for zero argument
7-
fails:StringIO#printf formatting flags # applies to formats aAeEfgG forces a decimal point to be added, even if no digits follow
8-
fails:StringIO#printf formatting flags # applies to gG does not remove trailing zeros
9-
fails:StringIO#printf formatting flags - left-justifies the result of conversion if width is specified
10-
fails:StringIO#printf formatting flags 0 (zero) applies to numeric formats bBdiouxXaAeEfgG and width is specified uses radix-1 when displays negative argument as a two's complement
11-
fails:StringIO#printf formatting flags * left-justifies the result if width is negative
12-
fails:StringIO#printf formatting flags * left-justifies the result if specified with $ argument is negative
13-
fails:StringIO#printf formatting precision string formats determines the maximum number of characters to be copied from the string
14-
fails:StringIO#printf formatting reference by name %{name} style supports flags, width and precision
15-
fails:StringIO#printf formatting other formats % alone raises an ArgumentError

0 commit comments

Comments
 (0)