# frozen_string_literal: true require_relative '../../../puppet/util/json' module Puppet::Pops module Serialization require_relative 'time_factory' require_relative 'abstract_reader' require_relative 'abstract_writer' module JSON # A Writer that writes output in JSON format # @api private class Writer < AbstractWriter def initialize(io, options = {}) super(Packer.new(io, options), options) end # Clear the underlying io stream but does not clear tabulation cache # Specifically designed to enable tabulation to span more than one # separately deserialized object. def clear_io @packer.clear_io end def extension_packer @packer end def packer @packer end def build_payload yield(@packer) end def to_a @packer.to_a end def to_json @packer.to_json end end # A Reader that reads JSON formatted input # @api private class Reader < AbstractReader def initialize(io) super(Unpacker.new(io)) end def re_initialize(io) @unpacker.re_initialize(io) end def read_payload(data) yield(@unpacker) end end # The JSON Packer. Modeled after the MessagePack::Packer # @api private class Packer def initialize(io, options = {}) @io = io @io << '[' @type_registry = {} @nested = [] @verbose = options[:verbose] @verbose = false if @verbose.nil? @indent = options[:indent] || 0 end def register_type(type, klass, &block) @type_registry[klass] = [type, klass, block] end def clear_io # Truncate everything except leading '[' if @io.is_a?(String) @io.slice!(1..-1) else @io.truncate(1) end end def empty? @io.is_a?(String) ? io.length == 1 : @io.pos == 1 end def flush # Drop last comma unless just start marker present if @io.is_a?(String) @io.chop! unless @io.length == 1 @io << ']' else pos = @io.pos @io.pos = pos - 1 unless pos == 1 @io << ']' @io.flush end end def write(obj) case obj when Array write_array_header(obj.size) obj.each { |x| write(x) } when Hash write_map_header(obj.size) obj.each_pair { |k, v| write(k); write(v) } when nil write_nil else write_scalar(obj) end end alias pack write def write_array_header(n) if n < 1 @io << '[]' else @io << '[' @nested << [false, n] end end def write_map_header(n) if n < 1 @io << '{}' else @io << '{' @nested << [true, n * 2] end end def write_nil @io << 'null' write_delim end def to_s to_json end def to_a ::Puppet::Util::Json.load(io_string) end def to_json if @indent > 0 ::Puppet::Util::Json.dump(to_a, { :pretty => true, :indent => ' ' * @indent }) else io_string end end # Write a payload object. Not subject to extensions def write_pl(obj) @io << Puppet::Util::Json.dump(obj) << ',' end def io_string if @io.is_a?(String) @io else @io.pos = 0 @io.read end end private :io_string def write_delim nesting = @nested.last cnt = nesting.nil? ? nil : nesting[1] case cnt when 1 @io << (nesting[0] ? '}' : ']') @nested.pop write_delim when Integer if cnt.even? || !nesting[0] @io << ',' else @io << ':' end nesting[1] = cnt - 1 else @io << ',' end end private :write_delim def write_scalar(obj) ext = @type_registry[obj.class] if ext.nil? case obj when Numeric, String, true, false, nil @io << Puppet::Util::Json.dump(obj) write_delim else raise SerializationError, _("Unable to serialize a %{obj}") % { obj: obj.class.name } end else write_extension(ext, obj) end end private :write_scalar def write_extension(ext, obj) @io << '[' << extension_indicator(ext).to_json << ',' @nested << nil write_extension_payload(ext, obj) @nested.pop if obj.is_a?(Extension::SequenceStart) && obj.sequence_size > 0 @nested << [false, obj.sequence_size] else if @io.is_a?(String) @io.chop! else @io.pos -= 1 end @io << ']' write_delim end end private :write_extension def write_extension_payload(ext, obj) ext[2].call(obj) end private :write_extension_payload def extension_indicator(ext) @verbose ? ext[1].name.sub(/^Puppet::Pops::Serialization::\w+::(.+)$/, '\1') : ext[0] end private :extension_indicator end class Unpacker def initialize(io) re_initialize(io) @type_registry = {} @nested = [] end def re_initialize(io) parsed = parse_io(io) raise SerializationError, _("JSON stream is not an array. It is a %{klass}") % { klass: io.class.name } unless parsed.is_a?(Array) @etor_stack = [parsed.each] end def read obj = nil loop do raise SerializationError, _('Unexpected end of input') if @etor_stack.empty? etor = @etor_stack.last begin obj = etor.next break rescue StopIteration @etor_stack.pop end end if obj.is_a?(Array) ext_etor = obj.each @etor_stack << ext_etor ext_no = ext_etor.next ext_block = @type_registry[ext_no] raise SerializationError, _("Invalid input. %{ext_no} is not a valid extension number") % { ext_no: ext_no } if ext_block.nil? obj = ext_block.call(nil) end obj end def register_type(extension_number, &block) @type_registry[extension_number] = block end private def parse_io(io) case io when IO, StringIO ::Puppet::Util::Json.load(io.read) when String ::Puppet::Util::Json.load(io) else io end end end end end end