# frozen_string_literal: true module Puppet::Pops module Parser class PNParser LIT_TRUE = 'true' LIT_FALSE = 'false' LIT_NIL = 'nil' TOKEN_END = 0 TOKEN_BOOL = 1 TOKEN_NIL = 2 TOKEN_INT = 3 TOKEN_FLOAT = 4 TOKEN_IDENTIFIER = 5 TOKEN_WS = 0x20 TOKEN_STRING = 0x22 TOKEN_KEY = 0x3a TOKEN_LP = 0x28 TOKEN_RP = 0x29 TOKEN_LB = 0x5b TOKEN_RB = 0x5d TOKEN_LC = 0x7b TOKEN_RC = 0x7d TYPE_END = 0 TYPE_WS = 1 TYPE_DELIM = 2 TYPE_KEY_START = 3 TYPE_STRING_START = 4 TYPE_IDENTIFIER = 5 TYPE_MINUS = 6 TYPE_DIGIT = 7 TYPE_ALPHA = 8 def initialize @char_types = self.class.char_types end def parse(text, locator = nil, offset = nil) @locator = locator @offset = offset @text = text @codepoints = text.codepoints.to_a.freeze @pos = 0 @token = TOKEN_END @token_value = nil next_token parse_next end def self.char_types unless instance_variable_defined?(:@char_types) @char_types = Array.new(0x80, TYPE_IDENTIFIER) @char_types[0] = TYPE_END [0x09, 0x0d, 0x0a, 0x20].each { |n| @char_types[n] = TYPE_WS } [TOKEN_LP, TOKEN_RP, TOKEN_LB, TOKEN_RB, TOKEN_LC, TOKEN_RC].each { |n| @char_types[n] = TYPE_DELIM } @char_types[0x2d] = TYPE_MINUS (0x30..0x39).each { |n| @char_types[n] = TYPE_DIGIT } (0x41..0x5a).each { |n| @char_types[n] = TYPE_ALPHA } (0x61..0x7a).each { |n| @char_types[n] = TYPE_ALPHA } @char_types[TOKEN_KEY] = TYPE_KEY_START @char_types[TOKEN_STRING] = TYPE_STRING_START @char_types.freeze end @char_types end private def parse_next case @token when TOKEN_LB parse_array when TOKEN_LC parse_map when TOKEN_LP parse_call when TOKEN_BOOL, TOKEN_INT, TOKEN_FLOAT, TOKEN_STRING, TOKEN_NIL parse_literal when TOKEN_END parse_error(_('unexpected end of input')) else parse_error(_('unexpected %{value}' % { value: @token_value })) end end def parse_error(message) file = '' line = 1 pos = 1 if @locator file = @locator.file line = @locator.line_for_offset(@offset) pos = @locator.pos_on_line(@offset) end @codepoints[0, @pos].each do |c| if c == 0x09 line += 1 pos = 1 end end raise Puppet::ParseError.new(message, file, line, pos) end def parse_literal pn = PN::Literal.new(@token_value) next_token pn end def parse_array next_token PN::List.new(parse_elements(TOKEN_RB)) end def parse_map next_token entries = [] while @token != TOKEN_RC && @token != TOKEN_END parse_error(_('map key expected')) unless @token == TOKEN_KEY key = @token_value next_token entries << parse_next.with_name(key) end next_token PN::Map.new(entries) end def parse_call next_token parse_error(_("expected identifier to follow '('")) unless @token == TOKEN_IDENTIFIER name = @token_value next_token PN::Call.new(name, *parse_elements(TOKEN_RP)) end def parse_elements(end_token) elements = [] while @token != end_token && @token != TOKEN_END elements << parse_next end parse_error(_("missing '%{token}' to end list") % { token: end_token.chr(Encoding::UTF_8) } ) unless @token == end_token next_token elements end # All methods below belong to the PN lexer def next_token skip_white s = @pos c = next_cp case @char_types[c] when TYPE_END @token_value = nil @token = TOKEN_END when TYPE_MINUS if @char_types[peek_cp] == TYPE_DIGIT next_token # consume float or integer @token_value = -@token_value else consume_identifier(s) end when TYPE_DIGIT skip_decimal_digits c = peek_cp if c == 0x2e # '.' @pos += 1 consume_float(s, c) else @token_value = @text[s..@pos].to_i @token = TOKEN_INT end when TYPE_DELIM @token_value = @text[s] @token = c when TYPE_KEY_START if @char_types[peek_cp] == TYPE_ALPHA next_token @token = TOKEN_KEY else parse_error(_("expected identifier after ':'")) end when TYPE_STRING_START consume_string else consume_identifier(s) end end def consume_identifier(s) while @char_types[peek_cp] >= TYPE_IDENTIFIER do @pos += 1 end id = @text[s...@pos] case id when LIT_TRUE @token = TOKEN_BOOL @token_value = true when LIT_FALSE @token = TOKEN_BOOL @token_value = false when LIT_NIL @token = TOKEN_NIL @token_value = nil else @token = TOKEN_IDENTIFIER @token_value = id end end def consume_string s = @pos b = ''.dup loop do c = next_cp case c when TOKEN_END @pos = s - 1 parse_error(_('unterminated quote')) when TOKEN_STRING @token_value = b @token = TOKEN_STRING break when 0x5c # '\' c = next_cp case c when 0x74 # 't' b << "\t" when 0x72 # 'r' b << "\r" when 0x6e # 'n' b << "\n" when TOKEN_STRING b << '"' when 0x5c # '\' b << "\\" when 0x6f # 'o' c = 0 3.times do n = next_cp if 0x30 <= n && n <= 0x37c c *= 8 c += n - 0x30 else parse_error(_('malformed octal quote')) end end b << c else b << "\\" b << c end else b << c end end end def consume_float(s, d) parse_error(_('digit expected')) if skip_decimal_digits == 0 c = peek_cp if d == 0x2e # '.' if c == 0x45 || c == 0x65 # 'E' or 'e' @pos += 1 parse_error(_('digit expected')) if skip_decimal_digits == 0 c = peek_cp end end parse_error(_('digit expected')) if @char_types[c] == TYPE_ALPHA @token_value = @text[s...@pos].to_f @token = TOKEN_FLOAT end def skip_decimal_digits count = 0 c = peek_cp if c == 0x2d || c == 0x2b # '-' or '+' @pos += 1 c = peek_cp end while @char_types[c] == TYPE_DIGIT do @pos += 1 c = peek_cp count += 1 end count end def skip_white while @char_types[peek_cp] == TYPE_WS do @pos += 1 end end def next_cp c = 0 if @pos < @codepoints.size c = @codepoints[@pos] @pos += 1 end c end def peek_cp @pos < @codepoints.size ? @codepoints[@pos] : 0 end end end end