summaryrefslogtreecommitdiffstats
path: root/lib/facter/util/plist/parser.rb
blob: 48e10343ec74688f8119b53c2bb96ff1f0c04013 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
#--###########################################################
# Copyright 2006, Ben Bleything <ben@bleything.net> and      #
# Patrick May <patrick@hexane.org>                           #
#                                                            #
# Distributed under the MIT license.                         #
##############################################################
#++
# Plist parses Mac OS X xml property list files into ruby data structures.
#
# === Load a plist file
# This is the main point of the library:
#
#   r = Plist::parse_xml( filename_or_xml )
module Plist
# Note that I don't use these two elements much:
#
#  + Date elements are returned as DateTime objects.
#  + Data elements are implemented as Tempfiles
#
# Plist::parse_xml will blow up if it encounters a data element.
# If you encounter such an error, or if you have a Date element which
# can't be parsed into a Time object, please send your plist file to
# plist@hexane.org so that I can implement the proper support.
    def Plist::parse_xml( filename_or_xml )
        listener = Listener.new
        #parser = REXML::Parsers::StreamParser.new(File.new(filename), listener)
        parser = StreamParser.new(filename_or_xml, listener)
        parser.parse
        listener.result
    end

    class Listener
        #include REXML::StreamListener

        attr_accessor :result, :open

        def initialize
            @result = nil
            @open   = Array.new
        end


        def tag_start(name, attributes)
            @open.push PTag::mappings[name].new
        end

        def text( contents )
            @open.last.text = contents if @open.last
        end

        def tag_end(name)
            last = @open.pop
            if @open.empty?
                @result = last.to_ruby
            else
                @open.last.children.push last
            end
        end
    end

    class StreamParser
        def initialize( filename_or_xml, listener )
            @filename_or_xml = filename_or_xml
            @listener = listener
        end

        TEXT            = /([^<]+)/
        XMLDECL_PATTERN = /<\?xml\s+(.*?)\?>*/um
        DOCTYPE_PATTERN = /\s*<!DOCTYPE\s+(.*?)(\[|>)/um
        COMMENT_START   = /\A<!--/u
        COMMENT_END     = /.*?-->/um

        def parse
            plist_tags = PTag::mappings.keys.join('|')
            start_tag  = /<(#{plist_tags})([^>]*)>/i
            end_tag    = /<\/(#{plist_tags})[^>]*>/i

            require 'strscan'

            contents = (
                if (File.exists? @filename_or_xml)
                    File.open(@filename_or_xml) {|f| f.read}
                else
                    @filename_or_xml
                end
            )

            @scanner = StringScanner.new( contents )
            until @scanner.eos?
                if @scanner.scan(COMMENT_START)
                    @scanner.scan(COMMENT_END)
                elsif @scanner.scan(XMLDECL_PATTERN)
                elsif @scanner.scan(DOCTYPE_PATTERN)
                elsif @scanner.scan(start_tag)
                    @listener.tag_start(@scanner[1], nil)
                    if (@scanner[2] =~ /\/$/)
                        @listener.tag_end(@scanner[1])
                    end
                elsif @scanner.scan(TEXT)
                    @listener.text(@scanner[1])
                elsif @scanner.scan(end_tag)
                    @listener.tag_end(@scanner[1])
                else
                    raise "Unimplemented element"
                end
            end
        end
    end

    class PTag
        @@mappings = { }
        def PTag::mappings
            @@mappings
        end

        def PTag::inherited( sub_class )
            key = sub_class.to_s.downcase
            key.gsub!(/^plist::/, '' )
            key.gsub!(/^p/, '')  unless key == "plist"

            @@mappings[key] = sub_class
        end

        attr_accessor :text, :children
        def initialize
            @children = Array.new
        end

        def to_ruby
            raise "Unimplemented: " + self.class.to_s + "#to_ruby on #{self.inspect}"
        end
    end

    class PList < PTag
        def to_ruby
            children.first.to_ruby if children.first
        end
    end

    class PDict < PTag
        def to_ruby
            dict = Hash.new
            key = nil

            children.each do |c|
                if key.nil?
                    key = c.to_ruby
                else
                    dict[key] = c.to_ruby
                    key = nil
                end
            end

            dict
        end
    end

    class PKey < PTag
        def to_ruby
            CGI::unescapeHTML(text || '')
        end
    end

    class PString < PTag
        def to_ruby
            CGI::unescapeHTML(text || '')
        end
    end

    class PArray < PTag
        def to_ruby
            children.collect do |c|
                c.to_ruby
            end
        end
    end

    class PInteger < PTag
        def to_ruby
            text.to_i
        end
    end

    class PTrue < PTag
        def to_ruby
            true
        end
    end

    class PFalse < PTag
        def to_ruby
            false
        end
    end

    class PReal < PTag
        def to_ruby
            text.to_f
        end
    end

    require 'date'
    class PDate < PTag
        def to_ruby
            DateTime.parse(text)
        end
    end

    require 'base64'
    class PData < PTag
        def to_ruby
            data = Base64.decode64(text.gsub(/\s+/, ''))

            begin
                return Marshal.load(data)
            rescue Exception => e
                io = StringIO.new
                io.write data
                io.rewind
                return io
            end
        end
    end
end

# $Id: parser.rb 1781 2006-10-16 01:01:35Z luke $