blob: b480826e75037be2c44c9a46fbf9819235e64a15 [file] [log] [blame]
'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;