'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 'UID': writeUID(entry); break; case 'array': writeArray(entry); break; case 'boolean': writeBoolean(entry); break; case 'string': case 'string-utf16': writeString(entry); break; case 'date': writeDate(entry); break; case 'data': writeData(entry); break; default: throw new Error("unhandled entry type: " + entry.type); } } function writeDate(entry) { writeByte(0x33); var date = (Date.parse(entry.value)/1000) - 978307200 writeDouble(date) } 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, true); } 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(0x14); writeBytes(entry.value, 8); } } else { writeByte(0x23); writeDouble(entry.value); } } function writeUID(entry) { if (debug) { console.log('0x' + buffer.size().toString(16), 'writeUID', entry.value, ' (type: ' + entry.type + ')', '(id: ' + entry.id + ')'); } writeIntHeader(0x8, 0x0); writeID(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' || mustBeUtf16(entry.value)) { 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, 'ascii'); 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, is_signedint) { // write low-order bytes big-endian style var buf = new Buffer(bytes); var z = 0; // javascript doesn't handle large numbers if(!is_signedint) { while (bytes > 4) { buf[z++] = 0; bytes--; } } for (var i = bytes - 1; i >= 0; i--) { buf[z++] = value >> (8 * i); } buffer.write(buf); } function mustBeUtf16(string) { return Buffer.byteLength(string, 'utf8') != string.length; } }; 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') { if (dicts instanceof Date) { return [ { type: 'date', value: dicts } ] } else if (Object.keys(dicts).length == 1 && typeof(dicts.UID) === 'number') { return [ { type: 'UID', value: dicts.UID } ] } else { 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 if (typeof(dicts) === 'bigint') { return [ { type: 'number', value: Number(BigInt.asIntN(32, 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;