plist2 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # = plist
4
+ #
5
+ # Copyright 2006-2010 Ben Bleything and Patrick May
6
+ # Distributed under the MIT License
7
+ #
8
+
9
+ module Plist ; end
10
+
11
+ # === Create a plist
12
+ # You can dump an object to a plist in one of two ways:
13
+ #
14
+ # * <tt>Plist::Emit.dump(obj)</tt>
15
+ # * <tt>obj.to_plist</tt>
16
+ # * This requires that you mixin the <tt>Plist::Emit</tt> module, which is already done for +Array+ and +Hash+.
17
+ #
18
+ # The following Ruby classes are converted into native plist types:
19
+ # Array, Bignum, Date, DateTime, Fixnum, Float, Hash, Integer, String, Symbol, Time, true, false
20
+ # * +Array+ and +Hash+ are both recursive; their elements will be converted into plist nodes inside the <array> and <dict> containers (respectively).
21
+ # * +IO+ (and its descendants) and +StringIO+ objects are read from and their contents placed in a <data> element.
22
+ # * User classes may implement +to_plist_node+ to dictate how they should be serialized; otherwise the object will be passed to <tt>Marshal.dump</tt> and the result placed in a <data> element.
23
+ #
24
+ # For detailed usage instructions, refer to USAGE[link:files/docs/USAGE.html] and the methods documented below.
25
+ module Plist::Emit
26
+ # Helper method for injecting into classes. Calls <tt>Plist::Emit.dump</tt> with +self+.
27
+ EMIT_DEFAULTS = {
28
+ :sort => true
29
+ }
30
+ def to_plist(envelope = true, options = {})
31
+ return Plist::Emit.dump(self, envelope, options)
32
+ end
33
+
34
+ # Helper method for injecting into classes. Calls <tt>Plist::Emit.save_plist</tt> with +self+.
35
+ def save_plist(filename)
36
+ Plist::Emit.save_plist(self, filename)
37
+ end
38
+
39
+ # The following Ruby classes are converted into native plist types:
40
+ # Array, Bignum, Date, DateTime, Fixnum, Float, Hash, Integer, String, Symbol, Time
41
+ #
42
+ # Write us (via RubyForge) if you think another class can be coerced safely into one of the expected plist classes.
43
+ #
44
+ # +IO+ and +StringIO+ objects are encoded and placed in <data> elements; other objects are <tt>Marshal.dump</tt>'ed unless they implement +to_plist_node+.
45
+ #
46
+ # The +envelope+ parameters dictates whether or not the resultant plist fragment is wrapped in the normal XML/plist header and footer. Set it to false if you only want the fragment.
47
+ def self.dump(obj, envelope = true, options = {})
48
+ options = EMIT_DEFAULTS.merge(options)
49
+ output = plist_node(obj, options)
50
+
51
+ output = wrap(output) if envelope
52
+
53
+ return output
54
+ end
55
+
56
+ # Writes the serialized object's plist to the specified filename.
57
+ def self.save_plist(obj, filename)
58
+ File.open(filename, 'wb') do |f|
59
+ f.write(obj.to_plist)
60
+ end
61
+ end
62
+
63
+ private
64
+ def self.plist_node(element, options)
65
+ output = ''
66
+
67
+ if element.respond_to? :to_plist_node
68
+ output << element.to_plist_node
69
+ else
70
+ case element
71
+ when Array
72
+ if element.empty?
73
+ output << "<array/>\n"
74
+ else
75
+ output << tag('array') {
76
+ element.collect {|e| plist_node(e, options)}
77
+ }
78
+ end
79
+ when Hash
80
+ if element.empty?
81
+ output << "<dict/>\n"
82
+ else
83
+ inner_tags = []
84
+
85
+ keys = element.keys
86
+ if options[:sort]
87
+ keys = keys.sort_by{|k| k.to_s }
88
+ end
89
+ keys.each do |k|
90
+ v = element[k]
91
+ inner_tags << tag('key', CGI::escapeHTML(k.to_s))
92
+ inner_tags << plist_node(v, options)
93
+ end
94
+
95
+ output << tag('dict') {
96
+ inner_tags
97
+ }
98
+ end
99
+ when true, false
100
+ output << "<#{element}/>\n"
101
+ when Time
102
+ output << tag('date', element.utc.strftime('%Y-%m-%dT%H:%M:%SZ'))
103
+ when Date # also catches DateTime
104
+ output << tag('date', element.strftime('%Y-%m-%dT%H:%M:%SZ'))
105
+ when String, Symbol, Fixnum, Bignum, Integer, Float
106
+ output << tag(element_type(element), CGI::escapeHTML(element.to_s))
107
+ when IO, StringIO
108
+ element.rewind
109
+ contents = element.read
110
+ # note that apple plists are wrapped at a different length then
111
+ # what ruby's base64 wraps by default.
112
+ # I used #encode64 instead of #b64encode (which allows a length arg)
113
+ # because b64encode is b0rked and ignores the length arg.
114
+ data = "\n"
115
+ Base64::encode64(contents).gsub(/\s+/, '').scan(/.{1,68}/o) { data << $& << "\n" }
116
+ output << tag('data', data)
117
+ else
118
+ output << comment( 'The <data> element below contains a Ruby object which has been serialized with Marshal.dump.' )
119
+ data = "\n"
120
+ Base64::encode64(Marshal.dump(element)).gsub(/\s+/, '').scan(/.{1,68}/o) { data << $& << "\n" }
121
+ output << tag('data', data )
122
+ end
123
+ end
124
+
125
+ return output
126
+ end
127
+
128
+ def self.comment(content)
129
+ return "<!-- #{content} -->\n"
130
+ end
131
+
132
+ def self.tag(type, contents = '', &block)
133
+ out = nil
134
+
135
+ if block_given?
136
+ out = IndentedString.new
137
+ out << "<#{type}>"
138
+ out.raise_indent
139
+
140
+ out << block.call
141
+
142
+ out.lower_indent
143
+ out << "</#{type}>"
144
+ else
145
+ out = "<#{type}>#{contents.to_s}</#{type}>\n"
146
+ end
147
+
148
+ return out.to_s
149
+ end
150
+
151
+ def self.wrap(contents)
152
+ output = ''
153
+
154
+ output << '<?xml version="1.0" encoding="UTF-8"?>' + "\n"
155
+ output << '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' + "\n"
156
+ output << '<plist version="1.0">' + "\n"
157
+
158
+ output << contents
159
+
160
+ output << '</plist>' + "\n"
161
+
162
+ return output
163
+ end
164
+
165
+ def self.element_type(item)
166
+ case item
167
+ when String, Symbol
168
+ 'string'
169
+
170
+ when Fixnum, Bignum, Integer
171
+ 'integer'
172
+
173
+ when Float
174
+ 'real'
175
+
176
+ else
177
+ raise "Don't know about this data type... something must be wrong!"
178
+ end
179
+ end
180
+ private
181
+ class IndentedString #:nodoc:
182
+ attr_accessor :indent_string
183
+
184
+ def initialize(str = "\t")
185
+ @indent_string = str
186
+ @contents = ''
187
+ @indent_level = 0
188
+ end
189
+
190
+ def to_s
191
+ return @contents
192
+ end
193
+
194
+ def raise_indent
195
+ @indent_level += 1
196
+ end
197
+
198
+ def lower_indent
199
+ @indent_level -= 1 if @indent_level > 0
200
+ end
201
+
202
+ def <<(val)
203
+ if val.is_a? Array
204
+ val.each do |f|
205
+ self << f
206
+ end
207
+ else
208
+ # if it's already indented, don't bother indenting further
209
+ unless val =~ /\A#{@indent_string}/
210
+ indent = @indent_string * @indent_level
211
+
212
+ @contents << val.gsub(/^/, indent)
213
+ else
214
+ @contents << val
215
+ end
216
+
217
+ # it already has a newline, don't add another
218
+ @contents << "\n" unless val =~ /\n$/
219
+ end
220
+ end
221
+ end
222
+ end
223
+
224
+ class Array #:nodoc:
225
+ include Plist::Emit
226
+ end
227
+
228
+ class Hash #:nodoc:
229
+ include Plist::Emit
230
+ end
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # = plist
4
+ #
5
+ # Copyright 2006-2010 Ben Bleything and Patrick May
6
+ # Distributed under the MIT License
7
+ #
8
+
9
+ # Plist parses Mac OS X xml property list files into ruby data structures.
10
+ #
11
+ # === Load a plist file
12
+ # This is the main point of the library:
13
+ #
14
+ # r = Plist::parse_xml( filename_or_xml )
15
+ module Plist
16
+ # Note that I don't use these two elements much:
17
+ #
18
+ # + Date elements are returned as DateTime objects.
19
+ # + Data elements are implemented as Tempfiles
20
+ #
21
+ # Plist::parse_xml will blow up if it encounters a Date element.
22
+ # If you encounter such an error, or if you have a Date element which
23
+ # can't be parsed into a Time object, please send your plist file to
24
+ # plist@hexane.org so that I can implement the proper support.
25
+ def Plist::parse_xml( filename_or_xml )
26
+ listener = Listener.new
27
+ #parser = REXML::Parsers::StreamParser.new(File.new(filename), listener)
28
+ parser = StreamParser.new(filename_or_xml, listener)
29
+ parser.parse
30
+ listener.result
31
+ end
32
+
33
+ class Listener
34
+ #include REXML::StreamListener
35
+
36
+ attr_accessor :result, :open
37
+
38
+ def initialize
39
+ @result = nil
40
+ @open = Array.new
41
+ end
42
+
43
+
44
+ def tag_start(name, attributes)
45
+ @open.push PTag::mappings[name].new
46
+ end
47
+
48
+ def text( contents )
49
+ @open.last.text = contents if @open.last
50
+ end
51
+
52
+ def tag_end(name)
53
+ last = @open.pop
54
+ if @open.empty?
55
+ @result = last.to_ruby
56
+ else
57
+ @open.last.children.push last
58
+ end
59
+ end
60
+ end
61
+
62
+ class StreamParser
63
+ def initialize( plist_data_or_file, listener )
64
+ if plist_data_or_file.respond_to? :read
65
+ @xml = plist_data_or_file.read
66
+ elsif File.exists? plist_data_or_file
67
+ @xml = File.read( plist_data_or_file )
68
+ else
69
+ @xml = plist_data_or_file
70
+ end
71
+
72
+ @listener = listener
73
+ end
74
+
75
+ TEXT = /([^<]+)/
76
+ XMLDECL_PATTERN = /<\?xml\s+(.*?)\?>*/um
77
+ DOCTYPE_PATTERN = /\s*<!DOCTYPE\s+(.*?)(\[|>)/um
78
+ COMMENT_START = /\A<!--/u
79
+ COMMENT_END = /.*?-->/um
80
+
81
+
82
+ def parse
83
+ plist_tags = PTag::mappings.keys.join('|')
84
+ start_tag = /<(#{plist_tags})([^>]*)>/i
85
+ end_tag = /<\/(#{plist_tags})[^>]*>/i
86
+
87
+ require 'strscan'
88
+
89
+ @scanner = StringScanner.new( @xml )
90
+ until @scanner.eos?
91
+ if @scanner.scan(COMMENT_START)
92
+ @scanner.scan(COMMENT_END)
93
+ elsif @scanner.scan(XMLDECL_PATTERN)
94
+ elsif @scanner.scan(DOCTYPE_PATTERN)
95
+ elsif @scanner.scan(start_tag)
96
+ @listener.tag_start(@scanner[1], nil)
97
+ if (@scanner[2] =~ /\/$/)
98
+ @listener.tag_end(@scanner[1])
99
+ end
100
+ elsif @scanner.scan(TEXT)
101
+ @listener.text(@scanner[1])
102
+ elsif @scanner.scan(end_tag)
103
+ @listener.tag_end(@scanner[1])
104
+ else
105
+ raise "Unimplemented element"
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ class PTag
112
+ @@mappings = { }
113
+ def PTag::mappings
114
+ @@mappings
115
+ end
116
+
117
+ def PTag::inherited( sub_class )
118
+ key = sub_class.to_s.downcase
119
+ key.gsub!(/^plist::/, '' )
120
+ key.gsub!(/^p/, '') unless key == "plist"
121
+
122
+ @@mappings[key] = sub_class
123
+ end
124
+
125
+ attr_accessor :text, :children
126
+ def initialize
127
+ @children = Array.new
128
+ end
129
+
130
+ def to_ruby
131
+ raise "Unimplemented: " + self.class.to_s + "#to_ruby on #{self.inspect}"
132
+ end
133
+ end
134
+
135
+ class PList < PTag
136
+ def to_ruby
137
+ children.first.to_ruby if children.first
138
+ end
139
+ end
140
+
141
+ class PDict < PTag
142
+ def to_ruby
143
+ dict = Hash.new
144
+ key = nil
145
+
146
+ children.each do |c|
147
+ if key.nil?
148
+ key = c.to_ruby
149
+ else
150
+ dict[key] = c.to_ruby
151
+ key = nil
152
+ end
153
+ end
154
+
155
+ dict
156
+ end
157
+ end
158
+
159
+ class PKey < PTag
160
+ def to_ruby
161
+ CGI::unescapeHTML(text || '')
162
+ end
163
+ end
164
+
165
+ class PString < PTag
166
+ def to_ruby
167
+ CGI::unescapeHTML(text || '')
168
+ end
169
+ end
170
+
171
+ class PArray < PTag
172
+ def to_ruby
173
+ children.collect do |c|
174
+ c.to_ruby
175
+ end
176
+ end
177
+ end
178
+
179
+ class PInteger < PTag
180
+ def to_ruby
181
+ text.to_i
182
+ end
183
+ end
184
+
185
+ class PTrue < PTag
186
+ def to_ruby
187
+ true
188
+ end
189
+ end
190
+
191
+ class PFalse < PTag
192
+ def to_ruby
193
+ false
194
+ end
195
+ end
196
+
197
+ class PReal < PTag
198
+ def to_ruby
199
+ text.to_f
200
+ end
201
+ end
202
+
203
+ require 'date'
204
+ class PDate < PTag
205
+ def to_ruby
206
+ DateTime.parse(text)
207
+ end
208
+ end
209
+
210
+ require 'base64'
211
+ class PData < PTag
212
+ def to_ruby
213
+ data = Base64.decode64(text.gsub(/\s+/, ''))
214
+
215
+ begin
216
+ return Marshal.load(data)
217
+ rescue Exception => e
218
+ io = StringIO.new
219
+ io.write data
220
+ io.rewind
221
+ return io
222
+ end
223
+ end
224
+ end
225
+ end