lib/cfpropertylist/rbPlainCFPropertyList.rb



# -*- coding: utf-8 -*-

require 'strscan'

module CFPropertyList
  # XML parser
  class PlainParser < XMLParserInterface
    # read a XML file
    # opts::
    # * :file - The filename of the file to load
    # * :data - The data to parse
    def load(opts)
      @doc = nil

      if(opts.has_key?(:file)) then
        File.open(opts[:file], :external_encoding => "ASCII") do |fd|
          @doc = StringScanner.new(fd.read)
        end
      else
        @doc = StringScanner.new(opts[:data])
      end

      if @doc
        root = import_plain
        raise CFFormatError.new('content after root object') unless @doc.eos?

        return root
      end

      raise CFFormatError.new('invalid plist string or file not found')
    end

    SPACES_AND_COMMENTS =  %r{((?:/\*.*?\*/)|(?://.*?$\n?)|(?:\s*))+}x

    # serialize CFPropertyList object to XML
    # opts = {}:: Specify options: :formatted - Use indention and line breaks
    def to_str(opts={})
      opts[:root].to_plain(self)
    end

    protected
    def skip_whitespaces
      @doc.skip SPACES_AND_COMMENTS
    end

    def read_dict
      skip_whitespaces
      hsh = {}

      while not @doc.scan(/\}/)
        key = import_plain
        raise CFFormatError.new("invalid dictionary format") if !key

        if key.is_a?(CFString)
          key = key.value
        elsif key.is_a?(CFInteger) or key.is_a?(CFReal)
          key = key.value.to_s
        else
          raise CFFormatError.new("invalid key format")
        end

        skip_whitespaces

        raise CFFormatError.new("invalid dictionary format") unless @doc.scan(/=/)

        skip_whitespaces
        val = import_plain

        skip_whitespaces
        raise CFFormatError.new("invalid dictionary format") unless @doc.scan(/;/)
        skip_whitespaces

        hsh[key] = val
        raise CFFormatError.new("invalid dictionary format") if @doc.eos?
      end

      CFDictionary.new(hsh)
    end

    def read_array
      skip_whitespaces
      ary = []

      while not @doc.scan(/\)/)
        val = import_plain

        return nil if not val or not val.value
        skip_whitespaces

        if not @doc.skip(/,\s*/)
          if @doc.scan(/\)/)
            ary << val
            return CFArray.new(ary)
          end

          raise CFFormatError.new("invalid array format")
        end

        ary << val
        raise CFFormatError.new("invalid array format") if @doc.eos?
      end

      CFArray.new(ary)
    end

    def escape_char
      case @doc.matched
      when '"'
        '"'
      when '\\'
        '\\'
      when 'a'
        "\a"
      when 'b'
        "\b"
      when 'f'
        "\f"
      when 'n'
        "\n"
      when 'v'
        "\v"
      when 'r'
        "\r"
      when 't'
        "\t"
      end
    end

    def read_quoted
      str = ''

      while not @doc.scan(/"/)
        if @doc.scan(/\\/)
          @doc.scan(/./)
          str << escape_char

        elsif @doc.eos?
          raise CFFormatError.new("unterminated string")

        else @doc.scan(/./)
          str << @doc.matched
        end
      end

      CFString.new(str)
    end

    def read_unquoted
      raise CFFormatError.new("unexpected end of file") if @doc.eos?

      if @doc.scan(/(\d\d\d\d)-(\d\d)-(\d\d)\s+(\d\d):(\d\d):(\d\d)(?:\s+(\+|-)(\d\d)(\d\d))?/)
        year,month,day,hour,min,sec,pl_min,tz_hour, tz_min = @doc[1], @doc[2], @doc[3], @doc[4], @doc[5], @doc[6], @doc[7], @doc[8], @doc[9]
        CFDate.new(Time.new(year, month, day, hour, min, sec, pl_min ? sprintf("%s%s:%s", pl_min, tz_hour, tz_min) : nil))

      elsif @doc.scan(/-?\d+?\.\d+\b/)
        CFReal.new(@doc.matched.to_f)

      elsif @doc.scan(/-?\d+\b/)
        CFInteger.new(@doc.matched.to_i)

      elsif @doc.scan(/\b(true|false)\b/)
        CFBoolean.new(@doc.matched == 'true')
      else
        CFString.new(@doc.scan(/\w+/))
      end
    end

    def read_binary
      @doc.scan(/(.*?)>/)

      hex_str = @doc[1].gsub(/ /, '')
      CFData.new([hex_str].pack("H*"), CFData::DATA_RAW)
    end

    # import the XML values
    def import_plain
      skip_whitespaces
      ret = nil

      if @doc.scan(/\{/) # dict
        ret = read_dict
      elsif @doc.scan(/\(/) # array
        ret = read_array
      elsif @doc.scan(/"/) # string
        ret = read_quoted
      elsif @doc.scan(/</) # binary
        ret = read_binary
      else # string w/o quotes
        ret = read_unquoted
      end

      return ret
    end
  end
end

# eof