feat(adb): start converting to Uint8Array

This commit is contained in:
Simon Chan 2022-02-23 18:14:59 +08:00
parent a7e259c5ad
commit 014145f775
32 changed files with 268 additions and 332 deletions

View file

@ -6,7 +6,7 @@ import { AdbFrameBuffer, AdbPower, AdbReverseCommand, AdbSubprocess, AdbSync, Ad
import { AdbFeatures } from './features';
import { AdbCommand } from './packet';
import { AdbLogger, AdbPacketDispatcher, AdbSocket } from './socket';
import { ReadableStream, WritableStream } from "./stream";
import { DecodeUtf8Stream, GatherStringStream, ReadableStream, WritableStream } from "./stream";
import { decodeUtf8 } from "./utils";
export enum AdbPropKey {
@ -52,8 +52,8 @@ export class Adb {
public constructor(
backend: AdbBackend,
readable: ReadableStream<ArrayBuffer>,
writable: WritableStream<ArrayBuffer>,
readable: ReadableStream<Uint8Array>,
writable: WritableStream<Uint8Array>,
logger?: AdbLogger
) {
this._backend = backend;
@ -225,11 +225,11 @@ export class Adb {
public async createSocketAndWait(service: string): Promise<string> {
const socket = await this.createSocket(service);
let result = '';
for await (const chunk of socket.readable) {
result += decodeUtf8(chunk);
}
return result;
const gatherStream = new GatherStringStream();
await socket.readable
.pipeThrough(new DecodeUtf8Stream())
.pipeTo(gatherStream.writable);
return gatherStream.result;
}
public async dispose(): Promise<void> {

View file

@ -5,15 +5,15 @@ import { calculatePublicKey, calculatePublicKeyLength, sign } from './crypto';
import { AdbCommand, AdbPacket, AdbPacketInit } from './packet';
import { calculateBase64EncodedLength, encodeBase64 } from './utils';
export type AdbKeyIterable = Iterable<ArrayBuffer> | AsyncIterable<ArrayBuffer>;
export type AdbKeyIterable = Iterable<Uint8Array> | AsyncIterable<Uint8Array>;
export interface AdbCredentialStore {
/**
* Generate and store a RSA private key with modulus length `2048` and public exponent `65537`.
*
* The returned `ArrayBuffer` is the private key in PKCS #8 format.
* The returned `Uint8Array` is the private key in PKCS #8 format.
*/
generateKey(): ValueOrPromise<ArrayBuffer>;
generateKey(): ValueOrPromise<Uint8Array>;
/**
* Synchronously or asynchronously iterate through all stored RSA private keys.
@ -63,7 +63,7 @@ export const AdbSignatureAuthenticator: AdbAuthenticator = async function* (
command: AdbCommand.Auth,
arg0: AdbAuthType.Signature,
arg1: 0,
payload: signature,
payload: new Uint8Array(signature),
};
}
};
@ -78,12 +78,13 @@ export const AdbPublicKeyAuthenticator: AdbAuthenticator = async function* (
return;
}
let privateKey: ArrayBuffer | undefined;
let privateKey: Uint8Array | undefined;
for await (const key of credentialStore.iterateKeys()) {
privateKey = key;
break;
}
if (!privateKey) {
privateKey = await credentialStore.generateKey();
}
@ -93,7 +94,7 @@ export const AdbPublicKeyAuthenticator: AdbAuthenticator = async function* (
// The public key is null terminated,
// So we allocate the buffer with one extra byte.
const publicKeyBuffer = new ArrayBuffer(publicKeyBase64Length + 1);
const publicKeyBuffer = new Uint8Array(publicKeyBase64Length + 1);
calculatePublicKey(privateKey, publicKeyBuffer);
encodeBase64(publicKeyBuffer, 0, publicKeyLength, publicKeyBuffer);

View file

@ -6,5 +6,5 @@ export interface AdbBackend {
readonly name: string | undefined;
connect(): ValueOrPromise<ReadableWritablePair<ArrayBuffer, ArrayBuffer>>;
connect(): ValueOrPromise<ReadableWritablePair<Uint8Array, Uint8Array>>;
}

View file

@ -33,7 +33,7 @@ export const AdbFrameBufferV1 =
.uint32('green_length')
.uint32('alpha_offset')
.uint32('alpha_length')
.uint8ClampedArray('data', { lengthField: 'size' });
.uint8Array('data', { lengthField: 'size' });
export type AdbFrameBufferV1 = typeof AdbFrameBufferV1['TDeserializeResult'];
@ -52,7 +52,7 @@ export const AdbFrameBufferV2 =
.uint32('green_length')
.uint32('alpha_offset')
.uint32('alpha_length')
.uint8ClampedArray('data', { lengthField: 'size' });
.uint8Array('data', { lengthField: 'size' });
export type AdbFrameBufferV2 = typeof AdbFrameBufferV2['TDeserializeResult'];

View file

@ -5,10 +5,10 @@ import { AdbSync } from "./sync";
export function install(
adb: Adb,
): WritableStream<ArrayBuffer> {
): WritableStream<Uint8Array> {
const filename = `/data/local/tmp/${Math.random().toString().substring(2)}.apk`;
return new WrapWritableStream<ArrayBuffer, WritableStream<ArrayBuffer>, AdbSync>({
return new WrapWritableStream<Uint8Array, WritableStream<Uint8Array>, AdbSync>({
async start() {
// Upload apk file to tmp folder
const sync = await adb.sync();

View file

@ -1,7 +1,7 @@
import { PromiseResolver } from "@yume-chan/async";
import type { Adb } from "../../adb";
import type { AdbSocket } from "../../socket";
import { ReadableStream, TransformStream } from "../../stream";
import { ReadableStream, ReadableStreamDefaultController, WrapReadableStream } from "../../stream";
import type { AdbSubprocessProtocol } from "./types";
/**
@ -24,26 +24,39 @@ export class AdbNoneSubprocessProtocol implements AdbSubprocessProtocol {
// Legacy shell forwards all data to stdin.
public get stdin() { return this.socket.writable; }
private _stdout: ReadableStream<ArrayBuffer>;
private _stdout: ReadableStream<Uint8Array>;
// Legacy shell doesn't support splitting output streams.
public get stdout() { return this._stdout; }
// `stderr` of Legacy shell is always empty.
private _stderr = new TransformStream<ArrayBuffer, ArrayBuffer>();
public get stderr() { return this._stderr.readable; }
private _stderr: ReadableStream<Uint8Array>;
public get stderr() { return this._stderr; }
private _exit = new PromiseResolver<number>();
public get exit() { return this._exit.promise; }
public constructor(socket: AdbSocket) {
this.socket = socket;
this._stdout = this.socket.readable
.pipeThrough(new TransformStream({
flush: () => {
this._stderr.writable.close();
this._exit.resolve(0);
let stderrController!: ReadableStreamDefaultController<Uint8Array>;
this._stderr = new ReadableStream<Uint8Array>({
start(controller) {
stderrController = controller;
},
}));
});
this._stdout = new WrapReadableStream<Uint8Array, ReadableStream<Uint8Array>, undefined>({
async start() {
return {
readable: socket.readable,
state: undefined,
};
},
async close() {
// Close `stderr` on exit.
stderrController.close();
}
});
}
public resize() {

View file

@ -20,13 +20,13 @@ export enum AdbShellProtocolId {
const AdbShellProtocolPacket = new Struct({ littleEndian: true })
.uint8('id', placeholder<AdbShellProtocolId>())
.uint32('length')
.arrayBuffer('data', { lengthField: 'length' });
.uint8Array('data', { lengthField: 'length' });
type AdbShellProtocolPacketInit = typeof AdbShellProtocolPacket['TInit'];
type AdbShellProtocolPacket = StructValueType<typeof AdbShellProtocolPacket>;
class StdinSerializeStream extends TransformStream<ArrayBuffer, AdbShellProtocolPacketInit>{
class StdinSerializeStream extends TransformStream<Uint8Array, AdbShellProtocolPacketInit>{
constructor() {
super({
transform(chunk, controller) {
@ -42,7 +42,7 @@ class StdinSerializeStream extends TransformStream<ArrayBuffer, AdbShellProtocol
}
}
class StdoutDeserializeStream extends TransformStream<AdbShellProtocolPacket, ArrayBuffer>{
class StdoutDeserializeStream extends TransformStream<AdbShellProtocolPacket, Uint8Array>{
constructor(type: AdbShellProtocolId.Stdout | AdbShellProtocolId.Stderr) {
super({
transform(chunk, controller) {
@ -106,13 +106,13 @@ export class AdbShellSubprocessProtocol implements AdbSubprocessProtocol {
private readonly _socket: AdbSocket;
private _socketWriter: WritableStreamDefaultWriter<AdbShellProtocolPacketInit>;
private _stdin = new TransformStream<ArrayBuffer, ArrayBuffer>();
public get stdin() { return this._stdin.writable; }
private _stdin: WritableStream<Uint8Array>;
public get stdin() { return this._stdin; }
private _stdout: ReadableStream<ArrayBuffer>;
private _stdout: ReadableStream<Uint8Array>;
public get stdout() { return this._stdout; }
private _stderr: ReadableStream<ArrayBuffer>;
private _stderr: ReadableStream<Uint8Array>;
public get stderr() { return this._stderr; }
private readonly _exit = new PromiseResolver<number>();
@ -125,9 +125,11 @@ export class AdbShellSubprocessProtocol implements AdbSubprocessProtocol {
// cspell: disable-next-line
// https://www.plantuml.com/plantuml/png/bL91QiCm4Bpx5SAdv90lb1JISmiw5XzaQKf5PIkiLZIqzEyLSg8ks13gYtOykpFhiOw93N6UGjVDqK7rZsxKqNw0U_NTgVAy4empOy2mm4_olC0VEVEE47GUpnGjKdgXoD76q4GIEpyFhOwP_m28hW0NNzxNUig1_JdW0bA7muFIJDco1daJ_1SAX9bgvoPJPyIkSekhNYctvIGXrCH6tIsPL5fs-s6J5yc9BpWXhKtNdF2LgVYPGM_6GlMwfhWUsIt4lbScANrwlgVVUifPSVi__t44qStnwPvZwobdSmHHlL57p2vFuHS0
// TODO: AdbShellSubprocessProtocol: Optimize stream graph
const [stdout, stderr] = socket.readable
.pipeThrough(new StructDeserializeStream(AdbShellProtocolPacket))
.pipeThrough(new TransformStream({
.pipeThrough(new TransformStream<AdbShellProtocolPacket, AdbShellProtocolPacket>({
transform: (chunk, controller) => {
if (chunk.id === AdbShellProtocolId.Exit) {
this._exit.resolve(new Uint8Array(chunk.data)[0]!);
@ -151,9 +153,9 @@ export class AdbShellSubprocessProtocol implements AdbSubprocessProtocol {
.pipeThrough(new StructSerializeStream(AdbShellProtocolPacket))
.pipeTo(socket.writable);
this._stdin.readable
.pipeThrough(new StdinSerializeStream())
.pipeTo(multiplexer.createWriteable());
const { readable, writable } = new StdinSerializeStream();
this._stdin = writable;
readable.pipeTo(multiplexer.createWriteable());
this._socketWriter = multiplexer.createWriteable().getWriter();
}

View file

@ -7,12 +7,12 @@ export interface AdbSubprocessProtocol {
/**
* A WritableStream that writes to the `stdin` pipe.
*/
readonly stdin: WritableStream<ArrayBuffer>;
readonly stdin: WritableStream<Uint8Array>;
/**
* The `stdout` pipe of the process.
*/
readonly stdout: ReadableStream<ArrayBuffer>;
readonly stdout: ReadableStream<Uint8Array>;
/**
* The `stderr` pipe of the process.
@ -20,7 +20,7 @@ export interface AdbSubprocessProtocol {
* Note: Some `AdbShell` doesn't separate `stdout` and `stderr`,
* All output will be sent to `stdout`.
*/
readonly stderr: ReadableStream<ArrayBuffer>;
readonly stderr: ReadableStream<Uint8Array>;
/**
* A `Promise` that resolves to the exit code of the process.

View file

@ -20,7 +20,7 @@ const ResponseTypes = {
export async function* adbSyncOpenDir(
stream: AdbBufferedStream,
writer: WritableStreamDefaultWriter<ArrayBuffer>,
writer: WritableStreamDefaultWriter<Uint8Array>,
path: string,
): AsyncGenerator<AdbSyncEntryResponse, void, void> {
await adbSyncWriteRequest(writer, AdbSyncRequestId.List, path);

View file

@ -6,7 +6,7 @@ import { AdbSyncDoneResponse, adbSyncReadResponse, AdbSyncResponseId } from './r
export const AdbSyncDataResponse =
new Struct({ littleEndian: true })
.uint32('dataLength')
.arrayBuffer('data', { lengthField: 'dataLength' })
.uint8Array('data', { lengthField: 'dataLength' })
.extra({ id: AdbSyncResponseId.Data as const });
const ResponseTypes = {
@ -16,9 +16,9 @@ const ResponseTypes = {
export function adbSyncPull(
stream: AdbBufferedStream,
writer: WritableStreamDefaultWriter<ArrayBuffer>,
writer: WritableStreamDefaultWriter<Uint8Array>,
path: string,
): ReadableStream<ArrayBuffer> {
): ReadableStream<Uint8Array> {
return new ReadableStream({
async start() {
await adbSyncWriteRequest(writer, AdbSyncRequestId.Receive, path);

View file

@ -1,6 +1,5 @@
import Struct from '@yume-chan/struct';
import { AdbBufferedStream, WritableStream, WritableStreamDefaultWriter } from '../../stream';
import { chunkArrayLike } from '../../utils';
import { AdbBufferedStream, ChunkStream, WritableStream, WritableStreamDefaultWriter } from '../../stream';
import { AdbSyncRequestId, adbSyncWriteRequest } from './request';
import { adbSyncReadResponse, AdbSyncResponseId } from './response';
import { LinuxFileType } from './stat';
@ -17,21 +16,20 @@ export const ADB_SYNC_MAX_PACKET_SIZE = 64 * 1024;
export function adbSyncPush(
stream: AdbBufferedStream,
writer: WritableStreamDefaultWriter<ArrayBuffer>,
writer: WritableStreamDefaultWriter<Uint8Array>,
filename: string,
mode: number = (LinuxFileType.File << 12) | 0o666,
mtime: number = (Date.now() / 1000) | 0,
packetSize: number = ADB_SYNC_MAX_PACKET_SIZE,
): WritableStream<ArrayBuffer> {
return new WritableStream({
): WritableStream<Uint8Array> {
const { readable, writable } = new ChunkStream(packetSize);
readable.pipeTo(new WritableStream({
async start() {
const pathAndMode = `${filename},${mode.toString()}`;
await adbSyncWriteRequest(writer, AdbSyncRequestId.Send, pathAndMode);
},
async write(chunk) {
for (const buffer of chunkArrayLike(chunk, packetSize)) {
await adbSyncWriteRequest(writer, AdbSyncRequestId.Data, buffer);
}
await adbSyncWriteRequest(writer, AdbSyncRequestId.Data, chunk);
},
async close() {
await adbSyncWriteRequest(writer, AdbSyncRequestId.Done, mtime);
@ -40,5 +38,6 @@ export function adbSyncPush(
}, {
highWaterMark: 16 * 1024,
size(chunk) { return chunk.byteLength; }
});
}));
return writable;
}

View file

@ -21,29 +21,29 @@ export const AdbSyncNumberRequest =
export const AdbSyncDataRequest =
new Struct({ littleEndian: true })
.fields(AdbSyncNumberRequest)
.arrayBuffer('data', { lengthField: 'arg' });
.uint8Array('data', { lengthField: 'arg' });
export async function adbSyncWriteRequest(
writer: WritableStreamDefaultWriter<ArrayBuffer>,
writer: WritableStreamDefaultWriter<Uint8Array>,
id: AdbSyncRequestId | string,
value: number | string | ArrayBuffer
value: number | string | Uint8Array
): Promise<void> {
let buffer: ArrayBuffer;
let buffer: Uint8Array;
if (typeof value === 'number') {
buffer = AdbSyncNumberRequest.serialize({
buffer = new Uint8Array(AdbSyncNumberRequest.serialize({
id,
arg: value,
});
}));
} else if (typeof value === 'string') {
buffer = AdbSyncDataRequest.serialize({
buffer = new Uint8Array(AdbSyncDataRequest.serialize({
id,
data: encodeUtf8(value),
});
}));
} else {
buffer = AdbSyncDataRequest.serialize({
buffer = new Uint8Array(AdbSyncDataRequest.serialize({
id,
data: value,
});
}));
}
await writer.write(buffer);
}

View file

@ -94,7 +94,7 @@ const Lstat2ResponseType = {
export async function adbSyncLstat(
stream: AdbBufferedStream,
writer: WritableStreamDefaultWriter<ArrayBuffer>,
writer: WritableStreamDefaultWriter<Uint8Array>,
path: string,
v2: boolean,
): Promise<AdbSyncLstatResponse | AdbSyncStatResponse> {
@ -115,7 +115,7 @@ export async function adbSyncLstat(
export async function adbSyncStat(
stream: AdbBufferedStream,
writer: WritableStreamDefaultWriter<ArrayBuffer>,
writer: WritableStreamDefaultWriter<Uint8Array>,
path: string,
): Promise<AdbSyncStatResponse> {
await adbSyncWriteRequest(writer, AdbSyncRequestId.Stat, path);

View file

@ -14,7 +14,7 @@ export class AdbSync extends AutoDisposable {
protected stream: AdbBufferedStream;
protected writer: WritableStreamDefaultWriter<ArrayBuffer>;
protected writer: WritableStreamDefaultWriter<Uint8Array>;
protected sendLock = this.addDisposable(new AutoResetEvent());
@ -89,8 +89,8 @@ export class AdbSync extends AutoDisposable {
* @param filename The full path of the file on device to read.
* @returns A `ReadableStream` that reads from the file.
*/
public read(filename: string): ReadableStream<ArrayBuffer> {
return new WrapReadableStream<ArrayBuffer, ReadableStream<ArrayBuffer>, undefined>({
public read(filename: string): ReadableStream<Uint8Array> {
return new WrapReadableStream<Uint8Array, ReadableStream<Uint8Array>, undefined>({
start: async () => {
await this.sendLock.wait();
return {
@ -116,7 +116,7 @@ export class AdbSync extends AutoDisposable {
filename: string,
mode?: number,
mtime?: number,
): WritableStream<ArrayBuffer> {
): WritableStream<Uint8Array> {
return new WrapWritableStream({
start: async () => {
await this.sendLock.wait();
@ -128,7 +128,7 @@ export class AdbSync extends AutoDisposable {
mode,
mtime,
),
state: {},
state: undefined,
};
},
close: async () => {

View file

@ -6,15 +6,15 @@ const BigInt2 = BigInt(2);
const BigInt64 = BigInt(64);
export function getBig(
buffer: ArrayBuffer,
array: Uint8Array,
offset = 0,
length = buffer.byteLength - offset
length = array.byteLength - offset
): bigint {
const view = new DataView(buffer);
const view = new DataView(array.buffer, array.byteOffset, array.byteLength);
let result = BigInt0;
// Now `length` must be a multiplication of 8
// Currently `length` must be a multiplication of 8
// Support for arbitrary length can be easily added
for (let i = offset; i < offset + length; i += 8) {
@ -40,8 +40,8 @@ export function setBig(buffer: ArrayBuffer, value: bigint, offset: number = 0) {
}
}
export function setBigLE(buffer: ArrayBuffer, value: bigint, offset = 0) {
const view = new DataView(buffer);
export function setBigLE(array: Uint8Array, value: bigint, offset = 0) {
const view = new DataView(array.buffer, array.byteOffset, array.byteLength);
while (value > BigInt0) {
setBigUint64(view, offset, value, true);
offset += 8;
@ -75,7 +75,7 @@ const RsaPrivateKeyNLength = 2048 / 8;
const RsaPrivateKeyDOffset = 303;
const RsaPrivateKeyDLength = 2048 / 8;
export function parsePrivateKey(key: ArrayBuffer): [n: bigint, d: bigint] {
export function parsePrivateKey(key: Uint8Array): [n: bigint, d: bigint] {
let n = getBig(key, RsaPrivateKeyNOffset, RsaPrivateKeyNLength);
let d = getBig(key, RsaPrivateKeyDOffset, RsaPrivateKeyDLength);
@ -114,18 +114,18 @@ export function calculatePublicKeyLength() {
}
export function calculatePublicKey(
privateKey: ArrayBuffer
): ArrayBuffer;
privateKey: Uint8Array
): Uint8Array;
export function calculatePublicKey(
privateKey: ArrayBuffer,
output: ArrayBuffer,
privateKey: Uint8Array,
output: Uint8Array,
outputOffset?: number
): number;
export function calculatePublicKey(
privateKey: ArrayBuffer,
output?: ArrayBuffer,
privateKey: Uint8Array,
output?: Uint8Array,
outputOffset: number = 0
): ArrayBuffer | number {
): Uint8Array | number {
// Android has its own public key generation algorithm
// See https://android.googlesource.com/platform/system/core.git/+/91784040db2b9273687f88d8b95f729d4a61ecc2/libcrypto_utils/android_pubkey.cpp#111
@ -149,7 +149,7 @@ export function calculatePublicKey(
let outputType: 'ArrayBuffer' | 'number';
const outputLength = calculatePublicKeyLength();
if (!output) {
output = new ArrayBuffer(outputLength);
output = new Uint8Array(outputLength);
outputType = 'ArrayBuffer';
} else {
if (output.byteLength - outputOffset < outputLength) {
@ -159,7 +159,7 @@ export function calculatePublicKey(
outputType = 'number';
}
const outputView = new DataView(output);
const outputView = new DataView(output.buffer, output.byteOffset, output.byteLength);
// modulusLengthInWords
outputView.setUint32(outputOffset, 2048 / 8 / 4, true);
@ -227,14 +227,14 @@ export const Asn1Null = 0x05;
export const Asn1Oid = 0x06;
// PKCS#1 SHA-1 hash digest info
export const Sha1DigestInfo = [
export const Sha1DigestInfo = new Uint8Array([
Asn1Sequence, 0x0d + Sha1DigestLength,
Asn1Sequence, 0x09,
// SHA-1 (1 3 14 3 2 26)
Asn1Oid, 0x05, 1 * 40 + 3, 14, 3, 2, 26,
Asn1Null, 0x00,
Asn1OctetString, Sha1DigestLength
];
]);
// SubtleCrypto.sign() will hash the given data and sign the hash
// But we don't need the hashing step
@ -242,7 +242,7 @@ export const Sha1DigestInfo = [
// encrypt the given data with its private key)
// However SubtileCrypto.encrypt() doesn't accept 'RSASSA-PKCS1-v1_5' algorithm
// So we need to implement the encryption by ourself
export function sign(privateKey: ArrayBuffer, data: ArrayBuffer): ArrayBuffer {
export function sign(privateKey: Uint8Array, data: Uint8Array): ArrayBuffer {
const [n, d] = parsePrivateKey(privateKey);
// PKCS#1 padding
@ -255,7 +255,7 @@ export function sign(privateKey: ArrayBuffer, data: ArrayBuffer): ArrayBuffer {
padded[index] = 1;
index += 1;
const fillLength = padded.length - Sha1DigestInfo.length - data.byteLength - 1;
const fillLength = padded.length - Sha1DigestInfo.length - data.length - 1;
while (index < fillLength) {
padded[index] = 0xff;
index += 1;
@ -264,14 +264,14 @@ export function sign(privateKey: ArrayBuffer, data: ArrayBuffer): ArrayBuffer {
padded[index] = 0;
index += 1;
padded.set(new Uint8Array(Sha1DigestInfo), index);
padded.set(Sha1DigestInfo, index);
index += Sha1DigestInfo.length;
padded.set(new Uint8Array(data), index);
padded.set(data, index);
// Encryption
// signature = padded ** d % n
let signature = powMod(getBig(padded.buffer), d, n);
let signature = powMod(getBig(padded), d, n);
// Put into an ArrayBuffer
const result = new ArrayBuffer(256);

View file

@ -1,3 +1,10 @@
declare global {
interface ArrayBuffer {
// Disallow assigning `Arraybuffer` to `Uint8Array`
__brand: never;
}
}
export * from './adb';
export * from './auth';
export * from './backend';

View file

@ -22,13 +22,13 @@ const AdbPacketHeader =
export const AdbPacket =
new Struct({ littleEndian: true })
.fields(AdbPacketHeader)
.arrayBuffer('payload', { lengthField: 'payloadLength' });
.uint8Array('payload', { lengthField: 'payloadLength' });
export type AdbPacket = typeof AdbPacket['TDeserializeResult'];
export type AdbPacketInit = Omit<typeof AdbPacket['TInit'], 'checksum' | 'magic'>;
export class AdbPacketSerializeStream extends TransformStream<AdbPacketInit, ArrayBuffer>{
export class AdbPacketSerializeStream extends TransformStream<AdbPacketInit, Uint8Array>{
public calculateChecksum = true;
public constructor() {
@ -49,7 +49,7 @@ export class AdbPacketSerializeStream extends TransformStream<AdbPacketInit, Arr
payloadLength: init.payload.byteLength,
};
controller.enqueue(AdbPacketHeader.serialize(packet));
controller.enqueue(new Uint8Array(AdbPacketHeader.serialize(packet)));
if (packet.payloadLength) {
controller.enqueue(packet.payload);

View file

@ -34,12 +34,12 @@ export class AdbSocketController implements AdbSocketInfo {
public readonly localCreated!: boolean;
public readonly serviceString!: string;
private readonly _readablePassthrough: TransformStream<ArrayBuffer, ArrayBuffer>;
private readonly _readablePassthroughWriter: WritableStreamDefaultWriter<ArrayBuffer>;
private readonly _readablePassthrough: TransformStream<Uint8Array, Uint8Array>;
private readonly _readablePassthroughWriter: WritableStreamDefaultWriter<Uint8Array>;
public get readable() { return this._readablePassthrough.readable; }
private _writePromise: PromiseResolver<void> | undefined;
public readonly writable: WritableStream<ArrayBuffer>;
public readonly writable: WritableStream<Uint8Array>;
private _writableClosed = false;
@ -61,7 +61,7 @@ export class AdbSocketController implements AdbSocketInfo {
const { readable, writable } = new ChunkStream(this.dispatcher.maxPayloadSize);
this.writable = writable;
readable.pipeTo(new WritableStream<ArrayBuffer>({
readable.pipeTo(new WritableStream<Uint8Array>({
write: async (chunk) => {
if (this._writableClosed) {
throw new Error('Socket closed');
@ -87,7 +87,7 @@ export class AdbSocketController implements AdbSocketInfo {
}));
}
public enqueue(packet: ArrayBuffer) {
public enqueue(packet: Uint8Array) {
return this._readablePassthroughWriter.write(packet);
}

View file

@ -23,7 +23,7 @@ export interface AdbIncomingSocketEventArgs {
socket: AdbSocket;
}
const EmptyArrayBuffer = new ArrayBuffer(0);
const EmptyUint8Array = new Uint8Array(0);
export class AdbPacketDispatcher extends AutoDisposable {
// ADB socket id starts from 1
@ -51,7 +51,7 @@ export class AdbPacketDispatcher extends AutoDisposable {
private _abortController = new AbortController();
public constructor(readable: ReadableStream<ArrayBuffer>, writable: WritableStream<ArrayBuffer>, logger?: AdbLogger) {
public constructor(readable: ReadableStream<Uint8Array>, writable: WritableStream<Uint8Array>, logger?: AdbLogger) {
super();
this.logger = logger;
@ -221,23 +221,27 @@ export class AdbPacketDispatcher extends AutoDisposable {
command: AdbCommand,
arg0: number,
arg1: number,
payload?: string | ArrayBuffer
payload?: string | Uint8Array
): Promise<void>;
public async sendPacket(
packetOrCommand: AdbPacketInit | AdbCommand,
arg0?: number,
arg1?: number,
payload: string | ArrayBuffer = EmptyArrayBuffer,
payload: string | Uint8Array = EmptyUint8Array,
): Promise<void> {
let init: AdbPacketInit;
if (arg0 === undefined) {
init = packetOrCommand as AdbPacketInit;
} else {
if (typeof payload === 'string') {
payload = encodeUtf8(payload);
}
init = {
command: packetOrCommand as AdbCommand,
arg0: arg0 as number,
arg1: arg1 as number,
payload: typeof payload === 'string' ? encodeUtf8(payload) : payload,
payload,
};
}

View file

@ -9,8 +9,8 @@ export class AdbSocket implements AdbSocketInfo {
public get localCreated() { return this.controller.localCreated; }
public get serviceString() { return this.controller.serviceString; }
public get readable(): ReadableStream<ArrayBuffer> { return this.controller.readable; }
public get writable(): WritableStream<ArrayBuffer> { return this.controller.writable; }
public get readable(): ReadableStream<Uint8Array> { return this.controller.readable; }
public get writable(): WritableStream<Uint8Array> { return this.controller.writable; }
public constructor(controller: AdbSocketController) {
this.controller = controller;

View file

@ -14,11 +14,11 @@ export class BufferedStreamEndedError extends Error {
export class BufferedStream {
private buffer: Uint8Array | undefined;
protected readonly stream: ReadableStream<ArrayBuffer>;
protected readonly stream: ReadableStream<Uint8Array>;
protected readonly reader: ReadableStreamDefaultReader<ArrayBuffer>;
protected readonly reader: ReadableStreamDefaultReader<Uint8Array>;
public constructor(stream: ReadableStream<ArrayBuffer>) {
public constructor(stream: ReadableStream<Uint8Array>) {
this.stream = stream;
this.reader = stream.getReader();
}
@ -29,14 +29,14 @@ export class BufferedStream {
* @param readToEnd When `true`, allow less data to be returned if the stream has reached its end.
* @returns
*/
public async read(length: number, readToEnd: boolean = false): Promise<ArrayBuffer> {
public async read(length: number, readToEnd: boolean = false): Promise<Uint8Array> {
let array: Uint8Array;
let index: number;
if (this.buffer) {
const buffer = this.buffer;
if (buffer.byteLength > length) {
this.buffer = buffer.subarray(length);
return buffer.slice(0, length).buffer;
return buffer.slice(0, length);
}
array = new Uint8Array(length);
@ -47,7 +47,7 @@ export class BufferedStream {
const result = await this.reader.read();
if (result.done) {
if (readToEnd) {
return new ArrayBuffer(0);
return new Uint8Array(0);
} else {
throw new Error('Unexpected end of stream');
}
@ -59,7 +59,7 @@ export class BufferedStream {
}
if (value.byteLength > length) {
this.buffer = new Uint8Array(value, length);
this.buffer = value.subarray(length);
return value.slice(0, length);
}
@ -74,7 +74,7 @@ export class BufferedStream {
const result = await this.reader.read();
if (result.done) {
if (readToEnd) {
return new ArrayBuffer(0);
return new Uint8Array(0);
} else {
throw new Error('Unexpected end of stream');
}
@ -82,16 +82,16 @@ export class BufferedStream {
const { value } = result;
if (value.byteLength > left) {
array.set(new Uint8Array(value, 0, left), index);
this.buffer = new Uint8Array(value, left);
return array.buffer;
array.set(value.subarray(0, left), index);
this.buffer = value.subarray(left);
return array;
}
array.set(new Uint8Array(value), index);
array.set(value, index);
index += value.byteLength;
}
return array.buffer;
return array;
}
public close() {

View file

@ -1,9 +1,9 @@
import Struct, { decodeUtf8, StructLike, StructValueType } from "@yume-chan/struct";
import { chunkArrayLike } from "../utils/chunk";
import Struct, { StructLike, StructValueType } from "@yume-chan/struct";
import { decodeUtf8 } from "../utils";
import { BufferedStream, BufferedStreamEndedError } from "./buffered";
import { TransformStream, WritableStream, WritableStreamDefaultWriter, ReadableStream, ReadableStreamDefaultReader } from "./detect";
import { ReadableStream, ReadableStreamDefaultReader, TransformStream, WritableStream, WritableStreamDefaultWriter } from "./detect";
export class DecodeUtf8Stream extends TransformStream<ArrayBuffer, string>{
export class DecodeUtf8Stream extends TransformStream<Uint8Array, string>{
public constructor() {
super({
transform(chunk, controller) {
@ -29,10 +29,10 @@ export class GatherStringStream extends TransformStream<string, string>{
// TODO: Find other ways to implement `StructTransformStream`
export class StructDeserializeStream<T extends StructLike<any>>
extends TransformStream<ArrayBuffer, StructValueType<T>>{
extends TransformStream<Uint8Array, StructValueType<T>>{
public constructor(struct: T) {
// Convert incoming chunk to a `ReadableStream`
const passthrough = new TransformStream<ArrayBuffer, ArrayBuffer>();
const passthrough = new TransformStream<Uint8Array, Uint8Array>();
const passthroughWriter = passthrough.writable.getWriter();
// Convert the `ReadableSteam` to a `BufferedStream`
const bufferedStream = new BufferedStream(passthrough.readable);
@ -66,11 +66,11 @@ export class StructDeserializeStream<T extends StructLike<any>>
}
export class StructSerializeStream<T extends Struct<any, any, any, any>>
extends TransformStream<T['TInit'], ArrayBuffer>{
extends TransformStream<T['TInit'], Uint8Array>{
constructor(struct: T) {
super({
transform(chunk, controller) {
controller.enqueue(struct.serialize(chunk));
controller.enqueue(new Uint8Array(struct.serialize(chunk)));
},
});
}
@ -149,12 +149,14 @@ export class WrapReadableStream<T, R extends ReadableStream<T>, S> extends Reada
}
}
export class ChunkStream extends TransformStream<ArrayBuffer, ArrayBuffer>{
export class ChunkStream extends TransformStream<Uint8Array, Uint8Array>{
public constructor(size: number) {
super({
transform(chunk, controller) {
for (const piece of chunkArrayLike(chunk, size)) {
controller.enqueue(piece);
for (let start = 0; start < chunk.length; start += size) {
const end = start + size;
controller.enqueue(chunk.slice(start, end));
start = end;
}
}
});

View file

@ -33,44 +33,42 @@ describe('base64', () => {
describe('decodeBase64', () => {
it("input length 0", () => {
expect(new Uint8Array(decodeBase64(''))).toEqual(new Uint8Array());
expect(decodeBase64('')).toEqual(new Uint8Array());
});
it("input length 1", () => {
expect(new Uint8Array(decodeBase64('AA=='))).toEqual(new Uint8Array([0]));
expect(decodeBase64('AA==')).toEqual(new Uint8Array([0]));
});
it("input length 2", () => {
expect(new Uint8Array(decodeBase64('AAE='))).toEqual(new Uint8Array([0, 1]));
expect(decodeBase64('AAE=')).toEqual(new Uint8Array([0, 1]));
});
it("input length 3", () => {
/* cspell: disable-next-line */
expect(new Uint8Array(decodeBase64('AAEC'))).toEqual(new Uint8Array([0, 1, 2]));
expect(decodeBase64('AAEC')).toEqual(new Uint8Array([0, 1, 2]));
});
it("input length 4", () => {
/* cspell: disable-next-line */
expect(new Uint8Array(decodeBase64('AAECAw=='))).toEqual(new Uint8Array([0, 1, 2, 3]));
expect(decodeBase64('AAECAw==')).toEqual(new Uint8Array([0, 1, 2, 3]));
});
it("input length 5", () => {
/* cspell: disable-next-line */
expect(new Uint8Array(decodeBase64('AAECAwQ='))).toEqual(new Uint8Array([0, 1, 2, 3, 4]));
expect(decodeBase64('AAECAwQ=')).toEqual(new Uint8Array([0, 1, 2, 3, 4]));
});
it("input length 6", () => {
/* cspell: disable-next-line */
expect(new Uint8Array(decodeBase64('AAECAwQF'))).toEqual(new Uint8Array([0, 1, 2, 3, 4, 5]));
expect(decodeBase64('AAECAwQF')).toEqual(new Uint8Array([0, 1, 2, 3, 4, 5]));
});
it("all byte values", () => {
expect(
new Uint8Array(
decodeBase64(
'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w=='
)
)
).toEqual(new Uint8Array(Array.from({ length: 256 }, (_, index) => index)));
});
});
@ -78,99 +76,83 @@ describe('base64', () => {
describe('encodeBase64', () => {
it('input length 0', () => {
expect(
new Uint8Array(
encodeBase64(
new Uint8Array()
)
)
).toEqual(new Uint8Array());
});
it('input length 1', () => {
expect(
new Uint8Array(
encodeBase64(
new Uint8Array(
[0]
)
)
)
).toEqual(new Uint8Array([65, 65, 61, 61])); // AA==
});
it('input length 2', () => {
expect(
new Uint8Array(
encodeBase64(
new Uint8Array(
[0, 1]
)
)
)
).toEqual(new Uint8Array([65, 65, 69, 61])); // AAE=
});
it('input length 3', () => {
expect(
new Uint8Array(
encodeBase64(
new Uint8Array(
[0, 1, 2]
)
)
)
/* cspell: disable-next-line */
).toEqual(new Uint8Array([65, 65, 69, 67])); // AAEC
});
it('input length 4', () => {
expect(
new Uint8Array(
encodeBase64(
new Uint8Array(
[0, 1, 2, 3]
)
)
)
/* cspell: disable-next-line */
).toEqual(new Uint8Array([65, 65, 69, 67, 65, 119, 61, 61])); // AAECAw==
});
it('input length 5', () => {
expect(
new Uint8Array(
encodeBase64(
new Uint8Array(
[0, 1, 2, 3, 4]
)
)
)
/* cspell: disable-next-line */
).toEqual(new Uint8Array([65, 65, 69, 67, 65, 119, 81, 61])); // AAECAwQ=
});
it('input length 6', () => {
expect(
new Uint8Array(
encodeBase64(
new Uint8Array(
[0, 1, 2, 3, 4, 5]
)
)
)
/* cspell: disable-next-line */
).toEqual(new Uint8Array([65, 65, 69, 67, 65, 119, 81, 70])); // AAECAwQF
});
it("all byte values", () => {
expect(
new Uint8Array(
encodeBase64(
new Uint8Array(
Array.from({ length: 256 }, (_, index) => index)
)
)
)
).toEqual(new Uint8Array([65, 65, 69, 67, 65, 119, 81, 70, 66, 103, 99, 73, 67, 81, 111, 76, 68, 65, 48, 79, 68, 120, 65, 82, 69, 104, 77, 85, 70, 82, 89, 88, 71, 66, 107, 97, 71, 120, 119, 100, 72, 104, 56, 103, 73, 83, 73, 106, 74, 67, 85, 109, 74, 121, 103, 112, 75, 105, 115, 115, 76, 83, 52, 118, 77, 68, 69, 121, 77, 122, 81, 49, 78, 106, 99, 52, 79, 84, 111, 55, 80, 68, 48, 43, 80, 48, 66, 66, 81, 107, 78, 69, 82, 85, 90, 72, 83, 69, 108, 75, 83, 48, 120, 78, 84, 107, 57, 81, 85, 86, 74, 84, 86, 70, 86, 87, 86, 49, 104, 90, 87, 108, 116, 99, 88, 86, 53, 102, 89, 71, 70, 105, 89, 50, 82, 108, 90, 109, 100, 111, 97, 87, 112, 114, 98, 71, 49, 117, 98, 51, 66, 120, 99, 110, 78, 48, 100, 88, 90, 51, 101, 72, 108, 54, 101, 51, 120, 57, 102, 110, 43, 65, 103, 89, 75, 68, 104, 73, 87, 71, 104, 52, 105, 74, 105, 111, 117, 77, 106, 89, 54, 80, 107, 74, 71, 83, 107, 53, 83, 86, 108, 112, 101, 89, 109, 90, 113, 98, 110, 74, 50, 101, 110, 54, 67, 104, 111, 113, 79, 107, 112, 97, 97, 110, 113, 75, 109, 113, 113, 54, 121, 116, 114, 113, 43, 119, 115, 98, 75, 122, 116, 76, 87, 50, 116, 55, 105, 53, 117, 114, 117, 56, 118, 98, 54, 47, 119, 77, 72, 67, 119, 56, 84, 70, 120, 115, 102, 73, 121, 99, 114, 76, 122, 77, 51, 79, 122, 57, 68, 82, 48, 116, 80, 85, 49, 100, 98, 88, 50, 78, 110, 97, 50, 57, 122, 100, 51, 116, 47, 103, 52, 101, 76, 106, 53, 79, 88, 109, 53, 43, 106, 112, 54, 117, 118, 115, 55, 101, 55, 118, 56, 80, 72, 121, 56, 47, 84, 49, 57, 118, 102, 52, 43, 102, 114, 55, 47, 80, 51, 43, 47, 119, 61, 61]));
// AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==
});

View file

@ -26,46 +26,35 @@ export function calculateBase64EncodedLength(inputLength: number): [outputLength
}
export function encodeBase64(
input: ArrayBuffer | Uint8Array,
input: Uint8Array,
inputOffset?: number,
inputLength?: number,
): ArrayBuffer; // overload 1
): Uint8Array; // overload 1
export function encodeBase64(
input: ArrayBuffer | Uint8Array,
output: ArrayBuffer | Uint8Array,
input: Uint8Array,
output: Uint8Array,
outputOffset?: number
): number; // overload 2
export function encodeBase64(
input: ArrayBuffer | Uint8Array,
input: Uint8Array,
inputOffset: number,
output: ArrayBuffer | Uint8Array,
output: Uint8Array,
outputOffset?: number
): number; // overload 3
export function encodeBase64(
input: ArrayBuffer | Uint8Array,
input: Uint8Array,
inputOffset: number,
inputLength: number,
output: ArrayBuffer | Uint8Array,
output: Uint8Array,
outputOffset?: number
): number; // overload 4
export function encodeBase64(
input: ArrayBuffer | Uint8Array,
arg1?: number | ArrayBuffer | Uint8Array,
arg2?: number | ArrayBuffer | Uint8Array,
_arg3?: number | ArrayBuffer | Uint8Array,
input: Uint8Array,
arg1?: number | Uint8Array,
arg2?: number | Uint8Array,
_arg3?: number | Uint8Array,
_arg4?: number,
): ArrayBuffer | number {
if (input instanceof ArrayBuffer) {
input = new Uint8Array(input);
}
// Because `Uint8Array` is type compatible with `ArrayBuffer`,
// TypeScript doesn't correctly narrow `input` to `Uint8Array` when assigning.
// Manually eliminate `ArrayBuffer` from `input` with a type guard.
if (input instanceof ArrayBuffer) {
return input;
}
): Uint8Array | number {
let inputOffset: number;
let inputLength: number;
let output: Uint8Array;
@ -82,46 +71,33 @@ export function encodeBase64(
outputArgumentIndex = 3;
} else {
// overload 3
inputLength = input.byteLength - inputOffset;
inputLength = input.length - inputOffset;
outputArgumentIndex = 2;
}
} else {
// overload 2
inputOffset = 0;
inputLength = input.byteLength;
inputLength = input.length;
outputArgumentIndex = 1;
}
const [outputLength, paddingLength] = calculateBase64EncodedLength(inputLength);
let maybeOutput: ArrayBuffer | Uint8Array | undefined = arguments[outputArgumentIndex];
let outputType: 'ArrayBuffer' | 'number';
let maybeOutput: Uint8Array | undefined = arguments[outputArgumentIndex];
let outputType: 'Uint8Array' | 'number';
if (maybeOutput) {
outputOffset = arguments[outputArgumentIndex + 1] ?? 0;
if (maybeOutput.byteLength - outputOffset < outputLength) {
if (maybeOutput.length - outputOffset < outputLength) {
throw new Error('output buffer is too small');
}
if (maybeOutput instanceof ArrayBuffer) {
output = new Uint8Array(maybeOutput);
} else {
output = maybeOutput;
}
outputType = 'number';
} else {
const buffer = new ArrayBuffer(outputLength);
output = new Uint8Array(buffer);
output = new Uint8Array(outputLength);
outputOffset = 0;
outputType = 'ArrayBuffer';
}
// Because `Uint8Array` is type compatible with `ArrayBuffer`,
// TypeScript doesn't correctly narrow `output` to `Uint8Array` when assigning.
// Manually eliminate `ArrayBuffer` from `output` with a type guard.
if (output instanceof ArrayBuffer) {
return output;
outputType = 'Uint8Array';
}
if (input.buffer === output.buffer) {
@ -209,14 +185,14 @@ export function encodeBase64(
outputIndex -= 1;
}
if (outputType === 'ArrayBuffer') {
return output.buffer;
if (outputType === 'Uint8Array') {
return output;
} else {
return outputLength;
}
}
export function decodeBase64(input: string): ArrayBuffer {
export function decodeBase64(input: string): Uint8Array {
let padding: number;
if (input[input.length - 2] === '=') {
padding = 2;
@ -275,5 +251,5 @@ export function decodeBase64(input: string): ArrayBuffer {
result[dIndex] = (a << 2) | ((b & 0b11_0000) >> 4);
}
return result.buffer;
return result;
}

View file

@ -1,70 +0,0 @@
export function* chunkArrayLike(
value: ArrayLike<number> | ArrayBufferLike,
size: number
): Generator<ArrayBuffer, void, void> {
if ('length' in value) {
value = new Uint8Array(value).buffer;
}
if (value.byteLength <= size) {
return yield value;
}
for (let i = 0; i < value.byteLength; i += size) {
yield value.slice(i, i + size);
}
}
export async function* chunkAsyncIterable(
value: AsyncIterable<ArrayBuffer>,
size: number
): AsyncGenerator<ArrayBuffer, void, void> {
let result = new Uint8Array(size);
let index = 0;
for await (let buffer of value) {
// `result` has some data, `result + buffer` is enough
if (index !== 0 && index + buffer.byteLength >= size) {
const remainder = size - index;
result.set(new Uint8Array(buffer, 0, remainder), index);
yield result.buffer;
result = new Uint8Array(size);
index = 0;
if (buffer.byteLength > remainder) {
// `buffer` still has some data
buffer = buffer.slice(remainder);
} else {
continue;
}
}
// `result` is empty, `buffer` alone is enough
if (buffer.byteLength >= size) {
let remainder = false;
for (const chunk of chunkArrayLike(buffer, size)) {
if (chunk.byteLength === size) {
yield chunk;
continue;
}
// `buffer` still has some data
remainder = true;
buffer = chunk;
}
if (!remainder) {
continue;
}
}
// `result` has some data but `result + buffer` is still not enough
// or after previous steps `buffer` still has some data
result.set(new Uint8Array(buffer), index);
index += buffer.byteLength;
}
if (index !== 0) {
yield result.buffer.slice(0, index);
}
}

View file

@ -4,10 +4,10 @@ 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 encodeUtf8(input: string): Uint8Array {
return Utf8Encoder.encode(input);
}
export function decodeUtf8(buffer: ArrayBuffer): string {
export function decodeUtf8(buffer: Uint8Array): string {
return Utf8Decoder.decode(buffer);
}

View file

@ -1,4 +1,3 @@
export * from './auto-reset-event';
export * from './base64';
export * from './chunk';
export * from './encoding';

View file

@ -1,6 +1,6 @@
import { StructAsyncDeserializeStream, StructDefaultOptions, StructDeserializeStream, StructFieldDefinition, StructFieldValue, StructOptions, StructValue } from './basic';
import { Struct } from './struct';
import { ArrayBufferFieldType, BigIntFieldDefinition, BigIntFieldType, FixedLengthArrayBufferLikeFieldDefinition, NumberFieldDefinition, NumberFieldType, StringFieldType, Uint8ClampedArrayFieldType, VariableLengthArrayBufferLikeFieldDefinition } from './types';
import { ArrayBufferFieldType, BigIntFieldDefinition, BigIntFieldType, FixedLengthArrayBufferLikeFieldDefinition, NumberFieldDefinition, NumberFieldType, StringFieldType, ArrayBufferViewFieldType, VariableLengthArrayBufferLikeFieldDefinition } from './types';
import { ValueOrPromise } from './utils';
class MockDeserializationStream implements StructDeserializeStream {
@ -181,7 +181,7 @@ describe('Struct', () => {
const definition = struct['_fields'][0]![1] as FixedLengthArrayBufferLikeFieldDefinition;
expect(definition).toBeInstanceOf(FixedLengthArrayBufferLikeFieldDefinition);
expect(definition.type).toBeInstanceOf(Uint8ClampedArrayFieldType);
expect(definition.type).toBeInstanceOf(ArrayBufferViewFieldType);
expect(definition.options.length).toBe(10);
});
@ -220,7 +220,7 @@ describe('Struct', () => {
const definition = struct['_fields'][1]![1] as VariableLengthArrayBufferLikeFieldDefinition;
expect(definition).toBeInstanceOf(VariableLengthArrayBufferLikeFieldDefinition);
expect(definition.type).toBeInstanceOf(Uint8ClampedArrayFieldType);
expect(definition.type).toBeInstanceOf(ArrayBufferViewFieldType);
expect(definition.options.lengthField).toBe('barLength');
});

View file

@ -3,7 +3,7 @@
import type { StructAsyncDeserializeStream, StructDeserializeStream, StructFieldDefinition, StructFieldValue, StructOptions } from './basic';
import { StructDefaultOptions, StructValue } from './basic';
import { Syncbird } from "./syncbird";
import { ArrayBufferFieldType, ArrayBufferLikeFieldType, BigIntFieldDefinition, BigIntFieldType, FixedLengthArrayBufferLikeFieldDefinition, FixedLengthArrayBufferLikeFieldOptions, LengthField, NumberFieldDefinition, NumberFieldType, StringFieldType, Uint8ClampedArrayFieldType, VariableLengthArrayBufferLikeFieldDefinition, VariableLengthArrayBufferLikeFieldOptions } from './types';
import { ArrayBufferFieldType, ArrayBufferLikeFieldType, BigIntFieldDefinition, BigIntFieldType, FixedLengthArrayBufferLikeFieldDefinition, FixedLengthArrayBufferLikeFieldOptions, LengthField, NumberFieldDefinition, NumberFieldType, StringFieldType, ArrayBufferViewFieldType, VariableLengthArrayBufferLikeFieldDefinition, VariableLengthArrayBufferLikeFieldOptions } from './types';
import { Evaluate, Identity, Overwrite, ValueOrPromise } from "./utils";
export interface StructLike<TValue> {
@ -450,17 +450,30 @@ export class Struct<
return this.arrayBufferLike(name, ArrayBufferFieldType.instance, options);
};
public uint8Array: BoundArrayBufferLikeFieldDefinitionCreator<
TFields,
TOmitInitKey,
TExtra,
TPostDeserialized,
ArrayBufferViewFieldType<Uint8Array>
> = (
name: PropertyKey,
options: any
): any => {
return this.arrayBufferLike(name, ArrayBufferViewFieldType.uint8Array, options);
};
public uint8ClampedArray: BoundArrayBufferLikeFieldDefinitionCreator<
TFields,
TOmitInitKey,
TExtra,
TPostDeserialized,
Uint8ClampedArrayFieldType
ArrayBufferViewFieldType<Uint8ClampedArray>
> = (
name: PropertyKey,
options: any
): any => {
return this.arrayBufferLike(name, Uint8ClampedArrayFieldType.instance, options);
return this.arrayBufferLike(name, ArrayBufferViewFieldType.uint8ClampedArray, options);
};
public string: BoundArrayBufferLikeFieldDefinitionCreator<

View file

@ -1,5 +1,5 @@
import { StructDefaultOptions, StructDeserializeStream, StructValue } from '../basic';
import { ArrayBufferFieldType, ArrayBufferLikeFieldDefinition, ArrayBufferLikeFieldType, StringFieldType, Uint8ClampedArrayFieldType } from './array-buffer';
import { ArrayBufferFieldType, ArrayBufferLikeFieldDefinition, ArrayBufferLikeFieldType, StringFieldType, ArrayBufferViewFieldType } from './array-buffer';
class MockDeserializationStream implements StructDeserializeStream {
public buffer = new ArrayBuffer(0);
@ -32,18 +32,18 @@ describe('Types', () => {
describe('Uint8ClampedArrayFieldType', () => {
it('should have a static instance', () => {
expect(Uint8ClampedArrayFieldType.instance).toBeInstanceOf(Uint8ClampedArrayFieldType);
expect(ArrayBufferViewFieldType.instance).toBeInstanceOf(ArrayBufferViewFieldType);
});
it('`#toArrayBuffer` should return its `buffer`', () => {
const array = new Uint8ClampedArray(10);
const buffer = array.buffer;
expect(Uint8ClampedArrayFieldType.instance.toArrayBuffer(array)).toBe(buffer);
expect(ArrayBufferViewFieldType.instance.toArrayBuffer(array)).toBe(buffer);
});
it('`#fromArrayBuffer` should return a view of the `ArrayBuffer`', () => {
const arrayBuffer = new ArrayBuffer(10);
const array = Uint8ClampedArrayFieldType.instance.fromArrayBuffer(arrayBuffer);
const array = ArrayBufferViewFieldType.instance.fromArrayBuffer(arrayBuffer);
expect(array).toHaveProperty('buffer', arrayBuffer);
expect(array).toHaveProperty('byteOffset', 0);
expect(array).toHaveProperty('byteLength', 10);
@ -51,7 +51,7 @@ describe('Types', () => {
it('`#getSize` should return the `byteLength` of the `Uint8ClampedArray`', () => {
const array = new Uint8ClampedArray(10);
expect(Uint8ClampedArrayFieldType.instance.getSize(array)).toBe(10);
expect(ArrayBufferViewFieldType.instance.getSize(array)).toBe(10);
});
});
@ -104,7 +104,7 @@ describe('Types', () => {
it('should work with `Uint8ClampedArrayFieldType`', async () => {
const size = 10;
const definition = new MockArrayBufferFieldDefinition(Uint8ClampedArrayFieldType.instance, size);
const definition = new MockArrayBufferFieldDefinition(ArrayBufferViewFieldType.instance, size);
const context = new MockDeserializationStream();
const buffer = new ArrayBuffer(size);

View file

@ -1,3 +1,5 @@
// cspell: ignore syncbird
import { StructAsyncDeserializeStream, StructDeserializeStream, StructFieldDefinition, StructFieldValue, StructOptions, StructValue } from '../basic';
import { Syncbird } from "../syncbird";
import { decodeUtf8, encodeUtf8, ValueOrPromise } from "../utils";
@ -56,24 +58,30 @@ export class ArrayBufferFieldType extends ArrayBufferLikeFieldType<ArrayBuffer>
}
}
/** Am ArrayBufferLike type that converts between `ArrayBuffer` and `Uint8ClampedArray` */
export class Uint8ClampedArrayFieldType
extends ArrayBufferLikeFieldType<Uint8ClampedArray, Uint8ClampedArray> {
public static readonly instance = new Uint8ClampedArrayFieldType();
export type ArrayBufferViewConstructor<T> = new (array: ArrayLike<number> | ArrayBufferLike) => T;
protected constructor() {
/** Am ArrayBufferLike type that converts between `ArrayBuffer` and `Uint8ClampedArray` */
export class ArrayBufferViewFieldType<T extends ArrayBufferView>
extends ArrayBufferLikeFieldType<T, T> {
public static readonly uint8Array = new ArrayBufferViewFieldType(Uint8Array);
public static readonly uint8ClampedArray = new ArrayBufferViewFieldType(Uint8ClampedArray);
protected type: ArrayBufferViewConstructor<T>;
public constructor(type: ArrayBufferViewConstructor<T>) {
super();
this.type = type;
}
public toArrayBuffer(value: Uint8ClampedArray): ArrayBuffer {
public toArrayBuffer(value: T): ArrayBuffer {
return value.buffer;
}
public fromArrayBuffer(arrayBuffer: ArrayBuffer): Uint8ClampedArray {
return new Uint8ClampedArray(arrayBuffer);
public fromArrayBuffer(arrayBuffer: ArrayBuffer): T {
return new this.type(arrayBuffer);
}
public getSize(value: Uint8ClampedArray): number {
public getSize(value: T): number {
return value.byteLength;
}
}

View file

@ -53,10 +53,10 @@ const Utf8Encoder = new TextEncoder();
// @ts-expect-error @types/node missing `TextDecoder`
const Utf8Decoder = new TextDecoder();
export function encodeUtf8(input: string): ArrayBuffer {
export function encodeUtf8(input: string): Uint8Array {
return Utf8Encoder.encode(input).buffer;
}
export function decodeUtf8(buffer: ArrayBuffer): string {
export function decodeUtf8(buffer: Uint8Array): string {
return Utf8Decoder.decode(buffer);
}