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

|

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