From 83720136174512dca09458e7ecf03a5d6c9a2010 Mon Sep 17 00:00:00 2001 From: aguspe Date: Mon, 15 Dec 2025 00:40:21 +0100 Subject: [PATCH 1/2] add force encoding for compatibility with json 3 --- .../selenium/webdriver/remote/http/common.rb | 32 +++++++++ .../webdriver/remote/http/common_spec.rb | 69 +++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/rb/lib/selenium/webdriver/remote/http/common.rb b/rb/lib/selenium/webdriver/remote/http/common.rb index 2f287e2703132..23d9079d96306 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 Encoding::Error => 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..cffe23c237de6 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,75 @@ 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;'.dup.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'.dup.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'.dup.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".dup.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 From dccd654b7cb9281ab12cd3f90ee1fc97b1e82c4f Mon Sep 17 00:00:00 2001 From: aguspe Date: Mon, 15 Dec 2025 01:04:22 +0100 Subject: [PATCH 2/2] fix rubocop issues --- rb/lib/selenium/webdriver/remote/http/common.rb | 2 +- .../selenium/webdriver/remote/http/common_spec.rb | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/rb/lib/selenium/webdriver/remote/http/common.rb b/rb/lib/selenium/webdriver/remote/http/common.rb index 23d9079d96306..6e4f387ebdb08 100644 --- a/rb/lib/selenium/webdriver/remote/http/common.rb +++ b/rb/lib/selenium/webdriver/remote/http/common.rb @@ -117,7 +117,7 @@ def encode_string_to_utf8(str) end str.encode(Encoding::UTF_8) - rescue Encoding::Error => e + rescue EncodingError => e raise Error::WebDriverError, "Unable to encode string to UTF-8: #{e.message}. " \ "String encoding: #{str.encoding}, content: #{str.inspect}" 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 cffe23c237de6..968780a799533 100644 --- a/rb/spec/unit/selenium/webdriver/remote/http/common_spec.rb +++ b/rb/spec/unit/selenium/webdriver/remote/http/common_spec.rb @@ -77,7 +77,8 @@ module Http context 'when encoding strings to UTF-8' do it 'converts binary-encoded strings that are valid UTF-8' do - binary_string = 'return navigator.userAgent;'.dup.force_encoding(Encoding::BINARY) + binary_string = +'return navigator.userAgent;' + binary_string.force_encoding(Encoding::BINARY) command_hash = {script: binary_string, args: []} common.call(:post, 'execute', command_hash) @@ -91,7 +92,8 @@ module Http end it 'converts binary-encoded strings in nested hashes' do - binary_string = 'test value'.dup.force_encoding(Encoding::BINARY) + binary_string = +'test value' + binary_string.force_encoding(Encoding::BINARY) command_hash = { outer: { inner: binary_string, @@ -109,7 +111,8 @@ module Http end it 'converts binary-encoded strings in arrays' do - binary_string = 'array item'.dup.force_encoding(Encoding::BINARY) + binary_string = +'array item' + binary_string.force_encoding(Encoding::BINARY) command_hash = {items: [binary_string, 'utf8 item']} common.call(:post, 'test', command_hash) @@ -123,7 +126,8 @@ module Http it 'raises error for invalid byte sequences' do # Create an invalid UTF-8 byte sequence - invalid_string = "\xFF\xFE".dup.force_encoding(Encoding::BINARY) + invalid_string = +"\xFF\xFE" + invalid_string.force_encoding(Encoding::BINARY) command_hash = {script: invalid_string} expect { common.call(:post, 'execute', command_hash) }