Skip to content

Commit ed0fbb5

Browse files
Implementing encoding and its unit-tests
1 parent d033245 commit ed0fbb5

File tree

3 files changed

+428
-6
lines changed

3 files changed

+428
-6
lines changed

lib/binary_parser.ts

Lines changed: 273 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,21 @@ type BitSizes =
211211
| 31
212212
| 32;
213213

214+
const TYPE_RANGES: {
215+
[type: string]: { min: number | bigint; max: number | bigint };
216+
} = {
217+
Int8: { min: -128, max: 127 },
218+
Int16: { min: -32768, max: 32767 },
219+
Int32: { min: -2147483648, max: 2147483647 },
220+
Uint8: { min: 0, max: 255 },
221+
Uint16: { min: 0, max: 65535 },
222+
Uint32: { min: 0, max: 4294967295 },
223+
BigInt64: { min: -9223372036854775808n, max: 9223372036854775807n },
224+
BigUint64: { min: 0n, max: 18446744073709551615n },
225+
Float32: { min: -3.4e38, max: 3.4e38 },
226+
Float64: { min: -1.8e308, max: 1.8e308 },
227+
};
228+
214229
const PRIMITIVE_SIZES: { [key in PrimitiveTypes]: number } = {
215230
uint8: 1,
216231
uint16le: 2,
@@ -706,7 +721,7 @@ export class Parser {
706721
}
707722

708723
pointer(varName: string, options: ParserOptions): this {
709-
if (options.offset == null) {
724+
if (!options.offset) {
710725
throw new Error("offset is required for pointer.");
711726
}
712727

@@ -897,7 +912,25 @@ export class Parser {
897912
return size;
898913
}
899914

900-
// Follow the parser chain till the root and start parsing from there
915+
/**
916+
* Parse the given buffer and return the parsed object
917+
*
918+
* @param buffer - the buffer to parse
919+
* @returns the parsed object
920+
*
921+
* @example
922+
* const parser = new Parser()
923+
* .endianess('little')
924+
* .uint16('num1')
925+
* .uint16('num2');
926+
*
927+
* const buf = Buffer.from([0x01, 0x02, 0x03, 0x04]);
928+
* parser.parse(buf); // => { num1: 513, num2: 1027 }
929+
*
930+
* @throws {Error} - Throws an error if the buffer is not a buffer
931+
* @throws {Error} - Throws an error if the buffer is too small
932+
*/
933+
901934
parse(buffer: Buffer | Uint8Array) {
902935
if (!this.compiled) {
903936
this.compile();
@@ -906,6 +939,244 @@ export class Parser {
906939
return this.compiled!(buffer, this.constructorFn);
907940
}
908941

942+
private encodeValue(
943+
dataView: DataView,
944+
object: any,
945+
offset: number = 0,
946+
): number {
947+
if (this.varName && !object.hasOwnProperty(this.varName)) {
948+
throw new Error(`Field "${this.varName}" not present`);
949+
}
950+
951+
if (Object.keys(PRIMITIVE_SIZES).indexOf(this.type as string) >= 0) {
952+
const val = object[this.varName];
953+
const size = PRIMITIVE_SIZES[this.type as PrimitiveTypes];
954+
const littleEndian =
955+
PRIMITIVE_LITTLE_ENDIANS[this.type as PrimitiveTypes];
956+
const typeName = PRIMITIVE_NAMES[this.type as PrimitiveTypes];
957+
958+
const range = TYPE_RANGES[typeName];
959+
const value = typeName.startsWith("Big") ? BigInt(val) : val;
960+
961+
if (value < range.min || value > range.max) {
962+
throw new Error(
963+
`Field "${this.varName}" should be a ${typeName} and should be between ${range.min} and ${range.max}`,
964+
);
965+
}
966+
967+
switch (typeName) {
968+
case "Int8":
969+
dataView.setInt8(offset, val);
970+
break;
971+
case "Int16":
972+
dataView.setInt16(offset, val, littleEndian);
973+
break;
974+
case "Int32":
975+
dataView.setInt32(offset, val, littleEndian);
976+
break;
977+
case "Uint8":
978+
dataView.setUint8(offset, val);
979+
break;
980+
case "Uint16":
981+
dataView.setUint16(offset, val, littleEndian);
982+
break;
983+
case "Uint32":
984+
dataView.setUint32(offset, val, littleEndian);
985+
break;
986+
case "Float32":
987+
dataView.setFloat32(offset, val, littleEndian);
988+
break;
989+
case "Float64":
990+
dataView.setFloat64(offset, val, littleEndian);
991+
break;
992+
case "BigInt64":
993+
dataView.setBigInt64(offset, BigInt(val), littleEndian);
994+
break;
995+
case "BigUint64":
996+
dataView.setBigUint64(offset, BigInt(val), littleEndian);
997+
break;
998+
}
999+
1000+
if (this.next) {
1001+
return this.next.encodeValue(dataView, object, offset + size);
1002+
}
1003+
1004+
return offset + size;
1005+
}
1006+
1007+
if (this.type === "bit") {
1008+
const val = object[this.varName];
1009+
const size = this.options.length as number;
1010+
1011+
if (typeof val !== "string") {
1012+
throw new Error("Bit field should be a string");
1013+
}
1014+
1015+
for (let i = 0; i < size; i++) {
1016+
dataView.setUint8(offset + i, val.charCodeAt(i));
1017+
}
1018+
1019+
if (this.next) {
1020+
return this.next.encodeValue(dataView, object, offset + size);
1021+
}
1022+
1023+
return offset + size;
1024+
}
1025+
1026+
if (this.type === "string") {
1027+
const val = object[this.varName];
1028+
const textEncoder = new TextEncoder();
1029+
const buffer = textEncoder.encode(val);
1030+
1031+
let length = buffer.length;
1032+
if (typeof this.options.length === "number") {
1033+
length = Math.min(length, this.options.length);
1034+
}
1035+
1036+
new Uint8Array(dataView.buffer, offset, length).set(
1037+
buffer.subarray(0, length),
1038+
);
1039+
1040+
if (this.options.zeroTerminated) {
1041+
dataView.setUint8(offset + length, 0);
1042+
length++;
1043+
}
1044+
const newOffset = offset + length;
1045+
if (this.next) {
1046+
return this.next.encodeValue(dataView, object, newOffset);
1047+
}
1048+
return newOffset;
1049+
} else if (this.type === "array") {
1050+
const arr = object[this.varName];
1051+
let newOffset = offset;
1052+
1053+
for (const item of arr) {
1054+
if (typeof this.options.type === "string") {
1055+
const parser = new Parser().primitiveN(
1056+
this.options.type as PrimitiveTypes,
1057+
"value",
1058+
{},
1059+
);
1060+
newOffset = parser.encodeValue(dataView, { value: item }, newOffset);
1061+
} else if (this.options.type instanceof Parser) {
1062+
newOffset = this.options.type.encodeValue(dataView, item, newOffset);
1063+
}
1064+
}
1065+
1066+
if (this.next) {
1067+
return this.next.encodeValue(dataView, object, newOffset);
1068+
}
1069+
return newOffset;
1070+
}
1071+
1072+
if (this.type === "nest") {
1073+
const val = object[this.varName];
1074+
if (this.options.type instanceof Parser) {
1075+
return this.options.type.encodeValue(dataView, val, offset);
1076+
}
1077+
}
1078+
1079+
if (this.next) {
1080+
return this.next.encodeValue(dataView, object, offset);
1081+
}
1082+
1083+
return offset;
1084+
}
1085+
1086+
calculateSize(object: any): number {
1087+
if (!this.type && !this.next) return 0;
1088+
1089+
let size = 0;
1090+
1091+
if (this.type) {
1092+
if (Object.keys(PRIMITIVE_SIZES).indexOf(this.type) >= 0) {
1093+
size += PRIMITIVE_SIZES[this.type as PrimitiveTypes];
1094+
} else if (this.type === "bit") {
1095+
size += this.options.length as number;
1096+
} else if (this.type === "string") {
1097+
const val = object[this.varName];
1098+
if (val) {
1099+
const textEncoder = new TextEncoder();
1100+
const encoded = textEncoder.encode(val);
1101+
1102+
if (typeof this.options.length === "number") {
1103+
size += Math.min(encoded.length, this.options.length);
1104+
} else if (this.options.zeroTerminated) {
1105+
size += encoded.length + 1; // +1 for null terminator
1106+
} else {
1107+
size += encoded.length;
1108+
}
1109+
}
1110+
} else if (this.type === "array") {
1111+
const arr = object[this.varName];
1112+
if (arr && Array.isArray(arr)) {
1113+
if (typeof this.options.type === "string") {
1114+
size +=
1115+
arr.length * PRIMITIVE_SIZES[this.options.type as PrimitiveTypes];
1116+
} else if (this.options.type instanceof Parser) {
1117+
size += arr.reduce(
1118+
(sum, item) =>
1119+
sum +
1120+
(this.options.type instanceof Parser
1121+
? this.options.type.calculateSize(item)
1122+
: 0),
1123+
0,
1124+
);
1125+
}
1126+
}
1127+
} else if (this.type === "nest") {
1128+
const val = object[this.varName];
1129+
if (val && this.options.type instanceof Parser) {
1130+
size += this.options.type.calculateSize(val);
1131+
}
1132+
}
1133+
}
1134+
1135+
if (this.next) {
1136+
size += this.next.calculateSize(object);
1137+
}
1138+
1139+
return size;
1140+
}
1141+
1142+
/**
1143+
* Encodes a JavaScript object into a binary buffer according to the parser's specification.
1144+
*
1145+
* @param {object} data - The object to encode
1146+
* @returns {Buffer} The encoded binary data
1147+
*
1148+
* @example
1149+
* // Basic usage with primitive types
1150+
* const parser = new Parser()
1151+
* .int8('age')
1152+
* .string('name', { zeroTerminated: true });
1153+
*
1154+
* const buffer = parser.encode({
1155+
* age: 25,
1156+
* name: 'John'
1157+
* });
1158+
*
1159+
* console.log(buffer); // <Buffer 19 4a 6f 68 6e 00>
1160+
*
1161+
* @throws {Error} When a required field is missing from the input object
1162+
* @throws {Error} When field values don't match their type constraints
1163+
* @throws {Error} When an integer overflows or underflows
1164+
*/
1165+
encode(data: any): Buffer {
1166+
const size = this.calculateSize(data);
1167+
1168+
const buffer = Buffer.alloc(size);
1169+
const dataView = new DataView(
1170+
buffer.buffer,
1171+
buffer.byteOffset,
1172+
buffer.length,
1173+
);
1174+
1175+
this.encodeValue(dataView, data);
1176+
1177+
return buffer;
1178+
}
1179+
9091180
private setNextParser(
9101181
type: Types,
9111182
varName: string,

0 commit comments

Comments
 (0)