refactor(adb): simplify sync request/response ID handling

This commit is contained in:
Simon Chan 2025-09-04 22:59:33 +08:00
parent c607d911e1
commit ba956da6bd
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
12 changed files with 90 additions and 74 deletions

View file

@ -0,0 +1,33 @@
import { getUint32LittleEndian } from "@yume-chan/no-data-view";
function encodeAsciiUnchecked(value: string): Uint8Array<ArrayBuffer> {
const result = new Uint8Array(value.length);
for (let i = 0; i < value.length; i += 1) {
result[i] = value.charCodeAt(i);
}
return result;
}
/**
* Encode ID to numbers for faster comparison.
*
* This function skips all checks. The caller must ensure the input is valid.
*
* @param value A 4 ASCII character string.
* @returns A 32-bit integer by encoding the string as little-endian
*
* #__NO_SIDE_EFFECTS__
*/
export function adbSyncEncodeId(value: string): number {
const buffer = encodeAsciiUnchecked(value);
return getUint32LittleEndian(buffer, 0);
}
// https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/adb/file_sync_protocol.h;l=23;drc=888a54dcbf954fdffacc8283a793290abcc589cd
export const Lstat = adbSyncEncodeId("STAT");
export const Stat = adbSyncEncodeId("STA2");
export const LstatV2 = adbSyncEncodeId("LST2");
export const Done = adbSyncEncodeId("DONE");
export const Data = adbSyncEncodeId("DATA");

View file

@ -0,0 +1,12 @@
import { adbSyncEncodeId } from "./id-common.js";
export { Data, Done, Lstat, LstatV2, Stat } from "./id-common.js";
// https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/adb/file_sync_protocol.h;l=23;drc=888a54dcbf954fdffacc8283a793290abcc589cd
export const List = adbSyncEncodeId("LIST");
export const ListV2 = adbSyncEncodeId("LIS2");
export const Send = adbSyncEncodeId("SEND");
export const SendV2 = adbSyncEncodeId("SND2");
export const Receive = adbSyncEncodeId("RECV");

View file

@ -0,0 +1,11 @@
import { adbSyncEncodeId } from "./id-common.js";
export { Data, Done, Lstat, LstatV2, Stat } from "./id-common.js";
// https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/adb/file_sync_protocol.h;l=23;drc=888a54dcbf954fdffacc8283a793290abcc589cd
export const Entry = adbSyncEncodeId("DENT");
export const EntryV2 = adbSyncEncodeId("DNT2");
export const Ok = adbSyncEncodeId("OKAY");
export const Fail = adbSyncEncodeId("FAIL");

View file

@ -0,0 +1,12 @@
import * as AdbSyncRequestId from "./id-request.js";
import * as AdbSyncResponseId from "./id-response.js";
// biome-ignore lint/suspicious/noRedeclare: TypeScript declaration merging for enum-like object
type AdbSyncRequestId =
(typeof AdbSyncRequestId)[keyof typeof AdbSyncRequestId];
// biome-ignore lint/suspicious/noRedeclare: TypeScript declaration merging for enum-like object
type AdbSyncResponseId =
(typeof AdbSyncResponseId)[keyof typeof AdbSyncResponseId];
export { AdbSyncRequestId, AdbSyncResponseId };

View file

@ -1,3 +1,4 @@
export * from "./id.js";
export * from "./list.js"; export * from "./list.js";
export * from "./pull.js"; export * from "./pull.js";
export * from "./push.js"; export * from "./push.js";

View file

@ -1,8 +1,9 @@
import type { StructValue } from "@yume-chan/struct"; import type { StructValue } from "@yume-chan/struct";
import { extend, string, u32 } from "@yume-chan/struct"; import { extend, string, u32 } from "@yume-chan/struct";
import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js"; import { AdbSyncRequestId, AdbSyncResponseId } from "./id.js";
import { AdbSyncResponseId, adbSyncReadResponses } from "./response.js"; import { adbSyncWriteRequest } from "./request.js";
import { adbSyncReadResponses } from "./response.js";
import type { AdbSyncSocket } from "./socket.js"; import type { AdbSyncSocket } from "./socket.js";
import type { AdbSyncStat } from "./stat.js"; import type { AdbSyncStat } from "./stat.js";
import { import {
@ -36,7 +37,7 @@ export async function* adbSyncOpenDirV2(
await adbSyncWriteRequest(locked, AdbSyncRequestId.ListV2, path); await adbSyncWriteRequest(locked, AdbSyncRequestId.ListV2, path);
for await (const item of adbSyncReadResponses( for await (const item of adbSyncReadResponses(
locked, locked,
AdbSyncResponseId.Entry2, AdbSyncResponseId.EntryV2,
AdbSyncEntry2Response, AdbSyncEntry2Response,
)) { )) {
// `LST2` can return error codes for failed `lstat` calls. // `LST2` can return error codes for failed `lstat` calls.

View file

@ -2,8 +2,9 @@ import { ReadableStream } from "@yume-chan/stream-extra";
import type { StructValue } from "@yume-chan/struct"; import type { StructValue } from "@yume-chan/struct";
import { buffer, struct, u32 } from "@yume-chan/struct"; import { buffer, struct, u32 } from "@yume-chan/struct";
import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js"; import { AdbSyncRequestId, AdbSyncResponseId } from "./id.js";
import { adbSyncReadResponses, AdbSyncResponseId } from "./response.js"; import { adbSyncWriteRequest } from "./request.js";
import { adbSyncReadResponses } from "./response.js";
import type { AdbSyncSocket } from "./socket.js"; import type { AdbSyncSocket } from "./socket.js";
export const AdbSyncDataResponse = struct( export const AdbSyncDataResponse = struct(

View file

@ -8,8 +8,9 @@ import { struct, u32 } from "@yume-chan/struct";
import { NOOP } from "../../utils/index.js"; import { NOOP } from "../../utils/index.js";
import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js"; import { AdbSyncRequestId, AdbSyncResponseId } from "./id.js";
import { AdbSyncResponseId, adbSyncReadResponse } from "./response.js"; import { adbSyncWriteRequest } from "./request.js";
import { adbSyncReadResponse } from "./response.js";
import type { AdbSyncSocket, AdbSyncSocketLocked } from "./socket.js"; import type { AdbSyncSocket, AdbSyncSocketLocked } from "./socket.js";
import { LinuxFileType } from "./stat.js"; import { LinuxFileType } from "./stat.js";

View file

@ -1,20 +1,5 @@
import { encodeUtf8, struct, u32 } from "@yume-chan/struct"; import { encodeUtf8, struct, u32 } from "@yume-chan/struct";
import { adbSyncEncodeId } from "./response.js";
export const AdbSyncRequestId = {
List: adbSyncEncodeId("LIST"),
ListV2: adbSyncEncodeId("LIS2"),
Send: adbSyncEncodeId("SEND"),
SendV2: adbSyncEncodeId("SND2"),
Lstat: adbSyncEncodeId("STAT"),
Stat: adbSyncEncodeId("STA2"),
LstatV2: adbSyncEncodeId("LST2"),
Data: adbSyncEncodeId("DATA"),
Done: adbSyncEncodeId("DONE"),
Receive: adbSyncEncodeId("RECV"),
} as const;
export const AdbSyncNumberRequest = struct( export const AdbSyncNumberRequest = struct(
{ id: u32, arg: u32 }, { id: u32, arg: u32 },
{ littleEndian: true }, { littleEndian: true },
@ -26,13 +11,9 @@ export interface AdbSyncWritable {
export async function adbSyncWriteRequest( export async function adbSyncWriteRequest(
writable: AdbSyncWritable, writable: AdbSyncWritable,
id: number | string, id: number,
value: number | string | Uint8Array, value: number | string | Uint8Array,
): Promise<void> { ): Promise<void> {
if (typeof id === "string") {
id = adbSyncEncodeId(id);
}
if (typeof value === "number") { if (typeof value === "number") {
await writable.write( await writable.write(
AdbSyncNumberRequest.serialize({ id, arg: value }), AdbSyncNumberRequest.serialize({ id, arg: value }),

View file

@ -2,39 +2,9 @@ import { getUint32LittleEndian } from "@yume-chan/no-data-view";
import type { AsyncExactReadable, StructDeserializer } from "@yume-chan/struct"; import type { AsyncExactReadable, StructDeserializer } from "@yume-chan/struct";
import { decodeUtf8, string, struct, u32 } from "@yume-chan/struct"; import { decodeUtf8, string, struct, u32 } from "@yume-chan/struct";
import { unreachable } from "../../utils/no-op.js"; import { unreachable } from "../../utils/index.js";
function encodeAsciiUnchecked(value: string): Uint8Array<ArrayBuffer> { import { AdbSyncResponseId } from "./id.js";
const result = new Uint8Array(value.length);
for (let i = 0; i < value.length; i += 1) {
result[i] = value.charCodeAt(i);
}
return result;
}
/**
* Encode ID to numbers for faster comparison
* @param value A 4-character string
* @returns A 32-bit integer by encoding the string as little-endian
*
* #__NO_SIDE_EFFECTS__
*/
export function adbSyncEncodeId(value: string): number {
const buffer = encodeAsciiUnchecked(value);
return getUint32LittleEndian(buffer, 0);
}
export const AdbSyncResponseId = {
Entry: adbSyncEncodeId("DENT"),
Entry2: adbSyncEncodeId("DNT2"),
Lstat: adbSyncEncodeId("STAT"),
Stat: adbSyncEncodeId("STA2"),
Lstat2: adbSyncEncodeId("LST2"),
Done: adbSyncEncodeId("DONE"),
Data: adbSyncEncodeId("DATA"),
Ok: adbSyncEncodeId("OKAY"),
Fail: adbSyncEncodeId("FAIL"),
};
export class AdbSyncError extends Error {} export class AdbSyncError extends Error {}
@ -50,18 +20,14 @@ export const AdbSyncFailResponse = struct(
export async function adbSyncReadResponse<T>( export async function adbSyncReadResponse<T>(
stream: AsyncExactReadable, stream: AsyncExactReadable,
id: number | string, id: number,
type: StructDeserializer<T>, type: StructDeserializer<T>,
): Promise<T> { ): Promise<T> {
if (typeof id === "string") {
id = adbSyncEncodeId(id);
}
const buffer = await stream.readExactly(4); const buffer = await stream.readExactly(4);
switch (getUint32LittleEndian(buffer, 0)) { switch (getUint32LittleEndian(buffer, 0)) {
case AdbSyncResponseId.Fail: case AdbSyncResponseId.Fail:
await AdbSyncFailResponse.deserialize(stream); await AdbSyncFailResponse.deserialize(stream);
throw new Error("Unreachable"); unreachable();
case id: case id:
return await type.deserialize(stream); return await type.deserialize(stream);
default: default:
@ -73,13 +39,9 @@ export async function adbSyncReadResponse<T>(
export async function* adbSyncReadResponses<T>( export async function* adbSyncReadResponses<T>(
stream: AsyncExactReadable, stream: AsyncExactReadable,
id: number | string, id: number,
type: StructDeserializer<T>, type: StructDeserializer<T>,
): AsyncGenerator<T, void, void> { ): AsyncGenerator<T, void, void> {
if (typeof id === "string") {
id = adbSyncEncodeId(id);
}
while (true) { while (true) {
const buffer = await stream.readExactly(4); const buffer = await stream.readExactly(4);
switch (getUint32LittleEndian(buffer, 0)) { switch (getUint32LittleEndian(buffer, 0)) {

View file

@ -1,8 +1,9 @@
import type { StructValue } from "@yume-chan/struct"; import type { StructValue } from "@yume-chan/struct";
import { struct, u32, u64 } from "@yume-chan/struct"; import { struct, u32, u64 } from "@yume-chan/struct";
import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js"; import { AdbSyncRequestId, AdbSyncResponseId } from "./id.js";
import { AdbSyncResponseId, adbSyncReadResponse } from "./response.js"; import { adbSyncWriteRequest } from "./request.js";
import { adbSyncReadResponse } from "./response.js";
import type { AdbSyncSocket } from "./socket.js"; import type { AdbSyncSocket } from "./socket.js";
// https://github.com/python/cpython/blob/4e581d64b8aff3e2eda99b12f080c877bb78dfca/Lib/stat.py#L36 // https://github.com/python/cpython/blob/4e581d64b8aff3e2eda99b12f080c877bb78dfca/Lib/stat.py#L36
@ -131,7 +132,7 @@ export async function adbSyncLstat(
await adbSyncWriteRequest(locked, AdbSyncRequestId.LstatV2, path); await adbSyncWriteRequest(locked, AdbSyncRequestId.LstatV2, path);
return await adbSyncReadResponse( return await adbSyncReadResponse(
locked, locked,
AdbSyncResponseId.Lstat2, AdbSyncResponseId.LstatV2,
AdbSyncStatResponse, AdbSyncStatResponse,
); );
} else { } else {

View file

@ -1,6 +1,6 @@
import * as AdbFeature from "./features-value.js"; import * as AdbFeature from "./features-value.js";
// enum // biome-ignore lint/suspicious/noRedeclare: TypeScript declaration merging for enum-like object
type AdbFeature = (typeof AdbFeature)[keyof typeof AdbFeature]; type AdbFeature = (typeof AdbFeature)[keyof typeof AdbFeature];
export { AdbFeature }; export { AdbFeature };