Merge pull request #188 from glarizza/bug/master/11299_cfpropertylist
[#11299] Replace facter/util/plist with cfpropertylist
Daniel Pittman
12 years ago
0 | Copyright (c) 2010 Christian Kruse, <cjk@wwwtech.de> | |
1 | ||
2 | Permission is hereby granted, free of charge, to any person obtaining a | |
3 | copy of this software and associated documentation files (the | |
4 | "Software"), to deal in the Software without restriction, including | |
5 | without limitation the rights to use, copy, modify, merge, publish, | |
6 | distribute, sublicense, and/or sell copies of the Software, and to | |
7 | permit persons to whom the Software is furnished to do so, subject to | |
8 | the following conditions: | |
9 | The above copyright notice and this permission notice shall be included | |
10 | in all copies or substantial portions of the Software. | |
11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
12 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
13 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | |
14 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY | |
15 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, | |
16 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE | |
17 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
18 |
0 | CFPropertyList implementation | |
1 | class to read, manipulate and write both XML and binary property list | |
2 | files (plist(5)) as defined by Apple. Have a look at CFPropertyList::List | |
3 | for more documentation. | |
4 | ||
5 | == Installation | |
6 | ||
7 | You could either use ruby gems and install it via | |
8 | ||
9 | gem install CFPropertyList | |
10 | ||
11 | or you could clone this repository and place it somewhere in your load path. | |
12 | ||
13 | == Example | |
14 | require 'cfpropertylist' | |
15 | ||
16 | # create a arbitrary data structure of basic data types | |
17 | data = { | |
18 | 'name' => 'John Doe', | |
19 | 'missing' => true, | |
20 | 'last_seen' => Time.now, | |
21 | 'friends' => ['Jane Doe','Julian Doe'], | |
22 | 'likes' => { | |
23 | 'me' => false | |
24 | } | |
25 | } | |
26 | ||
27 | # create CFPropertyList::List object | |
28 | plist = CFPropertyList::List.new | |
29 | ||
30 | # call CFPropertyList.guess() to create corresponding CFType values | |
31 | plist.value = CFPropertyList.guess(data) | |
32 | ||
33 | # write plist to file | |
34 | plist.save("example.plist", CFPropertyList::List::FORMAT_BINARY) | |
35 | ||
36 | # … later, read it again | |
37 | plist = CFPropertyList::List.new(:file => "example.plist") | |
38 | data = CFPropertyList.native_types(plist.value) | |
39 | ||
40 | Author:: Christian Kruse (mailto:cjk@wwwtech.de) | |
41 | Copyright:: Copyright (c) 2010 | |
42 | License:: MIT License | |
43 |
0 | require 'rubygems' | |
1 | ||
2 | require 'rubygems/package_task' | |
3 | require 'rdoc/task' | |
4 | require 'rake/testtask' | |
5 | ||
6 | spec = Gem::Specification.new do |s| | |
7 | s.name = "CFPropertyList" | |
8 | s.version = "2.1" | |
9 | s.author = "Christian Kruse" | |
10 | s.email = "cjk@wwwtech.de" | |
11 | s.homepage = "http://github.com/ckruse/CFPropertyList" | |
12 | s.platform = Gem::Platform::RUBY | |
13 | s.summary = "Read, write and manipulate both binary and XML property lists as defined by apple" | |
14 | s.description = "This is a module to read, write and manipulate both binary and XML property lists as defined by apple." | |
15 | s.files = FileList["lib/*"].to_a | |
16 | s.require_path = "lib" | |
17 | #s.autorequire = "name" | |
18 | #s.test_files = FileList["{test}/**/*test.rb"].to_a | |
19 | s.has_rdoc = true | |
20 | s.extra_rdoc_files = ["README"] | |
21 | s.add_development_dependency("rake",">=0.7.0") | |
22 | end | |
23 | ||
24 | desc 'Generate RDoc documentation for the CFPropertyList module.' | |
25 | Rake::RDocTask.new do |rdoc| | |
26 | files = ['README', 'LICENSE', 'lib/*.rb'] | |
27 | rdoc.rdoc_files.add(files) | |
28 | rdoc.main = 'README' | |
29 | rdoc.title = 'CFPropertyList RDoc' | |
30 | rdoc.rdoc_dir = 'doc' | |
31 | rdoc.options << '--line-numbers' << '--inline-source' << '-c utf8' | |
32 | end | |
33 | ||
34 | Gem::PackageTask.new(spec) do |pkg| | |
35 | pkg.need_tar = true | |
36 | end | |
37 | ||
38 | Rake::TestTask.new do |test| | |
39 | test.libs << 'test' | |
40 | test.test_files = Dir.glob('test/test*.rb') | |
41 | end | |
42 | ||
43 | # eof |
0 | Special thanks to: | |
1 | ||
2 | Steve Madsen for providing a lot of performance patches and bugfixes! | |
3 | Have a look at his Github account: <http://github.com/sjmadsen> | |
4 | ||
5 | ||
6 |
0 | # -*- coding: utf-8 -*- | |
1 | ||
2 | module Facter::Util::CFPropertyList | |
3 | # Binary PList parser class | |
4 | class Binary | |
5 | # Read a binary plist file | |
6 | def load(opts) | |
7 | @unique_table = {} | |
8 | @count_objects = 0 | |
9 | @object_refs = 0 | |
10 | ||
11 | @written_object_count = 0 | |
12 | @object_table = [] | |
13 | @object_ref_size = 0 | |
14 | ||
15 | @offsets = [] | |
16 | ||
17 | fd = nil | |
18 | if(opts.has_key?(:file)) | |
19 | fd = File.open(opts[:file],"rb") | |
20 | file = opts[:file] | |
21 | else | |
22 | fd = StringIO.new(opts[:data],"rb") | |
23 | file = "<string>" | |
24 | end | |
25 | ||
26 | # first, we read the trailer: 32 byte from the end | |
27 | fd.seek(-32,IO::SEEK_END) | |
28 | buff = fd.read(32) | |
29 | ||
30 | offset_size, object_ref_size, number_of_objects, top_object, table_offset = buff.unpack "x6CCx4Nx4Nx4N" | |
31 | ||
32 | # after that, get the offset table | |
33 | fd.seek(table_offset, IO::SEEK_SET) | |
34 | coded_offset_table = fd.read(number_of_objects * offset_size) | |
35 | raise CFFormatError.new("#{file}: Format error!") unless coded_offset_table.bytesize == number_of_objects * offset_size | |
36 | ||
37 | @count_objects = number_of_objects | |
38 | ||
39 | # decode offset table | |
40 | formats = ["","C*","n*","(H6)*","N*"] | |
41 | @offsets = coded_offset_table.unpack(formats[offset_size]) | |
42 | if(offset_size == 3) | |
43 | 0.upto(@offsets.size-1) { |i| @offsets[i] = @offsets[i].to_i(16) } | |
44 | end | |
45 | ||
46 | @object_ref_size = object_ref_size | |
47 | val = read_binary_object_at(file,fd,top_object) | |
48 | ||
49 | fd.close | |
50 | val | |
51 | end | |
52 | ||
53 | ||
54 | # Convert Facter::Util::CFPropertyList to binary format; since we have to count our objects we simply unique CFDictionary and CFArray | |
55 | def to_str(opts={}) | |
56 | @unique_table = {} | |
57 | @count_objects = 0 | |
58 | @object_refs = 0 | |
59 | ||
60 | @written_object_count = 0 | |
61 | @object_table = [] | |
62 | ||
63 | @offsets = [] | |
64 | ||
65 | binary_str = "bplist00" | |
66 | ||
67 | @object_refs = count_object_refs(opts[:root]) | |
68 | ||
69 | opts[:root].to_binary(self) | |
70 | ||
71 | next_offset = 8 | |
72 | offsets = @object_table.map do |object| | |
73 | offset = next_offset | |
74 | next_offset += object.bytesize | |
75 | offset | |
76 | end | |
77 | binary_str << @object_table.join | |
78 | ||
79 | table_offset = next_offset | |
80 | offset_size = Binary.bytes_needed(table_offset) | |
81 | ||
82 | if offset_size < 8 | |
83 | # Fast path: encode the entire offset array at once. | |
84 | binary_str << offsets.pack((%w(C n N N)[offset_size - 1]) + '*') | |
85 | else | |
86 | # Slow path: host may be little or big endian, must pack each offset | |
87 | # separately. | |
88 | offsets.each do |offset| | |
89 | binary_str << "#{Binary.pack_it_with_size(offset_size,offset)}" | |
90 | end | |
91 | end | |
92 | ||
93 | binary_str << [offset_size, object_ref_size(@object_refs)].pack("x6CC") | |
94 | binary_str << [@object_table.size].pack("x4N") | |
95 | binary_str << [0].pack("x4N") | |
96 | binary_str << [table_offset].pack("x4N") | |
97 | ||
98 | binary_str | |
99 | end | |
100 | ||
101 | def object_ref_size object_refs | |
102 | Binary.bytes_needed(object_refs) | |
103 | end | |
104 | ||
105 | # read a „null” type (i.e. null byte, marker byte, bool value) | |
106 | def read_binary_null_type(length) | |
107 | case length | |
108 | when 0 then 0 # null byte | |
109 | when 8 then CFBoolean.new(false) | |
110 | when 9 then CFBoolean.new(true) | |
111 | when 15 then 15 # fill type | |
112 | else | |
113 | raise CFFormatError.new("unknown null type: #{length}") | |
114 | end | |
115 | end | |
116 | protected :read_binary_null_type | |
117 | ||
118 | # read a binary int value | |
119 | def read_binary_int(fname,fd,length) | |
120 | if length > 3 | |
121 | raise CFFormatError.new("Integer greater than 8 bytes: #{length}") | |
122 | end | |
123 | ||
124 | nbytes = 1 << length | |
125 | ||
126 | buff = fd.read(nbytes) | |
127 | ||
128 | CFInteger.new( | |
129 | case length | |
130 | when 0 then buff.unpack("C")[0] | |
131 | when 1 then buff.unpack("n")[0] | |
132 | when 2 then buff.unpack("N")[0] | |
133 | when 3 | |
134 | hiword,loword = buff.unpack("NN") | |
135 | if (hiword & 0x80000000) != 0 | |
136 | # 8 byte integers are always signed, and are negative when bit 63 is | |
137 | # set. Decoding into either a Fixnum or Bignum is tricky, however, | |
138 | # because the size of a Fixnum varies among systems, and Ruby | |
139 | # doesn't consider the number to be negative, and won't sign extend. | |
140 | -(2**63 - ((hiword & 0x7fffffff) << 32 | loword)) | |
141 | else | |
142 | hiword << 32 | loword | |
143 | end | |
144 | end | |
145 | ) | |
146 | end | |
147 | protected :read_binary_int | |
148 | ||
149 | # read a binary real value | |
150 | def read_binary_real(fname,fd,length) | |
151 | raise CFFormatError.new("Real greater than 8 bytes: #{length}") if length > 3 | |
152 | ||
153 | nbytes = 1 << length | |
154 | buff = fd.read(nbytes) | |
155 | ||
156 | CFReal.new( | |
157 | case length | |
158 | when 0 # 1 byte float? must be an error | |
159 | raise CFFormatError.new("got #{length+1} byte float, must be an error!") | |
160 | when 1 # 2 byte float? must be an error | |
161 | raise CFFormatError.new("got #{length+1} byte float, must be an error!") | |
162 | when 2 then | |
163 | buff.reverse.unpack("f")[0] | |
164 | when 3 then | |
165 | buff.reverse.unpack("d")[0] | |
166 | else | |
167 | fail "unexpected length: #{length}" | |
168 | end | |
169 | ) | |
170 | end | |
171 | protected :read_binary_real | |
172 | ||
173 | # read a binary date value | |
174 | def read_binary_date(fname,fd,length) | |
175 | raise CFFormatError.new("Date greater than 8 bytes: #{length}") if length > 3 | |
176 | ||
177 | nbytes = 1 << length | |
178 | buff = fd.read(nbytes) | |
179 | ||
180 | CFDate.new( | |
181 | case length | |
182 | when 0 then # 1 byte CFDate is an error | |
183 | raise CFFormatError.new("#{length+1} byte CFDate, error") | |
184 | when 1 then # 2 byte CFDate is an error | |
185 | raise CFFormatError.new("#{length+1} byte CFDate, error") | |
186 | when 2 then | |
187 | buff.reverse.unpack("f")[0] | |
188 | when 3 then | |
189 | buff.reverse.unpack("d")[0] | |
190 | end, | |
191 | CFDate::TIMESTAMP_APPLE | |
192 | ) | |
193 | end | |
194 | protected :read_binary_date | |
195 | ||
196 | # Read a binary data value | |
197 | def read_binary_data(fname,fd,length) | |
198 | CFData.new(read_fd(fd, length), CFData::DATA_RAW) | |
199 | end | |
200 | protected :read_binary_data | |
201 | ||
202 | def read_fd fd, length | |
203 | length > 0 ? fd.read(length) : "" | |
204 | end | |
205 | ||
206 | # Read a binary string value | |
207 | def read_binary_string(fname,fd,length) | |
208 | buff = read_fd fd, length | |
209 | @unique_table[buff] = true unless @unique_table.has_key?(buff) | |
210 | CFString.new(buff) | |
211 | end | |
212 | protected :read_binary_string | |
213 | ||
214 | # Convert the given string from one charset to another | |
215 | def Binary.charset_convert(str,from,to="UTF-8") | |
216 | return str.clone.force_encoding(from).encode(to) if str.respond_to?("encode") | |
217 | Iconv.conv(to,from,str) | |
218 | end | |
219 | ||
220 | # Count characters considering character set | |
221 | def Binary.charset_strlen(str,charset="UTF-8") | |
222 | if str.respond_to?(:encode) | |
223 | size = str.length | |
224 | else | |
225 | utf8_str = Iconv.conv("UTF-8",charset,str) | |
226 | size = utf8_str.scan(/./mu).size | |
227 | end | |
228 | ||
229 | # UTF-16 code units in the range D800-DBFF are the beginning of | |
230 | # a surrogate pair, and count as one additional character for | |
231 | # length calculation. | |
232 | if charset =~ /^UTF-16/ | |
233 | if str.respond_to?(:encode) | |
234 | str.bytes.to_a.each_slice(2) { |pair| size += 1 if (0xd8..0xdb).include?(pair[0]) } | |
235 | else | |
236 | str.split('').each_slice(2) { |pair| size += 1 if ("\xd8".."\xdb").include?(pair[0]) } | |
237 | end | |
238 | end | |
239 | ||
240 | size | |
241 | end | |
242 | ||
243 | # Read a unicode string value, coded as UTF-16BE | |
244 | def read_binary_unicode_string(fname,fd,length) | |
245 | # The problem is: we get the length of the string IN CHARACTERS; | |
246 | # since a char in UTF-16 can be 16 or 32 bit long, we don't really know | |
247 | # how long the string is in bytes | |
248 | buff = fd.read(2*length) | |
249 | ||
250 | @unique_table[buff] = true unless @unique_table.has_key?(buff) | |
251 | CFString.new(Binary.charset_convert(buff,"UTF-16BE","UTF-8")) | |
252 | end | |
253 | protected :read_binary_unicode_string | |
254 | ||
255 | # Read an binary array value, including contained objects | |
256 | def read_binary_array(fname,fd,length) | |
257 | ary = [] | |
258 | ||
259 | # first: read object refs | |
260 | if(length != 0) | |
261 | buff = fd.read(length * @object_ref_size) | |
262 | objects = buff.unpack(@object_ref_size == 1 ? "C*" : "n*") | |
263 | ||
264 | # now: read objects | |
265 | 0.upto(length-1) do |i| | |
266 | object = read_binary_object_at(fname,fd,objects[i]) | |
267 | ary.push object | |
268 | end | |
269 | end | |
270 | ||
271 | CFArray.new(ary) | |
272 | end | |
273 | protected :read_binary_array | |
274 | ||
275 | # Read a dictionary value, including contained objects | |
276 | def read_binary_dict(fname,fd,length) | |
277 | dict = {} | |
278 | ||
279 | # first: read keys | |
280 | if(length != 0) then | |
281 | buff = fd.read(length * @object_ref_size) | |
282 | keys = buff.unpack(@object_ref_size == 1 ? "C*" : "n*") | |
283 | ||
284 | # second: read object refs | |
285 | buff = fd.read(length * @object_ref_size) | |
286 | objects = buff.unpack(@object_ref_size == 1 ? "C*" : "n*") | |
287 | ||
288 | # read real keys and objects | |
289 | 0.upto(length-1) do |i| | |
290 | key = read_binary_object_at(fname,fd,keys[i]) | |
291 | object = read_binary_object_at(fname,fd,objects[i]) | |
292 | dict[key.value] = object | |
293 | end | |
294 | end | |
295 | ||
296 | CFDictionary.new(dict) | |
297 | end | |
298 | protected :read_binary_dict | |
299 | ||
300 | # Read an object type byte, decode it and delegate to the correct reader function | |
301 | def read_binary_object(fname,fd) | |
302 | # first: read the marker byte | |
303 | buff = fd.read(1) | |
304 | ||
305 | object_length = buff.unpack("C*") | |
306 | object_length = object_length[0] & 0xF | |
307 | ||
308 | buff = buff.unpack("H*") | |
309 | object_type = buff[0][0].chr | |
310 | ||
311 | if(object_type != "0" && object_length == 15) then | |
312 | object_length = read_binary_object(fname,fd) | |
313 | object_length = object_length.value | |
314 | end | |
315 | ||
316 | case object_type | |
317 | when '0' # null, false, true, fillbyte | |
318 | read_binary_null_type(object_length) | |
319 | when '1' # integer | |
320 | read_binary_int(fname,fd,object_length) | |
321 | when '2' # real | |
322 | read_binary_real(fname,fd,object_length) | |
323 | when '3' # date | |
324 | read_binary_date(fname,fd,object_length) | |
325 | when '4' # data | |
326 | read_binary_data(fname,fd,object_length) | |
327 | when '5' # byte string, usually utf8 encoded | |
328 | read_binary_string(fname,fd,object_length) | |
329 | when '6' # unicode string (utf16be) | |
330 | read_binary_unicode_string(fname,fd,object_length) | |
331 | when 'a' # array | |
332 | read_binary_array(fname,fd,object_length) | |
333 | when 'd' # dictionary | |
334 | read_binary_dict(fname,fd,object_length) | |
335 | end | |
336 | end | |
337 | protected :read_binary_object | |
338 | ||
339 | # Read an object type byte at position $pos, decode it and delegate to the correct reader function | |
340 | def read_binary_object_at(fname,fd,pos) | |
341 | position = @offsets[pos] | |
342 | fd.seek(position,IO::SEEK_SET) | |
343 | read_binary_object(fname,fd) | |
344 | end | |
345 | protected :read_binary_object_at | |
346 | ||
347 | # pack an +int+ of +nbytes+ with size | |
348 | def Binary.pack_it_with_size(nbytes,int) | |
349 | case nbytes | |
350 | when 1 then [int].pack('c') | |
351 | when 2 then [int].pack('n') | |
352 | when 4 then [int].pack('N') | |
353 | when 8 | |
354 | [int >> 32, int & 0xFFFFFFFF].pack('NN') | |
355 | else | |
356 | raise CFFormatError.new("Don't know how to pack #{nbytes} byte integer") | |
357 | end | |
358 | end | |
359 | ||
360 | def Binary.pack_int_array_with_size(nbytes, array) | |
361 | case nbytes | |
362 | when 1 then array.pack('C*') | |
363 | when 2 then array.pack('n*') | |
364 | when 4 then array.pack('N*') | |
365 | when 8 | |
366 | array.map { |int| [int >> 32, int & 0xFFFFFFFF].pack('NN') }.join | |
367 | else | |
368 | raise CFFormatError.new("Don't know how to pack #{nbytes} byte integer") | |
369 | end | |
370 | end | |
371 | ||
372 | # calculate how many bytes are needed to save +count+ | |
373 | def Binary.bytes_needed(count) | |
374 | case | |
375 | when count < 2**8 then 1 | |
376 | when count < 2**16 then 2 | |
377 | when count < 2**32 then 4 | |
378 | when count < 2**64 then 8 | |
379 | else | |
380 | raise CFFormatError.new("Data size too large: #{count}") | |
381 | end | |
382 | end | |
383 | ||
384 | # Create a type byte for binary format as defined by apple | |
385 | def Binary.type_bytes(type, length) | |
386 | if length < 15 | |
387 | [(type << 4) | length].pack('C') | |
388 | else | |
389 | bytes = [(type << 4) | 0xF] | |
390 | if length <= 0xFF | |
391 | bytes.push(0x10, length).pack('CCC') # 1 byte length | |
392 | elsif length <= 0xFFFF | |
393 | bytes.push(0x11, length).pack('CCn') # 2 byte length | |
394 | elsif length <= 0xFFFFFFFF | |
395 | bytes.push(0x12, length).pack('CCN') # 4 byte length | |
396 | elsif length <= 0x7FFFFFFFFFFFFFFF | |
397 | bytes.push(0x13, length >> 32, length & 0xFFFFFFFF).pack('CCNN') # 8 byte length | |
398 | else | |
399 | raise CFFormatError.new("Integer too large: #{int}") | |
400 | end | |
401 | end | |
402 | end | |
403 | ||
404 | def count_object_refs(object) | |
405 | case object | |
406 | when CFArray | |
407 | contained_refs = 0 | |
408 | object.value.each do |element| | |
409 | if CFArray === element || CFDictionary === element | |
410 | contained_refs += count_object_refs(element) | |
411 | end | |
412 | end | |
413 | return object.value.size + contained_refs | |
414 | when CFDictionary | |
415 | contained_refs = 0 | |
416 | object.value.each_value do |value| | |
417 | if CFArray === value || CFDictionary === value | |
418 | contained_refs += count_object_refs(value) | |
419 | end | |
420 | end | |
421 | return object.value.keys.size * 2 + contained_refs | |
422 | else | |
423 | return 0 | |
424 | end | |
425 | end | |
426 | ||
427 | def Binary.ascii_string?(str) | |
428 | if str.respond_to?(:ascii_only?) | |
429 | str.ascii_only? | |
430 | else | |
431 | str !~ /[\x80-\xFF]/mn | |
432 | end | |
433 | end | |
434 | ||
435 | # Uniques and transforms a string value to binary format and adds it to the object table | |
436 | def string_to_binary(val) | |
437 | val = val.to_s | |
438 | ||
439 | @unique_table[val] ||= begin | |
440 | if !Binary.ascii_string?(val) | |
441 | utf8_strlen = Binary.charset_strlen(val, "UTF-8") | |
442 | val = Binary.charset_convert(val,"UTF-8","UTF-16BE") | |
443 | bdata = Binary.type_bytes(0b0110, Binary.charset_strlen(val,"UTF-16BE")) | |
444 | ||
445 | val.force_encoding("ASCII-8BIT") if val.respond_to?("encode") | |
446 | @object_table[@written_object_count] = bdata << val | |
447 | else | |
448 | utf8_strlen = val.bytesize | |
449 | bdata = Binary.type_bytes(0b0101,val.bytesize) | |
450 | @object_table[@written_object_count] = bdata << val | |
451 | end | |
452 | @written_object_count += 1 | |
453 | @written_object_count - 1 | |
454 | end | |
455 | end | |
456 | ||
457 | # Codes an integer to binary format | |
458 | def int_to_binary(value) | |
459 | nbytes = 0 | |
460 | nbytes = 1 if value > 0xFF # 1 byte integer | |
461 | nbytes += 1 if value > 0xFFFF # 4 byte integer | |
462 | nbytes += 1 if value > 0xFFFFFFFF # 8 byte integer | |
463 | nbytes = 3 if value < 0 # 8 byte integer, since signed | |
464 | ||
465 | Binary.type_bytes(0b0001, nbytes) << | |
466 | if nbytes < 3 | |
467 | [value].pack( | |
468 | if nbytes == 0 then "C" | |
469 | elsif nbytes == 1 then "n" | |
470 | else "N" | |
471 | end | |
472 | ) | |
473 | else | |
474 | # 64 bit signed integer; we need the higher and the lower 32 bit of the value | |
475 | high_word = value >> 32 | |
476 | low_word = value & 0xFFFFFFFF | |
477 | [high_word,low_word].pack("NN") | |
478 | end | |
479 | end | |
480 | ||
481 | # Codes a real value to binary format | |
482 | def real_to_binary(val) | |
483 | Binary.type_bytes(0b0010,3) << [val].pack("d").reverse | |
484 | end | |
485 | ||
486 | # Converts a numeric value to binary and adds it to the object table | |
487 | def num_to_binary(value) | |
488 | @object_table[@written_object_count] = | |
489 | if value.is_a?(CFInteger) | |
490 | int_to_binary(value.value) | |
491 | else | |
492 | real_to_binary(value.value) | |
493 | end | |
494 | ||
495 | @written_object_count += 1 | |
496 | @written_object_count - 1 | |
497 | end | |
498 | ||
499 | # Convert date value (apple format) to binary and adds it to the object table | |
500 | def date_to_binary(val) | |
501 | val = val.getutc.to_f - CFDate::DATE_DIFF_APPLE_UNIX # CFDate is a real, number of seconds since 01/01/2001 00:00:00 GMT | |
502 | ||
503 | @object_table[@written_object_count] = | |
504 | (Binary.type_bytes(0b0011, 3) << [val].pack("d").reverse) | |
505 | ||
506 | @written_object_count += 1 | |
507 | @written_object_count - 1 | |
508 | end | |
509 | ||
510 | # Convert a bool value to binary and add it to the object table | |
511 | def bool_to_binary(val) | |
512 | ||
513 | @object_table[@written_object_count] = val ? "\x9" : "\x8" # 0x9 is 1001, type indicator for true; 0x8 is 1000, type indicator for false | |
514 | @written_object_count += 1 | |
515 | @written_object_count - 1 | |
516 | end | |
517 | ||
518 | # Convert data value to binary format and add it to the object table | |
519 | def data_to_binary(val) | |
520 | @object_table[@written_object_count] = | |
521 | (Binary.type_bytes(0b0100, val.bytesize) << val) | |
522 | ||
523 | @written_object_count += 1 | |
524 | @written_object_count - 1 | |
525 | end | |
526 | ||
527 | # Convert array to binary format and add it to the object table | |
528 | def array_to_binary(val) | |
529 | saved_object_count = @written_object_count | |
530 | @written_object_count += 1 | |
531 | #@object_refs += val.value.size | |
532 | ||
533 | values = val.value.map { |v| v.to_binary(self) } | |
534 | bdata = Binary.type_bytes(0b1010, val.value.size) << | |
535 | Binary.pack_int_array_with_size(object_ref_size(@object_refs), | |
536 | values) | |
537 | ||
538 | @object_table[saved_object_count] = bdata | |
539 | saved_object_count | |
540 | end | |
541 | ||
542 | # Convert dictionary to binary format and add it to the object table | |
543 | def dict_to_binary(val) | |
544 | saved_object_count = @written_object_count | |
545 | @written_object_count += 1 | |
546 | ||
547 | #@object_refs += val.value.keys.size * 2 | |
548 | ||
549 | keys_and_values = val.value.keys.map { |k| CFString.new(k).to_binary(self) } | |
550 | keys_and_values.concat(val.value.values.map { |v| v.to_binary(self) }) | |
551 | ||
552 | bdata = Binary.type_bytes(0b1101,val.value.size) << | |
553 | Binary.pack_int_array_with_size(object_ref_size(@object_refs), keys_and_values) | |
554 | ||
555 | @object_table[saved_object_count] = bdata | |
556 | return saved_object_count | |
557 | end | |
558 | end | |
559 | end | |
560 | ||
561 | # eof |
0 | # -*- coding: utf-8 -*- | |
1 | # | |
2 | # Exceptions used: | |
3 | # CFPlistError:: General base exception | |
4 | # CFFormatError:: Format error | |
5 | # CFTypeError:: Type error | |
6 | # | |
7 | # Easy and simple :-) | |
8 | # | |
9 | # Author:: Christian Kruse (mailto:cjk@wwwtech.de) | |
10 | # Copyright:: Copyright (c) 2010 | |
11 | # License:: MIT License | |
12 | ||
13 | # general plist error. All exceptions thrown are derived from this class. | |
14 | class CFPlistError < Exception | |
15 | end | |
16 | ||
17 | # Exception thrown when format errors occur | |
18 | class CFFormatError < CFPlistError | |
19 | end | |
20 | ||
21 | # Exception thrown when type errors occur | |
22 | class CFTypeError < CFPlistError | |
23 | end | |
24 | ||
25 | # eof |
0 | # -*- coding: utf-8 -*- | |
1 | ||
2 | require 'kconv' | |
3 | require 'date' | |
4 | require 'time' | |
5 | ||
6 | # | |
7 | # Facter::Util::CFPropertyList implementation | |
8 | # | |
9 | # class to read, manipulate and write both XML and binary property list | |
10 | # files (plist(5)) as defined by Apple. Have a look at Facter::Util::CFPropertyList::List | |
11 | # for more documentation. | |
12 | # | |
13 | # == Example | |
14 | # require 'cfpropertylist' | |
15 | # | |
16 | # # create a arbitrary data structure of basic data types | |
17 | # data = { | |
18 | # 'name' => 'John Doe', | |
19 | # 'missing' => true, | |
20 | # 'last_seen' => Time.now, | |
21 | # 'friends' => ['Jane Doe','Julian Doe'], | |
22 | # 'likes' => { | |
23 | # 'me' => false | |
24 | # } | |
25 | # } | |
26 | # | |
27 | # # create Facter::Util::CFPropertyList::List object | |
28 | # plist = Facter::Util::CFPropertyList::List.new | |
29 | # | |
30 | # # call Facter::Util::CFPropertyList.guess() to create corresponding CFType values | |
31 | # # pass in optional :convert_unknown_to_string => true to convert things like symbols into strings. | |
32 | # plist.value = Facter::Util::CFPropertyList.guess(data) | |
33 | # | |
34 | # # write plist to file | |
35 | # plist.save("example.plist", Facter::Util::CFPropertyList::List::FORMAT_BINARY) | |
36 | # | |
37 | # # … later, read it again | |
38 | # plist = Facter::Util::CFPropertyList::List.new(:file => "example.plist") | |
39 | # data = Facter::Util::CFPropertyList.native_types(plist.value) | |
40 | # | |
41 | # Author:: Christian Kruse (mailto:cjk@wwwtech.de) | |
42 | # Copyright:: Copyright (c) 2010 | |
43 | # License:: MIT License | |
44 | module Facter::Util::CFPropertyList | |
45 | # interface class for PList parsers | |
46 | class ParserInterface | |
47 | # load a plist | |
48 | def load(opts={}) | |
49 | return "" | |
50 | end | |
51 | ||
52 | # convert a plist to string | |
53 | def to_str(opts={}) | |
54 | return true | |
55 | end | |
56 | end | |
57 | ||
58 | class XMLParserInterface < ParserInterface | |
59 | def new_node(name) | |
60 | end | |
61 | ||
62 | def new_text(val) | |
63 | end | |
64 | ||
65 | def append_node(parent, child) | |
66 | end | |
67 | end | |
68 | end | |
69 | ||
70 | class String | |
71 | unless("".respond_to?(:blob) && "".respond_to?(:blob=)) then | |
72 | # The blob status of this string (to set to true if a binary string) | |
73 | attr_accessor :blob | |
74 | end | |
75 | ||
76 | unless("".respond_to?(:blob?)) then | |
77 | # Returns whether or not +str+ is a blob. | |
78 | # @return [true,false] If true, this string contains binary data. If false, its a regular string | |
79 | def blob? | |
80 | blob | |
81 | end | |
82 | end | |
83 | ||
84 | unless("".respond_to?(:bytesize)) then | |
85 | def bytesize | |
86 | self.length | |
87 | end | |
88 | end | |
89 | end | |
90 | ||
91 | dirname = File.dirname(__FILE__) | |
92 | require dirname + '/rbCFPlistError.rb' | |
93 | require dirname + '/rbCFTypes.rb' | |
94 | require dirname + '/rbBinaryCFPropertyList.rb' | |
95 | ||
96 | require 'iconv' unless "".respond_to?("encode") | |
97 | ||
98 | begin | |
99 | Enumerable::Enumerator.new([]) | |
100 | rescue NameError => e | |
101 | module Enumerable | |
102 | class Enumerator | |
103 | end | |
104 | end | |
105 | end | |
106 | ||
107 | begin | |
108 | require dirname + '/rbLibXMLParser.rb' | |
109 | try_nokogiri = false | |
110 | rescue LoadError => e | |
111 | try_nokogiri = true | |
112 | end | |
113 | ||
114 | if try_nokogiri then | |
115 | begin | |
116 | require dirname + '/rbNokogiriParser.rb' | |
117 | rescue LoadError => e | |
118 | require dirname + '/rbREXMLParser.rb' | |
119 | end | |
120 | end | |
121 | ||
122 | ||
123 | module Facter::Util::CFPropertyList | |
124 | # Create CFType hierarchy by guessing the correct CFType, e.g. | |
125 | # | |
126 | # x = { | |
127 | # 'a' => ['b','c','d'] | |
128 | # } | |
129 | # cftypes = Facter::Util::CFPropertyList.guess(x) | |
130 | # | |
131 | # pass optional options hash. Only possible value actually: | |
132 | # +convert_unknown_to_string+:: Convert unknown objects to string calling to_str() | |
133 | # +converter_method+:: Convert unknown objects to known objects calling +method_name+ | |
134 | # | |
135 | # cftypes = Facter::Util::CFPropertyList.guess(x,:convert_unknown_to_string => true,:converter_method => :to_hash, :converter_with_opts => true) | |
136 | def guess(object, options = {}) | |
137 | case object | |
138 | when Fixnum, Integer then CFInteger.new(object) | |
139 | when Float then CFReal.new(object) | |
140 | when TrueClass, FalseClass then CFBoolean.new(object) | |
141 | ||
142 | when String | |
143 | object.blob? ? CFData.new(object, CFData::DATA_RAW) : CFString.new(object) | |
144 | ||
145 | when Time, DateTime, Date then CFDate.new(object) | |
146 | ||
147 | when Array, Enumerator, Enumerable::Enumerator | |
148 | ary = Array.new | |
149 | object.each do |o| | |
150 | ary.push Facter::Util::CFPropertyList.guess(o, options) | |
151 | end | |
152 | CFArray.new(ary) | |
153 | ||
154 | when Hash | |
155 | hsh = Hash.new | |
156 | object.each_pair do |k,v| | |
157 | k = k.to_s if k.is_a?(Symbol) | |
158 | hsh[k] = Facter::Util::CFPropertyList.guess(v, options) | |
159 | end | |
160 | CFDictionary.new(hsh) | |
161 | else | |
162 | case | |
163 | when Object.const_defined?('BigDecimal') && object.is_a?(BigDecimal) | |
164 | CFReal.new(object) | |
165 | when object.respond_to?(:read) | |
166 | CFData.new(object.read(), CFData::DATA_RAW) | |
167 | when options[:converter_method] && object.respond_to?(options[:converter_method]) | |
168 | if options[:converter_with_opts] | |
169 | Facter::Util::CFPropertyList.guess(object.send(options[:converter_method],options),options) | |
170 | else | |
171 | Facter::Util::CFPropertyList.guess(object.send(options[:converter_method]),options) | |
172 | end | |
173 | when options[:convert_unknown_to_string] | |
174 | CFString.new(object.to_s) | |
175 | else | |
176 | raise CFTypeError.new("Unknown class #{object.class.to_s}. Try using :convert_unknown_to_string if you want to use unknown object types!") | |
177 | end | |
178 | end | |
179 | end | |
180 | ||
181 | # Converts a CFType hiercharchy to native Ruby types | |
182 | def native_types(object,keys_as_symbols=false) | |
183 | return if object.nil? | |
184 | ||
185 | if(object.is_a?(CFDate) || object.is_a?(CFString) || object.is_a?(CFInteger) || object.is_a?(CFReal) || object.is_a?(CFBoolean)) then | |
186 | return object.value | |
187 | elsif(object.is_a?(CFData)) then | |
188 | return object.decoded_value | |
189 | elsif(object.is_a?(CFArray)) then | |
190 | ary = [] | |
191 | object.value.each do | |
192 | |v| | |
193 | ary.push Facter::Util::CFPropertyList.native_types(v) | |
194 | end | |
195 | ||
196 | return ary | |
197 | elsif(object.is_a?(CFDictionary)) then | |
198 | hsh = {} | |
199 | object.value.each_pair do | |
200 | |k,v| | |
201 | k = k.to_sym if keys_as_symbols | |
202 | hsh[k] = Facter::Util::CFPropertyList.native_types(v) | |
203 | end | |
204 | ||
205 | return hsh | |
206 | end | |
207 | end | |
208 | ||
209 | module_function :guess, :native_types | |
210 | ||
211 | # Class representing a Facter::Util::CFPropertyList. Instanciate with #new | |
212 | class List | |
213 | # Format constant for binary format | |
214 | FORMAT_BINARY = 1 | |
215 | ||
216 | # Format constant for XML format | |
217 | FORMAT_XML = 2 | |
218 | ||
219 | # Format constant for automatic format recognizing | |
220 | FORMAT_AUTO = 0 | |
221 | ||
222 | @@parsers = [Binary,XML] | |
223 | ||
224 | # Path of PropertyList | |
225 | attr_accessor :filename | |
226 | # Path of PropertyList | |
227 | attr_accessor :format | |
228 | # the root value in the plist file | |
229 | attr_accessor :value | |
230 | ||
231 | # initialize a new Facter::Util::CFPropertyList, arguments are: | |
232 | # | |
233 | # :file:: Parse a file | |
234 | # :format:: Format is one of FORMAT_BINARY or FORMAT_XML. Defaults to FORMAT_AUTO | |
235 | # :data:: Parse a string | |
236 | # | |
237 | # All arguments are optional | |
238 | def initialize(opts={}) | |
239 | @filename = opts[:file] | |
240 | @format = opts[:format] || FORMAT_AUTO | |
241 | @data = opts[:data] | |
242 | ||
243 | load(@filename) unless @filename.nil? | |
244 | load_str(@data) unless @data.nil? | |
245 | end | |
246 | ||
247 | # Load an XML PropertyList | |
248 | # filename = nil:: The filename to read from; if nil, read from the file defined by instance variable +filename+ | |
249 | def load_xml(filename=nil) | |
250 | load(filename,List::FORMAT_XML) | |
251 | end | |
252 | ||
253 | # read a binary plist file | |
254 | # filename = nil:: The filename to read from; if nil, read from the file defined by instance variable +filename+ | |
255 | def load_binary(filename=nil) | |
256 | load(filename,List::FORMAT_BINARY) | |
257 | end | |
258 | ||
259 | # load a plist from a XML string | |
260 | # str:: The string containing the plist | |
261 | def load_xml_str(str=nil) | |
262 | load_str(str,List::FORMAT_XML) | |
263 | end | |
264 | ||
265 | # load a plist from a binary string | |
266 | # str:: The string containing the plist | |
267 | def load_binary_str(str=nil) | |
268 | load_str(str,List::FORMAT_BINARY) | |
269 | end | |
270 | ||
271 | # load a plist from a string | |
272 | # str = nil:: The string containing the plist | |
273 | # format = nil:: The format of the plist | |
274 | def load_str(str=nil,format=nil) | |
275 | str = @data if str.nil? | |
276 | format = @format if format.nil? | |
277 | ||
278 | @value = {} | |
279 | case format | |
280 | when List::FORMAT_BINARY, List::FORMAT_XML then | |
281 | prsr = @@parsers[format-1].new | |
282 | @value = prsr.load({:data => str}) | |
283 | ||
284 | when List::FORMAT_AUTO then # what we now do is ugly, but neccessary to recognize the file format | |
285 | filetype = str[0..5] | |
286 | version = str[6..7] | |
287 | ||
288 | prsr = nil | |
289 | if filetype == "bplist" then | |
290 | raise CFFormatError.new("Wong file version #{version}") unless version == "00" | |
291 | prsr = Binary.new | |
292 | else | |
293 | prsr = XML.new | |
294 | end | |
295 | ||
296 | @value = prsr.load({:data => str}) | |
297 | end | |
298 | end | |
299 | ||
300 | # Read a plist file | |
301 | # file = nil:: The filename of the file to read. If nil, use +filename+ instance variable | |
302 | # format = nil:: The format of the plist file. Auto-detect if nil | |
303 | def load(file=nil,format=nil) | |
304 | file = @filename if file.nil? | |
305 | format = @format if format.nil? | |
306 | @value = {} | |
307 | ||
308 | raise IOError.new("File #{file} not readable!") unless File.readable? file | |
309 | ||
310 | case format | |
311 | when List::FORMAT_BINARY, List::FORMAT_XML then | |
312 | prsr = @@parsers[format-1].new | |
313 | @value = prsr.load({:file => file}) | |
314 | ||
315 | when List::FORMAT_AUTO then # what we now do is ugly, but neccessary to recognize the file format | |
316 | magic_number = IO.read(file,8) | |
317 | filetype = magic_number[0..5] | |
318 | version = magic_number[6..7] | |
319 | ||
320 | prsr = nil | |
321 | if filetype == "bplist" then | |
322 | raise CFFormatError.new("Wong file version #{version}") unless version == "00" | |
323 | prsr = Binary.new | |
324 | else | |
325 | prsr = XML.new | |
326 | end | |
327 | ||
328 | @value = prsr.load({:file => file}) | |
329 | end | |
330 | end | |
331 | ||
332 | # Serialize Facter::Util::CFPropertyList object to specified format and write it to file | |
333 | # file = nil:: The filename of the file to write to. Uses +filename+ instance variable if nil | |
334 | # format = nil:: The format to save in. Uses +format+ instance variable if nil | |
335 | def save(file=nil,format=nil,opts={}) | |
336 | format = @format if format.nil? | |
337 | file = @filename if file.nil? | |
338 | ||
339 | raise CFFormatError.new("Format #{format} not supported, use List::FORMAT_BINARY or List::FORMAT_XML") if format != FORMAT_BINARY && format != FORMAT_XML | |
340 | ||
341 | if(!File.exists?(file)) then | |
342 | raise IOError.new("File #{file} not writable!") unless File.writable?(File.dirname(file)) | |
343 | elsif(!File.writable?(file)) then | |
344 | raise IOError.new("File #{file} not writable!") | |
345 | end | |
346 | ||
347 | opts[:root] = @value | |
348 | prsr = @@parsers[format-1].new | |
349 | content = prsr.to_str(opts) | |
350 | ||
351 | File.open(file, 'wb') { | |
352 | |fd| | |
353 | fd.write content | |
354 | } | |
355 | end | |
356 | ||
357 | # convert plist to string | |
358 | # format = List::FORMAT_BINARY:: The format to save the plist | |
359 | # opts={}:: Pass parser options | |
360 | def to_str(format=List::FORMAT_BINARY,opts={}) | |
361 | prsr = @@parsers[format-1].new | |
362 | opts[:root] = @value | |
363 | return prsr.to_str(opts) | |
364 | end | |
365 | end | |
366 | end | |
367 | ||
368 | class Array | |
369 | # convert an array to plist format | |
370 | def to_plist(options={}) | |
371 | options[:plist_format] ||= Facter::Util::CFPropertyList::List::FORMAT_BINARY | |
372 | ||
373 | plist = Facter::Util::CFPropertyList::List.new | |
374 | plist.value = Facter::Util::CFPropertyList.guess(self, options) | |
375 | plist.to_str(options[:plist_format]) | |
376 | end | |
377 | end | |
378 | ||
379 | class Enumerator | |
380 | # convert an array to plist format | |
381 | def to_plist(options={}) | |
382 | options[:plist_format] ||= Facter::Util::CFPropertyList::List::FORMAT_BINARY | |
383 | ||
384 | plist = Facter::Util::CFPropertyList::List.new | |
385 | plist.value = Facter::Util::CFPropertyList.guess(self, options) | |
386 | plist.to_str(options[:plist_format]) | |
387 | end | |
388 | end | |
389 | ||
390 | class Hash | |
391 | # convert a hash to plist format | |
392 | def to_plist(options={}) | |
393 | options[:plist_format] ||= Facter::Util::CFPropertyList::List::FORMAT_BINARY | |
394 | ||
395 | plist = Facter::Util::CFPropertyList::List.new | |
396 | plist.value = Facter::Util::CFPropertyList.guess(self, options) | |
397 | plist.to_str(options[:plist_format]) | |
398 | end | |
399 | end | |
400 | ||
401 | # eof |
0 | # -*- coding: utf-8 -*- | |
1 | # | |
2 | # CFTypes, e.g. CFString, CFInteger | |
3 | # needed to create unambiguous plists | |
4 | # | |
5 | # Author:: Christian Kruse (mailto:cjk@wwwtech.de) | |
6 | # Copyright:: Copyright (c) 2009 | |
7 | # License:: MIT License | |
8 | ||
9 | require 'base64' | |
10 | ||
11 | module Facter::Util::CFPropertyList | |
12 | # This class defines the base class for all CFType classes | |
13 | # | |
14 | class CFType | |
15 | # value of the type | |
16 | attr_accessor :value | |
17 | ||
18 | def initialize(value=nil) | |
19 | @value = value | |
20 | end | |
21 | ||
22 | def to_xml(parser) | |
23 | end | |
24 | ||
25 | def to_binary(bplist) end | |
26 | end | |
27 | ||
28 | # This class holds string values, both, UTF-8 and UTF-16BE | |
29 | # It will convert the value to UTF-16BE if necessary (i.e. if non-ascii char contained) | |
30 | class CFString < CFType | |
31 | # convert to XML | |
32 | def to_xml(parser) | |
33 | n = parser.new_node('string') | |
34 | n = parser.append_node(n, parser.new_text(@value)) unless @value.nil? | |
35 | n | |
36 | end | |
37 | ||
38 | # convert to binary | |
39 | def to_binary(bplist) | |
40 | bplist.string_to_binary(@value); | |
41 | end | |
42 | end | |
43 | ||
44 | # This class holds integer/fixnum values | |
45 | class CFInteger < CFType | |
46 | # convert to XML | |
47 | def to_xml(parser) | |
48 | n = parser.new_node('integer') | |
49 | n = parser.append_node(n, parser.new_text(@value.to_s)) | |
50 | n | |
51 | end | |
52 | ||
53 | # convert to binary | |
54 | def to_binary(bplist) | |
55 | bplist.num_to_binary(self) | |
56 | end | |
57 | end | |
58 | ||
59 | # This class holds float values | |
60 | class CFReal < CFType | |
61 | # convert to XML | |
62 | def to_xml(parser) | |
63 | n = parser.new_node('real') | |
64 | n = parser.append_node(n, parser.new_text(@value.to_s)) | |
65 | n | |
66 | end | |
67 | ||
68 | # convert to binary | |
69 | def to_binary(bplist) | |
70 | bplist.num_to_binary(self) | |
71 | end | |
72 | end | |
73 | ||
74 | # This class holds Time values. While Apple uses seconds since 2001, | |
75 | # the rest of the world uses seconds since 1970. So if you access value | |
76 | # directly, you get the Time class. If you access via get_value you either | |
77 | # geht the timestamp or the Apple timestamp | |
78 | class CFDate < CFType | |
79 | TIMESTAMP_APPLE = 0 | |
80 | TIMESTAMP_UNIX = 1; | |
81 | DATE_DIFF_APPLE_UNIX = 978307200 | |
82 | ||
83 | # create a XML date strimg from a time object | |
84 | def CFDate.date_string(val) | |
85 | # 2009-05-13T20:23:43Z | |
86 | val.getutc.strftime("%Y-%m-%dT%H:%M:%SZ") | |
87 | end | |
88 | ||
89 | # parse a XML date string | |
90 | def CFDate.parse_date(val) | |
91 | # 2009-05-13T20:23:43Z | |
92 | val =~ %r{^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$} | |
93 | year,month,day,hour,min,sec = $1, $2, $3, $4, $5, $6 | |
94 | return Time.utc(year,month,day,hour,min,sec).getlocal | |
95 | end | |
96 | ||
97 | # set value to defined state | |
98 | def initialize(value = nil,format=CFDate::TIMESTAMP_UNIX) | |
99 | if(value.is_a?(Time) || value.nil?) then | |
100 | @value = value.nil? ? Time.now : value | |
101 | elsif value.instance_of? Date | |
102 | @value = Time.utc(value.year, value.month, value.day, 0, 0, 0) | |
103 | elsif value.instance_of? DateTime | |
104 | @value = value.to_time.utc | |
105 | else | |
106 | set_value(value,format) | |
107 | end | |
108 | end | |
109 | ||
110 | # set value with timestamp, either Apple or UNIX | |
111 | def set_value(value,format=CFDate::TIMESTAMP_UNIX) | |
112 | if(format == CFDate::TIMESTAMP_UNIX) then | |
113 | @value = Time.at(value) | |
114 | else | |
115 | @value = Time.at(value + CFDate::DATE_DIFF_APPLE_UNIX) | |
116 | end | |
117 | end | |
118 | ||
119 | # get timestamp, either UNIX or Apple timestamp | |
120 | def get_value(format=CFDate::TIMESTAMP_UNIX) | |
121 | if(format == CFDate::TIMESTAMP_UNIX) then | |
122 | @value.to_i | |
123 | else | |
124 | @value.to_f - CFDate::DATE_DIFF_APPLE_UNIX | |
125 | end | |
126 | end | |
127 | ||
128 | # convert to XML | |
129 | def to_xml(parser) | |
130 | n = parser.new_node('date') | |
131 | n = parser.append_node(n, parser.new_text(CFDate::date_string(@value))) | |
132 | n | |
133 | end | |
134 | ||
135 | # convert to binary | |
136 | def to_binary(bplist) | |
137 | bplist.date_to_binary(@value) | |
138 | end | |
139 | end | |
140 | ||
141 | # This class contains a boolean value | |
142 | class CFBoolean < CFType | |
143 | # convert to XML | |
144 | def to_xml(parser) | |
145 | parser.new_node(@value ? 'true' : 'false') | |
146 | end | |
147 | ||
148 | # convert to binary | |
149 | def to_binary(bplist) | |
150 | bplist.bool_to_binary(@value); | |
151 | end | |
152 | end | |
153 | ||
154 | # This class contains binary data values | |
155 | class CFData < CFType | |
156 | # Base64 encoded data | |
157 | DATA_BASE64 = 0 | |
158 | # Raw data | |
159 | DATA_RAW = 1 | |
160 | ||
161 | # set value to defined state, either base64 encoded or raw | |
162 | def initialize(value=nil,format=DATA_BASE64) | |
163 | if(format == DATA_RAW) | |
164 | @raw_value = value | |
165 | @raw_value.blob = true | |
166 | else | |
167 | @value = value | |
168 | end | |
169 | end | |
170 | ||
171 | # get base64 encoded value | |
172 | def encoded_value | |
173 | @value ||= "\n#{Base64.encode64(@raw_value).gsub("\n", '').scan(/.{1,76}/).join("\n")}\n" | |
174 | end | |
175 | ||
176 | # get base64 decoded value | |
177 | def decoded_value | |
178 | @raw_value ||= String.new(Base64.decode64(@value)) | |
179 | @raw_value.blob = true | |
180 | @raw_value | |
181 | end | |
182 | ||
183 | # convert to XML | |
184 | def to_xml(parser) | |
185 | n = parser.new_node('data') | |
186 | n = parser.append_node(n, parser.new_text(encoded_value())) | |
187 | n | |
188 | end | |
189 | ||
190 | # convert to binary | |
191 | def to_binary(bplist) | |
192 | bplist.data_to_binary(decoded_value()) | |
193 | end | |
194 | end | |
195 | ||
196 | # This class contains an array of values | |
197 | class CFArray < CFType | |
198 | # create a new array CFType | |
199 | def initialize(val=[]) | |
200 | @value = val | |
201 | end | |
202 | ||
203 | # convert to XML | |
204 | def to_xml(parser) | |
205 | n = parser.new_node('array') | |
206 | @value.each do |v| | |
207 | n = parser.append_node(n, v.to_xml(parser)) | |
208 | end | |
209 | n | |
210 | end | |
211 | ||
212 | # convert to binary | |
213 | def to_binary(bplist) | |
214 | bplist.array_to_binary(self) | |
215 | end | |
216 | end | |
217 | ||
218 | # this class contains a hash of values | |
219 | class CFDictionary < CFType | |
220 | # Create new CFDictonary type. | |
221 | def initialize(value={}) | |
222 | @value = value | |
223 | end | |
224 | ||
225 | # convert to XML | |
226 | def to_xml(parser) | |
227 | n = parser.new_node('dict') | |
228 | @value.each_pair do |key, value| | |
229 | k = parser.append_node(parser.new_node('key'), parser.new_text(key.to_s)) | |
230 | n = parser.append_node(n, k) | |
231 | n = parser.append_node(n, value.to_xml(parser)) | |
232 | end | |
233 | n | |
234 | end | |
235 | ||
236 | # convert to binary | |
237 | def to_binary(bplist) | |
238 | bplist.dict_to_binary(self) | |
239 | end | |
240 | end | |
241 | end | |
242 | ||
243 | # eof |
0 | # -*- coding: utf-8 -*- | |
1 | ||
2 | require 'libxml' | |
3 | ||
4 | module Facter::Util::CFPropertyList | |
5 | # XML parser | |
6 | class XML < XMLParserInterface | |
7 | # read a XML file | |
8 | # opts:: | |
9 | # * :file - The filename of the file to load | |
10 | # * :data - The data to parse | |
11 | def load(opts) | |
12 | if(opts.has_key?(:file)) then | |
13 | doc = LibXML::XML::Document.file(opts[:file],:options => LibXML::XML::Parser::Options::NOBLANKS|LibXML::XML::Parser::Options::NOENT) | |
14 | else | |
15 | doc = LibXML::XML::Document.string(opts[:data],:options => LibXML::XML::Parser::Options::NOBLANKS|LibXML::XML::Parser::Options::NOENT) | |
16 | end | |
17 | ||
18 | root = doc.root.first | |
19 | return import_xml(root) | |
20 | end | |
21 | ||
22 | # serialize Facter::Util::CFPropertyList object to XML | |
23 | # opts = {}:: Specify options: :formatted - Use indention and line breaks | |
24 | def to_str(opts={}) | |
25 | doc = LibXML::XML::Document.new | |
26 | ||
27 | doc.root = LibXML::XML::Node.new('plist') | |
28 | doc.encoding = LibXML::XML::Encoding::UTF_8 | |
29 | ||
30 | doc.root['version'] = '1.0' | |
31 | doc.root << opts[:root].to_xml(self) | |
32 | ||
33 | # ugly hack, but there's no other possibility I know | |
34 | str = doc.to_s(:indent => opts[:formatted]) | |
35 | str1 = String.new | |
36 | first = false | |
37 | str.each_line do |line| | |
38 | str1 << line | |
39 | unless(first) then | |
40 | str1 << "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n" if line =~ /^\s*<\?xml/ | |
41 | end | |
42 | ||
43 | first = true | |
44 | end | |
45 | ||
46 | str1.force_encoding('UTF-8') if str1.respond_to?(:force_encoding) | |
47 | return str1 | |
48 | end | |
49 | ||
50 | def new_node(name) | |
51 | LibXML::XML::Node.new(name) | |
52 | end | |
53 | ||
54 | def new_text(val) | |
55 | LibXML::XML::Node.new_text(val) | |
56 | end | |
57 | ||
58 | def append_node(parent, child) | |
59 | parent << child | |
60 | end | |
61 | ||
62 | protected | |
63 | ||
64 | # get the value of a DOM node | |
65 | def get_value(n) | |
66 | content = if n.children? | |
67 | n.first.content | |
68 | else | |
69 | n.content | |
70 | end | |
71 | ||
72 | content.force_encoding('UTF-8') if content.respond_to?(:force_encoding) | |
73 | content | |
74 | end | |
75 | ||
76 | # import the XML values | |
77 | def import_xml(node) | |
78 | ret = nil | |
79 | ||
80 | case node.name | |
81 | when 'dict' | |
82 | hsh = Hash.new | |
83 | key = nil | |
84 | ||
85 | if node.children? then | |
86 | node.children.each do |n| | |
87 | next if n.text? # avoid a bug of libxml | |
88 | next if n.comment? | |
89 | ||
90 | if n.name == "key" then | |
91 | key = get_value(n) | |
92 | else | |
93 | raise CFFormatError.new("Format error!") if key.nil? | |
94 | hsh[key] = import_xml(n) | |
95 | key = nil | |
96 | end | |
97 | end | |
98 | end | |
99 | ||
100 | ret = CFDictionary.new(hsh) | |
101 | ||
102 | when 'array' | |
103 | ary = Array.new | |
104 | ||
105 | if node.children? then | |
106 | node.children.each do |n| | |
107 | ary.push import_xml(n) | |
108 | end | |
109 | end | |
110 | ||
111 | ret = CFArray.new(ary) | |
112 | ||
113 | when 'true' | |
114 | ret = CFBoolean.new(true) | |
115 | when 'false' | |
116 | ret = CFBoolean.new(false) | |
117 | when 'real' | |
118 | ret = CFReal.new(get_value(node).to_f) | |
119 | when 'integer' | |
120 | ret = CFInteger.new(get_value(node).to_i) | |
121 | when 'string' | |
122 | ret = CFString.new(get_value(node)) | |
123 | when 'data' | |
124 | ret = CFData.new(get_value(node)) | |
125 | when 'date' | |
126 | ret = CFDate.new(CFDate.parse_date(get_value(node))) | |
127 | end | |
128 | ||
129 | return ret | |
130 | end | |
131 | end | |
132 | end | |
133 | ||
134 | # eof |
0 | # -*- coding: utf-8 -*- | |
1 | ||
2 | require 'nokogiri' | |
3 | ||
4 | module Facter::Util::CFPropertyList | |
5 | # XML parser | |
6 | class XML < ParserInterface | |
7 | # read a XML file | |
8 | # opts:: | |
9 | # * :file - The filename of the file to load | |
10 | # * :data - The data to parse | |
11 | def load(opts) | |
12 | if(opts.has_key?(:file)) then | |
13 | File.open(opts[:file], "rb") { |fd| doc = Nokogiri::XML::Document.parse(fd, nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS|Nokogiri::XML::ParseOptions::NOENT) } | |
14 | else | |
15 | doc = Nokogiri::XML::Document.parse(opts[:data], nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS|Nokogiri::XML::ParseOptions::NOENT) | |
16 | end | |
17 | ||
18 | root = doc.root.children.first | |
19 | return import_xml(root) | |
20 | end | |
21 | ||
22 | # serialize Facter::Util::CFPropertyList object to XML | |
23 | # opts = {}:: Specify options: :formatted - Use indention and line breaks | |
24 | def to_str(opts={}) | |
25 | doc = Nokogiri::XML::Document.new | |
26 | @doc = doc | |
27 | ||
28 | doc.root = doc.create_element 'plist', :version => '1.0' | |
29 | doc.encoding = 'UTF-8' | |
30 | ||
31 | doc.root << opts[:root].to_xml(self) | |
32 | ||
33 | # ugly hack, but there's no other possibility I know | |
34 | s_opts = Nokogiri::XML::Node::SaveOptions::AS_XML | |
35 | s_opts |= Nokogiri::XML::Node::SaveOptions::FORMAT if opts[:formatted] | |
36 | ||
37 | str = doc.serialize(:save_with => s_opts) | |
38 | str1 = String.new | |
39 | first = false | |
40 | str.each_line do |line| | |
41 | str1 << line | |
42 | unless(first) then | |
43 | str1 << "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n" if line =~ /^\s*<\?xml/ | |
44 | end | |
45 | ||
46 | first = true | |
47 | end | |
48 | ||
49 | str1.force_encoding('UTF-8') if str1.respond_to?(:force_encoding) | |
50 | return str1 | |
51 | end | |
52 | ||
53 | def new_node(name) | |
54 | @doc.create_element name | |
55 | end | |
56 | ||
57 | def new_text(val) | |
58 | @doc.create_text_node val | |
59 | end | |
60 | ||
61 | def append_node(parent, child) | |
62 | parent << child | |
63 | end | |
64 | ||
65 | protected | |
66 | ||
67 | # get the value of a DOM node | |
68 | def get_value(n) | |
69 | content = if n.children.empty? | |
70 | n.content | |
71 | else | |
72 | n.children.first.content | |
73 | end | |
74 | ||
75 | content.force_encoding('UTF-8') if content.respond_to?(:force_encoding) | |
76 | content | |
77 | end | |
78 | ||
79 | # import the XML values | |
80 | def import_xml(node) | |
81 | ret = nil | |
82 | ||
83 | case node.name | |
84 | when 'dict' | |
85 | hsh = Hash.new | |
86 | key = nil | |
87 | children = node.children | |
88 | ||
89 | unless children.empty? then | |
90 | children.each do |n| | |
91 | next if n.text? # avoid a bug of libxml | |
92 | next if n.comment? | |
93 | ||
94 | if n.name == "key" then | |
95 | key = get_value(n) | |
96 | else | |
97 | raise CFFormatError.new("Format error!") if key.nil? | |
98 | hsh[key] = import_xml(n) | |
99 | key = nil | |
100 | end | |
101 | end | |
102 | end | |
103 | ||
104 | ret = CFDictionary.new(hsh) | |
105 | ||
106 | when 'array' | |
107 | ary = Array.new | |
108 | children = node.children | |
109 | ||
110 | unless children.empty? then | |
111 | children.each do |n| | |
112 | ary.push import_xml(n) | |
113 | end | |
114 | end | |
115 | ||
116 | ret = CFArray.new(ary) | |
117 | ||
118 | when 'true' | |
119 | ret = CFBoolean.new(true) | |
120 | when 'false' | |
121 | ret = CFBoolean.new(false) | |
122 | when 'real' | |
123 | ret = CFReal.new(get_value(node).to_f) | |
124 | when 'integer' | |
125 | ret = CFInteger.new(get_value(node).to_i) | |
126 | when 'string' | |
127 | ret = CFString.new(get_value(node)) | |
128 | when 'data' | |
129 | ret = CFData.new(get_value(node)) | |
130 | when 'date' | |
131 | ret = CFDate.new(CFDate.parse_date(get_value(node))) | |
132 | end | |
133 | ||
134 | return ret | |
135 | end | |
136 | end | |
137 | end | |
138 | ||
139 | # eof |
0 | # -*- coding: utf-8 -*- | |
1 | ||
2 | require 'rexml/document' | |
3 | ||
4 | module Facter::Util::CFPropertyList | |
5 | # XML parser | |
6 | class XML < ParserInterface | |
7 | # read a XML file | |
8 | # opts:: | |
9 | # * :file - The filename of the file to load | |
10 | # * :data - The data to parse | |
11 | def load(opts) | |
12 | if(opts.has_key?(:file)) then | |
13 | File.open(opts[:file], "rb") { |fd| doc = REXML::Document.new(fd) } | |
14 | else | |
15 | doc = REXML::Document.new(opts[:data]) | |
16 | end | |
17 | ||
18 | root = doc.root.elements[1] | |
19 | return import_xml(root) | |
20 | end | |
21 | ||
22 | # serialize Facter::Util::CFPropertyList object to XML | |
23 | # opts = {}:: Specify options: :formatted - Use indention and line breaks | |
24 | def to_str(opts={}) | |
25 | doc = REXML::Document.new | |
26 | @doc = doc | |
27 | ||
28 | doc.context[:attribute_quote] = :quote | |
29 | ||
30 | doc.add_element 'plist', {'version' => '1.0'} | |
31 | doc.root << opts[:root].to_xml(self) | |
32 | ||
33 | formatter = if opts[:formatted] then | |
34 | f = REXML::Formatters::Pretty.new(2) | |
35 | f.compact = true | |
36 | f | |
37 | else | |
38 | REXML::Formatters::Default.new | |
39 | end | |
40 | ||
41 | str = formatter.write(doc.root, "") | |
42 | str1 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n" + str + "\n" | |
43 | str1.force_encoding('UTF-8') if str1.respond_to?(:force_encoding) | |
44 | ||
45 | return str1 | |
46 | end | |
47 | ||
48 | def new_node(name) | |
49 | #LibXML::XML::Node.new(name) | |
50 | REXML::Element.new(name) | |
51 | end | |
52 | ||
53 | def new_text(val) | |
54 | val | |
55 | end | |
56 | ||
57 | def append_node(parent, child) | |
58 | if child.is_a?(String) then | |
59 | parent.add_text child | |
60 | else | |
61 | parent.elements << child | |
62 | end | |
63 | parent | |
64 | end | |
65 | ||
66 | protected | |
67 | ||
68 | # get the value of a DOM node | |
69 | def get_value(n) | |
70 | content = n.text | |
71 | ||
72 | content.force_encoding('UTF-8') if content.respond_to?(:force_encoding) | |
73 | content | |
74 | end | |
75 | ||
76 | # import the XML values | |
77 | def import_xml(node) | |
78 | ret = nil | |
79 | ||
80 | case node.name | |
81 | when 'dict' | |
82 | hsh = Hash.new | |
83 | key = nil | |
84 | ||
85 | if node.has_elements? then | |
86 | node.elements.each do |n| | |
87 | #print n.name + "\n" | |
88 | next if n.name == '#text' # avoid a bug of libxml | |
89 | next if n.name == '#comment' | |
90 | ||
91 | if n.name == "key" then | |
92 | key = get_value(n) | |
93 | else | |
94 | raise CFFormatError.new("Format error!") if key.nil? | |
95 | hsh[key] = import_xml(n) | |
96 | key = nil | |
97 | end | |
98 | end | |
99 | end | |
100 | ||
101 | ret = CFDictionary.new(hsh) | |
102 | ||
103 | when 'array' | |
104 | ary = Array.new | |
105 | ||
106 | if node.has_elements? then | |
107 | node.elements.each do |n| | |
108 | ary.push import_xml(n) | |
109 | end | |
110 | end | |
111 | ||
112 | ret = CFArray.new(ary) | |
113 | ||
114 | when 'true' | |
115 | ret = CFBoolean.new(true) | |
116 | when 'false' | |
117 | ret = CFBoolean.new(false) | |
118 | when 'real' | |
119 | ret = CFReal.new(get_value(node).to_f) | |
120 | when 'integer' | |
121 | ret = CFInteger.new(get_value(node).to_i) | |
122 | when 'string' | |
123 | ret = CFString.new(get_value(node)) | |
124 | when 'data' | |
125 | ret = CFData.new(get_value(node)) | |
126 | when 'date' | |
127 | ret = CFDate.new(CFDate.parse_date(get_value(node))) | |
128 | end | |
129 | ||
130 | return ret | |
131 | end | |
132 | end | |
133 | end | |
134 | ||
135 | # eof |
0 | # -*- coding: utf-8 -*- | |
1 | ||
2 | require File.join(File.dirname(__FILE__), 'cfpropertylist', 'lib', 'rbCFPropertyList.rb') | |
3 | ||
4 | ||
5 | # eof |
6 | 6 | |
7 | 7 | module Facter::Util::Macosx |
8 | 8 | require 'thread' |
9 | require 'facter/util/plist' | |
9 | require 'facter/util/cfpropertylist' | |
10 | 10 | require 'facter/util/resolution' |
11 | ||
12 | Plist_Xml_Doctype = '<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' | |
11 | 13 | |
12 | 14 | # JJM I'd really like to dynamically generate these methods |
13 | 15 | # by looking at the _name key of the _items dict for each _dataType |
18 | 20 | |
19 | 21 | def self.intern_xml(xml) |
20 | 22 | return nil unless xml |
21 | Plist::parse_xml(xml) | |
23 | bad_xml_doctype = /^.*<!DOCTYPE plist PUBLIC -\/\/Apple Computer.*$/ | |
24 | if xml =~ bad_xml_doctype | |
25 | xml.gsub!( bad_xml_doctype, Plist_Xml_Doctype ) | |
26 | Facter.debug("Had to fix plist with incorrect DOCTYPE declaration") | |
27 | end | |
28 | plist = Facter::Util::CFPropertyList::List.new | |
29 | begin | |
30 | plist.load_str(xml) | |
31 | rescue => e | |
32 | fail("A plist file could not be properly read by Facter::Util::CFPropertyList: #{e.inspect}") | |
33 | end | |
34 | Facter::Util::CFPropertyList.native_types(plist.value) | |
22 | 35 | end |
23 | 36 | |
24 | 37 | # Return an xml result, modified as we need it. |
0 | #--########################################################### | |
1 | # Copyright 2006, Ben Bleything <ben@bleything.net> and # | |
2 | # Patrick May <patrick@hexane.org> # | |
3 | # # | |
4 | # Distributed under the MIT license. # | |
5 | ############################################################## | |
6 | #++ | |
7 | # See Plist::Emit. | |
8 | module Plist | |
9 | # === Create a plist | |
10 | # You can dump an object to a plist in one of two ways: | |
11 | # | |
12 | # * <tt>Plist::Emit.dump(obj)</tt> | |
13 | # * <tt>obj.to_plist</tt> | |
14 | # * This requires that you mixin the <tt>Plist::Emit</tt> module, which is already done for +Array+ and +Hash+. | |
15 | # | |
16 | # The following Ruby classes are converted into native plist types: | |
17 | # Array, Bignum, Date, DateTime, Fixnum, Float, Hash, Integer, String, Symbol, Time, true, false | |
18 | # * +Array+ and +Hash+ are both recursive; their elements will be converted into plist nodes inside the <array> and <dict> containers (respectively). | |
19 | # * +IO+ (and its descendants) and +StringIO+ objects are read from and their contents placed in a <data> element. | |
20 | # * 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. | |
21 | # | |
22 | # For detailed usage instructions, refer to USAGE[link:files/docs/USAGE.html] and the methods documented below. | |
23 | module Emit | |
24 | # Helper method for injecting into classes. Calls <tt>Plist::Emit.dump</tt> with +self+. | |
25 | def to_plist(envelope = true) | |
26 | return Plist::Emit.dump(self, envelope) | |
27 | end | |
28 | ||
29 | # Helper method for injecting into classes. Calls <tt>Plist::Emit.save_plist</tt> with +self+. | |
30 | def save_plist(filename) | |
31 | Plist::Emit.save_plist(self, filename) | |
32 | end | |
33 | ||
34 | # The following Ruby classes are converted into native plist types: | |
35 | # Array, Bignum, Date, DateTime, Fixnum, Float, Hash, Integer, String, Symbol, Time | |
36 | # | |
37 | # Write us (via RubyForge) if you think another class can be coerced safely into one of the expected plist classes. | |
38 | # | |
39 | # +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+. | |
40 | # | |
41 | # 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. | |
42 | def self.dump(obj, envelope = true) | |
43 | output = plist_node(obj) | |
44 | ||
45 | output = wrap(output) if envelope | |
46 | ||
47 | return output | |
48 | end | |
49 | ||
50 | # Writes the serialized object's plist to the specified filename. | |
51 | def self.save_plist(obj, filename) | |
52 | File.open(filename, 'wb') do |f| | |
53 | f.write(obj.to_plist) | |
54 | end | |
55 | end | |
56 | ||
57 | private | |
58 | def self.plist_node(element) | |
59 | output = '' | |
60 | ||
61 | if element.respond_to? :to_plist_node | |
62 | output << element.to_plist_node | |
63 | else | |
64 | case element | |
65 | when Array | |
66 | if element.empty? | |
67 | output << "<array/>\n" | |
68 | else | |
69 | output << tag('array') { | |
70 | element.collect {|e| plist_node(e)} | |
71 | } | |
72 | end | |
73 | when Hash | |
74 | if element.empty? | |
75 | output << "<dict/>\n" | |
76 | else | |
77 | inner_tags = [] | |
78 | ||
79 | element.keys.sort.each do |k| | |
80 | v = element[k] | |
81 | inner_tags << tag('key', CGI::escapeHTML(k.to_s)) | |
82 | inner_tags << plist_node(v) | |
83 | end | |
84 | ||
85 | output << tag('dict') { | |
86 | inner_tags | |
87 | } | |
88 | end | |
89 | when true, false | |
90 | output << "<#{element}/>\n" | |
91 | when Time | |
92 | output << tag('date', element.utc.strftime('%Y-%m-%dT%H:%M:%SZ')) | |
93 | when Date # also catches DateTime | |
94 | output << tag('date', element.strftime('%Y-%m-%dT%H:%M:%SZ')) | |
95 | when String, Symbol, Fixnum, Bignum, Integer, Float | |
96 | output << tag(element_type(element), CGI::escapeHTML(element.to_s)) | |
97 | when IO, StringIO | |
98 | element.rewind | |
99 | contents = element.read | |
100 | # note that apple plists are wrapped at a different length then | |
101 | # what ruby's base64 wraps by default. | |
102 | # I used #encode64 instead of #b64encode (which allows a length arg) | |
103 | # because b64encode is b0rked and ignores the length arg. | |
104 | data = "\n" | |
105 | Base64::encode64(contents).gsub(/\s+/, '').scan(/.{1,68}/o) { data << $& << "\n" } | |
106 | output << tag('data', data) | |
107 | else | |
108 | output << comment( 'The <data> element below contains a Ruby object which has been serialized with Marshal.dump.' ) | |
109 | data = "\n" | |
110 | Base64::encode64(Marshal.dump(element)).gsub(/\s+/, '').scan(/.{1,68}/o) { data << $& << "\n" } | |
111 | output << tag('data', data ) | |
112 | end | |
113 | end | |
114 | ||
115 | return output | |
116 | end | |
117 | ||
118 | def self.comment(content) | |
119 | return "<!-- #{content} -->\n" | |
120 | end | |
121 | ||
122 | def self.tag(type, contents = '', &block) | |
123 | out = nil | |
124 | ||
125 | if block_given? | |
126 | out = IndentedString.new | |
127 | out << "<#{type}>" | |
128 | out.raise_indent | |
129 | ||
130 | out << block.call | |
131 | ||
132 | out.lower_indent | |
133 | out << "</#{type}>" | |
134 | else | |
135 | out = "<#{type}>#{contents.to_s}</#{type}>\n" | |
136 | end | |
137 | ||
138 | return out.to_s | |
139 | end | |
140 | ||
141 | def self.wrap(contents) | |
142 | output = '' | |
143 | ||
144 | output << '<?xml version="1.0" encoding="UTF-8"?>' + "\n" | |
145 | output << '<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' + "\n" | |
146 | output << '<plist version="1.0">' + "\n" | |
147 | ||
148 | output << contents | |
149 | ||
150 | output << '</plist>' + "\n" | |
151 | ||
152 | return output | |
153 | end | |
154 | ||
155 | def self.element_type(item) | |
156 | return case item | |
157 | when String, Symbol; 'string' | |
158 | when Fixnum, Bignum, Integer; 'integer' | |
159 | when Float; 'real' | |
160 | else | |
161 | raise "Don't know about this data type... something must be wrong!" | |
162 | end | |
163 | end | |
164 | ||
165 | private | |
166 | ||
167 | class IndentedString #:nodoc: | |
168 | attr_accessor :indent_string | |
169 | ||
170 | @@indent_level = 0 | |
171 | ||
172 | def initialize(str = "\t") | |
173 | @indent_string = str | |
174 | @contents = '' | |
175 | end | |
176 | ||
177 | def to_s | |
178 | return @contents | |
179 | end | |
180 | ||
181 | def raise_indent | |
182 | @@indent_level += 1 | |
183 | end | |
184 | ||
185 | def lower_indent | |
186 | @@indent_level -= 1 if @@indent_level > 0 | |
187 | end | |
188 | ||
189 | def <<(val) | |
190 | if val.is_a? Array | |
191 | val.each do |f| | |
192 | self << f | |
193 | end | |
194 | else | |
195 | # if it's already indented, don't bother indenting further | |
196 | unless val =~ /\A#{@indent_string}/ | |
197 | indent = @indent_string * @@indent_level | |
198 | ||
199 | @contents << val.gsub(/^/, indent) | |
200 | else | |
201 | @contents << val | |
202 | end | |
203 | ||
204 | # it already has a newline, don't add another | |
205 | @contents << "\n" unless val =~ /\n$/ | |
206 | end | |
207 | end | |
208 | end | |
209 | end | |
210 | end | |
211 | ||
212 | # we need to add this so sorting hash keys works properly | |
213 | class Symbol #:nodoc: | |
214 | def <=> (other) | |
215 | self.to_s <=> other.to_s | |
216 | end | |
217 | end | |
218 | ||
219 | class Array #:nodoc: | |
220 | include Plist::Emit | |
221 | end | |
222 | ||
223 | class Hash #:nodoc: | |
224 | include Plist::Emit | |
225 | end | |
226 | ||
227 | # $Id: generator.rb 1781 2006-10-16 01:01:35Z luke $ |
0 | #--########################################################### | |
1 | # Copyright 2006, Ben Bleything <ben@bleything.net> and # | |
2 | # Patrick May <patrick@hexane.org> # | |
3 | # # | |
4 | # Distributed under the MIT license. # | |
5 | ############################################################## | |
6 | #++ | |
7 | # Plist parses Mac OS X xml property list files into ruby data structures. | |
8 | # | |
9 | # === Load a plist file | |
10 | # This is the main point of the library: | |
11 | # | |
12 | # r = Plist::parse_xml( filename_or_xml ) | |
13 | module Plist | |
14 | # Note that I don't use these two elements much: | |
15 | # | |
16 | # + Date elements are returned as DateTime objects. | |
17 | # + Data elements are implemented as Tempfiles | |
18 | # | |
19 | # Plist::parse_xml will blow up if it encounters a data element. | |
20 | # If you encounter such an error, or if you have a Date element which | |
21 | # can't be parsed into a Time object, please send your plist file to | |
22 | # plist@hexane.org so that I can implement the proper support. | |
23 | def Plist::parse_xml( filename_or_xml ) | |
24 | listener = Listener.new | |
25 | #parser = REXML::Parsers::StreamParser.new(File.new(filename), listener) | |
26 | parser = StreamParser.new(filename_or_xml, listener) | |
27 | parser.parse | |
28 | listener.result | |
29 | end | |
30 | ||
31 | class Listener | |
32 | #include REXML::StreamListener | |
33 | ||
34 | attr_accessor :result, :open | |
35 | ||
36 | def initialize | |
37 | @result = nil | |
38 | @open = Array.new | |
39 | end | |
40 | ||
41 | ||
42 | def tag_start(name, attributes) | |
43 | @open.push PTag::mappings[name].new | |
44 | end | |
45 | ||
46 | def text( contents ) | |
47 | @open.last.text = contents if @open.last | |
48 | end | |
49 | ||
50 | def tag_end(name) | |
51 | last = @open.pop | |
52 | if @open.empty? | |
53 | @result = last.to_ruby | |
54 | else | |
55 | @open.last.children.push last | |
56 | end | |
57 | end | |
58 | end | |
59 | ||
60 | class StreamParser | |
61 | def initialize( filename_or_xml, listener ) | |
62 | @filename_or_xml = filename_or_xml | |
63 | @listener = listener | |
64 | end | |
65 | ||
66 | TEXT = /([^<]+)/ | |
67 | XMLDECL_PATTERN = /<\?xml\s+(.*?)\?>*/um | |
68 | DOCTYPE_PATTERN = /\s*<!DOCTYPE\s+(.*?)(\[|>)/um | |
69 | COMMENT_START = /\A<!--/u | |
70 | COMMENT_END = /.*?-->/um | |
71 | ||
72 | def parse | |
73 | plist_tags = PTag::mappings.keys.join('|') | |
74 | start_tag = /<(#{plist_tags})([^>]*)>/i | |
75 | end_tag = /<\/(#{plist_tags})[^>]*>/i | |
76 | ||
77 | require 'strscan' | |
78 | ||
79 | contents = ( | |
80 | if (File.exists? @filename_or_xml) | |
81 | File.open(@filename_or_xml) {|f| f.read} | |
82 | else | |
83 | @filename_or_xml | |
84 | end | |
85 | ) | |
86 | ||
87 | @scanner = StringScanner.new( contents ) | |
88 | until @scanner.eos? | |
89 | if @scanner.scan(COMMENT_START) | |
90 | @scanner.scan(COMMENT_END) | |
91 | elsif @scanner.scan(XMLDECL_PATTERN) | |
92 | elsif @scanner.scan(DOCTYPE_PATTERN) | |
93 | elsif @scanner.scan(start_tag) | |
94 | @listener.tag_start(@scanner[1], nil) | |
95 | if (@scanner[2] =~ /\/$/) | |
96 | @listener.tag_end(@scanner[1]) | |
97 | end | |
98 | elsif @scanner.scan(TEXT) | |
99 | @listener.text(@scanner[1]) | |
100 | elsif @scanner.scan(end_tag) | |
101 | @listener.tag_end(@scanner[1]) | |
102 | else | |
103 | raise "Unimplemented element" | |
104 | end | |
105 | end | |
106 | end | |
107 | end | |
108 | ||
109 | class PTag | |
110 | @@mappings = { } | |
111 | def PTag::mappings | |
112 | @@mappings | |
113 | end | |
114 | ||
115 | def PTag::inherited( sub_class ) | |
116 | key = sub_class.to_s.downcase | |
117 | key.gsub!(/^plist::/, '' ) | |
118 | key.gsub!(/^p/, '') unless key == "plist" | |
119 | ||
120 | @@mappings[key] = sub_class | |
121 | end | |
122 | ||
123 | attr_accessor :text, :children | |
124 | def initialize | |
125 | @children = Array.new | |
126 | end | |
127 | ||
128 | def to_ruby | |
129 | raise "Unimplemented: " + self.class.to_s + "#to_ruby on #{self.inspect}" | |
130 | end | |
131 | end | |
132 | ||
133 | class PList < PTag | |
134 | def to_ruby | |
135 | children.first.to_ruby if children.first | |
136 | end | |
137 | end | |
138 | ||
139 | class PDict < PTag | |
140 | def to_ruby | |
141 | dict = Hash.new | |
142 | key = nil | |
143 | ||
144 | children.each do |c| | |
145 | if key.nil? | |
146 | key = c.to_ruby | |
147 | else | |
148 | dict[key] = c.to_ruby | |
149 | key = nil | |
150 | end | |
151 | end | |
152 | ||
153 | dict | |
154 | end | |
155 | end | |
156 | ||
157 | class PKey < PTag | |
158 | def to_ruby | |
159 | CGI::unescapeHTML(text || '') | |
160 | end | |
161 | end | |
162 | ||
163 | class PString < PTag | |
164 | def to_ruby | |
165 | CGI::unescapeHTML(text || '') | |
166 | end | |
167 | end | |
168 | ||
169 | class PArray < PTag | |
170 | def to_ruby | |
171 | children.collect do |c| | |
172 | c.to_ruby | |
173 | end | |
174 | end | |
175 | end | |
176 | ||
177 | class PInteger < PTag | |
178 | def to_ruby | |
179 | text.to_i | |
180 | end | |
181 | end | |
182 | ||
183 | class PTrue < PTag | |
184 | def to_ruby | |
185 | true | |
186 | end | |
187 | end | |
188 | ||
189 | class PFalse < PTag | |
190 | def to_ruby | |
191 | false | |
192 | end | |
193 | end | |
194 | ||
195 | class PReal < PTag | |
196 | def to_ruby | |
197 | text.to_f | |
198 | end | |
199 | end | |
200 | ||
201 | require 'date' | |
202 | class PDate < PTag | |
203 | def to_ruby | |
204 | DateTime.parse(text) | |
205 | end | |
206 | end | |
207 | ||
208 | require 'base64' | |
209 | class PData < PTag | |
210 | def to_ruby | |
211 | data = Base64.decode64(text.gsub(/\s+/, '')) | |
212 | ||
213 | begin | |
214 | return Marshal.load(data) | |
215 | rescue Exception => e | |
216 | io = StringIO.new | |
217 | io.write data | |
218 | io.rewind | |
219 | return io | |
220 | end | |
221 | end | |
222 | end | |
223 | end | |
224 | ||
225 | # $Id: parser.rb 1781 2006-10-16 01:01:35Z luke $ |
0 | #-- | |
1 | ############################################################## | |
2 | # Copyright 2006, Ben Bleything <ben@bleything.net> and # | |
3 | # Patrick May <patrick@hexane.org> # | |
4 | # # | |
5 | # Distributed under the MIT license. # | |
6 | ############################################################## | |
7 | #++ | |
8 | # = Plist | |
9 | # | |
10 | # This is the main file for plist. Everything interesting happens in Plist and Plist::Emit. | |
11 | ||
12 | require 'base64' | |
13 | require 'cgi' | |
14 | require 'stringio' | |
15 | ||
16 | require 'facter/util/plist/generator' | |
17 | require 'facter/util/plist/parser' | |
18 | ||
19 | module Plist | |
20 | VERSION = '3.0.0' | |
21 | end | |
22 | ||
23 | # $Id: plist.rb 1781 2006-10-16 01:01:35Z luke $ |
3 | 3 | require 'facter/util/macosx' |
4 | 4 | |
5 | 5 | describe Facter::Util::Macosx do |
6 | let(:badplist) do | |
7 | '<?xml version="1.0" encoding="UTF-8"?> | |
8 | <!DOCTYPE plist PUBLIC -//Apple Computer//DTD PLIST 1.0//EN http://www.apple.com/DTDs/PropertyList-1.0.dtd> | |
9 | <plist version="1.0"> | |
10 | <dict> | |
11 | <key>test</key> | |
12 | <string>file</string> | |
13 | </dict> | |
14 | </plist>' | |
15 | end | |
16 | ||
17 | let(:goodplist) do | |
18 | '<?xml version="1.0" encoding="UTF-8"?> | |
19 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
20 | <plist version="1.0"> | |
21 | <dict> | |
22 | <key>test</key> | |
23 | <string>file</string> | |
24 | </dict> | |
25 | </plist>' | |
26 | end | |
27 | ||
6 | 28 | it "should be able to retrieve profiler data as xml for a given data field" do |
7 | 29 | Facter::Util::Resolution.expects(:exec).with("/usr/sbin/system_profiler -xml foo").returns "yay" |
8 | 30 | Facter::Util::Macosx.profiler_xml("foo").should == "yay" |
9 | 31 | end |
10 | 32 | |
11 | it "should use PList to convert xml to data structures" do | |
12 | Plist.expects(:parse_xml).with("foo").returns "bar" | |
33 | it 'should correct a bad XML doctype string' do | |
34 | Facter.expects(:debug).with('Had to fix plist with incorrect DOCTYPE declaration') | |
35 | Facter::Util::Macosx.intern_xml(badplist) | |
36 | end | |
13 | 37 | |
14 | Facter::Util::Macosx.intern_xml("foo").should == "bar" | |
38 | it 'should return a hash given XML data' do | |
39 | test_hash = { 'test' => 'file' } | |
40 | Facter::Util::Macosx.intern_xml(goodplist).should == test_hash | |
41 | end | |
42 | ||
43 | it 'should fail when trying to read invalid XML' do | |
44 | expect { Facter::Util::Macosx.intern_xml('<bad}|%-->xml<--->') }.should \ | |
45 | raise_error(RuntimeError, /A plist file could not be properly read by Facter::Util::CFPropertyList/) | |
15 | 46 | end |
16 | 47 | |
17 | 48 | describe "when collecting profiler data" do |