feat(struct): allow structs to be used as fields directly (#741)

This commit is contained in:
Simon Chan 2025-04-04 00:26:42 +08:00 committed by GitHub
parent d3019ce738
commit b79df96301
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 956 additions and 429 deletions

View file

@ -0,0 +1,5 @@
---
"@yume-chan/struct": major
---
Refactor struct package to allow `struct`s to be used as `field`

View file

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

View file

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

View file

@ -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(
{
...AdbSyncLstatResponse.fields,
name: string(u32), 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(
{
...AdbSyncStatResponse.fields,
name: string(u32), name: string(u32),
}, });
{ littleEndian: true, extra: AdbSyncStatResponse.extra },
))();
export type AdbSyncEntry2Response = StructValue<typeof AdbSyncEntry2Response>; export type AdbSyncEntry2Response = StructValue<typeof AdbSyncEntry2Response>;

View file

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

View file

@ -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__ */ (() => ({
...AdbPacketHeader.fields,
payload: buffer("payloadLength"), payload: buffer("payloadLength"),
}))(), });
{ littleEndian: true },
);
export type AdbPacket = StructValue<typeof AdbPacket>; export type AdbPacket = StructValue<typeof AdbPacket>;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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({

View file

@ -20,18 +20,22 @@ function advance<T>(
} }
} }
export function bipedal<This, T, A extends unknown[]>( export type BipedalGenerator<This, T, A extends unknown[]> = (
fn: (
this: This, this: This,
then: <U>(value: U | PromiseLike<U>) => Iterable<unknown, U, unknown>, then: <U>(value: MaybePromiseLike<U>) => Iterable<unknown, U, unknown>,
...args: A ...args: A
) => Generator<unknown, T, unknown>, ) => Generator<unknown, T, unknown>;
/* #__NO_SIDE_EFFECTS__ */
export function bipedal<This, T, A extends unknown[]>(
fn: BipedalGenerator<This, T, A>,
bindThis?: This,
): { (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;
}
} }

View file

@ -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,252 +13,322 @@ 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 {
size: lengthOrField,
serialize: (value, { buffer, index }) => {
buffer.set(
(value as Uint8Array).slice(0, lengthOrField),
index,
);
}, },
deserialize: ({ reader }) => reader.readExactly(lengthOrField), );
};
} }
// Some Field type might be `function`s return field(
0,
"byob",
(value, { buffer, index }) => {
buffer.set(value.slice(0, lengthOrField), index);
},
// eslint-disable-next-line require-yield
function* (_then, reader) {
return reader.readExactly(lengthOrField);
},
);
}
// 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);
}),
};
}
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 = init(value) {
lengthOrField.dynamicSize?.((value as Uint8Array).length) ?? return converter.back(value);
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);
}),
};
} }
return field(
lengthOrField.size,
"default",
(value, { littleEndian }) => {
if (lengthOrField.type === "default") {
const lengthBuffer = lengthOrField.serialize(value.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;
}
},
function* (then, reader, context) {
const length = yield* then(
lengthOrField.deserialize(reader, context),
);
return yield* then(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) {
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;
},
dynamicSize: (value) => {
return (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) { if (length === 0) {
return EmptyUint8Array; return EmptyUint8Array;
} }
return reader.readExactly(length); return reader.readExactly(length);
}, },
}; {
init(value, dependencies) {
const array = converter.back(value);
dependencies[lengthOrField] = array.length;
return array;
},
},
);
} }
return field(
0,
"default",
(source) => source,
// eslint-disable-next-line require-yield
function* (_then, reader, { dependencies }) {
const length = dependencies[lengthOrField] as number;
if (length === 0) {
return EmptyUint8Array;
}
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,
// eslint-disable-next-line require-yield
function* (_then, reader, { dependencies }) {
const rawLength = dependencies[lengthOrField.field];
const length = lengthOrField.convert(rawLength);
if (length === 0) {
return EmptyUint8Array;
}
return reader.readExactly(length);
},
{
init(value, dependencies) {
dependencies[lengthOrField.field] = lengthOrField.back(
(value as Uint8Array).length, (value as Uint8Array).length,
); );
return undefined;
}, },
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);
if (length === 0) {
return EmptyUint8Array;
}
return reader.readExactly(length); export const buffer = _buffer;
},
};
}) as never;

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

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

View file

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

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

View file

@ -0,0 +1,3 @@
export * from "./factory.js";
export * from "./serialize.js";
export * from "./types.js";

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

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 StructInit< export type StructFields = Record<string, StructField>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
T extends Struct<any, any, any>, export type FieldsValue<T extends StructFields> = {
> = Omit< [K in keyof T]: T[K] extends FieldDeserializer<infer U, unknown>
FieldsType<T["fields"]>,
{
[K in keyof T["fields"]]: T["fields"][K] extends Field<
unknown,
infer U,
unknown
>
? U ? U
: never; : never;
}[keyof T["fields"]] };
>;
export type StructValue< 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
// eslint-disable-next-line @typescript-eslint/no-explicit-any ? never
> = T extends Struct<any, any, infer P> ? P : never; : U
: never;
export type FieldsOmitInits<T extends StructFields> = {
[K in keyof T]: FieldOmitInit<T[K]>;
}[keyof T];
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;

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

View file

@ -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": "",

View file

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