cfplist 0.1.1

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.
@@ -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
+ }