mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-12 23:40:45 -07:00
Fixes infinite loop in tokenizer on trailing stray '{'
The stray-{ else branch in tokenize_fast had a nested while loop that could
exit via its condition (when next_open >= len) without advancing pos.
The outer while pos < len loop would then find the same { again forever.
Reproduced by: Liquid::Template.parse('a{') -- hangs indefinitely.
Replaces the nested scan loop with two String#byteindex calls to find the
next '{%' and '{{' directly, then takes the minimum. Always O(n), eliminates
the nested loop entirely, and impossible to leave pos stranded.
Adds regression tests covering the three inputs that previously hung
plus adjacent stray-brace cases.
This commit is contained in:
+12
-18
@@ -146,24 +146,18 @@ module Liquid
|
|||||||
pos = idx + 2
|
pos = idx + 2
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
# { followed by something else — it's text
|
# Lone '{' — not the start of a tag or variable.
|
||||||
# Keep scanning from after this {
|
# Find the next '{{' or '{%' to know where this text token ends.
|
||||||
# Find next { that could be {% or {{
|
# Using two byteindex calls avoids a nested loop and is always O(n).
|
||||||
next_open = idx + 1
|
tag_start = src.byteindex('{%', idx + 1)
|
||||||
while next_open < len
|
var_start = src.byteindex('{{', idx + 1)
|
||||||
ni = src.byteindex('{', next_open)
|
next_token = [tag_start, var_start].compact.min
|
||||||
unless ni
|
if next_token
|
||||||
@tokens << src.byteslice(pos, len - pos)
|
@tokens << src.byteslice(pos, next_token - pos)
|
||||||
pos = len
|
pos = next_token
|
||||||
break
|
else
|
||||||
end
|
@tokens << src.byteslice(pos, len - pos)
|
||||||
nb = ni + 1 < len ? src.getbyte(ni + 1) : nil
|
pos = len
|
||||||
if nb == PERCENTAGE || nb == OPEN_CURLEY
|
|
||||||
@tokens << src.byteslice(pos, ni - pos)
|
|
||||||
pos = ni
|
|
||||||
break
|
|
||||||
end
|
|
||||||
next_open = ni + 1
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -48,6 +48,33 @@ class TokenizerTest < Minitest::Test
|
|||||||
assert_equal(["{%%}", "}"], tokenize('{%%}}'))
|
assert_equal(["{%%}", "}"], tokenize('{%%}}'))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Regression: lone '{' at or near end of string previously caused an infinite
|
||||||
|
# loop. The stray-{ else branch left `pos` unchanged when no further '{{' or
|
||||||
|
# '{%' existed, so the outer loop found the same '{' on every iteration.
|
||||||
|
def test_lone_brace_does_not_loop
|
||||||
|
assert_equal(["{"], tokenize('{'))
|
||||||
|
assert_equal(["a{"], tokenize('a{'))
|
||||||
|
assert_equal(["hello { world {"], tokenize('hello { world {'))
|
||||||
|
assert_equal(["{ world"], tokenize('{ world'))
|
||||||
|
assert_equal(["x{y"], tokenize('x{y'))
|
||||||
|
assert_equal(["{b{c"], tokenize('{b{c'))
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_lone_brace_before_real_token
|
||||||
|
assert_equal(
|
||||||
|
["a { b ", "{% if x %}", "yes", "{% endif %}", " c"],
|
||||||
|
tokenize('a { b {% if x %}yes{% endif %} c'),
|
||||||
|
)
|
||||||
|
assert_equal(
|
||||||
|
["x { ", "{{ var }}", " y"],
|
||||||
|
tokenize('x { {{ var }} y'),
|
||||||
|
)
|
||||||
|
assert_equal(
|
||||||
|
["{ ", "{{ var }}"],
|
||||||
|
tokenize('{ {{ var }}'),
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def new_tokenizer(source, parse_context: Liquid::ParseContext.new, start_line_number: nil)
|
def new_tokenizer(source, parse_context: Liquid::ParseContext.new, start_line_number: nil)
|
||||||
|
|||||||
Reference in New Issue
Block a user