Skip to content

Commit d2055b4

Browse files
authored
Merge pull request #9597 from ruby/clean-text-c1-controls
Strip C1 control characters from displayed gem text
2 parents f22056b + 95b6bfb commit d2055b4

4 files changed

Lines changed: 61 additions & 2 deletions

File tree

lib/rubygems/installer.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ def install
299299

300300
File.chmod(dir_mode, gem_dir) if dir_mode
301301

302-
say spec.post_install_message if options[:post_install_message] && !spec.post_install_message.nil?
302+
say clean_text(spec.post_install_message.to_s) if options[:post_install_message] && !spec.post_install_message.nil?
303303

304304
Gem::Specification.add_spec(spec) unless @install_dir
305305

lib/rubygems/text.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,16 @@ module Gem::Text
88
# Remove any non-printable characters and make the text suitable for
99
# printing.
1010
def clean_text(text)
11-
text.gsub(/[\000-\b\v-\f\016-\037\177]/, ".")
11+
text = text.gsub(/[\000-\b\v-\f\016-\037\177]/, ".")
12+
13+
# Match C1 control characters (U+0080-U+009F) as codepoints. This requires
14+
# a valid UTF-8 string so the regexp does not split a multibyte sequence;
15+
# strings in other encodings are left unchanged.
16+
if text.encoding == Encoding::UTF_8 && text.valid_encoding?
17+
text = text.gsub(/[\u0080-\u009f]/, ".")
18+
end
19+
20+
text
1221
end
1322

1423
def truncate_text(text, description, max_length = 100_000)

test/rubygems/test_gem_installer.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1481,6 +1481,39 @@ def test_install_with_skipped_message
14811481
refute_match(/I am a shiny gem!/, @ui.output)
14821482
end
14831483

1484+
def test_install_sanitizes_post_install_message
1485+
# Use for_spec so the in-memory message reaches the installer verbatim;
1486+
# building a gem would escape the control characters during serialization.
1487+
@spec = setup_base_spec
1488+
@spec.post_install_message = "shiny \e]2;pwn\a gem"
1489+
1490+
installer = Gem::Installer.for_spec @spec, post_install_message: true
1491+
installer.gem_home = @gemhome
1492+
1493+
use_ui @ui do
1494+
installer.install
1495+
end
1496+
1497+
assert_match(/shiny \.\]2;pwn\. gem/, @ui.output)
1498+
refute_match(/\e\]2;pwn/, @ui.output)
1499+
end
1500+
1501+
def test_install_handles_non_string_post_install_message
1502+
# post_install_message may be a non-String (the gemspec schema allows an
1503+
# array), so sanitizing must not assume it responds to gsub.
1504+
@spec = setup_base_spec
1505+
@spec.post_install_message = %w[one two]
1506+
1507+
installer = Gem::Installer.for_spec @spec, post_install_message: true
1508+
installer.gem_home = @gemhome
1509+
1510+
use_ui @ui do
1511+
installer.install
1512+
end
1513+
1514+
assert_match(/one/, @ui.output)
1515+
end
1516+
14841517
def test_install_extension_dir
14851518
gemhome2 = "#{@gemhome}2"
14861519

test/rubygems/test_gem_text.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,21 @@ def test_truncate_text
100100
def test_clean_text
101101
assert_equal ".]2;nyan.", clean_text("\e]2;nyan\a")
102102
end
103+
104+
def test_clean_text_strips_c1_control_characters
105+
text = [0x41, 0x9b, 0x42].pack("U*") # "A", CSI (U+009B), "B"
106+
assert_equal "A.B", clean_text(text)
107+
end
108+
109+
def test_clean_text_preserves_multibyte_characters
110+
# U+0400 encodes to bytes D0 80, whose 0x80 continuation byte must not be
111+
# mistaken for a C1 control byte. NEL (U+0085) is stripped.
112+
text = [0x400, 0x85].pack("U*")
113+
assert_equal [0x400, 0x2e].pack("U*"), clean_text(text)
114+
end
115+
116+
def test_clean_text_passes_through_non_unicode_encodings
117+
text = "x\x9by".dup.force_encoding("ISO-8859-1")
118+
assert_equal text, clean_text(text)
119+
end
103120
end

0 commit comments

Comments
 (0)