├── .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 |
--------------------------------------------------------------------------------