123456789_123456789_123456789_123456789_123456789_

Class: Gem::YAMLSerializer::Parser

Relationships & Source Files
Inherits: Object
Defined in: lib/rubygems/yaml_serializer.rb

Constant Summary

Class Method Summary

Instance Method Summary

Constructor Details

.new(source) ⇒ Parser

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 59

def initialize(source)
  @lines = source.split(/\r?\n/).each(&:freeze)
  @anchors = {}
  @depth = 0
  strip_document_prefix
end

Instance Method Details

#apply_tag(node, tag, anchor) (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 252

def apply_tag(node, tag, anchor)
  if node.is_a?(Mapping)
    node.tag = tag
    node.anchor = anchor
    node
  else
    Mapping.new(["value", node], tag, anchor)
  end
end

#coerce(val, depth = 0) (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 314

def coerce(val, depth = 0)
  raise_max_nesting! if depth > MAX_NESTING_DEPTH

  val = val.sub(/^! /, "") if val.start_with?("! ")

  if val.size >= 2 && val.start_with?("\"") && val.end_with?("\"")
    inner = val[1..-2]
    return inner unless inner.include?("\\")
    inner.gsub(/\\(?:["\\0abtnvfreN_LP]|x\h{2}|u\h{4}|U\h{8})/) do |m|
      char = STRING_UNESCAPES[m] || unescape_code_point(m)
      # Metadata is read as binary, where non-ASCII text stays as UTF-8 bytes.
      Encoding.compatible?(inner, char) ? char : char.b
    end
  elsif val.size >= 2 && val.start_with?("'") && val.end_with?("'")
    inner = val[1..-2]
    inner.include?("''") ? inner.gsub("''", "'") : inner
  elsif val == "true"
    true
  elsif val == "false"
    false
  elsif NULL_SCALARS.include?(val)
    nil
  elsif val == "{}"
    Mapping.new([])
  elsif val =~ /^\[(.*)\]$/
    inner = $1.strip
    return Sequence.new([]) if inner.empty?
    items = inner.scan(FLOW_SEQUENCE_ENTRY_RE).map {|e| Scalar.new(coerce(e, depth + 1)) }
    Sequence.new(items)
  elsif /\A\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}:\d{2})?/.match?(val)
    begin
      Time.new(val)
    rescue ArgumentError
      # date-only format like "2024-06-15" is not supported by Time.new
      if /\A(\d{4})-(\d{2})-(\d{2})\z/.match(val)
        Time.utc($1.to_i, $2.to_i, $3.to_i)
      else
        val
      end
    end
  elsif /^-?\d+$/.match?(val)
    val.to_i
  else
    val
  end
end

#consume_anchor (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 401

def consume_anchor
  line = @lines[0]
  stripped = line.lstrip
  return nil unless stripped.start_with?("&") && stripped =~ /^&(\S)\s/

  anchor = $1
  @lines[0] = line.sub(/&#{Regexp.escape(anchor)}\s+/, "")
  anchor
end

#consume_value_anchor(val) (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 418

def consume_value_anchor(val)
  return [nil, val] unless val =~ /^&(\S)\s/

  anchor = $1
  [anchor, val.sub(/^&#{Regexp.escape(anchor)}\s+/, "")]
end

#current_indent (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 391

def current_indent
  indent_of(@lines[0])
end

#decode_binary_tag(str) (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 367

def decode_binary_tag(str)
  content = str.sub(/\A!binary\s+/, "")
  content = $1 if content =~ /\A"(.*)"\z/ || content =~ /\A'(.*)'\z/
  content.unpack1("m")
end

#extract_item_anchor(content) (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 411

def extract_item_anchor(content)
  return [nil, content] unless content =~ /^&(\S+)/

  anchor = $1
  [anchor, content.sub(/^&#{Regexp.escape(anchor)}\s*/, "")]
end

#fold_continuation_lines(result, val, indent) (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 304

def fold_continuation_lines(result, val, indent)
  return unless result.is_a?(String)

  # "#" does not start a comment inside a quoted scalar that runs onto the next lines.
  stop = val.start_with?("'", "\"") && result == val ? BLANK_RE : BLANK_OR_COMMENT_RE
  while @lines.any? && !stop.match?(@lines[0]) && current_indent > indent
    result << " " << @lines.shift.strip
  end
end

#indent_of(line) (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 395

def indent_of(line)
  i = 0
  i += 1 while LSTRIP_BYTES.include?(line.getbyte(i))
  i
end

#parse

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 66

def parse
  return nil if @lines.empty?

  root = nil
  while @lines.any?
    before = @lines.size
    node = parse_node(-1)
    @lines.shift if @lines.size == before && @lines.any?

    if root.is_a?(Mapping) && node.is_a?(Mapping)
      root.pairs.concat(node.pairs)
    elsif root.nil?
      root = node
    end
  end
  root
end

#parse_alias_ref (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 383

def parse_alias_ref
  AliasRef.new(@lines.shift.lstrip[1..].strip)
end

#parse_binary_value(val, indent) (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 373

def parse_binary_value(val, indent)
  rest = val.sub(/\A!binary\s+/, "")
  if rest.start_with?("|")
    content = parse_block_scalar(indent, rest[1..].to_s.strip)
    Scalar.new(content.unpack1("m"))
  else
    Scalar.new(decode_binary_tag(val))
  end
end

#parse_block_scalar(base_indent, modifier) (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 262

def parse_block_scalar(base_indent, modifier)
  parts = []
  indentation = modifier[/\A[+-]?([1-9])/, 1]
  chomping = modifier[/\A[1-9]?([+-])/, 1]
  block_indent = indentation && base_indent + indentation.to_i

  while @lines.any?
    line = @lines[0]
    if BLANK_RE.match?(line)
      parts << line[block_indent..] if block_indent && line.size > block_indent
      parts << "\n"
      @lines.shift
    else
      line_indent = indent_of(line)
      break if line_indent <= base_indent
      block_indent ||= line_indent
      parts << @lines.shift[block_indent..].to_s << "\n"
    end
  end

  res = parts.join
  res.delete_suffix!("\n") while chomping == "-" && res.end_with?("\n")
  res
end

#parse_inline_alias(content) (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 387

def parse_inline_alias(content)
  AliasRef.new(content[1..].strip)
end

#parse_inline_scalar(val, indent) (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 296

def parse_inline_scalar(val, indent)
  result = coerce(val)
  return result if result.is_a?(Mapping) || result.is_a?(Sequence)

  fold_continuation_lines(result, val, indent)
  Scalar.new(result)
end

#parse_mapping(indent, anchor) (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 183

def parse_mapping(indent, anchor)
  pairs = []
  while @lines.any?
    skip_blank_and_comments
    break if @lines.empty?
    line = @lines[0]
    break unless indent_of(line) == indent

    if (match = QUOTED_KEY_RE.match(line, indent))
      key = coerce(match[1])
    elsif (match = MAPPING_KEY_RE.match(line, indent)) && !RUBY_OBJECT_TAG_RE.match?(line, indent)
      key = match[1]
      key.strip!
      key = decode_binary_tag(key) if key.start_with?("!binary ")
    else
      break
    end
    @lines.shift
    val = match[2]
    val&.strip!
    val = strip_comment(val || "")

    val_anchor = nil
    val_anchor, val = consume_value_anchor(val) if val.start_with?("&")
    value = parse_mapping_value(val, indent)
    value = register_anchor(val_anchor, value) if val_anchor

    pairs << key << value
  end
  register_anchor(anchor, Mapping.new(pairs))
end

#parse_mapping_value(val, indent) (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 215

def parse_mapping_value(val, indent)
  if val.start_with?("*")
    parse_inline_alias(val)
  elsif val.start_with?("!ruby/object:")
    parse_tagged_content(val.strip, indent)
  elsif val.start_with?("!binary ")
    parse_binary_value(val, indent)
  elsif val.empty?
    skip_blank_and_comments
    line = @lines[0]
    if line && indent_of(line) == indent && SEQUENCE_ENTRY_RE.match?(line, indent)
      parse_node(indent)
    else
      parse_node(indent + 1)
    end
  elsif val == "[]"
    Sequence.new([])
  elsif val == "{}"
    Mapping.new([])
  elsif val.start_with?("|")
    Scalar.new(parse_block_scalar(indent, val[1..].to_s.strip))
  else
    parse_inline_scalar(val, indent)
  end
end

#parse_node(base_indent) (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 97

def parse_node(base_indent)
  @depth += 1
  raise_max_nesting! if @depth > MAX_NESTING_DEPTH

  skip_blank_and_comments
  return nil if @lines.empty?

  line = @lines[0]
  indent = indent_of(line)
  return nil if indent < base_indent

  return parse_alias_ref if /\G\*/.match?(line, indent)

  pos = indent
  if /\G&/.match?(line, indent) && (anchor = consume_anchor)
    line = @lines[0]
    pos = indent_of(line)
  end

  if SEQUENCE_ENTRY_RE.match?(line, pos)
    parse_sequence(indent, anchor)
  elsif QUOTED_KEY_RE.match?(line, pos)
    # A mapping whose key is quoted, e.g. '"have: colon": value'.
    parse_mapping(indent, anchor)
  elsif (/\G"/.match?(line, pos) && line.end_with?("\"")) ||
        (/\G'/.match?(line, pos) && line.end_with?("'"))
    # A whole-line quoted scalar, e.g. '"system: foo: bar"'. It may
    # contain ": ", so it must be checked before MAPPING_KEY_RE.
    parse_plain_scalar(indent, anchor)
  elsif MAPPING_KEY_RE.match?(line, pos) && !RUBY_OBJECT_TAG_RE.match?(line, pos)
    parse_mapping(indent, anchor)
  elsif RUBY_OBJECT_TAG_RE.match?(line, pos)
    parse_tagged_node(indent, anchor)
  elsif /\G\|/.match?(line, pos)
    modifier = line[pos + 1, line.size].strip
    @lines.shift
    register_anchor(anchor, Scalar.new(parse_block_scalar(indent, modifier)))
  else
    parse_plain_scalar(indent, anchor)
  end
ensure
  @depth -= 1
end

#parse_plain_scalar(indent, anchor) (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 287

def parse_plain_scalar(indent, anchor)
  val = @lines.shift.strip
  result = coerce(val)
  return register_anchor(anchor, result) if result.is_a?(Mapping) || result.is_a?(Sequence)

  fold_continuation_lines(result, val, indent)
  register_anchor(anchor, Scalar.new(result))
end

#parse_sequence(indent, anchor) (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 141

def parse_sequence(indent, anchor)
  items = []
  while @lines.any?
    skip_blank_and_comments
    break if @lines.empty?
    line = @lines[0]
    break unless indent_of(line) == indent && SEQUENCE_ENTRY_RE.match?(line, indent)
    @lines.shift
    content = line[indent + 1, line.size]
    content.strip!
    item_anchor = nil
    item_anchor, content = extract_item_anchor(content) if content.start_with?("&")
    item = parse_sequence_item(content, indent)
    items << register_anchor(item_anchor, item)
  end
  register_anchor(anchor, Sequence.new(items))
end

#parse_sequence_item(content, indent) (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 159

def parse_sequence_item(content, indent)
  if content.start_with?("*")
    parse_inline_alias(content)
  elsif content.empty?
    skip_blank_and_comments
    @lines.any? && current_indent > indent ? parse_node(indent) : nil
  elsif content.start_with?("!ruby/object:")
    parse_tagged_content(content.strip, indent)
  elsif content.start_with?("!binary ")
    parse_binary_value(content, indent)
  elsif content.start_with?("-")
    @lines.unshift("#{" " * (indent + 2)}#{content}")
    parse_node(indent)
  elsif QUOTED_KEY_RE.match?(content) ||
        (MAPPING_KEY_RE.match?(content) && !content.start_with?("!ruby/object:"))
    @lines.unshift("#{" " * (indent + 2)}#{content}")
    parse_node(indent)
  elsif content.start_with?("|")
    Scalar.new(parse_block_scalar(indent, content[1..].to_s.strip))
  else
    parse_inline_scalar(content, indent)
  end
end

#parse_tagged_content(tag, indent) (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 247

def parse_tagged_content(tag, indent)
  nested = parse_node(indent)
  apply_tag(nested, tag, nil)
end

#parse_tagged_node(indent, anchor) (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 241

def parse_tagged_node(indent, anchor)
  tag = @lines.shift.strip
  nested = parse_node(indent)
  apply_tag(nested, tag, anchor)
end

#raise_max_nesting! (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 433

def raise_max_nesting!
  message = "exceeded maximum nesting depth (#{MAX_NESTING_DEPTH})"
  if defined?(Psych::VERSION)
    raise Psych::SyntaxError.new(nil, 0, 0, 0, message, nil)
  else
    raise Psych::SyntaxError, message
  end
end

#register_anchor(name, node) (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 425

def register_anchor(name, node)
  if name
    @anchors[name] = node
    node.anchor = name if node.respond_to?(:anchor=)
  end
  node
end

#skip_blank_and_comments (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 442

def skip_blank_and_comments
  @lines.shift while @lines.any? && BLANK_OR_COMMENT_RE.match?(@lines[0])
end

#strip_comment(val) (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 446

def strip_comment(val)
  return val unless val.include?("#")
  return "" if val.lstrip.start_with?("#")

  in_single = false
  in_double = false
  escape = false

  val.each_char.with_index do |ch, i|
    if escape
      escape = false
      next
    end

    if in_single
      in_single = false if ch == "'"
    elsif in_double
      if ch == "\\"
        escape = true
      elsif ch == '"'
        in_double = false
      end
    else
      case ch
      when "'" then in_single = true
      when '"' then in_double = true
      when "#"
        # A "#" starts a comment only when preceded by whitespace.
        return val[0...i].rstrip if [" ", "\t"].include?(val[i - 1])
      end
    end
  end

  val
end

#strip_document_prefix (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 86

def strip_document_prefix
  return if @lines.empty?
  return unless @lines[0]&.start_with?("---")

  if @lines[0].strip == "---"
    @lines.shift
  else
    @lines[0] = @lines[0].sub(/^---\s*/, "")
  end
end

#unescape_code_point(escape) (private)

[ GitHub ]

  
# File 'lib/rubygems/yaml_serializer.rb', line 361

def unescape_code_point(escape)
  code = escape[2..].hex
  return escape if code > 0x10FFFF || code.between?(0xD800, 0xDFFF)
  code.chr(Encoding::UTF_8)
end