Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ jobs:

- run: npm ci

- run: npm run typecheck

- run: npm test
26 changes: 0 additions & 26 deletions bplistParser.d.ts

This file was deleted.

99 changes: 52 additions & 47 deletions bplistParser.js → bplistParser.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
/* eslint-disable no-console */

'use strict';

// adapted from https://github.com/3breadt/dd-plist

import fs from 'node:fs';
Expand All @@ -13,11 +11,11 @@ export let maxObjectCount = 32768;
// Exported bindings are read-only to consumers (an ESM import binding cannot be
// assigned, and the CommonJS build exposes exports as getters), so these knobs
// are tuned through setters rather than by assigning to the exports.
export function setMaxObjectSize(value) {
export function setMaxObjectSize(value: number): void {
maxObjectSize = value;
}

export function setMaxObjectCount(value) {
export function setMaxObjectCount(value: number): void {
maxObjectCount = value;
}

Expand All @@ -26,21 +24,30 @@ export function setMaxObjectCount(value) {
// So we just hardcode the correct value.
const EPOCH = 978307200000;

// UID object definition
export const UID = function(id) {
this.UID = id;
};
/** Wrapper for a CoreFoundation keyed-archiver UID value. */
export class UID {
UID: number;

constructor(id: number) {
this.UID = id;
}
}

export type CallbackFunction<T = any> = (error: Error | null, result?: [T]) => void;

export const parseFile = function (fileNameOrBuffer, callback) {
return new Promise(function (resolve, reject) {
function tryParseBuffer(buffer) {
let err = null;
let result;
export function parseFile<T = any>(
fileNameOrBuffer: string | Buffer,
callback?: CallbackFunction<T>
): Promise<[T]> {
return new Promise<[T]>(function (resolve, reject) {
function tryParseBuffer(buffer: Buffer) {
let err: Error | null = null;
let result: [T] | undefined;
try {
result = parseBuffer(buffer);
result = parseBuffer<T>(buffer);
resolve(result);
} catch (ex) {
err = ex;
err = ex as Error;
reject(err);
} finally {
if (callback) callback(err, result);
Expand All @@ -59,16 +66,14 @@ export const parseFile = function (fileNameOrBuffer, callback) {
tryParseBuffer(data);
});
});
};
}

export const parseFileSync = function (fileNameOrBuffer) {
if (!Buffer.isBuffer(fileNameOrBuffer)) {
fileNameOrBuffer = fs.readFileSync(fileNameOrBuffer);
}
return parseBuffer(fileNameOrBuffer);
};
export function parseFileSync<T = any>(fileNameOrBuffer: string | Buffer): [T] {
const buffer = Buffer.isBuffer(fileNameOrBuffer) ? fileNameOrBuffer : fs.readFileSync(fileNameOrBuffer);
return parseBuffer<T>(buffer);
}

export const parseBuffer = function (buffer) {
export function parseBuffer<T = any>(buffer: Buffer): [T] {
// check header
const header = buffer.slice(0, 'bplist00'.length).toString('utf8');
if (header !== 'bplist00') {
Expand Down Expand Up @@ -107,7 +112,7 @@ export const parseBuffer = function (buffer) {
}

// Handle offset table
const offsetTable = [];
const offsetTable: number[] = [];

for (let i = 0; i < numObjects; i++) {
const offsetBytes = buffer.slice(offsetTableOffset + i * offsetSize, offsetTableOffset + (i + 1) * offsetSize);
Expand All @@ -121,7 +126,7 @@ export const parseBuffer = function (buffer) {
// 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) {
function parseObject(tableOffset: number): any {
const offset = offsetTable[tableOffset];
const type = buffer[offset];
const objType = (type & 0xF0) >> 4; //First 4 bits
Expand Down Expand Up @@ -151,7 +156,7 @@ export const parseBuffer = function (buffer) {
throw new Error("Unhandled type 0x" + objType.toString(16));
}

function parseSimple() {
function parseSimple(): any {
//Simple
switch (objInfo) {
case 0x0: // null
Expand All @@ -167,7 +172,7 @@ export const parseBuffer = function (buffer) {
}
}

function parseInteger() {
function parseInteger(): number | bigint {
const length = Math.pow(2, objInfo);
if (length < maxObjectSize) {
const data = buffer.slice(offset + 1, offset + 1 + length);
Expand All @@ -177,15 +182,15 @@ export const parseBuffer = function (buffer) {

}

function parseUID() {
function parseUID(): UID {
const length = objInfo + 1;
if (length < 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 " + maxObjectSize + " are available.");
}

function parseReal() {
function parseReal(): number | undefined {
const length = Math.pow(2, objInfo);
if (length < maxObjectSize) {
const realBuffer = buffer.slice(offset + 1, offset + 1 + length);
Expand All @@ -200,15 +205,15 @@ export const parseBuffer = function (buffer) {
}
}

function parseDate() {
function parseDate(): Date {
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() {
function parseData(): Buffer {
let dataoffset = 1;
let length = objInfo;
if (objInfo == 0xF) {
Expand All @@ -232,9 +237,9 @@ export const parseBuffer = function (buffer) {
throw new Error("Too little heap space available! Wanted to read " + length + " bytes, but only " + maxObjectSize + " are available.");
}

function parsePlistString (isUtf16) {
isUtf16 = isUtf16 || 0;
let enc = "utf8";
function parsePlistString(isUtf16?: boolean): string {
const utf16Flag = isUtf16 || false;
let enc: BufferEncoding = "utf8";
let length = objInfo;
let stroffset = 1;
if (objInfo == 0xF) {
Expand All @@ -253,10 +258,10 @@ export const parseBuffer = function (buffer) {
}
}
// length is String length -> to get byte length multiply by 2, as 1 character takes 2 bytes in UTF-16
length *= (isUtf16 + 1);
length *= (utf16Flag ? 2 : 1);
if (length < maxObjectSize) {
let plistString = Buffer.from(buffer.slice(offset + stroffset, offset + stroffset + length));
if (isUtf16) {
if (utf16Flag) {
plistString = swapBytes(plistString);
enc = "ucs2";
}
Expand All @@ -265,7 +270,7 @@ export const parseBuffer = function (buffer) {
throw new Error("Too little heap space available! Wanted to read " + length + " bytes, but only " + maxObjectSize + " are available.");
}

function parseArray() {
function parseArray(): any[] {
let length = objInfo;
let arrayoffset = 1;
if (objInfo == 0xF) {
Expand All @@ -286,15 +291,15 @@ export const parseBuffer = function (buffer) {
if (length * objectRefSize > maxObjectSize) {
throw new Error("Too little heap space available!");
}
const array = [];
const array: any[] = [];
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() {
function parseDictionary(): Record<string, any> {
let length = objInfo;
let dictoffset = 1;
if (objInfo == 0xF) {
Expand All @@ -318,7 +323,7 @@ export const parseBuffer = function (buffer) {
if (debug) {
console.log("Parsing dictionary #" + tableOffset);
}
const dict = {};
const dict: Record<string, any> = {};
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));
Expand All @@ -334,9 +339,9 @@ export const parseBuffer = function (buffer) {
}

return [ parseObject(topObject) ];
};
}

function readUInt(buffer, start) {
function readUInt(buffer: Buffer, start?: number): number {
start = start || 0;

let l = 0;
Expand All @@ -346,22 +351,22 @@ function readUInt(buffer, start) {
return l;
}

function readBigUInt(buffer) {
function readBigUInt(buffer: Buffer): bigint {
let value = 0n;
for (const byte of buffer) {
value = (value << 8n) | BigInt(byte);
}
return value;
}

function simplifyInteger(value) {
function simplifyInteger(value: bigint): number | bigint {
if (value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER)) {
return Number(value);
}
return value;
}

function readInteger(buffer) {
function readInteger(buffer: Buffer): number | bigint {
let value = readBigUInt(buffer);

if (buffer.length === 8 && (buffer[0] & 0x80)) {
Expand All @@ -372,12 +377,12 @@ function readInteger(buffer) {
}

// we're just going to toss the high order bits because javascript doesn't have 64-bit ints
function readUInt64BE(buffer, start) {
function readUInt64BE(buffer: Buffer, start: number): number {
const data = buffer.slice(start, start + 8);
return readUInt(data, 0);
}

function swapBytes(buffer) {
function swapBytes<T extends Buffer>(buffer: T): T {
const len = buffer.length;
for (let i = 0; i < len; i += 2) {
const a = buffer[i];
Expand Down
20 changes: 13 additions & 7 deletions build.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { build } from 'esbuild';
import { copyFile, mkdir, readFile, writeFile } from 'fs/promises';
import { execFileSync } from 'node:child_process';
import { mkdir, readFile, rm, writeFile } from 'fs/promises';

const entry = 'bplistParser.js';
const entry = 'bplistParser.ts';
const outdir = 'dist';

await mkdir(outdir, { recursive: true });
Expand All @@ -20,10 +21,15 @@ await Promise.all([
build({ ...common, format: 'cjs', outfile: `${outdir}/index.cjs` }),
]);

// Ship the same declarations under both extensions so `import` and `require`
// consumers each resolve types under their own resolution mode.
const types = await readFile('bplistParser.d.ts', 'utf8');
await writeFile(`${outdir}/index.d.ts`, types);
await writeFile(`${outdir}/index.d.cts`, types);
// tsc emits the declaration for the entry file into dist/types; ship the same
// declarations under both extensions so `import` and `require` consumers each
// resolve types under their own resolution mode.
execFileSync('npx', ['tsc', '-p', 'tsconfig.build.json'], { stdio: 'inherit' });
const types = await readFile(`${outdir}/types/bplistParser.d.ts`, 'utf8');
await Promise.all([
writeFile(`${outdir}/index.d.ts`, types),
writeFile(`${outdir}/index.d.cts`, types),
]);
await rm(`${outdir}/types`, { recursive: true, force: true });

console.log('built', outdir);
15 changes: 13 additions & 2 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import globals from 'globals';

export default [
export default tseslint.config(
{
ignores: ['dist/**', 'node_modules/**'],
},
js.configs.recommended,
{
files: ['**/*.ts'],
extends: [...tseslint.configs.recommended],
rules: {
// Parsing/creating plists is inherently dynamic (arbitrary nested
// dicts/arrays of unknown shape); `any` is the honest type here, not a
// shortcut.
'@typescript-eslint/no-explicit-any': 'off',
},
},
{
languageOptions: {
ecmaVersion: 2022,
Expand Down Expand Up @@ -48,4 +59,4 @@ export default [
},
},
},
];
);
Loading
Loading