mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-03 09:49:24 +02:00
feat(struct): allow structs to be used as fields directly (#741)
This commit is contained in:
parent
d3019ce738
commit
b79df96301
32 changed files with 956 additions and 429 deletions
5
.changeset/some-tigers-hide.md
Normal file
5
.changeset/some-tigers-hide.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"@yume-chan/struct": major
|
||||||
|
---
|
||||||
|
|
||||||
|
Refactor struct package to allow `struct`s to be used as `field`
|
|
@ -19,8 +19,7 @@ import {
|
||||||
ReadableStream,
|
ReadableStream,
|
||||||
pipeFrom,
|
pipeFrom,
|
||||||
} from "@yume-chan/stream-extra";
|
} from "@yume-chan/stream-extra";
|
||||||
import type { ExactReadable } from "@yume-chan/struct";
|
import { EmptyUint8Array, Uint8ArrayExactReadable } from "@yume-chan/struct";
|
||||||
import { EmptyUint8Array } from "@yume-chan/struct";
|
|
||||||
|
|
||||||
import { DeviceBusyError as _DeviceBusyError } from "./error.js";
|
import { DeviceBusyError as _DeviceBusyError } from "./error.js";
|
||||||
import type { UsbInterfaceFilter, UsbInterfaceIdentifier } from "./utils.js";
|
import type { UsbInterfaceFilter, UsbInterfaceIdentifier } from "./utils.js";
|
||||||
|
@ -52,29 +51,6 @@ export function mergeDefaultAdbInterfaceFilter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Uint8ArrayExactReadable implements ExactReadable {
|
|
||||||
#data: Uint8Array;
|
|
||||||
#position: number;
|
|
||||||
|
|
||||||
get position() {
|
|
||||||
return this.#position;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(data: Uint8Array) {
|
|
||||||
this.#data = data;
|
|
||||||
this.#position = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
readExactly(length: number): Uint8Array {
|
|
||||||
const result = this.#data.subarray(
|
|
||||||
this.#position,
|
|
||||||
this.#position + length,
|
|
||||||
);
|
|
||||||
this.#position += length;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AdbDaemonWebUsbConnection
|
export class AdbDaemonWebUsbConnection
|
||||||
implements ReadableWritablePair<AdbPacketData, Consumable<AdbPacketInit>>
|
implements ReadableWritablePair<AdbPacketData, Consumable<AdbPacketInit>>
|
||||||
{
|
{
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { BufferedReadableStream } from "@yume-chan/stream-extra";
|
||||||
import {
|
import {
|
||||||
encodeUtf8,
|
encodeUtf8,
|
||||||
ExactReadableEndedError,
|
ExactReadableEndedError,
|
||||||
|
extend,
|
||||||
string,
|
string,
|
||||||
struct,
|
struct,
|
||||||
} from "@yume-chan/struct";
|
} from "@yume-chan/struct";
|
||||||
|
@ -49,11 +50,11 @@ export class AdbReverseNotSupportedError extends AdbReverseError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const AdbReverseErrorResponse = struct(
|
const AdbReverseErrorResponse = extend(
|
||||||
/* #__PURE__ */ (() => AdbReverseStringResponse.fields)(),
|
AdbReverseStringResponse,
|
||||||
|
{},
|
||||||
{
|
{
|
||||||
littleEndian: true,
|
postDeserialize(value) {
|
||||||
postDeserialize: (value) => {
|
|
||||||
// https://issuetracker.google.com/issues/37066218
|
// https://issuetracker.google.com/issues/37066218
|
||||||
// ADB on Android <9 can't create reverse tunnels when connected wirelessly (ADB over Wi-Fi),
|
// ADB on Android <9 can't create reverse tunnels when connected wirelessly (ADB over Wi-Fi),
|
||||||
// and returns this confusing "more than one device/emulator" error.
|
// and returns this confusing "more than one device/emulator" error.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { StructValue } from "@yume-chan/struct";
|
import type { StructValue } from "@yume-chan/struct";
|
||||||
import { string, struct, u32 } from "@yume-chan/struct";
|
import { extend, string, u32 } from "@yume-chan/struct";
|
||||||
|
|
||||||
import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js";
|
import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js";
|
||||||
import { AdbSyncResponseId, adbSyncReadResponses } from "./response.js";
|
import { AdbSyncResponseId, adbSyncReadResponses } from "./response.js";
|
||||||
|
@ -15,25 +15,15 @@ export interface AdbSyncEntry extends AdbSyncStat {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AdbSyncEntryResponse = /* #__PURE__ */ (() =>
|
export const AdbSyncEntryResponse = extend(AdbSyncLstatResponse, {
|
||||||
struct(
|
name: string(u32),
|
||||||
{
|
});
|
||||||
...AdbSyncLstatResponse.fields,
|
|
||||||
name: string(u32),
|
|
||||||
},
|
|
||||||
{ littleEndian: true, extra: AdbSyncLstatResponse.extra },
|
|
||||||
))();
|
|
||||||
|
|
||||||
export type AdbSyncEntryResponse = StructValue<typeof AdbSyncEntryResponse>;
|
export type AdbSyncEntryResponse = StructValue<typeof AdbSyncEntryResponse>;
|
||||||
|
|
||||||
export const AdbSyncEntry2Response = /* #__PURE__ */ (() =>
|
export const AdbSyncEntry2Response = extend(AdbSyncStatResponse, {
|
||||||
struct(
|
name: string(u32),
|
||||||
{
|
});
|
||||||
...AdbSyncStatResponse.fields,
|
|
||||||
name: string(u32),
|
|
||||||
},
|
|
||||||
{ littleEndian: true, extra: AdbSyncStatResponse.extra },
|
|
||||||
))();
|
|
||||||
|
|
||||||
export type AdbSyncEntry2Response = StructValue<typeof AdbSyncEntry2Response>;
|
export type AdbSyncEntry2Response = StructValue<typeof AdbSyncEntry2Response>;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { getUint32LittleEndian } from "@yume-chan/no-data-view";
|
import { getUint32LittleEndian } from "@yume-chan/no-data-view";
|
||||||
import type { AsyncExactReadable, StructLike } from "@yume-chan/struct";
|
import type { AsyncExactReadable, StructDeserializer } from "@yume-chan/struct";
|
||||||
import { decodeUtf8, string, struct, u32 } from "@yume-chan/struct";
|
import { decodeUtf8, string, struct, u32 } from "@yume-chan/struct";
|
||||||
|
|
||||||
function encodeAsciiUnchecked(value: string): Uint8Array {
|
function encodeAsciiUnchecked(value: string): Uint8Array {
|
||||||
|
@ -49,7 +49,7 @@ export const AdbSyncFailResponse = struct(
|
||||||
export async function adbSyncReadResponse<T>(
|
export async function adbSyncReadResponse<T>(
|
||||||
stream: AsyncExactReadable,
|
stream: AsyncExactReadable,
|
||||||
id: number | string,
|
id: number | string,
|
||||||
type: StructLike<T>,
|
type: StructDeserializer<T>,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
if (typeof id === "string") {
|
if (typeof id === "string") {
|
||||||
id = adbSyncEncodeId(id);
|
id = adbSyncEncodeId(id);
|
||||||
|
@ -72,7 +72,7 @@ export async function adbSyncReadResponse<T>(
|
||||||
export async function* adbSyncReadResponses<T>(
|
export async function* adbSyncReadResponses<T>(
|
||||||
stream: AsyncExactReadable,
|
stream: AsyncExactReadable,
|
||||||
id: number | string,
|
id: number | string,
|
||||||
type: StructLike<T>,
|
type: StructDeserializer<T>,
|
||||||
): AsyncGenerator<T, void, void> {
|
): AsyncGenerator<T, void, void> {
|
||||||
if (typeof id === "string") {
|
if (typeof id === "string") {
|
||||||
id = adbSyncEncodeId(id);
|
id = adbSyncEncodeId(id);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Consumable, TransformStream } from "@yume-chan/stream-extra";
|
import { Consumable, TransformStream } from "@yume-chan/stream-extra";
|
||||||
import type { StructInit, StructValue } from "@yume-chan/struct";
|
import type { StructInit, StructValue } from "@yume-chan/struct";
|
||||||
import { buffer, s32, struct, u32 } from "@yume-chan/struct";
|
import { buffer, extend, s32, struct, u32 } from "@yume-chan/struct";
|
||||||
|
|
||||||
export const AdbCommand = {
|
export const AdbCommand = {
|
||||||
Auth: 0x48545541, // 'AUTH'
|
Auth: 0x48545541, // 'AUTH'
|
||||||
|
@ -29,13 +29,9 @@ export type AdbPacketHeader = StructValue<typeof AdbPacketHeader>;
|
||||||
|
|
||||||
type AdbPacketHeaderInit = StructInit<typeof AdbPacketHeader>;
|
type AdbPacketHeaderInit = StructInit<typeof AdbPacketHeader>;
|
||||||
|
|
||||||
export const AdbPacket = struct(
|
export const AdbPacket = extend(AdbPacketHeader, {
|
||||||
/* #__PURE__ */ (() => ({
|
payload: buffer("payloadLength"),
|
||||||
...AdbPacketHeader.fields,
|
});
|
||||||
payload: buffer("payloadLength"),
|
|
||||||
}))(),
|
|
||||||
{ littleEndian: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
export type AdbPacket = StructValue<typeof AdbPacket>;
|
export type AdbPacket = StructValue<typeof AdbPacket>;
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,27 @@
|
||||||
import { getUint16, setUint16 } from "@yume-chan/no-data-view";
|
import { getUint16, setUint16 } from "@yume-chan/no-data-view";
|
||||||
import type { Field, StructInit } from "@yume-chan/struct";
|
import type { Field, StructInit } from "@yume-chan/struct";
|
||||||
import { bipedal, struct, u16, u32, u64, u8 } from "@yume-chan/struct";
|
import { field, struct, u16, u32, u64, u8 } from "@yume-chan/struct";
|
||||||
|
|
||||||
import type { AndroidMotionEventAction } from "../../android/index.js";
|
import type { AndroidMotionEventAction } from "../../android/index.js";
|
||||||
import type { ScrcpyInjectTouchControlMessage } from "../../latest.js";
|
import type { ScrcpyInjectTouchControlMessage } from "../../latest.js";
|
||||||
import { clamp } from "../../utils/index.js";
|
import { clamp } from "../../utils/index.js";
|
||||||
|
|
||||||
export const UnsignedFloat: Field<number, never, never> = {
|
export const UnsignedFloat: Field<number, never, never> = field(
|
||||||
size: 2,
|
2,
|
||||||
serialize(value, { buffer, index, littleEndian }) {
|
"byob",
|
||||||
|
(source, { buffer, index, littleEndian }) => {
|
||||||
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/app/src/util/binary.h#L51
|
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/app/src/util/binary.h#L51
|
||||||
value = clamp(value, -1, 1);
|
source = clamp(source, -1, 1);
|
||||||
value = value === 1 ? 0xffff : value * 0x10000;
|
source = source === 1 ? 0xffff : source * 0x10000;
|
||||||
setUint16(buffer, index, value, littleEndian);
|
setUint16(buffer, index, source, littleEndian);
|
||||||
},
|
},
|
||||||
deserialize: bipedal(function* (then, { reader, littleEndian }) {
|
function* (then, reader, { littleEndian }) {
|
||||||
const data = yield* then(reader.readExactly(2));
|
const data = yield* then(reader.readExactly(2));
|
||||||
const value = getUint16(data, 0, littleEndian);
|
const value = getUint16(data, 0, littleEndian);
|
||||||
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/server/src/main/java/com/genymobile/scrcpy/Binary.java#L22
|
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/server/src/main/java/com/genymobile/scrcpy/Binary.java#L22
|
||||||
return value === 0xffff ? 1 : value / 0x10000;
|
return value === 0xffff ? 1 : value / 0x10000;
|
||||||
}),
|
},
|
||||||
};
|
);
|
||||||
|
|
||||||
export const PointerId = {
|
export const PointerId = {
|
||||||
Mouse: -1n,
|
Mouse: -1n,
|
||||||
|
|
|
@ -1,19 +1,15 @@
|
||||||
import type { StructInit } from "@yume-chan/struct";
|
import type { StructInit } from "@yume-chan/struct";
|
||||||
import { struct, u8 } from "@yume-chan/struct";
|
import { extend, u8 } from "@yume-chan/struct";
|
||||||
|
|
||||||
import type { AndroidKeyEventAction } from "../../android/index.js";
|
import type { AndroidKeyEventAction } from "../../android/index.js";
|
||||||
import type { ScrcpyBackOrScreenOnControlMessage } from "../../latest.js";
|
import type { ScrcpyBackOrScreenOnControlMessage } from "../../latest.js";
|
||||||
|
|
||||||
import { PrevImpl } from "./prev.js";
|
import { PrevImpl } from "./prev.js";
|
||||||
|
|
||||||
export const BackOrScreenOnControlMessage = /* #__PURE__ */ (() =>
|
export const BackOrScreenOnControlMessage = extend(
|
||||||
struct(
|
PrevImpl.BackOrScreenOnControlMessage,
|
||||||
{
|
{ action: u8<AndroidKeyEventAction>() },
|
||||||
...PrevImpl.BackOrScreenOnControlMessage.fields,
|
);
|
||||||
action: u8<AndroidKeyEventAction>(),
|
|
||||||
},
|
|
||||||
{ littleEndian: false },
|
|
||||||
))();
|
|
||||||
|
|
||||||
export type BackOrScreenOnControlMessage = StructInit<
|
export type BackOrScreenOnControlMessage = StructInit<
|
||||||
typeof BackOrScreenOnControlMessage
|
typeof BackOrScreenOnControlMessage
|
||||||
|
|
|
@ -1,19 +1,15 @@
|
||||||
import type { StructInit } from "@yume-chan/struct";
|
import type { StructInit } from "@yume-chan/struct";
|
||||||
import { s32, struct } from "@yume-chan/struct";
|
import { extend, s32 } from "@yume-chan/struct";
|
||||||
|
|
||||||
import type { ScrcpyScrollController } from "../../base/index.js";
|
import type { ScrcpyScrollController } from "../../base/index.js";
|
||||||
import type { ScrcpyInjectScrollControlMessage } from "../../latest.js";
|
import type { ScrcpyInjectScrollControlMessage } from "../../latest.js";
|
||||||
|
|
||||||
import { PrevImpl } from "./prev.js";
|
import { PrevImpl } from "./prev.js";
|
||||||
|
|
||||||
export const InjectScrollControlMessage = /* #__PURE__ */ (() =>
|
export const InjectScrollControlMessage = extend(
|
||||||
struct(
|
PrevImpl.InjectScrollControlMessage,
|
||||||
{
|
{ buttons: s32 },
|
||||||
...PrevImpl.InjectScrollControlMessage.fields,
|
);
|
||||||
buttons: s32,
|
|
||||||
},
|
|
||||||
{ littleEndian: false },
|
|
||||||
))();
|
|
||||||
|
|
||||||
export type InjectScrollControlMessage = StructInit<
|
export type InjectScrollControlMessage = StructInit<
|
||||||
typeof InjectScrollControlMessage
|
typeof InjectScrollControlMessage
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import * as assert from "node:assert";
|
import * as assert from "node:assert";
|
||||||
import { describe, it } from "node:test";
|
import { describe, it } from "node:test";
|
||||||
|
|
||||||
|
import { Uint8ArrayExactReadable } from "@yume-chan/struct";
|
||||||
|
|
||||||
import { ScrcpyControlMessageType } from "../../base/index.js";
|
import { ScrcpyControlMessageType } from "../../base/index.js";
|
||||||
|
|
||||||
import { ScrollController, SignedFloat } from "./scroll-controller.js";
|
import { ScrollController, SignedFloat } from "./scroll-controller.js";
|
||||||
|
@ -65,30 +67,27 @@ describe("SignedFloat", () => {
|
||||||
|
|
||||||
dataView.setInt16(0, -0x8000, true);
|
dataView.setInt16(0, -0x8000, true);
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
SignedFloat.deserialize({
|
SignedFloat.deserialize(new Uint8ArrayExactReadable(view), {
|
||||||
runtimeStruct: {} as never,
|
|
||||||
reader: { position: 0, readExactly: () => view },
|
|
||||||
littleEndian: true,
|
littleEndian: true,
|
||||||
|
dependencies: {} as never,
|
||||||
}),
|
}),
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
|
|
||||||
dataView.setInt16(0, 0, true);
|
dataView.setInt16(0, 0, true);
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
SignedFloat.deserialize({
|
SignedFloat.deserialize(new Uint8ArrayExactReadable(view), {
|
||||||
runtimeStruct: {} as never,
|
|
||||||
reader: { position: 0, readExactly: () => view },
|
|
||||||
littleEndian: true,
|
littleEndian: true,
|
||||||
|
dependencies: {} as never,
|
||||||
}),
|
}),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
dataView.setInt16(0, 0x7fff, true);
|
dataView.setInt16(0, 0x7fff, true);
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
SignedFloat.deserialize({
|
SignedFloat.deserialize(new Uint8ArrayExactReadable(view), {
|
||||||
runtimeStruct: {} as never,
|
|
||||||
reader: { position: 0, readExactly: () => view },
|
|
||||||
littleEndian: true,
|
littleEndian: true,
|
||||||
|
dependencies: {} as never,
|
||||||
}),
|
}),
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,27 +1,28 @@
|
||||||
import { getInt16, setInt16 } from "@yume-chan/no-data-view";
|
import { getInt16, setInt16 } from "@yume-chan/no-data-view";
|
||||||
import type { Field, StructInit } from "@yume-chan/struct";
|
import type { Field, StructInit } from "@yume-chan/struct";
|
||||||
import { bipedal, struct, u16, u32, u8 } from "@yume-chan/struct";
|
import { field, struct, u16, u32, u8 } from "@yume-chan/struct";
|
||||||
|
|
||||||
import type { ScrcpyScrollController } from "../../base/index.js";
|
import type { ScrcpyScrollController } from "../../base/index.js";
|
||||||
import { ScrcpyControlMessageType } from "../../base/index.js";
|
import { ScrcpyControlMessageType } from "../../base/index.js";
|
||||||
import type { ScrcpyInjectScrollControlMessage } from "../../latest.js";
|
import type { ScrcpyInjectScrollControlMessage } from "../../latest.js";
|
||||||
import { clamp } from "../../utils/index.js";
|
import { clamp } from "../../utils/index.js";
|
||||||
|
|
||||||
export const SignedFloat: Field<number, never, never> = {
|
export const SignedFloat: Field<number, never, never> = field(
|
||||||
size: 2,
|
2,
|
||||||
serialize(value, { buffer, index, littleEndian }) {
|
"byob",
|
||||||
|
(value, { buffer, index, littleEndian }) => {
|
||||||
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/app/src/util/binary.h#L51
|
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/app/src/util/binary.h#L51
|
||||||
value = clamp(value, -1, 1);
|
value = clamp(value, -1, 1);
|
||||||
value = value === 1 ? 0x7fff : value * 0x8000;
|
value = value === 1 ? 0x7fff : value * 0x8000;
|
||||||
setInt16(buffer, index, value, littleEndian);
|
setInt16(buffer, index, value, littleEndian);
|
||||||
},
|
},
|
||||||
deserialize: bipedal(function* (then, { reader, littleEndian }) {
|
function* (then, reader, { littleEndian }) {
|
||||||
const data = yield* then(reader.readExactly(2));
|
const data = yield* then(reader.readExactly(2));
|
||||||
const value = getInt16(data, 0, littleEndian);
|
const value = getInt16(data, 0, littleEndian);
|
||||||
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/server/src/main/java/com/genymobile/scrcpy/Binary.java#L34
|
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/server/src/main/java/com/genymobile/scrcpy/Binary.java#L34
|
||||||
return value === 0x7fff ? 1 : value / 0x8000;
|
return value === 0x7fff ? 1 : value / 0x8000;
|
||||||
}),
|
},
|
||||||
};
|
);
|
||||||
|
|
||||||
export const InjectScrollControlMessage = /* #__PURE__ */ (() =>
|
export const InjectScrollControlMessage = /* #__PURE__ */ (() =>
|
||||||
struct(
|
struct(
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import type { StructLike } from "@yume-chan/struct";
|
import type { StructDeserializer } from "@yume-chan/struct";
|
||||||
|
|
||||||
import { BufferedTransformStream } from "./buffered-transform.js";
|
import { BufferedTransformStream } from "./buffered-transform.js";
|
||||||
|
|
||||||
export class StructDeserializeStream<T> extends BufferedTransformStream<T> {
|
export class StructDeserializeStream<T> extends BufferedTransformStream<T> {
|
||||||
constructor(struct: StructLike<T>) {
|
constructor(struct: StructDeserializer<T>) {
|
||||||
super((stream) => {
|
super((stream) => {
|
||||||
return struct.deserialize(stream) as never;
|
return struct.deserialize(stream) as never;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import type { StructInit, StructLike } from "@yume-chan/struct";
|
import type { StructInit, StructSerializer } from "@yume-chan/struct";
|
||||||
|
|
||||||
import { TransformStream } from "./stream.js";
|
import { TransformStream } from "./stream.js";
|
||||||
|
|
||||||
export class StructSerializeStream<
|
export class StructSerializeStream<
|
||||||
T extends StructLike<unknown>,
|
T extends StructSerializer<unknown>,
|
||||||
> extends TransformStream<StructInit<T>, Uint8Array> {
|
> extends TransformStream<StructInit<T>, Uint8Array> {
|
||||||
constructor(struct: T) {
|
constructor(struct: T) {
|
||||||
super({
|
super({
|
||||||
|
|
|
@ -20,18 +20,22 @@ function advance<T>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type BipedalGenerator<This, T, A extends unknown[]> = (
|
||||||
|
this: This,
|
||||||
|
then: <U>(value: MaybePromiseLike<U>) => Iterable<unknown, U, unknown>,
|
||||||
|
...args: A
|
||||||
|
) => Generator<unknown, T, unknown>;
|
||||||
|
|
||||||
|
/* #__NO_SIDE_EFFECTS__ */
|
||||||
export function bipedal<This, T, A extends unknown[]>(
|
export function bipedal<This, T, A extends unknown[]>(
|
||||||
fn: (
|
fn: BipedalGenerator<This, T, A>,
|
||||||
this: This,
|
bindThis?: This,
|
||||||
then: <U>(value: U | PromiseLike<U>) => Iterable<unknown, U, unknown>,
|
|
||||||
...args: A
|
|
||||||
) => Generator<unknown, T, unknown>,
|
|
||||||
): { (this: This, ...args: A): MaybePromiseLike<T> } {
|
): { (this: This, ...args: A): MaybePromiseLike<T> } {
|
||||||
return function (this: This, ...args: A) {
|
function result(this: This, ...args: A): MaybePromiseLike<T> {
|
||||||
const iterator = fn.call(
|
const iterator = fn.call(
|
||||||
this,
|
this,
|
||||||
function* <U>(
|
function* <U>(
|
||||||
value: U | PromiseLike<U>,
|
value: MaybePromiseLike<U>,
|
||||||
): Generator<
|
): Generator<
|
||||||
PromiseLike<U>,
|
PromiseLike<U>,
|
||||||
U,
|
U,
|
||||||
|
@ -51,5 +55,11 @@ export function bipedal<This, T, A extends unknown[]>(
|
||||||
...args,
|
...args,
|
||||||
) as never;
|
) as never;
|
||||||
return advance(iterator, undefined);
|
return advance(iterator, undefined);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
if (bindThis) {
|
||||||
|
return result.bind(bindThis);
|
||||||
|
} else {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { bipedal } from "./bipedal.js";
|
import type { Field } from "./field/index.js";
|
||||||
import type { Field } from "./field.js";
|
import { field } from "./field/index.js";
|
||||||
|
|
||||||
|
export const EmptyUint8Array = new Uint8Array(0);
|
||||||
|
|
||||||
export interface Converter<From, To> {
|
export interface Converter<From, To> {
|
||||||
convert: (value: From) => To;
|
convert: (value: From) => To;
|
||||||
|
@ -11,246 +13,306 @@ export interface BufferLengthConverter<K, KT> extends Converter<KT, number> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BufferLike {
|
export interface BufferLike {
|
||||||
(length: number): Field<Uint8Array, never, never>;
|
(length: number): Field<Uint8Array, never, never, Uint8Array>;
|
||||||
<U>(
|
<U>(
|
||||||
length: number,
|
length: number,
|
||||||
converter: Converter<Uint8Array, U>,
|
converter: Converter<Uint8Array, U>,
|
||||||
): Field<U, never, never>;
|
): Field<U, never, never, Uint8Array>;
|
||||||
|
|
||||||
<K extends string>(lengthField: K): Field<Uint8Array, K, Record<K, number>>;
|
<K extends string>(
|
||||||
|
lengthField: K,
|
||||||
|
): Field<Uint8Array, K, Record<K, number>, Uint8Array>;
|
||||||
<K extends string, U>(
|
<K extends string, U>(
|
||||||
lengthField: K,
|
lengthField: K,
|
||||||
converter: Converter<Uint8Array, U>,
|
converter: Converter<Uint8Array, U>,
|
||||||
): Field<U, K, Record<K, number>>;
|
): Field<U, K, Record<K, number>, Uint8Array>;
|
||||||
|
|
||||||
<K extends string, KT>(
|
<K extends string, KT>(
|
||||||
length: BufferLengthConverter<K, KT>,
|
length: BufferLengthConverter<K, KT>,
|
||||||
): Field<Uint8Array, K, Record<K, KT>>;
|
): Field<Uint8Array, K, Record<K, KT>, Uint8Array>;
|
||||||
<K extends string, KT, U>(
|
<K extends string, KT, U>(
|
||||||
length: BufferLengthConverter<K, KT>,
|
length: BufferLengthConverter<K, KT>,
|
||||||
converter: Converter<Uint8Array, U>,
|
converter: Converter<Uint8Array, U>,
|
||||||
): Field<U, K, Record<K, KT>>;
|
): Field<U, K, Record<K, KT>, Uint8Array>;
|
||||||
|
|
||||||
<KOmitInit extends string, KS>(
|
<LengthOmitInit extends string, LengthDependencies>(
|
||||||
length: Field<number, KOmitInit, KS>,
|
length: Field<number, LengthOmitInit, LengthDependencies, number>,
|
||||||
): Field<Uint8Array, KOmitInit, KS>;
|
): Field<Uint8Array, LengthOmitInit, LengthDependencies, Uint8Array>;
|
||||||
<KOmitInit extends string, KS, U>(
|
<LengthOmitInit extends string, LengthDependencies, U>(
|
||||||
length: Field<number, KOmitInit, KS>,
|
length: Field<number, LengthOmitInit, LengthDependencies, number>,
|
||||||
converter: Converter<Uint8Array, U>,
|
converter: Converter<Uint8Array, U>,
|
||||||
): Field<U, KOmitInit, KS>;
|
): Field<U, LengthOmitInit, LengthDependencies, Uint8Array>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EmptyUint8Array = new Uint8Array(0);
|
function _buffer(length: number): Field<Uint8Array, never, never, Uint8Array>;
|
||||||
|
function _buffer<U>(
|
||||||
|
length: number,
|
||||||
|
converter: Converter<Uint8Array, U>,
|
||||||
|
): Field<U, never, never, Uint8Array>;
|
||||||
|
|
||||||
// Prettier will move the annotation and make it invalid
|
function _buffer<K extends string>(
|
||||||
// prettier-ignore
|
lengthField: K,
|
||||||
export const buffer: BufferLike = (/* #__NO_SIDE_EFFECTS__ */ (
|
): Field<Uint8Array, K, Record<K, number>, Uint8Array>;
|
||||||
|
function _buffer<K extends string, U>(
|
||||||
|
lengthField: K,
|
||||||
|
converter: Converter<Uint8Array, U>,
|
||||||
|
): Field<U, K, Record<K, number>, Uint8Array>;
|
||||||
|
|
||||||
|
function _buffer<K extends string, KT>(
|
||||||
|
length: BufferLengthConverter<K, KT>,
|
||||||
|
): Field<Uint8Array, K, Record<K, KT>, Uint8Array>;
|
||||||
|
function _buffer<K extends string, KT, U>(
|
||||||
|
length: BufferLengthConverter<K, KT>,
|
||||||
|
converter: Converter<Uint8Array, U>,
|
||||||
|
): Field<U, K, Record<K, KT>, Uint8Array>;
|
||||||
|
|
||||||
|
function _buffer<LengthOmitInit extends string, LengthDependencies>(
|
||||||
|
length: Field<number, LengthOmitInit, LengthDependencies, number>,
|
||||||
|
): Field<Uint8Array, LengthOmitInit, LengthDependencies, Uint8Array>;
|
||||||
|
function _buffer<LengthOmitInit extends string, LengthDependencies, U>(
|
||||||
|
length: Field<number, LengthOmitInit, LengthDependencies, number>,
|
||||||
|
converter: Converter<Uint8Array, U>,
|
||||||
|
): Field<U, LengthOmitInit, LengthDependencies, Uint8Array>;
|
||||||
|
|
||||||
|
/* #__NO_SIDE_EFFECTS__ */
|
||||||
|
function _buffer(
|
||||||
lengthOrField:
|
lengthOrField:
|
||||||
| string
|
| string
|
||||||
| number
|
| number
|
||||||
| Field<number, never, unknown>
|
| Field<number, string, unknown, number>
|
||||||
| BufferLengthConverter<string, unknown>,
|
| BufferLengthConverter<string, unknown>,
|
||||||
converter?: Converter<Uint8Array, unknown>,
|
converter?: Converter<Uint8Array, unknown>,
|
||||||
): Field<unknown, string, Record<string, unknown>> => {
|
): Field<unknown, string, Record<string, unknown>, Uint8Array> {
|
||||||
|
// Fixed length
|
||||||
if (typeof lengthOrField === "number") {
|
if (typeof lengthOrField === "number") {
|
||||||
if (converter) {
|
if (converter) {
|
||||||
if (lengthOrField === 0) {
|
if (lengthOrField === 0) {
|
||||||
return {
|
return field(
|
||||||
size: 0,
|
0,
|
||||||
serialize: () => {},
|
"byob",
|
||||||
deserialize: () => converter.convert(EmptyUint8Array),
|
() => {},
|
||||||
};
|
// eslint-disable-next-line require-yield
|
||||||
|
function* () {
|
||||||
|
return converter.convert(EmptyUint8Array);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return field(
|
||||||
size: lengthOrField,
|
0,
|
||||||
serialize: (value, { buffer, index }) => {
|
"byob",
|
||||||
buffer.set(
|
(value, { buffer, index }) => {
|
||||||
converter.back(value).slice(0, lengthOrField),
|
buffer.set(value.slice(0, lengthOrField), index);
|
||||||
index,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
deserialize: bipedal(function* (then, { reader }) {
|
function* (then, reader) {
|
||||||
const array = yield* then(
|
const array = yield* then(
|
||||||
reader.readExactly(lengthOrField),
|
reader.readExactly(lengthOrField),
|
||||||
);
|
);
|
||||||
return converter.convert(array);
|
return converter.convert(array);
|
||||||
}),
|
},
|
||||||
};
|
{
|
||||||
|
init(value) {
|
||||||
|
return converter.back(value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lengthOrField === 0) {
|
if (lengthOrField === 0) {
|
||||||
return {
|
return field(
|
||||||
size: 0,
|
0,
|
||||||
serialize: () => {},
|
"byob",
|
||||||
deserialize: () => EmptyUint8Array,
|
() => {},
|
||||||
};
|
// eslint-disable-next-line require-yield
|
||||||
|
function* () {
|
||||||
|
return EmptyUint8Array;
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return field(
|
||||||
size: lengthOrField,
|
0,
|
||||||
serialize: (value, { buffer, index }) => {
|
"byob",
|
||||||
buffer.set(
|
(value, { buffer, index }) => {
|
||||||
(value as Uint8Array).slice(0, lengthOrField),
|
buffer.set(value.slice(0, lengthOrField), index);
|
||||||
index,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
deserialize: ({ reader }) => reader.readExactly(lengthOrField),
|
// eslint-disable-next-line require-yield
|
||||||
};
|
function* (_then, reader) {
|
||||||
|
return reader.readExactly(lengthOrField);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some Field type might be `function`s
|
// Declare length field
|
||||||
|
// Some field types are `function`s
|
||||||
if (
|
if (
|
||||||
(typeof lengthOrField === "object" ||
|
(typeof lengthOrField === "object" ||
|
||||||
typeof lengthOrField === "function") &&
|
typeof lengthOrField === "function") &&
|
||||||
"serialize" in lengthOrField
|
"serialize" in lengthOrField
|
||||||
) {
|
) {
|
||||||
if (converter) {
|
if (converter) {
|
||||||
return {
|
return field(
|
||||||
size: 0,
|
lengthOrField.size,
|
||||||
dynamicSize(value) {
|
"default",
|
||||||
const array = converter.back(value);
|
(value, { littleEndian }) => {
|
||||||
const lengthFieldSize =
|
if (lengthOrField.type === "default") {
|
||||||
lengthOrField.dynamicSize?.(array.length) ??
|
const lengthBuffer = lengthOrField.serialize(
|
||||||
lengthOrField.size;
|
value.length,
|
||||||
return lengthFieldSize + array.length;
|
{ littleEndian },
|
||||||
|
);
|
||||||
|
const result = new Uint8Array(
|
||||||
|
lengthBuffer.length + value.length,
|
||||||
|
);
|
||||||
|
result.set(lengthBuffer, 0);
|
||||||
|
result.set(value, lengthBuffer.length);
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
const result = new Uint8Array(
|
||||||
|
lengthOrField.size + value.length,
|
||||||
|
);
|
||||||
|
lengthOrField.serialize(value.length, {
|
||||||
|
buffer: result,
|
||||||
|
index: 0,
|
||||||
|
littleEndian,
|
||||||
|
});
|
||||||
|
result.set(value, lengthOrField.size);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
serialize(value, context) {
|
function* (then, reader, 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(
|
const length = yield* then(
|
||||||
lengthOrField.deserialize(context),
|
lengthOrField.deserialize(reader, context),
|
||||||
);
|
|
||||||
const array = yield* then(
|
|
||||||
context.reader.readExactly(length),
|
|
||||||
);
|
);
|
||||||
|
const array = yield* then(reader.readExactly(length));
|
||||||
return converter.convert(array);
|
return converter.convert(array);
|
||||||
}),
|
},
|
||||||
};
|
{
|
||||||
|
init(value) {
|
||||||
|
return converter.back(value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return field(
|
||||||
size: 0,
|
lengthOrField.size,
|
||||||
dynamicSize(value) {
|
"default",
|
||||||
const lengthFieldSize =
|
(value, { littleEndian }) => {
|
||||||
lengthOrField.dynamicSize?.((value as Uint8Array).length) ??
|
if (lengthOrField.type === "default") {
|
||||||
lengthOrField.size;
|
const lengthBuffer = lengthOrField.serialize(value.length, {
|
||||||
return lengthFieldSize + (value as Uint8Array).length;
|
littleEndian,
|
||||||
|
});
|
||||||
|
const result = new Uint8Array(
|
||||||
|
lengthBuffer.length + value.length,
|
||||||
|
);
|
||||||
|
result.set(lengthBuffer, 0);
|
||||||
|
result.set(value, lengthBuffer.length);
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
const result = new Uint8Array(
|
||||||
|
lengthOrField.size + value.length,
|
||||||
|
);
|
||||||
|
lengthOrField.serialize(value.length, {
|
||||||
|
buffer: result,
|
||||||
|
index: 0,
|
||||||
|
littleEndian,
|
||||||
|
});
|
||||||
|
result.set(value, lengthOrField.size);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
serialize(value, context) {
|
function* (then, reader, context) {
|
||||||
const lengthFieldSize =
|
const length = yield* then(
|
||||||
lengthOrField.dynamicSize?.((value as Uint8Array).length) ??
|
lengthOrField.deserialize(reader, context),
|
||||||
lengthOrField.size;
|
|
||||||
lengthOrField.serialize((value as Uint8Array).length, context);
|
|
||||||
context.buffer.set(
|
|
||||||
value as Uint8Array,
|
|
||||||
context.index + lengthFieldSize,
|
|
||||||
);
|
);
|
||||||
|
return yield* then(reader.readExactly(length));
|
||||||
},
|
},
|
||||||
deserialize: bipedal(function* (then, context) {
|
);
|
||||||
const length = yield* then(lengthOrField.deserialize(context));
|
|
||||||
return context.reader.readExactly(length);
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reference exiting length field
|
||||||
if (typeof lengthOrField === "string") {
|
if (typeof lengthOrField === "string") {
|
||||||
if (converter) {
|
if (converter) {
|
||||||
return {
|
return field(
|
||||||
size: 0,
|
0,
|
||||||
preSerialize: (value, runtimeStruct) => {
|
"default",
|
||||||
runtimeStruct[lengthOrField] = converter.back(value).length;
|
(source) => source,
|
||||||
},
|
// eslint-disable-next-line require-yield
|
||||||
dynamicSize: (value) => {
|
function* (_then, reader, { dependencies }) {
|
||||||
return converter.back(value).length;
|
const length = dependencies[lengthOrField] as number;
|
||||||
},
|
|
||||||
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) {
|
if (length === 0) {
|
||||||
return converter.convert(EmptyUint8Array);
|
return EmptyUint8Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = yield* then(reader.readExactly(length));
|
return reader.readExactly(length);
|
||||||
return converter.convert(value);
|
},
|
||||||
}),
|
{
|
||||||
};
|
init(value, dependencies) {
|
||||||
|
const array = converter.back(value);
|
||||||
|
dependencies[lengthOrField] = array.length;
|
||||||
|
return array;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return field(
|
||||||
size: 0,
|
0,
|
||||||
preSerialize: (value, runtimeStruct) => {
|
"default",
|
||||||
runtimeStruct[lengthOrField] = (value as Uint8Array).length;
|
(source) => source,
|
||||||
},
|
// eslint-disable-next-line require-yield
|
||||||
dynamicSize: (value) => {
|
function* (_then, reader, { dependencies }) {
|
||||||
return (value as Uint8Array).length;
|
const length = dependencies[lengthOrField] as number;
|
||||||
},
|
|
||||||
serialize: (value, { buffer, index }) => {
|
|
||||||
buffer.set(value as Uint8Array, index);
|
|
||||||
},
|
|
||||||
deserialize: ({ reader, runtimeStruct }) => {
|
|
||||||
const length = runtimeStruct[lengthOrField] as number;
|
|
||||||
if (length === 0) {
|
if (length === 0) {
|
||||||
return EmptyUint8Array;
|
return EmptyUint8Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
return reader.readExactly(length);
|
return reader.readExactly(length);
|
||||||
},
|
},
|
||||||
};
|
{
|
||||||
|
init(value, dependencies) {
|
||||||
|
dependencies[lengthOrField] = (value as Uint8Array).length;
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reference existing length field + converter
|
||||||
if (converter) {
|
if (converter) {
|
||||||
return {
|
return field(
|
||||||
size: 0,
|
0,
|
||||||
preSerialize: (value, runtimeStruct) => {
|
"default",
|
||||||
const length = converter.back(value).length;
|
(source) => source,
|
||||||
runtimeStruct[lengthOrField.field] = lengthOrField.back(length);
|
// eslint-disable-next-line require-yield
|
||||||
},
|
function* (_then, reader, { dependencies }) {
|
||||||
dynamicSize: (value) => {
|
const rawLength = dependencies[lengthOrField.field];
|
||||||
return converter.back(value).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);
|
const length = lengthOrField.convert(rawLength);
|
||||||
if (length === 0) {
|
if (length === 0) {
|
||||||
return converter.convert(EmptyUint8Array);
|
return EmptyUint8Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = yield* then(reader.readExactly(length));
|
return reader.readExactly(length);
|
||||||
return converter.convert(value);
|
},
|
||||||
}),
|
{
|
||||||
};
|
init(value, dependencies) {
|
||||||
|
const array = converter.back(value);
|
||||||
|
dependencies[lengthOrField.field] = lengthOrField.back(
|
||||||
|
array.length,
|
||||||
|
);
|
||||||
|
return array;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return field(
|
||||||
size: 0,
|
0,
|
||||||
preSerialize: (value, runtimeStruct) => {
|
"default",
|
||||||
runtimeStruct[lengthOrField.field] = lengthOrField.back(
|
(source) => source,
|
||||||
(value as Uint8Array).length,
|
// eslint-disable-next-line require-yield
|
||||||
);
|
function* (_then, reader, { dependencies }) {
|
||||||
},
|
const rawLength = dependencies[lengthOrField.field];
|
||||||
dynamicSize: (value) => {
|
|
||||||
return (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);
|
const length = lengthOrField.convert(rawLength);
|
||||||
if (length === 0) {
|
if (length === 0) {
|
||||||
return EmptyUint8Array;
|
return EmptyUint8Array;
|
||||||
|
@ -258,5 +320,15 @@ export const buffer: BufferLike = (/* #__NO_SIDE_EFFECTS__ */ (
|
||||||
|
|
||||||
return reader.readExactly(length);
|
return reader.readExactly(length);
|
||||||
},
|
},
|
||||||
};
|
{
|
||||||
}) as never;
|
init(value, dependencies) {
|
||||||
|
dependencies[lengthOrField.field] = lengthOrField.back(
|
||||||
|
(value as Uint8Array).length,
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const buffer = _buffer;
|
||||||
|
|
78
libraries/struct/src/concat.ts
Normal file
78
libraries/struct/src/concat.ts
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import type { FieldsValue, Struct, StructFields } from "./struct.js";
|
||||||
|
import { struct } from "./struct.js";
|
||||||
|
|
||||||
|
type UnionToIntersection<U> = (
|
||||||
|
U extends unknown ? (x: U) => void : never
|
||||||
|
) extends (x: infer I) => void
|
||||||
|
? I
|
||||||
|
: never;
|
||||||
|
|
||||||
|
type As<T, U> = T extends infer V extends U ? V : never;
|
||||||
|
|
||||||
|
export type ConcatFields<
|
||||||
|
T extends Struct<
|
||||||
|
StructFields,
|
||||||
|
Record<PropertyKey, unknown> | undefined,
|
||||||
|
unknown
|
||||||
|
>[],
|
||||||
|
> = As<UnionToIntersection<T[number]["fields"]>, StructFields>;
|
||||||
|
|
||||||
|
type ConcatFieldValues<
|
||||||
|
T extends Struct<
|
||||||
|
StructFields,
|
||||||
|
Record<PropertyKey, unknown> | undefined,
|
||||||
|
unknown
|
||||||
|
>[],
|
||||||
|
> = FieldsValue<ConcatFields<T>>;
|
||||||
|
|
||||||
|
type ExtraToUnion<Extra extends Record<PropertyKey, unknown> | undefined> =
|
||||||
|
Extra extends undefined ? never : Extra;
|
||||||
|
|
||||||
|
export type ConcatExtras<
|
||||||
|
T extends Struct<
|
||||||
|
StructFields,
|
||||||
|
Record<PropertyKey, unknown> | undefined,
|
||||||
|
unknown
|
||||||
|
>[],
|
||||||
|
> = As<
|
||||||
|
UnionToIntersection<ExtraToUnion<T[number]["extra"]>>,
|
||||||
|
Record<PropertyKey, unknown>
|
||||||
|
>;
|
||||||
|
|
||||||
|
/* #__NO_SIDE_EFFECTS__ */
|
||||||
|
export function concat<
|
||||||
|
T extends Struct<
|
||||||
|
StructFields,
|
||||||
|
Record<PropertyKey, unknown> | undefined,
|
||||||
|
unknown
|
||||||
|
>[],
|
||||||
|
PostDeserialize = ConcatFieldValues<T> & ConcatExtras<T>,
|
||||||
|
>(
|
||||||
|
options: {
|
||||||
|
littleEndian: boolean;
|
||||||
|
postDeserialize?: (
|
||||||
|
this: ConcatFieldValues<T> & ConcatExtras<T>,
|
||||||
|
value: ConcatFieldValues<T> & ConcatExtras<T>,
|
||||||
|
) => PostDeserialize;
|
||||||
|
},
|
||||||
|
...structs: T
|
||||||
|
): Struct<ConcatFields<T>, ConcatExtras<T>, PostDeserialize> {
|
||||||
|
return struct(
|
||||||
|
structs.reduce(
|
||||||
|
(fields, struct) => Object.assign(fields, struct.fields),
|
||||||
|
{},
|
||||||
|
) as never,
|
||||||
|
{
|
||||||
|
littleEndian: options.littleEndian,
|
||||||
|
postDeserialize: options.postDeserialize,
|
||||||
|
extra: structs.reduce(
|
||||||
|
(extras, struct) =>
|
||||||
|
Object.defineProperties(
|
||||||
|
extras,
|
||||||
|
Object.getOwnPropertyDescriptors(struct.extra),
|
||||||
|
),
|
||||||
|
{},
|
||||||
|
) as never,
|
||||||
|
},
|
||||||
|
) as never;
|
||||||
|
}
|
37
libraries/struct/src/extend.ts
Normal file
37
libraries/struct/src/extend.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import type {
|
||||||
|
ExtraToIntersection,
|
||||||
|
FieldsValue,
|
||||||
|
Struct,
|
||||||
|
StructFields,
|
||||||
|
} from "./struct.js";
|
||||||
|
import { struct } from "./struct.js";
|
||||||
|
|
||||||
|
/* #__NO_SIDE_EFFECTS__ */
|
||||||
|
export function extend<
|
||||||
|
Base extends Struct<
|
||||||
|
StructFields,
|
||||||
|
Record<PropertyKey, unknown> | undefined,
|
||||||
|
unknown
|
||||||
|
>,
|
||||||
|
Fields extends StructFields,
|
||||||
|
PostDeserialize = FieldsValue<Base["fields"] & Fields> &
|
||||||
|
ExtraToIntersection<Base["extra"]>,
|
||||||
|
>(
|
||||||
|
base: Base,
|
||||||
|
fields: Fields,
|
||||||
|
options?: {
|
||||||
|
littleEndian?: boolean | undefined;
|
||||||
|
postDeserialize?: (
|
||||||
|
this: FieldsValue<Base["fields"] & Fields> &
|
||||||
|
ExtraToIntersection<Base["extra"]>,
|
||||||
|
value: FieldsValue<Base["fields"] & Fields> &
|
||||||
|
ExtraToIntersection<Base["extra"]>,
|
||||||
|
) => PostDeserialize;
|
||||||
|
},
|
||||||
|
): Struct<Base["fields"] & Fields, Base["extra"], PostDeserialize> {
|
||||||
|
return struct(Object.assign({}, base.fields, fields), {
|
||||||
|
littleEndian: options?.littleEndian ?? base.littleEndian,
|
||||||
|
extra: base.extra as never,
|
||||||
|
postDeserialize: options?.postDeserialize,
|
||||||
|
}) as never;
|
||||||
|
}
|
|
@ -1,27 +0,0 @@
|
||||||
import type { MaybePromiseLike } from "@yume-chan/async";
|
|
||||||
|
|
||||||
import type { AsyncExactReadable } from "./readable.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>;
|
|
||||||
}
|
|
64
libraries/struct/src/field/factory.ts
Normal file
64
libraries/struct/src/field/factory.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import type { BipedalGenerator } from "../bipedal.js";
|
||||||
|
import { bipedal } from "../bipedal.js";
|
||||||
|
import type { AsyncExactReadable } from "../readable.js";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ByobFieldSerializer,
|
||||||
|
DefaultFieldSerializer,
|
||||||
|
} from "./serialize.js";
|
||||||
|
import { byobFieldSerializer, defaultFieldSerializer } from "./serialize.js";
|
||||||
|
import type { Field, FieldDeserializeContext, FieldOptions } from "./types.js";
|
||||||
|
|
||||||
|
export type MaybeBipedalFieldDeserializer<T, D> = BipedalGenerator<
|
||||||
|
undefined,
|
||||||
|
T,
|
||||||
|
[reader: AsyncExactReadable, context: FieldDeserializeContext<D>]
|
||||||
|
>;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/max-params
|
||||||
|
function _field<T, OmitInit extends string, D, Raw = T>(
|
||||||
|
size: number,
|
||||||
|
type: "default",
|
||||||
|
serialize: DefaultFieldSerializer<Raw>,
|
||||||
|
deserialize: MaybeBipedalFieldDeserializer<T, D>,
|
||||||
|
options?: FieldOptions<T, OmitInit, D, Raw>,
|
||||||
|
): Field<T, OmitInit, D, Raw>;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/max-params
|
||||||
|
function _field<T, OmitInit extends string, D, Raw = T>(
|
||||||
|
size: number,
|
||||||
|
type: "byob",
|
||||||
|
serialize: ByobFieldSerializer<Raw>,
|
||||||
|
deserialize: MaybeBipedalFieldDeserializer<T, D>,
|
||||||
|
options?: FieldOptions<T, OmitInit, D, Raw>,
|
||||||
|
): Field<T, OmitInit, D, Raw>;
|
||||||
|
/* #__NO_SIDE_EFFECTS__ */
|
||||||
|
// eslint-disable-next-line @typescript-eslint/max-params
|
||||||
|
function _field<T, OmitInit extends string, D, Raw = T>(
|
||||||
|
size: number,
|
||||||
|
type: "default" | "byob",
|
||||||
|
serialize: DefaultFieldSerializer<Raw> | ByobFieldSerializer<Raw>,
|
||||||
|
deserialize: MaybeBipedalFieldDeserializer<T, D>,
|
||||||
|
options?: FieldOptions<T, OmitInit, D, Raw>,
|
||||||
|
): Field<T, OmitInit, D, Raw> {
|
||||||
|
const field: Field<T, OmitInit, D, Raw> = {
|
||||||
|
size,
|
||||||
|
type: type,
|
||||||
|
serialize:
|
||||||
|
type === "default"
|
||||||
|
? defaultFieldSerializer(
|
||||||
|
serialize as DefaultFieldSerializer<Raw>,
|
||||||
|
)
|
||||||
|
: byobFieldSerializer(
|
||||||
|
size,
|
||||||
|
serialize as ByobFieldSerializer<Raw>,
|
||||||
|
),
|
||||||
|
deserialize: bipedal(deserialize) as never,
|
||||||
|
omitInit: options?.omitInit,
|
||||||
|
};
|
||||||
|
if (options?.init) {
|
||||||
|
field.init = options.init;
|
||||||
|
}
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const field = _field;
|
3
libraries/struct/src/field/index.ts
Normal file
3
libraries/struct/src/field/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export * from "./factory.js";
|
||||||
|
export * from "./serialize.js";
|
||||||
|
export * from "./types.js";
|
58
libraries/struct/src/field/serialize.ts
Normal file
58
libraries/struct/src/field/serialize.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import type {
|
||||||
|
FieldByobSerializeContext,
|
||||||
|
FieldDefaultSerializeContext,
|
||||||
|
FieldSerializer,
|
||||||
|
} from "./types.js";
|
||||||
|
|
||||||
|
export type DefaultFieldSerializer<T> = (
|
||||||
|
source: T,
|
||||||
|
context: FieldDefaultSerializeContext,
|
||||||
|
) => Uint8Array;
|
||||||
|
|
||||||
|
/* Adapt default field serializer to universal field serializer */
|
||||||
|
export function defaultFieldSerializer<T>(
|
||||||
|
serializer: DefaultFieldSerializer<T>,
|
||||||
|
): FieldSerializer<T>["serialize"] {
|
||||||
|
return (
|
||||||
|
source,
|
||||||
|
context: FieldDefaultSerializeContext | FieldByobSerializeContext,
|
||||||
|
): never => {
|
||||||
|
if ("buffer" in context) {
|
||||||
|
const buffer = serializer(source, context);
|
||||||
|
context.buffer.set(buffer, context.index);
|
||||||
|
return buffer.length as never;
|
||||||
|
} else {
|
||||||
|
return serializer(source, context) as never;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ByobFieldSerializer<T> = (
|
||||||
|
source: T,
|
||||||
|
context: FieldByobSerializeContext & { index: number },
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
/* Adapt byob field serializer to universal field serializer */
|
||||||
|
export function byobFieldSerializer<T>(
|
||||||
|
size: number,
|
||||||
|
serializer: ByobFieldSerializer<T>,
|
||||||
|
): FieldSerializer<T>["serialize"] {
|
||||||
|
return (
|
||||||
|
source,
|
||||||
|
context: FieldDefaultSerializeContext | FieldByobSerializeContext,
|
||||||
|
): never => {
|
||||||
|
if ("buffer" in context) {
|
||||||
|
context.index ??= 0;
|
||||||
|
serializer(source, context as never);
|
||||||
|
return size as never;
|
||||||
|
} else {
|
||||||
|
const buffer = new Uint8Array(size);
|
||||||
|
serializer(source, {
|
||||||
|
buffer,
|
||||||
|
index: 0,
|
||||||
|
littleEndian: context.littleEndian,
|
||||||
|
});
|
||||||
|
return buffer as never;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
48
libraries/struct/src/field/types.ts
Normal file
48
libraries/struct/src/field/types.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import type { MaybePromiseLike } from "@yume-chan/async";
|
||||||
|
|
||||||
|
import type { AsyncExactReadable, ExactReadable } from "../readable.js";
|
||||||
|
|
||||||
|
export interface FieldDefaultSerializeContext {
|
||||||
|
littleEndian: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FieldByobSerializeContext
|
||||||
|
extends FieldDefaultSerializeContext {
|
||||||
|
buffer: Uint8Array;
|
||||||
|
index?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FieldSerializer<T> {
|
||||||
|
type: "default" | "byob";
|
||||||
|
size: number;
|
||||||
|
|
||||||
|
serialize(source: T, context: FieldDefaultSerializeContext): Uint8Array;
|
||||||
|
serialize(source: T, context: FieldByobSerializeContext): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Field<T, OmitInit extends string, D, Raw = T>
|
||||||
|
extends FieldSerializer<Raw>,
|
||||||
|
FieldDeserializer<T, D> {
|
||||||
|
omitInit: OmitInit | undefined;
|
||||||
|
|
||||||
|
init?(value: T, dependencies: D): Raw | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FieldDeserializeContext<D> {
|
||||||
|
littleEndian: boolean;
|
||||||
|
dependencies: D;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FieldDeserializer<T, D> {
|
||||||
|
deserialize(reader: ExactReadable, context: FieldDeserializeContext<D>): T;
|
||||||
|
deserialize(
|
||||||
|
reader: AsyncExactReadable,
|
||||||
|
context: FieldDeserializeContext<D>,
|
||||||
|
): MaybePromiseLike<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FieldOptions<T, OmitInit extends string, D, Raw = T> {
|
||||||
|
omitInit?: OmitInit;
|
||||||
|
dependencies?: D;
|
||||||
|
init?: (value: T, dependencies: D) => Raw | undefined;
|
||||||
|
}
|
|
@ -12,9 +12,12 @@ declare global {
|
||||||
|
|
||||||
export * from "./bipedal.js";
|
export * from "./bipedal.js";
|
||||||
export * from "./buffer.js";
|
export * from "./buffer.js";
|
||||||
export * from "./field.js";
|
export * from "./concat.js";
|
||||||
|
export * from "./extend.js";
|
||||||
|
export * from "./field/index.js";
|
||||||
export * from "./number.js";
|
export * from "./number.js";
|
||||||
export * from "./readable.js";
|
export * from "./readable.js";
|
||||||
export * from "./string.js";
|
export * from "./string.js";
|
||||||
export * from "./struct.js";
|
export * from "./struct.js";
|
||||||
|
export * from "./types.js";
|
||||||
export * from "./utils.js";
|
export * from "./utils.js";
|
||||||
|
|
71
libraries/struct/src/number.spec.ts
Normal file
71
libraries/struct/src/number.spec.ts
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import * as assert from "node:assert";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
|
||||||
|
import type { Field } from "./field/index.js";
|
||||||
|
import { s16, s32, s8, u16, u32, u8 } from "./number.js";
|
||||||
|
|
||||||
|
function testNumber(
|
||||||
|
name: string,
|
||||||
|
field: Field<number, never, never, number>,
|
||||||
|
size: number,
|
||||||
|
signed: boolean,
|
||||||
|
) {
|
||||||
|
describe(name, () => {
|
||||||
|
it("should match size", () => {
|
||||||
|
assert.strictEqual(field.size, size);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("serialize", () => {
|
||||||
|
it("should serialize min value", () => {
|
||||||
|
const minValue = signed ? -(2 ** (size * 8 - 1)) : 0;
|
||||||
|
const buffer = field.serialize(minValue, {
|
||||||
|
littleEndian: true,
|
||||||
|
});
|
||||||
|
const expected = new Uint8Array(size);
|
||||||
|
expected[size - 1] = signed ? 0x80 : 0x00;
|
||||||
|
assert.deepStrictEqual(buffer, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should serialize 0", () => {
|
||||||
|
const buffer = field.serialize(0, {
|
||||||
|
littleEndian: true,
|
||||||
|
});
|
||||||
|
const expected = new Uint8Array(size);
|
||||||
|
assert.deepStrictEqual(buffer, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should serialize 1", () => {
|
||||||
|
const buffer = field.serialize(1, {
|
||||||
|
littleEndian: true,
|
||||||
|
});
|
||||||
|
const expected = new Uint8Array(size);
|
||||||
|
expected[0] = 1;
|
||||||
|
assert.deepStrictEqual(buffer, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should serialize max value", () => {
|
||||||
|
const maxValue = signed
|
||||||
|
? 2 ** (size * 8 - 1) - 1
|
||||||
|
: 2 ** (size * 8) - 1;
|
||||||
|
const buffer = field.serialize(maxValue, {
|
||||||
|
littleEndian: true,
|
||||||
|
});
|
||||||
|
const expected = new Uint8Array(size);
|
||||||
|
for (let i = 0; i < size - 1; i += 1) {
|
||||||
|
expected[i] = 0xff;
|
||||||
|
}
|
||||||
|
expected[size - 1] = signed ? 0x7f : 0xff;
|
||||||
|
assert.deepStrictEqual(buffer, expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("number", () => {
|
||||||
|
testNumber("u8", u8, 1, false);
|
||||||
|
testNumber("s8", s8, 1, true);
|
||||||
|
testNumber("u16", u16, 2, false);
|
||||||
|
testNumber("s16", s16, 2, true);
|
||||||
|
testNumber("u32", u32, 4, false);
|
||||||
|
testNumber("s32", s32, 4, true);
|
||||||
|
});
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { MaybePromiseLike } from "@yume-chan/async";
|
||||||
import {
|
import {
|
||||||
getInt16,
|
getInt16,
|
||||||
getInt32,
|
getInt32,
|
||||||
|
@ -14,110 +15,120 @@ import {
|
||||||
setUint64,
|
setUint64,
|
||||||
} from "@yume-chan/no-data-view";
|
} from "@yume-chan/no-data-view";
|
||||||
|
|
||||||
import { bipedal } from "./bipedal.js";
|
import type {
|
||||||
import type { Field } from "./field.js";
|
Field,
|
||||||
|
FieldByobSerializeContext,
|
||||||
|
FieldDeserializeContext,
|
||||||
|
} from "./field/index.js";
|
||||||
|
import { field } from "./field/index.js";
|
||||||
|
import type { AsyncExactReadable } from "./readable.js";
|
||||||
|
|
||||||
export interface NumberField<T> extends Field<T, never, never> {
|
export interface NumberField<T> extends Field<T, never, never, T> {
|
||||||
<const U>(infer?: U): Field<U, never, never>;
|
<const U>(infer?: U): Field<U, never, never, T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* #__NO_SIDE_EFFECTS__ */
|
/* #__NO_SIDE_EFFECTS__ */
|
||||||
function factory<T>(
|
function number<T>(
|
||||||
size: number,
|
size: number,
|
||||||
serialize: Field<T, never, never>["serialize"],
|
serialize: (
|
||||||
deserialize: Field<T, never, never>["deserialize"],
|
source: T,
|
||||||
|
context: FieldByobSerializeContext & { index: number },
|
||||||
|
) => void,
|
||||||
|
deserialize: (
|
||||||
|
then: <U>(value: MaybePromiseLike<U>) => Iterable<unknown, U, unknown>,
|
||||||
|
reader: AsyncExactReadable,
|
||||||
|
context: FieldDeserializeContext<never>,
|
||||||
|
) => Generator<unknown, T, unknown>,
|
||||||
) {
|
) {
|
||||||
const fn: NumberField<T> = (() => fn) as never;
|
const fn: NumberField<T> = (() => fn) as never;
|
||||||
fn.size = size;
|
Object.assign(fn, field(size, "byob", serialize, deserialize));
|
||||||
fn.serialize = serialize;
|
|
||||||
fn.deserialize = deserialize;
|
|
||||||
return fn;
|
return fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const u8: NumberField<number> = factory(
|
export const u8: NumberField<number> = number(
|
||||||
1,
|
1,
|
||||||
(value, { buffer, index }) => {
|
(value, { buffer, index }) => {
|
||||||
buffer[index] = value;
|
buffer[index] = value;
|
||||||
},
|
},
|
||||||
bipedal(function* (then, { reader }) {
|
function* (then, reader) {
|
||||||
const data = yield* then(reader.readExactly(1));
|
const data = yield* then(reader.readExactly(1));
|
||||||
return data[0]!;
|
return data[0]!;
|
||||||
}),
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const s8: NumberField<number> = factory(
|
export const s8: NumberField<number> = number(
|
||||||
1,
|
1,
|
||||||
(value, { buffer, index }) => {
|
(value, { buffer, index }) => {
|
||||||
buffer[index] = value;
|
buffer[index] = value;
|
||||||
},
|
},
|
||||||
bipedal(function* (then, { reader }) {
|
function* (then, reader) {
|
||||||
const data = yield* then(reader.readExactly(1));
|
const data = yield* then(reader.readExactly(1));
|
||||||
return getInt8(data, 0);
|
return getInt8(data, 0);
|
||||||
}),
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const u16: NumberField<number> = factory(
|
export const u16: NumberField<number> = number(
|
||||||
2,
|
2,
|
||||||
(value, { buffer, index, littleEndian }) => {
|
(value, { buffer, index, littleEndian }) => {
|
||||||
setUint16(buffer, index, value, littleEndian);
|
setUint16(buffer, index, value, littleEndian);
|
||||||
},
|
},
|
||||||
bipedal(function* (then, { reader, littleEndian }) {
|
function* (then, reader, { littleEndian }) {
|
||||||
const data = yield* then(reader.readExactly(2));
|
const data = yield* then(reader.readExactly(2));
|
||||||
return getUint16(data, 0, littleEndian);
|
return getUint16(data, 0, littleEndian);
|
||||||
}),
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const s16: NumberField<number> = factory(
|
export const s16: NumberField<number> = number(
|
||||||
2,
|
2,
|
||||||
(value, { buffer, index, littleEndian }) => {
|
(value, { buffer, index, littleEndian }) => {
|
||||||
setInt16(buffer, index, value, littleEndian);
|
setInt16(buffer, index, value, littleEndian);
|
||||||
},
|
},
|
||||||
bipedal(function* (then, { reader, littleEndian }) {
|
function* (then, reader, { littleEndian }) {
|
||||||
const data = yield* then(reader.readExactly(2));
|
const data = yield* then(reader.readExactly(2));
|
||||||
return getInt16(data, 0, littleEndian);
|
return getInt16(data, 0, littleEndian);
|
||||||
}),
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const u32: NumberField<number> = factory(
|
export const u32: NumberField<number> = number(
|
||||||
4,
|
4,
|
||||||
(value, { buffer, index, littleEndian }) => {
|
(value, { buffer, index, littleEndian }) => {
|
||||||
setUint32(buffer, index, value, littleEndian);
|
setUint32(buffer, index, value, littleEndian);
|
||||||
},
|
},
|
||||||
bipedal(function* (then, { reader, littleEndian }) {
|
function* (then, reader, { littleEndian }) {
|
||||||
const data = yield* then(reader.readExactly(4));
|
const data = yield* then(reader.readExactly(4));
|
||||||
return getUint32(data, 0, littleEndian);
|
return getUint32(data, 0, littleEndian);
|
||||||
}),
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const s32: NumberField<number> = factory(
|
export const s32: NumberField<number> = number(
|
||||||
4,
|
4,
|
||||||
(value, { buffer, index, littleEndian }) => {
|
(value, { buffer, index, littleEndian }) => {
|
||||||
setInt32(buffer, index, value, littleEndian);
|
setInt32(buffer, index, value, littleEndian);
|
||||||
},
|
},
|
||||||
bipedal(function* (then, { reader, littleEndian }) {
|
function* (then, reader, { littleEndian }) {
|
||||||
const data = yield* then(reader.readExactly(4));
|
const data = yield* then(reader.readExactly(4));
|
||||||
return getInt32(data, 0, littleEndian);
|
return getInt32(data, 0, littleEndian);
|
||||||
}),
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const u64: NumberField<bigint> = factory(
|
export const u64: NumberField<bigint> = number(
|
||||||
8,
|
8,
|
||||||
(value, { buffer, index, littleEndian }) => {
|
(value, { buffer, index, littleEndian }) => {
|
||||||
setUint64(buffer, index, value, littleEndian);
|
setUint64(buffer, index, value, littleEndian);
|
||||||
},
|
},
|
||||||
bipedal(function* (then, { reader, littleEndian }) {
|
function* (then, reader, { littleEndian }) {
|
||||||
const data = yield* then(reader.readExactly(8));
|
const data = yield* then(reader.readExactly(8));
|
||||||
return getUint64(data, 0, littleEndian);
|
return getUint64(data, 0, littleEndian);
|
||||||
}),
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const s64: NumberField<bigint> = factory(
|
export const s64: NumberField<bigint> = number(
|
||||||
8,
|
8,
|
||||||
(value, { buffer, index, littleEndian }) => {
|
(value, { buffer, index, littleEndian }) => {
|
||||||
setInt64(buffer, index, value, littleEndian);
|
setInt64(buffer, index, value, littleEndian);
|
||||||
},
|
},
|
||||||
bipedal(function* (then, { reader, littleEndian }) {
|
function* (then, reader, { littleEndian }) {
|
||||||
const data = yield* then(reader.readExactly(8));
|
const data = yield* then(reader.readExactly(8));
|
||||||
return getInt64(data, 0, littleEndian);
|
return getInt64(data, 0, littleEndian);
|
||||||
}),
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -20,6 +20,34 @@ export interface ExactReadable {
|
||||||
readExactly(length: number): Uint8Array;
|
readExactly(length: number): Uint8Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class Uint8ArrayExactReadable implements ExactReadable {
|
||||||
|
#data: Uint8Array;
|
||||||
|
#position: number;
|
||||||
|
|
||||||
|
get position() {
|
||||||
|
return this.#position;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(data: Uint8Array) {
|
||||||
|
this.#data = data;
|
||||||
|
this.#position = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
readExactly(length: number): Uint8Array {
|
||||||
|
if (this.#position + length > this.#data.length) {
|
||||||
|
throw new ExactReadableEndedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = this.#data.subarray(
|
||||||
|
this.#position,
|
||||||
|
this.#position + length,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.#position += length;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface AsyncExactReadable {
|
export interface AsyncExactReadable {
|
||||||
readonly position: number;
|
readonly position: number;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { BufferLengthConverter } from "./buffer.js";
|
import type { BufferLengthConverter } from "./buffer.js";
|
||||||
import { buffer } from "./buffer.js";
|
import { buffer } from "./buffer.js";
|
||||||
import type { Field } from "./field.js";
|
import type { Field } from "./field/index.js";
|
||||||
import { decodeUtf8, encodeUtf8 } from "./utils.js";
|
import { decodeUtf8, encodeUtf8 } from "./utils.js";
|
||||||
|
|
||||||
export interface String {
|
export interface String {
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import * as assert from "node:assert";
|
import * as assert from "node:assert";
|
||||||
import { describe, it } from "node:test";
|
import { describe, it } from "node:test";
|
||||||
|
|
||||||
import { u8 } from "./number.js";
|
import { u16, u8 } from "./number.js";
|
||||||
|
import { Uint8ArrayExactReadable } from "./readable.js";
|
||||||
import { struct } from "./struct.js";
|
import { struct } from "./struct.js";
|
||||||
|
|
||||||
describe("Struct", () => {
|
describe("Struct", () => {
|
||||||
|
@ -9,4 +10,24 @@ describe("Struct", () => {
|
||||||
const A = struct({ id: u8 }, { littleEndian: true });
|
const A = struct({ id: u8 }, { littleEndian: true });
|
||||||
assert.deepStrictEqual(A.serialize({ id: 10 }), new Uint8Array([10]));
|
assert.deepStrictEqual(A.serialize({ id: 10 }), new Uint8Array([10]));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("use struct as field", () => {
|
||||||
|
const B = struct(
|
||||||
|
{ foo: struct({ bar: u8 }, { littleEndian: true }), baz: u16 },
|
||||||
|
{ littleEndian: true },
|
||||||
|
);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
B.serialize({ foo: { bar: 10 }, baz: 20 }),
|
||||||
|
new Uint8Array([10, 20, 0]),
|
||||||
|
);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
B.deserialize(
|
||||||
|
new Uint8ArrayExactReadable(new Uint8Array([10, 20, 0])),
|
||||||
|
),
|
||||||
|
{
|
||||||
|
foo: { bar: 10 },
|
||||||
|
baz: 20,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,37 +1,46 @@
|
||||||
import type { MaybePromiseLike } from "@yume-chan/async";
|
|
||||||
|
|
||||||
import { bipedal } from "./bipedal.js";
|
import { bipedal } from "./bipedal.js";
|
||||||
import type { DeserializeContext, Field, SerializeContext } from "./field.js";
|
import type {
|
||||||
import type { AsyncExactReadable, ExactReadable } from "./readable.js";
|
Field,
|
||||||
|
FieldByobSerializeContext,
|
||||||
|
FieldDefaultSerializeContext,
|
||||||
|
FieldDeserializeContext,
|
||||||
|
FieldDeserializer,
|
||||||
|
} from "./field/index.js";
|
||||||
|
import type { AsyncExactReadable } from "./readable.js";
|
||||||
import { ExactReadableEndedError } from "./readable.js";
|
import { ExactReadableEndedError } from "./readable.js";
|
||||||
|
import type {
|
||||||
|
StructDeserializer,
|
||||||
|
StructSerializeContext,
|
||||||
|
StructSerializer,
|
||||||
|
} from "./types.js";
|
||||||
|
|
||||||
export type FieldsType<
|
export type StructField =
|
||||||
T extends Record<string, Field<unknown, string, unknown>>,
|
| Field<unknown, string, unknown, unknown>
|
||||||
> = {
|
| (StructSerializer<unknown> & StructDeserializer<unknown>);
|
||||||
[K in keyof T]: T[K] extends Field<infer TK, string, unknown> ? TK : never;
|
|
||||||
|
export type StructFields = Record<string, StructField>;
|
||||||
|
|
||||||
|
export type FieldsValue<T extends StructFields> = {
|
||||||
|
[K in keyof T]: T[K] extends FieldDeserializer<infer U, unknown>
|
||||||
|
? U
|
||||||
|
: never;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StructInit<
|
export type FieldOmitInit<T extends StructField> =
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
T extends Field<unknown, infer U, unknown, unknown>
|
||||||
T extends Struct<any, any, any>,
|
? string extends U
|
||||||
> = Omit<
|
? never
|
||||||
FieldsType<T["fields"]>,
|
: U
|
||||||
{
|
: never;
|
||||||
[K in keyof T["fields"]]: T["fields"][K] extends Field<
|
|
||||||
unknown,
|
|
||||||
infer U,
|
|
||||||
unknown
|
|
||||||
>
|
|
||||||
? U
|
|
||||||
: never;
|
|
||||||
}[keyof T["fields"]]
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type StructValue<
|
export type FieldsOmitInits<T extends StructFields> = {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
[K in keyof T]: FieldOmitInit<T[K]>;
|
||||||
T extends Struct<any, any, any>,
|
}[keyof T];
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
> = T extends Struct<any, any, infer P> ? P : never;
|
export type FieldsInit<T extends StructFields> = Omit<
|
||||||
|
FieldsValue<T>,
|
||||||
|
FieldsOmitInits<T>
|
||||||
|
>;
|
||||||
|
|
||||||
export class StructDeserializeError extends Error {
|
export class StructDeserializeError extends Error {
|
||||||
constructor(message: string) {
|
constructor(message: string) {
|
||||||
|
@ -53,92 +62,125 @@ export class StructEmptyError extends StructDeserializeError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
export type ExtraToIntersection<
|
||||||
export type StructLike<T> = Struct<any, any, T>;
|
Extra extends Record<PropertyKey, unknown> | undefined,
|
||||||
|
> = Extra extends undefined ? unknown : Extra;
|
||||||
|
|
||||||
export interface Struct<
|
export interface Struct<
|
||||||
T extends Record<string, Field<unknown, string, Partial<FieldsType<T>>>>,
|
Fields extends StructFields,
|
||||||
Extra extends Record<PropertyKey, unknown> | undefined = undefined,
|
Extra extends Record<PropertyKey, unknown> | undefined = undefined,
|
||||||
PostDeserialize = FieldsType<T> & Extra,
|
PostDeserialize = FieldsValue<Fields> & Extra,
|
||||||
> {
|
> extends StructSerializer<FieldsInit<Fields>>,
|
||||||
fields: T;
|
StructDeserializer<PostDeserialize> {
|
||||||
size: number;
|
littleEndian: boolean;
|
||||||
|
fields: Fields;
|
||||||
extra: Extra;
|
extra: Extra;
|
||||||
|
|
||||||
serialize(runtimeStruct: StructInit<this>): Uint8Array;
|
|
||||||
serialize(runtimeStruct: StructInit<this>, buffer: Uint8Array): number;
|
|
||||||
|
|
||||||
deserialize(reader: ExactReadable): PostDeserialize;
|
|
||||||
deserialize(reader: AsyncExactReadable): MaybePromiseLike<PostDeserialize>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* #__NO_SIDE_EFFECTS__ */
|
/* #__NO_SIDE_EFFECTS__ */
|
||||||
export function struct<
|
export function struct<
|
||||||
T extends Record<string, Field<unknown, string, Partial<FieldsType<T>>>>,
|
Fields extends Record<
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
string,
|
||||||
Extra extends Record<PropertyKey, unknown> = {},
|
| Field<unknown, string, Partial<FieldsValue<Fields>>, unknown>
|
||||||
PostDeserialize = FieldsType<T> & Extra,
|
| (StructSerializer<unknown> & StructDeserializer<unknown>)
|
||||||
|
>,
|
||||||
|
Extra extends Record<PropertyKey, unknown> | undefined = undefined,
|
||||||
|
PostDeserialize = FieldsValue<Fields> & ExtraToIntersection<Extra>,
|
||||||
>(
|
>(
|
||||||
fields: T,
|
fields: Fields,
|
||||||
options: {
|
options: {
|
||||||
littleEndian?: boolean;
|
littleEndian: boolean;
|
||||||
extra?: Extra & ThisType<FieldsType<T>>;
|
extra?: (Extra & ThisType<FieldsValue<Fields>>) | undefined;
|
||||||
postDeserialize?: (
|
postDeserialize?:
|
||||||
this: FieldsType<T> & Extra,
|
| ((
|
||||||
fields: FieldsType<T> & Extra,
|
this: FieldsValue<Fields> & ExtraToIntersection<Extra>,
|
||||||
) => PostDeserialize;
|
value: FieldsValue<Fields> & ExtraToIntersection<Extra>,
|
||||||
|
) => PostDeserialize)
|
||||||
|
| undefined;
|
||||||
},
|
},
|
||||||
): Struct<T, Extra, PostDeserialize> {
|
): Struct<Fields, Extra, PostDeserialize> {
|
||||||
const fieldList = Object.entries(fields);
|
const fieldList = Object.entries(fields);
|
||||||
const size = fieldList.reduce((sum, [, field]) => sum + field.size, 0);
|
const size = fieldList.reduce((sum, [, field]) => sum + field.size, 0);
|
||||||
|
|
||||||
const littleEndian = !!options.littleEndian;
|
const littleEndian = options.littleEndian;
|
||||||
const extra = options.extra
|
const extra = options.extra
|
||||||
? Object.getOwnPropertyDescriptors(options.extra)
|
? Object.getOwnPropertyDescriptors(options.extra)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
littleEndian,
|
||||||
|
type: "byob",
|
||||||
fields,
|
fields,
|
||||||
size,
|
size,
|
||||||
extra: options.extra,
|
extra: options.extra,
|
||||||
serialize(
|
serialize(
|
||||||
runtimeStruct: StructInit<Struct<T, Extra, PostDeserialize>>,
|
source: FieldsInit<Fields>,
|
||||||
buffer?: Uint8Array,
|
bufferOrContext?: Uint8Array | StructSerializeContext,
|
||||||
): Uint8Array | number {
|
): Uint8Array | number {
|
||||||
|
const temp: Record<string, unknown> = { ...source };
|
||||||
|
|
||||||
for (const [key, field] of fieldList) {
|
for (const [key, field] of fieldList) {
|
||||||
if (key in runtimeStruct) {
|
if (key in temp && "init" in field) {
|
||||||
field.preSerialize?.(
|
const result = field.init?.(temp[key], temp as never);
|
||||||
runtimeStruct[key as never],
|
if (result !== undefined) {
|
||||||
runtimeStruct as never,
|
temp[key] = result;
|
||||||
);
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizes = new Array<number>(fieldList.length);
|
||||||
|
const buffers = new Array<Uint8Array | undefined>(fieldList.length);
|
||||||
|
{
|
||||||
|
const context: FieldDefaultSerializeContext = { littleEndian };
|
||||||
|
for (const [index, [key, field]] of fieldList.entries()) {
|
||||||
|
if (field.type === "byob") {
|
||||||
|
sizes[index] = field.size;
|
||||||
|
} else {
|
||||||
|
buffers[index] = field.serialize(temp[key], context);
|
||||||
|
sizes[index] = buffers[index].length;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sizes = fieldList.map(
|
|
||||||
([key, field]) =>
|
|
||||||
field.dynamicSize?.(runtimeStruct[key as never]) ??
|
|
||||||
field.size,
|
|
||||||
);
|
|
||||||
const size = sizes.reduce((sum, size) => sum + size, 0);
|
const size = sizes.reduce((sum, size) => sum + size, 0);
|
||||||
|
|
||||||
let externalBuffer = false;
|
let externalBuffer: boolean;
|
||||||
if (buffer) {
|
let buffer: Uint8Array;
|
||||||
if (buffer.length < size) {
|
let index: number;
|
||||||
|
if (bufferOrContext instanceof Uint8Array) {
|
||||||
|
if (bufferOrContext.length < size) {
|
||||||
throw new Error("Buffer too small");
|
throw new Error("Buffer too small");
|
||||||
}
|
}
|
||||||
|
|
||||||
externalBuffer = true;
|
externalBuffer = true;
|
||||||
|
buffer = bufferOrContext;
|
||||||
|
index = 0;
|
||||||
|
} else if (
|
||||||
|
typeof bufferOrContext === "object" &&
|
||||||
|
"buffer" in bufferOrContext
|
||||||
|
) {
|
||||||
|
externalBuffer = true;
|
||||||
|
buffer = bufferOrContext.buffer;
|
||||||
|
index = bufferOrContext.index ?? 0;
|
||||||
|
if (buffer.length - index < size) {
|
||||||
|
throw new Error("Buffer too small");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
externalBuffer = false;
|
||||||
buffer = new Uint8Array(size);
|
buffer = new Uint8Array(size);
|
||||||
|
index = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const context: SerializeContext = {
|
const context = {
|
||||||
buffer,
|
buffer,
|
||||||
index: 0,
|
index,
|
||||||
littleEndian,
|
littleEndian,
|
||||||
};
|
} satisfies FieldByobSerializeContext;
|
||||||
for (const [index, [key, field]] of fieldList.entries()) {
|
for (const [index, [key, field]] of fieldList.entries()) {
|
||||||
field.serialize(runtimeStruct[key as never], context);
|
if (buffers[index]) {
|
||||||
|
buffer.set(buffers[index], context.index);
|
||||||
|
} else {
|
||||||
|
field.serialize(temp[key], context);
|
||||||
|
}
|
||||||
context.index += sizes[index]!;
|
context.index += sizes[index]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,23 +191,24 @@ export function struct<
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deserialize: bipedal(function* (
|
deserialize: bipedal(function* (
|
||||||
this: Struct<T, Extra, PostDeserialize>,
|
this: Struct<Fields, Extra, PostDeserialize>,
|
||||||
then,
|
then,
|
||||||
reader: AsyncExactReadable,
|
reader: AsyncExactReadable,
|
||||||
) {
|
) {
|
||||||
const startPosition = reader.position;
|
const startPosition = reader.position;
|
||||||
|
|
||||||
const runtimeStruct = {} as Record<string, unknown>;
|
const result = {} as Record<string, unknown>;
|
||||||
const context: DeserializeContext<Partial<FieldsType<T>>> = {
|
const context: FieldDeserializeContext<
|
||||||
reader,
|
Partial<FieldsValue<Fields>>
|
||||||
runtimeStruct: runtimeStruct as never,
|
> = {
|
||||||
|
dependencies: result as never,
|
||||||
littleEndian: littleEndian,
|
littleEndian: littleEndian,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const [key, field] of fieldList) {
|
for (const [key, field] of fieldList) {
|
||||||
runtimeStruct[key] = yield* then(
|
result[key] = yield* then(
|
||||||
field.deserialize(context),
|
field.deserialize(reader, context),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -181,16 +224,16 @@ export function struct<
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extra) {
|
if (extra) {
|
||||||
Object.defineProperties(runtimeStruct, extra);
|
Object.defineProperties(result, extra);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.postDeserialize) {
|
if (options.postDeserialize) {
|
||||||
return options.postDeserialize.call(
|
return options.postDeserialize.call(
|
||||||
runtimeStruct as never,
|
result as never,
|
||||||
runtimeStruct as never,
|
result as never,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return runtimeStruct;
|
return result;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
} as never;
|
} as never;
|
||||||
|
|
37
libraries/struct/src/types.ts
Normal file
37
libraries/struct/src/types.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import type { MaybePromiseLike } from "@yume-chan/async";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
FieldByobSerializeContext,
|
||||||
|
FieldDeserializer,
|
||||||
|
FieldSerializer,
|
||||||
|
} from "./field/index.js";
|
||||||
|
import type { AsyncExactReadable, ExactReadable } from "./readable.js";
|
||||||
|
|
||||||
|
export type StructSerializeContext = Omit<
|
||||||
|
FieldByobSerializeContext,
|
||||||
|
"littleEndian"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface StructSerializer<T> extends FieldSerializer<T> {
|
||||||
|
type: "byob";
|
||||||
|
size: number;
|
||||||
|
|
||||||
|
serialize(source: T): Uint8Array;
|
||||||
|
serialize(source: T, buffer: Uint8Array): number;
|
||||||
|
serialize(source: T, context: StructSerializeContext): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StructInit<T extends StructSerializer<unknown>> =
|
||||||
|
T extends StructSerializer<infer U> ? U : never;
|
||||||
|
|
||||||
|
export interface StructDeserializer<T> extends FieldDeserializer<T, never> {
|
||||||
|
size: number;
|
||||||
|
|
||||||
|
deserialize(reader: ExactReadable): T;
|
||||||
|
deserialize(reader: AsyncExactReadable): MaybePromiseLike<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StructValue<T extends StructDeserializer<unknown>> =
|
||||||
|
T extends StructDeserializer<infer P> ? P : never;
|
||||||
|
|
||||||
|
export type StructLike<T> = StructSerializer<T> & StructDeserializer<T>;
|
|
@ -5,7 +5,8 @@
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "rollup -c rollup.config.ts --configPlugin @rollup/plugin-typescript --watch"
|
"start": "rollup -c rollup.config.ts --configPlugin @rollup/plugin-typescript --watch",
|
||||||
|
"test": "rollup -c rollup.config.ts --configPlugin @rollup/plugin-typescript"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import {
|
import {
|
||||||
bipedal,
|
bipedal,
|
||||||
buffer,
|
buffer,
|
||||||
|
concat,
|
||||||
decodeUtf8,
|
decodeUtf8,
|
||||||
encodeUtf8,
|
encodeUtf8,
|
||||||
|
extend,
|
||||||
s16,
|
s16,
|
||||||
s32,
|
s32,
|
||||||
s64,
|
s64,
|
||||||
|
@ -15,7 +17,7 @@ import {
|
||||||
u8,
|
u8,
|
||||||
} from "@yume-chan/struct";
|
} from "@yume-chan/struct";
|
||||||
|
|
||||||
bipedal(function () {});
|
bipedal(function* () {});
|
||||||
buffer(u8);
|
buffer(u8);
|
||||||
decodeUtf8(new Uint8Array());
|
decodeUtf8(new Uint8Array());
|
||||||
encodeUtf8("");
|
encodeUtf8("");
|
||||||
|
@ -28,7 +30,13 @@ u16(1);
|
||||||
u32(1);
|
u32(1);
|
||||||
u64(1);
|
u64(1);
|
||||||
u8(1);
|
u8(1);
|
||||||
struct({}, {});
|
struct({}, { littleEndian: true });
|
||||||
|
concat(
|
||||||
|
{ littleEndian: true },
|
||||||
|
struct({ a: u8 }, { littleEndian: true }),
|
||||||
|
struct({ b: u8 }, { littleEndian: true }),
|
||||||
|
);
|
||||||
|
extend(struct({ a: u16 }, { littleEndian: true }), { b: buffer(32) });
|
||||||
|
|
||||||
export * from "@yume-chan/scrcpy";
|
export * from "@yume-chan/scrcpy";
|
||||||
export * from "@yume-chan/struct";
|
export * from "@yume-chan/struct";
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue