diff --git a/packages/core/src/codecs/octetstream-codec.ts b/packages/core/src/codecs/octetstream-codec.ts index b90f78759..e1bb481c6 100644 --- a/packages/core/src/codecs/octetstream-codec.ts +++ b/packages/core/src/codecs/octetstream-codec.ts @@ -59,14 +59,41 @@ export default class OctetstreamCodec implements ContentCodec { debug("OctetstreamCodec parsing", bytes); debug("Parameters", parameters); - const bigEndian = !(parameters.byteSeq?.includes(Endianness.LITTLE_ENDIAN) === true); // default to big endian - let signed = parameters.signed !== "false"; // default to signed + const length = + parameters.length != null + ? parseInt(parameters.length) + : (warn("Missing 'length' parameter necessary for write. I'll do my best"), undefined); + + if (length !== undefined) { + if (isNaN(length) || length < 0) { + throw new Error("'length' parameter must be a non-negative number"); + } + if (length !== bytes.length) { + throw new Error(`Lengths do not match, required: ${length} provided: ${bytes.length}`); + } + } + + let signed = true; // default to signed + if (parameters.signed !== undefined) { + if (parameters.signed !== "true" && parameters.signed !== "false") { + throw new Error("'signed' parameter must be 'true' or 'false'"); + } + signed = parameters.signed === "true"; + } + + let bitLength = schema?.["ex:bitLength"] !== undefined ? parseInt(schema["ex:bitLength"]) : bytes.length * 8; + + if (isNaN(bitLength) || bitLength < 0) { + throw new Error("'ex:bitLength' must be a non-negative number"); + } + const offset = schema?.["ex:bitOffset"] !== undefined ? parseInt(schema["ex:bitOffset"]) : 0; - if (parameters.length != null && parseInt(parameters.length) !== bytes.length) { - throw new Error("Lengths do not match, required: " + parameters.length + " provided: " + bytes.length); + + if (isNaN(offset) || offset < 0) { + throw new Error("'ex:bitOffset' must be a non-negative number"); } - let bitLength: number = - schema?.["ex:bitLength"] !== undefined ? parseInt(schema["ex:bitLength"]) : bytes.length * 8; + + const bigEndian = !(parameters.byteSeq?.includes(Endianness.LITTLE_ENDIAN) === true); // default to big endian let dataType: string = schema?.type; if (!dataType) { @@ -214,7 +241,8 @@ export default class OctetstreamCodec implements ContentCodec { const sortedProperties = Object.getOwnPropertyNames(schema.properties); for (const propertyName of sortedProperties) { const propertySchema = schema.properties[propertyName]; - result[propertyName] = this.bytesToValue(bytes, propertySchema, parameters); + const length = bytes.length.toString(); + result[propertyName] = this.bytesToValue(bytes, propertySchema, { ...parameters, length }); } return result; } @@ -222,16 +250,38 @@ export default class OctetstreamCodec implements ContentCodec { valueToBytes(value: unknown, schema?: DataSchema, parameters: { [key: string]: string | undefined } = {}): Buffer { debug(`OctetstreamCodec serializing '${value}'`); - if (parameters.length == null) { - warn("Missing 'length' parameter necessary for write. I'll do my best"); + const bigEndian = !(parameters.byteSeq?.includes(Endianness.LITTLE_ENDIAN) === true); // default to big endian + + let signed = true; // default to true + + if (parameters.signed !== undefined) { + if (parameters.signed !== "true" && parameters.signed !== "false") { + throw new Error("'signed' parameter must be 'true' or 'false'"); + } + signed = parameters.signed === "true"; + } + + let length = + parameters.length != null + ? parseInt(parameters.length) + : (warn("Missing 'length' parameter necessary for write. I'll do my best"), undefined); + + if (length !== undefined && (isNaN(length) || length < 0)) { + throw new Error("'length' parameter must be a non-negative number"); } - const bigEndian = !(parameters.byteSeq?.includes(Endianness.LITTLE_ENDIAN) === true); // default to big endian - let signed = parameters.signed !== "false"; // default to signed - // byte length of the buffer to be returned - let length = parameters.length != null ? parseInt(parameters.length) : undefined; let bitLength = schema?.["ex:bitLength"] !== undefined ? parseInt(schema["ex:bitLength"]) : undefined; + + if (bitLength !== undefined && (isNaN(bitLength) || bitLength < 0)) { + throw new Error("'ex:bitLength' must be a non-negative number"); + } + const offset = schema?.["ex:bitOffset"] !== undefined ? parseInt(schema["ex:bitOffset"]) : 0; + + if (isNaN(offset) || offset < 0) { + throw new Error("'ex:bitOffset' must be a non-negative number"); + } + let dataType: string = schema?.type ?? undefined; if (value === undefined) { @@ -542,7 +592,18 @@ export default class OctetstreamCodec implements ContentCodec { throw new Error("Missing 'length' parameter necessary for write"); } - result = result ?? Buffer.alloc(parseInt(parameters.length)); + const length = parseInt(parameters.length); + const offset = schema["ex:bitOffset"] !== undefined ? parseInt(schema["ex:bitOffset"]) : 0; + + if (isNaN(offset) || offset < 0) { + throw new Error("'ex:bitOffset' must be a non-negative number"); + } + + if (offset > length * 8) { + throw new Error(`'ex:bitOffset' ${offset} exceeds 'length' ${length}`); + } + + result = result ?? Buffer.alloc(length); for (const propertyName in schema.properties) { if (Object.hasOwnProperty.call(value, propertyName) === false) { throw new Error(`Missing property '${propertyName}'`); @@ -557,7 +618,7 @@ export default class OctetstreamCodec implements ContentCodec { } else { buf = this.valueToBytes(propertyValue, propertySchema, parameters); } - this.copyBits(buf, propertyOffset, result, propertyOffset, propertyLength); + this.copyBits(buf, propertyOffset, result, offset + propertyOffset, propertyLength); } return result; } diff --git a/packages/core/test/ContentSerdesTest.ts b/packages/core/test/ContentSerdesTest.ts index 9f88a47ea..d67b460a7 100644 --- a/packages/core/test/ContentSerdesTest.ts +++ b/packages/core/test/ContentSerdesTest.ts @@ -444,6 +444,59 @@ class SerdesOctetTests { }, } ); + + checkStreamToValue( + [0x0e, 0x10, 0x10, 0x10, 0x0e], + { + flags1: { flag1: false, flag2: true }, + flags2: { flag1: true, flag2: false }, + }, + "object", + { + type: "object", + properties: { + flags1: { + type: "object", + "ex:bitOffset": 0, + "ex:bitLength": 8, + properties: { + flag1: { + type: "boolean", + title: "Bit 1", + "ex:bitOffset": 3, + "ex:bitLength": 1, + }, + flag2: { + type: "boolean", + title: "Bit 2", + "ex:bitOffset": 4, + "ex:bitLength": 1, + }, + }, + }, + flags2: { + type: "object", + "ex:bitOffset": 8, + "ex:bitLength": 8, + properties: { + flag1: { + type: "boolean", + title: "Bit 1", + "ex:bitOffset": 3, + "ex:bitLength": 1, + }, + flag2: { + type: "boolean", + title: "Bit 2", + "ex:bitOffset": 4, + "ex:bitLength": 1, + }, + }, + }, + }, + }, + { length: "5" } + ); } @test async "OctetStream to value should throw"() { @@ -531,6 +584,55 @@ class SerdesOctetTests { { type: "uint8" } ) ).to.throw(Error, "Type is unsigned but 'signed' is true"); + + expect(() => + ContentSerdes.contentToValue( + { type: `application/octet-stream;length=test`, body: Buffer.from([0x36]) }, + { type: "integer" } + ) + ).to.throw(Error, "'length' parameter must be a non-negative number"); + + expect(() => + ContentSerdes.contentToValue( + { type: `application/octet-stream;length=-1`, body: Buffer.from([0x36]) }, + { type: "integer" } + ) + ).to.throw(Error, "'length' parameter must be a non-negative number"); + + expect(() => + ContentSerdes.contentToValue( + { type: `application/octet-stream;signed=invalid`, body: Buffer.from([0x36]) }, + { type: "integer" } + ) + ).to.throw(Error, "'signed' parameter must be 'true' or 'false'"); + + expect(() => + ContentSerdes.contentToValue( + { type: `application/octet-stream`, body: Buffer.from([0x36]) }, + { type: "integer", "ex:bitOffset": "invalid" } + ) + ).to.throw(Error, "'ex:bitOffset' must be a non-negative number"); + + expect(() => + ContentSerdes.contentToValue( + { type: `application/octet-stream`, body: Buffer.from([0x36]) }, + { type: "integer", "ex:bitOffset": -1 } + ) + ).to.throw(Error, "'ex:bitOffset' must be a non-negative number"); + + expect(() => + ContentSerdes.contentToValue( + { type: `application/octet-stream`, body: Buffer.from([0x36]) }, + { type: "integer", "ex:bitLength": "invalid" } + ) + ).to.throw(Error, "'ex:bitLength' must be a non-negative number"); + + expect(() => + ContentSerdes.contentToValue( + { type: `application/octet-stream`, body: Buffer.from([0x36]) }, + { type: "integer", "ex:bitLength": -1 } + ) + ).to.throw(Error, "'ex:bitLength' must be a non-negative number"); } @test async "value to OctetStream"() { @@ -763,6 +865,39 @@ class SerdesOctetTests { ); body = await content.toBuffer(); expect(body).to.deep.equal(Buffer.from([0xc0])); + + content = ContentSerdes.valueToContent( + { + flags1: { flag1: false, flag2: true }, + flags2: { flag1: true, flag2: false }, + }, + { + type: "object", + properties: { + flags1: { + type: "object", + properties: { + flag1: { type: "boolean", "ex:bitOffset": 3, "ex:bitLength": 1 }, + flag2: { type: "boolean", "ex:bitOffset": 4, "ex:bitLength": 1 }, + }, + "ex:bitLength": 8, + }, + flags2: { + type: "object", + properties: { + flag1: { type: "boolean", "ex:bitOffset": 3, "ex:bitLength": 1 }, + flag2: { type: "boolean", "ex:bitOffset": 4, "ex:bitLength": 1 }, + }, + "ex:bitOffset": 8, + "ex:bitLength": 8, + }, + }, + "ex:bitLength": 16, + }, + "application/octet-stream;length=2;" + ); + body = await content.toBuffer(); + expect(body).to.deep.equal(Buffer.from([0x08, 0x10])); } @test "value to OctetStream should throw"() { @@ -851,6 +986,29 @@ class SerdesOctetTests { Error, "Missing 'type' property in schema" ); + expect(() => ContentSerdes.valueToContent(10, { type: "int8" }, "application/octet-stream;signed=8")).to.throw( + Error, + "'signed' parameter must be 'true' or 'false'" + ); + expect(() => + ContentSerdes.valueToContent(10, { type: "int8" }, "application/octet-stream;length=-1;") + ).to.throw(Error, "'length' parameter must be a non-negative number"); + expect(() => ContentSerdes.valueToContent(10, { type: "int8" }, "application/octet-stream;length=x;")).to.throw( + Error, + "'length' parameter must be a non-negative number" + ); + expect(() => + ContentSerdes.valueToContent(10, { type: "integer", "ex:bitOffset": -16 }, "application/octet-stream") + ).to.throw(Error, "'ex:bitOffset' must be a non-negative number"); + expect(() => + ContentSerdes.valueToContent(10, { type: "integer", "ex:bitOffset": "foo" }, "application/octet-stream") + ).to.throw(Error, "'ex:bitOffset' must be a non-negative number"); + expect(() => + ContentSerdes.valueToContent(10, { type: "integer", "ex:bitLength": -8 }, "application/octet-stream") + ).to.throw(Error, "'ex:bitLength' must be a non-negative number"); + expect(() => + ContentSerdes.valueToContent(10, { type: "integer", "ex:bitLength": "foo" }, "application/octet-stream") + ).to.throw(Error, "'ex:bitLength' must be a non-negative number"); } }