/** * @fileoverview Tests for indexer.js. */ goog.module('protobuf.binary.IndexerTest'); goog.setTestOnly(); // Note to the reader: // Since the index behavior changes with the checking level some of the tests // in this file have to know which checking level is enabled to make correct // assertions. // Test are run in all checking levels. const BinaryStorage = goog.require('protobuf.runtime.BinaryStorage'); const BufferDecoder = goog.require('protobuf.binary.BufferDecoder'); const WireType = goog.require('protobuf.binary.WireType'); const {CHECK_CRITICAL_STATE} = goog.require('protobuf.internal.checks'); const {Field, IndexEntry} = goog.require('protobuf.binary.field'); const {buildIndex} = goog.require('protobuf.binary.indexer'); const {createBufferDecoder} = goog.require('protobuf.binary.bufferDecoderHelper'); /** * Returns the number of fields stored. * * @param {!BinaryStorage} storage * @return {number} */ function getStorageSize(storage) { let size = 0; storage.forEach(() => void size++); return size; } /** * @type {number} */ const PIVOT = 1; /** * Asserts a single IndexEntry at a given field number. * @param {!BinaryStorage} storage * @param {number} fieldNumber * @param {...!IndexEntry} expectedEntries */ function assertStorageEntries(storage, fieldNumber, ...expectedEntries) { expect(getStorageSize(storage)).toBe(1); const entryArray = storage.get(fieldNumber).getIndexArray(); expect(entryArray).not.toBeUndefined(); expect(entryArray.length).toBe(expectedEntries.length); for (let i = 0; i < entryArray.length; i++) { const storageEntry = entryArray[i]; const expectedEntry = expectedEntries[i]; expect(storageEntry).toBe(expectedEntry); } } describe('Indexer does', () => { it('return empty storage for empty array', () => { const storage = buildIndex(createBufferDecoder(), PIVOT); expect(storage).not.toBeNull(); expect(getStorageSize(storage)).toBe(0); }); it('throw for null array', () => { expect( () => buildIndex( /** @type {!BufferDecoder} */ (/** @type {*} */ (null)), PIVOT)) .toThrow(); }); it('fail for invalid wire type (6)', () => { expect(() => buildIndex(createBufferDecoder(0x0E, 0x01), PIVOT)) .toThrowError('Unexpected wire type: 6'); }); it('fail for invalid wire type (7)', () => { expect(() => buildIndex(createBufferDecoder(0x0F, 0x01), PIVOT)) .toThrowError('Unexpected wire type: 7'); }); it('index varint', () => { const data = createBufferDecoder(0x08, 0x01, 0x08, 0x01); const storage = buildIndex(data, PIVOT); assertStorageEntries( storage, /* fieldNumber= */ 1, Field.encodeIndexEntry(WireType.VARINT, /* startIndex= */ 1), Field.encodeIndexEntry(WireType.VARINT, /* startIndex= */ 3)); }); it('index varint with two bytes field number', () => { const data = createBufferDecoder(0xF8, 0x01, 0x01); const storage = buildIndex(data, PIVOT); assertStorageEntries( storage, /* fieldNumber= */ 31, Field.encodeIndexEntry(WireType.VARINT, /* startIndex= */ 2)); }); it('fail for varints that are longer than 10 bytes', () => { const data = createBufferDecoder( 0x08, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00); if (CHECK_CRITICAL_STATE) { expect(() => buildIndex(data, PIVOT)) .toThrowError('Index out of bounds: index: 12 size: 11'); } else { // Note in unchecked mode we produce invalid output for invalid inputs. // This test just documents our behavior in those cases. // These values might change at any point and are not considered // what the implementation should be doing here. const storage = buildIndex(data, PIVOT); assertStorageEntries( storage, /* fieldNumber= */ 1, Field.encodeIndexEntry(WireType.VARINT, /* startIndex= */ 1)); } }); it('fail for varints with no data', () => { const data = createBufferDecoder(0x08); expect(() => buildIndex(data, PIVOT)).toThrow(); }); it('index fixed64', () => { const data = createBufferDecoder( /* first= */ 0x09, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, /* second= */ 0x09, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08); const storage = buildIndex(data, PIVOT); assertStorageEntries( storage, /* fieldNumber= */ 1, Field.encodeIndexEntry(WireType.FIXED64, /* startIndex= */ 1), Field.encodeIndexEntry(WireType.FIXED64, /* startIndex= */ 10)); }); it('fail for fixed64 data missing in input', () => { const data = createBufferDecoder(0x09, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07); if (CHECK_CRITICAL_STATE) { expect(() => buildIndex(data, PIVOT)) .toThrowError('Index out of bounds: index: 9 size: 8'); } else { // Note in unchecked mode we produce invalid output for invalid inputs. // This test just documents our behavior in those cases. // These values might change at any point and are not considered // what the implementation should be doing here. const storage = buildIndex(data, PIVOT); assertStorageEntries( storage, /* fieldNumber= */ 1, Field.encodeIndexEntry(WireType.FIXED64, /* startIndex= */ 1)); } }); it('fail for fixed64 tag that has no data after it', () => { if (CHECK_CRITICAL_STATE) { const data = createBufferDecoder(0x09); expect(() => buildIndex(data, PIVOT)) .toThrowError('Index out of bounds: index: 9 size: 1'); } else { // Note in unchecked mode we produce invalid output for invalid inputs. // This test just documents our behavior in those cases. // These values might change at any point and are not considered // what the implementation should be doing here. const data = createBufferDecoder(0x09); const storage = buildIndex(data, PIVOT); assertStorageEntries( storage, /* fieldNumber= */ 1, Field.encodeIndexEntry(WireType.FIXED64, /* startIndex= */ 1)); } }); it('index delimited', () => { const data = createBufferDecoder( /* first= */ 0x0A, 0x02, 0x00, 0x01, /* second= */ 0x0A, 0x02, 0x00, 0x01); const storage = buildIndex(data, PIVOT); assertStorageEntries( storage, /* fieldNumber= */ 1, Field.encodeIndexEntry(WireType.DELIMITED, /* startIndex= */ 1), Field.encodeIndexEntry(WireType.DELIMITED, /* startIndex= */ 5)); }); it('fail for length deliimted field data missing in input', () => { const data = createBufferDecoder(0x0A, 0x04, 0x00, 0x01); if (CHECK_CRITICAL_STATE) { expect(() => buildIndex(data, PIVOT)) .toThrowError('Index out of bounds: index: 6 size: 4'); } else { // Note in unchecked mode we produce invalid output for invalid inputs. // This test just documents our behavior in those cases. // These values might change at any point and are not considered // what the implementation should be doing here. const storage = buildIndex(data, PIVOT); assertStorageEntries( storage, /* fieldNumber= */ 1, Field.encodeIndexEntry(WireType.DELIMITED, /* startIndex= */ 1)); } }); it('fail for delimited tag that has no data after it', () => { const data = createBufferDecoder(0x0A); expect(() => buildIndex(data, PIVOT)).toThrow(); }); it('index fixed32', () => { const data = createBufferDecoder( /* first= */ 0x0D, 0x01, 0x02, 0x03, 0x04, /* second= */ 0x0D, 0x01, 0x02, 0x03, 0x04); const storage = buildIndex(data, PIVOT); assertStorageEntries( storage, /* fieldNumber= */ 1, Field.encodeIndexEntry(WireType.FIXED32, /* startIndex= */ 1), Field.encodeIndexEntry(WireType.FIXED32, /* startIndex= */ 6)); }); it('fail for fixed32 data missing in input', () => { const data = createBufferDecoder(0x0D, 0x01, 0x02, 0x03); if (CHECK_CRITICAL_STATE) { expect(() => buildIndex(data, PIVOT)) .toThrowError('Index out of bounds: index: 5 size: 4'); } else { // Note in unchecked mode we produce invalid output for invalid inputs. // This test just documents our behavior in those cases. // These values might change at any point and are not considered // what the implementation should be doing here. const storage = buildIndex(data, PIVOT); assertStorageEntries( storage, /* fieldNumber= */ 1, Field.encodeIndexEntry(WireType.FIXED32, /* startIndex= */ 1)); } }); it('fail for fixed32 tag that has no data after it', () => { if (CHECK_CRITICAL_STATE) { const data = createBufferDecoder(0x0D); expect(() => buildIndex(data, PIVOT)) .toThrowError('Index out of bounds: index: 5 size: 1'); } else { // Note in unchecked mode we produce invalid output for invalid inputs. // This test just documents our behavior in those cases. // These values might change at any point and are not considered // what the implementation should be doing here. const data = createBufferDecoder(0x0D); const storage = buildIndex(data, PIVOT); assertStorageEntries( storage, /* fieldNumber= */ 1, Field.encodeIndexEntry(WireType.FIXED32, /* startIndex= */ 1)); } }); it('index group', () => { const data = createBufferDecoder( /* first= */ 0x0B, 0x08, 0x01, 0x0C, /* second= */ 0x0B, 0x08, 0x01, 0x0C); const storage = buildIndex(data, PIVOT); assertStorageEntries( storage, /* fieldNumber= */ 1, Field.encodeIndexEntry(WireType.START_GROUP, /* startIndex= */ 1), Field.encodeIndexEntry(WireType.START_GROUP, /* startIndex= */ 5)); }); it('index group and skips inner group', () => { const data = createBufferDecoder(0x0B, 0x0B, 0x08, 0x01, 0x0C, 0x08, 0x01, 0x0C); const storage = buildIndex(data, PIVOT); assertStorageEntries( storage, /* fieldNumber= */ 1, Field.encodeIndexEntry(WireType.START_GROUP, /* startIndex= */ 1)); }); it('fail on unmatched stop group', () => { const data = createBufferDecoder(0x0C, 0x01); expect(() => buildIndex(data, PIVOT)) .toThrowError('Unexpected wire type: 4'); }); it('fail for groups without matching stop group', () => { const data = createBufferDecoder(0x0B, 0x08, 0x01, 0x1C); if (CHECK_CRITICAL_STATE) { expect(() => buildIndex(data, PIVOT)) .toThrowError('Expected stop group for fieldnumber 1 not found.'); } else { // Note in unchecked mode we produce invalid output for invalid inputs. // This test just documents our behavior in those cases. // These values might change at any point and are not considered // what the implementation should be doing here. const storage = buildIndex(data, PIVOT); assertStorageEntries( storage, /* fieldNumber= */ 1, Field.encodeIndexEntry(WireType.START_GROUP, /* startIndex= */ 1)); } }); it('fail for groups without stop group', () => { const data = createBufferDecoder(0x0B, 0x08, 0x01); if (CHECK_CRITICAL_STATE) { expect(() => buildIndex(data, PIVOT)).toThrowError('No end group found.'); } else { // Note in unchecked mode we produce invalid output for invalid inputs. // This test just documents our behavior in those cases. // These values might change at any point and are not considered // what the implementation should be doing here. const storage = buildIndex(data, PIVOT); assertStorageEntries( storage, /* fieldNumber= */ 1, Field.encodeIndexEntry(WireType.START_GROUP, /* startIndex= */ 1)); } }); it('fail for group tag that has no data after it', () => { const data = createBufferDecoder(0x0B); if (CHECK_CRITICAL_STATE) { expect(() => buildIndex(data, PIVOT)).toThrowError('No end group found.'); } else { // Note in unchecked mode we produce invalid output for invalid inputs. // This test just documents our behavior in those cases. // These values might change at any point and are not considered // what the implementation should be doing here. const storage = buildIndex(data, PIVOT); assertStorageEntries( storage, /* fieldNumber= */ 1, Field.encodeIndexEntry(WireType.START_GROUP, /* startIndex= */ 1)); } }); it('index too large tag', () => { const data = createBufferDecoder(0xF8, 0xFF, 0xFF, 0xFF, 0xFF); expect(() => buildIndex(data, PIVOT)).toThrow(); }); it('fail for varint tag that has no data after it', () => { const data = createBufferDecoder(0x08); expect(() => buildIndex(data, PIVOT)).toThrow(); }); });