├── test ├── uid.bplist ├── airplay.bplist ├── sample1.bplist ├── sample2.bplist ├── utf16.bplist ├── integers.bplist ├── binaryData.bplist ├── iTunes-small.bplist └── creatorTest.js ├── .gitignore ├── bplistCreator.d.ts ├── package.json ├── LICENSE ├── README.md └── bplistCreator.js /test/uid.bplist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/node-bplist-creator/HEAD/test/uid.bplist -------------------------------------------------------------------------------- /test/airplay.bplist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/node-bplist-creator/HEAD/test/airplay.bplist -------------------------------------------------------------------------------- /test/sample1.bplist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/node-bplist-creator/HEAD/test/sample1.bplist -------------------------------------------------------------------------------- /test/sample2.bplist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/node-bplist-creator/HEAD/test/sample2.bplist -------------------------------------------------------------------------------- /test/utf16.bplist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/node-bplist-creator/HEAD/test/utf16.bplist -------------------------------------------------------------------------------- /test/integers.bplist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/node-bplist-creator/HEAD/test/integers.bplist -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build/* 2 | node_modules 3 | *.node 4 | *.sh 5 | *.swp 6 | .lock* 7 | npm-debug.log 8 | .idea 9 | -------------------------------------------------------------------------------- /test/binaryData.bplist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/node-bplist-creator/HEAD/test/binaryData.bplist -------------------------------------------------------------------------------- /test/iTunes-small.bplist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/node-bplist-creator/HEAD/test/iTunes-small.bplist -------------------------------------------------------------------------------- /bplistCreator.d.ts: -------------------------------------------------------------------------------- 1 | declare module "bplist-creator" { 2 | type PlistJsObj = any[] | Record; 3 | 4 | type BPlistCreator = (object: PlistJsObj) => Buffer; 5 | 6 | const BPlistCreator: BPlistCreator; 7 | export = BPlistCreator; 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bplist-creator", 3 | "version": "0.1.1", 4 | "description": "Binary Mac OS X Plist (property list) creator.", 5 | "main": "bplistCreator.js", 6 | "scripts": { 7 | "test": "./node_modules/nodeunit/bin/nodeunit test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/nearinfinity/node-bplist-creator.git" 12 | }, 13 | "keywords": [ 14 | "bplist", 15 | "plist", 16 | "creator" 17 | ], 18 | "author": "Joe Ferner", 19 | "license": "MIT", 20 | "devDependencies": { 21 | "bplist-parser": "0.3.0", 22 | "is-buffer": "1.1.x", 23 | "nodeunit": "0.9.x" 24 | }, 25 | "dependencies": { 26 | "stream-buffers": "2.2.x" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2012 Near Infinity Corporation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software 6 | and associated documentation files (the "Software"), to deal in the Software without restriction, 7 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial 12 | portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 15 | LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 17 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 18 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | bplist-creator 2 | ============== 3 | 4 | Binary Mac OS X Plist (property list) creator. 5 | 6 | ## Installation 7 | 8 | ```bash 9 | $ npm install bplist-creator 10 | ``` 11 | 12 | ## Quick Examples 13 | 14 | ```javascript 15 | var bplist = require('bplist-creator'); 16 | 17 | var buffer = bplist({ 18 | key1: [1, 2, 3] 19 | }); 20 | ``` 21 | 22 | ## Real/Double/Float handling 23 | 24 | Javascript don't have different types for `1` and `1.0`. This package 25 | will automatically store numbers as the appropriate type, but can't 26 | detect floats that is also integers. 27 | 28 | If you need to force a value to be written with the `real` type pass 29 | an instance of `Real`. 30 | 31 | ```javascript 32 | var buffer = bplist({ 33 | backgroundRed: new bplist.Real(1), 34 | backgroundGreen: new bplist.Real(0), 35 | backgroundBlue: new bplist.Real(0) 36 | }); 37 | ``` 38 | 39 | In `xml` the corresponding tags is `` and ``. 40 | 41 | ## License 42 | 43 | (The MIT License) 44 | 45 | Copyright (c) 2012 Near Infinity Corporation 46 | 47 | Permission is hereby granted, free of charge, to any person obtaining 48 | a copy of this software and associated documentation files (the 49 | "Software"), to deal in the Software without restriction, including 50 | without limitation the rights to use, copy, modify, merge, publish, 51 | distribute, sublicense, and/or sell copies of the Software, and to 52 | permit persons to whom the Software is furnished to do so, subject to 53 | the following conditions: 54 | 55 | The above copyright notice and this permission notice shall be 56 | included in all copies or substantial portions of the Software. 57 | 58 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 59 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 60 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 61 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 62 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 63 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 64 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 65 | -------------------------------------------------------------------------------- /test/creatorTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var nodeunit = require('nodeunit'); 6 | var bplistParser = require('bplist-parser'); 7 | var bplistCreator = require('../'); 8 | 9 | module.exports = { 10 | // 'iTunes Small': function(test) { 11 | // var file = path.join(__dirname, "iTunes-small.bplist"); 12 | // testFile(test, file); 13 | // }, 14 | 15 | 'sample1': function(test) { 16 | var file = path.join(__dirname, "sample1.bplist"); 17 | testFile(test, file); 18 | }, 19 | 20 | 'sample2': function(test) { 21 | var file = path.join(__dirname, "sample2.bplist"); 22 | testFile(test, file); 23 | }, 24 | 25 | 'binary data': function(test) { 26 | var file = path.join(__dirname, "binaryData.bplist"); 27 | testFile(test, file); 28 | }, 29 | 30 | 'airplay': function(test) { 31 | var file = path.join(__dirname, "airplay.bplist"); 32 | testFile(test, file); 33 | }, 34 | 35 | 'integers': function(test) { 36 | var file = path.join(__dirname, "integers.bplist"); 37 | testFile(test, file); 38 | }, 39 | 40 | // 'utf16': function(test) { 41 | // var file = path.join(__dirname, "utf16.bplist"); 42 | // testFile(test, file); 43 | // }, 44 | 45 | // 'uid': function(test) { 46 | // var file = path.join(__dirname, "uid.bplist"); 47 | // testFile(test, file); 48 | // } 49 | }; 50 | 51 | function testFile(test, file) { 52 | fs.readFile(file, function(err, fileData) { 53 | if (err) { 54 | return test.done(err); 55 | } 56 | 57 | bplistParser.parseFile(file, function(err, dicts) { 58 | if (err) { 59 | return test.done(err); 60 | } 61 | 62 | // airplay overrides 63 | if (dicts && dicts[0] && dicts[0].loadedTimeRanges && dicts[0].loadedTimeRanges[0] && dicts[0].loadedTimeRanges[0].hasOwnProperty('start')) { 64 | dicts[0].loadedTimeRanges[0].start = { 65 | bplistOverride: true, 66 | type: 'double', 67 | value: dicts[0].loadedTimeRanges[0].start 68 | }; 69 | } 70 | if (dicts && dicts[0] && dicts[0].loadedTimeRanges && dicts[0].seekableTimeRanges[0] && dicts[0].seekableTimeRanges[0].hasOwnProperty('start')) { 71 | dicts[0].seekableTimeRanges[0].start = { 72 | bplistOverride: true, 73 | type: 'double', 74 | value: dicts[0].seekableTimeRanges[0].start 75 | }; 76 | } 77 | if (dicts && dicts[0] && dicts[0].hasOwnProperty('rate')) { 78 | dicts[0].rate = { 79 | bplistOverride: true, 80 | type: 'double', 81 | value: dicts[0].rate 82 | }; 83 | } 84 | 85 | // utf16 86 | if (dicts && dicts[0] && dicts[0].hasOwnProperty('NSHumanReadableCopyright')) { 87 | dicts[0].NSHumanReadableCopyright = { 88 | bplistOverride: true, 89 | type: 'string-utf16', 90 | value: dicts[0].NSHumanReadableCopyright 91 | }; 92 | } 93 | if (dicts && dicts[0] && dicts[0].hasOwnProperty('CFBundleExecutable')) { 94 | dicts[0].CFBundleExecutable = { 95 | bplistOverride: true, 96 | type: 'string', 97 | value: dicts[0].CFBundleExecutable 98 | }; 99 | } 100 | if (dicts && dicts[0] && dicts[0].CFBundleURLTypes && dicts[0].CFBundleURLTypes[0] && dicts[0].CFBundleURLTypes[0].hasOwnProperty('CFBundleURLSchemes')) { 101 | dicts[0].CFBundleURLTypes[0].CFBundleURLSchemes[0] = { 102 | bplistOverride: true, 103 | type: 'string', 104 | value: dicts[0].CFBundleURLTypes[0].CFBundleURLSchemes[0] 105 | }; 106 | } 107 | if (dicts && dicts[0] && dicts[0].hasOwnProperty('CFBundleDisplayName')) { 108 | dicts[0].CFBundleDisplayName = { 109 | bplistOverride: true, 110 | type: 'string', 111 | value: dicts[0].CFBundleDisplayName 112 | }; 113 | } 114 | if (dicts && dicts[0] && dicts[0].hasOwnProperty('DTPlatformBuild')) { 115 | dicts[0].DTPlatformBuild = { 116 | bplistOverride: true, 117 | type: 'string', 118 | value: dicts[0].DTPlatformBuild 119 | }; 120 | } 121 | 122 | // integer 123 | if (dicts && dicts[0] && dicts[0].hasOwnProperty('int64item')) { 124 | dicts[0].int64item = { 125 | bplistOverride: true, 126 | type: 'number', 127 | value: dicts[0].int64item.value 128 | }; 129 | } 130 | 131 | var buf = bplistCreator(dicts); 132 | compareBuffers(test, buf, fileData); 133 | return test.done(); 134 | }); 135 | }); 136 | } 137 | 138 | function compareBuffers(test, buf1, buf2) { 139 | if (buf1.length !== buf2.length) { 140 | printBuffers(buf1, buf2); 141 | return test.fail("buffer size mismatch. found: " + buf1.length + ", expected: " + buf2.length + "."); 142 | } 143 | for (var i = 0; i < buf1.length; i++) { 144 | if (buf1[i] !== buf2[i]) { 145 | printBuffers(buf1, buf2); 146 | return test.fail("buffer mismatch at offset 0x" + i.toString(16) + ". found: 0x" + buf1[i].toString(16) + ", expected: 0x" + buf2[i].toString(16) + "."); 147 | } 148 | } 149 | } 150 | 151 | function printBuffers(buf1, buf2) { 152 | var i, t; 153 | for (var lineOffset = 0; lineOffset < buf1.length || lineOffset < buf2.length; lineOffset += 16) { 154 | var line = ''; 155 | 156 | t = ('000000000' + lineOffset.toString(16)); 157 | line += t.substr(t.length - 8) + ': '; 158 | 159 | for (i = 0; i < 16; i++) { 160 | if (i == 8) { 161 | line += ' '; 162 | } 163 | if (lineOffset + i < buf1.length) { 164 | t = ('00' + buf1[lineOffset + i].toString(16)); 165 | line += t.substr(t.length - 2) + ' '; 166 | } else { 167 | line += ' '; 168 | } 169 | } 170 | line += ' '; 171 | for (i = 0; i < 16; i++) { 172 | if (lineOffset + i < buf1.length) { 173 | t = String.fromCharCode(buf1[lineOffset + i]); 174 | if (t < ' ' || t > '~') { 175 | t = '.'; 176 | } 177 | line += t; 178 | } else { 179 | line += ' '; 180 | } 181 | } 182 | 183 | line += ' - '; 184 | 185 | for (i = 0; i < 16; i++) { 186 | if (i == 8) { 187 | line += ' '; 188 | } 189 | if (lineOffset + i < buf2.length) { 190 | t = ('00' + buf2[lineOffset + i].toString(16)); 191 | line += t.substr(t.length - 2) + ' '; 192 | } else { 193 | line += ' '; 194 | } 195 | } 196 | line += ' '; 197 | for (i = 0; i < 16; i++) { 198 | if (lineOffset + i < buf2.length) { 199 | t = String.fromCharCode(buf2[lineOffset + i]); 200 | if (t < ' ' || t > '~') { 201 | t = '.'; 202 | } 203 | line += t; 204 | } else { 205 | line += ' '; 206 | } 207 | } 208 | 209 | console.log(line); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /bplistCreator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // adapted from http://code.google.com/p/plist/source/browse/trunk/src/main/java/com/dd/plist/BinaryPropertyListWriter.java 4 | 5 | var streamBuffers = require("stream-buffers"); 6 | 7 | var debug = false; 8 | 9 | function Real(value) { 10 | this.value = value; 11 | } 12 | 13 | module.exports = function(dicts) { 14 | var buffer = new streamBuffers.WritableStreamBuffer(); 15 | buffer.write(Buffer.from("bplist00")); 16 | 17 | if (debug) { 18 | console.log('create', require('util').inspect(dicts, false, 10)); 19 | } 20 | 21 | if (dicts instanceof Array && dicts.length === 1) { 22 | dicts = dicts[0]; 23 | } 24 | 25 | var entries = toEntries(dicts); 26 | if (debug) { 27 | console.log('entries', entries); 28 | } 29 | var idSizeInBytes = computeIdSizeInBytes(entries.length); 30 | var offsets = []; 31 | var offsetSizeInBytes; 32 | var offsetTableOffset; 33 | 34 | updateEntryIds(); 35 | 36 | entries.forEach(function(entry, entryIdx) { 37 | offsets[entryIdx] = buffer.size(); 38 | if (!entry) { 39 | buffer.write(0x00); 40 | } else { 41 | write(entry); 42 | } 43 | }); 44 | 45 | writeOffsetTable(); 46 | writeTrailer(); 47 | return buffer.getContents(); 48 | 49 | function updateEntryIds() { 50 | var strings = {}; 51 | var entryId = 0; 52 | entries.forEach(function(entry) { 53 | if (entry.id) { 54 | return; 55 | } 56 | if (entry.type === 'string') { 57 | if (!entry.bplistOverride && strings.hasOwnProperty(entry.value)) { 58 | entry.type = 'stringref'; 59 | entry.id = strings[entry.value]; 60 | } else { 61 | strings[entry.value] = entry.id = entryId++; 62 | } 63 | } else { 64 | entry.id = entryId++; 65 | } 66 | }); 67 | 68 | entries = entries.filter(function(entry) { 69 | return (entry.type !== 'stringref'); 70 | }); 71 | } 72 | 73 | function writeTrailer() { 74 | if (debug) { 75 | console.log('0x' + buffer.size().toString(16), 'writeTrailer'); 76 | } 77 | // 6 null bytes 78 | buffer.write(Buffer.from([0, 0, 0, 0, 0, 0])); 79 | 80 | // size of an offset 81 | if (debug) { 82 | console.log('0x' + buffer.size().toString(16), 'writeTrailer(offsetSizeInBytes):', offsetSizeInBytes); 83 | } 84 | writeByte(offsetSizeInBytes); 85 | 86 | // size of a ref 87 | if (debug) { 88 | console.log('0x' + buffer.size().toString(16), 'writeTrailer(offsetSizeInBytes):', idSizeInBytes); 89 | } 90 | writeByte(idSizeInBytes); 91 | 92 | // number of objects 93 | if (debug) { 94 | console.log('0x' + buffer.size().toString(16), 'writeTrailer(number of objects):', entries.length); 95 | } 96 | writeLong(entries.length); 97 | 98 | // top object 99 | if (debug) { 100 | console.log('0x' + buffer.size().toString(16), 'writeTrailer(top object)'); 101 | } 102 | writeLong(0); 103 | 104 | // offset table offset 105 | if (debug) { 106 | console.log('0x' + buffer.size().toString(16), 'writeTrailer(offset table offset):', offsetTableOffset); 107 | } 108 | writeLong(offsetTableOffset); 109 | } 110 | 111 | function writeOffsetTable() { 112 | if (debug) { 113 | console.log('0x' + buffer.size().toString(16), 'writeOffsetTable'); 114 | } 115 | offsetTableOffset = buffer.size(); 116 | offsetSizeInBytes = computeOffsetSizeInBytes(offsetTableOffset); 117 | offsets.forEach(function(offset) { 118 | writeBytes(offset, offsetSizeInBytes); 119 | }); 120 | } 121 | 122 | function write(entry) { 123 | switch (entry.type) { 124 | case 'dict': 125 | writeDict(entry); 126 | break; 127 | case 'number': 128 | case 'double': 129 | writeNumber(entry); 130 | break; 131 | case 'UID': 132 | writeUID(entry); 133 | break; 134 | case 'array': 135 | writeArray(entry); 136 | break; 137 | case 'boolean': 138 | writeBoolean(entry); 139 | break; 140 | case 'string': 141 | case 'string-utf16': 142 | writeString(entry); 143 | break; 144 | case 'date': 145 | writeDate(entry); 146 | break; 147 | case 'data': 148 | writeData(entry); 149 | break; 150 | default: 151 | throw new Error("unhandled entry type: " + entry.type); 152 | } 153 | } 154 | 155 | function writeDate(entry) { 156 | writeByte(0x33); 157 | var date = (Date.parse(entry.value)/1000) - 978307200 158 | writeDouble(date) 159 | } 160 | 161 | function writeDict(entry) { 162 | if (debug) { 163 | var keysStr = entry.entryKeys.map(function(k) {return k.id;}); 164 | var valsStr = entry.entryValues.map(function(k) {return k.id;}); 165 | console.log('0x' + buffer.size().toString(16), 'writeDict', '(id: ' + entry.id + ')', '(keys: ' + keysStr + ')', '(values: ' + valsStr + ')'); 166 | } 167 | writeIntHeader(0xD, entry.entryKeys.length); 168 | entry.entryKeys.forEach(function(entry) { 169 | writeID(entry.id); 170 | }); 171 | entry.entryValues.forEach(function(entry) { 172 | writeID(entry.id); 173 | }); 174 | } 175 | 176 | function writeNumber(entry) { 177 | if (debug) { 178 | console.log('0x' + buffer.size().toString(16), 'writeNumber', entry.value, ' (type: ' + entry.type + ')', '(id: ' + entry.id + ')'); 179 | } 180 | 181 | if (typeof entry.value === 'bigint') { 182 | var width = 16; 183 | var hex = entry.value.toString(width); 184 | var buf = Buffer.from(hex.padStart(width * 2, '0').slice(0, width * 2), 'hex'); 185 | writeByte(0x14); 186 | buffer.write(buf); 187 | } else if (entry.type !== 'double' && parseFloat(entry.value).toFixed() == entry.value) { 188 | if (entry.value < 0) { 189 | writeByte(0x13); 190 | writeBytes(entry.value, 8, true); 191 | } else if (entry.value <= 0xff) { 192 | writeByte(0x10); 193 | writeBytes(entry.value, 1); 194 | } else if (entry.value <= 0xffff) { 195 | writeByte(0x11); 196 | writeBytes(entry.value, 2); 197 | } else if (entry.value <= 0xffffffff) { 198 | writeByte(0x12); 199 | writeBytes(entry.value, 4); 200 | } else { 201 | writeByte(0x13); 202 | writeBytes(entry.value, 8); 203 | } 204 | } else { 205 | writeByte(0x23); 206 | writeDouble(entry.value); 207 | } 208 | } 209 | 210 | function writeUID(entry) { 211 | if (debug) { 212 | console.log('0x' + buffer.size().toString(16), 'writeUID', entry.value, ' (type: ' + entry.type + ')', '(id: ' + entry.id + ')'); 213 | } 214 | 215 | writeIntHeader(0x8, 0x0); 216 | writeID(entry.value); 217 | } 218 | 219 | function writeArray(entry) { 220 | if (debug) { 221 | console.log('0x' + buffer.size().toString(16), 'writeArray (length: ' + entry.entries.length + ')', '(id: ' + entry.id + ')'); 222 | } 223 | writeIntHeader(0xA, entry.entries.length); 224 | entry.entries.forEach(function(e) { 225 | writeID(e.id); 226 | }); 227 | } 228 | 229 | function writeBoolean(entry) { 230 | if (debug) { 231 | console.log('0x' + buffer.size().toString(16), 'writeBoolean', entry.value, '(id: ' + entry.id + ')'); 232 | } 233 | writeByte(entry.value ? 0x09 : 0x08); 234 | } 235 | 236 | function writeString(entry) { 237 | if (debug) { 238 | console.log('0x' + buffer.size().toString(16), 'writeString', entry.value, '(id: ' + entry.id + ')'); 239 | } 240 | if (entry.type === 'string-utf16' || mustBeUtf16(entry.value)) { 241 | var utf16 = Buffer.from(entry.value, 'ucs2'); 242 | writeIntHeader(0x6, utf16.length / 2); 243 | // needs to be big endian so swap the bytes 244 | for (var i = 0; i < utf16.length; i += 2) { 245 | var t = utf16[i + 0]; 246 | utf16[i + 0] = utf16[i + 1]; 247 | utf16[i + 1] = t; 248 | } 249 | buffer.write(utf16); 250 | } else { 251 | var utf8 = Buffer.from(entry.value, 'ascii'); 252 | writeIntHeader(0x5, utf8.length); 253 | buffer.write(utf8); 254 | } 255 | } 256 | 257 | function writeData(entry) { 258 | if (debug) { 259 | console.log('0x' + buffer.size().toString(16), 'writeData', entry.value, '(id: ' + entry.id + ')'); 260 | } 261 | writeIntHeader(0x4, entry.value.length); 262 | buffer.write(entry.value); 263 | } 264 | 265 | function writeLong(l) { 266 | writeBytes(l, 8); 267 | } 268 | 269 | function writeByte(b) { 270 | buffer.write(Buffer.from([b])); 271 | } 272 | 273 | function writeDouble(v) { 274 | var buf = Buffer.alloc(8); 275 | buf.writeDoubleBE(v, 0); 276 | buffer.write(buf); 277 | } 278 | 279 | function writeIntHeader(kind, value) { 280 | if (value < 15) { 281 | writeByte((kind << 4) + value); 282 | } else if (value < 256) { 283 | writeByte((kind << 4) + 15); 284 | writeByte(0x10); 285 | writeBytes(value, 1); 286 | } else if (value < 65536) { 287 | writeByte((kind << 4) + 15); 288 | writeByte(0x11); 289 | writeBytes(value, 2); 290 | } else { 291 | writeByte((kind << 4) + 15); 292 | writeByte(0x12); 293 | writeBytes(value, 4); 294 | } 295 | } 296 | 297 | function writeID(id) { 298 | writeBytes(id, idSizeInBytes); 299 | } 300 | 301 | function writeBytes(value, bytes, is_signedint) { 302 | // write low-order bytes big-endian style 303 | var buf = Buffer.alloc(bytes); 304 | var z = 0; 305 | 306 | // javascript doesn't handle large numbers 307 | while (bytes > 4) { 308 | buf[z++] = is_signedint ? 0xff : 0; 309 | bytes--; 310 | } 311 | 312 | for (var i = bytes - 1; i >= 0; i--) { 313 | buf[z++] = value >> (8 * i); 314 | } 315 | buffer.write(buf); 316 | } 317 | 318 | function mustBeUtf16(string) { 319 | return Buffer.byteLength(string, 'utf8') != string.length; 320 | } 321 | }; 322 | 323 | function toEntries(dicts) { 324 | if (dicts.bplistOverride) { 325 | return [dicts]; 326 | } 327 | 328 | if (dicts instanceof Array) { 329 | return toEntriesArray(dicts); 330 | } else if (dicts instanceof Buffer) { 331 | return [ 332 | { 333 | type: 'data', 334 | value: dicts 335 | } 336 | ]; 337 | } else if (dicts instanceof Real) { 338 | return [ 339 | { 340 | type: 'double', 341 | value: dicts.value 342 | } 343 | ]; 344 | } else if (typeof(dicts) === 'object') { 345 | if (dicts instanceof Date) { 346 | return [ 347 | { 348 | type: 'date', 349 | value: dicts 350 | } 351 | ] 352 | } else if (Object.keys(dicts).length == 1 && typeof(dicts.UID) === 'number') { 353 | return [ 354 | { 355 | type: 'UID', 356 | value: dicts.UID 357 | } 358 | ] 359 | } else { 360 | return toEntriesObject(dicts); 361 | } 362 | } else if (typeof(dicts) === 'string') { 363 | return [ 364 | { 365 | type: 'string', 366 | value: dicts 367 | } 368 | ]; 369 | } else if (typeof(dicts) === 'number') { 370 | return [ 371 | { 372 | type: 'number', 373 | value: dicts 374 | } 375 | ]; 376 | } else if (typeof(dicts) === 'boolean') { 377 | return [ 378 | { 379 | type: 'boolean', 380 | value: dicts 381 | } 382 | ]; 383 | } else if (typeof(dicts) === 'bigint') { 384 | return [ 385 | { 386 | type: 'number', 387 | value: dicts 388 | } 389 | ]; 390 | } else { 391 | throw new Error('unhandled entry: ' + dicts); 392 | } 393 | } 394 | 395 | function toEntriesArray(arr) { 396 | if (debug) { 397 | console.log('toEntriesArray'); 398 | } 399 | var results = [ 400 | { 401 | type: 'array', 402 | entries: [] 403 | } 404 | ]; 405 | arr.forEach(function(v) { 406 | var entry = toEntries(v); 407 | results[0].entries.push(entry[0]); 408 | results = results.concat(entry); 409 | }); 410 | return results; 411 | } 412 | 413 | function toEntriesObject(dict) { 414 | if (debug) { 415 | console.log('toEntriesObject'); 416 | } 417 | var results = [ 418 | { 419 | type: 'dict', 420 | entryKeys: [], 421 | entryValues: [] 422 | } 423 | ]; 424 | Object.keys(dict).forEach(function(key) { 425 | var entryKey = toEntries(key); 426 | results[0].entryKeys.push(entryKey[0]); 427 | results = results.concat(entryKey[0]); 428 | }); 429 | Object.keys(dict).forEach(function(key) { 430 | var entryValue = toEntries(dict[key]); 431 | results[0].entryValues.push(entryValue[0]); 432 | results = results.concat(entryValue); 433 | }); 434 | return results; 435 | } 436 | 437 | function computeOffsetSizeInBytes(maxOffset) { 438 | if (maxOffset < 256) { 439 | return 1; 440 | } 441 | if (maxOffset < 65536) { 442 | return 2; 443 | } 444 | if (maxOffset < 4294967296) { 445 | return 4; 446 | } 447 | return 8; 448 | } 449 | 450 | function computeIdSizeInBytes(numberOfIds) { 451 | if (numberOfIds < 256) { 452 | return 1; 453 | } 454 | if (numberOfIds < 65536) { 455 | return 2; 456 | } 457 | return 4; 458 | } 459 | 460 | module.exports.Real = Real; 461 | --------------------------------------------------------------------------------