├── .npmignore ├── .eslintignore ├── test ├── int64.bplist ├── uid.bplist ├── utf16.bplist ├── airplay.bplist ├── sample1.bplist ├── sample2.bplist ├── iTunes-small.bplist ├── utf16_chinese.plist ├── int64.xml └── parseTest.js ├── .gitignore ├── .editorconfig ├── bplistParser.d.ts ├── package.json ├── README.md ├── .eslintrc.js └── bplistParser.js /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /test/int64.bplist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/node-bplist-parser/HEAD/test/int64.bplist -------------------------------------------------------------------------------- /test/uid.bplist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/node-bplist-parser/HEAD/test/uid.bplist -------------------------------------------------------------------------------- /test/utf16.bplist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/node-bplist-parser/HEAD/test/utf16.bplist -------------------------------------------------------------------------------- /test/airplay.bplist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/node-bplist-parser/HEAD/test/airplay.bplist -------------------------------------------------------------------------------- /test/sample1.bplist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/node-bplist-parser/HEAD/test/sample1.bplist -------------------------------------------------------------------------------- /test/sample2.bplist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/node-bplist-parser/HEAD/test/sample2.bplist -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build/* 2 | node_modules 3 | *.node 4 | *.sh 5 | *.swp 6 | .lock* 7 | npm-debug.log 8 | .idea 9 | -------------------------------------------------------------------------------- /test/iTunes-small.bplist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/node-bplist-parser/HEAD/test/iTunes-small.bplist -------------------------------------------------------------------------------- /test/utf16_chinese.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/node-bplist-parser/HEAD/test/utf16_chinese.plist -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; EditorConfig file: https://EditorConfig.org 2 | ; Install the "EditorConfig" plugin into your editor to use 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | trim_trailing_whitespace = true 13 | -------------------------------------------------------------------------------- /test/int64.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | zero 6 | 0 7 | int32item 8 | 1234567890 9 | int32itemsigned 10 | -1234567890 11 | int64item 12 | 12345678901234567890 13 | 14 | 15 | -------------------------------------------------------------------------------- /bplistParser.d.ts: -------------------------------------------------------------------------------- 1 | export declare namespace bPlistParser { 2 | var maxObjectCount: number; 3 | var maxObjectSize: number; 4 | type CallbackFunction = (error: Error|null, result: [T]) => void 5 | export function parseFile(fileNameOrBuffer: string|Buffer, callback?: CallbackFunction): Promise<[T]> 6 | export function parseFileSync(fileNameOrBuffer: string|Buffer): [T] 7 | export function parseBuffer(buffer: string|Buffer): [T] 8 | } 9 | export default bPlistParser; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bplist-parser", 3 | "version": "0.3.2", 4 | "description": "Binary plist parser.", 5 | "main": "bplistParser.js", 6 | "type":"module", 7 | "scripts": { 8 | "test": "mocha test" 9 | }, 10 | "keywords": [ 11 | "bplist", 12 | "plist", 13 | "parser" 14 | ], 15 | "author": "Joe Ferner ", 16 | "contributors": [ 17 | "Brett Zamir" 18 | ], 19 | "license": "MIT", 20 | "devDependencies": { 21 | "eslint": "6.5.x", 22 | "mocha": "10.0.x" 23 | }, 24 | "homepage": "https://github.com/nearinfinity/node-bplist-parser", 25 | "bugs": "https://github.com/nearinfinity/node-bplist-parser/issues", 26 | "engines": { 27 | "node": ">= 5.10.0" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/nearinfinity/node-bplist-parser.git" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bplist-parser 2 | 3 | Binary Mac OS X Plist (property list) parser. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | $ npm install bplist-parser 9 | ``` 10 | 11 | ## Quick Examples 12 | 13 | ```javascript 14 | const bplist = require('bplist-parser'); 15 | 16 | (async () => { 17 | 18 | const obj = await bplist.parseFile('myPlist.bplist'); 19 | 20 | console.log(JSON.stringify(obj)); 21 | 22 | })(); 23 | ``` 24 | 25 | ## License 26 | 27 | (The MIT License) 28 | 29 | Copyright (c) 2012 Near Infinity Corporation 30 | 31 | Permission is hereby granted, free of charge, to any person obtaining 32 | a copy of this software and associated documentation files (the 33 | "Software"), to deal in the Software without restriction, including 34 | without limitation the rights to use, copy, modify, merge, publish, 35 | distribute, sublicense, and/or sell copies of the Software, and to 36 | permit persons to whom the Software is furnished to do so, subject to 37 | the following conditions: 38 | 39 | The above copyright notice and this permission notice shall be 40 | included in all copies or substantial portions of the Software. 41 | 42 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 43 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 44 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 45 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 46 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 47 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 48 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 49 | -------------------------------------------------------------------------------- /test/parseTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // tests are adapted from https://github.com/TooTallNate/node-plist 4 | 5 | import assert from 'assert'; 6 | import path from 'path'; 7 | import * as bplist from '../bplistParser.js'; 8 | 9 | const dirname = path.dirname(new URL(import.meta.url).pathname); 10 | 11 | describe('bplist-parser', function () { 12 | it('iTunes Small', async function () { 13 | const file = path.join(dirname, "iTunes-small.bplist"); 14 | const startTime1 = new Date(); 15 | 16 | const [dict] = await bplist.parseFile(file); 17 | const endTime = new Date(); 18 | console.log('Parsed "' + file + '" in ' + (endTime - startTime1) + 'ms'); 19 | assert.equal(dict['Application Version'], "9.0.3"); 20 | assert.equal(dict['Library Persistent ID'], "6F81D37F95101437"); 21 | assert.deepEqual(dict, bplist.parseFileSync(file)[0]); 22 | }); 23 | 24 | it('sample1', async function () { 25 | const file = path.join(dirname, "sample1.bplist"); 26 | const startTime = new Date(); 27 | 28 | const [dict] = await bplist.parseFile(file); 29 | const endTime = new Date(); 30 | console.log('Parsed "' + file + '" in ' + (endTime - startTime) + 'ms'); 31 | 32 | assert.equal(dict['CFBundleIdentifier'], 'com.apple.dictionary.MySample'); 33 | assert.deepEqual(dict, bplist.parseFileSync(file)[0]); 34 | }); 35 | 36 | it('sample2', async function () { 37 | const file = path.join(dirname, "sample2.bplist"); 38 | const startTime = new Date(); 39 | 40 | const [dict] = await bplist.parseFile(file); 41 | const endTime = new Date(); 42 | console.log('Parsed "' + file + '" in ' + (endTime - startTime) + 'ms'); 43 | 44 | assert.equal(dict['PopupMenu'][2]['Key'], "\n #import \n\n#import \n\nint main(int argc, char *argv[])\n{\n return macruby_main(\"rb_main.rb\", argc, argv);\n}\n"); 45 | assert.deepEqual(dict, bplist.parseFileSync(file)[0]); 46 | }); 47 | 48 | it('airplay', async function () { 49 | const file = path.join(dirname, "airplay.bplist"); 50 | const startTime = new Date(); 51 | 52 | const [dict] = await bplist.parseFile(file); 53 | const endTime = new Date(); 54 | console.log('Parsed "' + file + '" in ' + (endTime - startTime) + 'ms'); 55 | 56 | assert.equal(dict['duration'], 5555.0495000000001); 57 | assert.equal(dict['position'], 4.6269989039999997); 58 | assert.deepEqual(dict, bplist.parseFileSync(file)[0]); 59 | }); 60 | 61 | it('utf16', async function () { 62 | const file = path.join(dirname, "utf16.bplist"); 63 | const startTime = new Date(); 64 | 65 | const [dict] = await bplist.parseFile(file); 66 | const endTime = new Date(); 67 | console.log('Parsed "' + file + '" in ' + (endTime - startTime) + 'ms'); 68 | 69 | assert.equal(dict['CFBundleName'], 'sellStuff'); 70 | assert.equal(dict['CFBundleShortVersionString'], '2.6.1'); 71 | assert.equal(dict['NSHumanReadableCopyright'], '©2008-2012, sellStuff, Inc.'); 72 | assert.deepEqual(dict, bplist.parseFileSync(file)[0]); 73 | }); 74 | 75 | it('utf16chinese', async function () { 76 | const file = path.join(dirname, "utf16_chinese.plist"); 77 | const startTime = new Date(); 78 | 79 | const [dict] = await bplist.parseFile(file); 80 | const endTime = new Date(); 81 | console.log('Parsed "' + file + '" in ' + (endTime - startTime) + 'ms'); 82 | 83 | assert.equal(dict['CFBundleName'], '天翼阅读'); 84 | assert.equal(dict['CFBundleDisplayName'], '天翼阅读'); 85 | assert.deepEqual(dict, bplist.parseFileSync(file)[0]); 86 | }); 87 | 88 | it('uid', async function () { 89 | const file = path.join(dirname, "uid.bplist"); 90 | const startTime = new Date(); 91 | 92 | const [dict] = await bplist.parseFile(file); 93 | const endTime = new Date(); 94 | console.log('Parsed "' + file + '" in ' + (endTime - startTime) + 'ms'); 95 | 96 | assert.deepEqual(dict['$objects'][1]['NS.keys'], [{UID:2}, {UID:3}, {UID:4}]); 97 | assert.deepEqual(dict['$objects'][1]['NS.objects'], [{UID: 5}, {UID:6}, {UID:7}]); 98 | assert.deepEqual(dict['$top']['root'], {UID:1}); 99 | assert.deepEqual(dict, bplist.parseFileSync(file)[0]); 100 | }); 101 | 102 | it('int64', async function () { 103 | const file = path.join(dirname, "int64.bplist"); 104 | const startTime = new Date(); 105 | 106 | const [dict] = await bplist.parseFile(file); 107 | const endTime = new Date(); 108 | console.log('Parsed "' + file + '" in ' + (endTime - startTime) + 'ms'); 109 | 110 | assert.equal(dict['zero'], '0'); 111 | assert.equal(dict['int32item'], '1234567890'); 112 | assert.equal(dict['int32itemsigned'], '-1234567890'); 113 | assert.equal(dict['int64item'], '12345678901234567890'); 114 | assert.deepEqual(dict, bplist.parseFileSync(file)[0]); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "mocha": true, 4 | "node": true, 5 | "commonjs": true, 6 | "es6": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "globals": { 10 | "Atomics": "readonly", 11 | "SharedArrayBuffer": "readonly" 12 | }, 13 | "parserOptions": { 14 | "ecmaVersion": 2018 15 | }, 16 | "rules": { 17 | "accessor-pairs": "error", 18 | "array-bracket-newline": "error", 19 | "array-bracket-spacing": "off", 20 | "array-callback-return": "error", 21 | "array-element-newline": "off", 22 | "arrow-body-style": "error", 23 | "arrow-parens": "error", 24 | "arrow-spacing": "error", 25 | "block-scoped-var": "error", 26 | "block-spacing": "error", 27 | "brace-style": [ 28 | "error", 29 | "1tbs" 30 | ], 31 | "callback-return": "off", 32 | "camelcase": "off", 33 | "capitalized-comments": "off", 34 | "class-methods-use-this": "error", 35 | "comma-dangle": "error", 36 | "comma-spacing": [ 37 | "error", 38 | { 39 | "after": true, 40 | "before": false 41 | } 42 | ], 43 | "comma-style": "error", 44 | "complexity": "error", 45 | "computed-property-spacing": [ 46 | "error", 47 | "never" 48 | ], 49 | "consistent-return": "off", 50 | "consistent-this": "error", 51 | "curly": "off", 52 | "default-case": "error", 53 | "dot-location": "error", 54 | "dot-notation": "off", 55 | "eol-last": "error", 56 | "eqeqeq": "off", 57 | "func-call-spacing": "error", 58 | "func-name-matching": "error", 59 | "func-names": "off", 60 | "func-style": [ 61 | "error", 62 | "declaration" 63 | ], 64 | "function-paren-newline": "error", 65 | "generator-star-spacing": "error", 66 | "global-require": "error", 67 | "guard-for-in": "error", 68 | "handle-callback-err": "error", 69 | "id-blacklist": "error", 70 | "id-length": "off", 71 | "id-match": "error", 72 | "implicit-arrow-linebreak": "error", 73 | "indent": "off", 74 | "indent-legacy": "off", 75 | "init-declarations": "off", 76 | "jsx-quotes": "error", 77 | "key-spacing": "off", 78 | "keyword-spacing": [ 79 | "error", 80 | { 81 | "after": true, 82 | "before": true 83 | } 84 | ], 85 | "line-comment-position": "off", 86 | "linebreak-style": [ 87 | "error", 88 | "unix" 89 | ], 90 | "lines-around-comment": "error", 91 | "lines-around-directive": "error", 92 | "lines-between-class-members": "error", 93 | "max-classes-per-file": "error", 94 | "max-depth": "error", 95 | "max-len": "off", 96 | "max-lines": "off", 97 | "max-lines-per-function": "off", 98 | "max-nested-callbacks": "error", 99 | "max-params": "error", 100 | "max-statements": "off", 101 | "max-statements-per-line": "error", 102 | "multiline-comment-style": [ 103 | "error", 104 | "separate-lines" 105 | ], 106 | "multiline-ternary": "error", 107 | "new-cap": "error", 108 | "new-parens": "error", 109 | "newline-after-var": "off", 110 | "newline-before-return": "off", 111 | "newline-per-chained-call": "error", 112 | "no-alert": "error", 113 | "no-array-constructor": "error", 114 | "no-async-promise-executor": "error", 115 | "no-await-in-loop": "error", 116 | "no-bitwise": "off", 117 | "no-buffer-constructor": "error", 118 | "no-caller": "error", 119 | "no-catch-shadow": "error", 120 | "no-confusing-arrow": "error", 121 | "no-continue": "error", 122 | "no-div-regex": "error", 123 | "no-duplicate-imports": "error", 124 | "no-else-return": "error", 125 | "no-empty-function": "error", 126 | "no-eq-null": "error", 127 | "no-eval": "error", 128 | "no-extend-native": "error", 129 | "no-extra-bind": "error", 130 | "no-extra-label": "error", 131 | "no-extra-parens": "off", 132 | "no-floating-decimal": "error", 133 | "no-implicit-coercion": "error", 134 | "no-implicit-globals": "error", 135 | "no-implied-eval": "error", 136 | "no-inline-comments": "off", 137 | "no-invalid-this": "error", 138 | "no-iterator": "error", 139 | "no-label-var": "error", 140 | "no-labels": "error", 141 | "no-lone-blocks": "error", 142 | "no-lonely-if": "error", 143 | "no-loop-func": "error", 144 | "no-magic-numbers": "off", 145 | "no-misleading-character-class": "error", 146 | "no-mixed-operators": "off", 147 | "no-mixed-requires": "error", 148 | "no-multi-assign": "off", 149 | "no-multi-spaces": [ 150 | "error", 151 | { 152 | "ignoreEOLComments": true 153 | } 154 | ], 155 | "no-multi-str": "error", 156 | "no-multiple-empty-lines": "error", 157 | "no-native-reassign": "error", 158 | "no-negated-condition": "error", 159 | "no-negated-in-lhs": "error", 160 | "no-nested-ternary": "error", 161 | "no-new": "error", 162 | "no-new-func": "error", 163 | "no-new-object": "error", 164 | "no-new-require": "error", 165 | "no-new-wrappers": "error", 166 | "no-octal-escape": "error", 167 | "no-param-reassign": "off", 168 | "no-path-concat": "error", 169 | "no-plusplus": [ 170 | "error", 171 | { 172 | "allowForLoopAfterthoughts": true 173 | } 174 | ], 175 | "no-process-env": "error", 176 | "no-process-exit": "error", 177 | "no-proto": "error", 178 | "no-prototype-builtins": "error", 179 | "no-restricted-globals": "error", 180 | "no-restricted-imports": "error", 181 | "no-restricted-modules": "error", 182 | "no-restricted-properties": "error", 183 | "no-restricted-syntax": "error", 184 | "no-return-assign": "error", 185 | "no-return-await": "error", 186 | "no-script-url": "error", 187 | "no-self-compare": "error", 188 | "no-sequences": "error", 189 | "no-shadow": "off", 190 | "no-shadow-restricted-names": "error", 191 | "no-spaced-func": "error", 192 | // "no-sync": "error", 193 | "no-tabs": "error", 194 | "no-template-curly-in-string": "error", 195 | "no-ternary": "error", 196 | "no-throw-literal": "error", 197 | "no-trailing-spaces": "error", 198 | "no-undef-init": "error", 199 | "no-undefined": "error", 200 | "no-underscore-dangle": "error", 201 | "no-unmodified-loop-condition": "error", 202 | "no-unneeded-ternary": "error", 203 | "no-unused-expressions": "error", 204 | "no-use-before-define": "off", 205 | "no-useless-call": "error", 206 | "no-useless-catch": "error", 207 | "no-useless-computed-key": "error", 208 | "no-useless-concat": "error", 209 | "no-useless-constructor": "error", 210 | "no-useless-rename": "error", 211 | "no-useless-return": "error", 212 | "no-var": "error", 213 | "no-void": "error", 214 | "no-warning-comments": "error", 215 | "no-whitespace-before-property": "error", 216 | "no-with": "error", 217 | "nonblock-statement-body-position": "error", 218 | "object-curly-newline": "error", 219 | "object-curly-spacing": [ 220 | "error", 221 | "never" 222 | ], 223 | "object-property-newline": "error", 224 | "object-shorthand": "error", 225 | "one-var": "off", 226 | "one-var-declaration-per-line": "error", 227 | "operator-assignment": [ 228 | "error", 229 | "always" 230 | ], 231 | "operator-linebreak": "error", 232 | "padded-blocks": "off", 233 | "padding-line-between-statements": "error", 234 | "prefer-arrow-callback": "off", 235 | "prefer-const": "error", 236 | "prefer-destructuring": "error", 237 | "prefer-named-capture-group": "error", 238 | "prefer-numeric-literals": "error", 239 | "prefer-object-spread": "error", 240 | "prefer-promise-reject-errors": "error", 241 | "prefer-reflect": "error", 242 | "prefer-rest-params": "error", 243 | "prefer-spread": "error", 244 | "prefer-template": "off", 245 | "quote-props": "off", 246 | "quotes": "off", 247 | "radix": "error", 248 | "require-atomic-updates": "error", 249 | "require-await": "error", 250 | "require-jsdoc": "off", 251 | "require-unicode-regexp": "error", 252 | "rest-spread-spacing": "error", 253 | "semi": "error", 254 | "semi-spacing": [ 255 | "error", 256 | { 257 | "after": true, 258 | "before": false 259 | } 260 | ], 261 | "semi-style": [ 262 | "error", 263 | "last" 264 | ], 265 | "sort-imports": "error", 266 | "sort-keys": "error", 267 | "sort-vars": "error", 268 | "space-before-blocks": "error", 269 | "space-before-function-paren": "off", 270 | "space-in-parens": [ 271 | "error", 272 | "never" 273 | ], 274 | "space-infix-ops": "off", 275 | "space-unary-ops": "error", 276 | "spaced-comment": "off", 277 | "strict": "error", 278 | "switch-colon-spacing": "error", 279 | "symbol-description": "error", 280 | "template-curly-spacing": "error", 281 | "template-tag-spacing": "error", 282 | "unicode-bom": [ 283 | "error", 284 | "never" 285 | ], 286 | "valid-jsdoc": "error", 287 | "vars-on-top": "error", 288 | "wrap-iife": "error", 289 | "wrap-regex": "error", 290 | "yield-star-spacing": "error", 291 | "yoda": [ 292 | "error", 293 | "never" 294 | ] 295 | } 296 | }; 297 | -------------------------------------------------------------------------------- /bplistParser.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | 'use strict'; 4 | 5 | // adapted from https://github.com/3breadt/dd-plist 6 | 7 | import fs from 'fs'; 8 | const debug = false; 9 | 10 | export var maxObjectSize = 100 * 1000 * 1000; // 100Meg 11 | export var maxObjectCount = 32768; 12 | 13 | // EPOCH = new SimpleDateFormat("yyyy MM dd zzz").parse("2001 01 01 GMT").getTime(); 14 | // ...but that's annoying in a static initializer because it can throw exceptions, ick. 15 | // So we just hardcode the correct value. 16 | const EPOCH = 978307200000; 17 | 18 | // UID object definition 19 | export const UID = function(id) { 20 | this.UID = id; 21 | }; 22 | 23 | export const parseFile = function (fileNameOrBuffer, callback) { 24 | return new Promise(function (resolve, reject) { 25 | function tryParseBuffer(buffer) { 26 | let err = null; 27 | let result; 28 | try { 29 | result = parseBuffer(buffer); 30 | resolve(result); 31 | } catch (ex) { 32 | err = ex; 33 | reject(err); 34 | } finally { 35 | if (callback) callback(err, result); 36 | } 37 | } 38 | 39 | if (Buffer.isBuffer(fileNameOrBuffer)) { 40 | return tryParseBuffer(fileNameOrBuffer); 41 | } 42 | fs.readFile(fileNameOrBuffer, function (err, data) { 43 | if (err) { 44 | reject(err); 45 | return callback(err); 46 | } 47 | tryParseBuffer(data); 48 | }); 49 | }); 50 | }; 51 | 52 | export const parseFileSync = function (fileNameOrBuffer) { 53 | if (!Buffer.isBuffer(fileNameOrBuffer)) { 54 | fileNameOrBuffer = fs.readFileSync(fileNameOrBuffer); 55 | } 56 | return parseBuffer(fileNameOrBuffer); 57 | }; 58 | 59 | export const parseBuffer = function (buffer) { 60 | // check header 61 | const header = buffer.slice(0, 'bplist'.length).toString('utf8'); 62 | if (header !== 'bplist') { 63 | throw new Error("Invalid binary plist. Expected 'bplist' at offset 0."); 64 | } 65 | 66 | // Handle trailer, last 32 bytes of the file 67 | const trailer = buffer.slice(buffer.length - 32, buffer.length); 68 | // 6 null bytes (index 0 to 5) 69 | const offsetSize = trailer.readUInt8(6); 70 | if (debug) { 71 | console.log("offsetSize: " + offsetSize); 72 | } 73 | const objectRefSize = trailer.readUInt8(7); 74 | if (debug) { 75 | console.log("objectRefSize: " + objectRefSize); 76 | } 77 | const numObjects = readUInt64BE(trailer, 8); 78 | if (debug) { 79 | console.log("numObjects: " + numObjects); 80 | } 81 | const topObject = readUInt64BE(trailer, 16); 82 | if (debug) { 83 | console.log("topObject: " + topObject); 84 | } 85 | const offsetTableOffset = readUInt64BE(trailer, 24); 86 | if (debug) { 87 | console.log("offsetTableOffset: " + offsetTableOffset); 88 | } 89 | 90 | if (numObjects > maxObjectCount) { 91 | throw new Error("maxObjectCount exceeded"); 92 | } 93 | 94 | // Handle offset table 95 | const offsetTable = []; 96 | 97 | for (let i = 0; i < numObjects; i++) { 98 | const offsetBytes = buffer.slice(offsetTableOffset + i * offsetSize, offsetTableOffset + (i + 1) * offsetSize); 99 | offsetTable[i] = readUInt(offsetBytes, 0); 100 | if (debug) { 101 | console.log("Offset for Object #" + i + " is " + offsetTable[i] + " [" + offsetTable[i].toString(16) + "]"); 102 | } 103 | } 104 | 105 | // Parses an object inside the currently parsed binary property list. 106 | // For the format specification check 107 | // 108 | // Apple's binary property list parser implementation. 109 | function parseObject(tableOffset) { 110 | const offset = offsetTable[tableOffset]; 111 | const type = buffer[offset]; 112 | const objType = (type & 0xF0) >> 4; //First 4 bits 113 | const objInfo = (type & 0x0F); //Second 4 bits 114 | switch (objType) { 115 | case 0x0: 116 | return parseSimple(); 117 | case 0x1: 118 | return parseInteger(); 119 | case 0x8: 120 | return parseUID(); 121 | case 0x2: 122 | return parseReal(); 123 | case 0x3: 124 | return parseDate(); 125 | case 0x4: 126 | return parseData(); 127 | case 0x5: // ASCII 128 | return parsePlistString(); 129 | case 0x6: // UTF-16 130 | return parsePlistString(true); 131 | case 0xA: 132 | return parseArray(); 133 | case 0xD: 134 | return parseDictionary(); 135 | default: 136 | throw new Error("Unhandled type 0x" + objType.toString(16)); 137 | } 138 | 139 | function parseSimple() { 140 | //Simple 141 | switch (objInfo) { 142 | case 0x0: // null 143 | return null; 144 | case 0x8: // false 145 | return false; 146 | case 0x9: // true 147 | return true; 148 | case 0xF: // filler byte 149 | return null; 150 | default: 151 | throw new Error("Unhandled simple type 0x" + objType.toString(16)); 152 | } 153 | } 154 | 155 | function bufferToHexString(buffer) { 156 | let str = ''; 157 | let i; 158 | for (i = 0; i < buffer.length; i++) { 159 | if (buffer[i] != 0x00) { 160 | break; 161 | } 162 | } 163 | for (; i < buffer.length; i++) { 164 | const part = '00' + buffer[i].toString(16); 165 | str += part.substr(part.length - 2); 166 | } 167 | return str; 168 | } 169 | 170 | function parseInteger() { 171 | const length = Math.pow(2, objInfo); 172 | if (length < maxObjectSize) { 173 | const data = buffer.slice(offset + 1, offset + 1 + length); 174 | if (length === 16) { 175 | const str = bufferToHexString(data); 176 | return BigInt(`0x${str}`); 177 | } 178 | return data.reduce((acc, curr) => { 179 | acc <<= 8; 180 | acc |= curr & 255; 181 | return acc; 182 | }); 183 | } 184 | throw new Error("Too little heap space available! Wanted to read " + length + " bytes, but only " + maxObjectSize + " are available."); 185 | 186 | } 187 | 188 | function parseUID() { 189 | const length = objInfo + 1; 190 | if (length < maxObjectSize) { 191 | return new UID(readUInt(buffer.slice(offset + 1, offset + 1 + length))); 192 | } 193 | throw new Error("Too little heap space available! Wanted to read " + length + " bytes, but only " + maxObjectSize + " are available."); 194 | } 195 | 196 | function parseReal() { 197 | const length = Math.pow(2, objInfo); 198 | if (length < maxObjectSize) { 199 | const realBuffer = buffer.slice(offset + 1, offset + 1 + length); 200 | if (length === 4) { 201 | return realBuffer.readFloatBE(0); 202 | } 203 | if (length === 8) { 204 | return realBuffer.readDoubleBE(0); 205 | } 206 | } else { 207 | throw new Error("Too little heap space available! Wanted to read " + length + " bytes, but only " + maxObjectSize + " are available."); 208 | } 209 | } 210 | 211 | function parseDate() { 212 | if (objInfo != 0x3) { 213 | console.error("Unknown date type :" + objInfo + ". Parsing anyway..."); 214 | } 215 | const dateBuffer = buffer.slice(offset + 1, offset + 9); 216 | return new Date(EPOCH + (1000 * dateBuffer.readDoubleBE(0))); 217 | } 218 | 219 | function parseData() { 220 | let dataoffset = 1; 221 | let length = objInfo; 222 | if (objInfo == 0xF) { 223 | const int_type = buffer[offset + 1]; 224 | const intType = (int_type & 0xF0) / 0x10; 225 | if (intType != 0x1) { 226 | console.error("0x4: UNEXPECTED LENGTH-INT TYPE! " + intType); 227 | } 228 | const intInfo = int_type & 0x0F; 229 | const intLength = Math.pow(2, intInfo); 230 | dataoffset = 2 + intLength; 231 | if (intLength < 3) { 232 | length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength)); 233 | } else { 234 | length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength)); 235 | } 236 | } 237 | if (length < maxObjectSize) { 238 | return buffer.slice(offset + dataoffset, offset + dataoffset + length); 239 | } 240 | throw new Error("Too little heap space available! Wanted to read " + length + " bytes, but only " + maxObjectSize + " are available."); 241 | } 242 | 243 | function parsePlistString (isUtf16) { 244 | isUtf16 = isUtf16 || 0; 245 | let enc = "utf8"; 246 | let length = objInfo; 247 | let stroffset = 1; 248 | if (objInfo == 0xF) { 249 | const int_type = buffer[offset + 1]; 250 | const intType = (int_type & 0xF0) / 0x10; 251 | if (intType != 0x1) { 252 | console.error("UNEXPECTED LENGTH-INT TYPE! " + intType); 253 | } 254 | const intInfo = int_type & 0x0F; 255 | const intLength = Math.pow(2, intInfo); 256 | stroffset = 2 + intLength; 257 | if (intLength < 3) { 258 | length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength)); 259 | } else { 260 | length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength)); 261 | } 262 | } 263 | // length is String length -> to get byte length multiply by 2, as 1 character takes 2 bytes in UTF-16 264 | length *= (isUtf16 + 1); 265 | if (length < maxObjectSize) { 266 | let plistString = Buffer.from(buffer.slice(offset + stroffset, offset + stroffset + length)); 267 | if (isUtf16) { 268 | plistString = swapBytes(plistString); 269 | enc = "ucs2"; 270 | } 271 | return plistString.toString(enc); 272 | } 273 | throw new Error("Too little heap space available! Wanted to read " + length + " bytes, but only " + maxObjectSize + " are available."); 274 | } 275 | 276 | function parseArray() { 277 | let length = objInfo; 278 | let arrayoffset = 1; 279 | if (objInfo == 0xF) { 280 | const int_type = buffer[offset + 1]; 281 | const intType = (int_type & 0xF0) / 0x10; 282 | if (intType != 0x1) { 283 | console.error("0xa: UNEXPECTED LENGTH-INT TYPE! " + intType); 284 | } 285 | const intInfo = int_type & 0x0F; 286 | const intLength = Math.pow(2, intInfo); 287 | arrayoffset = 2 + intLength; 288 | if (intLength < 3) { 289 | length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength)); 290 | } else { 291 | length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength)); 292 | } 293 | } 294 | if (length * objectRefSize > maxObjectSize) { 295 | throw new Error("Too little heap space available!"); 296 | } 297 | const array = []; 298 | for (let i = 0; i < length; i++) { 299 | const objRef = readUInt(buffer.slice(offset + arrayoffset + i * objectRefSize, offset + arrayoffset + (i + 1) * objectRefSize)); 300 | array[i] = parseObject(objRef); 301 | } 302 | return array; 303 | } 304 | 305 | function parseDictionary() { 306 | let length = objInfo; 307 | let dictoffset = 1; 308 | if (objInfo == 0xF) { 309 | const int_type = buffer[offset + 1]; 310 | const intType = (int_type & 0xF0) / 0x10; 311 | if (intType != 0x1) { 312 | console.error("0xD: UNEXPECTED LENGTH-INT TYPE! " + intType); 313 | } 314 | const intInfo = int_type & 0x0F; 315 | const intLength = Math.pow(2, intInfo); 316 | dictoffset = 2 + intLength; 317 | if (intLength < 3) { 318 | length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength)); 319 | } else { 320 | length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength)); 321 | } 322 | } 323 | if (length * 2 * objectRefSize > maxObjectSize) { 324 | throw new Error("Too little heap space available!"); 325 | } 326 | if (debug) { 327 | console.log("Parsing dictionary #" + tableOffset); 328 | } 329 | const dict = {}; 330 | for (let i = 0; i < length; i++) { 331 | const keyRef = readUInt(buffer.slice(offset + dictoffset + i * objectRefSize, offset + dictoffset + (i + 1) * objectRefSize)); 332 | const valRef = readUInt(buffer.slice(offset + dictoffset + (length * objectRefSize) + i * objectRefSize, offset + dictoffset + (length * objectRefSize) + (i + 1) * objectRefSize)); 333 | const key = parseObject(keyRef); 334 | const val = parseObject(valRef); 335 | if (debug) { 336 | console.log(" DICT #" + tableOffset + ": Mapped " + key + " to " + val); 337 | } 338 | dict[key] = val; 339 | } 340 | return dict; 341 | } 342 | } 343 | 344 | return [ parseObject(topObject) ]; 345 | }; 346 | 347 | function readUInt(buffer, start) { 348 | start = start || 0; 349 | 350 | let l = 0; 351 | for (let i = start; i < buffer.length; i++) { 352 | l <<= 8; 353 | l |= buffer[i] & 0xFF; 354 | } 355 | return l; 356 | } 357 | 358 | // we're just going to toss the high order bits because javascript doesn't have 64-bit ints 359 | function readUInt64BE(buffer, start) { 360 | const data = buffer.slice(start, start + 8); 361 | return data.readUInt32BE(4, 8); 362 | } 363 | 364 | function swapBytes(buffer) { 365 | const len = buffer.length; 366 | for (let i = 0; i < len; i += 2) { 367 | const a = buffer[i]; 368 | buffer[i] = buffer[i+1]; 369 | buffer[i+1] = a; 370 | } 371 | return buffer; 372 | } 373 | --------------------------------------------------------------------------------