@yume-chan/adb 0.0.23 → 0.0.24
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 +33 -0
- package/CHANGELOG.md +15 -1
- package/LICENSE +1 -1
- package/esm/adb.d.ts +2 -2
- package/esm/adb.d.ts.map +1 -1
- package/esm/adb.js +2 -2
- package/esm/adb.js.map +1 -1
- package/esm/commands/power.d.ts.map +1 -1
- package/esm/commands/power.js +6 -5
- package/esm/commands/power.js.map +1 -1
- package/esm/commands/reverse.d.ts.map +1 -1
- package/esm/commands/reverse.js +22 -10
- package/esm/commands/reverse.js.map +1 -1
- package/esm/commands/subprocess/command.js +3 -3
- package/esm/commands/subprocess/command.js.map +1 -1
- package/esm/commands/subprocess/protocols/none.d.ts +2 -2
- package/esm/commands/subprocess/protocols/none.d.ts.map +1 -1
- package/esm/commands/subprocess/protocols/shell.d.ts +3 -3
- package/esm/commands/subprocess/protocols/shell.d.ts.map +1 -1
- package/esm/commands/subprocess/protocols/shell.js +5 -6
- package/esm/commands/subprocess/protocols/shell.js.map +1 -1
- package/esm/commands/subprocess/protocols/types.d.ts +2 -2
- package/esm/commands/subprocess/protocols/types.d.ts.map +1 -1
- package/esm/commands/sync/list.d.ts +2 -5
- package/esm/commands/sync/list.d.ts.map +1 -1
- package/esm/commands/sync/list.js +2 -4
- package/esm/commands/sync/list.js.map +1 -1
- package/esm/commands/sync/pull.d.ts +1 -4
- package/esm/commands/sync/pull.d.ts.map +1 -1
- package/esm/commands/sync/pull.js +2 -2
- package/esm/commands/sync/pull.js.map +1 -1
- package/esm/commands/sync/push.d.ts +4 -7
- package/esm/commands/sync/push.d.ts.map +1 -1
- package/esm/commands/sync/push.js +3 -6
- package/esm/commands/sync/push.js.map +1 -1
- package/esm/commands/sync/request.d.ts +13 -18
- package/esm/commands/sync/request.d.ts.map +1 -1
- package/esm/commands/sync/request.js +23 -28
- package/esm/commands/sync/request.js.map +1 -1
- package/esm/commands/sync/response.d.ts +18 -12
- package/esm/commands/sync/response.d.ts.map +1 -1
- package/esm/commands/sync/response.js +39 -17
- package/esm/commands/sync/response.js.map +1 -1
- package/esm/commands/sync/socket.d.ts +2 -2
- package/esm/commands/sync/socket.d.ts.map +1 -1
- package/esm/commands/sync/socket.js +10 -5
- package/esm/commands/sync/socket.js.map +1 -1
- package/esm/commands/sync/stat.d.ts +0 -3
- package/esm/commands/sync/stat.d.ts.map +1 -1
- package/esm/commands/sync/stat.js +0 -2
- package/esm/commands/sync/stat.js.map +1 -1
- package/esm/commands/sync/sync.d.ts +4 -3
- package/esm/commands/sync/sync.d.ts.map +1 -1
- package/esm/commands/sync/sync.js +4 -1
- package/esm/commands/sync/sync.js.map +1 -1
- package/esm/commands/tcpip.js +1 -1
- package/esm/commands/tcpip.js.map +1 -1
- package/esm/daemon/auth.js +1 -1
- package/esm/daemon/auth.js.map +1 -1
- package/esm/daemon/crypto.d.ts +3 -2
- package/esm/daemon/crypto.d.ts.map +1 -1
- package/esm/daemon/crypto.js +56 -49
- package/esm/daemon/crypto.js.map +1 -1
- package/esm/daemon/dispatcher.d.ts +35 -10
- package/esm/daemon/dispatcher.d.ts.map +1 -1
- package/esm/daemon/dispatcher.js +44 -31
- package/esm/daemon/dispatcher.js.map +1 -1
- package/esm/daemon/packet.d.ts +2 -2
- package/esm/daemon/packet.d.ts.map +1 -1
- package/esm/daemon/packet.js +13 -12
- package/esm/daemon/packet.js.map +1 -1
- package/esm/daemon/socket.d.ts +10 -5
- package/esm/daemon/socket.d.ts.map +1 -1
- package/esm/daemon/socket.js +17 -15
- package/esm/daemon/socket.js.map +1 -1
- package/esm/daemon/transport.d.ts +55 -15
- package/esm/daemon/transport.d.ts.map +1 -1
- package/esm/daemon/transport.js +9 -10
- package/esm/daemon/transport.js.map +1 -1
- package/esm/server/client.d.ts +128 -50
- package/esm/server/client.d.ts.map +1 -1
- package/esm/server/client.js +325 -124
- package/esm/server/client.js.map +1 -1
- package/esm/server/transport.js +4 -4
- package/esm/server/transport.js.map +1 -1
- package/esm/utils/base64.d.ts.map +1 -1
- package/esm/utils/base64.js +33 -22
- package/esm/utils/base64.js.map +1 -1
- package/esm/utils/hex.d.ts +1 -1
- package/esm/utils/hex.d.ts.map +1 -1
- package/esm/utils/hex.js +12 -14
- package/esm/utils/hex.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/esm/utils/sequence-equal.d.ts +2 -0
- package/esm/utils/sequence-equal.d.ts.map +1 -0
- package/esm/utils/sequence-equal.js +12 -0
- package/esm/utils/sequence-equal.js.map +1 -0
- package/package.json +12 -14
- package/src/adb.ts +7 -4
- package/src/commands/power.ts +7 -5
- package/src/commands/reverse.ts +30 -17
- package/src/commands/subprocess/command.ts +3 -3
- package/src/commands/subprocess/protocols/none.ts +2 -2
- package/src/commands/subprocess/protocols/shell.ts +7 -11
- package/src/commands/subprocess/protocols/types.ts +2 -2
- package/src/commands/sync/list.ts +2 -4
- package/src/commands/sync/pull.ts +2 -2
- package/src/commands/sync/push.ts +7 -10
- package/src/commands/sync/request.ts +32 -32
- package/src/commands/sync/response.ts +47 -20
- package/src/commands/sync/socket.ts +13 -8
- package/src/commands/sync/stat.ts +0 -2
- package/src/commands/sync/sync.ts +8 -3
- package/src/commands/tcpip.ts +1 -1
- package/src/daemon/auth.ts +1 -1
- package/src/daemon/crypto.ts +64 -55
- package/src/daemon/dispatcher.ts +104 -42
- package/src/daemon/packet.ts +21 -14
- package/src/daemon/socket.ts +28 -25
- package/src/daemon/transport.ts +71 -24
- package/src/server/client.ts +420 -208
- package/src/server/transport.ts +4 -4
- package/src/utils/base64.ts +33 -22
- package/src/utils/hex.ts +16 -14
- package/src/utils/index.ts +1 -1
- package/src/utils/sequence-equal.ts +13 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/esm/utils/conditional-variable.d.ts +0 -8
- package/esm/utils/conditional-variable.d.ts.map +0 -1
- package/esm/utils/conditional-variable.js +0 -34
- package/esm/utils/conditional-variable.js.map +0 -1
- package/src/utils/conditional-variable.ts +0 -45
package/src/daemon/crypto.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
getUint64BigEndian,
|
|
3
|
+
setInt64BigEndian,
|
|
4
|
+
setInt64LittleEndian,
|
|
5
|
+
} from "@yume-chan/no-data-view";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Gets the `BigInt` value at the specified byte offset and length from the start of the view. There is
|
|
@@ -11,7 +12,7 @@ import {
|
|
|
11
12
|
* @param byteOffset The place in the buffer at which the value should be retrieved.
|
|
12
13
|
*/
|
|
13
14
|
export function getBigUint(
|
|
14
|
-
|
|
15
|
+
array: Uint8Array,
|
|
15
16
|
byteOffset: number,
|
|
16
17
|
length: number,
|
|
17
18
|
): bigint {
|
|
@@ -22,8 +23,8 @@ export function getBigUint(
|
|
|
22
23
|
|
|
23
24
|
for (let i = byteOffset; i < byteOffset + length; i += 8) {
|
|
24
25
|
result <<= 64n;
|
|
25
|
-
const value =
|
|
26
|
-
result
|
|
26
|
+
const value = getUint64BigEndian(array, i);
|
|
27
|
+
result |= value;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
return result;
|
|
@@ -32,40 +33,32 @@ export function getBigUint(
|
|
|
32
33
|
/**
|
|
33
34
|
* Stores an arbitrary-precision positive `BigInt` value at the specified byte offset from the start of the view.
|
|
34
35
|
* @param byteOffset The place in the buffer at which the value should be set.
|
|
36
|
+
* @param length The number of bytes to set.
|
|
35
37
|
* @param value The value to set.
|
|
36
38
|
* @param littleEndian If `false` or `undefined`, a big-endian value should be written,
|
|
37
39
|
* otherwise a little-endian value should be written.
|
|
38
40
|
*/
|
|
39
41
|
export function setBigUint(
|
|
40
|
-
|
|
42
|
+
array: Uint8Array,
|
|
41
43
|
byteOffset: number,
|
|
44
|
+
length: number,
|
|
42
45
|
value: bigint,
|
|
43
46
|
littleEndian?: boolean,
|
|
44
47
|
) {
|
|
45
|
-
const start = byteOffset;
|
|
46
|
-
|
|
47
48
|
if (littleEndian) {
|
|
48
49
|
while (value > 0n) {
|
|
49
|
-
|
|
50
|
+
setInt64LittleEndian(array, byteOffset, value);
|
|
50
51
|
byteOffset += 8;
|
|
51
52
|
value >>= 64n;
|
|
52
53
|
}
|
|
53
54
|
} else {
|
|
54
|
-
|
|
55
|
-
// Convert it to an array of `uint64` first.
|
|
56
|
-
const uint64Array: bigint[] = [];
|
|
55
|
+
let position = byteOffset + length - 8;
|
|
57
56
|
while (value > 0n) {
|
|
58
|
-
|
|
57
|
+
setInt64BigEndian(array, position, value);
|
|
58
|
+
position -= 8;
|
|
59
59
|
value >>= 64n;
|
|
60
60
|
}
|
|
61
|
-
|
|
62
|
-
for (let i = uint64Array.length - 1; i >= 0; i -= 1) {
|
|
63
|
-
setBigUint64(dataView, byteOffset, uint64Array[i]!, false);
|
|
64
|
-
byteOffset += 8;
|
|
65
|
-
}
|
|
66
61
|
}
|
|
67
|
-
|
|
68
|
-
return byteOffset - start;
|
|
69
62
|
}
|
|
70
63
|
|
|
71
64
|
// These values are correct only if
|
|
@@ -95,17 +88,26 @@ const RsaPrivateKeyDOffset = 303;
|
|
|
95
88
|
const RsaPrivateKeyDLength = 2048 / 8;
|
|
96
89
|
|
|
97
90
|
export function rsaParsePrivateKey(key: Uint8Array): [n: bigint, d: bigint] {
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const d = getBigUint(view, RsaPrivateKeyDOffset, RsaPrivateKeyDLength);
|
|
91
|
+
const n = getBigUint(key, RsaPrivateKeyNOffset, RsaPrivateKeyNLength);
|
|
92
|
+
const d = getBigUint(key, RsaPrivateKeyDOffset, RsaPrivateKeyDLength);
|
|
101
93
|
return [n, d];
|
|
102
94
|
}
|
|
103
95
|
|
|
96
|
+
function nonNegativeMod(m: number, d: number) {
|
|
97
|
+
const r = m % d;
|
|
98
|
+
if (r > 0) {
|
|
99
|
+
return r;
|
|
100
|
+
}
|
|
101
|
+
return r + (d > 0 ? d : -d);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// https://en.wikipedia.org/wiki/Modular_multiplicative_inverse
|
|
105
|
+
// Solve for the smallest positive `x` in the equation `a * x ≡ 1 (mod m)`,
|
|
106
|
+
// or in other words, `a * x % m = 1`
|
|
104
107
|
// Taken from https://stackoverflow.com/a/51562038
|
|
105
|
-
// I can't understand, but it does work
|
|
106
108
|
// Only used with numbers smaller than 2^32 so doesn't need BigInt
|
|
107
109
|
export function modInverse(a: number, m: number) {
|
|
108
|
-
a = (
|
|
110
|
+
a = nonNegativeMod(a, m);
|
|
109
111
|
if (!a || m < 2) {
|
|
110
112
|
return NaN; // invalid input
|
|
111
113
|
}
|
|
@@ -116,6 +118,7 @@ export function modInverse(a: number, m: number) {
|
|
|
116
118
|
[a, b] = [b, a % b];
|
|
117
119
|
s.push({ a, b });
|
|
118
120
|
}
|
|
121
|
+
/* istanbul ignore next */
|
|
119
122
|
if (a !== 1) {
|
|
120
123
|
return NaN; // inverse does not exists
|
|
121
124
|
}
|
|
@@ -125,11 +128,14 @@ export function modInverse(a: number, m: number) {
|
|
|
125
128
|
for (let i = s.length - 2; i >= 0; i -= 1) {
|
|
126
129
|
[x, y] = [y, x - y * Math.floor(s[i]!.a / s[i]!.b)];
|
|
127
130
|
}
|
|
128
|
-
return (
|
|
131
|
+
return nonNegativeMod(y, m);
|
|
129
132
|
}
|
|
130
133
|
|
|
134
|
+
const ModulusLengthInBytes = 2048 / 8;
|
|
135
|
+
const ModulusLengthInWords = ModulusLengthInBytes / 4;
|
|
136
|
+
|
|
131
137
|
export function adbGetPublicKeySize() {
|
|
132
|
-
return 4 + 4 +
|
|
138
|
+
return 4 + 4 + ModulusLengthInBytes + ModulusLengthInBytes + 4;
|
|
133
139
|
}
|
|
134
140
|
|
|
135
141
|
export function adbGeneratePublicKey(privateKey: Uint8Array): Uint8Array;
|
|
@@ -141,22 +147,27 @@ export function adbGeneratePublicKey(
|
|
|
141
147
|
privateKey: Uint8Array,
|
|
142
148
|
output?: Uint8Array,
|
|
143
149
|
): Uint8Array | number {
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
//
|
|
150
|
+
// cspell: ignore: mincrypt
|
|
151
|
+
// Android 6 and earlier has its own encryption library called mincrypt
|
|
152
|
+
// This is the RSA public key format used by mincrypt:
|
|
153
|
+
// https://android.googlesource.com/platform/system/core/+/bb0c180e62703c2068a1b2c9f8ba6d634bf1553c/include/mincrypt/rsa.h#46
|
|
154
|
+
// `n0inv` and `rr` are pre-calculated to speed up RSA operations
|
|
155
|
+
|
|
156
|
+
// Android 7 switched its encryption library to BoringSSL, but still keeps the key format:
|
|
157
|
+
// https://android.googlesource.com/platform/system/core.git/+/91784040db2b9273687f88d8b95f729d4a61ecc2/libcrypto_utils/android_pubkey.cpp#38
|
|
158
|
+
// Except when reading a key, `n0inv` and `rr` are ignored (they are still populated when generating a key):
|
|
159
|
+
// https://android.googlesource.com/platform/system/core.git/+/91784040db2b9273687f88d8b95f729d4a61ecc2/libcrypto_utils/android_pubkey.cpp#55
|
|
160
|
+
|
|
161
|
+
// The public key is a struct (in little endian) of:
|
|
148
162
|
//
|
|
149
163
|
// [
|
|
150
164
|
// modulusLengthInWords, // 32-bit integer, a "word" is 32-bit so it must be 2048 / 8 / 4
|
|
151
|
-
// //
|
|
152
|
-
// n0inv, // 32-bit integer, the modular inverse of (
|
|
153
|
-
// modulus, // n
|
|
165
|
+
// // (the comment in Android source code is incorrect saying "This must be ANDROID_PUBKEY_MODULUS_SIZE")
|
|
166
|
+
// n0inv, // 32-bit integer, the modular inverse of (lower 32 bits of `n`)
|
|
167
|
+
// modulus, // `n`
|
|
154
168
|
// rr, // Montgomery parameter R^2
|
|
155
|
-
// exponent, // 32-bit integer, must be 65537
|
|
169
|
+
// exponent, // 32-bit integer, must be 3 or 65537
|
|
156
170
|
// ]
|
|
157
|
-
//
|
|
158
|
-
// (All in little endian)
|
|
159
|
-
// See https://android.googlesource.com/platform/system/core.git/+/91784040db2b9273687f88d8b95f729d4a61ecc2/libcrypto_utils/android_pubkey.cpp#38
|
|
160
171
|
|
|
161
172
|
let outputType: "Uint8Array" | "number";
|
|
162
173
|
const outputLength = adbGetPublicKeySize();
|
|
@@ -164,8 +175,8 @@ export function adbGeneratePublicKey(
|
|
|
164
175
|
output = new Uint8Array(outputLength);
|
|
165
176
|
outputType = "Uint8Array";
|
|
166
177
|
} else {
|
|
167
|
-
if (output.
|
|
168
|
-
throw new
|
|
178
|
+
if (output.length < outputLength) {
|
|
179
|
+
throw new TypeError("output buffer is too small");
|
|
169
180
|
}
|
|
170
181
|
|
|
171
182
|
outputType = "number";
|
|
@@ -174,31 +185,30 @@ export function adbGeneratePublicKey(
|
|
|
174
185
|
const outputView = new DataView(
|
|
175
186
|
output.buffer,
|
|
176
187
|
output.byteOffset,
|
|
177
|
-
output.
|
|
188
|
+
output.length,
|
|
178
189
|
);
|
|
179
190
|
let outputOffset = 0;
|
|
180
191
|
|
|
181
192
|
// modulusLengthInWords
|
|
182
|
-
outputView.setUint32(outputOffset,
|
|
193
|
+
outputView.setUint32(outputOffset, ModulusLengthInWords, true);
|
|
183
194
|
outputOffset += 4;
|
|
184
195
|
|
|
185
196
|
// extract `n` from private key
|
|
186
197
|
const [n] = rsaParsePrivateKey(privateKey);
|
|
187
198
|
|
|
188
199
|
// Calculate `n0inv`
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const n0inv = modInverse(-Number(BigInt.asUintN(32, n)), 2 ** 32);
|
|
192
|
-
outputView.setUint32(outputOffset, n0inv, true);
|
|
200
|
+
const n0inv = -modInverse(Number(n % 2n ** 32n), 2 ** 32);
|
|
201
|
+
outputView.setInt32(outputOffset, n0inv, true);
|
|
193
202
|
outputOffset += 4;
|
|
194
203
|
|
|
195
|
-
// Write n
|
|
196
|
-
setBigUint(
|
|
197
|
-
outputOffset +=
|
|
204
|
+
// Write `n` (a.k.a. `modulus`)
|
|
205
|
+
setBigUint(output, outputOffset, ModulusLengthInBytes, n, true);
|
|
206
|
+
outputOffset += ModulusLengthInBytes;
|
|
198
207
|
|
|
199
|
-
// Calculate rr = (2
|
|
208
|
+
// Calculate rr = (2 ** (rsa_size)) ** 2 % n
|
|
200
209
|
const rr = 2n ** 4096n % n;
|
|
201
|
-
|
|
210
|
+
setBigUint(output, outputOffset, ModulusLengthInBytes, rr, true);
|
|
211
|
+
outputOffset += ModulusLengthInBytes;
|
|
202
212
|
|
|
203
213
|
// exponent
|
|
204
214
|
outputView.setUint32(outputOffset, 65537, true);
|
|
@@ -305,12 +315,11 @@ export function rsaSign(privateKey: Uint8Array, data: Uint8Array): Uint8Array {
|
|
|
305
315
|
|
|
306
316
|
// Encryption
|
|
307
317
|
// signature = padded ** d % n
|
|
308
|
-
const
|
|
309
|
-
const signature = powMod(getBigUint(view, 0, view.byteLength), d, n);
|
|
318
|
+
const signature = powMod(getBigUint(padded, 0, padded.length), d, n);
|
|
310
319
|
|
|
311
320
|
// `padded` is not used anymore,
|
|
312
321
|
// re-use the buffer to store the result
|
|
313
|
-
setBigUint(
|
|
322
|
+
setBigUint(padded, 0, padded.length, signature, false);
|
|
314
323
|
|
|
315
324
|
return padded;
|
|
316
325
|
}
|
package/src/daemon/dispatcher.ts
CHANGED
|
@@ -3,47 +3,78 @@ import {
|
|
|
3
3
|
PromiseResolver,
|
|
4
4
|
delay,
|
|
5
5
|
} from "@yume-chan/async";
|
|
6
|
+
import {
|
|
7
|
+
getUint32LittleEndian,
|
|
8
|
+
setUint32LittleEndian,
|
|
9
|
+
} from "@yume-chan/no-data-view";
|
|
6
10
|
import type {
|
|
7
|
-
Consumable,
|
|
8
11
|
ReadableWritablePair,
|
|
9
12
|
WritableStreamDefaultWriter,
|
|
10
13
|
} from "@yume-chan/stream-extra";
|
|
11
14
|
import {
|
|
12
15
|
AbortController,
|
|
13
|
-
|
|
16
|
+
Consumable,
|
|
14
17
|
WritableStream,
|
|
15
18
|
} from "@yume-chan/stream-extra";
|
|
16
|
-
import { EMPTY_UINT8_ARRAY,
|
|
19
|
+
import { EMPTY_UINT8_ARRAY, decodeUtf8, encodeUtf8 } from "@yume-chan/struct";
|
|
17
20
|
|
|
18
21
|
import type { AdbIncomingSocketHandler, AdbSocket, Closeable } from "../adb.js";
|
|
19
|
-
import { decodeUtf8, encodeUtf8 } from "../utils/index.js";
|
|
20
22
|
|
|
21
23
|
import type { AdbPacketData, AdbPacketInit } from "./packet.js";
|
|
22
24
|
import { AdbCommand, calculateChecksum } from "./packet.js";
|
|
23
25
|
import { AdbDaemonSocketController } from "./socket.js";
|
|
24
26
|
|
|
25
27
|
export interface AdbPacketDispatcherOptions {
|
|
28
|
+
/**
|
|
29
|
+
* From Android 9.0, ADB stopped checking the checksum in packet header to improve performance.
|
|
30
|
+
*
|
|
31
|
+
* The value should be inferred from the device's ADB protocol version.
|
|
32
|
+
*/
|
|
26
33
|
calculateChecksum: boolean;
|
|
34
|
+
|
|
27
35
|
/**
|
|
28
|
-
* Before Android 9.0, ADB uses `char*` to parse service
|
|
36
|
+
* Before Android 9.0, ADB uses `char*` to parse service strings,
|
|
29
37
|
* thus requires a null character to terminate.
|
|
30
38
|
*
|
|
31
|
-
*
|
|
39
|
+
* The value should be inferred from the device's ADB protocol version.
|
|
40
|
+
* Usually it should have the same value as `calculateChecksum`, since they both changed
|
|
41
|
+
* in Android 9.0.
|
|
32
42
|
*/
|
|
33
43
|
appendNullToServiceString: boolean;
|
|
44
|
+
|
|
34
45
|
maxPayloadSize: number;
|
|
46
|
+
|
|
35
47
|
/**
|
|
36
|
-
*
|
|
48
|
+
* Whether to keep the `connection` open (don't call `writable.close` and `readable.cancel`)
|
|
49
|
+
* when `AdbPacketDispatcher.close` is called.
|
|
50
|
+
*
|
|
51
|
+
* @default false
|
|
52
|
+
*/
|
|
53
|
+
preserveConnection?: boolean | undefined;
|
|
37
54
|
|
|
38
|
-
|
|
39
|
-
*
|
|
55
|
+
/**
|
|
56
|
+
* The number of bytes the device can send before receiving an ack packet.
|
|
57
|
+
* Using delayed ack can improve the throughput,
|
|
58
|
+
* especially when the device is connected over Wi-Fi (so the latency is higher).
|
|
59
|
+
*
|
|
60
|
+
* This must be the negotiated value between the client and device. If the device enabled
|
|
61
|
+
* delayed ack but the client didn't, the device will throw an error when the client sends
|
|
62
|
+
* the first `WRTE` packet. And vice versa.
|
|
40
63
|
*/
|
|
41
64
|
initialDelayedAckBytes: number;
|
|
65
|
+
|
|
42
66
|
/**
|
|
43
|
-
*
|
|
67
|
+
* When set, the dispatcher will throw an error when
|
|
68
|
+
* one of the socket readable stalls for this amount of milliseconds.
|
|
69
|
+
*
|
|
70
|
+
* Because ADB is a multiplexed protocol, blocking one socket will also block all other sockets.
|
|
71
|
+
* It's important to always read from all sockets to prevent stalling.
|
|
72
|
+
*
|
|
73
|
+
* This option is helpful to detect bugs in the client code.
|
|
74
|
+
*
|
|
75
|
+
* @default false
|
|
44
76
|
*/
|
|
45
|
-
|
|
46
|
-
debugSlowRead?: boolean | undefined;
|
|
77
|
+
readTimeLimit?: number | undefined;
|
|
47
78
|
}
|
|
48
79
|
|
|
49
80
|
interface SocketOpenResult {
|
|
@@ -187,14 +218,14 @@ export class AdbPacketDispatcher implements Closeable {
|
|
|
187
218
|
#handleOkay(packet: AdbPacketData) {
|
|
188
219
|
let ackBytes: number;
|
|
189
220
|
if (this.options.initialDelayedAckBytes !== 0) {
|
|
190
|
-
if (packet.payload.
|
|
221
|
+
if (packet.payload.length !== 4) {
|
|
191
222
|
throw new Error(
|
|
192
223
|
"Invalid OKAY packet. Payload size should be 4",
|
|
193
224
|
);
|
|
194
225
|
}
|
|
195
|
-
ackBytes =
|
|
226
|
+
ackBytes = getUint32LittleEndian(packet.payload, 0);
|
|
196
227
|
} else {
|
|
197
|
-
if (packet.payload.
|
|
228
|
+
if (packet.payload.length !== 0) {
|
|
198
229
|
throw new Error(
|
|
199
230
|
"Invalid OKAY packet. Payload size should be 0",
|
|
200
231
|
);
|
|
@@ -224,14 +255,21 @@ export class AdbPacketDispatcher implements Closeable {
|
|
|
224
255
|
|
|
225
256
|
// Maybe the device is responding to a packet of last connection
|
|
226
257
|
// Tell the device to close the socket
|
|
227
|
-
void this.sendPacket(
|
|
258
|
+
void this.sendPacket(
|
|
259
|
+
AdbCommand.Close,
|
|
260
|
+
packet.arg1,
|
|
261
|
+
packet.arg0,
|
|
262
|
+
EMPTY_UINT8_ARRAY,
|
|
263
|
+
);
|
|
228
264
|
}
|
|
229
265
|
|
|
230
266
|
#sendOkay(localId: number, remoteId: number, ackBytes: number) {
|
|
231
267
|
let payload: Uint8Array;
|
|
232
268
|
if (this.options.initialDelayedAckBytes !== 0) {
|
|
269
|
+
// TODO: try reusing this buffer to reduce memory allocation
|
|
270
|
+
// However, that requires blocking reentrance of `sendOkay`, which might be more expensive
|
|
233
271
|
payload = new Uint8Array(4);
|
|
234
|
-
|
|
272
|
+
setUint32LittleEndian(payload, 0, ackBytes);
|
|
235
273
|
} else {
|
|
236
274
|
payload = EMPTY_UINT8_ARRAY;
|
|
237
275
|
}
|
|
@@ -240,22 +278,28 @@ export class AdbPacketDispatcher implements Closeable {
|
|
|
240
278
|
}
|
|
241
279
|
|
|
242
280
|
async #handleOpen(packet: AdbPacketData) {
|
|
243
|
-
//
|
|
244
|
-
//
|
|
281
|
+
// Allocate a local ID for the socket from `#initializers`.
|
|
282
|
+
// `AsyncOperationManager` doesn't directly support returning the next ID,
|
|
283
|
+
// so use `add` + `resolve` to simulate this
|
|
245
284
|
const [localId] = this.#initializers.add<number>();
|
|
246
285
|
this.#initializers.resolve(localId, undefined);
|
|
247
286
|
|
|
248
287
|
const remoteId = packet.arg0;
|
|
249
|
-
let
|
|
250
|
-
|
|
288
|
+
let availableWriteBytes = packet.arg1;
|
|
289
|
+
let service = decodeUtf8(packet.payload);
|
|
290
|
+
// ADB Daemon still adds a null character to the service string
|
|
291
|
+
if (service.endsWith("\0")) {
|
|
292
|
+
service = service.substring(0, service.length - 1);
|
|
293
|
+
}
|
|
251
294
|
|
|
295
|
+
// Check remote delayed ack enablement is consistent with local
|
|
252
296
|
if (this.options.initialDelayedAckBytes === 0) {
|
|
253
|
-
if (
|
|
297
|
+
if (availableWriteBytes !== 0) {
|
|
254
298
|
throw new Error("Invalid OPEN packet. arg1 should be 0");
|
|
255
299
|
}
|
|
256
|
-
|
|
300
|
+
availableWriteBytes = Infinity;
|
|
257
301
|
} else {
|
|
258
|
-
if (
|
|
302
|
+
if (availableWriteBytes === 0) {
|
|
259
303
|
throw new Error(
|
|
260
304
|
"Invalid OPEN packet. arg1 should be greater than 0",
|
|
261
305
|
);
|
|
@@ -264,7 +308,12 @@ export class AdbPacketDispatcher implements Closeable {
|
|
|
264
308
|
|
|
265
309
|
const handler = this.#incomingSocketHandlers.get(service);
|
|
266
310
|
if (!handler) {
|
|
267
|
-
await this.sendPacket(
|
|
311
|
+
await this.sendPacket(
|
|
312
|
+
AdbCommand.Close,
|
|
313
|
+
0,
|
|
314
|
+
remoteId,
|
|
315
|
+
EMPTY_UINT8_ARRAY,
|
|
316
|
+
);
|
|
268
317
|
return;
|
|
269
318
|
}
|
|
270
319
|
|
|
@@ -274,8 +323,8 @@ export class AdbPacketDispatcher implements Closeable {
|
|
|
274
323
|
remoteId,
|
|
275
324
|
localCreated: false,
|
|
276
325
|
service,
|
|
326
|
+
availableWriteBytes,
|
|
277
327
|
});
|
|
278
|
-
controller.ack(initialDelayedAckBytes);
|
|
279
328
|
|
|
280
329
|
try {
|
|
281
330
|
await handler(controller.socket);
|
|
@@ -286,7 +335,12 @@ export class AdbPacketDispatcher implements Closeable {
|
|
|
286
335
|
this.options.initialDelayedAckBytes,
|
|
287
336
|
);
|
|
288
337
|
} catch (e) {
|
|
289
|
-
await this.sendPacket(
|
|
338
|
+
await this.sendPacket(
|
|
339
|
+
AdbCommand.Close,
|
|
340
|
+
0,
|
|
341
|
+
remoteId,
|
|
342
|
+
EMPTY_UINT8_ARRAY,
|
|
343
|
+
);
|
|
290
344
|
}
|
|
291
345
|
}
|
|
292
346
|
|
|
@@ -297,14 +351,8 @@ export class AdbPacketDispatcher implements Closeable {
|
|
|
297
351
|
}
|
|
298
352
|
|
|
299
353
|
let handled = false;
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if (this.options.debugSlowRead && !handled) {
|
|
303
|
-
throw new Error(
|
|
304
|
-
`packet for \`${socket.service}\` not handled in 5 seconds`,
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
}),
|
|
354
|
+
|
|
355
|
+
const promises: Promise<void>[] = [
|
|
308
356
|
(async () => {
|
|
309
357
|
await socket.enqueue(packet.payload);
|
|
310
358
|
await this.#sendOkay(
|
|
@@ -314,9 +362,22 @@ export class AdbPacketDispatcher implements Closeable {
|
|
|
314
362
|
);
|
|
315
363
|
handled = true;
|
|
316
364
|
})(),
|
|
317
|
-
]
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
if (this.options.readTimeLimit) {
|
|
368
|
+
promises.push(
|
|
369
|
+
(async () => {
|
|
370
|
+
await delay(this.options.readTimeLimit!);
|
|
371
|
+
if (!handled) {
|
|
372
|
+
throw new Error(
|
|
373
|
+
`readable of \`${socket.service}\` has stalled for ${this.options.readTimeLimit} milliseconds`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
})(),
|
|
377
|
+
);
|
|
378
|
+
}
|
|
318
379
|
|
|
319
|
-
|
|
380
|
+
await Promise.race(promises);
|
|
320
381
|
}
|
|
321
382
|
|
|
322
383
|
async createSocket(service: string): Promise<AdbSocket> {
|
|
@@ -341,8 +402,8 @@ export class AdbPacketDispatcher implements Closeable {
|
|
|
341
402
|
remoteId,
|
|
342
403
|
localCreated: true,
|
|
343
404
|
service,
|
|
405
|
+
availableWriteBytes,
|
|
344
406
|
});
|
|
345
|
-
controller.ack(availableWriteBytes);
|
|
346
407
|
this.#sockets.set(localId, controller);
|
|
347
408
|
|
|
348
409
|
return controller.socket;
|
|
@@ -364,17 +425,18 @@ export class AdbPacketDispatcher implements Closeable {
|
|
|
364
425
|
command: AdbCommand,
|
|
365
426
|
arg0: number,
|
|
366
427
|
arg1: number,
|
|
367
|
-
|
|
428
|
+
// PERF: It's slightly faster to not use default parameter values
|
|
429
|
+
payload: string | Uint8Array,
|
|
368
430
|
): Promise<void> {
|
|
369
431
|
if (typeof payload === "string") {
|
|
370
432
|
payload = encodeUtf8(payload);
|
|
371
433
|
}
|
|
372
434
|
|
|
373
|
-
if (payload.
|
|
374
|
-
throw new
|
|
435
|
+
if (payload.length > this.options.maxPayloadSize) {
|
|
436
|
+
throw new TypeError("payload too large");
|
|
375
437
|
}
|
|
376
438
|
|
|
377
|
-
await
|
|
439
|
+
await Consumable.WritableStream.write(this.#writer, {
|
|
378
440
|
command,
|
|
379
441
|
arg0,
|
|
380
442
|
arg1,
|
package/src/daemon/packet.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Consumable, TransformStream } from "@yume-chan/stream-extra";
|
|
2
2
|
import Struct from "@yume-chan/struct";
|
|
3
3
|
|
|
4
4
|
export enum AdbCommand {
|
|
@@ -49,26 +49,33 @@ export function calculateChecksum(payload: Uint8Array): number {
|
|
|
49
49
|
return payload.reduce((result, item) => result + item, 0);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export class AdbPacketSerializeStream extends
|
|
53
|
-
AdbPacketInit
|
|
54
|
-
Uint8Array
|
|
52
|
+
export class AdbPacketSerializeStream extends TransformStream<
|
|
53
|
+
Consumable<AdbPacketInit>,
|
|
54
|
+
Consumable<Uint8Array>
|
|
55
55
|
> {
|
|
56
56
|
constructor() {
|
|
57
57
|
const headerBuffer = new Uint8Array(AdbPacketHeader.size);
|
|
58
58
|
super({
|
|
59
59
|
transform: async (chunk, controller) => {
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
await chunk.tryConsume(async (chunk) => {
|
|
61
|
+
const init = chunk as AdbPacketInit & AdbPacketHeaderInit;
|
|
62
|
+
init.payloadLength = init.payload.length;
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
await Consumable.ReadableStream.enqueue(
|
|
65
|
+
controller,
|
|
66
|
+
AdbPacketHeader.serialize(init, headerBuffer),
|
|
67
|
+
);
|
|
65
68
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
if (init.payloadLength) {
|
|
70
|
+
// USB protocol preserves packet boundaries,
|
|
71
|
+
// so we must write payload separately as native ADB does,
|
|
72
|
+
// otherwise the read operation on device will fail.
|
|
73
|
+
await Consumable.ReadableStream.enqueue(
|
|
74
|
+
controller,
|
|
75
|
+
init.payload,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
72
79
|
},
|
|
73
80
|
});
|
|
74
81
|
}
|