├── test ├── suite.rb ├── fixtures │ └── xml_plist └── test_plist.rb ├── ext └── plist │ ├── extconf.rb │ └── plist.c ├── Manifest.txt ├── History.txt ├── Rakefile ├── lib └── osx │ └── plist.rb ├── osx-plist.gemspec └── README.txt /test/suite.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | 3 | tests = Dir["#{File.dirname(__FILE__)}/test_*.rb"] 4 | tests.each do |file| 5 | require file 6 | end 7 | -------------------------------------------------------------------------------- /ext/plist/extconf.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | require 'mkmf' 3 | $LDFLAGS += ' -framework CoreFoundation -undefined suppress -flat_namespace' 4 | $LIBRUBYARG_SHARED="" 5 | create_makefile("osx/plist/ext/plist") 6 | -------------------------------------------------------------------------------- /Manifest.txt: -------------------------------------------------------------------------------- 1 | History.txt 2 | Manifest.txt 3 | README.txt 4 | Rakefile 5 | ext/plist/extconf.rb 6 | ext/plist/plist.c 7 | lib/osx/plist.rb 8 | test/fixtures/xml_plist 9 | test/suite.rb 10 | test/test_plist.rb 11 | -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | == 1.0.3 / 2009-09-21 2 | * Add two new methods OSX::PropertyList.load_file and OSX::PropertyList.dump_file 3 | * Clean up the RDoc documentation 4 | 5 | == 1.0.2 / 2009-09-17 6 | * Build properly under Mac OS X 10.6 7 | 8 | == 1.0.1 / 2009-02-05 9 | * Ruby 1.9.1 compatibility 10 | 11 | == 1.0 / 2008-04-25 12 | * First public release 13 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'hoe' 3 | 4 | Hoe.spec 'osx-plist' do 5 | developer("Kevin Ballard", "kevin@sb.org") 6 | self.version = "1.0.3" 7 | self.summary = "Property List manipulation for OS X" 8 | self.spec_extras = {:extensions => "ext/plist/extconf.rb"} 9 | end 10 | 11 | # override Hoe's default :test task 12 | Rake::Task["test"].clear 13 | desc "Run the unit tests" 14 | task :test do 15 | ruby "test/test_plist.rb" 16 | end 17 | -------------------------------------------------------------------------------- /lib/osx/plist.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/plist/ext/plist" 2 | 3 | module OSX 4 | module PropertyList 5 | # Loads a property list from the file at +filepath+ using OSX::PropertyList.load. 6 | def self.load_file(filepath, format = false) 7 | File.open(filepath, "r") do |f| 8 | OSX::PropertyList.load(f, format) 9 | end 10 | end 11 | # Writes the property list representation of +obj+ to the file at +filepath+ using OSX::PropertyList.dump. 12 | def self.dump_file(filepath, obj, format = :xml1) 13 | File.open(filepath, "w") do |f| 14 | OSX::PropertyList.dump(f, obj, format) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/fixtures/xml_plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | string! 6 | indeedy 7 | bar 8 | 9 | 1 10 | 2 11 | 3 12 | 13 | foo 14 | 15 | correct? 16 | 17 | pi 18 | 3.14159265 19 | random 20 | 21 | I0VniQ== 22 | 23 | today 24 | 2005-04-28T06:32:56Z 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /osx-plist.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | Gem::Specification.new do |s| 4 | s.name = %q{osx-plist} 5 | s.version = "1.0.3" 6 | 7 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 8 | s.authors = ["Kevin Ballard"] 9 | s.date = %q{2009-09-21} 10 | s.description = %q{osx-plist is a Ruby library for manipulating Property Lists natively using the built-in support in OS X.} 11 | s.email = ["kevin@sb.org"] 12 | s.extensions = ["ext/plist/extconf.rb"] 13 | s.extra_rdoc_files = ["History.txt", "Manifest.txt", "README.txt"] 14 | s.files = ["History.txt", "Manifest.txt", "README.txt", "Rakefile", "ext/plist/extconf.rb", "ext/plist/plist.c", "lib/osx/plist.rb", "test/fixtures/xml_plist", "test/suite.rb", "test/test_plist.rb"] 15 | s.homepage = %q{http://github.com/kballard/osx-plist} 16 | s.rdoc_options = ["--main", "README.txt"] 17 | s.require_paths = ["lib", "ext"] 18 | s.rubygems_version = %q{1.3.5} 19 | s.summary = %q{Property List manipulation for OS X} 20 | s.test_files = ["test/test_plist.rb"] 21 | 22 | if s.respond_to? :specification_version then 23 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 24 | s.specification_version = 3 25 | 26 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then 27 | s.add_development_dependency(%q, [">= 2.3.3"]) 28 | else 29 | s.add_dependency(%q, [">= 2.3.3"]) 30 | end 31 | else 32 | s.add_dependency(%q, [">= 2.3.3"]) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/test_plist.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'osx/plist' 3 | require 'stringio' 4 | require 'tempfile' 5 | require 'test/unit' 6 | 7 | class TestPlist < Test::Unit::TestCase 8 | def test_string 9 | plist = OSX::PropertyList.load("{foo = bar; }") 10 | assert_equal( { "foo" => "bar" }, plist ) 11 | 12 | plist, format = OSX::PropertyList.load("{foo = bar; }", true) 13 | assert_equal( { "foo" => "bar" }, plist ) 14 | assert_equal( :openstep, format ) 15 | 16 | # make sure sources < 6 characters work 17 | plist = OSX::PropertyList.load("foo") 18 | assert_equal( "foo", plist ) 19 | 20 | # make sure it works with format too 21 | plist, format = OSX::PropertyList.load("foo", true) 22 | assert_equal( "foo", plist ) 23 | assert_equal( :openstep, format ) 24 | 25 | assert_raise(OSX::PropertyListError) { OSX::PropertyList.load("") } 26 | end 27 | 28 | def setup_hash 29 | time = Time.gm(2005, 4, 28, 6, 32, 56) 30 | random = "\x23\x45\x67\x89" 31 | random.blob = true 32 | { 33 | "string!" => "indeedy", 34 | "bar" => [ 1, 2, 3 ], 35 | "foo" => { 36 | "correct?" => true, 37 | "pi" => 3.14159265, 38 | "random" => random, 39 | "today" => time, 40 | } 41 | } 42 | end 43 | 44 | def test_io 45 | plist, format = OSX::PropertyList.load(File.read("#{File.dirname(__FILE__)}/fixtures/xml_plist"), true) 46 | 47 | hash = setup_hash 48 | 49 | assert_equal(hash, plist) 50 | assert_equal(true, plist['foo']['random'].blob?) 51 | assert_equal(false, plist['string!'].blob?) 52 | 53 | assert_equal(:xml1, format) 54 | end 55 | 56 | def test_dump 57 | str = StringIO.new("", "w") 58 | hash = setup_hash 59 | OSX::PropertyList.dump(str, hash) 60 | hash2 = OSX::PropertyList.load(str.string) 61 | assert_equal(hash, hash2) 62 | end 63 | 64 | def test_to_plist 65 | assert_raise(OSX::PropertyListError) { "foo".to_plist(:openstep) } 66 | assert_equal("foo", OSX::PropertyList.load("foo".to_plist)) 67 | hash = setup_hash() 68 | assert_equal(hash, OSX::PropertyList.load(hash.to_plist)) 69 | end 70 | 71 | def test_load_file 72 | plist, format = OSX::PropertyList.load_file("#{File.dirname(__FILE__)}/fixtures/xml_plist", true) 73 | 74 | hash = setup_hash 75 | 76 | assert_equal(hash, plist) 77 | assert_equal(:xml1, format) 78 | end 79 | 80 | def test_dump_file 81 | hash = setup_hash 82 | Tempfile.open("test_plist") do |temp| 83 | OSX::PropertyList.dump_file(temp.path, hash) 84 | hash2 = OSX::PropertyList.load_file(temp.path) 85 | assert_equal(hash, hash2) 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | == osx-plist 2 | 3 | * http://github.com/kballard/osx-plist 4 | * by Kevin Ballard 5 | 6 | == DESCRIPTION: 7 | 8 | osx-plist is a Ruby library for manipulating Property Lists natively using the built-in support in OS X. 9 | 10 | == REQUIREMENTS: 11 | 12 | * CoreFoundation (i.e. Mac OS X) 13 | 14 | == INSTALL: 15 | 16 | $ gem sources -a http://gems.github.com/ (you only need to do this once) 17 | $ gem install kballard-osx-plist 18 | 19 | == SOURCE: 20 | 21 | osx-plist's git repo is available on GitHub, which can be browsed at: 22 | 23 | http://github.com/kballard/osx-plist 24 | 25 | and cloned from: 26 | 27 | git://github.com/kballard/osx-plist.git 28 | 29 | == USAGE: 30 | 31 | One new module is provided, named OSX::PropertyList. It has the following 4 methods: 32 | 33 | ==== OSX::PropertyList.load(input, format = false) 34 | 35 | Loads the property list from input, which is either an IO, StringIO, or a string. Format is an optional parameter - if false, the return value is the converted property list object. If true, the return value is a 2-element array, the first element being the returned value and the second being a symbol identifying the property list format. 36 | 37 | ==== OSX::PropertyList.dump(output, obj, format = :xml1) 38 | 39 | Dumps the property list object into output, which is either an IO or StringIO. Format determines the property list format to write out. The supported values are :xml1,, :binary1, and :openstep; however, OpenStep format appears to not be supported by the system for output anymore. 40 | 41 | The valid formats are :xml1, :binary1, and :openstep. When loading a property list, if the format is something else (not possible under any current OS, but perhaps if a future OS includes another type) then the format will be :unknown. 42 | 43 | ==== OSX::PropertyList.load_file(filepath, format = false) 44 | 45 | Calls OSX::PropertyList.load() on the file at the given path. 46 | 47 | ==== OSX::PropertyList.dump_file(filepath, obj, format = :xml1) 48 | 49 | Calls OSX::PropertyList.dump() on the file at the given path. 50 | 51 | This module also provides a method on Object: 52 | 53 | ==== Object#to_plist(format = :xml1) 54 | 55 | This is the same as PropertyList.dump except it outputs the property list as a string return value instead of writing it to a stream 56 | 57 | This module also provides 2 methods on String: 58 | 59 | ==== String#blob? 60 | 61 | Returns whether the string is a blob. 62 | 63 | ==== String#blob= 64 | 65 | Sets whether the string is a blob. 66 | 67 | A blob is a string that's been converted from a property list item. When dumping to a property list, any strings that are blobs are written as items rather than items. 68 | 69 | == LICENSE: 70 | 71 | (The MIT License) 72 | 73 | Copyright (c) 2008 Kevin Ballard 74 | 75 | Permission is hereby granted, free of charge, to any person obtaining 76 | a copy of this software and associated documentation files (the 77 | 'Software'), to deal in the Software without restriction, including 78 | without limitation the rights to use, copy, modify, merge, publish, 79 | distribute, sublicense, and/or sell copies of the Software, and to 80 | permit persons to whom the Software is furnished to do so, subject to 81 | the following conditions: 82 | 83 | The above copyright notice and this permission notice shall be 84 | included in all copies or substantial portions of the Software. 85 | 86 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 87 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 88 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 89 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 90 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 91 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 92 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 93 | -------------------------------------------------------------------------------- /ext/plist/plist.c: -------------------------------------------------------------------------------- 1 | /* 2 | * plist 3 | * Kevin Ballard 4 | * 5 | * This is a Ruby extension to read/write Cocoa property lists 6 | * Not surprisingly, it only works on OS X 7 | * 8 | * Copyright © 2005, Kevin Ballard 9 | * 10 | * Usage: 11 | * This extension provides a module named OSX::PropertyList 12 | * This module has two methods: 13 | * 14 | * PropertyList::load(obj, format = false) 15 | * Takes either an IO stream open for reading or a String object 16 | * Returns an object representing the property list 17 | * 18 | * Optionally takes a boolean format argument. If true, the 19 | * return value is an array with the second value being 20 | * the format of the plist, which can be one of 21 | * :xml1, :binary1, or :openstep 22 | * 23 | * PropertyList::dump(io, obj, type = :xml1) 24 | * Takes an IO stream (open for writing) and an object 25 | * Writes the object to the IO stream as a property list 26 | * Posible type values are :xml1 and :binary1 27 | * 28 | * It also adds a new method to Object: 29 | * 30 | * Object#to_plist(type = :xml1) 31 | * Returns a string representation of the property list 32 | * Possible type values are :xml1 and :binary1 33 | * 34 | * It also adds 2 new methods to String: 35 | * 36 | * String#blob=(b) 37 | * Sets whether the string is a blob 38 | * 39 | * String#blob? 40 | * Returns whether the string is a blob 41 | * 42 | * A blob string is turned into a CFData when dumped 43 | * 44 | */ 45 | 46 | #include 47 | #if HAVE_RUBY_ST_H 48 | #include 49 | #else 50 | #include 51 | #endif 52 | #include 53 | 54 | // Here's some convenience macros 55 | #ifndef StringValue 56 | #define StringValue(x) do { \ 57 | if (TYPE(x) != T_STRING) x = rb_str_to_str(x); \ 58 | } while (0) 59 | #endif 60 | 61 | static VALUE mOSX; 62 | static VALUE mPlist; 63 | static VALUE timeEpoch; 64 | static VALUE ePropertyListError; 65 | 66 | static VALUE id_gm; 67 | static VALUE id_plus; 68 | static VALUE id_minus; 69 | static VALUE id_read; 70 | static VALUE id_write; 71 | 72 | static VALUE id_xml; 73 | static VALUE id_binary; 74 | static VALUE id_openstep; 75 | 76 | static VALUE id_blob; 77 | 78 | VALUE convertPropertyListRef(CFPropertyListRef plist); 79 | VALUE convertStringRef(CFStringRef plist); 80 | VALUE convertDictionaryRef(CFDictionaryRef plist); 81 | VALUE convertArrayRef(CFArrayRef plist); 82 | VALUE convertNumberRef(CFNumberRef plist); 83 | VALUE convertBooleanRef(CFBooleanRef plist); 84 | VALUE convertDataRef(CFDataRef plist); 85 | VALUE convertDateRef(CFDateRef plist); 86 | VALUE str_blob(VALUE self); 87 | VALUE str_setBlob(VALUE self, VALUE b); 88 | 89 | // Raises a Ruby exception with the given string 90 | void raiseError(CFStringRef error) { 91 | char *errBuffer = (char *)CFStringGetCStringPtr(error, kCFStringEncodingUTF8); 92 | int freeBuffer = 0; 93 | if (!errBuffer) { 94 | int len = CFStringGetLength(error)*2+1; 95 | errBuffer = ALLOC_N(char, len); 96 | Boolean succ = CFStringGetCString(error, errBuffer, len, kCFStringEncodingUTF8); 97 | if (!succ) { 98 | CFStringGetCString(error, errBuffer, len, kCFStringEncodingMacRoman); 99 | } 100 | freeBuffer = 1; 101 | } 102 | rb_raise(ePropertyListError, (char *)errBuffer); 103 | if (freeBuffer) free(errBuffer); 104 | } 105 | 106 | /* call-seq: 107 | * load(obj, format = false) 108 | * 109 | * Loads a property list from an IO stream or a String and creates 110 | * an equivalent Object from it. 111 | * 112 | * If +format+ is +true+, it returns an array of [object, format] 113 | * where +format+ is one of :xml1, :binary1, or :openstep. 114 | */ 115 | VALUE plist_load(int argc, VALUE *argv, VALUE self) { 116 | VALUE io, retFormat; 117 | int count = rb_scan_args(argc, argv, "11", &io, &retFormat); 118 | if (count < 2) retFormat = Qfalse; 119 | VALUE buffer; 120 | if (RTEST(rb_respond_to(io, id_read))) { 121 | // Read from IO 122 | buffer = rb_funcall(io, id_read, 0); 123 | } else { 124 | StringValue(io); 125 | buffer = io; 126 | } 127 | // For some reason, the CFReadStream version doesn't work with input < 6 characters 128 | // but the CFDataRef version doesn't return format 129 | // So lets use the CFDataRef version unless format is requested 130 | CFStringRef error = NULL; 131 | CFPropertyListRef plist; 132 | CFPropertyListFormat format; 133 | if (RTEST(retFormat)) { 134 | // Format was requested 135 | // now just in case, if the input is < 6 characters, we will pad it out with newlines 136 | // we could do this in all cases, but I don't think it will work with binary 137 | // even though binary shouldn't be < 6 characters 138 | UInt8 *bytes; 139 | int len; 140 | if (RSTRING_LEN(buffer) < 6) { 141 | bytes = ALLOC_N(UInt8, 6); 142 | memset(bytes, '\n', 6); 143 | MEMCPY(bytes, RSTRING_PTR(buffer), UInt8, RSTRING_LEN(buffer)); 144 | len = 6; 145 | } else { 146 | bytes = (UInt8 *)RSTRING_PTR(buffer); 147 | len = RSTRING_LEN(buffer); 148 | } 149 | CFReadStreamRef readStream = CFReadStreamCreateWithBytesNoCopy(kCFAllocatorDefault, bytes, len, kCFAllocatorNull); 150 | CFReadStreamOpen(readStream); 151 | plist = CFPropertyListCreateFromStream(kCFAllocatorDefault, readStream, 0, kCFPropertyListImmutable, &format, &error); 152 | CFReadStreamClose(readStream); 153 | CFRelease(readStream); 154 | } else { 155 | // Format wasn't requested 156 | CFDataRef data = CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, (const UInt8*)RSTRING_PTR(buffer), RSTRING_LEN(buffer), kCFAllocatorNull); 157 | plist = CFPropertyListCreateFromXMLData(kCFAllocatorDefault, data, kCFPropertyListImmutable, &error); 158 | CFRelease(data); 159 | } 160 | if (error) { 161 | raiseError(error); 162 | CFRelease(error); 163 | return Qnil; 164 | } 165 | VALUE obj = convertPropertyListRef(plist); 166 | CFRelease(plist); 167 | if (RTEST(retFormat)) { 168 | VALUE ary = rb_ary_new(); 169 | rb_ary_push(ary, obj); 170 | if (format == kCFPropertyListOpenStepFormat) { 171 | retFormat = id_openstep; 172 | } else if (format == kCFPropertyListXMLFormat_v1_0) { 173 | retFormat = id_xml; 174 | } else if (format == kCFPropertyListBinaryFormat_v1_0) { 175 | retFormat = id_binary; 176 | } else { 177 | retFormat = rb_intern("unknown"); 178 | } 179 | rb_ary_push(ary, ID2SYM(retFormat)); 180 | return ary; 181 | } else { 182 | return obj; 183 | } 184 | } 185 | 186 | // Maps the property list object to a ruby object 187 | VALUE convertPropertyListRef(CFPropertyListRef plist) { 188 | CFTypeID typeID = CFGetTypeID(plist); 189 | if (typeID == CFStringGetTypeID()) { 190 | return convertStringRef((CFStringRef)plist); 191 | } else if (typeID == CFDictionaryGetTypeID()) { 192 | return convertDictionaryRef((CFDictionaryRef)plist); 193 | } else if (typeID == CFArrayGetTypeID()) { 194 | return convertArrayRef((CFArrayRef)plist); 195 | } else if (typeID == CFNumberGetTypeID()) { 196 | return convertNumberRef((CFNumberRef)plist); 197 | } else if (typeID == CFBooleanGetTypeID()) { 198 | return convertBooleanRef((CFBooleanRef)plist); 199 | } else if (typeID == CFDataGetTypeID()) { 200 | return convertDataRef((CFDataRef)plist); 201 | } else if (typeID == CFDateGetTypeID()) { 202 | return convertDateRef((CFDateRef)plist); 203 | } else { 204 | return Qnil; 205 | } 206 | } 207 | 208 | // Converts a CFStringRef to a String 209 | VALUE convertStringRef(CFStringRef plist) { 210 | CFIndex byteCount; 211 | CFRange range = CFRangeMake(0, CFStringGetLength(plist)); 212 | CFStringEncoding enc = kCFStringEncodingUTF8; 213 | Boolean succ = CFStringGetBytes(plist, range, enc, 0, false, NULL, 0, &byteCount); 214 | if (!succ) { 215 | enc = kCFStringEncodingMacRoman; 216 | CFStringGetBytes(plist, range, enc, 0, false, NULL, 0, &byteCount); 217 | } 218 | UInt8 *buffer = ALLOC_N(UInt8, byteCount); 219 | CFStringGetBytes(plist, range, enc, 0, false, buffer, byteCount, NULL); 220 | VALUE retval = rb_str_new((char *)buffer, (long)byteCount); 221 | free(buffer); 222 | return retval; 223 | } 224 | 225 | // Converts the keys and values of a CFDictionaryRef 226 | void dictionaryConverter(const void *key, const void *value, void *context) { 227 | rb_hash_aset((VALUE)context, convertPropertyListRef(key), convertPropertyListRef(value)); 228 | } 229 | 230 | // Converts a CFDictionaryRef to a Hash 231 | VALUE convertDictionaryRef(CFDictionaryRef plist) { 232 | VALUE hash = rb_hash_new(); 233 | CFDictionaryApplyFunction(plist, dictionaryConverter, (void *)hash); 234 | return hash; 235 | } 236 | 237 | // Converts the values of a CFArrayRef 238 | void arrayConverter(const void *value, void *context) { 239 | rb_ary_push((VALUE)context, convertPropertyListRef(value)); 240 | } 241 | 242 | // Converts a CFArrayRef to an Array 243 | VALUE convertArrayRef(CFArrayRef plist) { 244 | VALUE array = rb_ary_new(); 245 | CFRange range = CFRangeMake(0, CFArrayGetCount(plist)); 246 | CFArrayApplyFunction(plist, range, arrayConverter, (void *)array); 247 | return array; 248 | } 249 | 250 | // Converts a CFNumberRef to a Number 251 | VALUE convertNumberRef(CFNumberRef plist) { 252 | if (CFNumberIsFloatType(plist)) { 253 | double val; 254 | CFNumberGetValue(plist, kCFNumberDoubleType, &val); 255 | return rb_float_new(val); 256 | } else { 257 | #ifdef LL2NUM 258 | long long val; 259 | CFNumberGetValue(plist, kCFNumberLongLongType, &val); 260 | return LL2NUM(val); 261 | #else 262 | long val; 263 | CFNumberGetValue(plist, kCFNumberLongType, &val); 264 | return LONG2NUM(val); 265 | #endif 266 | } 267 | } 268 | 269 | // Converts a CFBooleanRef to a Boolean 270 | VALUE convertBooleanRef(CFBooleanRef plist) { 271 | if (CFBooleanGetValue(plist)) { 272 | return Qtrue; 273 | } else { 274 | return Qfalse; 275 | } 276 | } 277 | 278 | // Converts a CFDataRef to a String (with blob set to true) 279 | VALUE convertDataRef(CFDataRef plist) { 280 | const UInt8 *bytes = CFDataGetBytePtr(plist); 281 | CFIndex len = CFDataGetLength(plist); 282 | VALUE str = rb_str_new((char *)bytes, (long)len); 283 | str_setBlob(str, Qtrue); 284 | return str; 285 | } 286 | 287 | // Converts a CFDateRef to a Time 288 | VALUE convertDateRef(CFDateRef plist) { 289 | CFAbsoluteTime seconds = CFDateGetAbsoluteTime(plist); 290 | 291 | // trunace the time since Ruby's Time object stores it as a 32 bit signed offset from 1970 (undocumented) 292 | const float min_time = -3124310400.0f; 293 | const float max_time = 1169098047.0f; 294 | seconds = seconds < min_time ? min_time : (seconds > max_time ? max_time : seconds); 295 | 296 | return rb_funcall(timeEpoch, id_plus, 1, rb_float_new(seconds)); 297 | } 298 | 299 | CFPropertyListRef convertObject(VALUE obj); 300 | 301 | // Converts a PropertyList object to a string representation 302 | VALUE convertPlistToString(CFPropertyListRef plist, CFPropertyListFormat format) { 303 | CFWriteStreamRef writeStream = CFWriteStreamCreateWithAllocatedBuffers(kCFAllocatorDefault, kCFAllocatorDefault); 304 | CFWriteStreamOpen(writeStream); 305 | CFStringRef error = NULL; 306 | CFPropertyListWriteToStream(plist, writeStream, format, &error); 307 | CFWriteStreamClose(writeStream); 308 | if (error) { 309 | raiseError(error); 310 | return Qnil; 311 | } 312 | CFDataRef data = CFWriteStreamCopyProperty(writeStream, kCFStreamPropertyDataWritten); 313 | CFRelease(writeStream); 314 | VALUE plistData = convertDataRef(data); 315 | CFRelease(data); 316 | return plistData; 317 | } 318 | 319 | /* call-seq: 320 | * dump(io, obj, format = :xml1) 321 | * 322 | * Writes the property list representation of +obj+ 323 | * to the IO stream (must be open for writing). 324 | * 325 | * +format+ can be one of :xml1 or :binary1. 326 | * 327 | * Returns the number of bytes written, or +nil+ if 328 | * the object could not be represented as a property list 329 | */ 330 | VALUE plist_dump(int argc, VALUE *argv, VALUE self) { 331 | VALUE io, obj, type; 332 | int count = rb_scan_args(argc, argv, "21", &io, &obj, &type); 333 | if (count < 3) { 334 | type = id_xml; 335 | } else { 336 | type = rb_to_id(type); 337 | } 338 | if (!RTEST(rb_respond_to(io, id_write))) { 339 | rb_raise(rb_eArgError, "Argument 1 must be an IO object"); 340 | return Qnil; 341 | } 342 | CFPropertyListFormat format; 343 | if (type == id_xml) { 344 | format = kCFPropertyListXMLFormat_v1_0; 345 | } else if (type == id_binary) { 346 | format = kCFPropertyListBinaryFormat_v1_0; 347 | } else if (type == id_openstep) { 348 | format = kCFPropertyListOpenStepFormat; 349 | } else { 350 | rb_raise(rb_eArgError, "Argument 3 must be one of :xml1, :binary1, or :openstep"); 351 | return Qnil; 352 | } 353 | CFPropertyListRef plist = convertObject(obj); 354 | VALUE data = convertPlistToString(plist, format); 355 | if (NIL_P(data)) { 356 | return Qnil; 357 | } else { 358 | return rb_funcall(io, id_write, 1, data); 359 | } 360 | } 361 | 362 | /* call-seq: 363 | * object.to_plist(format = :xml1) 364 | * 365 | * Converts the object to a property list representation 366 | * and returns it as a string. 367 | * 368 | * +format+ can be one of :xml1 or :binary1. 369 | */ 370 | VALUE obj_to_plist(int argc, VALUE *argv, VALUE self) { 371 | VALUE type; 372 | int count = rb_scan_args(argc, argv, "01", &type); 373 | if (count < 1) { 374 | type = id_xml; 375 | } else { 376 | type = rb_to_id(type); 377 | } 378 | CFPropertyListFormat format; 379 | if (type == id_xml) { 380 | format = kCFPropertyListXMLFormat_v1_0; 381 | } else if (type == id_binary) { 382 | format = kCFPropertyListBinaryFormat_v1_0; 383 | } else if (type == id_openstep) { 384 | format = kCFPropertyListOpenStepFormat; 385 | } else { 386 | rb_raise(rb_eArgError, "Argument 2 must be one of :xml1, :binary1, or :openstep"); 387 | return Qnil; 388 | } 389 | CFPropertyListRef plist = convertObject(self); 390 | VALUE data = convertPlistToString(plist, format); 391 | CFRelease(plist); 392 | if (type == id_xml || type == id_binary) { 393 | str_setBlob(data, Qfalse); 394 | } 395 | return data; 396 | } 397 | 398 | CFPropertyListRef convertString(VALUE obj); 399 | CFDictionaryRef convertHash(VALUE obj); 400 | CFArrayRef convertArray(VALUE obj); 401 | CFNumberRef convertNumber(VALUE obj); 402 | CFDateRef convertTime(VALUE obj); 403 | 404 | // Converts an Object to a CFTypeRef 405 | CFPropertyListRef convertObject(VALUE obj) { 406 | switch (TYPE(obj)) { 407 | case T_STRING: return convertString(obj); break; 408 | case T_HASH: return convertHash(obj); break; 409 | case T_ARRAY: return convertArray(obj); break; 410 | case T_FLOAT: 411 | case T_FIXNUM: 412 | case T_BIGNUM: return convertNumber(obj); break; 413 | case T_TRUE: return kCFBooleanTrue; break; 414 | case T_FALSE: return kCFBooleanFalse; break; 415 | default: if (rb_obj_is_kind_of(obj, rb_cTime)) return convertTime(obj); 416 | } 417 | rb_raise(rb_eArgError, "An object in the argument tree could not be converted"); 418 | return NULL; 419 | } 420 | 421 | // Converts a String to a CFStringRef 422 | CFPropertyListRef convertString(VALUE obj) { 423 | if (RTEST(str_blob(obj))) { 424 | // convert to CFDataRef 425 | StringValue(obj); 426 | CFDataRef data = CFDataCreate(kCFAllocatorDefault, (const UInt8*)RSTRING_PTR(obj), (CFIndex)RSTRING_LEN(obj)); 427 | return data; 428 | } else { 429 | // convert to CFStringRef 430 | StringValue(obj); 431 | CFStringRef string = CFStringCreateWithBytes(kCFAllocatorDefault, (const UInt8*)RSTRING_PTR(obj), (CFIndex)RSTRING_LEN(obj), kCFStringEncodingUTF8, false); 432 | if (!string) { 433 | // try MacRoman 434 | string = CFStringCreateWithBytes(kCFAllocatorDefault, (const UInt8*)RSTRING_PTR(obj), (CFIndex)RSTRING_LEN(obj), kCFStringEncodingMacRoman, false); 435 | } 436 | return string; 437 | } 438 | } 439 | 440 | // Converts the keys and values of a Hash to CFTypeRefs 441 | int iterateHash(VALUE key, VALUE val, VALUE dict) { 442 | CFPropertyListRef dKey = convertObject(key); 443 | CFPropertyListRef dVal = convertObject(val); 444 | CFDictionaryAddValue((CFMutableDictionaryRef)dict, dKey, dVal); 445 | CFRelease(dKey); 446 | CFRelease(dVal); 447 | return ST_CONTINUE; 448 | } 449 | 450 | // Converts a Hash to a CFDictionaryREf 451 | CFDictionaryRef convertHash(VALUE obj) { 452 | // RHASH_TBL exists in ruby 1.8.7 but not ruby 1.8.6 453 | #ifdef RHASH_TBL 454 | st_table *tbl = RHASH_TBL(obj); 455 | #else 456 | st_table *tbl = RHASH(obj)->tbl; 457 | #endif 458 | CFIndex count = (CFIndex)tbl->num_entries; 459 | CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorDefault, count, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); 460 | st_foreach(tbl, iterateHash, (VALUE)dict); 461 | return dict; 462 | } 463 | 464 | // Converts an Array to a CFArrayRef 465 | CFArrayRef convertArray(VALUE obj) { 466 | CFIndex count = (CFIndex)RARRAY_LEN(obj); 467 | CFMutableArrayRef array = CFArrayCreateMutable(kCFAllocatorDefault, count, &kCFTypeArrayCallBacks); 468 | int i; 469 | for (i = 0; i < count; i++) { 470 | CFPropertyListRef aVal = convertObject(RARRAY_PTR(obj)[i]); 471 | CFArrayAppendValue(array, aVal); 472 | CFRelease(aVal); 473 | } 474 | return array; 475 | } 476 | 477 | // Converts a Number to a CFNumberRef 478 | CFNumberRef convertNumber(VALUE obj) { 479 | void *valuePtr; 480 | CFNumberType type; 481 | switch (TYPE(obj)) { 482 | case T_FLOAT: { 483 | double num = NUM2DBL(obj); 484 | valuePtr = # 485 | type = kCFNumberDoubleType; 486 | break; 487 | } 488 | case T_FIXNUM: { 489 | int num = NUM2INT(obj); 490 | valuePtr = # 491 | type = kCFNumberIntType; 492 | break; 493 | } 494 | case T_BIGNUM: { 495 | #ifdef NUM2LL 496 | long long num = NUM2LL(obj); 497 | type = kCFNumberLongLongType; 498 | #else 499 | long num = NUM2LONG(obj); 500 | type = kCFNumberLongType; 501 | #endif 502 | valuePtr = # 503 | break; 504 | } 505 | default: 506 | rb_raise(rb_eStandardError, "ERROR: Wrong object type passed to convertNumber"); 507 | return NULL; 508 | } 509 | CFNumberRef number = CFNumberCreate(kCFAllocatorDefault, type, valuePtr); 510 | return number; 511 | } 512 | 513 | // Converts a Time to a CFDateRef 514 | CFDateRef convertTime(VALUE obj) { 515 | VALUE secs = rb_funcall(obj, id_minus, 1, timeEpoch); 516 | CFDateRef date = CFDateCreate(kCFAllocatorDefault, NUM2DBL(secs)); 517 | return date; 518 | } 519 | 520 | /* call-seq: 521 | * str.blob? 522 | * 523 | * Returns whether or not +str+ is a blob. 524 | */ 525 | VALUE str_blob(VALUE self) { 526 | VALUE blob = rb_attr_get(self, id_blob); 527 | if (NIL_P(blob)) { 528 | return Qfalse; 529 | } else { 530 | return blob; 531 | } 532 | } 533 | 534 | /* call-seq: 535 | * str.blob = bool 536 | * 537 | * Sets the blob status of +str+. 538 | */ 539 | VALUE str_setBlob(VALUE self, VALUE b) { 540 | if (TYPE(b) == T_TRUE || TYPE(b) == T_FALSE) { 541 | return rb_ivar_set(self, id_blob, b); 542 | } else { 543 | rb_raise(rb_eArgError, "Argument 1 must be true or false"); 544 | return Qnil; 545 | } 546 | } 547 | 548 | /* 549 | * Document-module: OSX 550 | */ 551 | 552 | /* 553 | * Document-module: OSX::PropertyList 554 | * 555 | * The PropertyList module provides a means of converting a 556 | * Ruby Object to a Property List. 557 | * 558 | * The various Objects that can be converted are the ones 559 | * with an equivalent in CoreFoundation. This includes: String, 560 | * Integer, Float, Boolean, Time, Hash, and Array. 561 | * 562 | * See also: String#blob?, String#blob=, and Object#to_plist 563 | */ 564 | 565 | /* 566 | * Document-class: OSX::PropertyListError 567 | */ 568 | void Init_plist() { 569 | mOSX = rb_define_module("OSX"); 570 | mPlist = rb_define_module_under(mOSX, "PropertyList"); 571 | rb_define_module_function(mPlist, "load", plist_load, -1); 572 | rb_define_module_function(mPlist, "dump", plist_dump, -1); 573 | rb_define_method(rb_cObject, "to_plist", obj_to_plist, -1); 574 | rb_define_method(rb_cString, "blob?", str_blob, 0); 575 | rb_define_method(rb_cString, "blob=", str_setBlob, 1); 576 | ePropertyListError = rb_define_class_under(mOSX, "PropertyListError", rb_eStandardError); 577 | id_gm = rb_intern("gm"); 578 | timeEpoch = rb_funcall(rb_cTime, id_gm, 1, INT2FIX(2001)); 579 | /* Time.gm(2001): The Cocoa epoch of January 1st, 2001*/ 580 | rb_define_const(mPlist, "EPOCH", timeEpoch); 581 | id_plus = rb_intern("+"); 582 | id_minus = rb_intern("-"); 583 | id_read = rb_intern("read"); 584 | id_write = rb_intern("write"); 585 | id_xml = rb_intern("xml1"); 586 | id_binary = rb_intern("binary1"); 587 | id_openstep = rb_intern("openstep"); 588 | id_blob = rb_intern("@blob"); 589 | } 590 | --------------------------------------------------------------------------------