Skip to content

Commit bfb7d3d

Browse files
committed
getting repo ready for publishing
1 parent 215e111 commit bfb7d3d

17 files changed

+3012
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules/
2+
dist/

README.md

+605
Large diffs are not rendered by default.

jest.config.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module.exports = {
2+
preset: 'ts-jest',
3+
testEnvironment: 'node',
4+
roots: ['<rootDir>/test'],
5+
testMatch: ['**/*.test.ts'],
6+
transform: {
7+
'^.+\\.tsx?$': 'ts-jest'
8+
}
9+
};

package.json

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"name": "binary-layout",
3+
"version": "1.0.1",
4+
"license": "Apache-2.0",
5+
"author": "nonergodic",
6+
"description": "Typescript-native, declarative DSL for working with binary data",
7+
"keywords": [
8+
"typescript",
9+
"binary",
10+
"serialization",
11+
"deserialization",
12+
"parser",
13+
"encoder",
14+
"decoder",
15+
"Uint8Array",
16+
"buffer"
17+
],
18+
"repository": {
19+
"type": "git",
20+
"url": "git+https://github.com/nonergodic/layout.git"
21+
},
22+
"main": "./dist/index.js",
23+
"module": "./dist/index.mjs",
24+
"types": "./dist/index.d.ts",
25+
"exports": {
26+
".": {
27+
"types": "./dist/index.d.ts",
28+
"import": "./dist/index.mjs",
29+
"require": "./dist/index.js",
30+
"default": "./dist/index.js"
31+
}
32+
},
33+
"files": [
34+
"dist",
35+
"README.md"
36+
],
37+
"scripts": {
38+
"prepublishOnly": "jest && tsup",
39+
"build": "tsup",
40+
"test": "jest"
41+
},
42+
"devDependencies": {
43+
"@types/jest": "^29.5.14",
44+
"jest": "^29.7.0",
45+
"ts-jest": "^29.2.5",
46+
"tsup": "^8.3.5",
47+
"typescript": "^5.6.3"
48+
}
49+
}

src/deserialize.ts

+225
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import type {
2+
Endianness,
3+
Layout,
4+
Item,
5+
DeriveType,
6+
CustomConversion,
7+
NumSizeToPrimitive,
8+
NumType,
9+
BytesType,
10+
} from "./layout";
11+
import { defaultEndianness, numberMaxSize } from "./layout";
12+
13+
import {
14+
isNumType,
15+
isBytesType,
16+
isFixedBytesConversion,
17+
checkBytesTypeEqual,
18+
checkNumEquals,
19+
} from "./utils";
20+
import { getCachedSerializedFrom } from "./serialize";
21+
22+
type DeserializeReturn<L extends Layout, B extends boolean> =
23+
B extends true ? DeriveType<L> : readonly [DeriveType<L>, number];
24+
25+
export function deserialize<const L extends Layout, const B extends boolean = true>(
26+
layout: L,
27+
bytes: BytesType,
28+
consumeAll?: B,
29+
): DeserializeReturn<L, B> {
30+
const boolConsumeAll = consumeAll ?? true;
31+
const encoded = {
32+
bytes,
33+
offset: 0,
34+
end: bytes.length,
35+
};
36+
const decoded = internalDeserialize(layout, encoded);
37+
38+
if (boolConsumeAll && encoded.offset !== encoded.end)
39+
throw new Error(`encoded data is longer than expected: ${encoded.end} > ${encoded.offset}`);
40+
41+
return (boolConsumeAll ? decoded : [decoded, encoded.offset]) as DeserializeReturn<L, B>;
42+
}
43+
44+
// --- implementation ---
45+
46+
type BytesChunk = {
47+
bytes: BytesType,
48+
offset: number,
49+
end: number,
50+
};
51+
52+
function updateOffset(encoded: BytesChunk, size: number) {
53+
const newOffset = encoded.offset + size;
54+
if (newOffset > encoded.end)
55+
throw new Error(`chunk is shorter than expected: ${encoded.end} < ${newOffset}`);
56+
57+
encoded.offset = newOffset;
58+
}
59+
60+
function internalDeserialize(layout: Layout, encoded: BytesChunk): any {
61+
if (!Array.isArray(layout))
62+
return deserializeItem(layout as Item, encoded);
63+
64+
let decoded = {} as any;
65+
for (const item of layout)
66+
try {
67+
((item as any).omit ? {} : decoded)[item.name] = deserializeItem(item, encoded);
68+
}
69+
catch (e) {
70+
(e as Error).message = `when deserializing item '${item.name}': ${(e as Error).message}`;
71+
throw e;
72+
}
73+
74+
return decoded;
75+
}
76+
77+
function deserializeNum<S extends number>(
78+
encoded: BytesChunk,
79+
size: S,
80+
endianness: Endianness = defaultEndianness,
81+
signed: boolean = false,
82+
) {
83+
let val = 0n;
84+
for (let i = 0; i < size; ++i)
85+
val |= BigInt(encoded.bytes[encoded.offset + i]!)
86+
<< BigInt(8 * (endianness === "big" ? size - i - 1 : i));
87+
88+
//check sign bit if value is indeed signed and adjust accordingly
89+
if (signed && (encoded.bytes[encoded.offset + (endianness === "big" ? 0 : size - 1)]! & 0x80))
90+
val -= 1n << BigInt(8 * size);
91+
92+
updateOffset(encoded, size);
93+
94+
return ((size > numberMaxSize) ? val : Number(val)) as NumSizeToPrimitive<S>;
95+
}
96+
97+
function deserializeItem(item: Item, encoded: BytesChunk): any {
98+
switch (item.binary) {
99+
case "int":
100+
case "uint": {
101+
const value = deserializeNum(encoded, item.size, item.endianness, item.binary === "int");
102+
103+
const { custom } = item;
104+
if (isNumType(custom)) {
105+
checkNumEquals(custom, value);
106+
return custom;
107+
}
108+
if (isNumType(custom?.from)) {
109+
checkNumEquals(custom!.from, value);
110+
return custom!.to;
111+
}
112+
113+
//narrowing to CustomConversion<UintType, any> is a bit hacky here, since the true type
114+
// would be CustomConversion<number, any> | CustomConversion<bigint, any>, but then we'd
115+
// have to further tease that apart still for no real gain...
116+
return custom !== undefined ? (custom as CustomConversion<NumType, any>).to(value) : value;
117+
}
118+
case "bytes": {
119+
const expectedSize = ("lengthSize" in item && item.lengthSize !== undefined)
120+
? deserializeNum(encoded, item.lengthSize, item.lengthEndianness)
121+
: (item as {size?: number})?.size;
122+
123+
if ("layout" in item) { //handle layout conversions
124+
const { custom } = item;
125+
const offset = encoded.offset;
126+
let layoutData;
127+
if (expectedSize === undefined)
128+
layoutData = internalDeserialize(item.layout, encoded);
129+
else {
130+
const subChunk = {...encoded, end: encoded.offset + expectedSize};
131+
updateOffset(encoded, expectedSize);
132+
layoutData = internalDeserialize(item.layout, subChunk);
133+
if (subChunk.offset !== subChunk.end)
134+
throw new Error(
135+
`read less data than expected: ${subChunk.offset - encoded.offset} < ${expectedSize}`
136+
);
137+
}
138+
139+
if (custom !== undefined) {
140+
if (typeof custom.from !== "function") {
141+
checkBytesTypeEqual(
142+
getCachedSerializedFrom(item as any),
143+
encoded.bytes,
144+
{dataSlize: [offset, encoded.offset]}
145+
);
146+
return custom.to;
147+
}
148+
return custom.to(layoutData);
149+
}
150+
151+
return layoutData;
152+
}
153+
154+
const { custom } = item;
155+
{ //handle fixed conversions
156+
let fixedFrom;
157+
let fixedTo;
158+
if (isBytesType(custom))
159+
fixedFrom = custom;
160+
else if (isFixedBytesConversion(custom)) {
161+
fixedFrom = custom.from;
162+
fixedTo = custom.to;
163+
}
164+
if (fixedFrom !== undefined) {
165+
const size = expectedSize ?? fixedFrom.length;
166+
const value = encoded.bytes.subarray(encoded.offset, encoded.offset + size);
167+
checkBytesTypeEqual(fixedFrom, value);
168+
updateOffset(encoded, size);
169+
return fixedTo ?? fixedFrom;
170+
}
171+
}
172+
173+
//handle no or custom conversions
174+
const start = encoded.offset;
175+
const end = (expectedSize !== undefined) ? encoded.offset + expectedSize : encoded.end;
176+
updateOffset(encoded, end - start);
177+
178+
const value = encoded.bytes.subarray(start, end);
179+
return custom !== undefined ? (custom as CustomConversion<BytesType, any>).to(value) : value;
180+
}
181+
case "array": {
182+
let ret = [] as any[];
183+
const { layout } = item;
184+
const deserializeArrayItem = () => {
185+
const deserializedItem = internalDeserialize(layout, encoded);
186+
ret.push(deserializedItem);
187+
}
188+
189+
let length: number | null = null;
190+
if ("length" in item && item.length !== undefined)
191+
length = item.length;
192+
else if ("lengthSize" in item && item.lengthSize !== undefined)
193+
length = deserializeNum(encoded, item.lengthSize, item.lengthEndianness);
194+
195+
if (length !== null)
196+
for (let i = 0; i < length; ++i)
197+
deserializeArrayItem();
198+
else
199+
while (encoded.offset < encoded.end)
200+
deserializeArrayItem();
201+
202+
return ret;
203+
}
204+
case "switch": {
205+
const id = deserializeNum(encoded, item.idSize, item.idEndianness);
206+
const {layouts} = item;
207+
if (layouts.length === 0)
208+
throw new Error(`switch item has no layouts`);
209+
210+
const hasPlainIds = typeof layouts[0]![0] === "number";
211+
const pair = (layouts as readonly any[]).find(([idOrConversionId]) =>
212+
hasPlainIds ? idOrConversionId === id : (idOrConversionId)[0] === id);
213+
214+
if (pair === undefined)
215+
throw new Error(`unknown id value: ${id}`);
216+
217+
const [idOrConversionId, idLayout] = pair;
218+
const decoded = internalDeserialize(idLayout, encoded);
219+
return {
220+
[item.idTag ?? "id"]: hasPlainIds ? id : (idOrConversionId as any)[1],
221+
...decoded
222+
};
223+
}
224+
}
225+
}

0 commit comments

Comments
 (0)