446 lines
10 KiB
JavaScript
446 lines
10 KiB
JavaScript
|
'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);
|
||
|
} 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 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) {
|
||
|
// 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 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 {
|
||
|
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;
|