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,
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> {

View file

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

View file

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

View file

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

View file

@ -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() {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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()`,
* `socket.readable.cancel()`, `socket.readable.getReader().cancel()`,

View file

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

View file

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