@yume-chan/adb 0.0.11 → 0.0.14
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/CHANGELOG.json +39 -0
- package/CHANGELOG.md +23 -1
- package/README.md +24 -17
- package/esm/adb.d.ts +10 -9
- package/esm/adb.d.ts.map +1 -1
- package/esm/adb.js +87 -64
- package/esm/adb.js.map +1 -1
- package/esm/auth.d.ts +6 -6
- package/esm/auth.d.ts.map +1 -1
- package/esm/auth.js +8 -6
- package/esm/auth.js.map +1 -1
- package/esm/backend.d.ts +2 -2
- package/esm/commands/install.d.ts.map +1 -1
- package/esm/commands/install.js +5 -7
- package/esm/commands/install.js.map +1 -1
- package/esm/commands/reverse.d.ts +7 -10
- package/esm/commands/reverse.d.ts.map +1 -1
- package/esm/commands/reverse.js +9 -15
- package/esm/commands/reverse.js.map +1 -1
- package/esm/commands/subprocess/index.d.ts +19 -20
- package/esm/commands/subprocess/index.d.ts.map +1 -1
- package/esm/commands/subprocess/index.js +36 -31
- package/esm/commands/subprocess/index.js.map +1 -1
- package/esm/commands/subprocess/legacy.d.ts +14 -7
- package/esm/commands/subprocess/legacy.d.ts.map +1 -1
- package/esm/commands/subprocess/legacy.js +14 -5
- package/esm/commands/subprocess/legacy.js.map +1 -1
- package/esm/commands/subprocess/protocol.d.ts +3 -2
- package/esm/commands/subprocess/protocol.d.ts.map +1 -1
- package/esm/commands/subprocess/protocol.js +6 -4
- package/esm/commands/subprocess/protocol.js.map +1 -1
- package/esm/commands/subprocess/protocols/index.d.ts +4 -0
- package/esm/commands/subprocess/protocols/index.d.ts.map +1 -0
- package/esm/commands/subprocess/protocols/index.js +4 -0
- package/esm/commands/subprocess/protocols/index.js.map +1 -0
- package/esm/commands/subprocess/protocols/none.d.ts +35 -0
- package/esm/commands/subprocess/protocols/none.d.ts.map +1 -0
- package/esm/commands/subprocess/protocols/none.js +53 -0
- package/esm/commands/subprocess/protocols/none.js.map +1 -0
- package/esm/commands/subprocess/protocols/shell.d.ts +39 -0
- package/esm/commands/subprocess/protocols/shell.d.ts.map +1 -0
- package/esm/commands/subprocess/protocols/shell.js +155 -0
- package/esm/commands/subprocess/protocols/shell.js.map +1 -0
- package/esm/commands/subprocess/protocols/types.d.ts +50 -0
- package/esm/commands/subprocess/protocols/types.d.ts.map +1 -0
- package/esm/commands/subprocess/protocols/types.js +2 -0
- package/esm/commands/subprocess/protocols/types.js.map +1 -0
- package/esm/commands/subprocess/types.d.ts +9 -6
- package/esm/commands/subprocess/types.d.ts.map +1 -1
- package/esm/commands/sync/list.d.ts +25 -1
- package/esm/commands/sync/list.d.ts.map +1 -1
- package/esm/commands/sync/list.js +40 -5
- package/esm/commands/sync/list.js.map +1 -1
- package/esm/commands/sync/pull.d.ts.map +1 -1
- package/esm/commands/sync/pull.js +6 -3
- package/esm/commands/sync/pull.js.map +1 -1
- package/esm/commands/sync/request.d.ts +1 -0
- package/esm/commands/sync/request.d.ts.map +1 -1
- package/esm/commands/sync/request.js +1 -0
- package/esm/commands/sync/request.js.map +1 -1
- package/esm/commands/sync/response.d.ts +2 -1
- package/esm/commands/sync/response.d.ts.map +1 -1
- package/esm/commands/sync/response.js +2 -1
- package/esm/commands/sync/response.js.map +1 -1
- package/esm/commands/sync/stat.d.ts +13 -1
- package/esm/commands/sync/stat.d.ts.map +1 -1
- package/esm/commands/sync/stat.js +20 -7
- package/esm/commands/sync/stat.js.map +1 -1
- package/esm/commands/sync/sync.d.ts +13 -35
- package/esm/commands/sync/sync.d.ts.map +1 -1
- package/esm/commands/sync/sync.js +39 -9
- package/esm/commands/sync/sync.js.map +1 -1
- package/esm/crypto.d.ts +23 -10
- package/esm/crypto.d.ts.map +1 -1
- package/esm/crypto.js +64 -46
- package/esm/crypto.js.map +1 -1
- package/esm/features.d.ts +4 -2
- package/esm/features.d.ts.map +1 -1
- package/esm/features.js +6 -2
- package/esm/features.js.map +1 -1
- package/esm/packet.d.ts +21 -2
- package/esm/packet.d.ts.map +1 -1
- package/esm/packet.js +2 -2
- package/esm/packet.js.map +1 -1
- package/esm/socket/dispatcher.d.ts +34 -23
- package/esm/socket/dispatcher.d.ts.map +1 -1
- package/esm/socket/dispatcher.js +92 -70
- package/esm/socket/dispatcher.js.map +1 -1
- package/esm/socket/socket.d.ts +29 -8
- package/esm/socket/socket.d.ts.map +1 -1
- package/esm/socket/socket.js +41 -27
- package/esm/socket/socket.js.map +1 -1
- package/esm/stream/detect.d.ts +1 -17
- package/esm/stream/detect.d.ts.map +1 -1
- package/esm/stream/detect.js +4 -5
- package/esm/stream/detect.js.map +1 -1
- package/esm/stream/detect.polyfill.d.ts.map +1 -1
- package/esm/stream/detect.polyfill.js +4 -1
- package/esm/stream/detect.polyfill.js.map +1 -1
- package/esm/stream/transform.d.ts +37 -32
- package/esm/stream/transform.d.ts.map +1 -1
- package/esm/stream/transform.js +106 -92
- package/esm/stream/transform.js.map +1 -1
- package/esm/utils/encoding.d.ts.map +1 -1
- package/esm/utils/encoding.js.map +1 -1
- package/esm/utils/index.d.ts +1 -1
- package/esm/utils/index.d.ts.map +1 -1
- package/esm/utils/index.js +1 -1
- package/esm/utils/index.js.map +1 -1
- package/package.json +6 -6
- package/src/adb.ts +105 -76
- package/src/auth.ts +21 -18
- package/src/backend.ts +2 -2
- package/src/commands/install.ts +7 -9
- package/src/commands/reverse.ts +13 -24
- package/src/commands/subprocess/index.ts +57 -42
- package/src/commands/subprocess/protocols/index.ts +3 -0
- package/src/commands/subprocess/{legacy.ts → protocols/none.ts} +20 -10
- package/src/commands/subprocess/{protocol.ts → protocols/shell.ts} +22 -17
- package/src/commands/subprocess/{types.ts → protocols/types.ts} +13 -9
- package/src/commands/sync/list.ts +51 -5
- package/src/commands/sync/pull.ts +6 -3
- package/src/commands/sync/request.ts +1 -0
- package/src/commands/sync/response.ts +8 -2
- package/src/commands/sync/stat.ts +36 -9
- package/src/commands/sync/sync.ts +55 -19
- package/src/crypto.ts +72 -56
- package/src/features.ts +6 -2
- package/src/packet.ts +17 -6
- package/src/socket/dispatcher.ts +133 -88
- package/src/socket/socket.ts +61 -48
- package/src/stream/detect.polyfill.ts +4 -1
- package/src/stream/detect.ts +4 -24
- package/src/stream/transform.ts +133 -116
- package/src/utils/index.ts +1 -1
- package/tsconfig.build.tsbuildinfo +1 -1
- package/src/utils/encoding.ts +0 -13
package/src/crypto.ts
CHANGED
|
@@ -5,48 +5,60 @@ const BigInt1 = BigInt(1);
|
|
|
5
5
|
const BigInt2 = BigInt(2);
|
|
6
6
|
const BigInt64 = BigInt(64);
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Gets the `BigInt` value at the specified byte offset and length from the start of the view. There is
|
|
10
|
+
* no alignment constraint; multi-byte values may be fetched from any offset.
|
|
11
|
+
*
|
|
12
|
+
* Only supports Big-Endian, because that's what ADB uses.
|
|
13
|
+
* @param byteOffset The place in the buffer at which the value should be retrieved.
|
|
14
|
+
*/
|
|
15
|
+
export function getBigUint(dataView: DataView, byteOffset: number, length: number): bigint {
|
|
15
16
|
let result = BigInt0;
|
|
16
17
|
|
|
17
18
|
// Currently `length` must be a multiplication of 8
|
|
18
19
|
// Support for arbitrary length can be easily added
|
|
19
20
|
|
|
20
|
-
for (let i =
|
|
21
|
+
for (let i = byteOffset; i < byteOffset + length; i += 8) {
|
|
21
22
|
result <<= BigInt64;
|
|
22
|
-
const value = getBigUint64(
|
|
23
|
+
const value = getBigUint64(dataView, i, false);
|
|
23
24
|
result += value;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
return result;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Stores an arbitrary-precision positive `BigInt` value at the specified byte offset from the start of the view.
|
|
32
|
+
* @param byteOffset The place in the buffer at which the value should be set.
|
|
33
|
+
* @param value The value to set.
|
|
34
|
+
* @param littleEndian If `false` or `undefined`, a big-endian value should be written,
|
|
35
|
+
* otherwise a little-endian value should be written.
|
|
36
|
+
*/
|
|
37
|
+
export function setBigUint(dataView: DataView, byteOffset: number, value: bigint, littleEndian?: boolean) {
|
|
38
|
+
const start = byteOffset;
|
|
39
|
+
|
|
40
|
+
if (littleEndian) {
|
|
41
|
+
while (value > BigInt0) {
|
|
42
|
+
setBigUint64(dataView, byteOffset, value, true);
|
|
43
|
+
byteOffset += 8;
|
|
44
|
+
value >>= BigInt64;
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
// Because we don't know how long (in bits) the `value` is,
|
|
48
|
+
// Convert it to an array of `uint64` first.
|
|
49
|
+
const uint64Array: bigint[] = [];
|
|
50
|
+
while (value > BigInt0) {
|
|
51
|
+
uint64Array.push(BigInt.asUintN(64, value));
|
|
52
|
+
value >>= BigInt64;
|
|
53
|
+
}
|
|
35
54
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
55
|
+
for (let i = uint64Array.length - 1; i >= 0; i -= 1) {
|
|
56
|
+
setBigUint64(dataView, byteOffset, uint64Array[i]!, false);
|
|
57
|
+
byteOffset += 8;
|
|
58
|
+
}
|
|
40
59
|
}
|
|
41
|
-
}
|
|
42
60
|
|
|
43
|
-
|
|
44
|
-
const view = new DataView(array.buffer, array.byteOffset, array.byteLength);
|
|
45
|
-
while (value > BigInt0) {
|
|
46
|
-
setBigUint64(view, offset, value, true);
|
|
47
|
-
offset += 8;
|
|
48
|
-
value >>= BigInt64;
|
|
49
|
-
}
|
|
61
|
+
return byteOffset - start;
|
|
50
62
|
}
|
|
51
63
|
|
|
52
64
|
// These values are correct only if
|
|
@@ -76,9 +88,9 @@ const RsaPrivateKeyDOffset = 303;
|
|
|
76
88
|
const RsaPrivateKeyDLength = 2048 / 8;
|
|
77
89
|
|
|
78
90
|
export function parsePrivateKey(key: Uint8Array): [n: bigint, d: bigint] {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
const view = new DataView(key.buffer, key.byteOffset, key.byteLength);
|
|
92
|
+
const n = getBigUint(view, RsaPrivateKeyNOffset, RsaPrivateKeyNLength);
|
|
93
|
+
const d = getBigUint(view, RsaPrivateKeyDOffset, RsaPrivateKeyDLength);
|
|
82
94
|
return [n, d];
|
|
83
95
|
}
|
|
84
96
|
|
|
@@ -132,7 +144,7 @@ export function calculatePublicKey(
|
|
|
132
144
|
// [
|
|
133
145
|
// modulusLengthInWords, // 32-bit integer, a "word" is 32-bit so it must be 2048 / 8 / 4
|
|
134
146
|
// // Actually the comment in Android source code was wrong
|
|
135
|
-
// n0inv, // 32-bit integer, the modular inverse of (
|
|
147
|
+
// n0inv, // 32-bit integer, the modular inverse of (low 32 bits of n)
|
|
136
148
|
// modulus, // n
|
|
137
149
|
// rr, // Montgomery parameter R^2
|
|
138
150
|
// exponent, // 32-bit integer, must be 65537
|
|
@@ -172,13 +184,12 @@ export function calculatePublicKey(
|
|
|
172
184
|
outputOffset += 4;
|
|
173
185
|
|
|
174
186
|
// Write n
|
|
175
|
-
|
|
187
|
+
setBigUint(outputView, outputOffset, n, true);
|
|
176
188
|
outputOffset += 256;
|
|
177
189
|
|
|
178
190
|
// Calculate rr = (2^(rsa_size)) ^ 2 mod n
|
|
179
191
|
let rr = BigInt(2) ** BigInt(4096) % n;
|
|
180
|
-
|
|
181
|
-
outputOffset += 256;
|
|
192
|
+
outputOffset += setBigUint(outputView, outputOffset, rr, true);
|
|
182
193
|
|
|
183
194
|
// exponent
|
|
184
195
|
outputView.setUint32(outputOffset, 65537, true);
|
|
@@ -218,21 +229,21 @@ export function powMod(base: bigint, exponent: bigint, modulus: bigint): bigint
|
|
|
218
229
|
return r;
|
|
219
230
|
}
|
|
220
231
|
|
|
221
|
-
export const
|
|
232
|
+
export const SHA1_DIGEST_LENGTH = 20;
|
|
222
233
|
|
|
223
|
-
export const
|
|
224
|
-
export const
|
|
225
|
-
export const
|
|
226
|
-
export const
|
|
234
|
+
export const ASN1_SEQUENCE = 0x30;
|
|
235
|
+
export const ASN1_OCTET_STRING = 0x04;
|
|
236
|
+
export const ASN1_NULL = 0x05;
|
|
237
|
+
export const ASN1_OID = 0x06;
|
|
227
238
|
|
|
228
239
|
// PKCS#1 SHA-1 hash digest info
|
|
229
|
-
export const
|
|
230
|
-
|
|
231
|
-
|
|
240
|
+
export const SHA1_DIGEST_INFO = new Uint8Array([
|
|
241
|
+
ASN1_SEQUENCE, 0x0d + SHA1_DIGEST_LENGTH,
|
|
242
|
+
ASN1_SEQUENCE, 0x09,
|
|
232
243
|
// SHA-1 (1 3 14 3 2 26)
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
244
|
+
ASN1_OID, 0x05, 1 * 40 + 3, 14, 3, 2, 26,
|
|
245
|
+
ASN1_NULL, 0x00,
|
|
246
|
+
ASN1_OCTET_STRING, SHA1_DIGEST_LENGTH
|
|
236
247
|
]);
|
|
237
248
|
|
|
238
249
|
// SubtleCrypto.sign() will hash the given data and sign the hash
|
|
@@ -241,7 +252,7 @@ export const Sha1DigestInfo = new Uint8Array([
|
|
|
241
252
|
// encrypt the given data with its private key)
|
|
242
253
|
// However SubtileCrypto.encrypt() doesn't accept 'RSASSA-PKCS1-v1_5' algorithm
|
|
243
254
|
// So we need to implement the encryption by ourself
|
|
244
|
-
export function sign(privateKey: Uint8Array, data: Uint8Array):
|
|
255
|
+
export function sign(privateKey: Uint8Array, data: Uint8Array): Uint8Array {
|
|
245
256
|
const [n, d] = parsePrivateKey(privateKey);
|
|
246
257
|
|
|
247
258
|
// PKCS#1 padding
|
|
@@ -254,7 +265,7 @@ export function sign(privateKey: Uint8Array, data: Uint8Array): ArrayBuffer {
|
|
|
254
265
|
padded[index] = 1;
|
|
255
266
|
index += 1;
|
|
256
267
|
|
|
257
|
-
const fillLength = padded.length -
|
|
268
|
+
const fillLength = padded.length - SHA1_DIGEST_INFO.length - data.length - 1;
|
|
258
269
|
while (index < fillLength) {
|
|
259
270
|
padded[index] = 0xff;
|
|
260
271
|
index += 1;
|
|
@@ -263,18 +274,23 @@ export function sign(privateKey: Uint8Array, data: Uint8Array): ArrayBuffer {
|
|
|
263
274
|
padded[index] = 0;
|
|
264
275
|
index += 1;
|
|
265
276
|
|
|
266
|
-
padded.set(
|
|
267
|
-
index +=
|
|
277
|
+
padded.set(SHA1_DIGEST_INFO, index);
|
|
278
|
+
index += SHA1_DIGEST_INFO.length;
|
|
268
279
|
|
|
269
280
|
padded.set(data, index);
|
|
270
281
|
|
|
271
282
|
// Encryption
|
|
272
283
|
// signature = padded ** d % n
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
284
|
+
const view = new DataView(padded.buffer);
|
|
285
|
+
const signature = powMod(
|
|
286
|
+
getBigUint(view, 0, view.byteLength),
|
|
287
|
+
d,
|
|
288
|
+
n
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// `padded` is not used anymore,
|
|
292
|
+
// re-use the buffer to store the result
|
|
293
|
+
setBigUint(view, 0, signature, false);
|
|
294
|
+
|
|
295
|
+
return padded;
|
|
280
296
|
}
|
package/src/features.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
// The order follows
|
|
2
|
+
// https://android.googlesource.com/platform/packages/modules/adb/+/79010dc6d5ca7490c493df800d4421730f5466ca/transport.cpp#1252
|
|
1
3
|
export enum AdbFeatures {
|
|
2
|
-
|
|
4
|
+
ShellV2 = "shell_v2",
|
|
3
5
|
Cmd = 'cmd',
|
|
4
|
-
|
|
6
|
+
StatV2 = 'stat_v2',
|
|
7
|
+
ListV2 = 'ls_v2',
|
|
8
|
+
FixedPushMkdir = 'fixed_push_mkdir',
|
|
5
9
|
}
|
package/src/packet.ts
CHANGED
|
@@ -10,7 +10,7 @@ export enum AdbCommand {
|
|
|
10
10
|
Write = 0x45545257, // 'WRTE'
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
const AdbPacketHeader =
|
|
13
|
+
export const AdbPacketHeader =
|
|
14
14
|
new Struct({ littleEndian: true })
|
|
15
15
|
.uint32('command')
|
|
16
16
|
.uint32('arg0')
|
|
@@ -19,6 +19,8 @@ const AdbPacketHeader =
|
|
|
19
19
|
.uint32('checksum')
|
|
20
20
|
.int32('magic');
|
|
21
21
|
|
|
22
|
+
export type AdbPacketHeader = typeof AdbPacketHeader['TDeserializeResult'];
|
|
23
|
+
|
|
22
24
|
type AdbPacketHeaderInit = typeof AdbPacketHeader['TInit'];
|
|
23
25
|
|
|
24
26
|
export const AdbPacket =
|
|
@@ -28,15 +30,24 @@ export const AdbPacket =
|
|
|
28
30
|
|
|
29
31
|
export type AdbPacket = typeof AdbPacket['TDeserializeResult'];
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
/**
|
|
34
|
+
* `AdbPacketData` contains all the useful fields of `AdbPacket`.
|
|
35
|
+
*
|
|
36
|
+
* `AdbBackend#connect` will return a `ReadableStream<AdbPacketData>`,
|
|
37
|
+
* so each backend can encode `AdbPacket` in different ways.
|
|
38
|
+
*
|
|
39
|
+
* `AdbBackend#connect` will return a `WritableStream<AdbPacketInit>`,
|
|
40
|
+
* however, `AdbPacketDispatcher` will transform `AdbPacketData` to `AdbPacketInit` for you,
|
|
41
|
+
* so `AdbSocket#writable#write` only needs `AdbPacketData`.
|
|
42
|
+
*/
|
|
43
|
+
export type AdbPacketData = Omit<typeof AdbPacket['TInit'], 'checksum' | 'magic'>;
|
|
33
44
|
|
|
34
45
|
// All fields except `magic`, which can be calculated in `AdbPacketSerializeStream`
|
|
35
46
|
export type AdbPacketInit = Omit<typeof AdbPacket['TInit'], 'magic'>;
|
|
36
47
|
|
|
37
48
|
export function calculateChecksum(payload: Uint8Array): number;
|
|
38
|
-
export function calculateChecksum(init:
|
|
39
|
-
export function calculateChecksum(payload: Uint8Array |
|
|
49
|
+
export function calculateChecksum(init: AdbPacketData): AdbPacketInit;
|
|
50
|
+
export function calculateChecksum(payload: Uint8Array | AdbPacketData): number | AdbPacketInit {
|
|
40
51
|
if (payload instanceof Uint8Array) {
|
|
41
52
|
return payload.reduce((result, item) => result + item, 0);
|
|
42
53
|
} else {
|
|
@@ -49,7 +60,7 @@ export class AdbPacketSerializeStream extends TransformStream<AdbPacketInit, Uin
|
|
|
49
60
|
public constructor() {
|
|
50
61
|
super({
|
|
51
62
|
transform: async (init, controller) => {
|
|
52
|
-
// This syntax is ugly, but I don't want to create
|
|
63
|
+
// This syntax is ugly, but I don't want to create a new object.
|
|
53
64
|
(init as unknown as AdbPacketHeaderInit).magic = init.command ^ 0xFFFFFFFF;
|
|
54
65
|
(init as unknown as AdbPacketHeaderInit).payloadLength = init.payload.byteLength;
|
|
55
66
|
|
package/src/socket/dispatcher.ts
CHANGED
|
@@ -1,97 +1,113 @@
|
|
|
1
1
|
import { AsyncOperationManager, PromiseResolver } from '@yume-chan/async';
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
2
|
+
import type { RemoveEventListener } from '@yume-chan/event';
|
|
3
|
+
import type { ValueOrPromise } from "@yume-chan/struct";
|
|
4
|
+
|
|
5
|
+
import { AdbCommand, calculateChecksum, type AdbPacketData, type AdbPacketInit } from '../packet.js';
|
|
4
6
|
import { AbortController, WritableStream, WritableStreamDefaultWriter, type ReadableWritablePair } from '../stream/index.js';
|
|
5
7
|
import { decodeUtf8, encodeUtf8 } from '../utils/index.js';
|
|
6
|
-
import { AdbSocket } from './socket.js';
|
|
8
|
+
import { AdbSocket, AdbSocketController } from './socket.js';
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
handled: boolean;
|
|
10
|
+
const EmptyUint8Array = new Uint8Array(0);
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
export interface AdbPacketDispatcherOptions {
|
|
13
|
+
calculateChecksum: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Before Android 9.0, ADB uses `char*` to parse service string,
|
|
16
|
+
* thus requires a null character to terminate.
|
|
17
|
+
*
|
|
18
|
+
* Usually it should have the same value as `calculateChecksum`.
|
|
19
|
+
*/
|
|
20
|
+
appendNullToServiceString: boolean;
|
|
21
|
+
maxPayloadSize: number;
|
|
22
|
+
}
|
|
12
23
|
|
|
13
|
-
|
|
24
|
+
export type AdbIncomingSocketHandler = (socket: AdbSocket) => ValueOrPromise<boolean>;
|
|
14
25
|
|
|
15
|
-
|
|
26
|
+
export interface Closeable {
|
|
27
|
+
close(): ValueOrPromise<void>;
|
|
16
28
|
}
|
|
17
29
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
30
|
+
/**
|
|
31
|
+
* The dispatcher is the "dumb" part of the connection handling logic.
|
|
32
|
+
*
|
|
33
|
+
* Except some options to change some minor behaviors,
|
|
34
|
+
* its only job is forwarding packets between authenticated underlying streams
|
|
35
|
+
* and abstracted socket objects.
|
|
36
|
+
*
|
|
37
|
+
* The `Adb` class is responsible for doing the authentication,
|
|
38
|
+
* negotiating the options, and has shortcuts to high-level services.
|
|
39
|
+
*/
|
|
40
|
+
export class AdbPacketDispatcher implements Closeable {
|
|
21
41
|
// ADB socket id starts from 1
|
|
22
42
|
// (0 means open failed)
|
|
23
43
|
private readonly initializers = new AsyncOperationManager(1);
|
|
24
|
-
private readonly sockets = new Map<number,
|
|
44
|
+
private readonly sockets = new Map<number, AdbSocketController>();
|
|
25
45
|
|
|
26
46
|
private _writer!: WritableStreamDefaultWriter<AdbPacketInit>;
|
|
27
47
|
|
|
28
|
-
public
|
|
29
|
-
public calculateChecksum = true;
|
|
30
|
-
public appendNullToServiceString = true;
|
|
48
|
+
public readonly options: AdbPacketDispatcherOptions;
|
|
31
49
|
|
|
32
50
|
private _disconnected = new PromiseResolver<void>();
|
|
33
51
|
public get disconnected() { return this._disconnected.promise; }
|
|
34
52
|
|
|
35
|
-
private
|
|
36
|
-
public get onIncomingSocket() { return this.incomingSocketEvent.event; }
|
|
37
|
-
|
|
38
|
-
private readonly errorEvent = this.addDisposable(new EventEmitter<Error>());
|
|
39
|
-
public get onError() { return this.errorEvent.event; }
|
|
53
|
+
private _incomingSocketHandlers: Set<AdbIncomingSocketHandler> = new Set();
|
|
40
54
|
|
|
41
55
|
private _abortController = new AbortController();
|
|
42
56
|
|
|
43
57
|
public constructor(
|
|
44
|
-
connection: ReadableWritablePair<
|
|
58
|
+
connection: ReadableWritablePair<AdbPacketData, AdbPacketInit>,
|
|
59
|
+
options: AdbPacketDispatcherOptions
|
|
45
60
|
) {
|
|
46
|
-
|
|
61
|
+
this.options = options;
|
|
47
62
|
|
|
48
63
|
connection.readable
|
|
49
64
|
.pipeTo(new WritableStream({
|
|
50
65
|
write: async (packet) => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// Throw error here will stop the pipe
|
|
76
|
-
// But won't close `readable` because of `preventCancel: true`
|
|
77
|
-
throw e;
|
|
66
|
+
switch (packet.command) {
|
|
67
|
+
case AdbCommand.OK:
|
|
68
|
+
this.handleOk(packet);
|
|
69
|
+
break;
|
|
70
|
+
case AdbCommand.Close:
|
|
71
|
+
await this.handleClose(packet);
|
|
72
|
+
break;
|
|
73
|
+
case AdbCommand.Write:
|
|
74
|
+
if (this.sockets.has(packet.arg1)) {
|
|
75
|
+
await this.sockets.get(packet.arg1)!.enqueue(packet.payload);
|
|
76
|
+
await this.sendPacket(AdbCommand.OK, packet.arg1, packet.arg0);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
throw new Error(`Unknown local socket id: ${packet.arg1}`);
|
|
80
|
+
case AdbCommand.Open:
|
|
81
|
+
await this.handleOpen(packet);
|
|
82
|
+
break;
|
|
83
|
+
default:
|
|
84
|
+
// Junk data may only appear in the authentication phase,
|
|
85
|
+
// since the dispatcher only works after authentication,
|
|
86
|
+
// all packets should have a valid command.
|
|
87
|
+
// (although it's possible that Adb added new commands in the future)
|
|
88
|
+
throw new Error(`Unknown command: ${packet.command.toString(16)}`);
|
|
78
89
|
}
|
|
79
90
|
},
|
|
80
91
|
}), {
|
|
92
|
+
// There are multiple reasons for the pipe to stop,
|
|
93
|
+
// including device disconnection, protocol error, or user abort,
|
|
94
|
+
// if the underlying streams are still open,
|
|
95
|
+
// it's still possible to create another ADB connection.
|
|
96
|
+
// So don't close `readable` here.
|
|
81
97
|
preventCancel: false,
|
|
82
98
|
signal: this._abortController.signal,
|
|
83
99
|
})
|
|
84
100
|
.then(() => {
|
|
85
101
|
this.dispose();
|
|
86
|
-
}, () => {
|
|
87
|
-
|
|
102
|
+
}, (e) => {
|
|
103
|
+
this._disconnected.reject(e);
|
|
88
104
|
this.dispose();
|
|
89
105
|
});
|
|
90
106
|
|
|
91
107
|
this._writer = connection.writable.getWriter();
|
|
92
108
|
}
|
|
93
109
|
|
|
94
|
-
private handleOk(packet:
|
|
110
|
+
private handleOk(packet: AdbPacketData) {
|
|
95
111
|
if (this.initializers.resolve(packet.arg1, packet.arg0)) {
|
|
96
112
|
// Device successfully created the socket
|
|
97
113
|
return;
|
|
@@ -109,7 +125,7 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
|
|
109
125
|
this.sendPacket(AdbCommand.Close, packet.arg1, packet.arg0);
|
|
110
126
|
}
|
|
111
127
|
|
|
112
|
-
private async handleClose(packet:
|
|
128
|
+
private async handleClose(packet: AdbPacketData) {
|
|
113
129
|
// From https://android.googlesource.com/platform/packages/modules/adb/+/65d18e2c1cc48b585811954892311b28a4c3d188/adb.cpp#459
|
|
114
130
|
/* According to protocol.txt, p->msg.arg0 might be 0 to indicate
|
|
115
131
|
* a failed OPEN only. However, due to a bug in previous ADB
|
|
@@ -117,13 +133,18 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
|
|
117
133
|
* CLOSE() operations.
|
|
118
134
|
*/
|
|
119
135
|
|
|
120
|
-
//
|
|
136
|
+
// If the socket is still pending
|
|
121
137
|
if (packet.arg0 === 0 &&
|
|
122
138
|
this.initializers.reject(packet.arg1, new Error('Socket open failed'))) {
|
|
123
139
|
// Device failed to create the socket
|
|
140
|
+
// (unknown service string, failed to execute command, etc.)
|
|
141
|
+
// it doesn't break the connection,
|
|
142
|
+
// so only reject the socket creation promise,
|
|
143
|
+
// don't throw an error here.
|
|
124
144
|
return;
|
|
125
145
|
}
|
|
126
146
|
|
|
147
|
+
// Ignore `arg0` and search for the socket
|
|
127
148
|
const socket = this.sockets.get(packet.arg1);
|
|
128
149
|
if (socket) {
|
|
129
150
|
// The device want to close the socket
|
|
@@ -135,11 +156,21 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
|
|
135
156
|
return;
|
|
136
157
|
}
|
|
137
158
|
|
|
138
|
-
//
|
|
139
|
-
//
|
|
159
|
+
// TODO: adb: is double closing an socket a catastrophic error?
|
|
160
|
+
// If the client sends two `CLSE` packets for one socket,
|
|
161
|
+
// the device may also respond with two `CLSE` packets.
|
|
140
162
|
}
|
|
141
163
|
|
|
142
|
-
|
|
164
|
+
public addIncomingSocketHandler(handler: AdbIncomingSocketHandler): RemoveEventListener {
|
|
165
|
+
this._incomingSocketHandlers.add(handler);
|
|
166
|
+
const remove = () => {
|
|
167
|
+
this._incomingSocketHandlers.delete(handler);
|
|
168
|
+
};
|
|
169
|
+
remove.dispose = remove;
|
|
170
|
+
return remove;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private async handleOpen(packet: AdbPacketData) {
|
|
143
174
|
// AsyncOperationManager doesn't support get and skip an ID
|
|
144
175
|
// Use `add` + `resolve` to simulate this behavior
|
|
145
176
|
const [localId] = this.initializers.add<number>();
|
|
@@ -148,7 +179,7 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
|
|
148
179
|
const remoteId = packet.arg0;
|
|
149
180
|
const serviceString = decodeUtf8(packet.payload);
|
|
150
181
|
|
|
151
|
-
const
|
|
182
|
+
const controller = new AdbSocketController({
|
|
152
183
|
dispatcher: this,
|
|
153
184
|
localId,
|
|
154
185
|
remoteId,
|
|
@@ -156,41 +187,42 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
|
|
156
187
|
serviceString,
|
|
157
188
|
});
|
|
158
189
|
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
this.incomingSocketEvent.fire(args);
|
|
166
|
-
|
|
167
|
-
if (args.handled) {
|
|
168
|
-
this.sockets.set(localId, socket);
|
|
169
|
-
await this.sendPacket(AdbCommand.OK, localId, remoteId);
|
|
170
|
-
} else {
|
|
171
|
-
await this.sendPacket(AdbCommand.Close, 0, remoteId);
|
|
190
|
+
for (const handler of this._incomingSocketHandlers) {
|
|
191
|
+
if (await handler(controller.socket)) {
|
|
192
|
+
this.sockets.set(localId, controller);
|
|
193
|
+
await this.sendPacket(AdbCommand.OK, localId, remoteId);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
172
196
|
}
|
|
197
|
+
|
|
198
|
+
await this.sendPacket(AdbCommand.Close, 0, remoteId);
|
|
173
199
|
}
|
|
174
200
|
|
|
175
201
|
public async createSocket(serviceString: string): Promise<AdbSocket> {
|
|
176
|
-
if (this.appendNullToServiceString) {
|
|
202
|
+
if (this.options.appendNullToServiceString) {
|
|
177
203
|
serviceString += '\0';
|
|
178
204
|
}
|
|
179
205
|
|
|
180
206
|
const [localId, initializer] = this.initializers.add<number>();
|
|
181
|
-
await this.sendPacket(
|
|
207
|
+
await this.sendPacket(
|
|
208
|
+
AdbCommand.Open,
|
|
209
|
+
localId,
|
|
210
|
+
0,
|
|
211
|
+
serviceString
|
|
212
|
+
);
|
|
182
213
|
|
|
214
|
+
// Fulfilled by `handleOk`
|
|
183
215
|
const remoteId = await initializer;
|
|
184
|
-
const
|
|
216
|
+
const controller = new AdbSocketController({
|
|
185
217
|
dispatcher: this,
|
|
186
218
|
localId,
|
|
187
219
|
remoteId,
|
|
188
220
|
localCreated: true,
|
|
189
221
|
serviceString,
|
|
190
222
|
});
|
|
191
|
-
this.sockets.set(localId,
|
|
223
|
+
this.sockets.set(localId, controller);
|
|
192
224
|
|
|
193
|
-
return socket;
|
|
225
|
+
return controller.socket;
|
|
194
226
|
}
|
|
195
227
|
|
|
196
228
|
public sendPacket(packet: AdbPacketInit): Promise<void>;
|
|
@@ -206,7 +238,7 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
|
|
206
238
|
arg1?: number,
|
|
207
239
|
payload: string | Uint8Array = EmptyUint8Array,
|
|
208
240
|
): Promise<void> {
|
|
209
|
-
let init:
|
|
241
|
+
let init: AdbPacketData;
|
|
210
242
|
if (arg0 === undefined) {
|
|
211
243
|
init = packetOrCommand as AdbPacketInit;
|
|
212
244
|
} else {
|
|
@@ -223,11 +255,11 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
|
|
223
255
|
}
|
|
224
256
|
|
|
225
257
|
if (init.payload &&
|
|
226
|
-
init.payload.byteLength > this.maxPayloadSize) {
|
|
258
|
+
init.payload.byteLength > this.options.maxPayloadSize) {
|
|
227
259
|
throw new Error('payload too large');
|
|
228
260
|
}
|
|
229
261
|
|
|
230
|
-
if (this.calculateChecksum) {
|
|
262
|
+
if (this.options.calculateChecksum) {
|
|
231
263
|
calculateChecksum(init);
|
|
232
264
|
} else {
|
|
233
265
|
(init as AdbPacketInit).checksum = 0;
|
|
@@ -236,21 +268,34 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
|
|
236
268
|
await this._writer.write(init as AdbPacketInit);
|
|
237
269
|
}
|
|
238
270
|
|
|
239
|
-
public
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
271
|
+
public async close() {
|
|
272
|
+
// Send `CLSE` packets for all sockets
|
|
273
|
+
await Promise.all(
|
|
274
|
+
Array.from(
|
|
275
|
+
this.sockets.values(),
|
|
276
|
+
socket => socket.close(),
|
|
277
|
+
)
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
// Stop receiving
|
|
281
|
+
// It's possible that we haven't received all `CLSE` confirm packets,
|
|
282
|
+
// but it doesn't matter, the next connection can cope with them.
|
|
245
283
|
try {
|
|
246
|
-
// Stop pipes
|
|
247
284
|
this._abortController.abort();
|
|
248
285
|
} catch { }
|
|
249
286
|
|
|
287
|
+
// Adb connection doesn't have a method to confirm closing,
|
|
288
|
+
// so call `dispose` immediately
|
|
289
|
+
this.dispose();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private dispose() {
|
|
293
|
+
for (const socket of this.sockets.values()) {
|
|
294
|
+
socket.dispose();
|
|
295
|
+
}
|
|
296
|
+
|
|
250
297
|
this._writer.releaseLock();
|
|
251
298
|
|
|
252
299
|
this._disconnected.resolve();
|
|
253
|
-
|
|
254
|
-
super.dispose();
|
|
255
300
|
}
|
|
256
301
|
}
|