osx-plist 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|