@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.
Files changed (135) hide show
  1. package/CHANGELOG.json +33 -0
  2. package/CHANGELOG.md +15 -1
  3. package/LICENSE +1 -1
  4. package/esm/adb.d.ts +2 -2
  5. package/esm/adb.d.ts.map +1 -1
  6. package/esm/adb.js +2 -2
  7. package/esm/adb.js.map +1 -1
  8. package/esm/commands/power.d.ts.map +1 -1
  9. package/esm/commands/power.js +6 -5
  10. package/esm/commands/power.js.map +1 -1
  11. package/esm/commands/reverse.d.ts.map +1 -1
  12. package/esm/commands/reverse.js +22 -10
  13. package/esm/commands/reverse.js.map +1 -1
  14. package/esm/commands/subprocess/command.js +3 -3
  15. package/esm/commands/subprocess/command.js.map +1 -1
  16. package/esm/commands/subprocess/protocols/none.d.ts +2 -2
  17. package/esm/commands/subprocess/protocols/none.d.ts.map +1 -1
  18. package/esm/commands/subprocess/protocols/shell.d.ts +3 -3
  19. package/esm/commands/subprocess/protocols/shell.d.ts.map +1 -1
  20. package/esm/commands/subprocess/protocols/shell.js +5 -6
  21. package/esm/commands/subprocess/protocols/shell.js.map +1 -1
  22. package/esm/commands/subprocess/protocols/types.d.ts +2 -2
  23. package/esm/commands/subprocess/protocols/types.d.ts.map +1 -1
  24. package/esm/commands/sync/list.d.ts +2 -5
  25. package/esm/commands/sync/list.d.ts.map +1 -1
  26. package/esm/commands/sync/list.js +2 -4
  27. package/esm/commands/sync/list.js.map +1 -1
  28. package/esm/commands/sync/pull.d.ts +1 -4
  29. package/esm/commands/sync/pull.d.ts.map +1 -1
  30. package/esm/commands/sync/pull.js +2 -2
  31. package/esm/commands/sync/pull.js.map +1 -1
  32. package/esm/commands/sync/push.d.ts +4 -7
  33. package/esm/commands/sync/push.d.ts.map +1 -1
  34. package/esm/commands/sync/push.js +3 -6
  35. package/esm/commands/sync/push.js.map +1 -1
  36. package/esm/commands/sync/request.d.ts +13 -18
  37. package/esm/commands/sync/request.d.ts.map +1 -1
  38. package/esm/commands/sync/request.js +23 -28
  39. package/esm/commands/sync/request.js.map +1 -1
  40. package/esm/commands/sync/response.d.ts +18 -12
  41. package/esm/commands/sync/response.d.ts.map +1 -1
  42. package/esm/commands/sync/response.js +39 -17
  43. package/esm/commands/sync/response.js.map +1 -1
  44. package/esm/commands/sync/socket.d.ts +2 -2
  45. package/esm/commands/sync/socket.d.ts.map +1 -1
  46. package/esm/commands/sync/socket.js +10 -5
  47. package/esm/commands/sync/socket.js.map +1 -1
  48. package/esm/commands/sync/stat.d.ts +0 -3
  49. package/esm/commands/sync/stat.d.ts.map +1 -1
  50. package/esm/commands/sync/stat.js +0 -2
  51. package/esm/commands/sync/stat.js.map +1 -1
  52. package/esm/commands/sync/sync.d.ts +4 -3
  53. package/esm/commands/sync/sync.d.ts.map +1 -1
  54. package/esm/commands/sync/sync.js +4 -1
  55. package/esm/commands/sync/sync.js.map +1 -1
  56. package/esm/commands/tcpip.js +1 -1
  57. package/esm/commands/tcpip.js.map +1 -1
  58. package/esm/daemon/auth.js +1 -1
  59. package/esm/daemon/auth.js.map +1 -1
  60. package/esm/daemon/crypto.d.ts +3 -2
  61. package/esm/daemon/crypto.d.ts.map +1 -1
  62. package/esm/daemon/crypto.js +56 -49
  63. package/esm/daemon/crypto.js.map +1 -1
  64. package/esm/daemon/dispatcher.d.ts +35 -10
  65. package/esm/daemon/dispatcher.d.ts.map +1 -1
  66. package/esm/daemon/dispatcher.js +44 -31
  67. package/esm/daemon/dispatcher.js.map +1 -1
  68. package/esm/daemon/packet.d.ts +2 -2
  69. package/esm/daemon/packet.d.ts.map +1 -1
  70. package/esm/daemon/packet.js +13 -12
  71. package/esm/daemon/packet.js.map +1 -1
  72. package/esm/daemon/socket.d.ts +10 -5
  73. package/esm/daemon/socket.d.ts.map +1 -1
  74. package/esm/daemon/socket.js +17 -15
  75. package/esm/daemon/socket.js.map +1 -1
  76. package/esm/daemon/transport.d.ts +55 -15
  77. package/esm/daemon/transport.d.ts.map +1 -1
  78. package/esm/daemon/transport.js +9 -10
  79. package/esm/daemon/transport.js.map +1 -1
  80. package/esm/server/client.d.ts +128 -50
  81. package/esm/server/client.d.ts.map +1 -1
  82. package/esm/server/client.js +325 -124
  83. package/esm/server/client.js.map +1 -1
  84. package/esm/server/transport.js +4 -4
  85. package/esm/server/transport.js.map +1 -1
  86. package/esm/utils/base64.d.ts.map +1 -1
  87. package/esm/utils/base64.js +33 -22
  88. package/esm/utils/base64.js.map +1 -1
  89. package/esm/utils/hex.d.ts +1 -1
  90. package/esm/utils/hex.d.ts.map +1 -1
  91. package/esm/utils/hex.js +12 -14
  92. package/esm/utils/hex.js.map +1 -1
  93. package/esm/utils/index.d.ts +1 -1
  94. package/esm/utils/index.d.ts.map +1 -1
  95. package/esm/utils/index.js +1 -1
  96. package/esm/utils/index.js.map +1 -1
  97. package/esm/utils/sequence-equal.d.ts +2 -0
  98. package/esm/utils/sequence-equal.d.ts.map +1 -0
  99. package/esm/utils/sequence-equal.js +12 -0
  100. package/esm/utils/sequence-equal.js.map +1 -0
  101. package/package.json +12 -14
  102. package/src/adb.ts +7 -4
  103. package/src/commands/power.ts +7 -5
  104. package/src/commands/reverse.ts +30 -17
  105. package/src/commands/subprocess/command.ts +3 -3
  106. package/src/commands/subprocess/protocols/none.ts +2 -2
  107. package/src/commands/subprocess/protocols/shell.ts +7 -11
  108. package/src/commands/subprocess/protocols/types.ts +2 -2
  109. package/src/commands/sync/list.ts +2 -4
  110. package/src/commands/sync/pull.ts +2 -2
  111. package/src/commands/sync/push.ts +7 -10
  112. package/src/commands/sync/request.ts +32 -32
  113. package/src/commands/sync/response.ts +47 -20
  114. package/src/commands/sync/socket.ts +13 -8
  115. package/src/commands/sync/stat.ts +0 -2
  116. package/src/commands/sync/sync.ts +8 -3
  117. package/src/commands/tcpip.ts +1 -1
  118. package/src/daemon/auth.ts +1 -1
  119. package/src/daemon/crypto.ts +64 -55
  120. package/src/daemon/dispatcher.ts +104 -42
  121. package/src/daemon/packet.ts +21 -14
  122. package/src/daemon/socket.ts +28 -25
  123. package/src/daemon/transport.ts +71 -24
  124. package/src/server/client.ts +420 -208
  125. package/src/server/transport.ts +4 -4
  126. package/src/utils/base64.ts +33 -22
  127. package/src/utils/hex.ts +16 -14
  128. package/src/utils/index.ts +1 -1
  129. package/src/utils/sequence-equal.ts +13 -0
  130. package/tsconfig.build.tsbuildinfo +1 -1
  131. package/esm/utils/conditional-variable.d.ts +0 -8
  132. package/esm/utils/conditional-variable.d.ts.map +0 -1
  133. package/esm/utils/conditional-variable.js +0 -34
  134. package/esm/utils/conditional-variable.js.map +0 -1
  135. package/src/utils/conditional-variable.ts +0 -45
@@ -1,7 +1,8 @@
1
1
  import {
2
- getBigUint64,
3
- setBigUint64,
4
- } from "@yume-chan/dataview-bigint-polyfill/esm/fallback.js";
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
- dataView: DataView,
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 = getBigUint64(dataView, i, false);
26
- result += value;
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
- dataView: DataView,
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
- setBigUint64(dataView, byteOffset, value, true);
50
+ setInt64LittleEndian(array, byteOffset, value);
50
51
  byteOffset += 8;
51
52
  value >>= 64n;
52
53
  }
53
54
  } else {
54
- // Because we don't know how long (in bits) the `value` is,
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
- uint64Array.push(BigInt.asUintN(64, value));
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 view = new DataView(key.buffer, key.byteOffset, key.byteLength);
99
- const n = getBigUint(view, RsaPrivateKeyNOffset, RsaPrivateKeyNLength);
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 = ((a % m) + m) % m;
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 ((y % m) + m) % m;
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 + 2048 / 8 + 2048 / 8 + 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
- // Android has its own public key generation algorithm
145
- // See https://android.googlesource.com/platform/system/core.git/+/91784040db2b9273687f88d8b95f729d4a61ecc2/libcrypto_utils/android_pubkey.cpp#111
146
-
147
- // The public key is an array of
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
- // // Actually the comment in Android source code was wrong
152
- // n0inv, // 32-bit integer, the modular inverse of (low 32 bits of n)
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.byteLength < outputLength) {
168
- throw new Error("output buffer is too small");
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.byteLength,
188
+ output.length,
178
189
  );
179
190
  let outputOffset = 0;
180
191
 
181
192
  // modulusLengthInWords
182
- outputView.setUint32(outputOffset, 2048 / 8 / 4, true);
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
- // Don't know why need to multiply by -1
190
- // Didn't exist in Android codebase
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(outputView, outputOffset, n, true);
197
- outputOffset += 256;
204
+ // Write `n` (a.k.a. `modulus`)
205
+ setBigUint(output, outputOffset, ModulusLengthInBytes, n, true);
206
+ outputOffset += ModulusLengthInBytes;
198
207
 
199
- // Calculate rr = (2^(rsa_size)) ^ 2 mod n
208
+ // Calculate rr = (2 ** (rsa_size)) ** 2 % n
200
209
  const rr = 2n ** 4096n % n;
201
- outputOffset += setBigUint(outputView, outputOffset, rr, true);
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 view = new DataView(padded.buffer);
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(view, 0, signature, false);
322
+ setBigUint(padded, 0, padded.length, signature, false);
314
323
 
315
324
  return padded;
316
325
  }
@@ -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
- ConsumableWritableStream,
16
+ Consumable,
14
17
  WritableStream,
15
18
  } from "@yume-chan/stream-extra";
16
- import { EMPTY_UINT8_ARRAY, NumberFieldType } from "@yume-chan/struct";
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 string,
36
+ * Before Android 9.0, ADB uses `char*` to parse service strings,
29
37
  * thus requires a null character to terminate.
30
38
  *
31
- * Usually it should have the same value as `calculateChecksum`.
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
- * The number of bytes the device can send before receiving an ack packet.
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
- * Set to 0 or any negative value to disable delayed ack.
39
- * Otherwise the value must be in the range of unsigned 32-bit integer.
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
- * Whether to preserve the connection open after the `AdbPacketDispatcher` is closed.
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
- preserveConnection?: boolean | undefined;
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.byteLength !== 4) {
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 = NumberFieldType.Uint32.deserialize(packet.payload, true);
226
+ ackBytes = getUint32LittleEndian(packet.payload, 0);
196
227
  } else {
197
- if (packet.payload.byteLength !== 0) {
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(AdbCommand.Close, packet.arg1, packet.arg0);
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
- new DataView(payload.buffer).setUint32(0, ackBytes, true);
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
- // `AsyncOperationManager` doesn't support skipping IDs
244
- // Use `add` + `resolve` to simulate this behavior
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 initialDelayedAckBytes = packet.arg1;
250
- const service = decodeUtf8(packet.payload);
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 (initialDelayedAckBytes !== 0) {
297
+ if (availableWriteBytes !== 0) {
254
298
  throw new Error("Invalid OPEN packet. arg1 should be 0");
255
299
  }
256
- initialDelayedAckBytes = Infinity;
300
+ availableWriteBytes = Infinity;
257
301
  } else {
258
- if (initialDelayedAckBytes === 0) {
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(AdbCommand.Close, 0, remoteId);
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(AdbCommand.Close, 0, remoteId);
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
- await Promise.race([
301
- delay(5000).then(() => {
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
- return;
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
- payload: string | Uint8Array = EMPTY_UINT8_ARRAY,
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.byteLength > this.options.maxPayloadSize) {
374
- throw new Error("payload too large");
435
+ if (payload.length > this.options.maxPayloadSize) {
436
+ throw new TypeError("payload too large");
375
437
  }
376
438
 
377
- await ConsumableWritableStream.write(this.#writer, {
439
+ await Consumable.WritableStream.write(this.#writer, {
378
440
  command,
379
441
  arg0,
380
442
  arg1,
@@ -1,4 +1,4 @@
1
- import { ConsumableTransformStream } from "@yume-chan/stream-extra";
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 ConsumableTransformStream<
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
- const init = chunk as AdbPacketInit & AdbPacketHeaderInit;
61
- init.payloadLength = init.payload.byteLength;
60
+ await chunk.tryConsume(async (chunk) => {
61
+ const init = chunk as AdbPacketInit & AdbPacketHeaderInit;
62
+ init.payloadLength = init.payload.length;
62
63
 
63
- AdbPacketHeader.serialize(init, headerBuffer);
64
- await controller.enqueue(headerBuffer);
64
+ await Consumable.ReadableStream.enqueue(
65
+ controller,
66
+ AdbPacketHeader.serialize(init, headerBuffer),
67
+ );
65
68
 
66
- if (init.payload.byteLength) {
67
- // USB protocol preserves packet boundaries,
68
- // so we must write payload separately as native ADB does,
69
- // otherwise the read operation on device will fail.
70
- await controller.enqueue(init.payload);
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
  }