Skip to content

Commit 5a96838

Browse files
authored
Merge pull request #1262 from derwehr/master
[octetsream-codec] fix serialization and deserialization of nested objects
2 parents 8d2fae9 + 1d86750 commit 5a96838

2 files changed

Lines changed: 234 additions & 15 deletions

File tree

packages/core/src/codecs/octetstream-codec.ts

Lines changed: 76 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,41 @@ export default class OctetstreamCodec implements ContentCodec {
5959
debug("OctetstreamCodec parsing", bytes);
6060
debug("Parameters", parameters);
6161

62-
const bigEndian = !(parameters.byteSeq?.includes(Endianness.LITTLE_ENDIAN) === true); // default to big endian
63-
let signed = parameters.signed !== "false"; // default to signed
62+
const length =
63+
parameters.length != null
64+
? parseInt(parameters.length)
65+
: (warn("Missing 'length' parameter necessary for write. I'll do my best"), undefined);
66+
67+
if (length !== undefined) {
68+
if (isNaN(length) || length < 0) {
69+
throw new Error("'length' parameter must be a non-negative number");
70+
}
71+
if (length !== bytes.length) {
72+
throw new Error(`Lengths do not match, required: ${length} provided: ${bytes.length}`);
73+
}
74+
}
75+
76+
let signed = true; // default to signed
77+
if (parameters.signed !== undefined) {
78+
if (parameters.signed !== "true" && parameters.signed !== "false") {
79+
throw new Error("'signed' parameter must be 'true' or 'false'");
80+
}
81+
signed = parameters.signed === "true";
82+
}
83+
84+
let bitLength = schema?.["ex:bitLength"] !== undefined ? parseInt(schema["ex:bitLength"]) : bytes.length * 8;
85+
86+
if (isNaN(bitLength) || bitLength < 0) {
87+
throw new Error("'ex:bitLength' must be a non-negative number");
88+
}
89+
6490
const offset = schema?.["ex:bitOffset"] !== undefined ? parseInt(schema["ex:bitOffset"]) : 0;
65-
if (parameters.length != null && parseInt(parameters.length) !== bytes.length) {
66-
throw new Error("Lengths do not match, required: " + parameters.length + " provided: " + bytes.length);
91+
92+
if (isNaN(offset) || offset < 0) {
93+
throw new Error("'ex:bitOffset' must be a non-negative number");
6794
}
68-
let bitLength: number =
69-
schema?.["ex:bitLength"] !== undefined ? parseInt(schema["ex:bitLength"]) : bytes.length * 8;
95+
96+
const bigEndian = !(parameters.byteSeq?.includes(Endianness.LITTLE_ENDIAN) === true); // default to big endian
7097
let dataType: string = schema?.type;
7198

7299
if (!dataType) {
@@ -214,24 +241,47 @@ export default class OctetstreamCodec implements ContentCodec {
214241
const sortedProperties = Object.getOwnPropertyNames(schema.properties);
215242
for (const propertyName of sortedProperties) {
216243
const propertySchema = schema.properties[propertyName];
217-
result[propertyName] = this.bytesToValue(bytes, propertySchema, parameters);
244+
const length = bytes.length.toString();
245+
result[propertyName] = this.bytesToValue(bytes, propertySchema, { ...parameters, length });
218246
}
219247
return result;
220248
}
221249

222250
valueToBytes(value: unknown, schema?: DataSchema, parameters: { [key: string]: string | undefined } = {}): Buffer {
223251
debug(`OctetstreamCodec serializing '${value}'`);
224252

225-
if (parameters.length == null) {
226-
warn("Missing 'length' parameter necessary for write. I'll do my best");
253+
const bigEndian = !(parameters.byteSeq?.includes(Endianness.LITTLE_ENDIAN) === true); // default to big endian
254+
255+
let signed = true; // default to true
256+
257+
if (parameters.signed !== undefined) {
258+
if (parameters.signed !== "true" && parameters.signed !== "false") {
259+
throw new Error("'signed' parameter must be 'true' or 'false'");
260+
}
261+
signed = parameters.signed === "true";
262+
}
263+
264+
let length =
265+
parameters.length != null
266+
? parseInt(parameters.length)
267+
: (warn("Missing 'length' parameter necessary for write. I'll do my best"), undefined);
268+
269+
if (length !== undefined && (isNaN(length) || length < 0)) {
270+
throw new Error("'length' parameter must be a non-negative number");
227271
}
228272

229-
const bigEndian = !(parameters.byteSeq?.includes(Endianness.LITTLE_ENDIAN) === true); // default to big endian
230-
let signed = parameters.signed !== "false"; // default to signed
231-
// byte length of the buffer to be returned
232-
let length = parameters.length != null ? parseInt(parameters.length) : undefined;
233273
let bitLength = schema?.["ex:bitLength"] !== undefined ? parseInt(schema["ex:bitLength"]) : undefined;
274+
275+
if (bitLength !== undefined && (isNaN(bitLength) || bitLength < 0)) {
276+
throw new Error("'ex:bitLength' must be a non-negative number");
277+
}
278+
234279
const offset = schema?.["ex:bitOffset"] !== undefined ? parseInt(schema["ex:bitOffset"]) : 0;
280+
281+
if (isNaN(offset) || offset < 0) {
282+
throw new Error("'ex:bitOffset' must be a non-negative number");
283+
}
284+
235285
let dataType: string = schema?.type ?? undefined;
236286

237287
if (value === undefined) {
@@ -542,7 +592,18 @@ export default class OctetstreamCodec implements ContentCodec {
542592
throw new Error("Missing 'length' parameter necessary for write");
543593
}
544594

545-
result = result ?? Buffer.alloc(parseInt(parameters.length));
595+
const length = parseInt(parameters.length);
596+
const offset = schema["ex:bitOffset"] !== undefined ? parseInt(schema["ex:bitOffset"]) : 0;
597+
598+
if (isNaN(offset) || offset < 0) {
599+
throw new Error("'ex:bitOffset' must be a non-negative number");
600+
}
601+
602+
if (offset > length * 8) {
603+
throw new Error(`'ex:bitOffset' ${offset} exceeds 'length' ${length}`);
604+
}
605+
606+
result = result ?? Buffer.alloc(length);
546607
for (const propertyName in schema.properties) {
547608
if (Object.hasOwnProperty.call(value, propertyName) === false) {
548609
throw new Error(`Missing property '${propertyName}'`);
@@ -557,7 +618,7 @@ export default class OctetstreamCodec implements ContentCodec {
557618
} else {
558619
buf = this.valueToBytes(propertyValue, propertySchema, parameters);
559620
}
560-
this.copyBits(buf, propertyOffset, result, propertyOffset, propertyLength);
621+
this.copyBits(buf, propertyOffset, result, offset + propertyOffset, propertyLength);
561622
}
562623
return result;
563624
}

packages/core/test/ContentSerdesTest.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,59 @@ class SerdesOctetTests {
444444
},
445445
}
446446
);
447+
448+
checkStreamToValue(
449+
[0x0e, 0x10, 0x10, 0x10, 0x0e],
450+
{
451+
flags1: { flag1: false, flag2: true },
452+
flags2: { flag1: true, flag2: false },
453+
},
454+
"object",
455+
{
456+
type: "object",
457+
properties: {
458+
flags1: {
459+
type: "object",
460+
"ex:bitOffset": 0,
461+
"ex:bitLength": 8,
462+
properties: {
463+
flag1: {
464+
type: "boolean",
465+
title: "Bit 1",
466+
"ex:bitOffset": 3,
467+
"ex:bitLength": 1,
468+
},
469+
flag2: {
470+
type: "boolean",
471+
title: "Bit 2",
472+
"ex:bitOffset": 4,
473+
"ex:bitLength": 1,
474+
},
475+
},
476+
},
477+
flags2: {
478+
type: "object",
479+
"ex:bitOffset": 8,
480+
"ex:bitLength": 8,
481+
properties: {
482+
flag1: {
483+
type: "boolean",
484+
title: "Bit 1",
485+
"ex:bitOffset": 3,
486+
"ex:bitLength": 1,
487+
},
488+
flag2: {
489+
type: "boolean",
490+
title: "Bit 2",
491+
"ex:bitOffset": 4,
492+
"ex:bitLength": 1,
493+
},
494+
},
495+
},
496+
},
497+
},
498+
{ length: "5" }
499+
);
447500
}
448501

449502
@test async "OctetStream to value should throw"() {
@@ -531,6 +584,55 @@ class SerdesOctetTests {
531584
{ type: "uint8" }
532585
)
533586
).to.throw(Error, "Type is unsigned but 'signed' is true");
587+
588+
expect(() =>
589+
ContentSerdes.contentToValue(
590+
{ type: `application/octet-stream;length=test`, body: Buffer.from([0x36]) },
591+
{ type: "integer" }
592+
)
593+
).to.throw(Error, "'length' parameter must be a non-negative number");
594+
595+
expect(() =>
596+
ContentSerdes.contentToValue(
597+
{ type: `application/octet-stream;length=-1`, body: Buffer.from([0x36]) },
598+
{ type: "integer" }
599+
)
600+
).to.throw(Error, "'length' parameter must be a non-negative number");
601+
602+
expect(() =>
603+
ContentSerdes.contentToValue(
604+
{ type: `application/octet-stream;signed=invalid`, body: Buffer.from([0x36]) },
605+
{ type: "integer" }
606+
)
607+
).to.throw(Error, "'signed' parameter must be 'true' or 'false'");
608+
609+
expect(() =>
610+
ContentSerdes.contentToValue(
611+
{ type: `application/octet-stream`, body: Buffer.from([0x36]) },
612+
{ type: "integer", "ex:bitOffset": "invalid" }
613+
)
614+
).to.throw(Error, "'ex:bitOffset' must be a non-negative number");
615+
616+
expect(() =>
617+
ContentSerdes.contentToValue(
618+
{ type: `application/octet-stream`, body: Buffer.from([0x36]) },
619+
{ type: "integer", "ex:bitOffset": -1 }
620+
)
621+
).to.throw(Error, "'ex:bitOffset' must be a non-negative number");
622+
623+
expect(() =>
624+
ContentSerdes.contentToValue(
625+
{ type: `application/octet-stream`, body: Buffer.from([0x36]) },
626+
{ type: "integer", "ex:bitLength": "invalid" }
627+
)
628+
).to.throw(Error, "'ex:bitLength' must be a non-negative number");
629+
630+
expect(() =>
631+
ContentSerdes.contentToValue(
632+
{ type: `application/octet-stream`, body: Buffer.from([0x36]) },
633+
{ type: "integer", "ex:bitLength": -1 }
634+
)
635+
).to.throw(Error, "'ex:bitLength' must be a non-negative number");
534636
}
535637

536638
@test async "value to OctetStream"() {
@@ -763,6 +865,39 @@ class SerdesOctetTests {
763865
);
764866
body = await content.toBuffer();
765867
expect(body).to.deep.equal(Buffer.from([0xc0]));
868+
869+
content = ContentSerdes.valueToContent(
870+
{
871+
flags1: { flag1: false, flag2: true },
872+
flags2: { flag1: true, flag2: false },
873+
},
874+
{
875+
type: "object",
876+
properties: {
877+
flags1: {
878+
type: "object",
879+
properties: {
880+
flag1: { type: "boolean", "ex:bitOffset": 3, "ex:bitLength": 1 },
881+
flag2: { type: "boolean", "ex:bitOffset": 4, "ex:bitLength": 1 },
882+
},
883+
"ex:bitLength": 8,
884+
},
885+
flags2: {
886+
type: "object",
887+
properties: {
888+
flag1: { type: "boolean", "ex:bitOffset": 3, "ex:bitLength": 1 },
889+
flag2: { type: "boolean", "ex:bitOffset": 4, "ex:bitLength": 1 },
890+
},
891+
"ex:bitOffset": 8,
892+
"ex:bitLength": 8,
893+
},
894+
},
895+
"ex:bitLength": 16,
896+
},
897+
"application/octet-stream;length=2;"
898+
);
899+
body = await content.toBuffer();
900+
expect(body).to.deep.equal(Buffer.from([0x08, 0x10]));
766901
}
767902

768903
@test "value to OctetStream should throw"() {
@@ -851,6 +986,29 @@ class SerdesOctetTests {
851986
Error,
852987
"Missing 'type' property in schema"
853988
);
989+
expect(() => ContentSerdes.valueToContent(10, { type: "int8" }, "application/octet-stream;signed=8")).to.throw(
990+
Error,
991+
"'signed' parameter must be 'true' or 'false'"
992+
);
993+
expect(() =>
994+
ContentSerdes.valueToContent(10, { type: "int8" }, "application/octet-stream;length=-1;")
995+
).to.throw(Error, "'length' parameter must be a non-negative number");
996+
expect(() => ContentSerdes.valueToContent(10, { type: "int8" }, "application/octet-stream;length=x;")).to.throw(
997+
Error,
998+
"'length' parameter must be a non-negative number"
999+
);
1000+
expect(() =>
1001+
ContentSerdes.valueToContent(10, { type: "integer", "ex:bitOffset": -16 }, "application/octet-stream")
1002+
).to.throw(Error, "'ex:bitOffset' must be a non-negative number");
1003+
expect(() =>
1004+
ContentSerdes.valueToContent(10, { type: "integer", "ex:bitOffset": "foo" }, "application/octet-stream")
1005+
).to.throw(Error, "'ex:bitOffset' must be a non-negative number");
1006+
expect(() =>
1007+
ContentSerdes.valueToContent(10, { type: "integer", "ex:bitLength": -8 }, "application/octet-stream")
1008+
).to.throw(Error, "'ex:bitLength' must be a non-negative number");
1009+
expect(() =>
1010+
ContentSerdes.valueToContent(10, { type: "integer", "ex:bitLength": "foo" }, "application/octet-stream")
1011+
).to.throw(Error, "'ex:bitLength' must be a non-negative number");
8541012
}
8551013
}
8561014

0 commit comments

Comments
 (0)