Skip to content

Commit 7048f30

Browse files
authored
Merge pull request rails#47493 from olefriis/quote_binary_strings
Quote binary strings in Arel
2 parents 3dd5d49 + f424273 commit 7048f30

File tree

6 files changed

+87
-11
lines changed

6 files changed

+87
-11
lines changed

activemodel/lib/active_model/type/immutable_string.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,20 @@ def type
4646
def serialize(value)
4747
case value
4848
when ::Numeric, ::Symbol, ActiveSupport::Duration then value.to_s
49+
when ::String then serialize_cast_value(value)
4950
when true then @true
5051
when false then @false
5152
else super
5253
end
5354
end
5455

5556
def serialize_cast_value(value) # :nodoc:
57+
if value&.encoding == Encoding::BINARY
58+
# If we can treat the bytes as UTF-8 without changing them, then use UTF-8 as encoding
59+
new_value = value.dup.force_encoding(Encoding::UTF_8)
60+
return new_value if new_value.valid_encoding?
61+
end
62+
5663
value
5764
end
5865

activemodel/test/cases/type/immutable_string_test.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,26 @@ class ImmutableStringTest < ActiveModel::TestCase
1717
assert_same s, type.cast(s)
1818
assert_same s, type.deserialize(s)
1919
end
20+
21+
test "leaves validly encoded strings untouched" do
22+
s = "string with àccénts".encode(Encoding::ISO_8859_1)
23+
type = Type::ImmutableString.new
24+
assert_same s, type.serialize(s)
25+
end
26+
27+
test "serializes valid, binary-encoded strings to UTF-8" do
28+
s = "string with àccénts".b
29+
type = Type::ImmutableString.new
30+
serialized = type.serialize(s)
31+
assert_equal Encoding::UTF_8, serialized.encoding
32+
assert_equal s.bytes, serialized.bytes
33+
end
34+
35+
test "leaves true binary data untouched" do
36+
binary_data = "\xEE\x49\xC7".b
37+
type = Type::ImmutableString.new
38+
assert_same binary_data, type.serialize(binary_data)
39+
end
2040
end
2141
end
2242
end

activerecord/lib/active_record/connection_adapters/abstract/quoting.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ def quote(value)
1616
when false then quoted_false
1717
when nil then "NULL"
1818
# BigDecimals need to be put in a non-normalized form and quoted.
19-
when BigDecimal then value.to_s("F")
19+
# Additionally, for Ruby 2.7, the string returned by `to_s` is ASCII-8BIT.
20+
# We want to avoid that, as that will cause the string to be quoted as
21+
# binary. It is safe to force the encoding to US-ASCII.
22+
when BigDecimal then value.to_s("F").force_encoding(Encoding::US_ASCII)
2023
when Numeric then value.to_s
2124
when Type::Binary::Data then quoted_binary(value)
2225
when Type::Time::Value then "'#{quoted_time(value)}'"

activerecord/lib/active_record/connection_adapters/mysql/quoting.rb

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,30 @@ module ActiveRecord
66
module ConnectionAdapters
77
module MySQL
88
module Quoting # :nodoc:
9+
def quote(value)
10+
case value
11+
when String
12+
if value.encoding == Encoding::BINARY
13+
quoted_binary(value)
14+
else
15+
"'#{quote_string(value.to_s)}'"
16+
end
17+
else
18+
super
19+
end
20+
end
21+
922
def cast_bound_value(value)
1023
case value
1124
when Rational
1225
value.to_f.to_s
26+
when BigDecimal
27+
# For Ruby 2.7, the string returned by `to_s` is ASCII-8BIT.
28+
# We want to avoid that, as that will cause the string to be quoted as
29+
# binary. It is safe to force the encoding to US-ASCII.
30+
value.to_s("F").force_encoding(Encoding::US_ASCII)
1331
when Numeric
1432
value.to_s
15-
when BigDecimal
16-
value.to_s("F")
1733
when true
1834
"1"
1935
when false
@@ -51,7 +67,11 @@ def quoted_date(value)
5167
end
5268

5369
def quoted_binary(value)
54-
"x'#{value.hex}'"
70+
if value.is_a? String
71+
"x'#{value.unpack1("H*")}'"
72+
else
73+
"x'#{value.hex}'"
74+
end
5575
end
5676

5777
def unquote_identifier(identifier)

activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@ module ActiveRecord
44
module ConnectionAdapters
55
module SQLite3
66
module Quoting # :nodoc:
7+
def quote(value)
8+
case value
9+
when String
10+
if value.encoding == Encoding::BINARY
11+
quoted_binary(value)
12+
else
13+
"'#{quote_string(value.to_s)}'"
14+
end
15+
else
16+
super
17+
end
18+
end
19+
720
def quote_string(s)
821
::SQLite3::Database.quote(s)
922
end
@@ -26,7 +39,11 @@ def quoted_time(value)
2639
end
2740

2841
def quoted_binary(value)
29-
"x'#{value.hex}'"
42+
if value.is_a? String
43+
"x'#{value.unpack1("H*")}'"
44+
else
45+
"x'#{value.hex}'"
46+
end
3047
end
3148

3249
def quoted_true
@@ -62,12 +79,6 @@ def type_cast(value) # :nodoc:
6279
case value
6380
when BigDecimal
6481
value.to_f
65-
when String
66-
if value.encoding == Encoding::ASCII_8BIT
67-
super(value.encode(Encoding::UTF_8))
68-
else
69-
super
70-
end
7182
else
7283
super
7384
end

activerecord/test/cases/binary_test.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,19 @@ def test_load_save
3737
assert_equal data, bin.reload.data, "Reloaded data differs from original"
3838
end
3939
end
40+
41+
unless current_adapter?(:PostgreSQLAdapter)
42+
def test_does_not_cause_database_warnings
43+
original_db_warnings_action = ActiveRecord.db_warnings_action
44+
ActiveRecord.db_warnings_action = :raise
45+
46+
Binary.delete_all
47+
binary_data = "\xEE\x49\xC7".b
48+
Binary.connection.insert(Arel.sql("INSERT INTO binaries(data) VALUES(?)", binary_data))
49+
binary = Binary.first
50+
assert_equal binary_data, binary.data
51+
ensure
52+
ActiveRecord.db_warnings_action = original_db_warnings_action || :ignore
53+
end
54+
end
4055
end

0 commit comments

Comments
 (0)