Skip to content

Commit 06e9fbd

Browse files
authored
Merge pull request rails#45614 from Shopify/faster-output-buffer
Speedup ActionView::OutputBuffer
2 parents 71c59c6 + 4e8e828 commit 06e9fbd

File tree

6 files changed

+94
-14
lines changed

6 files changed

+94
-14
lines changed

actionview/lib/action_view/buffers.rb

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,64 @@ module ActionView
1818
# sbuf << 5
1919
# puts sbuf # => "hello\u0005"
2020
#
21-
class OutputBuffer < ActiveSupport::SafeBuffer # :nodoc:
22-
def initialize(*)
23-
super
24-
encode!
21+
class OutputBuffer # :nodoc:
22+
def initialize(buffer = "")
23+
@buffer = String.new(buffer)
24+
@buffer.encode!
25+
end
26+
27+
delegate :length, :blank?, :encoding, :encode!, :force_encoding, to: :@buffer
28+
29+
def to_s
30+
@buffer.html_safe
31+
end
32+
alias_method :html_safe, :to_s
33+
34+
def to_str
35+
@buffer.dup
36+
end
37+
38+
def html_safe?
39+
true
2540
end
2641

2742
def <<(value)
28-
return self if value.nil?
29-
super(value.to_s)
43+
unless value.nil?
44+
value = value.to_s
45+
@buffer << if value.html_safe?
46+
value
47+
else
48+
CGI.escapeHTML(value)
49+
end
50+
end
51+
self
3052
end
3153
alias :append= :<<
3254

55+
def safe_concat(value)
56+
@buffer << value
57+
self
58+
end
59+
alias :safe_append= :safe_concat
60+
3361
def safe_expr_append=(val)
3462
return self if val.nil?
35-
safe_concat val.to_s
63+
@buffer << val.to_s
64+
self
3665
end
3766

38-
alias :safe_append= :safe_concat
67+
def initialize_copy(other)
68+
@buffer = other.to_str
69+
end
70+
71+
# Don't use this
72+
def slice!(range)
73+
@buffer.slice!(range)
74+
end
75+
76+
def ==(other)
77+
other.class == self.class && @buffer == other.to_str
78+
end
3979
end
4080

4181
class StreamingBuffer # :nodoc:

actionview/lib/action_view/helpers/cache_helper.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,9 @@ def write_fragment_for(name, options)
285285
pos = output_buffer.length
286286
yield
287287
output_safe = output_buffer.html_safe?
288+
# We need to modify the buffer in place to be deal with the view handlers
289+
# like `builder` that don't access the buffer through `@output_buffer` but
290+
# keep the initial reference.
288291
fragment = output_buffer.slice!(pos..-1)
289292
if output_safe
290293
self.output_buffer = output_buffer.class.new(output_buffer)

actionview/lib/action_view/helpers/capture_helper.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,14 @@ module CaptureHelper
4343
def capture(*args)
4444
value = nil
4545
buffer = with_output_buffer { value = yield(*args) }
46-
if (string = buffer.presence || value) && string.is_a?(String)
47-
ERB::Util.html_escape string
46+
47+
case string = buffer.presence || value
48+
when OutputBuffer
49+
string.to_s
50+
when ActiveSupport::SafeBuffer
51+
string
52+
when String
53+
ERB::Util.html_escape(string)
4854
end
4955
end
5056

actionview/lib/action_view/renderer/abstract_renderer.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ class RenderedTemplate # :nodoc:
142142
attr_reader :body, :template
143143

144144
def initialize(body, template)
145-
@body = body
145+
@body = body.to_s
146146
@template = template
147147
end
148148

actionview/test/output_buffer_test.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# frozen_string_literal: true
2+
3+
require "abstract_unit"
4+
5+
class TestOutputBuffer < ActiveSupport::TestCase
6+
setup do
7+
@buffer = ActionView::OutputBuffer.new
8+
end
9+
10+
test "#<< maintains HTML safety" do
11+
@buffer << "<script>alert('pwned!')</script>"
12+
assert_predicate @buffer, :html_safe?
13+
assert_predicate @buffer.to_s, :html_safe?
14+
assert_equal "&lt;script&gt;alert(&#39;pwned!&#39;)&lt;/script&gt;", @buffer.to_s
15+
end
16+
17+
test "#safe_append= bypasses HTML safety" do
18+
@buffer.safe_append = "<p>This is fine</p>"
19+
assert_predicate @buffer, :html_safe?
20+
assert_predicate @buffer.to_s, :html_safe?
21+
assert_equal "<p>This is fine</p>", @buffer.to_s
22+
end
23+
24+
test "can be duped" do
25+
@buffer << "Hello"
26+
copy = @buffer.dup
27+
copy << " World!"
28+
assert_equal "Hello World!", copy.to_s
29+
assert_equal "Hello", @buffer.to_s
30+
end
31+
end

actionview/test/template/capture_helper_test.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ def test_with_output_buffer_swaps_the_output_buffer_given_no_argument
182182
buffer = @av.with_output_buffer do
183183
@av.output_buffer << "."
184184
end
185-
assert_equal ".", buffer
185+
assert_equal ".", buffer.to_s
186186
assert_nil @av.output_buffer
187187
end
188188

@@ -192,7 +192,7 @@ def test_with_output_buffer_swaps_the_output_buffer_with_an_argument
192192
@av.with_output_buffer(buffer) do
193193
@av.output_buffer << "."
194194
end
195-
assert_equal "..", buffer
195+
assert_equal "..", buffer.to_s
196196
assert_nil @av.output_buffer
197197
end
198198

@@ -219,7 +219,7 @@ def test_with_output_buffer_sets_proper_encoding
219219

220220
def test_with_output_buffer_does_not_assume_there_is_an_output_buffer
221221
assert_nil @av.output_buffer
222-
assert_equal "", @av.with_output_buffer { }
222+
assert_equal "", @av.with_output_buffer { }.to_s
223223
end
224224

225225
def alt_encoding(output_buffer)

0 commit comments

Comments
 (0)