@basmilius/apple-companion-link 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CODEOWNERS +1 -0
- package/LICENSE +21 -0
- package/dist/cli.d.ts +3 -0
- package/dist/const.d.ts +5 -0
- package/dist/crypto/chacha20.d.ts +7 -0
- package/dist/crypto/curve25519.d.ts +7 -0
- package/dist/crypto/hkdf.d.ts +8 -0
- package/dist/crypto/index.d.ts +3 -0
- package/dist/discovery/discovery.d.ts +10 -0
- package/dist/discovery/index.d.ts +1 -0
- package/dist/encoding/index.d.ts +3 -0
- package/dist/encoding/opack.d.ts +10 -0
- package/dist/encoding/plist.d.ts +2 -0
- package/dist/encoding/tlv8.d.ts +40 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +24 -0
- package/dist/protocol/api/companionLink.d.ts +71 -0
- package/dist/protocol/companionLink.d.ts +14 -0
- package/dist/protocol/index.d.ts +1 -0
- package/dist/protocol/pairing/companionLink.d.ts +24 -0
- package/dist/protocol/verify/companionLink.d.ts +18 -0
- package/dist/socket/base.d.ts +7 -0
- package/dist/socket/companionLink.d.ts +40 -0
- package/dist/socket/index.d.ts +2 -0
- package/package.json +56 -0
- package/src/cli.ts +19 -0
- package/src/const.ts +7 -0
- package/src/crypto/chacha20.ts +95 -0
- package/src/crypto/curve25519.ts +21 -0
- package/src/crypto/hkdf.ts +13 -0
- package/src/crypto/index.ts +13 -0
- package/src/discovery/discovery.ts +52 -0
- package/src/discovery/index.ts +1 -0
- package/src/encoding/index.ts +22 -0
- package/src/encoding/opack.ts +293 -0
- package/src/encoding/plist.ts +2 -0
- package/src/encoding/tlv8.ts +111 -0
- package/src/index.ts +3 -0
- package/src/net/getLocalIP.ts +25 -0
- package/src/net/getMacAddress.ts +25 -0
- package/src/net/index.ts +2 -0
- package/src/protocol/api/companionLink.ts +353 -0
- package/src/protocol/companionLink.ts +41 -0
- package/src/protocol/index.ts +1 -0
- package/src/protocol/pairing/companionLink.ts +286 -0
- package/src/protocol/verify/companionLink.ts +185 -0
- package/src/socket/base.ts +21 -0
- package/src/socket/companionLink.ts +249 -0
- package/src/socket/index.ts +2 -0
- package/src/test.ts +109 -0
- package/src/transient.ts +64 -0
- package/src/types.d.ts +40 -0
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@basmilius/apple-companion-link",
|
|
3
|
+
"description": "Implementation of Apple's Companion Link",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Bas Milius",
|
|
9
|
+
"email": "bas@mili.us",
|
|
10
|
+
"url": "https://bas.dev"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"apple",
|
|
14
|
+
"airplay",
|
|
15
|
+
"tv",
|
|
16
|
+
"apple tv",
|
|
17
|
+
"homekit"
|
|
18
|
+
],
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"src",
|
|
22
|
+
"CODEOWNERS",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public",
|
|
27
|
+
"provenance": true
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "bun -b build.ts"
|
|
31
|
+
},
|
|
32
|
+
"main": "./dist/index.js",
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"typings": "./dist/index.d.ts",
|
|
35
|
+
"sideEffects": false,
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"default": "./dist/index.js"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@noble/curves": "^2.0.1",
|
|
44
|
+
"@plist/binary.parse": "^1.1.0",
|
|
45
|
+
"@plist/binary.serialize": "^1.1.0",
|
|
46
|
+
"chacha": "^2.1.0",
|
|
47
|
+
"fast-srp-hap": "^2.0.4",
|
|
48
|
+
"node-dns-sd": "^1.0.1",
|
|
49
|
+
"tweetnacl": "^1.0.3",
|
|
50
|
+
"uuid": "^13.0.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@basmilius/tools": "^2.13.0",
|
|
54
|
+
"@types/bun": "^1.3.1"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import { styleText } from 'node:util';
|
|
3
|
+
|
|
4
|
+
const stdin = createInterface({
|
|
5
|
+
input: process.stdin,
|
|
6
|
+
output: process.stdout
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export function debug(...data: any[]): void {
|
|
10
|
+
console.debug(styleText('cyan', '[debug]'), ...data);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function prompt(message: string): Promise<string> {
|
|
14
|
+
return await new Promise<string>(resolve => stdin.question(`${message}: `, resolve));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function waitFor(ms: number): Promise<void> {
|
|
18
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
19
|
+
}
|
package/src/const.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createCipher, createDecipher } from 'chacha';
|
|
2
|
+
|
|
3
|
+
const AUTH_TAG_LENGTH = 16;
|
|
4
|
+
const NONCE_LENGTH = 12;
|
|
5
|
+
|
|
6
|
+
export function decrypt(key: Buffer, nonce: Buffer, add: Buffer | null, ciphertext: Buffer, authTag: Buffer): Buffer {
|
|
7
|
+
nonce = padNonce(nonce);
|
|
8
|
+
|
|
9
|
+
const decipher = createDecipher(key, nonce);
|
|
10
|
+
add && decipher.setAAD(add);
|
|
11
|
+
decipher.setAuthTag(authTag);
|
|
12
|
+
|
|
13
|
+
const plaintext = decipher._update(ciphertext);
|
|
14
|
+
decipher._final();
|
|
15
|
+
|
|
16
|
+
return plaintext;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function encrypt(key: Buffer, nonce: Buffer, aad: Buffer | null, plaintext: Buffer): EncryptedData {
|
|
20
|
+
nonce = padNonce(nonce);
|
|
21
|
+
|
|
22
|
+
const cipher = createCipher(key, nonce);
|
|
23
|
+
aad && cipher.setAAD(aad);
|
|
24
|
+
|
|
25
|
+
const ciphertext = cipher._update(plaintext);
|
|
26
|
+
cipher._final();
|
|
27
|
+
|
|
28
|
+
const authTag = cipher.getAuthTag();
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
ciphertext: ciphertext,
|
|
32
|
+
authTag: authTag
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function padNonce(nonce: Buffer): Buffer {
|
|
37
|
+
if (nonce.length >= NONCE_LENGTH) {
|
|
38
|
+
return nonce;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return Buffer.concat([
|
|
42
|
+
Buffer.alloc(NONCE_LENGTH - nonce.length, 0),
|
|
43
|
+
nonce
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// NOTE
|
|
48
|
+
// Uncomment when Bun supports chacha20-poly1305 out of box.
|
|
49
|
+
//
|
|
50
|
+
// import { createCipheriv, createDecipheriv } from 'node:crypto';
|
|
51
|
+
//
|
|
52
|
+
// export function decrypt(key: Buffer, nonce: Buffer, aad: Buffer | null, ciphertext: Buffer, authTag: Buffer): Buffer {
|
|
53
|
+
// if (nonce.length < NONCE_LENGTH) {
|
|
54
|
+
// nonce = Buffer.concat([
|
|
55
|
+
// Buffer.alloc(NONCE_LENGTH - nonce.length, 0),
|
|
56
|
+
// nonce
|
|
57
|
+
// ]);
|
|
58
|
+
// }
|
|
59
|
+
//
|
|
60
|
+
// const decipher = createDecipheriv('chacha20-poly1305', key, nonce, {authTagLength: AUTH_TAG_LENGTH});
|
|
61
|
+
// aad && decipher.setAAD(aad, {plaintextLength: ciphertext.length});
|
|
62
|
+
// decipher.setAuthTag(authTag);
|
|
63
|
+
//
|
|
64
|
+
// const plaintext = decipher.update(ciphertext);
|
|
65
|
+
// decipher.final();
|
|
66
|
+
//
|
|
67
|
+
// return plaintext;
|
|
68
|
+
// }
|
|
69
|
+
//
|
|
70
|
+
// export function encrypt(key: Buffer, nonce: Buffer, aad: Buffer | null, plaintext: Buffer): EncryptedData {
|
|
71
|
+
// if (nonce.length < NONCE_LENGTH) {
|
|
72
|
+
// nonce = Buffer.concat([
|
|
73
|
+
// Buffer.alloc(NONCE_LENGTH - nonce.length, 0),
|
|
74
|
+
// nonce
|
|
75
|
+
// ]);
|
|
76
|
+
// }
|
|
77
|
+
//
|
|
78
|
+
// const cipher = createCipheriv('chacha20-poly1305', key, nonce, {authTagLength: AUTH_TAG_LENGTH});
|
|
79
|
+
// aad && cipher.setAAD(aad, {plaintextLength: plaintext.length});
|
|
80
|
+
//
|
|
81
|
+
// const ciphertext = cipher.update(plaintext);
|
|
82
|
+
// cipher.final();
|
|
83
|
+
//
|
|
84
|
+
// const authTag = cipher.getAuthTag();
|
|
85
|
+
//
|
|
86
|
+
// return {
|
|
87
|
+
// ciphertext: ciphertext,
|
|
88
|
+
// authTag: authTag
|
|
89
|
+
// };
|
|
90
|
+
// }
|
|
91
|
+
|
|
92
|
+
export type EncryptedData = {
|
|
93
|
+
readonly ciphertext: Buffer;
|
|
94
|
+
readonly authTag: Buffer;
|
|
95
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { x25519 } from '@noble/curves/ed25519.js';
|
|
3
|
+
|
|
4
|
+
export function generateKeyPair(): KeyPair {
|
|
5
|
+
const secretKey = randomBytes(32);
|
|
6
|
+
const publicKey = x25519.getPublicKey(secretKey);
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
publicKey,
|
|
10
|
+
secretKey
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function generateSharedSecKey(priKey: Uint8Array, pubKey: Uint8Array): Uint8Array {
|
|
15
|
+
return x25519.getSharedSecret(priKey, pubKey);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface KeyPair {
|
|
19
|
+
readonly publicKey: Uint8Array;
|
|
20
|
+
readonly secretKey: Uint8Array;
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { hkdfSync } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export default function (options: HKDFOptions): Buffer {
|
|
4
|
+
return Buffer.from(hkdfSync(options.hash, options.key, options.salt, options.info, options.length));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type HKDFOptions = {
|
|
8
|
+
readonly hash: string;
|
|
9
|
+
readonly key: Buffer;
|
|
10
|
+
readonly length: number;
|
|
11
|
+
readonly salt: Buffer;
|
|
12
|
+
readonly info: Buffer;
|
|
13
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export {
|
|
2
|
+
decrypt as decryptChacha20,
|
|
3
|
+
encrypt as encryptChacha20
|
|
4
|
+
} from './chacha20';
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
generateKeyPair as generateCurve25519KeyPair,
|
|
8
|
+
generateSharedSecKey as generateCurve25519SharedSecKey
|
|
9
|
+
} from './curve25519'
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
default as hkdf
|
|
13
|
+
} from './hkdf';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import mdns, { Result } from 'node-dns-sd';
|
|
2
|
+
import { waitFor } from '@/cli';
|
|
3
|
+
import { AIRPLAY_SERVICE, COMPANION_LINK_SERVICE, RAOP_SERVICE } from '@/const';
|
|
4
|
+
|
|
5
|
+
export default class Discovery {
|
|
6
|
+
readonly #service: string;
|
|
7
|
+
|
|
8
|
+
constructor(service: string) {
|
|
9
|
+
this.#service = service;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async find(): Promise<Result[]> {
|
|
13
|
+
return await mdns.discover({
|
|
14
|
+
name: this.#service
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async findUntil(fqdn: string, tries: number = 10, timeout: number = 1000): Promise<Result> {
|
|
19
|
+
while (tries > 0) {
|
|
20
|
+
const devices = await this.find();
|
|
21
|
+
const device = devices.find(device => device.fqdn === fqdn);
|
|
22
|
+
|
|
23
|
+
if (device) {
|
|
24
|
+
return device;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log();
|
|
28
|
+
console.log(`Device not found, retrying in ${timeout}ms...`);
|
|
29
|
+
console.log(devices.map(d => ` ● ${d.fqdn}`).join('\n'));
|
|
30
|
+
|
|
31
|
+
tries--;
|
|
32
|
+
|
|
33
|
+
if (tries === 0) {
|
|
34
|
+
throw new Error('Device not found after serveral tries, aborting.');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await waitFor(timeout);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static airplay(): Discovery {
|
|
42
|
+
return new Discovery(AIRPLAY_SERVICE);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static companionLink(): Discovery {
|
|
46
|
+
return new Discovery(COMPANION_LINK_SERVICE);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static raop(): Discovery {
|
|
50
|
+
return new Discovery(RAOP_SERVICE);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Discovery } from './discovery';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export {
|
|
2
|
+
unpack as decodeOPack,
|
|
3
|
+
pack as encodeOPack,
|
|
4
|
+
float as opackFloat,
|
|
5
|
+
int as opackInt,
|
|
6
|
+
sizedInt as opackSizedInt
|
|
7
|
+
} from './opack';
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
parse as parseBinaryPlist,
|
|
11
|
+
serialize as serializeBinaryPlist
|
|
12
|
+
} from './plist';
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
bail as bailTlv,
|
|
16
|
+
decode as decodeTlv,
|
|
17
|
+
encode as encodeTlv,
|
|
18
|
+
Flags as TlvFlags,
|
|
19
|
+
Method as TlvMethod,
|
|
20
|
+
State as TlvState,
|
|
21
|
+
Value as TlvValue
|
|
22
|
+
} from './tlv8';
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
type Packed = Uint8Array;
|
|
2
|
+
type ObjectList = Packed[];
|
|
3
|
+
|
|
4
|
+
class SizedInt extends Number {
|
|
5
|
+
size: number;
|
|
6
|
+
|
|
7
|
+
constructor(value: number, size: number) {
|
|
8
|
+
super(value);
|
|
9
|
+
this.size = size;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const _SIZED_INT_TYPES: Record<number, typeof SizedInt> = {};
|
|
14
|
+
|
|
15
|
+
export function sizedInt(value: number, size: number): SizedInt {
|
|
16
|
+
return new SizedInt(value, size);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class OPACKFloat {
|
|
20
|
+
value: number;
|
|
21
|
+
|
|
22
|
+
constructor(value: number) {
|
|
23
|
+
this.value = value;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function float(value: number) {
|
|
28
|
+
return new OPACKFloat(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class OPACKInt {
|
|
32
|
+
value: number;
|
|
33
|
+
|
|
34
|
+
constructor(value: number) {
|
|
35
|
+
this.value = value;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function int(value: number) {
|
|
40
|
+
return new OPACKInt(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function concat(arr: Uint8Array[]): Uint8Array {
|
|
44
|
+
const total = arr.reduce((s, a) => s + a.length, 0);
|
|
45
|
+
const out = new Uint8Array(total);
|
|
46
|
+
let off = 0;
|
|
47
|
+
for (const a of arr) {
|
|
48
|
+
out.set(a, off);
|
|
49
|
+
off += a.length;
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function u8(b: number) {
|
|
55
|
+
return Uint8Array.of(b);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function uintToLEBytes(value: number | bigint, byteLen: number): Uint8Array {
|
|
59
|
+
const out = new Uint8Array(byteLen);
|
|
60
|
+
let v = BigInt(value);
|
|
61
|
+
for (let i = 0; i < byteLen; i++) {
|
|
62
|
+
out[i] = Number(v & 0xffn);
|
|
63
|
+
v >>= 8n;
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function concatUint8Arrays(arrays: Uint8Array[]): Uint8Array {
|
|
69
|
+
const total = arrays.reduce((sum, a) => sum + a.length, 0);
|
|
70
|
+
const out = new Uint8Array(total);
|
|
71
|
+
let offset = 0;
|
|
72
|
+
for (const a of arrays) {
|
|
73
|
+
out.set(a, offset);
|
|
74
|
+
offset += a.length;
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function pack(data: any): Uint8Array {
|
|
80
|
+
return _pack(data, []);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function _pack(data: any, objectList: ObjectList): Uint8Array {
|
|
84
|
+
let packed: Uint8Array | null = null;
|
|
85
|
+
|
|
86
|
+
if (data === null || data === undefined) packed = u8(0x04);
|
|
87
|
+
else if (typeof data === 'boolean') packed = u8(data ? 0x01 : 0x02);
|
|
88
|
+
else if (data instanceof OPACKFloat) {
|
|
89
|
+
const buf = new ArrayBuffer(8);
|
|
90
|
+
new DataView(buf).setFloat64(0, data.value, true);
|
|
91
|
+
packed = concat([u8(0x36), new Uint8Array(buf)]);
|
|
92
|
+
} else if (data instanceof OPACKInt) {
|
|
93
|
+
const val = data.value;
|
|
94
|
+
if (val < 0x28) packed = u8(0x08 + val);
|
|
95
|
+
else if (val <= 0xff) packed = concatUint8Arrays([u8(0x30), uintToLEBytes(val, 1)]);
|
|
96
|
+
else if (val <= 0xffff) packed = concatUint8Arrays([u8(0x31), uintToLEBytes(val, 2)]);
|
|
97
|
+
else if (val <= 0xffffffff) packed = concatUint8Arrays([u8(0x32), uintToLEBytes(val, 4)]);
|
|
98
|
+
else packed = concatUint8Arrays([u8(0x33), uintToLEBytes(val, 8)]);
|
|
99
|
+
} else if (typeof data === 'number') {
|
|
100
|
+
if (!Number.isInteger(data)) {
|
|
101
|
+
const buf = new ArrayBuffer(8);
|
|
102
|
+
new DataView(buf).setFloat64(0, data, true);
|
|
103
|
+
packed = concat([u8(0x36), new Uint8Array(buf)]);
|
|
104
|
+
} else {
|
|
105
|
+
if (data < 0x28) packed = u8(0x08 + data);
|
|
106
|
+
else if (data <= 0xff) packed = concat([u8(0x30), uintToLEBytes(data, 1)]);
|
|
107
|
+
else if (data <= 0xffff) packed = concat([u8(0x31), uintToLEBytes(data, 2)]);
|
|
108
|
+
else if (data <= 0xffffffff) packed = concat([u8(0x32), uintToLEBytes(data, 4)]);
|
|
109
|
+
else packed = concat([u8(0x33), uintToLEBytes(data, 8)]);
|
|
110
|
+
}
|
|
111
|
+
} else if (data instanceof SizedInt) {
|
|
112
|
+
packed = concat([u8(0x30 + Math.log2(data.size)), uintToLEBytes(data.valueOf(), data.size)]);
|
|
113
|
+
} else if (typeof data === 'string') {
|
|
114
|
+
const b = new TextEncoder().encode(data);
|
|
115
|
+
const len = b.length;
|
|
116
|
+
if (len <= 0x20) packed = concat([u8(0x40 + len), b]);
|
|
117
|
+
else if (len <= 0xff) packed = concat([u8(0x61), uintToLEBytes(len, 1), b]);
|
|
118
|
+
else if (len <= 0xffff) packed = concat([u8(0x62), uintToLEBytes(len, 2), b]);
|
|
119
|
+
else if (len <= 0xffffff) packed = concat([u8(0x63), uintToLEBytes(len, 3), b]);
|
|
120
|
+
else packed = concat([u8(0x64), uintToLEBytes(len, 4), b]);
|
|
121
|
+
} else if (data instanceof Uint8Array || Buffer.isBuffer(data)) {
|
|
122
|
+
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
123
|
+
const len = bytes.length;
|
|
124
|
+
if (len <= 0x20) packed = concat([u8(0x70 + len), bytes]);
|
|
125
|
+
else if (len <= 0xff) packed = concat([u8(0x91), uintToLEBytes(len, 1), bytes]);
|
|
126
|
+
else if (len <= 0xffff) packed = concat([u8(0x92), uintToLEBytes(len, 2), bytes]);
|
|
127
|
+
else packed = concat([u8(0x93), uintToLEBytes(len, 4), bytes]);
|
|
128
|
+
} else if (Array.isArray(data)) {
|
|
129
|
+
const body = concat(data.map(d => _pack(d, objectList)));
|
|
130
|
+
const len = data.length;
|
|
131
|
+
if (len <= 0x0f) {
|
|
132
|
+
packed = concat([u8(0xd0 + len), body]);
|
|
133
|
+
if (len >= 0x0f) packed = concat([packed, u8(0x03)]);
|
|
134
|
+
} else packed = concat([u8(0xdf), body, u8(0x03)]);
|
|
135
|
+
} else if (typeof data === 'object') {
|
|
136
|
+
const keys = Object.keys(data);
|
|
137
|
+
const len = keys.length;
|
|
138
|
+
const pairs: Uint8Array[] = [];
|
|
139
|
+
for (const k of keys) {
|
|
140
|
+
pairs.push(_pack(k, objectList));
|
|
141
|
+
pairs.push(_pack((data as any)[k], objectList));
|
|
142
|
+
}
|
|
143
|
+
let header: Uint8Array;
|
|
144
|
+
if (len <= 0x0f) {
|
|
145
|
+
header = u8(0xE0 + len);
|
|
146
|
+
} else {
|
|
147
|
+
header = u8(0xEF);
|
|
148
|
+
}
|
|
149
|
+
packed = concatUint8Arrays([header, concatUint8Arrays(pairs)]);
|
|
150
|
+
// terminator
|
|
151
|
+
if (len >= 0x0f || objectList.some(v => v === packed)) {
|
|
152
|
+
packed = concatUint8Arrays([packed, u8(0x81)]);
|
|
153
|
+
}
|
|
154
|
+
} else throw new TypeError(typeof data + '');
|
|
155
|
+
|
|
156
|
+
// Object reuse
|
|
157
|
+
const idx = objectList.findIndex(v => v.length === packed!.length && v.every((x, i) => x === packed![i]));
|
|
158
|
+
if (idx >= 0) {
|
|
159
|
+
if (idx < 0x21) packed = u8(0xA0 + idx);
|
|
160
|
+
else if (idx <= 0xff) packed = concat([u8(0xC1), uintToLEBytes(idx, 1)]);
|
|
161
|
+
else if (idx <= 0xffff) packed = concat([u8(0xC2), uintToLEBytes(idx, 2)]);
|
|
162
|
+
else if (idx <= 0xffffffff) packed = concat([u8(0xC3), uintToLEBytes(idx, 4)]);
|
|
163
|
+
else packed = concat([u8(0xC4), uintToLEBytes(idx, 8)]);
|
|
164
|
+
} else if (packed!.length > 1) objectList.push(packed!);
|
|
165
|
+
|
|
166
|
+
return packed!;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* UNPACK */
|
|
170
|
+
export function unpack(data: Uint8Array): [any, Uint8Array] {
|
|
171
|
+
return _unpack(data, []);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function _unpack(data: Uint8Array, objectList: ObjectList): [any, Uint8Array] {
|
|
175
|
+
if (data.length === 0) throw new TypeError('No data to unpack');
|
|
176
|
+
let addToObjectList = true;
|
|
177
|
+
const tag = data[0];
|
|
178
|
+
|
|
179
|
+
// simple tokens
|
|
180
|
+
if (tag === 0x01) return [true, data.subarray(1)];
|
|
181
|
+
if (tag === 0x02) return [false, data.subarray(1)];
|
|
182
|
+
if (tag === 0x04) return [null, data.subarray(1)];
|
|
183
|
+
if (tag === 0x05) {
|
|
184
|
+
const uuidBytes = data.subarray(1, 17);
|
|
185
|
+
return [uuidBytes, data.subarray(17)];
|
|
186
|
+
}
|
|
187
|
+
if (tag === 0x06) {
|
|
188
|
+
// pyatv: dummy parse as integer (little-endian 8 bytes)
|
|
189
|
+
const val = readLittleEndian(data, 1, 8);
|
|
190
|
+
return [val, data.subarray(9)];
|
|
191
|
+
}
|
|
192
|
+
if (tag >= 0x08 && tag <= 0x2f) return [tag - 8, data.subarray(1)];
|
|
193
|
+
if (tag === 0x35) {
|
|
194
|
+
const view = new DataView(data.buffer, data.byteOffset + 1, 4);
|
|
195
|
+
return [view.getFloat32(0, true), data.subarray(5)];
|
|
196
|
+
}
|
|
197
|
+
if (tag === 0x36) {
|
|
198
|
+
const view = new DataView(data.buffer, data.byteOffset + 1, 8);
|
|
199
|
+
return [view.getFloat64(0, true), data.subarray(9)];
|
|
200
|
+
}
|
|
201
|
+
if ((tag & 0xF0) === 0x30) {
|
|
202
|
+
const noOfBytes = 2 ** (tag & 0xF);
|
|
203
|
+
const val = readLittleEndian(data, 1, noOfBytes);
|
|
204
|
+
const sized = sizedInt(val, noOfBytes);
|
|
205
|
+
return [sized, data.subarray(1 + noOfBytes)];
|
|
206
|
+
}
|
|
207
|
+
if (tag >= 0x40 && tag <= 0x60) {
|
|
208
|
+
const length = tag - 0x40;
|
|
209
|
+
const str = new TextDecoder().decode(data.subarray(1, 1 + length));
|
|
210
|
+
return [str, data.subarray(1 + length)];
|
|
211
|
+
}
|
|
212
|
+
if (tag >= 0x61 && tag <= 0x64) {
|
|
213
|
+
const lenBytes = tag & 0xF;
|
|
214
|
+
const length = readLittleEndian(data, 1, lenBytes);
|
|
215
|
+
const str = new TextDecoder().decode(data.subarray(1 + lenBytes, 1 + lenBytes + length));
|
|
216
|
+
return [str, data.subarray(1 + lenBytes + length)];
|
|
217
|
+
}
|
|
218
|
+
if (tag >= 0x70 && tag <= 0x90) {
|
|
219
|
+
const length = tag - 0x70;
|
|
220
|
+
const bytes = data.subarray(1, 1 + length);
|
|
221
|
+
return [bytes, data.subarray(1 + length)];
|
|
222
|
+
}
|
|
223
|
+
if (tag >= 0x91 && tag <= 0x94) {
|
|
224
|
+
// number of length bytes = 1 << ((tag & 0xF) - 1)
|
|
225
|
+
const noOfBytes = 1 << ((tag & 0xF) - 1);
|
|
226
|
+
const length = readLittleEndian(data, 1, noOfBytes);
|
|
227
|
+
const start = 1 + noOfBytes;
|
|
228
|
+
return [data.subarray(start, start + length), data.subarray(start + length)];
|
|
229
|
+
}
|
|
230
|
+
if ((tag & 0xF0) === 0xD0) {
|
|
231
|
+
const count = tag & 0xF;
|
|
232
|
+
let ptr = data.subarray(1);
|
|
233
|
+
const output: any[] = [];
|
|
234
|
+
if (count === 0xF) {
|
|
235
|
+
// endless list
|
|
236
|
+
while (ptr[0] !== 0x03) {
|
|
237
|
+
const [val, rem] = _unpack(ptr, objectList);
|
|
238
|
+
output.push(val);
|
|
239
|
+
ptr = rem;
|
|
240
|
+
}
|
|
241
|
+
ptr = ptr.subarray(1); // skip terminator
|
|
242
|
+
} else {
|
|
243
|
+
for (let i = 0; i < count; i++) {
|
|
244
|
+
const [val, rem] = _unpack(ptr, objectList);
|
|
245
|
+
output.push(val);
|
|
246
|
+
ptr = rem;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
addToObjectList = false;
|
|
250
|
+
return [output, ptr];
|
|
251
|
+
}
|
|
252
|
+
if ((tag & 0xE0) === 0xE0) {
|
|
253
|
+
const count = tag & 0xF;
|
|
254
|
+
let ptr = data.subarray(1);
|
|
255
|
+
const output: Record<string, any> = {};
|
|
256
|
+
if (count === 0xF) {
|
|
257
|
+
// endless dict
|
|
258
|
+
while (ptr[0] !== 0x03) {
|
|
259
|
+
const [key, rem1] = _unpack(ptr, objectList);
|
|
260
|
+
const [val, rem2] = _unpack(rem1, objectList);
|
|
261
|
+
output[key] = val;
|
|
262
|
+
ptr = rem2;
|
|
263
|
+
}
|
|
264
|
+
ptr = ptr.subarray(1);
|
|
265
|
+
} else {
|
|
266
|
+
for (let i = 0; i < count; i++) {
|
|
267
|
+
const [key, rem1] = _unpack(ptr, objectList);
|
|
268
|
+
const [val, rem2] = _unpack(rem1, objectList);
|
|
269
|
+
output[key] = val;
|
|
270
|
+
ptr = rem2;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
addToObjectList = false;
|
|
274
|
+
return [output, ptr];
|
|
275
|
+
}
|
|
276
|
+
if (tag >= 0xA0 && tag <= 0xC0) {
|
|
277
|
+
const idx = tag - 0xA0;
|
|
278
|
+
return [objectList[idx], data.subarray(1)];
|
|
279
|
+
}
|
|
280
|
+
if (tag >= 0xC1 && tag <= 0xC4) {
|
|
281
|
+
const len = tag - 0xC0;
|
|
282
|
+
const uid = readLittleEndian(data, 1, len);
|
|
283
|
+
return [objectList[uid], data.subarray(1 + len)];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
throw new TypeError(`Unknown tag 0x${tag.toString(16)}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function readLittleEndian(buf: Uint8Array, offset: number, len: number) {
|
|
290
|
+
let v = 0n;
|
|
291
|
+
for (let i = len - 1; i >= 0; i--) v = (v << 8n) | BigInt(buf[offset + i]);
|
|
292
|
+
return Number(v);
|
|
293
|
+
}
|