osx-plist 1.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +12 -0
- data/Manifest.txt +10 -0
- data/README.txt +92 -0
- data/Rakefile +16 -0
- data/ext/plist/extconf.rb +5 -0
- data/ext/plist/plist.c +589 -0
- data/lib/osx/plist.rb +18 -0
- data/test/fixtures/xml_plist +27 -0
- data/test/suite.rb +6 -0
- data/test/test_plist.rb +88 -0
- metadata +94 -0
data/History.txt
ADDED
@@ -0,0 +1,12 @@
|
|
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
|
data/Manifest.txt
ADDED
data/README.txt
ADDED
@@ -0,0 +1,92 @@
|
|
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 <data> property list item. When dumping to a property list, any strings that are blobs are written as <data> items rather than <string> 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.
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
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
|
data/ext/plist/plist.c
ADDED
@@ -0,0 +1,589 @@
|
|
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 <ruby.h>
|
47
|
+
#if HAVE_RUBY_ST_H
|
48
|
+
#include <ruby/st.h>
|
49
|
+
#else
|
50
|
+
#include <st.h>
|
51
|
+
#endif
|
52
|
+
#include <CoreFoundation/CoreFoundation.h>
|
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 <tt>[object, format]</tt>
|
113
|
+
* where +format+ is one of <tt>:xml1</tt>, <tt>:binary1</tt>, or <tt>:openstep</tt>.
|
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 <tt>:xml1</tt> or <tt>:binary1</tt>.
|
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 <tt>:xml1</tt> or <tt>:binary1</tt>.
|
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
|
+
}
|
data/lib/osx/plist.rb
ADDED
@@ -0,0 +1,18 @@
|
|
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
|
@@ -0,0 +1,27 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/OSX::PropertyList-1.0.dtd">
|
3
|
+
<plist version="1.0">
|
4
|
+
<dict>
|
5
|
+
<key>string!</key>
|
6
|
+
<string>indeedy</string>
|
7
|
+
<key>bar</key>
|
8
|
+
<array>
|
9
|
+
<integer>1</integer>
|
10
|
+
<integer>2</integer>
|
11
|
+
<integer>3</integer>
|
12
|
+
</array>
|
13
|
+
<key>foo</key>
|
14
|
+
<dict>
|
15
|
+
<key>correct?</key>
|
16
|
+
<true/>
|
17
|
+
<key>pi</key>
|
18
|
+
<real>3.14159265</real>
|
19
|
+
<key>random</key>
|
20
|
+
<data>
|
21
|
+
I0VniQ==
|
22
|
+
</data>
|
23
|
+
<key>today</key>
|
24
|
+
<date>2005-04-28T06:32:56Z</date>
|
25
|
+
</dict>
|
26
|
+
</dict>
|
27
|
+
</plist>
|
data/test/suite.rb
ADDED
data/test/test_plist.rb
ADDED
@@ -0,0 +1,88 @@
|
|
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
|
metadata
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: osx-plist
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 17
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
- 3
|
10
|
+
version: 1.0.3
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Kevin Ballard
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2009-09-21 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: hoe
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 5
|
29
|
+
segments:
|
30
|
+
- 2
|
31
|
+
- 3
|
32
|
+
- 3
|
33
|
+
version: 2.3.3
|
34
|
+
type: :development
|
35
|
+
version_requirements: *id001
|
36
|
+
description: osx-plist is a Ruby library for manipulating Property Lists natively using the built-in support in OS X.
|
37
|
+
email:
|
38
|
+
- kevin@sb.org
|
39
|
+
executables: []
|
40
|
+
|
41
|
+
extensions:
|
42
|
+
- ext/plist/extconf.rb
|
43
|
+
extra_rdoc_files:
|
44
|
+
- History.txt
|
45
|
+
- Manifest.txt
|
46
|
+
- README.txt
|
47
|
+
files:
|
48
|
+
- History.txt
|
49
|
+
- Manifest.txt
|
50
|
+
- README.txt
|
51
|
+
- Rakefile
|
52
|
+
- ext/plist/extconf.rb
|
53
|
+
- ext/plist/plist.c
|
54
|
+
- lib/osx/plist.rb
|
55
|
+
- test/fixtures/xml_plist
|
56
|
+
- test/suite.rb
|
57
|
+
- test/test_plist.rb
|
58
|
+
homepage: http://github.com/kballard/osx-plist
|
59
|
+
licenses: []
|
60
|
+
|
61
|
+
post_install_message:
|
62
|
+
rdoc_options:
|
63
|
+
- --main
|
64
|
+
- README.txt
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
- ext
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
hash: 3
|
74
|
+
segments:
|
75
|
+
- 0
|
76
|
+
version: "0"
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
none: false
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
hash: 3
|
83
|
+
segments:
|
84
|
+
- 0
|
85
|
+
version: "0"
|
86
|
+
requirements: []
|
87
|
+
|
88
|
+
rubyforge_project: osx-plist
|
89
|
+
rubygems_version: 1.8.21
|
90
|
+
signing_key:
|
91
|
+
specification_version: 3
|
92
|
+
summary: Property List manipulation for OS X
|
93
|
+
test_files:
|
94
|
+
- test/test_plist.rb
|