fix(struct): buffer and struct may not have correct size

This commit is contained in:
Simon Chan 2025-04-04 21:33:14 +08:00
parent f8b40e83b0
commit 0bcb9b804b
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
6 changed files with 97 additions and 30 deletions

View file

@ -0,0 +1,5 @@
---
"@yume-chan/struct": patch
---
Fix `buffer` and `struct` may not have correct size

View file

@ -3,36 +3,41 @@ import { describe, it } from "node:test";
import { buffer } from "./buffer.js"; import { buffer } from "./buffer.js";
import type { ExactReadable } from "./readable.js"; import type { ExactReadable } from "./readable.js";
import { ExactReadableEndedError } from "./readable.js"; import {
ExactReadableEndedError,
Uint8ArrayExactReadable,
} from "./readable.js";
import { struct } from "./struct.js"; import { struct } from "./struct.js";
describe("buffer", () => { describe("buffer", () => {
describe("fixed size", () => { describe("fixed size", () => {
it("should have correct size", () => {
const a = buffer(10);
assert.strictEqual(a.size, 10);
});
it("should deserialize", () => { it("should deserialize", () => {
const A = struct({ value: buffer(10) }, { littleEndian: false }); const a = buffer(10);
const reader: ExactReadable = { const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
position: 0, assert.deepStrictEqual(
readExactly() { a.deserialize(new Uint8ArrayExactReadable(data), {
return new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); dependencies: {} as never,
}, littleEndian: true,
}; }),
assert.deepStrictEqual(A.deserialize(reader), { data,
value: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), );
});
}); });
it("should throw for not enough data", () => { it("should throw for not enough data", () => {
const A = struct({ value: buffer(10) }, { littleEndian: false }); const a = buffer(10);
const reader: ExactReadable = { const data = new Uint8Array([1, 2, 3, 4, 5]);
position: 0,
readExactly() {
(this as { position: number }).position = 5;
throw new ExactReadableEndedError();
},
};
assert.throws( assert.throws(
() => A.deserialize(reader), () =>
/The underlying readable was ended before the struct was fully deserialized/, a.deserialize(new Uint8ArrayExactReadable(data), {
dependencies: {} as never,
littleEndian: true,
}),
/Error: ExactReadable ended/,
); );
}); });

View file

@ -99,7 +99,7 @@ function _buffer(
} }
return field( return field(
0, lengthOrField,
"byob", "byob",
(value, { buffer, index }) => { (value, { buffer, index }) => {
buffer.set(value.slice(0, lengthOrField), index); buffer.set(value.slice(0, lengthOrField), index);
@ -131,7 +131,7 @@ function _buffer(
} }
return field( return field(
0, lengthOrField,
"byob", "byob",
(value, { buffer, index }) => { (value, { buffer, index }) => {
buffer.set(value.slice(0, lengthOrField), index); buffer.set(value.slice(0, lengthOrField), index);

View file

@ -1,8 +1,9 @@
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 { u16, u8 } from "./number.js"; import { u16, u32, u8 } from "./number.js";
import { Uint8ArrayExactReadable } from "./readable.js"; import { Uint8ArrayExactReadable } from "./readable.js";
import { string } from "./string.js";
import { struct } from "./struct.js"; import { struct } from "./struct.js";
describe("Struct", () => { describe("Struct", () => {
@ -30,4 +31,54 @@ describe("Struct", () => {
}, },
); );
}); });
describe("type", () => {
it("should be `byob` when empty", () => {
const A = struct({}, { littleEndian: true });
assert.strictEqual(A.type, "byob");
});
it("should be `byob` if all fields are byob", () => {
const A = struct({ a: u8 }, { littleEndian: true });
assert.strictEqual(A.type, "byob");
const B = struct({ a: u8, b: u16 }, { littleEndian: true });
assert.strictEqual(B.type, "byob");
const C = struct(
{ a: u8, b: u16, c: string(10) },
{ littleEndian: true },
);
assert.strictEqual(C.type, "byob");
});
it("should be `default` if any field is default", () => {
const A = struct({ a: string(u32) }, { littleEndian: true });
assert.strictEqual(A.type, "default");
const B = struct(
{ a: string(u32), b: u32 },
{ littleEndian: true },
);
assert.strictEqual(B.type, "default");
});
});
describe("size", () => {
it("should be 0 when empty", () => {
const A = struct({}, { littleEndian: true });
assert.strictEqual(A.size, 0);
});
it("should be sum of all fields", () => {
const A = struct({ a: u8, b: u16 }, { littleEndian: true });
assert.strictEqual(A.size, 3);
const B = struct({ a: string(10) }, { littleEndian: true });
assert.strictEqual(B.size, 10);
const C = struct({ a: string(u32) }, { littleEndian: true });
assert.strictEqual(C.size, 4);
});
});
}); });

View file

@ -100,7 +100,15 @@ export function struct<
}, },
): Struct<Fields, 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);
let size = 0;
let byob = true;
for (const [, field] of fieldList) {
size += field.size;
if (byob && field.type !== "byob") {
byob = false;
}
}
const littleEndian = options.littleEndian; const littleEndian = options.littleEndian;
const extra = options.extra const extra = options.extra
@ -109,10 +117,11 @@ export function struct<
return { return {
littleEndian, littleEndian,
type: "byob",
fields, fields,
size,
extra: options.extra, extra: options.extra,
type: byob ? "byob" : "default",
size,
serialize( serialize(
source: FieldsInit<Fields>, source: FieldsInit<Fields>,
bufferOrContext?: Uint8Array | StructSerializeContext, bufferOrContext?: Uint8Array | StructSerializeContext,

View file

@ -13,9 +13,6 @@ export type StructSerializeContext = Omit<
>; >;
export interface StructSerializer<T> extends FieldSerializer<T> { export interface StructSerializer<T> extends FieldSerializer<T> {
type: "byob";
size: number;
serialize(source: T): Uint8Array; serialize(source: T): Uint8Array;
serialize(source: T, buffer: Uint8Array): number; serialize(source: T, buffer: Uint8Array): number;
serialize(source: T, context: StructSerializeContext): number; serialize(source: T, context: StructSerializeContext): number;