Codebase list facter / c584f11
Merge pull request #188 from glarizza/bug/master/11299_cfpropertylist [#11299] Replace facter/util/plist with cfpropertylist Daniel Pittman 12 years ago
18 changed file(s) with 1820 addition(s) and 483 deletion(s). Raw diff Collapse all Expand all
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 require File.dirname(__FILE__) + '/rbCFPropertyList.rb'
3
4
5 # eof
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
66
77 module Facter::Util::Macosx
88 require 'thread'
9 require 'facter/util/plist'
9 require 'facter/util/cfpropertylist'
1010 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">'
1113
1214 # JJM I'd really like to dynamically generate these methods
1315 # by looking at the _name key of the _items dict for each _dataType
1820
1921 def self.intern_xml(xml)
2022 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)
2235 end
2336
2437 # Return an xml result, modified as we need it.
+0
-228
lib/facter/util/plist/generator.rb less more
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
-226
lib/facter/util/plist/parser.rb less more
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
-24
lib/facter/util/plist.rb less more
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 $
33 require 'facter/util/macosx'
44
55 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
628 it "should be able to retrieve profiler data as xml for a given data field" do
729 Facter::Util::Resolution.expects(:exec).with("/usr/sbin/system_profiler -xml foo").returns "yay"
830 Facter::Util::Macosx.profiler_xml("foo").should == "yay"
931 end
1032
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
1337
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/)
1546 end
1647
1748 describe "when collecting profiler data" do