@yume-chan/adb 0.0.11 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/CHANGELOG.json +39 -0
  2. package/CHANGELOG.md +23 -1
  3. package/README.md +24 -17
  4. package/esm/adb.d.ts +10 -9
  5. package/esm/adb.d.ts.map +1 -1
  6. package/esm/adb.js +87 -64
  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 +7 -10
  17. package/esm/commands/reverse.d.ts.map +1 -1
  18. package/esm/commands/reverse.js +9 -15
  19. package/esm/commands/reverse.js.map +1 -1
  20. package/esm/commands/subprocess/index.d.ts +19 -20
  21. package/esm/commands/subprocess/index.d.ts.map +1 -1
  22. package/esm/commands/subprocess/index.js +36 -31
  23. package/esm/commands/subprocess/index.js.map +1 -1
  24. package/esm/commands/subprocess/legacy.d.ts +14 -7
  25. package/esm/commands/subprocess/legacy.d.ts.map +1 -1
  26. package/esm/commands/subprocess/legacy.js +14 -5
  27. package/esm/commands/subprocess/legacy.js.map +1 -1
  28. package/esm/commands/subprocess/protocol.d.ts +3 -2
  29. package/esm/commands/subprocess/protocol.d.ts.map +1 -1
  30. package/esm/commands/subprocess/protocol.js +6 -4
  31. package/esm/commands/subprocess/protocol.js.map +1 -1
  32. package/esm/commands/subprocess/protocols/index.d.ts +4 -0
  33. package/esm/commands/subprocess/protocols/index.d.ts.map +1 -0
  34. package/esm/commands/subprocess/protocols/index.js +4 -0
  35. package/esm/commands/subprocess/protocols/index.js.map +1 -0
  36. package/esm/commands/subprocess/protocols/none.d.ts +35 -0
  37. package/esm/commands/subprocess/protocols/none.d.ts.map +1 -0
  38. package/esm/commands/subprocess/protocols/none.js +53 -0
  39. package/esm/commands/subprocess/protocols/none.js.map +1 -0
  40. package/esm/commands/subprocess/protocols/shell.d.ts +39 -0
  41. package/esm/commands/subprocess/protocols/shell.d.ts.map +1 -0
  42. package/esm/commands/subprocess/protocols/shell.js +155 -0
  43. package/esm/commands/subprocess/protocols/shell.js.map +1 -0
  44. package/esm/commands/subprocess/protocols/types.d.ts +50 -0
  45. package/esm/commands/subprocess/protocols/types.d.ts.map +1 -0
  46. package/esm/commands/subprocess/protocols/types.js +2 -0
  47. package/esm/commands/subprocess/protocols/types.js.map +1 -0
  48. package/esm/commands/subprocess/types.d.ts +9 -6
  49. package/esm/commands/subprocess/types.d.ts.map +1 -1
  50. package/esm/commands/sync/list.d.ts +25 -1
  51. package/esm/commands/sync/list.d.ts.map +1 -1
  52. package/esm/commands/sync/list.js +40 -5
  53. package/esm/commands/sync/list.js.map +1 -1
  54. package/esm/commands/sync/pull.d.ts.map +1 -1
  55. package/esm/commands/sync/pull.js +6 -3
  56. package/esm/commands/sync/pull.js.map +1 -1
  57. package/esm/commands/sync/request.d.ts +1 -0
  58. package/esm/commands/sync/request.d.ts.map +1 -1
  59. package/esm/commands/sync/request.js +1 -0
  60. package/esm/commands/sync/request.js.map +1 -1
  61. package/esm/commands/sync/response.d.ts +2 -1
  62. package/esm/commands/sync/response.d.ts.map +1 -1
  63. package/esm/commands/sync/response.js +2 -1
  64. package/esm/commands/sync/response.js.map +1 -1
  65. package/esm/commands/sync/stat.d.ts +13 -1
  66. package/esm/commands/sync/stat.d.ts.map +1 -1
  67. package/esm/commands/sync/stat.js +20 -7
  68. package/esm/commands/sync/stat.js.map +1 -1
  69. package/esm/commands/sync/sync.d.ts +13 -35
  70. package/esm/commands/sync/sync.d.ts.map +1 -1
  71. package/esm/commands/sync/sync.js +39 -9
  72. package/esm/commands/sync/sync.js.map +1 -1
  73. package/esm/crypto.d.ts +23 -10
  74. package/esm/crypto.d.ts.map +1 -1
  75. package/esm/crypto.js +64 -46
  76. package/esm/crypto.js.map +1 -1
  77. package/esm/features.d.ts +4 -2
  78. package/esm/features.d.ts.map +1 -1
  79. package/esm/features.js +6 -2
  80. package/esm/features.js.map +1 -1
  81. package/esm/packet.d.ts +21 -2
  82. package/esm/packet.d.ts.map +1 -1
  83. package/esm/packet.js +2 -2
  84. package/esm/packet.js.map +1 -1
  85. package/esm/socket/dispatcher.d.ts +34 -23
  86. package/esm/socket/dispatcher.d.ts.map +1 -1
  87. package/esm/socket/dispatcher.js +92 -70
  88. package/esm/socket/dispatcher.js.map +1 -1
  89. package/esm/socket/socket.d.ts +29 -8
  90. package/esm/socket/socket.d.ts.map +1 -1
  91. package/esm/socket/socket.js +41 -27
  92. package/esm/socket/socket.js.map +1 -1
  93. package/esm/stream/detect.d.ts +1 -17
  94. package/esm/stream/detect.d.ts.map +1 -1
  95. package/esm/stream/detect.js +4 -5
  96. package/esm/stream/detect.js.map +1 -1
  97. package/esm/stream/detect.polyfill.d.ts.map +1 -1
  98. package/esm/stream/detect.polyfill.js +4 -1
  99. package/esm/stream/detect.polyfill.js.map +1 -1
  100. package/esm/stream/transform.d.ts +37 -32
  101. package/esm/stream/transform.d.ts.map +1 -1
  102. package/esm/stream/transform.js +106 -92
  103. package/esm/stream/transform.js.map +1 -1
  104. package/esm/utils/encoding.d.ts.map +1 -1
  105. package/esm/utils/encoding.js.map +1 -1
  106. package/esm/utils/index.d.ts +1 -1
  107. package/esm/utils/index.d.ts.map +1 -1
  108. package/esm/utils/index.js +1 -1
  109. package/esm/utils/index.js.map +1 -1
  110. package/package.json +6 -6
  111. package/src/adb.ts +105 -76
  112. package/src/auth.ts +21 -18
  113. package/src/backend.ts +2 -2
  114. package/src/commands/install.ts +7 -9
  115. package/src/commands/reverse.ts +13 -24
  116. package/src/commands/subprocess/index.ts +57 -42
  117. package/src/commands/subprocess/protocols/index.ts +3 -0
  118. package/src/commands/subprocess/{legacy.ts → protocols/none.ts} +20 -10
  119. package/src/commands/subprocess/{protocol.ts → protocols/shell.ts} +22 -17
  120. package/src/commands/subprocess/{types.ts → protocols/types.ts} +13 -9
  121. package/src/commands/sync/list.ts +51 -5
  122. package/src/commands/sync/pull.ts +6 -3
  123. package/src/commands/sync/request.ts +1 -0
  124. package/src/commands/sync/response.ts +8 -2
  125. package/src/commands/sync/stat.ts +36 -9
  126. package/src/commands/sync/sync.ts +55 -19
  127. package/src/crypto.ts +72 -56
  128. package/src/features.ts +6 -2
  129. package/src/packet.ts +17 -6
  130. package/src/socket/dispatcher.ts +133 -88
  131. package/src/socket/socket.ts +61 -48
  132. package/src/stream/detect.polyfill.ts +4 -1
  133. package/src/stream/detect.ts +4 -24
  134. package/src/stream/transform.ts +133 -116
  135. package/src/utils/index.ts +1 -1
  136. package/tsconfig.build.tsbuildinfo +1 -1
  137. package/src/utils/encoding.ts +0 -13
@@ -0,0 +1,3 @@
1
+ export * from './none.js';
2
+ export * from './shell.js';
3
+ export * from './types.js';
@@ -1,6 +1,6 @@
1
- import type { Adb } from "../../adb.js";
2
- import type { AdbSocket } from "../../socket/index.js";
3
- import { DuplexStreamFactory, type ReadableStream } from "../../stream/index.js";
1
+ import type { Adb } from "../../../adb.js";
2
+ import type { AdbSocket } from "../../../socket/index.js";
3
+ import { DuplexStreamFactory, ReadableStream } from "../../../stream/index.js";
4
4
  import type { AdbSubprocessProtocol } from "./types.js";
5
5
 
6
6
  /**
@@ -11,11 +11,17 @@ import type { AdbSubprocessProtocol } from "./types.js";
11
11
  * * `exit` exit code: No
12
12
  * * `resize`: No
13
13
  */
14
- export class AdbNoneSubprocessProtocol implements AdbSubprocessProtocol {
14
+ export class AdbSubprocessNoneProtocol implements AdbSubprocessProtocol {
15
15
  public static isSupported() { return true; }
16
16
 
17
- public static async spawn(adb: Adb, command: string) {
18
- return new AdbNoneSubprocessProtocol(await adb.createSocket(`shell:${command}`));
17
+ public static async pty(adb: Adb, command: string) {
18
+ return new AdbSubprocessNoneProtocol(await adb.createSocket(`shell:${command}`));
19
+ }
20
+
21
+ public static async raw(adb: Adb, command: string) {
22
+ // `shell,raw:${command}` also triggers raw mode,
23
+ // But is not supported before Android 7.
24
+ return new AdbSubprocessNoneProtocol(await adb.createSocket(`exec:${command}`));
19
25
  }
20
26
 
21
27
  private readonly socket: AdbSocket;
@@ -24,11 +30,15 @@ export class AdbNoneSubprocessProtocol implements AdbSubprocessProtocol {
24
30
  public get stdin() { return this.socket.writable; }
25
31
 
26
32
  private _stdout: ReadableStream<Uint8Array>;
27
- // Legacy shell doesn't support splitting output streams.
33
+ /**
34
+ * Legacy shell mixes stdout and stderr.
35
+ */
28
36
  public get stdout() { return this._stdout; }
29
37
 
30
- // `stderr` of Legacy shell is always empty.
31
38
  private _stderr: ReadableStream<Uint8Array>;
39
+ /**
40
+ * `stderr` will always be empty.
41
+ */
32
42
  public get stderr() { return this._stderr; }
33
43
 
34
44
  private _exit: Promise<number>;
@@ -43,8 +53,8 @@ export class AdbNoneSubprocessProtocol implements AdbSubprocessProtocol {
43
53
  },
44
54
  });
45
55
 
46
- this._stdout = factory.createWrapReadable(this.socket.readable);
47
- this._stderr = factory.createReadable();
56
+ this._stdout = factory.wrapReadable(this.socket.readable);
57
+ this._stderr = factory.wrapReadable(new ReadableStream());
48
58
  this._exit = factory.closed.then(() => 0);
49
59
  }
50
60
 
@@ -1,10 +1,10 @@
1
1
  import { PromiseResolver } from "@yume-chan/async";
2
2
  import Struct, { placeholder, type StructValueType } from "@yume-chan/struct";
3
- import type { Adb } from "../../adb.js";
4
- import { AdbFeatures } from "../../features.js";
5
- import type { AdbSocket } from "../../socket/index.js";
6
- import { PushReadableStream, ReadableStream, StructDeserializeStream, StructSerializeStream, TransformStream, WritableStream, WritableStreamDefaultWriter, type PushReadableStreamController } from "../../stream/index.js";
7
- import { encodeUtf8 } from "../../utils/index.js";
3
+ import type { Adb } from "../../../adb.js";
4
+ import { AdbFeatures } from "../../../features.js";
5
+ import type { AdbSocket } from "../../../socket/index.js";
6
+ import { pipeFrom, PushReadableStream, ReadableStream, StructDeserializeStream, StructSerializeStream, TransformStream, WritableStream, WritableStreamDefaultWriter, type PushReadableStreamController } from "../../../stream/index.js";
7
+ import { encodeUtf8 } from "../../../utils/index.js";
8
8
  import type { AdbSubprocessProtocol } from "./types.js";
9
9
 
10
10
  export enum AdbShellProtocolId {
@@ -17,10 +17,11 @@ export enum AdbShellProtocolId {
17
17
  }
18
18
 
19
19
  // This packet format is used in both direction.
20
- const AdbShellProtocolPacket = new Struct({ littleEndian: true })
21
- .uint8('id', placeholder<AdbShellProtocolId>())
22
- .uint32('length')
23
- .uint8Array('data', { lengthField: 'length' });
20
+ const AdbShellProtocolPacket =
21
+ new Struct({ littleEndian: true })
22
+ .uint8('id', placeholder<AdbShellProtocolId>())
23
+ .uint32('length')
24
+ .uint8Array('data', { lengthField: 'length' });
24
25
 
25
26
  type AdbShellProtocolPacketInit = typeof AdbShellProtocolPacket['TInit'];
26
27
 
@@ -99,15 +100,18 @@ class MultiplexStream<T>{
99
100
  * * `exit` exit code: Yes
100
101
  * * `resize`: Yes
101
102
  */
102
- export class AdbShellSubprocessProtocol implements AdbSubprocessProtocol {
103
+ export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
103
104
  public static isSupported(adb: Adb) {
104
105
  return adb.features!.includes(AdbFeatures.ShellV2);
105
106
  }
106
107
 
107
- public static async spawn(adb: Adb, command: string) {
108
- // TODO: AdbShellSubprocessProtocol: Support raw mode
108
+ public static async pty(adb: Adb, command: string) {
109
109
  // TODO: AdbShellSubprocessProtocol: Support setting `XTERM` environment variable
110
- return new AdbShellSubprocessProtocol(await adb.createSocket(`shell,v2,pty:${command}`));
110
+ return new AdbSubprocessShellProtocol(await adb.createSocket(`shell,v2,pty:${command}`));
111
+ }
112
+
113
+ public static async raw(adb: Adb, command: string) {
114
+ return new AdbSubprocessShellProtocol(await adb.createSocket(`shell,v2,raw:${command}`));
111
115
  }
112
116
 
113
117
  private readonly _socket: AdbSocket;
@@ -140,7 +144,7 @@ export class AdbShellSubprocessProtocol implements AdbSubprocessProtocol {
140
144
  transform: (chunk, controller) => {
141
145
  if (chunk.id === AdbShellProtocolId.Exit) {
142
146
  this._exit.resolve(new Uint8Array(chunk.data)[0]!);
143
- // We can let `StdoutTransformStream` to process `AdbShellProtocolId.Exit`,
147
+ // We can let `StdoutDeserializeStream` to process `AdbShellProtocolId.Exit`,
144
148
  // but since we need this `TransformStream` to capture the exit code anyway,
145
149
  // terminating child streams here is killing two birds with one stone.
146
150
  controller.terminate();
@@ -160,9 +164,10 @@ export class AdbShellSubprocessProtocol implements AdbSubprocessProtocol {
160
164
  .pipeThrough(new StructSerializeStream(AdbShellProtocolPacket))
161
165
  .pipeTo(socket.writable);
162
166
 
163
- const { readable, writable } = new StdinSerializeStream();
164
- this._stdin = writable;
165
- readable.pipeTo(multiplexer.createWriteable());
167
+ this._stdin = pipeFrom(
168
+ multiplexer.createWriteable(),
169
+ new StdinSerializeStream()
170
+ );
166
171
 
167
172
  this._socketWriter = multiplexer.createWriteable().getWriter();
168
173
  }
@@ -1,7 +1,7 @@
1
1
  import type { ValueOrPromise } from "@yume-chan/struct";
2
- import type { Adb } from "../../adb.js";
3
- import type { AdbSocket } from "../../socket/index.js";
4
- import type { ReadableStream, WritableStream } from "../../stream/index.js";
2
+ import type { Adb } from "../../../adb.js";
3
+ import type { AdbSocket } from "../../../socket/index.js";
4
+ import type { ReadableStream, WritableStream } from "../../../stream/index.js";
5
5
 
6
6
  export interface AdbSubprocessProtocol {
7
7
  /**
@@ -17,7 +17,7 @@ export interface AdbSubprocessProtocol {
17
17
  /**
18
18
  * The `stderr` pipe of the process.
19
19
  *
20
- * Note: Some `AdbShell` doesn't separate `stdout` and `stderr`,
20
+ * Note: Some `AdbSubprocessProtocol` doesn't separate `stdout` and `stderr`,
21
21
  * All output will be sent to `stdout`.
22
22
  */
23
23
  readonly stderr: ReadableStream<Uint8Array>;
@@ -25,15 +25,16 @@ export interface AdbSubprocessProtocol {
25
25
  /**
26
26
  * A `Promise` that resolves to the exit code of the process.
27
27
  *
28
- * Note: Some `AdbShell` doesn't support exit code,
29
- * They will always resolve with `0`.
28
+ * Note: Some `AdbSubprocessProtocol` doesn't support exit code,
29
+ * They will always resolve it with `0`.
30
30
  */
31
31
  readonly exit: Promise<number>;
32
32
 
33
33
  /**
34
34
  * Resizes the current shell.
35
35
  *
36
- * Some `AdbShell`s may not support resizing and will always ignore calls to this method.
36
+ * Some `AdbSubprocessProtocol`s may not support resizing
37
+ * and will ignore calls to this method.
37
38
  */
38
39
  resize(rows: number, cols: number): ValueOrPromise<void>;
39
40
 
@@ -47,8 +48,11 @@ export interface AdbSubprocessProtocolConstructor {
47
48
  /** Returns `true` if the `adb` instance supports this shell */
48
49
  isSupported(adb: Adb): ValueOrPromise<boolean>;
49
50
 
50
- /** Creates a new `AdbShell` using the specified `Adb` and `command` */
51
- spawn(adb: Adb, command: string): ValueOrPromise<AdbSubprocessProtocol>;
51
+ /** Spawns an executable in PTY (interactive) mode. */
52
+ pty(adb: Adb, command: string): ValueOrPromise<AdbSubprocessProtocol>;
53
+
54
+ /** Spawns an executable and pipe the output. */
55
+ raw(adb: Adb, command: string): ValueOrPromise<AdbSubprocessProtocol>;
52
56
 
53
57
  /** Creates a new `AdbShell` by attaching to an exist `AdbSocket` */
54
58
  new(socket: AdbSocket): AdbSubprocessProtocol;
@@ -2,7 +2,11 @@ import Struct from '@yume-chan/struct';
2
2
  import type { AdbBufferedStream, WritableStreamDefaultWriter } from '../../stream/index.js';
3
3
  import { AdbSyncRequestId, adbSyncWriteRequest } from './request.js';
4
4
  import { AdbSyncDoneResponse, adbSyncReadResponse, AdbSyncResponseId } from './response.js';
5
- import { AdbSyncLstatResponse } from './stat.js';
5
+ import { AdbSyncLstatResponse, AdbSyncStatResponse, type AdbSyncStat } from './stat.js';
6
+
7
+ export interface AdbSyncEntry extends AdbSyncStat {
8
+ name: string;
9
+ }
6
10
 
7
11
  export const AdbSyncEntryResponse =
8
12
  new Struct({ littleEndian: true })
@@ -13,22 +17,64 @@ export const AdbSyncEntryResponse =
13
17
 
14
18
  export type AdbSyncEntryResponse = typeof AdbSyncEntryResponse['TDeserializeResult'];
15
19
 
16
- const ResponseTypes = {
20
+ export const AdbSyncEntry2Response =
21
+ new Struct({ littleEndian: true })
22
+ .fields(AdbSyncStatResponse)
23
+ .uint32('nameLength')
24
+ .string('name', { lengthField: 'nameLength' })
25
+ .extra({ id: AdbSyncResponseId.Entry2 as const });
26
+
27
+ export type AdbSyncEntry2Response = typeof AdbSyncEntry2Response['TDeserializeResult'];
28
+
29
+ const LIST_V1_RESPONSE_TYPES = {
17
30
  [AdbSyncResponseId.Entry]: AdbSyncEntryResponse,
18
31
  [AdbSyncResponseId.Done]: new AdbSyncDoneResponse(AdbSyncEntryResponse.size),
19
32
  };
20
33
 
34
+ const LIST_V2_RESPONSE_TYPES = {
35
+ [AdbSyncResponseId.Entry2]: AdbSyncEntry2Response,
36
+ [AdbSyncResponseId.Done]: new AdbSyncDoneResponse(AdbSyncEntry2Response.size),
37
+ };
38
+
21
39
  export async function* adbSyncOpenDir(
22
40
  stream: AdbBufferedStream,
23
41
  writer: WritableStreamDefaultWriter<Uint8Array>,
24
42
  path: string,
25
- ): AsyncGenerator<AdbSyncEntryResponse, void, void> {
26
- await adbSyncWriteRequest(writer, AdbSyncRequestId.List, path);
43
+ v2: boolean,
44
+ ): AsyncGenerator<AdbSyncEntry, void, void> {
45
+ let requestId: AdbSyncRequestId.List | AdbSyncRequestId.List2;
46
+ let responseType: typeof LIST_V1_RESPONSE_TYPES | typeof LIST_V2_RESPONSE_TYPES;
47
+
48
+ if (v2) {
49
+ requestId = AdbSyncRequestId.List2;
50
+ responseType = LIST_V2_RESPONSE_TYPES;
51
+ } else {
52
+ requestId = AdbSyncRequestId.List;
53
+ responseType = LIST_V1_RESPONSE_TYPES;
54
+ }
55
+
56
+ await adbSyncWriteRequest(writer, requestId, path);
27
57
 
28
58
  while (true) {
29
- const response = await adbSyncReadResponse(stream, ResponseTypes);
59
+ const response = await adbSyncReadResponse(stream, responseType);
30
60
  switch (response.id) {
31
61
  case AdbSyncResponseId.Entry:
62
+ yield {
63
+ mode: response.mode,
64
+ size: BigInt(response.size),
65
+ mtime: BigInt(response.mtime),
66
+ get type() { return response.type; },
67
+ get permission() { return response.permission; },
68
+ name: response.name,
69
+ };
70
+ break;
71
+ case AdbSyncResponseId.Entry2:
72
+ // `LST2` can return error codes for failed `lstat` calls.
73
+ // `LIST` just ignores them.
74
+ // But they only contain `name` so still pretty useless.
75
+ if (response.error !== 0) {
76
+ continue;
77
+ }
32
78
  yield response;
33
79
  break;
34
80
  case AdbSyncResponseId.Done:
@@ -9,7 +9,7 @@ export const AdbSyncDataResponse =
9
9
  .uint8Array('data', { lengthField: 'dataLength' })
10
10
  .extra({ id: AdbSyncResponseId.Data as const });
11
11
 
12
- const ResponseTypes = {
12
+ const RESPONSE_TYPES = {
13
13
  [AdbSyncResponseId.Data]: AdbSyncDataResponse,
14
14
  [AdbSyncResponseId.Done]: new AdbSyncDoneResponse(AdbSyncDataResponse.size),
15
15
  };
@@ -24,7 +24,7 @@ export function adbSyncPull(
24
24
  await adbSyncWriteRequest(writer, AdbSyncRequestId.Receive, path);
25
25
  },
26
26
  async pull(controller) {
27
- const response = await adbSyncReadResponse(stream, ResponseTypes);
27
+ const response = await adbSyncReadResponse(stream, RESPONSE_TYPES);
28
28
  switch (response.id) {
29
29
  case AdbSyncResponseId.Data:
30
30
  controller.enqueue(response.data!);
@@ -35,7 +35,10 @@ export function adbSyncPull(
35
35
  default:
36
36
  throw new Error('Unexpected response id');
37
37
  }
38
- }
38
+ },
39
+ cancel() {
40
+ throw new Error(`Sync commands don't support cancel.`);
41
+ },
39
42
  }, {
40
43
  highWaterMark: 16 * 1024,
41
44
  size(chunk) { return chunk.byteLength; }
@@ -4,6 +4,7 @@ import { encodeUtf8 } from "../../utils/index.js";
4
4
 
5
5
  export enum AdbSyncRequestId {
6
6
  List = 'LIST',
7
+ List2 = 'LIS2',
7
8
  Send = 'SEND',
8
9
  Lstat = 'STAT',
9
10
  Stat = 'STA2',
@@ -4,6 +4,7 @@ import { decodeUtf8 } from "../../utils/index.js";
4
4
 
5
5
  export enum AdbSyncResponseId {
6
6
  Entry = 'DENT',
7
+ Entry2 = 'DNT2',
7
8
  Lstat = 'STAT',
8
9
  Stat = 'STA2',
9
10
  Lstat2 = 'LST2',
@@ -43,7 +44,12 @@ export const AdbSyncFailResponse =
43
44
  export async function adbSyncReadResponse<T extends Record<string, StructLike<any>>>(
44
45
  stream: AdbBufferedStream,
45
46
  types: T,
46
- ): Promise<StructValueType<T[keyof T]>> {
47
+ // When `T` is a union type, `T[keyof T]` only includes their common keys.
48
+ // For example, let `type T = { a: string, b: string } | { a: string, c: string}`,
49
+ // `keyof T` is `'a'`, not `'a' | 'b' | 'c'`.
50
+ // However, `T extends unknown ? keyof T : never` will distribute `T`,
51
+ // so returns all keys.
52
+ ): Promise<StructValueType<T extends unknown ? T[keyof T] : never>> {
47
53
  const id = decodeUtf8(await stream.read(4));
48
54
 
49
55
  if (id === AdbSyncResponseId.Fail) {
@@ -54,5 +60,5 @@ export async function adbSyncReadResponse<T extends Record<string, StructLike<an
54
60
  return types[id]!.deserialize(stream);
55
61
  }
56
62
 
57
- throw new Error('Unexpected response id');
63
+ throw new Error(`Expected '${Object.keys(types).join(', ')}', but got '${id}'`);
58
64
  }
@@ -10,6 +10,19 @@ export enum LinuxFileType {
10
10
  Link = 0o12,
11
11
  }
12
12
 
13
+ export interface AdbSyncStat {
14
+ mode: number;
15
+ size: bigint;
16
+ mtime: bigint;
17
+ get type(): LinuxFileType;
18
+ get permission(): number;
19
+
20
+ uid?: number;
21
+ gid?: number;
22
+ atime?: bigint;
23
+ ctime?: bigint;
24
+ }
25
+
13
26
  export const AdbSyncLstatResponse =
14
27
  new Struct({ littleEndian: true })
15
28
  .int32('mode')
@@ -32,6 +45,7 @@ export const AdbSyncLstatResponse =
32
45
  export type AdbSyncLstatResponse = typeof AdbSyncLstatResponse['TDeserializeResult'];
33
46
 
34
47
  export enum AdbSyncStatErrorCode {
48
+ SUCCESS = 0,
35
49
  EACCES = 13,
36
50
  EEXIST = 17,
37
51
  EFAULT = 14,
@@ -80,15 +94,15 @@ export const AdbSyncStatResponse =
80
94
 
81
95
  export type AdbSyncStatResponse = typeof AdbSyncStatResponse['TDeserializeResult'];
82
96
 
83
- const StatResponseType = {
97
+ const STAT_RESPONSE_TYPES = {
84
98
  [AdbSyncResponseId.Stat]: AdbSyncStatResponse,
85
99
  };
86
100
 
87
- const LstatResponseType = {
101
+ const LSTAT_RESPONSE_TYPES = {
88
102
  [AdbSyncResponseId.Lstat]: AdbSyncLstatResponse,
89
103
  };
90
104
 
91
- const Lstat2ResponseType = {
105
+ const LSTAT_V2_RESPONSE_TYPES = {
92
106
  [AdbSyncResponseId.Lstat2]: AdbSyncStatResponse,
93
107
  };
94
108
 
@@ -97,20 +111,33 @@ export async function adbSyncLstat(
97
111
  writer: WritableStreamDefaultWriter<Uint8Array>,
98
112
  path: string,
99
113
  v2: boolean,
100
- ): Promise<AdbSyncLstatResponse | AdbSyncStatResponse> {
114
+ ): Promise<AdbSyncStat> {
101
115
  let requestId: AdbSyncRequestId.Lstat | AdbSyncRequestId.Lstat2;
102
- let responseType: typeof LstatResponseType | typeof Lstat2ResponseType;
116
+ let responseType: typeof LSTAT_RESPONSE_TYPES | typeof LSTAT_V2_RESPONSE_TYPES;
103
117
 
104
118
  if (v2) {
105
119
  requestId = AdbSyncRequestId.Lstat2;
106
- responseType = Lstat2ResponseType;
120
+ responseType = LSTAT_V2_RESPONSE_TYPES;
107
121
  } else {
108
122
  requestId = AdbSyncRequestId.Lstat;
109
- responseType = LstatResponseType;
123
+ responseType = LSTAT_RESPONSE_TYPES;
110
124
  }
111
125
 
112
126
  await adbSyncWriteRequest(writer, requestId, path);
113
- return adbSyncReadResponse(stream, responseType);
127
+ const response = await adbSyncReadResponse(stream, responseType);
128
+
129
+ switch (response.id) {
130
+ case AdbSyncResponseId.Lstat:
131
+ return {
132
+ mode: response.mode,
133
+ size: BigInt(response.size),
134
+ mtime: BigInt(response.mtime),
135
+ get type() { return response.type; },
136
+ get permission() { return response.permission; },
137
+ };
138
+ default:
139
+ return response;
140
+ }
114
141
  }
115
142
 
116
143
  export async function adbSyncStat(
@@ -119,5 +146,5 @@ export async function adbSyncStat(
119
146
  path: string,
120
147
  ): Promise<AdbSyncStatResponse> {
121
148
  await adbSyncWriteRequest(writer, AdbSyncRequestId.Stat, path);
122
- return await adbSyncReadResponse(stream, StatResponseType);
149
+ return await adbSyncReadResponse(stream, STAT_RESPONSE_TYPES);
123
150
  }
@@ -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();