@yume-chan/adb 0.0.12 → 0.0.13

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 (130) hide show
  1. package/CHANGELOG.json +18 -0
  2. package/CHANGELOG.md +10 -1
  3. package/README.md +24 -17
  4. package/esm/adb.d.ts +6 -6
  5. package/esm/adb.d.ts.map +1 -1
  6. package/esm/adb.js +77 -59
  7. package/esm/adb.js.map +1 -1
  8. package/esm/auth.d.ts +6 -6
  9. package/esm/auth.d.ts.map +1 -1
  10. package/esm/auth.js +8 -6
  11. package/esm/auth.js.map +1 -1
  12. package/esm/backend.d.ts +2 -2
  13. package/esm/commands/install.d.ts.map +1 -1
  14. package/esm/commands/install.js +5 -7
  15. package/esm/commands/install.js.map +1 -1
  16. package/esm/commands/reverse.d.ts +2 -2
  17. package/esm/commands/subprocess/index.d.ts +19 -20
  18. package/esm/commands/subprocess/index.d.ts.map +1 -1
  19. package/esm/commands/subprocess/index.js +36 -31
  20. package/esm/commands/subprocess/index.js.map +1 -1
  21. package/esm/commands/subprocess/legacy.d.ts +14 -7
  22. package/esm/commands/subprocess/legacy.d.ts.map +1 -1
  23. package/esm/commands/subprocess/legacy.js +14 -5
  24. package/esm/commands/subprocess/legacy.js.map +1 -1
  25. package/esm/commands/subprocess/protocol.d.ts +3 -2
  26. package/esm/commands/subprocess/protocol.d.ts.map +1 -1
  27. package/esm/commands/subprocess/protocol.js +6 -4
  28. package/esm/commands/subprocess/protocol.js.map +1 -1
  29. package/esm/commands/subprocess/protocols/index.d.ts +4 -0
  30. package/esm/commands/subprocess/protocols/index.d.ts.map +1 -0
  31. package/esm/commands/subprocess/protocols/index.js +4 -0
  32. package/esm/commands/subprocess/protocols/index.js.map +1 -0
  33. package/esm/commands/subprocess/protocols/none.d.ts +35 -0
  34. package/esm/commands/subprocess/protocols/none.d.ts.map +1 -0
  35. package/esm/commands/subprocess/protocols/none.js +53 -0
  36. package/esm/commands/subprocess/protocols/none.js.map +1 -0
  37. package/esm/commands/subprocess/protocols/shell.d.ts +39 -0
  38. package/esm/commands/subprocess/protocols/shell.d.ts.map +1 -0
  39. package/esm/commands/subprocess/protocols/shell.js +155 -0
  40. package/esm/commands/subprocess/protocols/shell.js.map +1 -0
  41. package/esm/commands/subprocess/protocols/types.d.ts +50 -0
  42. package/esm/commands/subprocess/protocols/types.d.ts.map +1 -0
  43. package/esm/commands/subprocess/protocols/types.js +2 -0
  44. package/esm/commands/subprocess/protocols/types.js.map +1 -0
  45. package/esm/commands/subprocess/types.d.ts +9 -6
  46. package/esm/commands/subprocess/types.d.ts.map +1 -1
  47. package/esm/commands/sync/list.d.ts +25 -1
  48. package/esm/commands/sync/list.d.ts.map +1 -1
  49. package/esm/commands/sync/list.js +40 -5
  50. package/esm/commands/sync/list.js.map +1 -1
  51. package/esm/commands/sync/request.d.ts +1 -0
  52. package/esm/commands/sync/request.d.ts.map +1 -1
  53. package/esm/commands/sync/request.js +1 -0
  54. package/esm/commands/sync/request.js.map +1 -1
  55. package/esm/commands/sync/response.d.ts +2 -1
  56. package/esm/commands/sync/response.d.ts.map +1 -1
  57. package/esm/commands/sync/response.js +2 -1
  58. package/esm/commands/sync/response.js.map +1 -1
  59. package/esm/commands/sync/stat.d.ts +13 -1
  60. package/esm/commands/sync/stat.d.ts.map +1 -1
  61. package/esm/commands/sync/stat.js +20 -7
  62. package/esm/commands/sync/stat.js.map +1 -1
  63. package/esm/commands/sync/sync.d.ts +13 -35
  64. package/esm/commands/sync/sync.d.ts.map +1 -1
  65. package/esm/commands/sync/sync.js +39 -9
  66. package/esm/commands/sync/sync.js.map +1 -1
  67. package/esm/crypto.d.ts +23 -10
  68. package/esm/crypto.d.ts.map +1 -1
  69. package/esm/crypto.js +64 -46
  70. package/esm/crypto.js.map +1 -1
  71. package/esm/features.d.ts +4 -2
  72. package/esm/features.d.ts.map +1 -1
  73. package/esm/features.js +6 -2
  74. package/esm/features.js.map +1 -1
  75. package/esm/packet.d.ts +21 -2
  76. package/esm/packet.d.ts.map +1 -1
  77. package/esm/packet.js +2 -2
  78. package/esm/packet.js.map +1 -1
  79. package/esm/socket/dispatcher.d.ts +15 -11
  80. package/esm/socket/dispatcher.d.ts.map +1 -1
  81. package/esm/socket/dispatcher.js +16 -19
  82. package/esm/socket/dispatcher.js.map +1 -1
  83. package/esm/socket/socket.d.ts +19 -7
  84. package/esm/socket/socket.d.ts.map +1 -1
  85. package/esm/socket/socket.js +20 -11
  86. package/esm/socket/socket.js.map +1 -1
  87. package/esm/stream/detect.d.ts +1 -17
  88. package/esm/stream/detect.d.ts.map +1 -1
  89. package/esm/stream/detect.js +4 -5
  90. package/esm/stream/detect.js.map +1 -1
  91. package/esm/stream/detect.polyfill.d.ts.map +1 -1
  92. package/esm/stream/detect.polyfill.js +4 -1
  93. package/esm/stream/detect.polyfill.js.map +1 -1
  94. package/esm/stream/transform.d.ts +23 -23
  95. package/esm/stream/transform.d.ts.map +1 -1
  96. package/esm/stream/transform.js +74 -24
  97. package/esm/stream/transform.js.map +1 -1
  98. package/esm/utils/encoding.d.ts.map +1 -1
  99. package/esm/utils/encoding.js.map +1 -1
  100. package/esm/utils/index.d.ts +1 -1
  101. package/esm/utils/index.d.ts.map +1 -1
  102. package/esm/utils/index.js +1 -1
  103. package/esm/utils/index.js.map +1 -1
  104. package/package.json +6 -6
  105. package/src/adb.ts +93 -70
  106. package/src/auth.ts +21 -18
  107. package/src/backend.ts +2 -2
  108. package/src/commands/install.ts +7 -9
  109. package/src/commands/reverse.ts +2 -2
  110. package/src/commands/subprocess/index.ts +57 -42
  111. package/src/commands/subprocess/protocols/index.ts +3 -0
  112. package/src/commands/subprocess/{legacy.ts → protocols/none.ts} +18 -8
  113. package/src/commands/subprocess/{protocol.ts → protocols/shell.ts} +22 -17
  114. package/src/commands/subprocess/{types.ts → protocols/types.ts} +13 -9
  115. package/src/commands/sync/list.ts +51 -5
  116. package/src/commands/sync/request.ts +1 -0
  117. package/src/commands/sync/response.ts +8 -2
  118. package/src/commands/sync/stat.ts +36 -9
  119. package/src/commands/sync/sync.ts +55 -19
  120. package/src/crypto.ts +72 -56
  121. package/src/features.ts +6 -2
  122. package/src/packet.ts +17 -6
  123. package/src/socket/dispatcher.ts +43 -29
  124. package/src/socket/socket.ts +29 -22
  125. package/src/stream/detect.polyfill.ts +4 -1
  126. package/src/stream/detect.ts +4 -24
  127. package/src/stream/transform.ts +96 -40
  128. package/src/utils/index.ts +1 -1
  129. package/tsconfig.build.tsbuildinfo +1 -1
  130. package/src/utils/encoding.ts +0 -13
@@ -4,11 +4,28 @@ import { AdbFeatures } from '../../features.js';
4
4
  import type { AdbSocket } from '../../socket/index.js';
5
5
  import { AdbBufferedStream, ReadableStream, WrapReadableStream, WrapWritableStream, WritableStream, WritableStreamDefaultWriter } from '../../stream/index.js';
6
6
  import { AutoResetEvent } from '../../utils/index.js';
7
- import { AdbSyncEntryResponse, adbSyncOpenDir } from './list.js';
7
+ import { escapeArg } from "../index.js";
8
+ import { adbSyncOpenDir, type AdbSyncEntry } from './list.js';
8
9
  import { adbSyncPull } from './pull.js';
9
10
  import { adbSyncPush } from './push.js';
10
11
  import { adbSyncLstat, adbSyncStat } from './stat.js';
11
12
 
13
+ /**
14
+ * A simplified `dirname` function that only handles absolute unix paths.
15
+ * @param path an absolute unix path
16
+ * @returns the directory name of the input path
17
+ */
18
+ export function dirname(path: string): string {
19
+ const end = path.lastIndexOf('/');
20
+ if (end === -1) {
21
+ throw new Error(`Invalid path`);
22
+ }
23
+ if (end === 0) {
24
+ return '/';
25
+ }
26
+ return path.substring(0, end);
27
+ }
28
+
12
29
  export class AdbSync extends AutoDisposable {
13
30
  protected adb: Adb;
14
31
 
@@ -22,6 +39,19 @@ export class AdbSync extends AutoDisposable {
22
39
  return this.adb.features!.includes(AdbFeatures.StatV2);
23
40
  }
24
41
 
42
+ public get supportsList2(): boolean {
43
+ return this.adb.features!.includes(AdbFeatures.ListV2);
44
+ }
45
+
46
+ public get fixedPushMkdir(): boolean {
47
+ return this.adb.features!.includes(AdbFeatures.FixedPushMkdir);
48
+ }
49
+
50
+ public get needPushMkdirWorkaround(): boolean {
51
+ // https://android.googlesource.com/platform/packages/modules/adb/+/91768a57b7138166e0a3d11f79cd55909dda7014/client/file_sync_client.cpp#1361
52
+ return this.adb.features!.includes(AdbFeatures.ShellV2) && !this.fixedPushMkdir;
53
+ }
54
+
25
55
  public constructor(adb: Adb, socket: AdbSocket) {
26
56
  super();
27
57
 
@@ -65,18 +95,18 @@ export class AdbSync extends AutoDisposable {
65
95
 
66
96
  public async *opendir(
67
97
  path: string
68
- ): AsyncGenerator<AdbSyncEntryResponse, void, void> {
98
+ ): AsyncGenerator<AdbSyncEntry, void, void> {
69
99
  await this.sendLock.wait();
70
100
 
71
101
  try {
72
- yield* adbSyncOpenDir(this.stream, this.writer, path);
102
+ yield* adbSyncOpenDir(this.stream, this.writer, path, this.supportsList2);
73
103
  } finally {
74
104
  this.sendLock.notify();
75
105
  }
76
106
  }
77
107
 
78
108
  public async readdir(path: string) {
79
- const results: AdbSyncEntryResponse[] = [];
109
+ const results: AdbSyncEntry[] = [];
80
110
  for await (const entry of this.opendir(path)) {
81
111
  results.push(entry);
82
112
  }
@@ -90,13 +120,10 @@ export class AdbSync extends AutoDisposable {
90
120
  * @returns A `ReadableStream` that reads from the file.
91
121
  */
92
122
  public read(filename: string): ReadableStream<Uint8Array> {
93
- return new WrapReadableStream<Uint8Array, ReadableStream<Uint8Array>, undefined>({
123
+ return new WrapReadableStream({
94
124
  start: async () => {
95
125
  await this.sendLock.wait();
96
- return {
97
- readable: adbSyncPull(this.stream, this.writer, filename),
98
- state: undefined,
99
- };
126
+ return adbSyncPull(this.stream, this.writer, filename);
100
127
  },
101
128
  close: async () => {
102
129
  this.sendLock.notify();
@@ -120,16 +147,25 @@ export class AdbSync extends AutoDisposable {
120
147
  return new WrapWritableStream({
121
148
  start: async () => {
122
149
  await this.sendLock.wait();
123
- return {
124
- writable: adbSyncPush(
125
- this.stream,
126
- this.writer,
127
- filename,
128
- mode,
129
- mtime,
130
- ),
131
- state: undefined,
132
- };
150
+
151
+ if (this.needPushMkdirWorkaround) {
152
+ // It may fail if the path is already existed.
153
+ // Ignore the result.
154
+ // TODO: sync: test this
155
+ await this.adb.subprocess.spawnAndWait([
156
+ 'mkdir',
157
+ '-p',
158
+ escapeArg(dirname(filename)),
159
+ ]);
160
+ }
161
+
162
+ return adbSyncPush(
163
+ this.stream,
164
+ this.writer,
165
+ filename,
166
+ mode,
167
+ mtime,
168
+ );
133
169
  },
134
170
  close: async () => {
135
171
  this.sendLock.notify();
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
- export function getBig(
9
- array: Uint8Array,
10
- offset = 0,
11
- length = array.byteLength - offset
12
- ): bigint {
13
- const view = new DataView(array.buffer, array.byteOffset, array.byteLength);
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 = offset; i < offset + length; i += 8) {
21
+ for (let i = byteOffset; i < byteOffset + length; i += 8) {
21
22
  result <<= BigInt64;
22
- const value = getBigUint64(view, i, false);
23
+ const value = getBigUint64(dataView, i, false);
23
24
  result += value;
24
25
  }
25
26
 
26
27
  return result;
27
28
  }
28
29
 
29
- export function setBig(buffer: ArrayBuffer, value: bigint, offset: number = 0) {
30
- const uint64Array: bigint[] = [];
31
- while (value > BigInt0) {
32
- uint64Array.push(BigInt.asUintN(64, value));
33
- value >>= BigInt64;
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
- const view = new DataView(buffer);
37
- for (let i = uint64Array.length - 1; i >= 0; i -= 1) {
38
- setBigUint64(view, offset, uint64Array[i]!, false);
39
- offset += 8;
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
- export function setBigLE(array: Uint8Array, value: bigint, offset = 0) {
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
- let n = getBig(key, RsaPrivateKeyNOffset, RsaPrivateKeyNLength);
80
- let d = getBig(key, RsaPrivateKeyDOffset, RsaPrivateKeyDLength);
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 (lower 32 bits of) n
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
- setBigLE(output, n, outputOffset);
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
- setBigLE(output, rr, outputOffset);
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 Sha1DigestLength = 20;
232
+ export const SHA1_DIGEST_LENGTH = 20;
222
233
 
223
- export const Asn1Sequence = 0x30;
224
- export const Asn1OctetString = 0x04;
225
- export const Asn1Null = 0x05;
226
- export const Asn1Oid = 0x06;
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 Sha1DigestInfo = new Uint8Array([
230
- Asn1Sequence, 0x0d + Sha1DigestLength,
231
- Asn1Sequence, 0x09,
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
- Asn1Oid, 0x05, 1 * 40 + 3, 14, 3, 2, 26,
234
- Asn1Null, 0x00,
235
- Asn1OctetString, Sha1DigestLength
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): ArrayBuffer {
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 - Sha1DigestInfo.length - data.length - 1;
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(Sha1DigestInfo, index);
267
- index += Sha1DigestInfo.length;
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
- let signature = powMod(getBig(padded), d, n);
274
-
275
- // Put into an ArrayBuffer
276
- const result = new ArrayBuffer(256);
277
- setBig(result, signature);
278
-
279
- return result;
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
- StatV2 = 'stat_v2',
4
+ ShellV2 = "shell_v2",
3
5
  Cmd = 'cmd',
4
- ShellV2 = "shell_v2"
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
- // All the useful fields
32
- export type AdbPacketCore = Omit<typeof AdbPacket['TInit'], 'checksum' | 'magic'>;
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: AdbPacketCore): AdbPacketInit;
39
- export function calculateChecksum(payload: Uint8Array | AdbPacketCore): number | AdbPacketInit {
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 an new object.
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
 
@@ -1,14 +1,14 @@
1
1
  import { AsyncOperationManager, PromiseResolver } from '@yume-chan/async';
2
2
  import { AutoDisposable, EventEmitter } from '@yume-chan/event';
3
- import { AdbCommand, calculateChecksum, type AdbPacketCore, type AdbPacketInit } from '../packet.js';
3
+ import { AdbCommand, calculateChecksum, type AdbPacketData, type AdbPacketInit } from '../packet.js';
4
4
  import { AbortController, WritableStream, WritableStreamDefaultWriter, type ReadableWritablePair } from '../stream/index.js';
5
5
  import { decodeUtf8, encodeUtf8 } from '../utils/index.js';
6
- import { AdbSocket } from './socket.js';
6
+ import { AdbSocket, AdbSocketController } from './socket.js';
7
7
 
8
8
  export interface AdbIncomingSocketEventArgs {
9
9
  handled: boolean;
10
10
 
11
- packet: AdbPacketCore;
11
+ packet: AdbPacketData;
12
12
 
13
13
  serviceString: string;
14
14
 
@@ -17,17 +17,27 @@ export interface AdbIncomingSocketEventArgs {
17
17
 
18
18
  const EmptyUint8Array = new Uint8Array(0);
19
19
 
20
+ export interface AdbPacketDispatcherOptions {
21
+ calculateChecksum: boolean;
22
+ /**
23
+ * Before Android 9.0, ADB uses `char*` to parse service string,
24
+ * thus requires a null character to terminate.
25
+ *
26
+ * Usually it should have the same value as `calculateChecksum`.
27
+ */
28
+ appendNullToServiceString: boolean;
29
+ maxPayloadSize: number;
30
+ }
31
+
20
32
  export class AdbPacketDispatcher extends AutoDisposable {
21
33
  // ADB socket id starts from 1
22
34
  // (0 means open failed)
23
35
  private readonly initializers = new AsyncOperationManager(1);
24
- private readonly sockets = new Map<number, AdbSocket>();
36
+ private readonly sockets = new Map<number, AdbSocketController>();
25
37
 
26
38
  private _writer!: WritableStreamDefaultWriter<AdbPacketInit>;
27
39
 
28
- public maxPayloadSize = 0;
29
- public calculateChecksum = true;
30
- public appendNullToServiceString = true;
40
+ public readonly options: AdbPacketDispatcherOptions;
31
41
 
32
42
  private _disconnected = new PromiseResolver<void>();
33
43
  public get disconnected() { return this._disconnected.promise; }
@@ -35,16 +45,16 @@ export class AdbPacketDispatcher extends AutoDisposable {
35
45
  private readonly incomingSocketEvent = this.addDisposable(new EventEmitter<AdbIncomingSocketEventArgs>());
36
46
  public get onIncomingSocket() { return this.incomingSocketEvent.event; }
37
47
 
38
- private readonly errorEvent = this.addDisposable(new EventEmitter<Error>());
39
- public get onError() { return this.errorEvent.event; }
40
-
41
48
  private _abortController = new AbortController();
42
49
 
43
50
  public constructor(
44
- connection: ReadableWritablePair<AdbPacketCore, AdbPacketInit>,
51
+ connection: ReadableWritablePair<AdbPacketData, AdbPacketInit>,
52
+ options: AdbPacketDispatcherOptions
45
53
  ) {
46
54
  super();
47
55
 
56
+ this.options = options;
57
+
48
58
  connection.readable
49
59
  .pipeTo(new WritableStream({
50
60
  write: async (packet) => {
@@ -70,8 +80,6 @@ export class AdbPacketDispatcher extends AutoDisposable {
70
80
  return;
71
81
  }
72
82
  } catch (e) {
73
- this.errorEvent.fire(e as Error);
74
-
75
83
  // Throw error here will stop the pipe
76
84
  // But won't close `readable` because of `preventCancel: true`
77
85
  throw e;
@@ -83,15 +91,15 @@ export class AdbPacketDispatcher extends AutoDisposable {
83
91
  })
84
92
  .then(() => {
85
93
  this.dispose();
86
- }, () => {
87
- // TODO: AdbPacketDispatcher: reject `_disconnected` when pipe errored?
94
+ }, (e) => {
95
+ this._disconnected.reject(e);
88
96
  this.dispose();
89
97
  });
90
98
 
91
99
  this._writer = connection.writable.getWriter();
92
100
  }
93
101
 
94
- private handleOk(packet: AdbPacketCore) {
102
+ private handleOk(packet: AdbPacketData) {
95
103
  if (this.initializers.resolve(packet.arg1, packet.arg0)) {
96
104
  // Device successfully created the socket
97
105
  return;
@@ -109,7 +117,7 @@ export class AdbPacketDispatcher extends AutoDisposable {
109
117
  this.sendPacket(AdbCommand.Close, packet.arg1, packet.arg0);
110
118
  }
111
119
 
112
- private async handleClose(packet: AdbPacketCore) {
120
+ private async handleClose(packet: AdbPacketData) {
113
121
  // From https://android.googlesource.com/platform/packages/modules/adb/+/65d18e2c1cc48b585811954892311b28a4c3d188/adb.cpp#459
114
122
  /* According to protocol.txt, p->msg.arg0 might be 0 to indicate
115
123
  * a failed OPEN only. However, due to a bug in previous ADB
@@ -139,7 +147,7 @@ export class AdbPacketDispatcher extends AutoDisposable {
139
147
  // Just ignore it
140
148
  }
141
149
 
142
- private async handleOpen(packet: AdbPacketCore) {
150
+ private async handleOpen(packet: AdbPacketData) {
143
151
  // AsyncOperationManager doesn't support get and skip an ID
144
152
  // Use `add` + `resolve` to simulate this behavior
145
153
  const [localId] = this.initializers.add<number>();
@@ -148,7 +156,7 @@ export class AdbPacketDispatcher extends AutoDisposable {
148
156
  const remoteId = packet.arg0;
149
157
  const serviceString = decodeUtf8(packet.payload);
150
158
 
151
- const socket = new AdbSocket({
159
+ const controller = new AdbSocketController({
152
160
  dispatcher: this,
153
161
  localId,
154
162
  remoteId,
@@ -160,12 +168,12 @@ export class AdbPacketDispatcher extends AutoDisposable {
160
168
  handled: false,
161
169
  packet,
162
170
  serviceString,
163
- socket,
171
+ socket: controller.socket,
164
172
  };
165
173
  this.incomingSocketEvent.fire(args);
166
174
 
167
175
  if (args.handled) {
168
- this.sockets.set(localId, socket);
176
+ this.sockets.set(localId, controller);
169
177
  await this.sendPacket(AdbCommand.OK, localId, remoteId);
170
178
  } else {
171
179
  await this.sendPacket(AdbCommand.Close, 0, remoteId);
@@ -173,24 +181,30 @@ export class AdbPacketDispatcher extends AutoDisposable {
173
181
  }
174
182
 
175
183
  public async createSocket(serviceString: string): Promise<AdbSocket> {
176
- if (this.appendNullToServiceString) {
184
+ if (this.options.appendNullToServiceString) {
177
185
  serviceString += '\0';
178
186
  }
179
187
 
180
188
  const [localId, initializer] = this.initializers.add<number>();
181
- await this.sendPacket(AdbCommand.Open, localId, 0, serviceString);
189
+ await this.sendPacket(
190
+ AdbCommand.Open,
191
+ localId,
192
+ 0,
193
+ serviceString
194
+ );
182
195
 
196
+ // Fulfilled by `handleOk`
183
197
  const remoteId = await initializer;
184
- const socket = new AdbSocket({
198
+ const controller = new AdbSocketController({
185
199
  dispatcher: this,
186
200
  localId,
187
201
  remoteId,
188
202
  localCreated: true,
189
203
  serviceString,
190
204
  });
191
- this.sockets.set(localId, socket);
205
+ this.sockets.set(localId, controller);
192
206
 
193
- return socket;
207
+ return controller.socket;
194
208
  }
195
209
 
196
210
  public sendPacket(packet: AdbPacketInit): Promise<void>;
@@ -206,7 +220,7 @@ export class AdbPacketDispatcher extends AutoDisposable {
206
220
  arg1?: number,
207
221
  payload: string | Uint8Array = EmptyUint8Array,
208
222
  ): Promise<void> {
209
- let init: AdbPacketCore;
223
+ let init: AdbPacketData;
210
224
  if (arg0 === undefined) {
211
225
  init = packetOrCommand as AdbPacketInit;
212
226
  } else {
@@ -223,11 +237,11 @@ export class AdbPacketDispatcher extends AutoDisposable {
223
237
  }
224
238
 
225
239
  if (init.payload &&
226
- init.payload.byteLength > this.maxPayloadSize) {
240
+ init.payload.byteLength > this.options.maxPayloadSize) {
227
241
  throw new Error('payload too large');
228
242
  }
229
243
 
230
- if (this.calculateChecksum) {
244
+ if (this.options.calculateChecksum) {
231
245
  calculateChecksum(init);
232
246
  } else {
233
247
  (init as AdbPacketInit).checksum = 0;