diff --git a/rb/lib/selenium/webdriver/remote/http/common.rb b/rb/lib/selenium/webdriver/remote/http/common.rb index 2f287e2703132..6e4f387ebdb08 100644 --- a/rb/lib/selenium/webdriver/remote/http/common.rb +++ b/rb/lib/selenium/webdriver/remote/http/common.rb @@ -28,6 +28,7 @@ class Common 'Accept' => CONTENT_TYPE, 'Content-Type' => "#{CONTENT_TYPE}; charset=UTF-8" }.freeze + BINARY_ENCODINGS = [Encoding::BINARY, Encoding::ASCII_8BIT].freeze class << self attr_accessor :extra_headers @@ -55,6 +56,7 @@ def call(verb, url, command_hash) headers['Cache-Control'] = 'no-cache' if verb == :get if command_hash + command_hash = ensure_utf8_encoding(command_hash) payload = JSON.generate(command_hash) headers['Content-Length'] = payload.bytesize.to_s if %i[post put].include?(verb) @@ -91,6 +93,36 @@ def request(*) raise NotImplementedError, 'subclass responsibility' end + def ensure_utf8_encoding(obj) + case obj + when String + encode_string_to_utf8(obj) + when Array + obj.map { |item| ensure_utf8_encoding(item) } + when Hash + obj.each_with_object({}) do |(key, value), result| + result[ensure_utf8_encoding(key)] = ensure_utf8_encoding(value) + end + else + obj + end + end + + def encode_string_to_utf8(str) + return str if str.encoding == Encoding::UTF_8 && str.valid_encoding? + + if BINARY_ENCODINGS.include?(str.encoding) + result = str.dup.force_encoding(Encoding::UTF_8) + return result if result.valid_encoding? + end + + str.encode(Encoding::UTF_8) + rescue EncodingError => e + raise Error::WebDriverError, + "Unable to encode string to UTF-8: #{e.message}. " \ + "String encoding: #{str.encoding}, content: #{str.inspect}" + end + def create_response(code, body, content_type) code = code.to_i body = body.to_s.strip diff --git a/rb/spec/unit/selenium/webdriver/remote/http/common_spec.rb b/rb/spec/unit/selenium/webdriver/remote/http/common_spec.rb index 6864a94e4d06e..968780a799533 100644 --- a/rb/spec/unit/selenium/webdriver/remote/http/common_spec.rb +++ b/rb/spec/unit/selenium/webdriver/remote/http/common_spec.rb @@ -74,6 +74,79 @@ module Http .with(:post, URI.parse('http://server/session'), hash_including('User-Agent' => 'rspec/1.0 (ruby 3.2)'), '{}') end + + context 'when encoding strings to UTF-8' do + it 'converts binary-encoded strings that are valid UTF-8' do + binary_string = +'return navigator.userAgent;' + binary_string.force_encoding(Encoding::BINARY) + command_hash = {script: binary_string, args: []} + + common.call(:post, 'execute', command_hash) + + expect(common).to have_received(:request) do |_verb, _url, _headers, payload| + expect { JSON.parse(payload) }.not_to raise_error + parsed = JSON.parse(payload) + expect(parsed['script']).to eq('return navigator.userAgent;') + expect(parsed['script'].encoding).to eq(Encoding::UTF_8) + end + end + + it 'converts binary-encoded strings in nested hashes' do + binary_string = +'test value' + binary_string.force_encoding(Encoding::BINARY) + command_hash = { + outer: { + inner: binary_string, + another: 'utf8 string' + } + } + + common.call(:post, 'test', command_hash) + + expect(common).to have_received(:request) do |_verb, _url, _headers, payload| + expect { JSON.parse(payload) }.not_to raise_error + parsed = JSON.parse(payload) + expect(parsed['outer']['inner']).to eq('test value') + end + end + + it 'converts binary-encoded strings in arrays' do + binary_string = +'array item' + binary_string.force_encoding(Encoding::BINARY) + command_hash = {items: [binary_string, 'utf8 item']} + + common.call(:post, 'test', command_hash) + + expect(common).to have_received(:request) do |_verb, _url, _headers, payload| + expect { JSON.parse(payload) }.not_to raise_error + parsed = JSON.parse(payload) + expect(parsed['items']).to eq(['array item', 'utf8 item']) + end + end + + it 'raises error for invalid byte sequences' do + # Create an invalid UTF-8 byte sequence + invalid_string = +"\xFF\xFE" + invalid_string.force_encoding(Encoding::BINARY) + command_hash = {script: invalid_string} + + expect { common.call(:post, 'execute', command_hash) } + .to raise_error(WebDriver::Error::WebDriverError, /Unable to encode string to UTF-8/) + end + + it 'handles already UTF-8 encoded strings' do + utf8_string = 'already utf-8' + command_hash = {script: utf8_string} + + common.call(:post, 'execute', command_hash) + + expect(common).to have_received(:request) do |_verb, _url, _headers, payload| + expect { JSON.parse(payload) }.not_to raise_error + parsed = JSON.parse(payload) + expect(parsed['script']).to eq('already utf-8') + end + end + end end end # Http end # Remote