Skip to content

Commit e9d4b37

Browse files
authored
Fix Reline crash with invalid encoding history (#751)
1 parent 5353924 commit e9d4b37

File tree

5 files changed

+57
-4
lines changed

5 files changed

+57
-4
lines changed

lib/reline/history.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def [](index)
1919

2020
def []=(index, val)
2121
index = check_index(index)
22-
super(index, String.new(val, encoding: Reline.encoding_system_needs))
22+
super(index, Reline::Unicode.safe_encode(val, Reline.encoding_system_needs))
2323
end
2424

2525
def concat(*val)
@@ -45,7 +45,7 @@ def push(*val)
4545
end
4646
end
4747
super(*(val.map{ |v|
48-
String.new(v, encoding: Reline.encoding_system_needs)
48+
Reline::Unicode.safe_encode(v, Reline.encoding_system_needs)
4949
}))
5050
end
5151

@@ -56,7 +56,7 @@ def <<(val)
5656
if @config.history_size.positive?
5757
shift if size + 1 > @config.history_size
5858
end
59-
super(String.new(val, encoding: Reline.encoding_system_needs))
59+
super(Reline::Unicode.safe_encode(val, Reline.encoding_system_needs))
6060
end
6161

6262
private def check_index(index)

lib/reline/line_editor.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1325,7 +1325,7 @@ def insert_multiline_text(text)
13251325
save_old_buffer
13261326
pre = @buffer_of_lines[@line_index].byteslice(0, @byte_pointer)
13271327
post = @buffer_of_lines[@line_index].byteslice(@byte_pointer..)
1328-
lines = (pre + text.gsub(/\r\n?/, "\n") + post).split("\n", -1)
1328+
lines = (pre + Reline::Unicode.safe_encode(text, @encoding).gsub(/\r\n?/, "\n") + post).split("\n", -1)
13291329
lines << '' if lines.empty?
13301330
@buffer_of_lines[@line_index, 1] = lines
13311331
@line_index += lines.size - 1

lib/reline/unicode.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,22 @@ def self.escape_for_print(str)
5454
}.join
5555
end
5656

57+
def self.safe_encode(str, encoding)
58+
# Reline only supports utf-8 convertible string.
59+
converted = str.encode(encoding, invalid: :replace, undef: :replace)
60+
return converted if str.encoding == Encoding::UTF_8 || converted.encoding == Encoding::UTF_8 || converted.ascii_only?
61+
62+
# This code is essentially doing the same thing as
63+
# `str.encode(utf8, **replace_options).encode(encoding, **replace_options)`
64+
# but also avoids unneccesary irreversible encoding conversion.
65+
converted.gsub(/\X/) do |c|
66+
c.encode(Encoding::UTF_8)
67+
c
68+
rescue Encoding::UndefinedConversionError
69+
'?'
70+
end
71+
end
72+
5773
require 'reline/unicode/east_asian_width'
5874

5975
def self.get_mbchar_width(mbchar)

test/reline/test_history.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,15 @@ def test_history_size_negative_unlimited
266266
assert_equal 5, history.size
267267
end
268268

269+
def test_history_encoding_conversion
270+
history = history_new
271+
text1 = String.new("a\u{65535}b\xFFc", encoding: Encoding::UTF_8)
272+
text2 = String.new("d\xFFe", encoding: Encoding::Shift_JIS)
273+
history.push(text1.dup, text2.dup)
274+
expected = [text1, text2].map { |s| s.encode(Reline.encoding_system_needs, invalid: :replace, undef: :replace) }
275+
assert_equal(expected, history.to_a)
276+
end
277+
269278
private
270279

271280
def history_new(history_size: 10)

test/reline/test_unicode.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,32 @@ def test_take_mbchar_range
8989
assert_equal ["\e[31mc\1ABC\2d\e[0mef", 2, 4], Reline::Unicode.take_mbchar_range("\e[31mabc\1ABC\2d\e[0mefghi", 2, 4)
9090
assert_equal ["\e[41m \e[42mい\e[43m ", 1, 4], Reline::Unicode.take_mbchar_range("\e[41mあ\e[42mい\e[43mう", 1, 4, padding: true)
9191
end
92+
93+
def test_encoding_conversion
94+
texts = [
95+
String.new("invalid\xFFutf8", encoding: 'utf-8'),
96+
String.new("invalid\xFFsjis", encoding: 'sjis'),
97+
"utf8#{33111.chr('sjis')}convertible",
98+
"utf8#{33222.chr('sjis')}inconvertible",
99+
"sjis->utf8->sjis#{60777.chr('sjis')}irreversible"
100+
]
101+
utf8_texts = [
102+
'invalid�utf8',
103+
'invalid�sjis',
104+
'utf8仝convertible',
105+
'utf8�inconvertible',
106+
'sjis->utf8->sjis劦irreversible'
107+
]
108+
sjis_texts = [
109+
'invalid?utf8',
110+
'invalid?sjis',
111+
"utf8#{33111.chr('sjis')}convertible",
112+
'utf8?inconvertible',
113+
"sjis->utf8->sjis#{60777.chr('sjis')}irreversible"
114+
]
115+
assert_equal(utf8_texts, texts.map { |s| Reline::Unicode.safe_encode(s, 'utf-8') })
116+
assert_equal(utf8_texts, texts.map { |s| Reline::Unicode.safe_encode(s, Encoding::UTF_8) })
117+
assert_equal(sjis_texts, texts.map { |s| Reline::Unicode.safe_encode(s, 'sjis') })
118+
assert_equal(sjis_texts, texts.map { |s| Reline::Unicode.safe_encode(s, Encoding::Windows_31J) })
119+
end
92120
end

0 commit comments

Comments
 (0)