add StringScannerPool for thread safety

This commit is contained in:
Michael Go
2025-01-07 14:32:07 -04:00
parent eff5c5de8e
commit 3c16c27ee1
5 changed files with 57 additions and 46 deletions
+1
View File
@@ -49,6 +49,7 @@ end
require "liquid/version" require "liquid/version"
require "liquid/deprecations" require "liquid/deprecations"
require "liquid/const" require "liquid/const"
require "liquid/string_scanner_pool"
require 'liquid/standardfilters' require 'liquid/standardfilters'
require 'liquid/file_system' require 'liquid/file_system'
require 'liquid/parser_switching' require 'liquid/parser_switching'
+3 -6
View File
@@ -72,10 +72,6 @@ module Liquid
CACHE = LruRedux::Cache.new(10_000) # most themes would have less than 2,000 unique expression CACHE = LruRedux::Cache.new(10_000) # most themes would have less than 2,000 unique expression
class << self class << self
def string_scanner
@ss ||= StringScanner.new("")
end
def parse(markup) def parse(markup)
return unless markup return unless markup
@@ -106,8 +102,7 @@ module Liquid
end end
def parse_number(markup) def parse_number(markup)
ss = string_scanner ss = StringScannerPool.pop(markup)
ss.string = markup
is_integer = true is_integer = true
last_dot_pos = nil last_dot_pos = nil
@@ -147,6 +142,8 @@ module Liquid
# we should never reach this point # we should never reach this point
false false
end end
ensure
StringScannerPool.release(ss)
end end
end end
end end
+27 -31
View File
@@ -157,40 +157,34 @@ module Liquid
table.freeze table.freeze
end end
class << self
def string_scanner
@string_scanner ||= StringScanner.new("")
end
end
def initialize(input) def initialize(input)
@ss = self.class.string_scanner @input = input
@ss.string = input
end end
# rubocop:disable Metrics/BlockNesting # rubocop:disable Metrics/BlockNesting
def tokenize def tokenize
ss = StringScannerPool.pop(@input)
@output = [] @output = []
until @ss.eos? until ss.eos?
@ss.skip(WHITESPACE_OR_NOTHING) ss.skip(WHITESPACE_OR_NOTHING)
break if @ss.eos? break if ss.eos?
start_pos = @ss.pos start_pos = ss.pos
peeked = @ss.peek_byte peeked = ss.peek_byte
if (special = SPECIAL_TABLE[peeked]) if (special = SPECIAL_TABLE[peeked])
@ss.scan_byte ss.scan_byte
# Special case for ".." # Special case for ".."
if special == DOT && @ss.peek_byte == DOT_ORD if special == DOT && ss.peek_byte == DOT_ORD
@ss.scan_byte ss.scan_byte
@output << DOTDOT @output << DOTDOT
elsif special == DASH elsif special == DASH
# Special case for negative numbers # Special case for negative numbers
if (peeked_byte = @ss.peek_byte) && NUMBER_TABLE[peeked_byte] if (peeked_byte = ss.peek_byte) && NUMBER_TABLE[peeked_byte]
@ss.pos -= 1 ss.pos -= 1
@output << [:number, @ss.scan(NUMBER_LITERAL)] @output << [:number, ss.scan(NUMBER_LITERAL)]
else else
@output << special @output << special
end end
@@ -198,25 +192,25 @@ module Liquid
@output << special @output << special
end end
elsif (sub_table = TWO_CHARS_COMPARISON_JUMP_TABLE[peeked]) elsif (sub_table = TWO_CHARS_COMPARISON_JUMP_TABLE[peeked])
@ss.scan_byte ss.scan_byte
if (peeked_byte = @ss.peek_byte) && (found = sub_table[peeked_byte]) if (peeked_byte = ss.peek_byte) && (found = sub_table[peeked_byte])
@output << found @output << found
@ss.scan_byte ss.scan_byte
else else
raise_syntax_error(start_pos) raise_syntax_error(start_pos, ss)
end end
elsif (sub_table = COMPARISON_JUMP_TABLE[peeked]) elsif (sub_table = COMPARISON_JUMP_TABLE[peeked])
@ss.scan_byte ss.scan_byte
if (peeked_byte = @ss.peek_byte) && (found = sub_table[peeked_byte]) if (peeked_byte = ss.peek_byte) && (found = sub_table[peeked_byte])
@output << found @output << found
@ss.scan_byte ss.scan_byte
else else
@output << SINGLE_COMPARISON_TOKENS[peeked] @output << SINGLE_COMPARISON_TOKENS[peeked]
end end
else else
type, pattern = NEXT_MATCHER_JUMP_TABLE[peeked] type, pattern = NEXT_MATCHER_JUMP_TABLE[peeked]
if type && (t = @ss.scan(pattern)) if type && (t = ss.scan(pattern))
# Special case for "contains" # Special case for "contains"
@output << if type == :id && t == "contains" && @output.last&.first != :dot @output << if type == :id && t == "contains" && @output.last&.first != :dot
COMPARISON_CONTAINS COMPARISON_CONTAINS
@@ -224,19 +218,21 @@ module Liquid
[type, t] [type, t]
end end
else else
raise_syntax_error(start_pos) raise_syntax_error(start_pos, ss)
end end
end end
end end
# rubocop:enable Metrics/BlockNesting # rubocop:enable Metrics/BlockNesting
@output << EOS @output << EOS
ensure
StringScannerPool.release(ss)
end end
def raise_syntax_error(start_pos) def raise_syntax_error(start_pos, ss)
@ss.pos = start_pos ss.pos = start_pos
# the character could be a UTF-8 character, use getch to get all the bytes # the character could be a UTF-8 character, use getch to get all the bytes
raise SyntaxError, "Unexpected character #{@ss.getch}" raise SyntaxError, "Unexpected character #{ss.getch}"
end end
end end
+23
View File
@@ -0,0 +1,23 @@
module Liquid
class StringScannerPool
class << self
def pop(input)
@ss_pool ||= [StringScanner.new("")] * 5
if @ss_pool.empty?
StringScanner.new(input)
else
ss = @ss_pool.pop
ss.string = input
ss
end
end
def release(ss)
binding.irb if ss.nil?
@ss_pool ||= []
@ss_pool << ss
end
end
end
end
+3 -9
View File
@@ -56,12 +56,6 @@ module Liquid
CLOSE_CURLEY = "}".ord CLOSE_CURLEY = "}".ord
PERCENTAGE = "%".ord PERCENTAGE = "%".ord
class << self
def string_scanner
@string_scanner ||= StringScanner.new("")
end
end
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false) def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
@line_number = line_number || (line_numbers ? 1 : nil) @line_number = line_number || (line_numbers ? 1 : nil)
@for_liquid_tag = for_liquid_tag @for_liquid_tag = for_liquid_tag
@@ -91,13 +85,13 @@ module Liquid
if @for_liquid_tag if @for_liquid_tag
@tokens = @source.split("\n") @tokens = @source.split("\n")
else else
@ss = self.class.string_scanner @ss = StringScannerPool.pop(@source)
@ss.string = @source
@tokens << shift_normal until @ss.eos? @tokens << shift_normal until @ss.eos?
end end
@ss = nil
@source = nil @source = nil
ensure
StringScannerPool.release(@ss) if @ss
end end
def shift_normal def shift_normal