UNPKG

@foxglove/rosmsg-serialization

Version:

ROS 1 message serialization, for reading and writing bags and network messages

432 lines (425 loc) 16.5 kB
"use strict"; // This file incorporates work covered by the following copyright and // permission notice: // // Copyright 2018-2021 Cruise LLC // // This source code is licensed under the Apache License, Version 2.0, // found at http://www.apache.org/licenses/LICENSE-2.0 // You may not use this file except in compliance with the License. Object.defineProperty(exports, "__esModule", { value: true }); const rosmsg_1 = require("@foxglove/rosmsg"); const MessageReader_1 = require("./MessageReader"); const MessageWriter_1 = require("./MessageWriter"); const getStringBytes = (str) => { const textData = new TextEncoder().encode(str); const output = new Uint8Array(4 + textData.length); new DataView(output.buffer).setUint32(0, textData.length, true); output.set(textData, 4); return output; }; function concat(list) { const length = list.reduce((sum, entry) => sum + entry.length, 0); const output = new Uint8Array(length); let i = 0; for (const entry of list) { output.set(entry, i); i += entry.length; } return output; } function writeInt8(data, value, offset) { new DataView(data.buffer, data.byteOffset).setInt8(offset, value); } function writeInt16LE(data, value, offset) { new DataView(data.buffer, data.byteOffset).setInt16(offset, value, true); } function writeUInt16LE(data, value, offset) { new DataView(data.buffer, data.byteOffset).setUint16(offset, value, true); } function writeInt32LE(data, value, offset) { new DataView(data.buffer, data.byteOffset).setInt32(offset, value, true); } function writeUInt32LE(data, value, offset) { new DataView(data.buffer, data.byteOffset).setUint32(offset, value, true); } function writeFloatLE(data, value, offset) { new DataView(data.buffer, data.byteOffset).setFloat32(offset, value, true); } function writeDoubleLE(data, value, offset) { new DataView(data.buffer, data.byteOffset).setFloat64(offset, value, true); } describe("MessageWriter", () => { describe("simple type", () => { const testNum = (type, size, expected, cb) => { const data = new Uint8Array(size); const message = { foo: expected }; cb(data); it(`writes message ${JSON.stringify(message)} containing ${type}`, () => { const writer = new MessageWriter_1.MessageWriter((0, rosmsg_1.parse)(`${type} foo`)); expect(writer.writeMessage({ foo: expected, })).toEqual(data); }); }; testNum("int8", 1, -3, (data) => { writeInt8(data, -3, 0); }); testNum("uint8", 1, 13, (data) => { writeInt8(data, 13, 0); }); testNum("int16", 2, -21, (data) => { writeInt16LE(data, -21, 0); }); testNum("uint16", 2, 21, (data) => { writeUInt16LE(data, 21, 0); }); testNum("int32", 4, -210010, (data) => { writeInt32LE(data, -210010, 0); }); testNum("uint32", 4, 210010, (data) => { writeUInt32LE(data, 210010, 0); }); testNum("float32", 4, 5.5, (data) => { writeFloatLE(data, 5.5, 0); }); // eslint-disable-next-line no-loss-of-precision testNum("float64", 8, 0xdeadbeefcafebabe, (data) => { // eslint-disable-next-line no-loss-of-precision writeDoubleLE(data, 0xdeadbeefcafebabe, 0); }); it("writes strings", () => { const writer = new MessageWriter_1.MessageWriter((0, rosmsg_1.parse)("string name")); const buff = getStringBytes("test"); expect(writer.writeMessage({ name: "test" })).toEqual(buff); }); it("writes time", () => { const writer = new MessageWriter_1.MessageWriter((0, rosmsg_1.parse)("time right_now")); const buff = new Uint8Array(8); const now = new Date(); now.setSeconds(31); now.setMilliseconds(0); const seconds = Math.round(now.getTime() / 1000); writeUInt32LE(buff, seconds, 0); writeUInt32LE(buff, 1000000, 4); now.setMilliseconds(1); expect(writer.writeMessage({ right_now: { nsec: 1000000, sec: seconds, }, })).toEqual(buff); }); }); describe("array", () => { it("writes variable length string array", () => { const writer = new MessageWriter_1.MessageWriter((0, rosmsg_1.parse)("string[] names")); const buff = concat([ // variable length array has uint32 as first entry new Uint8Array([0x03, 0x00, 0x00, 0x00]), getStringBytes("foo"), getStringBytes("bar"), getStringBytes("baz"), ]); expect(writer.writeMessage({ names: ["foo", "bar", "baz"], })).toEqual(buff); }); it("writes fixed length arrays", () => { const writer1 = new MessageWriter_1.MessageWriter((0, rosmsg_1.parse)("string[1] names")); const writer2 = new MessageWriter_1.MessageWriter((0, rosmsg_1.parse)("string[2] names")); const writer3 = new MessageWriter_1.MessageWriter((0, rosmsg_1.parse)("string[3] names")); expect(writer1.writeMessage({ names: ["foo", "bar", "baz"], })).toEqual(getStringBytes("foo")); expect(writer2.writeMessage({ names: ["foo", "bar", "baz"], })).toEqual(concat([getStringBytes("foo"), getStringBytes("bar")])); expect(writer3.writeMessage({ names: ["foo", "bar", "baz"], })).toEqual(concat([getStringBytes("foo"), getStringBytes("bar"), getStringBytes("baz")])); }); it("does not write any data for a zero length array", () => { const writer = new MessageWriter_1.MessageWriter((0, rosmsg_1.parse)("string[] names")); const buff = concat([ // variable length array has uint32 as first entry new Uint8Array([0x00, 0x00, 0x00, 0x00]), ]); const resultBuff = writer.writeMessage({ names: [] }); expect(resultBuff).toEqual(buff); }); describe("typed arrays", () => { it("writes a uint8[]", () => { const writer = new MessageWriter_1.MessageWriter((0, rosmsg_1.parse)("uint8[] values\nuint8 after")); const message = { values: Uint8Array.from([1, 2, 3]), after: 4 }; const result = writer.writeMessage(message); const buff = new Uint8Array([0x03, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04]); expect(result).toEqual(buff); }); it("writes a uint8[] with a fixed length", () => { const writer = new MessageWriter_1.MessageWriter((0, rosmsg_1.parse)("uint8[3] values\nuint8 after")); const message = { values: Uint8Array.from([1, 2, 3]), after: 4 }; const result = writer.writeMessage(message); const buff = new Uint8Array([0x01, 0x02, 0x03, 0x04]); expect(result).toEqual(buff); }); }); }); describe("complex types", () => { it("writes single complex type", () => { const writer = new MessageWriter_1.MessageWriter((0, rosmsg_1.parse)("string firstName \n string lastName\nuint16 age")); const buff = concat([ getStringBytes("foo"), getStringBytes("bar"), new Uint8Array([0x05, 0x00]), ]); const message = { firstName: "foo", lastName: "bar", age: 5, }; expect(writer.writeMessage(message)).toEqual(buff); }); it("writes nested complex types", () => { const messageDefinition = ` string username Account account ============ MSG: custom_type/Account string name uint16 id `; const writer = new MessageWriter_1.MessageWriter((0, rosmsg_1.parse)(messageDefinition)); const buff = concat([ getStringBytes("foo"), getStringBytes("bar"), new Uint8Array([100, 0x00]), ]); const message = { username: "foo", account: { name: "bar", id: 100, }, }; expect(writer.writeMessage(message)).toEqual(buff); }); it("writes nested complex types with arrays", () => { const messageDefinition = ` string username Account[] accounts ============ MSG: custom_type/Account string name uint16 id `; const writer = new MessageWriter_1.MessageWriter((0, rosmsg_1.parse)(messageDefinition)); const buff = concat([ getStringBytes("foo"), // uint32LE length of array (2) new Uint8Array([0x02, 0x00, 0x00, 0x00]), getStringBytes("bar"), new Uint8Array([100, 0x00]), getStringBytes("baz"), new Uint8Array([101, 0x00]), ]); const message = { username: "foo", accounts: [ { name: "bar", id: 100, }, { name: "baz", id: 101, }, ], }; expect(writer.writeMessage(message)).toEqual(buff); }); it("writes complex type with nested arrays", () => { const messageDefinition = ` string username Account[] accounts ============ MSG: custom_type/Account string name uint16 id Photo[] photos ======= MSG: custom_type/Photo string url uint8 id `; const writer = new MessageWriter_1.MessageWriter((0, rosmsg_1.parse)(messageDefinition)); const buff = concat([ getStringBytes("foo"), // uint32LE length of Account array (2) new Uint8Array([0x02, 0x00, 0x00, 0x00]), // name getStringBytes("bar"), // id new Uint8Array([100, 0x00]), // uint32LE length of Photo array (3) new Uint8Array([0x03, 0x00, 0x00, 0x00]), // photo url getStringBytes("http://foo.com"), // photo id new Uint8Array([10]), // photo url getStringBytes("http://bar.com"), // photo id new Uint8Array([12]), // photo url getStringBytes("http://zug.com"), // photo id new Uint8Array([16]), // next account getStringBytes("baz"), new Uint8Array([101, 0x00]), // uint32LE length of Photo array (0) new Uint8Array([0x00, 0x00, 0x00, 0x00]), ]); const message = { username: "foo", accounts: [ { name: "bar", id: 100, photos: [ { url: "http://foo.com", id: 10, }, { url: "http://bar.com", id: 12, }, { url: "http://zug.com", id: 16, }, ], }, { name: "baz", id: 101, photos: [], }, ], }; expect(writer.writeMessage(message)).toEqual(buff); }); const withBytesAndBools = ` byte OK=0 byte WARN=1 byte ERROR=2 byte STALE=3 byte FOO = 3 # the space exists in some topics byte FLOAT64 = 8 bool level\t\t# level of operation enumerated above DiagnosticStatus status ================================================================================ MSG: diagnostic_msgs/DiagnosticStatus # This message holds the status of an individual component of the robot. # # Possible levels of operations byte OK=0 byte WARN=1 # Comment # FLOATING OUT HERE byte ERROR=2 byte STALE=3 byte level # level of operation enumerated above string name # a description of the test/component reporting`; it("writes bytes and constants", () => { const writer = new MessageWriter_1.MessageWriter((0, rosmsg_1.parse)(withBytesAndBools)); const buff = concat([new Uint8Array([0x01]), new Uint8Array([0x00]), getStringBytes("foo")]); const message = { level: true, status: { leve: 0, name: "foo", }, }; expect(writer.writeMessage(message)).toEqual(buff); }); }); describe("calculateByteSize", () => { it("with a complex type", () => { const messageDefinition = ` string username Account[] accounts ============ MSG: custom_type/Account string name uint16 id bool isActive Photo[] photos ======= MSG: custom_type/Photo string url uint8[] ids `; const message = { username: "foo", accounts: [ { name: "bar", id: 100, isActive: true, photos: [ { url: "http://foo.com", ids: Uint8Array.from([10, 100]), }, { url: "http://bar.com", ids: Uint8Array.from([12]), }, { url: "http://zug.com", ids: Uint8Array.from([]), }, ], }, { name: "baz", id: 101, isActive: false, photos: [], }, ], }; const writer = new MessageWriter_1.MessageWriter((0, rosmsg_1.parse)(messageDefinition)); expect(writer.calculateByteSize(message)).toEqual(108); }); }); it.each([ "", "a", "ab", "abc", "abcd", "béta", "\xE9", "\u0000", "\u007f", "\u0080", "\u07ff", "\u0800", "\ud800\udc00", // surrogate pair, equivalent to "𐀀" or "\u{10000}" "\udbff\udfff", // surrogate pair, equivalent to "\u{10ffff}" "\u7fff", "\u8000", "\u8001", "\uffff", "\u{10000}", "\u{fffff}", "\u{100000}", "\u{10ffff}", ])("handles non-ascii strings", (str) => { const defs = (0, rosmsg_1.parse)("string data"); const writer = new MessageWriter_1.MessageWriter(defs); const reader = new MessageReader_1.MessageReader(defs); const msg = { data: str }; expect(reader.readMessage(writer.writeMessage(msg))).toEqual(msg); }); it.each([ "\ud800", // lone high surrogate "\udc00", // lone low surrogate ])("replaces lone surrogates with replacement character", (str) => { const defs = (0, rosmsg_1.parse)("string data"); const writer = new MessageWriter_1.MessageWriter(defs); const reader = new MessageReader_1.MessageReader(defs); const msg = { data: str }; expect(reader.readMessage(writer.writeMessage(msg))).toEqual({ data: "\ufffd" }); }); }); //# sourceMappingURL=MessageWriter.test.js.map