diff --git a/gem/lib/pagy/modules/abilities/linkable.rb b/gem/lib/pagy/modules/abilities/linkable.rb index e01600731..78ab0f59b 100644 --- a/gem/lib/pagy/modules/abilities/linkable.rb +++ b/gem/lib/pagy/modules/abilities/linkable.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require 'cgi/escape' require 'uri' class Pagy @@ -31,8 +32,13 @@ def build_nested_query(value, prefix = nil) end end + # CGI.escape is several times faster than URI.encode_www_form_component and returns the same bytes, + # except for '*' and '~', which are left to URI. URI returns US-ASCII, CGI the input encoding. def escape(str) - URI.encode_www_form_component(str) + bytes = str.to_s.b + return URI.encode_www_form_component(bytes) if bytes.match?(/[*~]/) + + CGI.escape(bytes).force_encoding(Encoding::US_ASCII) end end diff --git a/test/unit/pagy/modules/abilities/linkable_test.rb b/test/unit/pagy/modules/abilities/linkable_test.rb index 3246ec2ed..22f32748b 100644 --- a/test/unit/pagy/modules/abilities/linkable_test.rb +++ b/test/unit/pagy/modules/abilities/linkable_test.rb @@ -9,6 +9,33 @@ it 'escapes strings' do _(utils.escape('a b')).must_equal 'a+b' _(utils.escape('/?&')).must_equal '%2F%3F%26' + _(utils.escape('a*b~c')).must_equal 'a*b%7Ec' + _(utils.escape('café')).must_equal 'caf%C3%A9' + _(utils.escape(:alice)).must_equal 'alice' + _(utils.escape(42)).must_equal '42' + _(utils.escape("\xFF".b)).must_equal '%FF' + end + + it 'escapes like URI.encode_www_form_component' do + [nil, '', '*', "a*\xFF", 'é~'.encode('UTF-16LE'), 'é'.encode('ISO-8859-1'), "\0 🙂"].each do |value| + expected = URI.encode_www_form_component(value) + escaped = utils.escape(value) + _(escaped).must_equal expected + _(escaped.encoding).must_equal expected.encoding + end + end + + it 'escapes random strings like URI.encode_www_form_component' do + random = Random.new(42) + alphabet = [*'a'..'z', *'0'..'9', *" !\"\#$%&'()*+,-./:;<=>?@[\\]^_`{|}~".chars, 'é', '€', '日', '🙂', "\0"] + strings = Array.new(3000) { Array.new(random.rand(0..12)) { alphabet.sample(random:) }.join } + strings += Array.new(500) { random.bytes(random.rand(0..8)) } + strings.each do |string| + expected = URI.encode_www_form_component(string) + escaped = utils.escape(string) + _(escaped).must_equal expected + _(escaped.encoding).must_equal expected.encoding + end end it 'builds simple queries' do