From 12dc8433142985d863c6e0977a164b3c1e26c14f Mon Sep 17 00:00:00 2001 From: Ian Ker-Seymer Date: Wed, 4 Mar 2026 17:38:24 -0500 Subject: [PATCH] Fix escape_html/h aliases to use C extension instead of pure Ruby fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The snake_case aliases (escape_html, h, unescape_html) were defined before `require 'cgi/escape.so'`, so they captured references to the pure Ruby methods. After the C extension loads and prepends EscapeExt, only the camelCase names (escapeHTML, etc.) resolved to the fast C implementation — the aliases still called the ~10x slower Ruby path. Move alias definitions after the C extension loads, and define them on EscapeExt when available so they bind to the optimized C methods. --- lib/cgi/escape.rb | 15 ++++++++------- test/cgi/test_cgi_escape.rb | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/lib/cgi/escape.rb b/lib/cgi/escape.rb index 6d84773..6748198 100644 --- a/lib/cgi/escape.rb +++ b/lib/cgi/escape.rb @@ -160,13 +160,6 @@ def unescapeHTML(string) string.force_encoding enc end - # Synonym for CGI.escapeHTML(str) - alias escape_html escapeHTML - alias h escapeHTML - - # Synonym for CGI.unescapeHTML(str) - alias unescape_html unescapeHTML - # TruffleRuby runs the pure-Ruby variant faster, do not use the C extension there unless RUBY_ENGINE == 'truffleruby' begin @@ -175,6 +168,14 @@ def unescapeHTML(string) end end + # Aliases must be defined on EscapeExt so they resolve to the C methods. + target = defined?(CGI::EscapeExt) && CGI::EscapeExt.method_defined?(:escapeHTML) ? CGI::EscapeExt : self + target.module_eval do + alias escape_html escapeHTML + alias h escapeHTML + alias unescape_html unescapeHTML + end + # Escape only the tags of certain HTML elements in +string+. # # Takes an element or elements or array of elements. Each element diff --git a/test/cgi/test_cgi_escape.rb b/test/cgi/test_cgi_escape.rb index 73d99e8..4bf0d45 100644 --- a/test/cgi/test_cgi_escape.rb +++ b/test/cgi/test_cgi_escape.rb @@ -293,6 +293,39 @@ def test_cgi_unescapeElement end end +class CGIEscapeNativeExtTest < Test::Unit::TestCase + def test_escape_html_uses_native_implementation + omit "C extension not available" unless defined?(CGI::EscapeExt) && CGI::EscapeExt.method_defined?(:escapeHTML) + assert_equal CGI::EscapeExt, CGI.method(:escape_html).owner + assert_equal CGI::EscapeExt, CGI.method(:h).owner + assert_equal CGI::EscapeExt, CGI.method(:unescape_html).owner + end + + def test_escape_html_allocates_same_as_escapeHTML + omit "C extension not available" unless defined?(CGI::EscapeExt) && CGI::EscapeExt.method_defined?(:escapeHTML) + + input = "'&\"<>hello world" + n = 100 + + # Warm up + 2.times { n.times { CGI.escapeHTML(input) } } + 2.times { n.times { CGI.escape_html(input) } } + + GC.disable + before = GC.stat(:total_allocated_objects) + n.times { CGI.escapeHTML(input) } + camel_allocs = GC.stat(:total_allocated_objects) - before + + before = GC.stat(:total_allocated_objects) + n.times { CGI.escape_html(input) } + snake_allocs = GC.stat(:total_allocated_objects) - before + GC.enable + + assert_equal camel_allocs, snake_allocs, + "escape_html allocated #{snake_allocs} objects vs escapeHTML #{camel_allocs} — alias may not be using the C extension" + end +end + class CGIEscapePureRubyTest < Test::Unit::TestCase def setup CGI::EscapeExt.module_eval do