Skip to content

Commit ac9b219

Browse files
committed
Copy CGI.escapeHTML to ERB::Util.html_escape
1 parent 48a7566 commit ac9b219

8 files changed

Lines changed: 139 additions & 4 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@
77
/spec/reports/
88
/tmp/
99
Gemfile.lock
10+
*.so
11+
*.gem

Gemfile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,8 @@ source 'https://rubygems.org'
22

33
gemspec
44

5-
gem 'rake'
6-
gem 'test-unit'
5+
group :development do
6+
gem 'rake'
7+
gem 'rake-compiler'
8+
gem 'test-unit'
9+
end

Rakefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ Rake::TestTask.new(:test) do |t|
77
t.test_files = FileList['test/**/test_*.rb']
88
end
99

10+
if RUBY_ENGINE != 'jruby'
11+
require 'rake/extensiontask'
12+
Rake::ExtensionTask.new('erb')
13+
task test: :compile
14+
end
15+
1016
task :sync_tool do
1117
require 'fileutils'
1218
FileUtils.cp '../ruby/tool/lib/core_assertions.rb', './test/lib'

erb.gemspec

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,11 @@ Gem::Specification.new do |spec|
2727
spec.executables = ['erb']
2828
spec.require_paths = ['lib']
2929

30-
if RUBY_ENGINE != 'jruby'
30+
if RUBY_ENGINE == 'jruby'
31+
spec.platform = 'java'
32+
else
3133
spec.required_ruby_version = '>= 2.7.0'
34+
spec.extensions = ['ext/erb/extconf.rb']
3235
end
3336

3437
spec.add_dependency 'cgi', '>= 0.3.3'

ext/erb/erb.c

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#include "ruby.h"
2+
#include "ruby/encoding.h"
3+
4+
static VALUE rb_cERB, rb_mEscape;
5+
6+
#define HTML_ESCAPE_MAX_LEN 6
7+
8+
static const struct {
9+
uint8_t len;
10+
char str[HTML_ESCAPE_MAX_LEN+1];
11+
} html_escape_table[UCHAR_MAX+1] = {
12+
#define HTML_ESCAPE(c, str) [c] = {rb_strlen_lit(str), str}
13+
HTML_ESCAPE('\'', "'"),
14+
HTML_ESCAPE('&', "&"),
15+
HTML_ESCAPE('"', """),
16+
HTML_ESCAPE('<', "&lt;"),
17+
HTML_ESCAPE('>', "&gt;"),
18+
#undef HTML_ESCAPE
19+
};
20+
21+
static inline void
22+
preserve_original_state(VALUE orig, VALUE dest)
23+
{
24+
rb_enc_associate(dest, rb_enc_get(orig));
25+
}
26+
27+
static inline long
28+
escaped_length(VALUE str)
29+
{
30+
const long len = RSTRING_LEN(str);
31+
if (len >= LONG_MAX / HTML_ESCAPE_MAX_LEN) {
32+
ruby_malloc_size_overflow(len, HTML_ESCAPE_MAX_LEN);
33+
}
34+
return len * HTML_ESCAPE_MAX_LEN;
35+
}
36+
37+
static VALUE
38+
optimized_escape_html(VALUE str)
39+
{
40+
VALUE vbuf;
41+
char *buf = ALLOCV_N(char, vbuf, escaped_length(str));
42+
const char *cstr = RSTRING_PTR(str);
43+
const char *end = cstr + RSTRING_LEN(str);
44+
45+
char *dest = buf;
46+
while (cstr < end) {
47+
const unsigned char c = *cstr++;
48+
uint8_t len = html_escape_table[c].len;
49+
if (len) {
50+
memcpy(dest, html_escape_table[c].str, len);
51+
dest += len;
52+
}
53+
else {
54+
*dest++ = c;
55+
}
56+
}
57+
58+
VALUE escaped;
59+
if (RSTRING_LEN(str) < (dest - buf)) {
60+
escaped = rb_str_new(buf, dest - buf);
61+
preserve_original_state(str, escaped);
62+
}
63+
else {
64+
escaped = rb_str_dup(str);
65+
}
66+
ALLOCV_END(vbuf);
67+
return escaped;
68+
}
69+
70+
static VALUE
71+
cgiesc_escape_html(VALUE self, VALUE str)
72+
{
73+
StringValue(str);
74+
75+
if (rb_enc_str_asciicompat_p(str)) {
76+
return optimized_escape_html(str);
77+
}
78+
else {
79+
return rb_call_super(1, &str);
80+
}
81+
}
82+
83+
static VALUE
84+
erb_escape_html(VALUE self, VALUE str)
85+
{
86+
str = rb_funcall(str, rb_intern("to_s"), 0);
87+
return cgiesc_escape_html(self, str);
88+
}
89+
90+
void
91+
Init_erb(void)
92+
{
93+
rb_cERB = rb_define_class("ERB", rb_cObject);
94+
rb_mEscape = rb_define_module_under(rb_cERB, "Escape");
95+
rb_define_method(rb_mEscape, "html_escape", erb_escape_html, 1);
96+
}

ext/erb/extconf.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
require 'mkmf'
2+
create_makefile 'erb'

lib/erb.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -986,7 +986,6 @@ def def_class(superklass=Object, methodname='result')
986986
class ERB
987987
# A utility module for conversion routines, often handy in HTML generation.
988988
module Util
989-
public
990989
#
991990
# A utility method for escaping HTML tag characters in _s_.
992991
#
@@ -1002,6 +1001,17 @@ module Util
10021001
def html_escape(s)
10031002
CGI.escapeHTML(s.to_s)
10041003
end
1004+
end
1005+
1006+
begin
1007+
require 'erb.so'
1008+
rescue LoadError
1009+
else
1010+
private_constant :Escape
1011+
Util.prepend(Escape)
1012+
end
1013+
1014+
module Util
10051015
alias h html_escape
10061016
module_function :h
10071017
module_function :html_escape

test/erb/test_erb.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,24 @@ def test_html_escape
7373
assert_equal("", ERB::Util.html_escape(""))
7474
assert_equal("abc", ERB::Util.html_escape("abc"))
7575
assert_equal("&lt;&lt;", ERB::Util.html_escape("<\<"))
76+
assert_equal("&#39;&amp;&quot;&gt;&lt;", ERB::Util.html_escape("'&\"><"))
7677

7778
assert_equal("", ERB::Util.html_escape(nil))
7879
assert_equal("123", ERB::Util.html_escape(123))
7980
end
8081

82+
def test_html_escape_to_s
83+
object = Object.new
84+
def object.to_s
85+
"object"
86+
end
87+
assert_equal("object", ERB::Util.html_escape(object))
88+
end
89+
90+
def test_html_escape_extension
91+
assert_nil(ERB::Util.method(:html_escape).source_location)
92+
end if RUBY_ENGINE == 'ruby'
93+
8194
def test_concurrent_default_binding
8295
# This test randomly fails with JRuby -- NameError: undefined local variable or method `template2'
8396
pend if RUBY_ENGINE == 'jruby'

0 commit comments

Comments
 (0)