refactor(adb): remove text encoding from backend

This commit is contained in:
Simon Chan 2022-01-08 19:00:03 +08:00
parent 08767c7b71
commit 2b63aa630a
12 changed files with 50 additions and 66 deletions

View file

@ -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

View file

@ -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;

View file

@ -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>;

View file

@ -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);
} }

View file

@ -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
) )
); );
} }

View file

@ -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);
} }

View file

@ -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);

View file

@ -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);

View file

@ -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,
}; };
} }

View file

@ -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);
}
} }

View 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);
}

View file

@ -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';