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.
- checksums.yaml +7 -0
- data/.clang-format +149 -0
- data/.gitignore +18 -0
- data/.rspec +3 -0
- data/.rubocop.yml +99 -0
- data/.ruby-version +1 -0
- data/.simplecov +5 -0
- data/.travis.yml +6 -0
- data/CHANGELOG +0 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +78 -0
- data/LICENSE.txt +21 -0
- data/README.md +97 -0
- data/Rakefile +16 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/cfplist.gemspec +32 -0
- data/compile_commands.json +30 -0
- data/ext/cfplist/cfplist.c +718 -0
- data/ext/cfplist/extconf.rb +26 -0
- data/lib/cfplist.rb +104 -0
- data/lib/cfplist/version.rb +5 -0
- metadata +71 -0
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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).
|
data/Rakefile
ADDED
@@ -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]
|
data/bin/console
ADDED
@@ -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__)
|
data/bin/setup
ADDED
data/cfplist.gemspec
ADDED
@@ -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
|
+
}
|