feat(struct): new API full rewrite

This commit is contained in:
Simon Chan 2024-10-31 17:26:37 +08:00
parent a29268426d
commit d50a170ab8
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
89 changed files with 1487 additions and 5512 deletions

View file

@ -20,7 +20,7 @@ import {
pipeFrom,
} from "@yume-chan/stream-extra";
import type { ExactReadable } from "@yume-chan/struct";
import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct";
import { EmptyUint8Array } from "@yume-chan/struct";
import type { UsbInterfaceFilter } from "./utils.js";
import {
@ -185,7 +185,7 @@ export class AdbDaemonWebUsbConnection
if (zeroMask && (chunk.length & zeroMask) === 0) {
await device.raw.transferOut(
outEndpoint.endpointNumber,
EMPTY_UINT8_ARRAY,
EmptyUint8Array,
);
}
} catch (e) {
@ -234,7 +234,7 @@ export class AdbDaemonWebUsbConnection
);
packet.payload = new Uint8Array(result.data!.buffer);
} else {
packet.payload = EMPTY_UINT8_ARRAY;
packet.payload = EmptyUint8Array;
}
return packet;

View file

@ -13,7 +13,7 @@ import {
BufferedReadableStream,
PushReadableStream,
} from "@yume-chan/stream-extra";
import type { ValueOrPromise } from "@yume-chan/struct";
import type { MaybePromiseLike } from "@yume-chan/struct";
export interface AdbScrcpyConnectionOptions {
scid: number;
@ -54,7 +54,7 @@ export abstract class AdbScrcpyConnection implements Disposable {
this.socketName = this.getSocketName();
}
initialize(): ValueOrPromise<void> {
initialize(): MaybePromiseLike<void> {
// pure virtual method
}
@ -66,7 +66,7 @@ export abstract class AdbScrcpyConnection implements Disposable {
return socketName;
}
abstract getStreams(): ValueOrPromise<AdbScrcpyConnectionStreams>;
abstract getStreams(): MaybePromiseLike<AdbScrcpyConnectionStreams>;
dispose(): void {
// pure virtual method

View file

@ -7,7 +7,7 @@ import {
PushReadableStream,
tryClose,
} from "@yume-chan/stream-extra";
import type { ValueOrPromise } from "@yume-chan/struct";
import type { MaybePromiseLike } from "@yume-chan/struct";
function nodeSocketToConnection(
socket: Socket,
@ -138,7 +138,7 @@ export class AdbServerNodeTcpConnector
return address;
}
removeReverseTunnel(address: string): ValueOrPromise<void> {
removeReverseTunnel(address: string): MaybePromiseLike<void> {
const server = this.#listeners.get(address);
if (!server) {
return;
@ -147,7 +147,7 @@ export class AdbServerNodeTcpConnector
this.#listeners.delete(address);
}
clearReverseTunnels(): ValueOrPromise<void> {
clearReverseTunnels(): MaybePromiseLike<void> {
for (const server of this.#listeners.values()) {
server.close();
}

View file

@ -3,7 +3,7 @@ import type {
ReadableWritablePair,
} from "@yume-chan/stream-extra";
import { ConcatStringStream, TextDecoderStream } from "@yume-chan/stream-extra";
import type { ValueOrPromise } from "@yume-chan/struct";
import type { MaybePromiseLike } from "@yume-chan/struct";
import type { AdbBanner } from "./banner.js";
import type { AdbFrameBuffer } from "./commands/index.js";
@ -19,7 +19,7 @@ import {
import type { AdbFeature } from "./features.js";
export interface Closeable {
close(): ValueOrPromise<void>;
close(): MaybePromiseLike<void>;
}
/**
@ -37,7 +37,7 @@ export interface AdbSocket
export type AdbIncomingSocketHandler = (
socket: AdbSocket,
) => ValueOrPromise<void>;
) => MaybePromiseLike<void>;
export interface AdbTransport extends Closeable {
readonly serial: string;
@ -50,16 +50,16 @@ export interface AdbTransport extends Closeable {
readonly clientFeatures: readonly AdbFeature[];
connect(service: string): ValueOrPromise<AdbSocket>;
connect(service: string): MaybePromiseLike<AdbSocket>;
addReverseTunnel(
handler: AdbIncomingSocketHandler,
address?: string,
): ValueOrPromise<string>;
): MaybePromiseLike<string>;
removeReverseTunnel(address: string): ValueOrPromise<void>;
removeReverseTunnel(address: string): MaybePromiseLike<void>;
clearReverseTunnels(): ValueOrPromise<void>;
clearReverseTunnels(): MaybePromiseLike<void>;
}
export class Adb implements Closeable {

View file

@ -1,50 +1,53 @@
import { BufferedReadableStream } from "@yume-chan/stream-extra";
import Struct, { StructEmptyError } from "@yume-chan/struct";
import type { StructValue } from "@yume-chan/struct";
import { buffer, Struct, StructEmptyError, u32 } from "@yume-chan/struct";
import type { Adb } from "../adb.js";
const Version =
/* #__PURE__ */
new Struct({ littleEndian: true }).uint32("version");
const Version = new Struct({ version: u32 }, { littleEndian: true });
export const AdbFrameBufferV1 =
/* #__PURE__ */
new Struct({ littleEndian: true })
.uint32("bpp")
.uint32("size")
.uint32("width")
.uint32("height")
.uint32("red_offset")
.uint32("red_length")
.uint32("blue_offset")
.uint32("blue_length")
.uint32("green_offset")
.uint32("green_length")
.uint32("alpha_offset")
.uint32("alpha_length")
.uint8Array("data", { lengthField: "size" });
export const AdbFrameBufferV1 = new Struct(
{
bpp: u32,
size: u32,
width: u32,
height: u32,
red_offset: u32,
red_length: u32,
blue_offset: u32,
blue_length: u32,
green_offset: u32,
green_length: u32,
alpha_offset: u32,
alpha_length: u32,
data: buffer("size"),
},
{ littleEndian: true },
);
export type AdbFrameBufferV1 = (typeof AdbFrameBufferV1)["TDeserializeResult"];
export type AdbFrameBufferV1 = StructValue<typeof AdbFrameBufferV1>;
export const AdbFrameBufferV2 =
/* #__PURE__ */
new Struct({ littleEndian: true })
.uint32("bpp")
.uint32("colorSpace")
.uint32("size")
.uint32("width")
.uint32("height")
.uint32("red_offset")
.uint32("red_length")
.uint32("blue_offset")
.uint32("blue_length")
.uint32("green_offset")
.uint32("green_length")
.uint32("alpha_offset")
.uint32("alpha_length")
.uint8Array("data", { lengthField: "size" });
export const AdbFrameBufferV2 = new Struct(
{
bpp: u32,
colorSpace: u32,
size: u32,
width: u32,
height: u32,
red_offset: u32,
red_length: u32,
blue_offset: u32,
blue_length: u32,
green_offset: u32,
green_length: u32,
alpha_offset: u32,
alpha_length: u32,
data: buffer("size"),
},
{ littleEndian: true },
);
export type AdbFrameBufferV2 = (typeof AdbFrameBufferV2)["TDeserializeResult"];
export type AdbFrameBufferV2 = StructValue<typeof AdbFrameBufferV2>;
/**
* ADB uses 8 int32 fields to describe bit depths
@ -99,9 +102,9 @@ export async function framebuffer(adb: Adb): Promise<AdbFrameBuffer> {
switch (version) {
case 1:
// TODO: AdbFrameBuffer: does all v1 responses uses the same color space? Add it so the command returns same format for all versions.
return AdbFrameBufferV1.deserialize(stream);
return await AdbFrameBufferV1.deserialize(stream);
case 2:
return AdbFrameBufferV2.deserialize(stream);
return await AdbFrameBufferV2.deserialize(stream);
default:
throw new AdbFrameBufferUnsupportedVersionError(version);
}

View file

@ -1,7 +1,12 @@
// cspell: ignore killforward
import { BufferedReadableStream } from "@yume-chan/stream-extra";
import Struct, { ExactReadableEndedError, encodeUtf8 } from "@yume-chan/struct";
import {
ExactReadableEndedError,
Struct,
encodeUtf8,
string,
} from "@yume-chan/struct";
import type { Adb, AdbIncomingSocketHandler } from "../adb.js";
import { hexToNumber, sequenceEqual } from "../utils/index.js";
@ -14,11 +19,21 @@ export interface AdbForwardListener {
remoteName: string;
}
const AdbReverseStringResponse =
/* #__PURE__ */
new Struct()
.string("length", { length: 4 })
.string("content", { lengthField: "length", lengthFieldRadix: 16 });
const AdbReverseStringResponse = new Struct(
{
length: string(4),
content: string({
field: "length",
convert(value: string) {
return Number.parseInt(value);
},
back(value) {
return value.toString(16).padStart(4, "0");
},
}),
},
{ littleEndian: true },
);
export class AdbReverseError extends Error {
constructor(message: string) {
@ -35,9 +50,9 @@ export class AdbReverseNotSupportedError extends AdbReverseError {
}
}
const AdbReverseErrorResponse =
/* #__PURE__ */
new Struct().concat(AdbReverseStringResponse).postDeserialize((value) => {
const AdbReverseErrorResponse = new Struct(AdbReverseStringResponse.fields, {
littleEndian: true,
postDeserialize: (value) => {
// https://issuetracker.google.com/issues/37066218
// ADB on Android <9 can't create reverse tunnels when connected wirelessly (ADB over Wi-Fi),
// and returns this confusing "more than one device/emulator" error.
@ -46,7 +61,8 @@ const AdbReverseErrorResponse =
} else {
throw new AdbReverseError(value.content);
}
});
},
});
// Like `hexToNumber`, it's much faster than first converting `buffer` to a string
function decimalToNumber(buffer: Uint8Array) {

View file

@ -51,6 +51,18 @@ async function assertResolves<T>(promise: Promise<T>, expected: T) {
return assert.deepStrictEqual(await promise, expected);
}
describe("AdbShellProtocolPacket", () => {
it("should serialize", () => {
assert.deepStrictEqual(
AdbShellProtocolPacket.serialize({
id: AdbShellProtocolId.Stdout,
data: new Uint8Array([1, 2, 3, 4]),
}),
new Uint8Array([1, 4, 0, 0, 0, 1, 2, 3, 4]),
);
});
});
describe("AdbSubprocessShellProtocol", () => {
describe("`stdout` and `stderr`", () => {
it("should parse data from `socket", () => {

View file

@ -10,8 +10,8 @@ import {
StructDeserializeStream,
WritableStream,
} from "@yume-chan/stream-extra";
import type { StructValueType } from "@yume-chan/struct";
import Struct, { placeholder } from "@yume-chan/struct";
import type { StructValue } from "@yume-chan/struct";
import { Struct, buffer, u32, u8 } from "@yume-chan/struct";
import type { Adb, AdbSocket } from "../../../adb.js";
import { AdbFeature } from "../../../features.js";
@ -32,14 +32,15 @@ export type AdbShellProtocolId =
(typeof AdbShellProtocolId)[keyof typeof AdbShellProtocolId];
// This packet format is used in both directions.
export const AdbShellProtocolPacket =
/* #__PURE__ */
new Struct({ littleEndian: true })
.uint8("id", placeholder<AdbShellProtocolId>())
.uint32("length")
.uint8Array("data", { lengthField: "length" });
export const AdbShellProtocolPacket = new Struct(
{
id: u8.as<AdbShellProtocolId>(),
data: buffer(u32),
},
{ littleEndian: true },
);
type AdbShellProtocolPacket = StructValueType<typeof AdbShellProtocolPacket>;
type AdbShellProtocolPacket = StructValue<typeof AdbShellProtocolPacket>;
/**
* Shell v2 a.k.a Shell Protocol

View file

@ -3,7 +3,7 @@ import type {
ReadableStream,
WritableStream,
} from "@yume-chan/stream-extra";
import type { ValueOrPromise } from "@yume-chan/struct";
import type { MaybePromiseLike } from "@yume-chan/struct";
import type { Adb, AdbSocket } from "../../../adb.js";
@ -40,23 +40,23 @@ export interface AdbSubprocessProtocol {
* Some `AdbSubprocessProtocol`s may not support resizing
* and will ignore calls to this method.
*/
resize(rows: number, cols: number): ValueOrPromise<void>;
resize(rows: number, cols: number): MaybePromiseLike<void>;
/**
* Kills the current process.
*/
kill(): ValueOrPromise<void>;
kill(): MaybePromiseLike<void>;
}
export interface AdbSubprocessProtocolConstructor {
/** Returns `true` if the `adb` instance supports this shell */
isSupported(adb: Adb): ValueOrPromise<boolean>;
isSupported(adb: Adb): MaybePromiseLike<boolean>;
/** Spawns an executable in PTY (interactive) mode. */
pty(adb: Adb, command: string): ValueOrPromise<AdbSubprocessProtocol>;
pty(adb: Adb, command: string): MaybePromiseLike<AdbSubprocessProtocol>;
/** Spawns an executable and pipe the output. */
raw(adb: Adb, command: string): ValueOrPromise<AdbSubprocessProtocol>;
raw(adb: Adb, command: string): MaybePromiseLike<AdbSubprocessProtocol>;
/** Creates a new `AdbShell` by attaching to an exist `AdbSocket` */
new (socket: AdbSocket): AdbSubprocessProtocol;

View file

@ -1,4 +1,5 @@
import Struct from "@yume-chan/struct";
import type { StructValue } from "@yume-chan/struct";
import { Struct, string, u32 } from "@yume-chan/struct";
import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js";
import { AdbSyncResponseId, adbSyncReadResponses } from "./response.js";
@ -14,25 +15,25 @@ export interface AdbSyncEntry extends AdbSyncStat {
name: string;
}
export const AdbSyncEntryResponse =
/* #__PURE__ */
new Struct({ littleEndian: true })
.concat(AdbSyncLstatResponse)
.uint32("nameLength")
.string("name", { lengthField: "nameLength" });
export const AdbSyncEntryResponse = new Struct(
{
...AdbSyncLstatResponse.fields,
name: string(u32),
},
{ littleEndian: true, extra: AdbSyncLstatResponse.extra },
);
export type AdbSyncEntryResponse =
(typeof AdbSyncEntryResponse)["TDeserializeResult"];
export type AdbSyncEntryResponse = StructValue<typeof AdbSyncEntryResponse>;
export const AdbSyncEntry2Response =
/* #__PURE__ */
new Struct({ littleEndian: true })
.concat(AdbSyncStatResponse)
.uint32("nameLength")
.string("name", { lengthField: "nameLength" });
export const AdbSyncEntry2Response = new Struct(
{
...AdbSyncStatResponse.fields,
name: string(u32),
},
{ littleEndian: true, extra: AdbSyncStatResponse.extra },
);
export type AdbSyncEntry2Response =
(typeof AdbSyncEntry2Response)["TDeserializeResult"];
export type AdbSyncEntry2Response = StructValue<typeof AdbSyncEntry2Response>;
export async function* adbSyncOpenDirV2(
socket: AdbSyncSocket,

View file

@ -1,19 +1,18 @@
import type { ReadableStream } from "@yume-chan/stream-extra";
import { PushReadableStream } from "@yume-chan/stream-extra";
import Struct from "@yume-chan/struct";
import type { StructValue } from "@yume-chan/struct";
import { buffer, Struct, u32 } from "@yume-chan/struct";
import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js";
import { AdbSyncResponseId, adbSyncReadResponses } from "./response.js";
import { adbSyncReadResponses, AdbSyncResponseId } from "./response.js";
import type { AdbSyncSocket } from "./socket.js";
export const AdbSyncDataResponse =
/* #__PURE__ */
new Struct({ littleEndian: true })
.uint32("dataLength")
.uint8Array("data", { lengthField: "dataLength" });
export const AdbSyncDataResponse = new Struct(
{ data: buffer(u32) },
{ littleEndian: true },
);
export type AdbSyncDataResponse =
(typeof AdbSyncDataResponse)["TDeserializeResult"];
export type AdbSyncDataResponse = StructValue<typeof AdbSyncDataResponse>;
export async function* adbSyncPullGenerator(
socket: AdbSyncSocket,

View file

@ -4,7 +4,7 @@ import {
DistributionStream,
MaybeConsumable,
} from "@yume-chan/stream-extra";
import Struct, { placeholder } from "@yume-chan/struct";
import { Struct, u32 } from "@yume-chan/struct";
import { NOOP } from "../../utils/index.js";
@ -25,9 +25,10 @@ export interface AdbSyncPushV1Options {
packetSize?: number;
}
export const AdbSyncOkResponse =
/* #__PURE__ */
new Struct({ littleEndian: true }).uint32("unused");
export const AdbSyncOkResponse = new Struct(
{ unused: u32 },
{ littleEndian: true },
);
async function pipeFileData(
locked: AdbSyncSocketLocked,
@ -113,12 +114,10 @@ export interface AdbSyncPushV2Options extends AdbSyncPushV1Options {
dryRun?: boolean;
}
export const AdbSyncSendV2Request =
/* #__PURE__ */
new Struct({ littleEndian: true })
.uint32("id")
.uint32("mode")
.uint32("flags", placeholder<AdbSyncSendV2Flags>());
export const AdbSyncSendV2Request = new Struct(
{ id: u32, mode: u32, flags: u32.as<AdbSyncSendV2Flags>() },
{ littleEndian: true },
);
export async function adbSyncPushV2({
socket,

View file

@ -1,6 +1,4 @@
import Struct from "@yume-chan/struct";
import { encodeUtf8 } from "../../utils/index.js";
import { encodeUtf8, Struct, u32 } from "@yume-chan/struct";
import { adbSyncEncodeId } from "./response.js";
@ -17,9 +15,10 @@ export const AdbSyncRequestId = {
Receive: adbSyncEncodeId("RECV"),
} as const;
export const AdbSyncNumberRequest =
/* #__PURE__ */
new Struct({ littleEndian: true }).uint32("id").uint32("arg");
export const AdbSyncNumberRequest = new Struct(
{ id: u32, arg: u32 },
{ littleEndian: true },
);
export interface AdbSyncWritable {
write(buffer: Uint8Array): Promise<void>;

View file

@ -1,10 +1,6 @@
import { getUint32LittleEndian } from "@yume-chan/no-data-view";
import type {
AsyncExactReadable,
StructLike,
StructValueType,
} from "@yume-chan/struct";
import Struct, { decodeUtf8 } from "@yume-chan/struct";
import type { AsyncExactReadable, StructLike } from "@yume-chan/struct";
import { Struct, decodeUtf8, string, u32 } from "@yume-chan/struct";
function encodeAsciiUnchecked(value: string): Uint8Array {
const result = new Uint8Array(value.length);
@ -40,14 +36,15 @@ export const AdbSyncResponseId = {
export class AdbSyncError extends Error {}
export const AdbSyncFailResponse =
/* #__PURE__ */
new Struct({ littleEndian: true })
.uint32("messageLength")
.string("message", { lengthField: "messageLength" })
.postDeserialize((object) => {
throw new AdbSyncError(object.message);
});
export const AdbSyncFailResponse = new Struct(
{ message: string(u32) },
{
littleEndian: true,
postDeserialize(value) {
throw new AdbSyncError(value.message);
},
},
);
export async function adbSyncReadResponse<T>(
stream: AsyncExactReadable,
@ -72,13 +69,11 @@ export async function adbSyncReadResponse<T>(
}
}
export async function* adbSyncReadResponses<
T extends Struct<object, PropertyKey, object, unknown>,
>(
export async function* adbSyncReadResponses<T>(
stream: AsyncExactReadable,
id: number | string,
type: T,
): AsyncGenerator<StructValueType<T>, void, void> {
type: StructLike<T>,
): AsyncGenerator<T, void, void> {
if (typeof id === "string") {
id = adbSyncEncodeId(id);
}
@ -97,7 +92,7 @@ export async function* adbSyncReadResponses<
await stream.readExactly(type.size);
return;
case id:
yield (await type.deserialize(stream)) as StructValueType<T>;
yield await type.deserialize(stream);
break;
default:
throw new Error(

View file

@ -1,4 +1,5 @@
import Struct, { placeholder } from "@yume-chan/struct";
import type { StructValue } from "@yume-chan/struct";
import { Struct, u32, u64 } from "@yume-chan/struct";
import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js";
import { AdbSyncResponseId, adbSyncReadResponse } from "./response.js";
@ -26,28 +27,28 @@ export interface AdbSyncStat {
ctime?: bigint;
}
export const AdbSyncLstatResponse =
/* #__PURE__ */
new Struct({ littleEndian: true })
.int32("mode")
.int32("size")
.int32("mtime")
.extra({
get type() {
export const AdbSyncLstatResponse = new Struct(
{ mode: u32, size: u32, mtime: u32 },
{
littleEndian: true,
extra: {
get type(): LinuxFileType {
return (this.mode >> 12) as LinuxFileType;
},
get permission() {
get permission(): number {
return this.mode & 0b00001111_11111111;
},
})
.postDeserialize((object) => {
if (object.mode === 0 && object.size === 0 && object.mtime === 0) {
},
postDeserialize(value) {
if (value.mode === 0 && value.size === 0 && value.mtime === 0) {
throw new Error("lstat error");
}
});
return value;
},
},
);
export type AdbSyncLstatResponse =
(typeof AdbSyncLstatResponse)["TDeserializeResult"];
export type AdbSyncLstatResponse = StructValue<typeof AdbSyncLstatResponse>;
export const AdbSyncStatErrorCode = {
SUCCESS: 0,
@ -85,36 +86,40 @@ const AdbSyncStatErrorName =
]),
);
export const AdbSyncStatResponse =
/* #__PURE__ */
new Struct({ littleEndian: true })
.uint32("error", placeholder<AdbSyncStatErrorCode>())
.uint64("dev")
.uint64("ino")
.uint32("mode")
.uint32("nlink")
.uint32("uid")
.uint32("gid")
.uint64("size")
.uint64("atime")
.uint64("mtime")
.uint64("ctime")
.extra({
get type() {
export const AdbSyncStatResponse = new Struct(
{
error: u32.as<AdbSyncStatErrorCode>(),
dev: u64,
ino: u64,
mode: u32,
nlink: u32,
uid: u32,
gid: u32,
size: u64,
atime: u64,
mtime: u64,
ctime: u64,
},
{
littleEndian: true,
extra: {
get type(): LinuxFileType {
return (this.mode >> 12) as LinuxFileType;
},
get permission() {
get permission(): number {
return this.mode & 0b00001111_11111111;
},
})
.postDeserialize((object) => {
if (object.error) {
throw new Error(AdbSyncStatErrorName[object.error]);
},
postDeserialize(value) {
if (value.error) {
throw new Error(AdbSyncStatErrorName[value.error]);
}
});
return value;
},
},
);
export type AdbSyncStatResponse =
(typeof AdbSyncStatResponse)["TDeserializeResult"];
export type AdbSyncStatResponse = StructValue<typeof AdbSyncStatResponse>;
export async function adbSyncLstat(
socket: AdbSyncSocket,

View file

@ -1,7 +1,7 @@
import * as assert from "node:assert";
import { describe, it } from "node:test";
import { EMPTY_UINT8_ARRAY, encodeUtf8 } from "@yume-chan/struct";
import { EmptyUint8Array, encodeUtf8 } from "@yume-chan/struct";
import { decodeBase64 } from "../utils/base64.js";
@ -86,7 +86,7 @@ describe("auth", () => {
command: AdbCommand.Auth,
arg0: AdbAuthType.Token,
arg1: 0,
payload: EMPTY_UINT8_ARRAY,
payload: EmptyUint8Array,
}),
);
@ -118,7 +118,7 @@ describe("auth", () => {
command: AdbCommand.Auth,
arg0: AdbAuthType.Token,
arg1: 0,
payload: EMPTY_UINT8_ARRAY,
payload: EmptyUint8Array,
}),
);

View file

@ -1,7 +1,7 @@
import { PromiseResolver } from "@yume-chan/async";
import type { Disposable } from "@yume-chan/event";
import type { ValueOrPromise } from "@yume-chan/struct";
import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct";
import type { MaybePromiseLike } from "@yume-chan/struct";
import { EmptyUint8Array } from "@yume-chan/struct";
import {
calculateBase64EncodedLength,
@ -33,7 +33,7 @@ export interface AdbCredentialStore {
/**
* Generates and stores a RSA private key with modulus length `2048` and public exponent `65537`.
*/
generateKey(): ValueOrPromise<AdbPrivateKey>;
generateKey(): MaybePromiseLike<AdbPrivateKey>;
/**
* Synchronously or asynchronously iterates through all stored RSA private keys.
@ -114,7 +114,7 @@ export const AdbPublicKeyAuthenticator: AdbAuthenticator = async function* (
const nameBuffer = privateKey.name?.length
? encodeUtf8(privateKey.name)
: EMPTY_UINT8_ARRAY;
: EmptyUint8Array;
const publicKeyBuffer = new Uint8Array(
publicKeyBase64Length +
(nameBuffer.length ? nameBuffer.length + 1 : 0) + // Space character + name

View file

@ -1,5 +1,5 @@
import type { Consumable, ReadableWritablePair } from "@yume-chan/stream-extra";
import type { ValueOrPromise } from "@yume-chan/struct";
import type { MaybePromiseLike } from "@yume-chan/struct";
import type { AdbPacketData, AdbPacketInit } from "./packet.js";
@ -8,7 +8,7 @@ export interface AdbDaemonDevice {
readonly name: string | undefined;
connect(): ValueOrPromise<
connect(): MaybePromiseLike<
ReadableWritablePair<AdbPacketData, Consumable<AdbPacketInit>>
>;
}

View file

@ -16,7 +16,7 @@ import {
Consumable,
WritableStream,
} from "@yume-chan/stream-extra";
import { EMPTY_UINT8_ARRAY, decodeUtf8, encodeUtf8 } from "@yume-chan/struct";
import { EmptyUint8Array, decodeUtf8, encodeUtf8 } from "@yume-chan/struct";
import type { AdbIncomingSocketHandler, AdbSocket, Closeable } from "../adb.js";
@ -259,7 +259,7 @@ export class AdbPacketDispatcher implements Closeable {
AdbCommand.Close,
packet.arg1,
packet.arg0,
EMPTY_UINT8_ARRAY,
EmptyUint8Array,
);
}
@ -271,7 +271,7 @@ export class AdbPacketDispatcher implements Closeable {
payload = new Uint8Array(4);
setUint32LittleEndian(payload, 0, ackBytes);
} else {
payload = EMPTY_UINT8_ARRAY;
payload = EmptyUint8Array;
}
return this.sendPacket(AdbCommand.Okay, localId, remoteId, payload);
@ -312,7 +312,7 @@ export class AdbPacketDispatcher implements Closeable {
AdbCommand.Close,
0,
remoteId,
EMPTY_UINT8_ARRAY,
EmptyUint8Array,
);
return;
}
@ -339,7 +339,7 @@ export class AdbPacketDispatcher implements Closeable {
AdbCommand.Close,
0,
remoteId,
EMPTY_UINT8_ARRAY,
EmptyUint8Array,
);
}
}

View file

@ -1,5 +1,6 @@
import { Consumable, TransformStream } from "@yume-chan/stream-extra";
import Struct from "@yume-chan/struct";
import type { StructInit, StructValue } from "@yume-chan/struct";
import { buffer, s32, Struct, u32 } from "@yume-chan/struct";
export const AdbCommand = {
Auth: 0x48545541, // 'AUTH'
@ -12,27 +13,28 @@ export const AdbCommand = {
export type AdbCommand = (typeof AdbCommand)[keyof typeof AdbCommand];
export const AdbPacketHeader =
/* #__PURE__ */
new Struct({ littleEndian: true })
.uint32("command")
.uint32("arg0")
.uint32("arg1")
.uint32("payloadLength")
.uint32("checksum")
.int32("magic");
export const AdbPacketHeader = new Struct(
{
command: u32,
arg0: u32,
arg1: u32,
payloadLength: u32,
checksum: u32,
magic: s32,
},
{ littleEndian: true },
);
export type AdbPacketHeader = (typeof AdbPacketHeader)["TDeserializeResult"];
export type AdbPacketHeader = StructValue<typeof AdbPacketHeader>;
type AdbPacketHeaderInit = (typeof AdbPacketHeader)["TInit"];
type AdbPacketHeaderInit = StructInit<typeof AdbPacketHeader>;
export const AdbPacket =
/* #__PURE__ */
new Struct({ littleEndian: true })
.concat(AdbPacketHeader)
.uint8Array("payload", { lengthField: "payloadLength" });
export const AdbPacket = new Struct(
{ ...AdbPacketHeader.fields, payload: buffer("payloadLength") },
{ littleEndian: true },
);
export type AdbPacket = (typeof AdbPacket)["TDeserializeResult"];
export type AdbPacket = StructValue<typeof AdbPacket>;
/**
* `AdbPacketData` contains all the useful fields of `AdbPacket`.
@ -45,11 +47,11 @@ export type AdbPacket = (typeof AdbPacket)["TDeserializeResult"];
* so `AdbSocket#writable#write` only needs `AdbPacketData`.
*/
export type AdbPacketData = Omit<
(typeof AdbPacket)["TInit"],
StructInit<typeof AdbPacket>,
"checksum" | "magic"
>;
export type AdbPacketInit = (typeof AdbPacket)["TInit"];
export type AdbPacketInit = StructInit<typeof AdbPacket>;
export function calculateChecksum(payload: Uint8Array): number {
return payload.reduce((result, item) => result + item, 0);
@ -67,9 +69,10 @@ export class AdbPacketSerializeStream extends TransformStream<
const init = chunk as AdbPacketInit & AdbPacketHeaderInit;
init.payloadLength = init.payload.length;
AdbPacketHeader.serialize(init, headerBuffer);
await Consumable.ReadableStream.enqueue(
controller,
AdbPacketHeader.serialize(init, headerBuffer),
headerBuffer,
);
if (init.payloadLength) {

View file

@ -7,7 +7,7 @@ import type {
WritableStreamDefaultController,
} from "@yume-chan/stream-extra";
import { MaybeConsumable, PushReadableStream } from "@yume-chan/stream-extra";
import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct";
import { EmptyUint8Array } from "@yume-chan/struct";
import type { AdbSocket } from "../adb.js";
@ -164,7 +164,7 @@ export class AdbDaemonSocketController
AdbCommand.Close,
this.localId,
this.remoteId,
EMPTY_UINT8_ARRAY,
EmptyUint8Array,
);
}

View file

@ -5,7 +5,7 @@ import {
Consumable,
WritableStream,
} from "@yume-chan/stream-extra";
import type { ValueOrPromise } from "@yume-chan/struct";
import type { MaybePromiseLike } from "@yume-chan/struct";
import { decodeUtf8, encodeUtf8 } from "@yume-chan/struct";
import type {
@ -368,7 +368,7 @@ export class AdbDaemonTransport implements AdbTransport {
this.#protocolVersion = version;
}
connect(service: string): ValueOrPromise<AdbSocket> {
connect(service: string): MaybePromiseLike<AdbSocket> {
return this.#dispatcher.createSocket(service);
}
@ -392,7 +392,7 @@ export class AdbDaemonTransport implements AdbTransport {
this.#dispatcher.clearReverseTunnels();
}
close(): ValueOrPromise<void> {
close(): MaybePromiseLike<void> {
return this.#dispatcher.close();
}
}

View file

@ -4,22 +4,17 @@ import { PromiseResolver } from "@yume-chan/async";
import { getUint64LittleEndian } from "@yume-chan/no-data-view";
import type {
AbortSignal,
MaybeConsumable,
ReadableWritablePair,
WritableStreamDefaultWriter,
MaybeConsumable,
} from "@yume-chan/stream-extra";
import {
BufferedReadableStream,
tryCancel,
tryClose,
} from "@yume-chan/stream-extra";
import type { ValueOrPromise } from "@yume-chan/struct";
import {
EMPTY_UINT8_ARRAY,
SyncPromise,
decodeUtf8,
encodeUtf8,
} from "@yume-chan/struct";
import type { MaybePromiseLike } from "@yume-chan/struct";
import { bipedal, decodeUtf8, encodeUtf8 } from "@yume-chan/struct";
import type { AdbIncomingSocketHandler, AdbSocket, Closeable } from "../adb.js";
import { AdbBanner } from "../banner.js";
@ -42,21 +37,16 @@ class AdbServerStream {
this.#writer = connection.writable.getWriter();
}
readExactly(length: number): ValueOrPromise<Uint8Array> {
readExactly(length: number): MaybePromiseLike<Uint8Array> {
return this.#buffered.readExactly(length);
}
readString() {
return SyncPromise.try(() => this.readExactly(4))
.then((buffer) => {
const length = hexToNumber(buffer);
readString = bipedal(function* (this: AdbServerStream, then) {
const data = yield* then(this.readExactly(4));
const length = hexToNumber(data);
if (length === 0) {
return EMPTY_UINT8_ARRAY;
return "";
} else {
return this.readExactly(length);
}
})
.then((buffer) => {
// TODO: Investigate using stream mode `TextDecoder` for long strings.
// Because concatenating strings uses rope data structure,
// which only points to the original strings and doesn't copy the data,
@ -76,10 +66,9 @@ class AdbServerStream {
// `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();
return decodeUtf8(yield* then(this.readExactly(length)));
}
});
async writeString(value: string): Promise<void> {
// TODO: investigate using `encodeUtf8("0000" + value)` then modifying the length
@ -572,16 +561,16 @@ export namespace AdbServerClient {
export interface ServerConnector {
connect(
options?: ServerConnectionOptions,
): ValueOrPromise<ServerConnection>;
): MaybePromiseLike<ServerConnection>;
addReverseTunnel(
handler: AdbIncomingSocketHandler,
address?: string,
): ValueOrPromise<string>;
): MaybePromiseLike<string>;
removeReverseTunnel(address: string): ValueOrPromise<void>;
removeReverseTunnel(address: string): MaybePromiseLike<void>;
clearReverseTunnels(): ValueOrPromise<void>;
clearReverseTunnels(): MaybePromiseLike<void>;
}
export interface Socket extends AdbSocket {

View file

@ -1,6 +1,5 @@
import { PromiseResolver } from "@yume-chan/async";
import { AbortController } from "@yume-chan/stream-extra";
import type { ValueOrPromise } from "@yume-chan/struct";
import type {
AdbIncomingSocketHandler,
@ -96,7 +95,7 @@ export class AdbServerTransport implements AdbTransport {
await this.#client.connector.clearReverseTunnels();
}
close(): ValueOrPromise<void> {
close(): void | Promise<void> {
this.#closed.resolve();
this.#waitAbortController.abort();
}

View file

@ -10,8 +10,8 @@ import {
WrapReadableStream,
WritableStream,
} from "@yume-chan/stream-extra";
import type { AsyncExactReadable } from "@yume-chan/struct";
import Struct, { decodeUtf8 } from "@yume-chan/struct";
import type { AsyncExactReadable, StructValue } from "@yume-chan/struct";
import { Struct, decodeUtf8, u16, u32 } from "@yume-chan/struct";
// `adb logcat` is an alias to `adb shell logcat`
// so instead of adding to core library, it's implemented here
@ -99,27 +99,31 @@ export interface LogcatOptions {
const NANOSECONDS_PER_SECOND = /* #__PURE__ */ BigInt(1e9);
// https://cs.android.com/android/platform/superproject/+/master:system/logging/liblog/include/log/log_read.h;l=39;drc=82b5738732161dbaafb2e2f25cce19cd26b9157d
export const LoggerEntry =
/* #__PURE__ */
new Struct({ littleEndian: true })
.uint16("payloadSize")
.uint16("headerSize")
.int32("pid")
.uint32("tid")
.uint32("seconds")
.uint32("nanoseconds")
.uint32("logId")
.uint32("uid")
.extra({
get timestamp() {
export const LoggerEntry = new Struct(
{
payloadSize: u16,
headerSize: u16,
pid: u32,
tid: u32,
seconds: u32,
nanoseconds: u32,
logId: u32,
uid: u32,
},
{
littleEndian: true,
extra: {
get timestamp(): bigint {
return (
BigInt(this.seconds) * NANOSECONDS_PER_SECOND +
BigInt(this.nanoseconds)
);
},
});
},
},
);
export type LoggerEntry = (typeof LoggerEntry)["TDeserializeResult"];
export type LoggerEntry = StructValue<typeof LoggerEntry>;
// https://cs.android.com/android/platform/superproject/+/master:system/logging/liblog/logprint.cpp;drc=bbe77d66e7bee8bd1f0bc7e5492b5376b0207ef6;bpv=0
export interface AndroidLogEntry extends LoggerEntry {

View file

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

View file

@ -1,4 +1,5 @@
import Struct, { placeholder } from "@yume-chan/struct";
import type { StructInit } from "@yume-chan/struct";
import { Struct, u32, u8 } from "@yume-chan/struct";
export enum AndroidKeyEventAction {
Down = 0,
@ -205,14 +206,17 @@ export enum AndroidKeyCode {
AndroidPaste,
}
export const ScrcpyInjectKeyCodeControlMessage =
/* #__PURE__ */
new Struct()
.uint8("type")
.uint8("action", placeholder<AndroidKeyEventAction>())
.uint32("keyCode", placeholder<AndroidKeyCode>())
.uint32("repeat")
.uint32("metaState", placeholder<AndroidKeyEventMeta>());
export const ScrcpyInjectKeyCodeControlMessage = new Struct(
{
type: u8,
action: u8.as<AndroidKeyEventAction>(),
keyCode: u32.as<AndroidKeyCode>(),
repeat: u32,
metaState: u32.as<AndroidKeyEventMeta>(),
},
{ littleEndian: false },
);
export type ScrcpyInjectKeyCodeControlMessage =
(typeof ScrcpyInjectKeyCodeControlMessage)["TInit"];
export type ScrcpyInjectKeyCodeControlMessage = StructInit<
typeof ScrcpyInjectKeyCodeControlMessage
>;

View file

@ -1,11 +1,11 @@
import Struct from "@yume-chan/struct";
import type { StructInit } from "@yume-chan/struct";
import { string, Struct, u32, u8 } from "@yume-chan/struct";
export const ScrcpyInjectTextControlMessage =
/* #__PURE__ */
new Struct()
.uint8("type")
.uint32("length")
.string("text", { lengthField: "length" });
export const ScrcpyInjectTextControlMessage = new Struct(
{ type: u8, text: string(u32) },
{ littleEndian: false },
);
export type ScrcpyInjectTextControlMessage =
(typeof ScrcpyInjectTextControlMessage)["TInit"];
export type ScrcpyInjectTextControlMessage = StructInit<
typeof ScrcpyInjectTextControlMessage
>;

View file

@ -2,5 +2,4 @@ import { EmptyControlMessage } from "./empty.js";
export const ScrcpyRotateDeviceControlMessage = EmptyControlMessage;
export type ScrcpyRotateDeviceControlMessage =
(typeof ScrcpyRotateDeviceControlMessage)["TInit"];
export type ScrcpyRotateDeviceControlMessage = EmptyControlMessage;

View file

@ -1,16 +1,20 @@
import Struct, { placeholder } from "@yume-chan/struct";
import type { StructInit } from "@yume-chan/struct";
import { Struct, u8 } from "@yume-chan/struct";
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/SurfaceControl.java;l=659;drc=20303e05bf73796124ab70a279cf849b61b97905
export enum AndroidScreenPowerMode {
Off = 0,
Normal = 2,
}
export const AndroidScreenPowerMode = {
Off: 0,
Normal: 2,
} as const;
export const ScrcpySetScreenPowerModeControlMessage =
/* #__PURE__ */
new Struct()
.uint8("type")
.uint8("mode", placeholder<AndroidScreenPowerMode>());
export type AndroidScreenPowerMode =
(typeof AndroidScreenPowerMode)[keyof typeof AndroidScreenPowerMode];
export type ScrcpySetScreenPowerModeControlMessage =
(typeof ScrcpySetScreenPowerModeControlMessage)["TInit"];
export const ScrcpySetScreenPowerModeControlMessage = new Struct(
{ type: u8, mode: u8.as<AndroidScreenPowerMode>() },
{ littleEndian: false },
);
export type ScrcpySetScreenPowerModeControlMessage = StructInit<
typeof ScrcpySetScreenPowerModeControlMessage
>;

View file

@ -1,6 +1,6 @@
import { getUint16, setUint16 } from "@yume-chan/no-data-view";
import type { NumberFieldVariant } from "@yume-chan/struct";
import { NumberFieldDefinition } from "@yume-chan/struct";
import type { Field } from "@yume-chan/struct";
import { bipedal } from "@yume-chan/struct";
export function clamp(value: number, min: number, max: number): number {
if (value < min) {
@ -14,22 +14,18 @@ export function clamp(value: number, min: number, max: number): number {
return value;
}
export const ScrcpyUnsignedFloatNumberVariant: NumberFieldVariant = {
export const ScrcpyUnsignedFloat: Field<number, never, never> = {
size: 2,
signed: false,
deserialize(array, littleEndian) {
const value = getUint16(array, 0, littleEndian);
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/server/src/main/java/com/genymobile/scrcpy/Binary.java#L22
return value === 0xffff ? 1 : value / 0x10000;
},
serialize(array, offset, value, littleEndian) {
serialize(value, { buffer, index, littleEndian }) {
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/app/src/util/binary.h#L51
value = clamp(value, -1, 1);
value = value === 1 ? 0xffff : value * 0x10000;
setUint16(array, offset, value, littleEndian);
setUint16(buffer, index, value, littleEndian);
},
deserialize: bipedal(function* (then, { reader, littleEndian }) {
const data = yield* then(reader.readExactly(2));
const value = getUint16(data, 0, littleEndian);
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/server/src/main/java/com/genymobile/scrcpy/Binary.java#L22
return value === 0xffff ? 1 : value / 0x10000;
}),
};
export const ScrcpyUnsignedFloatFieldDefinition = new NumberFieldDefinition(
ScrcpyUnsignedFloatNumberVariant,
);

View file

@ -1,4 +1,5 @@
import Struct, { placeholder } from "@yume-chan/struct";
import type { StructInit } from "@yume-chan/struct";
import { Struct, buffer, string, u16, u32, u64, u8 } from "@yume-chan/struct";
import type { AndroidMotionEventAction } from "../../control/index.js";
import {
@ -6,7 +7,7 @@ import {
ScrcpyControlMessageType,
} from "../../control/index.js";
import { ScrcpyUnsignedFloatFieldDefinition } from "./float.js";
import { ScrcpyUnsignedFloat } from "./float.js";
export const SCRCPY_CONTROL_MESSAGE_TYPES_1_16: readonly ScrcpyControlMessageType[] =
[
@ -23,43 +24,44 @@ export const SCRCPY_CONTROL_MESSAGE_TYPES_1_16: readonly ScrcpyControlMessageTyp
/* 10 */ ScrcpyControlMessageType.RotateDevice,
];
export const ScrcpyMediaStreamRawPacket =
/* #__PURE__ */
new Struct()
.uint64("pts")
.uint32("size")
.uint8Array("data", { lengthField: "size" });
export const ScrcpyMediaStreamRawPacket = new Struct(
{ pts: u64, data: buffer(u32) },
{ littleEndian: false },
);
export const SCRCPY_MEDIA_PACKET_FLAG_CONFIG = 1n << 63n;
export const ScrcpyInjectTouchControlMessage1_16 =
/* #__PURE__ */
new Struct()
.uint8("type")
.uint8("action", placeholder<AndroidMotionEventAction>())
.uint64("pointerId")
.uint32("pointerX")
.uint32("pointerY")
.uint16("screenWidth")
.uint16("screenHeight")
.field("pressure", ScrcpyUnsignedFloatFieldDefinition)
.uint32("buttons");
export const ScrcpyInjectTouchControlMessage1_16 = new Struct(
{
type: u8,
action: u8.as<AndroidMotionEventAction>(),
pointerId: u64,
pointerX: u32,
pointerY: u32,
screenWidth: u16,
screenHeight: u16,
pressure: ScrcpyUnsignedFloat,
buttons: u32,
},
{ littleEndian: false },
);
export type ScrcpyInjectTouchControlMessage1_16 =
(typeof ScrcpyInjectTouchControlMessage1_16)["TInit"];
export type ScrcpyInjectTouchControlMessage1_16 = StructInit<
typeof ScrcpyInjectTouchControlMessage1_16
>;
export const ScrcpyBackOrScreenOnControlMessage1_16 = EmptyControlMessage;
export const ScrcpySetClipboardControlMessage1_15 =
/* #__PURE__ */
new Struct()
.uint8("type")
.uint32("length")
.string("content", { lengthField: "length" });
export const ScrcpySetClipboardControlMessage1_15 = new Struct(
{ type: u8, content: string(u32) },
{ littleEndian: false },
);
export type ScrcpySetClipboardControlMessage1_15 =
(typeof ScrcpySetClipboardControlMessage1_15)["TInit"];
export type ScrcpySetClipboardControlMessage1_15 = StructInit<
typeof ScrcpySetClipboardControlMessage1_15
>;
export const ScrcpyClipboardDeviceMessage =
/* #__PURE__ */
new Struct().uint32("length").string("content", { lengthField: "length" });
export const ScrcpyClipboardDeviceMessage = new Struct(
{ content: string(u32) },
{ littleEndian: false },
);

View file

@ -12,7 +12,7 @@ import {
StructDeserializeStream,
TransformStream,
} from "@yume-chan/stream-extra";
import type { AsyncExactReadable, ValueOrPromise } from "@yume-chan/struct";
import type { AsyncExactReadable, MaybePromiseLike } from "@yume-chan/struct";
import { decodeUtf8 } from "@yume-chan/struct";
import type {
@ -159,7 +159,7 @@ export class ScrcpyOptions1_16 extends ScrcpyOptions<ScrcpyOptionsInit1_16> {
override parseVideoStreamMetadata(
stream: ReadableStream<Uint8Array>,
): ValueOrPromise<ScrcpyVideoStream> {
): MaybePromiseLike<ScrcpyVideoStream> {
return (async () => {
const buffered = new BufferedReadableStream(stream);
const metadata: ScrcpyVideoStreamMetadata = {

View file

@ -1,6 +1,7 @@
import Struct from "@yume-chan/struct";
import { s32, Struct, u16, u32, u8 } from "@yume-chan/struct";
import type { ScrcpyInjectScrollControlMessage } from "../../control/index.js";
import { ScrcpyControlMessageType } from "../../control/index.js";
export interface ScrcpyScrollController {
serializeScrollMessage(
@ -8,16 +9,18 @@ export interface ScrcpyScrollController {
): Uint8Array | undefined;
}
export const ScrcpyInjectScrollControlMessage1_16 =
/* #__PURE__ */
new Struct()
.uint8("type")
.uint32("pointerX")
.uint32("pointerY")
.uint16("screenWidth")
.uint16("screenHeight")
.int32("scrollX")
.int32("scrollY");
export const ScrcpyInjectScrollControlMessage1_16 = new Struct(
{
type: u8.as(ScrcpyControlMessageType.InjectScroll as const),
pointerX: u32,
pointerY: u32,
screenWidth: u16,
screenHeight: u16,
scrollX: s32,
scrollY: s32,
},
{ littleEndian: false },
);
/**
* Old version of Scrcpy server only supports integer values for scroll.

View file

@ -1,4 +1,5 @@
import Struct, { placeholder } from "@yume-chan/struct";
import type { StructInit } from "@yume-chan/struct";
import { Struct, u8 } from "@yume-chan/struct";
import type {
AndroidKeyEventAction,
@ -42,14 +43,17 @@ export interface ScrcpyOptionsInit1_18
powerOffOnClose?: boolean;
}
export const ScrcpyBackOrScreenOnControlMessage1_18 =
/* #__PURE__ */
new Struct()
.concat(ScrcpyBackOrScreenOnControlMessage1_16)
.uint8("action", placeholder<AndroidKeyEventAction>());
export const ScrcpyBackOrScreenOnControlMessage1_18 = new Struct(
{
...ScrcpyBackOrScreenOnControlMessage1_16.fields,
action: u8.as<AndroidKeyEventAction>(),
},
{ littleEndian: false },
);
export type ScrcpyBackOrScreenOnControlMessage1_18 =
(typeof ScrcpyBackOrScreenOnControlMessage1_18)["TInit"];
export type ScrcpyBackOrScreenOnControlMessage1_18 = StructInit<
typeof ScrcpyBackOrScreenOnControlMessage1_18
>;
export const SCRCPY_CONTROL_MESSAGE_TYPES_1_18 =
SCRCPY_CONTROL_MESSAGE_TYPES_1_16.slice();

View file

@ -1,8 +1,8 @@
// cspell: ignore autosync
import { PromiseResolver } from "@yume-chan/async";
import type { AsyncExactReadable } from "@yume-chan/struct";
import Struct, { placeholder } from "@yume-chan/struct";
import type { AsyncExactReadable, StructInit } from "@yume-chan/struct";
import { Struct, string, u32, u64, u8 } from "@yume-chan/struct";
import type { ScrcpySetClipboardControlMessage } from "../control/index.js";
@ -10,9 +10,10 @@ import type { ScrcpyOptionsInit1_18 } from "./1_18.js";
import { ScrcpyOptions1_18 } from "./1_18.js";
import { ScrcpyOptions, toScrcpyOptionValue } from "./types.js";
export const ScrcpyAckClipboardDeviceMessage =
/* #__PURE__ */
new Struct().uint64("sequence");
export const ScrcpyAckClipboardDeviceMessage = new Struct(
{ sequence: u64 },
{ littleEndian: false },
);
export interface ScrcpyOptionsInit1_21 extends ScrcpyOptionsInit1_18 {
clipboardAutosync?: boolean;
@ -22,17 +23,19 @@ function toSnakeCase(input: string): string {
return input.replace(/([A-Z])/g, "_$1").toLowerCase();
}
export const ScrcpySetClipboardControlMessage1_21 =
/* #__PURE__ */
new Struct()
.uint8("type")
.uint64("sequence")
.int8("paste", placeholder<boolean>())
.uint32("length")
.string("content", { lengthField: "length" });
export const ScrcpySetClipboardControlMessage1_21 = new Struct(
{
type: u8,
sequence: u64,
paste: u8.as<boolean>(),
content: string(u32),
},
{ littleEndian: false },
);
export type ScrcpySetClipboardControlMessage1_21 =
(typeof ScrcpySetClipboardControlMessage1_21)["TInit"];
export type ScrcpySetClipboardControlMessage1_21 = StructInit<
typeof ScrcpySetClipboardControlMessage1_21
>;
export class ScrcpyOptions1_21 extends ScrcpyOptions<ScrcpyOptionsInit1_21> {
static readonly DEFAULTS = {

View file

@ -1,5 +1,5 @@
import type { ReadableStream } from "@yume-chan/stream-extra";
import type { ValueOrPromise } from "@yume-chan/struct";
import type { MaybePromiseLike } from "@yume-chan/struct";
import type { ScrcpyScrollController } from "../1_16/index.js";
import { ScrcpyOptions1_21 } from "../1_21.js";
@ -28,7 +28,7 @@ export class ScrcpyOptions1_22 extends ScrcpyOptions<ScrcpyOptionsInit1_22> {
override parseVideoStreamMetadata(
stream: ReadableStream<Uint8Array>,
): ValueOrPromise<ScrcpyVideoStream> {
): MaybePromiseLike<ScrcpyVideoStream> {
if (!this.value.sendDeviceMeta) {
return { stream, metadata: { codec: ScrcpyVideoCodecId.H264 } };
} else {

View file

@ -1,16 +1,19 @@
import Struct from "@yume-chan/struct";
import type { StructInit } from "@yume-chan/struct";
import { s32, Struct } from "@yume-chan/struct";
import {
ScrcpyInjectScrollControlMessage1_16,
ScrcpyScrollController1_16,
} from "../1_16/index.js";
export const ScrcpyInjectScrollControlMessage1_22 =
/* #__PURE__ */
new Struct().concat(ScrcpyInjectScrollControlMessage1_16).int32("buttons");
export const ScrcpyInjectScrollControlMessage1_22 = new Struct(
{ ...ScrcpyInjectScrollControlMessage1_16.fields, buttons: s32 },
{ littleEndian: false },
);
export type ScrcpyInjectScrollControlMessage1_22 =
(typeof ScrcpyInjectScrollControlMessage1_22)["TInit"];
export type ScrcpyInjectScrollControlMessage1_22 = StructInit<
typeof ScrcpyInjectScrollControlMessage1_22
>;
export class ScrcpyScrollController1_22 extends ScrcpyScrollController1_16 {
override serializeScrollMessage(

View file

@ -3,24 +3,33 @@ import { describe, it } from "node:test";
import { ScrcpyControlMessageType } from "../../control/index.js";
import {
ScrcpyScrollController1_25,
ScrcpySignedFloatNumberVariant,
} from "./scroll.js";
import { ScrcpyScrollController1_25, ScrcpySignedFloat } from "./scroll.js";
describe("ScrcpyFloatToInt16NumberType", () => {
describe("ScrcpySignedFloat", () => {
it("should serialize", () => {
const array = new Uint8Array(2);
ScrcpySignedFloatNumberVariant.serialize(array, 0, -1, true);
ScrcpySignedFloat.serialize(-1, {
buffer: array,
index: 0,
littleEndian: true,
});
assert.strictEqual(
new DataView(array.buffer).getInt16(0, true),
-0x8000,
);
ScrcpySignedFloatNumberVariant.serialize(array, 0, 0, true);
ScrcpySignedFloat.serialize(0, {
buffer: array,
index: 0,
littleEndian: true,
});
assert.strictEqual(new DataView(array.buffer).getInt16(0, true), 0);
ScrcpySignedFloatNumberVariant.serialize(array, 0, 1, true);
ScrcpySignedFloat.serialize(1, {
buffer: array,
index: 0,
littleEndian: true,
});
assert.strictEqual(
new DataView(array.buffer).getInt16(0, true),
0x7fff,
@ -29,13 +38,21 @@ describe("ScrcpyFloatToInt16NumberType", () => {
it("should clamp input values", () => {
const array = new Uint8Array(2);
ScrcpySignedFloatNumberVariant.serialize(array, 0, -2, true);
ScrcpySignedFloat.serialize(-2, {
buffer: array,
index: 0,
littleEndian: true,
});
assert.strictEqual(
new DataView(array.buffer).getInt16(0, true),
-0x8000,
);
ScrcpySignedFloatNumberVariant.serialize(array, 0, 2, true);
ScrcpySignedFloat.serialize(2, {
buffer: array,
index: 0,
littleEndian: true,
});
assert.strictEqual(
new DataView(array.buffer).getInt16(0, true),
0x7fff,
@ -48,19 +65,31 @@ describe("ScrcpyFloatToInt16NumberType", () => {
dataView.setInt16(0, -0x8000, true);
assert.strictEqual(
ScrcpySignedFloatNumberVariant.deserialize(view, true),
ScrcpySignedFloat.deserialize({
runtimeStruct: {} as never,
reader: { position: 0, readExactly: () => view },
littleEndian: true,
}),
-1,
);
dataView.setInt16(0, 0, true);
assert.strictEqual(
ScrcpySignedFloatNumberVariant.deserialize(view, true),
ScrcpySignedFloat.deserialize({
runtimeStruct: {} as never,
reader: { position: 0, readExactly: () => view },
littleEndian: true,
}),
0,
);
dataView.setInt16(0, 0x7fff, true);
assert.strictEqual(
ScrcpySignedFloatNumberVariant.deserialize(view, true),
ScrcpySignedFloat.deserialize({
runtimeStruct: {} as never,
reader: { position: 0, readExactly: () => view },
littleEndian: true,
}),
1,
);
});

View file

@ -1,46 +1,45 @@
import { getInt16, setInt16 } from "@yume-chan/no-data-view";
import type { NumberFieldVariant } from "@yume-chan/struct";
import Struct, { NumberFieldDefinition } from "@yume-chan/struct";
import type { Field, StructInit } from "@yume-chan/struct";
import { bipedal, Struct, u16, u32, u8 } from "@yume-chan/struct";
import type { ScrcpyInjectScrollControlMessage } from "../../control/index.js";
import { ScrcpyControlMessageType } from "../../control/index.js";
import type { ScrcpyScrollController } from "../1_16/index.js";
import { clamp } from "../1_16/index.js";
export const ScrcpySignedFloatNumberVariant: NumberFieldVariant = {
export const ScrcpySignedFloat: Field<number, never, never> = {
size: 2,
signed: true,
deserialize(array, littleEndian) {
const value = getInt16(array, 0, littleEndian);
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/server/src/main/java/com/genymobile/scrcpy/Binary.java#L34
return value === 0x7fff ? 1 : value / 0x8000;
},
serialize(array, offset, value, littleEndian) {
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/app/src/util/binary.h#L65
serialize(value, { buffer, index, littleEndian }) {
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/app/src/util/binary.h#L51
value = clamp(value, -1, 1);
value = value === 1 ? 0x7fff : value * 0x8000;
setInt16(array, offset, value, littleEndian);
setInt16(buffer, index, value, littleEndian);
},
deserialize: bipedal(function* (then, { reader, littleEndian }) {
const data = yield* then(reader.readExactly(2));
const value = getInt16(data, 0, littleEndian);
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/server/src/main/java/com/genymobile/scrcpy/Binary.java#L34
return value === 0x7fff ? 1 : value / 0x8000;
}),
};
const ScrcpySignedFloatFieldDefinition = new NumberFieldDefinition(
ScrcpySignedFloatNumberVariant,
export const ScrcpyInjectScrollControlMessage1_25 = new Struct(
{
type: u8.as(ScrcpyControlMessageType.InjectScroll as const),
pointerX: u32,
pointerY: u32,
screenWidth: u16,
screenHeight: u16,
scrollX: ScrcpySignedFloat,
scrollY: ScrcpySignedFloat,
buttons: u32,
},
{ littleEndian: false },
);
export const ScrcpyInjectScrollControlMessage1_25 =
/* #__PURE__ */
new Struct()
.uint8("type", ScrcpyControlMessageType.InjectScroll as const)
.uint32("pointerX")
.uint32("pointerY")
.uint16("screenWidth")
.uint16("screenHeight")
.field("scrollX", ScrcpySignedFloatFieldDefinition)
.field("scrollY", ScrcpySignedFloatFieldDefinition)
.int32("buttons");
export type ScrcpyInjectScrollControlMessage1_25 =
(typeof ScrcpyInjectScrollControlMessage1_25)["TInit"];
export type ScrcpyInjectScrollControlMessage1_25 = StructInit<
typeof ScrcpyInjectScrollControlMessage1_25
>;
export class ScrcpyScrollController1_25 implements ScrcpyScrollController {
serializeScrollMessage(

View file

@ -4,8 +4,8 @@ import {
BufferedReadableStream,
PushReadableStream,
} from "@yume-chan/stream-extra";
import type { ValueOrPromise } from "@yume-chan/struct";
import Struct, { placeholder } from "@yume-chan/struct";
import type { MaybePromiseLike, StructInit } from "@yume-chan/struct";
import { Struct, u16, u32, u64, u8 } from "@yume-chan/struct";
import type {
AndroidMotionEventAction,
@ -15,7 +15,7 @@ import type {
import {
CodecOptions,
ScrcpyOptions1_16,
ScrcpyUnsignedFloatFieldDefinition,
ScrcpyUnsignedFloat,
} from "./1_16/index.js";
import { ScrcpyOptions1_21 } from "./1_21.js";
import type { ScrcpyOptionsInit1_24 } from "./1_24.js";
@ -30,22 +30,25 @@ import type {
} from "./types.js";
import { ScrcpyOptions } from "./types.js";
export const ScrcpyInjectTouchControlMessage2_0 =
/* #__PURE__ */
new Struct()
.uint8("type")
.uint8("action", placeholder<AndroidMotionEventAction>())
.uint64("pointerId")
.uint32("pointerX")
.uint32("pointerY")
.uint16("screenWidth")
.uint16("screenHeight")
.field("pressure", ScrcpyUnsignedFloatFieldDefinition)
.uint32("actionButton")
.uint32("buttons");
export const ScrcpyInjectTouchControlMessage2_0 = new Struct(
{
type: u8,
action: u8.as<AndroidMotionEventAction>(),
pointerId: u64,
pointerX: u32,
pointerY: u32,
screenWidth: u16,
screenHeight: u16,
pressure: ScrcpyUnsignedFloat,
actionButton: u32,
buttons: u32,
},
{ littleEndian: false },
);
export type ScrcpyInjectTouchControlMessage2_0 =
(typeof ScrcpyInjectTouchControlMessage2_0)["TInit"];
export type ScrcpyInjectTouchControlMessage2_0 = StructInit<
typeof ScrcpyInjectTouchControlMessage2_0
>;
export class ScrcpyInstanceId implements ScrcpyOptionValue {
static readonly NONE = new ScrcpyInstanceId(-1);
@ -244,7 +247,7 @@ export class ScrcpyOptions2_0 extends ScrcpyOptions<ScrcpyOptionsInit2_0> {
override parseVideoStreamMetadata(
stream: ReadableStream<Uint8Array>,
): ValueOrPromise<ScrcpyVideoStream> {
): MaybePromiseLike<ScrcpyVideoStream> {
const { sendDeviceMeta, sendCodecMeta } = this.value;
if (!sendDeviceMeta && !sendCodecMeta) {
let codec: ScrcpyVideoCodecId;
@ -302,7 +305,7 @@ export class ScrcpyOptions2_0 extends ScrcpyOptions<ScrcpyOptionsInit2_0> {
override parseAudioStreamMetadata(
stream: ReadableStream<Uint8Array>,
): ValueOrPromise<ScrcpyAudioStreamMetadata> {
): MaybePromiseLike<ScrcpyAudioStreamMetadata> {
return ScrcpyOptions2_0.parseAudioMetadata(
stream,
this.value.sendCodecMeta,

View file

@ -1,5 +1,5 @@
import type { ReadableStream } from "@yume-chan/stream-extra";
import type { ValueOrPromise } from "@yume-chan/struct";
import type { MaybePromiseLike } from "@yume-chan/struct";
import { ScrcpyOptions1_21 } from "./1_21.js";
import { ScrcpyOptions2_0 } from "./2_0.js";
@ -33,7 +33,7 @@ export class ScrcpyOptions2_3 extends ScrcpyOptions<ScrcpyOptionsInit2_3> {
override parseAudioStreamMetadata(
stream: ReadableStream<Uint8Array>,
): ValueOrPromise<ScrcpyAudioStreamMetadata> {
): MaybePromiseLike<ScrcpyAudioStreamMetadata> {
return ScrcpyOptions2_0.parseAudioMetadata(
stream,
this.value.sendCodecMeta,

View file

@ -1,5 +1,5 @@
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable, ValueOrPromise } from "@yume-chan/struct";
import type { AsyncExactReadable, MaybePromiseLike } from "@yume-chan/struct";
import type {
ScrcpyBackOrScreenOnControlMessage,
@ -170,13 +170,13 @@ export abstract class ScrcpyOptions<T extends object> {
*/
parseVideoStreamMetadata(
stream: ReadableStream<Uint8Array>,
): ValueOrPromise<ScrcpyVideoStream> {
): MaybePromiseLike<ScrcpyVideoStream> {
return this.#base.parseVideoStreamMetadata(stream);
}
parseAudioStreamMetadata(
stream: ReadableStream<Uint8Array>,
): ValueOrPromise<ScrcpyAudioStreamMetadata> {
): MaybePromiseLike<ScrcpyAudioStreamMetadata> {
return this.#base.parseAudioStreamMetadata(stream);
}
@ -184,7 +184,7 @@ export abstract class ScrcpyOptions<T extends object> {
return this.#base.parseDeviceMessage(id, stream);
}
endDeviceMessageStream(e?: unknown): ValueOrPromise<void> {
endDeviceMessageStream(e?: unknown): MaybePromiseLike<void> {
return this.#base.endDeviceMessageStream(e);
}

View file

@ -1,4 +1,4 @@
import type { ValueOrPromise } from "@yume-chan/struct";
import type { MaybePromiseLike } from "@yume-chan/struct";
import { StructEmptyError } from "@yume-chan/struct";
import { BufferedReadableStream } from "./buffered.js";
@ -22,7 +22,7 @@ export class BufferedTransformStream<T>
}
constructor(
transform: (stream: BufferedReadableStream) => ValueOrPromise<T>,
transform: (stream: BufferedReadableStream) => MaybePromiseLike<T>,
) {
// Convert incoming chunks to a `BufferedReadableStream`
let sourceStreamController!: PushReadableStreamController<Uint8Array>;

View file

@ -1,5 +1,5 @@
import { PromiseResolver } from "@yume-chan/async";
import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct";
import { EmptyUint8Array } from "@yume-chan/struct";
import type { ReadableStreamDefaultController } from "./stream.js";
import { ReadableStream, WritableStream } from "./stream.js";
@ -101,7 +101,7 @@ export class ConcatBufferStream {
let offset = 0;
switch (this.#segments.length) {
case 0:
result = EMPTY_UINT8_ARRAY;
result = EmptyUint8Array;
break;
case 1:
result = this.#segments[0]!;

View file

@ -1,5 +1,5 @@
import { PromiseResolver } from "@yume-chan/async";
import type { ValueOrPromise } from "@yume-chan/struct";
import type { MaybePromiseLike } from "@yume-chan/struct";
import type {
QueuingStrategy,
@ -28,7 +28,7 @@ export interface DuplexStreamFactoryOptions {
* `DuplexStreamFactory#dispose` yourself, you can return `false`
* (or a `Promise` that resolves to `false`) to disable the automatic call.
*/
close?: (() => ValueOrPromise<boolean | void>) | undefined;
close?: (() => MaybePromiseLike<boolean | void>) | undefined;
/**
* Callback when any `ReadableStream` is closed (the other peer doesn't produce any more data),

View file

@ -1,12 +1,9 @@
import type Struct from "@yume-chan/struct";
import type { StructValueType } from "@yume-chan/struct";
import type { StructLike } from "@yume-chan/struct";
import { BufferedTransformStream } from "./buffered-transform.js";
export class StructDeserializeStream<
T extends Struct<object, PropertyKey, object, unknown>,
> extends BufferedTransformStream<StructValueType<T>> {
constructor(struct: T) {
export class StructDeserializeStream<T> extends BufferedTransformStream<T> {
constructor(struct: StructLike<T>) {
super((stream) => {
return struct.deserialize(stream) as never;
});

View file

@ -1,10 +1,10 @@
import type Struct from "@yume-chan/struct";
import type { StructInit, StructLike } from "@yume-chan/struct";
import { TransformStream } from "./stream.js";
export class StructSerializeStream<
T extends Struct<object, PropertyKey, object, unknown>,
> extends TransformStream<T["TInit"], Uint8Array> {
T extends StructLike<unknown>,
> extends TransformStream<StructInit<T>, Uint8Array> {
constructor(struct: T) {
super({
transform(chunk, controller) {

View file

@ -1,4 +1,4 @@
import type { ValueOrPromise } from "@yume-chan/struct";
import type { MaybePromiseLike } from "@yume-chan/struct";
import type {
QueuingStrategy,
@ -9,12 +9,12 @@ import { ReadableStream } from "./stream.js";
export type WrapReadableStreamStart<T> = (
controller: ReadableStreamDefaultController<T>,
) => ValueOrPromise<ReadableStream<T>>;
) => MaybePromiseLike<ReadableStream<T>>;
export interface ReadableStreamWrapper<T> {
start: WrapReadableStreamStart<T>;
cancel?(reason?: unknown): ValueOrPromise<void>;
close?(): ValueOrPromise<void>;
cancel?(reason?: unknown): MaybePromiseLike<void>;
close?(): MaybePromiseLike<void>;
}
function getWrappedReadableStream<T>(

View file

@ -1,9 +1,9 @@
import type { ValueOrPromise } from "@yume-chan/struct";
import type { MaybePromiseLike } from "@yume-chan/struct";
import type { TransformStream, WritableStreamDefaultWriter } from "./stream.js";
import { WritableStream } from "./stream.js";
export type WrapWritableStreamStart<T> = () => ValueOrPromise<
export type WrapWritableStreamStart<T> = () => MaybePromiseLike<
WritableStream<T>
>;

View file

@ -2,7 +2,6 @@
<!--
cspell: ignore Codecov
cspell: ignore uint8arraystring
-->
![license](https://img.shields.io/npm/l/@yume-chan/struct)
@ -13,6 +12,8 @@ cspell: ignore uint8arraystring
A C-style structure serializer and deserializer. Written in TypeScript and highly takes advantage of its type system.
The new API is inspired by [TypeGPU](https://docs.swmansion.com/TypeGPU/) which improves DX and tree-shaking.
**WARNING:** The public API is UNSTABLE. Open a GitHub discussion if you have any questions.
## Installation
@ -24,724 +25,97 @@ $ npm i @yume-chan/struct
## Quick Start
```ts
import Struct from "@yume-chan/struct";
import { Struct, u8, u16, s32, buffer, string } from "@yume-chan/struct";
const MyStruct = new Struct({ littleEndian: true })
.int8("foo")
.int64("bar")
.int32("bazLength")
.string("baz", { lengthField: "bazLength" });
const Message = new Struct(
{
a: u8,
b: u16,
c: s32,
d: buffer(4), // Fixed length Uint8Array
e: buffer("b"), // Use value of `b` as length
f: buffer(u32), // `u32` length prefix
g: buffer(4, {
// Custom conversion between `Uint8Array` and other types
convert(value: Uint8Array) {
return value[0];
},
back(value: number) {
return new Uint8Array([value, 0, 0, 0]);
},
}),
h: string(64), // `string` is an alias to `buffer` with UTF-8 string conversion
},
{ littleEndian: true },
);
const value = await MyStruct.deserialize(stream);
value.foo; // number
value.bar; // bigint
value.bazLength; // number
value.baz; // string
// Custom reader
const reader = {
position: 0,
readExactly(length) {
const slice = new Uint8Array(100).slice(
this.position,
this.position + length,
);
this.position += length;
return slice;
},
};
const buffer = MyStruct.serialize({
foo: 42,
bar: 42n,
// `bazLength` automatically set to `baz`'s byte length
baz: "Hello, World!",
const message1 = Message.deserialize(reader); // If `reader.readExactly` is synchronous, `deserialize` is also synchronous
const message2 = await Message.deserialize(reader); // If `reader.readExactly` is asynchronous, so do `deserialize`
const buffer: Uint8Array = Message.serialize(message1);
```
## Custom field types
```ts
import { Field, AsyncExactReadable, Struct, u8 } from "@yume-chan/struct";
const MyField: Field<number, never, never> = {
size: 4, // `0` if dynamically sized,
dynamicSize(value: number) {
// Optional, return dynamic size for value
return 0;
},
serialize(
value: number,
context: { buffer: Uint8Array; index: number; littleEndian: boolean },
) {
// Serialize value to `context.buffer` at `context.index`
},
deserialize(context: {
reader: AsyncExactReadable;
littleEndian: boolean;
}) {
// Deserialize value from `context.reader`
return 0;
},
};
const Message2 = new Struct({
a: u8,
b: MyField,
});
```
<!-- cspell: disable -->
## Bipedal
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Compatibility](#compatibility)
- [Basic usage](#basic-usage)
- [`int64`/`uint64`](#int64uint64)
- [`string`](#string)
- [API](#api)
- [`placeholder`](#placeholder)
- [`Struct`](#struct)
- [`int8`/`uint8`/`int16`/`uint16`/`int32`/`uint32`](#int8uint8int16uint16int32uint32)
- [`int64`/`uint64`](#int64uint64-1)
- [`uint8Array`/`string`](#uint8arraystring)
- [`concat`](#concat)
- [`extra`](#extra)
- [`postDeserialize`](#postdeserialize)
- [`deserialize`](#deserialize)
- [`serialize`](#serialize)
- [Custom field type](#custom-field-type)
- [`Struct#field`](#structfield)
- [Relationship between types](#relationship-between-types)
- [`StructFieldDefinition`](#structfielddefinition)
- [`TValue`/`TOmitInitKey`](#tvaluetomitinitkey)
- [`getSize`](#getsize)
- [`create`](#create)
- [`deserialize`](#deserialize-1)
- [`StructFieldValue`](#structfieldvalue)
- [`getSize`](#getsize-1)
- [`get`/`set`](#getset)
- [`serialize`](#serialize-1)
`bipedal` is a custom async helper that allows the same code to behave synchronously or asynchronously depends on the parameters.
<!-- cspell: enable -->
It's inspired by [gensync](https://github.com/loganfsmyth/gensync).
## Compatibility
Here is a list of features, their used APIs, and their compatibilities. If an optional feature is not actually used, its requirements can be ignored.
Some features can be polyfilled to support older runtimes, but this library doesn't ship with any polyfills.
### Basic usage
| API | Chrome | Edge | Firefox | Internet Explorer | Safari | Node.js |
| -------------------------------- | ------ | ---- | ------- | ----------------- | ------ | ------- |
| [`Promise`][mdn_promise] | 32 | 12 | 29 | No | 8 | 0.12 |
| [`ArrayBuffer`][mdn_arraybuffer] | 7 | 12 | 4 | 10 | 5.1 | 0.10 |
| [`Uint8Array`][mdn_uint8array] | 7 | 12 | 4 | 10 | 5.1 | 0.10 |
| _Overall_ | 32 | 12 | 29 | No | 8 | 0.12 |
### [`int64`/`uint64`](#int64uint64-1)
| API | Chrome | Edge | Firefox | Internet Explorer | Safari | Node.js |
| ---------------------------------- | ------ | ---- | ------- | ----------------- | ------ | ------- |
| [`BigInt`][mdn_bigint]<sup>1</sup> | 67 | 79 | 68 | No | 14 | 10.4 |
<sup>1</sup> Can't be polyfilled
### [`string`](#uint8arraystring)
| API | Chrome | Edge | Firefox | Internet Explorer | Safari | Node.js |
| -------------------------------- | ------ | ---- | ------- | ----------------- | ------ | ------------------- |
| [`TextEncoder`][mdn_textencoder] | 38 | 79 | 19 | No | 10.1 | 8.3<sup>1</sup>, 11 |
<sup>1</sup> `TextEncoder` and `TextDecoder` are only available in `util` module. Need to be assigned to `globalThis`.
[mdn_promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
[mdn_arraybuffer]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
[mdn_uint8array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
[mdn_bigint]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt
[mdn_textencoder]: https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder
## API
### `placeholder`
The word `bipedal` refers to animals who walk using two legs.
```ts
function placeholder<T>(): T {
return undefined as unknown as T;
}
import { bipedal } from "@yume-chan/struct";
const fn = bipedal(function* (then, name: string | Promise<string>) {
name = yield* then(name);
return "Hello, " + name;
});
fn("Simon"); // "Hello, Simon"
await fn(Promise.resolve("Simon")); // "Hello, Simon"
```
Returns a (fake) value of the given type. It's only useful in TypeScript, if you are using JavaScript, you shouldn't care about it.
Many methods in this library have multiple generic parameters, but TypeScript only allows users to specify none (let TypeScript inference all of them from arguments), or all generic arguments. ([Microsoft/TypeScript#26242](https://github.com/microsoft/TypeScript/issues/26242))
<details>
<summary>Detail explanation (click to expand)</summary>
When you have a generic method, where half generic parameters can be inferred.
```ts
declare function fn<A, B>(a: A): [A, B];
fn(42); // Expected 2 type arguments, but got 1. ts(2558)
```
Rather than force users repeat the type `A`, I declare a parameter for `B`.
```ts
declare function fn2<A, B>(a: A, b: B): [A, B];
```
I don't really need a value of type `B`, I only require its type information
```ts
fn2(42, placeholder<boolean>()); // fn2<number, boolean>
```
</details><br/>
To workaround this issue, these methods have an extra `_typescriptType` parameter, to let you specify a generic parameter, without passing all other generic arguments manually. The actual value of `_typescriptType` argument is never used, so you can pass any value, as long as it has the correct type, including values produced by this `placeholder` method.
**With that said, I don't expect you to specify any generic arguments manually when using this library.**
### `Struct`
```ts
class Struct<
TFields extends object = {},
TOmitInitKey extends string | number | symbol = never,
TExtra extends object = {},
TPostDeserialized = undefined,
> {
public constructor(options: Partial<StructOptions> = StructDefaultOptions);
}
```
Creates a new structure definition.
<details>
<summary>Generic parameters (click to expand)</summary>
This information was added to help you understand how does it work. These are considered as "internal state" so don't specify them manually.
1. `TFields`: Type of the Struct value. Modified when new fields are added.
2. `TOmitInitKey`: When serializing a structure containing variable length buffers, the length field can be calculate from the buffer field, so they doesn't need to be provided explicitly.
3. `TExtra`: Type of extra fields. Modified when `extra` is called.
4. `TPostDeserialized`: State of the `postDeserialize` function. Modified when `postDeserialize` is called. Affects return type of `deserialize`
</details><br/>
**Parameters**
1. `options`:
- `littleEndian:boolean = false`: Whether all multi-byte fields in this struct are [little-endian encoded][wikipeida_endianess].
[wikipeida_endianess]: https://en.wikipedia.org/wiki/Endianness
#### `int8`/`uint8`/`int16`/`uint16`/`int32`/`uint32`
```ts
int32<
TName extends string | number | symbol,
TTypeScriptType = number
>(
name: TName,
_typescriptType?: TTypeScriptType
): Struct<
TFields & Record<TName, TTypeScriptType>,
TOmitInitKey,
TExtra,
TPostDeserialized
>;
```
Appends an `int8`/`uint8`/`int16`/`uint16`/`int32`/`uint32` field to the `Struct`.
<details>
<summary>Generic parameters (click to expand)</summary>
1. `TName`: Literal type of the field's name.
2. `TTypeScriptType = number`: Type of the field in the result object. For example you can declare it as a number literal type, or some enum type.
</details><br/>
**Parameters**
1. `name`: (Required) Field name. Must be a string literal.
2. `_typescriptType`: Set field's type. See examples below.
**Note**
There is no generic constraints on the `TTypeScriptType`, because TypeScript doesn't allow casting enum types to `number`.
So it's technically possible to pass in an incompatible type (e.g. `string`). But obviously, it's a bad idea.
**Examples**
1. Append an `int32` field named `foo`
```ts
const struct = new Struct().int32("foo");
const value = await struct.deserialize(stream);
value.foo; // number
struct.serialize({}); // error: 'foo' is required
struct.serialize({ foo: "bar" }); // error: 'foo' must be a number
struct.serialize({ foo: 42 }); // ok
```
2. Set fields' type (can use [`placeholder` method](#placeholder))
```ts
enum MyEnum {
a,
b,
}
const struct = new Struct()
.int32("foo", placeholder<MyEnum>())
.int32("bar", MyEnum.a as const);
const value = await struct.deserialize(stream);
value.foo; // MyEnum
value.bar; // MyEnum.a
struct.serialize({ foo: 42, bar: MyEnum.a }); // error: 'foo' must be of type `MyEnum`
struct.serialize({ foo: MyEnum.a, bar: MyEnum.b }); // error: 'bar' must be of type `MyEnum.a`
struct.serialize({ foo: MyEnum.a, bar: MyEnum.b }); // ok
```
#### `int64`/`uint64`
```ts
int64<
TName extends string | number | symbol,
TTypeScriptType = bigint
>(
name: TName,
_typescriptType?: TTypeScriptType
): Struct<
TFields & Record<TName, TTypeScriptType>,
TOmitInitKey,
TExtra,
TPostDeserialized
>;
```
Appends an `int64`/`uint64` field to the `Struct`. The usage is same as `uint32`/`uint32`.
Requires native support for `BigInt`. Check [compatibility table](#compatibility) for more information.
#### `uint8Array`/`string`
```ts
uint8Array<
TName extends string | number | symbol,
TTypeScriptType = ArrayBuffer
>(
name: TName,
options: FixedLengthBufferLikeFieldOptions,
_typescriptType?: TTypeScriptType,
): Struct<
TFields & Record<TName, TTypeScriptType>,
TOmitInitKey,
TExtra,
TPostDeserialized
>;
uint8Array<
TName extends string | number | symbol,
TLengthField extends LengthField<TFields>,
TOptions extends VariableLengthBufferLikeFieldOptions<TFields, TLengthField>,
TTypeScriptType = ArrayBuffer,
>(
name: TName,
options: TOptions,
_typescriptType?: TTypeScriptType,
): Struct<
TFields & Record<TName, TTypeScriptType>,
TOmitInitKey | TLengthField,
TExtra,
TPostDeserialized
>;
```
Appends an `uint8Array`/`string` field to the `Struct`.
The `options` parameter defines its length, it supports two formats:
- `{ length: number }`: Presence of the `length` option indicates that it's a fixed length array.
- `{ lengthField: string; lengthFieldRadix?: number }`: Presence of the `lengthField` option indicates it's a variable length array. The `lengthField` options must refers to a `number` or `string` (can't be `bigint`) typed field that's already defined in this `Struct`. If the length field is a `string`, the optional `lengthFieldRadix` option (defaults to `10`) defines the radix when converting the string to a number. When deserializing, it will use that field's value as its length. When serializing, it will write its length to that field.
#### `concat`
```ts
concat<
TOther extends Struct<any, any, any, any>
>(
other: TOther
): Struct<
TFields & TOther['fieldsType'],
TOmitInitKey | TOther['omitInitType'],
TExtra & TOther['extraType'],
TPostDeserialized
>;
```
Merges (flats) another `Struct`'s fields and extra fields into the current one.
**Examples**
1. Extending another `Struct`
```ts
const MyStructV1 = new Struct().int32("field1");
const MyStructV2 = new Struct().concat(MyStructV1).int32("field2");
const structV2 = await MyStructV2.deserialize(stream);
structV2.field1; // number
structV2.field2; // number
// Fields are flatten
```
2. Also possible in any order
```ts
const MyStructV1 = new Struct().int32("field1");
const MyStructV2 = new Struct().int32("field2").concat(MyStructV1);
const structV2 = await MyStructV2.deserialize(stream);
structV2.field1; // number
structV2.field2; // number
// Same result as above, but serialize/deserialize order is reversed
```
#### `extra`
```ts
extra<
T extends Record<
Exclude<
keyof T,
Exclude<
keyof T,
keyof TFields
>
>,
never
>
>(
value: T & ThisType<Overwrite<Overwrite<TExtra, T>, TFields>>
): Struct<
TFields,
TInit,
Overwrite<TExtra, T>,
TPostDeserialized
>;
```
Adds extra fields into the `Struct`. Extra fields will be defined on prototype of each Struct values, so they don't affect serialize and deserialize process, and deserialized fields will overwrite extra fields.
Multiple calls merge all extra fields together.
**Generic Parameters**
1. `T`: Type of the extra fields. The scary looking generic constraint is used to forbid overwriting any already existed fields.
**Parameters**
1. `value`: An object containing anything you want to add to Struct values. Accessors and methods are also allowed.
**Examples**
1. Add an extra field
```ts
const struct = new Struct().int32("foo").extra({
bar: "hello",
});
const value = await struct.deserialize(stream);
value.foo; // number
value.bar; // 'hello'
struct.serialize({ foo: 42 }); // ok
struct.serialize({ foo: 42, bar: "hello" }); // error: 'bar' is redundant
```
2. Add getters and methods. `this` in functions refers to the result object.
```ts
const struct = new Struct().int32("foo").extra({
get bar() {
// `this` is the result Struct value
return this.foo + 1;
},
logBar() {
// `this` also contains other extra fields
console.log(this.bar);
},
});
const value = await struct.deserialize(stream);
value.foo; // number
value.bar; // number
value.logBar();
```
#### `postDeserialize`
```ts
postDeserialize(): Struct<TFields, TOmitInitKey, TExtra, undefined>;
```
Remove any registered post-deserialization callback.
```ts
postDeserialize(
callback: (this: TFields, object: TFields) => never
): Struct<TFields, TOmitInitKey, TExtra, never>;
postDeserialize(
callback: (this: TFields, object: TFields) => void
): Struct<TFields, TOmitInitKey, TExtra, undefined>;
```
Registers (or replaces) a custom callback to be run after deserialized.
`this` in `callback`, along with the first parameter `object` will both be the deserialized Struct value.
A callback returning `never` (always throws errors) will change the return type of `deserialize` to `never`.
A callback returning `void` means it modify the result object in-place (or doesn't modify it at all), so `deserialize` will still return the result object.
```ts
postDeserialize<TPostSerialize>(
callback: (this: TFields, object: TFields) => TPostSerialize
): Struct<TFields, TOmitInitKey, TExtra, TPostSerialize>;
```
Registers (or replaces) a custom callback to be run after deserialized.
A callback returning anything other than `undefined` will cause `deserialize` to return that value instead.
**Generic Parameters**
1. `TPostSerialize`: Type of the new result.
**Parameters**
1. `callback`: An function contains the custom logic to be run, optionally returns a new result. Or `undefined`, to remove any previously set `postDeserialize` callback.
**Examples**
1. Handle an "error" packet
```ts
// Say your protocol have an error packet,
// You want to throw a JavaScript Error when received such a packet,
// But you don't want to modify all receiving path
const struct = new Struct()
.int32("messageLength")
.string("message", { lengthField: "messageLength" })
.postDeserialize((value) => {
throw new Error(value.message);
});
```
2. Do anything you want
```ts
// I think this one doesn't need any code example
```
3. Replace result object
```ts
const struct1 = new Struct().int32("foo").postDeserialize((value) => {
return {
bar: value.foo,
};
});
const value = await struct.deserialize(stream);
value.foo; // error: not exist
value.bar; // number
```
#### `deserialize`
```ts
interface ExactReadable {
readonly position: number;
/**
* Read data from the underlying data source.
*
* The stream must return exactly `length` bytes or data. If that's not possible
* (due to end of file or other error condition), it must throw an {@link ExactReadableEndedError}.
*/
readExactly(length: number): Uint8Array;
}
interface AsyncExactReadable {
readonly position: number;
/**
* Read data from the underlying data source.
*
* The stream must return exactly `length` bytes or data. If that's not possible
* (due to end of file or other error condition), it must throw an {@link ExactReadableEndedError}.
*/
readExactly(length: number): ValueOrPromise<Uint8Array>;
}
deserialize(
stream: ExactReadable,
): TPostDeserialized extends undefined
? Overwrite<TExtra, TValue>
: TPostDeserialized
>;
deserialize(
stream: AsyncExactReadable,
): Promise<
TPostDeserialized extends undefined
? Overwrite<TExtra, TValue>
: TPostDeserialized
>
>;
```
Deserialize a struct value from `stream`.
It will be synchronous (returns a value) or asynchronous (returns a `Promise`) depending on the type of `stream`.
As the signature shows, if the `postDeserialize` callback returns any value, `deserialize` will return that value instead.
#### `serialize`
```ts
serialize(init: Evaluate<Omit<TFields, TOmitInitKey>>): Uint8Array;
serialize(init: Evaluate<Omit<TFields, TOmitInitKey>>, output: Uint8Array): number;
```
Serialize a struct value into an `Uint8Array`.
If an `output` is given, it will serialize the struct into it, and returns the number of bytes written.
## Custom field type
It's also possible to create your own field types.
### `Struct#field`
```ts
field<
TName extends string | number | symbol,
TDefinition extends StructFieldDefinition<any, any, any>
>(
name: TName,
definition: TDefinition
): Struct<
TFields & Record<TName, TDefinition['TValue']>,
TOmitInitKey | TDefinition['TOmitInitKey'],
TExtra,
TPostDeserialized
>;
```
Appends a `StructFieldDefinition` to the `Struct`.
All built-in field type methods are actually aliases to it. For example, calling
```ts
struct.int8("foo");
```
is same as
```ts
struct.field("foo", new NumberFieldDefinition(NumberFieldType.Int8));
```
### Relationship between types
- `StructFieldValue`: Contains value of a field, with optional metadata and accessor methods.
- `StructFieldDefinition`: Definition of a field, can deserialize `StructFieldValue`s from a stream or create them from exist values.
- `StructValue`: A map between field names and `StructFieldValue`s.
- `Struct`: Definition of a struct, a map between field names and `StructFieldDefintion`s. May contain extra metadata.
- Result of `Struct#deserialize()`: A map between field names and results of `StructFieldValue#get()`.
### `StructFieldDefinition`
```ts
abstract class StructFieldDefinition<
TOptions = void,
TValue = unknown,
TOmitInitKey extends PropertyKey = never,
> {
public readonly options: TOptions;
public constructor(options: TOptions);
}
```
A field definition defines how to deserialize a field.
It's an `abstract` class, means it can't be constructed (`new`ed) directly. It's only used as a base class for other field types.
#### `TValue`/`TOmitInitKey`
These two fields provide type information to TypeScript compiler. Their values will always be `undefined`, but having correct types is enough. You don't need to touch them.
#### `getSize`
```ts
abstract getSize(): number;
```
Derived classes must implement this method to return size (or minimal size if it's dynamic) of this field.
Actual size should be returned from `StructFieldValue#getSize`
#### `create`
```ts
abstract create(
options: Readonly<StructOptions>,
struct: StructValue,
value: TValue,
): StructFieldValue<this>;
```
Derived classes must implement this method to create its own field value instance for the current definition.
`Struct#serialize` will call this method, then call `StructFieldValue#serialize` to serialize one field value.
#### `deserialize`
```ts
abstract deserialize(
options: Readonly<StructOptions>,
stream: ExactReadable,
struct: StructValue,
): StructFieldValue<this>;
abstract deserialize(
options: Readonly<StructOptions>,
stream: AsyncExactReadable,
struct: StructValue,
): Promise<StructFieldValue<this>>;
```
Derived classes must implement this method to define how to deserialize a value from `stream`.
It must be synchronous (returns a value) or asynchronous (returns a `Promise`) depending on the type of `stream`.
Usually implementations should be:
1. Read required bytes from `stream`
2. Parse it to your type
3. Pass the value into your own `create` method
Sometimes, extra metadata is present when deserializing, but need to be calculated when serializing, for example a UTF-8 encoded string may have different length between itself (character count) and serialized form (byte length). So `deserialize` can save those metadata on the `StructFieldValue` instance for later use.
### `StructFieldValue`
```ts
abstract class StructFieldValue<
TDefinition extends StructFieldDefinition<any, any, any>
>
```
A field value defines how to serialize a field.
#### `getSize`
```ts
getSize(): number;
```
Gets size of this field. By default, it returns its `definition`'s size.
If this field's size can change based on some criteria, one must override `getSize` to return its actual size.
#### `get`/`set`
```ts
get(): TDefinition['TValue'];
set(value: TDefinition['TValue']): void;
```
Defines how to get or set this field's value. By default, it reads/writes its `value` field.
If one needs to manipulate other states when getting/setting values, they can override these methods.
#### `serialize`
```ts
abstract serialize(
array: Uint8Array,
offset: number
): void;
```
Derived classes must implement this method to serialize current value into `array`, from `offset`. It must not write more bytes than what its `getSize` returned.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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]!;
}
}

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

View 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/,
);
});
});
});

View 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;

View 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>;
}

View file

@ -1,7 +1,7 @@
import * as assert from "node:assert";
import { describe, it } from "node:test";
import Struct from "./index.js";
import { Struct } from "./index.js";
describe("Struct", () => {
describe("Index", () => {

View file

@ -10,9 +10,11 @@ declare global {
}
}
export * from "./basic/index.js";
export * from "./bipedal.js";
export * from "./buffer.js";
export * from "./field.js";
export * from "./number.js";
export * from "./readable.js";
export * from "./string.js";
export * from "./struct.js";
export { Struct as default } from "./struct.js";
export * from "./sync-promise.js";
export * from "./types/index.js";
export * from "./utils.js";

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

View file

@ -1,7 +1,7 @@
import type { ValueOrPromise } from "../utils.js";
// TODO: allow over reading (returning a `Uint8Array`, an `offset` and a `length`) to avoid copying
import type { MaybePromiseLike } from "./utils.js";
export class ExactReadableEndedError extends Error {
constructor() {
super("ExactReadable ended");
@ -30,5 +30,5 @@ export interface AsyncExactReadable {
* The stream must return exactly `length` bytes or data. If that's not possible
* (due to end of file or other error condition), it must throw an {@link ExactReadableEndedError}.
*/
readExactly(length: number): ValueOrPromise<Uint8Array>;
readExactly(length: number): MaybePromiseLike<Uint8Array>;
}

View 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;

View file

@ -1,538 +1,12 @@
import * as assert from "node:assert";
import { describe, it, mock } from "node:test";
import { describe, it } from "node:test";
import type {
AsyncExactReadable,
ExactReadable,
StructFieldValue,
StructOptions,
StructValue,
} from "./basic/index.js";
import { StructDefaultOptions, StructFieldDefinition } from "./basic/index.js";
import { u8 } from "./number.js";
import { Struct } from "./struct.js";
import type { ValueOrPromise } from "./utils.js";
import {
BigIntFieldDefinition,
BigIntFieldVariant,
BufferFieldConverter,
FixedLengthBufferLikeFieldDefinition,
NumberFieldDefinition,
NumberFieldVariant,
VariableLengthBufferLikeFieldDefinition,
} from "./index.js";
class MockDeserializationStream implements ExactReadable {
buffer = new Uint8Array(0);
position = 0;
readExactly = mock.fn(() => this.buffer);
}
describe("Struct", () => {
describe(".constructor", () => {
it("should initialize fields", () => {
const struct = /* #__PURE__ */ new Struct();
assert.deepStrictEqual(struct.options, StructDefaultOptions);
assert.strictEqual(struct.size, 0);
});
});
describe("#field", () => {
class MockFieldDefinition extends StructFieldDefinition<number> {
constructor(size: number) {
super(size);
}
getSize = mock.fn(() => {
return this.options;
});
override create(
options: Readonly<StructOptions>,
struct: StructValue,
value: unknown,
): StructFieldValue<this> {
void options;
void struct;
void value;
throw new Error("Method not implemented.");
}
override deserialize(
options: Readonly<StructOptions>,
stream: ExactReadable,
struct: StructValue,
): StructFieldValue<this>;
override deserialize(
options: Readonly<StructOptions>,
stream: AsyncExactReadable,
struct: StructValue,
): Promise<StructFieldValue<this>>;
override deserialize(
options: Readonly<StructOptions>,
stream: ExactReadable | AsyncExactReadable,
struct: StructValue,
): ValueOrPromise<StructFieldValue<this>> {
void options;
void stream;
void struct;
throw new Error("Method not implemented.");
}
}
it("should push a field and update size", () => {
const struct = /* #__PURE__ */ new Struct();
const field1 = "foo";
const fieldDefinition1 = new MockFieldDefinition(4);
struct.field(field1, fieldDefinition1);
assert.strictEqual(struct.size, 4);
assert.strictEqual(fieldDefinition1.getSize.mock.callCount(), 1);
assert.deepStrictEqual(struct.fields, [[field1, fieldDefinition1]]);
const field2 = "bar";
const fieldDefinition2 = new MockFieldDefinition(8);
struct.field(field2, fieldDefinition2);
assert.strictEqual(struct.size, 12);
assert.strictEqual(fieldDefinition2.getSize.mock.callCount(), 1);
assert.deepStrictEqual(struct.fields, [
[field1, fieldDefinition1],
[field2, fieldDefinition2],
]);
});
it("should throw an error if field name already exists", () => {
const struct = /* #__PURE__ */ new Struct();
const fieldName = "foo";
struct.field(fieldName, new MockFieldDefinition(4));
assert.throws(() => {
struct.field(fieldName, new MockFieldDefinition(4));
});
});
});
describe("#number", () => {
it("`int8` should append an `int8` field", () => {
const struct = /* #__PURE__ */ new Struct();
struct.int8("foo");
assert.strictEqual(struct.size, 1);
const definition = struct.fields[0]![1] as NumberFieldDefinition;
assert.ok(definition instanceof NumberFieldDefinition);
assert.strictEqual(definition.variant, NumberFieldVariant.Int8);
});
it("`uint8` should append an `uint8` field", () => {
const struct = /* #__PURE__ */ new Struct();
struct.uint8("foo");
assert.strictEqual(struct.size, 1);
const definition = struct.fields[0]![1] as NumberFieldDefinition;
assert.ok(definition instanceof NumberFieldDefinition);
assert.strictEqual(definition.variant, NumberFieldVariant.Uint8);
});
it("`int16` should append an `int16` field", () => {
const struct = /* #__PURE__ */ new Struct();
struct.int16("foo");
assert.strictEqual(struct.size, 2);
const definition = struct.fields[0]![1] as NumberFieldDefinition;
assert.ok(definition instanceof NumberFieldDefinition);
assert.strictEqual(definition.variant, NumberFieldVariant.Int16);
});
it("`uint16` should append an `uint16` field", () => {
const struct = /* #__PURE__ */ new Struct();
struct.uint16("foo");
assert.strictEqual(struct.size, 2);
const definition = struct.fields[0]![1] as NumberFieldDefinition;
assert.ok(definition instanceof NumberFieldDefinition);
assert.strictEqual(definition.variant, NumberFieldVariant.Uint16);
});
it("`int32` should append an `int32` field", () => {
const struct = /* #__PURE__ */ new Struct();
struct.int32("foo");
assert.strictEqual(struct.size, 4);
const definition = struct.fields[0]![1] as NumberFieldDefinition;
assert.ok(definition instanceof NumberFieldDefinition);
assert.strictEqual(definition.variant, NumberFieldVariant.Int32);
});
it("`uint32` should append an `uint32` field", () => {
const struct = /* #__PURE__ */ new Struct();
struct.uint32("foo");
assert.strictEqual(struct.size, 4);
const definition = struct.fields[0]![1] as NumberFieldDefinition;
assert.ok(definition instanceof NumberFieldDefinition);
assert.strictEqual(definition.variant, NumberFieldVariant.Uint32);
});
it("`int64` should append an `int64` field", () => {
const struct = /* #__PURE__ */ new Struct();
struct.int64("foo");
assert.strictEqual(struct.size, 8);
const definition = struct.fields[0]![1] as BigIntFieldDefinition;
assert.ok(definition instanceof BigIntFieldDefinition);
assert.strictEqual(definition.variant, BigIntFieldVariant.Int64);
});
it("`uint64` should append an `uint64` field", () => {
const struct = /* #__PURE__ */ new Struct();
struct.uint64("foo");
assert.strictEqual(struct.size, 8);
const definition = struct.fields[0]![1] as BigIntFieldDefinition;
assert.ok(definition instanceof BigIntFieldDefinition);
assert.strictEqual(definition.variant, BigIntFieldVariant.Uint64);
});
describe("#uint8ArrayLike", () => {
describe("FixedLengthBufferLikeFieldDefinition", () => {
it("`#uint8Array` with fixed length", () => {
const struct = /* #__PURE__ */ new Struct();
struct.uint8Array("foo", { length: 10 });
assert.strictEqual(struct.size, 10);
const definition = struct
.fields[0]![1] as FixedLengthBufferLikeFieldDefinition;
assert.ok(
definition instanceof
FixedLengthBufferLikeFieldDefinition,
);
assert.ok(
definition.converter instanceof BufferFieldConverter,
);
assert.strictEqual(definition.options.length, 10);
});
it("`#string` with fixed length", () => {
const struct = /* #__PURE__ */ new Struct();
struct.string("foo", { length: 10 });
assert.strictEqual(struct.size, 10);
const definition = struct
.fields[0]![1] as FixedLengthBufferLikeFieldDefinition;
assert.ok(
definition instanceof
FixedLengthBufferLikeFieldDefinition,
);
assert.ok(
definition.converter instanceof BufferFieldConverter,
);
assert.strictEqual(definition.options.length, 10);
});
});
describe("VariableLengthBufferLikeFieldDefinition", () => {
it("`#uint8Array` with variable length", () => {
const struct = /* #__PURE__ */ new Struct().int8(
"barLength",
);
assert.strictEqual(struct.size, 1);
struct.uint8Array("bar", { lengthField: "barLength" });
assert.strictEqual(struct.size, 1);
const definition = struct
.fields[1]![1] as VariableLengthBufferLikeFieldDefinition;
assert.ok(
definition instanceof
VariableLengthBufferLikeFieldDefinition,
);
assert.ok(
definition.converter instanceof BufferFieldConverter,
);
assert.strictEqual(
definition.options.lengthField,
"barLength",
);
});
it("`#string` with variable length", () => {
const struct = /* #__PURE__ */ new Struct().int8(
"barLength",
);
assert.strictEqual(struct.size, 1);
struct.string("bar", { lengthField: "barLength" });
assert.strictEqual(struct.size, 1);
const definition = struct
.fields[1]![1] as VariableLengthBufferLikeFieldDefinition;
assert.ok(
definition instanceof
VariableLengthBufferLikeFieldDefinition,
);
assert.ok(
definition.converter instanceof BufferFieldConverter,
);
assert.strictEqual(
definition.options.lengthField,
"barLength",
);
});
});
});
describe("#concat", () => {
it("should append all fields from other struct", () => {
const sub = /* #__PURE__ */ new Struct()
.int16("int16")
.int32("int32");
const struct = /* #__PURE__ */ new Struct()
.int8("int8")
.concat(sub)
.int64("int64");
const field0 = struct.fields[0]!;
assert.strictEqual(field0[0], "int8");
assert.strictEqual(
(field0[1] as NumberFieldDefinition).variant,
NumberFieldVariant.Int8,
);
const field1 = struct.fields[1]!;
assert.strictEqual(field1[0], "int16");
assert.strictEqual(
(field1[1] as NumberFieldDefinition).variant,
NumberFieldVariant.Int16,
);
const field2 = struct.fields[2]!;
assert.strictEqual(field2[0], "int32");
assert.strictEqual(
(field2[1] as NumberFieldDefinition).variant,
NumberFieldVariant.Int32,
);
});
});
describe("#deserialize", () => {
it("should deserialize without dynamic size fields", () => {
const struct = /* #__PURE__ */ new Struct()
.int8("foo")
.int16("bar");
const stream = new MockDeserializationStream();
stream.readExactly.mock.mockImplementationOnce(
() => new Uint8Array([2]),
0,
);
stream.readExactly.mock.mockImplementationOnce(
() => new Uint8Array([0, 16]),
1,
);
const result = struct.deserialize(stream);
assert.deepEqual(result, { foo: 2, bar: 16 });
assert.strictEqual(stream.readExactly.mock.callCount(), 2);
assert.deepStrictEqual(
stream.readExactly.mock.calls[0]!.arguments,
[1],
);
assert.deepStrictEqual(
stream.readExactly.mock.calls[1]!.arguments,
[2],
);
});
it("should deserialize with dynamic size fields", () => {
const struct = /* #__PURE__ */ new Struct()
.int8("fooLength")
.uint8Array("foo", { lengthField: "fooLength" });
const stream = new MockDeserializationStream();
stream.readExactly.mock.mockImplementationOnce(
() => new Uint8Array([2]),
0,
);
stream.readExactly.mock.mockImplementationOnce(
() => new Uint8Array([3, 4]),
1,
);
const result = struct.deserialize(stream);
assert.deepEqual(result, {
get fooLength() {
return 2;
},
get foo() {
return new Uint8Array([3, 4]);
},
});
assert.strictEqual(stream.readExactly.mock.callCount(), 2);
assert.deepStrictEqual(
stream.readExactly.mock.calls[0]!.arguments,
[1],
);
assert.deepStrictEqual(
stream.readExactly.mock.calls[1]!.arguments,
[2],
);
});
});
describe("#extra", () => {
it("should accept plain field", () => {
const struct = /* #__PURE__ */ new Struct().extra({
foo: 42,
bar: true,
});
const stream = new MockDeserializationStream();
const result = struct.deserialize(stream);
assert.deepStrictEqual(
Object.entries(
Object.getOwnPropertyDescriptors(
Object.getPrototypeOf(result),
),
),
[
[
"foo",
{
configurable: true,
enumerable: true,
writable: true,
value: 42,
},
],
[
"bar",
{
configurable: true,
enumerable: true,
writable: true,
value: true,
},
],
],
);
});
it("should accept accessors", () => {
const struct = /* #__PURE__ */ new Struct().extra({
get foo() {
return 42;
},
get bar() {
return true;
},
set bar(value) {
void value;
},
});
const stream = new MockDeserializationStream();
const result = struct.deserialize(stream);
const properties = Object.getOwnPropertyDescriptors(
Object.getPrototypeOf(result),
);
assert.strictEqual(properties.foo?.configurable, true);
assert.strictEqual(properties.foo?.enumerable, true);
assert.strictEqual(properties.bar?.configurable, true);
assert.strictEqual(properties.bar?.enumerable, true);
});
});
describe("#postDeserialize", () => {
it("can throw errors", () => {
const struct = /* #__PURE__ */ new Struct();
const callback = mock.fn(() => {
throw new Error("mock");
});
struct.postDeserialize(callback);
const stream = new MockDeserializationStream();
assert.throws(() => struct.deserialize(stream), /mock/);
assert.strictEqual(callback.mock.callCount(), 1);
});
it("can replace return value", () => {
const struct = /* #__PURE__ */ new Struct();
const callback = mock.fn(() => "mock");
struct.postDeserialize(callback);
const stream = new MockDeserializationStream();
assert.strictEqual(struct.deserialize(stream), "mock");
assert.strictEqual(callback.mock.callCount(), 1);
assert.deepEqual(callback.mock.calls[0]?.arguments, [{}]);
});
it("can return nothing", () => {
const struct = /* #__PURE__ */ new Struct();
const callback = mock.fn();
struct.postDeserialize(callback);
const stream = new MockDeserializationStream();
const result = struct.deserialize(stream);
assert.strictEqual(callback.mock.callCount(), 1);
assert.deepEqual(callback.mock.calls[0]?.arguments, [result]);
});
it("should overwrite callback", () => {
const struct = /* #__PURE__ */ new Struct();
const callback1 = mock.fn();
struct.postDeserialize(callback1);
const callback2 = mock.fn();
struct.postDeserialize(callback2);
const stream = new MockDeserializationStream();
struct.deserialize(stream);
assert.strictEqual(callback1.mock.callCount(), 0);
assert.strictEqual(callback2.mock.callCount(), 1);
assert.deepEqual(callback2.mock.calls[0]?.arguments, [{}]);
});
});
describe("#serialize", () => {
it("should serialize without dynamic size fields", () => {
const struct = /* #__PURE__ */ new Struct()
.int8("foo")
.int16("bar");
const result = new Uint8Array(
struct.serialize({ foo: 0x42, bar: 0x1024 }),
);
assert.deepStrictEqual(
result,
new Uint8Array([0x42, 0x10, 0x24]),
);
});
it("should serialize with dynamic size fields", () => {
const struct = /* #__PURE__ */ new Struct()
.int8("fooLength")
.uint8Array("foo", { lengthField: "fooLength" });
const result = new Uint8Array(
struct.serialize({
foo: new Uint8Array([0x03, 0x04, 0x05]),
}),
);
assert.deepStrictEqual(
result,
new Uint8Array([0x03, 0x03, 0x04, 0x05]),
);
});
});
it("serialize", () => {
const A = new Struct({ id: u8 }, { littleEndian: true });
assert.deepStrictEqual(A.serialize({ id: 10 }), new Uint8Array([10]));
});
});

View file

@ -1,197 +1,35 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { bipedal } from "./bipedal.js";
import type { DeserializeContext, Field, SerializeContext } from "./field.js";
import type { AsyncExactReadable, ExactReadable } from "./readable.js";
import { ExactReadableEndedError } from "./readable.js";
import type { MaybePromiseLike } from "./utils.js";
import type {
AsyncExactReadable,
ExactReadable,
StructFieldDefinition,
StructFieldValue,
StructOptions,
} from "./basic/index.js";
import {
ExactReadableEndedError,
STRUCT_VALUE_SYMBOL,
StructDefaultOptions,
StructValue,
isStructValueInit,
} from "./basic/index.js";
import { SyncPromise } from "./sync-promise.js";
import type {
BufferFieldConverter,
FixedLengthBufferLikeFieldOptions,
LengthField,
VariableLengthBufferLikeFieldOptions,
} from "./types/index.js";
import {
BigIntFieldDefinition,
BigIntFieldVariant,
FixedLengthBufferLikeFieldDefinition,
NumberFieldDefinition,
NumberFieldVariant,
StringBufferFieldConverter,
Uint8ArrayBufferFieldConverter,
VariableLengthBufferLikeFieldDefinition,
} from "./types/index.js";
import type { Evaluate, Identity, Overwrite, ValueOrPromise } from "./utils.js";
export type FieldsType<
T extends Record<string, Field<unknown, string, unknown>>,
> = {
[K in keyof T]: T[K] extends Field<infer TK, string, unknown> ? TK : never;
};
export interface StructLike<TValue> {
deserialize(stream: ExactReadable | AsyncExactReadable): Promise<TValue>;
}
/**
* Extract the value type of the specified `Struct`
*/
export type StructValueType<T extends StructLike<unknown>> = Awaited<
ReturnType<T["deserialize"]>
export type StructInit<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
T extends Struct<any, any, any>,
> = Omit<
FieldsType<T["fields"]>,
{
[K in keyof T["fields"]]: T["fields"][K] extends Field<
unknown,
infer U,
unknown
>
? U
: never;
}[keyof T["fields"]]
>;
/**
* Create a new `Struct` type with `TDefinition` appended
*/
type AddFieldDescriptor<
TFields extends object,
TOmitInitKey extends PropertyKey,
TExtra extends object,
TPostDeserialized,
TFieldName extends PropertyKey,
TDefinition extends StructFieldDefinition<unknown, unknown, PropertyKey>,
> = Identity<
Struct<
// Merge two types
// Evaluate immediately to optimize editor hover tooltip
Evaluate<TFields & Record<TFieldName, TDefinition["TValue"]>>,
// Merge two `TOmitInitKey`s
TOmitInitKey | TDefinition["TOmitInitKey"],
TExtra,
TPostDeserialized
>
>;
/**
* Overload methods to add an array buffer like field
*/
interface ArrayBufferLikeFieldCreator<
TFields extends object,
TOmitInitKey extends PropertyKey,
TExtra extends object,
TPostDeserialized,
> {
/**
* Append a fixed-length array buffer like field to the `Struct`
*
* @param name Name of the field
* @param type `Array.SubType.ArrayBuffer` or `Array.SubType.String`
* @param options Fixed-length array options
* @param typeScriptType Type of the field in TypeScript.
* For example, if this field is a string, you can declare it as a string enum or literal union.
*/
<
TName extends PropertyKey,
TType extends BufferFieldConverter<unknown, unknown>,
TTypeScriptType = TType["TTypeScriptType"],
>(
name: TName,
type: TType,
options: FixedLengthBufferLikeFieldOptions,
typeScriptType?: TTypeScriptType,
): AddFieldDescriptor<
TFields,
TOmitInitKey,
TExtra,
TPostDeserialized,
TName,
FixedLengthBufferLikeFieldDefinition<
TType,
FixedLengthBufferLikeFieldOptions
>
>;
/**
* Append a variable-length array buffer like field to the `Struct`
*/
<
TName extends PropertyKey,
TType extends BufferFieldConverter<unknown, unknown>,
TOptions extends VariableLengthBufferLikeFieldOptions<TFields>,
TTypeScriptType = TType["TTypeScriptType"],
>(
name: TName,
type: TType,
options: TOptions,
typeScriptType?: TTypeScriptType,
): AddFieldDescriptor<
TFields,
TOmitInitKey,
TExtra,
TPostDeserialized,
TName,
VariableLengthBufferLikeFieldDefinition<TType, TOptions>
>;
}
/**
* Similar to `ArrayBufferLikeFieldCreator`, but bind to `TType`
*/
interface BoundArrayBufferLikeFieldDefinitionCreator<
TFields extends object,
TOmitInitKey extends PropertyKey,
TExtra extends object,
TPostDeserialized,
TType extends BufferFieldConverter<unknown, unknown>,
> {
<TName extends PropertyKey, TTypeScriptType = TType["TTypeScriptType"]>(
name: TName,
options: FixedLengthBufferLikeFieldOptions,
typeScriptType?: TTypeScriptType,
): AddFieldDescriptor<
TFields,
TOmitInitKey,
TExtra,
TPostDeserialized,
TName,
FixedLengthBufferLikeFieldDefinition<
TType,
FixedLengthBufferLikeFieldOptions,
TTypeScriptType
>
>;
<
TName extends PropertyKey,
TOptions extends VariableLengthBufferLikeFieldOptions<
TFields,
LengthField<TFields>
>,
TTypeScriptType = TType["TTypeScriptType"],
>(
name: TName,
options: TOptions,
typeScriptType?: TTypeScriptType,
): AddFieldDescriptor<
TFields,
TOmitInitKey,
TExtra,
TPostDeserialized,
TName,
VariableLengthBufferLikeFieldDefinition<
TType,
TOptions,
TTypeScriptType
>
>;
}
export type StructPostDeserialized<TFields, TPostDeserialized> = (
this: TFields,
object: TFields,
) => TPostDeserialized;
export type StructDeserializedResult<
TFields extends object,
TExtra extends object,
TPostDeserialized,
> = TPostDeserialized extends undefined
? Overwrite<TExtra, TFields>
: TPostDeserialized;
export type StructValue<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
T extends Struct<any, any, any>,
> = ReturnType<Exclude<T["postDeserialize"], undefined>>;
export class StructDeserializeError extends Error {
constructor(message: string) {
@ -214,492 +52,147 @@ export class StructEmptyError extends StructDeserializeError {
}
}
interface StructDefinition<
TFields extends object,
TOmitInitKey extends PropertyKey,
TExtra extends object,
> {
readonly TFields: TFields;
readonly TOmitInitKey: TOmitInitKey;
readonly TExtra: TExtra;
readonly TInit: Evaluate<Omit<TFields, TOmitInitKey>>;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type StructLike<T> = Struct<any, any, T>;
export class Struct<
TFields extends object = Record<never, never>,
TOmitInitKey extends PropertyKey = never,
TExtra extends object = Record<never, never>,
TPostDeserialized = undefined,
> implements
StructLike<
StructDeserializedResult<TFields, TExtra, TPostDeserialized>
>
{
readonly TFields!: TFields;
T extends Record<string, Field<unknown, string, Partial<FieldsType<T>>>>,
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
Extra extends Record<PropertyKey, unknown> = {},
PostDeserialize = FieldsType<T> & Extra,
> {
fields: T;
size: number;
readonly TOmitInitKey!: TOmitInitKey;
#fieldList: [string, Field<unknown, string, unknown>][] = [];
readonly TExtra!: TExtra;
littleEndian: boolean;
readonly TInit!: Evaluate<Omit<TFields, TOmitInitKey>>;
extra: Extra;
readonly TDeserializeResult!: StructDeserializedResult<
TFields,
TExtra,
TPostDeserialized
>;
postDeserialize?:
| ((fields: FieldsType<T> & Extra) => PostDeserialize)
| undefined;
readonly options: Readonly<StructOptions>;
#size = 0;
/**
* Gets the static size (exclude fields that can change size at runtime)
*/
get size() {
return this.#size;
}
#fields: [
name: PropertyKey,
definition: StructFieldDefinition<unknown, unknown, PropertyKey>,
][] = [];
get fields(): readonly [
name: PropertyKey,
definition: StructFieldDefinition<unknown, unknown, PropertyKey>,
][] {
return this.#fields;
}
#extra: Record<PropertyKey, unknown> = {};
#postDeserialized?: StructPostDeserialized<never, unknown> | undefined;
constructor(options?: Partial<Readonly<StructOptions>>) {
this.options = { ...StructDefaultOptions, ...options };
}
/**
* Appends a `StructFieldDefinition` to the `Struct
*/
field<
TName extends PropertyKey,
TDefinition extends StructFieldDefinition<
unknown,
unknown,
PropertyKey
>,
>(
name: TName,
definition: TDefinition,
): AddFieldDescriptor<
TFields,
TOmitInitKey,
TExtra,
TPostDeserialized,
TName,
TDefinition
> {
for (const field of this.#fields) {
if (field[0] === name) {
// Convert Symbol to string
const nameString = String(name);
throw new Error(
`This struct already have a field with name '${nameString}'`,
);
}
}
this.#fields.push([name, definition]);
const size = definition.getSize();
this.#size += size;
// Force cast `this` to another type
return this as never;
}
/**
* Merges (flats) another `Struct`'s fields and extra fields into this one.
*
* `other`'s `postDeserialize` will be ignored.
*/
concat<TOther extends StructDefinition<object, PropertyKey, object>>(
other: TOther,
): Struct<
TFields & TOther["TFields"],
TOmitInitKey | TOther["TOmitInitKey"],
TExtra & TOther["TExtra"],
TPostDeserialized
> {
if (!(other instanceof Struct)) {
throw new TypeError("The other value must be a `Struct` instance");
}
for (const field of other.#fields) {
this.#fields.push(field);
}
this.#size += other.#size;
Object.defineProperties(
this.#extra,
Object.getOwnPropertyDescriptors(other.#extra),
);
return this as never;
}
#number<
TName extends PropertyKey,
TType extends NumberFieldVariant = NumberFieldVariant,
TTypeScriptType = number,
>(name: TName, type: TType, typeScriptType?: TTypeScriptType) {
return this.field(
name,
new NumberFieldDefinition(type, typeScriptType),
);
}
/**
* 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);
constructor(
fields: T,
options: {
littleEndian?: boolean;
extra?: Extra & ThisType<FieldsType<T>>;
postDeserialize?: (
this: FieldsType<T> & Extra,
fields: FieldsType<T> & Extra,
) => PostDeserialize;
},
(e) => {
) {
this.#fieldList = Object.entries(fields);
this.fields = fields;
this.size = this.#fieldList.reduce(
(sum, [, field]) => sum + field.size,
0,
);
this.littleEndian = !!options.littleEndian;
this.extra = options.extra!;
this.postDeserialize = options.postDeserialize;
}
serialize(runtimeStruct: StructInit<this>): Uint8Array;
serialize(runtimeStruct: StructInit<this>, buffer: Uint8Array): number;
serialize(
runtimeStruct: StructInit<this>,
buffer?: Uint8Array,
): Uint8Array | number {
for (const [key, field] of this.#fieldList) {
if (key in runtimeStruct) {
field.preSerialize?.(
runtimeStruct[key as never],
runtimeStruct,
);
}
}
const sizes = this.#fieldList.map(
([key, field]) =>
field.dynamicSize?.(runtimeStruct[key as never]) ?? field.size,
);
const size = sizes.reduce((sum, size) => sum + size, 0);
let externalBuffer = false;
if (buffer) {
if (buffer.length < size) {
throw new Error("Buffer too small");
}
externalBuffer = true;
} else {
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 (stream.position === startPosition) {
if (reader.position === startPosition) {
throw new StructEmptyError();
} else {
throw new StructNotEnoughDataError();
}
},
}
if (this.extra) {
Object.defineProperties(
runtimeStruct,
Object.getOwnPropertyDescriptors(this.extra),
);
}
return promise
.then(() => {
const value = structValue.value;
// Run `postDeserialized`
if (this.#postDeserialized) {
const override = this.#postDeserialized.call(
value as never,
value as never,
if (this.postDeserialize) {
return this.postDeserialize.call(
runtimeStruct,
runtimeStruct 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(
init: Evaluate<Omit<TFields, TOmitInitKey>>,
output?: Uint8Array,
): Uint8Array {
let structValue: StructValue;
if (isStructValueInit(init)) {
structValue = init[STRUCT_VALUE_SYMBOL];
for (const [key, value] of Object.entries(init)) {
const fieldValue = structValue.get(key);
if (fieldValue) {
fieldValue.set(value);
}
}
} else {
structValue = new StructValue({});
for (const [name, definition] of this.#fields) {
const fieldValue = definition.create(
this.options,
structValue,
(init as Record<PropertyKey, unknown>)[name],
);
structValue.set(name, fieldValue);
}
}
let structSize = 0;
const fieldsInfo: {
fieldValue: StructFieldValue<any>;
size: number;
}[] = [];
for (const [name] of this.#fields) {
const fieldValue = structValue.get(name);
const size = fieldValue.getSize();
fieldsInfo.push({ fieldValue, size });
structSize += size;
}
if (!output) {
output = new Uint8Array(structSize);
} else if (output.length < structSize) {
throw new TypeError("Output buffer is too small");
}
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 {
return output;
}
return runtimeStruct as never;
}
}) as never;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +0,0 @@
export * from "./base.js";
export * from "./fixed-length.js";
export * from "./variable-length.js";

View file

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

View file

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

View file

@ -1,3 +0,0 @@
export * from "./bigint.js";
export * from "./buffer/index.js";
export * from "./number.js";

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,58 +1,3 @@
/**
* When evaluating a very complex generic type alias,
* tell TypeScript to use `T`, instead of current type alias' name, as the result type name
*
* Example:
*
* ```ts
* type WithIdentity<T> = Identity<SomeType<T>>;
* type WithoutIdentity<T> = SomeType<T>;
*
* type WithIdentityResult = WithIdentity<number>;
* // Hover on this one shows `SomeType<number>`
*
* type WithoutIdentityResult = WithoutIdentity<number>;
* // Hover on this one shows `WithoutIdentity<number>`
* ```
*/
export type Identity<T> = T;
/**
* Collapse an intersection type (`{ foo: string } & { bar: number }`) to a simple type (`{ foo: string, bar: number }`)
*/
export type Evaluate<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
/**
* Overwrite fields in `TBase` with fields in `TNew`
*/
export type Overwrite<TBase extends object, TNew extends object> = Evaluate<
Omit<TBase, keyof TNew> & TNew
>;
/**
* Remove fields with `never` type
*/
export type OmitNever<T> = Pick<
T,
{ [K in keyof T]: [T[K]] extends [never] ? never : K }[keyof T]
>;
/**
* Extract keys of fields in `T` that has type `TValue`
*/
export type KeysOfType<T, TValue> = {
[TKey in keyof T]: T[TKey] extends TValue ? TKey : never;
}[keyof T];
export type ValueOrPromise<T> = T | PromiseLike<T>;
/**
* Returns a (fake) value of the given type.
*/
export function placeholder<T>(): T {
return undefined as unknown as T;
}
// This library can't use `@types/node` or `lib: dom`
// because they will pollute the global scope
// So `TextEncoder` and `TextDecoder` types are not available
@ -95,3 +40,7 @@ export function decodeUtf8(buffer: ArrayBufferView | ArrayBuffer): string {
// but this method is not for stream mode, so the instance can be reused
return SharedDecoder.decode(buffer);
}
export type MaybePromise<T> = T | Promise<T>;
export type MaybePromiseLike<T> = T | PromiseLike<T>;

153
pnpm-lock.yaml generated
View file

@ -52,8 +52,8 @@ importers:
libraries/adb:
dependencies:
'@yume-chan/async':
specifier: ^2.2.0
version: 2.2.0
specifier: ^4.0.0
version: 4.0.0
'@yume-chan/event':
specifier: workspace:^
version: link:../event
@ -145,8 +145,8 @@ importers:
specifier: workspace:^
version: link:../adb
'@yume-chan/async':
specifier: ^2.2.0
version: 2.2.0
specifier: ^4.0.0
version: 4.0.0
'@yume-chan/event':
specifier: workspace:^
version: link:../event
@ -254,8 +254,8 @@ importers:
libraries/event:
dependencies:
'@yume-chan/async':
specifier: ^2.2.0
version: 2.2.0
specifier: ^4.0.0
version: 4.0.0
devDependencies:
'@types/node':
specifier: ^22.7.7
@ -331,8 +331,8 @@ importers:
libraries/scrcpy:
dependencies:
'@yume-chan/async':
specifier: ^2.2.0
version: 2.2.0
specifier: ^4.0.0
version: 4.0.0
'@yume-chan/no-data-view':
specifier: workspace:^
version: link:../no-data-view
@ -365,8 +365,8 @@ importers:
libraries/scrcpy-decoder-tinyh264:
dependencies:
'@yume-chan/async':
specifier: ^2.2.0
version: 2.2.0
specifier: ^4.0.0
version: 4.0.0
'@yume-chan/event':
specifier: workspace:^
version: link:../event
@ -433,8 +433,8 @@ importers:
libraries/stream-extra:
dependencies:
'@yume-chan/async':
specifier: ^2.2.0
version: 2.2.0
specifier: ^4.0.0
version: 4.0.0
'@yume-chan/struct':
specifier: workspace:^
version: link:../struct
@ -501,8 +501,8 @@ importers:
specifier: ^5.6.3
version: 5.6.3
typescript-eslint:
specifier: ^8.10.0
version: 8.10.0(eslint@9.13.0)(typescript@5.6.3)
specifier: ^8.11.0
version: 8.11.0(eslint@9.13.0)(typescript@5.6.3)
devDependencies:
prettier:
specifier: ^3.3.3
@ -693,8 +693,8 @@ packages:
'@types/w3c-web-usb@1.0.10':
resolution: {integrity: sha512-CHgUI5kTc/QLMP8hODUHhge0D4vx+9UiAwIGiT0sTy/B2XpdX1U5rJt6JSISgr6ikRT7vxV9EVAFeYZqUnl1gQ==}
'@typescript-eslint/eslint-plugin@8.10.0':
resolution: {integrity: sha512-phuB3hoP7FFKbRXxjl+DRlQDuJqhpOnm5MmtROXyWi3uS/Xg2ZXqiQfcG2BJHiN4QKyzdOJi3NEn/qTnjUlkmQ==}
'@typescript-eslint/eslint-plugin@8.11.0':
resolution: {integrity: sha512-KhGn2LjW1PJT2A/GfDpiyOfS4a8xHQv2myUagTM5+zsormOmBlYsnQ6pobJ8XxJmh6hnHwa2Mbe3fPrDJoDhbA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
'@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0
@ -704,8 +704,8 @@ packages:
typescript:
optional: true
'@typescript-eslint/parser@8.10.0':
resolution: {integrity: sha512-E24l90SxuJhytWJ0pTQydFT46Nk0Z+bsLKo/L8rtQSL93rQ6byd1V/QbDpHUTdLPOMsBCcYXZweADNCfOCmOAg==}
'@typescript-eslint/parser@8.11.0':
resolution: {integrity: sha512-lmt73NeHdy1Q/2ul295Qy3uninSqi6wQI18XwSpm8w0ZbQXUpjCAWP1Vlv/obudoBiIjJVjlztjQ+d/Md98Yxg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
@ -718,8 +718,12 @@ packages:
resolution: {integrity: sha512-AgCaEjhfql9MDKjMUxWvH7HjLeBqMCBfIaBbzzIcBbQPZE7CPh1m6FF+L75NUMJFMLYhCywJXIDEMa3//1A0dw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/type-utils@8.10.0':
resolution: {integrity: sha512-PCpUOpyQSpxBn230yIcK+LeCQaXuxrgCm2Zk1S+PTIRJsEfU6nJ0TtwyH8pIwPK/vJoA+7TZtzyAJSGBz+s/dg==}
'@typescript-eslint/scope-manager@8.11.0':
resolution: {integrity: sha512-Uholz7tWhXmA4r6epo+vaeV7yjdKy5QFCERMjs1kMVsLRKIrSdM6o21W2He9ftp5PP6aWOVpD5zvrvuHZC0bMQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/type-utils@8.11.0':
resolution: {integrity: sha512-ItiMfJS6pQU0NIKAaybBKkuVzo6IdnAhPFZA/2Mba/uBjuPQPet/8+zh5GtLHwmuFRShZx+8lhIs7/QeDHflOg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '*'
@ -731,6 +735,10 @@ packages:
resolution: {integrity: sha512-k/E48uzsfJCRRbGLapdZgrX52csmWJ2rcowwPvOZ8lwPUv3xW6CcFeJAXgx4uJm+Ge4+a4tFOkdYvSpxhRhg1w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/types@8.11.0':
resolution: {integrity: sha512-tn6sNMHf6EBAYMvmPUaKaVeYvhUsrE6x+bXQTxjQRp360h1giATU0WvgeEys1spbvb5R+VpNOZ+XJmjD8wOUHw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.10.0':
resolution: {integrity: sha512-3OE0nlcOHaMvQ8Xu5gAfME3/tWVDpb/HxtpUZ1WeOAksZ/h/gwrBzCklaGzwZT97/lBbbxJ16dMA98JMEngW4w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -740,16 +748,35 @@ packages:
typescript:
optional: true
'@typescript-eslint/typescript-estree@8.11.0':
resolution: {integrity: sha512-yHC3s1z1RCHoCz5t06gf7jH24rr3vns08XXhfEqzYpd6Hll3z/3g23JRi0jM8A47UFKNc3u/y5KIMx8Ynbjohg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
'@typescript-eslint/utils@8.10.0':
resolution: {integrity: sha512-Oq4uZ7JFr9d1ZunE/QKy5egcDRXT/FrS2z/nlxzPua2VHFtmMvFNDvpq1m/hq0ra+T52aUezfcjGRIB7vNJF9w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
'@typescript-eslint/utils@8.11.0':
resolution: {integrity: sha512-CYiX6WZcbXNJV7UNB4PLDIBtSdRmRI/nb0FMyqHPTQD1rMjA0foPLaPUV39C/MxkTd/QKSeX+Gb34PPsDVC35g==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
'@typescript-eslint/visitor-keys@8.10.0':
resolution: {integrity: sha512-k8nekgqwr7FadWk548Lfph6V3r9OVqjzAIVskE7orMZR23cGJjAOVazsZSJW+ElyjfTM4wx/1g88Mi70DDtG9A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/visitor-keys@8.11.0':
resolution: {integrity: sha512-EaewX6lxSjRJnc+99+dqzTeoDZUfyrA52d2/HRrkI830kgovWsmIiTfmr0NZorzqic7ga+1bS60lRBUgR3n/Bw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@xhmikosr/archive-type@6.0.1':
resolution: {integrity: sha512-PB3NeJL8xARZt52yDBupK0dNPn8uIVQDe15qNehUpoeeLWCZyAOam4vGXnoZGz2N9D1VXtjievJuCsXam2TmbQ==}
engines: {node: ^14.14.0 || >=16.0.0}
@ -778,8 +805,8 @@ packages:
resolution: {integrity: sha512-mBvWew1kZJHfNQVVfVllMjUDwCGN9apPa0t4/z1zaUJ9MzpXjRL3w8fsfJKB8gHN/h4rik9HneKfDbh2fErN+w==}
engines: {node: ^14.14.0 || >=16.0.0}
'@yume-chan/async@2.2.0':
resolution: {integrity: sha512-jatCtX1/3DsR9Vt3EB8CGFy0MNrXP5f+eNiRGHLH+LkYz7MPLzpqL/DnvXSip+Z0EKBCDnzuNuELjsKEEzcdQA==}
'@yume-chan/async@4.0.0':
resolution: {integrity: sha512-T4DOnvaVqrx+PQh8bESdS6y2ozii7M0isJ5MpGU0girfz9kmwOaJ+rF1oeTJGZ0k+v92+eo/q6SpJjcjnO9tuQ==}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
@ -1658,8 +1685,8 @@ packages:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
typescript-eslint@8.10.0:
resolution: {integrity: sha512-YIu230PeN7z9zpu/EtqCIuRVHPs4iSlqW6TEvjbyDAE3MZsSl2RXBo+5ag+lbABCG8sFM1WVKEXhlQ8Ml8A3Fw==}
typescript-eslint@8.11.0:
resolution: {integrity: sha512-cBRGnW3FSlxaYwU8KfAewxFK5uzeOAp0l2KebIlPDOT5olVi65KDG/yjBooPBG0kGW/HLkoz1c/iuBFehcS3IA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '*'
@ -1971,14 +1998,14 @@ snapshots:
'@types/w3c-web-usb@1.0.10': {}
'@typescript-eslint/eslint-plugin@8.10.0(@typescript-eslint/parser@8.10.0(eslint@9.13.0)(typescript@5.6.3))(eslint@9.13.0)(typescript@5.6.3)':
'@typescript-eslint/eslint-plugin@8.11.0(@typescript-eslint/parser@8.11.0(eslint@9.13.0)(typescript@5.6.3))(eslint@9.13.0)(typescript@5.6.3)':
dependencies:
'@eslint-community/regexpp': 4.11.1
'@typescript-eslint/parser': 8.10.0(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/scope-manager': 8.10.0
'@typescript-eslint/type-utils': 8.10.0(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/utils': 8.10.0(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/visitor-keys': 8.10.0
'@typescript-eslint/parser': 8.11.0(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/scope-manager': 8.11.0
'@typescript-eslint/type-utils': 8.11.0(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/utils': 8.11.0(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/visitor-keys': 8.11.0
eslint: 9.13.0
graphemer: 1.4.0
ignore: 5.3.2
@ -1989,12 +2016,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.10.0(eslint@9.13.0)(typescript@5.6.3)':
'@typescript-eslint/parser@8.11.0(eslint@9.13.0)(typescript@5.6.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.10.0
'@typescript-eslint/types': 8.10.0
'@typescript-eslint/typescript-estree': 8.10.0(typescript@5.6.3)
'@typescript-eslint/visitor-keys': 8.10.0
'@typescript-eslint/scope-manager': 8.11.0
'@typescript-eslint/types': 8.11.0
'@typescript-eslint/typescript-estree': 8.11.0(typescript@5.6.3)
'@typescript-eslint/visitor-keys': 8.11.0
debug: 4.3.7
eslint: 9.13.0
optionalDependencies:
@ -2007,10 +2034,15 @@ snapshots:
'@typescript-eslint/types': 8.10.0
'@typescript-eslint/visitor-keys': 8.10.0
'@typescript-eslint/type-utils@8.10.0(eslint@9.13.0)(typescript@5.6.3)':
'@typescript-eslint/scope-manager@8.11.0':
dependencies:
'@typescript-eslint/typescript-estree': 8.10.0(typescript@5.6.3)
'@typescript-eslint/utils': 8.10.0(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/types': 8.11.0
'@typescript-eslint/visitor-keys': 8.11.0
'@typescript-eslint/type-utils@8.11.0(eslint@9.13.0)(typescript@5.6.3)':
dependencies:
'@typescript-eslint/typescript-estree': 8.11.0(typescript@5.6.3)
'@typescript-eslint/utils': 8.11.0(eslint@9.13.0)(typescript@5.6.3)
debug: 4.3.7
ts-api-utils: 1.3.0(typescript@5.6.3)
optionalDependencies:
@ -2021,6 +2053,8 @@ snapshots:
'@typescript-eslint/types@8.10.0': {}
'@typescript-eslint/types@8.11.0': {}
'@typescript-eslint/typescript-estree@8.10.0(typescript@5.6.3)':
dependencies:
'@typescript-eslint/types': 8.10.0
@ -2036,6 +2070,21 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/typescript-estree@8.11.0(typescript@5.6.3)':
dependencies:
'@typescript-eslint/types': 8.11.0
'@typescript-eslint/visitor-keys': 8.11.0
debug: 4.3.7
fast-glob: 3.3.2
is-glob: 4.0.3
minimatch: 9.0.5
semver: 7.6.3
ts-api-utils: 1.3.0(typescript@5.6.3)
optionalDependencies:
typescript: 5.6.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.10.0(eslint@9.13.0)(typescript@5.6.3)':
dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@9.13.0)
@ -2047,11 +2096,27 @@ snapshots:
- supports-color
- typescript
'@typescript-eslint/utils@8.11.0(eslint@9.13.0)(typescript@5.6.3)':
dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@9.13.0)
'@typescript-eslint/scope-manager': 8.11.0
'@typescript-eslint/types': 8.11.0
'@typescript-eslint/typescript-estree': 8.11.0(typescript@5.6.3)
eslint: 9.13.0
transitivePeerDependencies:
- supports-color
- typescript
'@typescript-eslint/visitor-keys@8.10.0':
dependencies:
'@typescript-eslint/types': 8.10.0
eslint-visitor-keys: 3.4.3
'@typescript-eslint/visitor-keys@8.11.0':
dependencies:
'@typescript-eslint/types': 8.11.0
eslint-visitor-keys: 3.4.3
'@xhmikosr/archive-type@6.0.1':
dependencies:
file-type: 18.7.0
@ -2105,9 +2170,7 @@ snapshots:
merge-options: 3.0.4
p-event: 5.0.1
'@yume-chan/async@2.2.0':
dependencies:
tslib: 2.8.0
'@yume-chan/async@4.0.0': {}
acorn-jsx@5.3.2(acorn@8.13.0):
dependencies:
@ -2928,11 +2991,11 @@ snapshots:
dependencies:
prelude-ls: 1.2.1
typescript-eslint@8.10.0(eslint@9.13.0)(typescript@5.6.3):
typescript-eslint@8.11.0(eslint@9.13.0)(typescript@5.6.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.10.0(@typescript-eslint/parser@8.10.0(eslint@9.13.0)(typescript@5.6.3))(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/parser': 8.10.0(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/utils': 8.10.0(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/eslint-plugin': 8.11.0(@typescript-eslint/parser@8.11.0(eslint@9.13.0)(typescript@5.6.3))(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/parser': 8.11.0(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/utils': 8.11.0(eslint@9.13.0)(typescript@5.6.3)
optionalDependencies:
typescript: 5.6.3
transitivePeerDependencies: