mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-03 09:49:24 +02:00
feat(struct): new API full rewrite
This commit is contained in:
parent
a29268426d
commit
d50a170ab8
89 changed files with 1487 additions and 5512 deletions
|
@ -20,7 +20,7 @@ import {
|
|||
pipeFrom,
|
||||
} from "@yume-chan/stream-extra";
|
||||
import type { ExactReadable } from "@yume-chan/struct";
|
||||
import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct";
|
||||
import { EmptyUint8Array } from "@yume-chan/struct";
|
||||
|
||||
import type { UsbInterfaceFilter } from "./utils.js";
|
||||
import {
|
||||
|
@ -185,7 +185,7 @@ export class AdbDaemonWebUsbConnection
|
|||
if (zeroMask && (chunk.length & zeroMask) === 0) {
|
||||
await device.raw.transferOut(
|
||||
outEndpoint.endpointNumber,
|
||||
EMPTY_UINT8_ARRAY,
|
||||
EmptyUint8Array,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
|
@ -234,7 +234,7 @@ export class AdbDaemonWebUsbConnection
|
|||
);
|
||||
packet.payload = new Uint8Array(result.data!.buffer);
|
||||
} else {
|
||||
packet.payload = EMPTY_UINT8_ARRAY;
|
||||
packet.payload = EmptyUint8Array;
|
||||
}
|
||||
|
||||
return packet;
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
BufferedReadableStream,
|
||||
PushReadableStream,
|
||||
} from "@yume-chan/stream-extra";
|
||||
import type { ValueOrPromise } from "@yume-chan/struct";
|
||||
import type { MaybePromiseLike } from "@yume-chan/struct";
|
||||
|
||||
export interface AdbScrcpyConnectionOptions {
|
||||
scid: number;
|
||||
|
@ -54,7 +54,7 @@ export abstract class AdbScrcpyConnection implements Disposable {
|
|||
this.socketName = this.getSocketName();
|
||||
}
|
||||
|
||||
initialize(): ValueOrPromise<void> {
|
||||
initialize(): MaybePromiseLike<void> {
|
||||
// pure virtual method
|
||||
}
|
||||
|
||||
|
@ -66,7 +66,7 @@ export abstract class AdbScrcpyConnection implements Disposable {
|
|||
return socketName;
|
||||
}
|
||||
|
||||
abstract getStreams(): ValueOrPromise<AdbScrcpyConnectionStreams>;
|
||||
abstract getStreams(): MaybePromiseLike<AdbScrcpyConnectionStreams>;
|
||||
|
||||
dispose(): void {
|
||||
// pure virtual method
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
PushReadableStream,
|
||||
tryClose,
|
||||
} from "@yume-chan/stream-extra";
|
||||
import type { ValueOrPromise } from "@yume-chan/struct";
|
||||
import type { MaybePromiseLike } from "@yume-chan/struct";
|
||||
|
||||
function nodeSocketToConnection(
|
||||
socket: Socket,
|
||||
|
@ -138,7 +138,7 @@ export class AdbServerNodeTcpConnector
|
|||
return address;
|
||||
}
|
||||
|
||||
removeReverseTunnel(address: string): ValueOrPromise<void> {
|
||||
removeReverseTunnel(address: string): MaybePromiseLike<void> {
|
||||
const server = this.#listeners.get(address);
|
||||
if (!server) {
|
||||
return;
|
||||
|
@ -147,7 +147,7 @@ export class AdbServerNodeTcpConnector
|
|||
this.#listeners.delete(address);
|
||||
}
|
||||
|
||||
clearReverseTunnels(): ValueOrPromise<void> {
|
||||
clearReverseTunnels(): MaybePromiseLike<void> {
|
||||
for (const server of this.#listeners.values()) {
|
||||
server.close();
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import type {
|
|||
ReadableWritablePair,
|
||||
} from "@yume-chan/stream-extra";
|
||||
import { ConcatStringStream, TextDecoderStream } from "@yume-chan/stream-extra";
|
||||
import type { ValueOrPromise } from "@yume-chan/struct";
|
||||
import type { MaybePromiseLike } from "@yume-chan/struct";
|
||||
|
||||
import type { AdbBanner } from "./banner.js";
|
||||
import type { AdbFrameBuffer } from "./commands/index.js";
|
||||
|
@ -19,7 +19,7 @@ import {
|
|||
import type { AdbFeature } from "./features.js";
|
||||
|
||||
export interface Closeable {
|
||||
close(): ValueOrPromise<void>;
|
||||
close(): MaybePromiseLike<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -37,7 +37,7 @@ export interface AdbSocket
|
|||
|
||||
export type AdbIncomingSocketHandler = (
|
||||
socket: AdbSocket,
|
||||
) => ValueOrPromise<void>;
|
||||
) => MaybePromiseLike<void>;
|
||||
|
||||
export interface AdbTransport extends Closeable {
|
||||
readonly serial: string;
|
||||
|
@ -50,16 +50,16 @@ export interface AdbTransport extends Closeable {
|
|||
|
||||
readonly clientFeatures: readonly AdbFeature[];
|
||||
|
||||
connect(service: string): ValueOrPromise<AdbSocket>;
|
||||
connect(service: string): MaybePromiseLike<AdbSocket>;
|
||||
|
||||
addReverseTunnel(
|
||||
handler: AdbIncomingSocketHandler,
|
||||
address?: string,
|
||||
): ValueOrPromise<string>;
|
||||
): MaybePromiseLike<string>;
|
||||
|
||||
removeReverseTunnel(address: string): ValueOrPromise<void>;
|
||||
removeReverseTunnel(address: string): MaybePromiseLike<void>;
|
||||
|
||||
clearReverseTunnels(): ValueOrPromise<void>;
|
||||
clearReverseTunnels(): MaybePromiseLike<void>;
|
||||
}
|
||||
|
||||
export class Adb implements Closeable {
|
||||
|
|
|
@ -1,50 +1,53 @@
|
|||
import { BufferedReadableStream } from "@yume-chan/stream-extra";
|
||||
import Struct, { StructEmptyError } from "@yume-chan/struct";
|
||||
import type { StructValue } from "@yume-chan/struct";
|
||||
import { buffer, Struct, StructEmptyError, u32 } from "@yume-chan/struct";
|
||||
|
||||
import type { Adb } from "../adb.js";
|
||||
|
||||
const Version =
|
||||
/* #__PURE__ */
|
||||
new Struct({ littleEndian: true }).uint32("version");
|
||||
const Version = new Struct({ version: u32 }, { littleEndian: true });
|
||||
|
||||
export const AdbFrameBufferV1 =
|
||||
/* #__PURE__ */
|
||||
new Struct({ littleEndian: true })
|
||||
.uint32("bpp")
|
||||
.uint32("size")
|
||||
.uint32("width")
|
||||
.uint32("height")
|
||||
.uint32("red_offset")
|
||||
.uint32("red_length")
|
||||
.uint32("blue_offset")
|
||||
.uint32("blue_length")
|
||||
.uint32("green_offset")
|
||||
.uint32("green_length")
|
||||
.uint32("alpha_offset")
|
||||
.uint32("alpha_length")
|
||||
.uint8Array("data", { lengthField: "size" });
|
||||
export const AdbFrameBufferV1 = new Struct(
|
||||
{
|
||||
bpp: u32,
|
||||
size: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
red_offset: u32,
|
||||
red_length: u32,
|
||||
blue_offset: u32,
|
||||
blue_length: u32,
|
||||
green_offset: u32,
|
||||
green_length: u32,
|
||||
alpha_offset: u32,
|
||||
alpha_length: u32,
|
||||
data: buffer("size"),
|
||||
},
|
||||
{ littleEndian: true },
|
||||
);
|
||||
|
||||
export type AdbFrameBufferV1 = (typeof AdbFrameBufferV1)["TDeserializeResult"];
|
||||
export type AdbFrameBufferV1 = StructValue<typeof AdbFrameBufferV1>;
|
||||
|
||||
export const AdbFrameBufferV2 =
|
||||
/* #__PURE__ */
|
||||
new Struct({ littleEndian: true })
|
||||
.uint32("bpp")
|
||||
.uint32("colorSpace")
|
||||
.uint32("size")
|
||||
.uint32("width")
|
||||
.uint32("height")
|
||||
.uint32("red_offset")
|
||||
.uint32("red_length")
|
||||
.uint32("blue_offset")
|
||||
.uint32("blue_length")
|
||||
.uint32("green_offset")
|
||||
.uint32("green_length")
|
||||
.uint32("alpha_offset")
|
||||
.uint32("alpha_length")
|
||||
.uint8Array("data", { lengthField: "size" });
|
||||
export const AdbFrameBufferV2 = new Struct(
|
||||
{
|
||||
bpp: u32,
|
||||
colorSpace: u32,
|
||||
size: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
red_offset: u32,
|
||||
red_length: u32,
|
||||
blue_offset: u32,
|
||||
blue_length: u32,
|
||||
green_offset: u32,
|
||||
green_length: u32,
|
||||
alpha_offset: u32,
|
||||
alpha_length: u32,
|
||||
data: buffer("size"),
|
||||
},
|
||||
{ littleEndian: true },
|
||||
);
|
||||
|
||||
export type AdbFrameBufferV2 = (typeof AdbFrameBufferV2)["TDeserializeResult"];
|
||||
export type AdbFrameBufferV2 = StructValue<typeof AdbFrameBufferV2>;
|
||||
|
||||
/**
|
||||
* ADB uses 8 int32 fields to describe bit depths
|
||||
|
@ -99,9 +102,9 @@ export async function framebuffer(adb: Adb): Promise<AdbFrameBuffer> {
|
|||
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);
|
||||
return await AdbFrameBufferV1.deserialize(stream);
|
||||
case 2:
|
||||
return AdbFrameBufferV2.deserialize(stream);
|
||||
return await AdbFrameBufferV2.deserialize(stream);
|
||||
default:
|
||||
throw new AdbFrameBufferUnsupportedVersionError(version);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
// cspell: ignore killforward
|
||||
|
||||
import { BufferedReadableStream } from "@yume-chan/stream-extra";
|
||||
import Struct, { ExactReadableEndedError, encodeUtf8 } from "@yume-chan/struct";
|
||||
import {
|
||||
ExactReadableEndedError,
|
||||
Struct,
|
||||
encodeUtf8,
|
||||
string,
|
||||
} from "@yume-chan/struct";
|
||||
|
||||
import type { Adb, AdbIncomingSocketHandler } from "../adb.js";
|
||||
import { hexToNumber, sequenceEqual } from "../utils/index.js";
|
||||
|
@ -14,11 +19,21 @@ export interface AdbForwardListener {
|
|||
remoteName: string;
|
||||
}
|
||||
|
||||
const AdbReverseStringResponse =
|
||||
/* #__PURE__ */
|
||||
new Struct()
|
||||
.string("length", { length: 4 })
|
||||
.string("content", { lengthField: "length", lengthFieldRadix: 16 });
|
||||
const AdbReverseStringResponse = new Struct(
|
||||
{
|
||||
length: string(4),
|
||||
content: string({
|
||||
field: "length",
|
||||
convert(value: string) {
|
||||
return Number.parseInt(value);
|
||||
},
|
||||
back(value) {
|
||||
return value.toString(16).padStart(4, "0");
|
||||
},
|
||||
}),
|
||||
},
|
||||
{ littleEndian: true },
|
||||
);
|
||||
|
||||
export class AdbReverseError extends Error {
|
||||
constructor(message: string) {
|
||||
|
@ -35,9 +50,9 @@ export class AdbReverseNotSupportedError extends AdbReverseError {
|
|||
}
|
||||
}
|
||||
|
||||
const AdbReverseErrorResponse =
|
||||
/* #__PURE__ */
|
||||
new Struct().concat(AdbReverseStringResponse).postDeserialize((value) => {
|
||||
const AdbReverseErrorResponse = new Struct(AdbReverseStringResponse.fields, {
|
||||
littleEndian: true,
|
||||
postDeserialize: (value) => {
|
||||
// https://issuetracker.google.com/issues/37066218
|
||||
// ADB on Android <9 can't create reverse tunnels when connected wirelessly (ADB over Wi-Fi),
|
||||
// and returns this confusing "more than one device/emulator" error.
|
||||
|
@ -46,7 +61,8 @@ const AdbReverseErrorResponse =
|
|||
} else {
|
||||
throw new AdbReverseError(value.content);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Like `hexToNumber`, it's much faster than first converting `buffer` to a string
|
||||
function decimalToNumber(buffer: Uint8Array) {
|
||||
|
|
|
@ -51,6 +51,18 @@ async function assertResolves<T>(promise: Promise<T>, expected: T) {
|
|||
return assert.deepStrictEqual(await promise, expected);
|
||||
}
|
||||
|
||||
describe("AdbShellProtocolPacket", () => {
|
||||
it("should serialize", () => {
|
||||
assert.deepStrictEqual(
|
||||
AdbShellProtocolPacket.serialize({
|
||||
id: AdbShellProtocolId.Stdout,
|
||||
data: new Uint8Array([1, 2, 3, 4]),
|
||||
}),
|
||||
new Uint8Array([1, 4, 0, 0, 0, 1, 2, 3, 4]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("AdbSubprocessShellProtocol", () => {
|
||||
describe("`stdout` and `stderr`", () => {
|
||||
it("should parse data from `socket", () => {
|
||||
|
|
|
@ -10,8 +10,8 @@ import {
|
|||
StructDeserializeStream,
|
||||
WritableStream,
|
||||
} from "@yume-chan/stream-extra";
|
||||
import type { StructValueType } from "@yume-chan/struct";
|
||||
import Struct, { placeholder } from "@yume-chan/struct";
|
||||
import type { StructValue } from "@yume-chan/struct";
|
||||
import { Struct, buffer, u32, u8 } from "@yume-chan/struct";
|
||||
|
||||
import type { Adb, AdbSocket } from "../../../adb.js";
|
||||
import { AdbFeature } from "../../../features.js";
|
||||
|
@ -32,14 +32,15 @@ export type AdbShellProtocolId =
|
|||
(typeof AdbShellProtocolId)[keyof typeof AdbShellProtocolId];
|
||||
|
||||
// This packet format is used in both directions.
|
||||
export const AdbShellProtocolPacket =
|
||||
/* #__PURE__ */
|
||||
new Struct({ littleEndian: true })
|
||||
.uint8("id", placeholder<AdbShellProtocolId>())
|
||||
.uint32("length")
|
||||
.uint8Array("data", { lengthField: "length" });
|
||||
export const AdbShellProtocolPacket = new Struct(
|
||||
{
|
||||
id: u8.as<AdbShellProtocolId>(),
|
||||
data: buffer(u32),
|
||||
},
|
||||
{ littleEndian: true },
|
||||
);
|
||||
|
||||
type AdbShellProtocolPacket = StructValueType<typeof AdbShellProtocolPacket>;
|
||||
type AdbShellProtocolPacket = StructValue<typeof AdbShellProtocolPacket>;
|
||||
|
||||
/**
|
||||
* Shell v2 a.k.a Shell Protocol
|
||||
|
|
|
@ -3,7 +3,7 @@ import type {
|
|||
ReadableStream,
|
||||
WritableStream,
|
||||
} from "@yume-chan/stream-extra";
|
||||
import type { ValueOrPromise } from "@yume-chan/struct";
|
||||
import type { MaybePromiseLike } from "@yume-chan/struct";
|
||||
|
||||
import type { Adb, AdbSocket } from "../../../adb.js";
|
||||
|
||||
|
@ -40,23 +40,23 @@ export interface AdbSubprocessProtocol {
|
|||
* Some `AdbSubprocessProtocol`s may not support resizing
|
||||
* and will ignore calls to this method.
|
||||
*/
|
||||
resize(rows: number, cols: number): ValueOrPromise<void>;
|
||||
resize(rows: number, cols: number): MaybePromiseLike<void>;
|
||||
|
||||
/**
|
||||
* Kills the current process.
|
||||
*/
|
||||
kill(): ValueOrPromise<void>;
|
||||
kill(): MaybePromiseLike<void>;
|
||||
}
|
||||
|
||||
export interface AdbSubprocessProtocolConstructor {
|
||||
/** Returns `true` if the `adb` instance supports this shell */
|
||||
isSupported(adb: Adb): ValueOrPromise<boolean>;
|
||||
isSupported(adb: Adb): MaybePromiseLike<boolean>;
|
||||
|
||||
/** Spawns an executable in PTY (interactive) mode. */
|
||||
pty(adb: Adb, command: string): ValueOrPromise<AdbSubprocessProtocol>;
|
||||
pty(adb: Adb, command: string): MaybePromiseLike<AdbSubprocessProtocol>;
|
||||
|
||||
/** Spawns an executable and pipe the output. */
|
||||
raw(adb: Adb, command: string): ValueOrPromise<AdbSubprocessProtocol>;
|
||||
raw(adb: Adb, command: string): MaybePromiseLike<AdbSubprocessProtocol>;
|
||||
|
||||
/** Creates a new `AdbShell` by attaching to an exist `AdbSocket` */
|
||||
new (socket: AdbSocket): AdbSubprocessProtocol;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import Struct from "@yume-chan/struct";
|
||||
import type { StructValue } from "@yume-chan/struct";
|
||||
import { Struct, string, u32 } from "@yume-chan/struct";
|
||||
|
||||
import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js";
|
||||
import { AdbSyncResponseId, adbSyncReadResponses } from "./response.js";
|
||||
|
@ -14,25 +15,25 @@ export interface AdbSyncEntry extends AdbSyncStat {
|
|||
name: string;
|
||||
}
|
||||
|
||||
export const AdbSyncEntryResponse =
|
||||
/* #__PURE__ */
|
||||
new Struct({ littleEndian: true })
|
||||
.concat(AdbSyncLstatResponse)
|
||||
.uint32("nameLength")
|
||||
.string("name", { lengthField: "nameLength" });
|
||||
export const AdbSyncEntryResponse = new Struct(
|
||||
{
|
||||
...AdbSyncLstatResponse.fields,
|
||||
name: string(u32),
|
||||
},
|
||||
{ littleEndian: true, extra: AdbSyncLstatResponse.extra },
|
||||
);
|
||||
|
||||
export type AdbSyncEntryResponse =
|
||||
(typeof AdbSyncEntryResponse)["TDeserializeResult"];
|
||||
export type AdbSyncEntryResponse = StructValue<typeof AdbSyncEntryResponse>;
|
||||
|
||||
export const AdbSyncEntry2Response =
|
||||
/* #__PURE__ */
|
||||
new Struct({ littleEndian: true })
|
||||
.concat(AdbSyncStatResponse)
|
||||
.uint32("nameLength")
|
||||
.string("name", { lengthField: "nameLength" });
|
||||
export const AdbSyncEntry2Response = new Struct(
|
||||
{
|
||||
...AdbSyncStatResponse.fields,
|
||||
name: string(u32),
|
||||
},
|
||||
{ littleEndian: true, extra: AdbSyncStatResponse.extra },
|
||||
);
|
||||
|
||||
export type AdbSyncEntry2Response =
|
||||
(typeof AdbSyncEntry2Response)["TDeserializeResult"];
|
||||
export type AdbSyncEntry2Response = StructValue<typeof AdbSyncEntry2Response>;
|
||||
|
||||
export async function* adbSyncOpenDirV2(
|
||||
socket: AdbSyncSocket,
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
import type { ReadableStream } from "@yume-chan/stream-extra";
|
||||
import { PushReadableStream } from "@yume-chan/stream-extra";
|
||||
import Struct from "@yume-chan/struct";
|
||||
import type { StructValue } from "@yume-chan/struct";
|
||||
import { buffer, Struct, u32 } from "@yume-chan/struct";
|
||||
|
||||
import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js";
|
||||
import { AdbSyncResponseId, adbSyncReadResponses } from "./response.js";
|
||||
import { adbSyncReadResponses, AdbSyncResponseId } from "./response.js";
|
||||
import type { AdbSyncSocket } from "./socket.js";
|
||||
|
||||
export const AdbSyncDataResponse =
|
||||
/* #__PURE__ */
|
||||
new Struct({ littleEndian: true })
|
||||
.uint32("dataLength")
|
||||
.uint8Array("data", { lengthField: "dataLength" });
|
||||
export const AdbSyncDataResponse = new Struct(
|
||||
{ data: buffer(u32) },
|
||||
{ littleEndian: true },
|
||||
);
|
||||
|
||||
export type AdbSyncDataResponse =
|
||||
(typeof AdbSyncDataResponse)["TDeserializeResult"];
|
||||
export type AdbSyncDataResponse = StructValue<typeof AdbSyncDataResponse>;
|
||||
|
||||
export async function* adbSyncPullGenerator(
|
||||
socket: AdbSyncSocket,
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
DistributionStream,
|
||||
MaybeConsumable,
|
||||
} from "@yume-chan/stream-extra";
|
||||
import Struct, { placeholder } from "@yume-chan/struct";
|
||||
import { Struct, u32 } from "@yume-chan/struct";
|
||||
|
||||
import { NOOP } from "../../utils/index.js";
|
||||
|
||||
|
@ -25,9 +25,10 @@ export interface AdbSyncPushV1Options {
|
|||
packetSize?: number;
|
||||
}
|
||||
|
||||
export const AdbSyncOkResponse =
|
||||
/* #__PURE__ */
|
||||
new Struct({ littleEndian: true }).uint32("unused");
|
||||
export const AdbSyncOkResponse = new Struct(
|
||||
{ unused: u32 },
|
||||
{ littleEndian: true },
|
||||
);
|
||||
|
||||
async function pipeFileData(
|
||||
locked: AdbSyncSocketLocked,
|
||||
|
@ -113,12 +114,10 @@ export interface AdbSyncPushV2Options extends AdbSyncPushV1Options {
|
|||
dryRun?: boolean;
|
||||
}
|
||||
|
||||
export const AdbSyncSendV2Request =
|
||||
/* #__PURE__ */
|
||||
new Struct({ littleEndian: true })
|
||||
.uint32("id")
|
||||
.uint32("mode")
|
||||
.uint32("flags", placeholder<AdbSyncSendV2Flags>());
|
||||
export const AdbSyncSendV2Request = new Struct(
|
||||
{ id: u32, mode: u32, flags: u32.as<AdbSyncSendV2Flags>() },
|
||||
{ littleEndian: true },
|
||||
);
|
||||
|
||||
export async function adbSyncPushV2({
|
||||
socket,
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import Struct from "@yume-chan/struct";
|
||||
|
||||
import { encodeUtf8 } from "../../utils/index.js";
|
||||
import { encodeUtf8, Struct, u32 } from "@yume-chan/struct";
|
||||
|
||||
import { adbSyncEncodeId } from "./response.js";
|
||||
|
||||
|
@ -17,9 +15,10 @@ export const AdbSyncRequestId = {
|
|||
Receive: adbSyncEncodeId("RECV"),
|
||||
} as const;
|
||||
|
||||
export const AdbSyncNumberRequest =
|
||||
/* #__PURE__ */
|
||||
new Struct({ littleEndian: true }).uint32("id").uint32("arg");
|
||||
export const AdbSyncNumberRequest = new Struct(
|
||||
{ id: u32, arg: u32 },
|
||||
{ littleEndian: true },
|
||||
);
|
||||
|
||||
export interface AdbSyncWritable {
|
||||
write(buffer: Uint8Array): Promise<void>;
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
import { getUint32LittleEndian } from "@yume-chan/no-data-view";
|
||||
import type {
|
||||
AsyncExactReadable,
|
||||
StructLike,
|
||||
StructValueType,
|
||||
} from "@yume-chan/struct";
|
||||
import Struct, { decodeUtf8 } from "@yume-chan/struct";
|
||||
import type { AsyncExactReadable, StructLike } from "@yume-chan/struct";
|
||||
import { Struct, decodeUtf8, string, u32 } from "@yume-chan/struct";
|
||||
|
||||
function encodeAsciiUnchecked(value: string): Uint8Array {
|
||||
const result = new Uint8Array(value.length);
|
||||
|
@ -40,14 +36,15 @@ export const AdbSyncResponseId = {
|
|||
|
||||
export class AdbSyncError extends Error {}
|
||||
|
||||
export const AdbSyncFailResponse =
|
||||
/* #__PURE__ */
|
||||
new Struct({ littleEndian: true })
|
||||
.uint32("messageLength")
|
||||
.string("message", { lengthField: "messageLength" })
|
||||
.postDeserialize((object) => {
|
||||
throw new AdbSyncError(object.message);
|
||||
});
|
||||
export const AdbSyncFailResponse = new Struct(
|
||||
{ message: string(u32) },
|
||||
{
|
||||
littleEndian: true,
|
||||
postDeserialize(value) {
|
||||
throw new AdbSyncError(value.message);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export async function adbSyncReadResponse<T>(
|
||||
stream: AsyncExactReadable,
|
||||
|
@ -72,13 +69,11 @@ export async function adbSyncReadResponse<T>(
|
|||
}
|
||||
}
|
||||
|
||||
export async function* adbSyncReadResponses<
|
||||
T extends Struct<object, PropertyKey, object, unknown>,
|
||||
>(
|
||||
export async function* adbSyncReadResponses<T>(
|
||||
stream: AsyncExactReadable,
|
||||
id: number | string,
|
||||
type: T,
|
||||
): AsyncGenerator<StructValueType<T>, void, void> {
|
||||
type: StructLike<T>,
|
||||
): AsyncGenerator<T, void, void> {
|
||||
if (typeof id === "string") {
|
||||
id = adbSyncEncodeId(id);
|
||||
}
|
||||
|
@ -97,7 +92,7 @@ export async function* adbSyncReadResponses<
|
|||
await stream.readExactly(type.size);
|
||||
return;
|
||||
case id:
|
||||
yield (await type.deserialize(stream)) as StructValueType<T>;
|
||||
yield await type.deserialize(stream);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import Struct, { placeholder } from "@yume-chan/struct";
|
||||
import type { StructValue } from "@yume-chan/struct";
|
||||
import { Struct, u32, u64 } from "@yume-chan/struct";
|
||||
|
||||
import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js";
|
||||
import { AdbSyncResponseId, adbSyncReadResponse } from "./response.js";
|
||||
|
@ -26,28 +27,28 @@ export interface AdbSyncStat {
|
|||
ctime?: bigint;
|
||||
}
|
||||
|
||||
export const AdbSyncLstatResponse =
|
||||
/* #__PURE__ */
|
||||
new Struct({ littleEndian: true })
|
||||
.int32("mode")
|
||||
.int32("size")
|
||||
.int32("mtime")
|
||||
.extra({
|
||||
get type() {
|
||||
export const AdbSyncLstatResponse = new Struct(
|
||||
{ mode: u32, size: u32, mtime: u32 },
|
||||
{
|
||||
littleEndian: true,
|
||||
extra: {
|
||||
get type(): LinuxFileType {
|
||||
return (this.mode >> 12) as LinuxFileType;
|
||||
},
|
||||
get permission() {
|
||||
get permission(): number {
|
||||
return this.mode & 0b00001111_11111111;
|
||||
},
|
||||
})
|
||||
.postDeserialize((object) => {
|
||||
if (object.mode === 0 && object.size === 0 && object.mtime === 0) {
|
||||
},
|
||||
postDeserialize(value) {
|
||||
if (value.mode === 0 && value.size === 0 && value.mtime === 0) {
|
||||
throw new Error("lstat error");
|
||||
}
|
||||
});
|
||||
return value;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export type AdbSyncLstatResponse =
|
||||
(typeof AdbSyncLstatResponse)["TDeserializeResult"];
|
||||
export type AdbSyncLstatResponse = StructValue<typeof AdbSyncLstatResponse>;
|
||||
|
||||
export const AdbSyncStatErrorCode = {
|
||||
SUCCESS: 0,
|
||||
|
@ -85,36 +86,40 @@ const AdbSyncStatErrorName =
|
|||
]),
|
||||
);
|
||||
|
||||
export const AdbSyncStatResponse =
|
||||
/* #__PURE__ */
|
||||
new Struct({ littleEndian: true })
|
||||
.uint32("error", placeholder<AdbSyncStatErrorCode>())
|
||||
.uint64("dev")
|
||||
.uint64("ino")
|
||||
.uint32("mode")
|
||||
.uint32("nlink")
|
||||
.uint32("uid")
|
||||
.uint32("gid")
|
||||
.uint64("size")
|
||||
.uint64("atime")
|
||||
.uint64("mtime")
|
||||
.uint64("ctime")
|
||||
.extra({
|
||||
get type() {
|
||||
export const AdbSyncStatResponse = new Struct(
|
||||
{
|
||||
error: u32.as<AdbSyncStatErrorCode>(),
|
||||
dev: u64,
|
||||
ino: u64,
|
||||
mode: u32,
|
||||
nlink: u32,
|
||||
uid: u32,
|
||||
gid: u32,
|
||||
size: u64,
|
||||
atime: u64,
|
||||
mtime: u64,
|
||||
ctime: u64,
|
||||
},
|
||||
{
|
||||
littleEndian: true,
|
||||
extra: {
|
||||
get type(): LinuxFileType {
|
||||
return (this.mode >> 12) as LinuxFileType;
|
||||
},
|
||||
get permission() {
|
||||
get permission(): number {
|
||||
return this.mode & 0b00001111_11111111;
|
||||
},
|
||||
})
|
||||
.postDeserialize((object) => {
|
||||
if (object.error) {
|
||||
throw new Error(AdbSyncStatErrorName[object.error]);
|
||||
},
|
||||
postDeserialize(value) {
|
||||
if (value.error) {
|
||||
throw new Error(AdbSyncStatErrorName[value.error]);
|
||||
}
|
||||
});
|
||||
return value;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export type AdbSyncStatResponse =
|
||||
(typeof AdbSyncStatResponse)["TDeserializeResult"];
|
||||
export type AdbSyncStatResponse = StructValue<typeof AdbSyncStatResponse>;
|
||||
|
||||
export async function adbSyncLstat(
|
||||
socket: AdbSyncSocket,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as assert from "node:assert";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import { EMPTY_UINT8_ARRAY, encodeUtf8 } from "@yume-chan/struct";
|
||||
import { EmptyUint8Array, encodeUtf8 } from "@yume-chan/struct";
|
||||
|
||||
import { decodeBase64 } from "../utils/base64.js";
|
||||
|
||||
|
@ -86,7 +86,7 @@ describe("auth", () => {
|
|||
command: AdbCommand.Auth,
|
||||
arg0: AdbAuthType.Token,
|
||||
arg1: 0,
|
||||
payload: EMPTY_UINT8_ARRAY,
|
||||
payload: EmptyUint8Array,
|
||||
}),
|
||||
);
|
||||
|
||||
|
@ -118,7 +118,7 @@ describe("auth", () => {
|
|||
command: AdbCommand.Auth,
|
||||
arg0: AdbAuthType.Token,
|
||||
arg1: 0,
|
||||
payload: EMPTY_UINT8_ARRAY,
|
||||
payload: EmptyUint8Array,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { PromiseResolver } from "@yume-chan/async";
|
||||
import type { Disposable } from "@yume-chan/event";
|
||||
import type { ValueOrPromise } from "@yume-chan/struct";
|
||||
import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct";
|
||||
import type { MaybePromiseLike } from "@yume-chan/struct";
|
||||
import { EmptyUint8Array } from "@yume-chan/struct";
|
||||
|
||||
import {
|
||||
calculateBase64EncodedLength,
|
||||
|
@ -33,7 +33,7 @@ export interface AdbCredentialStore {
|
|||
/**
|
||||
* Generates and stores a RSA private key with modulus length `2048` and public exponent `65537`.
|
||||
*/
|
||||
generateKey(): ValueOrPromise<AdbPrivateKey>;
|
||||
generateKey(): MaybePromiseLike<AdbPrivateKey>;
|
||||
|
||||
/**
|
||||
* Synchronously or asynchronously iterates through all stored RSA private keys.
|
||||
|
@ -114,7 +114,7 @@ export const AdbPublicKeyAuthenticator: AdbAuthenticator = async function* (
|
|||
|
||||
const nameBuffer = privateKey.name?.length
|
||||
? encodeUtf8(privateKey.name)
|
||||
: EMPTY_UINT8_ARRAY;
|
||||
: EmptyUint8Array;
|
||||
const publicKeyBuffer = new Uint8Array(
|
||||
publicKeyBase64Length +
|
||||
(nameBuffer.length ? nameBuffer.length + 1 : 0) + // Space character + name
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { Consumable, ReadableWritablePair } from "@yume-chan/stream-extra";
|
||||
import type { ValueOrPromise } from "@yume-chan/struct";
|
||||
import type { MaybePromiseLike } from "@yume-chan/struct";
|
||||
|
||||
import type { AdbPacketData, AdbPacketInit } from "./packet.js";
|
||||
|
||||
|
@ -8,7 +8,7 @@ export interface AdbDaemonDevice {
|
|||
|
||||
readonly name: string | undefined;
|
||||
|
||||
connect(): ValueOrPromise<
|
||||
connect(): MaybePromiseLike<
|
||||
ReadableWritablePair<AdbPacketData, Consumable<AdbPacketInit>>
|
||||
>;
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
Consumable,
|
||||
WritableStream,
|
||||
} from "@yume-chan/stream-extra";
|
||||
import { EMPTY_UINT8_ARRAY, decodeUtf8, encodeUtf8 } from "@yume-chan/struct";
|
||||
import { EmptyUint8Array, decodeUtf8, encodeUtf8 } from "@yume-chan/struct";
|
||||
|
||||
import type { AdbIncomingSocketHandler, AdbSocket, Closeable } from "../adb.js";
|
||||
|
||||
|
@ -259,7 +259,7 @@ export class AdbPacketDispatcher implements Closeable {
|
|||
AdbCommand.Close,
|
||||
packet.arg1,
|
||||
packet.arg0,
|
||||
EMPTY_UINT8_ARRAY,
|
||||
EmptyUint8Array,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -271,7 +271,7 @@ export class AdbPacketDispatcher implements Closeable {
|
|||
payload = new Uint8Array(4);
|
||||
setUint32LittleEndian(payload, 0, ackBytes);
|
||||
} else {
|
||||
payload = EMPTY_UINT8_ARRAY;
|
||||
payload = EmptyUint8Array;
|
||||
}
|
||||
|
||||
return this.sendPacket(AdbCommand.Okay, localId, remoteId, payload);
|
||||
|
@ -312,7 +312,7 @@ export class AdbPacketDispatcher implements Closeable {
|
|||
AdbCommand.Close,
|
||||
0,
|
||||
remoteId,
|
||||
EMPTY_UINT8_ARRAY,
|
||||
EmptyUint8Array,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
@ -339,7 +339,7 @@ export class AdbPacketDispatcher implements Closeable {
|
|||
AdbCommand.Close,
|
||||
0,
|
||||
remoteId,
|
||||
EMPTY_UINT8_ARRAY,
|
||||
EmptyUint8Array,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Consumable, TransformStream } from "@yume-chan/stream-extra";
|
||||
import Struct from "@yume-chan/struct";
|
||||
import type { StructInit, StructValue } from "@yume-chan/struct";
|
||||
import { buffer, s32, Struct, u32 } from "@yume-chan/struct";
|
||||
|
||||
export const AdbCommand = {
|
||||
Auth: 0x48545541, // 'AUTH'
|
||||
|
@ -12,27 +13,28 @@ export const AdbCommand = {
|
|||
|
||||
export type AdbCommand = (typeof AdbCommand)[keyof typeof AdbCommand];
|
||||
|
||||
export const AdbPacketHeader =
|
||||
/* #__PURE__ */
|
||||
new Struct({ littleEndian: true })
|
||||
.uint32("command")
|
||||
.uint32("arg0")
|
||||
.uint32("arg1")
|
||||
.uint32("payloadLength")
|
||||
.uint32("checksum")
|
||||
.int32("magic");
|
||||
export const AdbPacketHeader = new Struct(
|
||||
{
|
||||
command: u32,
|
||||
arg0: u32,
|
||||
arg1: u32,
|
||||
payloadLength: u32,
|
||||
checksum: u32,
|
||||
magic: s32,
|
||||
},
|
||||
{ littleEndian: true },
|
||||
);
|
||||
|
||||
export type AdbPacketHeader = (typeof AdbPacketHeader)["TDeserializeResult"];
|
||||
export type AdbPacketHeader = StructValue<typeof AdbPacketHeader>;
|
||||
|
||||
type AdbPacketHeaderInit = (typeof AdbPacketHeader)["TInit"];
|
||||
type AdbPacketHeaderInit = StructInit<typeof AdbPacketHeader>;
|
||||
|
||||
export const AdbPacket =
|
||||
/* #__PURE__ */
|
||||
new Struct({ littleEndian: true })
|
||||
.concat(AdbPacketHeader)
|
||||
.uint8Array("payload", { lengthField: "payloadLength" });
|
||||
export const AdbPacket = new Struct(
|
||||
{ ...AdbPacketHeader.fields, payload: buffer("payloadLength") },
|
||||
{ littleEndian: true },
|
||||
);
|
||||
|
||||
export type AdbPacket = (typeof AdbPacket)["TDeserializeResult"];
|
||||
export type AdbPacket = StructValue<typeof AdbPacket>;
|
||||
|
||||
/**
|
||||
* `AdbPacketData` contains all the useful fields of `AdbPacket`.
|
||||
|
@ -45,11 +47,11 @@ export type AdbPacket = (typeof AdbPacket)["TDeserializeResult"];
|
|||
* so `AdbSocket#writable#write` only needs `AdbPacketData`.
|
||||
*/
|
||||
export type AdbPacketData = Omit<
|
||||
(typeof AdbPacket)["TInit"],
|
||||
StructInit<typeof AdbPacket>,
|
||||
"checksum" | "magic"
|
||||
>;
|
||||
|
||||
export type AdbPacketInit = (typeof AdbPacket)["TInit"];
|
||||
export type AdbPacketInit = StructInit<typeof AdbPacket>;
|
||||
|
||||
export function calculateChecksum(payload: Uint8Array): number {
|
||||
return payload.reduce((result, item) => result + item, 0);
|
||||
|
@ -67,9 +69,10 @@ export class AdbPacketSerializeStream extends TransformStream<
|
|||
const init = chunk as AdbPacketInit & AdbPacketHeaderInit;
|
||||
init.payloadLength = init.payload.length;
|
||||
|
||||
AdbPacketHeader.serialize(init, headerBuffer);
|
||||
await Consumable.ReadableStream.enqueue(
|
||||
controller,
|
||||
AdbPacketHeader.serialize(init, headerBuffer),
|
||||
headerBuffer,
|
||||
);
|
||||
|
||||
if (init.payloadLength) {
|
||||
|
|
|
@ -7,7 +7,7 @@ import type {
|
|||
WritableStreamDefaultController,
|
||||
} from "@yume-chan/stream-extra";
|
||||
import { MaybeConsumable, PushReadableStream } from "@yume-chan/stream-extra";
|
||||
import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct";
|
||||
import { EmptyUint8Array } from "@yume-chan/struct";
|
||||
|
||||
import type { AdbSocket } from "../adb.js";
|
||||
|
||||
|
@ -164,7 +164,7 @@ export class AdbDaemonSocketController
|
|||
AdbCommand.Close,
|
||||
this.localId,
|
||||
this.remoteId,
|
||||
EMPTY_UINT8_ARRAY,
|
||||
EmptyUint8Array,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
Consumable,
|
||||
WritableStream,
|
||||
} from "@yume-chan/stream-extra";
|
||||
import type { ValueOrPromise } from "@yume-chan/struct";
|
||||
import type { MaybePromiseLike } from "@yume-chan/struct";
|
||||
import { decodeUtf8, encodeUtf8 } from "@yume-chan/struct";
|
||||
|
||||
import type {
|
||||
|
@ -368,7 +368,7 @@ export class AdbDaemonTransport implements AdbTransport {
|
|||
this.#protocolVersion = version;
|
||||
}
|
||||
|
||||
connect(service: string): ValueOrPromise<AdbSocket> {
|
||||
connect(service: string): MaybePromiseLike<AdbSocket> {
|
||||
return this.#dispatcher.createSocket(service);
|
||||
}
|
||||
|
||||
|
@ -392,7 +392,7 @@ export class AdbDaemonTransport implements AdbTransport {
|
|||
this.#dispatcher.clearReverseTunnels();
|
||||
}
|
||||
|
||||
close(): ValueOrPromise<void> {
|
||||
close(): MaybePromiseLike<void> {
|
||||
return this.#dispatcher.close();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,22 +4,17 @@ import { PromiseResolver } from "@yume-chan/async";
|
|||
import { getUint64LittleEndian } from "@yume-chan/no-data-view";
|
||||
import type {
|
||||
AbortSignal,
|
||||
MaybeConsumable,
|
||||
ReadableWritablePair,
|
||||
WritableStreamDefaultWriter,
|
||||
MaybeConsumable,
|
||||
} from "@yume-chan/stream-extra";
|
||||
import {
|
||||
BufferedReadableStream,
|
||||
tryCancel,
|
||||
tryClose,
|
||||
} from "@yume-chan/stream-extra";
|
||||
import type { ValueOrPromise } from "@yume-chan/struct";
|
||||
import {
|
||||
EMPTY_UINT8_ARRAY,
|
||||
SyncPromise,
|
||||
decodeUtf8,
|
||||
encodeUtf8,
|
||||
} from "@yume-chan/struct";
|
||||
import type { MaybePromiseLike } from "@yume-chan/struct";
|
||||
import { bipedal, decodeUtf8, encodeUtf8 } from "@yume-chan/struct";
|
||||
|
||||
import type { AdbIncomingSocketHandler, AdbSocket, Closeable } from "../adb.js";
|
||||
import { AdbBanner } from "../banner.js";
|
||||
|
@ -42,44 +37,38 @@ class AdbServerStream {
|
|||
this.#writer = connection.writable.getWriter();
|
||||
}
|
||||
|
||||
readExactly(length: number): ValueOrPromise<Uint8Array> {
|
||||
readExactly(length: number): MaybePromiseLike<Uint8Array> {
|
||||
return this.#buffered.readExactly(length);
|
||||
}
|
||||
|
||||
readString() {
|
||||
return SyncPromise.try(() => this.readExactly(4))
|
||||
.then((buffer) => {
|
||||
const length = hexToNumber(buffer);
|
||||
if (length === 0) {
|
||||
return EMPTY_UINT8_ARRAY;
|
||||
} else {
|
||||
return this.readExactly(length);
|
||||
}
|
||||
})
|
||||
.then((buffer) => {
|
||||
// TODO: Investigate using stream mode `TextDecoder` for long strings.
|
||||
// Because concatenating strings uses rope data structure,
|
||||
// which only points to the original strings and doesn't copy the data,
|
||||
// it's more efficient than concatenating `Uint8Array`s.
|
||||
//
|
||||
// ```
|
||||
// const decoder = new TextDecoder();
|
||||
// let result = '';
|
||||
// for await (const chunk of stream.iterateExactly(length)) {
|
||||
// result += decoder.decode(chunk, { stream: true });
|
||||
// }
|
||||
// result += decoder.decode();
|
||||
// return result;
|
||||
// ```
|
||||
//
|
||||
// Although, it will be super complex to use `SyncPromise` with async iterator,
|
||||
// `stream.iterateExactly` need to return an
|
||||
// `Iterator<Uint8Array | Promise<Uint8Array>>` instead of a true async iterator.
|
||||
// Maybe `SyncPromise` should support async iterators directly.
|
||||
return decodeUtf8(buffer);
|
||||
})
|
||||
.valueOrPromise();
|
||||
}
|
||||
readString = bipedal(function* (this: AdbServerStream, then) {
|
||||
const data = yield* then(this.readExactly(4));
|
||||
const length = hexToNumber(data);
|
||||
if (length === 0) {
|
||||
return "";
|
||||
} else {
|
||||
// TODO: Investigate using stream mode `TextDecoder` for long strings.
|
||||
// Because concatenating strings uses rope data structure,
|
||||
// which only points to the original strings and doesn't copy the data,
|
||||
// it's more efficient than concatenating `Uint8Array`s.
|
||||
//
|
||||
// ```
|
||||
// const decoder = new TextDecoder();
|
||||
// let result = '';
|
||||
// for await (const chunk of stream.iterateExactly(length)) {
|
||||
// result += decoder.decode(chunk, { stream: true });
|
||||
// }
|
||||
// result += decoder.decode();
|
||||
// return result;
|
||||
// ```
|
||||
//
|
||||
// Although, it will be super complex to use `SyncPromise` with async iterator,
|
||||
// `stream.iterateExactly` need to return an
|
||||
// `Iterator<Uint8Array | Promise<Uint8Array>>` instead of a true async iterator.
|
||||
// Maybe `SyncPromise` should support async iterators directly.
|
||||
return decodeUtf8(yield* then(this.readExactly(length)));
|
||||
}
|
||||
});
|
||||
|
||||
async writeString(value: string): Promise<void> {
|
||||
// TODO: investigate using `encodeUtf8("0000" + value)` then modifying the length
|
||||
|
@ -572,16 +561,16 @@ export namespace AdbServerClient {
|
|||
export interface ServerConnector {
|
||||
connect(
|
||||
options?: ServerConnectionOptions,
|
||||
): ValueOrPromise<ServerConnection>;
|
||||
): MaybePromiseLike<ServerConnection>;
|
||||
|
||||
addReverseTunnel(
|
||||
handler: AdbIncomingSocketHandler,
|
||||
address?: string,
|
||||
): ValueOrPromise<string>;
|
||||
): MaybePromiseLike<string>;
|
||||
|
||||
removeReverseTunnel(address: string): ValueOrPromise<void>;
|
||||
removeReverseTunnel(address: string): MaybePromiseLike<void>;
|
||||
|
||||
clearReverseTunnels(): ValueOrPromise<void>;
|
||||
clearReverseTunnels(): MaybePromiseLike<void>;
|
||||
}
|
||||
|
||||
export interface Socket extends AdbSocket {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { PromiseResolver } from "@yume-chan/async";
|
||||
import { AbortController } from "@yume-chan/stream-extra";
|
||||
import type { ValueOrPromise } from "@yume-chan/struct";
|
||||
|
||||
import type {
|
||||
AdbIncomingSocketHandler,
|
||||
|
@ -96,7 +95,7 @@ export class AdbServerTransport implements AdbTransport {
|
|||
await this.#client.connector.clearReverseTunnels();
|
||||
}
|
||||
|
||||
close(): ValueOrPromise<void> {
|
||||
close(): void | Promise<void> {
|
||||
this.#closed.resolve();
|
||||
this.#waitAbortController.abort();
|
||||
}
|
||||
|
|
|
@ -10,8 +10,8 @@ import {
|
|||
WrapReadableStream,
|
||||
WritableStream,
|
||||
} from "@yume-chan/stream-extra";
|
||||
import type { AsyncExactReadable } from "@yume-chan/struct";
|
||||
import Struct, { decodeUtf8 } from "@yume-chan/struct";
|
||||
import type { AsyncExactReadable, StructValue } from "@yume-chan/struct";
|
||||
import { Struct, decodeUtf8, u16, u32 } from "@yume-chan/struct";
|
||||
|
||||
// `adb logcat` is an alias to `adb shell logcat`
|
||||
// so instead of adding to core library, it's implemented here
|
||||
|
@ -99,27 +99,31 @@ export interface LogcatOptions {
|
|||
const NANOSECONDS_PER_SECOND = /* #__PURE__ */ BigInt(1e9);
|
||||
|
||||
// https://cs.android.com/android/platform/superproject/+/master:system/logging/liblog/include/log/log_read.h;l=39;drc=82b5738732161dbaafb2e2f25cce19cd26b9157d
|
||||
export const LoggerEntry =
|
||||
/* #__PURE__ */
|
||||
new Struct({ littleEndian: true })
|
||||
.uint16("payloadSize")
|
||||
.uint16("headerSize")
|
||||
.int32("pid")
|
||||
.uint32("tid")
|
||||
.uint32("seconds")
|
||||
.uint32("nanoseconds")
|
||||
.uint32("logId")
|
||||
.uint32("uid")
|
||||
.extra({
|
||||
get timestamp() {
|
||||
export const LoggerEntry = new Struct(
|
||||
{
|
||||
payloadSize: u16,
|
||||
headerSize: u16,
|
||||
pid: u32,
|
||||
tid: u32,
|
||||
seconds: u32,
|
||||
nanoseconds: u32,
|
||||
logId: u32,
|
||||
uid: u32,
|
||||
},
|
||||
{
|
||||
littleEndian: true,
|
||||
extra: {
|
||||
get timestamp(): bigint {
|
||||
return (
|
||||
BigInt(this.seconds) * NANOSECONDS_PER_SECOND +
|
||||
BigInt(this.nanoseconds)
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export type LoggerEntry = (typeof LoggerEntry)["TDeserializeResult"];
|
||||
export type LoggerEntry = StructValue<typeof LoggerEntry>;
|
||||
|
||||
// https://cs.android.com/android/platform/superproject/+/master:system/logging/liblog/logprint.cpp;drc=bbe77d66e7bee8bd1f0bc7e5492b5376b0207ef6;bpv=0
|
||||
export interface AndroidLogEntry extends LoggerEntry {
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
import { Struct } from "@yume-chan/struct";
|
||||
import type { StructInit } from "@yume-chan/struct";
|
||||
import { Struct, u8 } from "@yume-chan/struct";
|
||||
|
||||
export const EmptyControlMessage = /* #__PURE__ */ new Struct().uint8("type");
|
||||
export const EmptyControlMessage = new Struct(
|
||||
{ type: u8 },
|
||||
{ littleEndian: false },
|
||||
);
|
||||
|
||||
export type EmptyControlMessage = StructInit<typeof EmptyControlMessage>;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import Struct, { placeholder } from "@yume-chan/struct";
|
||||
import type { StructInit } from "@yume-chan/struct";
|
||||
import { Struct, u32, u8 } from "@yume-chan/struct";
|
||||
|
||||
export enum AndroidKeyEventAction {
|
||||
Down = 0,
|
||||
|
@ -205,14 +206,17 @@ export enum AndroidKeyCode {
|
|||
AndroidPaste,
|
||||
}
|
||||
|
||||
export const ScrcpyInjectKeyCodeControlMessage =
|
||||
/* #__PURE__ */
|
||||
new Struct()
|
||||
.uint8("type")
|
||||
.uint8("action", placeholder<AndroidKeyEventAction>())
|
||||
.uint32("keyCode", placeholder<AndroidKeyCode>())
|
||||
.uint32("repeat")
|
||||
.uint32("metaState", placeholder<AndroidKeyEventMeta>());
|
||||
export const ScrcpyInjectKeyCodeControlMessage = new Struct(
|
||||
{
|
||||
type: u8,
|
||||
action: u8.as<AndroidKeyEventAction>(),
|
||||
keyCode: u32.as<AndroidKeyCode>(),
|
||||
repeat: u32,
|
||||
metaState: u32.as<AndroidKeyEventMeta>(),
|
||||
},
|
||||
{ littleEndian: false },
|
||||
);
|
||||
|
||||
export type ScrcpyInjectKeyCodeControlMessage =
|
||||
(typeof ScrcpyInjectKeyCodeControlMessage)["TInit"];
|
||||
export type ScrcpyInjectKeyCodeControlMessage = StructInit<
|
||||
typeof ScrcpyInjectKeyCodeControlMessage
|
||||
>;
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import Struct from "@yume-chan/struct";
|
||||
import type { StructInit } from "@yume-chan/struct";
|
||||
import { string, Struct, u32, u8 } from "@yume-chan/struct";
|
||||
|
||||
export const ScrcpyInjectTextControlMessage =
|
||||
/* #__PURE__ */
|
||||
new Struct()
|
||||
.uint8("type")
|
||||
.uint32("length")
|
||||
.string("text", { lengthField: "length" });
|
||||
export const ScrcpyInjectTextControlMessage = new Struct(
|
||||
{ type: u8, text: string(u32) },
|
||||
{ littleEndian: false },
|
||||
);
|
||||
|
||||
export type ScrcpyInjectTextControlMessage =
|
||||
(typeof ScrcpyInjectTextControlMessage)["TInit"];
|
||||
export type ScrcpyInjectTextControlMessage = StructInit<
|
||||
typeof ScrcpyInjectTextControlMessage
|
||||
>;
|
||||
|
|
|
@ -2,5 +2,4 @@ import { EmptyControlMessage } from "./empty.js";
|
|||
|
||||
export const ScrcpyRotateDeviceControlMessage = EmptyControlMessage;
|
||||
|
||||
export type ScrcpyRotateDeviceControlMessage =
|
||||
(typeof ScrcpyRotateDeviceControlMessage)["TInit"];
|
||||
export type ScrcpyRotateDeviceControlMessage = EmptyControlMessage;
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
import Struct, { placeholder } from "@yume-chan/struct";
|
||||
import type { StructInit } from "@yume-chan/struct";
|
||||
import { Struct, u8 } from "@yume-chan/struct";
|
||||
|
||||
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/SurfaceControl.java;l=659;drc=20303e05bf73796124ab70a279cf849b61b97905
|
||||
export enum AndroidScreenPowerMode {
|
||||
Off = 0,
|
||||
Normal = 2,
|
||||
}
|
||||
export const AndroidScreenPowerMode = {
|
||||
Off: 0,
|
||||
Normal: 2,
|
||||
} as const;
|
||||
|
||||
export const ScrcpySetScreenPowerModeControlMessage =
|
||||
/* #__PURE__ */
|
||||
new Struct()
|
||||
.uint8("type")
|
||||
.uint8("mode", placeholder<AndroidScreenPowerMode>());
|
||||
export type AndroidScreenPowerMode =
|
||||
(typeof AndroidScreenPowerMode)[keyof typeof AndroidScreenPowerMode];
|
||||
|
||||
export type ScrcpySetScreenPowerModeControlMessage =
|
||||
(typeof ScrcpySetScreenPowerModeControlMessage)["TInit"];
|
||||
export const ScrcpySetScreenPowerModeControlMessage = new Struct(
|
||||
{ type: u8, mode: u8.as<AndroidScreenPowerMode>() },
|
||||
{ littleEndian: false },
|
||||
);
|
||||
|
||||
export type ScrcpySetScreenPowerModeControlMessage = StructInit<
|
||||
typeof ScrcpySetScreenPowerModeControlMessage
|
||||
>;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { getUint16, setUint16 } from "@yume-chan/no-data-view";
|
||||
import type { NumberFieldVariant } from "@yume-chan/struct";
|
||||
import { NumberFieldDefinition } from "@yume-chan/struct";
|
||||
import type { Field } from "@yume-chan/struct";
|
||||
import { bipedal } from "@yume-chan/struct";
|
||||
|
||||
export function clamp(value: number, min: number, max: number): number {
|
||||
if (value < min) {
|
||||
|
@ -14,22 +14,18 @@ export function clamp(value: number, min: number, max: number): number {
|
|||
return value;
|
||||
}
|
||||
|
||||
export const ScrcpyUnsignedFloatNumberVariant: NumberFieldVariant = {
|
||||
export const ScrcpyUnsignedFloat: Field<number, never, never> = {
|
||||
size: 2,
|
||||
signed: false,
|
||||
deserialize(array, littleEndian) {
|
||||
const value = getUint16(array, 0, littleEndian);
|
||||
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/server/src/main/java/com/genymobile/scrcpy/Binary.java#L22
|
||||
return value === 0xffff ? 1 : value / 0x10000;
|
||||
},
|
||||
serialize(array, offset, value, littleEndian) {
|
||||
serialize(value, { buffer, index, littleEndian }) {
|
||||
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/app/src/util/binary.h#L51
|
||||
value = clamp(value, -1, 1);
|
||||
value = value === 1 ? 0xffff : value * 0x10000;
|
||||
setUint16(array, offset, value, littleEndian);
|
||||
setUint16(buffer, index, value, littleEndian);
|
||||
},
|
||||
deserialize: bipedal(function* (then, { reader, littleEndian }) {
|
||||
const data = yield* then(reader.readExactly(2));
|
||||
const value = getUint16(data, 0, littleEndian);
|
||||
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/server/src/main/java/com/genymobile/scrcpy/Binary.java#L22
|
||||
return value === 0xffff ? 1 : value / 0x10000;
|
||||
}),
|
||||
};
|
||||
|
||||
export const ScrcpyUnsignedFloatFieldDefinition = new NumberFieldDefinition(
|
||||
ScrcpyUnsignedFloatNumberVariant,
|
||||
);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import Struct, { placeholder } from "@yume-chan/struct";
|
||||
import type { StructInit } from "@yume-chan/struct";
|
||||
import { Struct, buffer, string, u16, u32, u64, u8 } from "@yume-chan/struct";
|
||||
|
||||
import type { AndroidMotionEventAction } from "../../control/index.js";
|
||||
import {
|
||||
|
@ -6,7 +7,7 @@ import {
|
|||
ScrcpyControlMessageType,
|
||||
} from "../../control/index.js";
|
||||
|
||||
import { ScrcpyUnsignedFloatFieldDefinition } from "./float.js";
|
||||
import { ScrcpyUnsignedFloat } from "./float.js";
|
||||
|
||||
export const SCRCPY_CONTROL_MESSAGE_TYPES_1_16: readonly ScrcpyControlMessageType[] =
|
||||
[
|
||||
|
@ -23,43 +24,44 @@ export const SCRCPY_CONTROL_MESSAGE_TYPES_1_16: readonly ScrcpyControlMessageTyp
|
|||
/* 10 */ ScrcpyControlMessageType.RotateDevice,
|
||||
];
|
||||
|
||||
export const ScrcpyMediaStreamRawPacket =
|
||||
/* #__PURE__ */
|
||||
new Struct()
|
||||
.uint64("pts")
|
||||
.uint32("size")
|
||||
.uint8Array("data", { lengthField: "size" });
|
||||
export const ScrcpyMediaStreamRawPacket = new Struct(
|
||||
{ pts: u64, data: buffer(u32) },
|
||||
{ littleEndian: false },
|
||||
);
|
||||
|
||||
export const SCRCPY_MEDIA_PACKET_FLAG_CONFIG = 1n << 63n;
|
||||
|
||||
export const ScrcpyInjectTouchControlMessage1_16 =
|
||||
/* #__PURE__ */
|
||||
new Struct()
|
||||
.uint8("type")
|
||||
.uint8("action", placeholder<AndroidMotionEventAction>())
|
||||
.uint64("pointerId")
|
||||
.uint32("pointerX")
|
||||
.uint32("pointerY")
|
||||
.uint16("screenWidth")
|
||||
.uint16("screenHeight")
|
||||
.field("pressure", ScrcpyUnsignedFloatFieldDefinition)
|
||||
.uint32("buttons");
|
||||
export const ScrcpyInjectTouchControlMessage1_16 = new Struct(
|
||||
{
|
||||
type: u8,
|
||||
action: u8.as<AndroidMotionEventAction>(),
|
||||
pointerId: u64,
|
||||
pointerX: u32,
|
||||
pointerY: u32,
|
||||
screenWidth: u16,
|
||||
screenHeight: u16,
|
||||
pressure: ScrcpyUnsignedFloat,
|
||||
buttons: u32,
|
||||
},
|
||||
{ littleEndian: false },
|
||||
);
|
||||
|
||||
export type ScrcpyInjectTouchControlMessage1_16 =
|
||||
(typeof ScrcpyInjectTouchControlMessage1_16)["TInit"];
|
||||
export type ScrcpyInjectTouchControlMessage1_16 = StructInit<
|
||||
typeof ScrcpyInjectTouchControlMessage1_16
|
||||
>;
|
||||
|
||||
export const ScrcpyBackOrScreenOnControlMessage1_16 = EmptyControlMessage;
|
||||
|
||||
export const ScrcpySetClipboardControlMessage1_15 =
|
||||
/* #__PURE__ */
|
||||
new Struct()
|
||||
.uint8("type")
|
||||
.uint32("length")
|
||||
.string("content", { lengthField: "length" });
|
||||
export const ScrcpySetClipboardControlMessage1_15 = new Struct(
|
||||
{ type: u8, content: string(u32) },
|
||||
{ littleEndian: false },
|
||||
);
|
||||
|
||||
export type ScrcpySetClipboardControlMessage1_15 =
|
||||
(typeof ScrcpySetClipboardControlMessage1_15)["TInit"];
|
||||
export type ScrcpySetClipboardControlMessage1_15 = StructInit<
|
||||
typeof ScrcpySetClipboardControlMessage1_15
|
||||
>;
|
||||
|
||||
export const ScrcpyClipboardDeviceMessage =
|
||||
/* #__PURE__ */
|
||||
new Struct().uint32("length").string("content", { lengthField: "length" });
|
||||
export const ScrcpyClipboardDeviceMessage = new Struct(
|
||||
{ content: string(u32) },
|
||||
{ littleEndian: false },
|
||||
);
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
StructDeserializeStream,
|
||||
TransformStream,
|
||||
} from "@yume-chan/stream-extra";
|
||||
import type { AsyncExactReadable, ValueOrPromise } from "@yume-chan/struct";
|
||||
import type { AsyncExactReadable, MaybePromiseLike } from "@yume-chan/struct";
|
||||
import { decodeUtf8 } from "@yume-chan/struct";
|
||||
|
||||
import type {
|
||||
|
@ -159,7 +159,7 @@ export class ScrcpyOptions1_16 extends ScrcpyOptions<ScrcpyOptionsInit1_16> {
|
|||
|
||||
override parseVideoStreamMetadata(
|
||||
stream: ReadableStream<Uint8Array>,
|
||||
): ValueOrPromise<ScrcpyVideoStream> {
|
||||
): MaybePromiseLike<ScrcpyVideoStream> {
|
||||
return (async () => {
|
||||
const buffered = new BufferedReadableStream(stream);
|
||||
const metadata: ScrcpyVideoStreamMetadata = {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Struct from "@yume-chan/struct";
|
||||
import { s32, Struct, u16, u32, u8 } from "@yume-chan/struct";
|
||||
|
||||
import type { ScrcpyInjectScrollControlMessage } from "../../control/index.js";
|
||||
import { ScrcpyControlMessageType } from "../../control/index.js";
|
||||
|
||||
export interface ScrcpyScrollController {
|
||||
serializeScrollMessage(
|
||||
|
@ -8,16 +9,18 @@ export interface ScrcpyScrollController {
|
|||
): Uint8Array | undefined;
|
||||
}
|
||||
|
||||
export const ScrcpyInjectScrollControlMessage1_16 =
|
||||
/* #__PURE__ */
|
||||
new Struct()
|
||||
.uint8("type")
|
||||
.uint32("pointerX")
|
||||
.uint32("pointerY")
|
||||
.uint16("screenWidth")
|
||||
.uint16("screenHeight")
|
||||
.int32("scrollX")
|
||||
.int32("scrollY");
|
||||
export const ScrcpyInjectScrollControlMessage1_16 = new Struct(
|
||||
{
|
||||
type: u8.as(ScrcpyControlMessageType.InjectScroll as const),
|
||||
pointerX: u32,
|
||||
pointerY: u32,
|
||||
screenWidth: u16,
|
||||
screenHeight: u16,
|
||||
scrollX: s32,
|
||||
scrollY: s32,
|
||||
},
|
||||
{ littleEndian: false },
|
||||
);
|
||||
|
||||
/**
|
||||
* Old version of Scrcpy server only supports integer values for scroll.
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import Struct, { placeholder } from "@yume-chan/struct";
|
||||
import type { StructInit } from "@yume-chan/struct";
|
||||
import { Struct, u8 } from "@yume-chan/struct";
|
||||
|
||||
import type {
|
||||
AndroidKeyEventAction,
|
||||
|
@ -42,14 +43,17 @@ export interface ScrcpyOptionsInit1_18
|
|||
powerOffOnClose?: boolean;
|
||||
}
|
||||
|
||||
export const ScrcpyBackOrScreenOnControlMessage1_18 =
|
||||
/* #__PURE__ */
|
||||
new Struct()
|
||||
.concat(ScrcpyBackOrScreenOnControlMessage1_16)
|
||||
.uint8("action", placeholder<AndroidKeyEventAction>());
|
||||
export const ScrcpyBackOrScreenOnControlMessage1_18 = new Struct(
|
||||
{
|
||||
...ScrcpyBackOrScreenOnControlMessage1_16.fields,
|
||||
action: u8.as<AndroidKeyEventAction>(),
|
||||
},
|
||||
{ littleEndian: false },
|
||||
);
|
||||
|
||||
export type ScrcpyBackOrScreenOnControlMessage1_18 =
|
||||
(typeof ScrcpyBackOrScreenOnControlMessage1_18)["TInit"];
|
||||
export type ScrcpyBackOrScreenOnControlMessage1_18 = StructInit<
|
||||
typeof ScrcpyBackOrScreenOnControlMessage1_18
|
||||
>;
|
||||
|
||||
export const SCRCPY_CONTROL_MESSAGE_TYPES_1_18 =
|
||||
SCRCPY_CONTROL_MESSAGE_TYPES_1_16.slice();
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
// cspell: ignore autosync
|
||||
|
||||
import { PromiseResolver } from "@yume-chan/async";
|
||||
import type { AsyncExactReadable } from "@yume-chan/struct";
|
||||
import Struct, { placeholder } from "@yume-chan/struct";
|
||||
import type { AsyncExactReadable, StructInit } from "@yume-chan/struct";
|
||||
import { Struct, string, u32, u64, u8 } from "@yume-chan/struct";
|
||||
|
||||
import type { ScrcpySetClipboardControlMessage } from "../control/index.js";
|
||||
|
||||
|
@ -10,9 +10,10 @@ import type { ScrcpyOptionsInit1_18 } from "./1_18.js";
|
|||
import { ScrcpyOptions1_18 } from "./1_18.js";
|
||||
import { ScrcpyOptions, toScrcpyOptionValue } from "./types.js";
|
||||
|
||||
export const ScrcpyAckClipboardDeviceMessage =
|
||||
/* #__PURE__ */
|
||||
new Struct().uint64("sequence");
|
||||
export const ScrcpyAckClipboardDeviceMessage = new Struct(
|
||||
{ sequence: u64 },
|
||||
{ littleEndian: false },
|
||||
);
|
||||
|
||||
export interface ScrcpyOptionsInit1_21 extends ScrcpyOptionsInit1_18 {
|
||||
clipboardAutosync?: boolean;
|
||||
|
@ -22,17 +23,19 @@ function toSnakeCase(input: string): string {
|
|||
return input.replace(/([A-Z])/g, "_$1").toLowerCase();
|
||||
}
|
||||
|
||||
export const ScrcpySetClipboardControlMessage1_21 =
|
||||
/* #__PURE__ */
|
||||
new Struct()
|
||||
.uint8("type")
|
||||
.uint64("sequence")
|
||||
.int8("paste", placeholder<boolean>())
|
||||
.uint32("length")
|
||||
.string("content", { lengthField: "length" });
|
||||
export const ScrcpySetClipboardControlMessage1_21 = new Struct(
|
||||
{
|
||||
type: u8,
|
||||
sequence: u64,
|
||||
paste: u8.as<boolean>(),
|
||||
content: string(u32),
|
||||
},
|
||||
{ littleEndian: false },
|
||||
);
|
||||
|
||||
export type ScrcpySetClipboardControlMessage1_21 =
|
||||
(typeof ScrcpySetClipboardControlMessage1_21)["TInit"];
|
||||
export type ScrcpySetClipboardControlMessage1_21 = StructInit<
|
||||
typeof ScrcpySetClipboardControlMessage1_21
|
||||
>;
|
||||
|
||||
export class ScrcpyOptions1_21 extends ScrcpyOptions<ScrcpyOptionsInit1_21> {
|
||||
static readonly DEFAULTS = {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { ReadableStream } from "@yume-chan/stream-extra";
|
||||
import type { ValueOrPromise } from "@yume-chan/struct";
|
||||
import type { MaybePromiseLike } from "@yume-chan/struct";
|
||||
|
||||
import type { ScrcpyScrollController } from "../1_16/index.js";
|
||||
import { ScrcpyOptions1_21 } from "../1_21.js";
|
||||
|
@ -28,7 +28,7 @@ export class ScrcpyOptions1_22 extends ScrcpyOptions<ScrcpyOptionsInit1_22> {
|
|||
|
||||
override parseVideoStreamMetadata(
|
||||
stream: ReadableStream<Uint8Array>,
|
||||
): ValueOrPromise<ScrcpyVideoStream> {
|
||||
): MaybePromiseLike<ScrcpyVideoStream> {
|
||||
if (!this.value.sendDeviceMeta) {
|
||||
return { stream, metadata: { codec: ScrcpyVideoCodecId.H264 } };
|
||||
} else {
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
import Struct from "@yume-chan/struct";
|
||||
import type { StructInit } from "@yume-chan/struct";
|
||||
import { s32, Struct } from "@yume-chan/struct";
|
||||
|
||||
import {
|
||||
ScrcpyInjectScrollControlMessage1_16,
|
||||
ScrcpyScrollController1_16,
|
||||
} from "../1_16/index.js";
|
||||
|
||||
export const ScrcpyInjectScrollControlMessage1_22 =
|
||||
/* #__PURE__ */
|
||||
new Struct().concat(ScrcpyInjectScrollControlMessage1_16).int32("buttons");
|
||||
export const ScrcpyInjectScrollControlMessage1_22 = new Struct(
|
||||
{ ...ScrcpyInjectScrollControlMessage1_16.fields, buttons: s32 },
|
||||
{ littleEndian: false },
|
||||
);
|
||||
|
||||
export type ScrcpyInjectScrollControlMessage1_22 =
|
||||
(typeof ScrcpyInjectScrollControlMessage1_22)["TInit"];
|
||||
export type ScrcpyInjectScrollControlMessage1_22 = StructInit<
|
||||
typeof ScrcpyInjectScrollControlMessage1_22
|
||||
>;
|
||||
|
||||
export class ScrcpyScrollController1_22 extends ScrcpyScrollController1_16 {
|
||||
override serializeScrollMessage(
|
||||
|
|
|
@ -3,24 +3,33 @@ import { describe, it } from "node:test";
|
|||
|
||||
import { ScrcpyControlMessageType } from "../../control/index.js";
|
||||
|
||||
import {
|
||||
ScrcpyScrollController1_25,
|
||||
ScrcpySignedFloatNumberVariant,
|
||||
} from "./scroll.js";
|
||||
import { ScrcpyScrollController1_25, ScrcpySignedFloat } from "./scroll.js";
|
||||
|
||||
describe("ScrcpyFloatToInt16NumberType", () => {
|
||||
describe("ScrcpySignedFloat", () => {
|
||||
it("should serialize", () => {
|
||||
const array = new Uint8Array(2);
|
||||
ScrcpySignedFloatNumberVariant.serialize(array, 0, -1, true);
|
||||
ScrcpySignedFloat.serialize(-1, {
|
||||
buffer: array,
|
||||
index: 0,
|
||||
littleEndian: true,
|
||||
});
|
||||
assert.strictEqual(
|
||||
new DataView(array.buffer).getInt16(0, true),
|
||||
-0x8000,
|
||||
);
|
||||
|
||||
ScrcpySignedFloatNumberVariant.serialize(array, 0, 0, true);
|
||||
ScrcpySignedFloat.serialize(0, {
|
||||
buffer: array,
|
||||
index: 0,
|
||||
littleEndian: true,
|
||||
});
|
||||
assert.strictEqual(new DataView(array.buffer).getInt16(0, true), 0);
|
||||
|
||||
ScrcpySignedFloatNumberVariant.serialize(array, 0, 1, true);
|
||||
ScrcpySignedFloat.serialize(1, {
|
||||
buffer: array,
|
||||
index: 0,
|
||||
littleEndian: true,
|
||||
});
|
||||
assert.strictEqual(
|
||||
new DataView(array.buffer).getInt16(0, true),
|
||||
0x7fff,
|
||||
|
@ -29,13 +38,21 @@ describe("ScrcpyFloatToInt16NumberType", () => {
|
|||
|
||||
it("should clamp input values", () => {
|
||||
const array = new Uint8Array(2);
|
||||
ScrcpySignedFloatNumberVariant.serialize(array, 0, -2, true);
|
||||
ScrcpySignedFloat.serialize(-2, {
|
||||
buffer: array,
|
||||
index: 0,
|
||||
littleEndian: true,
|
||||
});
|
||||
assert.strictEqual(
|
||||
new DataView(array.buffer).getInt16(0, true),
|
||||
-0x8000,
|
||||
);
|
||||
|
||||
ScrcpySignedFloatNumberVariant.serialize(array, 0, 2, true);
|
||||
ScrcpySignedFloat.serialize(2, {
|
||||
buffer: array,
|
||||
index: 0,
|
||||
littleEndian: true,
|
||||
});
|
||||
assert.strictEqual(
|
||||
new DataView(array.buffer).getInt16(0, true),
|
||||
0x7fff,
|
||||
|
@ -48,19 +65,31 @@ describe("ScrcpyFloatToInt16NumberType", () => {
|
|||
|
||||
dataView.setInt16(0, -0x8000, true);
|
||||
assert.strictEqual(
|
||||
ScrcpySignedFloatNumberVariant.deserialize(view, true),
|
||||
ScrcpySignedFloat.deserialize({
|
||||
runtimeStruct: {} as never,
|
||||
reader: { position: 0, readExactly: () => view },
|
||||
littleEndian: true,
|
||||
}),
|
||||
-1,
|
||||
);
|
||||
|
||||
dataView.setInt16(0, 0, true);
|
||||
assert.strictEqual(
|
||||
ScrcpySignedFloatNumberVariant.deserialize(view, true),
|
||||
ScrcpySignedFloat.deserialize({
|
||||
runtimeStruct: {} as never,
|
||||
reader: { position: 0, readExactly: () => view },
|
||||
littleEndian: true,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
|
||||
dataView.setInt16(0, 0x7fff, true);
|
||||
assert.strictEqual(
|
||||
ScrcpySignedFloatNumberVariant.deserialize(view, true),
|
||||
ScrcpySignedFloat.deserialize({
|
||||
runtimeStruct: {} as never,
|
||||
reader: { position: 0, readExactly: () => view },
|
||||
littleEndian: true,
|
||||
}),
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,46 +1,45 @@
|
|||
import { getInt16, setInt16 } from "@yume-chan/no-data-view";
|
||||
import type { NumberFieldVariant } from "@yume-chan/struct";
|
||||
import Struct, { NumberFieldDefinition } from "@yume-chan/struct";
|
||||
import type { Field, StructInit } from "@yume-chan/struct";
|
||||
import { bipedal, Struct, u16, u32, u8 } from "@yume-chan/struct";
|
||||
|
||||
import type { ScrcpyInjectScrollControlMessage } from "../../control/index.js";
|
||||
import { ScrcpyControlMessageType } from "../../control/index.js";
|
||||
import type { ScrcpyScrollController } from "../1_16/index.js";
|
||||
import { clamp } from "../1_16/index.js";
|
||||
|
||||
export const ScrcpySignedFloatNumberVariant: NumberFieldVariant = {
|
||||
export const ScrcpySignedFloat: Field<number, never, never> = {
|
||||
size: 2,
|
||||
signed: true,
|
||||
deserialize(array, littleEndian) {
|
||||
const value = getInt16(array, 0, littleEndian);
|
||||
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/server/src/main/java/com/genymobile/scrcpy/Binary.java#L34
|
||||
return value === 0x7fff ? 1 : value / 0x8000;
|
||||
},
|
||||
serialize(array, offset, value, littleEndian) {
|
||||
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/app/src/util/binary.h#L65
|
||||
serialize(value, { buffer, index, littleEndian }) {
|
||||
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/app/src/util/binary.h#L51
|
||||
value = clamp(value, -1, 1);
|
||||
value = value === 1 ? 0x7fff : value * 0x8000;
|
||||
setInt16(array, offset, value, littleEndian);
|
||||
setInt16(buffer, index, value, littleEndian);
|
||||
},
|
||||
deserialize: bipedal(function* (then, { reader, littleEndian }) {
|
||||
const data = yield* then(reader.readExactly(2));
|
||||
const value = getInt16(data, 0, littleEndian);
|
||||
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/server/src/main/java/com/genymobile/scrcpy/Binary.java#L34
|
||||
return value === 0x7fff ? 1 : value / 0x8000;
|
||||
}),
|
||||
};
|
||||
|
||||
const ScrcpySignedFloatFieldDefinition = new NumberFieldDefinition(
|
||||
ScrcpySignedFloatNumberVariant,
|
||||
export const ScrcpyInjectScrollControlMessage1_25 = new Struct(
|
||||
{
|
||||
type: u8.as(ScrcpyControlMessageType.InjectScroll as const),
|
||||
pointerX: u32,
|
||||
pointerY: u32,
|
||||
screenWidth: u16,
|
||||
screenHeight: u16,
|
||||
scrollX: ScrcpySignedFloat,
|
||||
scrollY: ScrcpySignedFloat,
|
||||
buttons: u32,
|
||||
},
|
||||
{ littleEndian: false },
|
||||
);
|
||||
|
||||
export const ScrcpyInjectScrollControlMessage1_25 =
|
||||
/* #__PURE__ */
|
||||
new Struct()
|
||||
.uint8("type", ScrcpyControlMessageType.InjectScroll as const)
|
||||
.uint32("pointerX")
|
||||
.uint32("pointerY")
|
||||
.uint16("screenWidth")
|
||||
.uint16("screenHeight")
|
||||
.field("scrollX", ScrcpySignedFloatFieldDefinition)
|
||||
.field("scrollY", ScrcpySignedFloatFieldDefinition)
|
||||
.int32("buttons");
|
||||
|
||||
export type ScrcpyInjectScrollControlMessage1_25 =
|
||||
(typeof ScrcpyInjectScrollControlMessage1_25)["TInit"];
|
||||
export type ScrcpyInjectScrollControlMessage1_25 = StructInit<
|
||||
typeof ScrcpyInjectScrollControlMessage1_25
|
||||
>;
|
||||
|
||||
export class ScrcpyScrollController1_25 implements ScrcpyScrollController {
|
||||
serializeScrollMessage(
|
||||
|
|
|
@ -4,8 +4,8 @@ import {
|
|||
BufferedReadableStream,
|
||||
PushReadableStream,
|
||||
} from "@yume-chan/stream-extra";
|
||||
import type { ValueOrPromise } from "@yume-chan/struct";
|
||||
import Struct, { placeholder } from "@yume-chan/struct";
|
||||
import type { MaybePromiseLike, StructInit } from "@yume-chan/struct";
|
||||
import { Struct, u16, u32, u64, u8 } from "@yume-chan/struct";
|
||||
|
||||
import type {
|
||||
AndroidMotionEventAction,
|
||||
|
@ -15,7 +15,7 @@ import type {
|
|||
import {
|
||||
CodecOptions,
|
||||
ScrcpyOptions1_16,
|
||||
ScrcpyUnsignedFloatFieldDefinition,
|
||||
ScrcpyUnsignedFloat,
|
||||
} from "./1_16/index.js";
|
||||
import { ScrcpyOptions1_21 } from "./1_21.js";
|
||||
import type { ScrcpyOptionsInit1_24 } from "./1_24.js";
|
||||
|
@ -30,22 +30,25 @@ import type {
|
|||
} from "./types.js";
|
||||
import { ScrcpyOptions } from "./types.js";
|
||||
|
||||
export const ScrcpyInjectTouchControlMessage2_0 =
|
||||
/* #__PURE__ */
|
||||
new Struct()
|
||||
.uint8("type")
|
||||
.uint8("action", placeholder<AndroidMotionEventAction>())
|
||||
.uint64("pointerId")
|
||||
.uint32("pointerX")
|
||||
.uint32("pointerY")
|
||||
.uint16("screenWidth")
|
||||
.uint16("screenHeight")
|
||||
.field("pressure", ScrcpyUnsignedFloatFieldDefinition)
|
||||
.uint32("actionButton")
|
||||
.uint32("buttons");
|
||||
export const ScrcpyInjectTouchControlMessage2_0 = new Struct(
|
||||
{
|
||||
type: u8,
|
||||
action: u8.as<AndroidMotionEventAction>(),
|
||||
pointerId: u64,
|
||||
pointerX: u32,
|
||||
pointerY: u32,
|
||||
screenWidth: u16,
|
||||
screenHeight: u16,
|
||||
pressure: ScrcpyUnsignedFloat,
|
||||
actionButton: u32,
|
||||
buttons: u32,
|
||||
},
|
||||
{ littleEndian: false },
|
||||
);
|
||||
|
||||
export type ScrcpyInjectTouchControlMessage2_0 =
|
||||
(typeof ScrcpyInjectTouchControlMessage2_0)["TInit"];
|
||||
export type ScrcpyInjectTouchControlMessage2_0 = StructInit<
|
||||
typeof ScrcpyInjectTouchControlMessage2_0
|
||||
>;
|
||||
|
||||
export class ScrcpyInstanceId implements ScrcpyOptionValue {
|
||||
static readonly NONE = new ScrcpyInstanceId(-1);
|
||||
|
@ -244,7 +247,7 @@ export class ScrcpyOptions2_0 extends ScrcpyOptions<ScrcpyOptionsInit2_0> {
|
|||
|
||||
override parseVideoStreamMetadata(
|
||||
stream: ReadableStream<Uint8Array>,
|
||||
): ValueOrPromise<ScrcpyVideoStream> {
|
||||
): MaybePromiseLike<ScrcpyVideoStream> {
|
||||
const { sendDeviceMeta, sendCodecMeta } = this.value;
|
||||
if (!sendDeviceMeta && !sendCodecMeta) {
|
||||
let codec: ScrcpyVideoCodecId;
|
||||
|
@ -302,7 +305,7 @@ export class ScrcpyOptions2_0 extends ScrcpyOptions<ScrcpyOptionsInit2_0> {
|
|||
|
||||
override parseAudioStreamMetadata(
|
||||
stream: ReadableStream<Uint8Array>,
|
||||
): ValueOrPromise<ScrcpyAudioStreamMetadata> {
|
||||
): MaybePromiseLike<ScrcpyAudioStreamMetadata> {
|
||||
return ScrcpyOptions2_0.parseAudioMetadata(
|
||||
stream,
|
||||
this.value.sendCodecMeta,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { ReadableStream } from "@yume-chan/stream-extra";
|
||||
import type { ValueOrPromise } from "@yume-chan/struct";
|
||||
import type { MaybePromiseLike } from "@yume-chan/struct";
|
||||
|
||||
import { ScrcpyOptions1_21 } from "./1_21.js";
|
||||
import { ScrcpyOptions2_0 } from "./2_0.js";
|
||||
|
@ -33,7 +33,7 @@ export class ScrcpyOptions2_3 extends ScrcpyOptions<ScrcpyOptionsInit2_3> {
|
|||
|
||||
override parseAudioStreamMetadata(
|
||||
stream: ReadableStream<Uint8Array>,
|
||||
): ValueOrPromise<ScrcpyAudioStreamMetadata> {
|
||||
): MaybePromiseLike<ScrcpyAudioStreamMetadata> {
|
||||
return ScrcpyOptions2_0.parseAudioMetadata(
|
||||
stream,
|
||||
this.value.sendCodecMeta,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
|
||||
import type { AsyncExactReadable, ValueOrPromise } from "@yume-chan/struct";
|
||||
import type { AsyncExactReadable, MaybePromiseLike } from "@yume-chan/struct";
|
||||
|
||||
import type {
|
||||
ScrcpyBackOrScreenOnControlMessage,
|
||||
|
@ -170,13 +170,13 @@ export abstract class ScrcpyOptions<T extends object> {
|
|||
*/
|
||||
parseVideoStreamMetadata(
|
||||
stream: ReadableStream<Uint8Array>,
|
||||
): ValueOrPromise<ScrcpyVideoStream> {
|
||||
): MaybePromiseLike<ScrcpyVideoStream> {
|
||||
return this.#base.parseVideoStreamMetadata(stream);
|
||||
}
|
||||
|
||||
parseAudioStreamMetadata(
|
||||
stream: ReadableStream<Uint8Array>,
|
||||
): ValueOrPromise<ScrcpyAudioStreamMetadata> {
|
||||
): MaybePromiseLike<ScrcpyAudioStreamMetadata> {
|
||||
return this.#base.parseAudioStreamMetadata(stream);
|
||||
}
|
||||
|
||||
|
@ -184,7 +184,7 @@ export abstract class ScrcpyOptions<T extends object> {
|
|||
return this.#base.parseDeviceMessage(id, stream);
|
||||
}
|
||||
|
||||
endDeviceMessageStream(e?: unknown): ValueOrPromise<void> {
|
||||
endDeviceMessageStream(e?: unknown): MaybePromiseLike<void> {
|
||||
return this.#base.endDeviceMessageStream(e);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { ValueOrPromise } from "@yume-chan/struct";
|
||||
import type { MaybePromiseLike } from "@yume-chan/struct";
|
||||
import { StructEmptyError } from "@yume-chan/struct";
|
||||
|
||||
import { BufferedReadableStream } from "./buffered.js";
|
||||
|
@ -22,7 +22,7 @@ export class BufferedTransformStream<T>
|
|||
}
|
||||
|
||||
constructor(
|
||||
transform: (stream: BufferedReadableStream) => ValueOrPromise<T>,
|
||||
transform: (stream: BufferedReadableStream) => MaybePromiseLike<T>,
|
||||
) {
|
||||
// Convert incoming chunks to a `BufferedReadableStream`
|
||||
let sourceStreamController!: PushReadableStreamController<Uint8Array>;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { PromiseResolver } from "@yume-chan/async";
|
||||
import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct";
|
||||
import { EmptyUint8Array } from "@yume-chan/struct";
|
||||
|
||||
import type { ReadableStreamDefaultController } from "./stream.js";
|
||||
import { ReadableStream, WritableStream } from "./stream.js";
|
||||
|
@ -101,7 +101,7 @@ export class ConcatBufferStream {
|
|||
let offset = 0;
|
||||
switch (this.#segments.length) {
|
||||
case 0:
|
||||
result = EMPTY_UINT8_ARRAY;
|
||||
result = EmptyUint8Array;
|
||||
break;
|
||||
case 1:
|
||||
result = this.#segments[0]!;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { PromiseResolver } from "@yume-chan/async";
|
||||
import type { ValueOrPromise } from "@yume-chan/struct";
|
||||
import type { MaybePromiseLike } from "@yume-chan/struct";
|
||||
|
||||
import type {
|
||||
QueuingStrategy,
|
||||
|
@ -28,7 +28,7 @@ export interface DuplexStreamFactoryOptions {
|
|||
* `DuplexStreamFactory#dispose` yourself, you can return `false`
|
||||
* (or a `Promise` that resolves to `false`) to disable the automatic call.
|
||||
*/
|
||||
close?: (() => ValueOrPromise<boolean | void>) | undefined;
|
||||
close?: (() => MaybePromiseLike<boolean | void>) | undefined;
|
||||
|
||||
/**
|
||||
* Callback when any `ReadableStream` is closed (the other peer doesn't produce any more data),
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
import type Struct from "@yume-chan/struct";
|
||||
import type { StructValueType } from "@yume-chan/struct";
|
||||
import type { StructLike } from "@yume-chan/struct";
|
||||
|
||||
import { BufferedTransformStream } from "./buffered-transform.js";
|
||||
|
||||
export class StructDeserializeStream<
|
||||
T extends Struct<object, PropertyKey, object, unknown>,
|
||||
> extends BufferedTransformStream<StructValueType<T>> {
|
||||
constructor(struct: T) {
|
||||
export class StructDeserializeStream<T> extends BufferedTransformStream<T> {
|
||||
constructor(struct: StructLike<T>) {
|
||||
super((stream) => {
|
||||
return struct.deserialize(stream) as never;
|
||||
});
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import type Struct from "@yume-chan/struct";
|
||||
import type { StructInit, StructLike } from "@yume-chan/struct";
|
||||
|
||||
import { TransformStream } from "./stream.js";
|
||||
|
||||
export class StructSerializeStream<
|
||||
T extends Struct<object, PropertyKey, object, unknown>,
|
||||
> extends TransformStream<T["TInit"], Uint8Array> {
|
||||
T extends StructLike<unknown>,
|
||||
> extends TransformStream<StructInit<T>, Uint8Array> {
|
||||
constructor(struct: T) {
|
||||
super({
|
||||
transform(chunk, controller) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { ValueOrPromise } from "@yume-chan/struct";
|
||||
import type { MaybePromiseLike } from "@yume-chan/struct";
|
||||
|
||||
import type {
|
||||
QueuingStrategy,
|
||||
|
@ -9,12 +9,12 @@ import { ReadableStream } from "./stream.js";
|
|||
|
||||
export type WrapReadableStreamStart<T> = (
|
||||
controller: ReadableStreamDefaultController<T>,
|
||||
) => ValueOrPromise<ReadableStream<T>>;
|
||||
) => MaybePromiseLike<ReadableStream<T>>;
|
||||
|
||||
export interface ReadableStreamWrapper<T> {
|
||||
start: WrapReadableStreamStart<T>;
|
||||
cancel?(reason?: unknown): ValueOrPromise<void>;
|
||||
close?(): ValueOrPromise<void>;
|
||||
cancel?(reason?: unknown): MaybePromiseLike<void>;
|
||||
close?(): MaybePromiseLike<void>;
|
||||
}
|
||||
|
||||
function getWrappedReadableStream<T>(
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import type { ValueOrPromise } from "@yume-chan/struct";
|
||||
import type { MaybePromiseLike } from "@yume-chan/struct";
|
||||
|
||||
import type { TransformStream, WritableStreamDefaultWriter } from "./stream.js";
|
||||
import { WritableStream } from "./stream.js";
|
||||
|
||||
export type WrapWritableStreamStart<T> = () => ValueOrPromise<
|
||||
export type WrapWritableStreamStart<T> = () => MaybePromiseLike<
|
||||
WritableStream<T>
|
||||
>;
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
<!--
|
||||
cspell: ignore Codecov
|
||||
cspell: ignore uint8arraystring
|
||||
-->
|
||||
|
||||

|
||||
|
@ -13,6 +12,8 @@ cspell: ignore uint8arraystring
|
|||
|
||||
A C-style structure serializer and deserializer. Written in TypeScript and highly takes advantage of its type system.
|
||||
|
||||
The new API is inspired by [TypeGPU](https://docs.swmansion.com/TypeGPU/) which improves DX and tree-shaking.
|
||||
|
||||
**WARNING:** The public API is UNSTABLE. Open a GitHub discussion if you have any questions.
|
||||
|
||||
## Installation
|
||||
|
@ -24,724 +25,97 @@ $ npm i @yume-chan/struct
|
|||
## Quick Start
|
||||
|
||||
```ts
|
||||
import Struct from "@yume-chan/struct";
|
||||
import { Struct, u8, u16, s32, buffer, string } from "@yume-chan/struct";
|
||||
|
||||
const MyStruct = new Struct({ littleEndian: true })
|
||||
.int8("foo")
|
||||
.int64("bar")
|
||||
.int32("bazLength")
|
||||
.string("baz", { lengthField: "bazLength" });
|
||||
const Message = new Struct(
|
||||
{
|
||||
a: u8,
|
||||
b: u16,
|
||||
c: s32,
|
||||
d: buffer(4), // Fixed length Uint8Array
|
||||
e: buffer("b"), // Use value of `b` as length
|
||||
f: buffer(u32), // `u32` length prefix
|
||||
g: buffer(4, {
|
||||
// Custom conversion between `Uint8Array` and other types
|
||||
convert(value: Uint8Array) {
|
||||
return value[0];
|
||||
},
|
||||
back(value: number) {
|
||||
return new Uint8Array([value, 0, 0, 0]);
|
||||
},
|
||||
}),
|
||||
h: string(64), // `string` is an alias to `buffer` with UTF-8 string conversion
|
||||
},
|
||||
{ littleEndian: true },
|
||||
);
|
||||
|
||||
const value = await MyStruct.deserialize(stream);
|
||||
value.foo; // number
|
||||
value.bar; // bigint
|
||||
value.bazLength; // number
|
||||
value.baz; // string
|
||||
// Custom reader
|
||||
const reader = {
|
||||
position: 0,
|
||||
readExactly(length) {
|
||||
const slice = new Uint8Array(100).slice(
|
||||
this.position,
|
||||
this.position + length,
|
||||
);
|
||||
this.position += length;
|
||||
return slice;
|
||||
},
|
||||
};
|
||||
|
||||
const buffer = MyStruct.serialize({
|
||||
foo: 42,
|
||||
bar: 42n,
|
||||
// `bazLength` automatically set to `baz`'s byte length
|
||||
baz: "Hello, World!",
|
||||
const message1 = Message.deserialize(reader); // If `reader.readExactly` is synchronous, `deserialize` is also synchronous
|
||||
const message2 = await Message.deserialize(reader); // If `reader.readExactly` is asynchronous, so do `deserialize`
|
||||
|
||||
const buffer: Uint8Array = Message.serialize(message1);
|
||||
```
|
||||
|
||||
## Custom field types
|
||||
|
||||
```ts
|
||||
import { Field, AsyncExactReadable, Struct, u8 } from "@yume-chan/struct";
|
||||
|
||||
const MyField: Field<number, never, never> = {
|
||||
size: 4, // `0` if dynamically sized,
|
||||
dynamicSize(value: number) {
|
||||
// Optional, return dynamic size for value
|
||||
return 0;
|
||||
},
|
||||
serialize(
|
||||
value: number,
|
||||
context: { buffer: Uint8Array; index: number; littleEndian: boolean },
|
||||
) {
|
||||
// Serialize value to `context.buffer` at `context.index`
|
||||
},
|
||||
deserialize(context: {
|
||||
reader: AsyncExactReadable;
|
||||
littleEndian: boolean;
|
||||
}) {
|
||||
// Deserialize value from `context.reader`
|
||||
return 0;
|
||||
},
|
||||
};
|
||||
|
||||
const Message2 = new Struct({
|
||||
a: u8,
|
||||
b: MyField,
|
||||
});
|
||||
```
|
||||
|
||||
<!-- cspell: disable -->
|
||||
## Bipedal
|
||||
|
||||
- [Installation](#installation)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Compatibility](#compatibility)
|
||||
- [Basic usage](#basic-usage)
|
||||
- [`int64`/`uint64`](#int64uint64)
|
||||
- [`string`](#string)
|
||||
- [API](#api)
|
||||
- [`placeholder`](#placeholder)
|
||||
- [`Struct`](#struct)
|
||||
- [`int8`/`uint8`/`int16`/`uint16`/`int32`/`uint32`](#int8uint8int16uint16int32uint32)
|
||||
- [`int64`/`uint64`](#int64uint64-1)
|
||||
- [`uint8Array`/`string`](#uint8arraystring)
|
||||
- [`concat`](#concat)
|
||||
- [`extra`](#extra)
|
||||
- [`postDeserialize`](#postdeserialize)
|
||||
- [`deserialize`](#deserialize)
|
||||
- [`serialize`](#serialize)
|
||||
- [Custom field type](#custom-field-type)
|
||||
- [`Struct#field`](#structfield)
|
||||
- [Relationship between types](#relationship-between-types)
|
||||
- [`StructFieldDefinition`](#structfielddefinition)
|
||||
- [`TValue`/`TOmitInitKey`](#tvaluetomitinitkey)
|
||||
- [`getSize`](#getsize)
|
||||
- [`create`](#create)
|
||||
- [`deserialize`](#deserialize-1)
|
||||
- [`StructFieldValue`](#structfieldvalue)
|
||||
- [`getSize`](#getsize-1)
|
||||
- [`get`/`set`](#getset)
|
||||
- [`serialize`](#serialize-1)
|
||||
`bipedal` is a custom async helper that allows the same code to behave synchronously or asynchronously depends on the parameters.
|
||||
|
||||
<!-- cspell: enable -->
|
||||
It's inspired by [gensync](https://github.com/loganfsmyth/gensync).
|
||||
|
||||
## Compatibility
|
||||
|
||||
Here is a list of features, their used APIs, and their compatibilities. If an optional feature is not actually used, its requirements can be ignored.
|
||||
|
||||
Some features can be polyfilled to support older runtimes, but this library doesn't ship with any polyfills.
|
||||
|
||||
### Basic usage
|
||||
|
||||
| API | Chrome | Edge | Firefox | Internet Explorer | Safari | Node.js |
|
||||
| -------------------------------- | ------ | ---- | ------- | ----------------- | ------ | ------- |
|
||||
| [`Promise`][mdn_promise] | 32 | 12 | 29 | No | 8 | 0.12 |
|
||||
| [`ArrayBuffer`][mdn_arraybuffer] | 7 | 12 | 4 | 10 | 5.1 | 0.10 |
|
||||
| [`Uint8Array`][mdn_uint8array] | 7 | 12 | 4 | 10 | 5.1 | 0.10 |
|
||||
| _Overall_ | 32 | 12 | 29 | No | 8 | 0.12 |
|
||||
|
||||
### [`int64`/`uint64`](#int64uint64-1)
|
||||
|
||||
| API | Chrome | Edge | Firefox | Internet Explorer | Safari | Node.js |
|
||||
| ---------------------------------- | ------ | ---- | ------- | ----------------- | ------ | ------- |
|
||||
| [`BigInt`][mdn_bigint]<sup>1</sup> | 67 | 79 | 68 | No | 14 | 10.4 |
|
||||
|
||||
<sup>1</sup> Can't be polyfilled
|
||||
|
||||
### [`string`](#uint8arraystring)
|
||||
|
||||
| API | Chrome | Edge | Firefox | Internet Explorer | Safari | Node.js |
|
||||
| -------------------------------- | ------ | ---- | ------- | ----------------- | ------ | ------------------- |
|
||||
| [`TextEncoder`][mdn_textencoder] | 38 | 79 | 19 | No | 10.1 | 8.3<sup>1</sup>, 11 |
|
||||
|
||||
<sup>1</sup> `TextEncoder` and `TextDecoder` are only available in `util` module. Need to be assigned to `globalThis`.
|
||||
|
||||
[mdn_promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
|
||||
[mdn_arraybuffer]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
|
||||
[mdn_uint8array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
|
||||
[mdn_bigint]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt
|
||||
[mdn_textencoder]: https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder
|
||||
|
||||
## API
|
||||
|
||||
### `placeholder`
|
||||
The word `bipedal` refers to animals who walk using two legs.
|
||||
|
||||
```ts
|
||||
function placeholder<T>(): T {
|
||||
return undefined as unknown as T;
|
||||
}
|
||||
import { bipedal } from "@yume-chan/struct";
|
||||
|
||||
const fn = bipedal(function* (then, name: string | Promise<string>) {
|
||||
name = yield* then(name);
|
||||
return "Hello, " + name;
|
||||
});
|
||||
|
||||
fn("Simon"); // "Hello, Simon"
|
||||
await fn(Promise.resolve("Simon")); // "Hello, Simon"
|
||||
```
|
||||
|
||||
Returns a (fake) value of the given type. It's only useful in TypeScript, if you are using JavaScript, you shouldn't care about it.
|
||||
|
||||
Many methods in this library have multiple generic parameters, but TypeScript only allows users to specify none (let TypeScript inference all of them from arguments), or all generic arguments. ([Microsoft/TypeScript#26242](https://github.com/microsoft/TypeScript/issues/26242))
|
||||
|
||||
<details>
|
||||
<summary>Detail explanation (click to expand)</summary>
|
||||
|
||||
When you have a generic method, where half generic parameters can be inferred.
|
||||
|
||||
```ts
|
||||
declare function fn<A, B>(a: A): [A, B];
|
||||
fn(42); // Expected 2 type arguments, but got 1. ts(2558)
|
||||
```
|
||||
|
||||
Rather than force users repeat the type `A`, I declare a parameter for `B`.
|
||||
|
||||
```ts
|
||||
declare function fn2<A, B>(a: A, b: B): [A, B];
|
||||
```
|
||||
|
||||
I don't really need a value of type `B`, I only require its type information
|
||||
|
||||
```ts
|
||||
fn2(42, placeholder<boolean>()); // fn2<number, boolean>
|
||||
```
|
||||
|
||||
</details><br/>
|
||||
|
||||
To workaround this issue, these methods have an extra `_typescriptType` parameter, to let you specify a generic parameter, without passing all other generic arguments manually. The actual value of `_typescriptType` argument is never used, so you can pass any value, as long as it has the correct type, including values produced by this `placeholder` method.
|
||||
|
||||
**With that said, I don't expect you to specify any generic arguments manually when using this library.**
|
||||
|
||||
### `Struct`
|
||||
|
||||
```ts
|
||||
class Struct<
|
||||
TFields extends object = {},
|
||||
TOmitInitKey extends string | number | symbol = never,
|
||||
TExtra extends object = {},
|
||||
TPostDeserialized = undefined,
|
||||
> {
|
||||
public constructor(options: Partial<StructOptions> = StructDefaultOptions);
|
||||
}
|
||||
```
|
||||
|
||||
Creates a new structure definition.
|
||||
|
||||
<details>
|
||||
<summary>Generic parameters (click to expand)</summary>
|
||||
|
||||
This information was added to help you understand how does it work. These are considered as "internal state" so don't specify them manually.
|
||||
|
||||
1. `TFields`: Type of the Struct value. Modified when new fields are added.
|
||||
2. `TOmitInitKey`: When serializing a structure containing variable length buffers, the length field can be calculate from the buffer field, so they doesn't need to be provided explicitly.
|
||||
3. `TExtra`: Type of extra fields. Modified when `extra` is called.
|
||||
4. `TPostDeserialized`: State of the `postDeserialize` function. Modified when `postDeserialize` is called. Affects return type of `deserialize`
|
||||
|
||||
</details><br/>
|
||||
|
||||
**Parameters**
|
||||
|
||||
1. `options`:
|
||||
- `littleEndian:boolean = false`: Whether all multi-byte fields in this struct are [little-endian encoded][wikipeida_endianess].
|
||||
|
||||
[wikipeida_endianess]: https://en.wikipedia.org/wiki/Endianness
|
||||
|
||||
#### `int8`/`uint8`/`int16`/`uint16`/`int32`/`uint32`
|
||||
|
||||
```ts
|
||||
int32<
|
||||
TName extends string | number | symbol,
|
||||
TTypeScriptType = number
|
||||
>(
|
||||
name: TName,
|
||||
_typescriptType?: TTypeScriptType
|
||||
): Struct<
|
||||
TFields & Record<TName, TTypeScriptType>,
|
||||
TOmitInitKey,
|
||||
TExtra,
|
||||
TPostDeserialized
|
||||
>;
|
||||
```
|
||||
|
||||
Appends an `int8`/`uint8`/`int16`/`uint16`/`int32`/`uint32` field to the `Struct`.
|
||||
|
||||
<details>
|
||||
<summary>Generic parameters (click to expand)</summary>
|
||||
|
||||
1. `TName`: Literal type of the field's name.
|
||||
2. `TTypeScriptType = number`: Type of the field in the result object. For example you can declare it as a number literal type, or some enum type.
|
||||
|
||||
</details><br/>
|
||||
|
||||
**Parameters**
|
||||
|
||||
1. `name`: (Required) Field name. Must be a string literal.
|
||||
2. `_typescriptType`: Set field's type. See examples below.
|
||||
|
||||
**Note**
|
||||
|
||||
There is no generic constraints on the `TTypeScriptType`, because TypeScript doesn't allow casting enum types to `number`.
|
||||
|
||||
So it's technically possible to pass in an incompatible type (e.g. `string`). But obviously, it's a bad idea.
|
||||
|
||||
**Examples**
|
||||
|
||||
1. Append an `int32` field named `foo`
|
||||
|
||||
```ts
|
||||
const struct = new Struct().int32("foo");
|
||||
|
||||
const value = await struct.deserialize(stream);
|
||||
value.foo; // number
|
||||
|
||||
struct.serialize({}); // error: 'foo' is required
|
||||
struct.serialize({ foo: "bar" }); // error: 'foo' must be a number
|
||||
struct.serialize({ foo: 42 }); // ok
|
||||
```
|
||||
|
||||
2. Set fields' type (can use [`placeholder` method](#placeholder))
|
||||
|
||||
```ts
|
||||
enum MyEnum {
|
||||
a,
|
||||
b,
|
||||
}
|
||||
|
||||
const struct = new Struct()
|
||||
.int32("foo", placeholder<MyEnum>())
|
||||
.int32("bar", MyEnum.a as const);
|
||||
|
||||
const value = await struct.deserialize(stream);
|
||||
value.foo; // MyEnum
|
||||
value.bar; // MyEnum.a
|
||||
|
||||
struct.serialize({ foo: 42, bar: MyEnum.a }); // error: 'foo' must be of type `MyEnum`
|
||||
struct.serialize({ foo: MyEnum.a, bar: MyEnum.b }); // error: 'bar' must be of type `MyEnum.a`
|
||||
struct.serialize({ foo: MyEnum.a, bar: MyEnum.b }); // ok
|
||||
```
|
||||
|
||||
#### `int64`/`uint64`
|
||||
|
||||
```ts
|
||||
int64<
|
||||
TName extends string | number | symbol,
|
||||
TTypeScriptType = bigint
|
||||
>(
|
||||
name: TName,
|
||||
_typescriptType?: TTypeScriptType
|
||||
): Struct<
|
||||
TFields & Record<TName, TTypeScriptType>,
|
||||
TOmitInitKey,
|
||||
TExtra,
|
||||
TPostDeserialized
|
||||
>;
|
||||
```
|
||||
|
||||
Appends an `int64`/`uint64` field to the `Struct`. The usage is same as `uint32`/`uint32`.
|
||||
|
||||
Requires native support for `BigInt`. Check [compatibility table](#compatibility) for more information.
|
||||
|
||||
#### `uint8Array`/`string`
|
||||
|
||||
```ts
|
||||
uint8Array<
|
||||
TName extends string | number | symbol,
|
||||
TTypeScriptType = ArrayBuffer
|
||||
>(
|
||||
name: TName,
|
||||
options: FixedLengthBufferLikeFieldOptions,
|
||||
_typescriptType?: TTypeScriptType,
|
||||
): Struct<
|
||||
TFields & Record<TName, TTypeScriptType>,
|
||||
TOmitInitKey,
|
||||
TExtra,
|
||||
TPostDeserialized
|
||||
>;
|
||||
|
||||
uint8Array<
|
||||
TName extends string | number | symbol,
|
||||
TLengthField extends LengthField<TFields>,
|
||||
TOptions extends VariableLengthBufferLikeFieldOptions<TFields, TLengthField>,
|
||||
TTypeScriptType = ArrayBuffer,
|
||||
>(
|
||||
name: TName,
|
||||
options: TOptions,
|
||||
_typescriptType?: TTypeScriptType,
|
||||
): Struct<
|
||||
TFields & Record<TName, TTypeScriptType>,
|
||||
TOmitInitKey | TLengthField,
|
||||
TExtra,
|
||||
TPostDeserialized
|
||||
>;
|
||||
```
|
||||
|
||||
Appends an `uint8Array`/`string` field to the `Struct`.
|
||||
|
||||
The `options` parameter defines its length, it supports two formats:
|
||||
|
||||
- `{ length: number }`: Presence of the `length` option indicates that it's a fixed length array.
|
||||
- `{ lengthField: string; lengthFieldRadix?: number }`: Presence of the `lengthField` option indicates it's a variable length array. The `lengthField` options must refers to a `number` or `string` (can't be `bigint`) typed field that's already defined in this `Struct`. If the length field is a `string`, the optional `lengthFieldRadix` option (defaults to `10`) defines the radix when converting the string to a number. When deserializing, it will use that field's value as its length. When serializing, it will write its length to that field.
|
||||
|
||||
#### `concat`
|
||||
|
||||
```ts
|
||||
concat<
|
||||
TOther extends Struct<any, any, any, any>
|
||||
>(
|
||||
other: TOther
|
||||
): Struct<
|
||||
TFields & TOther['fieldsType'],
|
||||
TOmitInitKey | TOther['omitInitType'],
|
||||
TExtra & TOther['extraType'],
|
||||
TPostDeserialized
|
||||
>;
|
||||
```
|
||||
|
||||
Merges (flats) another `Struct`'s fields and extra fields into the current one.
|
||||
|
||||
**Examples**
|
||||
|
||||
1. Extending another `Struct`
|
||||
|
||||
```ts
|
||||
const MyStructV1 = new Struct().int32("field1");
|
||||
|
||||
const MyStructV2 = new Struct().concat(MyStructV1).int32("field2");
|
||||
|
||||
const structV2 = await MyStructV2.deserialize(stream);
|
||||
structV2.field1; // number
|
||||
structV2.field2; // number
|
||||
// Fields are flatten
|
||||
```
|
||||
|
||||
2. Also possible in any order
|
||||
|
||||
```ts
|
||||
const MyStructV1 = new Struct().int32("field1");
|
||||
|
||||
const MyStructV2 = new Struct().int32("field2").concat(MyStructV1);
|
||||
|
||||
const structV2 = await MyStructV2.deserialize(stream);
|
||||
structV2.field1; // number
|
||||
structV2.field2; // number
|
||||
// Same result as above, but serialize/deserialize order is reversed
|
||||
```
|
||||
|
||||
#### `extra`
|
||||
|
||||
```ts
|
||||
extra<
|
||||
T extends Record<
|
||||
Exclude<
|
||||
keyof T,
|
||||
Exclude<
|
||||
keyof T,
|
||||
keyof TFields
|
||||
>
|
||||
>,
|
||||
never
|
||||
>
|
||||
>(
|
||||
value: T & ThisType<Overwrite<Overwrite<TExtra, T>, TFields>>
|
||||
): Struct<
|
||||
TFields,
|
||||
TInit,
|
||||
Overwrite<TExtra, T>,
|
||||
TPostDeserialized
|
||||
>;
|
||||
```
|
||||
|
||||
Adds extra fields into the `Struct`. Extra fields will be defined on prototype of each Struct values, so they don't affect serialize and deserialize process, and deserialized fields will overwrite extra fields.
|
||||
|
||||
Multiple calls merge all extra fields together.
|
||||
|
||||
**Generic Parameters**
|
||||
|
||||
1. `T`: Type of the extra fields. The scary looking generic constraint is used to forbid overwriting any already existed fields.
|
||||
|
||||
**Parameters**
|
||||
|
||||
1. `value`: An object containing anything you want to add to Struct values. Accessors and methods are also allowed.
|
||||
|
||||
**Examples**
|
||||
|
||||
1. Add an extra field
|
||||
|
||||
```ts
|
||||
const struct = new Struct().int32("foo").extra({
|
||||
bar: "hello",
|
||||
});
|
||||
|
||||
const value = await struct.deserialize(stream);
|
||||
value.foo; // number
|
||||
value.bar; // 'hello'
|
||||
|
||||
struct.serialize({ foo: 42 }); // ok
|
||||
struct.serialize({ foo: 42, bar: "hello" }); // error: 'bar' is redundant
|
||||
```
|
||||
|
||||
2. Add getters and methods. `this` in functions refers to the result object.
|
||||
|
||||
```ts
|
||||
const struct = new Struct().int32("foo").extra({
|
||||
get bar() {
|
||||
// `this` is the result Struct value
|
||||
return this.foo + 1;
|
||||
},
|
||||
logBar() {
|
||||
// `this` also contains other extra fields
|
||||
console.log(this.bar);
|
||||
},
|
||||
});
|
||||
|
||||
const value = await struct.deserialize(stream);
|
||||
value.foo; // number
|
||||
value.bar; // number
|
||||
value.logBar();
|
||||
```
|
||||
|
||||
#### `postDeserialize`
|
||||
|
||||
```ts
|
||||
postDeserialize(): Struct<TFields, TOmitInitKey, TExtra, undefined>;
|
||||
```
|
||||
|
||||
Remove any registered post-deserialization callback.
|
||||
|
||||
```ts
|
||||
postDeserialize(
|
||||
callback: (this: TFields, object: TFields) => never
|
||||
): Struct<TFields, TOmitInitKey, TExtra, never>;
|
||||
postDeserialize(
|
||||
callback: (this: TFields, object: TFields) => void
|
||||
): Struct<TFields, TOmitInitKey, TExtra, undefined>;
|
||||
```
|
||||
|
||||
Registers (or replaces) a custom callback to be run after deserialized.
|
||||
|
||||
`this` in `callback`, along with the first parameter `object` will both be the deserialized Struct value.
|
||||
|
||||
A callback returning `never` (always throws errors) will change the return type of `deserialize` to `never`.
|
||||
|
||||
A callback returning `void` means it modify the result object in-place (or doesn't modify it at all), so `deserialize` will still return the result object.
|
||||
|
||||
```ts
|
||||
postDeserialize<TPostSerialize>(
|
||||
callback: (this: TFields, object: TFields) => TPostSerialize
|
||||
): Struct<TFields, TOmitInitKey, TExtra, TPostSerialize>;
|
||||
```
|
||||
|
||||
Registers (or replaces) a custom callback to be run after deserialized.
|
||||
|
||||
A callback returning anything other than `undefined` will cause `deserialize` to return that value instead.
|
||||
|
||||
**Generic Parameters**
|
||||
|
||||
1. `TPostSerialize`: Type of the new result.
|
||||
|
||||
**Parameters**
|
||||
|
||||
1. `callback`: An function contains the custom logic to be run, optionally returns a new result. Or `undefined`, to remove any previously set `postDeserialize` callback.
|
||||
|
||||
**Examples**
|
||||
|
||||
1. Handle an "error" packet
|
||||
|
||||
```ts
|
||||
// Say your protocol have an error packet,
|
||||
// You want to throw a JavaScript Error when received such a packet,
|
||||
// But you don't want to modify all receiving path
|
||||
|
||||
const struct = new Struct()
|
||||
.int32("messageLength")
|
||||
.string("message", { lengthField: "messageLength" })
|
||||
.postDeserialize((value) => {
|
||||
throw new Error(value.message);
|
||||
});
|
||||
```
|
||||
|
||||
2. Do anything you want
|
||||
|
||||
```ts
|
||||
// I think this one doesn't need any code example
|
||||
```
|
||||
|
||||
3. Replace result object
|
||||
|
||||
```ts
|
||||
const struct1 = new Struct().int32("foo").postDeserialize((value) => {
|
||||
return {
|
||||
bar: value.foo,
|
||||
};
|
||||
});
|
||||
|
||||
const value = await struct.deserialize(stream);
|
||||
value.foo; // error: not exist
|
||||
value.bar; // number
|
||||
```
|
||||
|
||||
#### `deserialize`
|
||||
|
||||
```ts
|
||||
|
||||
interface ExactReadable {
|
||||
readonly position: number;
|
||||
|
||||
/**
|
||||
* Read data from the underlying data source.
|
||||
*
|
||||
* The stream must return exactly `length` bytes or data. If that's not possible
|
||||
* (due to end of file or other error condition), it must throw an {@link ExactReadableEndedError}.
|
||||
*/
|
||||
readExactly(length: number): Uint8Array;
|
||||
}
|
||||
|
||||
interface AsyncExactReadable {
|
||||
readonly position: number;
|
||||
|
||||
/**
|
||||
* Read data from the underlying data source.
|
||||
*
|
||||
* The stream must return exactly `length` bytes or data. If that's not possible
|
||||
* (due to end of file or other error condition), it must throw an {@link ExactReadableEndedError}.
|
||||
*/
|
||||
readExactly(length: number): ValueOrPromise<Uint8Array>;
|
||||
}
|
||||
|
||||
deserialize(
|
||||
stream: ExactReadable,
|
||||
): TPostDeserialized extends undefined
|
||||
? Overwrite<TExtra, TValue>
|
||||
: TPostDeserialized
|
||||
>;
|
||||
deserialize(
|
||||
stream: AsyncExactReadable,
|
||||
): Promise<
|
||||
TPostDeserialized extends undefined
|
||||
? Overwrite<TExtra, TValue>
|
||||
: TPostDeserialized
|
||||
>
|
||||
>;
|
||||
```
|
||||
|
||||
Deserialize a struct value from `stream`.
|
||||
|
||||
It will be synchronous (returns a value) or asynchronous (returns a `Promise`) depending on the type of `stream`.
|
||||
|
||||
As the signature shows, if the `postDeserialize` callback returns any value, `deserialize` will return that value instead.
|
||||
|
||||
#### `serialize`
|
||||
|
||||
```ts
|
||||
serialize(init: Evaluate<Omit<TFields, TOmitInitKey>>): Uint8Array;
|
||||
serialize(init: Evaluate<Omit<TFields, TOmitInitKey>>, output: Uint8Array): number;
|
||||
```
|
||||
|
||||
Serialize a struct value into an `Uint8Array`.
|
||||
|
||||
If an `output` is given, it will serialize the struct into it, and returns the number of bytes written.
|
||||
|
||||
## Custom field type
|
||||
|
||||
It's also possible to create your own field types.
|
||||
|
||||
### `Struct#field`
|
||||
|
||||
```ts
|
||||
field<
|
||||
TName extends string | number | symbol,
|
||||
TDefinition extends StructFieldDefinition<any, any, any>
|
||||
>(
|
||||
name: TName,
|
||||
definition: TDefinition
|
||||
): Struct<
|
||||
TFields & Record<TName, TDefinition['TValue']>,
|
||||
TOmitInitKey | TDefinition['TOmitInitKey'],
|
||||
TExtra,
|
||||
TPostDeserialized
|
||||
>;
|
||||
```
|
||||
|
||||
Appends a `StructFieldDefinition` to the `Struct`.
|
||||
|
||||
All built-in field type methods are actually aliases to it. For example, calling
|
||||
|
||||
```ts
|
||||
struct.int8("foo");
|
||||
```
|
||||
|
||||
is same as
|
||||
|
||||
```ts
|
||||
struct.field("foo", new NumberFieldDefinition(NumberFieldType.Int8));
|
||||
```
|
||||
|
||||
### Relationship between types
|
||||
|
||||
- `StructFieldValue`: Contains value of a field, with optional metadata and accessor methods.
|
||||
- `StructFieldDefinition`: Definition of a field, can deserialize `StructFieldValue`s from a stream or create them from exist values.
|
||||
- `StructValue`: A map between field names and `StructFieldValue`s.
|
||||
- `Struct`: Definition of a struct, a map between field names and `StructFieldDefintion`s. May contain extra metadata.
|
||||
- Result of `Struct#deserialize()`: A map between field names and results of `StructFieldValue#get()`.
|
||||
|
||||
### `StructFieldDefinition`
|
||||
|
||||
```ts
|
||||
abstract class StructFieldDefinition<
|
||||
TOptions = void,
|
||||
TValue = unknown,
|
||||
TOmitInitKey extends PropertyKey = never,
|
||||
> {
|
||||
public readonly options: TOptions;
|
||||
|
||||
public constructor(options: TOptions);
|
||||
}
|
||||
```
|
||||
|
||||
A field definition defines how to deserialize a field.
|
||||
|
||||
It's an `abstract` class, means it can't be constructed (`new`ed) directly. It's only used as a base class for other field types.
|
||||
|
||||
#### `TValue`/`TOmitInitKey`
|
||||
|
||||
These two fields provide type information to TypeScript compiler. Their values will always be `undefined`, but having correct types is enough. You don't need to touch them.
|
||||
|
||||
#### `getSize`
|
||||
|
||||
```ts
|
||||
abstract getSize(): number;
|
||||
```
|
||||
|
||||
Derived classes must implement this method to return size (or minimal size if it's dynamic) of this field.
|
||||
|
||||
Actual size should be returned from `StructFieldValue#getSize`
|
||||
|
||||
#### `create`
|
||||
|
||||
```ts
|
||||
abstract create(
|
||||
options: Readonly<StructOptions>,
|
||||
struct: StructValue,
|
||||
value: TValue,
|
||||
): StructFieldValue<this>;
|
||||
```
|
||||
|
||||
Derived classes must implement this method to create its own field value instance for the current definition.
|
||||
|
||||
`Struct#serialize` will call this method, then call `StructFieldValue#serialize` to serialize one field value.
|
||||
|
||||
#### `deserialize`
|
||||
|
||||
```ts
|
||||
abstract deserialize(
|
||||
options: Readonly<StructOptions>,
|
||||
stream: ExactReadable,
|
||||
struct: StructValue,
|
||||
): StructFieldValue<this>;
|
||||
abstract deserialize(
|
||||
options: Readonly<StructOptions>,
|
||||
stream: AsyncExactReadable,
|
||||
struct: StructValue,
|
||||
): Promise<StructFieldValue<this>>;
|
||||
```
|
||||
|
||||
Derived classes must implement this method to define how to deserialize a value from `stream`.
|
||||
|
||||
It must be synchronous (returns a value) or asynchronous (returns a `Promise`) depending on the type of `stream`.
|
||||
|
||||
Usually implementations should be:
|
||||
|
||||
1. Read required bytes from `stream`
|
||||
2. Parse it to your type
|
||||
3. Pass the value into your own `create` method
|
||||
|
||||
Sometimes, extra metadata is present when deserializing, but need to be calculated when serializing, for example a UTF-8 encoded string may have different length between itself (character count) and serialized form (byte length). So `deserialize` can save those metadata on the `StructFieldValue` instance for later use.
|
||||
|
||||
### `StructFieldValue`
|
||||
|
||||
```ts
|
||||
abstract class StructFieldValue<
|
||||
TDefinition extends StructFieldDefinition<any, any, any>
|
||||
>
|
||||
```
|
||||
|
||||
A field value defines how to serialize a field.
|
||||
|
||||
#### `getSize`
|
||||
|
||||
```ts
|
||||
getSize(): number;
|
||||
```
|
||||
|
||||
Gets size of this field. By default, it returns its `definition`'s size.
|
||||
|
||||
If this field's size can change based on some criteria, one must override `getSize` to return its actual size.
|
||||
|
||||
#### `get`/`set`
|
||||
|
||||
```ts
|
||||
get(): TDefinition['TValue'];
|
||||
set(value: TDefinition['TValue']): void;
|
||||
```
|
||||
|
||||
Defines how to get or set this field's value. By default, it reads/writes its `value` field.
|
||||
|
||||
If one needs to manipulate other states when getting/setting values, they can override these methods.
|
||||
|
||||
#### `serialize`
|
||||
|
||||
```ts
|
||||
abstract serialize(
|
||||
array: Uint8Array,
|
||||
offset: number
|
||||
): void;
|
||||
```
|
||||
|
||||
Derived classes must implement this method to serialize current value into `array`, from `offset`. It must not write more bytes than what its `getSize` returned.
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
import * as assert from "node:assert";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import type { ValueOrPromise } from "../utils.js";
|
||||
|
||||
import { StructFieldDefinition } from "./definition.js";
|
||||
import type { StructFieldValue } from "./field-value.js";
|
||||
import type { StructOptions } from "./options.js";
|
||||
import type { AsyncExactReadable, ExactReadable } from "./stream.js";
|
||||
import type { StructValue } from "./struct-value.js";
|
||||
|
||||
describe("StructFieldDefinition", () => {
|
||||
describe(".constructor", () => {
|
||||
it("should save the `options` parameter", () => {
|
||||
class MockFieldDefinition extends StructFieldDefinition<number> {
|
||||
constructor(options: number) {
|
||||
super(options);
|
||||
}
|
||||
override getSize(): number {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
override create(
|
||||
options: Readonly<StructOptions>,
|
||||
struct: StructValue,
|
||||
value: unknown,
|
||||
): StructFieldValue<this> {
|
||||
void options;
|
||||
void struct;
|
||||
void value;
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
override deserialize(
|
||||
options: Readonly<StructOptions>,
|
||||
stream: ExactReadable,
|
||||
struct: StructValue,
|
||||
): StructFieldValue<this>;
|
||||
override deserialize(
|
||||
options: Readonly<StructOptions>,
|
||||
stream: AsyncExactReadable,
|
||||
struct: StructValue,
|
||||
): Promise<StructFieldValue<this>>;
|
||||
override deserialize(
|
||||
options: Readonly<StructOptions>,
|
||||
stream: ExactReadable | AsyncExactReadable,
|
||||
struct: StructValue,
|
||||
): ValueOrPromise<StructFieldValue<this>> {
|
||||
void options;
|
||||
void stream;
|
||||
void struct;
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
assert.strictEqual(new MockFieldDefinition(42).options, 42);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,68 +0,0 @@
|
|||
import type { StructFieldValue } from "./field-value.js";
|
||||
import type { StructOptions } from "./options.js";
|
||||
import type { AsyncExactReadable, ExactReadable } from "./stream.js";
|
||||
import type { StructValue } from "./struct-value.js";
|
||||
|
||||
/**
|
||||
* A field definition defines how to deserialize a field.
|
||||
*
|
||||
* @template TOptions TypeScript type of this definition's `options`.
|
||||
* @template TValue TypeScript type of this field.
|
||||
* @template TOmitInitKey Optionally remove some fields from the init type. Should be a union of string literal types.
|
||||
*/
|
||||
export abstract class StructFieldDefinition<
|
||||
TOptions = void,
|
||||
TValue = unknown,
|
||||
TOmitInitKey extends PropertyKey = never,
|
||||
> {
|
||||
/**
|
||||
* When `T` is a type initiated `StructFieldDefinition`,
|
||||
* use `T['TValue']` to retrieve its `TValue` type parameter.
|
||||
*/
|
||||
readonly TValue!: TValue;
|
||||
|
||||
/**
|
||||
* When `T` is a type initiated `StructFieldDefinition`,
|
||||
* use `T['TOmitInitKey']` to retrieve its `TOmitInitKey` type parameter.
|
||||
*/
|
||||
readonly TOmitInitKey!: TOmitInitKey;
|
||||
|
||||
readonly options: TOptions;
|
||||
|
||||
constructor(options: TOptions) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* When implemented in derived classes, returns the size (or minimal size if it's dynamic) of this field.
|
||||
*
|
||||
* Actual size can be retrieved from `StructFieldValue#getSize`
|
||||
*/
|
||||
abstract getSize(): number;
|
||||
|
||||
/**
|
||||
* When implemented in derived classes, creates a `StructFieldValue` from a given `value`.
|
||||
*/
|
||||
abstract create(
|
||||
options: Readonly<StructOptions>,
|
||||
structValue: StructValue,
|
||||
value: TValue,
|
||||
): StructFieldValue<this>;
|
||||
|
||||
/**
|
||||
* When implemented in derived classes,It must be synchronous (returns a value) or asynchronous (returns a `Promise`) depending
|
||||
* on the type of `stream`. reads and creates a `StructFieldValue` from `stream`.
|
||||
*
|
||||
* `SyncPromise` can be used to simplify implementation.
|
||||
*/
|
||||
abstract deserialize(
|
||||
options: Readonly<StructOptions>,
|
||||
stream: ExactReadable,
|
||||
structValue: StructValue,
|
||||
): StructFieldValue<this>;
|
||||
abstract deserialize(
|
||||
options: Readonly<StructOptions>,
|
||||
stream: AsyncExactReadable,
|
||||
struct: StructValue,
|
||||
): Promise<StructFieldValue<this>>;
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import * as assert from "node:assert";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import type { ValueOrPromise } from "../utils.js";
|
||||
|
||||
import { StructFieldDefinition } from "./definition.js";
|
||||
import { StructFieldValue } from "./field-value.js";
|
||||
import type { StructOptions } from "./options.js";
|
||||
import type { AsyncExactReadable, ExactReadable } from "./stream.js";
|
||||
import type { StructValue } from "./struct-value.js";
|
||||
|
||||
describe("StructFieldValue", () => {
|
||||
describe(".constructor", () => {
|
||||
it("should save parameters", () => {
|
||||
class MockStructFieldValue extends StructFieldValue<never> {
|
||||
override serialize(array: Uint8Array, offset: number): void {
|
||||
void array;
|
||||
void offset;
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
const definition = {};
|
||||
const options = {};
|
||||
const struct = {};
|
||||
const value = {};
|
||||
|
||||
const fieldValue = new MockStructFieldValue(
|
||||
definition as never,
|
||||
options as never,
|
||||
struct as never,
|
||||
value as never,
|
||||
);
|
||||
assert.strictEqual(fieldValue.definition, definition);
|
||||
assert.strictEqual(fieldValue.options, options);
|
||||
assert.strictEqual(fieldValue.struct, struct);
|
||||
assert.strictEqual(fieldValue.get(), value);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#getSize", () => {
|
||||
it("should return same value as definition's", () => {
|
||||
class MockFieldDefinition extends StructFieldDefinition {
|
||||
override getSize(): number {
|
||||
return 42;
|
||||
}
|
||||
override create(
|
||||
options: Readonly<StructOptions>,
|
||||
struct: StructValue,
|
||||
value: unknown,
|
||||
): StructFieldValue<this> {
|
||||
void options;
|
||||
void struct;
|
||||
void value;
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
override deserialize(
|
||||
options: Readonly<StructOptions>,
|
||||
stream: ExactReadable,
|
||||
struct: StructValue,
|
||||
): StructFieldValue<this>;
|
||||
override deserialize(
|
||||
options: Readonly<StructOptions>,
|
||||
stream: AsyncExactReadable,
|
||||
struct: StructValue,
|
||||
): Promise<StructFieldValue<this>>;
|
||||
override deserialize(
|
||||
options: Readonly<StructOptions>,
|
||||
stream: ExactReadable | AsyncExactReadable,
|
||||
struct: StructValue,
|
||||
): ValueOrPromise<StructFieldValue<this>> {
|
||||
void options;
|
||||
void stream;
|
||||
void struct;
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
class MockStructFieldValue extends StructFieldValue<any> {
|
||||
override serialize(array: Uint8Array, offset: number): void {
|
||||
void array;
|
||||
void offset;
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
const fieldDefinition = new MockFieldDefinition();
|
||||
const fieldValue = new MockStructFieldValue(
|
||||
fieldDefinition,
|
||||
undefined as never,
|
||||
undefined as never,
|
||||
undefined as never,
|
||||
);
|
||||
assert.strictEqual(fieldValue.getSize(), 42);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#set", () => {
|
||||
it("should update its internal value", () => {
|
||||
class MockStructFieldValue extends StructFieldValue<any> {
|
||||
override serialize(array: Uint8Array, offset: number): void {
|
||||
void array;
|
||||
void offset;
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
const fieldValue = new MockStructFieldValue(
|
||||
undefined as never,
|
||||
undefined as never,
|
||||
undefined as never,
|
||||
undefined as never,
|
||||
);
|
||||
fieldValue.set(1);
|
||||
assert.strictEqual(fieldValue.get(), 1);
|
||||
|
||||
fieldValue.set(2);
|
||||
assert.strictEqual(fieldValue.get(), 2);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,71 +0,0 @@
|
|||
import type { StructFieldDefinition } from "./definition.js";
|
||||
import type { StructOptions } from "./options.js";
|
||||
import type { StructValue } from "./struct-value.js";
|
||||
|
||||
/**
|
||||
* A field value defines how to serialize a field.
|
||||
*
|
||||
* It may contains extra metadata about the value which are essential or
|
||||
* helpful for the serialization process.
|
||||
*/
|
||||
export abstract class StructFieldValue<
|
||||
TDefinition extends StructFieldDefinition<unknown, unknown, PropertyKey>,
|
||||
> {
|
||||
/** Gets the definition associated with this runtime value */
|
||||
readonly definition: TDefinition;
|
||||
|
||||
/** Gets the options of the associated `Struct` */
|
||||
readonly options: Readonly<StructOptions>;
|
||||
|
||||
/** Gets the associated `Struct` instance */
|
||||
readonly struct: StructValue;
|
||||
|
||||
get hasCustomAccessors(): boolean {
|
||||
return (
|
||||
this.get !== StructFieldValue.prototype.get ||
|
||||
this.set !== StructFieldValue.prototype.set
|
||||
);
|
||||
}
|
||||
|
||||
protected value: TDefinition["TValue"];
|
||||
|
||||
constructor(
|
||||
definition: TDefinition,
|
||||
options: Readonly<StructOptions>,
|
||||
struct: StructValue,
|
||||
value: TDefinition["TValue"],
|
||||
) {
|
||||
this.definition = definition;
|
||||
this.options = options;
|
||||
this.struct = struct;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets size of this field. By default, it returns its `definition`'s size.
|
||||
*
|
||||
* When overridden in derived classes, can have custom logic to calculate the actual size.
|
||||
*/
|
||||
getSize(): number {
|
||||
return this.definition.getSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* When implemented in derived classes, reads current field's value.
|
||||
*/
|
||||
get(): TDefinition["TValue"] {
|
||||
return this.value as never;
|
||||
}
|
||||
|
||||
/**
|
||||
* When implemented in derived classes, updates current field's value.
|
||||
*/
|
||||
set(value: TDefinition["TValue"]): void {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* When implemented in derived classes, serializes this field into `array` at `offset`
|
||||
*/
|
||||
abstract serialize(array: Uint8Array, offset: number): void;
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
export * from "./definition.js";
|
||||
export * from "./field-value.js";
|
||||
export * from "./options.js";
|
||||
export * from "./stream.js";
|
||||
export * from "./struct-value.js";
|
|
@ -1,12 +0,0 @@
|
|||
import * as assert from "node:assert";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import { StructDefaultOptions } from "./options.js";
|
||||
|
||||
describe("StructDefaultOptions", () => {
|
||||
describe(".littleEndian", () => {
|
||||
it("should be `false`", () => {
|
||||
assert.strictEqual(StructDefaultOptions.littleEndian, false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,19 +0,0 @@
|
|||
export interface StructOptions {
|
||||
/**
|
||||
* Whether all multi-byte fields in this struct are little-endian encoded.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
littleEndian: boolean;
|
||||
|
||||
// TODO: StructOptions: investigate whether this is necessary
|
||||
// I can't think about any other options which need to be struct wide.
|
||||
// Even endianness can be set on a per-field basis (because it's not meaningful
|
||||
// for some field types like `Uint8Array`, and very rarely, a struct may contain
|
||||
// mixed endianness).
|
||||
// It's just more common and a little more convenient to have it here.
|
||||
}
|
||||
|
||||
export const StructDefaultOptions: Readonly<StructOptions> = {
|
||||
littleEndian: false,
|
||||
};
|
|
@ -1,107 +0,0 @@
|
|||
import * as assert from "node:assert";
|
||||
import { describe, it, mock } from "node:test";
|
||||
|
||||
import type { StructFieldDefinition } from "./definition.js";
|
||||
import type { StructFieldValue } from "./field-value.js";
|
||||
import { StructValue } from "./struct-value.js";
|
||||
|
||||
describe("StructValue", () => {
|
||||
describe(".constructor", () => {
|
||||
it("should create `fieldValues` and `value`", () => {
|
||||
const foo = new StructValue({});
|
||||
const bar = new StructValue({});
|
||||
|
||||
assert.deepStrictEqual(foo.fieldValues, {});
|
||||
assert.deepEqual(foo.value, {});
|
||||
assert.deepStrictEqual(bar.fieldValues, {});
|
||||
assert.deepEqual(bar.value, {});
|
||||
assert.notStrictEqual(foo.fieldValues, bar.fieldValues);
|
||||
assert.notStrictEqual(foo.value, bar.fieldValues);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#set", () => {
|
||||
it("should save the `StructFieldValue`", () => {
|
||||
const object = new StructValue({});
|
||||
|
||||
const foo = "foo";
|
||||
const fooValue = {
|
||||
get() {
|
||||
return 42;
|
||||
},
|
||||
} as StructFieldValue<
|
||||
StructFieldDefinition<unknown, unknown, PropertyKey>
|
||||
>;
|
||||
object.set(foo, fooValue);
|
||||
|
||||
const bar = "bar";
|
||||
const barValue = {
|
||||
get() {
|
||||
return "foo";
|
||||
},
|
||||
} as StructFieldValue<
|
||||
StructFieldDefinition<unknown, unknown, PropertyKey>
|
||||
>;
|
||||
object.set(bar, barValue);
|
||||
|
||||
assert.strictEqual(object.fieldValues[foo], fooValue);
|
||||
assert.strictEqual(object.fieldValues[bar], barValue);
|
||||
});
|
||||
|
||||
it("should define a property for `key`", () => {
|
||||
const object = new StructValue({});
|
||||
|
||||
const foo = "foo";
|
||||
const fooGetter = mock.fn(() => 42);
|
||||
const fooSetter = mock.fn((value: number) => {
|
||||
void value;
|
||||
});
|
||||
const fooValue = {
|
||||
get: fooGetter,
|
||||
set: fooSetter,
|
||||
} as unknown as StructFieldValue<
|
||||
StructFieldDefinition<unknown, unknown, PropertyKey>
|
||||
>;
|
||||
object.set(foo, fooValue);
|
||||
|
||||
const bar = "bar";
|
||||
const barGetter = mock.fn(() => true);
|
||||
const barSetter = mock.fn((value: boolean) => {
|
||||
void value;
|
||||
});
|
||||
const barValue = {
|
||||
get: barGetter,
|
||||
set: barSetter,
|
||||
} as unknown as StructFieldValue<
|
||||
StructFieldDefinition<unknown, unknown, PropertyKey>
|
||||
>;
|
||||
object.set(bar, barValue);
|
||||
|
||||
assert.strictEqual(object.value[foo], 42);
|
||||
assert.strictEqual(fooGetter.mock.callCount(), 1);
|
||||
assert.strictEqual(barGetter.mock.callCount(), 1);
|
||||
|
||||
object.value[foo] = 100;
|
||||
assert.strictEqual(fooSetter.mock.callCount(), 0);
|
||||
assert.strictEqual(barSetter.mock.callCount(), 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#get", () => {
|
||||
it("should return previously set `StructFieldValue`", () => {
|
||||
const object = new StructValue({});
|
||||
|
||||
const foo = "foo";
|
||||
const fooValue = {
|
||||
get() {
|
||||
return "foo";
|
||||
},
|
||||
} as StructFieldValue<
|
||||
StructFieldDefinition<unknown, unknown, PropertyKey>
|
||||
>;
|
||||
object.set(foo, fooValue);
|
||||
|
||||
assert.strictEqual(object.get(foo), fooValue);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,85 +0,0 @@
|
|||
import type { StructFieldDefinition } from "./definition.js";
|
||||
import type { StructFieldValue } from "./field-value.js";
|
||||
|
||||
export const STRUCT_VALUE_SYMBOL = Symbol("struct-value");
|
||||
|
||||
export function isStructValueInit(
|
||||
value: unknown,
|
||||
): value is { [STRUCT_VALUE_SYMBOL]: StructValue } {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
STRUCT_VALUE_SYMBOL in value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A struct value is a map between keys in a struct and their field values.
|
||||
*/
|
||||
export class StructValue {
|
||||
/** @internal */ readonly fieldValues: Record<
|
||||
PropertyKey,
|
||||
StructFieldValue<StructFieldDefinition<unknown, unknown, PropertyKey>>
|
||||
> = {};
|
||||
|
||||
/**
|
||||
* Gets the result struct value object
|
||||
*/
|
||||
readonly value: Record<PropertyKey, unknown>;
|
||||
|
||||
constructor(prototype: object) {
|
||||
// PERF: `Object.create(extra)` is 50% faster
|
||||
// than `Object.defineProperties(this.value, extra)`
|
||||
this.value = Object.create(prototype) as Record<PropertyKey, unknown>;
|
||||
|
||||
// PERF: `Object.defineProperty` is slow
|
||||
// but we need it to be non-enumerable
|
||||
Object.defineProperty(this.value, STRUCT_VALUE_SYMBOL, {
|
||||
enumerable: false,
|
||||
value: this,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a `StructFieldValue` for `key`
|
||||
*
|
||||
* @param name The field name
|
||||
* @param fieldValue The associated `StructFieldValue`
|
||||
*/
|
||||
set(
|
||||
name: PropertyKey,
|
||||
fieldValue: StructFieldValue<
|
||||
StructFieldDefinition<unknown, unknown, PropertyKey>
|
||||
>,
|
||||
): void {
|
||||
this.fieldValues[name] = fieldValue;
|
||||
|
||||
// PERF: `Object.defineProperty` is slow
|
||||
// use normal property when possible
|
||||
if (fieldValue.hasCustomAccessors) {
|
||||
Object.defineProperty(this.value, name, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get() {
|
||||
return fieldValue.get();
|
||||
},
|
||||
set(v) {
|
||||
fieldValue.set(v);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.value[name] = fieldValue.get();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the `StructFieldValue` for `key`
|
||||
*
|
||||
* @param name The field name
|
||||
*/
|
||||
get(
|
||||
name: PropertyKey,
|
||||
): StructFieldValue<StructFieldDefinition<unknown, unknown, PropertyKey>> {
|
||||
return this.fieldValues[name]!;
|
||||
}
|
||||
}
|
58
libraries/struct/src/bipedal.ts
Normal file
58
libraries/struct/src/bipedal.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import type { MaybePromiseLike } from "./utils.js";
|
||||
|
||||
function isPromiseLike<T>(value: unknown): value is PromiseLike<T> {
|
||||
return typeof value === "object" && value !== null && "then" in value;
|
||||
}
|
||||
|
||||
function advance<T>(
|
||||
iterator: Iterator<unknown, T, unknown>,
|
||||
next: unknown,
|
||||
): MaybePromiseLike<T> {
|
||||
while (true) {
|
||||
const { done, value } = iterator.next(next);
|
||||
if (done) {
|
||||
return value;
|
||||
}
|
||||
if (isPromiseLike(value)) {
|
||||
return value.then(
|
||||
(value) => advance(iterator, { resolved: value }),
|
||||
(error: unknown) => advance(iterator, { error }),
|
||||
);
|
||||
}
|
||||
next = value;
|
||||
}
|
||||
}
|
||||
|
||||
export function bipedal<This, T, A extends unknown[]>(
|
||||
fn: (
|
||||
this: This,
|
||||
then: <U>(value: U | PromiseLike<U>) => Iterable<unknown, U, unknown>,
|
||||
...args: A
|
||||
) => Generator<unknown, T, unknown>,
|
||||
): { (this: This, ...args: A): MaybePromiseLike<T> } {
|
||||
return function (this: This, ...args: A) {
|
||||
const iterator = fn.call(
|
||||
this,
|
||||
function* <U>(
|
||||
value: U | PromiseLike<U>,
|
||||
): Generator<
|
||||
PromiseLike<U>,
|
||||
U,
|
||||
{ resolved: U } | { error: unknown }
|
||||
> {
|
||||
if (isPromiseLike(value)) {
|
||||
const result = yield value;
|
||||
if ("resolved" in result) {
|
||||
return result.resolved;
|
||||
} else {
|
||||
throw result.error;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
...args,
|
||||
) as never;
|
||||
return advance(iterator, undefined);
|
||||
};
|
||||
}
|
62
libraries/struct/src/buffer.spec.ts
Normal file
62
libraries/struct/src/buffer.spec.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
import * as assert from "node:assert";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import { buffer } from "./buffer.js";
|
||||
import type { ExactReadable } from "./readable.js";
|
||||
import { ExactReadableEndedError } from "./readable.js";
|
||||
import { Struct } from "./struct.js";
|
||||
|
||||
describe("buffer", () => {
|
||||
describe("fixed size", () => {
|
||||
it("should deserialize", () => {
|
||||
const A = new Struct(
|
||||
{ value: buffer(10) },
|
||||
{ littleEndian: false },
|
||||
);
|
||||
const reader: ExactReadable = {
|
||||
position: 0,
|
||||
readExactly() {
|
||||
return new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
||||
},
|
||||
};
|
||||
assert.deepStrictEqual(A.deserialize(reader), {
|
||||
value: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw for not enough data", () => {
|
||||
const A = new Struct(
|
||||
{ value: buffer(10) },
|
||||
{ littleEndian: false },
|
||||
);
|
||||
const reader: ExactReadable = {
|
||||
position: 0,
|
||||
readExactly() {
|
||||
(this as { position: number }).position = 5;
|
||||
throw new ExactReadableEndedError();
|
||||
},
|
||||
};
|
||||
assert.throws(
|
||||
() => A.deserialize(reader),
|
||||
/The underlying readable was ended before the struct was fully deserialized/,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw for no data", () => {
|
||||
const A = new Struct(
|
||||
{ value: buffer(10) },
|
||||
{ littleEndian: false },
|
||||
);
|
||||
const reader: ExactReadable = {
|
||||
position: 0,
|
||||
readExactly() {
|
||||
throw new ExactReadableEndedError();
|
||||
},
|
||||
};
|
||||
assert.throws(
|
||||
() => A.deserialize(reader),
|
||||
/The underlying readable doesn't contain any more struct/,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
229
libraries/struct/src/buffer.ts
Normal file
229
libraries/struct/src/buffer.ts
Normal file
|
@ -0,0 +1,229 @@
|
|||
import { bipedal } from "./bipedal.js";
|
||||
import type { Field } from "./field.js";
|
||||
|
||||
export interface Converter<From, To> {
|
||||
convert: (value: From) => To;
|
||||
back: (value: To) => From;
|
||||
}
|
||||
|
||||
export interface BufferLengthConverter<K, KT> extends Converter<KT, number> {
|
||||
field: K;
|
||||
}
|
||||
|
||||
export interface BufferLike {
|
||||
(length: number): Field<Uint8Array, never, never>;
|
||||
<U>(
|
||||
length: number,
|
||||
converter: Converter<Uint8Array, U>,
|
||||
): Field<U, never, never>;
|
||||
|
||||
<K extends string>(lengthField: K): Field<Uint8Array, K, Record<K, number>>;
|
||||
<K extends string, U>(
|
||||
lengthField: K,
|
||||
converter: Converter<Uint8Array, U>,
|
||||
): Field<U, K, Record<K, number>>;
|
||||
|
||||
<K extends string, KT>(
|
||||
length: BufferLengthConverter<K, KT>,
|
||||
): Field<Uint8Array, K, Record<K, KT>>;
|
||||
<K extends string, KT, U>(
|
||||
length: BufferLengthConverter<K, KT>,
|
||||
converter: Converter<Uint8Array, U>,
|
||||
): Field<U, K, Record<K, KT>>;
|
||||
|
||||
<KOmitInit extends string, KS>(
|
||||
length: Field<number, KOmitInit, KS>,
|
||||
): Field<Uint8Array, KOmitInit, KS>;
|
||||
<KOmitInit extends string, KS, U>(
|
||||
length: Field<number, KOmitInit, KS>,
|
||||
converter: Converter<Uint8Array, U>,
|
||||
): Field<U, KOmitInit, KS>;
|
||||
}
|
||||
|
||||
export const EmptyUint8Array = new Uint8Array(0);
|
||||
|
||||
export const buffer: BufferLike = ((
|
||||
lengthOrField:
|
||||
| string
|
||||
| number
|
||||
| Field<number, never, unknown>
|
||||
| BufferLengthConverter<string, unknown>,
|
||||
converter?: Converter<Uint8Array, unknown>,
|
||||
): Field<unknown, string, Record<string, unknown>> => {
|
||||
if (typeof lengthOrField === "number") {
|
||||
if (converter) {
|
||||
return {
|
||||
size: lengthOrField,
|
||||
serialize: (value, { buffer, index }) => {
|
||||
buffer.set(converter.back(value), index);
|
||||
},
|
||||
deserialize: bipedal(function* (then, { reader }) {
|
||||
const value = yield* then(
|
||||
reader.readExactly(lengthOrField),
|
||||
);
|
||||
return converter.convert(value);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (lengthOrField === 0) {
|
||||
return {
|
||||
size: 0,
|
||||
serialize: () => {},
|
||||
deserialize: () => EmptyUint8Array,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
size: lengthOrField,
|
||||
serialize: (value, { buffer, index }) => {
|
||||
buffer.set(value as Uint8Array, index);
|
||||
},
|
||||
deserialize: ({ reader }) => reader.readExactly(lengthOrField),
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof lengthOrField === "string") {
|
||||
if (converter) {
|
||||
return {
|
||||
size: 0,
|
||||
preSerialize: (value, runtimeStruct) => {
|
||||
runtimeStruct[lengthOrField] = converter.back(value).length;
|
||||
},
|
||||
serialize: (value, { buffer, index }) => {
|
||||
buffer.set(converter.back(value), index);
|
||||
},
|
||||
deserialize: bipedal(function* (
|
||||
then,
|
||||
{ reader, runtimeStruct },
|
||||
) {
|
||||
const length = runtimeStruct[lengthOrField] as number;
|
||||
if (length === 0) {
|
||||
return converter.convert(EmptyUint8Array);
|
||||
}
|
||||
|
||||
const value = yield* then(reader.readExactly(length));
|
||||
return converter.convert(value);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
size: 0,
|
||||
preSerialize: (value, runtimeStruct) => {
|
||||
runtimeStruct[lengthOrField] = (value as Uint8Array).length;
|
||||
},
|
||||
serialize: (value, { buffer, index }) => {
|
||||
buffer.set(value as Uint8Array, index);
|
||||
},
|
||||
deserialize: ({ reader, runtimeStruct }) => {
|
||||
const length = runtimeStruct[lengthOrField] as number;
|
||||
if (length === 0) {
|
||||
return EmptyUint8Array;
|
||||
}
|
||||
|
||||
return reader.readExactly(length);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if ("serialize" in lengthOrField) {
|
||||
if (converter) {
|
||||
return {
|
||||
size: 0,
|
||||
dynamicSize(value) {
|
||||
const array = converter.back(value);
|
||||
const lengthFieldSize =
|
||||
lengthOrField.dynamicSize?.(array.length) ??
|
||||
lengthOrField.size;
|
||||
return lengthFieldSize + array.length;
|
||||
},
|
||||
serialize(value, context) {
|
||||
const array = converter.back(value);
|
||||
const lengthFieldSize =
|
||||
lengthOrField.dynamicSize?.(array.length) ??
|
||||
lengthOrField.size;
|
||||
lengthOrField.serialize(array.length, context);
|
||||
context.buffer.set(array, context.index + lengthFieldSize);
|
||||
},
|
||||
deserialize: bipedal(function* (then, context) {
|
||||
const length = yield* then(
|
||||
lengthOrField.deserialize(context),
|
||||
);
|
||||
const value = yield* then(
|
||||
context.reader.readExactly(length),
|
||||
);
|
||||
return converter.convert(value);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
size: 0,
|
||||
dynamicSize(value) {
|
||||
const lengthFieldSize =
|
||||
lengthOrField.dynamicSize?.((value as Uint8Array).length) ??
|
||||
lengthOrField.size;
|
||||
return lengthFieldSize + (value as Uint8Array).length;
|
||||
},
|
||||
serialize(value, context) {
|
||||
const lengthFieldSize =
|
||||
lengthOrField.dynamicSize?.((value as Uint8Array).length) ??
|
||||
lengthOrField.size;
|
||||
lengthOrField.serialize((value as Uint8Array).length, context);
|
||||
context.buffer.set(
|
||||
value as Uint8Array,
|
||||
context.index + lengthFieldSize,
|
||||
);
|
||||
},
|
||||
deserialize: bipedal(function* (then, context) {
|
||||
const length = yield* then(lengthOrField.deserialize(context));
|
||||
return context.reader.readExactly(length);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (converter) {
|
||||
return {
|
||||
size: 0,
|
||||
preSerialize: (value, runtimeStruct) => {
|
||||
const length = converter.back(value).length;
|
||||
runtimeStruct[lengthOrField.field] = lengthOrField.back(length);
|
||||
},
|
||||
serialize: (value, { buffer, index }) => {
|
||||
buffer.set(converter.back(value), index);
|
||||
},
|
||||
deserialize: bipedal(function* (then, { reader, runtimeStruct }) {
|
||||
const rawLength = runtimeStruct[lengthOrField.field];
|
||||
const length = lengthOrField.convert(rawLength);
|
||||
if (length === 0) {
|
||||
return converter.convert(EmptyUint8Array);
|
||||
}
|
||||
|
||||
const value = yield* then(reader.readExactly(length));
|
||||
return converter.convert(value);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
size: 0,
|
||||
preSerialize: (value, runtimeStruct) => {
|
||||
runtimeStruct[lengthOrField.field] = lengthOrField.back(
|
||||
(value as Uint8Array).length,
|
||||
);
|
||||
},
|
||||
serialize: (value, { buffer, index }) => {
|
||||
buffer.set(value as Uint8Array, index);
|
||||
},
|
||||
deserialize: ({ reader, runtimeStruct }) => {
|
||||
const rawLength = runtimeStruct[lengthOrField.field];
|
||||
const length = lengthOrField.convert(rawLength);
|
||||
if (length === 0) {
|
||||
return EmptyUint8Array;
|
||||
}
|
||||
|
||||
return reader.readExactly(length);
|
||||
},
|
||||
};
|
||||
}) as never;
|
26
libraries/struct/src/field.ts
Normal file
26
libraries/struct/src/field.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import type { AsyncExactReadable } from "./readable.js";
|
||||
import type { MaybePromiseLike } from "./utils.js";
|
||||
|
||||
export interface SerializeContext {
|
||||
buffer: Uint8Array;
|
||||
index: number;
|
||||
littleEndian: boolean;
|
||||
}
|
||||
|
||||
export interface DeserializeContext<S> {
|
||||
reader: AsyncExactReadable;
|
||||
littleEndian: boolean;
|
||||
runtimeStruct: S;
|
||||
}
|
||||
|
||||
export interface Field<T, OmitInit extends string, S> {
|
||||
__invariant?: OmitInit;
|
||||
|
||||
size: number;
|
||||
|
||||
dynamicSize?(value: T): number;
|
||||
preSerialize?(value: T, runtimeStruct: S): void;
|
||||
serialize(value: T, context: SerializeContext): void;
|
||||
|
||||
deserialize(context: DeserializeContext<S>): MaybePromiseLike<T>;
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import * as assert from "node:assert";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import Struct from "./index.js";
|
||||
import { Struct } from "./index.js";
|
||||
|
||||
describe("Struct", () => {
|
||||
describe("Index", () => {
|
||||
|
|
|
@ -10,9 +10,11 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
export * from "./basic/index.js";
|
||||
export * from "./bipedal.js";
|
||||
export * from "./buffer.js";
|
||||
export * from "./field.js";
|
||||
export * from "./number.js";
|
||||
export * from "./readable.js";
|
||||
export * from "./string.js";
|
||||
export * from "./struct.js";
|
||||
export { Struct as default } from "./struct.js";
|
||||
export * from "./sync-promise.js";
|
||||
export * from "./types/index.js";
|
||||
export * from "./utils.js";
|
||||
|
|
128
libraries/struct/src/number.ts
Normal file
128
libraries/struct/src/number.ts
Normal file
|
@ -0,0 +1,128 @@
|
|||
import {
|
||||
getInt16,
|
||||
getInt32,
|
||||
getInt64,
|
||||
getInt8,
|
||||
getUint16,
|
||||
getUint32,
|
||||
setInt16,
|
||||
setInt32,
|
||||
setInt64,
|
||||
setUint16,
|
||||
setUint32,
|
||||
} from "@yume-chan/no-data-view";
|
||||
|
||||
import { bipedal } from "./bipedal.js";
|
||||
import type { Field } from "./field.js";
|
||||
|
||||
export const u8: Field<number, never, never> & {
|
||||
as: <T>(infer?: T) => Field<T, never, never>;
|
||||
} = {
|
||||
size: 1,
|
||||
serialize(value, { buffer, index }) {
|
||||
buffer[index] = value;
|
||||
},
|
||||
deserialize: bipedal(function* (then, { reader }) {
|
||||
const data = yield* then(reader.readExactly(1));
|
||||
return data[0]!;
|
||||
}),
|
||||
as: () => u8 as never,
|
||||
};
|
||||
|
||||
export const s8: Field<number, never, never> & {
|
||||
as: <T>(infer?: T) => Field<T, never, never>;
|
||||
} = {
|
||||
size: 1,
|
||||
serialize(value, { buffer, index }) {
|
||||
buffer[index] = value;
|
||||
},
|
||||
deserialize: bipedal(function* (then, { reader }) {
|
||||
const data = yield* then(reader.readExactly(1));
|
||||
return getInt8(data, 0);
|
||||
}),
|
||||
as: () => s8 as never,
|
||||
};
|
||||
|
||||
export const u16: Field<number, never, never> & {
|
||||
as: <T>(infer?: T) => Field<T, never, never>;
|
||||
} = {
|
||||
size: 2,
|
||||
serialize(value, { buffer, index, littleEndian }) {
|
||||
setUint16(buffer, index, value, littleEndian);
|
||||
},
|
||||
deserialize: bipedal(function* (then, { reader, littleEndian }) {
|
||||
const data = yield* then(reader.readExactly(2));
|
||||
return getUint16(data, 0, littleEndian);
|
||||
}),
|
||||
as: () => u16 as never,
|
||||
};
|
||||
|
||||
export const s16: Field<number, never, never> & {
|
||||
as: <T>(infer?: T) => Field<T, never, never>;
|
||||
} = {
|
||||
size: 2,
|
||||
serialize(value, { buffer, index, littleEndian }) {
|
||||
setInt16(buffer, index, value, littleEndian);
|
||||
},
|
||||
deserialize: bipedal(function* (then, { reader, littleEndian }) {
|
||||
const data = yield* then(reader.readExactly(2));
|
||||
return getInt16(data, 0, littleEndian);
|
||||
}),
|
||||
as: () => s16 as never,
|
||||
};
|
||||
|
||||
export const u32: Field<number, never, never> & {
|
||||
as: <T>(infer?: T) => Field<T, never, never>;
|
||||
} = {
|
||||
size: 4,
|
||||
serialize(value, { buffer, index, littleEndian }) {
|
||||
setUint32(buffer, index, value, littleEndian);
|
||||
},
|
||||
deserialize: bipedal(function* (then, { reader, littleEndian }) {
|
||||
const data = yield* then(reader.readExactly(4));
|
||||
return getUint32(data, 0, littleEndian);
|
||||
}),
|
||||
as: () => u32 as never,
|
||||
};
|
||||
|
||||
export const s32: Field<number, never, never> & {
|
||||
as: <T>(infer?: T) => Field<T, never, never>;
|
||||
} = {
|
||||
size: 4,
|
||||
serialize(value, { buffer, index, littleEndian }) {
|
||||
setInt32(buffer, index, value, littleEndian);
|
||||
},
|
||||
deserialize: bipedal(function* (then, { reader, littleEndian }) {
|
||||
const data = yield* then(reader.readExactly(4));
|
||||
return getInt32(data, 0, littleEndian);
|
||||
}),
|
||||
as: () => s32 as never,
|
||||
};
|
||||
|
||||
export const u64: Field<bigint, never, never> & {
|
||||
as: <T>(infer?: T) => Field<T, never, never>;
|
||||
} = {
|
||||
size: 8,
|
||||
serialize(value, { buffer, index, littleEndian }) {
|
||||
setInt64(buffer, index, value, littleEndian);
|
||||
},
|
||||
deserialize: bipedal(function* (then, { reader, littleEndian }) {
|
||||
const data = yield* then(reader.readExactly(8));
|
||||
return getInt64(data, 0, littleEndian);
|
||||
}),
|
||||
as: () => u64 as never,
|
||||
};
|
||||
|
||||
export const s64: Field<bigint, never, never> & {
|
||||
as: <T>(infer?: T) => Field<T, never, never>;
|
||||
} = {
|
||||
size: 8,
|
||||
serialize(value, { buffer, index, littleEndian }) {
|
||||
setInt64(buffer, index, value, littleEndian);
|
||||
},
|
||||
deserialize: bipedal(function* (then, { reader, littleEndian }) {
|
||||
const data = yield* then(reader.readExactly(8));
|
||||
return getInt64(data, 0, littleEndian);
|
||||
}),
|
||||
as: () => s64 as never,
|
||||
};
|
|
@ -1,7 +1,7 @@
|
|||
import type { ValueOrPromise } from "../utils.js";
|
||||
|
||||
// TODO: allow over reading (returning a `Uint8Array`, an `offset` and a `length`) to avoid copying
|
||||
|
||||
import type { MaybePromiseLike } from "./utils.js";
|
||||
|
||||
export class ExactReadableEndedError extends Error {
|
||||
constructor() {
|
||||
super("ExactReadable ended");
|
||||
|
@ -30,5 +30,5 @@ export interface AsyncExactReadable {
|
|||
* The stream must return exactly `length` bytes or data. If that's not possible
|
||||
* (due to end of file or other error condition), it must throw an {@link ExactReadableEndedError}.
|
||||
*/
|
||||
readExactly(length: number): ValueOrPromise<Uint8Array>;
|
||||
readExactly(length: number): MaybePromiseLike<Uint8Array>;
|
||||
}
|
39
libraries/struct/src/string.ts
Normal file
39
libraries/struct/src/string.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import type { BufferLengthConverter } from "./buffer.js";
|
||||
import { buffer } from "./buffer.js";
|
||||
import type { Field } from "./field.js";
|
||||
import { decodeUtf8, encodeUtf8 } from "./utils.js";
|
||||
|
||||
export interface String {
|
||||
(length: number): Field<string, never, never> & {
|
||||
as: <T>(infer: T) => Field<T, never, never>;
|
||||
};
|
||||
|
||||
<K extends string>(
|
||||
lengthField: K,
|
||||
): Field<string, K, Record<K, number>> & {
|
||||
as: <T>(infer: T) => Field<T, K, Record<K, number>>;
|
||||
};
|
||||
|
||||
<const K extends string, KT>(
|
||||
length: BufferLengthConverter<K, KT>,
|
||||
): Field<string, K, Record<K, KT>> & {
|
||||
as: <T>(infer: T) => Field<T, K, Record<K, KT>>;
|
||||
};
|
||||
|
||||
<KOmitInit extends string, KS>(
|
||||
length: Field<number, KOmitInit, KS>,
|
||||
): Field<string, KOmitInit, KS>;
|
||||
}
|
||||
|
||||
export const string: String = ((
|
||||
lengthOrField: string | number | BufferLengthConverter<string, unknown>,
|
||||
): Field<string, string, Record<string, unknown>> & {
|
||||
as: <T>(infer: T) => Field<T, string, Record<string, unknown>>;
|
||||
} => {
|
||||
const field = buffer(lengthOrField as never, {
|
||||
convert: decodeUtf8,
|
||||
back: encodeUtf8,
|
||||
});
|
||||
(field as never as { as: unknown }).as = () => field;
|
||||
return field as never;
|
||||
}) as never;
|
|
@ -1,538 +1,12 @@
|
|||
import * as assert from "node:assert";
|
||||
import { describe, it, mock } from "node:test";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import type {
|
||||
AsyncExactReadable,
|
||||
ExactReadable,
|
||||
StructFieldValue,
|
||||
StructOptions,
|
||||
StructValue,
|
||||
} from "./basic/index.js";
|
||||
import { StructDefaultOptions, StructFieldDefinition } from "./basic/index.js";
|
||||
import { u8 } from "./number.js";
|
||||
import { Struct } from "./struct.js";
|
||||
import type { ValueOrPromise } from "./utils.js";
|
||||
|
||||
import {
|
||||
BigIntFieldDefinition,
|
||||
BigIntFieldVariant,
|
||||
BufferFieldConverter,
|
||||
FixedLengthBufferLikeFieldDefinition,
|
||||
NumberFieldDefinition,
|
||||
NumberFieldVariant,
|
||||
VariableLengthBufferLikeFieldDefinition,
|
||||
} from "./index.js";
|
||||
|
||||
class MockDeserializationStream implements ExactReadable {
|
||||
buffer = new Uint8Array(0);
|
||||
|
||||
position = 0;
|
||||
|
||||
readExactly = mock.fn(() => this.buffer);
|
||||
}
|
||||
|
||||
describe("Struct", () => {
|
||||
describe(".constructor", () => {
|
||||
it("should initialize fields", () => {
|
||||
const struct = /* #__PURE__ */ new Struct();
|
||||
assert.deepStrictEqual(struct.options, StructDefaultOptions);
|
||||
assert.strictEqual(struct.size, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#field", () => {
|
||||
class MockFieldDefinition extends StructFieldDefinition<number> {
|
||||
constructor(size: number) {
|
||||
super(size);
|
||||
}
|
||||
|
||||
getSize = mock.fn(() => {
|
||||
return this.options;
|
||||
});
|
||||
|
||||
override create(
|
||||
options: Readonly<StructOptions>,
|
||||
struct: StructValue,
|
||||
value: unknown,
|
||||
): StructFieldValue<this> {
|
||||
void options;
|
||||
void struct;
|
||||
void value;
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
override deserialize(
|
||||
options: Readonly<StructOptions>,
|
||||
stream: ExactReadable,
|
||||
struct: StructValue,
|
||||
): StructFieldValue<this>;
|
||||
override deserialize(
|
||||
options: Readonly<StructOptions>,
|
||||
stream: AsyncExactReadable,
|
||||
struct: StructValue,
|
||||
): Promise<StructFieldValue<this>>;
|
||||
override deserialize(
|
||||
options: Readonly<StructOptions>,
|
||||
stream: ExactReadable | AsyncExactReadable,
|
||||
struct: StructValue,
|
||||
): ValueOrPromise<StructFieldValue<this>> {
|
||||
void options;
|
||||
void stream;
|
||||
void struct;
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
it("should push a field and update size", () => {
|
||||
const struct = /* #__PURE__ */ new Struct();
|
||||
|
||||
const field1 = "foo";
|
||||
const fieldDefinition1 = new MockFieldDefinition(4);
|
||||
|
||||
struct.field(field1, fieldDefinition1);
|
||||
assert.strictEqual(struct.size, 4);
|
||||
assert.strictEqual(fieldDefinition1.getSize.mock.callCount(), 1);
|
||||
assert.deepStrictEqual(struct.fields, [[field1, fieldDefinition1]]);
|
||||
|
||||
const field2 = "bar";
|
||||
const fieldDefinition2 = new MockFieldDefinition(8);
|
||||
struct.field(field2, fieldDefinition2);
|
||||
assert.strictEqual(struct.size, 12);
|
||||
assert.strictEqual(fieldDefinition2.getSize.mock.callCount(), 1);
|
||||
assert.deepStrictEqual(struct.fields, [
|
||||
[field1, fieldDefinition1],
|
||||
[field2, fieldDefinition2],
|
||||
]);
|
||||
});
|
||||
|
||||
it("should throw an error if field name already exists", () => {
|
||||
const struct = /* #__PURE__ */ new Struct();
|
||||
const fieldName = "foo";
|
||||
struct.field(fieldName, new MockFieldDefinition(4));
|
||||
assert.throws(() => {
|
||||
struct.field(fieldName, new MockFieldDefinition(4));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#number", () => {
|
||||
it("`int8` should append an `int8` field", () => {
|
||||
const struct = /* #__PURE__ */ new Struct();
|
||||
struct.int8("foo");
|
||||
assert.strictEqual(struct.size, 1);
|
||||
|
||||
const definition = struct.fields[0]![1] as NumberFieldDefinition;
|
||||
assert.ok(definition instanceof NumberFieldDefinition);
|
||||
assert.strictEqual(definition.variant, NumberFieldVariant.Int8);
|
||||
});
|
||||
|
||||
it("`uint8` should append an `uint8` field", () => {
|
||||
const struct = /* #__PURE__ */ new Struct();
|
||||
struct.uint8("foo");
|
||||
assert.strictEqual(struct.size, 1);
|
||||
|
||||
const definition = struct.fields[0]![1] as NumberFieldDefinition;
|
||||
assert.ok(definition instanceof NumberFieldDefinition);
|
||||
assert.strictEqual(definition.variant, NumberFieldVariant.Uint8);
|
||||
});
|
||||
|
||||
it("`int16` should append an `int16` field", () => {
|
||||
const struct = /* #__PURE__ */ new Struct();
|
||||
struct.int16("foo");
|
||||
assert.strictEqual(struct.size, 2);
|
||||
|
||||
const definition = struct.fields[0]![1] as NumberFieldDefinition;
|
||||
assert.ok(definition instanceof NumberFieldDefinition);
|
||||
assert.strictEqual(definition.variant, NumberFieldVariant.Int16);
|
||||
});
|
||||
|
||||
it("`uint16` should append an `uint16` field", () => {
|
||||
const struct = /* #__PURE__ */ new Struct();
|
||||
struct.uint16("foo");
|
||||
assert.strictEqual(struct.size, 2);
|
||||
|
||||
const definition = struct.fields[0]![1] as NumberFieldDefinition;
|
||||
assert.ok(definition instanceof NumberFieldDefinition);
|
||||
assert.strictEqual(definition.variant, NumberFieldVariant.Uint16);
|
||||
});
|
||||
|
||||
it("`int32` should append an `int32` field", () => {
|
||||
const struct = /* #__PURE__ */ new Struct();
|
||||
struct.int32("foo");
|
||||
assert.strictEqual(struct.size, 4);
|
||||
|
||||
const definition = struct.fields[0]![1] as NumberFieldDefinition;
|
||||
assert.ok(definition instanceof NumberFieldDefinition);
|
||||
assert.strictEqual(definition.variant, NumberFieldVariant.Int32);
|
||||
});
|
||||
|
||||
it("`uint32` should append an `uint32` field", () => {
|
||||
const struct = /* #__PURE__ */ new Struct();
|
||||
struct.uint32("foo");
|
||||
assert.strictEqual(struct.size, 4);
|
||||
|
||||
const definition = struct.fields[0]![1] as NumberFieldDefinition;
|
||||
assert.ok(definition instanceof NumberFieldDefinition);
|
||||
assert.strictEqual(definition.variant, NumberFieldVariant.Uint32);
|
||||
});
|
||||
|
||||
it("`int64` should append an `int64` field", () => {
|
||||
const struct = /* #__PURE__ */ new Struct();
|
||||
struct.int64("foo");
|
||||
assert.strictEqual(struct.size, 8);
|
||||
|
||||
const definition = struct.fields[0]![1] as BigIntFieldDefinition;
|
||||
assert.ok(definition instanceof BigIntFieldDefinition);
|
||||
assert.strictEqual(definition.variant, BigIntFieldVariant.Int64);
|
||||
});
|
||||
|
||||
it("`uint64` should append an `uint64` field", () => {
|
||||
const struct = /* #__PURE__ */ new Struct();
|
||||
struct.uint64("foo");
|
||||
assert.strictEqual(struct.size, 8);
|
||||
|
||||
const definition = struct.fields[0]![1] as BigIntFieldDefinition;
|
||||
assert.ok(definition instanceof BigIntFieldDefinition);
|
||||
assert.strictEqual(definition.variant, BigIntFieldVariant.Uint64);
|
||||
});
|
||||
|
||||
describe("#uint8ArrayLike", () => {
|
||||
describe("FixedLengthBufferLikeFieldDefinition", () => {
|
||||
it("`#uint8Array` with fixed length", () => {
|
||||
const struct = /* #__PURE__ */ new Struct();
|
||||
struct.uint8Array("foo", { length: 10 });
|
||||
assert.strictEqual(struct.size, 10);
|
||||
|
||||
const definition = struct
|
||||
.fields[0]![1] as FixedLengthBufferLikeFieldDefinition;
|
||||
assert.ok(
|
||||
definition instanceof
|
||||
FixedLengthBufferLikeFieldDefinition,
|
||||
);
|
||||
assert.ok(
|
||||
definition.converter instanceof BufferFieldConverter,
|
||||
);
|
||||
assert.strictEqual(definition.options.length, 10);
|
||||
});
|
||||
|
||||
it("`#string` with fixed length", () => {
|
||||
const struct = /* #__PURE__ */ new Struct();
|
||||
struct.string("foo", { length: 10 });
|
||||
assert.strictEqual(struct.size, 10);
|
||||
|
||||
const definition = struct
|
||||
.fields[0]![1] as FixedLengthBufferLikeFieldDefinition;
|
||||
assert.ok(
|
||||
definition instanceof
|
||||
FixedLengthBufferLikeFieldDefinition,
|
||||
);
|
||||
assert.ok(
|
||||
definition.converter instanceof BufferFieldConverter,
|
||||
);
|
||||
assert.strictEqual(definition.options.length, 10);
|
||||
});
|
||||
});
|
||||
|
||||
describe("VariableLengthBufferLikeFieldDefinition", () => {
|
||||
it("`#uint8Array` with variable length", () => {
|
||||
const struct = /* #__PURE__ */ new Struct().int8(
|
||||
"barLength",
|
||||
);
|
||||
assert.strictEqual(struct.size, 1);
|
||||
|
||||
struct.uint8Array("bar", { lengthField: "barLength" });
|
||||
assert.strictEqual(struct.size, 1);
|
||||
|
||||
const definition = struct
|
||||
.fields[1]![1] as VariableLengthBufferLikeFieldDefinition;
|
||||
assert.ok(
|
||||
definition instanceof
|
||||
VariableLengthBufferLikeFieldDefinition,
|
||||
);
|
||||
assert.ok(
|
||||
definition.converter instanceof BufferFieldConverter,
|
||||
);
|
||||
assert.strictEqual(
|
||||
definition.options.lengthField,
|
||||
"barLength",
|
||||
);
|
||||
});
|
||||
|
||||
it("`#string` with variable length", () => {
|
||||
const struct = /* #__PURE__ */ new Struct().int8(
|
||||
"barLength",
|
||||
);
|
||||
assert.strictEqual(struct.size, 1);
|
||||
|
||||
struct.string("bar", { lengthField: "barLength" });
|
||||
assert.strictEqual(struct.size, 1);
|
||||
|
||||
const definition = struct
|
||||
.fields[1]![1] as VariableLengthBufferLikeFieldDefinition;
|
||||
assert.ok(
|
||||
definition instanceof
|
||||
VariableLengthBufferLikeFieldDefinition,
|
||||
);
|
||||
assert.ok(
|
||||
definition.converter instanceof BufferFieldConverter,
|
||||
);
|
||||
assert.strictEqual(
|
||||
definition.options.lengthField,
|
||||
"barLength",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#concat", () => {
|
||||
it("should append all fields from other struct", () => {
|
||||
const sub = /* #__PURE__ */ new Struct()
|
||||
.int16("int16")
|
||||
.int32("int32");
|
||||
|
||||
const struct = /* #__PURE__ */ new Struct()
|
||||
.int8("int8")
|
||||
.concat(sub)
|
||||
.int64("int64");
|
||||
|
||||
const field0 = struct.fields[0]!;
|
||||
assert.strictEqual(field0[0], "int8");
|
||||
assert.strictEqual(
|
||||
(field0[1] as NumberFieldDefinition).variant,
|
||||
NumberFieldVariant.Int8,
|
||||
);
|
||||
|
||||
const field1 = struct.fields[1]!;
|
||||
assert.strictEqual(field1[0], "int16");
|
||||
assert.strictEqual(
|
||||
(field1[1] as NumberFieldDefinition).variant,
|
||||
NumberFieldVariant.Int16,
|
||||
);
|
||||
|
||||
const field2 = struct.fields[2]!;
|
||||
assert.strictEqual(field2[0], "int32");
|
||||
assert.strictEqual(
|
||||
(field2[1] as NumberFieldDefinition).variant,
|
||||
NumberFieldVariant.Int32,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#deserialize", () => {
|
||||
it("should deserialize without dynamic size fields", () => {
|
||||
const struct = /* #__PURE__ */ new Struct()
|
||||
.int8("foo")
|
||||
.int16("bar");
|
||||
|
||||
const stream = new MockDeserializationStream();
|
||||
stream.readExactly.mock.mockImplementationOnce(
|
||||
() => new Uint8Array([2]),
|
||||
0,
|
||||
);
|
||||
stream.readExactly.mock.mockImplementationOnce(
|
||||
() => new Uint8Array([0, 16]),
|
||||
1,
|
||||
);
|
||||
|
||||
const result = struct.deserialize(stream);
|
||||
assert.deepEqual(result, { foo: 2, bar: 16 });
|
||||
|
||||
assert.strictEqual(stream.readExactly.mock.callCount(), 2);
|
||||
assert.deepStrictEqual(
|
||||
stream.readExactly.mock.calls[0]!.arguments,
|
||||
[1],
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
stream.readExactly.mock.calls[1]!.arguments,
|
||||
[2],
|
||||
);
|
||||
});
|
||||
|
||||
it("should deserialize with dynamic size fields", () => {
|
||||
const struct = /* #__PURE__ */ new Struct()
|
||||
.int8("fooLength")
|
||||
.uint8Array("foo", { lengthField: "fooLength" });
|
||||
|
||||
const stream = new MockDeserializationStream();
|
||||
stream.readExactly.mock.mockImplementationOnce(
|
||||
() => new Uint8Array([2]),
|
||||
0,
|
||||
);
|
||||
stream.readExactly.mock.mockImplementationOnce(
|
||||
() => new Uint8Array([3, 4]),
|
||||
1,
|
||||
);
|
||||
|
||||
const result = struct.deserialize(stream);
|
||||
assert.deepEqual(result, {
|
||||
get fooLength() {
|
||||
return 2;
|
||||
},
|
||||
get foo() {
|
||||
return new Uint8Array([3, 4]);
|
||||
},
|
||||
});
|
||||
assert.strictEqual(stream.readExactly.mock.callCount(), 2);
|
||||
assert.deepStrictEqual(
|
||||
stream.readExactly.mock.calls[0]!.arguments,
|
||||
[1],
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
stream.readExactly.mock.calls[1]!.arguments,
|
||||
[2],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#extra", () => {
|
||||
it("should accept plain field", () => {
|
||||
const struct = /* #__PURE__ */ new Struct().extra({
|
||||
foo: 42,
|
||||
bar: true,
|
||||
});
|
||||
|
||||
const stream = new MockDeserializationStream();
|
||||
const result = struct.deserialize(stream);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
Object.entries(
|
||||
Object.getOwnPropertyDescriptors(
|
||||
Object.getPrototypeOf(result),
|
||||
),
|
||||
),
|
||||
[
|
||||
[
|
||||
"foo",
|
||||
{
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: 42,
|
||||
},
|
||||
],
|
||||
[
|
||||
"bar",
|
||||
{
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
it("should accept accessors", () => {
|
||||
const struct = /* #__PURE__ */ new Struct().extra({
|
||||
get foo() {
|
||||
return 42;
|
||||
},
|
||||
get bar() {
|
||||
return true;
|
||||
},
|
||||
set bar(value) {
|
||||
void value;
|
||||
},
|
||||
});
|
||||
|
||||
const stream = new MockDeserializationStream();
|
||||
const result = struct.deserialize(stream);
|
||||
|
||||
const properties = Object.getOwnPropertyDescriptors(
|
||||
Object.getPrototypeOf(result),
|
||||
);
|
||||
assert.strictEqual(properties.foo?.configurable, true);
|
||||
assert.strictEqual(properties.foo?.enumerable, true);
|
||||
assert.strictEqual(properties.bar?.configurable, true);
|
||||
assert.strictEqual(properties.bar?.enumerable, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#postDeserialize", () => {
|
||||
it("can throw errors", () => {
|
||||
const struct = /* #__PURE__ */ new Struct();
|
||||
const callback = mock.fn(() => {
|
||||
throw new Error("mock");
|
||||
});
|
||||
struct.postDeserialize(callback);
|
||||
|
||||
const stream = new MockDeserializationStream();
|
||||
assert.throws(() => struct.deserialize(stream), /mock/);
|
||||
assert.strictEqual(callback.mock.callCount(), 1);
|
||||
});
|
||||
|
||||
it("can replace return value", () => {
|
||||
const struct = /* #__PURE__ */ new Struct();
|
||||
const callback = mock.fn(() => "mock");
|
||||
struct.postDeserialize(callback);
|
||||
|
||||
const stream = new MockDeserializationStream();
|
||||
assert.strictEqual(struct.deserialize(stream), "mock");
|
||||
assert.strictEqual(callback.mock.callCount(), 1);
|
||||
assert.deepEqual(callback.mock.calls[0]?.arguments, [{}]);
|
||||
});
|
||||
|
||||
it("can return nothing", () => {
|
||||
const struct = /* #__PURE__ */ new Struct();
|
||||
const callback = mock.fn();
|
||||
struct.postDeserialize(callback);
|
||||
|
||||
const stream = new MockDeserializationStream();
|
||||
const result = struct.deserialize(stream);
|
||||
|
||||
assert.strictEqual(callback.mock.callCount(), 1);
|
||||
assert.deepEqual(callback.mock.calls[0]?.arguments, [result]);
|
||||
});
|
||||
|
||||
it("should overwrite callback", () => {
|
||||
const struct = /* #__PURE__ */ new Struct();
|
||||
|
||||
const callback1 = mock.fn();
|
||||
struct.postDeserialize(callback1);
|
||||
|
||||
const callback2 = mock.fn();
|
||||
struct.postDeserialize(callback2);
|
||||
|
||||
const stream = new MockDeserializationStream();
|
||||
struct.deserialize(stream);
|
||||
|
||||
assert.strictEqual(callback1.mock.callCount(), 0);
|
||||
assert.strictEqual(callback2.mock.callCount(), 1);
|
||||
assert.deepEqual(callback2.mock.calls[0]?.arguments, [{}]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#serialize", () => {
|
||||
it("should serialize without dynamic size fields", () => {
|
||||
const struct = /* #__PURE__ */ new Struct()
|
||||
.int8("foo")
|
||||
.int16("bar");
|
||||
|
||||
const result = new Uint8Array(
|
||||
struct.serialize({ foo: 0x42, bar: 0x1024 }),
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
result,
|
||||
new Uint8Array([0x42, 0x10, 0x24]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should serialize with dynamic size fields", () => {
|
||||
const struct = /* #__PURE__ */ new Struct()
|
||||
.int8("fooLength")
|
||||
.uint8Array("foo", { lengthField: "fooLength" });
|
||||
|
||||
const result = new Uint8Array(
|
||||
struct.serialize({
|
||||
foo: new Uint8Array([0x03, 0x04, 0x05]),
|
||||
}),
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
result,
|
||||
new Uint8Array([0x03, 0x03, 0x04, 0x05]),
|
||||
);
|
||||
});
|
||||
});
|
||||
it("serialize", () => {
|
||||
const A = new Struct({ id: u8 }, { littleEndian: true });
|
||||
assert.deepStrictEqual(A.serialize({ id: 10 }), new Uint8Array([10]));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,197 +1,35 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { bipedal } from "./bipedal.js";
|
||||
import type { DeserializeContext, Field, SerializeContext } from "./field.js";
|
||||
import type { AsyncExactReadable, ExactReadable } from "./readable.js";
|
||||
import { ExactReadableEndedError } from "./readable.js";
|
||||
import type { MaybePromiseLike } from "./utils.js";
|
||||
|
||||
import type {
|
||||
AsyncExactReadable,
|
||||
ExactReadable,
|
||||
StructFieldDefinition,
|
||||
StructFieldValue,
|
||||
StructOptions,
|
||||
} from "./basic/index.js";
|
||||
import {
|
||||
ExactReadableEndedError,
|
||||
STRUCT_VALUE_SYMBOL,
|
||||
StructDefaultOptions,
|
||||
StructValue,
|
||||
isStructValueInit,
|
||||
} from "./basic/index.js";
|
||||
import { SyncPromise } from "./sync-promise.js";
|
||||
import type {
|
||||
BufferFieldConverter,
|
||||
FixedLengthBufferLikeFieldOptions,
|
||||
LengthField,
|
||||
VariableLengthBufferLikeFieldOptions,
|
||||
} from "./types/index.js";
|
||||
import {
|
||||
BigIntFieldDefinition,
|
||||
BigIntFieldVariant,
|
||||
FixedLengthBufferLikeFieldDefinition,
|
||||
NumberFieldDefinition,
|
||||
NumberFieldVariant,
|
||||
StringBufferFieldConverter,
|
||||
Uint8ArrayBufferFieldConverter,
|
||||
VariableLengthBufferLikeFieldDefinition,
|
||||
} from "./types/index.js";
|
||||
import type { Evaluate, Identity, Overwrite, ValueOrPromise } from "./utils.js";
|
||||
export type FieldsType<
|
||||
T extends Record<string, Field<unknown, string, unknown>>,
|
||||
> = {
|
||||
[K in keyof T]: T[K] extends Field<infer TK, string, unknown> ? TK : never;
|
||||
};
|
||||
|
||||
export interface StructLike<TValue> {
|
||||
deserialize(stream: ExactReadable | AsyncExactReadable): Promise<TValue>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the value type of the specified `Struct`
|
||||
*/
|
||||
export type StructValueType<T extends StructLike<unknown>> = Awaited<
|
||||
ReturnType<T["deserialize"]>
|
||||
export type StructInit<
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
T extends Struct<any, any, any>,
|
||||
> = Omit<
|
||||
FieldsType<T["fields"]>,
|
||||
{
|
||||
[K in keyof T["fields"]]: T["fields"][K] extends Field<
|
||||
unknown,
|
||||
infer U,
|
||||
unknown
|
||||
>
|
||||
? U
|
||||
: never;
|
||||
}[keyof T["fields"]]
|
||||
>;
|
||||
|
||||
/**
|
||||
* Create a new `Struct` type with `TDefinition` appended
|
||||
*/
|
||||
type AddFieldDescriptor<
|
||||
TFields extends object,
|
||||
TOmitInitKey extends PropertyKey,
|
||||
TExtra extends object,
|
||||
TPostDeserialized,
|
||||
TFieldName extends PropertyKey,
|
||||
TDefinition extends StructFieldDefinition<unknown, unknown, PropertyKey>,
|
||||
> = Identity<
|
||||
Struct<
|
||||
// Merge two types
|
||||
// Evaluate immediately to optimize editor hover tooltip
|
||||
Evaluate<TFields & Record<TFieldName, TDefinition["TValue"]>>,
|
||||
// Merge two `TOmitInitKey`s
|
||||
TOmitInitKey | TDefinition["TOmitInitKey"],
|
||||
TExtra,
|
||||
TPostDeserialized
|
||||
>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Overload methods to add an array buffer like field
|
||||
*/
|
||||
interface ArrayBufferLikeFieldCreator<
|
||||
TFields extends object,
|
||||
TOmitInitKey extends PropertyKey,
|
||||
TExtra extends object,
|
||||
TPostDeserialized,
|
||||
> {
|
||||
/**
|
||||
* Append a fixed-length array buffer like field to the `Struct`
|
||||
*
|
||||
* @param name Name of the field
|
||||
* @param type `Array.SubType.ArrayBuffer` or `Array.SubType.String`
|
||||
* @param options Fixed-length array options
|
||||
* @param typeScriptType Type of the field in TypeScript.
|
||||
* For example, if this field is a string, you can declare it as a string enum or literal union.
|
||||
*/
|
||||
<
|
||||
TName extends PropertyKey,
|
||||
TType extends BufferFieldConverter<unknown, unknown>,
|
||||
TTypeScriptType = TType["TTypeScriptType"],
|
||||
>(
|
||||
name: TName,
|
||||
type: TType,
|
||||
options: FixedLengthBufferLikeFieldOptions,
|
||||
typeScriptType?: TTypeScriptType,
|
||||
): AddFieldDescriptor<
|
||||
TFields,
|
||||
TOmitInitKey,
|
||||
TExtra,
|
||||
TPostDeserialized,
|
||||
TName,
|
||||
FixedLengthBufferLikeFieldDefinition<
|
||||
TType,
|
||||
FixedLengthBufferLikeFieldOptions
|
||||
>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Append a variable-length array buffer like field to the `Struct`
|
||||
*/
|
||||
<
|
||||
TName extends PropertyKey,
|
||||
TType extends BufferFieldConverter<unknown, unknown>,
|
||||
TOptions extends VariableLengthBufferLikeFieldOptions<TFields>,
|
||||
TTypeScriptType = TType["TTypeScriptType"],
|
||||
>(
|
||||
name: TName,
|
||||
type: TType,
|
||||
options: TOptions,
|
||||
typeScriptType?: TTypeScriptType,
|
||||
): AddFieldDescriptor<
|
||||
TFields,
|
||||
TOmitInitKey,
|
||||
TExtra,
|
||||
TPostDeserialized,
|
||||
TName,
|
||||
VariableLengthBufferLikeFieldDefinition<TType, TOptions>
|
||||
>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to `ArrayBufferLikeFieldCreator`, but bind to `TType`
|
||||
*/
|
||||
interface BoundArrayBufferLikeFieldDefinitionCreator<
|
||||
TFields extends object,
|
||||
TOmitInitKey extends PropertyKey,
|
||||
TExtra extends object,
|
||||
TPostDeserialized,
|
||||
TType extends BufferFieldConverter<unknown, unknown>,
|
||||
> {
|
||||
<TName extends PropertyKey, TTypeScriptType = TType["TTypeScriptType"]>(
|
||||
name: TName,
|
||||
options: FixedLengthBufferLikeFieldOptions,
|
||||
typeScriptType?: TTypeScriptType,
|
||||
): AddFieldDescriptor<
|
||||
TFields,
|
||||
TOmitInitKey,
|
||||
TExtra,
|
||||
TPostDeserialized,
|
||||
TName,
|
||||
FixedLengthBufferLikeFieldDefinition<
|
||||
TType,
|
||||
FixedLengthBufferLikeFieldOptions,
|
||||
TTypeScriptType
|
||||
>
|
||||
>;
|
||||
|
||||
<
|
||||
TName extends PropertyKey,
|
||||
TOptions extends VariableLengthBufferLikeFieldOptions<
|
||||
TFields,
|
||||
LengthField<TFields>
|
||||
>,
|
||||
TTypeScriptType = TType["TTypeScriptType"],
|
||||
>(
|
||||
name: TName,
|
||||
options: TOptions,
|
||||
typeScriptType?: TTypeScriptType,
|
||||
): AddFieldDescriptor<
|
||||
TFields,
|
||||
TOmitInitKey,
|
||||
TExtra,
|
||||
TPostDeserialized,
|
||||
TName,
|
||||
VariableLengthBufferLikeFieldDefinition<
|
||||
TType,
|
||||
TOptions,
|
||||
TTypeScriptType
|
||||
>
|
||||
>;
|
||||
}
|
||||
|
||||
export type StructPostDeserialized<TFields, TPostDeserialized> = (
|
||||
this: TFields,
|
||||
object: TFields,
|
||||
) => TPostDeserialized;
|
||||
|
||||
export type StructDeserializedResult<
|
||||
TFields extends object,
|
||||
TExtra extends object,
|
||||
TPostDeserialized,
|
||||
> = TPostDeserialized extends undefined
|
||||
? Overwrite<TExtra, TFields>
|
||||
: TPostDeserialized;
|
||||
export type StructValue<
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
T extends Struct<any, any, any>,
|
||||
> = ReturnType<Exclude<T["postDeserialize"], undefined>>;
|
||||
|
||||
export class StructDeserializeError extends Error {
|
||||
constructor(message: string) {
|
||||
|
@ -214,492 +52,147 @@ export class StructEmptyError extends StructDeserializeError {
|
|||
}
|
||||
}
|
||||
|
||||
interface StructDefinition<
|
||||
TFields extends object,
|
||||
TOmitInitKey extends PropertyKey,
|
||||
TExtra extends object,
|
||||
> {
|
||||
readonly TFields: TFields;
|
||||
|
||||
readonly TOmitInitKey: TOmitInitKey;
|
||||
|
||||
readonly TExtra: TExtra;
|
||||
|
||||
readonly TInit: Evaluate<Omit<TFields, TOmitInitKey>>;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type StructLike<T> = Struct<any, any, T>;
|
||||
|
||||
export class Struct<
|
||||
TFields extends object = Record<never, never>,
|
||||
TOmitInitKey extends PropertyKey = never,
|
||||
TExtra extends object = Record<never, never>,
|
||||
TPostDeserialized = undefined,
|
||||
> implements
|
||||
StructLike<
|
||||
StructDeserializedResult<TFields, TExtra, TPostDeserialized>
|
||||
>
|
||||
{
|
||||
readonly TFields!: TFields;
|
||||
T extends Record<string, Field<unknown, string, Partial<FieldsType<T>>>>,
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
Extra extends Record<PropertyKey, unknown> = {},
|
||||
PostDeserialize = FieldsType<T> & Extra,
|
||||
> {
|
||||
fields: T;
|
||||
size: number;
|
||||
|
||||
readonly TOmitInitKey!: TOmitInitKey;
|
||||
#fieldList: [string, Field<unknown, string, unknown>][] = [];
|
||||
|
||||
readonly TExtra!: TExtra;
|
||||
littleEndian: boolean;
|
||||
|
||||
readonly TInit!: Evaluate<Omit<TFields, TOmitInitKey>>;
|
||||
extra: Extra;
|
||||
|
||||
readonly TDeserializeResult!: StructDeserializedResult<
|
||||
TFields,
|
||||
TExtra,
|
||||
TPostDeserialized
|
||||
>;
|
||||
postDeserialize?:
|
||||
| ((fields: FieldsType<T> & Extra) => PostDeserialize)
|
||||
| undefined;
|
||||
|
||||
readonly options: Readonly<StructOptions>;
|
||||
|
||||
#size = 0;
|
||||
/**
|
||||
* Gets the static size (exclude fields that can change size at runtime)
|
||||
*/
|
||||
get size() {
|
||||
return this.#size;
|
||||
}
|
||||
|
||||
#fields: [
|
||||
name: PropertyKey,
|
||||
definition: StructFieldDefinition<unknown, unknown, PropertyKey>,
|
||||
][] = [];
|
||||
get fields(): readonly [
|
||||
name: PropertyKey,
|
||||
definition: StructFieldDefinition<unknown, unknown, PropertyKey>,
|
||||
][] {
|
||||
return this.#fields;
|
||||
}
|
||||
|
||||
#extra: Record<PropertyKey, unknown> = {};
|
||||
|
||||
#postDeserialized?: StructPostDeserialized<never, unknown> | undefined;
|
||||
|
||||
constructor(options?: Partial<Readonly<StructOptions>>) {
|
||||
this.options = { ...StructDefaultOptions, ...options };
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a `StructFieldDefinition` to the `Struct
|
||||
*/
|
||||
field<
|
||||
TName extends PropertyKey,
|
||||
TDefinition extends StructFieldDefinition<
|
||||
unknown,
|
||||
unknown,
|
||||
PropertyKey
|
||||
>,
|
||||
>(
|
||||
name: TName,
|
||||
definition: TDefinition,
|
||||
): AddFieldDescriptor<
|
||||
TFields,
|
||||
TOmitInitKey,
|
||||
TExtra,
|
||||
TPostDeserialized,
|
||||
TName,
|
||||
TDefinition
|
||||
> {
|
||||
for (const field of this.#fields) {
|
||||
if (field[0] === name) {
|
||||
// Convert Symbol to string
|
||||
const nameString = String(name);
|
||||
throw new Error(
|
||||
`This struct already have a field with name '${nameString}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.#fields.push([name, definition]);
|
||||
|
||||
const size = definition.getSize();
|
||||
this.#size += size;
|
||||
|
||||
// Force cast `this` to another type
|
||||
return this as never;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges (flats) another `Struct`'s fields and extra fields into this one.
|
||||
*
|
||||
* `other`'s `postDeserialize` will be ignored.
|
||||
*/
|
||||
concat<TOther extends StructDefinition<object, PropertyKey, object>>(
|
||||
other: TOther,
|
||||
): Struct<
|
||||
TFields & TOther["TFields"],
|
||||
TOmitInitKey | TOther["TOmitInitKey"],
|
||||
TExtra & TOther["TExtra"],
|
||||
TPostDeserialized
|
||||
> {
|
||||
if (!(other instanceof Struct)) {
|
||||
throw new TypeError("The other value must be a `Struct` instance");
|
||||
}
|
||||
|
||||
for (const field of other.#fields) {
|
||||
this.#fields.push(field);
|
||||
}
|
||||
this.#size += other.#size;
|
||||
Object.defineProperties(
|
||||
this.#extra,
|
||||
Object.getOwnPropertyDescriptors(other.#extra),
|
||||
constructor(
|
||||
fields: T,
|
||||
options: {
|
||||
littleEndian?: boolean;
|
||||
extra?: Extra & ThisType<FieldsType<T>>;
|
||||
postDeserialize?: (
|
||||
this: FieldsType<T> & Extra,
|
||||
fields: FieldsType<T> & Extra,
|
||||
) => PostDeserialize;
|
||||
},
|
||||
) {
|
||||
this.#fieldList = Object.entries(fields);
|
||||
this.fields = fields;
|
||||
this.size = this.#fieldList.reduce(
|
||||
(sum, [, field]) => sum + field.size,
|
||||
0,
|
||||
);
|
||||
return this as never;
|
||||
|
||||
this.littleEndian = !!options.littleEndian;
|
||||
this.extra = options.extra!;
|
||||
this.postDeserialize = options.postDeserialize;
|
||||
}
|
||||
|
||||
#number<
|
||||
TName extends PropertyKey,
|
||||
TType extends NumberFieldVariant = NumberFieldVariant,
|
||||
TTypeScriptType = number,
|
||||
>(name: TName, type: TType, typeScriptType?: TTypeScriptType) {
|
||||
return this.field(
|
||||
name,
|
||||
new NumberFieldDefinition(type, typeScriptType),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an `int8` field to the `Struct`
|
||||
*/
|
||||
int8<TName extends PropertyKey, TTypeScriptType = number>(
|
||||
name: TName,
|
||||
typeScriptType?: TTypeScriptType,
|
||||
) {
|
||||
return this.#number(name, NumberFieldVariant.Int8, typeScriptType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an `uint8` field to the `Struct`
|
||||
*/
|
||||
uint8<TName extends PropertyKey, TTypeScriptType = number>(
|
||||
name: TName,
|
||||
typeScriptType?: TTypeScriptType,
|
||||
) {
|
||||
return this.#number(name, NumberFieldVariant.Uint8, typeScriptType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an `int16` field to the `Struct`
|
||||
*/
|
||||
int16<TName extends PropertyKey, TTypeScriptType = number>(
|
||||
name: TName,
|
||||
typeScriptType?: TTypeScriptType,
|
||||
) {
|
||||
return this.#number(name, NumberFieldVariant.Int16, typeScriptType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an `uint16` field to the `Struct`
|
||||
*/
|
||||
uint16<TName extends PropertyKey, TTypeScriptType = number>(
|
||||
name: TName,
|
||||
typeScriptType?: TTypeScriptType,
|
||||
) {
|
||||
return this.#number(name, NumberFieldVariant.Uint16, typeScriptType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an `int32` field to the `Struct`
|
||||
*/
|
||||
int32<TName extends PropertyKey, TTypeScriptType = number>(
|
||||
name: TName,
|
||||
typeScriptType?: TTypeScriptType,
|
||||
) {
|
||||
return this.#number(name, NumberFieldVariant.Int32, typeScriptType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an `uint32` field to the `Struct`
|
||||
*/
|
||||
uint32<TName extends PropertyKey, TTypeScriptType = number>(
|
||||
name: TName,
|
||||
typeScriptType?: TTypeScriptType,
|
||||
) {
|
||||
return this.#number(name, NumberFieldVariant.Uint32, typeScriptType);
|
||||
}
|
||||
|
||||
#bigint<
|
||||
TName extends PropertyKey,
|
||||
TType extends BigIntFieldVariant = BigIntFieldVariant,
|
||||
TTypeScriptType = TType["TTypeScriptType"],
|
||||
>(name: TName, type: TType, typeScriptType?: TTypeScriptType) {
|
||||
return this.field(
|
||||
name,
|
||||
new BigIntFieldDefinition(type, typeScriptType),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an `int64` field to the `Struct`
|
||||
*
|
||||
* Requires native `BigInt` support
|
||||
*/
|
||||
int64<
|
||||
TName extends PropertyKey,
|
||||
TTypeScriptType = BigIntFieldVariant["TTypeScriptType"],
|
||||
>(name: TName, typeScriptType?: TTypeScriptType) {
|
||||
return this.#bigint(name, BigIntFieldVariant.Int64, typeScriptType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an `uint64` field to the `Struct`
|
||||
*
|
||||
* Requires native `BigInt` support
|
||||
*/
|
||||
uint64<
|
||||
TName extends PropertyKey,
|
||||
TTypeScriptType = BigIntFieldVariant["TTypeScriptType"],
|
||||
>(name: TName, typeScriptType?: TTypeScriptType) {
|
||||
return this.#bigint(name, BigIntFieldVariant.Uint64, typeScriptType);
|
||||
}
|
||||
|
||||
#arrayBufferLike: ArrayBufferLikeFieldCreator<
|
||||
TFields,
|
||||
TOmitInitKey,
|
||||
TExtra,
|
||||
TPostDeserialized
|
||||
> = (
|
||||
name: PropertyKey,
|
||||
type: BufferFieldConverter,
|
||||
options:
|
||||
| FixedLengthBufferLikeFieldOptions
|
||||
| VariableLengthBufferLikeFieldOptions,
|
||||
): never => {
|
||||
if ("length" in options) {
|
||||
return this.field(
|
||||
name,
|
||||
new FixedLengthBufferLikeFieldDefinition(type, options),
|
||||
) as never;
|
||||
} else {
|
||||
return this.field(
|
||||
name,
|
||||
new VariableLengthBufferLikeFieldDefinition(type, options),
|
||||
) as never;
|
||||
}
|
||||
};
|
||||
|
||||
uint8Array: BoundArrayBufferLikeFieldDefinitionCreator<
|
||||
TFields,
|
||||
TOmitInitKey,
|
||||
TExtra,
|
||||
TPostDeserialized,
|
||||
Uint8ArrayBufferFieldConverter
|
||||
> = (
|
||||
name: PropertyKey,
|
||||
options: unknown,
|
||||
typeScriptType: unknown,
|
||||
): never => {
|
||||
return this.#arrayBufferLike(
|
||||
name,
|
||||
Uint8ArrayBufferFieldConverter.Instance,
|
||||
options as never,
|
||||
typeScriptType,
|
||||
) as never;
|
||||
};
|
||||
|
||||
string: BoundArrayBufferLikeFieldDefinitionCreator<
|
||||
TFields,
|
||||
TOmitInitKey,
|
||||
TExtra,
|
||||
TPostDeserialized,
|
||||
StringBufferFieldConverter
|
||||
> = (
|
||||
name: PropertyKey,
|
||||
options: unknown,
|
||||
typeScriptType: unknown,
|
||||
): never => {
|
||||
return this.#arrayBufferLike(
|
||||
name,
|
||||
StringBufferFieldConverter.Instance,
|
||||
options as never,
|
||||
typeScriptType,
|
||||
) as never;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds some extra properties into every `Struct` value.
|
||||
*
|
||||
* Extra properties will not affect serialize or deserialize process.
|
||||
*
|
||||
* Multiple calls to `extra` will merge all properties together.
|
||||
*
|
||||
* @param value
|
||||
* An object containing properties to be added to the result value. Accessors and methods are also allowed.
|
||||
*/
|
||||
extra<
|
||||
T extends Record<
|
||||
// This trick disallows any keys that are already in `TValue`
|
||||
Exclude<keyof T, Exclude<keyof T, keyof TFields>>,
|
||||
never
|
||||
>,
|
||||
>(
|
||||
value: T & ThisType<Overwrite<Overwrite<TExtra, T>, TFields>>,
|
||||
): Struct<TFields, TOmitInitKey, Overwrite<TExtra, T>, TPostDeserialized> {
|
||||
Object.defineProperties(
|
||||
this.#extra,
|
||||
Object.getOwnPropertyDescriptors(value),
|
||||
);
|
||||
return this as never;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers (or replaces) a custom callback to be run after deserialized.
|
||||
*
|
||||
* A callback returning `never` (always throw an error)
|
||||
* will also change the return type of `deserialize` to `never`.
|
||||
*/
|
||||
postDeserialize(
|
||||
callback: StructPostDeserialized<TFields, never>,
|
||||
): Struct<TFields, TOmitInitKey, TExtra, never>;
|
||||
/**
|
||||
* Registers (or replaces) a custom callback to be run after deserialized.
|
||||
*
|
||||
* A callback returning `void` means it modify the result object in-place
|
||||
* (or doesn't modify it at all), so `deserialize` will still return the result object.
|
||||
*/
|
||||
postDeserialize(
|
||||
callback?: StructPostDeserialized<TFields, void>,
|
||||
): Struct<TFields, TOmitInitKey, TExtra, undefined>;
|
||||
/**
|
||||
* Registers (or replaces) a custom callback to be run after deserialized.
|
||||
*
|
||||
* A callback returning anything other than `undefined`
|
||||
* will `deserialize` to return that object instead.
|
||||
*/
|
||||
postDeserialize<TPostSerialize>(
|
||||
callback?: StructPostDeserialized<TFields, TPostSerialize>,
|
||||
): Struct<TFields, TOmitInitKey, TExtra, TPostSerialize>;
|
||||
postDeserialize(callback?: StructPostDeserialized<TFields, unknown>) {
|
||||
this.#postDeserialized = callback;
|
||||
return this as never;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize a struct value from `stream`.
|
||||
*/
|
||||
deserialize(
|
||||
stream: ExactReadable,
|
||||
): StructDeserializedResult<TFields, TExtra, TPostDeserialized>;
|
||||
deserialize(
|
||||
stream: AsyncExactReadable,
|
||||
): Promise<StructDeserializedResult<TFields, TExtra, TPostDeserialized>>;
|
||||
deserialize(
|
||||
stream: ExactReadable | AsyncExactReadable,
|
||||
): ValueOrPromise<
|
||||
StructDeserializedResult<TFields, TExtra, TPostDeserialized>
|
||||
> {
|
||||
const structValue = new StructValue(this.#extra);
|
||||
|
||||
let promise = SyncPromise.resolve();
|
||||
|
||||
const startPosition = stream.position;
|
||||
for (const [name, definition] of this.#fields) {
|
||||
promise = promise
|
||||
.then(() =>
|
||||
definition.deserialize(this.options, stream, structValue),
|
||||
)
|
||||
.then(
|
||||
(fieldValue) => {
|
||||
structValue.set(name, fieldValue);
|
||||
},
|
||||
(e) => {
|
||||
if (!(e instanceof ExactReadableEndedError)) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (stream.position === startPosition) {
|
||||
throw new StructEmptyError();
|
||||
} else {
|
||||
throw new StructNotEnoughDataError();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return promise
|
||||
.then(() => {
|
||||
const value = structValue.value;
|
||||
|
||||
// Run `postDeserialized`
|
||||
if (this.#postDeserialized) {
|
||||
const override = this.#postDeserialized.call(
|
||||
value as never,
|
||||
value as never,
|
||||
);
|
||||
// If it returns a new value, use that as result
|
||||
// Otherwise it only inspects/mutates the object in place.
|
||||
if (override !== undefined) {
|
||||
return override as never;
|
||||
}
|
||||
}
|
||||
|
||||
return value as never;
|
||||
})
|
||||
.valueOrPromise();
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a struct value to a buffer.
|
||||
* @param init Fields of the struct
|
||||
* @param output The buffer to serialize the struct to. It must be large enough to hold the entire struct. If not provided, a new buffer will be created.
|
||||
* @returns A view of `output` that contains the serialized struct, or a new buffer if `output` is not provided.
|
||||
*/
|
||||
serialize(runtimeStruct: StructInit<this>): Uint8Array;
|
||||
serialize(runtimeStruct: StructInit<this>, buffer: Uint8Array): number;
|
||||
serialize(
|
||||
init: Evaluate<Omit<TFields, TOmitInitKey>>,
|
||||
output?: Uint8Array,
|
||||
): Uint8Array {
|
||||
let structValue: StructValue;
|
||||
if (isStructValueInit(init)) {
|
||||
structValue = init[STRUCT_VALUE_SYMBOL];
|
||||
for (const [key, value] of Object.entries(init)) {
|
||||
const fieldValue = structValue.get(key);
|
||||
if (fieldValue) {
|
||||
fieldValue.set(value);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
structValue = new StructValue({});
|
||||
for (const [name, definition] of this.#fields) {
|
||||
const fieldValue = definition.create(
|
||||
this.options,
|
||||
structValue,
|
||||
(init as Record<PropertyKey, unknown>)[name],
|
||||
runtimeStruct: StructInit<this>,
|
||||
buffer?: Uint8Array,
|
||||
): Uint8Array | number {
|
||||
for (const [key, field] of this.#fieldList) {
|
||||
if (key in runtimeStruct) {
|
||||
field.preSerialize?.(
|
||||
runtimeStruct[key as never],
|
||||
runtimeStruct,
|
||||
);
|
||||
structValue.set(name, fieldValue);
|
||||
}
|
||||
}
|
||||
|
||||
let structSize = 0;
|
||||
const fieldsInfo: {
|
||||
fieldValue: StructFieldValue<any>;
|
||||
size: number;
|
||||
}[] = [];
|
||||
const sizes = this.#fieldList.map(
|
||||
([key, field]) =>
|
||||
field.dynamicSize?.(runtimeStruct[key as never]) ?? field.size,
|
||||
);
|
||||
const size = sizes.reduce((sum, size) => sum + size, 0);
|
||||
|
||||
for (const [name] of this.#fields) {
|
||||
const fieldValue = structValue.get(name);
|
||||
const size = fieldValue.getSize();
|
||||
fieldsInfo.push({ fieldValue, size });
|
||||
structSize += size;
|
||||
}
|
||||
let externalBuffer = false;
|
||||
if (buffer) {
|
||||
if (buffer.length < size) {
|
||||
throw new Error("Buffer too small");
|
||||
}
|
||||
|
||||
if (!output) {
|
||||
output = new Uint8Array(structSize);
|
||||
} else if (output.length < structSize) {
|
||||
throw new TypeError("Output buffer is too small");
|
||||
}
|
||||
|
||||
let offset = 0;
|
||||
for (const { fieldValue, size } of fieldsInfo) {
|
||||
fieldValue.serialize(output, offset);
|
||||
offset += size;
|
||||
}
|
||||
|
||||
if (output.length !== structSize) {
|
||||
return output.subarray(0, structSize);
|
||||
externalBuffer = true;
|
||||
} else {
|
||||
return output;
|
||||
buffer = new Uint8Array(size);
|
||||
}
|
||||
|
||||
const context: SerializeContext = {
|
||||
buffer,
|
||||
index: 0,
|
||||
littleEndian: this.littleEndian,
|
||||
};
|
||||
for (const [index, [key, field]] of this.#fieldList.entries()) {
|
||||
field.serialize(runtimeStruct[key as never], context);
|
||||
context.index += sizes[index]!;
|
||||
}
|
||||
|
||||
if (externalBuffer) {
|
||||
return size;
|
||||
} else {
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
deserialize: {
|
||||
(reader: ExactReadable): PostDeserialize;
|
||||
(reader: AsyncExactReadable): MaybePromiseLike<PostDeserialize>;
|
||||
} = bipedal(function* (
|
||||
this: Struct<T, Extra, PostDeserialize>,
|
||||
then,
|
||||
reader: AsyncExactReadable,
|
||||
) {
|
||||
const startPosition = reader.position;
|
||||
|
||||
const runtimeStruct = {} as Record<string, unknown>;
|
||||
const context: DeserializeContext<unknown> = {
|
||||
reader,
|
||||
runtimeStruct,
|
||||
littleEndian: this.littleEndian,
|
||||
};
|
||||
|
||||
try {
|
||||
for (const [key, field] of this.#fieldList) {
|
||||
runtimeStruct[key] = yield* then(field.deserialize(context));
|
||||
}
|
||||
} catch (e) {
|
||||
if (!(e instanceof ExactReadableEndedError)) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (reader.position === startPosition) {
|
||||
throw new StructEmptyError();
|
||||
} else {
|
||||
throw new StructNotEnoughDataError();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.extra) {
|
||||
Object.defineProperties(
|
||||
runtimeStruct,
|
||||
Object.getOwnPropertyDescriptors(this.extra),
|
||||
);
|
||||
}
|
||||
|
||||
if (this.postDeserialize) {
|
||||
return this.postDeserialize.call(
|
||||
runtimeStruct,
|
||||
runtimeStruct as never,
|
||||
);
|
||||
} else {
|
||||
return runtimeStruct as never;
|
||||
}
|
||||
}) as never;
|
||||
}
|
||||
|
|
|
@ -1,133 +0,0 @@
|
|||
import * as assert from "node:assert";
|
||||
import { describe, it, mock } from "node:test";
|
||||
|
||||
import { SyncPromise } from "./sync-promise.js";
|
||||
|
||||
function delay(timeout: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, timeout));
|
||||
}
|
||||
|
||||
describe("SyncPromise", () => {
|
||||
describe(".resolve", () => {
|
||||
it("should resolve with undefined", () => {
|
||||
const promise = SyncPromise.resolve();
|
||||
assert.strictEqual(promise.valueOrPromise(), undefined);
|
||||
});
|
||||
|
||||
it("should resolve with a value", () => {
|
||||
const promise = SyncPromise.resolve(42);
|
||||
assert.strictEqual(promise.valueOrPromise(), 42);
|
||||
});
|
||||
|
||||
it("should resolve with a promise", async () => {
|
||||
const promise = SyncPromise.resolve(Promise.resolve(42));
|
||||
const value = promise.valueOrPromise();
|
||||
assert.ok(value instanceof Promise);
|
||||
assert.strictEqual(await value, 42);
|
||||
});
|
||||
|
||||
it("should resolve with a pending SyncPromise", async () => {
|
||||
const promise = SyncPromise.resolve(
|
||||
SyncPromise.resolve(Promise.resolve(42)),
|
||||
);
|
||||
const value = promise.valueOrPromise();
|
||||
assert.ok(value instanceof Promise);
|
||||
assert.strictEqual(await value, 42);
|
||||
});
|
||||
|
||||
it("should resolve with a resolved SyncPromise", () => {
|
||||
const promise = SyncPromise.resolve(SyncPromise.resolve(42));
|
||||
assert.strictEqual(promise.valueOrPromise(), 42);
|
||||
});
|
||||
|
||||
it("should resolve with a rejected SyncPromise", () => {
|
||||
const promise = SyncPromise.resolve(
|
||||
SyncPromise.reject(new Error("SyncPromise error")),
|
||||
);
|
||||
assert.throws(() => promise.valueOrPromise(), /SyncPromise error/);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".reject", () => {
|
||||
it("should reject with the reason", () => {
|
||||
const promise = SyncPromise.reject(new Error("error"));
|
||||
assert.throws(() => promise.valueOrPromise(), { message: "error" });
|
||||
});
|
||||
});
|
||||
|
||||
describe(".try", () => {
|
||||
it("should call executor", () => {
|
||||
const executor = mock.fn(() => {
|
||||
return 42;
|
||||
});
|
||||
void SyncPromise.try(executor);
|
||||
assert.strictEqual(executor.mock.callCount(), 1);
|
||||
});
|
||||
|
||||
it("should resolve with a value", () => {
|
||||
const promise = SyncPromise.try(() => 42);
|
||||
assert.strictEqual(promise.valueOrPromise(), 42);
|
||||
});
|
||||
|
||||
it("should resolve with a promise", async () => {
|
||||
const promise = SyncPromise.try(() => Promise.resolve(42));
|
||||
const value = promise.valueOrPromise();
|
||||
assert.ok(value instanceof Promise);
|
||||
assert.strictEqual(await value, 42);
|
||||
});
|
||||
|
||||
it("should resolve with a pending SyncPromise", async () => {
|
||||
const promise = SyncPromise.try(() =>
|
||||
SyncPromise.resolve(Promise.resolve(42)),
|
||||
);
|
||||
const value = promise.valueOrPromise();
|
||||
assert.ok(value instanceof Promise);
|
||||
assert.strictEqual(await value, 42);
|
||||
});
|
||||
|
||||
it("should resolve with a resolved SyncPromise", () => {
|
||||
const promise = SyncPromise.try(() => SyncPromise.resolve(42));
|
||||
assert.strictEqual(promise.valueOrPromise(), 42);
|
||||
});
|
||||
|
||||
it("should resolve with a rejected SyncPromise", () => {
|
||||
const promise = SyncPromise.try(() =>
|
||||
SyncPromise.reject(new Error("error")),
|
||||
);
|
||||
assert.throws(() => promise.valueOrPromise(), { message: "error" });
|
||||
});
|
||||
|
||||
it("should reject with the error thrown", () => {
|
||||
const promise = SyncPromise.try(() => {
|
||||
throw new Error("error");
|
||||
});
|
||||
assert.throws(() => promise.valueOrPromise(), { message: "error" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("#then", () => {
|
||||
it("chain a pending SyncPromise with value", async () => {
|
||||
const promise = SyncPromise.resolve(Promise.resolve(42));
|
||||
const handler = mock.fn(() => "foo");
|
||||
const result = promise.then(handler);
|
||||
|
||||
await delay(0);
|
||||
assert.strictEqual(handler.mock.callCount(), 1);
|
||||
assert.deepStrictEqual(handler.mock.calls[0]!.arguments, [42]);
|
||||
|
||||
assert.strictEqual(await result.valueOrPromise(), "foo");
|
||||
});
|
||||
|
||||
it("chian a pending SyncPromise with a promise", async () => {
|
||||
const promise = SyncPromise.resolve(Promise.resolve(42));
|
||||
const handler = mock.fn(() => Promise.resolve("foo"));
|
||||
const result = promise.then(handler);
|
||||
|
||||
await delay(0);
|
||||
|
||||
assert.strictEqual(handler.mock.callCount(), 1);
|
||||
assert.deepStrictEqual(handler.mock.calls[0]!.arguments, [42]);
|
||||
assert.strictEqual(await result.valueOrPromise(), "foo");
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,119 +0,0 @@
|
|||
export interface SyncPromise<T> {
|
||||
then<TResult1 = T, TResult2 = never>(
|
||||
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
|
||||
onrejected?:
|
||||
| ((reason: unknown) => TResult2 | PromiseLike<TResult2>)
|
||||
| null,
|
||||
): SyncPromise<TResult1 | TResult2>;
|
||||
|
||||
valueOrPromise(): T | PromiseLike<T>;
|
||||
}
|
||||
|
||||
interface SyncPromiseStatic {
|
||||
reject<T = never>(reason?: unknown): SyncPromise<T>;
|
||||
|
||||
resolve(): SyncPromise<void>;
|
||||
resolve<T>(value: T | PromiseLike<T>): SyncPromise<T>;
|
||||
|
||||
try<T>(executor: () => T | PromiseLike<T>): SyncPromise<T>;
|
||||
}
|
||||
|
||||
export const SyncPromise: SyncPromiseStatic = {
|
||||
reject<T = never>(reason?: unknown): SyncPromise<T> {
|
||||
return new RejectedSyncPromise(reason);
|
||||
},
|
||||
resolve<T>(value?: T | PromiseLike<T>): SyncPromise<T> {
|
||||
if (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
typeof (value as PromiseLike<T>).then === "function"
|
||||
) {
|
||||
if (
|
||||
value instanceof PendingSyncPromise ||
|
||||
value instanceof ResolvedSyncPromise ||
|
||||
value instanceof RejectedSyncPromise
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return new PendingSyncPromise(value as PromiseLike<T>);
|
||||
} else {
|
||||
return new ResolvedSyncPromise(value as T);
|
||||
}
|
||||
},
|
||||
try<T>(executor: () => T | PromiseLike<T>): SyncPromise<T> {
|
||||
try {
|
||||
return SyncPromise.resolve(executor());
|
||||
} catch (e) {
|
||||
return SyncPromise.reject(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
class PendingSyncPromise<T> implements SyncPromise<T> {
|
||||
#promise: PromiseLike<T>;
|
||||
|
||||
constructor(promise: PromiseLike<T>) {
|
||||
this.#promise = promise;
|
||||
}
|
||||
|
||||
then<TResult1 = T, TResult2 = never>(
|
||||
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
|
||||
onrejected?:
|
||||
| ((reason: unknown) => TResult2 | PromiseLike<TResult2>)
|
||||
| null,
|
||||
) {
|
||||
return new PendingSyncPromise<TResult1 | TResult2>(
|
||||
this.#promise.then(onfulfilled, onrejected),
|
||||
);
|
||||
}
|
||||
|
||||
valueOrPromise(): T | PromiseLike<T> {
|
||||
return this.#promise;
|
||||
}
|
||||
}
|
||||
|
||||
class ResolvedSyncPromise<T> implements SyncPromise<T> {
|
||||
#value: T;
|
||||
|
||||
constructor(value: T) {
|
||||
this.#value = value;
|
||||
}
|
||||
|
||||
then<TResult1 = T>(
|
||||
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
|
||||
) {
|
||||
if (!onfulfilled) {
|
||||
return this as never;
|
||||
}
|
||||
return SyncPromise.try(() => onfulfilled(this.#value));
|
||||
}
|
||||
|
||||
valueOrPromise(): T | PromiseLike<T> {
|
||||
return this.#value;
|
||||
}
|
||||
}
|
||||
|
||||
class RejectedSyncPromise<T> implements SyncPromise<T> {
|
||||
#reason: unknown;
|
||||
|
||||
constructor(reason: unknown) {
|
||||
this.#reason = reason;
|
||||
}
|
||||
|
||||
then<TResult1 = T, TResult2 = never>(
|
||||
_?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
|
||||
onrejected?:
|
||||
| ((reason: unknown) => TResult2 | PromiseLike<TResult2>)
|
||||
| null,
|
||||
) {
|
||||
if (!onrejected) {
|
||||
return this as never;
|
||||
}
|
||||
return SyncPromise.try(() => onrejected(this.#reason));
|
||||
}
|
||||
|
||||
valueOrPromise(): T | PromiseLike<T> {
|
||||
throw this.#reason;
|
||||
}
|
||||
}
|
|
@ -1,128 +0,0 @@
|
|||
import {
|
||||
getInt64,
|
||||
getUint64,
|
||||
setInt64,
|
||||
setUint64,
|
||||
} from "@yume-chan/no-data-view";
|
||||
|
||||
import type {
|
||||
AsyncExactReadable,
|
||||
ExactReadable,
|
||||
StructOptions,
|
||||
StructValue,
|
||||
} from "../basic/index.js";
|
||||
import { StructFieldDefinition, StructFieldValue } from "../basic/index.js";
|
||||
import { SyncPromise } from "../sync-promise.js";
|
||||
import type { ValueOrPromise } from "../utils.js";
|
||||
|
||||
export type BigIntDeserializer = (
|
||||
array: Uint8Array,
|
||||
byteOffset: number,
|
||||
littleEndian: boolean,
|
||||
) => bigint;
|
||||
|
||||
export type BigIntSerializer = (
|
||||
array: Uint8Array,
|
||||
byteOffset: number,
|
||||
value: bigint,
|
||||
littleEndian: boolean,
|
||||
) => void;
|
||||
|
||||
export class BigIntFieldVariant {
|
||||
readonly TTypeScriptType!: bigint;
|
||||
|
||||
readonly size: number;
|
||||
|
||||
readonly deserialize: BigIntDeserializer;
|
||||
|
||||
readonly serialize: BigIntSerializer;
|
||||
|
||||
constructor(
|
||||
size: number,
|
||||
deserialize: BigIntDeserializer,
|
||||
serialize: BigIntSerializer,
|
||||
) {
|
||||
this.size = size;
|
||||
this.deserialize = deserialize;
|
||||
this.serialize = serialize;
|
||||
}
|
||||
|
||||
static readonly Int64 = /* #__PURE__ */ new BigIntFieldVariant(
|
||||
8,
|
||||
getInt64,
|
||||
setInt64,
|
||||
);
|
||||
|
||||
static readonly Uint64 = /* #__PURE__ */ new BigIntFieldVariant(
|
||||
8,
|
||||
getUint64,
|
||||
setUint64,
|
||||
);
|
||||
}
|
||||
|
||||
export class BigIntFieldDefinition<
|
||||
TVariant extends BigIntFieldVariant = BigIntFieldVariant,
|
||||
TTypeScriptType = TVariant["TTypeScriptType"],
|
||||
> extends StructFieldDefinition<void, TTypeScriptType> {
|
||||
readonly variant: TVariant;
|
||||
|
||||
constructor(variant: TVariant, typescriptType?: TTypeScriptType) {
|
||||
void typescriptType;
|
||||
super();
|
||||
this.variant = variant;
|
||||
}
|
||||
|
||||
getSize(): number {
|
||||
return this.variant.size;
|
||||
}
|
||||
|
||||
create(
|
||||
options: Readonly<StructOptions>,
|
||||
struct: StructValue,
|
||||
value: TTypeScriptType,
|
||||
): BigIntFieldValue<this> {
|
||||
return new BigIntFieldValue(this, options, struct, value);
|
||||
}
|
||||
|
||||
override deserialize(
|
||||
options: Readonly<StructOptions>,
|
||||
stream: ExactReadable,
|
||||
struct: StructValue,
|
||||
): BigIntFieldValue<this>;
|
||||
override deserialize(
|
||||
options: Readonly<StructOptions>,
|
||||
stream: AsyncExactReadable,
|
||||
struct: StructValue,
|
||||
): Promise<BigIntFieldValue<this>>;
|
||||
override deserialize(
|
||||
options: Readonly<StructOptions>,
|
||||
stream: ExactReadable | AsyncExactReadable,
|
||||
struct: StructValue,
|
||||
): ValueOrPromise<BigIntFieldValue<this>> {
|
||||
return SyncPromise.try(() => {
|
||||
return stream.readExactly(this.getSize());
|
||||
})
|
||||
.then((array) => {
|
||||
const value = this.variant.deserialize(
|
||||
array,
|
||||
0,
|
||||
options.littleEndian,
|
||||
);
|
||||
return this.create(options, struct, value as never);
|
||||
})
|
||||
.valueOrPromise();
|
||||
}
|
||||
}
|
||||
|
||||
export class BigIntFieldValue<
|
||||
TDefinition extends BigIntFieldDefinition<BigIntFieldVariant, unknown>,
|
||||
> extends StructFieldValue<TDefinition> {
|
||||
override serialize(array: Uint8Array, offset: number): void {
|
||||
this.definition.variant.serialize(
|
||||
array,
|
||||
offset,
|
||||
this.value as never,
|
||||
this.options.littleEndian,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,241 +0,0 @@
|
|||
import * as assert from "node:assert";
|
||||
import { describe, it, mock } from "node:test";
|
||||
|
||||
import type { ExactReadable } from "../../basic/index.js";
|
||||
import { StructDefaultOptions, StructValue } from "../../basic/index.js";
|
||||
|
||||
import type { BufferFieldConverter } from "./base.js";
|
||||
import {
|
||||
BufferLikeFieldDefinition,
|
||||
EMPTY_UINT8_ARRAY,
|
||||
StringBufferFieldConverter,
|
||||
Uint8ArrayBufferFieldConverter,
|
||||
} from "./base.js";
|
||||
|
||||
class MockDeserializationStream implements ExactReadable {
|
||||
array = EMPTY_UINT8_ARRAY;
|
||||
|
||||
position = 0;
|
||||
|
||||
readExactly = mock.fn(() => this.array);
|
||||
}
|
||||
|
||||
describe("Types", () => {
|
||||
describe("Buffer", () => {
|
||||
describe("Uint8ArrayBufferFieldSubType", () => {
|
||||
it("should have a static instance", () => {
|
||||
assert.ok(
|
||||
Uint8ArrayBufferFieldConverter.Instance instanceof
|
||||
Uint8ArrayBufferFieldConverter,
|
||||
);
|
||||
});
|
||||
|
||||
it("`#toBuffer` should return the same `Uint8Array`", () => {
|
||||
const array = new Uint8Array(10);
|
||||
assert.strictEqual(
|
||||
Uint8ArrayBufferFieldConverter.Instance.toBuffer(array),
|
||||
array,
|
||||
);
|
||||
});
|
||||
|
||||
it("`#fromBuffer` should return the same `Uint8Array`", () => {
|
||||
const buffer = new Uint8Array(10);
|
||||
assert.strictEqual(
|
||||
Uint8ArrayBufferFieldConverter.Instance.toValue(buffer),
|
||||
buffer,
|
||||
);
|
||||
});
|
||||
|
||||
it("`#getSize` should return the `byteLength` of the `Uint8Array`", () => {
|
||||
const array = new Uint8Array(10);
|
||||
assert.strictEqual(
|
||||
Uint8ArrayBufferFieldConverter.Instance.getSize(array),
|
||||
10,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("StringBufferFieldSubType", () => {
|
||||
it("should have a static instance", () => {
|
||||
assert.ok(
|
||||
StringBufferFieldConverter.Instance instanceof
|
||||
StringBufferFieldConverter,
|
||||
);
|
||||
});
|
||||
|
||||
it("`#toBuffer` should return the decoded string", () => {
|
||||
const text = "foo";
|
||||
const array = new Uint8Array(Buffer.from(text, "utf-8"));
|
||||
assert.deepStrictEqual(
|
||||
StringBufferFieldConverter.Instance.toBuffer(text),
|
||||
array,
|
||||
);
|
||||
});
|
||||
|
||||
it("`#fromBuffer` should return the encoded ArrayBuffer", () => {
|
||||
const text = "foo";
|
||||
const array = new Uint8Array(Buffer.from(text, "utf-8"));
|
||||
assert.strictEqual(
|
||||
StringBufferFieldConverter.Instance.toValue(array),
|
||||
text,
|
||||
);
|
||||
});
|
||||
|
||||
it("`#getSize` should return -1", () => {
|
||||
assert.strictEqual(
|
||||
StringBufferFieldConverter.Instance.getSize(),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
class MockArrayBufferFieldDefinition<
|
||||
TType extends BufferFieldConverter,
|
||||
> extends BufferLikeFieldDefinition<TType, number> {
|
||||
getSize(): number {
|
||||
return this.options;
|
||||
}
|
||||
}
|
||||
|
||||
describe("BufferLikeFieldDefinition", () => {
|
||||
it("should work with `Uint8ArrayBufferFieldSubType`", () => {
|
||||
const size = 10;
|
||||
const definition = new MockArrayBufferFieldDefinition(
|
||||
Uint8ArrayBufferFieldConverter.Instance,
|
||||
size,
|
||||
);
|
||||
|
||||
const context = new MockDeserializationStream();
|
||||
const array = new Uint8Array(size);
|
||||
context.array = array;
|
||||
const struct = new StructValue({});
|
||||
|
||||
const fieldValue = definition.deserialize(
|
||||
StructDefaultOptions,
|
||||
context,
|
||||
struct,
|
||||
);
|
||||
assert.strictEqual(context.readExactly.mock.callCount(), 1);
|
||||
assert.deepStrictEqual(
|
||||
context.readExactly.mock.calls[0]?.arguments,
|
||||
[size],
|
||||
);
|
||||
assert.strictEqual(fieldValue["array"], array);
|
||||
|
||||
assert.strictEqual(fieldValue.get(), array);
|
||||
});
|
||||
|
||||
it("should work when `#getSize` returns `0`", () => {
|
||||
const size = 0;
|
||||
const definition = new MockArrayBufferFieldDefinition(
|
||||
Uint8ArrayBufferFieldConverter.Instance,
|
||||
size,
|
||||
);
|
||||
|
||||
const context = new MockDeserializationStream();
|
||||
const buffer = new Uint8Array(size);
|
||||
context.array = buffer;
|
||||
const struct = new StructValue({});
|
||||
|
||||
const fieldValue = definition.deserialize(
|
||||
StructDefaultOptions,
|
||||
context,
|
||||
struct,
|
||||
);
|
||||
assert.strictEqual(context.readExactly.mock.callCount(), 0);
|
||||
assert.ok(fieldValue["array"] instanceof Uint8Array);
|
||||
assert.strictEqual(fieldValue["array"].byteLength, 0);
|
||||
|
||||
const value = fieldValue.get();
|
||||
assert.ok(value instanceof Uint8Array);
|
||||
assert.strictEqual(value.byteLength, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ArrayBufferLikeFieldValue", () => {
|
||||
describe("#set", () => {
|
||||
it("should clear `array` field", () => {
|
||||
const size = 0;
|
||||
const definition = new MockArrayBufferFieldDefinition(
|
||||
Uint8ArrayBufferFieldConverter.Instance,
|
||||
size,
|
||||
);
|
||||
|
||||
const context = new MockDeserializationStream();
|
||||
const array = new Uint8Array(size);
|
||||
context.array = array;
|
||||
const struct = new StructValue({});
|
||||
|
||||
const fieldValue = definition.deserialize(
|
||||
StructDefaultOptions,
|
||||
context,
|
||||
struct,
|
||||
);
|
||||
|
||||
const newValue = new Uint8Array(20);
|
||||
fieldValue.set(newValue);
|
||||
assert.deepStrictEqual(fieldValue.get(), newValue);
|
||||
assert.strictEqual(fieldValue["array"], undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#serialize", () => {
|
||||
it("should be able to serialize with cached `array`", () => {
|
||||
const size = 0;
|
||||
const definition = new MockArrayBufferFieldDefinition(
|
||||
Uint8ArrayBufferFieldConverter.Instance,
|
||||
size,
|
||||
);
|
||||
|
||||
const context = new MockDeserializationStream();
|
||||
const sourceArray = new Uint8Array(
|
||||
Array.from({ length: size }, (_, i) => i),
|
||||
);
|
||||
const array = sourceArray;
|
||||
context.array = array;
|
||||
const struct = new StructValue({});
|
||||
|
||||
const fieldValue = definition.deserialize(
|
||||
StructDefaultOptions,
|
||||
context,
|
||||
struct,
|
||||
);
|
||||
|
||||
const targetArray = new Uint8Array(size);
|
||||
fieldValue.serialize(targetArray, 0);
|
||||
|
||||
assert.deepStrictEqual(targetArray, sourceArray);
|
||||
});
|
||||
|
||||
it("should be able to serialize a modified value", () => {
|
||||
const size = 0;
|
||||
const definition = new MockArrayBufferFieldDefinition(
|
||||
Uint8ArrayBufferFieldConverter.Instance,
|
||||
size,
|
||||
);
|
||||
|
||||
const context = new MockDeserializationStream();
|
||||
const sourceArray = new Uint8Array(
|
||||
Array.from({ length: size }, (_, i) => i),
|
||||
);
|
||||
const array = sourceArray;
|
||||
context.array = array;
|
||||
const struct = new StructValue({});
|
||||
|
||||
const fieldValue = definition.deserialize(
|
||||
StructDefaultOptions,
|
||||
context,
|
||||
struct,
|
||||
);
|
||||
|
||||
fieldValue.set(sourceArray);
|
||||
|
||||
const targetArray = new Uint8Array(size);
|
||||
fieldValue.serialize(targetArray, 0);
|
||||
|
||||
assert.deepStrictEqual(targetArray, sourceArray);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,196 +0,0 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type {
|
||||
AsyncExactReadable,
|
||||
ExactReadable,
|
||||
StructOptions,
|
||||
StructValue,
|
||||
} from "../../basic/index.js";
|
||||
import { StructFieldDefinition, StructFieldValue } from "../../basic/index.js";
|
||||
import { SyncPromise } from "../../sync-promise.js";
|
||||
import type { ValueOrPromise } from "../../utils.js";
|
||||
import { decodeUtf8, encodeUtf8 } from "../../utils.js";
|
||||
|
||||
/**
|
||||
* A converter for buffer-like fields.
|
||||
* It converts `Uint8Array`s to custom-typed values when deserializing,
|
||||
* and convert values back to `Uint8Array`s when serializing.
|
||||
*
|
||||
* @template TValue The type of the value that the converter converts to/from `Uint8Array`.
|
||||
* @template TTypeScriptType Optionally another type to refine `TValue`.
|
||||
* For example, `TValue` is `string`, and `TTypeScriptType` is `"foo" | "bar"`.
|
||||
* `TValue` is specified by the developer when creating an converter implementation,
|
||||
* `TTypeScriptType` is specified by the user when creating a field.
|
||||
*/
|
||||
export abstract class BufferFieldConverter<
|
||||
TValue = unknown,
|
||||
TTypeScriptType = TValue,
|
||||
> {
|
||||
readonly TTypeScriptType!: TTypeScriptType;
|
||||
|
||||
/**
|
||||
* When implemented in derived classes, converts the custom `value` to an `Uint8Array`
|
||||
*
|
||||
* This function should be "pure", i.e.,
|
||||
* same `value` should always be converted to `Uint8Array`s that have same content.
|
||||
*/
|
||||
abstract toBuffer(value: TValue): Uint8Array;
|
||||
|
||||
/** When implemented in derived classes, converts the `Uint8Array` to a custom value */
|
||||
abstract toValue(array: Uint8Array): TValue;
|
||||
|
||||
/**
|
||||
* When implemented in derived classes, gets the size in byte of the custom `value`.
|
||||
*
|
||||
* If the size can't be determined without first converting the `value` back to an `Uint8Array`,
|
||||
* the implementer should return `undefined`. In which case, the caller will call `toBuffer` to
|
||||
* convert the value to a `Uint8Array`, then read the length of the `Uint8Array`. The caller can
|
||||
* cache the result so the serialization process doesn't need to call `toBuffer` again.
|
||||
*/
|
||||
abstract getSize(value: TValue): number | undefined;
|
||||
}
|
||||
|
||||
/** An identity converter, doesn't convert to anything else. */
|
||||
export class Uint8ArrayBufferFieldConverter<
|
||||
TTypeScriptType = Uint8Array,
|
||||
> extends BufferFieldConverter<Uint8Array, TTypeScriptType> {
|
||||
static readonly Instance =
|
||||
/* #__PURE__ */ new Uint8ArrayBufferFieldConverter();
|
||||
|
||||
protected constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
override toBuffer(value: Uint8Array): Uint8Array {
|
||||
return value;
|
||||
}
|
||||
|
||||
override toValue(buffer: Uint8Array): Uint8Array {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
override getSize(value: Uint8Array): number {
|
||||
return value.length;
|
||||
}
|
||||
}
|
||||
|
||||
/** An `BufferFieldSubType` that converts between `Uint8Array` and `string` */
|
||||
export class StringBufferFieldConverter<
|
||||
TTypeScriptType = string,
|
||||
> extends BufferFieldConverter<string, TTypeScriptType> {
|
||||
static readonly Instance = /* #__PURE__ */ new StringBufferFieldConverter();
|
||||
|
||||
override toBuffer(value: string): Uint8Array {
|
||||
return encodeUtf8(value);
|
||||
}
|
||||
|
||||
override toValue(array: Uint8Array): string {
|
||||
return decodeUtf8(array);
|
||||
}
|
||||
|
||||
override getSize(): number | undefined {
|
||||
// See the note in `BufferFieldConverter.getSize`
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export const EMPTY_UINT8_ARRAY = new Uint8Array(0);
|
||||
|
||||
export abstract class BufferLikeFieldDefinition<
|
||||
TConverter extends BufferFieldConverter<
|
||||
unknown,
|
||||
unknown
|
||||
> = BufferFieldConverter<unknown, unknown>,
|
||||
TOptions = void,
|
||||
TOmitInitKey extends PropertyKey = never,
|
||||
TTypeScriptType = TConverter["TTypeScriptType"],
|
||||
> extends StructFieldDefinition<TOptions, TTypeScriptType, TOmitInitKey> {
|
||||
readonly converter: TConverter;
|
||||
readonly TTypeScriptType!: TTypeScriptType;
|
||||
|
||||
constructor(converter: TConverter, options: TOptions) {
|
||||
super(options);
|
||||
this.converter = converter;
|
||||
}
|
||||
|
||||
protected getDeserializeSize(struct: StructValue): number {
|
||||
void struct;
|
||||
return this.getSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* When implemented in derived classes, creates a `StructFieldValue` for the current field definition.
|
||||
*/
|
||||
create(
|
||||
options: Readonly<StructOptions>,
|
||||
struct: StructValue,
|
||||
value: TTypeScriptType,
|
||||
array?: Uint8Array,
|
||||
): BufferLikeFieldValue<this> {
|
||||
return new BufferLikeFieldValue(this, options, struct, value, array);
|
||||
}
|
||||
|
||||
override deserialize(
|
||||
options: Readonly<StructOptions>,
|
||||
stream: ExactReadable,
|
||||
struct: StructValue,
|
||||
): BufferLikeFieldValue<this>;
|
||||
override deserialize(
|
||||
options: Readonly<StructOptions>,
|
||||
stream: AsyncExactReadable,
|
||||
struct: StructValue,
|
||||
): Promise<BufferLikeFieldValue<this>>;
|
||||
override deserialize(
|
||||
options: Readonly<StructOptions>,
|
||||
stream: ExactReadable | AsyncExactReadable,
|
||||
struct: StructValue,
|
||||
): ValueOrPromise<BufferLikeFieldValue<this>> {
|
||||
return SyncPromise.try(() => {
|
||||
const size = this.getDeserializeSize(struct);
|
||||
if (size === 0) {
|
||||
return EMPTY_UINT8_ARRAY;
|
||||
} else {
|
||||
return stream.readExactly(size);
|
||||
}
|
||||
})
|
||||
.then((array) => {
|
||||
const value = this.converter.toValue(array) as TTypeScriptType;
|
||||
return this.create(options, struct, value, array);
|
||||
})
|
||||
.valueOrPromise();
|
||||
}
|
||||
}
|
||||
|
||||
export class BufferLikeFieldValue<
|
||||
TDefinition extends BufferLikeFieldDefinition<
|
||||
BufferFieldConverter<unknown, unknown>,
|
||||
any,
|
||||
any,
|
||||
any
|
||||
>,
|
||||
> extends StructFieldValue<TDefinition> {
|
||||
protected array: Uint8Array | undefined;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/max-params
|
||||
constructor(
|
||||
definition: TDefinition,
|
||||
options: Readonly<StructOptions>,
|
||||
struct: StructValue,
|
||||
value: TDefinition["TTypeScriptType"],
|
||||
array?: Uint8Array,
|
||||
) {
|
||||
super(definition, options, struct, value);
|
||||
this.array = array;
|
||||
}
|
||||
|
||||
override set(value: TDefinition["TValue"]): void {
|
||||
super.set(value);
|
||||
// When value changes, clear the cached `array`
|
||||
// It will be lazily calculated in `serialize()`
|
||||
this.array = undefined;
|
||||
}
|
||||
|
||||
override serialize(array: Uint8Array, offset: number): void {
|
||||
this.array ??= this.definition.converter.toBuffer(this.value);
|
||||
array.set(this.array, offset);
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
import * as assert from "node:assert";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import { Uint8ArrayBufferFieldConverter } from "./base.js";
|
||||
import { FixedLengthBufferLikeFieldDefinition } from "./fixed-length.js";
|
||||
|
||||
describe("Types", () => {
|
||||
describe("FixedLengthArrayBufferLikeFieldDefinition", () => {
|
||||
describe("#getSize", () => {
|
||||
it("should return size in its options", () => {
|
||||
const definition = new FixedLengthBufferLikeFieldDefinition(
|
||||
Uint8ArrayBufferFieldConverter.Instance,
|
||||
{ length: 10 },
|
||||
);
|
||||
assert.strictEqual(definition.getSize(), 10);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,22 +0,0 @@
|
|||
import type { BufferFieldConverter } from "./base.js";
|
||||
import { BufferLikeFieldDefinition } from "./base.js";
|
||||
|
||||
export interface FixedLengthBufferLikeFieldOptions {
|
||||
length: number;
|
||||
}
|
||||
|
||||
export class FixedLengthBufferLikeFieldDefinition<
|
||||
TConverter extends BufferFieldConverter = BufferFieldConverter,
|
||||
TOptions extends
|
||||
FixedLengthBufferLikeFieldOptions = FixedLengthBufferLikeFieldOptions,
|
||||
TTypeScriptType = TConverter["TTypeScriptType"],
|
||||
> extends BufferLikeFieldDefinition<
|
||||
TConverter,
|
||||
TOptions,
|
||||
never,
|
||||
TTypeScriptType
|
||||
> {
|
||||
getSize(): number {
|
||||
return this.options.length;
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export * from "./base.js";
|
||||
export * from "./fixed-length.js";
|
||||
export * from "./variable-length.js";
|
|
@ -1,874 +0,0 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import * as assert from "node:assert";
|
||||
import { describe, it, mock } from "node:test";
|
||||
|
||||
import {
|
||||
StructDefaultOptions,
|
||||
StructFieldValue,
|
||||
StructValue,
|
||||
} from "../../basic/index.js";
|
||||
|
||||
import {
|
||||
BufferFieldConverter,
|
||||
EMPTY_UINT8_ARRAY,
|
||||
Uint8ArrayBufferFieldConverter,
|
||||
} from "./base.js";
|
||||
import type { VariableLengthBufferLikeFieldOptions } from "./variable-length.js";
|
||||
import {
|
||||
VariableLengthBufferLikeFieldDefinition,
|
||||
VariableLengthBufferLikeFieldLengthValue,
|
||||
VariableLengthBufferLikeStructFieldValue,
|
||||
} from "./variable-length.js";
|
||||
|
||||
class MockLengthFieldValue extends StructFieldValue<any> {
|
||||
constructor() {
|
||||
super({} as any, {} as any, {} as any, {});
|
||||
}
|
||||
|
||||
override value: string | number = 0;
|
||||
|
||||
override get = mock.fn((): string | number => this.value);
|
||||
|
||||
size = 0;
|
||||
|
||||
override getSize = mock.fn((): number => this.size);
|
||||
|
||||
override set = mock.fn((value: string | number) => {
|
||||
void value;
|
||||
});
|
||||
|
||||
serialize = mock.fn((array: Uint8Array, offset: number): void => {
|
||||
void array;
|
||||
void offset;
|
||||
});
|
||||
}
|
||||
|
||||
describe("Types", () => {
|
||||
describe("VariableLengthBufferLikeFieldLengthValue", () => {
|
||||
class MockBufferLikeFieldValue extends StructFieldValue<
|
||||
VariableLengthBufferLikeFieldDefinition<
|
||||
any,
|
||||
VariableLengthBufferLikeFieldOptions<any, any>,
|
||||
any
|
||||
>
|
||||
> {
|
||||
constructor() {
|
||||
super({ options: {} } as any, {} as any, {} as any, {});
|
||||
}
|
||||
|
||||
size = 0;
|
||||
|
||||
override getSize = mock.fn(() => this.size);
|
||||
|
||||
override serialize(array: Uint8Array, offset: number): void {
|
||||
void array;
|
||||
void offset;
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
describe("#getSize", () => {
|
||||
it("should return size of its original field value", () => {
|
||||
const mockOriginalFieldValue = new MockLengthFieldValue();
|
||||
const mockBufferFieldValue = new MockBufferLikeFieldValue();
|
||||
const lengthFieldValue =
|
||||
new VariableLengthBufferLikeFieldLengthValue(
|
||||
mockOriginalFieldValue,
|
||||
mockBufferFieldValue,
|
||||
);
|
||||
|
||||
mockOriginalFieldValue.size = 0;
|
||||
assert.strictEqual(lengthFieldValue.getSize(), 0);
|
||||
assert.strictEqual(
|
||||
mockOriginalFieldValue.getSize.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
|
||||
mockOriginalFieldValue.getSize.mock.resetCalls();
|
||||
mockOriginalFieldValue.size = 100;
|
||||
assert.strictEqual(lengthFieldValue.getSize(), 100);
|
||||
assert.strictEqual(
|
||||
mockOriginalFieldValue.getSize.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#get", () => {
|
||||
it("should return size of its `bufferValue`", () => {
|
||||
const mockOriginalFieldValue = new MockLengthFieldValue();
|
||||
const mockBufferFieldValue = new MockBufferLikeFieldValue();
|
||||
const lengthFieldValue =
|
||||
new VariableLengthBufferLikeFieldLengthValue(
|
||||
mockOriginalFieldValue,
|
||||
mockBufferFieldValue,
|
||||
);
|
||||
|
||||
mockOriginalFieldValue.value = 0;
|
||||
mockBufferFieldValue.size = 0;
|
||||
assert.strictEqual(lengthFieldValue.get(), 0);
|
||||
assert.strictEqual(
|
||||
mockBufferFieldValue.getSize.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
assert.strictEqual(
|
||||
mockOriginalFieldValue.get.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
|
||||
mockBufferFieldValue.getSize.mock.resetCalls();
|
||||
mockOriginalFieldValue.get.mock.resetCalls();
|
||||
mockBufferFieldValue.size = 100;
|
||||
assert.strictEqual(lengthFieldValue.get(), 100);
|
||||
assert.strictEqual(
|
||||
mockBufferFieldValue.getSize.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
assert.strictEqual(
|
||||
mockOriginalFieldValue.get.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it("should return size of its `bufferValue` as string", () => {
|
||||
const mockOriginalFieldValue = new MockLengthFieldValue();
|
||||
mockOriginalFieldValue.value = "0";
|
||||
const mockBufferFieldValue = new MockBufferLikeFieldValue();
|
||||
const lengthFieldValue =
|
||||
new VariableLengthBufferLikeFieldLengthValue(
|
||||
mockOriginalFieldValue,
|
||||
mockBufferFieldValue,
|
||||
);
|
||||
|
||||
mockBufferFieldValue.size = 0;
|
||||
assert.strictEqual(lengthFieldValue.get(), "0");
|
||||
assert.strictEqual(
|
||||
mockBufferFieldValue.getSize.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
assert.strictEqual(
|
||||
mockOriginalFieldValue.get.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
|
||||
mockBufferFieldValue.getSize.mock.resetCalls();
|
||||
mockOriginalFieldValue.get.mock.resetCalls();
|
||||
mockBufferFieldValue.size = 100;
|
||||
assert.strictEqual(lengthFieldValue.get(), "100");
|
||||
assert.strictEqual(
|
||||
mockBufferFieldValue.getSize.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
assert.strictEqual(
|
||||
mockOriginalFieldValue.get.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#set", () => {
|
||||
it("should does nothing", () => {
|
||||
const mockOriginalFieldValue = new MockLengthFieldValue();
|
||||
const mockBufferFieldValue = new MockBufferLikeFieldValue();
|
||||
const lengthFieldValue =
|
||||
new VariableLengthBufferLikeFieldLengthValue(
|
||||
mockOriginalFieldValue,
|
||||
mockBufferFieldValue,
|
||||
);
|
||||
|
||||
mockOriginalFieldValue.value = 0;
|
||||
mockBufferFieldValue.size = 0;
|
||||
assert.strictEqual(lengthFieldValue.get(), 0);
|
||||
|
||||
(lengthFieldValue as StructFieldValue<any>).set(100);
|
||||
assert.strictEqual(lengthFieldValue.get(), 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#serialize", () => {
|
||||
it("should call `serialize` of its `originalValue`", () => {
|
||||
const mockOriginalFieldValue = new MockLengthFieldValue();
|
||||
const mockBufferFieldValue = new MockBufferLikeFieldValue();
|
||||
const lengthFieldValue =
|
||||
new VariableLengthBufferLikeFieldLengthValue(
|
||||
mockOriginalFieldValue,
|
||||
mockBufferFieldValue,
|
||||
);
|
||||
|
||||
const array = {} as any;
|
||||
const offset = {} as any;
|
||||
|
||||
mockOriginalFieldValue.value = 10;
|
||||
mockBufferFieldValue.size = 0;
|
||||
lengthFieldValue.serialize(array, offset);
|
||||
assert.strictEqual(
|
||||
mockOriginalFieldValue.get.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
assert.strictEqual(
|
||||
mockOriginalFieldValue.get.mock.calls[0]?.result,
|
||||
10,
|
||||
);
|
||||
assert.strictEqual(
|
||||
mockOriginalFieldValue.set.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
mockOriginalFieldValue.set.mock.calls[0]?.arguments,
|
||||
[0],
|
||||
);
|
||||
assert.strictEqual(
|
||||
mockOriginalFieldValue.serialize.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
mockOriginalFieldValue.serialize.mock.calls[0]?.arguments,
|
||||
[array, offset],
|
||||
);
|
||||
|
||||
mockOriginalFieldValue.set.mock.resetCalls();
|
||||
mockOriginalFieldValue.serialize.mock.resetCalls();
|
||||
mockBufferFieldValue.size = 100;
|
||||
lengthFieldValue.serialize(array, offset);
|
||||
assert.strictEqual(
|
||||
mockOriginalFieldValue.set.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
mockOriginalFieldValue.set.mock.calls[0]?.arguments,
|
||||
[100],
|
||||
);
|
||||
assert.strictEqual(
|
||||
mockOriginalFieldValue.serialize.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
mockOriginalFieldValue.serialize.mock.calls[0]?.arguments,
|
||||
[array, offset],
|
||||
);
|
||||
});
|
||||
|
||||
it("should stringify its length if `originalValue` is a string", () => {
|
||||
const mockOriginalFieldValue = new MockLengthFieldValue();
|
||||
const mockBufferFieldValue = new MockBufferLikeFieldValue();
|
||||
const lengthFieldValue =
|
||||
new VariableLengthBufferLikeFieldLengthValue(
|
||||
mockOriginalFieldValue,
|
||||
mockBufferFieldValue,
|
||||
);
|
||||
|
||||
const array = {} as any;
|
||||
const offset = {} as any;
|
||||
|
||||
mockOriginalFieldValue.value = "10";
|
||||
mockBufferFieldValue.size = 0;
|
||||
lengthFieldValue.serialize(array, offset);
|
||||
assert.strictEqual(
|
||||
mockOriginalFieldValue.get.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
assert.strictEqual(
|
||||
mockOriginalFieldValue.get.mock.calls[0]?.result,
|
||||
"10",
|
||||
);
|
||||
assert.strictEqual(
|
||||
mockOriginalFieldValue.set.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
mockOriginalFieldValue.set.mock.calls[0]?.arguments,
|
||||
["0"],
|
||||
);
|
||||
assert.strictEqual(
|
||||
mockOriginalFieldValue.serialize.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
mockOriginalFieldValue.serialize.mock.calls[0]?.arguments,
|
||||
[array, offset],
|
||||
);
|
||||
|
||||
mockOriginalFieldValue.set.mock.resetCalls();
|
||||
mockOriginalFieldValue.serialize.mock.resetCalls();
|
||||
mockBufferFieldValue.size = 100;
|
||||
lengthFieldValue.serialize(array, offset);
|
||||
assert.strictEqual(
|
||||
mockOriginalFieldValue.set.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
mockOriginalFieldValue.set.mock.calls[0]?.arguments,
|
||||
["100"],
|
||||
);
|
||||
assert.strictEqual(
|
||||
mockOriginalFieldValue.serialize.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
mockOriginalFieldValue.serialize.mock.calls[0]?.arguments,
|
||||
[array, offset],
|
||||
);
|
||||
});
|
||||
|
||||
it("should stringify its length in specified radix if `originalValue` is a string", () => {
|
||||
const mockOriginalFieldValue = new MockLengthFieldValue();
|
||||
const mockBufferFieldValue = new MockBufferLikeFieldValue();
|
||||
const lengthFieldValue =
|
||||
new VariableLengthBufferLikeFieldLengthValue(
|
||||
mockOriginalFieldValue,
|
||||
mockBufferFieldValue,
|
||||
);
|
||||
|
||||
const radix = 16;
|
||||
mockBufferFieldValue.definition.options.lengthFieldRadix =
|
||||
radix;
|
||||
|
||||
const array = {} as any;
|
||||
const offset = {} as any;
|
||||
|
||||
mockOriginalFieldValue.value = "10";
|
||||
mockBufferFieldValue.size = 0;
|
||||
lengthFieldValue.serialize(array, offset);
|
||||
assert.strictEqual(
|
||||
mockOriginalFieldValue.get.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
assert.strictEqual(
|
||||
mockOriginalFieldValue.get.mock.calls[0]?.result,
|
||||
"10",
|
||||
);
|
||||
assert.strictEqual(
|
||||
mockOriginalFieldValue.set.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
mockOriginalFieldValue.set.mock.calls[0]?.arguments,
|
||||
["0"],
|
||||
);
|
||||
assert.strictEqual(
|
||||
mockOriginalFieldValue.serialize.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
mockOriginalFieldValue.serialize.mock.calls[0]?.arguments,
|
||||
[array, offset],
|
||||
);
|
||||
|
||||
mockOriginalFieldValue.set.mock.resetCalls();
|
||||
mockOriginalFieldValue.serialize.mock.resetCalls();
|
||||
mockBufferFieldValue.size = 100;
|
||||
lengthFieldValue.serialize(array, offset);
|
||||
assert.strictEqual(
|
||||
mockOriginalFieldValue.set.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
mockOriginalFieldValue.set.mock.calls[0]?.arguments,
|
||||
[(100).toString(radix)],
|
||||
);
|
||||
assert.strictEqual(
|
||||
mockOriginalFieldValue.serialize.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
mockOriginalFieldValue.serialize.mock.calls[0]?.arguments,
|
||||
[array, offset],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("VariableLengthBufferLikeStructFieldValue", () => {
|
||||
describe(".constructor", () => {
|
||||
it("should forward parameters", () => {
|
||||
const struct = new StructValue({});
|
||||
|
||||
const lengthField = "foo";
|
||||
const originalLengthFieldValue = new MockLengthFieldValue();
|
||||
struct.set(lengthField, originalLengthFieldValue);
|
||||
|
||||
const bufferFieldDefinition =
|
||||
new VariableLengthBufferLikeFieldDefinition(
|
||||
Uint8ArrayBufferFieldConverter.Instance,
|
||||
{ lengthField },
|
||||
);
|
||||
|
||||
const value = EMPTY_UINT8_ARRAY;
|
||||
|
||||
const bufferFieldValue =
|
||||
new VariableLengthBufferLikeStructFieldValue(
|
||||
bufferFieldDefinition,
|
||||
StructDefaultOptions,
|
||||
struct,
|
||||
value,
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
bufferFieldValue.definition,
|
||||
bufferFieldDefinition,
|
||||
);
|
||||
assert.strictEqual(
|
||||
bufferFieldValue.options,
|
||||
StructDefaultOptions,
|
||||
);
|
||||
assert.strictEqual(bufferFieldValue.struct, struct);
|
||||
assert.deepStrictEqual(bufferFieldValue["value"], value);
|
||||
assert.strictEqual(bufferFieldValue["array"], undefined);
|
||||
assert.strictEqual(bufferFieldValue["length"], undefined);
|
||||
});
|
||||
|
||||
it("should accept initial `array`", () => {
|
||||
const struct = new StructValue({});
|
||||
|
||||
const lengthField = "foo";
|
||||
const originalLengthFieldValue = new MockLengthFieldValue();
|
||||
struct.set(lengthField, originalLengthFieldValue);
|
||||
|
||||
const bufferFieldDefinition =
|
||||
new VariableLengthBufferLikeFieldDefinition(
|
||||
Uint8ArrayBufferFieldConverter.Instance,
|
||||
{ lengthField },
|
||||
);
|
||||
|
||||
const value = new Uint8Array(100);
|
||||
|
||||
const bufferFieldValue =
|
||||
new VariableLengthBufferLikeStructFieldValue(
|
||||
bufferFieldDefinition,
|
||||
StructDefaultOptions,
|
||||
struct,
|
||||
value,
|
||||
value,
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
bufferFieldValue.definition,
|
||||
bufferFieldDefinition,
|
||||
);
|
||||
assert.strictEqual(
|
||||
bufferFieldValue.options,
|
||||
StructDefaultOptions,
|
||||
);
|
||||
assert.strictEqual(bufferFieldValue.struct, struct);
|
||||
assert.deepStrictEqual(bufferFieldValue["value"], value);
|
||||
assert.deepStrictEqual(bufferFieldValue["array"], value);
|
||||
assert.strictEqual(bufferFieldValue["length"], value.length);
|
||||
});
|
||||
|
||||
it("should replace `lengthField` on `struct`", () => {
|
||||
const struct = new StructValue({});
|
||||
|
||||
const lengthField = "foo";
|
||||
const originalLengthFieldValue = new MockLengthFieldValue();
|
||||
struct.set(lengthField, originalLengthFieldValue);
|
||||
|
||||
const bufferFieldDefinition =
|
||||
new VariableLengthBufferLikeFieldDefinition(
|
||||
Uint8ArrayBufferFieldConverter.Instance,
|
||||
{ lengthField },
|
||||
);
|
||||
|
||||
const value = EMPTY_UINT8_ARRAY;
|
||||
|
||||
const bufferFieldValue =
|
||||
new VariableLengthBufferLikeStructFieldValue(
|
||||
bufferFieldDefinition,
|
||||
StructDefaultOptions,
|
||||
struct,
|
||||
value,
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
bufferFieldValue["lengthFieldValue"] instanceof
|
||||
StructFieldValue,
|
||||
);
|
||||
assert.strictEqual(
|
||||
struct.fieldValues[lengthField],
|
||||
bufferFieldValue["lengthFieldValue"],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#getSize", () => {
|
||||
class MockBufferFieldConverter extends BufferFieldConverter<Uint8Array> {
|
||||
override toBuffer = mock.fn((value: Uint8Array): Uint8Array => {
|
||||
return value;
|
||||
});
|
||||
|
||||
override toValue = mock.fn(
|
||||
(arrayBuffer: Uint8Array): Uint8Array => {
|
||||
return arrayBuffer;
|
||||
},
|
||||
);
|
||||
|
||||
size: number | undefined = 0;
|
||||
|
||||
override getSize = mock.fn(
|
||||
(value: Uint8Array): number | undefined => {
|
||||
void value;
|
||||
return this.size;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
it("should return cached size if exist", () => {
|
||||
const struct = new StructValue({});
|
||||
|
||||
const lengthField = "foo";
|
||||
const originalLengthFieldValue = new MockLengthFieldValue();
|
||||
struct.set(lengthField, originalLengthFieldValue);
|
||||
|
||||
const bufferFieldConverter = new MockBufferFieldConverter();
|
||||
const bufferFieldDefinition =
|
||||
new VariableLengthBufferLikeFieldDefinition(
|
||||
bufferFieldConverter,
|
||||
{ lengthField },
|
||||
);
|
||||
|
||||
const value = new Uint8Array(100);
|
||||
|
||||
const bufferFieldValue =
|
||||
new VariableLengthBufferLikeStructFieldValue(
|
||||
bufferFieldDefinition,
|
||||
StructDefaultOptions,
|
||||
struct,
|
||||
value,
|
||||
value,
|
||||
);
|
||||
|
||||
assert.strictEqual(bufferFieldValue.getSize(), 100);
|
||||
assert.strictEqual(
|
||||
bufferFieldConverter.toValue.mock.callCount(),
|
||||
0,
|
||||
);
|
||||
assert.strictEqual(
|
||||
bufferFieldConverter.toBuffer.mock.callCount(),
|
||||
0,
|
||||
);
|
||||
assert.strictEqual(
|
||||
bufferFieldConverter.getSize.mock.callCount(),
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
it("should call `getSize` of its `converter`", () => {
|
||||
const struct = new StructValue({});
|
||||
|
||||
const lengthField = "foo";
|
||||
const originalLengthFieldValue = new MockLengthFieldValue();
|
||||
struct.set(lengthField, originalLengthFieldValue);
|
||||
|
||||
const bufferFieldConverter = new MockBufferFieldConverter();
|
||||
const bufferFieldDefinition =
|
||||
new VariableLengthBufferLikeFieldDefinition(
|
||||
bufferFieldConverter,
|
||||
{ lengthField },
|
||||
);
|
||||
|
||||
const value = new Uint8Array(100);
|
||||
|
||||
const bufferFieldValue =
|
||||
new VariableLengthBufferLikeStructFieldValue(
|
||||
bufferFieldDefinition,
|
||||
StructDefaultOptions,
|
||||
struct,
|
||||
value,
|
||||
);
|
||||
|
||||
bufferFieldConverter.size = 100;
|
||||
assert.strictEqual(bufferFieldValue.getSize(), 100);
|
||||
assert.strictEqual(
|
||||
bufferFieldConverter.toValue.mock.callCount(),
|
||||
0,
|
||||
);
|
||||
assert.strictEqual(
|
||||
bufferFieldConverter.toBuffer.mock.callCount(),
|
||||
0,
|
||||
);
|
||||
assert.strictEqual(
|
||||
bufferFieldConverter.getSize.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
assert.strictEqual(bufferFieldValue["array"], undefined);
|
||||
assert.strictEqual(bufferFieldValue["length"], 100);
|
||||
});
|
||||
|
||||
it("should call `toBuffer` of its `converter` if it does not support `getSize`", () => {
|
||||
const struct = new StructValue({});
|
||||
|
||||
const lengthField = "foo";
|
||||
const originalLengthFieldValue = new MockLengthFieldValue();
|
||||
struct.set(lengthField, originalLengthFieldValue);
|
||||
|
||||
const bufferFieldConverter = new MockBufferFieldConverter();
|
||||
const bufferFieldDefinition =
|
||||
new VariableLengthBufferLikeFieldDefinition(
|
||||
bufferFieldConverter,
|
||||
{ lengthField },
|
||||
);
|
||||
|
||||
const value = new Uint8Array(100);
|
||||
|
||||
const bufferFieldValue =
|
||||
new VariableLengthBufferLikeStructFieldValue(
|
||||
bufferFieldDefinition,
|
||||
StructDefaultOptions,
|
||||
struct,
|
||||
value,
|
||||
);
|
||||
|
||||
bufferFieldConverter.size = undefined;
|
||||
assert.strictEqual(bufferFieldValue.getSize(), 100);
|
||||
assert.strictEqual(
|
||||
bufferFieldConverter.toValue.mock.callCount(),
|
||||
0,
|
||||
);
|
||||
assert.strictEqual(
|
||||
bufferFieldConverter.toBuffer.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
assert.strictEqual(
|
||||
bufferFieldConverter.getSize.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
assert.strictEqual(bufferFieldValue["array"], value);
|
||||
assert.strictEqual(bufferFieldValue["length"], 100);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#set", () => {
|
||||
it("should call `BufferLikeFieldValue#set`", () => {
|
||||
const struct = new StructValue({});
|
||||
|
||||
const lengthField = "foo";
|
||||
const originalLengthFieldValue = new MockLengthFieldValue();
|
||||
struct.set(lengthField, originalLengthFieldValue);
|
||||
|
||||
const bufferFieldDefinition =
|
||||
new VariableLengthBufferLikeFieldDefinition(
|
||||
Uint8ArrayBufferFieldConverter.Instance,
|
||||
{ lengthField },
|
||||
);
|
||||
|
||||
const value = new Uint8Array(100);
|
||||
|
||||
const bufferFieldValue =
|
||||
new VariableLengthBufferLikeStructFieldValue(
|
||||
bufferFieldDefinition,
|
||||
StructDefaultOptions,
|
||||
struct,
|
||||
value,
|
||||
value,
|
||||
);
|
||||
|
||||
const newValue = new ArrayBuffer(100);
|
||||
bufferFieldValue.set(newValue);
|
||||
assert.strictEqual(bufferFieldValue.get(), newValue);
|
||||
assert.strictEqual(bufferFieldValue["array"], undefined);
|
||||
});
|
||||
|
||||
it("should clear length", () => {
|
||||
const struct = new StructValue({});
|
||||
|
||||
const lengthField = "foo";
|
||||
const originalLengthFieldValue = new MockLengthFieldValue();
|
||||
struct.set(lengthField, originalLengthFieldValue);
|
||||
|
||||
const bufferFieldDefinition =
|
||||
new VariableLengthBufferLikeFieldDefinition(
|
||||
Uint8ArrayBufferFieldConverter.Instance,
|
||||
{ lengthField },
|
||||
);
|
||||
|
||||
const value = new Uint8Array(100);
|
||||
|
||||
const bufferFieldValue =
|
||||
new VariableLengthBufferLikeStructFieldValue(
|
||||
bufferFieldDefinition,
|
||||
StructDefaultOptions,
|
||||
struct,
|
||||
value,
|
||||
value,
|
||||
);
|
||||
|
||||
const newValue = new ArrayBuffer(100);
|
||||
bufferFieldValue.set(newValue);
|
||||
assert.strictEqual(bufferFieldValue["length"], undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("VariableLengthBufferLikeFieldDefinition", () => {
|
||||
describe("#getSize", () => {
|
||||
it("should always return `0`", () => {
|
||||
const definition = new VariableLengthBufferLikeFieldDefinition(
|
||||
Uint8ArrayBufferFieldConverter.Instance,
|
||||
{ lengthField: "foo" },
|
||||
);
|
||||
assert.strictEqual(definition.getSize(), 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#getDeserializeSize", () => {
|
||||
it("should return value of its `lengthField`", () => {
|
||||
const struct = new StructValue({});
|
||||
|
||||
const lengthField = "foo";
|
||||
const originalLengthFieldValue = new MockLengthFieldValue();
|
||||
struct.set(lengthField, originalLengthFieldValue);
|
||||
|
||||
const definition = new VariableLengthBufferLikeFieldDefinition(
|
||||
Uint8ArrayBufferFieldConverter.Instance,
|
||||
{ lengthField },
|
||||
);
|
||||
|
||||
originalLengthFieldValue.value = 0;
|
||||
assert.strictEqual(definition["getDeserializeSize"](struct), 0);
|
||||
assert.strictEqual(
|
||||
originalLengthFieldValue.get.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
|
||||
originalLengthFieldValue.get.mock.resetCalls();
|
||||
originalLengthFieldValue.value = 100;
|
||||
assert.strictEqual(
|
||||
definition["getDeserializeSize"](struct),
|
||||
100,
|
||||
);
|
||||
assert.strictEqual(
|
||||
originalLengthFieldValue.get.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it("should return value of its `lengthField` as number", () => {
|
||||
const struct = new StructValue({});
|
||||
|
||||
const lengthField = "foo";
|
||||
const originalLengthFieldValue = new MockLengthFieldValue();
|
||||
struct.set(lengthField, originalLengthFieldValue);
|
||||
|
||||
const definition = new VariableLengthBufferLikeFieldDefinition(
|
||||
Uint8ArrayBufferFieldConverter.Instance,
|
||||
{ lengthField },
|
||||
);
|
||||
|
||||
originalLengthFieldValue.value = "0";
|
||||
assert.strictEqual(definition["getDeserializeSize"](struct), 0);
|
||||
assert.strictEqual(
|
||||
originalLengthFieldValue.get.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
|
||||
originalLengthFieldValue.get.mock.resetCalls();
|
||||
originalLengthFieldValue.value = "100";
|
||||
assert.strictEqual(
|
||||
definition["getDeserializeSize"](struct),
|
||||
100,
|
||||
);
|
||||
assert.strictEqual(
|
||||
originalLengthFieldValue.get.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it("should return value of its `lengthField` as number with specified radix", () => {
|
||||
const struct = new StructValue({});
|
||||
|
||||
const lengthField = "foo";
|
||||
const originalLengthFieldValue = new MockLengthFieldValue();
|
||||
struct.set(lengthField, originalLengthFieldValue);
|
||||
|
||||
const radix = 8;
|
||||
const definition = new VariableLengthBufferLikeFieldDefinition(
|
||||
Uint8ArrayBufferFieldConverter.Instance,
|
||||
{ lengthField, lengthFieldRadix: radix },
|
||||
);
|
||||
|
||||
originalLengthFieldValue.value = "0";
|
||||
assert.strictEqual(definition["getDeserializeSize"](struct), 0);
|
||||
assert.strictEqual(
|
||||
originalLengthFieldValue.get.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
|
||||
originalLengthFieldValue.get.mock.resetCalls();
|
||||
originalLengthFieldValue.value = "100";
|
||||
assert.strictEqual(
|
||||
definition["getDeserializeSize"](struct),
|
||||
Number.parseInt("100", radix),
|
||||
);
|
||||
assert.strictEqual(
|
||||
originalLengthFieldValue.get.mock.callCount(),
|
||||
1,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#create", () => {
|
||||
it("should create a `VariableLengthBufferLikeStructFieldValue`", () => {
|
||||
const struct = new StructValue({});
|
||||
|
||||
const lengthField = "foo";
|
||||
const originalLengthFieldValue = new MockLengthFieldValue();
|
||||
struct.set(lengthField, originalLengthFieldValue);
|
||||
|
||||
const definition = new VariableLengthBufferLikeFieldDefinition(
|
||||
Uint8ArrayBufferFieldConverter.Instance,
|
||||
{ lengthField },
|
||||
);
|
||||
|
||||
const value = new Uint8Array(100);
|
||||
const bufferFieldValue = definition.create(
|
||||
StructDefaultOptions,
|
||||
struct,
|
||||
value,
|
||||
);
|
||||
|
||||
assert.strictEqual(bufferFieldValue.definition, definition);
|
||||
assert.strictEqual(
|
||||
bufferFieldValue.options,
|
||||
StructDefaultOptions,
|
||||
);
|
||||
assert.strictEqual(bufferFieldValue.struct, struct);
|
||||
assert.strictEqual(bufferFieldValue["value"], value);
|
||||
assert.strictEqual(bufferFieldValue["array"], undefined);
|
||||
assert.strictEqual(bufferFieldValue["length"], undefined);
|
||||
});
|
||||
|
||||
it("should create a `VariableLengthBufferLikeStructFieldValue` with `arrayBuffer`", () => {
|
||||
const struct = new StructValue({});
|
||||
|
||||
const lengthField = "foo";
|
||||
const originalLengthFieldValue = new MockLengthFieldValue();
|
||||
struct.set(lengthField, originalLengthFieldValue);
|
||||
|
||||
const definition = new VariableLengthBufferLikeFieldDefinition(
|
||||
Uint8ArrayBufferFieldConverter.Instance,
|
||||
{ lengthField },
|
||||
);
|
||||
|
||||
const value = new Uint8Array(100);
|
||||
const bufferFieldValue = definition.create(
|
||||
StructDefaultOptions,
|
||||
struct,
|
||||
value,
|
||||
value,
|
||||
);
|
||||
|
||||
assert.strictEqual(bufferFieldValue.definition, definition);
|
||||
assert.strictEqual(
|
||||
bufferFieldValue.options,
|
||||
StructDefaultOptions,
|
||||
);
|
||||
assert.strictEqual(bufferFieldValue.struct, struct);
|
||||
assert.strictEqual(bufferFieldValue["value"], value);
|
||||
assert.strictEqual(bufferFieldValue["array"], value);
|
||||
assert.strictEqual(bufferFieldValue["length"], 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,199 +0,0 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import type {
|
||||
StructFieldDefinition,
|
||||
StructOptions,
|
||||
StructValue,
|
||||
} from "../../basic/index.js";
|
||||
import { StructFieldValue } from "../../basic/index.js";
|
||||
import type { KeysOfType } from "../../utils.js";
|
||||
|
||||
import type { BufferFieldConverter } from "./base.js";
|
||||
import { BufferLikeFieldDefinition, BufferLikeFieldValue } from "./base.js";
|
||||
|
||||
export type LengthField<TFields> = KeysOfType<TFields, number | string>;
|
||||
|
||||
export interface VariableLengthBufferLikeFieldOptions<
|
||||
TFields extends object = object,
|
||||
TLengthField extends LengthField<TFields> = any,
|
||||
> {
|
||||
/**
|
||||
* The name of the field that contains the length of the buffer.
|
||||
*
|
||||
* This field must be a `number` or `string` (can't be `bigint`) field.
|
||||
*/
|
||||
lengthField: TLengthField;
|
||||
|
||||
/**
|
||||
* If the `lengthField` refers to a string field,
|
||||
* what radix to use when converting the string to a number.
|
||||
*
|
||||
* @default 10
|
||||
*/
|
||||
lengthFieldRadix?: number;
|
||||
}
|
||||
|
||||
export class VariableLengthBufferLikeFieldDefinition<
|
||||
TConverter extends BufferFieldConverter = BufferFieldConverter,
|
||||
TOptions extends
|
||||
VariableLengthBufferLikeFieldOptions = VariableLengthBufferLikeFieldOptions,
|
||||
TTypeScriptType = TConverter["TTypeScriptType"],
|
||||
> extends BufferLikeFieldDefinition<
|
||||
TConverter,
|
||||
TOptions,
|
||||
TOptions["lengthField"],
|
||||
TTypeScriptType
|
||||
> {
|
||||
override getSize(): number {
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected override getDeserializeSize(struct: StructValue) {
|
||||
let value = struct.value[this.options.lengthField] as number | string;
|
||||
if (typeof value === "string") {
|
||||
value = Number.parseInt(value, this.options.lengthFieldRadix ?? 10);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
override create(
|
||||
options: Readonly<StructOptions>,
|
||||
struct: StructValue,
|
||||
value: TTypeScriptType,
|
||||
array?: Uint8Array,
|
||||
): VariableLengthBufferLikeStructFieldValue<this> {
|
||||
return new VariableLengthBufferLikeStructFieldValue(
|
||||
this,
|
||||
options,
|
||||
struct,
|
||||
value,
|
||||
array,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class VariableLengthBufferLikeStructFieldValue<
|
||||
TDefinition extends
|
||||
VariableLengthBufferLikeFieldDefinition = VariableLengthBufferLikeFieldDefinition,
|
||||
> extends BufferLikeFieldValue<TDefinition> {
|
||||
protected length: number | undefined;
|
||||
|
||||
protected lengthFieldValue: VariableLengthBufferLikeFieldLengthValue;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/max-params
|
||||
constructor(
|
||||
definition: TDefinition,
|
||||
options: Readonly<StructOptions>,
|
||||
struct: StructValue,
|
||||
value: TDefinition["TValue"],
|
||||
array?: Uint8Array,
|
||||
) {
|
||||
super(definition, options, struct, value, array);
|
||||
|
||||
if (array) {
|
||||
this.length = array.length;
|
||||
}
|
||||
|
||||
// Patch the associated length field.
|
||||
const lengthField = this.definition.options.lengthField as PropertyKey;
|
||||
|
||||
const originalValue = struct.get(lengthField);
|
||||
this.lengthFieldValue = new VariableLengthBufferLikeFieldLengthValue(
|
||||
originalValue,
|
||||
this,
|
||||
);
|
||||
struct.set(lengthField, this.lengthFieldValue);
|
||||
}
|
||||
|
||||
#tryGetSize() {
|
||||
const length = this.definition.converter.getSize(this.value);
|
||||
if (length !== undefined && length < 0) {
|
||||
throw new Error("Invalid length");
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
override getSize(): number {
|
||||
if (this.length === undefined) {
|
||||
// first try to get the size from the converter
|
||||
this.length = this.#tryGetSize();
|
||||
}
|
||||
|
||||
if (this.length === undefined) {
|
||||
// The converter doesn't know the size, so convert the value to a buffer to get its size
|
||||
this.array = this.definition.converter.toBuffer(this.value);
|
||||
this.length = this.array.length;
|
||||
}
|
||||
|
||||
return this.length;
|
||||
}
|
||||
|
||||
override set(value: unknown) {
|
||||
super.set(value);
|
||||
this.array = undefined;
|
||||
this.length = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Not using `VariableLengthBufferLikeStructFieldValue` directly makes writing tests much easier...
|
||||
type VariableLengthBufferLikeFieldValueLike = StructFieldValue<
|
||||
StructFieldDefinition<
|
||||
VariableLengthBufferLikeFieldOptions<any, any>,
|
||||
any,
|
||||
any
|
||||
>
|
||||
>;
|
||||
|
||||
export class VariableLengthBufferLikeFieldLengthValue extends StructFieldValue<
|
||||
StructFieldDefinition<unknown, unknown, PropertyKey>
|
||||
> {
|
||||
protected originalValue: StructFieldValue<
|
||||
StructFieldDefinition<unknown, unknown, PropertyKey>
|
||||
>;
|
||||
|
||||
protected bufferValue: VariableLengthBufferLikeFieldValueLike;
|
||||
|
||||
constructor(
|
||||
originalValue: StructFieldValue<
|
||||
StructFieldDefinition<unknown, unknown, PropertyKey>
|
||||
>,
|
||||
bufferValue: VariableLengthBufferLikeFieldValueLike,
|
||||
) {
|
||||
super(
|
||||
originalValue.definition,
|
||||
originalValue.options,
|
||||
originalValue.struct,
|
||||
0,
|
||||
);
|
||||
this.originalValue = originalValue;
|
||||
this.bufferValue = bufferValue;
|
||||
}
|
||||
|
||||
override getSize() {
|
||||
return this.originalValue.getSize();
|
||||
}
|
||||
|
||||
override get() {
|
||||
let value: string | number = this.bufferValue.getSize();
|
||||
|
||||
const originalValue = this.originalValue.get();
|
||||
if (typeof originalValue === "string") {
|
||||
value = value.toString(
|
||||
this.bufferValue.definition.options.lengthFieldRadix ?? 10,
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
override set() {
|
||||
// Ignore setting
|
||||
// It will always be in sync with the buffer size
|
||||
}
|
||||
|
||||
serialize(array: Uint8Array, offset: number) {
|
||||
this.originalValue.set(this.get());
|
||||
this.originalValue.serialize(array, offset);
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export * from "./bigint.js";
|
||||
export * from "./buffer/index.js";
|
||||
export * from "./number.js";
|
|
@ -1,70 +0,0 @@
|
|||
import {
|
||||
getInt16,
|
||||
getInt32,
|
||||
getInt8,
|
||||
getUint16,
|
||||
getUint32,
|
||||
setInt16,
|
||||
setInt32,
|
||||
setUint16,
|
||||
setUint32,
|
||||
} from "@yume-chan/no-data-view";
|
||||
import type { NumberFieldVariant } from "./number-reexports.js";
|
||||
|
||||
export const Uint8: NumberFieldVariant = {
|
||||
signed: false,
|
||||
size: 1,
|
||||
deserialize(array) {
|
||||
return array[0]!;
|
||||
},
|
||||
serialize(array, offset, value) {
|
||||
array[offset] = value;
|
||||
},
|
||||
};
|
||||
|
||||
export const Int8: NumberFieldVariant = {
|
||||
signed: true,
|
||||
size: 1,
|
||||
deserialize(array) {
|
||||
return getInt8(array, 0);
|
||||
},
|
||||
serialize(array, offset, value) {
|
||||
array[offset] = value;
|
||||
},
|
||||
};
|
||||
|
||||
export const Uint16: NumberFieldVariant = {
|
||||
signed: false,
|
||||
size: 2,
|
||||
deserialize(array, littleEndian) {
|
||||
return getUint16(array, 0, littleEndian);
|
||||
},
|
||||
serialize: setUint16,
|
||||
};
|
||||
|
||||
export const Int16: NumberFieldVariant = {
|
||||
signed: true,
|
||||
size: 2,
|
||||
deserialize(array, littleEndian) {
|
||||
return getInt16(array, 0, littleEndian);
|
||||
},
|
||||
serialize: setInt16,
|
||||
};
|
||||
|
||||
export const Uint32: NumberFieldVariant = {
|
||||
signed: false,
|
||||
size: 4,
|
||||
deserialize(array, littleEndian) {
|
||||
return getUint32(array, 0, littleEndian);
|
||||
},
|
||||
serialize: setUint32,
|
||||
};
|
||||
|
||||
export const Int32: NumberFieldVariant = {
|
||||
signed: true,
|
||||
size: 4,
|
||||
deserialize(array, littleEndian) {
|
||||
return getInt32(array, 0, littleEndian);
|
||||
},
|
||||
serialize: setInt32,
|
||||
};
|
|
@ -1,13 +0,0 @@
|
|||
export * as NumberFieldVariant from "./number-namespace.js";
|
||||
|
||||
export interface NumberFieldVariant {
|
||||
signed: boolean;
|
||||
size: number;
|
||||
deserialize(array: Uint8Array, littleEndian: boolean): number;
|
||||
serialize(
|
||||
array: Uint8Array,
|
||||
offset: number,
|
||||
value: number,
|
||||
littleEndian: boolean,
|
||||
): void;
|
||||
}
|
|
@ -1,344 +0,0 @@
|
|||
import * as assert from "node:assert";
|
||||
import { describe, it, mock } from "node:test";
|
||||
|
||||
import type { ExactReadable } from "../basic/index.js";
|
||||
import { StructDefaultOptions, StructValue } from "../basic/index.js";
|
||||
|
||||
import { NumberFieldDefinition, NumberFieldVariant } from "./number.js";
|
||||
|
||||
function testEndian(
|
||||
type: NumberFieldVariant,
|
||||
min: number,
|
||||
max: number,
|
||||
littleEndian: boolean,
|
||||
) {
|
||||
it(`min = ${min}`, () => {
|
||||
const buffer = new ArrayBuffer(type.size);
|
||||
const view = new DataView(buffer);
|
||||
(
|
||||
view[
|
||||
`set${type.signed ? "I" : "Ui"}nt${
|
||||
type.size * 8
|
||||
}` as keyof DataView
|
||||
] as (offset: number, value: number, littleEndian: boolean) => void
|
||||
)(0, min, littleEndian);
|
||||
const output = type.deserialize(new Uint8Array(buffer), littleEndian);
|
||||
assert.strictEqual(output, min);
|
||||
});
|
||||
|
||||
it("1", () => {
|
||||
const buffer = new ArrayBuffer(type.size);
|
||||
const view = new DataView(buffer);
|
||||
const input = 1;
|
||||
(
|
||||
view[
|
||||
`set${type.signed ? "I" : "Ui"}nt${
|
||||
type.size * 8
|
||||
}` as keyof DataView
|
||||
] as (offset: number, value: number, littleEndian: boolean) => void
|
||||
)(0, input, littleEndian);
|
||||
const output = type.deserialize(new Uint8Array(buffer), littleEndian);
|
||||
assert.strictEqual(output, input);
|
||||
});
|
||||
|
||||
it(`max = ${max}`, () => {
|
||||
const buffer = new ArrayBuffer(type.size);
|
||||
const view = new DataView(buffer);
|
||||
(
|
||||
view[
|
||||
`set${type.signed ? "I" : "Ui"}nt${
|
||||
type.size * 8
|
||||
}` as keyof DataView
|
||||
] as (offset: number, value: number, littleEndian: boolean) => void
|
||||
)(0, max, littleEndian);
|
||||
const output = type.deserialize(new Uint8Array(buffer), littleEndian);
|
||||
assert.strictEqual(output, max);
|
||||
});
|
||||
}
|
||||
|
||||
function testDeserialize(type: NumberFieldVariant) {
|
||||
if (type.size === 1) {
|
||||
if (type.signed) {
|
||||
const MIN = -(2 ** (type.size * 8 - 1));
|
||||
const MAX = -MIN - 1;
|
||||
testEndian(type, MIN, MAX, false);
|
||||
} else {
|
||||
const MAX = 2 ** (type.size * 8) - 1;
|
||||
testEndian(type, 0, MAX, false);
|
||||
}
|
||||
} else {
|
||||
if (type.signed) {
|
||||
const MIN = -(2 ** (type.size * 8 - 1));
|
||||
const MAX = -MIN - 1;
|
||||
describe("big endian", () => {
|
||||
testEndian(type, MIN, MAX, false);
|
||||
});
|
||||
describe("little endian", () => {
|
||||
testEndian(type, MIN, MAX, true);
|
||||
});
|
||||
} else {
|
||||
const MAX = 2 ** (type.size * 8) - 1;
|
||||
describe("big endian", () => {
|
||||
testEndian(type, 0, MAX, false);
|
||||
});
|
||||
describe("little endian", () => {
|
||||
testEndian(type, 0, MAX, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("Types", () => {
|
||||
describe("Number", () => {
|
||||
describe("NumberFieldVariant", () => {
|
||||
describe("Int8", () => {
|
||||
const key = "Int8";
|
||||
|
||||
it("basic", () => {
|
||||
assert.strictEqual(NumberFieldVariant[key].size, 1);
|
||||
});
|
||||
|
||||
testDeserialize(NumberFieldVariant[key]);
|
||||
});
|
||||
|
||||
describe("Uint8", () => {
|
||||
const key = "Uint8";
|
||||
|
||||
it("basic", () => {
|
||||
assert.strictEqual(NumberFieldVariant[key].size, 1);
|
||||
});
|
||||
|
||||
testDeserialize(NumberFieldVariant[key]);
|
||||
});
|
||||
|
||||
describe("Int16", () => {
|
||||
const key = "Int16";
|
||||
|
||||
it("basic", () => {
|
||||
assert.strictEqual(NumberFieldVariant[key].size, 2);
|
||||
});
|
||||
|
||||
testDeserialize(NumberFieldVariant[key]);
|
||||
});
|
||||
|
||||
describe("Uint16", () => {
|
||||
const key = "Uint16";
|
||||
|
||||
it("basic", () => {
|
||||
assert.strictEqual(NumberFieldVariant[key].size, 2);
|
||||
});
|
||||
|
||||
testDeserialize(NumberFieldVariant[key]);
|
||||
});
|
||||
|
||||
describe("Int32", () => {
|
||||
const key = "Int32";
|
||||
|
||||
it("basic", () => {
|
||||
assert.strictEqual(NumberFieldVariant[key].size, 4);
|
||||
});
|
||||
|
||||
testDeserialize(NumberFieldVariant[key]);
|
||||
});
|
||||
|
||||
describe("Uint32", () => {
|
||||
const key = "Uint32";
|
||||
|
||||
it("basic", () => {
|
||||
assert.strictEqual(NumberFieldVariant[key].size, 4);
|
||||
});
|
||||
|
||||
testDeserialize(NumberFieldVariant[key]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("NumberFieldDefinition", () => {
|
||||
describe("#getSize", () => {
|
||||
it("should return size of its type", () => {
|
||||
assert.strictEqual(
|
||||
new NumberFieldDefinition(
|
||||
NumberFieldVariant.Int8,
|
||||
).getSize(),
|
||||
1,
|
||||
);
|
||||
assert.strictEqual(
|
||||
new NumberFieldDefinition(
|
||||
NumberFieldVariant.Uint8,
|
||||
).getSize(),
|
||||
1,
|
||||
);
|
||||
assert.strictEqual(
|
||||
new NumberFieldDefinition(
|
||||
NumberFieldVariant.Int16,
|
||||
).getSize(),
|
||||
2,
|
||||
);
|
||||
assert.strictEqual(
|
||||
new NumberFieldDefinition(
|
||||
NumberFieldVariant.Uint16,
|
||||
).getSize(),
|
||||
2,
|
||||
);
|
||||
assert.strictEqual(
|
||||
new NumberFieldDefinition(
|
||||
NumberFieldVariant.Int32,
|
||||
).getSize(),
|
||||
4,
|
||||
);
|
||||
assert.strictEqual(
|
||||
new NumberFieldDefinition(
|
||||
NumberFieldVariant.Uint32,
|
||||
).getSize(),
|
||||
4,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#deserialize", () => {
|
||||
it("should deserialize Uint8", () => {
|
||||
const readExactly = mock.fn(
|
||||
() => new Uint8Array([1, 2, 3, 4]),
|
||||
);
|
||||
const stream: ExactReadable = { position: 0, readExactly };
|
||||
|
||||
const definition = new NumberFieldDefinition(
|
||||
NumberFieldVariant.Uint8,
|
||||
);
|
||||
const struct = new StructValue({});
|
||||
const value = definition.deserialize(
|
||||
StructDefaultOptions,
|
||||
stream,
|
||||
struct,
|
||||
);
|
||||
|
||||
assert.strictEqual(value.get(), 1);
|
||||
assert.strictEqual(readExactly.mock.callCount(), 1);
|
||||
assert.deepStrictEqual(
|
||||
readExactly.mock.calls[0]?.arguments,
|
||||
[NumberFieldVariant.Uint8.size],
|
||||
);
|
||||
});
|
||||
|
||||
it("should deserialize Uint16", () => {
|
||||
const readExactly = mock.fn(
|
||||
() => new Uint8Array([1, 2, 3, 4]),
|
||||
);
|
||||
const stream: ExactReadable = { position: 0, readExactly };
|
||||
|
||||
const definition = new NumberFieldDefinition(
|
||||
NumberFieldVariant.Uint16,
|
||||
);
|
||||
const struct = new StructValue({});
|
||||
const value = definition.deserialize(
|
||||
StructDefaultOptions,
|
||||
stream,
|
||||
struct,
|
||||
);
|
||||
|
||||
assert.strictEqual(value.get(), (1 << 8) | 2);
|
||||
assert.strictEqual(readExactly.mock.callCount(), 1);
|
||||
assert.deepStrictEqual(
|
||||
readExactly.mock.calls[0]?.arguments,
|
||||
[NumberFieldVariant.Uint16.size],
|
||||
);
|
||||
});
|
||||
|
||||
it("should deserialize Uint16LE", () => {
|
||||
const readExactly = mock.fn(
|
||||
() => new Uint8Array([1, 2, 3, 4]),
|
||||
);
|
||||
const stream: ExactReadable = { position: 0, readExactly };
|
||||
|
||||
const definition = new NumberFieldDefinition(
|
||||
NumberFieldVariant.Uint16,
|
||||
);
|
||||
const struct = new StructValue({});
|
||||
const value = definition.deserialize(
|
||||
{ ...StructDefaultOptions, littleEndian: true },
|
||||
stream,
|
||||
struct,
|
||||
);
|
||||
|
||||
assert.strictEqual(value.get(), (2 << 8) | 1);
|
||||
assert.strictEqual(readExactly.mock.callCount(), 1);
|
||||
assert.deepStrictEqual(
|
||||
readExactly.mock.calls[0]?.arguments,
|
||||
[NumberFieldVariant.Uint16.size],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("NumberFieldValue", () => {
|
||||
describe("#getSize", () => {
|
||||
it("should return size of its definition", () => {
|
||||
const struct = new StructValue({});
|
||||
|
||||
assert.strictEqual(
|
||||
new NumberFieldDefinition(NumberFieldVariant.Int8)
|
||||
.create(StructDefaultOptions, struct, 42)
|
||||
.getSize(),
|
||||
1,
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
new NumberFieldDefinition(NumberFieldVariant.Uint8)
|
||||
.create(StructDefaultOptions, struct, 42)
|
||||
.getSize(),
|
||||
1,
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
new NumberFieldDefinition(NumberFieldVariant.Int16)
|
||||
.create(StructDefaultOptions, struct, 42)
|
||||
.getSize(),
|
||||
2,
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
new NumberFieldDefinition(NumberFieldVariant.Uint16)
|
||||
.create(StructDefaultOptions, struct, 42)
|
||||
.getSize(),
|
||||
2,
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
new NumberFieldDefinition(NumberFieldVariant.Int32)
|
||||
.create(StructDefaultOptions, struct, 42)
|
||||
.getSize(),
|
||||
4,
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
new NumberFieldDefinition(NumberFieldVariant.Uint32)
|
||||
.create(StructDefaultOptions, struct, 42)
|
||||
.getSize(),
|
||||
4,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#serialize", () => {
|
||||
it("should serialize uint8", () => {
|
||||
const definition = new NumberFieldDefinition(
|
||||
NumberFieldVariant.Int8,
|
||||
);
|
||||
const struct = new StructValue({});
|
||||
const value = definition.create(
|
||||
StructDefaultOptions,
|
||||
struct,
|
||||
42,
|
||||
);
|
||||
|
||||
const array = new Uint8Array(10);
|
||||
value.serialize(array, 2);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
Array.from(array),
|
||||
[0, 0, 42, 0, 0, 0, 0, 0, 0, 0],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,78 +0,0 @@
|
|||
import type {
|
||||
AsyncExactReadable,
|
||||
ExactReadable,
|
||||
StructOptions,
|
||||
StructValue,
|
||||
} from "../basic/index.js";
|
||||
import { StructFieldDefinition, StructFieldValue } from "../basic/index.js";
|
||||
import { SyncPromise } from "../sync-promise.js";
|
||||
import type { ValueOrPromise } from "../utils.js";
|
||||
import type { NumberFieldVariant } from "./number-reexports.js";
|
||||
|
||||
export * from "./number-reexports.js";
|
||||
|
||||
export class NumberFieldDefinition<
|
||||
TVariant extends NumberFieldVariant = NumberFieldVariant,
|
||||
TTypeScriptType = number,
|
||||
> extends StructFieldDefinition<void, TTypeScriptType> {
|
||||
readonly variant: TVariant;
|
||||
|
||||
constructor(variant: TVariant, typescriptType?: TTypeScriptType) {
|
||||
void typescriptType;
|
||||
super();
|
||||
this.variant = variant;
|
||||
}
|
||||
|
||||
getSize(): number {
|
||||
return this.variant.size;
|
||||
}
|
||||
|
||||
create(
|
||||
options: Readonly<StructOptions>,
|
||||
struct: StructValue,
|
||||
value: TTypeScriptType,
|
||||
): NumberFieldValue<this> {
|
||||
return new NumberFieldValue(this, options, struct, value);
|
||||
}
|
||||
|
||||
override deserialize(
|
||||
options: Readonly<StructOptions>,
|
||||
stream: ExactReadable,
|
||||
struct: StructValue,
|
||||
): NumberFieldValue<this>;
|
||||
override deserialize(
|
||||
options: Readonly<StructOptions>,
|
||||
stream: AsyncExactReadable,
|
||||
struct: StructValue,
|
||||
): Promise<NumberFieldValue<this>>;
|
||||
override deserialize(
|
||||
options: Readonly<StructOptions>,
|
||||
stream: ExactReadable | AsyncExactReadable,
|
||||
struct: StructValue,
|
||||
): ValueOrPromise<NumberFieldValue<this>> {
|
||||
return SyncPromise.try(() => {
|
||||
return stream.readExactly(this.getSize());
|
||||
})
|
||||
.then((array) => {
|
||||
const value = this.variant.deserialize(
|
||||
array,
|
||||
options.littleEndian,
|
||||
);
|
||||
return this.create(options, struct, value as never);
|
||||
})
|
||||
.valueOrPromise();
|
||||
}
|
||||
}
|
||||
|
||||
export class NumberFieldValue<
|
||||
TDefinition extends NumberFieldDefinition<NumberFieldVariant, unknown>,
|
||||
> extends StructFieldValue<TDefinition> {
|
||||
serialize(array: Uint8Array, offset: number): void {
|
||||
this.definition.variant.serialize(
|
||||
array,
|
||||
offset,
|
||||
this.value as never,
|
||||
this.options.littleEndian,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
import * as assert from "node:assert";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import { placeholder } from "./utils.js";
|
||||
|
||||
describe("placeholder", () => {
|
||||
it("should return `undefined`", () => {
|
||||
assert.ok(placeholder);
|
||||
});
|
||||
});
|
|
@ -1,58 +1,3 @@
|
|||
/**
|
||||
* When evaluating a very complex generic type alias,
|
||||
* tell TypeScript to use `T`, instead of current type alias' name, as the result type name
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```ts
|
||||
* type WithIdentity<T> = Identity<SomeType<T>>;
|
||||
* type WithoutIdentity<T> = SomeType<T>;
|
||||
*
|
||||
* type WithIdentityResult = WithIdentity<number>;
|
||||
* // Hover on this one shows `SomeType<number>`
|
||||
*
|
||||
* type WithoutIdentityResult = WithoutIdentity<number>;
|
||||
* // Hover on this one shows `WithoutIdentity<number>`
|
||||
* ```
|
||||
*/
|
||||
export type Identity<T> = T;
|
||||
|
||||
/**
|
||||
* Collapse an intersection type (`{ foo: string } & { bar: number }`) to a simple type (`{ foo: string, bar: number }`)
|
||||
*/
|
||||
export type Evaluate<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
|
||||
|
||||
/**
|
||||
* Overwrite fields in `TBase` with fields in `TNew`
|
||||
*/
|
||||
export type Overwrite<TBase extends object, TNew extends object> = Evaluate<
|
||||
Omit<TBase, keyof TNew> & TNew
|
||||
>;
|
||||
|
||||
/**
|
||||
* Remove fields with `never` type
|
||||
*/
|
||||
export type OmitNever<T> = Pick<
|
||||
T,
|
||||
{ [K in keyof T]: [T[K]] extends [never] ? never : K }[keyof T]
|
||||
>;
|
||||
|
||||
/**
|
||||
* Extract keys of fields in `T` that has type `TValue`
|
||||
*/
|
||||
export type KeysOfType<T, TValue> = {
|
||||
[TKey in keyof T]: T[TKey] extends TValue ? TKey : never;
|
||||
}[keyof T];
|
||||
|
||||
export type ValueOrPromise<T> = T | PromiseLike<T>;
|
||||
|
||||
/**
|
||||
* Returns a (fake) value of the given type.
|
||||
*/
|
||||
export function placeholder<T>(): T {
|
||||
return undefined as unknown as T;
|
||||
}
|
||||
|
||||
// This library can't use `@types/node` or `lib: dom`
|
||||
// because they will pollute the global scope
|
||||
// So `TextEncoder` and `TextDecoder` types are not available
|
||||
|
@ -95,3 +40,7 @@ export function decodeUtf8(buffer: ArrayBufferView | ArrayBuffer): string {
|
|||
// but this method is not for stream mode, so the instance can be reused
|
||||
return SharedDecoder.decode(buffer);
|
||||
}
|
||||
|
||||
export type MaybePromise<T> = T | Promise<T>;
|
||||
|
||||
export type MaybePromiseLike<T> = T | PromiseLike<T>;
|
||||
|
|
153
pnpm-lock.yaml
generated
153
pnpm-lock.yaml
generated
|
@ -52,8 +52,8 @@ importers:
|
|||
libraries/adb:
|
||||
dependencies:
|
||||
'@yume-chan/async':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
'@yume-chan/event':
|
||||
specifier: workspace:^
|
||||
version: link:../event
|
||||
|
@ -145,8 +145,8 @@ importers:
|
|||
specifier: workspace:^
|
||||
version: link:../adb
|
||||
'@yume-chan/async':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
'@yume-chan/event':
|
||||
specifier: workspace:^
|
||||
version: link:../event
|
||||
|
@ -254,8 +254,8 @@ importers:
|
|||
libraries/event:
|
||||
dependencies:
|
||||
'@yume-chan/async':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22.7.7
|
||||
|
@ -331,8 +331,8 @@ importers:
|
|||
libraries/scrcpy:
|
||||
dependencies:
|
||||
'@yume-chan/async':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
'@yume-chan/no-data-view':
|
||||
specifier: workspace:^
|
||||
version: link:../no-data-view
|
||||
|
@ -365,8 +365,8 @@ importers:
|
|||
libraries/scrcpy-decoder-tinyh264:
|
||||
dependencies:
|
||||
'@yume-chan/async':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
'@yume-chan/event':
|
||||
specifier: workspace:^
|
||||
version: link:../event
|
||||
|
@ -433,8 +433,8 @@ importers:
|
|||
libraries/stream-extra:
|
||||
dependencies:
|
||||
'@yume-chan/async':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
'@yume-chan/struct':
|
||||
specifier: workspace:^
|
||||
version: link:../struct
|
||||
|
@ -501,8 +501,8 @@ importers:
|
|||
specifier: ^5.6.3
|
||||
version: 5.6.3
|
||||
typescript-eslint:
|
||||
specifier: ^8.10.0
|
||||
version: 8.10.0(eslint@9.13.0)(typescript@5.6.3)
|
||||
specifier: ^8.11.0
|
||||
version: 8.11.0(eslint@9.13.0)(typescript@5.6.3)
|
||||
devDependencies:
|
||||
prettier:
|
||||
specifier: ^3.3.3
|
||||
|
@ -693,8 +693,8 @@ packages:
|
|||
'@types/w3c-web-usb@1.0.10':
|
||||
resolution: {integrity: sha512-CHgUI5kTc/QLMP8hODUHhge0D4vx+9UiAwIGiT0sTy/B2XpdX1U5rJt6JSISgr6ikRT7vxV9EVAFeYZqUnl1gQ==}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.10.0':
|
||||
resolution: {integrity: sha512-phuB3hoP7FFKbRXxjl+DRlQDuJqhpOnm5MmtROXyWi3uS/Xg2ZXqiQfcG2BJHiN4QKyzdOJi3NEn/qTnjUlkmQ==}
|
||||
'@typescript-eslint/eslint-plugin@8.11.0':
|
||||
resolution: {integrity: sha512-KhGn2LjW1PJT2A/GfDpiyOfS4a8xHQv2myUagTM5+zsormOmBlYsnQ6pobJ8XxJmh6hnHwa2Mbe3fPrDJoDhbA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
'@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0
|
||||
|
@ -704,8 +704,8 @@ packages:
|
|||
typescript:
|
||||
optional: true
|
||||
|
||||
'@typescript-eslint/parser@8.10.0':
|
||||
resolution: {integrity: sha512-E24l90SxuJhytWJ0pTQydFT46Nk0Z+bsLKo/L8rtQSL93rQ6byd1V/QbDpHUTdLPOMsBCcYXZweADNCfOCmOAg==}
|
||||
'@typescript-eslint/parser@8.11.0':
|
||||
resolution: {integrity: sha512-lmt73NeHdy1Q/2ul295Qy3uninSqi6wQI18XwSpm8w0ZbQXUpjCAWP1Vlv/obudoBiIjJVjlztjQ+d/Md98Yxg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
|
@ -718,8 +718,12 @@ packages:
|
|||
resolution: {integrity: sha512-AgCaEjhfql9MDKjMUxWvH7HjLeBqMCBfIaBbzzIcBbQPZE7CPh1m6FF+L75NUMJFMLYhCywJXIDEMa3//1A0dw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/type-utils@8.10.0':
|
||||
resolution: {integrity: sha512-PCpUOpyQSpxBn230yIcK+LeCQaXuxrgCm2Zk1S+PTIRJsEfU6nJ0TtwyH8pIwPK/vJoA+7TZtzyAJSGBz+s/dg==}
|
||||
'@typescript-eslint/scope-manager@8.11.0':
|
||||
resolution: {integrity: sha512-Uholz7tWhXmA4r6epo+vaeV7yjdKy5QFCERMjs1kMVsLRKIrSdM6o21W2He9ftp5PP6aWOVpD5zvrvuHZC0bMQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/type-utils@8.11.0':
|
||||
resolution: {integrity: sha512-ItiMfJS6pQU0NIKAaybBKkuVzo6IdnAhPFZA/2Mba/uBjuPQPet/8+zh5GtLHwmuFRShZx+8lhIs7/QeDHflOg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '*'
|
||||
|
@ -731,6 +735,10 @@ packages:
|
|||
resolution: {integrity: sha512-k/E48uzsfJCRRbGLapdZgrX52csmWJ2rcowwPvOZ8lwPUv3xW6CcFeJAXgx4uJm+Ge4+a4tFOkdYvSpxhRhg1w==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/types@8.11.0':
|
||||
resolution: {integrity: sha512-tn6sNMHf6EBAYMvmPUaKaVeYvhUsrE6x+bXQTxjQRp360h1giATU0WvgeEys1spbvb5R+VpNOZ+XJmjD8wOUHw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.10.0':
|
||||
resolution: {integrity: sha512-3OE0nlcOHaMvQ8Xu5gAfME3/tWVDpb/HxtpUZ1WeOAksZ/h/gwrBzCklaGzwZT97/lBbbxJ16dMA98JMEngW4w==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
@ -740,16 +748,35 @@ packages:
|
|||
typescript:
|
||||
optional: true
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.11.0':
|
||||
resolution: {integrity: sha512-yHC3s1z1RCHoCz5t06gf7jH24rr3vns08XXhfEqzYpd6Hll3z/3g23JRi0jM8A47UFKNc3u/y5KIMx8Ynbjohg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '*'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@typescript-eslint/utils@8.10.0':
|
||||
resolution: {integrity: sha512-Oq4uZ7JFr9d1ZunE/QKy5egcDRXT/FrS2z/nlxzPua2VHFtmMvFNDvpq1m/hq0ra+T52aUezfcjGRIB7vNJF9w==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
|
||||
'@typescript-eslint/utils@8.11.0':
|
||||
resolution: {integrity: sha512-CYiX6WZcbXNJV7UNB4PLDIBtSdRmRI/nb0FMyqHPTQD1rMjA0foPLaPUV39C/MxkTd/QKSeX+Gb34PPsDVC35g==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
|
||||
'@typescript-eslint/visitor-keys@8.10.0':
|
||||
resolution: {integrity: sha512-k8nekgqwr7FadWk548Lfph6V3r9OVqjzAIVskE7orMZR23cGJjAOVazsZSJW+ElyjfTM4wx/1g88Mi70DDtG9A==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/visitor-keys@8.11.0':
|
||||
resolution: {integrity: sha512-EaewX6lxSjRJnc+99+dqzTeoDZUfyrA52d2/HRrkI830kgovWsmIiTfmr0NZorzqic7ga+1bS60lRBUgR3n/Bw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@xhmikosr/archive-type@6.0.1':
|
||||
resolution: {integrity: sha512-PB3NeJL8xARZt52yDBupK0dNPn8uIVQDe15qNehUpoeeLWCZyAOam4vGXnoZGz2N9D1VXtjievJuCsXam2TmbQ==}
|
||||
engines: {node: ^14.14.0 || >=16.0.0}
|
||||
|
@ -778,8 +805,8 @@ packages:
|
|||
resolution: {integrity: sha512-mBvWew1kZJHfNQVVfVllMjUDwCGN9apPa0t4/z1zaUJ9MzpXjRL3w8fsfJKB8gHN/h4rik9HneKfDbh2fErN+w==}
|
||||
engines: {node: ^14.14.0 || >=16.0.0}
|
||||
|
||||
'@yume-chan/async@2.2.0':
|
||||
resolution: {integrity: sha512-jatCtX1/3DsR9Vt3EB8CGFy0MNrXP5f+eNiRGHLH+LkYz7MPLzpqL/DnvXSip+Z0EKBCDnzuNuELjsKEEzcdQA==}
|
||||
'@yume-chan/async@4.0.0':
|
||||
resolution: {integrity: sha512-T4DOnvaVqrx+PQh8bESdS6y2ozii7M0isJ5MpGU0girfz9kmwOaJ+rF1oeTJGZ0k+v92+eo/q6SpJjcjnO9tuQ==}
|
||||
|
||||
acorn-jsx@5.3.2:
|
||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||
|
@ -1658,8 +1685,8 @@ packages:
|
|||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
typescript-eslint@8.10.0:
|
||||
resolution: {integrity: sha512-YIu230PeN7z9zpu/EtqCIuRVHPs4iSlqW6TEvjbyDAE3MZsSl2RXBo+5ag+lbABCG8sFM1WVKEXhlQ8Ml8A3Fw==}
|
||||
typescript-eslint@8.11.0:
|
||||
resolution: {integrity: sha512-cBRGnW3FSlxaYwU8KfAewxFK5uzeOAp0l2KebIlPDOT5olVi65KDG/yjBooPBG0kGW/HLkoz1c/iuBFehcS3IA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '*'
|
||||
|
@ -1971,14 +1998,14 @@ snapshots:
|
|||
|
||||
'@types/w3c-web-usb@1.0.10': {}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.10.0(@typescript-eslint/parser@8.10.0(eslint@9.13.0)(typescript@5.6.3))(eslint@9.13.0)(typescript@5.6.3)':
|
||||
'@typescript-eslint/eslint-plugin@8.11.0(@typescript-eslint/parser@8.11.0(eslint@9.13.0)(typescript@5.6.3))(eslint@9.13.0)(typescript@5.6.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.11.1
|
||||
'@typescript-eslint/parser': 8.10.0(eslint@9.13.0)(typescript@5.6.3)
|
||||
'@typescript-eslint/scope-manager': 8.10.0
|
||||
'@typescript-eslint/type-utils': 8.10.0(eslint@9.13.0)(typescript@5.6.3)
|
||||
'@typescript-eslint/utils': 8.10.0(eslint@9.13.0)(typescript@5.6.3)
|
||||
'@typescript-eslint/visitor-keys': 8.10.0
|
||||
'@typescript-eslint/parser': 8.11.0(eslint@9.13.0)(typescript@5.6.3)
|
||||
'@typescript-eslint/scope-manager': 8.11.0
|
||||
'@typescript-eslint/type-utils': 8.11.0(eslint@9.13.0)(typescript@5.6.3)
|
||||
'@typescript-eslint/utils': 8.11.0(eslint@9.13.0)(typescript@5.6.3)
|
||||
'@typescript-eslint/visitor-keys': 8.11.0
|
||||
eslint: 9.13.0
|
||||
graphemer: 1.4.0
|
||||
ignore: 5.3.2
|
||||
|
@ -1989,12 +2016,12 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/parser@8.10.0(eslint@9.13.0)(typescript@5.6.3)':
|
||||
'@typescript-eslint/parser@8.11.0(eslint@9.13.0)(typescript@5.6.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.10.0
|
||||
'@typescript-eslint/types': 8.10.0
|
||||
'@typescript-eslint/typescript-estree': 8.10.0(typescript@5.6.3)
|
||||
'@typescript-eslint/visitor-keys': 8.10.0
|
||||
'@typescript-eslint/scope-manager': 8.11.0
|
||||
'@typescript-eslint/types': 8.11.0
|
||||
'@typescript-eslint/typescript-estree': 8.11.0(typescript@5.6.3)
|
||||
'@typescript-eslint/visitor-keys': 8.11.0
|
||||
debug: 4.3.7
|
||||
eslint: 9.13.0
|
||||
optionalDependencies:
|
||||
|
@ -2007,10 +2034,15 @@ snapshots:
|
|||
'@typescript-eslint/types': 8.10.0
|
||||
'@typescript-eslint/visitor-keys': 8.10.0
|
||||
|
||||
'@typescript-eslint/type-utils@8.10.0(eslint@9.13.0)(typescript@5.6.3)':
|
||||
'@typescript-eslint/scope-manager@8.11.0':
|
||||
dependencies:
|
||||
'@typescript-eslint/typescript-estree': 8.10.0(typescript@5.6.3)
|
||||
'@typescript-eslint/utils': 8.10.0(eslint@9.13.0)(typescript@5.6.3)
|
||||
'@typescript-eslint/types': 8.11.0
|
||||
'@typescript-eslint/visitor-keys': 8.11.0
|
||||
|
||||
'@typescript-eslint/type-utils@8.11.0(eslint@9.13.0)(typescript@5.6.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/typescript-estree': 8.11.0(typescript@5.6.3)
|
||||
'@typescript-eslint/utils': 8.11.0(eslint@9.13.0)(typescript@5.6.3)
|
||||
debug: 4.3.7
|
||||
ts-api-utils: 1.3.0(typescript@5.6.3)
|
||||
optionalDependencies:
|
||||
|
@ -2021,6 +2053,8 @@ snapshots:
|
|||
|
||||
'@typescript-eslint/types@8.10.0': {}
|
||||
|
||||
'@typescript-eslint/types@8.11.0': {}
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.10.0(typescript@5.6.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.10.0
|
||||
|
@ -2036,6 +2070,21 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.11.0(typescript@5.6.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.11.0
|
||||
'@typescript-eslint/visitor-keys': 8.11.0
|
||||
debug: 4.3.7
|
||||
fast-glob: 3.3.2
|
||||
is-glob: 4.0.3
|
||||
minimatch: 9.0.5
|
||||
semver: 7.6.3
|
||||
ts-api-utils: 1.3.0(typescript@5.6.3)
|
||||
optionalDependencies:
|
||||
typescript: 5.6.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/utils@8.10.0(eslint@9.13.0)(typescript@5.6.3)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.4.0(eslint@9.13.0)
|
||||
|
@ -2047,11 +2096,27 @@ snapshots:
|
|||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@typescript-eslint/utils@8.11.0(eslint@9.13.0)(typescript@5.6.3)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.4.0(eslint@9.13.0)
|
||||
'@typescript-eslint/scope-manager': 8.11.0
|
||||
'@typescript-eslint/types': 8.11.0
|
||||
'@typescript-eslint/typescript-estree': 8.11.0(typescript@5.6.3)
|
||||
eslint: 9.13.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@typescript-eslint/visitor-keys@8.10.0':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.10.0
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
'@typescript-eslint/visitor-keys@8.11.0':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.11.0
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
'@xhmikosr/archive-type@6.0.1':
|
||||
dependencies:
|
||||
file-type: 18.7.0
|
||||
|
@ -2105,9 +2170,7 @@ snapshots:
|
|||
merge-options: 3.0.4
|
||||
p-event: 5.0.1
|
||||
|
||||
'@yume-chan/async@2.2.0':
|
||||
dependencies:
|
||||
tslib: 2.8.0
|
||||
'@yume-chan/async@4.0.0': {}
|
||||
|
||||
acorn-jsx@5.3.2(acorn@8.13.0):
|
||||
dependencies:
|
||||
|
@ -2928,11 +2991,11 @@ snapshots:
|
|||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
|
||||
typescript-eslint@8.10.0(eslint@9.13.0)(typescript@5.6.3):
|
||||
typescript-eslint@8.11.0(eslint@9.13.0)(typescript@5.6.3):
|
||||
dependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.10.0(@typescript-eslint/parser@8.10.0(eslint@9.13.0)(typescript@5.6.3))(eslint@9.13.0)(typescript@5.6.3)
|
||||
'@typescript-eslint/parser': 8.10.0(eslint@9.13.0)(typescript@5.6.3)
|
||||
'@typescript-eslint/utils': 8.10.0(eslint@9.13.0)(typescript@5.6.3)
|
||||
'@typescript-eslint/eslint-plugin': 8.11.0(@typescript-eslint/parser@8.11.0(eslint@9.13.0)(typescript@5.6.3))(eslint@9.13.0)(typescript@5.6.3)
|
||||
'@typescript-eslint/parser': 8.11.0(eslint@9.13.0)(typescript@5.6.3)
|
||||
'@typescript-eslint/utils': 8.11.0(eslint@9.13.0)(typescript@5.6.3)
|
||||
optionalDependencies:
|
||||
typescript: 5.6.3
|
||||
transitivePeerDependencies:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue