├── test
├── suite.rb
├── fixtures
│ └── xml_plist
└── test_plist.rb
├── ext
└── plist
│ ├── extconf.rb
│ └── plist.c
├── Manifest.txt
├── History.txt
├── Rakefile
├── lib
└── osx
│ └── plist.rb
├── osx-plist.gemspec
└── README.txt
/test/suite.rb:
--------------------------------------------------------------------------------
1 | require 'test/unit'
2 |
3 | tests = Dir["#{File.dirname(__FILE__)}/test_*.rb"]
4 | tests.each do |file|
5 | require file
6 | end
7 |
--------------------------------------------------------------------------------
/ext/plist/extconf.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/ruby
2 | require 'mkmf'
3 | $LDFLAGS += ' -framework CoreFoundation -undefined suppress -flat_namespace'
4 | $LIBRUBYARG_SHARED=""
5 | create_makefile("osx/plist/ext/plist")
6 |
--------------------------------------------------------------------------------
/Manifest.txt:
--------------------------------------------------------------------------------
1 | History.txt
2 | Manifest.txt
3 | README.txt
4 | Rakefile
5 | ext/plist/extconf.rb
6 | ext/plist/plist.c
7 | lib/osx/plist.rb
8 | test/fixtures/xml_plist
9 | test/suite.rb
10 | test/test_plist.rb
11 |
--------------------------------------------------------------------------------
/History.txt:
--------------------------------------------------------------------------------
1 | == 1.0.3 / 2009-09-21
2 | * Add two new methods OSX::PropertyList.load_file and OSX::PropertyList.dump_file
3 | * Clean up the RDoc documentation
4 |
5 | == 1.0.2 / 2009-09-17
6 | * Build properly under Mac OS X 10.6
7 |
8 | == 1.0.1 / 2009-02-05
9 | * Ruby 1.9.1 compatibility
10 |
11 | == 1.0 / 2008-04-25
12 | * First public release
13 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'hoe'
3 |
4 | Hoe.spec 'osx-plist' do
5 | developer("Kevin Ballard", "kevin@sb.org")
6 | self.version = "1.0.3"
7 | self.summary = "Property List manipulation for OS X"
8 | self.spec_extras = {:extensions => "ext/plist/extconf.rb"}
9 | end
10 |
11 | # override Hoe's default :test task
12 | Rake::Task["test"].clear
13 | desc "Run the unit tests"
14 | task :test do
15 | ruby "test/test_plist.rb"
16 | end
17 |
--------------------------------------------------------------------------------
/lib/osx/plist.rb:
--------------------------------------------------------------------------------
1 | require "#{File.dirname(__FILE__)}/plist/ext/plist"
2 |
3 | module OSX
4 | module PropertyList
5 | # Loads a property list from the file at +filepath+ using OSX::PropertyList.load.
6 | def self.load_file(filepath, format = false)
7 | File.open(filepath, "r") do |f|
8 | OSX::PropertyList.load(f, format)
9 | end
10 | end
11 | # Writes the property list representation of +obj+ to the file at +filepath+ using OSX::PropertyList.dump.
12 | def self.dump_file(filepath, obj, format = :xml1)
13 | File.open(filepath, "w") do |f|
14 | OSX::PropertyList.dump(f, obj, format)
15 | end
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/test/fixtures/xml_plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | string!
6 | indeedy
7 | bar
8 |
9 | 1
10 | 2
11 | 3
12 |
13 | foo
14 |
15 | correct?
16 |
17 | pi
18 | 3.14159265
19 | random
20 |
21 | I0VniQ==
22 |
23 | today
24 | 2005-04-28T06:32:56Z
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/osx-plist.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | Gem::Specification.new do |s|
4 | s.name = %q{osx-plist}
5 | s.version = "1.0.3"
6 |
7 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8 | s.authors = ["Kevin Ballard"]
9 | s.date = %q{2009-09-21}
10 | s.description = %q{osx-plist is a Ruby library for manipulating Property Lists natively using the built-in support in OS X.}
11 | s.email = ["kevin@sb.org"]
12 | s.extensions = ["ext/plist/extconf.rb"]
13 | s.extra_rdoc_files = ["History.txt", "Manifest.txt", "README.txt"]
14 | s.files = ["History.txt", "Manifest.txt", "README.txt", "Rakefile", "ext/plist/extconf.rb", "ext/plist/plist.c", "lib/osx/plist.rb", "test/fixtures/xml_plist", "test/suite.rb", "test/test_plist.rb"]
15 | s.homepage = %q{http://github.com/kballard/osx-plist}
16 | s.rdoc_options = ["--main", "README.txt"]
17 | s.require_paths = ["lib", "ext"]
18 | s.rubygems_version = %q{1.3.5}
19 | s.summary = %q{Property List manipulation for OS X}
20 | s.test_files = ["test/test_plist.rb"]
21 |
22 | if s.respond_to? :specification_version then
23 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
24 | s.specification_version = 3
25 |
26 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
27 | s.add_development_dependency(%q, [">= 2.3.3"])
28 | else
29 | s.add_dependency(%q, [">= 2.3.3"])
30 | end
31 | else
32 | s.add_dependency(%q, [">= 2.3.3"])
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/test/test_plist.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'osx/plist'
3 | require 'stringio'
4 | require 'tempfile'
5 | require 'test/unit'
6 |
7 | class TestPlist < Test::Unit::TestCase
8 | def test_string
9 | plist = OSX::PropertyList.load("{foo = bar; }")
10 | assert_equal( { "foo" => "bar" }, plist )
11 |
12 | plist, format = OSX::PropertyList.load("{foo = bar; }", true)
13 | assert_equal( { "foo" => "bar" }, plist )
14 | assert_equal( :openstep, format )
15 |
16 | # make sure sources < 6 characters work
17 | plist = OSX::PropertyList.load("foo")
18 | assert_equal( "foo", plist )
19 |
20 | # make sure it works with format too
21 | plist, format = OSX::PropertyList.load("foo", true)
22 | assert_equal( "foo", plist )
23 | assert_equal( :openstep, format )
24 |
25 | assert_raise(OSX::PropertyListError) { OSX::PropertyList.load("") }
26 | end
27 |
28 | def setup_hash
29 | time = Time.gm(2005, 4, 28, 6, 32, 56)
30 | random = "\x23\x45\x67\x89"
31 | random.blob = true
32 | {
33 | "string!" => "indeedy",
34 | "bar" => [ 1, 2, 3 ],
35 | "foo" => {
36 | "correct?" => true,
37 | "pi" => 3.14159265,
38 | "random" => random,
39 | "today" => time,
40 | }
41 | }
42 | end
43 |
44 | def test_io
45 | plist, format = OSX::PropertyList.load(File.read("#{File.dirname(__FILE__)}/fixtures/xml_plist"), true)
46 |
47 | hash = setup_hash
48 |
49 | assert_equal(hash, plist)
50 | assert_equal(true, plist['foo']['random'].blob?)
51 | assert_equal(false, plist['string!'].blob?)
52 |
53 | assert_equal(:xml1, format)
54 | end
55 |
56 | def test_dump
57 | str = StringIO.new("", "w")
58 | hash = setup_hash
59 | OSX::PropertyList.dump(str, hash)
60 | hash2 = OSX::PropertyList.load(str.string)
61 | assert_equal(hash, hash2)
62 | end
63 |
64 | def test_to_plist
65 | assert_raise(OSX::PropertyListError) { "foo".to_plist(:openstep) }
66 | assert_equal("foo", OSX::PropertyList.load("foo".to_plist))
67 | hash = setup_hash()
68 | assert_equal(hash, OSX::PropertyList.load(hash.to_plist))
69 | end
70 |
71 | def test_load_file
72 | plist, format = OSX::PropertyList.load_file("#{File.dirname(__FILE__)}/fixtures/xml_plist", true)
73 |
74 | hash = setup_hash
75 |
76 | assert_equal(hash, plist)
77 | assert_equal(:xml1, format)
78 | end
79 |
80 | def test_dump_file
81 | hash = setup_hash
82 | Tempfile.open("test_plist") do |temp|
83 | OSX::PropertyList.dump_file(temp.path, hash)
84 | hash2 = OSX::PropertyList.load_file(temp.path)
85 | assert_equal(hash, hash2)
86 | end
87 | end
88 | end
89 |
--------------------------------------------------------------------------------
/README.txt:
--------------------------------------------------------------------------------
1 | == osx-plist
2 |
3 | * http://github.com/kballard/osx-plist
4 | * by Kevin Ballard
5 |
6 | == DESCRIPTION:
7 |
8 | osx-plist is a Ruby library for manipulating Property Lists natively using the built-in support in OS X.
9 |
10 | == REQUIREMENTS:
11 |
12 | * CoreFoundation (i.e. Mac OS X)
13 |
14 | == INSTALL:
15 |
16 | $ gem sources -a http://gems.github.com/ (you only need to do this once)
17 | $ gem install kballard-osx-plist
18 |
19 | == SOURCE:
20 |
21 | osx-plist's git repo is available on GitHub, which can be browsed at:
22 |
23 | http://github.com/kballard/osx-plist
24 |
25 | and cloned from:
26 |
27 | git://github.com/kballard/osx-plist.git
28 |
29 | == USAGE:
30 |
31 | One new module is provided, named OSX::PropertyList. It has the following 4 methods:
32 |
33 | ==== OSX::PropertyList.load(input, format = false)
34 |
35 | Loads the property list from input, which is either an IO, StringIO, or a string. Format is an optional parameter - if false, the return value is the converted property list object. If true, the return value is a 2-element array, the first element being the returned value and the second being a symbol identifying the property list format.
36 |
37 | ==== OSX::PropertyList.dump(output, obj, format = :xml1)
38 |
39 | Dumps the property list object into output, which is either an IO or StringIO. Format determines the property list format to write out. The supported values are :xml1,, :binary1, and :openstep; however, OpenStep format appears to not be supported by the system for output anymore.
40 |
41 | The valid formats are :xml1, :binary1, and :openstep. When loading a property list, if the format is something else (not possible under any current OS, but perhaps if a future OS includes another type) then the format will be :unknown.
42 |
43 | ==== OSX::PropertyList.load_file(filepath, format = false)
44 |
45 | Calls OSX::PropertyList.load() on the file at the given path.
46 |
47 | ==== OSX::PropertyList.dump_file(filepath, obj, format = :xml1)
48 |
49 | Calls OSX::PropertyList.dump() on the file at the given path.
50 |
51 | This module also provides a method on Object:
52 |
53 | ==== Object#to_plist(format = :xml1)
54 |
55 | This is the same as PropertyList.dump except it outputs the property list as a string return value instead of writing it to a stream
56 |
57 | This module also provides 2 methods on String:
58 |
59 | ==== String#blob?
60 |
61 | Returns whether the string is a blob.
62 |
63 | ==== String#blob=
64 |
65 | Sets whether the string is a blob.
66 |
67 | A blob is a string that's been converted from a property list item. When dumping to a property list, any strings that are blobs are written as items rather than items.
68 |
69 | == LICENSE:
70 |
71 | (The MIT License)
72 |
73 | Copyright (c) 2008 Kevin Ballard
74 |
75 | Permission is hereby granted, free of charge, to any person obtaining
76 | a copy of this software and associated documentation files (the
77 | 'Software'), to deal in the Software without restriction, including
78 | without limitation the rights to use, copy, modify, merge, publish,
79 | distribute, sublicense, and/or sell copies of the Software, and to
80 | permit persons to whom the Software is furnished to do so, subject to
81 | the following conditions:
82 |
83 | The above copyright notice and this permission notice shall be
84 | included in all copies or substantial portions of the Software.
85 |
86 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
87 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
88 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
89 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
90 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
91 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
92 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
93 |
--------------------------------------------------------------------------------
/ext/plist/plist.c:
--------------------------------------------------------------------------------
1 | /*
2 | * plist
3 | * Kevin Ballard
4 | *
5 | * This is a Ruby extension to read/write Cocoa property lists
6 | * Not surprisingly, it only works on OS X
7 | *
8 | * Copyright © 2005, Kevin Ballard
9 | *
10 | * Usage:
11 | * This extension provides a module named OSX::PropertyList
12 | * This module has two methods:
13 | *
14 | * PropertyList::load(obj, format = false)
15 | * Takes either an IO stream open for reading or a String object
16 | * Returns an object representing the property list
17 | *
18 | * Optionally takes a boolean format argument. If true, the
19 | * return value is an array with the second value being
20 | * the format of the plist, which can be one of
21 | * :xml1, :binary1, or :openstep
22 | *
23 | * PropertyList::dump(io, obj, type = :xml1)
24 | * Takes an IO stream (open for writing) and an object
25 | * Writes the object to the IO stream as a property list
26 | * Posible type values are :xml1 and :binary1
27 | *
28 | * It also adds a new method to Object:
29 | *
30 | * Object#to_plist(type = :xml1)
31 | * Returns a string representation of the property list
32 | * Possible type values are :xml1 and :binary1
33 | *
34 | * It also adds 2 new methods to String:
35 | *
36 | * String#blob=(b)
37 | * Sets whether the string is a blob
38 | *
39 | * String#blob?
40 | * Returns whether the string is a blob
41 | *
42 | * A blob string is turned into a CFData when dumped
43 | *
44 | */
45 |
46 | #include
47 | #if HAVE_RUBY_ST_H
48 | #include
49 | #else
50 | #include
51 | #endif
52 | #include
53 |
54 | // Here's some convenience macros
55 | #ifndef StringValue
56 | #define StringValue(x) do { \
57 | if (TYPE(x) != T_STRING) x = rb_str_to_str(x); \
58 | } while (0)
59 | #endif
60 |
61 | static VALUE mOSX;
62 | static VALUE mPlist;
63 | static VALUE timeEpoch;
64 | static VALUE ePropertyListError;
65 |
66 | static VALUE id_gm;
67 | static VALUE id_plus;
68 | static VALUE id_minus;
69 | static VALUE id_read;
70 | static VALUE id_write;
71 |
72 | static VALUE id_xml;
73 | static VALUE id_binary;
74 | static VALUE id_openstep;
75 |
76 | static VALUE id_blob;
77 |
78 | VALUE convertPropertyListRef(CFPropertyListRef plist);
79 | VALUE convertStringRef(CFStringRef plist);
80 | VALUE convertDictionaryRef(CFDictionaryRef plist);
81 | VALUE convertArrayRef(CFArrayRef plist);
82 | VALUE convertNumberRef(CFNumberRef plist);
83 | VALUE convertBooleanRef(CFBooleanRef plist);
84 | VALUE convertDataRef(CFDataRef plist);
85 | VALUE convertDateRef(CFDateRef plist);
86 | VALUE str_blob(VALUE self);
87 | VALUE str_setBlob(VALUE self, VALUE b);
88 |
89 | // Raises a Ruby exception with the given string
90 | void raiseError(CFStringRef error) {
91 | char *errBuffer = (char *)CFStringGetCStringPtr(error, kCFStringEncodingUTF8);
92 | int freeBuffer = 0;
93 | if (!errBuffer) {
94 | int len = CFStringGetLength(error)*2+1;
95 | errBuffer = ALLOC_N(char, len);
96 | Boolean succ = CFStringGetCString(error, errBuffer, len, kCFStringEncodingUTF8);
97 | if (!succ) {
98 | CFStringGetCString(error, errBuffer, len, kCFStringEncodingMacRoman);
99 | }
100 | freeBuffer = 1;
101 | }
102 | rb_raise(ePropertyListError, (char *)errBuffer);
103 | if (freeBuffer) free(errBuffer);
104 | }
105 |
106 | /* call-seq:
107 | * load(obj, format = false)
108 | *
109 | * Loads a property list from an IO stream or a String and creates
110 | * an equivalent Object from it.
111 | *
112 | * If +format+ is +true+, it returns an array of [object, format]
113 | * where +format+ is one of :xml1, :binary1, or :openstep.
114 | */
115 | VALUE plist_load(int argc, VALUE *argv, VALUE self) {
116 | VALUE io, retFormat;
117 | int count = rb_scan_args(argc, argv, "11", &io, &retFormat);
118 | if (count < 2) retFormat = Qfalse;
119 | VALUE buffer;
120 | if (RTEST(rb_respond_to(io, id_read))) {
121 | // Read from IO
122 | buffer = rb_funcall(io, id_read, 0);
123 | } else {
124 | StringValue(io);
125 | buffer = io;
126 | }
127 | // For some reason, the CFReadStream version doesn't work with input < 6 characters
128 | // but the CFDataRef version doesn't return format
129 | // So lets use the CFDataRef version unless format is requested
130 | CFStringRef error = NULL;
131 | CFPropertyListRef plist;
132 | CFPropertyListFormat format;
133 | if (RTEST(retFormat)) {
134 | // Format was requested
135 | // now just in case, if the input is < 6 characters, we will pad it out with newlines
136 | // we could do this in all cases, but I don't think it will work with binary
137 | // even though binary shouldn't be < 6 characters
138 | UInt8 *bytes;
139 | int len;
140 | if (RSTRING_LEN(buffer) < 6) {
141 | bytes = ALLOC_N(UInt8, 6);
142 | memset(bytes, '\n', 6);
143 | MEMCPY(bytes, RSTRING_PTR(buffer), UInt8, RSTRING_LEN(buffer));
144 | len = 6;
145 | } else {
146 | bytes = (UInt8 *)RSTRING_PTR(buffer);
147 | len = RSTRING_LEN(buffer);
148 | }
149 | CFReadStreamRef readStream = CFReadStreamCreateWithBytesNoCopy(kCFAllocatorDefault, bytes, len, kCFAllocatorNull);
150 | CFReadStreamOpen(readStream);
151 | plist = CFPropertyListCreateFromStream(kCFAllocatorDefault, readStream, 0, kCFPropertyListImmutable, &format, &error);
152 | CFReadStreamClose(readStream);
153 | CFRelease(readStream);
154 | } else {
155 | // Format wasn't requested
156 | CFDataRef data = CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, (const UInt8*)RSTRING_PTR(buffer), RSTRING_LEN(buffer), kCFAllocatorNull);
157 | plist = CFPropertyListCreateFromXMLData(kCFAllocatorDefault, data, kCFPropertyListImmutable, &error);
158 | CFRelease(data);
159 | }
160 | if (error) {
161 | raiseError(error);
162 | CFRelease(error);
163 | return Qnil;
164 | }
165 | VALUE obj = convertPropertyListRef(plist);
166 | CFRelease(plist);
167 | if (RTEST(retFormat)) {
168 | VALUE ary = rb_ary_new();
169 | rb_ary_push(ary, obj);
170 | if (format == kCFPropertyListOpenStepFormat) {
171 | retFormat = id_openstep;
172 | } else if (format == kCFPropertyListXMLFormat_v1_0) {
173 | retFormat = id_xml;
174 | } else if (format == kCFPropertyListBinaryFormat_v1_0) {
175 | retFormat = id_binary;
176 | } else {
177 | retFormat = rb_intern("unknown");
178 | }
179 | rb_ary_push(ary, ID2SYM(retFormat));
180 | return ary;
181 | } else {
182 | return obj;
183 | }
184 | }
185 |
186 | // Maps the property list object to a ruby object
187 | VALUE convertPropertyListRef(CFPropertyListRef plist) {
188 | CFTypeID typeID = CFGetTypeID(plist);
189 | if (typeID == CFStringGetTypeID()) {
190 | return convertStringRef((CFStringRef)plist);
191 | } else if (typeID == CFDictionaryGetTypeID()) {
192 | return convertDictionaryRef((CFDictionaryRef)plist);
193 | } else if (typeID == CFArrayGetTypeID()) {
194 | return convertArrayRef((CFArrayRef)plist);
195 | } else if (typeID == CFNumberGetTypeID()) {
196 | return convertNumberRef((CFNumberRef)plist);
197 | } else if (typeID == CFBooleanGetTypeID()) {
198 | return convertBooleanRef((CFBooleanRef)plist);
199 | } else if (typeID == CFDataGetTypeID()) {
200 | return convertDataRef((CFDataRef)plist);
201 | } else if (typeID == CFDateGetTypeID()) {
202 | return convertDateRef((CFDateRef)plist);
203 | } else {
204 | return Qnil;
205 | }
206 | }
207 |
208 | // Converts a CFStringRef to a String
209 | VALUE convertStringRef(CFStringRef plist) {
210 | CFIndex byteCount;
211 | CFRange range = CFRangeMake(0, CFStringGetLength(plist));
212 | CFStringEncoding enc = kCFStringEncodingUTF8;
213 | Boolean succ = CFStringGetBytes(plist, range, enc, 0, false, NULL, 0, &byteCount);
214 | if (!succ) {
215 | enc = kCFStringEncodingMacRoman;
216 | CFStringGetBytes(plist, range, enc, 0, false, NULL, 0, &byteCount);
217 | }
218 | UInt8 *buffer = ALLOC_N(UInt8, byteCount);
219 | CFStringGetBytes(plist, range, enc, 0, false, buffer, byteCount, NULL);
220 | VALUE retval = rb_str_new((char *)buffer, (long)byteCount);
221 | free(buffer);
222 | return retval;
223 | }
224 |
225 | // Converts the keys and values of a CFDictionaryRef
226 | void dictionaryConverter(const void *key, const void *value, void *context) {
227 | rb_hash_aset((VALUE)context, convertPropertyListRef(key), convertPropertyListRef(value));
228 | }
229 |
230 | // Converts a CFDictionaryRef to a Hash
231 | VALUE convertDictionaryRef(CFDictionaryRef plist) {
232 | VALUE hash = rb_hash_new();
233 | CFDictionaryApplyFunction(plist, dictionaryConverter, (void *)hash);
234 | return hash;
235 | }
236 |
237 | // Converts the values of a CFArrayRef
238 | void arrayConverter(const void *value, void *context) {
239 | rb_ary_push((VALUE)context, convertPropertyListRef(value));
240 | }
241 |
242 | // Converts a CFArrayRef to an Array
243 | VALUE convertArrayRef(CFArrayRef plist) {
244 | VALUE array = rb_ary_new();
245 | CFRange range = CFRangeMake(0, CFArrayGetCount(plist));
246 | CFArrayApplyFunction(plist, range, arrayConverter, (void *)array);
247 | return array;
248 | }
249 |
250 | // Converts a CFNumberRef to a Number
251 | VALUE convertNumberRef(CFNumberRef plist) {
252 | if (CFNumberIsFloatType(plist)) {
253 | double val;
254 | CFNumberGetValue(plist, kCFNumberDoubleType, &val);
255 | return rb_float_new(val);
256 | } else {
257 | #ifdef LL2NUM
258 | long long val;
259 | CFNumberGetValue(plist, kCFNumberLongLongType, &val);
260 | return LL2NUM(val);
261 | #else
262 | long val;
263 | CFNumberGetValue(plist, kCFNumberLongType, &val);
264 | return LONG2NUM(val);
265 | #endif
266 | }
267 | }
268 |
269 | // Converts a CFBooleanRef to a Boolean
270 | VALUE convertBooleanRef(CFBooleanRef plist) {
271 | if (CFBooleanGetValue(plist)) {
272 | return Qtrue;
273 | } else {
274 | return Qfalse;
275 | }
276 | }
277 |
278 | // Converts a CFDataRef to a String (with blob set to true)
279 | VALUE convertDataRef(CFDataRef plist) {
280 | const UInt8 *bytes = CFDataGetBytePtr(plist);
281 | CFIndex len = CFDataGetLength(plist);
282 | VALUE str = rb_str_new((char *)bytes, (long)len);
283 | str_setBlob(str, Qtrue);
284 | return str;
285 | }
286 |
287 | // Converts a CFDateRef to a Time
288 | VALUE convertDateRef(CFDateRef plist) {
289 | CFAbsoluteTime seconds = CFDateGetAbsoluteTime(plist);
290 |
291 | // trunace the time since Ruby's Time object stores it as a 32 bit signed offset from 1970 (undocumented)
292 | const float min_time = -3124310400.0f;
293 | const float max_time = 1169098047.0f;
294 | seconds = seconds < min_time ? min_time : (seconds > max_time ? max_time : seconds);
295 |
296 | return rb_funcall(timeEpoch, id_plus, 1, rb_float_new(seconds));
297 | }
298 |
299 | CFPropertyListRef convertObject(VALUE obj);
300 |
301 | // Converts a PropertyList object to a string representation
302 | VALUE convertPlistToString(CFPropertyListRef plist, CFPropertyListFormat format) {
303 | CFWriteStreamRef writeStream = CFWriteStreamCreateWithAllocatedBuffers(kCFAllocatorDefault, kCFAllocatorDefault);
304 | CFWriteStreamOpen(writeStream);
305 | CFStringRef error = NULL;
306 | CFPropertyListWriteToStream(plist, writeStream, format, &error);
307 | CFWriteStreamClose(writeStream);
308 | if (error) {
309 | raiseError(error);
310 | return Qnil;
311 | }
312 | CFDataRef data = CFWriteStreamCopyProperty(writeStream, kCFStreamPropertyDataWritten);
313 | CFRelease(writeStream);
314 | VALUE plistData = convertDataRef(data);
315 | CFRelease(data);
316 | return plistData;
317 | }
318 |
319 | /* call-seq:
320 | * dump(io, obj, format = :xml1)
321 | *
322 | * Writes the property list representation of +obj+
323 | * to the IO stream (must be open for writing).
324 | *
325 | * +format+ can be one of :xml1 or :binary1.
326 | *
327 | * Returns the number of bytes written, or +nil+ if
328 | * the object could not be represented as a property list
329 | */
330 | VALUE plist_dump(int argc, VALUE *argv, VALUE self) {
331 | VALUE io, obj, type;
332 | int count = rb_scan_args(argc, argv, "21", &io, &obj, &type);
333 | if (count < 3) {
334 | type = id_xml;
335 | } else {
336 | type = rb_to_id(type);
337 | }
338 | if (!RTEST(rb_respond_to(io, id_write))) {
339 | rb_raise(rb_eArgError, "Argument 1 must be an IO object");
340 | return Qnil;
341 | }
342 | CFPropertyListFormat format;
343 | if (type == id_xml) {
344 | format = kCFPropertyListXMLFormat_v1_0;
345 | } else if (type == id_binary) {
346 | format = kCFPropertyListBinaryFormat_v1_0;
347 | } else if (type == id_openstep) {
348 | format = kCFPropertyListOpenStepFormat;
349 | } else {
350 | rb_raise(rb_eArgError, "Argument 3 must be one of :xml1, :binary1, or :openstep");
351 | return Qnil;
352 | }
353 | CFPropertyListRef plist = convertObject(obj);
354 | VALUE data = convertPlistToString(plist, format);
355 | if (NIL_P(data)) {
356 | return Qnil;
357 | } else {
358 | return rb_funcall(io, id_write, 1, data);
359 | }
360 | }
361 |
362 | /* call-seq:
363 | * object.to_plist(format = :xml1)
364 | *
365 | * Converts the object to a property list representation
366 | * and returns it as a string.
367 | *
368 | * +format+ can be one of :xml1 or :binary1.
369 | */
370 | VALUE obj_to_plist(int argc, VALUE *argv, VALUE self) {
371 | VALUE type;
372 | int count = rb_scan_args(argc, argv, "01", &type);
373 | if (count < 1) {
374 | type = id_xml;
375 | } else {
376 | type = rb_to_id(type);
377 | }
378 | CFPropertyListFormat format;
379 | if (type == id_xml) {
380 | format = kCFPropertyListXMLFormat_v1_0;
381 | } else if (type == id_binary) {
382 | format = kCFPropertyListBinaryFormat_v1_0;
383 | } else if (type == id_openstep) {
384 | format = kCFPropertyListOpenStepFormat;
385 | } else {
386 | rb_raise(rb_eArgError, "Argument 2 must be one of :xml1, :binary1, or :openstep");
387 | return Qnil;
388 | }
389 | CFPropertyListRef plist = convertObject(self);
390 | VALUE data = convertPlistToString(plist, format);
391 | CFRelease(plist);
392 | if (type == id_xml || type == id_binary) {
393 | str_setBlob(data, Qfalse);
394 | }
395 | return data;
396 | }
397 |
398 | CFPropertyListRef convertString(VALUE obj);
399 | CFDictionaryRef convertHash(VALUE obj);
400 | CFArrayRef convertArray(VALUE obj);
401 | CFNumberRef convertNumber(VALUE obj);
402 | CFDateRef convertTime(VALUE obj);
403 |
404 | // Converts an Object to a CFTypeRef
405 | CFPropertyListRef convertObject(VALUE obj) {
406 | switch (TYPE(obj)) {
407 | case T_STRING: return convertString(obj); break;
408 | case T_HASH: return convertHash(obj); break;
409 | case T_ARRAY: return convertArray(obj); break;
410 | case T_FLOAT:
411 | case T_FIXNUM:
412 | case T_BIGNUM: return convertNumber(obj); break;
413 | case T_TRUE: return kCFBooleanTrue; break;
414 | case T_FALSE: return kCFBooleanFalse; break;
415 | default: if (rb_obj_is_kind_of(obj, rb_cTime)) return convertTime(obj);
416 | }
417 | rb_raise(rb_eArgError, "An object in the argument tree could not be converted");
418 | return NULL;
419 | }
420 |
421 | // Converts a String to a CFStringRef
422 | CFPropertyListRef convertString(VALUE obj) {
423 | if (RTEST(str_blob(obj))) {
424 | // convert to CFDataRef
425 | StringValue(obj);
426 | CFDataRef data = CFDataCreate(kCFAllocatorDefault, (const UInt8*)RSTRING_PTR(obj), (CFIndex)RSTRING_LEN(obj));
427 | return data;
428 | } else {
429 | // convert to CFStringRef
430 | StringValue(obj);
431 | CFStringRef string = CFStringCreateWithBytes(kCFAllocatorDefault, (const UInt8*)RSTRING_PTR(obj), (CFIndex)RSTRING_LEN(obj), kCFStringEncodingUTF8, false);
432 | if (!string) {
433 | // try MacRoman
434 | string = CFStringCreateWithBytes(kCFAllocatorDefault, (const UInt8*)RSTRING_PTR(obj), (CFIndex)RSTRING_LEN(obj), kCFStringEncodingMacRoman, false);
435 | }
436 | return string;
437 | }
438 | }
439 |
440 | // Converts the keys and values of a Hash to CFTypeRefs
441 | int iterateHash(VALUE key, VALUE val, VALUE dict) {
442 | CFPropertyListRef dKey = convertObject(key);
443 | CFPropertyListRef dVal = convertObject(val);
444 | CFDictionaryAddValue((CFMutableDictionaryRef)dict, dKey, dVal);
445 | CFRelease(dKey);
446 | CFRelease(dVal);
447 | return ST_CONTINUE;
448 | }
449 |
450 | // Converts a Hash to a CFDictionaryREf
451 | CFDictionaryRef convertHash(VALUE obj) {
452 | // RHASH_TBL exists in ruby 1.8.7 but not ruby 1.8.6
453 | #ifdef RHASH_TBL
454 | st_table *tbl = RHASH_TBL(obj);
455 | #else
456 | st_table *tbl = RHASH(obj)->tbl;
457 | #endif
458 | CFIndex count = (CFIndex)tbl->num_entries;
459 | CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorDefault, count, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
460 | st_foreach(tbl, iterateHash, (VALUE)dict);
461 | return dict;
462 | }
463 |
464 | // Converts an Array to a CFArrayRef
465 | CFArrayRef convertArray(VALUE obj) {
466 | CFIndex count = (CFIndex)RARRAY_LEN(obj);
467 | CFMutableArrayRef array = CFArrayCreateMutable(kCFAllocatorDefault, count, &kCFTypeArrayCallBacks);
468 | int i;
469 | for (i = 0; i < count; i++) {
470 | CFPropertyListRef aVal = convertObject(RARRAY_PTR(obj)[i]);
471 | CFArrayAppendValue(array, aVal);
472 | CFRelease(aVal);
473 | }
474 | return array;
475 | }
476 |
477 | // Converts a Number to a CFNumberRef
478 | CFNumberRef convertNumber(VALUE obj) {
479 | void *valuePtr;
480 | CFNumberType type;
481 | switch (TYPE(obj)) {
482 | case T_FLOAT: {
483 | double num = NUM2DBL(obj);
484 | valuePtr = #
485 | type = kCFNumberDoubleType;
486 | break;
487 | }
488 | case T_FIXNUM: {
489 | int num = NUM2INT(obj);
490 | valuePtr = #
491 | type = kCFNumberIntType;
492 | break;
493 | }
494 | case T_BIGNUM: {
495 | #ifdef NUM2LL
496 | long long num = NUM2LL(obj);
497 | type = kCFNumberLongLongType;
498 | #else
499 | long num = NUM2LONG(obj);
500 | type = kCFNumberLongType;
501 | #endif
502 | valuePtr = #
503 | break;
504 | }
505 | default:
506 | rb_raise(rb_eStandardError, "ERROR: Wrong object type passed to convertNumber");
507 | return NULL;
508 | }
509 | CFNumberRef number = CFNumberCreate(kCFAllocatorDefault, type, valuePtr);
510 | return number;
511 | }
512 |
513 | // Converts a Time to a CFDateRef
514 | CFDateRef convertTime(VALUE obj) {
515 | VALUE secs = rb_funcall(obj, id_minus, 1, timeEpoch);
516 | CFDateRef date = CFDateCreate(kCFAllocatorDefault, NUM2DBL(secs));
517 | return date;
518 | }
519 |
520 | /* call-seq:
521 | * str.blob?
522 | *
523 | * Returns whether or not +str+ is a blob.
524 | */
525 | VALUE str_blob(VALUE self) {
526 | VALUE blob = rb_attr_get(self, id_blob);
527 | if (NIL_P(blob)) {
528 | return Qfalse;
529 | } else {
530 | return blob;
531 | }
532 | }
533 |
534 | /* call-seq:
535 | * str.blob = bool
536 | *
537 | * Sets the blob status of +str+.
538 | */
539 | VALUE str_setBlob(VALUE self, VALUE b) {
540 | if (TYPE(b) == T_TRUE || TYPE(b) == T_FALSE) {
541 | return rb_ivar_set(self, id_blob, b);
542 | } else {
543 | rb_raise(rb_eArgError, "Argument 1 must be true or false");
544 | return Qnil;
545 | }
546 | }
547 |
548 | /*
549 | * Document-module: OSX
550 | */
551 |
552 | /*
553 | * Document-module: OSX::PropertyList
554 | *
555 | * The PropertyList module provides a means of converting a
556 | * Ruby Object to a Property List.
557 | *
558 | * The various Objects that can be converted are the ones
559 | * with an equivalent in CoreFoundation. This includes: String,
560 | * Integer, Float, Boolean, Time, Hash, and Array.
561 | *
562 | * See also: String#blob?, String#blob=, and Object#to_plist
563 | */
564 |
565 | /*
566 | * Document-class: OSX::PropertyListError
567 | */
568 | void Init_plist() {
569 | mOSX = rb_define_module("OSX");
570 | mPlist = rb_define_module_under(mOSX, "PropertyList");
571 | rb_define_module_function(mPlist, "load", plist_load, -1);
572 | rb_define_module_function(mPlist, "dump", plist_dump, -1);
573 | rb_define_method(rb_cObject, "to_plist", obj_to_plist, -1);
574 | rb_define_method(rb_cString, "blob?", str_blob, 0);
575 | rb_define_method(rb_cString, "blob=", str_setBlob, 1);
576 | ePropertyListError = rb_define_class_under(mOSX, "PropertyListError", rb_eStandardError);
577 | id_gm = rb_intern("gm");
578 | timeEpoch = rb_funcall(rb_cTime, id_gm, 1, INT2FIX(2001));
579 | /* Time.gm(2001): The Cocoa epoch of January 1st, 2001*/
580 | rb_define_const(mPlist, "EPOCH", timeEpoch);
581 | id_plus = rb_intern("+");
582 | id_minus = rb_intern("-");
583 | id_read = rb_intern("read");
584 | id_write = rb_intern("write");
585 | id_xml = rb_intern("xml1");
586 | id_binary = rb_intern("binary1");
587 | id_openstep = rb_intern("openstep");
588 | id_blob = rb_intern("@blob");
589 | }
590 |
--------------------------------------------------------------------------------