feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013 Joe Wollard
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,130 @@
# `simple-plist`
[![npm](https://img.shields.io/npm/dw/simple-plist.svg?style=popout&logo=npm)](https://www.npmjs.org/package/simple-plist)
[![npm](https://img.shields.io/npm/v/simple-plist.svg?style=popout&logo=npm)](https://www.npmjs.com/package/simple-plist)
A simple API for interacting with binary and plain text
[plist](https://en.wikipedia.org/wiki/Property_list) data.
## Installation
```sh
# via npm
npm install simple-plist
# via yarn
yarn add simple-plist
```
## Synchronous API
```js
const plist = require("simple-plist");
let data;
// read
data = plist.readFileSync("/path/to/some.plist");
// write xml
plist.writeFileSync("/path/to/plaintext.plist", data);
// write binary
plist.writeBinaryFileSync("/path/to/binary.plist", data);
```
## Asynchronous API
> Note: all of the async examples can optionally be converted to promises using
> node's [`util.promisify`](https://nodejs.org/dist/latest-v8.x/docs/api/util.html#util_util_promisify_original).
```js
const plist = require("simple-plist");
let data;
function callback(err, contents) {
if (err) throw err;
data = contents;
}
// read
plist.readFile("/path/to/some.plist", callback);
// write xml
plist.writeFile("/path/to/plaintext.plist", data, callback);
// write binary
plist.writeBinaryFile("/path/to/binary.plist", data, callback);
```
## In Memory
### `plist.stringify()`
```js
const plist = require("simple-plist");
// Convert an object to a plist xml string
plist.stringify({ name: "Joe", answer: 42 });
/*
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>name</key>
<string>Joe</string>
<key>answer</key>
<integer>42</integer>
</dict>
</plist>
*/
```
### `plist.parse()`
```js
const plist = require("simple-plist");
const xml = `<plist>
<dict>
<key>name</key>
<string>Joe</string>
</dict>
</plist>`;
plist.parse(xml);
// { "name": "Joe" }
```
## TypeScript Support
All functions have typescript signatures, but there are a few handy generics
that are worth pointing out. Those generics belong to `parse`, `readFile`,
and `readFileSync`. Here's an example:
```tsx
import { parse, readFile, readFileSync } from "simple-plist";
type Profile = {
name: string;
answer: number;
};
const xml = `<plist>
<dict>
<key>name</key>
<string>Joe</string>
<key>answer</key>
<integer>42</integer>
</dict>
</plist>`;
// typed string parsing
const { answer } = parse<Profile>(xml);
// answer = 42;
// typed file loading
const { name } = readFileSync<Profile>("/path/to/profile.plist");
```

View File

@@ -0,0 +1,24 @@
import bplistCreator from "bplist-creator";
import bplistParser from "bplist-parser";
import { parse } from "./parse";
import { readFile } from "./readFile";
import { readFileSync } from "./readFileSync";
import { stringify } from "./stringify";
import { writeBinaryFile } from "./writeBinaryFile";
import { writeBinaryFileSync } from "./writeBinaryFileSync";
import { writeFile } from "./writeFile";
import { writeFileSync } from "./writeFileSync";
declare const SimplePlist: {
bplistCreator: bplistCreator;
bplistParser: bplistParser;
parse: typeof parse;
readFile: typeof readFile;
readFileSync: typeof readFileSync;
stringify: typeof stringify;
writeBinaryFile: typeof writeBinaryFile;
writeBinaryFileSync: typeof writeBinaryFileSync;
writeFile: typeof writeFile;
writeFileSync: typeof writeFileSync;
};
export default SimplePlist;
export type { callbackFn, PlistJsObj, StringOrBuffer } from "./types";

View File

@@ -0,0 +1,41 @@
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports);
if (v !== undefined) module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports", "bplist-creator", "bplist-parser", "./parse", "./readFile", "./readFileSync", "./stringify", "./writeBinaryFile", "./writeBinaryFileSync", "./writeFile", "./writeFileSync"], factory);
}
})(function (require, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var bplist_creator_1 = __importDefault(require("bplist-creator"));
var bplist_parser_1 = __importDefault(require("bplist-parser"));
var parse_1 = require("./parse");
var readFile_1 = require("./readFile");
var readFileSync_1 = require("./readFileSync");
var stringify_1 = require("./stringify");
var writeBinaryFile_1 = require("./writeBinaryFile");
var writeBinaryFileSync_1 = require("./writeBinaryFileSync");
var writeFile_1 = require("./writeFile");
var writeFileSync_1 = require("./writeFileSync");
// "modern" named exports
var SimplePlist = {
bplistCreator: bplist_creator_1.default,
bplistParser: bplist_parser_1.default,
parse: parse_1.parse,
readFile: readFile_1.readFile,
readFileSync: readFileSync_1.readFileSync,
stringify: stringify_1.stringify,
writeBinaryFile: writeBinaryFile_1.writeBinaryFile,
writeBinaryFileSync: writeBinaryFileSync_1.writeBinaryFileSync,
writeFile: writeFile_1.writeFile,
writeFileSync: writeFileSync_1.writeFileSync,
};
exports.default = SimplePlist;
// preserve backwards compatibility
module.exports = SimplePlist;
});

View File

@@ -0,0 +1,8 @@
/// <reference types="node" />
import { PathOrFileDescriptor } from "fs";
import { PlistJsObj, StringOrBuffer } from "./types";
/**
* Detects the format of the given string or buffer, then attempts to parse the
* payload using the appropriate tooling.
*/
export declare function parse<T = PlistJsObj>(aStringOrBuffer: StringOrBuffer, aFile?: PathOrFileDescriptor): T;

View File

@@ -0,0 +1,45 @@
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports);
if (v !== undefined) module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports", "bplist-parser", "plist"], factory);
}
})(function (require, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parse = void 0;
var bplist_parser_1 = __importDefault(require("bplist-parser"));
var plist_1 = __importDefault(require("plist"));
/**
* Detects the format of the given string or buffer, then attempts to parse the
* payload using the appropriate tooling.
*/
function parse(aStringOrBuffer, aFile) {
var firstByte = aStringOrBuffer[0];
var results;
try {
if (firstByte === 60 || firstByte === "<") {
results = plist_1.default.parse(aStringOrBuffer.toString());
}
else if (firstByte === 98) {
results = bplist_parser_1.default.parseBuffer(aStringOrBuffer)[0];
}
else if (aFile) {
throw new Error("Unable to determine format for '" + aFile + "'");
}
else {
throw new Error("Unable to determine format for plist aStringOrBuffer");
}
}
catch (error) {
throw error instanceof Error ? error : new Error("error parsing " + aFile);
}
return results;
}
exports.parse = parse;
});

View File

@@ -0,0 +1,4 @@
/// <reference types="node" />
import { PathOrFileDescriptor } from "fs";
import { callbackFn } from "./types";
export declare function readFile<T = unknown>(aFile: PathOrFileDescriptor, callback: callbackFn<T>): void;

View File

@@ -0,0 +1,36 @@
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports);
if (v !== undefined) module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports", "fs", "./parse"], factory);
}
})(function (require, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.readFile = void 0;
var fs_1 = __importDefault(require("fs"));
var parse_1 = require("./parse");
function readFile(aFile, callback) {
fs_1.default.readFile(aFile, function (err, contents) {
if (err) {
return callback(err);
}
var results;
try {
results = (0, parse_1.parse)(contents, aFile);
}
catch (error) {
return callback(error instanceof Error
? error
: new Error("failed to read file " + aFile));
}
callback(null, results);
});
}
exports.readFile = readFile;
});

View File

@@ -0,0 +1,3 @@
/// <reference types="node" />
import { PathOrFileDescriptor } from "fs";
export declare function readFileSync<T = unknown>(aFile: PathOrFileDescriptor): T;

View File

@@ -0,0 +1,26 @@
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports);
if (v !== undefined) module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports", "fs", "./parse"], factory);
}
})(function (require, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.readFileSync = void 0;
var fs_1 = __importDefault(require("fs"));
var parse_1 = require("./parse");
function readFileSync(aFile) {
var contents = fs_1.default.readFileSync(aFile);
if (contents.length === 0) {
return {};
}
return (0, parse_1.parse)(contents, aFile);
}
exports.readFileSync = readFileSync;
});

View File

@@ -0,0 +1,2 @@
import { PlistJsObj } from "./types";
export declare function stringify(anObject: PlistJsObj): string;

View File

@@ -0,0 +1,21 @@
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports);
if (v !== undefined) module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports", "plist"], factory);
}
})(function (require, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.stringify = void 0;
var plist_1 = __importDefault(require("plist"));
function stringify(anObject) {
return plist_1.default.build(anObject);
}
exports.stringify = stringify;
});

View File

@@ -0,0 +1,4 @@
/// <reference types="node" />
export declare type callbackFn<T> = (error: Error | null, result?: T) => void;
export declare type StringOrBuffer = string | Buffer;
export declare type PlistJsObj = Record<any, any> | any[];

View File

@@ -0,0 +1,12 @@
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports);
if (v !== undefined) module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports"], factory);
}
})(function (require, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
});

View File

@@ -0,0 +1,5 @@
/// <reference types="node" />
import { PathOrFileDescriptor, WriteFileOptions } from "fs";
import { callbackFn, PlistJsObj } from "./types";
export declare function writeBinaryFile(aFile: PathOrFileDescriptor, anObject: PlistJsObj, callback: callbackFn<void>): void;
export declare function writeBinaryFile(aFile: PathOrFileDescriptor, anObject: PlistJsObj, options: WriteFileOptions, callback: callbackFn<void>): void;

View File

@@ -0,0 +1,30 @@
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports);
if (v !== undefined) module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports", "bplist-creator", "fs"], factory);
}
})(function (require, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.writeBinaryFile = void 0;
var bplist_creator_1 = __importDefault(require("bplist-creator"));
var fs_1 = __importDefault(require("fs"));
function writeBinaryFile(aFile, anObject, options, callback) {
if (typeof options === "function" && callback === undefined) {
fs_1.default.writeFile(aFile, (0, bplist_creator_1.default)(anObject), options);
}
else if (typeof options === "object" && typeof callback === "function") {
fs_1.default.writeFile(aFile, (0, bplist_creator_1.default)(anObject), options, callback);
}
else {
throw new Error("Invalid parameters passed to writeBinaryFile");
}
}
exports.writeBinaryFile = writeBinaryFile;
});

View File

@@ -0,0 +1,4 @@
/// <reference types="node" />
import { PathOrFileDescriptor, WriteFileOptions } from "fs";
import { PlistJsObj } from "./types";
export declare function writeBinaryFileSync(aFile: PathOrFileDescriptor, anObject: PlistJsObj, options?: WriteFileOptions): void;

View File

@@ -0,0 +1,22 @@
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports);
if (v !== undefined) module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports", "bplist-creator", "fs"], factory);
}
})(function (require, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.writeBinaryFileSync = void 0;
var bplist_creator_1 = __importDefault(require("bplist-creator"));
var fs_1 = __importDefault(require("fs"));
function writeBinaryFileSync(aFile, anObject, options) {
return fs_1.default.writeFileSync(aFile, (0, bplist_creator_1.default)(anObject), options);
}
exports.writeBinaryFileSync = writeBinaryFileSync;
});

View File

@@ -0,0 +1,5 @@
/// <reference types="node" />
import { PathOrFileDescriptor, WriteFileOptions } from "fs";
import { callbackFn, PlistJsObj } from "./types";
export declare function writeFile(aFile: PathOrFileDescriptor, anObject: PlistJsObj, options: callbackFn<void>): void;
export declare function writeFile(aFile: PathOrFileDescriptor, anObject: PlistJsObj, options: WriteFileOptions, callback: callbackFn<void>): void;

View File

@@ -0,0 +1,30 @@
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports);
if (v !== undefined) module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports", "fs", "plist"], factory);
}
})(function (require, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.writeFile = void 0;
var fs_1 = __importDefault(require("fs"));
var plist_1 = __importDefault(require("plist"));
function writeFile(aFile, anObject, options, callback) {
if (typeof options === "function" && callback === undefined) {
fs_1.default.writeFile(aFile, plist_1.default.build(anObject), options);
}
else if (typeof options === "object" && typeof callback === "function") {
fs_1.default.writeFile(aFile, plist_1.default.build(anObject), options, callback);
}
else {
throw new Error("Invalid parameters passed to writeFile");
}
}
exports.writeFile = writeFile;
});

View File

@@ -0,0 +1,4 @@
/// <reference types="node" />
import { PathOrFileDescriptor, WriteFileOptions } from "fs";
import { PlistJsObj } from "./types";
export declare function writeFileSync(aFile: PathOrFileDescriptor, anObject: PlistJsObj, options?: WriteFileOptions): void;

View File

@@ -0,0 +1,23 @@
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports);
if (v !== undefined) module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports", "fs", "plist"], factory);
}
})(function (require, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.writeFileSync = void 0;
var fs_1 = __importDefault(require("fs"));
var plist_1 = __importDefault(require("plist"));
function writeFileSync(aFile, anObject, options) {
var data = plist_1.default.build(anObject);
return fs_1.default.writeFileSync(aFile, data, options);
}
exports.writeFileSync = writeFileSync;
});

View File

@@ -0,0 +1,18 @@
(The MIT License)
Copyright (c) 2012 Near Infinity Corporation
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,64 @@
bplist-creator
==============
Binary Mac OS X Plist (property list) creator.
## Installation
```bash
$ npm install bplist-creator
```
## Quick Examples
```javascript
var bplist = require('bplist-creator');
var buffer = bplist({
key1: [1, 2, 3]
});
```
## Real/Double/Float handling
Javascript don't have different types for `1` and `1.0`. This package
will automatically store numbers as the appropriate type, but can't
detect floats that is also integers.
If you need to force a value to be written with the `real` type pass
an instance of `Real`.
```javascript
var buffer = bplist({
backgroundRed: new bplist.Real(1),
backgroundGreen: new bplist.Real(0),
backgroundBlue: new bplist.Real(0)
});
```
In `xml` the corresponding tags is `<integer>` and `<real>`.
## License
(The MIT License)
Copyright (c) 2012 Near Infinity Corporation
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,460 @@
'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(Buffer.from("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(Buffer.from([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 (typeof entry.value === 'bigint') {
var width = 16;
var hex = entry.value.toString(width);
var buf = Buffer.from(hex.padStart(width * 2, '0').slice(0, width * 2), 'hex');
writeByte(0x14);
buffer.write(buf);
} else if (entry.type !== 'double' && parseFloat(entry.value).toFixed() == entry.value) {
if (entry.value < 0) {
writeByte(0x13);
writeBytes(entry.value, 8, true);
} 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 = Buffer.from(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 = Buffer.from(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(Buffer.from([b]));
}
function writeDouble(v) {
var buf = Buffer.alloc(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, is_signedint) {
// write low-order bytes big-endian style
var buf = Buffer.alloc(bytes);
var z = 0;
// javascript doesn't handle large numbers
while (bytes > 4) {
buf[z++] = is_signedint ? 0xff : 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 if (typeof(dicts) === 'bigint') {
return [
{
type: 'number',
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;

View File

@@ -0,0 +1,28 @@
{
"name": "bplist-creator",
"version": "0.1.0",
"description": "Binary Mac OS X Plist (property list) creator.",
"main": "bplistCreator.js",
"scripts": {
"test": "./node_modules/nodeunit/bin/nodeunit test"
},
"repository": {
"type": "git",
"url": "https://github.com/nearinfinity/node-bplist-creator.git"
},
"keywords": [
"bplist",
"plist",
"creator"
],
"author": "Joe Ferner",
"license": "MIT",
"devDependencies": {
"bplist-parser": "0.3.0",
"is-buffer": "1.1.x",
"nodeunit": "0.9.x"
},
"dependencies": {
"stream-buffers": "2.2.x"
}
}

View File

@@ -0,0 +1,211 @@
'use strict';
var fs = require('fs');
var path = require('path');
var nodeunit = require('nodeunit');
var bplistParser = require('bplist-parser');
var bplistCreator = require('../');
module.exports = {
// 'iTunes Small': function(test) {
// var file = path.join(__dirname, "iTunes-small.bplist");
// testFile(test, file);
// },
'sample1': function(test) {
var file = path.join(__dirname, "sample1.bplist");
testFile(test, file);
},
'sample2': function(test) {
var file = path.join(__dirname, "sample2.bplist");
testFile(test, file);
},
'binary data': function(test) {
var file = path.join(__dirname, "binaryData.bplist");
testFile(test, file);
},
'airplay': function(test) {
var file = path.join(__dirname, "airplay.bplist");
testFile(test, file);
},
'integers': function(test) {
var file = path.join(__dirname, "integers.bplist");
testFile(test, file);
},
// 'utf16': function(test) {
// var file = path.join(__dirname, "utf16.bplist");
// testFile(test, file);
// },
// 'uid': function(test) {
// var file = path.join(__dirname, "uid.bplist");
// testFile(test, file);
// }
};
function testFile(test, file) {
fs.readFile(file, function(err, fileData) {
if (err) {
return test.done(err);
}
bplistParser.parseFile(file, function(err, dicts) {
if (err) {
return test.done(err);
}
// airplay overrides
if (dicts && dicts[0] && dicts[0].loadedTimeRanges && dicts[0].loadedTimeRanges[0] && dicts[0].loadedTimeRanges[0].hasOwnProperty('start')) {
dicts[0].loadedTimeRanges[0].start = {
bplistOverride: true,
type: 'double',
value: dicts[0].loadedTimeRanges[0].start
};
}
if (dicts && dicts[0] && dicts[0].loadedTimeRanges && dicts[0].seekableTimeRanges[0] && dicts[0].seekableTimeRanges[0].hasOwnProperty('start')) {
dicts[0].seekableTimeRanges[0].start = {
bplistOverride: true,
type: 'double',
value: dicts[0].seekableTimeRanges[0].start
};
}
if (dicts && dicts[0] && dicts[0].hasOwnProperty('rate')) {
dicts[0].rate = {
bplistOverride: true,
type: 'double',
value: dicts[0].rate
};
}
// utf16
if (dicts && dicts[0] && dicts[0].hasOwnProperty('NSHumanReadableCopyright')) {
dicts[0].NSHumanReadableCopyright = {
bplistOverride: true,
type: 'string-utf16',
value: dicts[0].NSHumanReadableCopyright
};
}
if (dicts && dicts[0] && dicts[0].hasOwnProperty('CFBundleExecutable')) {
dicts[0].CFBundleExecutable = {
bplistOverride: true,
type: 'string',
value: dicts[0].CFBundleExecutable
};
}
if (dicts && dicts[0] && dicts[0].CFBundleURLTypes && dicts[0].CFBundleURLTypes[0] && dicts[0].CFBundleURLTypes[0].hasOwnProperty('CFBundleURLSchemes')) {
dicts[0].CFBundleURLTypes[0].CFBundleURLSchemes[0] = {
bplistOverride: true,
type: 'string',
value: dicts[0].CFBundleURLTypes[0].CFBundleURLSchemes[0]
};
}
if (dicts && dicts[0] && dicts[0].hasOwnProperty('CFBundleDisplayName')) {
dicts[0].CFBundleDisplayName = {
bplistOverride: true,
type: 'string',
value: dicts[0].CFBundleDisplayName
};
}
if (dicts && dicts[0] && dicts[0].hasOwnProperty('DTPlatformBuild')) {
dicts[0].DTPlatformBuild = {
bplistOverride: true,
type: 'string',
value: dicts[0].DTPlatformBuild
};
}
// integer
if (dicts && dicts[0] && dicts[0].hasOwnProperty('int64item')) {
dicts[0].int64item = {
bplistOverride: true,
type: 'number',
value: dicts[0].int64item.value
};
}
var buf = bplistCreator(dicts);
compareBuffers(test, buf, fileData);
return test.done();
});
});
}
function compareBuffers(test, buf1, buf2) {
if (buf1.length !== buf2.length) {
printBuffers(buf1, buf2);
return test.fail("buffer size mismatch. found: " + buf1.length + ", expected: " + buf2.length + ".");
}
for (var i = 0; i < buf1.length; i++) {
if (buf1[i] !== buf2[i]) {
printBuffers(buf1, buf2);
return test.fail("buffer mismatch at offset 0x" + i.toString(16) + ". found: 0x" + buf1[i].toString(16) + ", expected: 0x" + buf2[i].toString(16) + ".");
}
}
}
function printBuffers(buf1, buf2) {
var i, t;
for (var lineOffset = 0; lineOffset < buf1.length || lineOffset < buf2.length; lineOffset += 16) {
var line = '';
t = ('000000000' + lineOffset.toString(16));
line += t.substr(t.length - 8) + ': ';
for (i = 0; i < 16; i++) {
if (i == 8) {
line += ' ';
}
if (lineOffset + i < buf1.length) {
t = ('00' + buf1[lineOffset + i].toString(16));
line += t.substr(t.length - 2) + ' ';
} else {
line += ' ';
}
}
line += ' ';
for (i = 0; i < 16; i++) {
if (lineOffset + i < buf1.length) {
t = String.fromCharCode(buf1[lineOffset + i]);
if (t < ' ' || t > '~') {
t = '.';
}
line += t;
} else {
line += ' ';
}
}
line += ' - ';
for (i = 0; i < 16; i++) {
if (i == 8) {
line += ' ';
}
if (lineOffset + i < buf2.length) {
t = ('00' + buf2[lineOffset + i].toString(16));
line += t.substr(t.length - 2) + ' ';
} else {
line += ' ';
}
}
line += ' ';
for (i = 0; i < 16; i++) {
if (lineOffset + i < buf2.length) {
t = String.fromCharCode(buf2[lineOffset + i]);
if (t < ' ' || t > '~') {
t = '.';
}
line += t;
} else {
line += ' ';
}
}
console.log(line);
}
}

View File

@@ -0,0 +1,12 @@
; EditorConfig file: https://EditorConfig.org
; Install the "EditorConfig" plugin into your editor to use
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true

View File

@@ -0,0 +1 @@
node_modules

View File

@@ -0,0 +1,295 @@
module.exports = {
"env": {
"node": true,
"commonjs": true,
"es6": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
"accessor-pairs": "error",
"array-bracket-newline": "error",
"array-bracket-spacing": "off",
"array-callback-return": "error",
"array-element-newline": "off",
"arrow-body-style": "error",
"arrow-parens": "error",
"arrow-spacing": "error",
"block-scoped-var": "error",
"block-spacing": "error",
"brace-style": [
"error",
"1tbs"
],
"callback-return": "off",
"camelcase": "off",
"capitalized-comments": "off",
"class-methods-use-this": "error",
"comma-dangle": "error",
"comma-spacing": [
"error",
{
"after": true,
"before": false
}
],
"comma-style": "error",
"complexity": "error",
"computed-property-spacing": [
"error",
"never"
],
"consistent-return": "off",
"consistent-this": "error",
"curly": "off",
"default-case": "error",
"dot-location": "error",
"dot-notation": "off",
"eol-last": "error",
"eqeqeq": "off",
"func-call-spacing": "error",
"func-name-matching": "error",
"func-names": "off",
"func-style": [
"error",
"declaration"
],
"function-paren-newline": "error",
"generator-star-spacing": "error",
"global-require": "error",
"guard-for-in": "error",
"handle-callback-err": "error",
"id-blacklist": "error",
"id-length": "off",
"id-match": "error",
"implicit-arrow-linebreak": "error",
"indent": "off",
"indent-legacy": "off",
"init-declarations": "off",
"jsx-quotes": "error",
"key-spacing": "off",
"keyword-spacing": [
"error",
{
"after": true,
"before": true
}
],
"line-comment-position": "off",
"linebreak-style": [
"error",
"unix"
],
"lines-around-comment": "error",
"lines-around-directive": "error",
"lines-between-class-members": "error",
"max-classes-per-file": "error",
"max-depth": "error",
"max-len": "off",
"max-lines": "off",
"max-lines-per-function": "off",
"max-nested-callbacks": "error",
"max-params": "error",
"max-statements": "off",
"max-statements-per-line": "error",
"multiline-comment-style": [
"error",
"separate-lines"
],
"multiline-ternary": "error",
"new-cap": "error",
"new-parens": "error",
"newline-after-var": "off",
"newline-before-return": "off",
"newline-per-chained-call": "error",
"no-alert": "error",
"no-array-constructor": "error",
"no-async-promise-executor": "error",
"no-await-in-loop": "error",
"no-bitwise": "off",
"no-buffer-constructor": "error",
"no-caller": "error",
"no-catch-shadow": "error",
"no-confusing-arrow": "error",
"no-continue": "error",
"no-div-regex": "error",
"no-duplicate-imports": "error",
"no-else-return": "error",
"no-empty-function": "error",
"no-eq-null": "error",
"no-eval": "error",
"no-extend-native": "error",
"no-extra-bind": "error",
"no-extra-label": "error",
"no-extra-parens": "off",
"no-floating-decimal": "error",
"no-implicit-coercion": "error",
"no-implicit-globals": "error",
"no-implied-eval": "error",
"no-inline-comments": "off",
"no-invalid-this": "error",
"no-iterator": "error",
"no-label-var": "error",
"no-labels": "error",
"no-lone-blocks": "error",
"no-lonely-if": "error",
"no-loop-func": "error",
"no-magic-numbers": "off",
"no-misleading-character-class": "error",
"no-mixed-operators": "off",
"no-mixed-requires": "error",
"no-multi-assign": "off",
"no-multi-spaces": [
"error",
{
"ignoreEOLComments": true
}
],
"no-multi-str": "error",
"no-multiple-empty-lines": "error",
"no-native-reassign": "error",
"no-negated-condition": "error",
"no-negated-in-lhs": "error",
"no-nested-ternary": "error",
"no-new": "error",
"no-new-func": "error",
"no-new-object": "error",
"no-new-require": "error",
"no-new-wrappers": "error",
"no-octal-escape": "error",
"no-param-reassign": "off",
"no-path-concat": "error",
"no-plusplus": [
"error",
{
"allowForLoopAfterthoughts": true
}
],
"no-process-env": "error",
"no-process-exit": "error",
"no-proto": "error",
"no-prototype-builtins": "error",
"no-restricted-globals": "error",
"no-restricted-imports": "error",
"no-restricted-modules": "error",
"no-restricted-properties": "error",
"no-restricted-syntax": "error",
"no-return-assign": "error",
"no-return-await": "error",
"no-script-url": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-shadow": "off",
"no-shadow-restricted-names": "error",
"no-spaced-func": "error",
"no-sync": "error",
"no-tabs": "error",
"no-template-curly-in-string": "error",
"no-ternary": "error",
"no-throw-literal": "error",
"no-trailing-spaces": "error",
"no-undef-init": "error",
"no-undefined": "error",
"no-underscore-dangle": "error",
"no-unmodified-loop-condition": "error",
"no-unneeded-ternary": "error",
"no-unused-expressions": "error",
"no-use-before-define": "off",
"no-useless-call": "error",
"no-useless-catch": "error",
"no-useless-computed-key": "error",
"no-useless-concat": "error",
"no-useless-constructor": "error",
"no-useless-rename": "error",
"no-useless-return": "error",
"no-var": "error",
"no-void": "error",
"no-warning-comments": "error",
"no-whitespace-before-property": "error",
"no-with": "error",
"nonblock-statement-body-position": "error",
"object-curly-newline": "error",
"object-curly-spacing": [
"error",
"never"
],
"object-property-newline": "error",
"object-shorthand": "error",
"one-var": "off",
"one-var-declaration-per-line": "error",
"operator-assignment": [
"error",
"always"
],
"operator-linebreak": "error",
"padded-blocks": "off",
"padding-line-between-statements": "error",
"prefer-arrow-callback": "off",
"prefer-const": "error",
"prefer-destructuring": "error",
"prefer-named-capture-group": "error",
"prefer-numeric-literals": "error",
"prefer-object-spread": "error",
"prefer-promise-reject-errors": "error",
"prefer-reflect": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"prefer-template": "off",
"quote-props": "off",
"quotes": "off",
"radix": "error",
"require-atomic-updates": "error",
"require-await": "error",
"require-jsdoc": "off",
"require-unicode-regexp": "error",
"rest-spread-spacing": "error",
"semi": "error",
"semi-spacing": [
"error",
{
"after": true,
"before": false
}
],
"semi-style": [
"error",
"last"
],
"sort-imports": "error",
"sort-keys": "error",
"sort-vars": "error",
"space-before-blocks": "error",
"space-before-function-paren": "off",
"space-in-parens": [
"error",
"never"
],
"space-infix-ops": "off",
"space-unary-ops": "error",
"spaced-comment": "off",
"strict": "error",
"switch-colon-spacing": "error",
"symbol-description": "error",
"template-curly-spacing": "error",
"template-tag-spacing": "error",
"unicode-bom": [
"error",
"never"
],
"valid-jsdoc": "error",
"vars-on-top": "error",
"wrap-iife": "error",
"wrap-regex": "error",
"yield-star-spacing": "error",
"yoda": [
"error",
"never"
]
}
};

View File

@@ -0,0 +1,48 @@
# bplist-parser
Binary Mac OS X Plist (property list) parser.
## Installation
```bash
$ npm install bplist-parser
```
## Quick Examples
```javascript
const bplist = require('bplist-parser');
(async () => {
const obj = await bplist.parseFile('myPlist.bplist');
console.log(JSON.stringify(obj));
})();
```
## License
(The MIT License)
Copyright (c) 2012 Near Infinity Corporation
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,6 @@
declare namespace bPlistParser {
type CallbackFunction<T = any> = (error: Error|null, result: [T]) => void
export function parseFile<T = any>(fileNameOrBuffer: string|Buffer, callback?: CallbackFunction<T>): Promise<[T]>
}
export = bPlistParser

View File

@@ -0,0 +1,366 @@
/* eslint-disable no-console */
'use strict';
// adapted from https://github.com/3breadt/dd-plist
const fs = require('fs');
const bigInt = require('big-integer');
const debug = false;
exports.maxObjectSize = 100 * 1000 * 1000; // 100Meg
exports.maxObjectCount = 32768;
// EPOCH = new SimpleDateFormat("yyyy MM dd zzz").parse("2001 01 01 GMT").getTime();
// ...but that's annoying in a static initializer because it can throw exceptions, ick.
// So we just hardcode the correct value.
const EPOCH = 978307200000;
// UID object definition
const UID = exports.UID = function(id) {
this.UID = id;
};
exports.parseFile = function (fileNameOrBuffer, callback) {
return new Promise(function (resolve, reject) {
function tryParseBuffer(buffer) {
let err = null;
let result;
try {
result = parseBuffer(buffer);
resolve(result);
} catch (ex) {
err = ex;
reject(err);
} finally {
if (callback) callback(err, result);
}
}
if (Buffer.isBuffer(fileNameOrBuffer)) {
return tryParseBuffer(fileNameOrBuffer);
}
fs.readFile(fileNameOrBuffer, function (err, data) {
if (err) {
reject(err);
return callback(err);
}
tryParseBuffer(data);
});
});
};
const parseBuffer = exports.parseBuffer = function (buffer) {
// check header
const header = buffer.slice(0, 'bplist'.length).toString('utf8');
if (header !== 'bplist') {
throw new Error("Invalid binary plist. Expected 'bplist' at offset 0.");
}
// Handle trailer, last 32 bytes of the file
const trailer = buffer.slice(buffer.length - 32, buffer.length);
// 6 null bytes (index 0 to 5)
const offsetSize = trailer.readUInt8(6);
if (debug) {
console.log("offsetSize: " + offsetSize);
}
const objectRefSize = trailer.readUInt8(7);
if (debug) {
console.log("objectRefSize: " + objectRefSize);
}
const numObjects = readUInt64BE(trailer, 8);
if (debug) {
console.log("numObjects: " + numObjects);
}
const topObject = readUInt64BE(trailer, 16);
if (debug) {
console.log("topObject: " + topObject);
}
const offsetTableOffset = readUInt64BE(trailer, 24);
if (debug) {
console.log("offsetTableOffset: " + offsetTableOffset);
}
if (numObjects > exports.maxObjectCount) {
throw new Error("maxObjectCount exceeded");
}
// Handle offset table
const offsetTable = [];
for (let i = 0; i < numObjects; i++) {
const offsetBytes = buffer.slice(offsetTableOffset + i * offsetSize, offsetTableOffset + (i + 1) * offsetSize);
offsetTable[i] = readUInt(offsetBytes, 0);
if (debug) {
console.log("Offset for Object #" + i + " is " + offsetTable[i] + " [" + offsetTable[i].toString(16) + "]");
}
}
// Parses an object inside the currently parsed binary property list.
// For the format specification check
// <a href="https://www.opensource.apple.com/source/CF/CF-635/CFBinaryPList.c">
// Apple's binary property list parser implementation</a>.
function parseObject(tableOffset) {
const offset = offsetTable[tableOffset];
const type = buffer[offset];
const objType = (type & 0xF0) >> 4; //First 4 bits
const objInfo = (type & 0x0F); //Second 4 bits
switch (objType) {
case 0x0:
return parseSimple();
case 0x1:
return parseInteger();
case 0x8:
return parseUID();
case 0x2:
return parseReal();
case 0x3:
return parseDate();
case 0x4:
return parseData();
case 0x5: // ASCII
return parsePlistString();
case 0x6: // UTF-16
return parsePlistString(true);
case 0xA:
return parseArray();
case 0xD:
return parseDictionary();
default:
throw new Error("Unhandled type 0x" + objType.toString(16));
}
function parseSimple() {
//Simple
switch (objInfo) {
case 0x0: // null
return null;
case 0x8: // false
return false;
case 0x9: // true
return true;
case 0xF: // filler byte
return null;
default:
throw new Error("Unhandled simple type 0x" + objType.toString(16));
}
}
function bufferToHexString(buffer) {
let str = '';
let i;
for (i = 0; i < buffer.length; i++) {
if (buffer[i] != 0x00) {
break;
}
}
for (; i < buffer.length; i++) {
const part = '00' + buffer[i].toString(16);
str += part.substr(part.length - 2);
}
return str;
}
function parseInteger() {
const length = Math.pow(2, objInfo);
if (length < exports.maxObjectSize) {
const data = buffer.slice(offset + 1, offset + 1 + length);
if (length === 16) {
const str = bufferToHexString(data);
return bigInt(str, 16);
}
return data.reduce((acc, curr) => {
acc <<= 8;
acc |= curr & 255;
return acc;
});
} else {
throw new Error("Too little heap space available! Wanted to read " + length + " bytes, but only " + exports.maxObjectSize + " are available.");
}
}
function parseUID() {
const length = objInfo + 1;
if (length < exports.maxObjectSize) {
return new UID(readUInt(buffer.slice(offset + 1, offset + 1 + length)));
}
throw new Error("Too little heap space available! Wanted to read " + length + " bytes, but only " + exports.maxObjectSize + " are available.");
}
function parseReal() {
const length = Math.pow(2, objInfo);
if (length < exports.maxObjectSize) {
const realBuffer = buffer.slice(offset + 1, offset + 1 + length);
if (length === 4) {
return realBuffer.readFloatBE(0);
}
if (length === 8) {
return realBuffer.readDoubleBE(0);
}
} else {
throw new Error("Too little heap space available! Wanted to read " + length + " bytes, but only " + exports.maxObjectSize + " are available.");
}
}
function parseDate() {
if (objInfo != 0x3) {
console.error("Unknown date type :" + objInfo + ". Parsing anyway...");
}
const dateBuffer = buffer.slice(offset + 1, offset + 9);
return new Date(EPOCH + (1000 * dateBuffer.readDoubleBE(0)));
}
function parseData() {
let dataoffset = 1;
let length = objInfo;
if (objInfo == 0xF) {
const int_type = buffer[offset + 1];
const intType = (int_type & 0xF0) / 0x10;
if (intType != 0x1) {
console.error("0x4: UNEXPECTED LENGTH-INT TYPE! " + intType);
}
const intInfo = int_type & 0x0F;
const intLength = Math.pow(2, intInfo);
dataoffset = 2 + intLength;
if (intLength < 3) {
length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength));
} else {
length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength));
}
}
if (length < exports.maxObjectSize) {
return buffer.slice(offset + dataoffset, offset + dataoffset + length);
}
throw new Error("Too little heap space available! Wanted to read " + length + " bytes, but only " + exports.maxObjectSize + " are available.");
}
function parsePlistString (isUtf16) {
isUtf16 = isUtf16 || 0;
let enc = "utf8";
let length = objInfo;
let stroffset = 1;
if (objInfo == 0xF) {
const int_type = buffer[offset + 1];
const intType = (int_type & 0xF0) / 0x10;
if (intType != 0x1) {
console.error("UNEXPECTED LENGTH-INT TYPE! " + intType);
}
const intInfo = int_type & 0x0F;
const intLength = Math.pow(2, intInfo);
stroffset = 2 + intLength;
if (intLength < 3) {
length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength));
} else {
length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength));
}
}
// length is String length -> to get byte length multiply by 2, as 1 character takes 2 bytes in UTF-16
length *= (isUtf16 + 1);
if (length < exports.maxObjectSize) {
let plistString = Buffer.from(buffer.slice(offset + stroffset, offset + stroffset + length));
if (isUtf16) {
plistString = swapBytes(plistString);
enc = "ucs2";
}
return plistString.toString(enc);
}
throw new Error("Too little heap space available! Wanted to read " + length + " bytes, but only " + exports.maxObjectSize + " are available.");
}
function parseArray() {
let length = objInfo;
let arrayoffset = 1;
if (objInfo == 0xF) {
const int_type = buffer[offset + 1];
const intType = (int_type & 0xF0) / 0x10;
if (intType != 0x1) {
console.error("0xa: UNEXPECTED LENGTH-INT TYPE! " + intType);
}
const intInfo = int_type & 0x0F;
const intLength = Math.pow(2, intInfo);
arrayoffset = 2 + intLength;
if (intLength < 3) {
length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength));
} else {
length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength));
}
}
if (length * objectRefSize > exports.maxObjectSize) {
throw new Error("Too little heap space available!");
}
const array = [];
for (let i = 0; i < length; i++) {
const objRef = readUInt(buffer.slice(offset + arrayoffset + i * objectRefSize, offset + arrayoffset + (i + 1) * objectRefSize));
array[i] = parseObject(objRef);
}
return array;
}
function parseDictionary() {
let length = objInfo;
let dictoffset = 1;
if (objInfo == 0xF) {
const int_type = buffer[offset + 1];
const intType = (int_type & 0xF0) / 0x10;
if (intType != 0x1) {
console.error("0xD: UNEXPECTED LENGTH-INT TYPE! " + intType);
}
const intInfo = int_type & 0x0F;
const intLength = Math.pow(2, intInfo);
dictoffset = 2 + intLength;
if (intLength < 3) {
length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength));
} else {
length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength));
}
}
if (length * 2 * objectRefSize > exports.maxObjectSize) {
throw new Error("Too little heap space available!");
}
if (debug) {
console.log("Parsing dictionary #" + tableOffset);
}
const dict = {};
for (let i = 0; i < length; i++) {
const keyRef = readUInt(buffer.slice(offset + dictoffset + i * objectRefSize, offset + dictoffset + (i + 1) * objectRefSize));
const valRef = readUInt(buffer.slice(offset + dictoffset + (length * objectRefSize) + i * objectRefSize, offset + dictoffset + (length * objectRefSize) + (i + 1) * objectRefSize));
const key = parseObject(keyRef);
const val = parseObject(valRef);
if (debug) {
console.log(" DICT #" + tableOffset + ": Mapped " + key + " to " + val);
}
dict[key] = val;
}
return dict;
}
}
return [ parseObject(topObject) ];
};
function readUInt(buffer, start) {
start = start || 0;
let l = 0;
for (let i = start; i < buffer.length; i++) {
l <<= 8;
l |= buffer[i] & 0xFF;
}
return l;
}
// we're just going to toss the high order bits because javascript doesn't have 64-bit ints
function readUInt64BE(buffer, start) {
const data = buffer.slice(start, start + 8);
return data.readUInt32BE(4, 8);
}
function swapBytes(buffer) {
const len = buffer.length;
for (let i = 0; i < len; i += 2) {
const a = buffer[i];
buffer[i] = buffer[i+1];
buffer[i+1] = a;
}
return buffer;
}

View File

@@ -0,0 +1,35 @@
{
"name": "bplist-parser",
"version": "0.3.1",
"description": "Binary plist parser.",
"main": "bplistParser.js",
"scripts": {
"test": "mocha test"
},
"keywords": [
"bplist",
"plist",
"parser"
],
"author": "Joe Ferner <joe.ferner@nearinfinity.com>",
"contributors": [
"Brett Zamir"
],
"license": "MIT",
"devDependencies": {
"eslint": "6.5.x",
"mocha": "6.2.x"
},
"homepage": "https://github.com/nearinfinity/node-bplist-parser",
"bugs": "https://github.com/nearinfinity/node-bplist-parser/issues",
"engines": {
"node": ">= 5.10.0"
},
"repository": {
"type": "git",
"url": "https://github.com/nearinfinity/node-bplist-parser.git"
},
"dependencies": {
"big-integer": "1.6.x"
}
}

View File

@@ -0,0 +1,53 @@
{
"name": "simple-plist",
"author": "Joe Wollard",
"license": "MIT",
"homepage": "https://github.com/wollardj/simple-plist.git",
"repository": {
"type": "git",
"url": "https://github.com/wollardj/simple-plist.git"
},
"version": "1.3.1",
"description": "A wrapper utility for interacting with plist data.",
"main": "dist/index",
"files": [
"./dist"
],
"types": "./dist/index.d.ts",
"keywords": [
"plist",
"binary",
"bplist",
"xml"
],
"scripts": {
"build:tsc": "tsc --project ./tsconfig-build.json",
"build": "run-s clean:build build:tsc",
"clean:build": "rimraf dist",
"clean": "rimraf __tests__/write-test* coverage",
"prepare": "husky install",
"pretest": "clean",
"test": "jest --coverage --verbose"
},
"dependencies": {
"bplist-creator": "0.1.0",
"bplist-parser": "0.3.1",
"plist": "^3.0.5"
},
"devDependencies": {
"@types/jest": "^27.4.1",
"@types/node": "^16.11.26",
"@types/plist": "^3.0.2",
"husky": "^7.0.4",
"jest": "^27.5.1",
"lint-staged": "^11.2.6",
"npm-run-all": "^4.1.5",
"prettier": "^2.6.1",
"rimraf": "^3.0.2",
"ts-jest": "^27.0.7",
"typescript": "^4.4.4"
},
"lint-staged": {
"*.{ts,js,json,md}": "yarn prettier --write"
}
}