public
Rubygem
Description: Ruby osx/plist extension for reading/writing property lists
Clone URL: git://github.com/kballard/osx-plist.git
kballard (author)
Fri Apr 25 09:27:36 -0700 2008
commit  c1aa253bce6be187765b8ddfe613ba755457e3a4
tree    e653ee6e94ca884ae9a49c015ebb74fb6071fa7d
parent  1045625fa4581907cd6e755958af50ed481a946f
osx-plist / plist.c
100644 580 lines (542 sloc) 17.869 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
/*
* plist
* Kevin Ballard
*
* This is a Ruby extension to read/write Cocoa property lists
* Not surprisingly, it only works on OS X
*
* Copyright © 2005, Kevin Ballard
*
* Usage:
* This extension provides a module named OSX::PropertyList
* This module has two methods:
*
* PropertyList::load(obj, format = false)
* Takes either an IO stream open for reading or a String object
* Returns an object representing the property list
*
* Optionally takes a boolean format argument. If true, the
* return value is an array with the second value being
* the format of the plist, which can be one of
* :xml1, :binary1, or :openstep
*
* PropertyList::dump(io, obj, type = :xml1)
* Takes an IO stream (open for writing) and an object
* Writes the object to the IO stream as a property list
* Posible type values are :xml1 and :binary1
*
* It also adds a new method to Object:
*
* Object#to_plist(type = :xml1)
* Returns a string representation of the property list
* Possible type values are :xml1 and :binary1
*
* It also adds 2 new methods to String:
*
* String#blob=(b)
* Sets whether the string is a blob
*
* String#blob?
* Returns whether the string is a blob
*
* A blob string is turned into a CFData when dumped
*
*/
 
/*
* Document-class: PropertyList
*
* The PropertyList module provides a means of converting a
* Ruby Object to a Property List.
*
* The various Objects that can be converted are the ones
* with an equivalent in CoreFoundation. This includes: String,
* Integer, Float, Boolean, Time, Hash, and Array.
*
* See also: String#blob?, String#blob=, and Object#to_plist
*/
 
#include <ruby.h>
#include <st.h>
#include <CoreFoundation/CoreFoundation.h>
 
// Here's some convenience macros
#ifndef StringValue
#define StringValue(x) do {                \
    if (TYPE(x) != T_STRING) x = rb_str_to_str(x);  \
  } while (0)
#endif
 
static VALUE mOSX;
static VALUE mPlist;
static VALUE timeEpoch;
static VALUE ePropertyListError;
 
static VALUE id_gm;
static VALUE id_plus;
static VALUE id_minus;
static VALUE id_read;
static VALUE id_write;
 
static VALUE id_xml;
static VALUE id_binary;
static VALUE id_openstep;
 
static VALUE id_blob;
 
VALUE convertPropertyListRef(CFPropertyListRef plist);
VALUE convertStringRef(CFStringRef plist);
VALUE convertDictionaryRef(CFDictionaryRef plist);
VALUE convertArrayRef(CFArrayRef plist);
VALUE convertNumberRef(CFNumberRef plist);
VALUE convertBooleanRef(CFBooleanRef plist);
VALUE convertDataRef(CFDataRef plist);
VALUE convertDateRef(CFDateRef plist);
VALUE str_blob(VALUE self);
VALUE str_setBlob(VALUE self, VALUE b);
 
// Raises a Ruby exception with the given string
void raiseError(CFStringRef error) {
    char *errBuffer = (char *)CFStringGetCStringPtr(error, kCFStringEncodingUTF8);
    int freeBuffer = 0;
    if (!errBuffer) {
      int len = CFStringGetLength(error)*2+1;
      errBuffer = ALLOC_N(char, len);
      Boolean succ = CFStringGetCString(error, errBuffer, len, kCFStringEncodingUTF8);
      if (!succ) {
        CFStringGetCString(error, errBuffer, len, kCFStringEncodingMacRoman);
      }
      freeBuffer = 1;
    }
    rb_raise(ePropertyListError, (char *)errBuffer);
    if (freeBuffer) free(errBuffer);
}
 
/* call-seq:
* PropertyList.load(obj) -> object
* PropertyList.load(obj, format) -> [object, format]
*
* Loads a property list from an IO stream or a String and creates
* an equivalent Object from it.
*
* If +format+ is provided, it returns one of
* <tt>:xml1</tt>, <tt>:binary1</tt>, or <tt>:openstep</tt>.
*/
VALUE plist_load(int argc, VALUE *argv, VALUE self) {
  VALUE io, retFormat;
  int count = rb_scan_args(argc, argv, "11", &io, &retFormat);
  if (count < 2) retFormat = Qfalse;
  VALUE buffer;
  if (RTEST(rb_respond_to(io, id_read))) {
    // Read from IO
    buffer = rb_funcall(io, id_read, 0);
  } else {
    StringValue(io);
    buffer = io;
  }
  // For some reason, the CFReadStream version doesn't work with input < 6 characters
  // but the CFDataRef version doesn't return format
  // So lets use the CFDataRef version unless format is requested
  CFStringRef error = NULL;
  CFPropertyListRef plist;
  CFPropertyListFormat format;
  if (RTEST(retFormat)) {
    // Format was requested
    // now just in case, if the input is < 6 characters, we will pad it out with newlines
    // we could do this in all cases, but I don't think it will work with binary
    // even though binary shouldn't be < 6 characters
    UInt8 *bytes;
    int len;
    if (RSTRING(buffer)->len < 6) {
      bytes = ALLOC_N(UInt8, 6);
      memset(bytes, '\n', 6);
      MEMCPY(bytes, RSTRING(buffer)->ptr, UInt8, RSTRING(buffer)->len);
      len = 6;
    } else {
      bytes = (UInt8 *)RSTRING(buffer)->ptr;
      len = RSTRING(buffer)->len;
    }
    CFReadStreamRef readStream = CFReadStreamCreateWithBytesNoCopy(kCFAllocatorDefault, bytes, len, kCFAllocatorNull);
    CFReadStreamOpen(readStream);
    plist = CFPropertyListCreateFromStream(kCFAllocatorDefault, readStream, 0, kCFPropertyListImmutable, &format, &error);
    CFReadStreamClose(readStream);
    CFRelease(readStream);
  } else {
    // Format wasn't requested
    CFDataRef data = CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, (const UInt8*)RSTRING(buffer)->ptr, RSTRING(buffer)->len, kCFAllocatorNull);
    plist = CFPropertyListCreateFromXMLData(kCFAllocatorDefault, data, kCFPropertyListImmutable, &error);
    CFRelease(data);
  }
  if (error) {
    raiseError(error);
    CFRelease(error);
    return Qnil;
  }
  VALUE obj = convertPropertyListRef(plist);
  CFRelease(plist);
  if (RTEST(retFormat)) {
    VALUE ary = rb_ary_new();
    rb_ary_push(ary, obj);
    if (format == kCFPropertyListOpenStepFormat) {
      retFormat = id_openstep;
    } else if (format == kCFPropertyListXMLFormat_v1_0) {
      retFormat = id_xml;
    } else if (format == kCFPropertyListBinaryFormat_v1_0) {
      retFormat = id_binary;
    } else {
      retFormat = rb_intern("unknown");
    }
    rb_ary_push(ary, ID2SYM(retFormat));
    return ary;
  } else {
    return obj;
  }
}
 
// Maps the property list object to a ruby object
VALUE convertPropertyListRef(CFPropertyListRef plist) {
  CFTypeID typeID = CFGetTypeID(plist);
  if (typeID == CFStringGetTypeID()) {
    return convertStringRef((CFStringRef)plist);
  } else if (typeID == CFDictionaryGetTypeID()) {
    return convertDictionaryRef((CFDictionaryRef)plist);
  } else if (typeID == CFArrayGetTypeID()) {
    return convertArrayRef((CFArrayRef)plist);
  } else if (typeID == CFNumberGetTypeID()) {
    return convertNumberRef((CFNumberRef)plist);
  } else if (typeID == CFBooleanGetTypeID()) {
    return convertBooleanRef((CFBooleanRef)plist);
  } else if (typeID == CFDataGetTypeID()) {
    return convertDataRef((CFDataRef)plist);
  } else if (typeID == CFDateGetTypeID()) {
    return convertDateRef((CFDateRef)plist);
  } else {
    return Qnil;
  }
}
 
// Converts a CFStringRef to a String
VALUE convertStringRef(CFStringRef plist) {
  CFIndex byteCount;
  CFRange range = CFRangeMake(0, CFStringGetLength(plist));
  CFStringEncoding enc = kCFStringEncodingUTF8;
  Boolean succ = CFStringGetBytes(plist, range, enc, 0, false, NULL, 0, &byteCount);
  if (!succ) {
    enc = kCFStringEncodingMacRoman;
    CFStringGetBytes(plist, range, enc, 0, false, NULL, 0, &byteCount);
  }
  UInt8 *buffer = ALLOC_N(UInt8, byteCount);
  CFStringGetBytes(plist, range, enc, 0, false, buffer, byteCount, NULL);
  VALUE retval = rb_str_new((char *)buffer, (long)byteCount);
  free(buffer);
  return retval;
}
 
// Converts the keys and values of a CFDictionaryRef
void dictionaryConverter(const void *key, const void *value, void *context) {
  rb_hash_aset((VALUE)context, convertPropertyListRef(key), convertPropertyListRef(value));
}
 
// Converts a CFDictionaryRef to a Hash
VALUE convertDictionaryRef(CFDictionaryRef plist) {
  VALUE hash = rb_hash_new();
  CFDictionaryApplyFunction(plist, dictionaryConverter, (void *)hash);
  return hash;
}
 
// Converts the values of a CFArrayRef
void arrayConverter(const void *value, void *context) {
  rb_ary_push((VALUE)context, convertPropertyListRef(value));
}
 
// Converts a CFArrayRef to an Array
VALUE convertArrayRef(CFArrayRef plist) {
  VALUE array = rb_ary_new();
  CFRange range = CFRangeMake(0, CFArrayGetCount(plist));
  CFArrayApplyFunction(plist, range, arrayConverter, (void *)array);
  return array;
}
 
// Converts a CFNumberRef to a Number
VALUE convertNumberRef(CFNumberRef plist) {
  if (CFNumberIsFloatType(plist)) {
    double val;
    CFNumberGetValue(plist, kCFNumberDoubleType, &val);
    return rb_float_new(val);
  } else {
#ifdef LL2NUM
    long long val;
    CFNumberGetValue(plist, kCFNumberLongLongType, &val);
    return LL2NUM(val);
#else
    long val;
    CFNumberGetValue(plist, kCFNumberLongType, &val);
    return LONG2NUM(val);
#endif
  }
}
 
// Converts a CFBooleanRef to a Boolean
VALUE convertBooleanRef(CFBooleanRef plist) {
  if (CFBooleanGetValue(plist)) {
    return Qtrue;
  } else {
    return Qfalse;
  }
}
 
// Converts a CFDataRef to a String (with blob set to true)
VALUE convertDataRef(CFDataRef plist) {
  const UInt8 *bytes = CFDataGetBytePtr(plist);
  CFIndex len = CFDataGetLength(plist);
  VALUE str = rb_str_new((char *)bytes, (long)len);
  str_setBlob(str, Qtrue);
  return str;
}
 
// Converts a CFDateRef to a Time
VALUE convertDateRef(CFDateRef plist) {
  CFAbsoluteTime seconds = CFDateGetAbsoluteTime(plist);
 
  // trunace the time since Ruby's Time object stores it as a 32 bit signed offset from 1970 (undocumented)
  const float min_time = -3124310400.0f;
  const float max_time = 1169098047.0f;
  seconds = seconds < min_time ? min_time : (seconds > max_time ? max_time : seconds);
 
  return rb_funcall(timeEpoch, id_plus, 1, rb_float_new(seconds));
}
 
CFPropertyListRef convertObject(VALUE obj);
 
// Converts a PropertyList object to a string representation
VALUE convertPlistToString(CFPropertyListRef plist, CFPropertyListFormat format) {
  CFWriteStreamRef writeStream = CFWriteStreamCreateWithAllocatedBuffers(kCFAllocatorDefault, kCFAllocatorDefault);
  CFWriteStreamOpen(writeStream);
  CFStringRef error = NULL;
  CFPropertyListWriteToStream(plist, writeStream, format, &error);
  CFWriteStreamClose(writeStream);
  if (error) {
    raiseError(error);
    return Qnil;
  }
  CFDataRef data = CFWriteStreamCopyProperty(writeStream, kCFStreamPropertyDataWritten);
  CFRelease(writeStream);
  VALUE plistData = convertDataRef(data);
  CFRelease(data);
  return plistData;
}
 
/* call-seq:
* PropertyList.dump(io, obj) -> Integer
* PropertyList.dump(io, obj, format) -> Integer
*
* Writes the property list representation of +obj+
* to the IO stream (must be open for writing).
*
* +format+ can be one of <tt>:xml1</tt> or <tt>:binary1</tt>.
*
* Returns the number of bytes written, or +nil+ if
* the object could not be represented as a property list
*/
VALUE plist_dump(int argc, VALUE *argv, VALUE self) {
  VALUE io, obj, type;
  int count = rb_scan_args(argc, argv, "21", &io, &obj, &type);
  if (count < 3) {
    type = id_xml;
  } else {
    type = rb_to_id(type);
  }
  if (type != id_xml && type != id_binary && type != id_openstep) {
    rb_raise(rb_eArgError, "Argument 3 must be one of :xml1, :binary1, or :openstep");
    return Qnil;
  }
  if (!RTEST(rb_respond_to(io, id_write))) {
    rb_raise(rb_eArgError, "Argument 1 must be an IO object");
    return Qnil;
  }
  CFPropertyListRef plist = convertObject(obj);
  CFPropertyListFormat format;
  if (type == id_xml) {
    format = kCFPropertyListXMLFormat_v1_0;
  } else if (type == id_binary) {
    format = kCFPropertyListBinaryFormat_v1_0;
  } else if (type == id_openstep) {
    format = kCFPropertyListOpenStepFormat;
  }
  VALUE data = convertPlistToString(plist, format);
  if (NIL_P(data)) {
    return Qnil;
  } else {
    return rb_funcall(io, id_write, 1, data);
  }
}
 
/* call-seq:
* object.to_plist -> String
* object.to_plist(format) -> String
*
* Converts the object to a property list representation
* and returns it as a string.
*
* +format+ can be one of <tt>:xml1</tt> or <tt>:binary1</tt>.
*/
VALUE obj_to_plist(int argc, VALUE *argv, VALUE self) {
  VALUE type;
  int count = rb_scan_args(argc, argv, "01", &type);
  if (count < 1) {
    type = id_xml;
  } else {
    type = rb_to_id(type);
  }
  if (type != id_xml && type != id_binary && type != id_openstep) {
    rb_raise(rb_eArgError, "Argument 2 must be one of :xml1, :binary1, or :openstep");
    return Qnil;
  }
  CFPropertyListRef plist = convertObject(self);
  CFPropertyListFormat format;
  if (type == id_xml) {
    format = kCFPropertyListXMLFormat_v1_0;
  } else if (type == id_binary) {
    format = kCFPropertyListBinaryFormat_v1_0;
  } else if (type == id_openstep) {
    format = kCFPropertyListOpenStepFormat;
  }
  VALUE data = convertPlistToString(plist, format);
  CFRelease(plist);
  if (type == id_xml || type == id_binary) {
    str_setBlob(data, Qfalse);
  }
  return data;
}
 
CFPropertyListRef convertString(VALUE obj);
CFDictionaryRef convertHash(VALUE obj);
CFArrayRef convertArray(VALUE obj);
CFNumberRef convertNumber(VALUE obj);
CFDateRef convertTime(VALUE obj);
 
// Converts an Object to a CFTypeRef
CFPropertyListRef convertObject(VALUE obj) {
  switch (TYPE(obj)) {
    case T_STRING: return convertString(obj); break;
    case T_HASH: return convertHash(obj); break;
    case T_ARRAY: return convertArray(obj); break;
    case T_FLOAT:
    case T_FIXNUM:
    case T_BIGNUM: return convertNumber(obj); break;
    case T_TRUE: return kCFBooleanTrue; break;
    case T_FALSE: return kCFBooleanFalse; break;
    default: if (rb_obj_is_kind_of(obj, rb_cTime)) return convertTime(obj);
  }
  rb_raise(rb_eArgError, "An object in the argument tree could not be converted");
  return NULL;
}
 
// Converts a String to a CFStringRef
CFPropertyListRef convertString(VALUE obj) {
  if (RTEST(str_blob(obj))) {
    // convert to CFDataRef
    StringValue(obj);
    CFDataRef data = CFDataCreate(kCFAllocatorDefault, (const UInt8*)RSTRING(obj)->ptr, (CFIndex)RSTRING(obj)->len);
    return data;
  } else {
    // convert to CFStringRef
    StringValue(obj);
    CFStringRef string = CFStringCreateWithBytes(kCFAllocatorDefault, (const UInt8*)RSTRING(obj)->ptr, (CFIndex)RSTRING(obj)->len, kCFStringEncodingUTF8, false);
    if (!string) {
      // try MacRoman
      string = CFStringCreateWithBytes(kCFAllocatorDefault, (const UInt8*)RSTRING(obj)->ptr, (CFIndex)RSTRING(obj)->len, kCFStringEncodingMacRoman, false);
    }
    return string;
  }
}
 
// Converts the keys and values of a Hash to CFTypeRefs
int iterateHash(VALUE key, VALUE val, VALUE dict) {
  CFPropertyListRef dKey = convertObject(key);
  CFPropertyListRef dVal = convertObject(val);
  CFDictionaryAddValue((CFMutableDictionaryRef)dict, dKey, dVal);
  CFRelease(dKey);
  CFRelease(dVal);
  return ST_CONTINUE;
}
 
// Converts a Hash to a CFDictionaryREf
CFDictionaryRef convertHash(VALUE obj) {
  CFIndex count = (CFIndex)RHASH(obj)->tbl->num_entries;
  CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorDefault, count, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
  st_foreach(RHASH(obj)->tbl, iterateHash, (VALUE)dict);
  return dict;
}
 
// Converts an Array to a CFArrayRef
CFArrayRef convertArray(VALUE obj) {
  CFIndex count = (CFIndex)RARRAY(obj)->len;
  CFMutableArrayRef array = CFArrayCreateMutable(kCFAllocatorDefault, count, &kCFTypeArrayCallBacks);
  int i;
  for (i = 0; i < count; i++) {
    CFPropertyListRef aVal = convertObject(RARRAY(obj)->ptr[i]);
    CFArrayAppendValue(array, aVal);
    CFRelease(aVal);
  }
  return array;
}
 
// Converts a Number to a CFNumberRef
CFNumberRef convertNumber(VALUE obj) {
  void *valuePtr;
  CFNumberType type;
  switch (TYPE(obj)) {
    case T_FLOAT: {
      double num = NUM2DBL(obj);
      valuePtr = &num;
      type = kCFNumberDoubleType;
      break;
    }
    case T_FIXNUM: {
      int num = NUM2INT(obj);
      valuePtr = &num;
      type = kCFNumberIntType;
      break;
    }
    case T_BIGNUM: {
#ifdef NUM2LL
      long long num = NUM2LL(obj);
      type = kCFNumberLongLongType;
#else
      long num = NUM2LONG(obj);
      type = kCFNumberLongType;
#endif
      valuePtr = &num;
      break;
    }
    default:
      rb_raise(rb_eStandardError, "ERROR: Wrong object type passed to convertNumber");
      return NULL;
  }
  CFNumberRef number = CFNumberCreate(kCFAllocatorDefault, type, valuePtr);
  return number;
}
 
// Converts a Time to a CFDateRef
CFDateRef convertTime(VALUE obj) {
  VALUE secs = rb_funcall(obj, id_minus, 1, timeEpoch);
  CFDateRef date = CFDateCreate(kCFAllocatorDefault, NUM2DBL(secs));
  return date;
}
 
/* call-seq:
* str.blob? -> Boolean
*
* Returns whether or not +str+ is a blob.
*/
VALUE str_blob(VALUE self) {
  VALUE blob = rb_attr_get(self, id_blob);
  if (NIL_P(blob)) {
    return Qfalse;
  } else {
    return blob;
  }
}
 
/* call-seq:
* str.blob = bool -> bool
*
* Sets the blob status of +str+.
*/
VALUE str_setBlob(VALUE self, VALUE b) {
  if (TYPE(b) == T_TRUE || TYPE(b) == T_FALSE) {
    return rb_ivar_set(self, id_blob, b);
  } else {
    rb_raise(rb_eArgError, "Argument 1 must be true or false");
    return Qnil;
  <