@nnilky/structo 1.0.7 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/datatypes/containers/array.js +9 -0
- package/dist/datatypes/containers/exhuastiveArray.d.ts +2 -2
- package/dist/datatypes/containers/exhuastiveArray.js +13 -2
- package/dist/datatypes/containers/fastObject.d.ts +2 -2
- package/dist/datatypes/containers/fastObject.js +2 -2
- package/dist/datatypes/containers/list.js +14 -2
- package/dist/datatypes/containers/object.js +8 -8
- package/dist/datatypes/values/string.test.js +3 -0
- package/dist/index.d.ts +1 -1
- package/dist/read.d.ts +2 -6
- package/dist/read.js +9 -1
- package/dist/transforms/encode.d.ts +4 -1
- package/dist/transforms/encode.js +2 -1
- package/dist/transforms/enum.js +11 -8
- package/dist/transforms/index.d.ts +1 -2
- package/dist/transforms/index.js +1 -2
- package/dist/transforms/literal.js +11 -8
- package/dist/transforms/modify.d.ts +19 -1
- package/dist/transforms/modify.js +21 -1
- package/dist/transforms/modify.test.d.ts +1 -0
- package/dist/transforms/modify.test.js +17 -0
- package/dist/transforms/offset.d.ts +2 -0
- package/dist/transforms/offset.js +20 -0
- package/dist/transforms/offset.test.d.ts +1 -0
- package/dist/transforms/offset.test.js +39 -0
- package/dist/transforms/pipe.js +1 -1
- package/dist/transforms/toAscii.js +6 -3
- package/dist/transforms/toBase64.js +4 -1
- package/dist/transforms/toBytes.js +4 -1
- package/dist/transforms/toHex.js +4 -1
- package/dist/transforms/toTypedArray.js +4 -1
- package/dist/types.d.ts +3 -0
- package/dist/utilities/index.d.ts +1 -0
- package/dist/utilities/index.js +1 -0
- package/dist/utilities/lazy.js +4 -4
- package/dist/utilities/reference.d.ts +18 -0
- package/dist/utilities/reference.js +91 -0
- package/dist/utilities/reference.test.d.ts +1 -0
- package/dist/utilities/reference.test.js +63 -0
- package/dist/utilities/remember.d.ts +6 -5
- package/dist/utilities/remember.js +45 -8
- package/dist/utilities/remember.test.js +1 -5
- package/dist/write.js +9 -1
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -21,9 +21,9 @@ const Entity = st.object({
|
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
- Lightweight, base size is <1KB and each datatype is a few hundred bytes
|
|
24
|
-
- Fast
|
|
25
|
-
-
|
|
26
|
-
- Easily
|
|
24
|
+
- Fast show only 1.5-5x slower than ideal implementaiton
|
|
25
|
+
- Designed for both Web/Node.js compatible
|
|
26
|
+
- Easily implement your own serializers
|
|
27
27
|
|
|
28
28
|
Each serializer is completely seperate from the base library, meaning you only pay for what you use.
|
|
29
29
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
*/
|
|
9
9
|
export function array(size, type) {
|
|
10
|
+
const SHOW_ERRORS = size < 4096;
|
|
10
11
|
const { read: readType, write: writeType, size: typeSize } = type;
|
|
11
12
|
return {
|
|
12
13
|
size: type.size ? size * type.size : undefined,
|
|
@@ -16,13 +17,21 @@ export function array(size, type) {
|
|
|
16
17
|
if (typeSize)
|
|
17
18
|
ctx.alloc(size * typeSize);
|
|
18
19
|
for (let i = 0; i < size; i++) {
|
|
20
|
+
if (SHOW_ERRORS)
|
|
21
|
+
ctx.stack.push(`[${i}]`);
|
|
19
22
|
writeType(ctx, value[i]);
|
|
23
|
+
if (SHOW_ERRORS)
|
|
24
|
+
ctx.stack.pop();
|
|
20
25
|
}
|
|
21
26
|
},
|
|
22
27
|
read: (ctx) => {
|
|
23
28
|
const arr = new Array(size);
|
|
24
29
|
for (let i = 0; i < size; i++) {
|
|
30
|
+
if (SHOW_ERRORS)
|
|
31
|
+
ctx.stack.push(`[${i}]`);
|
|
25
32
|
arr[i] = readType(ctx);
|
|
33
|
+
if (SHOW_ERRORS)
|
|
34
|
+
ctx.stack.pop();
|
|
26
35
|
}
|
|
27
36
|
return arr;
|
|
28
37
|
},
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import * as st from "
|
|
1
|
+
import * as st from "../../index";
|
|
2
2
|
/**
|
|
3
3
|
* exhuastiveArray is read until the end of the data
|
|
4
4
|
*
|
|
5
5
|
* ```py
|
|
6
|
-
* exhuastiveArray(st.
|
|
6
|
+
* exhuastiveArray(st.string(st.u8()))
|
|
7
7
|
* ```
|
|
8
8
|
*/
|
|
9
9
|
export declare function exhuastiveArray<T>(type: st.Serializer<T>): st.Serializer<T[]>;
|
|
@@ -1,23 +1,34 @@
|
|
|
1
|
-
import * as st from "
|
|
1
|
+
import * as st from "../../index";
|
|
2
2
|
/**
|
|
3
3
|
* exhuastiveArray is read until the end of the data
|
|
4
4
|
*
|
|
5
5
|
* ```py
|
|
6
|
-
* exhuastiveArray(st.
|
|
6
|
+
* exhuastiveArray(st.string(st.u8()))
|
|
7
7
|
* ```
|
|
8
8
|
*/
|
|
9
9
|
export function exhuastiveArray(type) {
|
|
10
10
|
return {
|
|
11
11
|
read(ctx) {
|
|
12
12
|
let arr = [];
|
|
13
|
+
let i = 0;
|
|
13
14
|
while (ctx.offset < ctx.view.byteLength) {
|
|
15
|
+
const TRACK_STACK = i++ < 4096;
|
|
16
|
+
if (TRACK_STACK)
|
|
17
|
+
ctx.stack.push(`[${arr.length - 1}]`);
|
|
14
18
|
arr.push(type.read(ctx));
|
|
19
|
+
if (TRACK_STACK)
|
|
20
|
+
ctx.stack.pop();
|
|
15
21
|
}
|
|
16
22
|
return arr;
|
|
17
23
|
},
|
|
18
24
|
write(ctx, value) {
|
|
25
|
+
const TRACK_STACK = value.length < 4096;
|
|
19
26
|
for (let i = 0; i < value.length; i++) {
|
|
27
|
+
if (TRACK_STACK)
|
|
28
|
+
ctx.stack.push(`[${i}]`);
|
|
20
29
|
type.write(ctx, value[i]);
|
|
30
|
+
if (TRACK_STACK)
|
|
31
|
+
ctx.stack.pop();
|
|
21
32
|
}
|
|
22
33
|
},
|
|
23
34
|
};
|
|
@@ -3,7 +3,7 @@ type InferObject<T> = T extends Record<string, Serializer<any>> ? {
|
|
|
3
3
|
[Key in keyof T]: InferInput<T[Key]>;
|
|
4
4
|
} : never;
|
|
5
5
|
/**
|
|
6
|
-
* `fastObject` is equivelent to object but it uses eval to improve performance
|
|
6
|
+
* `fastObject` is equivelent to object but it uses eval to improve performance, additionally it omits error reporting
|
|
7
7
|
*
|
|
8
8
|
* This means that is should be avoided in scenarios where CSP is required (so can't be the default), but should be fine for all other cases
|
|
9
9
|
*
|
|
@@ -15,7 +15,7 @@ type InferObject<T> = T extends Record<string, Serializer<any>> ? {
|
|
|
15
15
|
* })
|
|
16
16
|
* ```
|
|
17
17
|
*
|
|
18
|
-
* Note: Using st.lazy affects the performance of fastObject as it prevents discovery of subobjects, avoid when performance is a requirement
|
|
18
|
+
* Note: Using `st.lazy` affects the performance of fastObject as it prevents discovery of subobjects, avoid when performance is a requirement
|
|
19
19
|
*/
|
|
20
20
|
export declare function fastObject<T extends Record<string, Serializer<any>>>(definition: T): Serializer<InferObject<T>>;
|
|
21
21
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const definitionSymbol = Symbol();
|
|
2
2
|
/**
|
|
3
|
-
* `fastObject` is equivelent to object but it uses eval to improve performance
|
|
3
|
+
* `fastObject` is equivelent to object but it uses eval to improve performance, additionally it omits error reporting
|
|
4
4
|
*
|
|
5
5
|
* This means that is should be avoided in scenarios where CSP is required (so can't be the default), but should be fine for all other cases
|
|
6
6
|
*
|
|
@@ -12,7 +12,7 @@ const definitionSymbol = Symbol();
|
|
|
12
12
|
* })
|
|
13
13
|
* ```
|
|
14
14
|
*
|
|
15
|
-
* Note: Using st.lazy affects the performance of fastObject as it prevents discovery of subobjects, avoid when performance is a requirement
|
|
15
|
+
* Note: Using `st.lazy` affects the performance of fastObject as it prevents discovery of subobjects, avoid when performance is a requirement
|
|
16
16
|
*/
|
|
17
17
|
export function fastObject(definition) {
|
|
18
18
|
let serializers = [];
|
|
@@ -12,17 +12,29 @@ export function list(length, type) {
|
|
|
12
12
|
return {
|
|
13
13
|
write: (ctx, value) => {
|
|
14
14
|
length.write(ctx, value.length);
|
|
15
|
-
|
|
15
|
+
const TRACK_STACK = value.length < 4096;
|
|
16
|
+
if (type.size) {
|
|
16
17
|
ctx.alloc(value.length * type.size);
|
|
17
|
-
|
|
18
|
+
}
|
|
19
|
+
for (let i = 0; i < value.length; i++) {
|
|
20
|
+
if (TRACK_STACK)
|
|
21
|
+
ctx.stack.push(`[${i}]`);
|
|
22
|
+
const v = value[i];
|
|
18
23
|
type.write(ctx, v);
|
|
24
|
+
if (TRACK_STACK)
|
|
25
|
+
ctx.stack.pop();
|
|
19
26
|
}
|
|
20
27
|
},
|
|
21
28
|
read: (ctx) => {
|
|
22
29
|
const size = length.read(ctx);
|
|
30
|
+
const TRACK_STACK = size < 4096;
|
|
23
31
|
const arr = new Array(size);
|
|
24
32
|
for (let i = 0; i < size; i++) {
|
|
33
|
+
if (TRACK_STACK)
|
|
34
|
+
ctx.stack.push(`[${i}]`);
|
|
25
35
|
arr[i] = type.read(ctx);
|
|
36
|
+
if (TRACK_STACK)
|
|
37
|
+
ctx.stack.pop();
|
|
26
38
|
}
|
|
27
39
|
return arr;
|
|
28
40
|
},
|
|
@@ -21,19 +21,19 @@ export function object(definition) {
|
|
|
21
21
|
if (size)
|
|
22
22
|
ctx.alloc(size);
|
|
23
23
|
for (let i = 0; i < entires.length; i++) {
|
|
24
|
-
const key = entires[i]
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
catch (e) {
|
|
29
|
-
throw new Error(`Failed to encode key '${key}'`, { cause: e });
|
|
30
|
-
}
|
|
24
|
+
const [key, serializer] = entires[i];
|
|
25
|
+
ctx.stack.push(`.${key}`);
|
|
26
|
+
serializer.write(ctx, value[key]);
|
|
27
|
+
ctx.stack.pop();
|
|
31
28
|
}
|
|
32
29
|
},
|
|
33
30
|
read: (ctx) => {
|
|
34
31
|
const output = new Array(entires.length);
|
|
35
32
|
for (let i = 0; i < entires.length; i++) {
|
|
36
|
-
|
|
33
|
+
const [key, serializer] = entires[i];
|
|
34
|
+
ctx.stack.push(`.${key}`);
|
|
35
|
+
output[i] = [key, serializer.read(ctx)];
|
|
36
|
+
ctx.stack.pop();
|
|
37
37
|
}
|
|
38
38
|
return Object.fromEntries(output);
|
|
39
39
|
},
|
|
@@ -12,6 +12,9 @@ describe("st.string", () => {
|
|
|
12
12
|
//@ts-expect-error, intentional mistake
|
|
13
13
|
encodeFailTest(st.string(st.u8()), 8);
|
|
14
14
|
});
|
|
15
|
+
it("works on long strings", () => {
|
|
16
|
+
encodeTest(st.string(st.u32()), "A".repeat(2 ** 24));
|
|
17
|
+
});
|
|
15
18
|
it("errors on too long strings", () => {
|
|
16
19
|
encodeFailTest(st.string(st.u8()), "A".repeat(256));
|
|
17
20
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type {
|
|
1
|
+
export type { InferOutput as Infer, InferInput, InferOutput, ReaderContext, WriterContext, Serializer, } from "./types";
|
|
2
2
|
export { read, createReaderContext } from "./read";
|
|
3
3
|
export { write, createdWriterContext } from "./write";
|
|
4
4
|
export * from "./datatypes/index";
|
package/dist/read.d.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import type { Serializer } from "./types";
|
|
1
|
+
import type { Serializer, ReaderContext } from "./types";
|
|
2
2
|
export declare function read<TIn, TOut>(serializer: Serializer<TIn, TOut>, buffer: ArrayBuffer): TOut;
|
|
3
|
-
export declare function createReaderContext(buffer: ArrayBuffer):
|
|
4
|
-
offset: number;
|
|
5
|
-
buffer: ArrayBuffer;
|
|
6
|
-
view: DataView<ArrayBuffer>;
|
|
7
|
-
};
|
|
3
|
+
export declare function createReaderContext(buffer: ArrayBuffer): ReaderContext;
|
package/dist/read.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
export function read(serializer, buffer) {
|
|
2
|
-
|
|
2
|
+
const ctx = createReaderContext(buffer);
|
|
3
|
+
try {
|
|
4
|
+
return serializer.read(ctx);
|
|
5
|
+
}
|
|
6
|
+
catch (e) {
|
|
7
|
+
const path = ctx.stack.join("") ?? "root";
|
|
8
|
+
throw new Error(`Deserialization error at serializer${path}`, { cause: e });
|
|
9
|
+
}
|
|
3
10
|
}
|
|
4
11
|
export function createReaderContext(buffer) {
|
|
5
12
|
return {
|
|
13
|
+
stack: [],
|
|
6
14
|
offset: 0,
|
|
7
15
|
buffer,
|
|
8
16
|
view: new DataView(buffer),
|
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
import type { Transform } from "./pipe";
|
|
2
|
-
export declare function encode<TIn, TOut>(
|
|
2
|
+
export declare function encode<TIn, TOut>(options: {
|
|
3
|
+
encode: (value: TOut) => TIn;
|
|
4
|
+
decode: (value: TIn) => TOut;
|
|
5
|
+
}): Transform<TOut, TIn>;
|
package/dist/transforms/enum.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { encode } from "./encode";
|
|
2
2
|
export function enum_(values) {
|
|
3
|
-
return encode(
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
return encode({
|
|
4
|
+
encode: (v) => {
|
|
5
|
+
if (!values.includes(v))
|
|
6
|
+
throw new Error(`Invalid enum variant: ${v}`);
|
|
7
|
+
return v;
|
|
8
|
+
},
|
|
9
|
+
decode: (v) => {
|
|
10
|
+
if (!values.includes(v))
|
|
11
|
+
throw new Error(`Invalid enum variant: ${v}`);
|
|
12
|
+
return v;
|
|
13
|
+
},
|
|
11
14
|
});
|
|
12
15
|
}
|
|
13
16
|
export { enum_ as enum };
|
|
@@ -3,8 +3,7 @@ export { encode } from "./encode";
|
|
|
3
3
|
export { modify } from "./modify";
|
|
4
4
|
export { literal } from "./literal";
|
|
5
5
|
export { enum } from "./enum";
|
|
6
|
-
export {
|
|
7
|
-
export { noAdvance } from "./noAdvance";
|
|
6
|
+
export { offset } from "./offset";
|
|
8
7
|
export { toAscii } from "./toAscii";
|
|
9
8
|
export { toBytes } from "./toBytes";
|
|
10
9
|
export { toHex } from "./toHex";
|
package/dist/transforms/index.js
CHANGED
|
@@ -3,8 +3,7 @@ export { encode } from "./encode";
|
|
|
3
3
|
export { modify } from "./modify";
|
|
4
4
|
export { literal } from "./literal";
|
|
5
5
|
export { enum } from "./enum";
|
|
6
|
-
export {
|
|
7
|
-
export { noAdvance } from "./noAdvance";
|
|
6
|
+
export { offset } from "./offset";
|
|
8
7
|
export { toAscii } from "./toAscii";
|
|
9
8
|
export { toBytes } from "./toBytes";
|
|
10
9
|
export { toHex } from "./toHex";
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { encode } from "./encode";
|
|
2
2
|
export function literal(value) {
|
|
3
|
-
return encode(
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
return encode({
|
|
4
|
+
encode: (v) => {
|
|
5
|
+
if (v !== value)
|
|
6
|
+
throw new Error(`Invalid literal variant: ${v}`);
|
|
7
|
+
return v;
|
|
8
|
+
},
|
|
9
|
+
decode: (v) => {
|
|
10
|
+
if (v !== value)
|
|
11
|
+
throw new Error(`Invalid literal variant: ${v}`);
|
|
12
|
+
return v;
|
|
13
|
+
},
|
|
11
14
|
});
|
|
12
15
|
}
|
|
@@ -1,2 +1,20 @@
|
|
|
1
1
|
import type { Transform } from "./pipe";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* A readonly modification function, useful for sizes or offsets.
|
|
4
|
+
*
|
|
5
|
+
* If you want a writable value, use `st.encode` instead
|
|
6
|
+
*
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* length: length.save(st.u32())
|
|
10
|
+
* st.sizedBytes(
|
|
11
|
+
* st.pipe(
|
|
12
|
+
* length.load(),
|
|
13
|
+
* st.modify(v => v - 8),
|
|
14
|
+
* )
|
|
15
|
+
* ))
|
|
16
|
+
*
|
|
17
|
+
* st.pipe()
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function modify<TIn, TOut>(callback: (value: TIn) => TOut): Transform<TOut, TIn>;
|
|
@@ -1,7 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A readonly modification function, useful for sizes or offsets.
|
|
3
|
+
*
|
|
4
|
+
* If you want a writable value, use `st.encode` instead
|
|
5
|
+
*
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* length: length.save(st.u32())
|
|
9
|
+
* st.sizedBytes(
|
|
10
|
+
* st.pipe(
|
|
11
|
+
* length.load(),
|
|
12
|
+
* st.modify(v => v - 8),
|
|
13
|
+
* )
|
|
14
|
+
* ))
|
|
15
|
+
*
|
|
16
|
+
* st.pipe()
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
1
19
|
export function modify(callback) {
|
|
2
20
|
return (type) => ({
|
|
3
21
|
size: type.size,
|
|
4
22
|
read: (ctx) => callback(type.read(ctx)),
|
|
5
|
-
write: (
|
|
23
|
+
write: () => {
|
|
24
|
+
throw new Error("Cannot write an encoded value");
|
|
25
|
+
},
|
|
6
26
|
});
|
|
7
27
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { encodeFailTest } from "../utils.test";
|
|
3
|
+
import * as st from "../index";
|
|
4
|
+
describe("st.modify", () => {
|
|
5
|
+
it("encodes correctly", () => {
|
|
6
|
+
const data = st.write(st.u32(), 10);
|
|
7
|
+
const output = st.read(st.pipe(st.u32(), st.modify((v) => v + 8)), data);
|
|
8
|
+
expect(output).toBe(18);
|
|
9
|
+
});
|
|
10
|
+
it("cannot be written", () => {
|
|
11
|
+
encodeFailTest(st.pipe(st.u32(), st.modify((v) => v + 8)), 3);
|
|
12
|
+
});
|
|
13
|
+
it("does not modify size", () => {
|
|
14
|
+
const spec = st.pipe(st.u32(), st.modify((v) => v + 8));
|
|
15
|
+
expect(spec.size).toBe(4);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function offset(behaviour, offset) {
|
|
2
|
+
return (type) => ({
|
|
3
|
+
size: type.size,
|
|
4
|
+
read: (ctx) => {
|
|
5
|
+
let start = ctx.offset;
|
|
6
|
+
const offsetValue = typeof offset === "number" ? offset : offset.read(ctx);
|
|
7
|
+
ctx.offset = behaviour === "absolute" ? offsetValue : ctx.offset + offsetValue;
|
|
8
|
+
const value = type.read(ctx);
|
|
9
|
+
ctx.offset = start;
|
|
10
|
+
return value;
|
|
11
|
+
},
|
|
12
|
+
write: (ctx, value) => {
|
|
13
|
+
let start = ctx.offset;
|
|
14
|
+
const offsetValue = typeof offset === "number" ? offset : offset.read(ctx);
|
|
15
|
+
ctx.offset = behaviour === "absolute" ? offsetValue : ctx.offset + offsetValue;
|
|
16
|
+
type.write(ctx, value);
|
|
17
|
+
ctx.offset = start;
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it } from "bun:test";
|
|
2
|
+
import { encodeTest } from "../utils.test";
|
|
3
|
+
import * as st from "../index";
|
|
4
|
+
describe("st.offset(relative)", () => {
|
|
5
|
+
it("encodes correctly with fixed offset", () => {
|
|
6
|
+
const spec = st.object({
|
|
7
|
+
foo: st.u32(),
|
|
8
|
+
bar: st.pipe(st.u32(), st.offset("relative", -4)),
|
|
9
|
+
});
|
|
10
|
+
encodeTest(spec, { foo: 1, bar: 5 }, { foo: 5, bar: 5 });
|
|
11
|
+
});
|
|
12
|
+
it("encodes correctly with read offset", () => {
|
|
13
|
+
const offset = st.createRememberedValue();
|
|
14
|
+
const spec = st.object({
|
|
15
|
+
foo: st.u32(),
|
|
16
|
+
offset: offset.save(st.s16()),
|
|
17
|
+
bar: st.pipe(st.u32(), st.offset("relative", offset.load())),
|
|
18
|
+
});
|
|
19
|
+
encodeTest(spec, { foo: 0, offset: -6, bar: 5 }, { foo: 5, offset: -6, bar: 5 });
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
describe("st.offset(absolute)", () => {
|
|
23
|
+
it("encodes correctly with fixed offset", () => {
|
|
24
|
+
const spec = st.object({
|
|
25
|
+
foo: st.u32(),
|
|
26
|
+
bar: st.pipe(st.u32(), st.offset("absolute", 0)),
|
|
27
|
+
});
|
|
28
|
+
encodeTest(spec, { foo: 0, bar: 5 }, { foo: 5, bar: 5 });
|
|
29
|
+
});
|
|
30
|
+
it("encodes correctly with read offset", () => {
|
|
31
|
+
const offset = st.createRememberedValue();
|
|
32
|
+
const spec = st.object({
|
|
33
|
+
foo: st.u32(),
|
|
34
|
+
offset: offset.save(st.u32()),
|
|
35
|
+
bar: st.pipe(st.u32(), st.offset("absolute", offset.load())),
|
|
36
|
+
});
|
|
37
|
+
encodeTest(spec, { foo: 0, offset: 0, bar: 5 }, { foo: 5, offset: 0, bar: 5 });
|
|
38
|
+
});
|
|
39
|
+
});
|
package/dist/transforms/pipe.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { encode } from "./encode";
|
|
2
2
|
export function toAscii() {
|
|
3
|
-
return encode(
|
|
4
|
-
.map((
|
|
5
|
-
.
|
|
3
|
+
return encode({
|
|
4
|
+
encode: (v) => new Uint8Array(Array.from(v).map((char) => validateAscii(char.charCodeAt(0)))).buffer,
|
|
5
|
+
decode: (v) => Array.from(new Uint8Array(v))
|
|
6
|
+
.map((v) => String.fromCharCode(v))
|
|
7
|
+
.join(""),
|
|
8
|
+
});
|
|
6
9
|
}
|
|
7
10
|
const validateAscii = (v) => {
|
|
8
11
|
if (v >= 0 && v <= 127)
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { encode } from "./encode";
|
|
2
2
|
export function toBase64() {
|
|
3
|
-
return encode(
|
|
3
|
+
return encode({
|
|
4
|
+
encode: (v) => Uint8Array.fromBase64(v).buffer,
|
|
5
|
+
decode: (v) => new Uint8Array(v).toBase64(),
|
|
6
|
+
});
|
|
4
7
|
}
|
|
@@ -12,7 +12,10 @@ import { encode } from "./encode";
|
|
|
12
12
|
* `ArrayBuffer([0, 255, c])` => `[0, 255, ArrayBuffer([0, 255, 0])]`
|
|
13
13
|
*/
|
|
14
14
|
export function toBytes() {
|
|
15
|
-
return encode(
|
|
15
|
+
return encode({
|
|
16
|
+
encode: (v) => new Uint8Array(v.map(validateByte)).buffer,
|
|
17
|
+
decode: (v) => Array.from(new Uint8Array(v)),
|
|
18
|
+
});
|
|
16
19
|
}
|
|
17
20
|
const validateByte = (v) => {
|
|
18
21
|
if (v >= 0 && v <= 255)
|
package/dist/transforms/toHex.js
CHANGED
|
@@ -12,5 +12,8 @@ import { encode } from "./encode";
|
|
|
12
12
|
* `ArrayBuffer([0, 255, 0])` => `00FF00`
|
|
13
13
|
*/
|
|
14
14
|
export function toHex() {
|
|
15
|
-
return encode(
|
|
15
|
+
return encode({
|
|
16
|
+
encode: (v) => Uint8Array.fromHex(v).buffer,
|
|
17
|
+
decode: (v) => new Uint8Array(v).toHex().toUpperCase(),
|
|
18
|
+
});
|
|
16
19
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -3,13 +3,16 @@ export type Serializer<TIn, TOut = TIn> = {
|
|
|
3
3
|
write: (ctx: WriterContext, value: TIn) => void;
|
|
4
4
|
read: (ctx: ReaderContext) => TOut;
|
|
5
5
|
};
|
|
6
|
+
export type SerializationContext = WriterContext | ReaderContext;
|
|
6
7
|
export interface WriterContext {
|
|
8
|
+
stack: string[];
|
|
7
9
|
buffer: ArrayBuffer;
|
|
8
10
|
view: DataView;
|
|
9
11
|
offset: number;
|
|
10
12
|
alloc: (length: number) => void;
|
|
11
13
|
}
|
|
12
14
|
export interface ReaderContext {
|
|
15
|
+
stack: string[];
|
|
13
16
|
buffer: ArrayBuffer;
|
|
14
17
|
view: DataView;
|
|
15
18
|
offset: number;
|
package/dist/utilities/index.js
CHANGED
package/dist/utilities/lazy.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
export function lazy(type) {
|
|
2
2
|
let _size;
|
|
3
3
|
let resolve = () => {
|
|
4
|
-
const t = type();
|
|
5
|
-
_size = t.size ?? undefined;
|
|
6
|
-
serializer.read = t.read;
|
|
7
|
-
serializer.write = t.write;
|
|
8
4
|
resolve = () => { };
|
|
5
|
+
const { size, write, read } = type();
|
|
6
|
+
_size = size ?? undefined;
|
|
7
|
+
serializer.read = read;
|
|
8
|
+
serializer.write = write;
|
|
9
9
|
};
|
|
10
10
|
const serializer = {
|
|
11
11
|
get size() {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Serializer } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* createReference lets you defer writing a value until later, useful for lengths and sizes
|
|
4
|
+
*
|
|
5
|
+
* ---
|
|
6
|
+
* ```
|
|
7
|
+
* const length = st.createReference<number>()
|
|
8
|
+
* st.object({
|
|
9
|
+
* length: length.pointer(st.u32()),
|
|
10
|
+
* type: st.u8(),
|
|
11
|
+
* data: st.sizedBytes(length.deref())
|
|
12
|
+
* })
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare function createReference<T>(): {
|
|
16
|
+
pointer: (serializer: Serializer<T>) => Serializer<T>;
|
|
17
|
+
deref: () => Serializer<T>;
|
|
18
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createReference lets you defer writing a value until later, useful for lengths and sizes
|
|
3
|
+
*
|
|
4
|
+
* ---
|
|
5
|
+
* ```
|
|
6
|
+
* const length = st.createReference<number>()
|
|
7
|
+
* st.object({
|
|
8
|
+
* length: length.pointer(st.u32()),
|
|
9
|
+
* type: st.u8(),
|
|
10
|
+
* data: st.sizedBytes(length.deref())
|
|
11
|
+
* })
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export function createReference() {
|
|
15
|
+
const readStack = [];
|
|
16
|
+
const writeStack = [];
|
|
17
|
+
/**
|
|
18
|
+
* When reading: reads the value normal and saves it for later
|
|
19
|
+
*
|
|
20
|
+
* When writing: omits this value, it is written by deref
|
|
21
|
+
*
|
|
22
|
+
* ---
|
|
23
|
+
* ```
|
|
24
|
+
* const length = st.createReference<number>()
|
|
25
|
+
* st.object({
|
|
26
|
+
* length: length.pointer(st.u32()),
|
|
27
|
+
* type: st.u8(),
|
|
28
|
+
* data: st.sizedBytes(length.deref())
|
|
29
|
+
* })
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
function pointer(serializer) {
|
|
33
|
+
const size = serializer.size;
|
|
34
|
+
if (size === undefined)
|
|
35
|
+
throw new Error("Serializer must be sized");
|
|
36
|
+
return {
|
|
37
|
+
size,
|
|
38
|
+
read: (ctx) => {
|
|
39
|
+
const value = serializer.read(ctx);
|
|
40
|
+
readStack.push(value);
|
|
41
|
+
return value;
|
|
42
|
+
},
|
|
43
|
+
write: (ctx, value) => {
|
|
44
|
+
writeStack.push({
|
|
45
|
+
offset: ctx.offset,
|
|
46
|
+
serializer,
|
|
47
|
+
});
|
|
48
|
+
ctx.offset += size;
|
|
49
|
+
return value;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* When reading: gets the value stored in pointer
|
|
55
|
+
*
|
|
56
|
+
* When writing: saves the value into the pointer
|
|
57
|
+
*
|
|
58
|
+
* By default the value is removed from the pointer,
|
|
59
|
+
* but you can retain it to ensure the next value is
|
|
60
|
+
*
|
|
61
|
+
* ---
|
|
62
|
+
* ```
|
|
63
|
+
* const length = st.createReference<number>()
|
|
64
|
+
* st.object({
|
|
65
|
+
* length: length.pointer(st.u32()),
|
|
66
|
+
* type: st.u8(),
|
|
67
|
+
* data: st.sizedBytes(length.deref())
|
|
68
|
+
* })
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
function deref() {
|
|
72
|
+
return {
|
|
73
|
+
size: 0,
|
|
74
|
+
read: () => {
|
|
75
|
+
if (readStack.length === 0)
|
|
76
|
+
throw new Error("Read Stack Empty");
|
|
77
|
+
return readStack.pop();
|
|
78
|
+
},
|
|
79
|
+
write: (ctx, value) => {
|
|
80
|
+
if (writeStack.length === 0)
|
|
81
|
+
throw new Error("Write Stack is emtpy");
|
|
82
|
+
const { offset, serializer } = writeStack.pop();
|
|
83
|
+
const start = ctx.offset;
|
|
84
|
+
ctx.offset = offset;
|
|
85
|
+
serializer.write(ctx, value);
|
|
86
|
+
ctx.offset = start;
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return { pointer, deref };
|
|
91
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { encodeTest, encodeSnapshotTest, expectError } from "../utils.test";
|
|
3
|
+
import * as st from "../index";
|
|
4
|
+
describe("st.createReference", () => {
|
|
5
|
+
it("encodes correctly", () => {
|
|
6
|
+
const length = st.createReference();
|
|
7
|
+
const spec = st.object({
|
|
8
|
+
length: length.pointer(st.u32()),
|
|
9
|
+
numbers: st.list(length.deref(), st.u8()),
|
|
10
|
+
});
|
|
11
|
+
expect(spec.size).toBe(undefined);
|
|
12
|
+
encodeTest(spec, //
|
|
13
|
+
{ length: 0, numbers: [1, 3, 4] }, { length: 3, numbers: [1, 3, 4] });
|
|
14
|
+
});
|
|
15
|
+
it("cant create reference to pointer", () => {
|
|
16
|
+
const length = st.createReference();
|
|
17
|
+
expectError(() => {
|
|
18
|
+
length.pointer(st.sizedBytes(st.u8()));
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
it("multiple runs function", () => {
|
|
22
|
+
const v = st.createReference();
|
|
23
|
+
const spec = st.object({
|
|
24
|
+
a: v.pointer(st.u32()),
|
|
25
|
+
b: st.list(v.deref(), st.u8()),
|
|
26
|
+
});
|
|
27
|
+
for (let i = 0; i < 3; i++) {
|
|
28
|
+
encodeTest(spec, //
|
|
29
|
+
{ a: NaN, b: [1, 3, 4] }, { a: 3, b: [1, 3, 4] });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
it("handles recursive objects", () => {
|
|
33
|
+
const size = st.createReference();
|
|
34
|
+
//@ts-expect-error, recursive types
|
|
35
|
+
const Node = st.lazy(() => st.object({
|
|
36
|
+
size: size.pointer(st.u32()),
|
|
37
|
+
children: st.list(size.deref(), Node),
|
|
38
|
+
}));
|
|
39
|
+
encodeTest(Node, {
|
|
40
|
+
size: NaN,
|
|
41
|
+
children: [
|
|
42
|
+
{ size: NaN, children: [] },
|
|
43
|
+
{ size: NaN, children: [{ size: NaN, children: [] }] },
|
|
44
|
+
],
|
|
45
|
+
}, {
|
|
46
|
+
size: 2,
|
|
47
|
+
children: [
|
|
48
|
+
{ size: 0, children: [] },
|
|
49
|
+
{ size: 1, children: [{ size: 0, children: [] }] },
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
it("snapshots are correct", () => {
|
|
54
|
+
const v = st.createReference();
|
|
55
|
+
encodeSnapshotTest(st.object({
|
|
56
|
+
a: v.pointer(st.u32()),
|
|
57
|
+
b: st.list(v.deref(), st.u8()),
|
|
58
|
+
}), {
|
|
59
|
+
a: 3,
|
|
60
|
+
b: [1, 3, 4],
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -2,16 +2,17 @@ import type { Serializer } from "../types";
|
|
|
2
2
|
/**
|
|
3
3
|
* createRememberedValue lets you save and recall a value when serializing
|
|
4
4
|
*
|
|
5
|
+
* This is useful for checksums and other values calculated from a writen value
|
|
6
|
+
*
|
|
5
7
|
* ```
|
|
6
|
-
* const
|
|
8
|
+
* const data = st.createRememberedValue<ArrayBuffer>()
|
|
7
9
|
* st.object({
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* data: st.sizedBytes(length.load())
|
|
10
|
+
* data: data.save(st.bytes(1024))
|
|
11
|
+
* checksum: crc32(data.load())
|
|
11
12
|
* })
|
|
12
13
|
* ```
|
|
13
14
|
*/
|
|
14
15
|
export declare function createRememberedValue<T>(): {
|
|
15
16
|
save: (serializer: Serializer<T>) => Serializer<T>;
|
|
16
|
-
load: (
|
|
17
|
+
load: (behaviour?: "pop" | "retain") => Serializer<T>;
|
|
17
18
|
};
|
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* createRememberedValue lets you save and recall a value when serializing
|
|
3
3
|
*
|
|
4
|
+
* This is useful for checksums and other values calculated from a writen value
|
|
5
|
+
*
|
|
4
6
|
* ```
|
|
5
|
-
* const
|
|
7
|
+
* const data = st.createRememberedValue<ArrayBuffer>()
|
|
6
8
|
* st.object({
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* data: st.sizedBytes(length.load())
|
|
9
|
+
* data: data.save(st.bytes(1024))
|
|
10
|
+
* checksum: crc32(data.load())
|
|
10
11
|
* })
|
|
11
12
|
* ```
|
|
12
13
|
*/
|
|
13
14
|
export function createRememberedValue() {
|
|
14
15
|
const stack = [];
|
|
16
|
+
/**
|
|
17
|
+
* Transparently writes/reads the underlying value but stores the value written.
|
|
18
|
+
*
|
|
19
|
+
* ---
|
|
20
|
+
* ```
|
|
21
|
+
* const data = st.createRememberedValue<ArrayBuffer>()
|
|
22
|
+
* st.object({
|
|
23
|
+
* data: data.save(st.bytes(1024))
|
|
24
|
+
* checksum: crc32(data.load())
|
|
25
|
+
* })
|
|
26
|
+
* ```
|
|
27
|
+
* */
|
|
15
28
|
function save(serializer) {
|
|
16
29
|
return {
|
|
17
30
|
size: serializer.size,
|
|
@@ -27,11 +40,35 @@ export function createRememberedValue() {
|
|
|
27
40
|
},
|
|
28
41
|
};
|
|
29
42
|
}
|
|
30
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Retrieves the last value written by `.save()`
|
|
45
|
+
*
|
|
46
|
+
* By default, the value is forgotten when written so that memory isn't leaked.
|
|
47
|
+
* It can be retained by setting `behaviour` to `"retain"`
|
|
48
|
+
*
|
|
49
|
+
* When writing this type does nothing
|
|
50
|
+
*
|
|
51
|
+
* ---
|
|
52
|
+
* ```
|
|
53
|
+
* const data = st.createRememberedValue<ArrayBuffer>()
|
|
54
|
+
* st.object({
|
|
55
|
+
* data: data.save(st.bytes(1024))
|
|
56
|
+
* checksum: crc32(data.load())
|
|
57
|
+
* })
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
function load(behaviour = "pop") {
|
|
31
61
|
return {
|
|
32
|
-
size:
|
|
33
|
-
read: () =>
|
|
34
|
-
|
|
62
|
+
size: 0,
|
|
63
|
+
read: () => {
|
|
64
|
+
if (behaviour === "pop") {
|
|
65
|
+
return stack.pop();
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
return stack.at(-1);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
write: () => { },
|
|
35
72
|
};
|
|
36
73
|
}
|
|
37
74
|
return { save: save, load: load };
|
|
@@ -37,7 +37,7 @@ describe("st.remember", () => {
|
|
|
37
37
|
const v = st.createRememberedValue();
|
|
38
38
|
const spec = st.object({
|
|
39
39
|
a: v.save(st.u32()),
|
|
40
|
-
b: v.load(
|
|
40
|
+
b: v.load(),
|
|
41
41
|
});
|
|
42
42
|
encodeTest(spec, { a: 3, b: 0 }, { a: 3, b: 3 });
|
|
43
43
|
});
|
|
@@ -68,9 +68,5 @@ describe("st.remember", () => {
|
|
|
68
68
|
a: 3,
|
|
69
69
|
b: [1, 3, 4],
|
|
70
70
|
});
|
|
71
|
-
encodeSnapshotTest(st.object({
|
|
72
|
-
a: v.save(st.u32()),
|
|
73
|
-
b: v.load(st.u8()),
|
|
74
|
-
}), { a: 3, b: 0 });
|
|
75
71
|
});
|
|
76
72
|
});
|
package/dist/write.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export function write(serializer, value) {
|
|
2
2
|
const ctx = createdWriterContext(serializer);
|
|
3
|
-
|
|
3
|
+
try {
|
|
4
|
+
serializer.write(ctx, value);
|
|
5
|
+
}
|
|
6
|
+
catch (e) {
|
|
7
|
+
const path = ctx.stack.join("") ?? "root";
|
|
8
|
+
throw new Error(`Serialization error at value${path}`, { cause: e });
|
|
9
|
+
}
|
|
4
10
|
if (ctx.buffer.byteLength === ctx.offset) {
|
|
5
11
|
return ctx.buffer;
|
|
6
12
|
}
|
|
@@ -13,6 +19,7 @@ export function createdWriterContext(type) {
|
|
|
13
19
|
let buffer = new ArrayBuffer(type.size);
|
|
14
20
|
const view = new DataView(buffer);
|
|
15
21
|
return {
|
|
22
|
+
stack: [],
|
|
16
23
|
offset: 0,
|
|
17
24
|
buffer,
|
|
18
25
|
view,
|
|
@@ -24,6 +31,7 @@ export function createdWriterContext(type) {
|
|
|
24
31
|
let buffer = new ArrayBuffer(bufferLength);
|
|
25
32
|
const view = new DataView(buffer);
|
|
26
33
|
return {
|
|
34
|
+
stack: [],
|
|
27
35
|
offset: 0,
|
|
28
36
|
buffer,
|
|
29
37
|
view,
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"node"
|
|
10
10
|
],
|
|
11
11
|
"license": "ISC",
|
|
12
|
-
"version": "1.0.
|
|
12
|
+
"version": "1.0.8",
|
|
13
13
|
"author": "Ben Brady",
|
|
14
14
|
"private": false,
|
|
15
15
|
"publishConfig": {
|
|
@@ -44,12 +44,6 @@
|
|
|
44
44
|
"./dist/**/*.js.map",
|
|
45
45
|
"./dist/**/*.js"
|
|
46
46
|
],
|
|
47
|
-
"scripts": {
|
|
48
|
-
"build": "tsc",
|
|
49
|
-
"prepublish": "tsc",
|
|
50
|
-
"check": "tsc --noEmit",
|
|
51
|
-
"test": "bun test src --only-failures --coverage"
|
|
52
|
-
},
|
|
53
47
|
"devDependencies": {
|
|
54
48
|
"@types/bun": "^1.3.14",
|
|
55
49
|
"@types/node": "^25.9.2",
|
|
@@ -57,5 +51,11 @@
|
|
|
57
51
|
"jest": "^30.4.2",
|
|
58
52
|
"typescript": "^6.0.3"
|
|
59
53
|
},
|
|
60
|
-
"
|
|
61
|
-
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsc",
|
|
56
|
+
"prepublish": "tsc",
|
|
57
|
+
"check": "tsc --noEmit",
|
|
58
|
+
"test": "bun test src --only-failures",
|
|
59
|
+
"test:coverage": "bun test src --only-failures --coverage"
|
|
60
|
+
}
|
|
61
|
+
}
|