diff --git a/libraries/adb-daemon-webusb/src/device.ts b/libraries/adb-daemon-webusb/src/device.ts index e4e0c0dc6..cb830b90e 100644 --- a/libraries/adb-daemon-webusb/src/device.ts +++ b/libraries/adb-daemon-webusb/src/device.ts @@ -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; diff --git a/libraries/adb-scrcpy/src/connection.ts b/libraries/adb-scrcpy/src/connection.ts index ef68711c3..60905f7b0 100644 --- a/libraries/adb-scrcpy/src/connection.ts +++ b/libraries/adb-scrcpy/src/connection.ts @@ -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 { + initialize(): MaybePromiseLike { // pure virtual method } @@ -66,7 +66,7 @@ export abstract class AdbScrcpyConnection implements Disposable { return socketName; } - abstract getStreams(): ValueOrPromise; + abstract getStreams(): MaybePromiseLike; dispose(): void { // pure virtual method diff --git a/libraries/adb-server-node-tcp/src/index.ts b/libraries/adb-server-node-tcp/src/index.ts index 7df1b1217..07634c3b6 100644 --- a/libraries/adb-server-node-tcp/src/index.ts +++ b/libraries/adb-server-node-tcp/src/index.ts @@ -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 { + removeReverseTunnel(address: string): MaybePromiseLike { const server = this.#listeners.get(address); if (!server) { return; @@ -147,7 +147,7 @@ export class AdbServerNodeTcpConnector this.#listeners.delete(address); } - clearReverseTunnels(): ValueOrPromise { + clearReverseTunnels(): MaybePromiseLike { for (const server of this.#listeners.values()) { server.close(); } diff --git a/libraries/adb/src/adb.ts b/libraries/adb/src/adb.ts index 96c451e49..4378a1ac9 100644 --- a/libraries/adb/src/adb.ts +++ b/libraries/adb/src/adb.ts @@ -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; + close(): MaybePromiseLike; } /** @@ -37,7 +37,7 @@ export interface AdbSocket export type AdbIncomingSocketHandler = ( socket: AdbSocket, -) => ValueOrPromise; +) => MaybePromiseLike; 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; + connect(service: string): MaybePromiseLike; addReverseTunnel( handler: AdbIncomingSocketHandler, address?: string, - ): ValueOrPromise; + ): MaybePromiseLike; - removeReverseTunnel(address: string): ValueOrPromise; + removeReverseTunnel(address: string): MaybePromiseLike; - clearReverseTunnels(): ValueOrPromise; + clearReverseTunnels(): MaybePromiseLike; } export class Adb implements Closeable { diff --git a/libraries/adb/src/commands/framebuffer.ts b/libraries/adb/src/commands/framebuffer.ts index 72a76c9b7..38ab753ec 100644 --- a/libraries/adb/src/commands/framebuffer.ts +++ b/libraries/adb/src/commands/framebuffer.ts @@ -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; -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; /** * ADB uses 8 int32 fields to describe bit depths @@ -99,9 +102,9 @@ export async function framebuffer(adb: Adb): Promise { 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); } diff --git a/libraries/adb/src/commands/reverse.ts b/libraries/adb/src/commands/reverse.ts index 66f2bfe20..82b377268 100644 --- a/libraries/adb/src/commands/reverse.ts +++ b/libraries/adb/src/commands/reverse.ts @@ -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) { diff --git a/libraries/adb/src/commands/subprocess/protocols/shell.spec.ts b/libraries/adb/src/commands/subprocess/protocols/shell.spec.ts index e89661890..0ba939fc2 100644 --- a/libraries/adb/src/commands/subprocess/protocols/shell.spec.ts +++ b/libraries/adb/src/commands/subprocess/protocols/shell.spec.ts @@ -51,6 +51,18 @@ async function assertResolves(promise: Promise, 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", () => { diff --git a/libraries/adb/src/commands/subprocess/protocols/shell.ts b/libraries/adb/src/commands/subprocess/protocols/shell.ts index f6d7bff44..511bc46c5 100644 --- a/libraries/adb/src/commands/subprocess/protocols/shell.ts +++ b/libraries/adb/src/commands/subprocess/protocols/shell.ts @@ -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()) - .uint32("length") - .uint8Array("data", { lengthField: "length" }); - -type AdbShellProtocolPacket = StructValueType; +export const AdbShellProtocolPacket = new Struct( + { + id: u8.as(), + data: buffer(u32), + }, + { littleEndian: true }, +); + +type AdbShellProtocolPacket = StructValue; /** * Shell v2 a.k.a Shell Protocol diff --git a/libraries/adb/src/commands/subprocess/protocols/types.ts b/libraries/adb/src/commands/subprocess/protocols/types.ts index bfd86043a..f575449e3 100644 --- a/libraries/adb/src/commands/subprocess/protocols/types.ts +++ b/libraries/adb/src/commands/subprocess/protocols/types.ts @@ -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; + resize(rows: number, cols: number): MaybePromiseLike; /** * Kills the current process. */ - kill(): ValueOrPromise; + kill(): MaybePromiseLike; } export interface AdbSubprocessProtocolConstructor { /** Returns `true` if the `adb` instance supports this shell */ - isSupported(adb: Adb): ValueOrPromise; + isSupported(adb: Adb): MaybePromiseLike; /** Spawns an executable in PTY (interactive) mode. */ - pty(adb: Adb, command: string): ValueOrPromise; + pty(adb: Adb, command: string): MaybePromiseLike; /** Spawns an executable and pipe the output. */ - raw(adb: Adb, command: string): ValueOrPromise; + raw(adb: Adb, command: string): MaybePromiseLike; /** Creates a new `AdbShell` by attaching to an exist `AdbSocket` */ new (socket: AdbSocket): AdbSubprocessProtocol; diff --git a/libraries/adb/src/commands/sync/list.ts b/libraries/adb/src/commands/sync/list.ts index 321f37632..b153f1313 100644 --- a/libraries/adb/src/commands/sync/list.ts +++ b/libraries/adb/src/commands/sync/list.ts @@ -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; -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; export async function* adbSyncOpenDirV2( socket: AdbSyncSocket, diff --git a/libraries/adb/src/commands/sync/pull.ts b/libraries/adb/src/commands/sync/pull.ts index b9001526f..f29c03804 100644 --- a/libraries/adb/src/commands/sync/pull.ts +++ b/libraries/adb/src/commands/sync/pull.ts @@ -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; export async function* adbSyncPullGenerator( socket: AdbSyncSocket, diff --git a/libraries/adb/src/commands/sync/push.ts b/libraries/adb/src/commands/sync/push.ts index 206f2e045..d8e6a1b0f 100644 --- a/libraries/adb/src/commands/sync/push.ts +++ b/libraries/adb/src/commands/sync/push.ts @@ -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()); +export const AdbSyncSendV2Request = new Struct( + { id: u32, mode: u32, flags: u32.as() }, + { littleEndian: true }, +); export async function adbSyncPushV2({ socket, diff --git a/libraries/adb/src/commands/sync/request.ts b/libraries/adb/src/commands/sync/request.ts index eecdec29f..f70d3d389 100644 --- a/libraries/adb/src/commands/sync/request.ts +++ b/libraries/adb/src/commands/sync/request.ts @@ -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; diff --git a/libraries/adb/src/commands/sync/response.ts b/libraries/adb/src/commands/sync/response.ts index caa7c5a3e..968a60824 100644 --- a/libraries/adb/src/commands/sync/response.ts +++ b/libraries/adb/src/commands/sync/response.ts @@ -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( stream: AsyncExactReadable, @@ -72,13 +69,11 @@ export async function adbSyncReadResponse( } } -export async function* adbSyncReadResponses< - T extends Struct, ->( +export async function* adbSyncReadResponses( stream: AsyncExactReadable, id: number | string, - type: T, -): AsyncGenerator, void, void> { + type: StructLike, +): AsyncGenerator { 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; + yield await type.deserialize(stream); break; default: throw new Error( diff --git a/libraries/adb/src/commands/sync/stat.ts b/libraries/adb/src/commands/sync/stat.ts index 26339b680..99a4757a3 100644 --- a/libraries/adb/src/commands/sync/stat.ts +++ b/libraries/adb/src/commands/sync/stat.ts @@ -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; export const AdbSyncStatErrorCode = { SUCCESS: 0, @@ -85,36 +86,40 @@ const AdbSyncStatErrorName = ]), ); -export const AdbSyncStatResponse = - /* #__PURE__ */ - new Struct({ littleEndian: true }) - .uint32("error", placeholder()) - .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(), + 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; export async function adbSyncLstat( socket: AdbSyncSocket, diff --git a/libraries/adb/src/daemon/auth.spec.ts b/libraries/adb/src/daemon/auth.spec.ts index 7e41a0d7b..0b239ed55 100644 --- a/libraries/adb/src/daemon/auth.spec.ts +++ b/libraries/adb/src/daemon/auth.spec.ts @@ -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, }), ); diff --git a/libraries/adb/src/daemon/auth.ts b/libraries/adb/src/daemon/auth.ts index ca5918829..87d93ddd7 100644 --- a/libraries/adb/src/daemon/auth.ts +++ b/libraries/adb/src/daemon/auth.ts @@ -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; + generateKey(): MaybePromiseLike; /** * 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 diff --git a/libraries/adb/src/daemon/device.ts b/libraries/adb/src/daemon/device.ts index 5691c818f..23b7e767e 100644 --- a/libraries/adb/src/daemon/device.ts +++ b/libraries/adb/src/daemon/device.ts @@ -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> >; } diff --git a/libraries/adb/src/daemon/dispatcher.ts b/libraries/adb/src/daemon/dispatcher.ts index 92e916e81..3b9acf181 100644 --- a/libraries/adb/src/daemon/dispatcher.ts +++ b/libraries/adb/src/daemon/dispatcher.ts @@ -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, ); } } diff --git a/libraries/adb/src/daemon/packet.ts b/libraries/adb/src/daemon/packet.ts index f7e266c7a..1fc411609 100644 --- a/libraries/adb/src/daemon/packet.ts +++ b/libraries/adb/src/daemon/packet.ts @@ -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; -type AdbPacketHeaderInit = (typeof AdbPacketHeader)["TInit"]; +type AdbPacketHeaderInit = StructInit; -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; /** * `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, "checksum" | "magic" >; -export type AdbPacketInit = (typeof AdbPacket)["TInit"]; +export type AdbPacketInit = StructInit; 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) { diff --git a/libraries/adb/src/daemon/socket.ts b/libraries/adb/src/daemon/socket.ts index d1e6bb03a..981edaf0e 100644 --- a/libraries/adb/src/daemon/socket.ts +++ b/libraries/adb/src/daemon/socket.ts @@ -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, ); } diff --git a/libraries/adb/src/daemon/transport.ts b/libraries/adb/src/daemon/transport.ts index 47eadb35b..d65595598 100644 --- a/libraries/adb/src/daemon/transport.ts +++ b/libraries/adb/src/daemon/transport.ts @@ -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 { + connect(service: string): MaybePromiseLike { return this.#dispatcher.createSocket(service); } @@ -392,7 +392,7 @@ export class AdbDaemonTransport implements AdbTransport { this.#dispatcher.clearReverseTunnels(); } - close(): ValueOrPromise { + close(): MaybePromiseLike { return this.#dispatcher.close(); } } diff --git a/libraries/adb/src/server/client.ts b/libraries/adb/src/server/client.ts index 2e454c6ce..470690f95 100644 --- a/libraries/adb/src/server/client.ts +++ b/libraries/adb/src/server/client.ts @@ -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 { + readExactly(length: number): MaybePromiseLike { 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>` 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>` 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 { // TODO: investigate using `encodeUtf8("0000" + value)` then modifying the length @@ -572,16 +561,16 @@ export namespace AdbServerClient { export interface ServerConnector { connect( options?: ServerConnectionOptions, - ): ValueOrPromise; + ): MaybePromiseLike; addReverseTunnel( handler: AdbIncomingSocketHandler, address?: string, - ): ValueOrPromise; + ): MaybePromiseLike; - removeReverseTunnel(address: string): ValueOrPromise; + removeReverseTunnel(address: string): MaybePromiseLike; - clearReverseTunnels(): ValueOrPromise; + clearReverseTunnels(): MaybePromiseLike; } export interface Socket extends AdbSocket { diff --git a/libraries/adb/src/server/transport.ts b/libraries/adb/src/server/transport.ts index 3eceb9374..434d7eb88 100644 --- a/libraries/adb/src/server/transport.ts +++ b/libraries/adb/src/server/transport.ts @@ -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 { + close(): void | Promise { this.#closed.resolve(); this.#waitAbortController.abort(); } diff --git a/libraries/android-bin/src/logcat.ts b/libraries/android-bin/src/logcat.ts index 8077561f2..81e8acd2f 100644 --- a/libraries/android-bin/src/logcat.ts +++ b/libraries/android-bin/src/logcat.ts @@ -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; // https://cs.android.com/android/platform/superproject/+/master:system/logging/liblog/logprint.cpp;drc=bbe77d66e7bee8bd1f0bc7e5492b5376b0207ef6;bpv=0 export interface AndroidLogEntry extends LoggerEntry { diff --git a/libraries/scrcpy/src/control/empty.ts b/libraries/scrcpy/src/control/empty.ts index c8e18600e..1d7471242 100644 --- a/libraries/scrcpy/src/control/empty.ts +++ b/libraries/scrcpy/src/control/empty.ts @@ -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; diff --git a/libraries/scrcpy/src/control/inject-keycode.ts b/libraries/scrcpy/src/control/inject-keycode.ts index 142d34778..ccbc469e7 100644 --- a/libraries/scrcpy/src/control/inject-keycode.ts +++ b/libraries/scrcpy/src/control/inject-keycode.ts @@ -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()) - .uint32("keyCode", placeholder()) - .uint32("repeat") - .uint32("metaState", placeholder()); - -export type ScrcpyInjectKeyCodeControlMessage = - (typeof ScrcpyInjectKeyCodeControlMessage)["TInit"]; +export const ScrcpyInjectKeyCodeControlMessage = new Struct( + { + type: u8, + action: u8.as(), + keyCode: u32.as(), + repeat: u32, + metaState: u32.as(), + }, + { littleEndian: false }, +); + +export type ScrcpyInjectKeyCodeControlMessage = StructInit< + typeof ScrcpyInjectKeyCodeControlMessage +>; diff --git a/libraries/scrcpy/src/control/inject-text.ts b/libraries/scrcpy/src/control/inject-text.ts index dea929163..3bebb7c5c 100644 --- a/libraries/scrcpy/src/control/inject-text.ts +++ b/libraries/scrcpy/src/control/inject-text.ts @@ -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 +>; diff --git a/libraries/scrcpy/src/control/rotate-device.ts b/libraries/scrcpy/src/control/rotate-device.ts index f2058ee9b..6ea7afa4b 100644 --- a/libraries/scrcpy/src/control/rotate-device.ts +++ b/libraries/scrcpy/src/control/rotate-device.ts @@ -2,5 +2,4 @@ import { EmptyControlMessage } from "./empty.js"; export const ScrcpyRotateDeviceControlMessage = EmptyControlMessage; -export type ScrcpyRotateDeviceControlMessage = - (typeof ScrcpyRotateDeviceControlMessage)["TInit"]; +export type ScrcpyRotateDeviceControlMessage = EmptyControlMessage; diff --git a/libraries/scrcpy/src/control/set-screen-power-mode.ts b/libraries/scrcpy/src/control/set-screen-power-mode.ts index 47eb78557..5d4073347 100644 --- a/libraries/scrcpy/src/control/set-screen-power-mode.ts +++ b/libraries/scrcpy/src/control/set-screen-power-mode.ts @@ -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()); +export type AndroidScreenPowerMode = + (typeof AndroidScreenPowerMode)[keyof typeof AndroidScreenPowerMode]; -export type ScrcpySetScreenPowerModeControlMessage = - (typeof ScrcpySetScreenPowerModeControlMessage)["TInit"]; +export const ScrcpySetScreenPowerModeControlMessage = new Struct( + { type: u8, mode: u8.as() }, + { littleEndian: false }, +); + +export type ScrcpySetScreenPowerModeControlMessage = StructInit< + typeof ScrcpySetScreenPowerModeControlMessage +>; diff --git a/libraries/scrcpy/src/options/1_16/float.ts b/libraries/scrcpy/src/options/1_16/float.ts index c8acd737a..4f5caf3ac 100644 --- a/libraries/scrcpy/src/options/1_16/float.ts +++ b/libraries/scrcpy/src/options/1_16/float.ts @@ -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 = { 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, -); diff --git a/libraries/scrcpy/src/options/1_16/message.ts b/libraries/scrcpy/src/options/1_16/message.ts index c493a4bd9..b7a729ddc 100644 --- a/libraries/scrcpy/src/options/1_16/message.ts +++ b/libraries/scrcpy/src/options/1_16/message.ts @@ -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()) - .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(), + 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 }, +); diff --git a/libraries/scrcpy/src/options/1_16/options.ts b/libraries/scrcpy/src/options/1_16/options.ts index c3866a60c..2a0d4294d 100644 --- a/libraries/scrcpy/src/options/1_16/options.ts +++ b/libraries/scrcpy/src/options/1_16/options.ts @@ -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 { override parseVideoStreamMetadata( stream: ReadableStream, - ): ValueOrPromise { + ): MaybePromiseLike { return (async () => { const buffered = new BufferedReadableStream(stream); const metadata: ScrcpyVideoStreamMetadata = { diff --git a/libraries/scrcpy/src/options/1_16/scroll.ts b/libraries/scrcpy/src/options/1_16/scroll.ts index 25ba76c59..15571f56b 100644 --- a/libraries/scrcpy/src/options/1_16/scroll.ts +++ b/libraries/scrcpy/src/options/1_16/scroll.ts @@ -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. diff --git a/libraries/scrcpy/src/options/1_18.ts b/libraries/scrcpy/src/options/1_18.ts index 2df783aab..dbef72857 100644 --- a/libraries/scrcpy/src/options/1_18.ts +++ b/libraries/scrcpy/src/options/1_18.ts @@ -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()); +export const ScrcpyBackOrScreenOnControlMessage1_18 = new Struct( + { + ...ScrcpyBackOrScreenOnControlMessage1_16.fields, + action: u8.as(), + }, + { 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(); diff --git a/libraries/scrcpy/src/options/1_21.ts b/libraries/scrcpy/src/options/1_21.ts index 49732c68d..a14d1c42e 100644 --- a/libraries/scrcpy/src/options/1_21.ts +++ b/libraries/scrcpy/src/options/1_21.ts @@ -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()) - .uint32("length") - .string("content", { lengthField: "length" }); - -export type ScrcpySetClipboardControlMessage1_21 = - (typeof ScrcpySetClipboardControlMessage1_21)["TInit"]; +export const ScrcpySetClipboardControlMessage1_21 = new Struct( + { + type: u8, + sequence: u64, + paste: u8.as(), + content: string(u32), + }, + { littleEndian: false }, +); + +export type ScrcpySetClipboardControlMessage1_21 = StructInit< + typeof ScrcpySetClipboardControlMessage1_21 +>; export class ScrcpyOptions1_21 extends ScrcpyOptions { static readonly DEFAULTS = { diff --git a/libraries/scrcpy/src/options/1_22/options.ts b/libraries/scrcpy/src/options/1_22/options.ts index 12a4c71a8..ab41b90a7 100644 --- a/libraries/scrcpy/src/options/1_22/options.ts +++ b/libraries/scrcpy/src/options/1_22/options.ts @@ -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 { override parseVideoStreamMetadata( stream: ReadableStream, - ): ValueOrPromise { + ): MaybePromiseLike { if (!this.value.sendDeviceMeta) { return { stream, metadata: { codec: ScrcpyVideoCodecId.H264 } }; } else { diff --git a/libraries/scrcpy/src/options/1_22/scroll.ts b/libraries/scrcpy/src/options/1_22/scroll.ts index 38cf15143..e3e060022 100644 --- a/libraries/scrcpy/src/options/1_22/scroll.ts +++ b/libraries/scrcpy/src/options/1_22/scroll.ts @@ -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( diff --git a/libraries/scrcpy/src/options/1_25/scroll.spec.ts b/libraries/scrcpy/src/options/1_25/scroll.spec.ts index 8f8826237..6c053da89 100644 --- a/libraries/scrcpy/src/options/1_25/scroll.spec.ts +++ b/libraries/scrcpy/src/options/1_25/scroll.spec.ts @@ -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, ); }); diff --git a/libraries/scrcpy/src/options/1_25/scroll.ts b/libraries/scrcpy/src/options/1_25/scroll.ts index 2c671bc47..919665947 100644 --- a/libraries/scrcpy/src/options/1_25/scroll.ts +++ b/libraries/scrcpy/src/options/1_25/scroll.ts @@ -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 = { 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( diff --git a/libraries/scrcpy/src/options/2_0.ts b/libraries/scrcpy/src/options/2_0.ts index d6b7dcfec..76a1e83b5 100644 --- a/libraries/scrcpy/src/options/2_0.ts +++ b/libraries/scrcpy/src/options/2_0.ts @@ -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()) - .uint64("pointerId") - .uint32("pointerX") - .uint32("pointerY") - .uint16("screenWidth") - .uint16("screenHeight") - .field("pressure", ScrcpyUnsignedFloatFieldDefinition) - .uint32("actionButton") - .uint32("buttons"); - -export type ScrcpyInjectTouchControlMessage2_0 = - (typeof ScrcpyInjectTouchControlMessage2_0)["TInit"]; +export const ScrcpyInjectTouchControlMessage2_0 = new Struct( + { + type: u8, + action: u8.as(), + pointerId: u64, + pointerX: u32, + pointerY: u32, + screenWidth: u16, + screenHeight: u16, + pressure: ScrcpyUnsignedFloat, + actionButton: u32, + buttons: u32, + }, + { littleEndian: false }, +); + +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 { override parseVideoStreamMetadata( stream: ReadableStream, - ): ValueOrPromise { + ): MaybePromiseLike { const { sendDeviceMeta, sendCodecMeta } = this.value; if (!sendDeviceMeta && !sendCodecMeta) { let codec: ScrcpyVideoCodecId; @@ -302,7 +305,7 @@ export class ScrcpyOptions2_0 extends ScrcpyOptions { override parseAudioStreamMetadata( stream: ReadableStream, - ): ValueOrPromise { + ): MaybePromiseLike { return ScrcpyOptions2_0.parseAudioMetadata( stream, this.value.sendCodecMeta, diff --git a/libraries/scrcpy/src/options/2_3.ts b/libraries/scrcpy/src/options/2_3.ts index be21e5c72..8f51a9c46 100644 --- a/libraries/scrcpy/src/options/2_3.ts +++ b/libraries/scrcpy/src/options/2_3.ts @@ -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 { override parseAudioStreamMetadata( stream: ReadableStream, - ): ValueOrPromise { + ): MaybePromiseLike { return ScrcpyOptions2_0.parseAudioMetadata( stream, this.value.sendCodecMeta, diff --git a/libraries/scrcpy/src/options/types.ts b/libraries/scrcpy/src/options/types.ts index 73bd271ab..0a16b2c55 100644 --- a/libraries/scrcpy/src/options/types.ts +++ b/libraries/scrcpy/src/options/types.ts @@ -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 { */ parseVideoStreamMetadata( stream: ReadableStream, - ): ValueOrPromise { + ): MaybePromiseLike { return this.#base.parseVideoStreamMetadata(stream); } parseAudioStreamMetadata( stream: ReadableStream, - ): ValueOrPromise { + ): MaybePromiseLike { return this.#base.parseAudioStreamMetadata(stream); } @@ -184,7 +184,7 @@ export abstract class ScrcpyOptions { return this.#base.parseDeviceMessage(id, stream); } - endDeviceMessageStream(e?: unknown): ValueOrPromise { + endDeviceMessageStream(e?: unknown): MaybePromiseLike { return this.#base.endDeviceMessageStream(e); } diff --git a/libraries/stream-extra/src/buffered-transform.ts b/libraries/stream-extra/src/buffered-transform.ts index 27b597072..0b59f0ebc 100644 --- a/libraries/stream-extra/src/buffered-transform.ts +++ b/libraries/stream-extra/src/buffered-transform.ts @@ -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 } constructor( - transform: (stream: BufferedReadableStream) => ValueOrPromise, + transform: (stream: BufferedReadableStream) => MaybePromiseLike, ) { // Convert incoming chunks to a `BufferedReadableStream` let sourceStreamController!: PushReadableStreamController; diff --git a/libraries/stream-extra/src/concat.ts b/libraries/stream-extra/src/concat.ts index b605071a7..90841c101 100644 --- a/libraries/stream-extra/src/concat.ts +++ b/libraries/stream-extra/src/concat.ts @@ -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]!; diff --git a/libraries/stream-extra/src/duplex.ts b/libraries/stream-extra/src/duplex.ts index 1139822a3..d1f605315 100644 --- a/libraries/stream-extra/src/duplex.ts +++ b/libraries/stream-extra/src/duplex.ts @@ -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) | undefined; + close?: (() => MaybePromiseLike) | undefined; /** * Callback when any `ReadableStream` is closed (the other peer doesn't produce any more data), diff --git a/libraries/stream-extra/src/struct-deserialize.ts b/libraries/stream-extra/src/struct-deserialize.ts index 2297216f0..f24c039c2 100644 --- a/libraries/stream-extra/src/struct-deserialize.ts +++ b/libraries/stream-extra/src/struct-deserialize.ts @@ -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, -> extends BufferedTransformStream> { - constructor(struct: T) { +export class StructDeserializeStream extends BufferedTransformStream { + constructor(struct: StructLike) { super((stream) => { return struct.deserialize(stream) as never; }); diff --git a/libraries/stream-extra/src/struct-serialize.ts b/libraries/stream-extra/src/struct-serialize.ts index 24023a3b8..86e9a6ff2 100644 --- a/libraries/stream-extra/src/struct-serialize.ts +++ b/libraries/stream-extra/src/struct-serialize.ts @@ -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, -> extends TransformStream { + T extends StructLike, +> extends TransformStream, Uint8Array> { constructor(struct: T) { super({ transform(chunk, controller) { diff --git a/libraries/stream-extra/src/wrap-readable.ts b/libraries/stream-extra/src/wrap-readable.ts index 3efcf26d1..4c1fd3b2d 100644 --- a/libraries/stream-extra/src/wrap-readable.ts +++ b/libraries/stream-extra/src/wrap-readable.ts @@ -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 = ( controller: ReadableStreamDefaultController, -) => ValueOrPromise>; +) => MaybePromiseLike>; export interface ReadableStreamWrapper { start: WrapReadableStreamStart; - cancel?(reason?: unknown): ValueOrPromise; - close?(): ValueOrPromise; + cancel?(reason?: unknown): MaybePromiseLike; + close?(): MaybePromiseLike; } function getWrappedReadableStream( diff --git a/libraries/stream-extra/src/wrap-writable.ts b/libraries/stream-extra/src/wrap-writable.ts index d53cf2853..c6ef1837f 100644 --- a/libraries/stream-extra/src/wrap-writable.ts +++ b/libraries/stream-extra/src/wrap-writable.ts @@ -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 = () => ValueOrPromise< +export type WrapWritableStreamStart = () => MaybePromiseLike< WritableStream >; diff --git a/libraries/struct/README.md b/libraries/struct/README.md index d89c13453..9768ced7c 100644 --- a/libraries/struct/README.md +++ b/libraries/struct/README.md @@ -2,7 +2,6 @@ ![license](https://img.shields.io/npm/l/@yume-chan/struct) @@ -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"; - -const MyStruct = new Struct({ littleEndian: true }) - .int8("foo") - .int64("bar") - .int32("bazLength") - .string("baz", { lengthField: "bazLength" }); - -const value = await MyStruct.deserialize(stream); -value.foo; // number -value.bar; // bigint -value.bazLength; // number -value.baz; // string - -const buffer = MyStruct.serialize({ - foo: 42, - bar: 42n, - // `bazLength` automatically set to `baz`'s byte length - baz: "Hello, World!", +import { Struct, u8, u16, s32, buffer, string } from "@yume-chan/struct"; + +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 }, +); + +// 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 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 = { + 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, }); ``` - - -- [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) - - - -## 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]1 | 67 | 79 | 68 | No | 14 | 10.4 | - -1 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.31, 11 | - -1 `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` - -```ts -function placeholder(): T { - return undefined as unknown as T; -} -``` - -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)) - -
-Detail explanation (click to expand) - -When you have a generic method, where half generic parameters can be inferred. - -```ts -declare function fn(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: 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()); // fn2 -``` - -

- -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 = StructDefaultOptions); -} -``` - -Creates a new structure definition. - -
-Generic parameters (click to expand) - -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` - -

- -**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, - TOmitInitKey, - TExtra, - TPostDeserialized ->; -``` - -Appends an `int8`/`uint8`/`int16`/`uint16`/`int32`/`uint32` field to the `Struct`. - -
-Generic parameters (click to expand) - -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. - -

- -**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()) - .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, - 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, - TOmitInitKey, - TExtra, - TPostDeserialized ->; - -uint8Array< - TName extends string | number | symbol, - TLengthField extends LengthField, - TOptions extends VariableLengthBufferLikeFieldOptions, - TTypeScriptType = ArrayBuffer, ->( - name: TName, - options: TOptions, - _typescriptType?: TTypeScriptType, -): Struct< - TFields & Record, - 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 ->( - 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, TFields>> -): Struct< - TFields, - TInit, - Overwrite, - 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; -``` - -Remove any registered post-deserialization callback. - -```ts -postDeserialize( - callback: (this: TFields, object: TFields) => never -): Struct; -postDeserialize( - callback: (this: TFields, object: TFields) => void -): Struct; -``` - -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( - callback: (this: TFields, object: TFields) => TPostSerialize -): Struct; -``` - -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; -} - -deserialize( - stream: ExactReadable, -): TPostDeserialized extends undefined - ? Overwrite - : TPostDeserialized ->; -deserialize( - stream: AsyncExactReadable, -): Promise< - TPostDeserialized extends undefined - ? Overwrite - : 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>): Uint8Array; -serialize(init: Evaluate>, 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 ->( - name: TName, - definition: TDefinition -): Struct< - TFields & Record, - 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, - struct: StructValue, - value: TValue, -): StructFieldValue; -``` - -Derived classes must implement this method to create its own field value instance for the current definition. +## Bipedal -`Struct#serialize` will call this method, then call `StructFieldValue#serialize` to serialize one field value. - -#### `deserialize` - -```ts -abstract deserialize( - options: Readonly, - stream: ExactReadable, - struct: StructValue, -): StructFieldValue; -abstract deserialize( - options: Readonly, - stream: AsyncExactReadable, - struct: StructValue, -): Promise>; -``` +`bipedal` is a custom async helper that allows the same code to behave synchronously or asynchronously depends on the parameters. -Derived classes must implement this method to define how to deserialize a value from `stream`. +It's inspired by [gensync](https://github.com/loganfsmyth/gensync). -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` +The word `bipedal` refers to animals who walk using two legs. ```ts -abstract class StructFieldValue< - TDefinition extends StructFieldDefinition -> -``` - -A field value defines how to serialize a field. - -#### `getSize` +import { bipedal } from "@yume-chan/struct"; -```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` +const fn = bipedal(function* (then, name: string | Promise) { + name = yield* then(name); + return "Hello, " + name; +}); -```ts -abstract serialize( - array: Uint8Array, - offset: number -): void; +fn("Simon"); // "Hello, Simon" +await fn(Promise.resolve("Simon")); // "Hello, Simon" ``` - -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. diff --git a/libraries/struct/src/basic/definition.spec.ts b/libraries/struct/src/basic/definition.spec.ts deleted file mode 100644 index d92065d5c..000000000 --- a/libraries/struct/src/basic/definition.spec.ts +++ /dev/null @@ -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 { - constructor(options: number) { - super(options); - } - override getSize(): number { - throw new Error("Method not implemented."); - } - override create( - options: Readonly, - struct: StructValue, - value: unknown, - ): StructFieldValue { - void options; - void struct; - void value; - throw new Error("Method not implemented."); - } - override deserialize( - options: Readonly, - stream: ExactReadable, - struct: StructValue, - ): StructFieldValue; - override deserialize( - options: Readonly, - stream: AsyncExactReadable, - struct: StructValue, - ): Promise>; - override deserialize( - options: Readonly, - stream: ExactReadable | AsyncExactReadable, - struct: StructValue, - ): ValueOrPromise> { - void options; - void stream; - void struct; - throw new Error("Method not implemented."); - } - } - - assert.strictEqual(new MockFieldDefinition(42).options, 42); - }); - }); -}); diff --git a/libraries/struct/src/basic/definition.ts b/libraries/struct/src/basic/definition.ts deleted file mode 100644 index 333dd4134..000000000 --- a/libraries/struct/src/basic/definition.ts +++ /dev/null @@ -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, - structValue: StructValue, - value: TValue, - ): StructFieldValue; - - /** - * 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, - stream: ExactReadable, - structValue: StructValue, - ): StructFieldValue; - abstract deserialize( - options: Readonly, - stream: AsyncExactReadable, - struct: StructValue, - ): Promise>; -} diff --git a/libraries/struct/src/basic/field-value.spec.ts b/libraries/struct/src/basic/field-value.spec.ts deleted file mode 100644 index f6f10edc0..000000000 --- a/libraries/struct/src/basic/field-value.spec.ts +++ /dev/null @@ -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 { - 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, - struct: StructValue, - value: unknown, - ): StructFieldValue { - void options; - void struct; - void value; - throw new Error("Method not implemented."); - } - - override deserialize( - options: Readonly, - stream: ExactReadable, - struct: StructValue, - ): StructFieldValue; - override deserialize( - options: Readonly, - stream: AsyncExactReadable, - struct: StructValue, - ): Promise>; - override deserialize( - options: Readonly, - stream: ExactReadable | AsyncExactReadable, - struct: StructValue, - ): ValueOrPromise> { - void options; - void stream; - void struct; - throw new Error("Method not implemented."); - } - } - - class MockStructFieldValue extends StructFieldValue { - 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 { - 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); - }); - }); -}); diff --git a/libraries/struct/src/basic/field-value.ts b/libraries/struct/src/basic/field-value.ts deleted file mode 100644 index ea25990b7..000000000 --- a/libraries/struct/src/basic/field-value.ts +++ /dev/null @@ -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, -> { - /** Gets the definition associated with this runtime value */ - readonly definition: TDefinition; - - /** Gets the options of the associated `Struct` */ - readonly options: Readonly; - - /** 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, - 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; -} diff --git a/libraries/struct/src/basic/index.ts b/libraries/struct/src/basic/index.ts deleted file mode 100644 index 363941511..000000000 --- a/libraries/struct/src/basic/index.ts +++ /dev/null @@ -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"; diff --git a/libraries/struct/src/basic/options.spec.ts b/libraries/struct/src/basic/options.spec.ts deleted file mode 100644 index 34a7fa745..000000000 --- a/libraries/struct/src/basic/options.spec.ts +++ /dev/null @@ -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); - }); - }); -}); diff --git a/libraries/struct/src/basic/options.ts b/libraries/struct/src/basic/options.ts deleted file mode 100644 index 9b9c6aaba..000000000 --- a/libraries/struct/src/basic/options.ts +++ /dev/null @@ -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 = { - littleEndian: false, -}; diff --git a/libraries/struct/src/basic/struct-value.spec.ts b/libraries/struct/src/basic/struct-value.spec.ts deleted file mode 100644 index 18811cd57..000000000 --- a/libraries/struct/src/basic/struct-value.spec.ts +++ /dev/null @@ -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 - >; - object.set(foo, fooValue); - - const bar = "bar"; - const barValue = { - get() { - return "foo"; - }, - } as StructFieldValue< - StructFieldDefinition - >; - 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 - >; - 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 - >; - 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 - >; - object.set(foo, fooValue); - - assert.strictEqual(object.get(foo), fooValue); - }); - }); -}); diff --git a/libraries/struct/src/basic/struct-value.ts b/libraries/struct/src/basic/struct-value.ts deleted file mode 100644 index 7b26b6171..000000000 --- a/libraries/struct/src/basic/struct-value.ts +++ /dev/null @@ -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> - > = {}; - - /** - * Gets the result struct value object - */ - readonly value: Record; - - constructor(prototype: object) { - // PERF: `Object.create(extra)` is 50% faster - // than `Object.defineProperties(this.value, extra)` - this.value = Object.create(prototype) as Record; - - // 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 - >, - ): 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> { - return this.fieldValues[name]!; - } -} diff --git a/libraries/struct/src/bipedal.ts b/libraries/struct/src/bipedal.ts new file mode 100644 index 000000000..a6930f6be --- /dev/null +++ b/libraries/struct/src/bipedal.ts @@ -0,0 +1,58 @@ +import type { MaybePromiseLike } from "./utils.js"; + +function isPromiseLike(value: unknown): value is PromiseLike { + return typeof value === "object" && value !== null && "then" in value; +} + +function advance( + iterator: Iterator, + next: unknown, +): MaybePromiseLike { + 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( + fn: ( + this: This, + then: (value: U | PromiseLike) => Iterable, + ...args: A + ) => Generator, +): { (this: This, ...args: A): MaybePromiseLike } { + return function (this: This, ...args: A) { + const iterator = fn.call( + this, + function* ( + value: U | PromiseLike, + ): Generator< + PromiseLike, + 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); + }; +} diff --git a/libraries/struct/src/buffer.spec.ts b/libraries/struct/src/buffer.spec.ts new file mode 100644 index 000000000..7e0e6240e --- /dev/null +++ b/libraries/struct/src/buffer.spec.ts @@ -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/, + ); + }); + }); +}); diff --git a/libraries/struct/src/buffer.ts b/libraries/struct/src/buffer.ts new file mode 100644 index 000000000..94a78ed09 --- /dev/null +++ b/libraries/struct/src/buffer.ts @@ -0,0 +1,229 @@ +import { bipedal } from "./bipedal.js"; +import type { Field } from "./field.js"; + +export interface Converter { + convert: (value: From) => To; + back: (value: To) => From; +} + +export interface BufferLengthConverter extends Converter { + field: K; +} + +export interface BufferLike { + (length: number): Field; + ( + length: number, + converter: Converter, + ): Field; + + (lengthField: K): Field>; + ( + lengthField: K, + converter: Converter, + ): Field>; + + ( + length: BufferLengthConverter, + ): Field>; + ( + length: BufferLengthConverter, + converter: Converter, + ): Field>; + + ( + length: Field, + ): Field; + ( + length: Field, + converter: Converter, + ): Field; +} + +export const EmptyUint8Array = new Uint8Array(0); + +export const buffer: BufferLike = (( + lengthOrField: + | string + | number + | Field + | BufferLengthConverter, + converter?: Converter, +): Field> => { + 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; diff --git a/libraries/struct/src/field.ts b/libraries/struct/src/field.ts new file mode 100644 index 000000000..595bffa11 --- /dev/null +++ b/libraries/struct/src/field.ts @@ -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 { + reader: AsyncExactReadable; + littleEndian: boolean; + runtimeStruct: S; +} + +export interface Field { + __invariant?: OmitInit; + + size: number; + + dynamicSize?(value: T): number; + preSerialize?(value: T, runtimeStruct: S): void; + serialize(value: T, context: SerializeContext): void; + + deserialize(context: DeserializeContext): MaybePromiseLike; +} diff --git a/libraries/struct/src/index.spec.ts b/libraries/struct/src/index.spec.ts index 8aee68454..7f9d9b7c2 100644 --- a/libraries/struct/src/index.spec.ts +++ b/libraries/struct/src/index.spec.ts @@ -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", () => { diff --git a/libraries/struct/src/index.ts b/libraries/struct/src/index.ts index 1d89365b1..bf82fbb9b 100644 --- a/libraries/struct/src/index.ts +++ b/libraries/struct/src/index.ts @@ -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"; diff --git a/libraries/struct/src/number.ts b/libraries/struct/src/number.ts new file mode 100644 index 000000000..975892c5c --- /dev/null +++ b/libraries/struct/src/number.ts @@ -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 & { + as: (infer?: T) => Field; +} = { + 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 & { + as: (infer?: T) => Field; +} = { + 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 & { + as: (infer?: T) => Field; +} = { + 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 & { + as: (infer?: T) => Field; +} = { + 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 & { + as: (infer?: T) => Field; +} = { + 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 & { + as: (infer?: T) => Field; +} = { + 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 & { + as: (infer?: T) => Field; +} = { + 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 & { + as: (infer?: T) => Field; +} = { + 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, +}; diff --git a/libraries/struct/src/basic/stream.ts b/libraries/struct/src/readable.ts similarity index 89% rename from libraries/struct/src/basic/stream.ts rename to libraries/struct/src/readable.ts index 0757837a5..48b1888f9 100644 --- a/libraries/struct/src/basic/stream.ts +++ b/libraries/struct/src/readable.ts @@ -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; + readExactly(length: number): MaybePromiseLike; } diff --git a/libraries/struct/src/string.ts b/libraries/struct/src/string.ts new file mode 100644 index 000000000..965f2712e --- /dev/null +++ b/libraries/struct/src/string.ts @@ -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 & { + as: (infer: T) => Field; + }; + + ( + lengthField: K, + ): Field> & { + as: (infer: T) => Field>; + }; + + ( + length: BufferLengthConverter, + ): Field> & { + as: (infer: T) => Field>; + }; + + ( + length: Field, + ): Field; +} + +export const string: String = (( + lengthOrField: string | number | BufferLengthConverter, +): Field> & { + as: (infer: T) => Field>; +} => { + const field = buffer(lengthOrField as never, { + convert: decodeUtf8, + back: encodeUtf8, + }); + (field as never as { as: unknown }).as = () => field; + return field as never; +}) as never; diff --git a/libraries/struct/src/struct.spec.ts b/libraries/struct/src/struct.spec.ts index 666b51dfe..d5d8715ea 100644 --- a/libraries/struct/src/struct.spec.ts +++ b/libraries/struct/src/struct.spec.ts @@ -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 { - constructor(size: number) { - super(size); - } - - getSize = mock.fn(() => { - return this.options; - }); - - override create( - options: Readonly, - struct: StructValue, - value: unknown, - ): StructFieldValue { - void options; - void struct; - void value; - throw new Error("Method not implemented."); - } - override deserialize( - options: Readonly, - stream: ExactReadable, - struct: StructValue, - ): StructFieldValue; - override deserialize( - options: Readonly, - stream: AsyncExactReadable, - struct: StructValue, - ): Promise>; - override deserialize( - options: Readonly, - stream: ExactReadable | AsyncExactReadable, - struct: StructValue, - ): ValueOrPromise> { - 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])); }); }); diff --git a/libraries/struct/src/struct.ts b/libraries/struct/src/struct.ts index 3b8b464f8..0ed7ec1fd 100644 --- a/libraries/struct/src/struct.ts +++ b/libraries/struct/src/struct.ts @@ -1,197 +1,35 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -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 interface StructLike { - deserialize(stream: ExactReadable | AsyncExactReadable): Promise; -} - -/** - * Extract the value type of the specified `Struct` - */ -export type StructValueType> = Awaited< - ReturnType ->; - -/** - * 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, -> = Identity< - Struct< - // Merge two types - // Evaluate immediately to optimize editor hover tooltip - Evaluate>, - // 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, - 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, - TOptions extends VariableLengthBufferLikeFieldOptions, - TTypeScriptType = TType["TTypeScriptType"], - >( - name: TName, - type: TType, - options: TOptions, - typeScriptType?: TTypeScriptType, - ): AddFieldDescriptor< - TFields, - TOmitInitKey, - TExtra, - TPostDeserialized, - TName, - VariableLengthBufferLikeFieldDefinition - >; -} - -/** - * Similar to `ArrayBufferLikeFieldCreator`, but bind to `TType` - */ -interface BoundArrayBufferLikeFieldDefinitionCreator< - TFields extends object, - TOmitInitKey extends PropertyKey, - TExtra extends object, - TPostDeserialized, - TType extends BufferFieldConverter, -> { - ( - name: TName, - options: FixedLengthBufferLikeFieldOptions, - typeScriptType?: TTypeScriptType, - ): AddFieldDescriptor< - TFields, - TOmitInitKey, - TExtra, - TPostDeserialized, - TName, - FixedLengthBufferLikeFieldDefinition< - TType, - FixedLengthBufferLikeFieldOptions, - TTypeScriptType - > - >; - - < - TName extends PropertyKey, - TOptions extends VariableLengthBufferLikeFieldOptions< - TFields, - LengthField - >, - TTypeScriptType = TType["TTypeScriptType"], - >( - name: TName, - options: TOptions, - typeScriptType?: TTypeScriptType, - ): AddFieldDescriptor< - TFields, - TOmitInitKey, - TExtra, - TPostDeserialized, - TName, - VariableLengthBufferLikeFieldDefinition< - TType, - TOptions, - TTypeScriptType +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"; + +export type FieldsType< + T extends Record>, +> = { + [K in keyof T]: T[K] extends Field ? TK : never; +}; + +export type StructInit< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + T extends Struct, +> = Omit< + FieldsType, + { + [K in keyof T["fields"]]: T["fields"][K] extends Field< + unknown, + infer U, + unknown > - >; -} - -export type StructPostDeserialized = ( - this: TFields, - object: TFields, -) => TPostDeserialized; + ? U + : never; + }[keyof T["fields"]] +>; -export type StructDeserializedResult< - TFields extends object, - TExtra extends object, - TPostDeserialized, -> = TPostDeserialized extends undefined - ? Overwrite - : TPostDeserialized; +export type StructValue< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + T extends Struct, +> = ReturnType>; 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>; -} +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type StructLike = Struct; export class Struct< - TFields extends object = Record, - TOmitInitKey extends PropertyKey = never, - TExtra extends object = Record, - TPostDeserialized = undefined, -> implements - StructLike< - StructDeserializedResult - > -{ - readonly TFields!: TFields; - - readonly TOmitInitKey!: TOmitInitKey; - - readonly TExtra!: TExtra; - - readonly TInit!: Evaluate>; + T extends Record>>>, + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + Extra extends Record = {}, + PostDeserialize = FieldsType & Extra, +> { + fields: T; + size: number; - readonly TDeserializeResult!: StructDeserializedResult< - TFields, - TExtra, - TPostDeserialized - >; + #fieldList: [string, Field][] = []; - readonly options: Readonly; + littleEndian: boolean; - #size = 0; - /** - * Gets the static size (exclude fields that can change size at runtime) - */ - get size() { - return this.#size; - } + extra: Extra; - #fields: [ - name: PropertyKey, - definition: StructFieldDefinition, - ][] = []; - get fields(): readonly [ - name: PropertyKey, - definition: StructFieldDefinition, - ][] { - return this.#fields; - } + postDeserialize?: + | ((fields: FieldsType & Extra) => PostDeserialize) + | undefined; - #extra: Record = {}; - - #postDeserialized?: StructPostDeserialized | undefined; + constructor( + fields: T, + options: { + littleEndian?: boolean; + extra?: Extra & ThisType>; + postDeserialize?: ( + this: FieldsType & Extra, + fields: FieldsType & Extra, + ) => PostDeserialize; + }, + ) { + this.#fieldList = Object.entries(fields); + this.fields = fields; + this.size = this.#fieldList.reduce( + (sum, [, field]) => sum + field.size, + 0, + ); - constructor(options?: Partial>) { - this.options = { ...StructDefaultOptions, ...options }; + this.littleEndian = !!options.littleEndian; + this.extra = options.extra!; + this.postDeserialize = options.postDeserialize; } - /** - * 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}'`, + serialize(runtimeStruct: StructInit): Uint8Array; + serialize(runtimeStruct: StructInit, buffer: Uint8Array): number; + serialize( + runtimeStruct: StructInit, + buffer?: Uint8Array, + ): Uint8Array | number { + for (const [key, field] of this.#fieldList) { + if (key in runtimeStruct) { + field.preSerialize?.( + runtimeStruct[key as never], + runtimeStruct, ); } } - 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>( - 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), - ); - return this as never; - } - - #number< - TName extends PropertyKey, - TType extends NumberFieldVariant = NumberFieldVariant, - TTypeScriptType = number, - >(name: TName, type: TType, typeScriptType?: TTypeScriptType) { - return this.field( - name, - new NumberFieldDefinition(type, typeScriptType), + const sizes = this.#fieldList.map( + ([key, field]) => + field.dynamicSize?.(runtimeStruct[key as never]) ?? field.size, ); - } + const size = sizes.reduce((sum, size) => sum + size, 0); - /** - * Appends an `int8` field to the `Struct` - */ - int8( - name: TName, - typeScriptType?: TTypeScriptType, - ) { - return this.#number(name, NumberFieldVariant.Int8, typeScriptType); - } - - /** - * Appends an `uint8` field to the `Struct` - */ - uint8( - name: TName, - typeScriptType?: TTypeScriptType, - ) { - return this.#number(name, NumberFieldVariant.Uint8, typeScriptType); - } - - /** - * Appends an `int16` field to the `Struct` - */ - int16( - name: TName, - typeScriptType?: TTypeScriptType, - ) { - return this.#number(name, NumberFieldVariant.Int16, typeScriptType); - } - - /** - * Appends an `uint16` field to the `Struct` - */ - uint16( - name: TName, - typeScriptType?: TTypeScriptType, - ) { - return this.#number(name, NumberFieldVariant.Uint16, typeScriptType); - } - - /** - * Appends an `int32` field to the `Struct` - */ - int32( - name: TName, - typeScriptType?: TTypeScriptType, - ) { - return this.#number(name, NumberFieldVariant.Int32, typeScriptType); - } - - /** - * Appends an `uint32` field to the `Struct` - */ - uint32( - 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); - } + let externalBuffer = false; + if (buffer) { + if (buffer.length < size) { + throw new Error("Buffer too small"); + } - #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; + externalBuffer = true; } else { - return this.field( - name, - new VariableLengthBufferLikeFieldDefinition(type, options), - ) as never; + buffer = new Uint8Array(size); } - }; - - 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>, - never - >, - >( - value: T & ThisType, TFields>>, - ): Struct, 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, - ): Struct; - /** - * 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, - ): Struct; - /** - * 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( - callback?: StructPostDeserialized, - ): Struct; - postDeserialize(callback?: StructPostDeserialized) { - this.#postDeserialized = callback; - return this as never; - } - - /** - * Deserialize a struct value from `stream`. - */ - deserialize( - stream: ExactReadable, - ): StructDeserializedResult; - deserialize( - stream: AsyncExactReadable, - ): Promise>; - deserialize( - stream: ExactReadable | AsyncExactReadable, - ): ValueOrPromise< - StructDeserializedResult - > { - 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(); - } - }, - ); + 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]!; } - 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(); + if (externalBuffer) { + return size; + } else { + return buffer; + } } - /** - * 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( - init: Evaluate>, - 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); - } + deserialize: { + (reader: ExactReadable): PostDeserialize; + (reader: AsyncExactReadable): MaybePromiseLike; + } = bipedal(function* ( + this: Struct, + then, + reader: AsyncExactReadable, + ) { + const startPosition = reader.position; + + const runtimeStruct = {} as Record; + const context: DeserializeContext = { + reader, + runtimeStruct, + littleEndian: this.littleEndian, + }; + + try { + for (const [key, field] of this.#fieldList) { + runtimeStruct[key] = yield* then(field.deserialize(context)); } - } else { - structValue = new StructValue({}); - for (const [name, definition] of this.#fields) { - const fieldValue = definition.create( - this.options, - structValue, - (init as Record)[name], - ); - structValue.set(name, fieldValue); + } catch (e) { + if (!(e instanceof ExactReadableEndedError)) { + throw e; } - } - - let structSize = 0; - const fieldsInfo: { - fieldValue: StructFieldValue; - size: number; - }[] = []; - - for (const [name] of this.#fields) { - const fieldValue = structValue.get(name); - const size = fieldValue.getSize(); - fieldsInfo.push({ fieldValue, size }); - structSize += size; - } - if (!output) { - output = new Uint8Array(structSize); - } else if (output.length < structSize) { - throw new TypeError("Output buffer is too small"); + if (reader.position === startPosition) { + throw new StructEmptyError(); + } else { + throw new StructNotEnoughDataError(); + } } - let offset = 0; - for (const { fieldValue, size } of fieldsInfo) { - fieldValue.serialize(output, offset); - offset += size; + if (this.extra) { + Object.defineProperties( + runtimeStruct, + Object.getOwnPropertyDescriptors(this.extra), + ); } - if (output.length !== structSize) { - return output.subarray(0, structSize); + if (this.postDeserialize) { + return this.postDeserialize.call( + runtimeStruct, + runtimeStruct as never, + ); } else { - return output; + return runtimeStruct as never; } - } + }) as never; } diff --git a/libraries/struct/src/sync-promise.spec.ts b/libraries/struct/src/sync-promise.spec.ts deleted file mode 100644 index b05662e04..000000000 --- a/libraries/struct/src/sync-promise.spec.ts +++ /dev/null @@ -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"); - }); - }); -}); diff --git a/libraries/struct/src/sync-promise.ts b/libraries/struct/src/sync-promise.ts deleted file mode 100644 index f84eda748..000000000 --- a/libraries/struct/src/sync-promise.ts +++ /dev/null @@ -1,119 +0,0 @@ -export interface SyncPromise { - then( - onfulfilled?: ((value: T) => TResult1 | PromiseLike) | null, - onrejected?: - | ((reason: unknown) => TResult2 | PromiseLike) - | null, - ): SyncPromise; - - valueOrPromise(): T | PromiseLike; -} - -interface SyncPromiseStatic { - reject(reason?: unknown): SyncPromise; - - resolve(): SyncPromise; - resolve(value: T | PromiseLike): SyncPromise; - - try(executor: () => T | PromiseLike): SyncPromise; -} - -export const SyncPromise: SyncPromiseStatic = { - reject(reason?: unknown): SyncPromise { - return new RejectedSyncPromise(reason); - }, - resolve(value?: T | PromiseLike): SyncPromise { - if ( - typeof value === "object" && - value !== null && - typeof (value as PromiseLike).then === "function" - ) { - if ( - value instanceof PendingSyncPromise || - value instanceof ResolvedSyncPromise || - value instanceof RejectedSyncPromise - ) { - return value; - } - - return new PendingSyncPromise(value as PromiseLike); - } else { - return new ResolvedSyncPromise(value as T); - } - }, - try(executor: () => T | PromiseLike): SyncPromise { - try { - return SyncPromise.resolve(executor()); - } catch (e) { - return SyncPromise.reject(e); - } - }, -}; - -class PendingSyncPromise implements SyncPromise { - #promise: PromiseLike; - - constructor(promise: PromiseLike) { - this.#promise = promise; - } - - then( - onfulfilled?: ((value: T) => TResult1 | PromiseLike) | null, - onrejected?: - | ((reason: unknown) => TResult2 | PromiseLike) - | null, - ) { - return new PendingSyncPromise( - this.#promise.then(onfulfilled, onrejected), - ); - } - - valueOrPromise(): T | PromiseLike { - return this.#promise; - } -} - -class ResolvedSyncPromise implements SyncPromise { - #value: T; - - constructor(value: T) { - this.#value = value; - } - - then( - onfulfilled?: ((value: T) => TResult1 | PromiseLike) | null, - ) { - if (!onfulfilled) { - return this as never; - } - return SyncPromise.try(() => onfulfilled(this.#value)); - } - - valueOrPromise(): T | PromiseLike { - return this.#value; - } -} - -class RejectedSyncPromise implements SyncPromise { - #reason: unknown; - - constructor(reason: unknown) { - this.#reason = reason; - } - - then( - _?: ((value: T) => TResult1 | PromiseLike) | null, - onrejected?: - | ((reason: unknown) => TResult2 | PromiseLike) - | null, - ) { - if (!onrejected) { - return this as never; - } - return SyncPromise.try(() => onrejected(this.#reason)); - } - - valueOrPromise(): T | PromiseLike { - throw this.#reason; - } -} diff --git a/libraries/struct/src/types/bigint.ts b/libraries/struct/src/types/bigint.ts deleted file mode 100644 index 1cb96e8e6..000000000 --- a/libraries/struct/src/types/bigint.ts +++ /dev/null @@ -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 { - readonly variant: TVariant; - - constructor(variant: TVariant, typescriptType?: TTypeScriptType) { - void typescriptType; - super(); - this.variant = variant; - } - - getSize(): number { - return this.variant.size; - } - - create( - options: Readonly, - struct: StructValue, - value: TTypeScriptType, - ): BigIntFieldValue { - return new BigIntFieldValue(this, options, struct, value); - } - - override deserialize( - options: Readonly, - stream: ExactReadable, - struct: StructValue, - ): BigIntFieldValue; - override deserialize( - options: Readonly, - stream: AsyncExactReadable, - struct: StructValue, - ): Promise>; - override deserialize( - options: Readonly, - stream: ExactReadable | AsyncExactReadable, - struct: StructValue, - ): ValueOrPromise> { - 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, -> extends StructFieldValue { - override serialize(array: Uint8Array, offset: number): void { - this.definition.variant.serialize( - array, - offset, - this.value as never, - this.options.littleEndian, - ); - } -} diff --git a/libraries/struct/src/types/buffer/base.spec.ts b/libraries/struct/src/types/buffer/base.spec.ts deleted file mode 100644 index 1f1996969..000000000 --- a/libraries/struct/src/types/buffer/base.spec.ts +++ /dev/null @@ -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 { - 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); - }); - }); - }); - }); -}); diff --git a/libraries/struct/src/types/buffer/base.ts b/libraries/struct/src/types/buffer/base.ts deleted file mode 100644 index 76d2cb1ea..000000000 --- a/libraries/struct/src/types/buffer/base.ts +++ /dev/null @@ -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 { - 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 { - 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, - TOptions = void, - TOmitInitKey extends PropertyKey = never, - TTypeScriptType = TConverter["TTypeScriptType"], -> extends StructFieldDefinition { - 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, - struct: StructValue, - value: TTypeScriptType, - array?: Uint8Array, - ): BufferLikeFieldValue { - return new BufferLikeFieldValue(this, options, struct, value, array); - } - - override deserialize( - options: Readonly, - stream: ExactReadable, - struct: StructValue, - ): BufferLikeFieldValue; - override deserialize( - options: Readonly, - stream: AsyncExactReadable, - struct: StructValue, - ): Promise>; - override deserialize( - options: Readonly, - stream: ExactReadable | AsyncExactReadable, - struct: StructValue, - ): ValueOrPromise> { - 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, - any, - any, - any - >, -> extends StructFieldValue { - protected array: Uint8Array | undefined; - - // eslint-disable-next-line @typescript-eslint/max-params - constructor( - definition: TDefinition, - options: Readonly, - 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); - } -} diff --git a/libraries/struct/src/types/buffer/fixed-length.spec.ts b/libraries/struct/src/types/buffer/fixed-length.spec.ts deleted file mode 100644 index e92e306af..000000000 --- a/libraries/struct/src/types/buffer/fixed-length.spec.ts +++ /dev/null @@ -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); - }); - }); - }); -}); diff --git a/libraries/struct/src/types/buffer/fixed-length.ts b/libraries/struct/src/types/buffer/fixed-length.ts deleted file mode 100644 index f55f3561f..000000000 --- a/libraries/struct/src/types/buffer/fixed-length.ts +++ /dev/null @@ -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; - } -} diff --git a/libraries/struct/src/types/buffer/index.ts b/libraries/struct/src/types/buffer/index.ts deleted file mode 100644 index 8a16c1e45..000000000 --- a/libraries/struct/src/types/buffer/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./base.js"; -export * from "./fixed-length.js"; -export * from "./variable-length.js"; diff --git a/libraries/struct/src/types/buffer/variable-length.spec.ts b/libraries/struct/src/types/buffer/variable-length.spec.ts deleted file mode 100644 index 54c9697ef..000000000 --- a/libraries/struct/src/types/buffer/variable-length.spec.ts +++ /dev/null @@ -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 { - 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 - > - > { - 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).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 { - 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); - }); - }); - }); -}); diff --git a/libraries/struct/src/types/buffer/variable-length.ts b/libraries/struct/src/types/buffer/variable-length.ts deleted file mode 100644 index 34c3349df..000000000 --- a/libraries/struct/src/types/buffer/variable-length.ts +++ /dev/null @@ -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 = KeysOfType; - -export interface VariableLengthBufferLikeFieldOptions< - TFields extends object = object, - TLengthField extends LengthField = 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, - struct: StructValue, - value: TTypeScriptType, - array?: Uint8Array, - ): VariableLengthBufferLikeStructFieldValue { - return new VariableLengthBufferLikeStructFieldValue( - this, - options, - struct, - value, - array, - ); - } -} - -export class VariableLengthBufferLikeStructFieldValue< - TDefinition extends - VariableLengthBufferLikeFieldDefinition = VariableLengthBufferLikeFieldDefinition, -> extends BufferLikeFieldValue { - protected length: number | undefined; - - protected lengthFieldValue: VariableLengthBufferLikeFieldLengthValue; - - // eslint-disable-next-line @typescript-eslint/max-params - constructor( - definition: TDefinition, - options: Readonly, - 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 - > ->; - -export class VariableLengthBufferLikeFieldLengthValue extends StructFieldValue< - StructFieldDefinition -> { - protected originalValue: StructFieldValue< - StructFieldDefinition - >; - - protected bufferValue: VariableLengthBufferLikeFieldValueLike; - - constructor( - originalValue: StructFieldValue< - StructFieldDefinition - >, - 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); - } -} diff --git a/libraries/struct/src/types/index.ts b/libraries/struct/src/types/index.ts deleted file mode 100644 index c724c3ab1..000000000 --- a/libraries/struct/src/types/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./bigint.js"; -export * from "./buffer/index.js"; -export * from "./number.js"; diff --git a/libraries/struct/src/types/number-namespace.ts b/libraries/struct/src/types/number-namespace.ts deleted file mode 100644 index 16d1ac819..000000000 --- a/libraries/struct/src/types/number-namespace.ts +++ /dev/null @@ -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, -}; diff --git a/libraries/struct/src/types/number-reexports.ts b/libraries/struct/src/types/number-reexports.ts deleted file mode 100644 index 5b9e663c5..000000000 --- a/libraries/struct/src/types/number-reexports.ts +++ /dev/null @@ -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; -} diff --git a/libraries/struct/src/types/number.spec.ts b/libraries/struct/src/types/number.spec.ts deleted file mode 100644 index f735738e0..000000000 --- a/libraries/struct/src/types/number.spec.ts +++ /dev/null @@ -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], - ); - }); - }); - }); - }); -}); diff --git a/libraries/struct/src/types/number.ts b/libraries/struct/src/types/number.ts deleted file mode 100644 index caaaeeb15..000000000 --- a/libraries/struct/src/types/number.ts +++ /dev/null @@ -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 { - readonly variant: TVariant; - - constructor(variant: TVariant, typescriptType?: TTypeScriptType) { - void typescriptType; - super(); - this.variant = variant; - } - - getSize(): number { - return this.variant.size; - } - - create( - options: Readonly, - struct: StructValue, - value: TTypeScriptType, - ): NumberFieldValue { - return new NumberFieldValue(this, options, struct, value); - } - - override deserialize( - options: Readonly, - stream: ExactReadable, - struct: StructValue, - ): NumberFieldValue; - override deserialize( - options: Readonly, - stream: AsyncExactReadable, - struct: StructValue, - ): Promise>; - override deserialize( - options: Readonly, - stream: ExactReadable | AsyncExactReadable, - struct: StructValue, - ): ValueOrPromise> { - 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, -> extends StructFieldValue { - serialize(array: Uint8Array, offset: number): void { - this.definition.variant.serialize( - array, - offset, - this.value as never, - this.options.littleEndian, - ); - } -} diff --git a/libraries/struct/src/utils.spec.ts b/libraries/struct/src/utils.spec.ts deleted file mode 100644 index 4c96759b9..000000000 --- a/libraries/struct/src/utils.spec.ts +++ /dev/null @@ -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); - }); -}); diff --git a/libraries/struct/src/utils.ts b/libraries/struct/src/utils.ts index e3108404d..7955a8a58 100644 --- a/libraries/struct/src/utils.ts +++ b/libraries/struct/src/utils.ts @@ -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 = Identity>; - * type WithoutIdentity = SomeType; - * - * type WithIdentityResult = WithIdentity; - * // Hover on this one shows `SomeType` - * - * type WithoutIdentityResult = WithoutIdentity; - * // Hover on this one shows `WithoutIdentity` - * ``` - */ -export type Identity = T; - -/** - * Collapse an intersection type (`{ foo: string } & { bar: number }`) to a simple type (`{ foo: string, bar: number }`) - */ -export type Evaluate = T extends infer U ? { [K in keyof U]: U[K] } : never; - -/** - * Overwrite fields in `TBase` with fields in `TNew` - */ -export type Overwrite = Evaluate< - Omit & TNew ->; - -/** - * Remove fields with `never` type - */ -export type OmitNever = 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 = { - [TKey in keyof T]: T[TKey] extends TValue ? TKey : never; -}[keyof T]; - -export type ValueOrPromise = T | PromiseLike; - -/** - * Returns a (fake) value of the given type. - */ -export function placeholder(): 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 | Promise; + +export type MaybePromiseLike = T | PromiseLike; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 29a2899a8..3c3409d63 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: