| 'use strict'; |
| |
| // adapted from http://code.google.com/p/plist/source/browse/trunk/src/main/java/com/dd/plist/BinaryPropertyListWriter.java |
| |
| var streamBuffers = require("stream-buffers"); |
| |
| var debug = false; |
| |
| function Real(value) { |
| this.value = value; |
| } |
| |
| module.exports = function(dicts) { |
| var buffer = new streamBuffers.WritableStreamBuffer(); |
| buffer.write(new Buffer("bplist00")); |
| |
| if (debug) { |
| console.log('create', require('util').inspect(dicts, false, 10)); |
| } |
| |
| if (dicts instanceof Array && dicts.length === 1) { |
| dicts = dicts[0]; |
| } |
| |
| var entries = toEntries(dicts); |
| if (debug) { |
| console.log('entries', entries); |
| } |
| var idSizeInBytes = computeIdSizeInBytes(entries.length); |
| var offsets = []; |
| var offsetSizeInBytes; |
| var offsetTableOffset; |
| |
| updateEntryIds(); |
| |
| entries.forEach(function(entry, entryIdx) { |
| offsets[entryIdx] = buffer.size(); |
| if (!entry) { |
| buffer.write(0x00); |
| } else { |
| write(entry); |
| } |
| }); |
| |
| writeOffsetTable(); |
| writeTrailer(); |
| return buffer.getContents(); |
| |
| function updateEntryIds() { |
| var strings = {}; |
| var entryId = 0; |
| entries.forEach(function(entry) { |
| if (entry.id) { |
| return; |
| } |
| if (entry.type === 'string') { |
| if (!entry.bplistOverride && strings.hasOwnProperty(entry.value)) { |
| entry.type = 'stringref'; |
| entry.id = strings[entry.value]; |
| } else { |
| strings[entry.value] = entry.id = entryId++; |
| } |
| } else { |
| entry.id = entryId++; |
| } |
| }); |
| |
| entries = entries.filter(function(entry) { |
| return (entry.type !== 'stringref'); |
| }); |
| } |
| |
| function writeTrailer() { |
| if (debug) { |
| console.log('0x' + buffer.size().toString(16), 'writeTrailer'); |
| } |
| // 6 null bytes |
| buffer.write(new Buffer([0, 0, 0, 0, 0, 0])); |
| |
| // size of an offset |
| if (debug) { |
| console.log('0x' + buffer.size().toString(16), 'writeTrailer(offsetSizeInBytes):', offsetSizeInBytes); |
| } |
| writeByte(offsetSizeInBytes); |
| |
| // size of a ref |
| if (debug) { |
| console.log('0x' + buffer.size().toString(16), 'writeTrailer(offsetSizeInBytes):', idSizeInBytes); |
| } |
| writeByte(idSizeInBytes); |
| |
| // number of objects |
| if (debug) { |
| console.log('0x' + buffer.size().toString(16), 'writeTrailer(number of objects):', entries.length); |
| } |
| writeLong(entries.length); |
| |
| // top object |
| if (debug) { |
| console.log('0x' + buffer.size().toString(16), 'writeTrailer(top object)'); |
| } |
| writeLong(0); |
| |
| // offset table offset |
| if (debug) { |
| console.log('0x' + buffer.size().toString(16), 'writeTrailer(offset table offset):', offsetTableOffset); |
| } |
| writeLong(offsetTableOffset); |
| } |
| |
| function writeOffsetTable() { |
| if (debug) { |
| console.log('0x' + buffer.size().toString(16), 'writeOffsetTable'); |
| } |
| offsetTableOffset = buffer.size(); |
| offsetSizeInBytes = computeOffsetSizeInBytes(offsetTableOffset); |
| offsets.forEach(function(offset) { |
| writeBytes(offset, offsetSizeInBytes); |
| }); |
| } |
| |
| function write(entry) { |
| switch (entry.type) { |
| case 'dict': |
| writeDict(entry); |
| break; |
| case 'number': |
| case 'double': |
| writeNumber(entry); |
| break; |
| case 'array': |
| writeArray(entry); |
| break; |
| case 'boolean': |
| writeBoolean(entry); |
| break; |
| case 'string': |
| case 'string-utf16': |
| writeString(entry); |
| break; |
| case 'data': |
| writeData(entry); |
| break; |
| default: |
| throw new Error("unhandled entry type: " + entry.type); |
| } |
| } |
| |
| function writeDict(entry) { |
| if (debug) { |
| var keysStr = entry.entryKeys.map(function(k) {return k.id;}); |
| var valsStr = entry.entryValues.map(function(k) {return k.id;}); |
| console.log('0x' + buffer.size().toString(16), 'writeDict', '(id: ' + entry.id + ')', '(keys: ' + keysStr + ')', '(values: ' + valsStr + ')'); |
| } |
| writeIntHeader(0xD, entry.entryKeys.length); |
| entry.entryKeys.forEach(function(entry) { |
| writeID(entry.id); |
| }); |
| entry.entryValues.forEach(function(entry) { |
| writeID(entry.id); |
| }); |
| } |
| |
| function writeNumber(entry) { |
| if (debug) { |
| console.log('0x' + buffer.size().toString(16), 'writeNumber', entry.value, ' (type: ' + entry.type + ')', '(id: ' + entry.id + ')'); |
| } |
| |
| if (entry.type !== 'double' && parseFloat(entry.value.toFixed()) == entry.value) { |
| if (entry.value < 0) { |
| writeByte(0x13); |
| writeBytes(entry.value, 8); |
| } else if (entry.value <= 0xff) { |
| writeByte(0x10); |
| writeBytes(entry.value, 1); |
| } else if (entry.value <= 0xffff) { |
| writeByte(0x11); |
| writeBytes(entry.value, 2); |
| } else if (entry.value <= 0xffffffff) { |
| writeByte(0x12); |
| writeBytes(entry.value, 4); |
| } else { |
| writeByte(0x13); |
| writeBytes(entry.value, 8); |
| } |
| } else { |
| writeByte(0x23); |
| writeDouble(entry.value); |
| } |
| } |
| |
| function writeArray(entry) { |
| if (debug) { |
| console.log('0x' + buffer.size().toString(16), 'writeArray (length: ' + entry.entries.length + ')', '(id: ' + entry.id + ')'); |
| } |
| writeIntHeader(0xA, entry.entries.length); |
| entry.entries.forEach(function(e) { |
| writeID(e.id); |
| }); |
| } |
| |
| function writeBoolean(entry) { |
| if (debug) { |
| console.log('0x' + buffer.size().toString(16), 'writeBoolean', entry.value, '(id: ' + entry.id + ')'); |
| } |
| writeByte(entry.value ? 0x09 : 0x08); |
| } |
| |
| function writeString(entry) { |
| if (debug) { |
| console.log('0x' + buffer.size().toString(16), 'writeString', entry.value, '(id: ' + entry.id + ')'); |
| } |
| if (entry.type === 'string-utf16') { |
| var utf16 = new Buffer(entry.value, 'ucs2'); |
| writeIntHeader(0x6, utf16.length / 2); |
| // needs to be big endian so swap the bytes |
| for (var i = 0; i < utf16.length; i += 2) { |
| var t = utf16[i + 0]; |
| utf16[i + 0] = utf16[i + 1]; |
| utf16[i + 1] = t; |
| } |
| buffer.write(utf16); |
| } else { |
| var utf8 = new Buffer(entry.value, 'utf8'); |
| writeIntHeader(0x5, utf8.length); |
| buffer.write(utf8); |
| } |
| } |
| |
| function writeData(entry) { |
| if (debug) { |
| console.log('0x' + buffer.size().toString(16), 'writeData', entry.value, '(id: ' + entry.id + ')'); |
| } |
| writeIntHeader(0x4, entry.value.length); |
| buffer.write(entry.value); |
| } |
| |
| function writeLong(l) { |
| writeBytes(l, 8); |
| } |
| |
| function writeByte(b) { |
| buffer.write(new Buffer([b])); |
| } |
| |
| function writeDouble(v) { |
| var buf = new Buffer(8); |
| buf.writeDoubleBE(v, 0); |
| buffer.write(buf); |
| } |
| |
| function writeIntHeader(kind, value) { |
| if (value < 15) { |
| writeByte((kind << 4) + value); |
| } else if (value < 256) { |
| writeByte((kind << 4) + 15); |
| writeByte(0x10); |
| writeBytes(value, 1); |
| } else if (value < 65536) { |
| writeByte((kind << 4) + 15); |
| writeByte(0x11); |
| writeBytes(value, 2); |
| } else { |
| writeByte((kind << 4) + 15); |
| writeByte(0x12); |
| writeBytes(value, 4); |
| } |
| } |
| |
| function writeID(id) { |
| writeBytes(id, idSizeInBytes); |
| } |
| |
| function writeBytes(value, bytes) { |
| // write low-order bytes big-endian style |
| var buf = new Buffer(bytes); |
| var z = 0; |
| // javascript doesn't handle large numbers |
| while (bytes > 4) { |
| buf[z++] = 0; |
| bytes--; |
| } |
| for (var i = bytes - 1; i >= 0; i--) { |
| buf[z++] = value >> (8 * i); |
| } |
| buffer.write(buf); |
| } |
| }; |
| |
| function toEntries(dicts) { |
| if (dicts.bplistOverride) { |
| return [dicts]; |
| } |
| |
| if (dicts instanceof Array) { |
| return toEntriesArray(dicts); |
| } else if (dicts instanceof Buffer) { |
| return [ |
| { |
| type: 'data', |
| value: dicts |
| } |
| ]; |
| } else if (dicts instanceof Real) { |
| return [ |
| { |
| type: 'double', |
| value: dicts.value |
| } |
| ]; |
| } else if (typeof(dicts) === 'object') { |
| return toEntriesObject(dicts); |
| } else if (typeof(dicts) === 'string') { |
| return [ |
| { |
| type: 'string', |
| value: dicts |
| } |
| ]; |
| } else if (typeof(dicts) === 'number') { |
| return [ |
| { |
| type: 'number', |
| value: dicts |
| } |
| ]; |
| } else if (typeof(dicts) === 'boolean') { |
| return [ |
| { |
| type: 'boolean', |
| value: dicts |
| } |
| ]; |
| } else { |
| throw new Error('unhandled entry: ' + dicts); |
| } |
| } |
| |
| function toEntriesArray(arr) { |
| if (debug) { |
| console.log('toEntriesArray'); |
| } |
| var results = [ |
| { |
| type: 'array', |
| entries: [] |
| } |
| ]; |
| arr.forEach(function(v) { |
| var entry = toEntries(v); |
| results[0].entries.push(entry[0]); |
| results = results.concat(entry); |
| }); |
| return results; |
| } |
| |
| function toEntriesObject(dict) { |
| if (debug) { |
| console.log('toEntriesObject'); |
| } |
| var results = [ |
| { |
| type: 'dict', |
| entryKeys: [], |
| entryValues: [] |
| } |
| ]; |
| Object.keys(dict).forEach(function(key) { |
| var entryKey = toEntries(key); |
| results[0].entryKeys.push(entryKey[0]); |
| results = results.concat(entryKey[0]); |
| }); |
| Object.keys(dict).forEach(function(key) { |
| var entryValue = toEntries(dict[key]); |
| results[0].entryValues.push(entryValue[0]); |
| results = results.concat(entryValue); |
| }); |
| return results; |
| } |
| |
| function computeOffsetSizeInBytes(maxOffset) { |
| if (maxOffset < 256) { |
| return 1; |
| } |
| if (maxOffset < 65536) { |
| return 2; |
| } |
| if (maxOffset < 4294967296) { |
| return 4; |
| } |
| return 8; |
| } |
| |
| function computeIdSizeInBytes(numberOfIds) { |
| if (numberOfIds < 256) { |
| return 1; |
| } |
| if (numberOfIds < 65536) { |
| return 2; |
| } |
| return 4; |
| } |
| |
| module.exports.Real = Real; |