Property list file encoding and decoding
- Pure TypeScript, run anywhere
- Zero dependencies
- Tree shaking friendly design
- Support for both encoding and decoding multiple formats
- OpenStep / Strings
- XML
- Binary
- Optional support for non-UTF XML encodings (BYO decoder)
- Fast custom parsers that correctly handle the edge cases of offical parsers
- Well tested against hundreds of samples generated from official libraries
- Additionally tested against samples that offical libraries do not produce
- Support for obscure data types supported by official libraries
- Supports encoding output identical to official libraries
- Encoders and decoders preserve the data structure as closely as possible
- Includes a type-safe walk function to walk a property list
import {
encode,
encodeBinary,
FORMAT_BINARY_V1_0,
PLDictionary,
PLInteger,
PLString,
} from '@hqtsm/plist';
const plist = new PLDictionary();
plist.set(new PLString('Name'), new PLString('John Smith'));
plist.set(new PLString('Age'), new PLInteger(42n));
const expected = `
62 70 6C 69 73 74 30 30 D2 01 02 03 04 54 4E 61
6D 65 53 41 67 65 5A 4A 6F 68 6E 20 53 6D 69 74
68 10 2A 08 0D 12 16 21 00 00 00 00 00 00 01 01
00 00 00 00 00 00 00 05 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 23
`.trim().split(/\s+/).map((s) => parseInt(s, 16));
// OR: encodeBinary(plist);
const enc = encode(plist, { format: FORMAT_BINARY_V1_0 });
console.assert(String.fromCharCode(...enc.slice(0, 8)) === 'bplist00');
console.assert(JSON.stringify([...enc]) === JSON.stringify(expected));NOTE: It is possible to encode types that official libraries to not support encoding and/or decoding (null type, set type, non-string keys, non-primitive keys).
Only FORMAT_BINARY_V1_0 is valid for binary.
A list of types or values to be duplicated in the offset table. Only useful to create a 1:1 identical encode as an official encoder.
import {
encode,
FORMAT_XML_V1_0,
PLDictionary,
PLInteger,
PLString,
PLUID,
} from '@hqtsm/plist';
const plist = new PLDictionary();
plist.set(new PLString('First Name'), new PLString('John'));
plist.set(new PLString('Last Name'), new PLString('Smith'));
plist.set(new PLString('Age'), new PLInteger(42n));
plist.set(new PLString('ID'), new PLUID(1234n));
const expected = `<?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>First Name</key>
<string>John</string>
<key>Last Name</key>
<string>Smith</string>
<key>Age</key>
<integer>42</integer>
<key>ID</key>
<dict>
<key>CF$UID</key>
<integer>1234</integer>
</dict>
</dict>
</plist>
`;
// OR: encodeXml(plist);
const enc = encode(plist, { format: FORMAT_XML_V1_0 });
console.assert(new TextDecoder().decode(enc) === expected);NOTE: Not every type is supported in the XML format. An exception will throw for null type, set type, and non-string keys.
Either FORMAT_XML_V1_0 or FORMAT_XML_V0_9 (different headers).
A custom indent strings of tabs or spaces.
Encode -0.0 as just 0.0 as official encoders do.
Encode smallest 128-bit integer as -0 as official encoders do. 128-bit integers are a private API in official encoders, limited compatibility.
import { encode, FORMAT_OPENSTEP, PLDictionary, PLString } from '@hqtsm/plist';
const plist = new PLDictionary();
plist.set(new PLString('First Name'), new PLString('John'));
plist.set(new PLString('Last Name'), new PLString('Smith'));
const expected = `{
"First Name" = John;
"Last Name" = Smith;
}
`;
// OR: encodeOpenStep(plist);
const enc = encode(plist, { format: FORMAT_OPENSTEP });
console.assert(new TextDecoder().decode(enc) === expected);import { encode, FORMAT_STRINGS, PLDictionary, PLString } from '@hqtsm/plist';
const plist = new PLDictionary();
plist.set(new PLString('First Name'), new PLString('John'));
plist.set(new PLString('Last Name'), new PLString('Smith'));
const expected = `"First Name" = John;
"Last Name" = Smith;
`;
// OR: encodeOpenStep(plist, { format: FORMAT_STRINGS });
const enc = encode(plist, { format: FORMAT_STRINGS });
console.assert(new TextDecoder().decode(enc) === expected);NOTE: Not every type is supported in the OpenStep formats. An exception will throw for any values not a dictionary, array, string, or data type, and for keys that are not string type. With the strings format only the dictionary type can be encoded.
Either FORMAT_OPENSTEP or FORMAT_STRINGS.
A custom indent strings of tabs or spaces.
Set the string quote character.
Option to always make strings quoted even when it is optional.
Use the "shortcut" style for keys and values that are reference equal.
{
"shortcut";
}
VS
{
"shortcut" = "shortcut";
}
import { decode, FORMAT_BINARY_V1_0, PLDictionary, walk } from '@hqtsm/plist';
const encoded = new Uint8Array(
`
62 70 6C 69 73 74 30 30 D2 01 02 03 04 54 4E 61
6D 65 53 41 67 65 5A 4A 6F 68 6E 20 53 6D 69 74
68 10 2A 08 0D 12 16 21 00 00 00 00 00 00 01 01
00 00 00 00 00 00 00 05 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 23
`.trim().split(/\s+/).map((s) => parseInt(s, 16)),
);
// OR: decodeBinary(encoded):
const { format, plist } = decode(encoded);
console.assert(format === FORMAT_BINARY_V1_0);
console.assert(PLDictionary.is(plist));
walk(plist, {
PLDictionary(dict) {
console.assert(dict.size === 2);
},
PLString(str, _, key) {
if (key) {
console.assert(str.value === 'John Smith');
} else {
const keys = ['Name', 'Age'];
console.assert(keys.includes(str.value));
}
},
PLInteger(int) {
console.assert(int.value === 42n);
},
});Optionally limit integers to the range of 64-bit signed or unsigned values. 128-bit integers in official decoders is limited to unsigned 64-bit values.
Optionally limit key types to primitive types. The open source CF encoder does this.
Optionally limit key types to string type. The closed source CF encoder does this.
import { decode, FORMAT_XML_V1_0, PLDictionary, walk } from '@hqtsm/plist';
const encoded = new TextEncoder().encode(`<?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>First Name</key>
<string>John</string>
<key>Last Name</key>
<string>Smith</string>
<key>Age</key>
<integer>42</integer>
<key>ID</key>
<dict>
<key>CF$UID</key>
<integer>1234</integer>
</dict>
</dict>
</plist>
`);
// OR: decodeXml(encoded);
const { format, plist } = decode(encoded);
console.assert(format === FORMAT_XML_V1_0);
console.assert(PLDictionary.is(plist));
walk(plist, {
PLDictionary(dict) {
console.assert(dict.size === 4);
},
PLString(str, _, key) {
if (key) {
const values = ['John', 'Smith'];
console.assert(values.includes(str.value));
} else {
const keys = ['First Name', 'Last Name', 'Age', 'ID'];
console.assert(keys.includes(str.value));
}
},
PLInteger(int) {
console.assert(int.value === 42n);
},
PLUID(int) {
console.assert(int.value === 1234n);
},
});Flag to skip decoding and assumed UTF-8 without BOM.
Optonal decoder function for converting XML to UTF-8 based on the header. Useful to decode an XML document with a non-UTF encoding. Official encoders always use UTF-8 but many encodings can be decoded depending on what the host platform has available. Using TextDecoder can be helpful, but it may be different in some edge cases.
Optionally limit integers to the range of 64-bit signed or unsigned values. 128-bit integers in official decoders is limited to unsigned 64-bit values.
Optional UTF-16 endian flag when no BOM available. Defaults to auto detect based on which character is null. Official decoders assume it will match host endian.
import { decode, FORMAT_OPENSTEP, PLDictionary, walk } from '@hqtsm/plist';
const encoded = new TextEncoder().encode(`{
"First Name" = John;
"Last Name" = Smith;
}
`);
// OR: decodeOpenStep(encoded);
const { format, plist } = decode(encoded);
console.assert(format === FORMAT_OPENSTEP);
console.assert(PLDictionary.is(plist));
walk(plist, {
PLDictionary(dict) {
console.assert(dict.size === 2);
},
PLString(str, _, key) {
if (key) {
const values = ['John', 'Smith'];
console.assert(values.includes(str.value));
} else {
const keys = ['First Name', 'Last Name'];
console.assert(keys.includes(str.value));
}
},
});import { decode, FORMAT_STRINGS, PLDictionary, walk } from '@hqtsm/plist';
const encoded = new TextEncoder().encode(`"First Name" = John;
"Last Name" = Smith;
`);
// OR: decodeOpenStep(encoded);
const { format, plist } = decode(encoded);
console.assert(format === FORMAT_STRINGS);
console.assert(PLDictionary.is(plist));
walk(plist, {
PLDictionary(dict) {
console.assert(dict.size === 2);
},
PLString(str, _, key) {
if (key) {
const values = ['John', 'Smith'];
console.assert(values.includes(str.value));
} else {
const keys = ['First Name', 'Last Name'];
console.assert(keys.includes(str.value));
}
},
});Allow missing semicolon on the last dictionary item. Official decoders once allowed this but deprecated and removed it.
Flag to skip decoding and assumed UTF-8 without BOM. OpenStep does not store encoding information so UTF is assumed by decoders. If another encoding is used it must first be converted to UTF before decoding.
Optional UTF-16 endian flag when no BOM available. Defaults to auto detect based on which character is null. Official decoders assume it will match host endian.