UNPKG

rosbag

Version:

`rosbag` is a node.js & browser compatible module for reading [rosbag](http://wiki.ros.org/rosbag) binary data files.

516 lines (460 loc) 17.9 kB
// Copyright (c) 2018-present, Cruise LLC // This source code is licensed under the Apache License, Version 2.0, // found in the LICENSE file in the root directory of this source tree. // You may not use this file except in compliance with the License. import range from "lodash.range"; import util from "util"; import { MessageReader } from "./MessageReader"; import { parseMessageDefinition } from "./parseMessageDefinition"; const getStringBuffer = (str: string) => { const data = Buffer.from(str, "utf8"); const len = Buffer.alloc(4); len.writeInt32LE(data.byteLength, 0); return Buffer.concat([len, data]); }; type ValuesAfterMessage = { values: Uint8Array; after: number; }; const getMessageReader = (messageDefinitions: string, options: { typeName?: string; readerOptions?: object } = {}) => { const typeName = options.typeName || "custom_type/CustomMessage"; const parsedMessageDefinitions = parseMessageDefinition(messageDefinitions, typeName); return new MessageReader(parsedMessageDefinitions, typeName, options.readerOptions); }; describe("MessageReader", () => { describe("simple type", () => { const testNum = (type: string, size: number, expected: any, cb: (buffer: Buffer) => any) => { const buffer = Buffer.alloc(size); cb(buffer); it(`parses buffer ${JSON.stringify(buffer)} containing ${type}`, () => { const reader = getMessageReader(`${type} foo`); expect(reader.readMessage(buffer)).toEqual({ foo: expected, }); }); }; testNum("int8", 1, -3, (buffer) => buffer.writeInt8(-3, 0)); testNum("uint8", 1, 13, (buffer) => buffer.writeInt8(13, 0)); testNum("int16", 2, -21, (buffer) => buffer.writeInt16LE(-21, 0)); testNum("uint16", 2, 21, (buffer) => buffer.writeUInt16LE(21, 0)); testNum("int32", 4, -210010, (buffer) => buffer.writeInt32LE(-210010, 0)); testNum("uint32", 4, 210010, (buffer) => buffer.writeUInt32LE(210010, 0)); testNum("float32", 4, 5.5, (buffer) => buffer.writeFloatLE(5.5, 0)); testNum("float64", 8, 1.7976931348623157e308, (buffer) => buffer.writeDoubleLE(1.7976931348623157e308, 0)); testNum("int64", 8, BigInt(Number.MAX_SAFE_INTEGER), (buffer) => buffer.writeBigInt64LE(BigInt(Number.MAX_SAFE_INTEGER), 0) ); testNum("uint64", 8, BigInt(Number.MAX_SAFE_INTEGER), (buffer) => buffer.writeBigUInt64LE(BigInt(Number.MAX_SAFE_INTEGER), 0) ); it("parses string", () => { const reader = getMessageReader("string name"); const buff = getStringBuffer("test"); expect(reader.readMessage(buff)).toEqual({ name: "test", }); }); // Our tests are currently run in node v10 and our code is run in the browser. Node v10 does not support "ascii" // encoding out of the box despite it being supported in the browser; later versions of node do support "ascii" out // of the box. // TODO: re-enable this test when pinning to node 14+. xit("parses long strings with TextDecoder available", () => { // Remove TextDecoder expect(typeof TextDecoder).toEqual("undefined"); expect(() => new util.TextDecoder("ascii")).not.toThrow(); (global as any).TextDecoder = util.TextDecoder; const reader = getMessageReader("string name"); const string = range(0, 5000) .map(() => String.fromCharCode(Math.floor(Math.random() * 255))) .join(""); const buff = getStringBuffer(string); expect(reader.readMessage(buff)).toEqual({ name: string }); // Reset the TextDecoder delete (global as any).TextDecoder; }); it("parses JSON", () => { const reader = getMessageReader("#pragma rosbag_parse_json\nstring dummy"); const buff = getStringBuffer('{"foo":123,"bar":{"nestedFoo":456}}'); expect(reader.readMessage(buff)).toEqual({ dummy: { foo: 123, bar: { nestedFoo: 456 } }, }); const readerWithSpaces = getMessageReader(" #pragma rosbag_parse_json \n string dummy"); expect(readerWithSpaces.readMessage(buff)).toEqual({ dummy: { foo: 123, bar: { nestedFoo: 456 } }, }); const readerWithNewlines = getMessageReader("#pragma rosbag_parse_json\n\n\nstring dummy"); expect(readerWithNewlines.readMessage(buff)).toEqual({ dummy: { foo: 123, bar: { nestedFoo: 456 } }, }); const readerWithNestedComplexType = getMessageReader(`#pragma rosbag_parse_json string dummy Account account ============ MSG: custom_type/Account string name uint16 id `); expect( readerWithNestedComplexType.readMessage( Buffer.concat([buff, getStringBuffer('{"first":"First","last":"Last"}}'), Buffer.from([100, 0x00])]) ) ).toEqual({ dummy: { foo: 123, bar: { nestedFoo: 456 } }, account: { name: '{"first":"First","last":"Last"}}', id: 100 }, }); const readerWithTrailingPragmaComment = getMessageReader(`#pragma rosbag_parse_json string dummy Account account #pragma rosbag_parse_json ============ MSG: custom_type/Account string name uint16 id `); expect( readerWithTrailingPragmaComment.readMessage( Buffer.concat([buff, getStringBuffer('{"first":"First","last":"Last"}}'), Buffer.from([100, 0x00])]) ) ).toEqual({ dummy: { foo: 123, bar: { nestedFoo: 456 } }, account: { name: '{"first":"First","last":"Last"}}', id: 100 }, }); }); it("parses time", () => { const reader = getMessageReader("time right_now"); const buff = Buffer.alloc(8); const now = new Date(); now.setSeconds(31); now.setMilliseconds(0); const seconds = Math.round(now.getTime() / 1000); buff.writeUInt32LE(seconds, 0); buff.writeUInt32LE(1000000, 4); now.setMilliseconds(1); expect(reader.readMessage(buff)).toEqual({ right_now: { nsec: 1000000, sec: seconds, }, }); }); }); it("ignores comment lines", () => { const messageDefinition = ` # your first name goes here string firstName # last name here ### foo bar baz? string lastName `; const reader = getMessageReader(messageDefinition); const buffer = Buffer.concat([getStringBuffer("foo"), getStringBuffer("bar")]); expect(reader.readMessage(buffer)).toEqual({ firstName: "foo", lastName: "bar", }); }); it("still works given string message definitions", () => { const messageDefinition = "string value"; const reader = getMessageReader(messageDefinition); const buffer = getStringBuffer("foo"); expect(reader.readMessage(buffer)).toEqual({ value: "foo" }); }); describe("array", () => { it("parses variable length string array", () => { const reader = getMessageReader("string[] names"); const buffer = Buffer.concat([ // variable length array has int32 as first entry Buffer.from([0x03, 0x00, 0x00, 0x00]), getStringBuffer("foo"), getStringBuffer("bar"), getStringBuffer("baz"), ]); expect(reader.readMessage(buffer)).toEqual({ names: ["foo", "bar", "baz"], }); }); it("parses fixed length arrays", () => { const parser1 = getMessageReader("string[1] names"); const parser2 = getMessageReader("string[2] names"); const parser3 = getMessageReader("string[3] names"); const buffer = Buffer.concat([getStringBuffer("foo"), getStringBuffer("bar"), getStringBuffer("baz")]); expect(parser1.readMessage(buffer)).toEqual({ names: ["foo"], }); expect(parser2.readMessage(buffer)).toEqual({ names: ["foo", "bar"], }); expect(parser3.readMessage(buffer)).toEqual({ names: ["foo", "bar", "baz"], }); }); it("uses an empty array for a 0 length array", () => { const reader = getMessageReader("string[] names"); const buffer = Buffer.concat([ // variable length array has int32 as first entry Buffer.from([0x00, 0x00, 0x00, 0x00]), ]); const message = reader.readMessage(buffer); expect(message).toEqual({ names: [] }); }); describe("typed arrays", () => { it("uint8[] uses the same backing buffer", () => { const reader = getMessageReader("uint8[] values\nuint8 after"); const buffer = Buffer.from([0x03, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04]); const result = reader.readMessage(buffer) as ValuesAfterMessage; const { values, after } = result; expect(values instanceof Uint8Array).toBe(true); expect(values.buffer).toBe(buffer.buffer); expect(values.length).toBe(3); expect(values[0]).toBe(1); expect(values[1]).toBe(2); expect(values[2]).toBe(3); // Ensure the next value after the array gets read properly expect(after).toBe(4); expect(values.buffer.byteLength).toBeGreaterThan(3); }); it("parses uint8[] with a fixed length", () => { const reader = getMessageReader("uint8[3] values\nuint8 after"); const buffer = Buffer.from([0x01, 0x02, 0x03, 0x04]); const result = reader.readMessage(buffer) as ValuesAfterMessage; const { values, after } = result; expect(values instanceof Uint8Array).toBe(true); expect(values.buffer).toBe(buffer.buffer); expect(values.length).toBe(3); expect(values[0]).toBe(1); expect(values[1]).toBe(2); expect(values[2]).toBe(3); // Ensure the next value after the array gets read properly expect(after).toBe(4); expect(values.buffer.byteLength).toBeGreaterThan(3); }); it("int8[] uses the same backing buffer", () => { const reader = getMessageReader("int8[] values\nint8 after"); const buffer = Buffer.from([0x03, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04]); const result = reader.readMessage(buffer) as ValuesAfterMessage; const { values, after } = result; expect(values instanceof Int8Array).toBe(true); expect(values.buffer).toBe(buffer.buffer); expect(values.length).toBe(3); expect(values[0]).toBe(1); expect(values[1]).toBe(2); expect(values[2]).toBe(3); // Ensure the next value after the array gets read properly expect(after).toBe(4); expect(values.buffer.byteLength).toBeGreaterThan(3); }); it("parses int8[] with a fixed length", () => { const reader = getMessageReader("int8[3] values\nint8 after"); const buffer = Buffer.from([0x01, 0x02, 0x03, 0x04]); const result = reader.readMessage(buffer) as ValuesAfterMessage; const { values, after } = result; expect(values instanceof Int8Array).toBe(true); expect(values.buffer).toBe(buffer.buffer); expect(values.length).toBe(3); expect(values[0]).toBe(1); expect(values[1]).toBe(2); expect(values[2]).toBe(3); // Ensure the next value after the array gets read properly expect(after).toBe(4); expect(values.buffer.byteLength).toBeGreaterThan(3); }); it("parses combinations of typed arrays", () => { const reader = getMessageReader("int8[] first\nuint8[2] second"); const buffer = Buffer.from([0x02, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04]); const result = reader.readMessage(buffer) as { first: Int8Array; second: Uint8Array }; const { first, second } = result; expect(first instanceof Int8Array).toBe(true); expect(first.buffer).toBe(buffer.buffer); expect(first.length).toBe(2); expect(first[0]).toBe(1); expect(first[1]).toBe(2); expect(second instanceof Uint8Array).toBe(true); expect(second.buffer).toBe(buffer.buffer); expect(second.length).toBe(2); expect(second[0]).toBe(3); expect(second[1]).toBe(4); }); }); }); describe("complex types", () => { it("parses single complex type", () => { const messageDefinition = "string firstName \n string lastName\nuint16 age"; const reader = getMessageReader(messageDefinition); const buffer = Buffer.concat([getStringBuffer("foo"), getStringBuffer("bar"), Buffer.from([0x05, 0x00])]); expect(reader.readMessage(buffer)).toEqual({ firstName: "foo", lastName: "bar", age: 5, }); }); it("parses nested complex types", () => { const messageDefinition = ` string username Account account ============ MSG: custom_type/Account string name uint16 id `; const reader = getMessageReader(messageDefinition); const buffer = Buffer.concat([getStringBuffer("foo"), getStringBuffer("bar"), Buffer.from([100, 0x00])]); expect(reader.readMessage(buffer)).toEqual({ username: "foo", account: { name: "bar", id: 100, }, }); }); it("parses nested complex types with arrays", () => { const messageDefinition = ` string username Account[] accounts ============ MSG: custom_type/Account string name uint16 id `; const reader = getMessageReader(messageDefinition); const buffer = Buffer.concat([ getStringBuffer("foo"), // uint32LE length of array (2) Buffer.from([0x02, 0x00, 0x00, 0x00]), getStringBuffer("bar"), Buffer.from([100, 0x00]), getStringBuffer("baz"), Buffer.from([101, 0x00]), ]); expect(reader.readMessage(buffer)).toEqual({ username: "foo", accounts: [ { name: "bar", id: 100, }, { name: "baz", id: 101, }, ], }); }); it("parses 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 reader = getMessageReader(messageDefinition); const buffer = Buffer.concat([ getStringBuffer("foo"), // uint32LE length of Account array (2) Buffer.from([0x02, 0x00, 0x00, 0x00]), // name getStringBuffer("bar"), // id Buffer.from([100, 0x00]), // uint32LE length of Photo array (3) Buffer.from([0x03, 0x00, 0x00, 0x00]), // photo url getStringBuffer("http://foo.com"), // photo id Buffer.from([10]), // photo url getStringBuffer("http://bar.com"), // photo id Buffer.from([12]), // photo url getStringBuffer("http://zug.com"), // photo id Buffer.from([16]), // next account getStringBuffer("baz"), Buffer.from([101, 0x00]), // uint32LE length of Photo array (0) Buffer.from([0x00, 0x00, 0x00, 0x00]), ]); expect(reader.readMessage(buffer)).toEqual({ 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: [], }, ], }); }); 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("parses bytes and constants", () => { const reader = getMessageReader(withBytesAndBools, { typeName: "diagnostic_msgs/DiagnosticArray", readerOptions: {}, }); const buffer = Buffer.concat([Buffer.from([0x01]), Buffer.from([0x00]), getStringBuffer("foo")]); const message = reader.readMessage(buffer) as any; const { level, status } = message; expect(level).toBe(true); expect(status.level).toBe(0); expect(status.name).toBe("foo"); // We shouldn't expose constants on the message. expect(Object.keys(message).some((key) => key === "STALE")).toBe(false); }); it("freezes the resulting message if requested", () => { const reader = getMessageReader("string firstName \n string lastName\nuint16 age", { readerOptions: { freeze: true }, }); const buffer = Buffer.concat([getStringBuffer("foo"), getStringBuffer("bar"), Buffer.from([0x05, 0x00])]); const output = reader.readMessage(buffer) as { firstName: string; lastName: string; age: number }; expect(output).toEqual({ firstName: "foo", lastName: "bar", age: 5 }); expect(() => { output.firstName = "boooo"; }).toThrow(); }); }); });