From 0fd864b04c0badca2b8dbb90f01144614c36c37d Mon Sep 17 00:00:00 2001 From: Thomas Wehr Date: Fri, 15 Dec 2023 17:06:55 +0100 Subject: [PATCH] Support Objects in octet-stream codec #1099 (#1125) * feat(core) add bit length and offset to octetstream-codec * fix: remove `console.log` * feat(core) add `object` support to octet-stream codec * fix: statisfy linter * fix: run prettier * fix: statisfy linter * fix: statisfy prettier and linter * chore: add more tests * fix re-vert moving contentType parameters to schema, sort properties before iterating over them * fix: statisfy linter * fix: remove console.debug * fix: use `getOwnPropertyNames` instead of `sort` * fix: simplify `writeBits` * fix: return correct Buffer in `valueToString`, fix: re-add contentType parameter `length` --- packages/core/src/codecs/octetstream-codec.ts | 451 +++++++++--- packages/core/test/ContentSerdesTest.ts | 683 ++++++++++++++++-- 2 files changed, 1011 insertions(+), 123 deletions(-) diff --git a/packages/core/src/codecs/octetstream-codec.ts b/packages/core/src/codecs/octetstream-codec.ts index 97d5c667e..2fa122100 100644 --- a/packages/core/src/codecs/octetstream-codec.ts +++ b/packages/core/src/codecs/octetstream-codec.ts @@ -59,16 +59,19 @@ 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 + const bigEndian = !(parameters.byteSeq?.includes(Endianness.LITTLE_ENDIAN) === true); // default to big endian let signed = parameters.signed !== "false"; // default to signed - - // check length if specified + 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); } + let bitLength: number = + schema?.["ex:bitLength"] !== undefined ? parseInt(schema["ex:bitLength"]) : bytes.length * 8; + let dataType: string = schema?.type; - let dataLength = bytes.length; - let dataType: string = schema ? schema.type : "undefined"; + if (!dataType) { + throw new Error("Missing 'type' property in schema"); + } // Check type specification // according paragraph 3.3.3 of https://datatracker.ietf.org/doc/rfc8927/ @@ -76,17 +79,39 @@ export default class OctetstreamCodec implements ContentCodec { if (/(short|(u)?int(8|16|32)?$|float(16|32|64)?|byte)/.test(dataType.toLowerCase())) { const typeSem = /(u)?(short|int|float|byte)(8|16|32|64)?/.exec(dataType.toLowerCase()); if (typeSem) { - signed = typeSem[1] === undefined; + if (typeSem[1] === "u") { + // compare with schema information + if (parameters?.signed === "true") { + throw new Error("Type is unsigned but 'signed' is true"); + } + // no schema, but type is unsigned + signed = false; + } dataType = typeSem[2]; - dataLength = +typeSem[3] / 8 ?? bytes.length; + if (parseInt(typeSem[3]) !== bitLength) { + throw new Error( + `Type is '${(typeSem[1] ?? "") + typeSem[2] + typeSem[3]}' but 'ex:bitLength' is ` + bitLength + ); + } } } + if (bitLength > bytes.length * 8 - offset) { + throw new Error( + `'ex:bitLength' is ${bitLength}, but buffer length at offset ${offset} is ${bytes.length * 8 - offset}` + ); + } + // Handle byte swapping - if (parameters.byteSeq?.includes("BYTE_SWAP") === true && dataLength > 1) { + if (parameters?.byteSeq?.includes("BYTE_SWAP") === true && bytes.length > 1) { bytes.swap16(); } + if (offset !== undefined && bitLength < bytes.length * 8) { + bytes = this.readBits(bytes, offset, bitLength); + bitLength = bytes.length * 8; + } + // determine return type switch (dataType) { case "boolean": @@ -96,18 +121,21 @@ export default class OctetstreamCodec implements ContentCodec { case "short": case "int": case "integer": - return this.integerToValue(bytes, { dataLength, bigendian, signed }); + return this.integerToValue(bytes, { dataLength: bitLength, bigEndian, signed }); case "float": case "double": case "number": - return this.numberToValue(bytes, { dataLength, bigendian }); + return this.numberToValue(bytes, { dataLength: bitLength, bigEndian }); case "string": return bytes.toString(parameters.charset as BufferEncoding); - case "array": case "object": - throw new Error("Unable to handle dataType " + dataType); + if (schema === undefined || schema.properties === undefined) { + throw new Error("Missing schema for object"); + } + return this.objectToValue(bytes, schema, parameters); case "null": return null; + case "array": default: throw new Error("Unable to handle dataType " + dataType); } @@ -115,14 +143,15 @@ export default class OctetstreamCodec implements ContentCodec { private integerToValue( bytes: Buffer, - options: { dataLength: number; bigendian: boolean; signed: boolean } + options: { dataLength: number; bigEndian: boolean; signed: boolean } ): number { - const { dataLength, bigendian, signed } = options; + const { dataLength, bigEndian, signed } = options; + switch (dataLength) { - case 1: + case 8: return signed ? bytes.readInt8(0) : bytes.readUInt8(0); - case 2: - return bigendian + case 16: + return bigEndian ? signed ? bytes.readInt16BE(0) : bytes.readUInt16BE(0) @@ -130,8 +159,8 @@ export default class OctetstreamCodec implements ContentCodec { ? bytes.readInt16LE(0) : bytes.readUInt16LE(0); - case 4: - return bigendian + case 32: + return bigEndian ? signed ? bytes.readInt32BE(0) : bytes.readUInt32BE(0) @@ -140,47 +169,56 @@ export default class OctetstreamCodec implements ContentCodec { : bytes.readUInt32LE(0); default: { - let result = 0; - let negative; - - if (bigendian) { - result = bytes.reduce((prev, curr) => prev << (8 + curr)); - negative = bytes.readInt8(0) < 0; - } else { - result = bytes.reduceRight((prev, curr) => prev << (8 + curr)); - negative = bytes.readInt8(dataLength - 1) < 0; - } - - if (signed && negative) { - result -= 1 << (8 * dataLength); - } - + const result = bigEndian + ? signed + ? bytes.readIntBE(0, dataLength / 8) + : bytes.readUIntBE(0, dataLength / 8) + : signed + ? bytes.readIntLE(0, dataLength / 8) + : bytes.readUIntLE(0, dataLength / 8); // warn about numbers being too big to be represented as safe integers if (!Number.isSafeInteger(result)) { warn("Result is not a safe integer"); } - return result; } } } - private numberToValue(bytes: Buffer, options: { dataLength: number; bigendian: boolean }): number { - const { dataLength, bigendian } = options; + private numberToValue(bytes: Buffer, options: { dataLength: number; bigEndian: boolean }): number { + const { dataLength, bigEndian } = options; switch (dataLength) { - case 2: - return getFloat16(new DataView(bytes.buffer), bytes.byteOffset, !bigendian); - case 4: - return bigendian ? bytes.readFloatBE(0) : bytes.readFloatLE(0); + case 16: + return getFloat16(new DataView(bytes.buffer), bytes.byteOffset, !bigEndian); + case 32: + return bigEndian ? bytes.readFloatBE(0) : bytes.readFloatLE(0); - case 8: - return bigendian ? bytes.readDoubleBE(0) : bytes.readDoubleLE(0); + case 64: + return bigEndian ? bytes.readDoubleBE(0) : bytes.readDoubleLE(0); default: - throw new Error("Wrong buffer length for type 'number', must be 2, 4, 8, or is " + dataLength); + throw new Error("Wrong buffer length for type 'number', must be 16, 32, or 64 is " + dataLength); } } + private objectToValue( + bytes: Buffer, + schema?: DataSchema, + parameters: { [key: string]: string | undefined } = {} + ): DataSchemaValue { + if (schema?.type !== "object") { + throw new Error("Schema must be of type 'object'"); + } + + const result: { [key: string]: unknown } = {}; + const sortedProperties = Object.getOwnPropertyNames(schema.properties); + for (const propertyName of sortedProperties) { + const propertySchema = schema.properties[propertyName]; + result[propertyName] = this.bytesToValue(bytes, propertySchema, parameters); + } + return result; + } + valueToBytes(value: unknown, schema?: DataSchema, parameters: { [key: string]: string | undefined } = {}): Buffer { debug(`OctetstreamCodec serializing '${value}'`); @@ -188,15 +226,21 @@ export default class OctetstreamCodec implements ContentCodec { warn("Missing 'length' parameter necessary for write. I'll do my best"); } - const bigendian = !(parameters.byteSeq?.includes(Endianness.LITTLE_ENDIAN) === true); // default to bigendian - let signed = parameters.signed !== "false"; // if signed is undefined -> true (default) + 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; + const offset = schema?.["ex:bitOffset"] !== undefined ? parseInt(schema["ex:bitOffset"]) : 0; + let dataType: string = schema?.type ?? undefined; if (value === undefined) { throw new Error("Undefined value"); } - let dataType: string = schema ? schema.type : "undefined"; + if (dataType === undefined) { + throw new Error("Missing 'type' property in schema"); + } // Check type specification // according paragraph 3.3.3 of https://datatracker.ietf.org/doc/rfc8927/ @@ -204,34 +248,93 @@ export default class OctetstreamCodec implements ContentCodec { if (/(short|(u)?int(8|16|32)?$|float(16|32|64)?|byte)/.test(dataType.toLowerCase())) { const typeSem = /(u)?(short|int|float|byte)(8|16|32|64)?/.exec(dataType.toLowerCase()); if (typeSem) { - signed = typeSem[1] === undefined; + if (typeSem[1] === "u") { + // compare with schema information + if (parameters?.signed === "true") { + throw new Error("Type is unsigned but 'signed' is true"); + } + // no schema, but type is unsigned + signed = false; + } dataType = typeSem[2]; - length = +typeSem[3] / 8 ?? length; + if (bitLength !== undefined) { + if (parseInt(typeSem[3]) !== bitLength) { + throw new Error( + `Type is '${(typeSem[1] ?? "") + typeSem[2] + typeSem[3]}' but 'ex:bitLength' is ` + + bitLength + ); + } + } else { + bitLength = +typeSem[3]; + } + } + } + + // determine buffer length + if (length === undefined) { + if (bitLength !== undefined) { + length = Math.ceil((offset + bitLength) / 8); + } + warn("Missing 'length' parameter necessary for write. I'll do my best"); + } else { + if (bitLength === undefined) { + bitLength = length * 8; + } else { + if (length * 8 < bitLength + offset) { + throw new Error("Length is too short for 'ex:bitLength' and 'ex:bitOffset'"); + } } } switch (dataType) { case "boolean": - return Buffer.alloc(length ?? 1, value != null ? 255 : 0); + if (value === true) { + // Write 1's to bits at offset to offset + bitLength + const buf = Buffer.alloc(length ?? 1, 0); + for (let i = offset; i < offset + (bitLength ?? buf.length * 8); ++i) { + buf[Math.floor(i / 8)] |= 1 << (7 - (i % 8)); + } + return buf; + } else { + return Buffer.alloc(length ?? 1, 0); + } case "byte": case "short": case "int": case "integer": return this.valueToInteger(value, { - dataLength: length, - bigendian, + bitLength, + byteLength: length, + bigEndian, + offset, signed, byteSeq: parameters.byteSeq ?? "", }); case "float": case "number": - return this.valueToNumber(value, { dataLength: length, bigendian, byteSeq: parameters.byteSeq ?? "" }); + return this.valueToNumber(value, { + bitLength, + byteLength: length, + bigEndian, + offset, + byteSeq: parameters.byteSeq ?? "", + }); case "string": { - const str = String(value); - return Buffer.from(str /*, params.charset */); + return this.valueToString(value, { + bitLength, + byteLength: length, + offset, + charset: parameters.charset ?? "utf8", + }); } - case "array": case "object": + if (schema === undefined || schema.properties === undefined) { + throw new Error("Missing schema for object"); + } + return value === null + ? Buffer.alloc(0) + : this.valueToObject(value as { [key: string]: any }, schema, parameters); // eslint-disable-line @typescript-eslint/no-explicit-any + case "array": case "undefined": throw new Error("Unable to handle dataType " + dataType); case "null": @@ -243,10 +346,19 @@ export default class OctetstreamCodec implements ContentCodec { private valueToInteger( value: unknown, - options: { dataLength: number | undefined; bigendian: boolean; signed: boolean; byteSeq: string } + options: { + bitLength: number | undefined; + byteLength: number | undefined; + offset: number | undefined; + bigEndian: boolean; + signed: boolean; + byteSeq: string; + } ): Buffer { - const length = options.dataLength ?? 4; - const { bigendian, signed, byteSeq } = options; + const length = options.bitLength ?? 32; + const offset = options.offset ?? 0; + const byteLength = options.byteLength ?? Math.ceil((offset + length) / 8); + const { bigEndian, signed, byteSeq } = options; if (typeof value !== "number") { throw new Error("Value is not a number"); @@ -256,30 +368,36 @@ export default class OctetstreamCodec implements ContentCodec { if (!Number.isSafeInteger(value)) { warn("Value is not a safe integer", value); } - const limit = Math.pow(2, 8 * length) - 1; + const limit = Math.pow(2, signed ? length - 1 : length) - 1; // throw error on overflow if (signed) { - if (value < -limit || value >= limit) { - throw new Error("Integer overflow when representing signed " + value + " in " + length + " byte(s)"); + if (value < -limit - 1 || value >= limit) { + throw new Error("Integer overflow when representing signed " + value + " in " + length + " bit(s)"); } } else { if (value < 0 || value >= limit) { - throw new Error("Integer overflow when representing unsigned " + value + " in " + length + " byte(s)"); + throw new Error("Integer overflow when representing unsigned " + value + " in " + length + " bit(s)"); } } - const buf = Buffer.alloc(length); + const buf = Buffer.alloc(byteLength); + + if (offset !== 0) { + this.writeBits(buf, value, offset, length, bigEndian); + return buf; + } // Handle byte swapping - if (byteSeq?.includes("BYTE_SwAP") && length > 1) { + + if (byteSeq?.includes("BYTE_SwAP") && byteLength > 1) { buf.swap16(); } - switch (length) { + switch (byteLength) { case 1: signed ? buf.writeInt8(value, 0) : buf.writeUInt8(value, 0); break; case 2: - bigendian + bigEndian ? signed ? buf.writeInt16BE(value, 0) : buf.writeUInt16BE(value, 0) @@ -289,7 +407,7 @@ export default class OctetstreamCodec implements ContentCodec { break; case 4: - bigendian + bigEndian ? signed ? buf.writeInt32BE(value, 0) : buf.writeUInt32BE(value, 0) @@ -305,10 +423,10 @@ export default class OctetstreamCodec implements ContentCodec { } // use arithmetic instead of shift to cover more than 32 bits - for (let i = 0; i < length; ++i) { + for (let i = 0; i < byteLength; ++i) { const byte = value % 0x100; value /= 0x100; - buf.writeInt8(byte, bigendian ? length - i - 1 : i); + buf.writeInt8(byte, bigEndian ? byteLength - i - 1 : i); } } @@ -317,36 +435,205 @@ export default class OctetstreamCodec implements ContentCodec { private valueToNumber( value: unknown, - options: { dataLength: number | undefined; bigendian: boolean; byteSeq: string } + options: { + bitLength: number | undefined; + byteLength: number | undefined; + offset: number | undefined; + bigEndian: boolean; + byteSeq: string; + } ): Buffer { if (typeof value !== "number") { throw new Error("Value is not a number"); } - const length = options.dataLength ?? 8; - const { bigendian, byteSeq } = options; - const buf = Buffer.alloc(length); + const length = options.bitLength ?? (options.byteLength !== undefined ? options.byteLength * 8 : 32); + const offset = options.offset ?? 0; + const { bigEndian, byteSeq } = options; + const byteLength = options.byteLength ?? Math.ceil((offset + length) / 8); + const byteOffset = Math.floor(offset / 8); + const buf = Buffer.alloc(byteLength); + + if (offset % 8 !== 0) { + throw new Error("Offset must be a multiple of 8"); + } // Handle byte swapping - if (byteSeq && length > 1) { + if (byteSeq && byteLength > 1) { buf.swap16(); } switch (length) { - case 2: - setFloat16(new DataView(buf.buffer), 0, value, !bigendian); + case 16: + setFloat16(new DataView(buf.buffer), byteOffset, value, !bigEndian); break; - case 4: - bigendian ? buf.writeFloatBE(value, 0) : buf.writeFloatLE(value, 0); + case 32: + bigEndian ? buf.writeFloatBE(value, byteOffset) : buf.writeFloatLE(value, 0); break; - case 8: - bigendian ? buf.writeDoubleBE(value, 0) : buf.writeDoubleLE(value, 0); + case 64: + bigEndian ? buf.writeDoubleBE(value, byteOffset) : buf.writeDoubleLE(value, 0); break; default: - throw new Error("Wrong buffer length for type 'number', must be 4 or 8, is " + length); + throw new Error("Wrong buffer length for type 'number', must be 16, 32, or 64 is " + length); } return buf; } + + private valueToString( + value: unknown, + options: { + bitLength: number | undefined; + byteLength: number | undefined; + offset: number | undefined; + charset: string; + } + ): Buffer { + if (typeof value !== "string") { + throw new Error("Value is not a string"); + } + + const offset = options.offset ?? 0; + const { charset } = options; + + const str = String(value); + // Check if charset is BufferEncoding + if (!Buffer.isEncoding(charset)) { + throw new Error("Invalid charset " + charset); + } + + const buf = Buffer.from(str, charset); + const bitLength = options.bitLength ?? buf.length * 8; + if (buf.length > bitLength) { + throw new Error(`String is ${buf.length * 8} bits long, but 'ex:bitLength' is ${bitLength}`); + } + + // write string to buffer at offset + const byteLength = options.byteLength ?? Math.ceil((offset + bitLength) / 8); + if (offset % 8 === 0) { + return Buffer.concat([Buffer.alloc(byteLength - bitLength / 8), buf]); + } else { + const buffer = Buffer.alloc(byteLength); + this.copyBits(buf, 0, buffer, offset, bitLength); + return buffer; + } + } + + private valueToObject( + value: { [key: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any + schema: DataSchema, + parameters: { [key: string]: string | undefined } = {}, + result?: Buffer | undefined + ): Buffer { + if (typeof value !== "object" || value === null) { + throw new Error("Value is not an object"); + } + + if (parameters.length === undefined) { + throw new Error("Missing 'length' parameter necessary for write"); + } + + result = result ?? Buffer.alloc(parseInt(parameters.length)); + for (const propertyName in schema.properties) { + if (Object.hasOwnProperty.call(value, propertyName) === false) { + throw new Error(`Missing property '${propertyName}'`); + } + const propertySchema = schema.properties[propertyName]; + const propertyValue = value[propertyName]; + const propertyOffset = parseInt(propertySchema["ex:bitOffset"]); + const propertyLength = parseInt(propertySchema["ex:bitLength"]); + let buf: Buffer; + if (propertySchema.type === "object") { + buf = this.valueToObject(propertyValue, propertySchema, parameters, result); + } else { + buf = this.valueToBytes(propertyValue, propertySchema, parameters); + } + this.copyBits(buf, propertyOffset, result, propertyOffset, propertyLength); + } + return result; + } + + private readBits(buffer: Buffer, bitOffset: number, bitLength: number) { + if (bitOffset < 0) { + throw new Error("bitOffset must be >= 0"); + } + + if (bitLength < 0) { + throw new Error("bitLength must be >= 0"); + } + + if (bitOffset + bitLength > buffer.length * 8) { + throw new Error("bitOffset + bitLength must be <= buffer.length * 8"); + } + + // Convert the result to a Buffer of the correct length. + const resultBuffer = Buffer.alloc(Math.ceil(bitLength / 8)); + + let byteOffset = Math.floor(bitOffset / 8); + let bitOffsetInByte = bitOffset % 8; + let targetByte = buffer[byteOffset]; + let result = 0; + let resultOffset = 0; + + for (let i = 0; i < bitLength; i++) { + const bit = (targetByte >> (7 - bitOffsetInByte)) & 0x01; + result = (result << 1) | bit; + bitOffsetInByte++; + + if (bitOffsetInByte > 7) { + byteOffset++; + bitOffsetInByte = 0; + targetByte = buffer[byteOffset]; + } + + // Write full bytes. + if (i + 1 === bitLength % 8 || (i + 1) % 8 === bitLength % 8 || i === bitLength - 1) { + resultBuffer[resultOffset] = result; + result = 0; + resultOffset++; + } + } + + return resultBuffer; + } + + private writeBits(buffer: Buffer, value: number, offsetBits: number, length: number, bigEndian: boolean) { + let byteIndex = Math.floor(offsetBits / 8); + let bitIndex = offsetBits % 8; + + for (let i = 0; i < length; i++) { + const bitValue = bigEndian ? (value >> (length - 1 - i)) & 1 : (value >> i) & 1; + buffer[byteIndex] |= bitValue << (bigEndian ? 7 - bitIndex : bitIndex); + + bitIndex++; + if (bitIndex === 8) { + bitIndex = 0; + byteIndex++; + } + } + } + + private copyBits( + source: Buffer, + sourceBitOffset: number, + target: Buffer, + targetBitOffset: number, + bitLength: number + ) { + if (sourceBitOffset % 8 === 0 && targetBitOffset % 8 === 0 && bitLength % 8 === 0) { + source.copy(target, targetBitOffset / 8, sourceBitOffset / 8, sourceBitOffset + bitLength / 8); + } else { + const bits = this.readBits(source, sourceBitOffset, bitLength); + if (bits.length <= 6) { + this.writeBits(target, bits.readUIntBE(0, bits.length), targetBitOffset, bitLength, true); + } else { + // iterate over bytes and write them to the buffer + for (let i = 0; i < bits.length; i++) { + const byte = bits.readUInt8(i); + this.writeBits(target, byte, targetBitOffset + i * 8, 8, true); + } + } + } + } } diff --git a/packages/core/test/ContentSerdesTest.ts b/packages/core/test/ContentSerdesTest.ts index 841adf19d..ed64af90e 100644 --- a/packages/core/test/ContentSerdesTest.ts +++ b/packages/core/test/ContentSerdesTest.ts @@ -20,7 +20,7 @@ import { suite, test } from "@testdeck/mocha"; import { expect, should } from "chai"; -import { DataSchemaValue } from "wot-typescript-definitions"; +import { DataSchema, DataSchemaValue } from "wot-typescript-definitions"; import cbor from "cbor"; import ContentSerdes, { ContentCodec } from "../src/content-serdes"; @@ -56,15 +56,22 @@ const checkJsToCbor = async (value: DataSchemaValue) => { expect(reparsed).to.deep.equal(value); }; -const checkStreamToValue = (value: number[], match: unknown, type: string, endianness?: string): void => { +const checkStreamToValue = ( + value: number[], + match: unknown, + type: string, + schema?: DataSchema, + parameters: { [key: string]: string | undefined } = {} +): void => { const octectBuffer = Buffer.from(value); + // append parameters to content type + const contentType = `application/octet-stream;${Object.keys(parameters) + .map((key) => `${key}=${parameters[key]}`) + .join(";")}`; expect( ContentSerdes.contentToValue( - { - type: `application/octet-stream${endianness != null ? `;byteSeq=${endianness}` : ""}`, - body: octectBuffer, - }, - { type: type ?? "integer", properties: {} } + { type: contentType, body: octectBuffer }, + { type: type ?? "integer", properties: {}, ...schema } ) ).to.deep.equal(match); }; @@ -88,75 +95,444 @@ class HodorCodec implements ContentCodec { class SerdesOctetTests { @test "OctetStream to value"() { checkStreamToValue([0x36, 0x30], 13872, "uint16"); - checkStreamToValue([0x36, 0x30], 13872, "uint16", Endianness.BIG_ENDIAN); - checkStreamToValue([0x30, 0x36], 13872, "uint16", Endianness.LITTLE_ENDIAN); + checkStreamToValue([0x36, 0x30], 13872, "uint16", {}, { byteSeq: Endianness.BIG_ENDIAN }); + checkStreamToValue([0x30, 0x36], 13872, "uint16", {}, { byteSeq: Endianness.LITTLE_ENDIAN }); checkStreamToValue([0x49, 0x91, 0xa1, 0xc2], 1234280898, "int32"); - checkStreamToValue([0x49, 0x91, 0xa1, 0xc2], 1234280898, "int32", Endianness.BIG_ENDIAN); - checkStreamToValue([0xc2, 0xa1, 0x91, 0x49], 1234280898, "int32", Endianness.LITTLE_ENDIAN); - checkStreamToValue([0xa1, 0xc2, 0x49, 0x91], 1234280898, "int32", Endianness.LITTLE_ENDIAN_BYTE_SWAP); - checkStreamToValue([0x91, 0x49, 0xc2, 0xa1], 1234280898, "int32", Endianness.BIG_ENDIAN_BYTE_SWAP); + checkStreamToValue([0x49, 0x91, 0xa1, 0xc2], 1234280898, "int32", {}, { byteSeq: Endianness.BIG_ENDIAN }); + checkStreamToValue([0xc2, 0xa1, 0x91, 0x49], 1234280898, "int32", {}, { byteSeq: Endianness.LITTLE_ENDIAN }); + checkStreamToValue( + [0xa1, 0xc2, 0x49, 0x91], + 1234280898, + "int32", + {}, + { + byteSeq: Endianness.LITTLE_ENDIAN_BYTE_SWAP, + } + ); + checkStreamToValue( + [0x91, 0x49, 0xc2, 0xa1], + 1234280898, + "int32", + {}, + { byteSeq: Endianness.BIG_ENDIAN_BYTE_SWAP } + ); checkStreamToValue([0x3d, 0xd6, 0xea, 0xfc], 0.10494038462638855, "float32"); - checkStreamToValue([0x3d, 0xd6, 0xea, 0xfc], 0.10494038462638855, "float32", Endianness.BIG_ENDIAN); - checkStreamToValue([0xfc, 0xea, 0xd6, 0x3d], 0.10494038462638855, "float32", Endianness.LITTLE_ENDIAN); - checkStreamToValue([0xd6, 0x3d, 0xfc, 0xea], 0.10494038462638855, "float32", Endianness.BIG_ENDIAN_BYTE_SWAP); + checkStreamToValue( + [0x3d, 0xd6, 0xea, 0xfc], + 0.10494038462638855, + "float32", + {}, + { + byteSeq: Endianness.BIG_ENDIAN, + } + ); + checkStreamToValue( + [0xfc, 0xea, 0xd6, 0x3d], + 0.10494038462638855, + "float32", + {}, + { + byteSeq: Endianness.LITTLE_ENDIAN, + } + ); + checkStreamToValue( + [0xd6, 0x3d, 0xfc, 0xea], + 0.10494038462638855, + "float32", + {}, + { + byteSeq: Endianness.BIG_ENDIAN_BYTE_SWAP, + } + ); checkStreamToValue( [0xea, 0xfc, 0x3d, 0xd6], 0.10494038462638855, "float32", - Endianness.LITTLE_ENDIAN_BYTE_SWAP + {}, + { + byteSeq: Endianness.LITTLE_ENDIAN_BYTE_SWAP, + } ); checkStreamToValue([0x49, 0x25], 18725, "int16"); - checkStreamToValue([0x49, 0x25], 18725, "int16", Endianness.BIG_ENDIAN); - checkStreamToValue([0x25, 0x49], 18725, "int16", Endianness.LITTLE_ENDIAN); + checkStreamToValue([0x49, 0x25], 18725, "int16", {}, { byteSeq: Endianness.BIG_ENDIAN }); + checkStreamToValue([0x25, 0x49], 18725, "int16", {}, { byteSeq: Endianness.LITTLE_ENDIAN }); checkStreamToValue([0x49, 0x25], 18725, "integer"); - checkStreamToValue([0x49, 0x25], 18725, "integer", Endianness.BIG_ENDIAN); - checkStreamToValue([0x25, 0x49], 18725, "integer", Endianness.LITTLE_ENDIAN); + checkStreamToValue([0x49, 0x25], 18725, "integer", {}, { byteSeq: Endianness.BIG_ENDIAN }); + checkStreamToValue([0x25, 0x49], 18725, "integer", {}, { byteSeq: Endianness.LITTLE_ENDIAN }); checkStreamToValue([0xa4, 0x78], -23432, "int16"); - checkStreamToValue([0xa4, 0x78], -23432, "int16", Endianness.BIG_ENDIAN); - checkStreamToValue([0x78, 0xa4], -23432, "int16", Endianness.LITTLE_ENDIAN); + checkStreamToValue([0xa4, 0x78], -23432, "int16", {}, { byteSeq: Endianness.BIG_ENDIAN }); + checkStreamToValue([0x78, 0xa4], -23432, "int16", {}, { byteSeq: Endianness.LITTLE_ENDIAN }); checkStreamToValue([0xeb, 0xe6, 0x90, 0x49], -5.5746861179443064e26, "number"); - checkStreamToValue([0xeb, 0xe6, 0x90, 0x49], -5.5746861179443064e26, "number", Endianness.BIG_ENDIAN); - checkStreamToValue([0x49, 0x90, 0xe6, 0xeb], -5.5746861179443064e26, "number", Endianness.LITTLE_ENDIAN); - checkStreamToValue([0xe6, 0xeb, 0x49, 0x90], -5.5746861179443064e26, "number", Endianness.BIG_ENDIAN_BYTE_SWAP); + checkStreamToValue( + [0xeb, 0xe6, 0x90, 0x49], + -5.5746861179443064e26, + "number", + {}, + { + byteSeq: Endianness.BIG_ENDIAN, + } + ); + checkStreamToValue( + [0x49, 0x90, 0xe6, 0xeb], + -5.5746861179443064e26, + "number", + {}, + { + byteSeq: Endianness.LITTLE_ENDIAN, + } + ); + checkStreamToValue( + [0xe6, 0xeb, 0x49, 0x90], + -5.5746861179443064e26, + "number", + {}, + { + byteSeq: Endianness.BIG_ENDIAN_BYTE_SWAP, + } + ); checkStreamToValue( [0x90, 0x49, 0xeb, 0xe6], -5.5746861179443064e26, "number", - Endianness.LITTLE_ENDIAN_BYTE_SWAP + {}, + { + byteSeq: Endianness.LITTLE_ENDIAN_BYTE_SWAP, + } ); checkStreamToValue([0x44, 0x80], 4.5, "float16"); - checkStreamToValue([0x44, 0x80], 4.5, "float16", Endianness.BIG_ENDIAN); - checkStreamToValue([0x80, 0x44], 4.5, "float16", Endianness.LITTLE_ENDIAN); + checkStreamToValue([0x44, 0x80], 4.5, "float16", {}, { byteSeq: Endianness.BIG_ENDIAN }); + checkStreamToValue([0x80, 0x44], 4.5, "float16", {}, { byteSeq: Endianness.LITTLE_ENDIAN }); checkStreamToValue([0xeb, 0xe6, 0x90, 0x49], -5.5746861179443064e26, "float32"); - checkStreamToValue([0xeb, 0xe6, 0x90, 0x49], -5.5746861179443064e26, "float32", Endianness.BIG_ENDIAN); - checkStreamToValue([0x49, 0x90, 0xe6, 0xeb], -5.5746861179443064e26, "float32", Endianness.LITTLE_ENDIAN); + checkStreamToValue( + [0xeb, 0xe6, 0x90, 0x49], + -5.5746861179443064e26, + "float32", + {}, + { + byteSeq: Endianness.BIG_ENDIAN, + } + ); + checkStreamToValue( + [0x49, 0x90, 0xe6, 0xeb], + -5.5746861179443064e26, + "float32", + {}, + { + byteSeq: Endianness.LITTLE_ENDIAN, + } + ); checkStreamToValue( [0xe6, 0xeb, 0x49, 0x90], -5.5746861179443064e26, "float32", - Endianness.BIG_ENDIAN_BYTE_SWAP + {}, + { + byteSeq: Endianness.BIG_ENDIAN_BYTE_SWAP, + } ); checkStreamToValue( [0x90, 0x49, 0xeb, 0xe6], -5.5746861179443064e26, "float32", - Endianness.LITTLE_ENDIAN_BYTE_SWAP + {}, + { + byteSeq: Endianness.LITTLE_ENDIAN_BYTE_SWAP, + } ); checkStreamToValue([0xd3, 0xcd, 0xcc, 0xcc, 0xc1, 0xb4, 0x82, 0x70], -4.9728447076484896e95, "float64"); checkStreamToValue( [0xd3, 0xcd, 0xcc, 0xcc, 0xc1, 0xb4, 0x82, 0x70], -4.9728447076484896e95, "float64", - Endianness.BIG_ENDIAN + {}, + { + byteSeq: Endianness.BIG_ENDIAN, + } ); checkStreamToValue( [0x70, 0x82, 0xb4, 0xc1, 0xcc, 0xcc, 0xcd, 0xd3], -4.9728447076484896e95, "float64", - Endianness.LITTLE_ENDIAN + {}, + { + byteSeq: Endianness.LITTLE_ENDIAN, + } + ); + // 0011 0110 0011 0000 -> 0011 + checkStreamToValue([0x36, 0x30], 3, "integer", { "ex:bitOffset": 0, "ex:bitLength": 4 }); + + // 0011 0000 0011 0110 -> 0011 + checkStreamToValue( + [0x30, 0x36], + 3, + "integer", + { + "ex:bitOffset": 8, + "ex:bitLength": 4, + }, + { byteSeq: Endianness.LITTLE_ENDIAN } + ); + + // 0011 0110 0011 0000 -> 0 0110 + checkStreamToValue([0x36, 0x30], 6, "integer", { "ex:bitOffset": 0, "ex:bitLength": 5 }); + + // 0011 0110 0011 0000 -> 011 0001 + checkStreamToValue([0x36, 0x30], 49, "integer", { "ex:bitOffset": 4, "ex:bitLength": 7 }); + + // 0011 0110 0011 0000 -> 0110 0011 + checkStreamToValue([0x36, 0x30], 99, "integer", { "ex:bitOffset": 4, "ex:bitLength": 8 }); + + // 0011 0110 0011 0000 -> 001 1011 0001 + checkStreamToValue([0x36, 0x30], 433, "integer", { "ex:bitOffset": 0, "ex:bitLength": 11 }); + + // 1110 1011 1110 0110 1001 0000 0100 1001 -> 111 1001 1010 0100 0001 + checkStreamToValue([0xeb, 0xe6, 0x90, 0x49], 498241, "integer", { "ex:bitOffset": 7, "ex:bitLength": 19 }); + + // 1110 1011 1110 0110 1001 0000 0100 1001 -> 111 1001 1010 0100 0001 + checkStreamToValue( + [0xeb, 0xe6, 0x90, 0x49], + 4299271, + "integer", + { + "ex:bitOffset": 7, + "ex:bitLength": 19, + }, + { byteSeq: Endianness.LITTLE_ENDIAN } + ); + + // 0011 0110 0011 0000 -> 1101 1000 + checkStreamToValue([0x36, 0x30], 216, "uint8", { "ex:bitOffset": 2, "ex:bitLength": 8 }); + checkStreamToValue( + [0x36, 0x30], + 216, + "uint8", + { + "ex:bitOffset": 2, + "ex:bitLength": 8, + }, + { byteSeq: Endianness.BIG_ENDIAN } + ); + // 0011 0000 0011 0110 -> 1100 0000 + checkStreamToValue( + [0x30, 0x36], + 192, + "uint8", + { + "ex:bitOffset": 2, + "ex:bitLength": 8, + }, + { byteSeq: Endianness.LITTLE_ENDIAN } + ); + + // 0000 0110 1000 1001 0000 0000 -> 0100 0100 1000 0000 + checkStreamToValue([0x06, 0x89, 0x00], 4.5, "float16", { "ex:bitOffset": 7, "ex:bitLength": 16 }); + + // 1100 1110 1011 1110 0110 1001 0000 0100 1001 1101 -> 1110 1011 1110 0110 1001 0000 0100 1001 + // CE BE 69 04 9D -> Eb E6 90 49 + checkStreamToValue([0xce, 0xbe, 0x69, 0x04, 0x9d], -5.5746861179443064e26, "float32", { + "ex:bitOffset": 4, + "ex:bitLength": 32, + }); + + // 0011 1110 1001 1110 0110 1110 0110 0110 0110 0110 0000 1101 1010 0100 0001 0011 1000 0111 + // -> 1101 0011 1100 1101 1100 1100 1100 1100 1100 0001 1011 0100 1000 0010 0111 0000 + // 3E 9E 6E 66 66 0D A4 13 87 -> D3 CD CC CC C1 B4 82 70 + checkStreamToValue([0x3e, 0x9e, 0x6e, 0x66, 0x66, 0x0d, 0xa4, 0x13, 0x87], -4.9728447076484896e95, "float64", { + "ex:bitOffset": 5, + "ex:bitLength": 64, + }); + + // bit 4 to 36 are the same as a few lines above, so -5.5746861179443064e26 should be the result again + checkStreamToValue([0xce, 0xbe, 0x69, 0x04, 0x9d], -5.5746861179443064e26, "number", { + "ex:bitOffset": 4, + "ex:bitLength": 32, + }); + + // Value verified with https://evanw.github.io/float-toy/ + checkStreamToValue([0xff, 0xff], 0.00012201070785522461, "number", { "ex:bitOffset": 2, "ex:bitLength": 11 }); + + // 0001 0101 0111 0110 0101 0110 0010 0011 -> 101 0111 0110 0101 0110 0010 + checkStreamToValue([0xf5, 0x76, 0x56, 0x23], "Web", "string", { "ex:bitOffset": 5, "ex:bitLength": 23 }); + checkStreamToValue( + [0x01, 0xb1, 0xd7, 0x65, 0x62], + { flag1: true, flag2: false, numberProperty: 99, stringProperty: "Web" }, + "object", + { + type: "object", + properties: { + flag1: { type: "boolean", "ex:bitOffset": 8, "ex:bitLength": 1 }, + flag2: { type: "boolean", "ex:bitOffset": 9, "ex:bitLength": 1 }, + numberProperty: { type: "integer", "ex:bitOffset": 10, "ex:bitLength": 7 }, + stringProperty: { type: "string", "ex:bitOffset": 17, "ex:bitLength": 23 }, + }, + } + ); + checkStreamToValue( + [0x01, 0xb1, 0xd7, 0x65, 0x62, 0x01, 0xb1, 0xd7, 0x65, 0x62], + { + flag1: true, + flag2: false, + numberProperty: 99, + stringProperty: "Web", + objectProperty: { flag1: true, flag2: false, numberProperty: 99, stringProperty: "Web" }, + }, + "object", + { + type: "object", + properties: { + flag1: { type: "boolean", "ex:bitOffset": 8, "ex:bitLength": 1 }, + flag2: { type: "boolean", "ex:bitOffset": 9, "ex:bitLength": 1 }, + numberProperty: { type: "integer", "ex:bitOffset": 10, "ex:bitLength": 7 }, + stringProperty: { type: "string", "ex:bitOffset": 17, "ex:bitLength": 23 }, + objectProperty: { + type: "object", + "ex:bitOffset": 40, + "ex:bitLength": 40, + properties: { + flag1: { type: "boolean", "ex:bitOffset": 8, "ex:bitLength": 1 }, + flag2: { type: "boolean", "ex:bitOffset": 9, "ex:bitLength": 1 }, + numberProperty: { type: "integer", "ex:bitOffset": 10, "ex:bitLength": 7 }, + stringProperty: { type: "string", "ex:bitOffset": 17, "ex:bitLength": 23 }, + }, + }, + }, + } + ); + checkStreamToValue( + [0xc0], + { deepCascased: { level1: { level2: { level3: { bool: true, level4: { bool: true, bool2: false } } } } } }, + "object", + { + type: "object", + properties: { + deepCascased: { + type: "object", + properties: { + level1: { + type: "object", + properties: { + level2: { + type: "object", + properties: { + level3: { + type: "object", + properties: { + bool: { type: "boolean", "ex:bitOffset": 0, "ex:bitLength": 1 }, + level4: { + type: "object", + properties: { + bool: { + type: "boolean", + "ex:bitOffset": 1, + "ex:bitLength": 1, + }, + bool2: { + type: "boolean", + "ex:bitOffset": 2, + "ex:bitLength": 1, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } ); } + @test async "OctetStream to value should throw"() { + expect(() => + ContentSerdes.contentToValue( + { type: "application/octet-stream", body: Buffer.from([0x36]) }, + { type: "array" } + ) + ).to.throw(Error, "Unable to handle dataType array"); + expect(() => + ContentSerdes.contentToValue( + { type: "application/octet-stream", body: Buffer.from([0x36]) }, + { type: "foo" } + ) + ).to.throw(Error, "Unable to handle dataType foo"); + expect(() => + ContentSerdes.contentToValue( + { type: "application/octet-stream", body: Buffer.from([0x36]) }, + { type: "int8", "ex:bitOffset": 3, "ex:bitLength": 1 } + ) + ).to.throw(Error, "Type is 'int8' but 'ex:bitLength' is 1"); + expect(() => + ContentSerdes.contentToValue( + { type: "application/octet-stream", body: Buffer.from([0x36]) }, + { type: "int8", "ex:bitOffset": 0, "ex:bitLength": 9 } + ) + ).to.throw(Error, "Type is 'int8' but 'ex:bitLength' is 9"); + expect(() => + ContentSerdes.contentToValue( + { type: "application/octet-stream", body: Buffer.from([0x36]) }, + { type: "uint16" } + ) + ).to.throw(Error, "Type is 'uint16' but 'ex:bitLength' is 8"); + expect(() => + ContentSerdes.contentToValue( + { type: "application/octet-stream", body: Buffer.from([0x36, 0x47]) }, + { type: "int32" } + ) + ).to.throw(Error, "Type is 'int32' but 'ex:bitLength' is 16"); + expect(() => + ContentSerdes.contentToValue( + { type: "application/octet-stream", body: Buffer.from([0x36]) }, + { type: "float64" } + ) + ).to.throw(Error, "Type is 'float64' but 'ex:bitLength' is 8"); + expect(() => + ContentSerdes.contentToValue( + { type: "application/octet-stream", body: Buffer.from([0x36]) }, + { type: "integer", "ex:bitOffset": 0, "ex:bitLength": 9 } + ) + ).to.throw(Error, "'ex:bitLength' is 9, but buffer length at offset 0 is 8"); + expect(() => + ContentSerdes.contentToValue( + { type: "application/octet-stream", body: Buffer.from([0x36]) }, + { type: "integer", "ex:bitOffset": 1, "ex:bitLength": 8 } + ) + ).to.throw(Error, "'ex:bitLength' is 8, but buffer length at offset 1 is 7"); + expect(() => + ContentSerdes.contentToValue( + { type: "application/octet-stream", body: Buffer.from([0x36]) }, + { type: "number", "ex:bitLength": 8 } + ) + ).to.throw(Error, "Wrong buffer length for type 'number', must be 16, 32, or 64 is " + 8); + expect(() => + ContentSerdes.contentToValue( + { type: "application/octet-stream", body: Buffer.from([0x36]) }, + { type: "object", "ex:bitOffset": 1, "ex:bitLength": 5 } + ) + ).to.throw(Error, "Missing schema for object"); + expect(() => + ContentSerdes.contentToValue( + { type: "application/octet-stream", body: Buffer.from([0x36, 0x30]) }, + { + type: "object", + properties: { + prop1: { type: "boolean", "ex:bitOffset": 1, "ex:bitLength": 1 }, + prop2: {}, + }, + } + ) + ).to.throw(Error, "Missing 'type' property in schema"); + expect(() => + ContentSerdes.contentToValue( + { type: `application/octet-stream;signed=true;`, body: Buffer.from([0x36]) }, + { type: "uint8" } + ) + ).to.throw(Error, "Type is unsigned but 'signed' is true"); + } + @test async "value to OctetStream"() { let content = ContentSerdes.valueToContent(2345, { type: "integer" }, "application/octet-stream"); let body = await content.toBuffer(); @@ -174,7 +550,7 @@ class SerdesOctetTests { content = ContentSerdes.valueToContent( 2345, { type: "int16" }, - "application/octet-stream;byteSeq=LITTLE_ENDIAN" + `application/octet-stream;byteSeq=${Endianness.LITTLE_ENDIAN};` ); body = await content.toBuffer(); expect(body).to.deep.equal(Buffer.from([0x29, 0x09])); @@ -184,6 +560,10 @@ class SerdesOctetTests { body = await content.toBuffer(); expect(body).to.deep.equal(Buffer.from([0x0a])); + content = ContentSerdes.valueToContent(12342, { type: "uint16" }, "application/octet-stream"); + body = await content.toBuffer(); + expect(body).to.deep.equal(Buffer.from([0x30, 0x36])); + // should serialize a number as a float16 // @ts-ignore new dataschema types are not yet supported in the td type definitions content = ContentSerdes.valueToContent(4.5, { type: "float16" }, "application/octet-stream"); @@ -193,26 +573,247 @@ class SerdesOctetTests { content = ContentSerdes.valueToContent( 4.5, { type: "float16" }, - "application/octet-stream;byteSeq=LITTLE_ENDIAN" + `application/octet-stream;byteSeq=${Endianness.LITTLE_ENDIAN};` ); body = await content.toBuffer(); expect(body).to.deep.equal(Buffer.from([0x80, 0x44])); + + content = ContentSerdes.valueToContent( + 2345, + { type: "integer", "ex:bitLength": 24 }, + "application/octet-stream" + ); + body = await content.toBuffer(); + expect(body).to.deep.equal(Buffer.from([0x00, 0x09, 0x29])); + + content = ContentSerdes.valueToContent( + -32768, + { type: "integer", "ex:bitOffset": 0, "ex:bitLength": 16 }, + "application/octet-stream" + ); + body = await content.toBuffer(); + expect(body).to.deep.equal(Buffer.from([0x80, 0x00])); + + content = ContentSerdes.valueToContent( + -32768, + { type: "integer", "ex:bitOffset": 5, "ex:bitLength": 16 }, + "application/octet-stream" + ); + body = await content.toBuffer(); + expect(body).to.deep.equal(Buffer.from([0x04, 0x00, 0x00])); + + content = ContentSerdes.valueToContent( + -32767, + { type: "integer", "ex:bitOffset": 5, "ex:bitLength": 16 }, + "application/octet-stream" + ); + body = await content.toBuffer(); + expect(body).to.deep.equal(Buffer.from([0x04, 0x00, 0x08])); + + content = ContentSerdes.valueToContent( + -32767, + { type: "integer", "ex:bitOffset": 16, "ex:bitLength": 16 }, + "application/octet-stream" + ); + body = await content.toBuffer(); + expect(body).to.deep.equal(Buffer.from([0x00, 0x00, 0x80, 0x01])); + + content = ContentSerdes.valueToContent( + 4.5, + { type: "float16", "ex:bitOffset": 16, "ex:bitLength": 16 }, + "application/octet-stream" + ); + body = await content.toBuffer(); + expect(body).to.deep.equal(Buffer.from([0x00, 0x00, 0x44, 0x80])); + + content = ContentSerdes.valueToContent( + -4.9728447076484896e95, + { type: "float64", "ex:bitOffset": 16, "ex:bitLength": 64 }, + "application/octet-stream" + ); + body = await content.toBuffer(); + expect(body).to.deep.equal(Buffer.from([0x00, 0x00, 0xd3, 0xcd, 0xcc, 0xcc, 0xc1, 0xb4, 0x82, 0x70])); + + content = ContentSerdes.valueToContent( + "Web", + { type: "string", "ex:bitOffset": 16 }, + "application/octet-stream" + ); + body = await content.toBuffer(); + expect(body).to.deep.equal(Buffer.from([0x00, 0x00, 0x57, 0x65, 0x62])); + + content = ContentSerdes.valueToContent( + "Web", + { type: "string", "ex:bitOffset": 1 }, + "application/octet-stream" + ); + body = await content.toBuffer(); + // Web is 57 65 62 in hex and 01010111 01100101 01100010 in binary + // with offset 1 -> 00101011 10110010 10110001 000000000 + expect(body).to.deep.equal(Buffer.from([0x2b, 0xb2, 0xb1, 0x00])); + + content = ContentSerdes.valueToContent( + { flag1: true, flag2: false, numberProperty: 99, stringProperty: "Web" }, + { + type: "object", + properties: { + flag1: { type: "boolean", "ex:bitOffset": 7, "ex:bitLength": 1 }, + flag2: { type: "boolean", "ex:bitOffset": 8, "ex:bitLength": 1 }, + numberProperty: { type: "integer", "ex:bitOffset": 9, "ex:bitLength": 7 }, + stringProperty: { type: "string", "ex:bitOffset": 16, "ex:bitLength": 24 }, + }, + }, + "application/octet-stream;length=5;signed=false;" + ); + body = await content.toBuffer(); + // properties are: true -> 1, false -> 0, 90 -> 1100011, Web -> 01010111 01100101 01100010 + // resulting bits should be 00000001 01100011 01010111 01100101 01100010 + expect(body).to.deep.equal(Buffer.from([0x01, 0x63, 0x57, 0x65, 0x62])); + + const longString = + "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum."; + content = ContentSerdes.valueToContent( + { longString }, + { + type: "object", + properties: { + longString: { type: "string", "ex:bitOffset": 8, "ex:bitLength": 1696 }, + }, + "ex:bitLength": 1704, + }, + "application/octet-stream;length=213;" + ); + body = await content.toBuffer(); + expect(body).to.deep.equal(Buffer.concat([Buffer.from([0x00]), Buffer.from(longString)])); + + content = ContentSerdes.valueToContent( + { deepCascased: { level1: { level2: { level3: { bool: true, level4: { bool: true, bool2: false } } } } } }, + { + type: "object", + properties: { + deepCascased: { + type: "object", + properties: { + level1: { + type: "object", + properties: { + level2: { + type: "object", + properties: { + level3: { + type: "object", + properties: { + bool: { type: "boolean", "ex:bitOffset": 0, "ex:bitLength": 1 }, + level4: { + type: "object", + properties: { + bool: { + type: "boolean", + "ex:bitOffset": 1, + "ex:bitLength": 1, + }, + bool2: { + type: "boolean", + "ex:bitOffset": 2, + "ex:bitLength": 1, + }, + }, + "ex:bitLength": 2, + }, + }, + "ex:bitLength": 3, + }, + }, + "ex:bitLength": 3, + }, + }, + "ex:bitLength": 3, + }, + }, + "ex:bitLength": 3, + }, + }, + "ex:bitLength": 3, + }, + "application/octet-stream;length=1;" + ); + body = await content.toBuffer(); + expect(body).to.deep.equal(Buffer.from([0xc0])); } @test "value to OctetStream should throw"() { // @ts-ignore new dataschema types are not yet supported in the td type definitions expect(() => ContentSerdes.valueToContent(2345, { type: "int8" }, "application/octet-stream")).to.throw( Error, - "Integer overflow when representing signed 2345 in 1 byte(s)" + "Integer overflow when representing signed 2345 in 8 bit(s)" ); // @ts-ignore new dataschema types are not yet supported in the td type definitions expect(() => ContentSerdes.valueToContent(23450000, { type: "int16" }, "application/octet-stream")).to.throw( Error, - "Integer overflow when representing signed 23450000 in 2 byte(s)" + "Integer overflow when representing signed 23450000 in 16 bit(s)" + ); + expect(() => ContentSerdes.valueToContent(2345, { type: "foo" }, "application/octet-stream")).to.throw( + Error, + "Unable to handle dataType foo" ); - expect(() => ContentSerdes.valueToContent(2345, undefined, "application/octet-stream")).to.throw( + expect(() => + ContentSerdes.valueToContent(10, { type: "uint8" }, "application/octet-stream;signed=true") + ).to.throw(Error, "Type is unsigned but 'signed' is true"); + expect(() => + ContentSerdes.valueToContent(47, { type: "int8", "ex:bitLength": 9 }, "application/octet-stream") + ).to.throw(Error, "Type is 'int8' but 'ex:bitLength' is 9"); + expect(() => + ContentSerdes.valueToContent( + -2345, + { type: "integer", "ex:bitOffset": 0, "ex:bitLength": 10 }, + "application/octet-stream" + ) + ).to.throw(Error, "Integer overflow when representing signed -2345 in 10 bit(s)"); + expect(() => + ContentSerdes.valueToContent( + -32769, + { type: "integer", "ex:bitOffset": 0, "ex:bitLength": 16 }, + "application/octet-stream" + ) + ).to.throw(Error, "Integer overflow when representing signed -32769 in 16 bit(s)"); + expect(() => + ContentSerdes.valueToContent( + { flag1: true, flag2: false, numberProperty: 99, stringProperty: "Web" }, + { + type: "object", + properties: { + flag1: { type: "boolean", "ex:bitOffset": 8, "ex:bitLength": 1 }, + flag2: { type: "boolean", "ex:bitOffset": 9, "ex:bitLength": 1 }, + numberProperty: { type: "integer", "ex:bitOffset": 10, "ex:bitLength": 7 }, + stringProperty: { type: "string", "ex:bitOffset": 17, "ex:bitLength": 23 }, + }, + }, + "application/octet-stream" + ) + ).to.throw(Error, "Missing 'length' parameter necessary for write"); + expect(() => + ContentSerdes.valueToContent( + { intProp: 42 }, + { + type: "object", + properties: { + intProp: { type: "integer", "ex:bitOffset": 0, "ex:bitLength": 8 }, + stringProp: { type: "string", "ex:bitOffset": 8, "ex:bitLength": 8 }, + }, + }, + "application/octet-stream;length=2;" + ) + ).to.throw(Error, "Missing property 'stringProp'"); + expect(() => + ContentSerdes.valueToContent( + undefined as unknown as DataSchemaValue, + { type: "int8" }, + "application/octet-stream;length=1;" + ) + ).to.throw(Error, "Undefined value"); + expect(() => ContentSerdes.valueToContent(10, {}, "application/octet-stream")).to.throw( Error, - "Unable to handle dataType undefined" + "Missing 'type' property in schema" ); } }