mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-05 02:39:26 +02:00
refactor(adb): remove text encoding from backend
This commit is contained in:
parent
08767c7b71
commit
2b63aa630a
12 changed files with 50 additions and 66 deletions
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
TypeScript implementation of Android Debug Bridge (ADB) protocol.
|
TypeScript implementation of Android Debug Bridge (ADB) protocol.
|
||||||
|
|
||||||
|
**WARNING:** The public API is UNSTABLE. If you have any questions, please open an issue.
|
||||||
|
|
||||||
- [Compatibility](#compatibility)
|
- [Compatibility](#compatibility)
|
||||||
- [Connection](#connection)
|
- [Connection](#connection)
|
||||||
- [Backend](#backend)
|
- [Backend](#backend)
|
||||||
|
@ -16,8 +18,6 @@ TypeScript implementation of Android Debug Bridge (ADB) protocol.
|
||||||
- [AdbAuthenticator](#adbauthenticator)
|
- [AdbAuthenticator](#adbauthenticator)
|
||||||
- [Stream multiplex](#stream-multiplex)
|
- [Stream multiplex](#stream-multiplex)
|
||||||
- [Backend](#backend-1)
|
- [Backend](#backend-1)
|
||||||
- [`encodeUtf8`](#encodeutf8)
|
|
||||||
- [`decodeUtf8`](#decodeutf8)
|
|
||||||
- [Commands](#commands)
|
- [Commands](#commands)
|
||||||
- [childProcess](#childprocess)
|
- [childProcess](#childprocess)
|
||||||
- [usb](#usb)
|
- [usb](#usb)
|
||||||
|
@ -133,26 +133,10 @@ ADB commands are all based on streams. Multiple streams can send and receive at
|
||||||
3. Client and server read/write on the stream.
|
3. Client and server read/write on the stream.
|
||||||
4. Client/server sends a `CLSE` to close the stream.
|
4. Client/server sends a `CLSE` to close the stream.
|
||||||
|
|
||||||
The `Backend` is responsible for encoding and decoding UTF-8 strings.
|
The `Backend` is responsible for reading and writing data from underlying source.
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
|
|
||||||
#### `encodeUtf8`
|
|
||||||
|
|
||||||
```ts
|
|
||||||
encodeUtf8(input: string): ArrayBuffer
|
|
||||||
```
|
|
||||||
|
|
||||||
Encode `input` into an `ArrayBuffer` with UTF-8 encoding.
|
|
||||||
|
|
||||||
#### `decodeUtf8`
|
|
||||||
|
|
||||||
```ts
|
|
||||||
decodeUtf8(buffer: ArrayBuffer): string
|
|
||||||
```
|
|
||||||
|
|
||||||
Decode `buffer` into a string with UTF-8 encoding.
|
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
### childProcess
|
### childProcess
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { AdbChildProcess, AdbDemoMode, AdbFrameBuffer, AdbReverseCommand, AdbSyn
|
||||||
import { AdbFeatures } from './features';
|
import { AdbFeatures } from './features';
|
||||||
import { AdbCommand } from './packet';
|
import { AdbCommand } from './packet';
|
||||||
import { AdbLogger, AdbPacketDispatcher, AdbSocket } from './socket';
|
import { AdbLogger, AdbPacketDispatcher, AdbSocket } from './socket';
|
||||||
|
import { decodeUtf8 } from "./utils";
|
||||||
|
|
||||||
export enum AdbPropKey {
|
export enum AdbPropKey {
|
||||||
Product = 'ro.product.name',
|
Product = 'ro.product.name',
|
||||||
|
@ -118,7 +119,7 @@ export class Adb {
|
||||||
this.packetDispatcher.appendNullToServiceString = false;
|
this.packetDispatcher.appendNullToServiceString = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.parseBanner(this.backend.decodeUtf8(packet.payload!));
|
this.parseBanner(decodeUtf8(packet.payload!));
|
||||||
resolver.resolve();
|
resolver.resolve();
|
||||||
break;
|
break;
|
||||||
case AdbCommand.Auth:
|
case AdbCommand.Auth:
|
||||||
|
@ -229,7 +230,7 @@ export class Adb {
|
||||||
const resolver = new PromiseResolver<string>();
|
const resolver = new PromiseResolver<string>();
|
||||||
let result = '';
|
let result = '';
|
||||||
socket.onData(buffer => {
|
socket.onData(buffer => {
|
||||||
result += this.backend.decodeUtf8(buffer);
|
result += decodeUtf8(buffer);
|
||||||
});
|
});
|
||||||
socket.onClose(() => resolver.resolve(result));
|
socket.onClose(() => resolver.resolve(result));
|
||||||
return resolver.promise;
|
return resolver.promise;
|
||||||
|
|
|
@ -12,10 +12,6 @@ export interface AdbBackend {
|
||||||
|
|
||||||
connect?(): ValueOrPromise<void>;
|
connect?(): ValueOrPromise<void>;
|
||||||
|
|
||||||
encodeUtf8(input: string): ArrayBuffer;
|
|
||||||
|
|
||||||
decodeUtf8(buffer: ArrayBuffer): string;
|
|
||||||
|
|
||||||
read(length: number): ValueOrPromise<ArrayBuffer>;
|
read(length: number): ValueOrPromise<ArrayBuffer>;
|
||||||
|
|
||||||
write(buffer: ArrayBuffer): ValueOrPromise<void>;
|
write(buffer: ArrayBuffer): ValueOrPromise<void>;
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { AutoDisposable } from '@yume-chan/event';
|
import { AutoDisposable } from '@yume-chan/event';
|
||||||
import Struct from '@yume-chan/struct';
|
import Struct from '@yume-chan/struct';
|
||||||
import { AdbPacket } from '../packet';
|
import type { AdbPacket } from '../packet';
|
||||||
import { AdbIncomingSocketEventArgs, AdbPacketDispatcher, AdbSocket } from '../socket';
|
import type { AdbIncomingSocketEventArgs, AdbPacketDispatcher, AdbSocket } from '../socket';
|
||||||
import { AdbBufferedStream } from '../stream';
|
import { AdbBufferedStream } from '../stream';
|
||||||
|
import { decodeUtf8 } from "../utils";
|
||||||
|
|
||||||
export interface AdbReverseHandler {
|
export interface AdbReverseHandler {
|
||||||
onSocket(packet: AdbPacket, socket: AdbSocket): void;
|
onSocket(packet: AdbPacket, socket: AdbSocket): void;
|
||||||
|
@ -49,7 +50,7 @@ export class AdbReverseCommand extends AutoDisposable {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const address = this.dispatcher.backend.decodeUtf8(e.packet.payload!);
|
const address = decodeUtf8(e.packet.payload!);
|
||||||
// tcp:1234\0
|
// tcp:1234\0
|
||||||
const port = Number.parseInt(address.substring(4));
|
const port = Number.parseInt(address.substring(4));
|
||||||
if (this.localPortToHandler.has(port)) {
|
if (this.localPortToHandler.has(port)) {
|
||||||
|
@ -65,7 +66,7 @@ export class AdbReverseCommand extends AutoDisposable {
|
||||||
|
|
||||||
private async sendRequest(service: string) {
|
private async sendRequest(service: string) {
|
||||||
const stream = await this.createBufferedStream(service);
|
const stream = await this.createBufferedStream(service);
|
||||||
const success = this.dispatcher.backend.decodeUtf8(await stream.read(4)) === 'OKAY';
|
const success = decodeUtf8(await stream.read(4)) === 'OKAY';
|
||||||
if (!success) {
|
if (!success) {
|
||||||
await AdbReverseErrorResponse.deserialize(stream);
|
await AdbReverseErrorResponse.deserialize(stream);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import type { Adb } from "../../adb";
|
||||||
import { AdbFeatures } from "../../features";
|
import { AdbFeatures } from "../../features";
|
||||||
import type { AdbSocket } from "../../socket";
|
import type { AdbSocket } from "../../socket";
|
||||||
import { AdbBufferedStream } from "../../stream";
|
import { AdbBufferedStream } from "../../stream";
|
||||||
|
import { encodeUtf8 } from "../../utils";
|
||||||
import type { AdbShell } from "./types";
|
import type { AdbShell } from "./types";
|
||||||
|
|
||||||
export enum AdbShellProtocolId {
|
export enum AdbShellProtocolId {
|
||||||
|
@ -93,8 +94,7 @@ export class AdbShellProtocol implements AdbShell {
|
||||||
{
|
{
|
||||||
id: AdbShellProtocolId.Stdin,
|
id: AdbShellProtocolId.Stdin,
|
||||||
data,
|
data,
|
||||||
},
|
}
|
||||||
this.stream
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -104,14 +104,13 @@ export class AdbShellProtocol implements AdbShell {
|
||||||
AdbShellProtocolPacket.serialize(
|
AdbShellProtocolPacket.serialize(
|
||||||
{
|
{
|
||||||
id: AdbShellProtocolId.WindowSizeChange,
|
id: AdbShellProtocolId.WindowSizeChange,
|
||||||
data: this.stream.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 pass `0` is fine.
|
// `x_pixels` and `y_pixels` are not used, so always pass `0` is fine.
|
||||||
`${rows}x${cols},0x0\0`
|
`${rows}x${cols},0x0\0`
|
||||||
),
|
),
|
||||||
},
|
}
|
||||||
this.stream
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import Struct from '@yume-chan/struct';
|
import Struct from '@yume-chan/struct';
|
||||||
import { AdbBufferedStream } from '../../stream';
|
import type { AdbBufferedStream } from '../../stream';
|
||||||
|
import { encodeUtf8 } from "../../utils";
|
||||||
|
|
||||||
export enum AdbSyncRequestId {
|
export enum AdbSyncRequestId {
|
||||||
List = 'LIST',
|
List = 'LIST',
|
||||||
|
@ -32,17 +33,17 @@ export async function adbSyncWriteRequest(
|
||||||
buffer = AdbSyncNumberRequest.serialize({
|
buffer = AdbSyncNumberRequest.serialize({
|
||||||
id,
|
id,
|
||||||
arg: value,
|
arg: value,
|
||||||
}, stream);
|
});
|
||||||
} else if (typeof value === 'string') {
|
} else if (typeof value === 'string') {
|
||||||
buffer = AdbSyncDataRequest.serialize({
|
buffer = AdbSyncDataRequest.serialize({
|
||||||
id,
|
id,
|
||||||
data: stream.encodeUtf8(value),
|
data: encodeUtf8(value),
|
||||||
}, stream);
|
});
|
||||||
} else {
|
} else {
|
||||||
buffer = AdbSyncDataRequest.serialize({
|
buffer = AdbSyncDataRequest.serialize({
|
||||||
id,
|
id,
|
||||||
data: value,
|
data: value,
|
||||||
}, stream);
|
});
|
||||||
}
|
}
|
||||||
await stream.write(buffer);
|
await stream.write(buffer);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import Struct, { StructDeserializationContext, StructLike, StructValueType } from '@yume-chan/struct';
|
import Struct, { StructAsyncDeserializeStream, StructLike, StructValueType } from '@yume-chan/struct';
|
||||||
import { AdbBufferedStream } from '../../stream';
|
import { AdbBufferedStream } from '../../stream';
|
||||||
|
import { decodeUtf8 } from "../../utils";
|
||||||
|
|
||||||
export enum AdbSyncResponseId {
|
export enum AdbSyncResponseId {
|
||||||
Entry = 'DENT',
|
Entry = 'DENT',
|
||||||
|
@ -25,8 +26,8 @@ export class AdbSyncDoneResponse implements StructLike<AdbSyncDoneResponse> {
|
||||||
this.length = length;
|
this.length = length;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deserialize(context: StructDeserializationContext): Promise<this> {
|
public async deserialize(stream: StructAsyncDeserializeStream): Promise<this> {
|
||||||
await context.read(this.length);
|
await stream.read(this.length);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,7 +44,7 @@ export async function adbSyncReadResponse<T extends Record<string, StructLike<an
|
||||||
stream: AdbBufferedStream,
|
stream: AdbBufferedStream,
|
||||||
types: T,
|
types: T,
|
||||||
): Promise<StructValueType<T[keyof T]>> {
|
): Promise<StructValueType<T[keyof T]>> {
|
||||||
const id = stream.backend.decodeUtf8(await stream.read(4));
|
const id = decodeUtf8(await stream.read(4));
|
||||||
|
|
||||||
if (id === AdbSyncResponseId.Fail) {
|
if (id === AdbSyncResponseId.Fail) {
|
||||||
await AdbSyncFailResponse.deserialize(stream);
|
await AdbSyncFailResponse.deserialize(stream);
|
||||||
|
|
|
@ -31,15 +31,14 @@ export type AdbPacketInit = Omit<typeof AdbPacketStruct['TInit'], 'checksum' | '
|
||||||
|
|
||||||
export namespace AdbPacket {
|
export namespace AdbPacket {
|
||||||
export async function read(backend: AdbBackend): Promise<AdbPacket> {
|
export async function read(backend: AdbBackend): Promise<AdbPacket> {
|
||||||
let buffer = await backend.read(24);
|
|
||||||
|
|
||||||
// Detect boundary
|
// Detect boundary
|
||||||
// Note that it relies on the backend to only return data from one write operation
|
// Note that it relies on the backend to only return data from one write operation
|
||||||
while (buffer.byteLength !== 24) {
|
let buffer: ArrayBuffer;
|
||||||
|
do {
|
||||||
// Maybe it's a payload from last connection.
|
// Maybe it's a payload from last connection.
|
||||||
// Ignore and try again
|
// Ignore and try again
|
||||||
buffer = await backend.read(24);
|
buffer = await backend.read(24);
|
||||||
}
|
} while (buffer.byteLength !== 24);
|
||||||
|
|
||||||
let bufferUsed = false;
|
let bufferUsed = false;
|
||||||
const stream = new BufferedStream({
|
const stream = new BufferedStream({
|
||||||
|
@ -52,11 +51,7 @@ export namespace AdbPacket {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return AdbPacketStruct.deserialize({
|
return AdbPacketStruct.deserialize(stream);
|
||||||
read: stream.read.bind(stream),
|
|
||||||
decodeUtf8: backend.decodeUtf8.bind(backend),
|
|
||||||
encodeUtf8: backend.encodeUtf8.bind(backend),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function write(
|
export async function write(
|
||||||
|
@ -80,7 +75,7 @@ export namespace AdbPacket {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Write payload separately to avoid an extra copy
|
// Write payload separately to avoid an extra copy
|
||||||
const header = AdbPacketHeader.serialize(packet, backend);
|
const header = AdbPacketHeader.serialize(packet);
|
||||||
await backend.write(header);
|
await backend.write(header);
|
||||||
if (packet.payload.byteLength) {
|
if (packet.payload.byteLength) {
|
||||||
await backend.write(packet.payload);
|
await backend.write(packet.payload);
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { AsyncOperationManager } from '@yume-chan/async';
|
||||||
import { AutoDisposable, EventEmitter } from '@yume-chan/event';
|
import { AutoDisposable, EventEmitter } from '@yume-chan/event';
|
||||||
import { AdbBackend } from '../backend';
|
import { AdbBackend } from '../backend';
|
||||||
import { AdbCommand, AdbPacket, AdbPacketInit } from '../packet';
|
import { AdbCommand, AdbPacket, AdbPacketInit } from '../packet';
|
||||||
import { AutoResetEvent } from '../utils';
|
import { AutoResetEvent, decodeUtf8, encodeUtf8 } from '../utils';
|
||||||
import { AdbSocketController } from './controller';
|
import { AdbSocketController } from './controller';
|
||||||
import { AdbLogger } from './logger';
|
import { AdbLogger } from './logger';
|
||||||
import { AdbSocket } from './socket';
|
import { AdbSocket } from './socket';
|
||||||
|
@ -160,7 +160,7 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
||||||
this.initializers.resolve(localId, undefined);
|
this.initializers.resolve(localId, undefined);
|
||||||
|
|
||||||
const remoteId = packet.arg0;
|
const remoteId = packet.arg0;
|
||||||
const serviceString = this.backend.decodeUtf8(packet.payload!);
|
const serviceString = decodeUtf8(packet.payload!);
|
||||||
|
|
||||||
const controller = new AdbSocketController({
|
const controller = new AdbSocketController({
|
||||||
dispatcher: this,
|
dispatcher: this,
|
||||||
|
@ -234,7 +234,7 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
||||||
command: packetOrCommand as AdbCommand,
|
command: packetOrCommand as AdbCommand,
|
||||||
arg0: arg0 as number,
|
arg0: arg0 as number,
|
||||||
arg1: arg1 as number,
|
arg1: arg1 as number,
|
||||||
payload: typeof payload === 'string' ? this.backend.encodeUtf8(payload) : payload,
|
payload: typeof payload === 'string' ? encodeUtf8(payload) : payload,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { StructDeserializationContext } from '@yume-chan/struct';
|
import { StructAsyncDeserializeStream } from '@yume-chan/struct';
|
||||||
import { AdbSocket, AdbSocketInfo } from '../socket';
|
import { AdbSocket, AdbSocketInfo } from '../socket';
|
||||||
import { AdbSocketStream } from './stream';
|
import { AdbSocketStream } from './stream';
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@ export class BufferedStream<T extends Stream> {
|
||||||
|
|
||||||
export class AdbBufferedStream
|
export class AdbBufferedStream
|
||||||
extends BufferedStream<AdbSocketStream>
|
extends BufferedStream<AdbSocketStream>
|
||||||
implements AdbSocketInfo, StructDeserializationContext {
|
implements AdbSocketInfo, StructAsyncDeserializeStream {
|
||||||
public get backend() { return this.stream.backend; }
|
public get backend() { return this.stream.backend; }
|
||||||
public get localId() { return this.stream.localId; }
|
public get localId() { return this.stream.localId; }
|
||||||
public get remoteId() { return this.stream.remoteId; }
|
public get remoteId() { return this.stream.remoteId; }
|
||||||
|
@ -89,12 +89,4 @@ export class AdbBufferedStream
|
||||||
public write(data: ArrayBuffer): Promise<void> {
|
public write(data: ArrayBuffer): Promise<void> {
|
||||||
return this.stream.write(data);
|
return this.stream.write(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public decodeUtf8(buffer: ArrayBuffer): string {
|
|
||||||
return this.backend.decodeUtf8(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
public encodeUtf8(input: string): ArrayBuffer {
|
|
||||||
return this.backend.encodeUtf8(input);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
13
libraries/adb/src/utils/encoding.ts
Normal file
13
libraries/adb/src/utils/encoding.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
|
||||||
|
// @ts-expect-error @types/node missing `TextEncoder`
|
||||||
|
const Utf8Encoder = new TextEncoder();
|
||||||
|
// @ts-expect-error @types/node missing `TextDecoder`
|
||||||
|
const Utf8Decoder = new TextDecoder();
|
||||||
|
|
||||||
|
export function encodeUtf8(input: string): ArrayBuffer {
|
||||||
|
return Utf8Encoder.encode(input).buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeUtf8(buffer: ArrayBuffer): string {
|
||||||
|
return Utf8Decoder.decode(buffer);
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
export * from './auto-reset-event';
|
export * from './auto-reset-event';
|
||||||
export * from './base64';
|
export * from './base64';
|
||||||
export * from './chunk';
|
export * from './chunk';
|
||||||
|
export * from './encoding';
|
||||||
export * from './event-queue';
|
export * from './event-queue';
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue