@yume-chan/adb 0.0.23 → 1.0.0

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 (178) hide show
  1. package/CHANGELOG.md +51 -2
  2. package/LICENSE +1 -1
  3. package/README.md +4 -1
  4. package/esm/adb.d.ts +15 -9
  5. package/esm/adb.d.ts.map +1 -1
  6. package/esm/adb.js +5 -2
  7. package/esm/adb.js.map +1 -1
  8. package/esm/banner.d.ts +7 -6
  9. package/esm/banner.d.ts.map +1 -1
  10. package/esm/banner.js +6 -7
  11. package/esm/banner.js.map +1 -1
  12. package/esm/commands/framebuffer.d.ts +63 -34
  13. package/esm/commands/framebuffer.d.ts.map +1 -1
  14. package/esm/commands/framebuffer.js +35 -33
  15. package/esm/commands/framebuffer.js.map +1 -1
  16. package/esm/commands/power.d.ts.map +1 -1
  17. package/esm/commands/power.js +6 -5
  18. package/esm/commands/power.js.map +1 -1
  19. package/esm/commands/reverse.d.ts +12 -12
  20. package/esm/commands/reverse.d.ts.map +1 -1
  21. package/esm/commands/reverse.js +60 -39
  22. package/esm/commands/reverse.js.map +1 -1
  23. package/esm/commands/subprocess/command.js +3 -3
  24. package/esm/commands/subprocess/command.js.map +1 -1
  25. package/esm/commands/subprocess/protocols/none.d.ts +2 -2
  26. package/esm/commands/subprocess/protocols/none.d.ts.map +1 -1
  27. package/esm/commands/subprocess/protocols/none.js +3 -5
  28. package/esm/commands/subprocess/protocols/none.js.map +1 -1
  29. package/esm/commands/subprocess/protocols/shell.d.ts +22 -14
  30. package/esm/commands/subprocess/protocols/shell.d.ts.map +1 -1
  31. package/esm/commands/subprocess/protocols/shell.js +21 -27
  32. package/esm/commands/subprocess/protocols/shell.js.map +1 -1
  33. package/esm/commands/subprocess/protocols/types.d.ts +8 -8
  34. package/esm/commands/subprocess/protocols/types.d.ts.map +1 -1
  35. package/esm/commands/sync/list.d.ts +49 -30
  36. package/esm/commands/sync/list.d.ts.map +1 -1
  37. package/esm/commands/sync/list.js +9 -11
  38. package/esm/commands/sync/list.js.map +1 -1
  39. package/esm/commands/sync/pull.d.ts +7 -9
  40. package/esm/commands/sync/pull.d.ts.map +1 -1
  41. package/esm/commands/sync/pull.js +4 -6
  42. package/esm/commands/sync/pull.js.map +1 -1
  43. package/esm/commands/sync/push.d.ts +25 -22
  44. package/esm/commands/sync/push.d.ts.map +1 -1
  45. package/esm/commands/sync/push.js +14 -21
  46. package/esm/commands/sync/push.js.map +1 -1
  47. package/esm/commands/sync/request.d.ts +20 -23
  48. package/esm/commands/sync/request.d.ts.map +1 -1
  49. package/esm/commands/sync/request.js +26 -35
  50. package/esm/commands/sync/request.js.map +1 -1
  51. package/esm/commands/sync/response.d.ts +25 -19
  52. package/esm/commands/sync/response.d.ts.map +1 -1
  53. package/esm/commands/sync/response.js +49 -26
  54. package/esm/commands/sync/response.js.map +1 -1
  55. package/esm/commands/sync/socket.d.ts +3 -3
  56. package/esm/commands/sync/socket.d.ts.map +1 -1
  57. package/esm/commands/sync/socket.js +10 -5
  58. package/esm/commands/sync/socket.js.map +1 -1
  59. package/esm/commands/sync/stat.d.ts +75 -54
  60. package/esm/commands/sync/stat.d.ts.map +1 -1
  61. package/esm/commands/sync/stat.js +73 -69
  62. package/esm/commands/sync/stat.js.map +1 -1
  63. package/esm/commands/sync/sync.d.ts +18 -17
  64. package/esm/commands/sync/sync.d.ts.map +1 -1
  65. package/esm/commands/sync/sync.js +8 -8
  66. package/esm/commands/sync/sync.js.map +1 -1
  67. package/esm/commands/tcpip.d.ts +0 -1
  68. package/esm/commands/tcpip.d.ts.map +1 -1
  69. package/esm/commands/tcpip.js +9 -9
  70. package/esm/commands/tcpip.js.map +1 -1
  71. package/esm/daemon/auth.d.ts +8 -7
  72. package/esm/daemon/auth.d.ts.map +1 -1
  73. package/esm/daemon/auth.js +8 -9
  74. package/esm/daemon/auth.js.map +1 -1
  75. package/esm/daemon/crypto.d.ts +4 -3
  76. package/esm/daemon/crypto.d.ts.map +1 -1
  77. package/esm/daemon/crypto.js +57 -49
  78. package/esm/daemon/crypto.js.map +1 -1
  79. package/esm/daemon/device.d.ts +2 -2
  80. package/esm/daemon/device.d.ts.map +1 -1
  81. package/esm/daemon/dispatcher.d.ts +35 -10
  82. package/esm/daemon/dispatcher.d.ts.map +1 -1
  83. package/esm/daemon/dispatcher.js +46 -33
  84. package/esm/daemon/dispatcher.js.map +1 -1
  85. package/esm/daemon/packet.d.ts +48 -32
  86. package/esm/daemon/packet.d.ts.map +1 -1
  87. package/esm/daemon/packet.js +36 -32
  88. package/esm/daemon/packet.js.map +1 -1
  89. package/esm/daemon/socket.d.ts +11 -11
  90. package/esm/daemon/socket.d.ts.map +1 -1
  91. package/esm/daemon/socket.js +19 -40
  92. package/esm/daemon/socket.js.map +1 -1
  93. package/esm/daemon/transport.d.ts +60 -20
  94. package/esm/daemon/transport.d.ts.map +1 -1
  95. package/esm/daemon/transport.js +12 -15
  96. package/esm/daemon/transport.js.map +1 -1
  97. package/esm/device-observer.d.ts +10 -0
  98. package/esm/device-observer.d.ts.map +1 -0
  99. package/esm/device-observer.js +2 -0
  100. package/esm/device-observer.js.map +1 -0
  101. package/esm/features.d.ts +12 -11
  102. package/esm/features.d.ts.map +1 -1
  103. package/esm/features.js +11 -12
  104. package/esm/features.js.map +1 -1
  105. package/esm/index.d.ts +1 -0
  106. package/esm/index.d.ts.map +1 -1
  107. package/esm/index.js +1 -0
  108. package/esm/index.js.map +1 -1
  109. package/esm/server/client.d.ts +140 -50
  110. package/esm/server/client.d.ts.map +1 -1
  111. package/esm/server/client.js +396 -180
  112. package/esm/server/client.js.map +1 -1
  113. package/esm/server/transport.d.ts +2 -2
  114. package/esm/server/transport.d.ts.map +1 -1
  115. package/esm/server/transport.js +4 -6
  116. package/esm/server/transport.js.map +1 -1
  117. package/esm/utils/base64.d.ts.map +1 -1
  118. package/esm/utils/base64.js +52 -38
  119. package/esm/utils/base64.js.map +1 -1
  120. package/esm/utils/hex.d.ts +1 -1
  121. package/esm/utils/hex.d.ts.map +1 -1
  122. package/esm/utils/hex.js +12 -14
  123. package/esm/utils/hex.js.map +1 -1
  124. package/esm/utils/index.d.ts +1 -1
  125. package/esm/utils/index.d.ts.map +1 -1
  126. package/esm/utils/index.js +1 -1
  127. package/esm/utils/index.js.map +1 -1
  128. package/esm/utils/no-op.d.ts.map +1 -1
  129. package/esm/utils/no-op.js +1 -0
  130. package/esm/utils/no-op.js.map +1 -1
  131. package/esm/utils/sequence-equal.d.ts +2 -0
  132. package/esm/utils/sequence-equal.d.ts.map +1 -0
  133. package/esm/utils/sequence-equal.js +12 -0
  134. package/esm/utils/sequence-equal.js.map +1 -0
  135. package/package.json +14 -19
  136. package/src/adb.ts +20 -11
  137. package/src/banner.ts +8 -6
  138. package/src/commands/framebuffer.ts +44 -35
  139. package/src/commands/power.ts +7 -5
  140. package/src/commands/reverse.ts +78 -47
  141. package/src/commands/subprocess/command.ts +3 -3
  142. package/src/commands/subprocess/protocols/none.ts +5 -7
  143. package/src/commands/subprocess/protocols/shell.ts +33 -35
  144. package/src/commands/subprocess/protocols/types.ts +8 -8
  145. package/src/commands/sync/list.ts +20 -15
  146. package/src/commands/sync/pull.ts +9 -8
  147. package/src/commands/sync/push.ts +25 -24
  148. package/src/commands/sync/request.ts +37 -38
  149. package/src/commands/sync/response.ts +63 -37
  150. package/src/commands/sync/socket.ts +13 -8
  151. package/src/commands/sync/stat.ts +93 -73
  152. package/src/commands/sync/sync.ts +12 -11
  153. package/src/commands/tcpip.ts +9 -9
  154. package/src/daemon/auth.ts +12 -10
  155. package/src/daemon/crypto.ts +66 -55
  156. package/src/daemon/device.ts +2 -2
  157. package/src/daemon/dispatcher.ts +106 -44
  158. package/src/daemon/packet.ts +57 -38
  159. package/src/daemon/socket.ts +30 -50
  160. package/src/daemon/transport.ts +98 -50
  161. package/src/device-observer.ts +10 -0
  162. package/src/features.ts +13 -11
  163. package/src/index.ts +1 -0
  164. package/src/server/client.ts +505 -268
  165. package/src/server/transport.ts +7 -9
  166. package/src/utils/base64.ts +59 -40
  167. package/src/utils/hex.ts +16 -14
  168. package/src/utils/index.ts +1 -1
  169. package/src/utils/no-op.ts +1 -0
  170. package/src/utils/sequence-equal.ts +13 -0
  171. package/tsconfig.build.json +1 -12
  172. package/tsconfig.build.tsbuildinfo +1 -1
  173. package/CHANGELOG.json +0 -265
  174. package/esm/utils/conditional-variable.d.ts +0 -8
  175. package/esm/utils/conditional-variable.d.ts.map +0 -1
  176. package/esm/utils/conditional-variable.js +0 -34
  177. package/esm/utils/conditional-variable.js.map +0 -1
  178. package/src/utils/conditional-variable.ts +0 -45
@@ -20,14 +20,14 @@ export interface AdbTcpIpListenAddresses {
20
20
  persistPort: number | undefined;
21
21
  }
22
22
 
23
- export class AdbTcpIpCommand extends AdbCommandBase {
24
- #parsePort(value: string): number | undefined {
25
- if (!value || value === "0") {
26
- return undefined;
27
- }
28
- return Number.parseInt(value, 10);
23
+ function parsePort(value: string): number | undefined {
24
+ if (!value || value === "0") {
25
+ return undefined;
29
26
  }
27
+ return Number.parseInt(value, 10);
28
+ }
30
29
 
30
+ export class AdbTcpIpCommand extends AdbCommandBase {
31
31
  async getListenAddresses(): Promise<AdbTcpIpListenAddresses> {
32
32
  const serviceListenAddresses = await this.adb.getProp(
33
33
  "service.adb.listen_addrs",
@@ -40,14 +40,14 @@ export class AdbTcpIpCommand extends AdbCommandBase {
40
40
  serviceListenAddresses != ""
41
41
  ? serviceListenAddresses.split(",")
42
42
  : [],
43
- servicePort: this.#parsePort(servicePort),
44
- persistPort: this.#parsePort(persistPort),
43
+ servicePort: parsePort(servicePort),
44
+ persistPort: parsePort(persistPort),
45
45
  };
46
46
  }
47
47
 
48
48
  async setPort(port: number): Promise<string> {
49
49
  if (port <= 0) {
50
- throw new Error(`Invalid port ${port}`);
50
+ throw new TypeError(`Invalid port ${port}`);
51
51
  }
52
52
 
53
53
  const output = await this.adb.createSocketAndWait(`tcpip:${port}`);
@@ -1,7 +1,7 @@
1
+ import type { MaybePromiseLike } from "@yume-chan/async";
1
2
  import { PromiseResolver } from "@yume-chan/async";
2
3
  import type { Disposable } from "@yume-chan/event";
3
- import type { ValueOrPromise } from "@yume-chan/struct";
4
- import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct";
4
+ import { EmptyUint8Array } from "@yume-chan/struct";
5
5
 
6
6
  import {
7
7
  calculateBase64EncodedLength,
@@ -33,7 +33,7 @@ export interface AdbCredentialStore {
33
33
  /**
34
34
  * Generates and stores a RSA private key with modulus length `2048` and public exponent `65537`.
35
35
  */
36
- generateKey(): ValueOrPromise<AdbPrivateKey>;
36
+ generateKey(): MaybePromiseLike<AdbPrivateKey>;
37
37
 
38
38
  /**
39
39
  * Synchronously or asynchronously iterates through all stored RSA private keys.
@@ -43,11 +43,13 @@ export interface AdbCredentialStore {
43
43
  iterateKeys(): AdbKeyIterable;
44
44
  }
45
45
 
46
- export enum AdbAuthType {
47
- Token = 1,
48
- Signature = 2,
49
- PublicKey = 3,
50
- }
46
+ export const AdbAuthType = {
47
+ Token: 1,
48
+ Signature: 2,
49
+ PublicKey: 3,
50
+ } as const;
51
+
52
+ export type AdbAuthType = (typeof AdbAuthType)[keyof typeof AdbAuthType];
51
53
 
52
54
  export interface AdbAuthenticator {
53
55
  /**
@@ -83,7 +85,7 @@ export const AdbSignatureAuthenticator: AdbAuthenticator = async function* (
83
85
  command: AdbCommand.Auth,
84
86
  arg0: AdbAuthType.Signature,
85
87
  arg1: 0,
86
- payload: new Uint8Array(signature),
88
+ payload: signature,
87
89
  };
88
90
  }
89
91
  };
@@ -114,7 +116,7 @@ export const AdbPublicKeyAuthenticator: AdbAuthenticator = async function* (
114
116
 
115
117
  const nameBuffer = privateKey.name?.length
116
118
  ? encodeUtf8(privateKey.name)
117
- : EMPTY_UINT8_ARRAY;
119
+ : EmptyUint8Array;
118
120
  const publicKeyBuffer = new Uint8Array(
119
121
  publicKeyBase64Length +
120
122
  (nameBuffer.length ? nameBuffer.length + 1 : 0) + // Space character + name
@@ -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,34 @@ 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
  */
41
+
42
+ // eslint-disable-next-line @typescript-eslint/max-params
39
43
  export function setBigUint(
40
- dataView: DataView,
44
+ array: Uint8Array,
41
45
  byteOffset: number,
46
+ length: number,
42
47
  value: bigint,
43
48
  littleEndian?: boolean,
44
49
  ) {
45
- const start = byteOffset;
46
-
47
50
  if (littleEndian) {
48
51
  while (value > 0n) {
49
- setBigUint64(dataView, byteOffset, value, true);
52
+ setInt64LittleEndian(array, byteOffset, value);
50
53
  byteOffset += 8;
51
54
  value >>= 64n;
52
55
  }
53
56
  } 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[] = [];
57
+ let position = byteOffset + length - 8;
57
58
  while (value > 0n) {
58
- uint64Array.push(BigInt.asUintN(64, value));
59
+ setInt64BigEndian(array, position, value);
60
+ position -= 8;
59
61
  value >>= 64n;
60
62
  }
61
-
62
- for (let i = uint64Array.length - 1; i >= 0; i -= 1) {
63
- setBigUint64(dataView, byteOffset, uint64Array[i]!, false);
64
- byteOffset += 8;
65
- }
66
63
  }
67
-
68
- return byteOffset - start;
69
64
  }
70
65
 
71
66
  // These values are correct only if
@@ -95,17 +90,26 @@ const RsaPrivateKeyDOffset = 303;
95
90
  const RsaPrivateKeyDLength = 2048 / 8;
96
91
 
97
92
  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);
93
+ const n = getBigUint(key, RsaPrivateKeyNOffset, RsaPrivateKeyNLength);
94
+ const d = getBigUint(key, RsaPrivateKeyDOffset, RsaPrivateKeyDLength);
101
95
  return [n, d];
102
96
  }
103
97
 
98
+ function nonNegativeMod(m: number, d: number) {
99
+ const r = m % d;
100
+ if (r > 0) {
101
+ return r;
102
+ }
103
+ return r + (d > 0 ? d : -d);
104
+ }
105
+
106
+ // https://en.wikipedia.org/wiki/Modular_multiplicative_inverse
107
+ // Solve for the smallest positive `x` in the equation `a * x ≡ 1 (mod m)`,
108
+ // or in other words, `a * x % m = 1`
104
109
  // Taken from https://stackoverflow.com/a/51562038
105
- // I can't understand, but it does work
106
110
  // Only used with numbers smaller than 2^32 so doesn't need BigInt
107
111
  export function modInverse(a: number, m: number) {
108
- a = ((a % m) + m) % m;
112
+ a = nonNegativeMod(a, m);
109
113
  if (!a || m < 2) {
110
114
  return NaN; // invalid input
111
115
  }
@@ -116,6 +120,7 @@ export function modInverse(a: number, m: number) {
116
120
  [a, b] = [b, a % b];
117
121
  s.push({ a, b });
118
122
  }
123
+ /* istanbul ignore next */
119
124
  if (a !== 1) {
120
125
  return NaN; // inverse does not exists
121
126
  }
@@ -125,11 +130,14 @@ export function modInverse(a: number, m: number) {
125
130
  for (let i = s.length - 2; i >= 0; i -= 1) {
126
131
  [x, y] = [y, x - y * Math.floor(s[i]!.a / s[i]!.b)];
127
132
  }
128
- return ((y % m) + m) % m;
133
+ return nonNegativeMod(y, m);
129
134
  }
130
135
 
136
+ const ModulusLengthInBytes = 2048 / 8;
137
+ const ModulusLengthInWords = ModulusLengthInBytes / 4;
138
+
131
139
  export function adbGetPublicKeySize() {
132
- return 4 + 4 + 2048 / 8 + 2048 / 8 + 4;
140
+ return 4 + 4 + ModulusLengthInBytes + ModulusLengthInBytes + 4;
133
141
  }
134
142
 
135
143
  export function adbGeneratePublicKey(privateKey: Uint8Array): Uint8Array;
@@ -141,22 +149,27 @@ export function adbGeneratePublicKey(
141
149
  privateKey: Uint8Array,
142
150
  output?: Uint8Array,
143
151
  ): 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
152
+ // cspell: ignore: mincrypt
153
+ // Android 6 and earlier has its own encryption library called mincrypt
154
+ // This is the RSA public key format used by mincrypt:
155
+ // https://android.googlesource.com/platform/system/core/+/bb0c180e62703c2068a1b2c9f8ba6d634bf1553c/include/mincrypt/rsa.h#46
156
+ // `n0inv` and `rr` are pre-calculated to speed up RSA operations
157
+
158
+ // Android 7 switched its encryption library to BoringSSL, but still keeps the key format:
159
+ // https://android.googlesource.com/platform/system/core.git/+/91784040db2b9273687f88d8b95f729d4a61ecc2/libcrypto_utils/android_pubkey.cpp#38
160
+ // Except when reading a key, `n0inv` and `rr` are ignored (they are still populated when generating a key):
161
+ // https://android.googlesource.com/platform/system/core.git/+/91784040db2b9273687f88d8b95f729d4a61ecc2/libcrypto_utils/android_pubkey.cpp#55
162
+
163
+ // The public key is a struct (in little endian) of:
148
164
  //
149
165
  // [
150
166
  // 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
167
+ // // (the comment in Android source code is incorrect saying "This must be ANDROID_PUBKEY_MODULUS_SIZE")
168
+ // n0inv, // 32-bit integer, the modular inverse of (lower 32 bits of `n`)
169
+ // modulus, // `n`
154
170
  // rr, // Montgomery parameter R^2
155
- // exponent, // 32-bit integer, must be 65537
171
+ // exponent, // 32-bit integer, must be 3 or 65537
156
172
  // ]
157
- //
158
- // (All in little endian)
159
- // See https://android.googlesource.com/platform/system/core.git/+/91784040db2b9273687f88d8b95f729d4a61ecc2/libcrypto_utils/android_pubkey.cpp#38
160
173
 
161
174
  let outputType: "Uint8Array" | "number";
162
175
  const outputLength = adbGetPublicKeySize();
@@ -164,8 +177,8 @@ export function adbGeneratePublicKey(
164
177
  output = new Uint8Array(outputLength);
165
178
  outputType = "Uint8Array";
166
179
  } else {
167
- if (output.byteLength < outputLength) {
168
- throw new Error("output buffer is too small");
180
+ if (output.length < outputLength) {
181
+ throw new TypeError("output buffer is too small");
169
182
  }
170
183
 
171
184
  outputType = "number";
@@ -174,31 +187,30 @@ export function adbGeneratePublicKey(
174
187
  const outputView = new DataView(
175
188
  output.buffer,
176
189
  output.byteOffset,
177
- output.byteLength,
190
+ output.length,
178
191
  );
179
192
  let outputOffset = 0;
180
193
 
181
194
  // modulusLengthInWords
182
- outputView.setUint32(outputOffset, 2048 / 8 / 4, true);
195
+ outputView.setUint32(outputOffset, ModulusLengthInWords, true);
183
196
  outputOffset += 4;
184
197
 
185
198
  // extract `n` from private key
186
199
  const [n] = rsaParsePrivateKey(privateKey);
187
200
 
188
201
  // 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);
202
+ const n0inv = -modInverse(Number(n % 2n ** 32n), 2 ** 32);
203
+ outputView.setInt32(outputOffset, n0inv, true);
193
204
  outputOffset += 4;
194
205
 
195
- // Write n
196
- setBigUint(outputView, outputOffset, n, true);
197
- outputOffset += 256;
206
+ // Write `n` (a.k.a. `modulus`)
207
+ setBigUint(output, outputOffset, ModulusLengthInBytes, n, true);
208
+ outputOffset += ModulusLengthInBytes;
198
209
 
199
- // Calculate rr = (2^(rsa_size)) ^ 2 mod n
210
+ // Calculate rr = (2 ** (rsa_size)) ** 2 % n
200
211
  const rr = 2n ** 4096n % n;
201
- outputOffset += setBigUint(outputView, outputOffset, rr, true);
212
+ setBigUint(output, outputOffset, ModulusLengthInBytes, rr, true);
213
+ outputOffset += ModulusLengthInBytes;
202
214
 
203
215
  // exponent
204
216
  outputView.setUint32(outputOffset, 65537, true);
@@ -305,12 +317,11 @@ export function rsaSign(privateKey: Uint8Array, data: Uint8Array): Uint8Array {
305
317
 
306
318
  // Encryption
307
319
  // signature = padded ** d % n
308
- const view = new DataView(padded.buffer);
309
- const signature = powMod(getBigUint(view, 0, view.byteLength), d, n);
320
+ const signature = powMod(getBigUint(padded, 0, padded.length), d, n);
310
321
 
311
322
  // `padded` is not used anymore,
312
323
  // re-use the buffer to store the result
313
- setBigUint(view, 0, signature, false);
324
+ setBigUint(padded, 0, padded.length, signature, false);
314
325
 
315
326
  return padded;
316
327
  }
@@ -1,5 +1,5 @@
1
+ import type { MaybePromiseLike } from "@yume-chan/async";
1
2
  import type { Consumable, ReadableWritablePair } from "@yume-chan/stream-extra";
2
- import type { ValueOrPromise } from "@yume-chan/struct";
3
3
 
4
4
  import type { AdbPacketData, AdbPacketInit } from "./packet.js";
5
5
 
@@ -8,7 +8,7 @@ export interface AdbDaemonDevice {
8
8
 
9
9
  readonly name: string | undefined;
10
10
 
11
- connect(): ValueOrPromise<
11
+ connect(): MaybePromiseLike<
12
12
  ReadableWritablePair<AdbPacketData, Consumable<AdbPacketInit>>
13
13
  >;
14
14
  }
@@ -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 { EmptyUint8Array, 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,38 +255,51 @@ 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
+ EmptyUint8Array,
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
- payload = EMPTY_UINT8_ARRAY;
274
+ payload = EmptyUint8Array;
237
275
  }
238
276
 
239
277
  return this.sendPacket(AdbCommand.Okay, localId, remoteId, payload);
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
+ EmptyUint8Array,
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);
@@ -285,8 +334,13 @@ export class AdbPacketDispatcher implements Closeable {
285
334
  remoteId,
286
335
  this.options.initialDelayedAckBytes,
287
336
  );
288
- } catch (e) {
289
- await this.sendPacket(AdbCommand.Close, 0, remoteId);
337
+ } catch {
338
+ await this.sendPacket(
339
+ AdbCommand.Close,
340
+ 0,
341
+ remoteId,
342
+ EmptyUint8Array,
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,