refactor(adb): simplify reading sync responses

This commit is contained in:
Simon Chan 2022-10-07 20:33:11 +08:00
parent c9000c5beb
commit 1486eed3cf
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
18 changed files with 164 additions and 183 deletions

View file

@ -63,6 +63,7 @@ export class Adb implements Closeable {
} }
} }
}), { }), {
// Don't cancel the source ReadableStream on AbortSignal abort.
preventCancel: true, preventCancel: true,
signal: abortController.signal, signal: abortController.signal,
}) })
@ -146,7 +147,7 @@ export class Adb implements Closeable {
private _device: string | undefined; private _device: string | undefined;
public get device() { return this._device; } public get device() { return this._device; }
private _features: AdbFeatures[] | undefined; private _features: AdbFeatures[] = [];
public get features() { return this._features; } public get features() { return this._features; }
public readonly subprocess: AdbSubprocess; public readonly subprocess: AdbSubprocess;
@ -190,8 +191,6 @@ export class Adb implements Closeable {
} }
private parseBanner(banner: string): void { private parseBanner(banner: string): void {
this._features = [];
const pieces = banner.split('::'); const pieces = banner.split('::');
if (pieces.length > 1) { if (pieces.length > 1) {
const props = pieces[1]!; const props = pieces[1]!;
@ -224,8 +223,13 @@ export class Adb implements Closeable {
} }
} }
public addIncomingSocketHandler(handler: AdbIncomingSocketHandler) { /**
return this.dispatcher.addIncomingSocketHandler(handler); * Add a handler for incoming socket.
* @param handler A function to call with new incoming sockets. It must return `true` if it accepts the socket.
* @returns A function to remove the handler.
*/
public onIncomingSocket(handler: AdbIncomingSocketHandler) {
return this.dispatcher.onIncomingSocket(handler);
} }
public async createSocket(service: string): Promise<AdbSocket> { public async createSocket(service: string): Promise<AdbSocket> {

View file

@ -7,19 +7,6 @@ const Version =
new Struct({ littleEndian: true }) new Struct({ littleEndian: true })
.uint32('version'); .uint32('version');
/*
* ADB uses 8 int32 fields to describe bit depths
* The only combination I have seen is RGBA8888, which is
* red_offset: 0
* red_length: 8
* blue_offset: 16
* blue_length: 8
* green_offset: 8
* green_length: 8
* alpha_offset: 24
* alpha_length: 8
*/
export const AdbFrameBufferV1 = export const AdbFrameBufferV1 =
new Struct({ littleEndian: true }) new Struct({ littleEndian: true })
.uint32('bpp') .uint32('bpp')
@ -57,6 +44,22 @@ export const AdbFrameBufferV2 =
export type AdbFrameBufferV2 = typeof AdbFrameBufferV2['TDeserializeResult']; export type AdbFrameBufferV2 = typeof AdbFrameBufferV2['TDeserializeResult'];
/**
* ADB uses 8 int32 fields to describe bit depths
*
* The only combination I have seen is RGBA8888, which is
*
* red_offset: 0
* red_length: 8
* blue_offset: 16
* blue_length: 8
* green_offset: 8
* green_length: 8
* alpha_offset: 24
* alpha_length: 8
*
* But it doesn't mean that other combinations are not possible.
*/
export type AdbFrameBuffer = AdbFrameBufferV1 | AdbFrameBufferV2; export type AdbFrameBuffer = AdbFrameBufferV1 | AdbFrameBufferV2;
export async function framebuffer(adb: Adb): Promise<AdbFrameBuffer> { export async function framebuffer(adb: Adb): Promise<AdbFrameBuffer> {
@ -65,6 +68,7 @@ export async function framebuffer(adb: Adb): Promise<AdbFrameBuffer> {
const { version } = await Version.deserialize(stream); const { version } = await Version.deserialize(stream);
switch (version) { switch (version) {
case 1: case 1:
// TODO: AdbFrameBuffer: does all v1 responses uses the same color space? Add it so the command returns same format for all versions.
return AdbFrameBufferV1.deserialize(stream); return AdbFrameBufferV1.deserialize(stream);
case 2: case 2:
return AdbFrameBufferV2.deserialize(stream); return AdbFrameBufferV2.deserialize(stream);

View file

@ -13,6 +13,7 @@ export function install(
return new WrapWritableStream<Uint8Array>({ return new WrapWritableStream<Uint8Array>({
async start() { async start() {
// TODO: install: support other install apk methods (streaming, etc.) // TODO: install: support other install apk methods (streaming, etc.)
// TODO: install: support split apk formats (`adb install-multiple`)
// Upload apk file to tmp folder // Upload apk file to tmp folder
sync = await adb.sync(); sync = await adb.sync();

View file

@ -41,7 +41,7 @@ export class AdbReverseCommand extends AutoDisposable {
super(); super();
this.adb = adb; this.adb = adb;
this.addDisposable(this.adb.addIncomingSocketHandler(this.handleIncomingSocket)); this.addDisposable(this.adb.onIncomingSocket(this.handleIncomingSocket));
} }
protected handleIncomingSocket = async (socket: AdbSocket) => { protected handleIncomingSocket = async (socket: AdbSocket) => {
@ -80,8 +80,8 @@ export class AdbReverseCommand extends AutoDisposable {
/** /**
* @param deviceAddress The address adbd on device is listening on. Can be `tcp:0` to let adbd choose an available TCP port by itself. * @param deviceAddress The address adbd on device is listening on. Can be `tcp:0` to let adbd choose an available TCP port by itself.
* @param localAddress Native ADB client will open a connection to this address when reverse connection received. In WebADB, it's only used to uniquely identify a reverse tunnel registry, `handler` will be called to handle the connection. * @param localAddress Native ADB client will open a connection to this address when reverse connection received. In WebADB, it's only used to uniquely identify a reverse tunnel registry, `handler` will be called to handle the connection.
* @param handler A callback to handle incoming connections * @param handler A callback to handle incoming connections. It must return `true` if it accepts the connection.
* @returns If `deviceAddress` is `tcp:0`, return `tcp:{ACTUAL_LISTENING_PORT}`; otherwise, return `deviceAddress`. * @returns `tcp:{ACTUAL_LISTENING_PORT}`, If `deviceAddress` is `tcp:0`; otherwise, `deviceAddress`.
*/ */
public async add( public async add(
deviceAddress: string, deviceAddress: string,
@ -91,7 +91,7 @@ export class AdbReverseCommand extends AutoDisposable {
const stream = await this.sendRequest(`reverse:forward:${deviceAddress};${localAddress}`); const stream = await this.sendRequest(`reverse:forward:${deviceAddress};${localAddress}`);
// `tcp:0` tells the device to pick an available port. // `tcp:0` tells the device to pick an available port.
// Begin with Android 8, device will respond with the selected port for all `tcp:` requests. // On Android >=8, device will respond with the selected port for all `tcp:` requests.
if (deviceAddress.startsWith('tcp:')) { if (deviceAddress.startsWith('tcp:')) {
let length: number | undefined; let length: number | undefined;
try { try {
@ -101,7 +101,7 @@ export class AdbReverseCommand extends AutoDisposable {
throw e; throw e;
} }
// Device before Android 8 doesn't have this response. // Android <8 doesn't have this response.
// (the stream is closed now) // (the stream is closed now)
// Can be safely ignored. // Can be safely ignored.
} }

View file

@ -21,7 +21,7 @@ export class AdbSubprocessNoneProtocol implements AdbSubprocessProtocol {
public static async raw(adb: Adb, command: string) { public static async raw(adb: Adb, command: string) {
// `shell,raw:${command}` also triggers raw mode, // `shell,raw:${command}` also triggers raw mode,
// But is not supported before Android 7. // But is not supported on Android version <7.
return new AdbSubprocessNoneProtocol(await adb.createSocket(`exec:${command}`)); return new AdbSubprocessNoneProtocol(await adb.createSocket(`exec:${command}`));
} }
@ -50,6 +50,8 @@ export class AdbSubprocessNoneProtocol implements AdbSubprocessProtocol {
public constructor(socket: AdbSocket) { public constructor(socket: AdbSocket) {
this.socket = socket; this.socket = socket;
// Link `stdout`, `stderr` and `stdin` together,
// so closing any of them will close the others.
this.duplex = new DuplexStreamFactory<Uint8Array, Uint8Array>({ this.duplex = new DuplexStreamFactory<Uint8Array, Uint8Array>({
close: async () => { close: async () => {
await this.socket.close(); await this.socket.close();
@ -62,7 +64,7 @@ export class AdbSubprocessNoneProtocol implements AdbSubprocessProtocol {
} }
public resize() { public resize() {
// Not supported // Not supported, but don't throw.
} }
public kill() { public kill() {

View file

@ -1,5 +1,5 @@
import { PromiseResolver } from '@yume-chan/async'; import { PromiseResolver } from '@yume-chan/async';
import { pipeFrom, PushReadableStream, StructDeserializeStream, StructSerializeStream, TransformStream, WritableStream, type WritableStreamDefaultWriter, type PushReadableStreamController, type ReadableStream } from '@yume-chan/stream-extra'; import { pipeFrom, PushReadableStream, StructDeserializeStream, StructSerializeStream, TransformStream, WritableStream, type PushReadableStreamController, type ReadableStream, type WritableStreamDefaultWriter } from '@yume-chan/stream-extra';
import Struct, { placeholder, type StructValueType } from '@yume-chan/struct'; import Struct, { placeholder, type StructValueType } from '@yume-chan/struct';
import type { Adb } from '../../../adb.js'; import type { Adb } from '../../../adb.js';
@ -103,7 +103,7 @@ class MultiplexStream<T>{
*/ */
export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol { export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
public static isSupported(adb: Adb) { public static isSupported(adb: Adb) {
return adb.features!.includes(AdbFeatures.ShellV2); return adb.features.includes(AdbFeatures.ShellV2);
} }
public static async pty(adb: Adb, command: string) { public static async pty(adb: Adb, command: string) {
@ -179,7 +179,7 @@ export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
data: encodeUtf8( data: encodeUtf8(
// The "correct" format is `${rows}x${cols},${x_pixels}x${y_pixels}` // The "correct" format is `${rows}x${cols},${x_pixels}x${y_pixels}`
// However, according to https://linux.die.net/man/4/tty_ioctl // However, according to https://linux.die.net/man/4/tty_ioctl
// `x_pixels` and `y_pixels` are not used, so always passing `0` is fine. // `x_pixels` and `y_pixels` are unused, so always sending `0` should be fine.
`${rows}x${cols},0x0\0` `${rows}x${cols},0x0\0`
), ),
}); });

View file

@ -6,17 +6,17 @@ import type { AdbSocket } from '../../../socket/index.js';
export interface AdbSubprocessProtocol { export interface AdbSubprocessProtocol {
/** /**
* A WritableStream that writes to the `stdin` pipe. * A WritableStream that writes to the `stdin` stream.
*/ */
readonly stdin: WritableStream<Uint8Array>; readonly stdin: WritableStream<Uint8Array>;
/** /**
* The `stdout` pipe of the process. * The `stdout` stream of the process.
*/ */
readonly stdout: ReadableStream<Uint8Array>; readonly stdout: ReadableStream<Uint8Array>;
/** /**
* The `stderr` pipe of the process. * The `stderr` stream of the process.
* *
* Note: Some `AdbSubprocessProtocol` doesn't separate `stdout` and `stderr`, * Note: Some `AdbSubprocessProtocol` doesn't separate `stdout` and `stderr`,
* All output will be sent to `stdout`. * All output will be sent to `stdout`.

View file

@ -2,7 +2,7 @@ import type { BufferedReadableStream, WritableStreamDefaultWriter } from '@yume-
import Struct from '@yume-chan/struct'; import Struct from '@yume-chan/struct';
import { AdbSyncRequestId, adbSyncWriteRequest } from './request.js'; import { AdbSyncRequestId, adbSyncWriteRequest } from './request.js';
import { AdbSyncDoneResponse, adbSyncReadResponse, AdbSyncResponseId } from './response.js'; import { adbSyncReadResponses, AdbSyncResponseId } from './response.js';
import { AdbSyncLstatResponse, AdbSyncStatResponse, type AdbSyncStat } from './stat.js'; import { AdbSyncLstatResponse, AdbSyncStatResponse, type AdbSyncStat } from './stat.js';
export interface AdbSyncEntry extends AdbSyncStat { export interface AdbSyncEntry extends AdbSyncStat {
@ -27,61 +27,28 @@ export const AdbSyncEntry2Response =
export type AdbSyncEntry2Response = typeof AdbSyncEntry2Response['TDeserializeResult']; export type AdbSyncEntry2Response = typeof AdbSyncEntry2Response['TDeserializeResult'];
const LIST_V1_RESPONSE_TYPES = {
[AdbSyncResponseId.Entry]: AdbSyncEntryResponse,
[AdbSyncResponseId.Done]: new AdbSyncDoneResponse(AdbSyncEntryResponse.size),
};
const LIST_V2_RESPONSE_TYPES = {
[AdbSyncResponseId.Entry2]: AdbSyncEntry2Response,
[AdbSyncResponseId.Done]: new AdbSyncDoneResponse(AdbSyncEntry2Response.size),
};
export async function* adbSyncOpenDir( export async function* adbSyncOpenDir(
stream: BufferedReadableStream, stream: BufferedReadableStream,
writer: WritableStreamDefaultWriter<Uint8Array>, writer: WritableStreamDefaultWriter<Uint8Array>,
path: string, path: string,
v2: boolean, v2: boolean,
): AsyncGenerator<AdbSyncEntry, void, void> { ): AsyncGenerator<AdbSyncEntry, void, void> {
let requestId: AdbSyncRequestId.List | AdbSyncRequestId.List2;
let responseTypes: typeof LIST_V1_RESPONSE_TYPES | typeof LIST_V2_RESPONSE_TYPES;
if (v2) { if (v2) {
requestId = AdbSyncRequestId.List2; await adbSyncWriteRequest(writer, AdbSyncRequestId.List2, path);
responseTypes = LIST_V2_RESPONSE_TYPES; yield* adbSyncReadResponses(stream, AdbSyncResponseId.Entry2, AdbSyncEntry2Response);
} else { } else {
requestId = AdbSyncRequestId.List; await adbSyncWriteRequest(writer, AdbSyncRequestId.List, path);
responseTypes = LIST_V1_RESPONSE_TYPES; for await (const item of adbSyncReadResponses(stream, AdbSyncResponseId.Entry, AdbSyncEntryResponse)) {
} // Convert to same format as `AdbSyncEntry2Response` for easier consumption.
// However it will add some overhead.
await adbSyncWriteRequest(writer, requestId, path);
while (true) {
const response = await adbSyncReadResponse(stream, responseTypes);
switch (response.id) {
case AdbSyncResponseId.Entry:
yield { yield {
mode: response.mode, mode: item.mode,
size: BigInt(response.size), size: BigInt(item.size),
mtime: BigInt(response.mtime), mtime: BigInt(item.mtime),
get type() { return response.type; }, get type() { return item.type; },
get permission() { return response.permission; }, get permission() { return item.permission; },
name: response.name, name: item.name,
}; };
break;
case AdbSyncResponseId.Entry2:
// `LST2` can return error codes for failed `lstat` calls.
// `LIST` just ignores them.
// But they only contain `name` so still pretty useless.
if (response.error !== 0) {
continue;
}
yield response;
break;
case AdbSyncResponseId.Done:
return;
default:
throw new Error('Unexpected response id');
} }
} }
} }

View file

@ -2,7 +2,7 @@ import { BufferedReadableStream, ReadableStream, WritableStreamDefaultWriter } f
import Struct from '@yume-chan/struct'; import Struct from '@yume-chan/struct';
import { AdbSyncRequestId, adbSyncWriteRequest } from './request.js'; import { AdbSyncRequestId, adbSyncWriteRequest } from './request.js';
import { AdbSyncDoneResponse, adbSyncReadResponse, AdbSyncResponseId } from './response.js'; import { adbSyncReadResponses, AdbSyncResponseId } from './response.js';
export const AdbSyncDataResponse = export const AdbSyncDataResponse =
new Struct({ littleEndian: true }) new Struct({ littleEndian: true })
@ -10,35 +10,33 @@ export const AdbSyncDataResponse =
.uint8Array('data', { lengthField: 'dataLength' }) .uint8Array('data', { lengthField: 'dataLength' })
.extra({ id: AdbSyncResponseId.Data as const }); .extra({ id: AdbSyncResponseId.Data as const });
const RESPONSE_TYPES = { export type AdbSyncDataResponse = typeof AdbSyncDataResponse['TDeserializeResult'];
[AdbSyncResponseId.Data]: AdbSyncDataResponse,
[AdbSyncResponseId.Done]: new AdbSyncDoneResponse(AdbSyncDataResponse.size),
};
export function adbSyncPull( export function adbSyncPull(
stream: BufferedReadableStream, stream: BufferedReadableStream,
writer: WritableStreamDefaultWriter<Uint8Array>, writer: WritableStreamDefaultWriter<Uint8Array>,
path: string, path: string,
): ReadableStream<Uint8Array> { ): ReadableStream<Uint8Array> {
let generator!: AsyncGenerator<AdbSyncDataResponse, void, void>;
return new ReadableStream<Uint8Array>({ return new ReadableStream<Uint8Array>({
async start() { async start() {
// TODO: If `ReadableStream.from(AsyncGenerator)` is added to spec, use it instead.
await adbSyncWriteRequest(writer, AdbSyncRequestId.Receive, path); await adbSyncWriteRequest(writer, AdbSyncRequestId.Receive, path);
generator = adbSyncReadResponses(stream, AdbSyncResponseId.Data, AdbSyncDataResponse);
}, },
async pull(controller) { async pull(controller) {
const response = await adbSyncReadResponse(stream, RESPONSE_TYPES); const { done, value } = await generator.next();
switch (response.id) { if (done) {
case AdbSyncResponseId.Data:
controller.enqueue(response.data!);
break;
case AdbSyncResponseId.Done:
controller.close(); controller.close();
break; return;
default:
throw new Error('Unexpected response id');
} }
controller.enqueue(value.data);
}, },
cancel() { cancel() {
throw new Error(`Sync commands don't support cancel.`); try {
generator.return();
} catch { }
throw new Error(`Sync commands can't be canceled.`);
}, },
}, { }, {
highWaterMark: 16 * 1024, highWaterMark: 16 * 1024,

View file

@ -9,10 +9,6 @@ export const AdbSyncOkResponse =
new Struct({ littleEndian: true }) new Struct({ littleEndian: true })
.uint32('unused'); .uint32('unused');
const ResponseTypes = {
[AdbSyncResponseId.Ok]: AdbSyncOkResponse,
};
export const ADB_SYNC_MAX_PACKET_SIZE = 64 * 1024; export const ADB_SYNC_MAX_PACKET_SIZE = 64 * 1024;
export function adbSyncPush( export function adbSyncPush(
@ -34,7 +30,7 @@ export function adbSyncPush(
}, },
async close() { async close() {
await adbSyncWriteRequest(writer, AdbSyncRequestId.Done, mtime); await adbSyncWriteRequest(writer, AdbSyncRequestId.Done, mtime);
await adbSyncReadResponse(stream, ResponseTypes); await adbSyncReadResponse(stream, AdbSyncResponseId.Ok, AdbSyncOkResponse);
}, },
}), }),
new ChunkStream(packetSize) new ChunkStream(packetSize)

View file

@ -1,5 +1,5 @@
import type { BufferedReadableStream } from '@yume-chan/stream-extra'; import type { BufferedReadableStream } from '@yume-chan/stream-extra';
import Struct, { type StructAsyncDeserializeStream, type StructLike, type StructValueType } from '@yume-chan/struct'; import Struct, { StructValueType, type StructLike } from '@yume-chan/struct';
import { decodeUtf8 } from '../../utils/index.js'; import { decodeUtf8 } from '../../utils/index.js';
@ -15,25 +15,6 @@ export enum AdbSyncResponseId {
Fail = 'FAIL', Fail = 'FAIL',
} }
// DONE responses' size are always same as the request's normal response.
// For example DONE responses for LIST requests are 16 bytes (same as DENT responses),
// but DONE responses for STAT requests are 12 bytes (same as STAT responses)
// So we need to know responses' size in advance.
export class AdbSyncDoneResponse implements StructLike<AdbSyncDoneResponse> {
private length: number;
public readonly id = AdbSyncResponseId.Done;
public constructor(length: number) {
this.length = length;
}
public async deserialize(stream: StructAsyncDeserializeStream): Promise<this> {
await stream.read(this.length);
return this;
}
}
export const AdbSyncFailResponse = export const AdbSyncFailResponse =
new Struct({ littleEndian: true }) new Struct({ littleEndian: true })
.uint32('messageLength') .uint32('messageLength')
@ -42,24 +23,46 @@ export const AdbSyncFailResponse =
throw new Error(object.message); throw new Error(object.message);
}); });
export async function adbSyncReadResponse<T extends Record<string, StructLike<any>>>( export async function adbSyncReadResponse<T>(
stream: BufferedReadableStream, stream: BufferedReadableStream,
types: T, id: AdbSyncResponseId,
// When `T` is a union type, `T[keyof T]` only includes their common keys. type: StructLike<T>,
// For example, let `type T = { a: string, b: string } | { a: string, c: string}`, ): Promise<T> {
// `keyof T` is `'a'`, not `'a' | 'b' | 'c'`. const actualId = decodeUtf8(await stream.read(4));
// However, `T extends unknown ? keyof T : never` will distribute `T`, switch (actualId) {
// so returns all keys. case AdbSyncResponseId.Fail:
): Promise<StructValueType<T extends unknown ? T[keyof T] : never>> {
const id = decodeUtf8(await stream.read(4));
if (id === AdbSyncResponseId.Fail) {
await AdbSyncFailResponse.deserialize(stream); await AdbSyncFailResponse.deserialize(stream);
throw new Error('Unreachable');
case id:
return await type.deserialize(stream);
default:
throw new Error(`Expected '${id}', but got '${actualId}'`);
}
} }
if (types[id]) { export async function* adbSyncReadResponses<T extends Struct<any, any, any, any>>(
return types[id]!.deserialize(stream); stream: BufferedReadableStream,
id: AdbSyncResponseId,
type: T,
): AsyncGenerator<StructValueType<T>, void, void> {
while (true) {
const actualId = decodeUtf8(await stream.read(4));
switch (actualId) {
case AdbSyncResponseId.Fail:
await AdbSyncFailResponse.deserialize(stream);
throw new Error('Unreachable');
case AdbSyncResponseId.Done:
// `DONE` responses' size are always same as the request's normal response.
//
// For example, `DONE` responses for `LIST` requests are 16 bytes (same as `DENT` responses),
// but `DONE` responses for `STAT` requests are 12 bytes (same as `STAT` responses).
await stream.read(type.size);
return;
case id:
yield await type.deserialize(stream);
break;
default:
throw new Error(`Expected '${id}' or '${AdbSyncResponseId.Done}', but got '${actualId}'`);
}
} }
throw new Error(`Expected '${Object.keys(types).join(', ')}', but got '${id}'`);
} }

View file

@ -96,40 +96,18 @@ export const AdbSyncStatResponse =
export type AdbSyncStatResponse = typeof AdbSyncStatResponse['TDeserializeResult']; export type AdbSyncStatResponse = typeof AdbSyncStatResponse['TDeserializeResult'];
const STAT_RESPONSE_TYPES = {
[AdbSyncResponseId.Stat]: AdbSyncStatResponse,
};
const LSTAT_RESPONSE_TYPES = {
[AdbSyncResponseId.Lstat]: AdbSyncLstatResponse,
};
const LSTAT_V2_RESPONSE_TYPES = {
[AdbSyncResponseId.Lstat2]: AdbSyncStatResponse,
};
export async function adbSyncLstat( export async function adbSyncLstat(
stream: BufferedReadableStream, stream: BufferedReadableStream,
writer: WritableStreamDefaultWriter<Uint8Array>, writer: WritableStreamDefaultWriter<Uint8Array>,
path: string, path: string,
v2: boolean, v2: boolean,
): Promise<AdbSyncStat> { ): Promise<AdbSyncStat> {
let requestId: AdbSyncRequestId.Lstat | AdbSyncRequestId.Lstat2;
let responseTypes: typeof LSTAT_RESPONSE_TYPES | typeof LSTAT_V2_RESPONSE_TYPES;
if (v2) { if (v2) {
requestId = AdbSyncRequestId.Lstat2; await adbSyncWriteRequest(writer, AdbSyncRequestId.Lstat2, path);
responseTypes = LSTAT_V2_RESPONSE_TYPES; return await adbSyncReadResponse(stream, AdbSyncResponseId.Lstat2, AdbSyncStatResponse);
} else { } else {
requestId = AdbSyncRequestId.Lstat; await adbSyncWriteRequest(writer, AdbSyncRequestId.Lstat, path);
responseTypes = LSTAT_RESPONSE_TYPES; const response = await adbSyncReadResponse(stream, AdbSyncResponseId.Lstat, AdbSyncLstatResponse);
}
await adbSyncWriteRequest(writer, requestId, path);
const response = await adbSyncReadResponse(stream, responseTypes);
switch (response.id) {
case AdbSyncResponseId.Lstat:
return { return {
mode: response.mode, mode: response.mode,
// Convert to `BigInt` to make it compatible with `AdbSyncStatResponse` // Convert to `BigInt` to make it compatible with `AdbSyncStatResponse`
@ -138,8 +116,6 @@ export async function adbSyncLstat(
get type() { return response.type; }, get type() { return response.type; },
get permission() { return response.permission; }, get permission() { return response.permission; },
}; };
default:
return response;
} }
} }
@ -149,5 +125,5 @@ export async function adbSyncStat(
path: string, path: string,
): Promise<AdbSyncStatResponse> { ): Promise<AdbSyncStatResponse> {
await adbSyncWriteRequest(writer, AdbSyncRequestId.Stat, path); await adbSyncWriteRequest(writer, AdbSyncRequestId.Stat, path);
return await adbSyncReadResponse(stream, STAT_RESPONSE_TYPES); return await adbSyncReadResponse(stream, AdbSyncResponseId.Stat, AdbSyncStatResponse);
} }

View file

@ -38,20 +38,20 @@ export class AdbSync extends AutoDisposable {
protected sendLock = this.addDisposable(new AutoResetEvent()); protected sendLock = this.addDisposable(new AutoResetEvent());
public get supportsStat(): boolean { public get supportsStat(): boolean {
return this.adb.features!.includes(AdbFeatures.StatV2); return this.adb.features.includes(AdbFeatures.StatV2);
} }
public get supportsList2(): boolean { public get supportsList2(): boolean {
return this.adb.features!.includes(AdbFeatures.ListV2); return this.adb.features.includes(AdbFeatures.ListV2);
} }
public get fixedPushMkdir(): boolean { public get fixedPushMkdir(): boolean {
return this.adb.features!.includes(AdbFeatures.FixedPushMkdir); return this.adb.features.includes(AdbFeatures.FixedPushMkdir);
} }
public get needPushMkdirWorkaround(): boolean { public get needPushMkdirWorkaround(): boolean {
// https://android.googlesource.com/platform/packages/modules/adb/+/91768a57b7138166e0a3d11f79cd55909dda7014/client/file_sync_client.cpp#1361 // https://android.googlesource.com/platform/packages/modules/adb/+/91768a57b7138166e0a3d11f79cd55909dda7014/client/file_sync_client.cpp#1361
return this.adb.features!.includes(AdbFeatures.ShellV2) && !this.fixedPushMkdir; return this.adb.features.includes(AdbFeatures.ShellV2) && !this.fixedPushMkdir;
} }
public constructor(adb: Adb, socket: AdbSocket) { public constructor(adb: Adb, socket: AdbSocket) {

View file

@ -96,7 +96,7 @@ export function parsePrivateKey(key: Uint8Array): [n: bigint, d: bigint] {
// Taken from https://stackoverflow.com/a/51562038 // Taken from https://stackoverflow.com/a/51562038
// I can't understand, but it does work // I can't understand, but it does work
// Only used with numbers less than 2^32 so doesn't need BigInt // Only used with numbers smaller than 2^32 so doesn't need BigInt
export function modInverse(a: number, m: number) { export function modInverse(a: number, m: number) {
a = (a % m + m) % m; a = (a % m + m) % m;
if (!a || m < 2) { if (!a || m < 2) {

View file

@ -168,7 +168,12 @@ export class AdbPacketDispatcher implements Closeable {
// the device may also respond with two `CLSE` packets. // the device may also respond with two `CLSE` packets.
} }
public addIncomingSocketHandler(handler: AdbIncomingSocketHandler): RemoveEventListener { /**
* Add a handler for incoming socket.
* @param handler A function to call with new incoming sockets. It must return `true` if it accepts the socket.
* @returns A function to remove the handler.
*/
public onIncomingSocket(handler: AdbIncomingSocketHandler): RemoveEventListener {
this._incomingSocketHandlers.add(handler); this._incomingSocketHandlers.add(handler);
const remove = () => { const remove = () => {
this._incomingSocketHandlers.delete(handler); this._incomingSocketHandlers.delete(handler);
@ -178,7 +183,7 @@ export class AdbPacketDispatcher implements Closeable {
} }
private async handleOpen(packet: AdbPacketData) { private async handleOpen(packet: AdbPacketData) {
// AsyncOperationManager doesn't support get and skip an ID // `AsyncOperationManager` doesn't support skipping IDs
// Use `add` + `resolve` to simulate this behavior // Use `add` + `resolve` to simulate this behavior
const [localId] = this.initializers.add<number>(); const [localId] = this.initializers.add<number>();
this.initializers.resolve(localId, undefined); this.initializers.resolve(localId, undefined);

View file

@ -128,7 +128,7 @@ export class AdbSocketController implements AdbSocketInfo, ReadableWritablePair<
} }
/** /**
* AdbSocket is a duplex stream. * A duplex stream representing a socket to ADB daemon.
* *
* To close it, call either `socket.close()`, * To close it, call either `socket.close()`,
* `socket.readable.cancel()`, `socket.readable.getReader().cancel()`, * `socket.readable.cancel()`, `socket.readable.getReader().cancel()`,

View file

@ -19,15 +19,38 @@ addRange('0', '9');
addRange('+', '+'); addRange('+', '+');
addRange('/', '/'); addRange('/', '/');
/**
* Calculate the required length of the output buffer for the given input length.
*
* @param inputLength Length of the input in bytes
* @returns Length of the output in bytes
*/
export function calculateBase64EncodedLength(inputLength: number): [outputLength: number, paddingLength: number] { export function calculateBase64EncodedLength(inputLength: number): [outputLength: number, paddingLength: number] {
const remainder = inputLength % 3; const remainder = inputLength % 3;
const paddingLength = remainder !== 0 ? 3 - remainder : 0; const paddingLength = remainder !== 0 ? 3 - remainder : 0;
return [(inputLength + paddingLength) / 3 * 4, paddingLength]; return [(inputLength + paddingLength) / 3 * 4, paddingLength];
} }
/**
* Encode the given input buffer into base64.
*
* @param input The input buffer
* @returns The encoded output buffer
*/
export function encodeBase64( export function encodeBase64(
input: Uint8Array, input: Uint8Array,
): Uint8Array; ): Uint8Array;
/**
* Encode the given input into base64 and write it to the output buffer.
*
* The output buffer must be at least as long as the value returned by `calculateBase64EncodedLength`.
* It can points to the same buffer as the input, as long as `output.offset <= input.offset - input.length / 3`,
* or `output.offset >= input.offset - 1`
*
* @param input The input buffer
* @param output The output buffer
* @returns The number of bytes written to the output buffer
*/
export function encodeBase64( export function encodeBase64(
input: Uint8Array, input: Uint8Array,
output: Uint8Array, output: Uint8Array,

View file

@ -1,5 +1,7 @@
import type { ValueOrPromise } from "../utils.js"; import type { ValueOrPromise } from "../utils.js";
// TODO: allow over reading (returning a `Uint8Array`, an `offset` and a `length`) to avoid copying
export interface StructDeserializeStream { export interface StructDeserializeStream {
/** /**
* Read data from the underlying data source. * Read data from the underlying data source.