cfplist 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 J. Morgan Lieberthal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,97 @@
1
+ # CFPlist
2
+
3
+ Native bindings for CoreFoundation Property List files in ruby.
4
+
5
+ Note this this gem will only work on platforms where the
6
+ `CoreFoundation.framework` is present, which means macOS.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'cfplist'
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ $ bundle install
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install cfplist
23
+
24
+ ## Usage
25
+
26
+ To load a property list from a string, do this:
27
+
28
+ ```ruby
29
+ require "cfplist"
30
+
31
+ data = File.read("/path/to/whatever.plist")
32
+ plist = CFPlist.parse(data)
33
+
34
+ ```
35
+ This will return either an `Array` or a `Hash`, depending on the structure of
36
+ the property list.
37
+
38
+
39
+ To generate a property list from an an `Array` or `Hash`, do this:
40
+
41
+ ```ruby
42
+ require "cfplist"
43
+
44
+ my_hash = { "foo" => "bar", "baz" => "quux" }
45
+ data = CFPlist.generate(my_hash) # => data is a string containing the generated plist
46
+
47
+ File.open("/path/to/whatever.plist", "r") do |io|
48
+ io.write(data)
49
+ end
50
+
51
+ ```
52
+
53
+
54
+ The following methods are also implemented for compatibility with the `json` gem
55
+ and the `Marshal` API:
56
+
57
+ * `.[](object, opts = {})`
58
+ - If _object_ is string-like, parse the string and return the parsed result as
59
+ a ruby data structure. Otherwise, generate Property List text from the Ruby
60
+ data structure and return it.
61
+
62
+ * `.dump(object, an_io = nil, limit = nil)`
63
+ - Dumps _obj_ as a Property List string, i.e. calls `.generate` on the object
64
+ and returns the result.
65
+ - If `an_io` (an IO-like object or an object that responds to the #write)
66
+ method was given, the resulting Property List is written to it.
67
+ - If the number of nested arrays or objects exceeds limit, an ArgumentError
68
+ exception is raised. This argument is similar (but not exactly the same!)
69
+ to the limit argument in Marshal.dump.
70
+
71
+ * `.load(source, proc = nil, options = {})`
72
+ - Load a ruby data structure from a Property List source and return it.
73
+ A source can either be a string-like object, an IO-like object, or an object
74
+ responding to the read method. If proc was given, it will be called with any
75
+ nested Ruby object as an argument recursively in depth first order.
76
+ - BEWARE: This method is meant to serialise data from trusted user input,
77
+ like from your own database server or clients under your control, it could
78
+ be dangerous to allow untrusted users to pass JSON sources into it.
79
+
80
+ ## Development
81
+
82
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
83
+
84
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
85
+
86
+ ## Contributing
87
+
88
+ Bug reports and pull requests are welcome on GitHub at https://github.com/baberthal/cfplist. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/baberthal/cfplist/blob/prime/CODE_OF_CONDUCT.md).
89
+
90
+
91
+ ## License
92
+
93
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
94
+
95
+ ## Code of Conduct
96
+
97
+ Everyone interacting in the Cfplist project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/baberthal/cfplist/blob/prime/CODE_OF_CONDUCT.md).
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rake/extensiontask"
9
+
10
+ task build: :compile
11
+
12
+ Rake::ExtensionTask.new("cfplist") do |ext|
13
+ ext.lib_dir = "lib/cfplist"
14
+ end
15
+
16
+ task default: %i[clobber compile spec]
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "cfplist"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ require "pry"
11
+ Pry.start
12
+
13
+ # require "irb"
14
+ # IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/cfplist/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "cfplist"
7
+ spec.version = CFPlist::VERSION
8
+ spec.authors = ["J. Morgan Lieberthal"]
9
+ spec.email = ["j.morgan.lieberthal@gmail.com"]
10
+
11
+ spec.summary = "CoreFoundation PropertyList Native Bindings"
12
+ spec.description = "Native bindings for CoreFoundation PropertyList " \
13
+ "files. Note that this gem requires the CoreFoundation framework " \
14
+ "be present on the system, so it will only work on macOS."
15
+ spec.homepage = "https://github.com/baberthal/cfplist"
16
+ spec.license = "MIT"
17
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0")
18
+
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = spec.homepage
21
+ spec.metadata["changelog_uri"] = "https://github.com/baberthal/cfplist/blob/prime/CHANGELOG"
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
+ %x(git ls-files -z).split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
27
+ end
28
+ spec.bindir = "exe"
29
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ["lib"]
31
+ spec.extensions = ["ext/cfplist/extconf.rb"]
32
+ end
@@ -0,0 +1,30 @@
1
+ [
2
+ {
3
+ "directory": "/Users/morgan/Projects/cfplist/tmp/x86_64-darwin17/cfplist/2.7.2",
4
+ "arguments": [
5
+ "clang",
6
+ "-I.",
7
+ "-I/Users/morgan/.rbenv/versions/2.7.2/include/ruby-2.7.0/x86_64-darwin17",
8
+ "-I/Users/morgan/.rbenv/versions/2.7.2/include/ruby-2.7.0/ruby/backward",
9
+ "-I/Users/morgan/.rbenv/versions/2.7.2/include/ruby-2.7.0",
10
+ "-I../../../../ext/cfplist",
11
+ "-DHAVE_FRAMEWORK_COREFOUNDATION",
12
+ "-DHAVE_COREFOUNDATION_COREFOUNDATION_H",
13
+ "-I/Users/morgan/.rbenv/versions/2.7.2/include",
14
+ "-D_XOPEN_SOURCE",
15
+ "-D_DARWIN_C_SOURCE",
16
+ "-D_DARWIN_UNLIMITED_SELECT",
17
+ "-D_REENTRANT",
18
+ "-fno-common",
19
+ "-O3",
20
+ "-Wno-error=shorten-64-to-32",
21
+ "-fno-common",
22
+ "-pipe",
23
+ "-o",
24
+ "cfplist.o",
25
+ "-c",
26
+ "../../../../ext/cfplist/cfplist.c"
27
+ ],
28
+ "file": "../../../../ext/cfplist/cfplist.c"
29
+ }
30
+ ]
@@ -0,0 +1,718 @@
1
+ //===- cfplist.c - Main c implementation file for cfplist ext ---*- C -*-===//
2
+ //
3
+ // This source file is part of the cfplist open source project.
4
+ //
5
+ // Copyright (c) 2020 J. Morgan Lieberthal and the cfplist authors
6
+ // Licensed under Apache License, Version 2.0
7
+ //
8
+ //===----------------------------------------------------------------------===//
9
+
10
+ #include <CoreFoundation/CoreFoundation.h>
11
+
12
+ #include "ruby.h"
13
+ #include "ruby/intern.h"
14
+ #include "ruby/ruby.h"
15
+
16
+ /*******************************************************************************
17
+ * Macros *
18
+ *******************************************************************************/
19
+
20
+ #define CF2RB(X) corefoundation_to_ruby((X))
21
+ #define CFSTR2RB(STR) rb_CFString_convert((STR))
22
+ #define CFDATA2RB(D) rb_CFData_convert((D))
23
+ #define CFDATE2RB(D) rb_CFDate_convert((D))
24
+ #define CFBOOL2RB(B) rb_CFBoolean_convert((B))
25
+ #define CFNUM2RB(N) rb_CFNumber_convert((N))
26
+ #define CFARR2RB(A) rb_CFArray_convert((A))
27
+ #define CFDICT2RB(D) rb_CFDictionary_convert((D))
28
+
29
+ #define CFSTR2SYM(STR) rb_to_symbol(rb_CFString_convert((STR)))
30
+ #define CFINTERN(X) rb_CFString_intern((X))
31
+
32
+ #define cfcheckmem(PTR, M, ...) \
33
+ do { \
34
+ if ((PTR) == NULL) { \
35
+ rb_raise(rb_eNoMemError, M##__VA_ARGS__); \
36
+ } \
37
+ } while (0)
38
+
39
+ #define IS_DOMAIN(S, D) (CFStringCompare((S), (D), 0) == kCFCompareEqualTo)
40
+
41
+ /*******************************************************************************
42
+ * Declarations *
43
+ *******************************************************************************/
44
+
45
+ static VALUE
46
+ corefoundation_to_ruby(CFTypeRef cf_type);
47
+
48
+ static CFTypeRef
49
+ ruby_to_corefoundation(VALUE obj);
50
+
51
+ static void
52
+ rb_raise_CFError(CFErrorRef error);
53
+
54
+ VALUE rb_mCFPlist;
55
+ VALUE rb_eCFError;
56
+ VALUE rb_eCFErrorOSStatus;
57
+ VALUE rb_eCFErrorMach;
58
+ VALUE rb_eCFErrorCocoa;
59
+ static ID id_to_s, id_keys, id_vals, id_count;
60
+
61
+ /*******************************************************************************
62
+ * CoreFoundation Type => Ruby Object *
63
+ *******************************************************************************/
64
+
65
+ /*
66
+ * Rather than calling CFStringGetCStringPtr here, we want to actually copy
67
+ * the memory of the string represented by the CFStringRef. This is because
68
+ * these CF functions we are calling follow the "Get Rule", meaning that we
69
+ * don't "own" these objects, and their memory could be freed by
70
+ * CoreFoundation at any time.
71
+ *
72
+ * Even though the function we call hash 'Get' in the name, it does
73
+ * actually perform a copy of the string, according to the documentation.
74
+ */
75
+ static inline VALUE
76
+ rb_CFString_convert(CFStringRef str)
77
+ {
78
+ CFRetain(str); /* retain the string for the duration of this function */
79
+
80
+ CFIndex len = CFStringGetLength(str); /* get length of string */
81
+ CFIndex buflen = len + 1; /* size of our char buffer, plus NULL terminator */
82
+
83
+ /*
84
+ * Allocate a char buffer to hold the string. Size is 1+len to account for the
85
+ * null terminator. sizeof(char) is 1, but I feel this more accurately depicts
86
+ * what we are doing.
87
+ */
88
+ char *cstr = calloc((1 + len), sizeof(char));
89
+
90
+ Boolean success;
91
+ /* get the characters from the string, using UTF8 encoding. */
92
+ success = CFStringGetCString(str, cstr, buflen, kCFStringEncodingUTF8);
93
+
94
+ if (!success) {
95
+ /* TODO: Raise a better error here. */
96
+ rb_raise(rb_eRuntimeError, "Unable to convert string to UTF8");
97
+ }
98
+
99
+ CFRelease(str); /* matches retain from the beginning of this function. */
100
+
101
+ /* Let ruby know this string is UTF8 encoded. */
102
+ return rb_utf8_str_new(cstr, (long)len);
103
+ }
104
+
105
+ /* This isn't ideal, but since Ruby handles unknown byte sequences as a string,
106
+ * we will too. To make this readable, you'll likely have to call String#unpack
107
+ * on the result.
108
+ *
109
+ * TODO: Figure out a safe way to Marshal this data
110
+ */
111
+ static inline VALUE
112
+ rb_CFData_convert(CFDataRef data)
113
+ {
114
+ CFRetain(data); /* retain the data for the duration of this function */
115
+
116
+ CFIndex len = CFDataGetLength(data); /* get length of data, in bytes */
117
+ CFRange rng = CFRangeMake(0, len); /* range of bytes to copy (i.e. all) */
118
+
119
+ /* allocate our data buffer on the heap, unlike the analogous CFString
120
+ * function, we don't need to account for a NUL terminator
121
+ */
122
+ uint8_t *databuf = calloc(len, sizeof(uint8_t)); /* yes, I know it's 1... */
123
+ cfcheckmem(databuf, "Unable to allocate data buffer for CFData\n");
124
+
125
+ /* this function doesn't return anything, so no real error checking */
126
+ CFDataGetBytes(data, rng, databuf); /* copy the bytes */
127
+
128
+ CFRelease(data); /* matches retain above */
129
+ /* return a tainted string, because this data could be literally anything */
130
+ return rb_tainted_str_new((const char *)databuf, len);
131
+ }
132
+
133
+ /* why is CFBoolean a thing? I think this one is pretty self-explanatory. */
134
+ static inline VALUE
135
+ rb_CFBoolean_convert(CFBooleanRef boolean)
136
+ {
137
+ Boolean val = CFBooleanGetValue(boolean);
138
+ if (val) {
139
+ return Qtrue;
140
+ }
141
+ return Qfalse;
142
+ }
143
+
144
+ /*
145
+ * Convert a CFNumber into a ruby number.
146
+ */
147
+ static inline VALUE
148
+ rb_CFNumber_convert(CFNumberRef number)
149
+ {
150
+ CFRetain(number); /* retain for the duration of this function */
151
+ intptr_t rawval; /* get a value big enough to hold a pointer to an int */
152
+ CFNumberType num_type = CFNumberGetType(number); /* what kind of number? */
153
+ Boolean success = CFNumberGetValue(number, num_type, &rawval);
154
+ VALUE result = Qnil; /* assume it will be nil, in case we can't convert */
155
+
156
+ if (!success) {
157
+ CFRelease(number);
158
+ return result; /* TODO: raise here? */
159
+ }
160
+
161
+ switch (num_type) {
162
+ case kCFNumberSInt8Type: /* char */
163
+ case kCFNumberCharType: /* char */
164
+ result = LONG2FIX(rawval); /* smaller than 32-bit, so use FIXNUM */
165
+ break;
166
+
167
+ case kCFNumberShortType: /* short */
168
+ case kCFNumberSInt16Type: /* short */
169
+ case kCFNumberSInt32Type: /* int */
170
+ case kCFNumberIntType: /* int */
171
+ result = INT2FIX(rawval); /* smaller than 32-bit, so use FIXNUM */
172
+ break;
173
+
174
+ case kCFNumberLongType: /* long */
175
+ case kCFNumberNSIntegerType: /* long */
176
+ case kCFNumberCFIndexType: /* signed long */
177
+ result = LONG2NUM(rawval); /* maybe 64-bit, so use Numeric */
178
+ break;
179
+
180
+ case kCFNumberSInt64Type: /* long long */
181
+ case kCFNumberLongLongType: /* long long */
182
+ result = LL2NUM(rawval); /* maybe 64-bit, so use Numeric */
183
+ break;
184
+
185
+ case kCFNumberFloatType: /* float */
186
+ case kCFNumberDoubleType: /* double */
187
+ case kCFNumberFloat64Type: /* 64-bit floating point (MacTypes.h)*/
188
+ case kCFNumberFloat32Type: /* 32-bit floating point (MacTypes.h)*/
189
+ case kCFNumberCGFloatType: /* float or double */
190
+ result = DBL2NUM(rawval); /* maybe 64-bit, so use Numeric */
191
+ break;
192
+ }
193
+
194
+ CFRelease(number); /* matches retain above */
195
+ return result;
196
+ }
197
+
198
+ static VALUE
199
+ rb_CFArray_convert(CFArrayRef array)
200
+ {
201
+ CFRetain(array); /* retain for the duration of this function */
202
+ CFIndex i, count = CFArrayGetCount(array); /* number of elements in array */
203
+
204
+ /* faster to allocate the memory for our ruby array in one shot */
205
+ VALUE result = rb_ary_new_capa(count);
206
+
207
+ /* loop over the array, and coerce the types to ruby types */
208
+ for (i = 0; i < count; i++) {
209
+ CFTypeRef val = CFArrayGetValueAtIndex(array, i);
210
+ /* NOTE: this macro copies the memory of the values */
211
+ rb_ary_push(result, CF2RB(val));
212
+ }
213
+
214
+ CFRelease(array); /* matches retain above */
215
+
216
+ return result;
217
+ }
218
+
219
+ static VALUE
220
+ rb_CFDictionary_convert(CFDictionaryRef dict, bool keys2sym)
221
+ {
222
+ CFRetain(dict); /* retain for the duration */
223
+ VALUE result = rb_hash_new(); /* create a new hash */
224
+ CFIndex i, count = CFDictionaryGetCount(dict); /* count of y<->v pairs */
225
+
226
+ /* Since we are getting these keys from a Property List, they are
227
+ * guaranteed to be CFStrings, and we are safe to allocate this 'array'
228
+ * of
229
+ * CFStringRefs. If they weren't guaranteed to be CFStrings, we would
230
+ * have
231
+ * to do something else.
232
+ */
233
+ CFStringRef *keys = calloc(count, sizeof(CFStringRef));
234
+
235
+ /* The only guarantee about the values our CFDictionaryRef contains
236
+ * is that they will be one of the following:
237
+ * - CFData
238
+ * - CFString
239
+ * - CFArray
240
+ * - CFDictionary
241
+ * - CFDate
242
+ * - CFBoolean
243
+ * - CFNumber
244
+ * Since we won't know beforehand, we need to allocate an 'array' of
245
+ * CFTypeRefs to hold them.
246
+ */
247
+ CFTypeRef *values = calloc(count, sizeof(CFTypeRef));
248
+
249
+ /* Get the keys and values from the CFDictionary. Note that ownership
250
+ * follows the "Get Rule", so we will need to copy the values during
251
+ * their
252
+ * own conversion functions.
253
+ */
254
+ CFDictionaryGetKeysAndValues(dict, (const void **)keys,
255
+ (const void **)values);
256
+ /* loop through keys and values, convert to ruby objects, and add them
257
+ * to
258
+ * our hash
259
+ */
260
+ for (i = 0; i < count; i++) {
261
+ if (keys2sym) { /* convert the CFStringRefs to Symbols */
262
+ rb_hash_aset(result, CFSTR2SYM(keys[i]), CF2RB(values[i]));
263
+ } else { /* just use plain strings as the keys */
264
+ rb_hash_aset(result, CFSTR2RB(keys[i]), CF2RB(values[i]));
265
+ }
266
+ }
267
+
268
+ CFRelease(dict); /* matches retain above */
269
+ return result;
270
+ }
271
+
272
+ static inline VALUE
273
+ rb_CFDate_convert(CFDateRef date)
274
+ {
275
+ /* WHY IS THERE NO CFDateGetTimeIntervalSince1970 ??? */
276
+ /* This function returns a representation a specific point in time
277
+ * relative to the "absolute reference date of 1 Jan 2001 00:00:00 GMT."
278
+ *
279
+ * How arbitrary...
280
+ */
281
+ CFAbsoluteTime abstime = CFDateGetAbsoluteTime(date);
282
+ /* add seconds between epoch and our 'reference date' to the time
283
+ * interval,
284
+ * so we can get the time interval in seconds since epoch
285
+ */
286
+ CFTimeInterval since_epoch = abstime + kCFAbsoluteTimeIntervalSince1970;
287
+ return rb_time_new(since_epoch, 0); /* return the time object */
288
+ }
289
+
290
+ static VALUE
291
+ corefoundation_to_ruby(CFTypeRef cf_type)
292
+ {
293
+ /*
294
+ * - CFData <=>
295
+ * - CFString <=> String
296
+ * - CFArray <=> Array
297
+ * - CFDictionary <=> Hash
298
+ * - CFDate <=> Date/Time
299
+ * - CFBoolean <=> TrueClass, FalseClass
300
+ * - CFNumber <=> Numeric
301
+ */
302
+ if (!cf_type)
303
+ return Qnil; /* nil if NULL */
304
+
305
+ CFTypeID tid = CFGetTypeID(cf_type);
306
+ if (tid == CFDataGetTypeID()) {
307
+ return rb_CFData_convert(cf_type);
308
+ } else if (tid == CFStringGetTypeID()) {
309
+ return rb_CFString_convert(cf_type);
310
+ } else if (tid == CFArrayGetTypeID()) {
311
+ return rb_CFArray_convert(cf_type);
312
+ } else if (tid == CFDictionaryGetTypeID()) {
313
+ return rb_CFDictionary_convert(cf_type, false);
314
+ } else if (tid == CFDateGetTypeID()) {
315
+ return rb_CFDate_convert(cf_type);
316
+ } else if (tid == CFBooleanGetTypeID()) {
317
+ return rb_CFBoolean_convert(cf_type);
318
+ } else if (tid == CFNumberGetTypeID()) {
319
+ return rb_CFNumber_convert(cf_type);
320
+ } else {
321
+ return Qnil;
322
+ }
323
+ }
324
+
325
+ /*******************************************************************************
326
+ * Ruby Object => CoreFoundation Type *
327
+ *******************************************************************************/
328
+
329
+ /**
330
+ * Convert a ruby string to a CFStringRef
331
+ */
332
+ static CFStringRef
333
+ cf_rstring_convert(VALUE obj)
334
+ {
335
+ StringValue(obj);
336
+ CFIndex len;
337
+ CFStringRef result;
338
+
339
+ /* get the length of the ruby string */
340
+ len = RSTRING_LEN(obj);
341
+ /* allocate a uint8_t buffer for the result string */
342
+ const uint8_t *cstr = (uint8_t *)RSTRING_PTR(obj);
343
+
344
+ result = CFStringCreateWithBytes(kCFAllocatorDefault, cstr, len,
345
+ kCFStringEncodingUTF8, true);
346
+ return result;
347
+ }
348
+
349
+ /**
350
+ * Convert a ruby symbol into a CFStringRef
351
+ */
352
+ static inline CFStringRef
353
+ cf_rsym_convert(VALUE obj)
354
+ {
355
+ return cf_rstring_convert(rb_sym2str(obj));
356
+ }
357
+
358
+ /**
359
+ * Convert a ruby object to a CFTypeRef.
360
+ *
361
+ * We really should figure out how to convert this to data or Marshal it, but
362
+ * that's too complicated right now.
363
+ *
364
+ * TODO: Figure out how to Marshal this, or how to convert it to data.
365
+ */
366
+ static CFTypeRef
367
+ cf_robject_convert(VALUE obj)
368
+ {
369
+ if (rb_respond_to(obj, id_to_s)) {
370
+ return cf_rstring_convert(obj);
371
+ }
372
+
373
+ return NULL; /* not ideal */
374
+ }
375
+
376
+ /**
377
+ * Convert a Ruby Hash to a CFDictionaryRef
378
+ */
379
+ static CFDictionaryRef
380
+ cf_rhash_convert(VALUE obj)
381
+ {
382
+ /*
383
+ * This is where we will build the CFDictionary.
384
+ * It is Mutable because we want to be able to set keys and values.
385
+ */
386
+ CFMutableDictionaryRef tmp_result;
387
+ /* This is the final immutable result. */
388
+ CFDictionaryRef result;
389
+ tmp_result = CFDictionaryCreateMutable(kCFAllocatorDefault, 0,
390
+ &kCFTypeDictionaryKeyCallBacks,
391
+ &kCFTypeDictionaryValueCallBacks);
392
+
393
+ /*
394
+ * These are the keys and values of the ruby hash we want to convert.
395
+ */
396
+ VALUE keys, vals;
397
+ keys = rb_funcall(obj, id_keys, 0);
398
+ vals = rb_funcall(obj, id_vals, 0);
399
+
400
+ CFIndex i, count = NUM2LONG(rb_hash_size(obj));
401
+
402
+ /* These are used in the below loop. */
403
+ VALUE rb_k, rb_v;
404
+ CFTypeRef cf_k, cf_v;
405
+
406
+ for (i = 0; i < count; i++) {
407
+ rb_k = rb_ary_entry(keys, i);
408
+ rb_v = rb_ary_entry(vals, i);
409
+
410
+ cf_k = ruby_to_corefoundation(rb_k);
411
+ cf_v = ruby_to_corefoundation(rb_v);
412
+
413
+ CFDictionarySetValue(tmp_result, cf_k, cf_v);
414
+ }
415
+
416
+ result = CFDictionaryCreateCopy(kCFAllocatorDefault, tmp_result);
417
+ CFRelease(tmp_result); /* Matches initial create */
418
+ return result;
419
+ }
420
+
421
+ /**
422
+ * Convert a Ruby Array to a CFArrayRef.
423
+ */
424
+ static CFArrayRef
425
+ cf_rarray_convert(VALUE obj)
426
+ {
427
+ Check_Type(obj, T_ARRAY);
428
+ CFIndex i, count = RARRAY_LEN(obj);
429
+
430
+ CFMutableArrayRef tmp_result;
431
+ CFArrayRef result;
432
+
433
+ tmp_result =
434
+ CFArrayCreateMutable(kCFAllocatorDefault, count, &kCFTypeArrayCallBacks);
435
+
436
+ for (i = 0; i < count; i++) {
437
+ VALUE rval = rb_ary_entry(obj, i);
438
+ CFTypeRef cfval = ruby_to_corefoundation(rval);
439
+ CFArraySetValueAtIndex(tmp_result, i, cfval);
440
+ }
441
+
442
+ result = CFArrayCreateCopy(kCFAllocatorDefault, tmp_result);
443
+ CFRelease(tmp_result); /* matches initial create */
444
+ return result;
445
+ }
446
+
447
+ /**
448
+ * Convert a CFBooleanRef to ruby.
449
+ */
450
+ static CFBooleanRef
451
+ cf_rbool_convert(VALUE obj)
452
+ {
453
+ if (obj == Qtrue) {
454
+ return kCFBooleanTrue;
455
+ } else {
456
+ return kCFBooleanFalse;
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Convert a Ruby Numeric type to a CFNumberRef.
462
+ */
463
+ static CFNumberRef
464
+ cf_rnumeric_convert(VALUE obj)
465
+ {
466
+ CFNumberRef result;
467
+
468
+ switch (TYPE(obj)) {
469
+ case T_FLOAT:
470
+ case T_RATIONAL:
471
+ case T_COMPLEX: {
472
+ double val = NUM2DBL(obj);
473
+ result = CFNumberCreate(kCFAllocatorDefault, kCFNumberDoubleType, &val);
474
+ break;
475
+ }
476
+ default: {
477
+ CFIndex val = NUM2LONG(obj); /* 64-bit for overflow safety */
478
+ result = CFNumberCreate(kCFAllocatorDefault, kCFNumberCFIndexType, &val);
479
+ break;
480
+ }
481
+ }
482
+
483
+ return result;
484
+ }
485
+
486
+ /**
487
+ * Convert any ruby type to a CFTypeRef.
488
+ */
489
+ static CFTypeRef
490
+ ruby_to_corefoundation(VALUE obj)
491
+ {
492
+ switch (TYPE(obj)) {
493
+ case T_STRING:
494
+ return cf_rstring_convert(obj);
495
+ case T_SYMBOL:
496
+ return cf_rsym_convert(obj);
497
+ case T_ARRAY:
498
+ return cf_rarray_convert(obj);
499
+ case T_HASH:
500
+ return cf_rhash_convert(obj);
501
+ case T_TRUE:
502
+ case T_FALSE:
503
+ return cf_rbool_convert(obj);
504
+ case T_FLOAT:
505
+ case T_BIGNUM:
506
+ case T_FIXNUM:
507
+ return cf_rnumeric_convert(obj);
508
+
509
+ default:
510
+ return cf_robject_convert(obj);
511
+ }
512
+ }
513
+
514
+ /**
515
+ * Convert a CFPropertyListRef to a ruby object.
516
+ */
517
+ static inline VALUE
518
+ cfplist_to_ruby(CFPropertyListRef plist, Boolean symbolize_keys)
519
+ {
520
+ CFTypeID plist_type = CFGetTypeID(plist);
521
+ if (plist_type == CFArrayGetTypeID()) {
522
+ return rb_CFArray_convert(plist);
523
+ } else if (plist_type == CFDictionaryGetTypeID()) {
524
+ return rb_CFDictionary_convert(plist, symbolize_keys);
525
+ }
526
+
527
+ /* Return nil if it's not an array or hash */
528
+ return Qnil;
529
+ }
530
+
531
+ /**
532
+ * Convert a ruby object to a CFPropertyListRef.
533
+ */
534
+ static inline CFPropertyListRef
535
+ ruby_to_cfplist(VALUE obj)
536
+ {
537
+ switch (TYPE(obj)) {
538
+ case T_ARRAY:
539
+ return cf_rarray_convert(obj);
540
+ case T_HASH:
541
+ return cf_rhash_convert(obj);
542
+ default:
543
+ return NULL;
544
+ }
545
+ }
546
+
547
+ /*******************************************************************************
548
+ * Ruby Method Defs *
549
+ *******************************************************************************/
550
+
551
+ /**
552
+ * Raises a CFError, casting to the appropriate ruby type
553
+ */
554
+ static void
555
+ rb_raise_CFError(CFErrorRef error)
556
+ {
557
+ CFStringRef domain = CFErrorGetDomain(error);
558
+ CFIndex code = CFErrorGetCode(error);
559
+
560
+ /* These next two calls use the Copy/Create rule, so we must release the refs
561
+ * when we are done. */
562
+ CFStringRef cf_desc = CFErrorCopyDescription(error);
563
+ CFStringRef cf_reason = CFErrorCopyFailureReason(error);
564
+
565
+ /* It's okay to get pointers for these strings, since we "own" them. */
566
+ const char *desc = CFStringGetCStringPtr(cf_desc, kCFStringEncodingUTF8);
567
+ const char *reason = CFStringGetCStringPtr(cf_reason, kCFStringEncodingUTF8);
568
+
569
+ if (IS_DOMAIN(domain, kCFErrorDomainPOSIX)) {
570
+ /* we have a POSIX error. We raise these as Ruby SysErrors */
571
+ VALUE msg = rb_sprintf("%s %s", desc, reason);
572
+ /* at this point, we know "code" is an "errno", so we cast to int */
573
+ VALUE exc = rb_syserr_new_str((int)code, msg);
574
+ rb_exc_raise(exc);
575
+ } else if (IS_DOMAIN(domain, kCFErrorDomainOSStatus)) {
576
+ rb_raise(rb_eCFErrorOSStatus, "%s %s (%ld)", desc, reason, code);
577
+ } else if (IS_DOMAIN(domain, kCFErrorDomainMach)) {
578
+ rb_raise(rb_eCFErrorMach, "Mach Error - %s %s (%ld)", desc, reason, code);
579
+ } else if (IS_DOMAIN(domain, kCFErrorDomainCocoa)) {
580
+ rb_raise(rb_eCFErrorCocoa, "%s %s (%ld)", desc, reason, code);
581
+ } else {
582
+ /* This is probably unreachable */
583
+ rb_raise(rb_eCFError, "%s %s (%ld)", desc, reason, code);
584
+ }
585
+ }
586
+
587
+ /**
588
+ * Parses a string representation of a PList to a ruby hash.
589
+ *
590
+ * TODO: Add more complex options
591
+ */
592
+ static VALUE
593
+ plist_parse(int argc, VALUE *argv, VALUE self)
594
+ {
595
+ VALUE plist_str, v_symbolize_keys;
596
+ Boolean symbolize_keys;
597
+
598
+ if (1 == rb_scan_args(argc, argv, "11", &plist_str, &v_symbolize_keys)) {
599
+ v_symbolize_keys = Qfalse;
600
+ }
601
+
602
+ if (NIL_P(v_symbolize_keys) || v_symbolize_keys == Qfalse) {
603
+ symbolize_keys = false;
604
+ } else {
605
+ symbolize_keys = true;
606
+ }
607
+
608
+ StringValue(plist_str);
609
+
610
+ /* allocate a buffer to hold the string data */
611
+ const uint8_t *strdata = (const uint8_t *)StringValuePtr(plist_str);
612
+ CFIndex strlen = RSTRING_LEN(plist_str);
613
+
614
+ /* Create a CFDataRef with the data from the string */
615
+ CFDataRef plist_data;
616
+ plist_data = CFDataCreate(kCFAllocatorDefault, strdata, strlen);
617
+
618
+ CFErrorRef err = NULL;
619
+ CFPropertyListRef plist;
620
+
621
+ /* create the plist from the data */
622
+ plist = CFPropertyListCreateWithData(kCFAllocatorDefault, plist_data,
623
+ kCFPropertyListImmutable, NULL, &err);
624
+
625
+ /* check to make sure no error occured */
626
+ if (err != NULL) {
627
+ /* if an error did occur, clean up our references */
628
+ if (plist != NULL)
629
+ CFRelease(plist); /* matches initial create */
630
+ if (plist_data != NULL)
631
+ CFRelease(plist_data); /* matches initial create */
632
+ rb_raise_CFError(err);
633
+ }
634
+
635
+ /* Convert the CFPropertyListRef to a ruby object */
636
+ VALUE result = cfplist_to_ruby(plist, symbolize_keys);
637
+
638
+ /* clean up our references */
639
+ if (plist != NULL)
640
+ CFRelease(plist);
641
+ if (plist_data != NULL)
642
+ CFRelease(plist_data);
643
+ if (err != NULL)
644
+ CFRelease(err);
645
+
646
+ return result;
647
+ }
648
+
649
+ static VALUE
650
+ plist_generate(int argc, VALUE *argv, VALUE self)
651
+ {
652
+ VALUE obj;
653
+ VALUE opts;
654
+
655
+ /* Scan the arguments. This method is called like this:
656
+ * CFPlist.generate(obj, opts = {})
657
+ */
658
+ rb_scan_args(argc, argv, "11", &obj, &opts);
659
+
660
+ /* opts will be nil (rather than {}) if no option hash was passed. */
661
+ if (NIL_P(opts))
662
+ opts = rb_hash_new();
663
+
664
+ /* opts is currently reserved for future use, and we don't actually respect
665
+ * any options passed. */
666
+ CFPropertyListRef obj_as_plist;
667
+ CFErrorRef error = NULL;
668
+ CFDataRef xml_data;
669
+
670
+ obj_as_plist = ruby_to_cfplist(obj);
671
+ xml_data = CFPropertyListCreateData(kCFAllocatorDefault, obj_as_plist,
672
+ kCFPropertyListXMLFormat_v1_0, 0, &error);
673
+
674
+ /* Check to make sure no error occured */
675
+ if (error != NULL) {
676
+ /* if an error did occur, clean up the refs */
677
+ if (obj_as_plist != NULL)
678
+ CFRelease(obj_as_plist);
679
+ if (xml_data != NULL)
680
+ CFRelease(xml_data);
681
+ rb_raise_CFError(error);
682
+ }
683
+
684
+ /* Convert the CFData object to a ruby string */
685
+ VALUE result = rb_CFData_convert(xml_data);
686
+
687
+ /* clean up references */
688
+ if (obj_as_plist != NULL)
689
+ CFRelease(obj_as_plist);
690
+ if (xml_data != NULL)
691
+ CFRelease(xml_data);
692
+ if (error != NULL)
693
+ CFRelease(error);
694
+
695
+ return result;
696
+ }
697
+
698
+ void
699
+ Init_cfplist(void)
700
+ {
701
+ id_to_s = rb_intern("to_s");
702
+ id_keys = rb_intern("keys");
703
+ id_vals = rb_intern("values");
704
+ id_count = rb_intern("count");
705
+
706
+ rb_mCFPlist = rb_define_module("CFPlist");
707
+ rb_eCFError =
708
+ rb_define_class_under(rb_mCFPlist, "CFError", rb_eStandardError);
709
+ rb_eCFErrorOSStatus =
710
+ rb_define_class_under(rb_mCFPlist, "OSStatusError", rb_eCFError);
711
+ rb_eCFErrorMach =
712
+ rb_define_class_under(rb_mCFPlist, "MachError", rb_eCFError);
713
+ rb_eCFErrorCocoa =
714
+ rb_define_class_under(rb_mCFPlist, "CocoaError", rb_eCFError);
715
+
716
+ rb_define_module_function(rb_mCFPlist, "_parse", plist_parse, -1);
717
+ rb_define_module_function(rb_mCFPlist, "_generate", plist_generate, -1);
718
+ }