plist4r 0.2.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. data/.gitignore +4 -0
  2. data/.yardopts +11 -0
  3. data/LICENSE +3 -1
  4. data/README.rdoc +25 -122
  5. data/Rakefile +14 -0
  6. data/VERSION +1 -1
  7. data/bin/plist4r +2 -0
  8. data/ext/osx_plist/Makefile +157 -0
  9. data/ext/osx_plist/extconf.rb +9 -0
  10. data/ext/osx_plist/plist.c +606 -0
  11. data/ext/osx_plist/plist.o +0 -0
  12. data/lib/plist4r.rb +6 -3
  13. data/lib/plist4r/application.rb +1 -2
  14. data/lib/plist4r/backend.rb +102 -34
  15. data/lib/plist4r/backend/c_f_property_list.rb +65 -0
  16. data/lib/plist4r/backend/c_f_property_list/LICENSE +19 -0
  17. data/lib/plist4r/backend/c_f_property_list/README +34 -0
  18. data/lib/plist4r/backend/c_f_property_list/cfpropertylist.rb +6 -0
  19. data/lib/plist4r/backend/c_f_property_list/rbBinaryCFPropertyList.rb +663 -0
  20. data/lib/plist4r/backend/c_f_property_list/rbCFPlistError.rb +26 -0
  21. data/lib/plist4r/backend/c_f_property_list/rbCFPropertyList.rb +348 -0
  22. data/lib/plist4r/backend/c_f_property_list/rbCFTypes.rb +241 -0
  23. data/lib/plist4r/backend/c_f_property_list/rbXMLCFPropertyList.rb +116 -0
  24. data/lib/plist4r/backend/example.rb +37 -52
  25. data/lib/plist4r/backend/haml.rb +47 -36
  26. data/lib/plist4r/backend/libxml4r.rb +24 -20
  27. data/lib/plist4r/backend/osx_plist.rb +82 -0
  28. data/lib/plist4r/backend/ruby_cocoa.rb +172 -54
  29. data/lib/plist4r/backend/test/data_types.rb +163 -0
  30. data/lib/plist4r/backend/test/harness.rb +255 -0
  31. data/lib/plist4r/backend/test/output.rb +47 -0
  32. data/lib/plist4r/backend_base.rb +4 -2
  33. data/lib/plist4r/{options.rb → cli.rb} +2 -1
  34. data/lib/plist4r/commands.rb +13 -8
  35. data/lib/plist4r/config.rb +36 -9
  36. data/lib/plist4r/docs/Backends.html +59 -0
  37. data/lib/plist4r/docs/DeveloperGuide.rdoc +53 -0
  38. data/lib/plist4r/docs/EditingPlistFiles.rdoc +88 -0
  39. data/lib/plist4r/docs/InfoPlistExample.rdoc +33 -0
  40. data/lib/plist4r/docs/LaunchdPlistExample.rdoc +33 -0
  41. data/lib/plist4r/docs/PlistKeyNames.rdoc +47 -0
  42. data/lib/plist4r/mixin/array_dict.rb +61 -0
  43. data/lib/plist4r/mixin/data_methods.rb +178 -54
  44. data/lib/plist4r/mixin/haml4r.rb +4 -0
  45. data/lib/plist4r/mixin/haml4r/css_attributes.rb +19 -0
  46. data/lib/plist4r/mixin/haml4r/examples.rb +261 -0
  47. data/lib/plist4r/mixin/haml4r/haml_table_example.rb +79 -0
  48. data/lib/plist4r/mixin/haml4r/table.rb +157 -0
  49. data/lib/plist4r/mixin/haml4r/table_cell.rb +160 -0
  50. data/lib/plist4r/mixin/haml4r/table_cells.rb +485 -0
  51. data/lib/plist4r/mixin/haml4r/table_section.rb +101 -0
  52. data/lib/plist4r/mixin/ordered_hash.rb +9 -1
  53. data/lib/plist4r/mixin/popen4.rb +1 -1
  54. data/lib/plist4r/mixin/ruby_stdlib.rb +154 -1
  55. data/lib/plist4r/mixin/script.rb +133 -0
  56. data/lib/plist4r/mixin/table.rb +435 -0
  57. data/lib/plist4r/plist.rb +272 -94
  58. data/lib/plist4r/plist_cache.rb +42 -43
  59. data/lib/plist4r/plist_type.rb +31 -74
  60. data/lib/plist4r/plist_type/info.rb +157 -3
  61. data/lib/plist4r/plist_type/launchd.rb +54 -48
  62. data/lib/plist4r/plist_type/plist.rb +1 -3
  63. data/plist4r.gemspec +74 -14
  64. data/spec/{examples.rb → launchd_examples.rb} +131 -139
  65. data/spec/plist4r/application_spec.rb +37 -0
  66. data/spec/plist4r/backend_spec.rb +256 -0
  67. data/spec/plist4r/cli_spec.rb +25 -0
  68. data/spec/plist4r/commands_spec.rb +20 -0
  69. data/spec/plist4r/config_spec.rb +38 -0
  70. data/spec/plist4r/mixin/array_dict_spec.rb +120 -0
  71. data/spec/plist4r/mixin/data_methods_spec.rb +96 -0
  72. data/spec/plist4r/mixin/haml4r/examples.rb +261 -0
  73. data/spec/plist4r/mixin/ruby_stdlib_spec.rb +228 -0
  74. data/spec/plist4r/plist_cache_spec.rb +261 -0
  75. data/spec/plist4r/plist_spec.rb +841 -23
  76. data/spec/plist4r/plist_type_spec.rb +126 -0
  77. data/spec/plist4r_spec.rb +53 -27
  78. data/spec/scratchpad.rb +226 -0
  79. data/spec/spec_helper.rb +5 -1
  80. metadata +109 -23
  81. data/lib/plist4r/backend/plutil.rb +0 -25
  82. data/lib/plist4r/mixin.rb +0 -7
  83. data/plists/array_mini.xml +0 -14
  84. data/plists/example_big_binary.plist +0 -0
  85. data/plists/example_medium_binary_launchd.plist +0 -0
  86. data/plists/example_medium_launchd.xml +0 -53
  87. data/plists/mini.xml +0 -12
  88. data/test.rb +0 -40
Binary file
data/lib/plist4r.rb CHANGED
@@ -15,8 +15,8 @@ module Plist4r
15
15
  # @see Plist4r::Plist#initialize
16
16
  # @example Create new, empty plist
17
17
  # Plist4r.new => #<Plist4r::Plist:0x111546c @file_format=nil, ...>
18
+ # @api public
18
19
  def new *args, &blk
19
- # puts args.inspect
20
20
  return Plist.new *args, &blk
21
21
  end
22
22
 
@@ -28,6 +28,7 @@ module Plist4r
28
28
  # Plist4r.open("example.plist") => #<Plist4r::Plist:0x1152d1c @file_format="xml", ...>
29
29
  # @see Plist4r::Plist#initialize
30
30
  # @see Plist4r::Plist#open
31
+ # @api public
31
32
  def open filename, *args, &blk
32
33
  p = Plist.new filename, *args, &blk
33
34
  p.open
@@ -39,12 +40,12 @@ module Plist4r
39
40
  # @return [Symbol] A Symbol representing the plist data type. One of: Plist4r::Plist.FileFormats
40
41
  # @see Plist4r::Plist.FileFormats
41
42
  # @example
42
- # Plist4r.string_detect_format("{ \"key1\" = \"value1\"; \"key2\" = \"value2\"; }") => :next_step
43
+ # Plist4r.string_detect_format("{ \"key1\" = \"value1\"; \"key2\" = \"value2\"; }") => :gnustep
43
44
  def string_detect_format string
44
45
  string.strip! if string[0,1] =~ /\s/
45
46
  case string[0,1]
46
47
  when "{","("
47
- :next_step
48
+ :gnustep
48
49
  when "b"
49
50
  if string =~ /^bplist/
50
51
  :binary
@@ -65,6 +66,7 @@ module Plist4r
65
66
  # Given a Plist filename, peek the first few bytes and detect the file format
66
67
  #
67
68
  # @param [String] filename plist file to check
69
+ # @return [Symbol] A Symbol representing the plist data type. One of: Plist4r::Plist.FileFormats
68
70
  # @see Plist4r.string_detect_format
69
71
  # @see Plist4r::Plist.FileFormats
70
72
  def file_detect_format filename
@@ -79,6 +81,7 @@ class String
79
81
  #
80
82
  # @return [Plist4r::Plist] The new Plist object
81
83
  # @see Plist4r::Plist#initialize
84
+ # @api public
82
85
  def to_plist
83
86
  return ::Plist4r.new(:from_string => self)
84
87
  end
@@ -1,5 +1,5 @@
1
1
  require 'plist4r/config'
2
- require 'plist4r/options'
2
+ require 'plist4r/cli'
3
3
  require 'plist4r/commands'
4
4
 
5
5
  module Plist4r
@@ -11,7 +11,6 @@ module Plist4r
11
11
  @cli = Plist4r::CLI.new
12
12
  Plist4r::Config[:args] = @cli.parse
13
13
 
14
- puts "Plist4r::Config[:args] = " + Plist4r::Config[:args].inspect
15
14
  @commands = Plist4r::Commands.new
16
15
  @commands.run
17
16
  end
@@ -1,73 +1,141 @@
1
1
 
2
2
  require 'plist4r/config'
3
- require 'plist4r/backend_base'
4
3
  require 'plist4r/mixin/ordered_hash'
4
+ require 'timeout'
5
5
 
6
6
  module Plist4r
7
7
  # This class is the Backend broker. The purpose of this object is to manage and handle API
8
8
  # calls, passing them over to the appropriate Plist4r backends.
9
+ # Also see the {file:Backends} Rdoc page.
9
10
  class Backend
10
- # The list of known backend API methods. Any combination or subset of API methods
11
- # can be implemented by an individual backend.
11
+ # The list backend API methods. A Plist4r::Backend should implement 1 or more of these methods
12
12
  # @see Plist4r::Backend::Example
13
- ApiMethods = %w[from_string to_xml to_binary to_next_step open save]
14
-
13
+ ApiMethods = %w[ from_xml from_binary from_gnustep to_xml to_binary to_gnustep ]
14
+
15
+ # The set of Plist4r API methods which are generated by the {#generic_call} method
16
+ # These methods don't need to be implemented by a Plist4r::Backend
17
+ PrivateApiMethods = %w[ from_string open save ]
18
+
19
+ # The set of Plist4r API methods with are invoked by {Plist4r::PlistCache}
20
+ PlistCacheApiMethods = %w[from_string to_xml to_binary to_gnustep open save]
21
+
15
22
  # A new instance of Backend. A single Backend will exist for the the life of
16
23
  # the Plist object. The attribute @plist is set during initialization and
17
24
  # refers back to the plist instance object.
18
25
  def initialize plist, *args, &blk
19
26
  @plist = plist
20
- @backends = plist.backends.collect do |b|
21
- case b
22
- when Module
23
- b
24
- when Symbol, String
25
- eval "::Plist4r::Backend::#{b.to_s.camelcase}"
27
+ end
28
+
29
+ # Implements a generic version of each of the Plist4r Private API Calls.
30
+ # @param [Symbol] backend the currently iterated backend from which we will try to generate the API call
31
+ # @param [Symbol] method_sym The API method call to execute. One of {PrivateApiMethods}
32
+ def generic_call backend, method_sym
33
+ case method_sym
34
+
35
+ when :save
36
+ fmt = @plist.file_format || Plist4r::Config[:default_format]
37
+ unless backend.respond_to? "to_#{fmt}"
38
+ return Exception.new "Plist4r: No backend found to handle method :to_#{fmt}. Could not execute method :save on plist #{@plist.inspect}"
39
+ end
40
+ File.open(@plist.filename_path,'w') do |out|
41
+ out << @plist.instance_eval { @plist_cache.send("to_#{fmt}".to_sym) }
42
+ end
43
+
44
+ when :open
45
+ unless @open_fmt
46
+ @plist.instance_eval "@from_string = File.read(filename_path)"
47
+ @open_fmt = Plist4r.string_detect_format @plist.from_string
48
+ end
49
+ fmt = @open_fmt
50
+ if backend.respond_to? "from_#{fmt}"
51
+ @from_string_fmt = @open_fmt
52
+ @open_fmt = nil
53
+
54
+ @plist.instance_eval { @plist_cache.send :from_string }
55
+ else
56
+ return Exception.new "Plist4r: No backend found to handle method :from_#{fmt}. Could not execute method :open on plist #{@plist.inspect}"
57
+ end
58
+
59
+ when :from_string
60
+ unless @from_string_fmt
61
+ @from_string_fmt = Plist4r.string_detect_format @plist.from_string
62
+ end
63
+ fmt = @from_string_fmt
64
+ if backend.respond_to? "from_#{fmt}"
65
+ @from_string_fmt = nil
66
+
67
+ Timeout::timeout(Plist4r::Config[:backend_timeout]) do
68
+ backend.send("from_#{fmt}".to_sym, @plist)
69
+ end
70
+ @plist.file_format fmt
71
+ @plist
26
72
  else
27
- raise "Backend #{b.inspect} is of unsupported type: #{b.class}"
73
+ return Exception.new "Plist4r: No backend found to handle method :from_#{fmt}. Could not execute method :from_string on plist #{@plist.inspect}"
28
74
  end
29
75
  end
30
76
  end
31
77
 
32
- # vv We need a version of this to call our matrix test harness vv
33
78
  # Call a Plist4r API Method. Here, we usually pass a {Plist4r::Plist} object
34
79
  # as one of the parameters, which will also contain all the input data to work on.
35
80
  #
36
81
  # This function loops through the array of available backends, and calls the
37
- # same method on the first backend found to implemente the specific request.
82
+ # first backend found to implement the appropriate fullfilment request.
38
83
  #
39
- # If the request fails, the call is re-executed on the next available
84
+ # If the request fails, the call is re-executed on the next available
40
85
  # backend.
41
86
  #
42
- # The plist object is updated in-place.
87
+ # The plist object is updated in-place and also usually placed as the return argument.
43
88
  #
44
- # @raise if no backend is able to sucessfully execute the request.
89
+ # @raise if no backend was able to sucessfully execute the request.
45
90
  # @param [Symbol] method_sym The API method call to execute
46
- # @param *args Any optional arguments to pass to the backend
47
- def call method_sym, *args, &blk
48
- # puts "in call"
49
- # puts "#{method_sym.inspect} #{args.inspect}"
50
- raise "Unsupported api call #{method_sym.inspect}" unless ApiMethods.include? method_sym.to_s
91
+ def call method_sym
92
+ raise "Unsupported api call #{method_sym.inspect}" unless PlistCacheApiMethods.include? method_sym.to_s
51
93
  exceptions = []
52
- @backends.each do |backend|
53
- if backend.respond_to? method_sym
54
- begin
55
- result = backend.send(method_sym, @plist, *args, &blk)
56
- # puts "result = #{result.inspect}"
57
- return result
58
- # return backend.send(method_sym, @plist, *args, &blk)
59
- rescue LoadError
60
- exceptions << $!
61
- rescue
62
- exceptions << $!
94
+ generic_call_exception = nil
95
+
96
+ @plist.backends.each do |b_sym|
97
+ backend = eval "Plist4r::Backend::#{b_sym.to_s.camelcase}"
98
+
99
+ begin
100
+ if backend.respond_to?(method_sym) && method_sym != :from_string
101
+ Timeout::timeout(Plist4r::Config[:backend_timeout]) do
102
+ return eval("#{backend}.#{method_sym} @plist")
103
+ end
104
+
105
+ elsif PrivateApiMethods.include? method_sym.to_s
106
+ result = generic_call backend, method_sym
107
+ if result.is_a? Exception
108
+ generic_call_exception = result
109
+ else
110
+ return result
111
+ end
63
112
  end
113
+
114
+ rescue LoadError
115
+ exceptions << $!
116
+ rescue RuntimeError
117
+ exceptions << $!
118
+ rescue SyntaxError
119
+ exceptions << $!
120
+ rescue Exception
121
+ exceptions << $!
122
+ rescue Timeout::Error
123
+ exceptions << $!
124
+ rescue
125
+ exceptions << $!
64
126
  end
127
+
65
128
  if Config[:raise_any_failure] && exceptions.first
66
129
  raise exceptions.first
67
130
  end
68
131
  end
132
+
69
133
  if exceptions.empty?
70
- raise "Plist4r: No backend found to handle method #{method_sym.inspect}. Could not execute method #{method_sym.inspect} on plist #{@plist.inspect}"
134
+ if generic_call_exception
135
+ raise generic_call_exception
136
+ else
137
+ raise "Plist4r: No backend found to handle method #{method_sym.inspect}. Could not execute method #{method_sym.inspect} on plist #{@plist.inspect}"
138
+ end
71
139
  else
72
140
  # $stderr.puts "Failure(s) while executing method #{method_sym.inspect} on plist #{@plist}."
73
141
  exceptions.each do |e|
@@ -0,0 +1,65 @@
1
+
2
+ require 'plist4r/backend_base'
3
+ require 'plist4r/backend/c_f_property_list/rbCFPropertyList'
4
+
5
+ # C.Kruse's CFPropertyList Library. Used by {Plist4r::Backend::CFPropertyList}
6
+ # @author Christian Kruse (http://github.com/ckruse)
7
+ module CFPropertyList
8
+ end
9
+
10
+ # Source: http://github.com/ckruse/CFPropertyList
11
+ #
12
+ # C.Kruse's CFPropertyList is an independant Ruby Library and written natively in Ruby.
13
+ # Supports binary and xml format property lists. With a dependency on libxml-ruby
14
+ # for reading/writing the xml plists.
15
+ # @author Christian Kruse (http://github.com/ckruse)
16
+ module Plist4r::Backend::CFPropertyList
17
+ class << self
18
+ def from_string plist
19
+ cf_plist = CFPropertyList::List.new
20
+ cf_plist.load_str(plist.from_string)
21
+ ruby_object = CFPropertyList.native_types(cf_plist.value)
22
+
23
+ hash_obj = nil
24
+ if ruby_object.is_a? Hash
25
+ hash_obj = ruby_object
26
+
27
+ elsif ruby_object
28
+ hash_obj = { ruby_object.class.to_s => ruby_object }
29
+
30
+ else
31
+ raise "Conversion tp plist object failed"
32
+ end
33
+
34
+ hash = ::Plist4r::OrderedHash.new
35
+ hash.replace hash_obj
36
+ plist.import_hash hash
37
+
38
+ return plist
39
+ end
40
+
41
+ def from_xml plist
42
+ from_string plist
43
+ end
44
+
45
+ def from_binary plist
46
+ from_string plist
47
+ end
48
+
49
+ def to_xml plist
50
+ cf_plist = CFPropertyList::List.new
51
+ cf_plist.value = CFPropertyList.guess(plist.to_hash)
52
+ return cf_plist.to_str(CFPropertyList::List::FORMAT_XML)
53
+ end
54
+
55
+ def to_binary plist
56
+ cf_plist = CFPropertyList::List.new
57
+ cf_plist.value = CFPropertyList.guess(plist.to_hash)
58
+ return cf_plist.to_str(CFPropertyList::List::FORMAT_BINARY)
59
+ end
60
+
61
+ end
62
+ end
63
+
64
+
65
+
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2010 Christian Kruse, <cjk@wwwtech.de>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a
4
+ copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+ The above copyright notice and this permission notice shall be included
11
+ in all copies or substantial portions of the Software.
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
13
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
14
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
15
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
16
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
17
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
18
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19
+
@@ -0,0 +1,34 @@
1
+ CFPropertyList implementation
2
+ class to read, manipulate and write both XML and binary property list
3
+ files (plist(5)) as defined by Apple
4
+
5
+ == Example
6
+
7
+ # create a arbitrary data structure of basic data types
8
+ data = {
9
+ 'name' => 'John Doe',
10
+ 'missing' => true,
11
+ 'last_seen' => Time.now,
12
+ 'friends' => ['Jane Doe','Julian Doe'],
13
+ 'likes' => {
14
+ 'me' => false
15
+ }
16
+ }
17
+
18
+ # create CFPropertyList::List object
19
+ plist = CFPropertyList::List.new
20
+
21
+ # call CFPropertyList.guess() to create corresponding CFType values
22
+ plist.value = CFPropertyList.guess(data)
23
+
24
+ # write plist to file
25
+ plist.save("example.plist", CFPropertyList::List::FORMAT_BINARY)
26
+
27
+ # … later, read it again
28
+ plist = CFPropertyList::List.new("example.plist")
29
+ data = CFPropertyList.native_types(plist.value)
30
+
31
+ Author:: Christian Kruse (mailto:cjk@wwwtech.de)
32
+ Copyright:: Copyright (c) 2010
33
+ License:: Distributes under the same terms as Ruby
34
+
@@ -0,0 +1,6 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require File.dirname(__FILE__) + '/rbCFPropertyList.rb'
4
+
5
+
6
+ # eof
@@ -0,0 +1,663 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module CFPropertyList
4
+ # Binary PList parser class
5
+ class Binary
6
+ # Read a binary plist file
7
+ def load(opts)
8
+ @unique_table = {}
9
+ @count_objects = 0
10
+ @string_size = 0
11
+ @int_size = 0
12
+ @misc_size = 0
13
+ @object_refs = 0
14
+
15
+ @written_object_count = 0
16
+ @object_table = []
17
+ @object_ref_size = 0
18
+
19
+ @offsets = []
20
+
21
+ fd = nil
22
+ if(opts.has_key?(:file)) then
23
+ fd = File.open(opts[:file],"rb")
24
+ file = opts[:file]
25
+ else
26
+ fd = StringIO.new(opts[:data],"rb")
27
+ file = "<string>"
28
+ end
29
+
30
+ # first, we read the trailer: 32 byte from the end
31
+ fd.seek(-32,IO::SEEK_END)
32
+ buff = fd.read(32)
33
+
34
+ offset_size, object_ref_size, number_of_objects, top_object, table_offset = buff.unpack "x6CCx4Nx4Nx4N"
35
+
36
+ # after that, get the offset table
37
+ fd.seek(table_offset, IO::SEEK_SET)
38
+ coded_offset_table = fd.read(number_of_objects * offset_size)
39
+ raise CFFormatError.new("#{file}: Format error!") unless coded_offset_table.bytesize == number_of_objects * offset_size
40
+
41
+ @count_objects = number_of_objects
42
+
43
+ # decode offset table
44
+ formats = ["","C*","n*","(H6)*","N*"]
45
+ @offsets = coded_offset_table.unpack(formats[offset_size])
46
+ if(offset_size == 3) then
47
+ 0.upto(@offsets.size-1) { |i| @offsets[i] = @offsets[i].to_i(16) }
48
+ end
49
+
50
+ @object_ref_size = object_ref_size
51
+ val = read_binary_object_at(file,fd,top_object)
52
+
53
+ fd.close
54
+ return val
55
+ end
56
+
57
+
58
+ # Convert CFPropertyList to binary format; since we have to count our objects we simply unique CFDictionary and CFArray
59
+ def to_str(opts={})
60
+ @unique_table = {}
61
+ @count_objects = 0
62
+ @string_size = 0
63
+ @int_size = 0
64
+ @misc_size = 0
65
+ @object_refs = 0
66
+
67
+ @written_object_count = 0
68
+ @object_table = []
69
+ @object_ref_size = 0
70
+
71
+ @offsets = []
72
+
73
+ binary_str = "bplist00"
74
+ unique_and_count_values(opts[:root])
75
+
76
+ @count_objects += @unique_table.size
77
+ @object_ref_size = Binary.bytes_needed(@count_objects)
78
+
79
+ file_size = @string_size + @int_size + @misc_size + @object_refs * @object_ref_size + 40
80
+ offset_size = Binary.bytes_needed(file_size)
81
+ table_offset = file_size - 32
82
+
83
+ @object_table = []
84
+ @written_object_count = 0
85
+ @unique_table = {} # we needed it to calculate several values, but now we need an empty table
86
+
87
+ opts[:root].to_binary(self)
88
+
89
+ object_offset = 8
90
+ offsets = []
91
+
92
+ 0.upto(@object_table.size-1) do |i|
93
+ binary_str += @object_table[i]
94
+ offsets[i] = object_offset
95
+ object_offset += @object_table[i].bytesize
96
+ end
97
+
98
+ offsets.each do |offset|
99
+ binary_str += Binary.pack_it_with_size(offset_size,offset)
100
+ end
101
+
102
+ binary_str += [offset_size, @object_ref_size].pack("x6CC")
103
+ binary_str += [@count_objects].pack("x4N")
104
+ binary_str += [0].pack("x4N")
105
+ binary_str += [table_offset].pack("x4N")
106
+
107
+ return binary_str
108
+ end
109
+
110
+ # read a „null” type (i.e. null byte, marker byte, bool value)
111
+ def read_binary_null_type(length)
112
+ case length
113
+ when 0 then return 0 # null byte
114
+ when 8 then return CFBoolean.new(false)
115
+ when 9 then return CFBoolean.new(true)
116
+ when 15 then return 15 # fill type
117
+ end
118
+
119
+ raise CFFormatError.new("unknown null type: #{length}")
120
+ end
121
+ protected :read_binary_null_type
122
+
123
+ # read a binary int value
124
+ def read_binary_int(fname,fd,length)
125
+ raise CFFormatError.new("Integer greater than 8 bytes: #{length}") if length > 3
126
+
127
+ nbytes = 1 << length
128
+
129
+ val = nil
130
+ buff = fd.read(nbytes)
131
+
132
+ case length
133
+ when 0 then
134
+ val = buff.unpack("C")
135
+ val = val[0]
136
+ when 1 then
137
+ val = buff.unpack("n")
138
+ val = val[0]
139
+ when 2 then
140
+ val = buff.unpack("N")
141
+ val = val[0]
142
+ when 3
143
+ hiword,loword = buff.unpack("NN")
144
+ val = hiword << 32 | loword
145
+ end
146
+
147
+ return CFInteger.new(val);
148
+ end
149
+ protected :read_binary_int
150
+
151
+ # read a binary real value
152
+ def read_binary_real(fname,fd,length)
153
+ raise CFFormatError.new("Real greater than 8 bytes: #{length}") if length > 3
154
+
155
+ nbytes = 1 << length
156
+ val = nil
157
+ buff = fd.read(nbytes)
158
+
159
+ case length
160
+ when 0 then # 1 byte float? must be an error
161
+ raise CFFormatError.new("got #{length+1} byte float, must be an error!")
162
+ when 1 then # 2 byte float? must be an error
163
+ raise CFFormatError.new("got #{length+1} byte float, must be an error!")
164
+ when 2 then
165
+ val = buff.reverse.unpack("f")
166
+ val = val[0]
167
+ when 3 then
168
+ val = buff.reverse.unpack("d")
169
+ val = val[0]
170
+ end
171
+
172
+ return CFReal.new(val)
173
+ end
174
+ protected :read_binary_real
175
+
176
+ # read a binary date value
177
+ def read_binary_date(fname,fd,length)
178
+ raise CFFormatError.new("Date greater than 8 bytes: #{length}") if length > 3
179
+
180
+ nbytes = 1 << length
181
+ val = nil
182
+ buff = fd.read(nbytes)
183
+
184
+ case length
185
+ when 0 then # 1 byte CFDate is an error
186
+ raise CFFormatError.new("#{length+1} byte CFDate, error")
187
+ when 1 then # 2 byte CFDate is an error
188
+ raise CFFormatError.new("#{length+1} byte CFDate, error")
189
+ when 2 then
190
+ val = buff.reverse.unpack("f")
191
+ val = val[0]
192
+ when 3 then
193
+ val = buff.reverse.unpack("d")
194
+ val = val[0]
195
+ end
196
+
197
+ return CFDate.new(val,CFDate::TIMESTAMP_APPLE)
198
+ end
199
+ protected :read_binary_date
200
+
201
+ # Read a binary data value
202
+ def read_binary_data(fname,fd,length)
203
+ buff = "";
204
+ buff = fd.read(length) if length > 0
205
+ return CFData.new(buff,CFData::DATA_RAW)
206
+ end
207
+ protected :read_binary_data
208
+
209
+ # Read a binary string value
210
+ def read_binary_string(fname,fd,length)
211
+ buff = ""
212
+ buff = fd.read(length) if length > 0
213
+
214
+ @unique_table[buff] = true unless @unique_table.has_key?(buff)
215
+ return CFString.new(buff)
216
+ end
217
+ protected :read_binary_string
218
+
219
+ # Convert the given string from one charset to another
220
+ def Binary.charset_convert(str,from,to="UTF-8")
221
+ return str.clone.force_encoding(from).encode(to) if str.respond_to?("encode")
222
+ return Iconv.conv(to,from,str)
223
+ end
224
+
225
+ # Count characters considering character set
226
+ def Binary.charset_strlen(str,charset="UTF-8")
227
+ return str.length if str.respond_to?("encode")
228
+
229
+ str = Iconv.conv("UTF-8",charset,str) if charset != "UTF-8"
230
+ return str.scan(/./mu).size
231
+ end
232
+
233
+ # Read a unicode string value, coded as UTF-16BE
234
+ def read_binary_unicode_string(fname,fd,length)
235
+ # The problem is: we get the length of the string IN CHARACTERS;
236
+ # since a char in UTF-16 can be 16 or 32 bit long, we don't really know
237
+ # how long the string is in bytes
238
+ buff = fd.read(2*length)
239
+
240
+ @unique_table[buff] = true unless @unique_table.has_key?(buff)
241
+ return CFString.new(Binary.charset_convert(buff,"UTF-16BE","UTF-8"))
242
+ end
243
+ protected :read_binary_unicode_string
244
+
245
+ # Read an binary array value, including contained objects
246
+ def read_binary_array(fname,fd,length)
247
+ ary = []
248
+
249
+ # first: read object refs
250
+ if(length != 0) then
251
+ buff = fd.read(length * @object_ref_size)
252
+ objects = buff.unpack(@object_ref_size == 1 ? "C*" : "n*")
253
+
254
+ # now: read objects
255
+ 0.upto(length-1) do |i|
256
+ object = read_binary_object_at(fname,fd,objects[i])
257
+ ary.push object
258
+ end
259
+ end
260
+
261
+ return CFArray.new(ary)
262
+ end
263
+ protected :read_binary_array
264
+
265
+ # Read a dictionary value, including contained objects
266
+ def read_binary_dict(fname,fd,length)
267
+ dict = {}
268
+
269
+ # first: read keys
270
+ if(length != 0) then
271
+ buff = fd.read(length * @object_ref_size)
272
+ keys = buff.unpack(@object_ref_size == 1 ? "C*" : "n*")
273
+
274
+ # second: read object refs
275
+ buff = fd.read(length * @object_ref_size)
276
+ objects = buff.unpack(@object_ref_size == 1 ? "C*" : "n*")
277
+
278
+ # read real keys and objects
279
+ 0.upto(length-1) do |i|
280
+ key = read_binary_object_at(fname,fd,keys[i])
281
+ object = read_binary_object_at(fname,fd,objects[i])
282
+ dict[key.value] = object
283
+ end
284
+ end
285
+
286
+ return CFDictionary.new(dict)
287
+ end
288
+ protected :read_binary_dict
289
+
290
+ # Read an object type byte, decode it and delegate to the correct reader function
291
+ def read_binary_object(fname,fd)
292
+ # first: read the marker byte
293
+ buff = fd.read(1)
294
+
295
+ object_length = buff.unpack("C*")
296
+ object_length = object_length[0] & 0xF
297
+
298
+ buff = buff.unpack("H*")
299
+ object_type = buff[0][0].chr
300
+
301
+ if(object_type != "0" && object_length == 15) then
302
+ object_length = read_binary_object(fname,fd)
303
+ object_length = object_length.value
304
+ end
305
+
306
+ retval = nil
307
+ case object_type
308
+ when '0' then # null, false, true, fillbyte
309
+ retval = read_binary_null_type(object_length)
310
+ when '1' then # integer
311
+ retval = read_binary_int(fname,fd,object_length)
312
+ when '2' then # real
313
+ retval = read_binary_real(fname,fd,object_length)
314
+ when '3' then # date
315
+ retval = read_binary_date(fname,fd,object_length)
316
+ when '4' then # data
317
+ retval = read_binary_data(fname,fd,object_length)
318
+ when '5' then # byte string, usually utf8 encoded
319
+ retval = read_binary_string(fname,fd,object_length)
320
+ when '6' then # unicode string (utf16be)
321
+ retval = read_binary_unicode_string(fname,fd,object_length)
322
+ when 'a' then # array
323
+ retval = read_binary_array(fname,fd,object_length)
324
+ when 'd' then # dictionary
325
+ retval = read_binary_dict(fname,fd,object_length)
326
+ end
327
+
328
+ return retval
329
+ end
330
+ protected :read_binary_object
331
+
332
+ # Read an object type byte at position $pos, decode it and delegate to the correct reader function
333
+ def read_binary_object_at(fname,fd,pos)
334
+ position = @offsets[pos]
335
+ fd.seek(position,IO::SEEK_SET)
336
+ return read_binary_object(fname,fd)
337
+ end
338
+ protected :read_binary_object_at
339
+
340
+ # calculate the bytes needed for a size integer value
341
+ def Binary.bytes_size_int(int)
342
+ nbytes = 0
343
+
344
+ nbytes += 2 if int > 0xE # 2 bytes int
345
+ nbytes += 1 if int > 0xFF # 3 bytes int
346
+ nbytes += 2 if int > 0xFFFF # 5 bytes int
347
+
348
+ return nbytes
349
+ end
350
+
351
+ # Calculate the byte needed for a „normal” integer value
352
+ def Binary.bytes_int(int)
353
+ nbytes = 1
354
+
355
+ nbytes += 1 if int > 0xFF # 2 byte int
356
+ nbytes += 2 if int > 0xFFFF # 4 byte int
357
+ nbytes += 4 if int > 0xFFFFFFFF # 8 byte int
358
+ nbytes += 7 if int < 0 # 8 byte int (since it is signed)
359
+
360
+ return nbytes + 1 # one „marker” byte
361
+ end
362
+
363
+ # pack an +int+ of +nbytes+ with size
364
+ def Binary.pack_it_with_size(nbytes,int)
365
+ format = ["C", "n", "N", "N"][nbytes-1]
366
+
367
+ if(nbytes == 3) then
368
+ val = [int].pack(format)
369
+ return val.slice(-3)
370
+ end
371
+
372
+ return [int].pack(format)
373
+ end
374
+
375
+ # calculate how many bytes are needed to save +count+
376
+ def Binary.bytes_needed(count)
377
+ nbytes = 0
378
+
379
+ while count >= 1 do
380
+ nbytes += 1
381
+ count /= 256
382
+ end
383
+
384
+ return nbytes
385
+ end
386
+
387
+ # create integer bytes of +int+
388
+ def Binary.int_bytes(int)
389
+ intbytes = ""
390
+
391
+ if(int > 0xFFFF) then
392
+ intbytes = "\x12"+[int].pack("N") # 4 byte integer
393
+ elsif(int > 0xFF) then
394
+ intbytes = "\x11"+[int].pack("n") # 2 byte integer
395
+ else
396
+ intbytes = "\x10"+[int].pack("C") # 8 byte integer
397
+ end
398
+
399
+ return intbytes;
400
+ end
401
+
402
+ # Create a type byte for binary format as defined by apple
403
+ def Binary.type_bytes(type,type_len)
404
+ optional_int = ""
405
+
406
+ if(type_len < 15) then
407
+ type += sprintf("%x",type_len)
408
+ else
409
+ type += "f"
410
+ optional_int = Binary.int_bytes(type_len)
411
+ end
412
+
413
+ return [type].pack("H*") + optional_int
414
+ end
415
+
416
+ # „unique” and count values. „Unique” means, several objects (e.g. strings)
417
+ # will only be saved once and referenced later
418
+ def unique_and_count_values(value)
419
+ # no uniquing for other types than CFString and CFData
420
+ if(value.is_a?(CFInteger) || value.is_a?(CFReal)) then
421
+ val = value.value
422
+ if(value.is_a?(CFInteger)) then
423
+ @int_size += Binary.bytes_int(val)
424
+ else
425
+ @misc_size += 9 # 9 bytes (8 + marker byte) for real
426
+ end
427
+
428
+ @count_objects += 1
429
+ return
430
+ elsif(value.is_a?(CFDate)) then
431
+ @misc_size += 9
432
+ @count_objects += 1
433
+ return
434
+ elsif(value.is_a?(CFBoolean)) then
435
+ @count_objects += 1
436
+ @misc_size += 1
437
+ return
438
+ elsif(value.is_a?(CFArray)) then
439
+ cnt = 0
440
+
441
+ value.value.each do |v|
442
+ cnt += 1
443
+ unique_and_count_values(v)
444
+ @object_refs += 1 # each array member is a ref
445
+ end
446
+
447
+ @count_objects += 1
448
+ @int_size += Binary.bytes_size_int(cnt)
449
+ @misc_size += 1 # marker byte for array
450
+ return
451
+ elsif(value.is_a?(CFDictionary)) then
452
+ cnt = 0
453
+
454
+ value.value.each_pair do |k,v|
455
+ cnt += 1
456
+
457
+ if(!@unique_table.has_key?(k))
458
+ @unique_table[k] = 0
459
+ @string_size += Binary.binary_strlen(k) + 1
460
+ @int_size += Binary.bytes_size_int(Binary.charset_strlen(k,'UTF-8'))
461
+ end
462
+
463
+ @object_refs += 2 # both, key and value, are refs
464
+ @unique_table[k] += 1
465
+ unique_and_count_values(v)
466
+ end
467
+
468
+ @count_objects += 1
469
+ @misc_size += 1 # marker byte for dict
470
+ @int_size += Binary.bytes_size_int(cnt)
471
+ return
472
+ elsif(value.is_a?(CFData)) then
473
+ val = value.decoded_value
474
+ @int_size += Binary.bytes_size_int(val.length)
475
+ @misc_size += val.length + 1
476
+ @count_objects += 1
477
+ return
478
+ end
479
+
480
+ val = value.value
481
+ if(!@unique_table.has_key?(val)) then
482
+ @unique_table[val] = 0
483
+ @string_size += Binary.binary_strlen(val) + 1
484
+ @int_size += Binary.bytes_size_int(Binary.charset_strlen(val,'UTF-8'))
485
+ end
486
+
487
+ @unique_table[val] += 1
488
+ end
489
+ protected :unique_and_count_values
490
+
491
+ # Counts the number of bytes the string will have when coded; utf-16be if non-ascii characters are present.
492
+ def Binary.binary_strlen(val)
493
+ val.each_byte do |b|
494
+ if(b > 127) then
495
+ val = Binary.charset_convert(val, 'UTF-8', 'UTF-16BE')
496
+ return val.bytesize
497
+ end
498
+ end
499
+
500
+ return val.bytesize
501
+ end
502
+
503
+ # Uniques and transforms a string value to binary format and adds it to the object table
504
+ def string_to_binary(val)
505
+ saved_object_count = -1
506
+
507
+ unless(@unique_table.has_key?(val)) then
508
+ saved_object_count = @written_object_count
509
+ @written_object_count += 1
510
+
511
+ @unique_table[val] = saved_object_count
512
+ utf16 = false
513
+
514
+ val.each_byte do |b|
515
+ if(b > 127) then
516
+ utf16 = true
517
+ break
518
+ end
519
+ end
520
+
521
+ if(utf16) then
522
+ bdata = Binary.type_bytes("6",Binary.charset_strlen(val,"UTF-8")) # 6 is 0110, unicode string (utf16be)
523
+ val = Binary.charset_convert(val,"UTF-8","UTF-16BE")
524
+
525
+ val.force_encoding("ASCII-8BIT") if val.respond_to?("encode")
526
+ @object_table[saved_object_count] = bdata + val
527
+ else
528
+ bdata = Binary.type_bytes("5",val.bytesize) # 5 is 0101 which is an ASCII string (seems to be ASCII encoded)
529
+ @object_table[saved_object_count] = bdata + val
530
+ end
531
+ else
532
+ saved_object_count = @unique_table[val]
533
+ end
534
+
535
+ return saved_object_count
536
+ end
537
+
538
+ # Codes an integer to binary format
539
+ def int_to_binary(value)
540
+ nbytes = 0
541
+ nbytes = 1 if value > 0xFF # 1 byte integer
542
+ nbytes += 1 if value > 0xFFFF # 4 byte integer
543
+ nbytes += 1 if value > 0xFFFFFFFF # 8 byte integer
544
+ nbytes = 3 if value < 0 # 8 byte integer, since signed
545
+
546
+ bdata = Binary.type_bytes("1", nbytes) # 1 is 0001, type indicator for integer
547
+ buff = ""
548
+
549
+ if(nbytes < 3) then
550
+ fmt = "N"
551
+
552
+ if(nbytes == 0) then
553
+ fmt = "C"
554
+ elsif(nbytes == 1)
555
+ fmt = "n"
556
+ end
557
+
558
+ buff = [value].pack(fmt)
559
+ else
560
+ # 64 bit signed integer; we need the higher and the lower 32 bit of the value
561
+ high_word = value >> 32
562
+ low_word = value & 0xFFFFFFFF
563
+ buff = [high_word,low_word].pack("NN")
564
+ end
565
+
566
+ return bdata + buff
567
+ end
568
+
569
+ # Codes a real value to binary format
570
+ def real_to_binary(val)
571
+ bdata = Binary.type_bytes("2",3) # 2 is 0010, type indicator for reals
572
+ buff = [val].pack("d")
573
+ return bdata + buff.reverse
574
+ end
575
+
576
+ # Converts a numeric value to binary and adds it to the object table
577
+ def num_to_binary(value)
578
+ saved_object_count = @written_object_count
579
+ @written_object_count += 1
580
+
581
+ val = ""
582
+ if(value.is_a?(CFInteger)) then
583
+ val = int_to_binary(value.value)
584
+ else
585
+ val = real_to_binary(value.value)
586
+ end
587
+
588
+ @object_table[saved_object_count] = val
589
+ return saved_object_count
590
+ end
591
+
592
+ # Convert date value (apple format) to binary and adds it to the object table
593
+ def date_to_binary(val)
594
+ saved_object_count = @written_object_count
595
+ @written_object_count += 1
596
+
597
+ 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
598
+
599
+ bdata = Binary.type_bytes("3", 3) # 3 is 0011, type indicator for date
600
+ @object_table[saved_object_count] = bdata + [val].pack("d").reverse
601
+
602
+ return saved_object_count
603
+ end
604
+
605
+ # Convert a bool value to binary and add it to the object table
606
+ def bool_to_binary(val)
607
+ saved_object_count = @written_object_count
608
+ @written_object_count += 1
609
+
610
+ @object_table[saved_object_count] = val ? "\x9" : "\x8" # 0x9 is 1001, type indicator for true; 0x8 is 1000, type indicator for false
611
+ return saved_object_count
612
+ end
613
+
614
+ # Convert data value to binary format and add it to the object table
615
+ def data_to_binary(val)
616
+ saved_object_count = @written_object_count
617
+ @written_object_count += 1
618
+
619
+ bdata = Binary.type_bytes("4", val.bytesize) # a is 1000, type indicator for data
620
+ @object_table[saved_object_count] = bdata + val
621
+
622
+ return saved_object_count
623
+ end
624
+
625
+ # Convert array to binary format and add it to the object table
626
+ def array_to_binary(val)
627
+ saved_object_count = @written_object_count
628
+ @written_object_count += 1
629
+
630
+ bdata = Binary.type_bytes("a", val.value.size) # a is 1010, type indicator for arrays
631
+
632
+ val.value.each do |v|
633
+ bdata += Binary.pack_it_with_size(@object_ref_size, v.to_binary(self));
634
+ end
635
+
636
+ @object_table[saved_object_count] = bdata
637
+ return saved_object_count
638
+ end
639
+
640
+ # Convert dictionary to binary format and add it to the object table
641
+ def dict_to_binary(val)
642
+ saved_object_count = @written_object_count
643
+ @written_object_count += 1
644
+
645
+ bdata = Binary.type_bytes("d",val.value.size) # d=1101, type indicator for dictionary
646
+
647
+ val.value.each_key do |k|
648
+ str = CFString.new(k)
649
+ key = str.to_binary(self)
650
+ bdata += Binary.pack_it_with_size(@object_ref_size,key)
651
+ end
652
+
653
+ val.value.each_value do |v|
654
+ bdata += Binary.pack_it_with_size(@object_ref_size,v.to_binary(self))
655
+ end
656
+
657
+ @object_table[saved_object_count] = bdata
658
+ return saved_object_count
659
+ end
660
+ end
661
+ end
662
+
663
+ # eof