mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-04 10:19:17 +02:00
refactor(adb): simplify reading sync responses
This commit is contained in:
parent
c9000c5beb
commit
1486eed3cf
18 changed files with 164 additions and 183 deletions
|
@ -63,6 +63,7 @@ export class Adb implements Closeable {
|
|||
}
|
||||
}
|
||||
}), {
|
||||
// Don't cancel the source ReadableStream on AbortSignal abort.
|
||||
preventCancel: true,
|
||||
signal: abortController.signal,
|
||||
})
|
||||
|
@ -146,7 +147,7 @@ export class Adb implements Closeable {
|
|||
private _device: string | undefined;
|
||||
public get device() { return this._device; }
|
||||
|
||||
private _features: AdbFeatures[] | undefined;
|
||||
private _features: AdbFeatures[] = [];
|
||||
public get features() { return this._features; }
|
||||
|
||||
public readonly subprocess: AdbSubprocess;
|
||||
|
@ -190,8 +191,6 @@ export class Adb implements Closeable {
|
|||
}
|
||||
|
||||
private parseBanner(banner: string): void {
|
||||
this._features = [];
|
||||
|
||||
const pieces = banner.split('::');
|
||||
if (pieces.length > 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> {
|
||||
|
|
|
@ -7,19 +7,6 @@ const Version =
|
|||
new Struct({ littleEndian: true })
|
||||
.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 =
|
||||
new Struct({ littleEndian: true })
|
||||
.uint32('bpp')
|
||||
|
@ -57,6 +44,22 @@ export const AdbFrameBufferV2 =
|
|||
|
||||
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 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);
|
||||
switch (version) {
|
||||
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);
|
||||
case 2:
|
||||
return AdbFrameBufferV2.deserialize(stream);
|
||||
|
|
|
@ -13,6 +13,7 @@ export function install(
|
|||
return new WrapWritableStream<Uint8Array>({
|
||||
async start() {
|
||||
// TODO: install: support other install apk methods (streaming, etc.)
|
||||
// TODO: install: support split apk formats (`adb install-multiple`)
|
||||
|
||||
// Upload apk file to tmp folder
|
||||
sync = await adb.sync();
|
||||
|
|
|
@ -41,7 +41,7 @@ export class AdbReverseCommand extends AutoDisposable {
|
|||
super();
|
||||
|
||||
this.adb = adb;
|
||||
this.addDisposable(this.adb.addIncomingSocketHandler(this.handleIncomingSocket));
|
||||
this.addDisposable(this.adb.onIncomingSocket(this.handleIncomingSocket));
|
||||
}
|
||||
|
||||
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 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
|
||||
* @returns If `deviceAddress` is `tcp:0`, return `tcp:{ACTUAL_LISTENING_PORT}`; otherwise, return `deviceAddress`.
|
||||
* @param handler A callback to handle incoming connections. It must return `true` if it accepts the connection.
|
||||
* @returns `tcp:{ACTUAL_LISTENING_PORT}`, If `deviceAddress` is `tcp:0`; otherwise, `deviceAddress`.
|
||||
*/
|
||||
public async add(
|
||||
deviceAddress: string,
|
||||
|
@ -91,7 +91,7 @@ export class AdbReverseCommand extends AutoDisposable {
|
|||
const stream = await this.sendRequest(`reverse:forward:${deviceAddress};${localAddress}`);
|
||||
|
||||
// `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:')) {
|
||||
let length: number | undefined;
|
||||
try {
|
||||
|
@ -101,7 +101,7 @@ export class AdbReverseCommand extends AutoDisposable {
|
|||
throw e;
|
||||
}
|
||||
|
||||
// Device before Android 8 doesn't have this response.
|
||||
// Android <8 doesn't have this response.
|
||||
// (the stream is closed now)
|
||||
// Can be safely ignored.
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ export class AdbSubprocessNoneProtocol implements AdbSubprocessProtocol {
|
|||
|
||||
public static async raw(adb: Adb, command: string) {
|
||||
// `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}`));
|
||||
}
|
||||
|
||||
|
@ -50,6 +50,8 @@ export class AdbSubprocessNoneProtocol implements AdbSubprocessProtocol {
|
|||
public constructor(socket: AdbSocket) {
|
||||
this.socket = socket;
|
||||
|
||||
// Link `stdout`, `stderr` and `stdin` together,
|
||||
// so closing any of them will close the others.
|
||||
this.duplex = new DuplexStreamFactory<Uint8Array, Uint8Array>({
|
||||
close: async () => {
|
||||
await this.socket.close();
|
||||
|
@ -62,7 +64,7 @@ export class AdbSubprocessNoneProtocol implements AdbSubprocessProtocol {
|
|||
}
|
||||
|
||||
public resize() {
|
||||
// Not supported
|
||||
// Not supported, but don't throw.
|
||||
}
|
||||
|
||||
public kill() {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 type { Adb } from '../../../adb.js';
|
||||
|
@ -103,7 +103,7 @@ class MultiplexStream<T>{
|
|||
*/
|
||||
export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
|
||||
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) {
|
||||
|
@ -179,7 +179,7 @@ export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
|
|||
data: encodeUtf8(
|
||||
// The "correct" format is `${rows}x${cols},${x_pixels}x${y_pixels}`
|
||||
// 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`
|
||||
),
|
||||
});
|
||||
|
|
|
@ -6,17 +6,17 @@ import type { AdbSocket } from '../../../socket/index.js';
|
|||
|
||||
export interface AdbSubprocessProtocol {
|
||||
/**
|
||||
* A WritableStream that writes to the `stdin` pipe.
|
||||
* A WritableStream that writes to the `stdin` stream.
|
||||
*/
|
||||
readonly stdin: WritableStream<Uint8Array>;
|
||||
|
||||
/**
|
||||
* The `stdout` pipe of the process.
|
||||
* The `stdout` stream of the process.
|
||||
*/
|
||||
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`,
|
||||
* All output will be sent to `stdout`.
|
||||
|
|
|
@ -2,7 +2,7 @@ import type { BufferedReadableStream, WritableStreamDefaultWriter } from '@yume-
|
|||
import Struct from '@yume-chan/struct';
|
||||
|
||||
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';
|
||||
|
||||
export interface AdbSyncEntry extends AdbSyncStat {
|
||||
|
@ -27,61 +27,28 @@ export const AdbSyncEntry2Response =
|
|||
|
||||
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(
|
||||
stream: BufferedReadableStream,
|
||||
writer: WritableStreamDefaultWriter<Uint8Array>,
|
||||
path: string,
|
||||
v2: boolean,
|
||||
): AsyncGenerator<AdbSyncEntry, void, void> {
|
||||
let requestId: AdbSyncRequestId.List | AdbSyncRequestId.List2;
|
||||
let responseTypes: typeof LIST_V1_RESPONSE_TYPES | typeof LIST_V2_RESPONSE_TYPES;
|
||||
|
||||
if (v2) {
|
||||
requestId = AdbSyncRequestId.List2;
|
||||
responseTypes = LIST_V2_RESPONSE_TYPES;
|
||||
await adbSyncWriteRequest(writer, AdbSyncRequestId.List2, path);
|
||||
yield* adbSyncReadResponses(stream, AdbSyncResponseId.Entry2, AdbSyncEntry2Response);
|
||||
} else {
|
||||
requestId = AdbSyncRequestId.List;
|
||||
responseTypes = LIST_V1_RESPONSE_TYPES;
|
||||
}
|
||||
|
||||
await adbSyncWriteRequest(writer, requestId, path);
|
||||
|
||||
while (true) {
|
||||
const response = await adbSyncReadResponse(stream, responseTypes);
|
||||
switch (response.id) {
|
||||
case AdbSyncResponseId.Entry:
|
||||
yield {
|
||||
mode: response.mode,
|
||||
size: BigInt(response.size),
|
||||
mtime: BigInt(response.mtime),
|
||||
get type() { return response.type; },
|
||||
get permission() { return response.permission; },
|
||||
name: response.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');
|
||||
await adbSyncWriteRequest(writer, AdbSyncRequestId.List, path);
|
||||
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.
|
||||
yield {
|
||||
mode: item.mode,
|
||||
size: BigInt(item.size),
|
||||
mtime: BigInt(item.mtime),
|
||||
get type() { return item.type; },
|
||||
get permission() { return item.permission; },
|
||||
name: item.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { BufferedReadableStream, ReadableStream, WritableStreamDefaultWriter } f
|
|||
import Struct from '@yume-chan/struct';
|
||||
|
||||
import { AdbSyncRequestId, adbSyncWriteRequest } from './request.js';
|
||||
import { AdbSyncDoneResponse, adbSyncReadResponse, AdbSyncResponseId } from './response.js';
|
||||
import { adbSyncReadResponses, AdbSyncResponseId } from './response.js';
|
||||
|
||||
export const AdbSyncDataResponse =
|
||||
new Struct({ littleEndian: true })
|
||||
|
@ -10,35 +10,33 @@ export const AdbSyncDataResponse =
|
|||
.uint8Array('data', { lengthField: 'dataLength' })
|
||||
.extra({ id: AdbSyncResponseId.Data as const });
|
||||
|
||||
const RESPONSE_TYPES = {
|
||||
[AdbSyncResponseId.Data]: AdbSyncDataResponse,
|
||||
[AdbSyncResponseId.Done]: new AdbSyncDoneResponse(AdbSyncDataResponse.size),
|
||||
};
|
||||
export type AdbSyncDataResponse = typeof AdbSyncDataResponse['TDeserializeResult'];
|
||||
|
||||
export function adbSyncPull(
|
||||
stream: BufferedReadableStream,
|
||||
writer: WritableStreamDefaultWriter<Uint8Array>,
|
||||
path: string,
|
||||
): ReadableStream<Uint8Array> {
|
||||
let generator!: AsyncGenerator<AdbSyncDataResponse, void, void>;
|
||||
return new ReadableStream<Uint8Array>({
|
||||
async start() {
|
||||
// TODO: If `ReadableStream.from(AsyncGenerator)` is added to spec, use it instead.
|
||||
await adbSyncWriteRequest(writer, AdbSyncRequestId.Receive, path);
|
||||
generator = adbSyncReadResponses(stream, AdbSyncResponseId.Data, AdbSyncDataResponse);
|
||||
},
|
||||
async pull(controller) {
|
||||
const response = await adbSyncReadResponse(stream, RESPONSE_TYPES);
|
||||
switch (response.id) {
|
||||
case AdbSyncResponseId.Data:
|
||||
controller.enqueue(response.data!);
|
||||
break;
|
||||
case AdbSyncResponseId.Done:
|
||||
controller.close();
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unexpected response id');
|
||||
const { done, value } = await generator.next();
|
||||
if (done) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
controller.enqueue(value.data);
|
||||
},
|
||||
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,
|
||||
|
|
|
@ -9,10 +9,6 @@ export const AdbSyncOkResponse =
|
|||
new Struct({ littleEndian: true })
|
||||
.uint32('unused');
|
||||
|
||||
const ResponseTypes = {
|
||||
[AdbSyncResponseId.Ok]: AdbSyncOkResponse,
|
||||
};
|
||||
|
||||
export const ADB_SYNC_MAX_PACKET_SIZE = 64 * 1024;
|
||||
|
||||
export function adbSyncPush(
|
||||
|
@ -34,7 +30,7 @@ export function adbSyncPush(
|
|||
},
|
||||
async close() {
|
||||
await adbSyncWriteRequest(writer, AdbSyncRequestId.Done, mtime);
|
||||
await adbSyncReadResponse(stream, ResponseTypes);
|
||||
await adbSyncReadResponse(stream, AdbSyncResponseId.Ok, AdbSyncOkResponse);
|
||||
},
|
||||
}),
|
||||
new ChunkStream(packetSize)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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';
|
||||
|
||||
|
@ -15,25 +15,6 @@ export enum AdbSyncResponseId {
|
|||
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 =
|
||||
new Struct({ littleEndian: true })
|
||||
.uint32('messageLength')
|
||||
|
@ -42,24 +23,46 @@ export const AdbSyncFailResponse =
|
|||
throw new Error(object.message);
|
||||
});
|
||||
|
||||
export async function adbSyncReadResponse<T extends Record<string, StructLike<any>>>(
|
||||
export async function adbSyncReadResponse<T>(
|
||||
stream: BufferedReadableStream,
|
||||
types: T,
|
||||
// When `T` is a union type, `T[keyof T]` only includes their common keys.
|
||||
// For example, let `type T = { a: string, b: string } | { a: string, c: string}`,
|
||||
// `keyof T` is `'a'`, not `'a' | 'b' | 'c'`.
|
||||
// However, `T extends unknown ? keyof T : never` will distribute `T`,
|
||||
// so returns all keys.
|
||||
): Promise<StructValueType<T extends unknown ? T[keyof T] : never>> {
|
||||
const id = decodeUtf8(await stream.read(4));
|
||||
|
||||
if (id === AdbSyncResponseId.Fail) {
|
||||
await AdbSyncFailResponse.deserialize(stream);
|
||||
id: AdbSyncResponseId,
|
||||
type: StructLike<T>,
|
||||
): Promise<T> {
|
||||
const actualId = decodeUtf8(await stream.read(4));
|
||||
switch (actualId) {
|
||||
case AdbSyncResponseId.Fail:
|
||||
await AdbSyncFailResponse.deserialize(stream);
|
||||
throw new Error('Unreachable');
|
||||
case id:
|
||||
return await type.deserialize(stream);
|
||||
default:
|
||||
throw new Error(`Expected '${id}', but got '${actualId}'`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function* adbSyncReadResponses<T extends Struct<any, any, any, any>>(
|
||||
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}'`);
|
||||
}
|
||||
}
|
||||
|
||||
if (types[id]) {
|
||||
return types[id]!.deserialize(stream);
|
||||
}
|
||||
|
||||
throw new Error(`Expected '${Object.keys(types).join(', ')}', but got '${id}'`);
|
||||
}
|
||||
|
|
|
@ -96,50 +96,26 @@ export const AdbSyncStatResponse =
|
|||
|
||||
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(
|
||||
stream: BufferedReadableStream,
|
||||
writer: WritableStreamDefaultWriter<Uint8Array>,
|
||||
path: string,
|
||||
v2: boolean,
|
||||
): Promise<AdbSyncStat> {
|
||||
let requestId: AdbSyncRequestId.Lstat | AdbSyncRequestId.Lstat2;
|
||||
let responseTypes: typeof LSTAT_RESPONSE_TYPES | typeof LSTAT_V2_RESPONSE_TYPES;
|
||||
|
||||
if (v2) {
|
||||
requestId = AdbSyncRequestId.Lstat2;
|
||||
responseTypes = LSTAT_V2_RESPONSE_TYPES;
|
||||
await adbSyncWriteRequest(writer, AdbSyncRequestId.Lstat2, path);
|
||||
return await adbSyncReadResponse(stream, AdbSyncResponseId.Lstat2, AdbSyncStatResponse);
|
||||
} else {
|
||||
requestId = AdbSyncRequestId.Lstat;
|
||||
responseTypes = LSTAT_RESPONSE_TYPES;
|
||||
}
|
||||
|
||||
await adbSyncWriteRequest(writer, requestId, path);
|
||||
const response = await adbSyncReadResponse(stream, responseTypes);
|
||||
|
||||
switch (response.id) {
|
||||
case AdbSyncResponseId.Lstat:
|
||||
return {
|
||||
mode: response.mode,
|
||||
// Convert to `BigInt` to make it compatible with `AdbSyncStatResponse`
|
||||
size: BigInt(response.size),
|
||||
mtime: BigInt(response.mtime),
|
||||
get type() { return response.type; },
|
||||
get permission() { return response.permission; },
|
||||
};
|
||||
default:
|
||||
return response;
|
||||
await adbSyncWriteRequest(writer, AdbSyncRequestId.Lstat, path);
|
||||
const response = await adbSyncReadResponse(stream, AdbSyncResponseId.Lstat, AdbSyncLstatResponse);
|
||||
return {
|
||||
mode: response.mode,
|
||||
// Convert to `BigInt` to make it compatible with `AdbSyncStatResponse`
|
||||
size: BigInt(response.size),
|
||||
mtime: BigInt(response.mtime),
|
||||
get type() { return response.type; },
|
||||
get permission() { return response.permission; },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -149,5 +125,5 @@ export async function adbSyncStat(
|
|||
path: string,
|
||||
): Promise<AdbSyncStatResponse> {
|
||||
await adbSyncWriteRequest(writer, AdbSyncRequestId.Stat, path);
|
||||
return await adbSyncReadResponse(stream, STAT_RESPONSE_TYPES);
|
||||
return await adbSyncReadResponse(stream, AdbSyncResponseId.Stat, AdbSyncStatResponse);
|
||||
}
|
||||
|
|
|
@ -38,20 +38,20 @@ export class AdbSync extends AutoDisposable {
|
|||
protected sendLock = this.addDisposable(new AutoResetEvent());
|
||||
|
||||
public get supportsStat(): boolean {
|
||||
return this.adb.features!.includes(AdbFeatures.StatV2);
|
||||
return this.adb.features.includes(AdbFeatures.StatV2);
|
||||
}
|
||||
|
||||
public get supportsList2(): boolean {
|
||||
return this.adb.features!.includes(AdbFeatures.ListV2);
|
||||
return this.adb.features.includes(AdbFeatures.ListV2);
|
||||
}
|
||||
|
||||
public get fixedPushMkdir(): boolean {
|
||||
return this.adb.features!.includes(AdbFeatures.FixedPushMkdir);
|
||||
return this.adb.features.includes(AdbFeatures.FixedPushMkdir);
|
||||
}
|
||||
|
||||
public get needPushMkdirWorkaround(): boolean {
|
||||
// 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) {
|
||||
|
|
|
@ -96,7 +96,7 @@ export function parsePrivateKey(key: Uint8Array): [n: bigint, d: bigint] {
|
|||
|
||||
// Taken from https://stackoverflow.com/a/51562038
|
||||
// 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) {
|
||||
a = (a % m + m) % m;
|
||||
if (!a || m < 2) {
|
||||
|
|
|
@ -168,7 +168,12 @@ export class AdbPacketDispatcher implements Closeable {
|
|||
// 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);
|
||||
const remove = () => {
|
||||
this._incomingSocketHandlers.delete(handler);
|
||||
|
@ -178,7 +183,7 @@ export class AdbPacketDispatcher implements Closeable {
|
|||
}
|
||||
|
||||
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
|
||||
const [localId] = this.initializers.add<number>();
|
||||
this.initializers.resolve(localId, undefined);
|
||||
|
|
|
@ -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()`,
|
||||
* `socket.readable.cancel()`, `socket.readable.getReader().cancel()`,
|
||||
|
|
|
@ -19,15 +19,38 @@ addRange('0', '9');
|
|||
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] {
|
||||
const remainder = inputLength % 3;
|
||||
const paddingLength = remainder !== 0 ? 3 - remainder : 0;
|
||||
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(
|
||||
input: 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(
|
||||
input: Uint8Array,
|
||||
output: Uint8Array,
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import type { ValueOrPromise } from "../utils.js";
|
||||
|
||||
// TODO: allow over reading (returning a `Uint8Array`, an `offset` and a `length`) to avoid copying
|
||||
|
||||
export interface StructDeserializeStream {
|
||||
/**
|
||||
* Read data from the underlying data source.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue