mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-03 17:59:50 +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,
|
||||
pipeFrom,
|
||||
} from "@yume-chan/stream-extra";
|
||||
import type { ExactReadable } from "@yume-chan/struct";
|
||||
import { EmptyUint8Array } from "@yume-chan/struct";
|
||||
import { EmptyUint8Array, Uint8ArrayExactReadable } from "@yume-chan/struct";
|
||||
|
||||
import { DeviceBusyError as _DeviceBusyError } from "./error.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
|
||||
implements ReadableWritablePair<AdbPacketData, Consumable<AdbPacketInit>>
|
||||
{
|
||||
|
|
|
@ -4,6 +4,7 @@ import { BufferedReadableStream } from "@yume-chan/stream-extra";
|
|||
import {
|
||||
encodeUtf8,
|
||||
ExactReadableEndedError,
|
||||
extend,
|
||||
string,
|
||||
struct,
|
||||
} from "@yume-chan/struct";
|
||||
|
@ -49,11 +50,11 @@ export class AdbReverseNotSupportedError extends AdbReverseError {
|
|||
}
|
||||
}
|
||||
|
||||
const AdbReverseErrorResponse = struct(
|
||||
/* #__PURE__ */ (() => AdbReverseStringResponse.fields)(),
|
||||
const AdbReverseErrorResponse = extend(
|
||||
AdbReverseStringResponse,
|
||||
{},
|
||||
{
|
||||
littleEndian: true,
|
||||
postDeserialize: (value) => {
|
||||
postDeserialize(value) {
|
||||
// https://issuetracker.google.com/issues/37066218
|
||||
// ADB on Android <9 can't create reverse tunnels when connected wirelessly (ADB over Wi-Fi),
|
||||
// and returns this confusing "more than one device/emulator" error.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { AdbSyncResponseId, adbSyncReadResponses } from "./response.js";
|
||||
|
@ -15,25 +15,15 @@ export interface AdbSyncEntry extends AdbSyncStat {
|
|||
name: string;
|
||||
}
|
||||
|
||||
export const AdbSyncEntryResponse = /* #__PURE__ */ (() =>
|
||||
struct(
|
||||
{
|
||||
...AdbSyncLstatResponse.fields,
|
||||
export const AdbSyncEntryResponse = extend(AdbSyncLstatResponse, {
|
||||
name: string(u32),
|
||||
},
|
||||
{ littleEndian: true, extra: AdbSyncLstatResponse.extra },
|
||||
))();
|
||||
});
|
||||
|
||||
export type AdbSyncEntryResponse = StructValue<typeof AdbSyncEntryResponse>;
|
||||
|
||||
export const AdbSyncEntry2Response = /* #__PURE__ */ (() =>
|
||||
struct(
|
||||
{
|
||||
...AdbSyncStatResponse.fields,
|
||||
export const AdbSyncEntry2Response = extend(AdbSyncStatResponse, {
|
||||
name: string(u32),
|
||||
},
|
||||
{ littleEndian: true, extra: AdbSyncStatResponse.extra },
|
||||
))();
|
||||
});
|
||||
|
||||
export type AdbSyncEntry2Response = StructValue<typeof AdbSyncEntry2Response>;
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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";
|
||||
|
||||
function encodeAsciiUnchecked(value: string): Uint8Array {
|
||||
|
@ -49,7 +49,7 @@ export const AdbSyncFailResponse = struct(
|
|||
export async function adbSyncReadResponse<T>(
|
||||
stream: AsyncExactReadable,
|
||||
id: number | string,
|
||||
type: StructLike<T>,
|
||||
type: StructDeserializer<T>,
|
||||
): Promise<T> {
|
||||
if (typeof id === "string") {
|
||||
id = adbSyncEncodeId(id);
|
||||
|
@ -72,7 +72,7 @@ export async function adbSyncReadResponse<T>(
|
|||
export async function* adbSyncReadResponses<T>(
|
||||
stream: AsyncExactReadable,
|
||||
id: number | string,
|
||||
type: StructLike<T>,
|
||||
type: StructDeserializer<T>,
|
||||
): AsyncGenerator<T, void, void> {
|
||||
if (typeof id === "string") {
|
||||
id = adbSyncEncodeId(id);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Consumable, TransformStream } from "@yume-chan/stream-extra";
|
||||
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 = {
|
||||
Auth: 0x48545541, // 'AUTH'
|
||||
|
@ -29,13 +29,9 @@ export type AdbPacketHeader = StructValue<typeof AdbPacketHeader>;
|
|||
|
||||
type AdbPacketHeaderInit = StructInit<typeof AdbPacketHeader>;
|
||||
|
||||
export const AdbPacket = struct(
|
||||
/* #__PURE__ */ (() => ({
|
||||
...AdbPacketHeader.fields,
|
||||
export const AdbPacket = extend(AdbPacketHeader, {
|
||||
payload: buffer("payloadLength"),
|
||||
}))(),
|
||||
{ littleEndian: true },
|
||||
);
|
||||
});
|
||||
|
||||
export type AdbPacket = StructValue<typeof AdbPacket>;
|
||||
|
||||
|
|
|
@ -1,26 +1,27 @@
|
|||
import { getUint16, setUint16 } from "@yume-chan/no-data-view";
|
||||
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 { ScrcpyInjectTouchControlMessage } from "../../latest.js";
|
||||
import { clamp } from "../../utils/index.js";
|
||||
|
||||
export const UnsignedFloat: Field<number, never, never> = {
|
||||
size: 2,
|
||||
serialize(value, { buffer, index, littleEndian }) {
|
||||
export const UnsignedFloat: Field<number, never, never> = field(
|
||||
2,
|
||||
"byob",
|
||||
(source, { buffer, index, littleEndian }) => {
|
||||
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/app/src/util/binary.h#L51
|
||||
value = clamp(value, -1, 1);
|
||||
value = value === 1 ? 0xffff : value * 0x10000;
|
||||
setUint16(buffer, index, value, littleEndian);
|
||||
source = clamp(source, -1, 1);
|
||||
source = source === 1 ? 0xffff : source * 0x10000;
|
||||
setUint16(buffer, index, source, littleEndian);
|
||||
},
|
||||
deserialize: bipedal(function* (then, { reader, littleEndian }) {
|
||||
function* (then, reader, { littleEndian }) {
|
||||
const data = yield* then(reader.readExactly(2));
|
||||
const value = getUint16(data, 0, littleEndian);
|
||||
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/server/src/main/java/com/genymobile/scrcpy/Binary.java#L22
|
||||
return value === 0xffff ? 1 : value / 0x10000;
|
||||
}),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const PointerId = {
|
||||
Mouse: -1n,
|
||||
|
|
|
@ -1,19 +1,15 @@
|
|||
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 { ScrcpyBackOrScreenOnControlMessage } from "../../latest.js";
|
||||
|
||||
import { PrevImpl } from "./prev.js";
|
||||
|
||||
export const BackOrScreenOnControlMessage = /* #__PURE__ */ (() =>
|
||||
struct(
|
||||
{
|
||||
...PrevImpl.BackOrScreenOnControlMessage.fields,
|
||||
action: u8<AndroidKeyEventAction>(),
|
||||
},
|
||||
{ littleEndian: false },
|
||||
))();
|
||||
export const BackOrScreenOnControlMessage = extend(
|
||||
PrevImpl.BackOrScreenOnControlMessage,
|
||||
{ action: u8<AndroidKeyEventAction>() },
|
||||
);
|
||||
|
||||
export type BackOrScreenOnControlMessage = StructInit<
|
||||
typeof BackOrScreenOnControlMessage
|
||||
|
|
|
@ -1,19 +1,15 @@
|
|||
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 { ScrcpyInjectScrollControlMessage } from "../../latest.js";
|
||||
|
||||
import { PrevImpl } from "./prev.js";
|
||||
|
||||
export const InjectScrollControlMessage = /* #__PURE__ */ (() =>
|
||||
struct(
|
||||
{
|
||||
...PrevImpl.InjectScrollControlMessage.fields,
|
||||
buttons: s32,
|
||||
},
|
||||
{ littleEndian: false },
|
||||
))();
|
||||
export const InjectScrollControlMessage = extend(
|
||||
PrevImpl.InjectScrollControlMessage,
|
||||
{ buttons: s32 },
|
||||
);
|
||||
|
||||
export type InjectScrollControlMessage = StructInit<
|
||||
typeof InjectScrollControlMessage
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import * as assert from "node:assert";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import { Uint8ArrayExactReadable } from "@yume-chan/struct";
|
||||
|
||||
import { ScrcpyControlMessageType } from "../../base/index.js";
|
||||
|
||||
import { ScrollController, SignedFloat } from "./scroll-controller.js";
|
||||
|
@ -65,30 +67,27 @@ describe("SignedFloat", () => {
|
|||
|
||||
dataView.setInt16(0, -0x8000, true);
|
||||
assert.strictEqual(
|
||||
SignedFloat.deserialize({
|
||||
runtimeStruct: {} as never,
|
||||
reader: { position: 0, readExactly: () => view },
|
||||
SignedFloat.deserialize(new Uint8ArrayExactReadable(view), {
|
||||
littleEndian: true,
|
||||
dependencies: {} as never,
|
||||
}),
|
||||
-1,
|
||||
);
|
||||
|
||||
dataView.setInt16(0, 0, true);
|
||||
assert.strictEqual(
|
||||
SignedFloat.deserialize({
|
||||
runtimeStruct: {} as never,
|
||||
reader: { position: 0, readExactly: () => view },
|
||||
SignedFloat.deserialize(new Uint8ArrayExactReadable(view), {
|
||||
littleEndian: true,
|
||||
dependencies: {} as never,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
|
||||
dataView.setInt16(0, 0x7fff, true);
|
||||
assert.strictEqual(
|
||||
SignedFloat.deserialize({
|
||||
runtimeStruct: {} as never,
|
||||
reader: { position: 0, readExactly: () => view },
|
||||
SignedFloat.deserialize(new Uint8ArrayExactReadable(view), {
|
||||
littleEndian: true,
|
||||
dependencies: {} as never,
|
||||
}),
|
||||
1,
|
||||
);
|
||||
|
|
|
@ -1,27 +1,28 @@
|
|||
import { getInt16, setInt16 } from "@yume-chan/no-data-view";
|
||||
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 { ScrcpyControlMessageType } from "../../base/index.js";
|
||||
import type { ScrcpyInjectScrollControlMessage } from "../../latest.js";
|
||||
import { clamp } from "../../utils/index.js";
|
||||
|
||||
export const SignedFloat: Field<number, never, never> = {
|
||||
size: 2,
|
||||
serialize(value, { buffer, index, littleEndian }) {
|
||||
export const SignedFloat: Field<number, never, never> = field(
|
||||
2,
|
||||
"byob",
|
||||
(value, { buffer, index, littleEndian }) => {
|
||||
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/app/src/util/binary.h#L51
|
||||
value = clamp(value, -1, 1);
|
||||
value = value === 1 ? 0x7fff : value * 0x8000;
|
||||
setInt16(buffer, index, value, littleEndian);
|
||||
},
|
||||
deserialize: bipedal(function* (then, { reader, littleEndian }) {
|
||||
function* (then, reader, { littleEndian }) {
|
||||
const data = yield* then(reader.readExactly(2));
|
||||
const value = getInt16(data, 0, littleEndian);
|
||||
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/server/src/main/java/com/genymobile/scrcpy/Binary.java#L34
|
||||
return value === 0x7fff ? 1 : value / 0x8000;
|
||||
}),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const InjectScrollControlMessage = /* #__PURE__ */ (() =>
|
||||
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";
|
||||
|
||||
export class StructDeserializeStream<T> extends BufferedTransformStream<T> {
|
||||
constructor(struct: StructLike<T>) {
|
||||
constructor(struct: StructDeserializer<T>) {
|
||||
super((stream) => {
|
||||
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";
|
||||
|
||||
export class StructSerializeStream<
|
||||
T extends StructLike<unknown>,
|
||||
T extends StructSerializer<unknown>,
|
||||
> extends TransformStream<StructInit<T>, Uint8Array> {
|
||||
constructor(struct: T) {
|
||||
super({
|
||||
|
|
|
@ -20,18 +20,22 @@ function advance<T>(
|
|||
}
|
||||
}
|
||||
|
||||
export function bipedal<This, T, A extends unknown[]>(
|
||||
fn: (
|
||||
export type BipedalGenerator<This, T, A extends unknown[]> = (
|
||||
this: This,
|
||||
then: <U>(value: U | PromiseLike<U>) => Iterable<unknown, U, unknown>,
|
||||
then: <U>(value: MaybePromiseLike<U>) => Iterable<unknown, U, unknown>,
|
||||
...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> } {
|
||||
return function (this: This, ...args: A) {
|
||||
function result(this: This, ...args: A): MaybePromiseLike<T> {
|
||||
const iterator = fn.call(
|
||||
this,
|
||||
function* <U>(
|
||||
value: U | PromiseLike<U>,
|
||||
value: MaybePromiseLike<U>,
|
||||
): Generator<
|
||||
PromiseLike<U>,
|
||||
U,
|
||||
|
@ -51,5 +55,11 @@ export function bipedal<This, T, A extends unknown[]>(
|
|||
...args,
|
||||
) as never;
|
||||
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.js";
|
||||
import type { Field } from "./field/index.js";
|
||||
import { field } from "./field/index.js";
|
||||
|
||||
export const EmptyUint8Array = new Uint8Array(0);
|
||||
|
||||
export interface Converter<From, To> {
|
||||
convert: (value: From) => To;
|
||||
|
@ -11,252 +13,322 @@ export interface BufferLengthConverter<K, KT> extends Converter<KT, number> {
|
|||
}
|
||||
|
||||
export interface BufferLike {
|
||||
(length: number): Field<Uint8Array, never, never>;
|
||||
(length: number): Field<Uint8Array, never, never, Uint8Array>;
|
||||
<U>(
|
||||
length: number,
|
||||
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>(
|
||||
lengthField: K,
|
||||
converter: Converter<Uint8Array, U>,
|
||||
): Field<U, K, Record<K, number>>;
|
||||
): Field<U, K, Record<K, number>, Uint8Array>;
|
||||
|
||||
<K extends string, KT>(
|
||||
length: BufferLengthConverter<K, KT>,
|
||||
): Field<Uint8Array, K, Record<K, KT>>;
|
||||
): Field<Uint8Array, K, Record<K, KT>, Uint8Array>;
|
||||
<K extends string, KT, U>(
|
||||
length: BufferLengthConverter<K, KT>,
|
||||
converter: Converter<Uint8Array, U>,
|
||||
): Field<U, K, Record<K, KT>>;
|
||||
): Field<U, K, Record<K, KT>, Uint8Array>;
|
||||
|
||||
<KOmitInit extends string, KS>(
|
||||
length: Field<number, KOmitInit, KS>,
|
||||
): Field<Uint8Array, KOmitInit, KS>;
|
||||
<KOmitInit extends string, KS, U>(
|
||||
length: Field<number, KOmitInit, KS>,
|
||||
<LengthOmitInit extends string, LengthDependencies>(
|
||||
length: Field<number, LengthOmitInit, LengthDependencies, number>,
|
||||
): Field<Uint8Array, LengthOmitInit, LengthDependencies, Uint8Array>;
|
||||
<LengthOmitInit extends string, LengthDependencies, U>(
|
||||
length: Field<number, LengthOmitInit, LengthDependencies, number>,
|
||||
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
|
||||
// prettier-ignore
|
||||
export const buffer: BufferLike = (/* #__NO_SIDE_EFFECTS__ */ (
|
||||
function _buffer<K extends string>(
|
||||
lengthField: K,
|
||||
): 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:
|
||||
| string
|
||||
| number
|
||||
| Field<number, never, unknown>
|
||||
| Field<number, string, unknown, number>
|
||||
| BufferLengthConverter<string, 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 (converter) {
|
||||
if (lengthOrField === 0) {
|
||||
return {
|
||||
size: 0,
|
||||
serialize: () => {},
|
||||
deserialize: () => converter.convert(EmptyUint8Array),
|
||||
};
|
||||
return field(
|
||||
0,
|
||||
"byob",
|
||||
() => {},
|
||||
// eslint-disable-next-line require-yield
|
||||
function* () {
|
||||
return converter.convert(EmptyUint8Array);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
size: lengthOrField,
|
||||
serialize: (value, { buffer, index }) => {
|
||||
buffer.set(
|
||||
converter.back(value).slice(0, lengthOrField),
|
||||
index,
|
||||
);
|
||||
return field(
|
||||
0,
|
||||
"byob",
|
||||
(value, { buffer, index }) => {
|
||||
buffer.set(value.slice(0, lengthOrField), index);
|
||||
},
|
||||
deserialize: bipedal(function* (then, { reader }) {
|
||||
function* (then, reader) {
|
||||
const array = yield* then(
|
||||
reader.readExactly(lengthOrField),
|
||||
);
|
||||
return converter.convert(array);
|
||||
}),
|
||||
};
|
||||
},
|
||||
{
|
||||
init(value) {
|
||||
return converter.back(value);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (lengthOrField === 0) {
|
||||
return {
|
||||
size: 0,
|
||||
serialize: () => {},
|
||||
deserialize: () => EmptyUint8Array,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
size: lengthOrField,
|
||||
serialize: (value, { buffer, index }) => {
|
||||
buffer.set(
|
||||
(value as Uint8Array).slice(0, lengthOrField),
|
||||
index,
|
||||
);
|
||||
return field(
|
||||
0,
|
||||
"byob",
|
||||
() => {},
|
||||
// eslint-disable-next-line require-yield
|
||||
function* () {
|
||||
return EmptyUint8Array;
|
||||
},
|
||||
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 (
|
||||
(typeof lengthOrField === "object" ||
|
||||
typeof lengthOrField === "function") &&
|
||||
"serialize" in lengthOrField
|
||||
) {
|
||||
if (converter) {
|
||||
return {
|
||||
size: 0,
|
||||
dynamicSize(value) {
|
||||
const array = converter.back(value);
|
||||
const lengthFieldSize =
|
||||
lengthOrField.dynamicSize?.(array.length) ??
|
||||
lengthOrField.size;
|
||||
return lengthFieldSize + array.length;
|
||||
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;
|
||||
}
|
||||
},
|
||||
serialize(value, context) {
|
||||
const array = converter.back(value);
|
||||
const lengthFieldSize =
|
||||
lengthOrField.dynamicSize?.(array.length) ??
|
||||
lengthOrField.size;
|
||||
lengthOrField.serialize(array.length, context);
|
||||
context.buffer.set(array, context.index + lengthFieldSize);
|
||||
},
|
||||
deserialize: bipedal(function* (then, context) {
|
||||
function* (then, reader, context) {
|
||||
const length = yield* then(
|
||||
lengthOrField.deserialize(context),
|
||||
);
|
||||
const array = yield* then(
|
||||
context.reader.readExactly(length),
|
||||
lengthOrField.deserialize(reader, context),
|
||||
);
|
||||
const array = yield* then(reader.readExactly(length));
|
||||
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 =
|
||||
lengthOrField.dynamicSize?.((value as Uint8Array).length) ??
|
||||
lengthOrField.size;
|
||||
lengthOrField.serialize((value as Uint8Array).length, context);
|
||||
context.buffer.set(
|
||||
value as Uint8Array,
|
||||
context.index + lengthFieldSize,
|
||||
{
|
||||
init(value) {
|
||||
return converter.back(value);
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
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 (converter) {
|
||||
return {
|
||||
size: 0,
|
||||
preSerialize: (value, runtimeStruct) => {
|
||||
runtimeStruct[lengthOrField] = converter.back(value).length;
|
||||
},
|
||||
dynamicSize: (value) => {
|
||||
return converter.back(value).length;
|
||||
},
|
||||
serialize: (value, { buffer, index }) => {
|
||||
buffer.set(converter.back(value), index);
|
||||
},
|
||||
deserialize: bipedal(function* (
|
||||
then,
|
||||
{ reader, runtimeStruct },
|
||||
) {
|
||||
const length = runtimeStruct[lengthOrField] as number;
|
||||
if (length === 0) {
|
||||
return converter.convert(EmptyUint8Array);
|
||||
}
|
||||
|
||||
const value = yield* then(reader.readExactly(length));
|
||||
return converter.convert(value);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
size: 0,
|
||||
preSerialize: (value, runtimeStruct) => {
|
||||
runtimeStruct[lengthOrField] = (value as Uint8Array).length;
|
||||
},
|
||||
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;
|
||||
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) {
|
||||
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) {
|
||||
return {
|
||||
size: 0,
|
||||
preSerialize: (value, runtimeStruct) => {
|
||||
const length = converter.back(value).length;
|
||||
runtimeStruct[lengthOrField.field] = lengthOrField.back(length);
|
||||
},
|
||||
dynamicSize: (value) => {
|
||||
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];
|
||||
return field(
|
||||
0,
|
||||
"default",
|
||||
(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 converter.convert(EmptyUint8Array);
|
||||
return EmptyUint8Array;
|
||||
}
|
||||
|
||||
const value = yield* then(reader.readExactly(length));
|
||||
return converter.convert(value);
|
||||
}),
|
||||
};
|
||||
return reader.readExactly(length);
|
||||
},
|
||||
{
|
||||
init(value, dependencies) {
|
||||
const array = converter.back(value);
|
||||
dependencies[lengthOrField.field] = lengthOrField.back(
|
||||
array.length,
|
||||
);
|
||||
return array;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
size: 0,
|
||||
preSerialize: (value, runtimeStruct) => {
|
||||
runtimeStruct[lengthOrField.field] = lengthOrField.back(
|
||||
return field(
|
||||
0,
|
||||
"default",
|
||||
(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,
|
||||
);
|
||||
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);
|
||||
},
|
||||
};
|
||||
}) as never;
|
||||
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 "./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 "./readable.js";
|
||||
export * from "./string.js";
|
||||
export * from "./struct.js";
|
||||
export * from "./types.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 {
|
||||
getInt16,
|
||||
getInt32,
|
||||
|
@ -14,110 +15,120 @@ import {
|
|||
setUint64,
|
||||
} from "@yume-chan/no-data-view";
|
||||
|
||||
import { bipedal } from "./bipedal.js";
|
||||
import type { Field } from "./field.js";
|
||||
import type {
|
||||
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> {
|
||||
<const U>(infer?: U): Field<U, never, never>;
|
||||
export interface NumberField<T> extends Field<T, never, never, T> {
|
||||
<const U>(infer?: U): Field<U, never, never, T>;
|
||||
}
|
||||
|
||||
/* #__NO_SIDE_EFFECTS__ */
|
||||
function factory<T>(
|
||||
function number<T>(
|
||||
size: number,
|
||||
serialize: Field<T, never, never>["serialize"],
|
||||
deserialize: Field<T, never, never>["deserialize"],
|
||||
serialize: (
|
||||
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;
|
||||
fn.size = size;
|
||||
fn.serialize = serialize;
|
||||
fn.deserialize = deserialize;
|
||||
Object.assign(fn, field(size, "byob", serialize, deserialize));
|
||||
return fn;
|
||||
}
|
||||
|
||||
export const u8: NumberField<number> = factory(
|
||||
export const u8: NumberField<number> = number(
|
||||
1,
|
||||
(value, { buffer, index }) => {
|
||||
buffer[index] = value;
|
||||
},
|
||||
bipedal(function* (then, { reader }) {
|
||||
function* (then, reader) {
|
||||
const data = yield* then(reader.readExactly(1));
|
||||
return data[0]!;
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
export const s8: NumberField<number> = factory(
|
||||
export const s8: NumberField<number> = number(
|
||||
1,
|
||||
(value, { buffer, index }) => {
|
||||
buffer[index] = value;
|
||||
},
|
||||
bipedal(function* (then, { reader }) {
|
||||
function* (then, reader) {
|
||||
const data = yield* then(reader.readExactly(1));
|
||||
return getInt8(data, 0);
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
export const u16: NumberField<number> = factory(
|
||||
export const u16: NumberField<number> = number(
|
||||
2,
|
||||
(value, { buffer, index, littleEndian }) => {
|
||||
setUint16(buffer, index, value, littleEndian);
|
||||
},
|
||||
bipedal(function* (then, { reader, littleEndian }) {
|
||||
function* (then, reader, { littleEndian }) {
|
||||
const data = yield* then(reader.readExactly(2));
|
||||
return getUint16(data, 0, littleEndian);
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
export const s16: NumberField<number> = factory(
|
||||
export const s16: NumberField<number> = number(
|
||||
2,
|
||||
(value, { buffer, index, littleEndian }) => {
|
||||
setInt16(buffer, index, value, littleEndian);
|
||||
},
|
||||
bipedal(function* (then, { reader, littleEndian }) {
|
||||
function* (then, reader, { littleEndian }) {
|
||||
const data = yield* then(reader.readExactly(2));
|
||||
return getInt16(data, 0, littleEndian);
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
export const u32: NumberField<number> = factory(
|
||||
export const u32: NumberField<number> = number(
|
||||
4,
|
||||
(value, { buffer, index, littleEndian }) => {
|
||||
setUint32(buffer, index, value, littleEndian);
|
||||
},
|
||||
bipedal(function* (then, { reader, littleEndian }) {
|
||||
function* (then, reader, { littleEndian }) {
|
||||
const data = yield* then(reader.readExactly(4));
|
||||
return getUint32(data, 0, littleEndian);
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
export const s32: NumberField<number> = factory(
|
||||
export const s32: NumberField<number> = number(
|
||||
4,
|
||||
(value, { buffer, index, littleEndian }) => {
|
||||
setInt32(buffer, index, value, littleEndian);
|
||||
},
|
||||
bipedal(function* (then, { reader, littleEndian }) {
|
||||
function* (then, reader, { littleEndian }) {
|
||||
const data = yield* then(reader.readExactly(4));
|
||||
return getInt32(data, 0, littleEndian);
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
export const u64: NumberField<bigint> = factory(
|
||||
export const u64: NumberField<bigint> = number(
|
||||
8,
|
||||
(value, { buffer, index, littleEndian }) => {
|
||||
setUint64(buffer, index, value, littleEndian);
|
||||
},
|
||||
bipedal(function* (then, { reader, littleEndian }) {
|
||||
function* (then, reader, { littleEndian }) {
|
||||
const data = yield* then(reader.readExactly(8));
|
||||
return getUint64(data, 0, littleEndian);
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
export const s64: NumberField<bigint> = factory(
|
||||
export const s64: NumberField<bigint> = number(
|
||||
8,
|
||||
(value, { buffer, index, littleEndian }) => {
|
||||
setInt64(buffer, index, value, littleEndian);
|
||||
},
|
||||
bipedal(function* (then, { reader, littleEndian }) {
|
||||
function* (then, reader, { littleEndian }) {
|
||||
const data = yield* then(reader.readExactly(8));
|
||||
return getInt64(data, 0, littleEndian);
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
|
|
@ -20,6 +20,34 @@ export interface ExactReadable {
|
|||
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 {
|
||||
readonly position: number;
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { BufferLengthConverter } 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";
|
||||
|
||||
export interface String {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import * as assert from "node:assert";
|
||||
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";
|
||||
|
||||
describe("Struct", () => {
|
||||
|
@ -9,4 +10,24 @@ describe("Struct", () => {
|
|||
const A = struct({ id: u8 }, { littleEndian: true });
|
||||
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 type { DeserializeContext, Field, SerializeContext } from "./field.js";
|
||||
import type { AsyncExactReadable, ExactReadable } from "./readable.js";
|
||||
import type {
|
||||
Field,
|
||||
FieldByobSerializeContext,
|
||||
FieldDefaultSerializeContext,
|
||||
FieldDeserializeContext,
|
||||
FieldDeserializer,
|
||||
} from "./field/index.js";
|
||||
import type { AsyncExactReadable } from "./readable.js";
|
||||
import { ExactReadableEndedError } from "./readable.js";
|
||||
import type {
|
||||
StructDeserializer,
|
||||
StructSerializeContext,
|
||||
StructSerializer,
|
||||
} from "./types.js";
|
||||
|
||||
export type FieldsType<
|
||||
T extends Record<string, Field<unknown, string, unknown>>,
|
||||
> = {
|
||||
[K in keyof T]: T[K] extends Field<infer TK, string, unknown> ? TK : never;
|
||||
};
|
||||
export type StructField =
|
||||
| Field<unknown, string, unknown, unknown>
|
||||
| (StructSerializer<unknown> & StructDeserializer<unknown>);
|
||||
|
||||
export type StructInit<
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
T extends Struct<any, any, any>,
|
||||
> = Omit<
|
||||
FieldsType<T["fields"]>,
|
||||
{
|
||||
[K in keyof T["fields"]]: T["fields"][K] extends Field<
|
||||
unknown,
|
||||
infer U,
|
||||
unknown
|
||||
>
|
||||
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;
|
||||
}[keyof T["fields"]]
|
||||
>;
|
||||
};
|
||||
|
||||
export type StructValue<
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
T extends Struct<any, any, any>,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
> = T extends Struct<any, any, infer P> ? P : never;
|
||||
export type FieldOmitInit<T extends StructField> =
|
||||
T extends Field<unknown, infer U, unknown, unknown>
|
||||
? string extends U
|
||||
? 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 {
|
||||
constructor(message: string) {
|
||||
|
@ -53,92 +62,125 @@ export class StructEmptyError extends StructDeserializeError {
|
|||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type StructLike<T> = Struct<any, any, T>;
|
||||
export type ExtraToIntersection<
|
||||
Extra extends Record<PropertyKey, unknown> | undefined,
|
||||
> = Extra extends undefined ? unknown : Extra;
|
||||
|
||||
export interface Struct<
|
||||
T extends Record<string, Field<unknown, string, Partial<FieldsType<T>>>>,
|
||||
Fields extends StructFields,
|
||||
Extra extends Record<PropertyKey, unknown> | undefined = undefined,
|
||||
PostDeserialize = FieldsType<T> & Extra,
|
||||
> {
|
||||
fields: T;
|
||||
size: number;
|
||||
PostDeserialize = FieldsValue<Fields> & Extra,
|
||||
> extends StructSerializer<FieldsInit<Fields>>,
|
||||
StructDeserializer<PostDeserialize> {
|
||||
littleEndian: boolean;
|
||||
fields: Fields;
|
||||
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__ */
|
||||
export function struct<
|
||||
T extends Record<string, Field<unknown, string, Partial<FieldsType<T>>>>,
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
Extra extends Record<PropertyKey, unknown> = {},
|
||||
PostDeserialize = FieldsType<T> & Extra,
|
||||
Fields extends Record<
|
||||
string,
|
||||
| Field<unknown, string, Partial<FieldsValue<Fields>>, unknown>
|
||||
| (StructSerializer<unknown> & StructDeserializer<unknown>)
|
||||
>,
|
||||
Extra extends Record<PropertyKey, unknown> | undefined = undefined,
|
||||
PostDeserialize = FieldsValue<Fields> & ExtraToIntersection<Extra>,
|
||||
>(
|
||||
fields: T,
|
||||
fields: Fields,
|
||||
options: {
|
||||
littleEndian?: boolean;
|
||||
extra?: Extra & ThisType<FieldsType<T>>;
|
||||
postDeserialize?: (
|
||||
this: FieldsType<T> & Extra,
|
||||
fields: FieldsType<T> & Extra,
|
||||
) => PostDeserialize;
|
||||
littleEndian: boolean;
|
||||
extra?: (Extra & ThisType<FieldsValue<Fields>>) | undefined;
|
||||
postDeserialize?:
|
||||
| ((
|
||||
this: FieldsValue<Fields> & ExtraToIntersection<Extra>,
|
||||
value: FieldsValue<Fields> & ExtraToIntersection<Extra>,
|
||||
) => PostDeserialize)
|
||||
| undefined;
|
||||
},
|
||||
): Struct<T, Extra, PostDeserialize> {
|
||||
): Struct<Fields, Extra, PostDeserialize> {
|
||||
const fieldList = Object.entries(fields);
|
||||
const size = fieldList.reduce((sum, [, field]) => sum + field.size, 0);
|
||||
|
||||
const littleEndian = !!options.littleEndian;
|
||||
const littleEndian = options.littleEndian;
|
||||
const extra = options.extra
|
||||
? Object.getOwnPropertyDescriptors(options.extra)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
littleEndian,
|
||||
type: "byob",
|
||||
fields,
|
||||
size,
|
||||
extra: options.extra,
|
||||
serialize(
|
||||
runtimeStruct: StructInit<Struct<T, Extra, PostDeserialize>>,
|
||||
buffer?: Uint8Array,
|
||||
source: FieldsInit<Fields>,
|
||||
bufferOrContext?: Uint8Array | StructSerializeContext,
|
||||
): Uint8Array | number {
|
||||
const temp: Record<string, unknown> = { ...source };
|
||||
|
||||
for (const [key, field] of fieldList) {
|
||||
if (key in runtimeStruct) {
|
||||
field.preSerialize?.(
|
||||
runtimeStruct[key as never],
|
||||
runtimeStruct as never,
|
||||
);
|
||||
if (key in temp && "init" in field) {
|
||||
const result = field.init?.(temp[key], temp as never);
|
||||
if (result !== undefined) {
|
||||
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);
|
||||
|
||||
let externalBuffer = false;
|
||||
if (buffer) {
|
||||
if (buffer.length < size) {
|
||||
let externalBuffer: boolean;
|
||||
let buffer: Uint8Array;
|
||||
let index: number;
|
||||
if (bufferOrContext instanceof Uint8Array) {
|
||||
if (bufferOrContext.length < size) {
|
||||
throw new Error("Buffer too small");
|
||||
}
|
||||
|
||||
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 {
|
||||
externalBuffer = false;
|
||||
buffer = new Uint8Array(size);
|
||||
index = 0;
|
||||
}
|
||||
|
||||
const context: SerializeContext = {
|
||||
const context = {
|
||||
buffer,
|
||||
index: 0,
|
||||
index,
|
||||
littleEndian,
|
||||
};
|
||||
} satisfies FieldByobSerializeContext;
|
||||
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]!;
|
||||
}
|
||||
|
||||
|
@ -149,23 +191,24 @@ export function struct<
|
|||
}
|
||||
},
|
||||
deserialize: bipedal(function* (
|
||||
this: Struct<T, Extra, PostDeserialize>,
|
||||
this: Struct<Fields, Extra, PostDeserialize>,
|
||||
then,
|
||||
reader: AsyncExactReadable,
|
||||
) {
|
||||
const startPosition = reader.position;
|
||||
|
||||
const runtimeStruct = {} as Record<string, unknown>;
|
||||
const context: DeserializeContext<Partial<FieldsType<T>>> = {
|
||||
reader,
|
||||
runtimeStruct: runtimeStruct as never,
|
||||
const result = {} as Record<string, unknown>;
|
||||
const context: FieldDeserializeContext<
|
||||
Partial<FieldsValue<Fields>>
|
||||
> = {
|
||||
dependencies: result as never,
|
||||
littleEndian: littleEndian,
|
||||
};
|
||||
|
||||
try {
|
||||
for (const [key, field] of fieldList) {
|
||||
runtimeStruct[key] = yield* then(
|
||||
field.deserialize(context),
|
||||
result[key] = yield* then(
|
||||
field.deserialize(reader, context),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
|
@ -181,16 +224,16 @@ export function struct<
|
|||
}
|
||||
|
||||
if (extra) {
|
||||
Object.defineProperties(runtimeStruct, extra);
|
||||
Object.defineProperties(result, extra);
|
||||
}
|
||||
|
||||
if (options.postDeserialize) {
|
||||
return options.postDeserialize.call(
|
||||
runtimeStruct as never,
|
||||
runtimeStruct as never,
|
||||
result as never,
|
||||
result as never,
|
||||
);
|
||||
} else {
|
||||
return runtimeStruct;
|
||||
return result;
|
||||
}
|
||||
}),
|
||||
} 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": "",
|
||||
"main": "index.js",
|
||||
"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": [],
|
||||
"author": "",
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import {
|
||||
bipedal,
|
||||
buffer,
|
||||
concat,
|
||||
decodeUtf8,
|
||||
encodeUtf8,
|
||||
extend,
|
||||
s16,
|
||||
s32,
|
||||
s64,
|
||||
|
@ -15,7 +17,7 @@ import {
|
|||
u8,
|
||||
} from "@yume-chan/struct";
|
||||
|
||||
bipedal(function () {});
|
||||
bipedal(function* () {});
|
||||
buffer(u8);
|
||||
decodeUtf8(new Uint8Array());
|
||||
encodeUtf8("");
|
||||
|
@ -28,7 +30,13 @@ u16(1);
|
|||
u32(1);
|
||||
u64(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/struct";
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue