diff --git a/js/experimental/runtime/kernel/conformance/conformance_testee.js b/js/experimental/runtime/kernel/conformance/conformance_testee.js old mode 100644 new mode 100755 diff --git a/js/experimental/runtime/kernel/conformance/conformance_testee_runner_node.js b/js/experimental/runtime/kernel/conformance/conformance_testee_runner_node.js old mode 100644 new mode 100755 diff --git a/js/experimental/runtime/kernel/indexer.js b/js/experimental/runtime/kernel/indexer.js index d4dea13515..205a34e44d 100644 --- a/js/experimental/runtime/kernel/indexer.js +++ b/js/experimental/runtime/kernel/indexer.js @@ -8,7 +8,8 @@ const BinaryStorage = goog.require('protobuf.runtime.BinaryStorage'); const BufferDecoder = goog.require('protobuf.binary.BufferDecoder'); const WireType = goog.require('protobuf.binary.WireType'); const {Field} = goog.require('protobuf.binary.field'); -const {checkCriticalElementIndex, checkCriticalState} = goog.require('protobuf.internal.checks'); +const {checkCriticalState} = goog.require('protobuf.internal.checks'); +const {skipField, tagToFieldNumber, tagToWireType} = goog.require('protobuf.binary.tag'); /** * Appends a new entry in the index array for the given field number. @@ -26,26 +27,6 @@ function addIndexEntry(storage, fieldNumber, wireType, startIndex) { } } -/** - * Returns wire type stored in a tag. - * Protos store the wire type as the first 3 bit of a tag. - * @param {number} tag - * @return {!WireType} - */ -function tagToWireType(tag) { - return /** @type {!WireType} */ (tag & 0x07); -} - -/** - * Returns the field number stored in a tag. - * Protos store the field number in the upper 29 bits of a 32 bit number. - * @param {number} tag - * @return {number} - */ -function tagToFieldNumber(tag) { - return tag >>> 3; -} - /** * Creates an index of field locations in a given binary protobuf. * @param {!BufferDecoder} bufferDecoder @@ -62,83 +43,13 @@ function buildIndex(bufferDecoder, pivot) { const wireType = tagToWireType(tag); const fieldNumber = tagToFieldNumber(tag); checkCriticalState(fieldNumber > 0, `Invalid field number ${fieldNumber}`); - addIndexEntry(storage, fieldNumber, wireType, bufferDecoder.cursor()); - - checkCriticalState( - !skipField_(bufferDecoder, wireType, fieldNumber), - 'Found unmatched stop group.'); + skipField(bufferDecoder, wireType, fieldNumber); } return storage; } -/** - * Skips over fields until the next field of the message. - * @param {!BufferDecoder} bufferDecoder - * @param {!WireType} wireType - * @param {number} fieldNumber - * @return {boolean} Whether the field we skipped over was a stop group. - * @private - */ -function skipField_(bufferDecoder, wireType, fieldNumber) { - switch (wireType) { - case WireType.VARINT: - checkCriticalElementIndex( - bufferDecoder.cursor(), bufferDecoder.endIndex()); - bufferDecoder.skipVarint(); - return false; - case WireType.FIXED64: - bufferDecoder.skip(8); - return false; - case WireType.DELIMITED: - checkCriticalElementIndex( - bufferDecoder.cursor(), bufferDecoder.endIndex()); - const length = bufferDecoder.getUnsignedVarint32(); - bufferDecoder.skip(length); - return false; - case WireType.START_GROUP: - checkCriticalState( - skipGroup_(bufferDecoder, fieldNumber), 'No end group found.'); - return false; - case WireType.END_GROUP: - // Signal that we found a stop group to the caller - return true; - case WireType.FIXED32: - bufferDecoder.skip(4); - return false; - default: - throw new Error(`Invalid wire type: ${wireType}`); - } -} - -/** - * Skips over fields until it finds the end of a given group. - * @param {!BufferDecoder} bufferDecoder - * @param {number} groupFieldNumber - * @return {boolean} Returns true if an end was found. - * @private - */ -function skipGroup_(bufferDecoder, groupFieldNumber) { - // On a start group we need to keep skipping fields until we find a - // corresponding stop group - // Note: Since we are calling skipField from here nested groups will be - // handled by recursion of this method and thus we will not see a nested - // STOP GROUP here unless there is something wrong with the input data. - while (bufferDecoder.hasNext()) { - const tag = bufferDecoder.getUnsignedVarint32(); - const wireType = tagToWireType(tag); - const fieldNumber = tagToFieldNumber(tag); - - if (skipField_(bufferDecoder, wireType, fieldNumber)) { - checkCriticalState( - groupFieldNumber === fieldNumber, - `Expected stop group for fieldnumber ${groupFieldNumber} not found.`); - return true; - } - } - return false; -} - exports = { buildIndex, + tagToWireType, }; diff --git a/js/experimental/runtime/kernel/indexer_test.js b/js/experimental/runtime/kernel/indexer_test.js index 101e44283f..ffb8807994 100644 --- a/js/experimental/runtime/kernel/indexer_test.js +++ b/js/experimental/runtime/kernel/indexer_test.js @@ -72,12 +72,12 @@ describe('Indexer does', () => { it('fail for invalid wire type (6)', () => { expect(() => buildIndex(createBufferDecoder(0x0E, 0x01), PIVOT)) - .toThrowError('Invalid wire type: 6'); + .toThrowError('Unexpected wire type: 6'); }); it('fail for invalid wire type (7)', () => { expect(() => buildIndex(createBufferDecoder(0x0F, 0x01), PIVOT)) - .toThrowError('Invalid wire type: 7'); + .toThrowError('Unexpected wire type: 7'); }); it('index varint', () => { @@ -269,33 +269,8 @@ describe('Indexer does', () => { it('fail on unmatched stop group', () => { const data = createBufferDecoder(0x0C, 0x01); - if (CHECK_CRITICAL_STATE) { - expect(() => buildIndex(data, PIVOT)) - .toThrowError('Found unmatched stop group.'); - } 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); - - expect(getStorageSize(storage)).toBe(1); - const entryArray = storage.get(1).getIndexArray(); - expect(entryArray).not.toBeUndefined(); - expect(entryArray.length).toBe(1); - const entry = entryArray[0]; - - expect(Field.getWireType(entry)).toBe(WireType.END_GROUP); - expect(Field.getStartIndex(entry)).toBe(1); - - const entryArray2 = storage.get(0).getIndexArray(); - expect(entryArray2).not.toBeUndefined(); - expect(entryArray2.length).toBe(1); - const entry2 = entryArray2[0]; - - expect(Field.getWireType(entry2)).toBe(WireType.FIXED64); - expect(Field.getStartIndex(entry2)).toBe(2); - } + expect(() => buildIndex(data, PIVOT)) + .toThrowError('Unexpected wire type: 4'); }); it('fail for groups without matching stop group', () => { diff --git a/js/experimental/runtime/kernel/kernel.js b/js/experimental/runtime/kernel/kernel.js index 2a2b2d8c48..bb2608398a 100644 --- a/js/experimental/runtime/kernel/kernel.js +++ b/js/experimental/runtime/kernel/kernel.js @@ -26,6 +26,7 @@ const reader = goog.require('protobuf.binary.reader'); const {CHECK_TYPE, checkCriticalElementIndex, checkCriticalState, checkCriticalType, checkCriticalTypeBool, checkCriticalTypeBoolArray, checkCriticalTypeByteString, checkCriticalTypeByteStringArray, checkCriticalTypeDouble, checkCriticalTypeDoubleArray, checkCriticalTypeFloat, checkCriticalTypeFloatIterable, checkCriticalTypeMessageArray, checkCriticalTypeSignedInt32, checkCriticalTypeSignedInt32Array, checkCriticalTypeSignedInt64, checkCriticalTypeSignedInt64Array, checkCriticalTypeString, checkCriticalTypeStringArray, checkCriticalTypeUnsignedInt32, checkCriticalTypeUnsignedInt32Array, checkDefAndNotNull, checkElementIndex, checkFieldNumber, checkFunctionExists, checkState, checkTypeDouble, checkTypeFloat, checkTypeSignedInt32, checkTypeSignedInt64, checkTypeUnsignedInt32} = goog.require('protobuf.internal.checks'); const {Field, IndexEntry} = goog.require('protobuf.binary.field'); const {buildIndex} = goog.require('protobuf.binary.indexer'); +const {createTag, get32BitVarintLength, getTagLength} = goog.require('protobuf.binary.tag'); /** @@ -139,6 +140,28 @@ function readRepeatedNonPrimitive(indexArray, bufferDecoder, singularReadFunc) { return result; } +/** + * Converts all entries of the index array to the template type using the given + * read function and return an Array containing those converted values. This is + * used to implement parsing repeated non-primitive fields. + * @param {!Array} indexArray + * @param {!BufferDecoder} bufferDecoder + * @param {number} fieldNumber + * @param {function(!Kernel):T} instanceCreator + * @param {number=} pivot + * @return {!Array} + * @template T + */ +function readRepeatedGroup( + indexArray, bufferDecoder, fieldNumber, instanceCreator, pivot) { + const result = new Array(indexArray.length); + for (let i = 0; i < indexArray.length; i++) { + result[i] = doReadGroup( + bufferDecoder, indexArray[i], fieldNumber, instanceCreator, pivot); + } + return result; +} + /** * Creates a new bytes array to contain all data of a submessage. * When there are multiple entries, merge them together. @@ -193,6 +216,51 @@ function readMessage(indexArray, bufferDecoder, instanceCreator, pivot) { return instanceCreator(accessor); } +/** + * Merges all index entries of the index array using the given read function. + * This is used to implement parsing singular group fields. + * @param {!Array} indexArray + * @param {!BufferDecoder} bufferDecoder + * @param {number} fieldNumber + * @param {function(!Kernel):T} instanceCreator + * @param {number=} pivot + * @return {T} + * @template T + */ +function readGroup( + indexArray, bufferDecoder, fieldNumber, instanceCreator, pivot) { + checkInstanceCreator(instanceCreator); + checkState(indexArray.length > 0); + return doReadGroup( + bufferDecoder, indexArray[indexArray.length - 1], fieldNumber, + instanceCreator, pivot); +} + +/** + * Merges all index entries of the index array using the given read function. + * This is used to implement parsing singular message fields. + * @param {!BufferDecoder} bufferDecoder + * @param {!IndexEntry} indexEntry + * @param {number} fieldNumber + * @param {function(!Kernel):T} instanceCreator + * @param {number=} pivot + * @return {T} + * @template T + */ +function doReadGroup( + bufferDecoder, indexEntry, fieldNumber, instanceCreator, pivot) { + validateWireType(indexEntry, WireType.START_GROUP); + const fieldStartIndex = Field.getStartIndex(indexEntry); + const tag = createTag(WireType.START_GROUP, fieldNumber); + const groupTagLength = get32BitVarintLength(tag); + const groupLength = getTagLength( + bufferDecoder, fieldStartIndex, WireType.START_GROUP, fieldNumber); + const accessorBuffer = bufferDecoder.subBufferDecoder( + fieldStartIndex, groupLength - groupTagLength); + const kernel = Kernel.fromBufferDecoder_(accessorBuffer, pivot); + return instanceCreator(kernel); +} + /** * @param {!Writer} writer * @param {number} fieldNumber @@ -203,6 +271,18 @@ function writeMessage(writer, fieldNumber, value) { fieldNumber, checkDefAndNotNull(value).internalGetKernel().serialize()); } +/** + * @param {!Writer} writer + * @param {number} fieldNumber + * @param {?InternalMessage} value + */ +function writeGroup(writer, fieldNumber, value) { + const kernel = checkDefAndNotNull(value).internalGetKernel(); + writer.writeStartGroup(fieldNumber); + kernel.serializeToWriter(writer); + writer.writeEndGroup(fieldNumber); +} + /** * Writes the array of Messages into the writer for the given field number. * @param {!Writer} writer @@ -215,6 +295,18 @@ function writeRepeatedMessage(writer, fieldNumber, values) { } } +/** + * Writes the array of Messages into the writer for the given field number. + * @param {!Writer} writer + * @param {number} fieldNumber + * @param {!Array} values + */ +function writeRepeatedGroup(writer, fieldNumber, values) { + for (const value of values) { + writeGroup(writer, fieldNumber, value); + } +} + /** * Array.from has a weird type definition in google3/javascript/externs/es6.js * and wants the mapping function accept strings. @@ -406,7 +498,8 @@ class Kernel { writer.writeTag(fieldNumber, Field.getWireType(indexEntry)); writer.writeBufferDecoder( checkDefAndNotNull(this.bufferDecoder_), - Field.getStartIndex(indexEntry), Field.getWireType(indexEntry)); + Field.getStartIndex(indexEntry), Field.getWireType(indexEntry), + fieldNumber); } } }); @@ -690,6 +783,28 @@ class Kernel { writeMessage); } + /** + * Returns data as a mutable proto Message for the given field number. + * If no value has been set, return null. + * If hasFieldNumber(fieldNumber) == false before calling, it remains false. + * + * This method should not be used along with getMessage, since calling + * getMessageOrNull after getMessage will not register the encoder. + * + * @param {number} fieldNumber + * @param {function(!Kernel):T} instanceCreator + * @param {number=} pivot + * @return {?T} + * @template T + */ + getGroupOrNull(fieldNumber, instanceCreator, pivot = undefined) { + return this.getFieldWithDefault_( + fieldNumber, null, + (indexArray, bytes) => + readGroup(indexArray, bytes, fieldNumber, instanceCreator, pivot), + writeGroup); + } + /** * Returns data as a mutable proto Message for the given field number. * If no value has been set previously, creates and attaches an instance. @@ -714,6 +829,30 @@ class Kernel { return instance; } + /** + * Returns data as a mutable proto Message for the given field number. + * If no value has been set previously, creates and attaches an instance. + * Postcondition: hasFieldNumber(fieldNumber) == true. + * + * This method should not be used along with getMessage, since calling + * getMessageAttach after getMessage will not register the encoder. + * + * @param {number} fieldNumber + * @param {function(!Kernel):T} instanceCreator + * @param {number=} pivot + * @return {T} + * @template T + */ + getGroupAttach(fieldNumber, instanceCreator, pivot = undefined) { + checkInstanceCreator(instanceCreator); + let instance = this.getGroupOrNull(fieldNumber, instanceCreator, pivot); + if (!instance) { + instance = instanceCreator(Kernel.createEmpty()); + this.setField_(fieldNumber, instance, writeGroup); + } + return instance; + } + /** * Returns data as a proto Message for the given field number. * If no value has been set, return a default instance. @@ -744,6 +883,36 @@ class Kernel { return message === null ? instanceCreator(Kernel.createEmpty()) : message; } + /** + * Returns data as a proto Message for the given field number. + * If no value has been set, return a default instance. + * This default instance is guaranteed to be the same instance, unless this + * field is cleared. + * Does not register the encoder, so changes made to the returned + * sub-message will not be included when serializing the parent message. + * Use getMessageAttach() if the resulting sub-message should be mutable. + * + * This method should not be used along with getMessageOrNull or + * getMessageAttach, since these methods register the encoder. + * + * @param {number} fieldNumber + * @param {function(!Kernel):T} instanceCreator + * @param {number=} pivot + * @return {T} + * @template T + */ + getGroup(fieldNumber, instanceCreator, pivot = undefined) { + checkInstanceCreator(instanceCreator); + const message = this.getFieldWithDefault_( + fieldNumber, null, + (indexArray, bytes) => + readGroup(indexArray, bytes, fieldNumber, instanceCreator, pivot)); + // Returns an empty message as the default value if the field doesn't exist. + // We don't pass the default value to getFieldWithDefault_ to reduce object + // allocation. + return message === null ? instanceCreator(Kernel.createEmpty()) : message; + } + /** * Returns the accessor for the given singular message, or returns null if * it hasn't been set. @@ -1614,6 +1783,71 @@ class Kernel { .length; } + /** + * Returns an Array instance containing boolean values for the given field + * number. + * @param {number} fieldNumber + * @param {function(!Kernel):T} instanceCreator + * @param {number|undefined} pivot + * @return {!Array} + * @template T + * @private + */ + getRepeatedGroupArray_(fieldNumber, instanceCreator, pivot) { + return this.getFieldWithDefault_( + fieldNumber, [], + (indexArray, bufferDecoder) => readRepeatedGroup( + indexArray, bufferDecoder, fieldNumber, instanceCreator, pivot), + writeRepeatedGroup); + } + + /** + * Returns the element at index for the given field number as a group. + * @param {number} fieldNumber + * @param {function(!Kernel):T} instanceCreator + * @param {number} index + * @param {number=} pivot + * @return {T} + * @template T + */ + getRepeatedGroupElement( + fieldNumber, instanceCreator, index, pivot = undefined) { + const array = + this.getRepeatedGroupArray_(fieldNumber, instanceCreator, pivot); + checkCriticalElementIndex(index, array.length); + return array[index]; + } + + /** + * Returns an Iterable instance containing group values for the given field + * number. + * @param {number} fieldNumber + * @param {function(!Kernel):T} instanceCreator + * @param {number=} pivot + * @return {!Iterable} + * @template T + */ + getRepeatedGroupIterable(fieldNumber, instanceCreator, pivot = undefined) { + // Don't split this statement unless needed. JS compiler thinks + // getRepeatedMessageArray_ might have side effects and doesn't inline the + // call in the compiled code. See cl/293894484 for details. + return new ArrayIterable( + this.getRepeatedGroupArray_(fieldNumber, instanceCreator, pivot)); + } + + /** + * Returns the size of the repeated field. + * @param {number} fieldNumber + * @param {function(!Kernel):T} instanceCreator + * @return {number} + * @param {number=} pivot + * @template T + */ + getRepeatedGroupSize(fieldNumber, instanceCreator, pivot = undefined) { + return this.getRepeatedGroupArray_(fieldNumber, instanceCreator, pivot) + .length; + } + /*************************************************************************** * OPTIONAL SETTER METHODS ***************************************************************************/ @@ -1801,6 +2035,20 @@ class Kernel { this.setInt64(fieldNumber, value); } + /** + * Sets a proto Group to the field with the given field number. + * Instead of working with the Kernel inside of the message directly, we + * need the message instance to keep its reference equality for subsequent + * gettings. + * @param {number} fieldNumber + * @param {!InternalMessage} value + */ + setGroup(fieldNumber, value) { + checkCriticalType( + value !== null, 'Given value is not a message instance: null'); + this.setField_(fieldNumber, value, writeGroup); + } + /** * Sets a proto Message to the field with the given field number. * Instead of working with the Kernel inside of the message directly, we @@ -3806,6 +4054,69 @@ class Kernel { this.addRepeatedMessageIterable( fieldNumber, [value], instanceCreator, pivot); } + + // Groups + /** + * Sets all message values into the field for the given field number. + * @param {number} fieldNumber + * @param {!Iterable} values + */ + setRepeatedGroupIterable(fieldNumber, values) { + const /** !Array */ array = Array.from(values); + checkCriticalTypeMessageArray(array); + this.setField_(fieldNumber, array, writeRepeatedGroup); + } + + /** + * Adds all message values into the field for the given field number. + * @param {number} fieldNumber + * @param {!Iterable} values + * @param {function(!Kernel):!InternalMessage} instanceCreator + * @param {number=} pivot + */ + addRepeatedGroupIterable( + fieldNumber, values, instanceCreator, pivot = undefined) { + const array = [ + ...this.getRepeatedGroupArray_(fieldNumber, instanceCreator, pivot), + ...values, + ]; + checkCriticalTypeMessageArray(array); + // Needs to set it back with the new array. + this.setField_(fieldNumber, array, writeRepeatedGroup); + } + + /** + * Sets a single message value into the field for the given field number at + * the given index. + * @param {number} fieldNumber + * @param {!InternalMessage} value + * @param {function(!Kernel):!InternalMessage} instanceCreator + * @param {number} index + * @param {number=} pivot + * @throws {!Error} if index is out of range when check mode is critical + */ + setRepeatedGroupElement( + fieldNumber, value, instanceCreator, index, pivot = undefined) { + checkInstanceCreator(instanceCreator); + checkCriticalType( + value !== null, 'Given value is not a message instance: null'); + const array = + this.getRepeatedGroupArray_(fieldNumber, instanceCreator, pivot); + checkCriticalElementIndex(index, array.length); + array[index] = value; + } + + /** + * Adds a single message value into the field for the given field number. + * @param {number} fieldNumber + * @param {!InternalMessage} value + * @param {function(!Kernel):!InternalMessage} instanceCreator + * @param {number=} pivot + */ + addRepeatedGroupElement( + fieldNumber, value, instanceCreator, pivot = undefined) { + this.addRepeatedGroupIterable(fieldNumber, [value], instanceCreator, pivot); + } } exports = Kernel; diff --git a/js/experimental/runtime/kernel/kernel_repeated_test.js b/js/experimental/runtime/kernel/kernel_repeated_test.js index 7741c35c38..6a798b6c09 100644 --- a/js/experimental/runtime/kernel/kernel_repeated_test.js +++ b/js/experimental/runtime/kernel/kernel_repeated_test.js @@ -7425,3 +7425,383 @@ describe('Kernel for repeated message does', () => { } }); }); + +describe('Kernel for repeated groups does', () => { + it('return empty array for the empty input', () => { + const accessor = Kernel.createEmpty(); + expectEqualToArray( + accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator), []); + }); + + it('ensure not the same instance returned for the empty input', () => { + const accessor = Kernel.createEmpty(); + const list1 = + accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator); + const list2 = + accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator); + expect(list1).not.toBe(list2); + }); + + it('return size for the empty input', () => { + const accessor = Kernel.createEmpty(); + expect(accessor.getRepeatedGroupSize(1, TestMessage.instanceCreator)) + .toEqual(0); + }); + + it('return values from the input', () => { + const bytes1 = createArrayBuffer(0x08, 0x01); + const bytes2 = createArrayBuffer(0x08, 0x02); + const msg1 = new TestMessage(Kernel.fromArrayBuffer(bytes1)); + const msg2 = new TestMessage(Kernel.fromArrayBuffer(bytes2)); + + const bytes = + createArrayBuffer(0x0B, 0x08, 0x01, 0x0C, 0x0B, 0x08, 0x02, 0x0C); + const accessor = Kernel.fromArrayBuffer(bytes); + expectEqualToMessageArray( + accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator), + [msg1, msg2]); + }); + + it('ensure not the same array instance returned', () => { + const bytes = + createArrayBuffer(0x0B, 0x08, 0x01, 0x0C, 0x0B, 0x08, 0x02, 0x0C); + const accessor = Kernel.fromArrayBuffer(bytes); + const list1 = + accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator); + const list2 = + accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator); + expect(list1).not.toBe(list2); + }); + + it('ensure the same array element returned for get iterable', () => { + const bytes = + createArrayBuffer(0x0B, 0x08, 0x01, 0x0C, 0x0B, 0x08, 0x02, 0x0C); + const accessor = Kernel.fromArrayBuffer(bytes); + const list1 = + accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator); + const list2 = accessor.getRepeatedGroupIterable( + 1, TestMessage.instanceCreator, /* pivot= */ 0); + const array1 = Array.from(list1); + const array2 = Array.from(list2); + for (let i = 0; i < array1.length; i++) { + expect(array1[i]).toBe(array2[i]); + } + }); + + it('return accessors from the input', () => { + const bytes = + createArrayBuffer(0x0B, 0x08, 0x01, 0x0C, 0x0B, 0x08, 0x02, 0x0C); + const accessor = Kernel.fromArrayBuffer(bytes); + const [accessor1, accessor2] = + [...accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator)]; + expect(accessor1.getInt32WithDefault(1)).toEqual(1); + expect(accessor2.getInt32WithDefault(1)).toEqual(2); + }); + + it('return accessors from the input when pivot is set', () => { + const bytes = + createArrayBuffer(0x0B, 0x08, 0x01, 0x0C, 0x0B, 0x08, 0x02, 0x0C); + const accessor = Kernel.fromArrayBuffer(bytes); + const [accessor1, accessor2] = [...accessor.getRepeatedGroupIterable( + 1, TestMessage.instanceCreator, /* pivot= */ 0)]; + expect(accessor1.getInt32WithDefault(1)).toEqual(1); + expect(accessor2.getInt32WithDefault(1)).toEqual(2); + }); + + it('return the repeated field element from the input', () => { + const bytes = + createArrayBuffer(0x0B, 0x08, 0x01, 0x0C, 0x0B, 0x08, 0x02, 0x0C); + const accessor = Kernel.fromArrayBuffer(bytes); + const msg1 = accessor.getRepeatedGroupElement( + /* fieldNumber= */ 1, TestMessage.instanceCreator, + /* index= */ 0); + const msg2 = accessor.getRepeatedGroupElement( + /* fieldNumber= */ 1, TestMessage.instanceCreator, + /* index= */ 1, /* pivot= */ 0); + expect(msg1.getInt32WithDefault( + /* fieldNumber= */ 1, /* default= */ 0)) + .toEqual(1); + expect(msg2.getInt32WithDefault( + /* fieldNumber= */ 1, /* default= */ 0)) + .toEqual(2); + }); + + it('ensure the same array element returned', () => { + const bytes = + createArrayBuffer(0x0B, 0x08, 0x01, 0x0C, 0x0B, 0x08, 0x02, 0x0C); + const accessor = Kernel.fromArrayBuffer(bytes); + const msg1 = accessor.getRepeatedGroupElement( + /* fieldNumber= */ 1, TestMessage.instanceCreator, + /* index= */ 0); + const msg2 = accessor.getRepeatedGroupElement( + /* fieldNumber= */ 1, TestMessage.instanceCreator, + /* index= */ 0); + expect(msg1).toBe(msg2); + }); + + it('return the size from the input', () => { + const bytes = + createArrayBuffer(0x0B, 0x08, 0x01, 0x0C, 0x0B, 0x08, 0x02, 0x0C); + const accessor = Kernel.fromArrayBuffer(bytes); + expect(accessor.getRepeatedGroupSize(1, TestMessage.instanceCreator)) + .toEqual(2); + }); + + it('encode repeated message from the input', () => { + const bytes = + createArrayBuffer(0x0B, 0x08, 0x01, 0x0C, 0x0B, 0x08, 0x02, 0x0C); + const accessor = Kernel.fromArrayBuffer(bytes); + expect(accessor.serialize()).toEqual(bytes); + }); + + it('add a single value', () => { + const accessor = Kernel.createEmpty(); + const bytes1 = createArrayBuffer(0x08, 0x01); + const msg1 = new TestMessage(Kernel.fromArrayBuffer(bytes1)); + const bytes2 = createArrayBuffer(0x08, 0x02); + const msg2 = new TestMessage(Kernel.fromArrayBuffer(bytes2)); + + accessor.addRepeatedGroupElement(1, msg1, TestMessage.instanceCreator); + accessor.addRepeatedGroupElement(1, msg2, TestMessage.instanceCreator); + const result = + accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator); + + expect(Array.from(result)).toEqual([msg1, msg2]); + }); + + it('add values', () => { + const accessor = Kernel.createEmpty(); + const bytes1 = createArrayBuffer(0x08, 0x01); + const msg1 = new TestMessage(Kernel.fromArrayBuffer(bytes1)); + const bytes2 = createArrayBuffer(0x08, 0x02); + const msg2 = new TestMessage(Kernel.fromArrayBuffer(bytes2)); + + accessor.addRepeatedGroupIterable(1, [msg1], TestMessage.instanceCreator); + accessor.addRepeatedGroupIterable(1, [msg2], TestMessage.instanceCreator); + const result = + accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator); + + expect(Array.from(result)).toEqual([msg1, msg2]); + }); + + it('set a single value', () => { + const bytes = createArrayBuffer(0x0B, 0x08, 0x01, 0x0C); + const accessor = Kernel.fromArrayBuffer(bytes); + const subbytes = createArrayBuffer(0x08, 0x01); + const submsg = new TestMessage(Kernel.fromArrayBuffer(subbytes)); + + accessor.setRepeatedGroupElement( + /* fieldNumber= */ 1, submsg, TestMessage.instanceCreator, + /* index= */ 0); + const result = + accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator); + + expect(Array.from(result)).toEqual([submsg]); + }); + + it('write submessage changes made via getRepeatedGroupElement', () => { + const bytes = createArrayBuffer(0x0B, 0x08, 0x05, 0x0C); + const expected = createArrayBuffer(0x0B, 0x08, 0x00, 0x0C); + const accessor = Kernel.fromArrayBuffer(bytes); + const submsg = accessor.getRepeatedGroupElement( + /* fieldNumber= */ 1, TestMessage.instanceCreator, + /* index= */ 0); + expect(submsg.getInt32WithDefault(1, 0)).toEqual(5); + submsg.setInt32(1, 0); + + expect(accessor.serialize()).toEqual(expected); + }); + + it('set values', () => { + const accessor = Kernel.createEmpty(); + const subbytes = createArrayBuffer(0x08, 0x01); + const submsg = new TestMessage(Kernel.fromArrayBuffer(subbytes)); + + accessor.setRepeatedGroupIterable(1, [submsg]); + const result = + accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator); + + expect(Array.from(result)).toEqual([submsg]); + }); + + it('encode for adding single value', () => { + const accessor = Kernel.createEmpty(); + const bytes1 = createArrayBuffer(0x08, 0x01); + const msg1 = new TestMessage(Kernel.fromArrayBuffer(bytes1)); + const bytes2 = createArrayBuffer(0x08, 0x00); + const msg2 = new TestMessage(Kernel.fromArrayBuffer(bytes2)); + const expected = + createArrayBuffer(0x0B, 0x08, 0x01, 0x0C, 0x0B, 0x08, 0x00, 0x0C); + + accessor.addRepeatedGroupElement(1, msg1, TestMessage.instanceCreator); + accessor.addRepeatedGroupElement(1, msg2, TestMessage.instanceCreator); + const result = accessor.serialize(); + + expect(result).toEqual(expected); + }); + + it('encode for adding values', () => { + const accessor = Kernel.createEmpty(); + const bytes1 = createArrayBuffer(0x08, 0x01); + const msg1 = new TestMessage(Kernel.fromArrayBuffer(bytes1)); + const bytes2 = createArrayBuffer(0x08, 0x00); + const msg2 = new TestMessage(Kernel.fromArrayBuffer(bytes2)); + const expected = + createArrayBuffer(0x0B, 0x08, 0x01, 0x0C, 0x0B, 0x08, 0x00, 0x0C); + + accessor.addRepeatedGroupIterable( + 1, [msg1, msg2], TestMessage.instanceCreator); + const result = accessor.serialize(); + + expect(result).toEqual(expected); + }); + + it('encode for setting single value', () => { + const bytes = createArrayBuffer(0x0B, 0x08, 0x00, 0x0C); + const accessor = Kernel.fromArrayBuffer(bytes); + const subbytes = createArrayBuffer(0x08, 0x01); + const submsg = new TestMessage(Kernel.fromArrayBuffer(subbytes)); + const expected = createArrayBuffer(0x0B, 0x08, 0x01, 0x0C); + + accessor.setRepeatedGroupElement( + /* fieldNumber= */ 1, submsg, TestMessage.instanceCreator, + /* index= */ 0); + const result = accessor.serialize(); + + expect(result).toEqual(expected); + }); + + it('encode for setting values', () => { + const accessor = Kernel.createEmpty(); + const subbytes = createArrayBuffer(0x08, 0x01); + const submsg = new TestMessage(Kernel.fromArrayBuffer(subbytes)); + const expected = createArrayBuffer(0x0B, 0x08, 0x01, 0x0C); + + accessor.setRepeatedGroupIterable(1, [submsg]); + const result = accessor.serialize(); + + expect(result).toEqual(expected); + }); + + it('fail when getting groups value with other wire types', () => { + const accessor = Kernel.fromArrayBuffer(createArrayBuffer( + 0x09, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)); + + if (CHECK_CRITICAL_STATE) { + expect(() => { + accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator); + }).toThrow(); + } + }); + + it('fail when adding group values with wrong type value', () => { + const accessor = Kernel.createEmpty(); + const fakeValue = /** @type {!TestMessage} */ (/** @type {*} */ (null)); + if (CHECK_CRITICAL_STATE) { + expect( + () => accessor.addRepeatedGroupIterable( + 1, [fakeValue], TestMessage.instanceCreator)) + .toThrowError('Given value is not a message instance: null'); + } 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. + accessor.addRepeatedGroupIterable( + 1, [fakeValue], TestMessage.instanceCreator); + const list = + accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator); + expect(Array.from(list)).toEqual([null]); + } + }); + + it('fail when adding single group value with wrong type value', () => { + const accessor = Kernel.createEmpty(); + const fakeValue = /** @type {!TestMessage} */ (/** @type {*} */ (null)); + if (CHECK_CRITICAL_STATE) { + expect( + () => accessor.addRepeatedGroupElement( + 1, fakeValue, TestMessage.instanceCreator)) + .toThrowError('Given value is not a message instance: null'); + } 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. + accessor.addRepeatedGroupElement( + 1, fakeValue, TestMessage.instanceCreator); + const list = + accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator); + expect(Array.from(list)).toEqual([null]); + } + }); + + it('fail when setting message values with wrong type value', () => { + const accessor = Kernel.createEmpty(); + const fakeValue = /** @type {!TestMessage} */ (/** @type {*} */ (null)); + if (CHECK_CRITICAL_STATE) { + expect(() => accessor.setRepeatedGroupIterable(1, [fakeValue])) + .toThrowError('Given value is not a message instance: null'); + } 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. + accessor.setRepeatedGroupIterable(1, [fakeValue]); + const list = + accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator); + expect(Array.from(list)).toEqual([null]); + } + }); + + it('fail when setting single value with wrong type value', () => { + const accessor = + Kernel.fromArrayBuffer(createArrayBuffer(0x0B, 0x08, 0x00, 0x0C)); + const fakeValue = /** @type {!TestMessage} */ (/** @type {*} */ (null)); + if (CHECK_CRITICAL_STATE) { + expect( + () => accessor.setRepeatedGroupElement( + /* fieldNumber= */ 1, fakeValue, TestMessage.instanceCreator, + /* index= */ 0)) + .toThrowError('Given value is not a message instance: null'); + } 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. + accessor.setRepeatedGroupElement( + /* fieldNumber= */ 1, fakeValue, TestMessage.instanceCreator, + /* index= */ 0); + const list = + accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator); + expect(Array.from(list).length).toEqual(1); + } + }); + + it('fail when setting single value with out-of-bound index', () => { + const accessor = + Kernel.fromArrayBuffer(createArrayBuffer(0x0B, 0x08, 0x00, 0x0C)); + const msg1 = + accessor.getRepeatedGroupElement(1, TestMessage.instanceCreator, 0); + const bytes2 = createArrayBuffer(0x08, 0x01); + const msg2 = new TestMessage(Kernel.fromArrayBuffer(bytes2)); + if (CHECK_CRITICAL_STATE) { + expect( + () => accessor.setRepeatedGroupElement( + /* fieldNumber= */ 1, msg2, TestMessage.instanceCreator, + /* index= */ 1)) + .toThrowError('Index out of bounds: index: 1 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. + accessor.setRepeatedGroupElement( + /* fieldNumber= */ 1, msg2, TestMessage.instanceCreator, + /* index= */ 1); + expectEqualToArray( + accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator), + [msg1, msg2]); + } + }); +}); diff --git a/js/experimental/runtime/kernel/kernel_test.js b/js/experimental/runtime/kernel/kernel_test.js index 61cdeec657..e72be4f3b6 100644 --- a/js/experimental/runtime/kernel/kernel_test.js +++ b/js/experimental/runtime/kernel/kernel_test.js @@ -2075,3 +2075,255 @@ describe('Double access', () => { } }); }); + +describe('Kernel for singular group does', () => { + it('return group from the input', () => { + const bytes = createArrayBuffer(0x0B, 0x08, 0x01, 0x0C); + const accessor = Kernel.fromArrayBuffer(bytes); + const msg = accessor.getGroupOrNull(1, TestMessage.instanceCreator); + expect(msg.getBoolWithDefault(1, false)).toBe(true); + }); + + it('return group from the input when pivot is set', () => { + const bytes = createArrayBuffer(0x0B, 0x08, 0x01, 0x0C); + const accessor = Kernel.fromArrayBuffer(bytes); + const msg = accessor.getGroupOrNull(1, TestMessage.instanceCreator, 0); + expect(msg.getBoolWithDefault(1, false)).toBe(true); + }); + + it('encode group from the input', () => { + const bytes = createArrayBuffer(0x0B, 0x08, 0x01, 0x0C); + const accessor = Kernel.fromArrayBuffer(bytes); + expect(accessor.serialize()).toEqual(bytes); + }); + + it('encode group from the input after read', () => { + const bytes = createArrayBuffer(0x0B, 0x08, 0x01, 0x0C); + const accessor = Kernel.fromArrayBuffer(bytes); + accessor.getGroupOrNull(1, TestMessage.instanceCreator); + expect(accessor.serialize()).toEqual(bytes); + }); + + it('return last group from multiple inputs', () => { + const bytes = + createArrayBuffer(0x0B, 0x08, 0x00, 0x0C, 0x0B, 0x08, 0x01, 0x0C); + const accessor = Kernel.fromArrayBuffer(bytes); + const msg = accessor.getGroupOrNull(1, TestMessage.instanceCreator); + expect(msg.getBoolWithDefault(1, false)).toBe(true); + }); + + it('removes duplicated group when serializing', () => { + const bytes = + createArrayBuffer(0x0B, 0x08, 0x00, 0x0C, 0x0B, 0x08, 0x01, 0x0C); + const accessor = Kernel.fromArrayBuffer(bytes); + accessor.getGroupOrNull(1, TestMessage.instanceCreator); + expect(accessor.serialize()) + .toEqual(createArrayBuffer(0x0B, 0x08, 0x01, 0x0C)); + }); + + it('encode group from multiple inputs', () => { + const bytes = + createArrayBuffer(0x0B, 0x08, 0x00, 0x0C, 0x0B, 0x08, 0x01, 0x0C); + const accessor = Kernel.fromArrayBuffer(bytes); + expect(accessor.serialize()).toEqual(bytes); + }); + + it('encode group after read', () => { + const bytes = + createArrayBuffer(0x0B, 0x08, 0x00, 0x0C, 0x0B, 0x08, 0x01, 0x0C); + const expected = createArrayBuffer(0x0B, 0x08, 0x01, 0x0C); + const accessor = Kernel.fromArrayBuffer(bytes); + accessor.getGroupOrNull(1, TestMessage.instanceCreator); + expect(accessor.serialize()).toEqual(expected); + }); + + it('return group from setter', () => { + const bytes = createArrayBuffer(0x08, 0x01); + const accessor = Kernel.fromArrayBuffer(new ArrayBuffer(0)); + const subaccessor = Kernel.fromArrayBuffer(bytes); + const submsg1 = new TestMessage(subaccessor); + accessor.setGroup(1, submsg1); + const submsg2 = accessor.getGroup(1, TestMessage.instanceCreator); + expect(submsg1).toBe(submsg2); + }); + + it('encode group from setter', () => { + const accessor = Kernel.fromArrayBuffer(new ArrayBuffer(0)); + const subaccessor = Kernel.fromArrayBuffer(createArrayBuffer(0x08, 0x01)); + const submsg = new TestMessage(subaccessor); + accessor.setGroup(1, submsg); + const expected = createArrayBuffer(0x0B, 0x08, 0x01, 0x0C); + expect(accessor.serialize()).toEqual(expected); + }); + + it('leave hasFieldNumber unchanged after getGroupOrNull', () => { + const accessor = Kernel.createEmpty(); + expect(accessor.hasFieldNumber(1)).toBe(false); + expect(accessor.getGroupOrNull(1, TestMessage.instanceCreator)).toBe(null); + expect(accessor.hasFieldNumber(1)).toBe(false); + }); + + it('serialize changes to subgroups made with getGroupsOrNull', () => { + const intTwoBytes = createArrayBuffer(0x0B, 0x08, 0x02, 0x0C); + const accessor = Kernel.fromArrayBuffer(intTwoBytes); + const mutableSubMessage = + accessor.getGroupOrNull(1, TestMessage.instanceCreator); + mutableSubMessage.setInt32(1, 10); + const intTenBytes = createArrayBuffer(0x0B, 0x08, 0x0A, 0x0C); + expect(accessor.serialize()).toEqual(intTenBytes); + }); + + it('serialize additions to subgroups made with getGroupOrNull', () => { + const intTwoBytes = createArrayBuffer(0x0B, 0x08, 0x02, 0x0C); + const accessor = Kernel.fromArrayBuffer(intTwoBytes); + const mutableSubMessage = + accessor.getGroupOrNull(1, TestMessage.instanceCreator); + mutableSubMessage.setInt32(2, 3); + // Sub group contains the original field, plus the new one. + expect(accessor.serialize()) + .toEqual(createArrayBuffer(0x0B, 0x08, 0x02, 0x10, 0x03, 0x0C)); + }); + + it('fail with getGroupOrNull if immutable group exist in cache', () => { + const intTwoBytes = createArrayBuffer(0x0B, 0x08, 0x02, 0x0C); + const accessor = Kernel.fromArrayBuffer(intTwoBytes); + + const readOnly = accessor.getGroup(1, TestMessage.instanceCreator); + if (CHECK_TYPE) { + expect(() => accessor.getGroupOrNull(1, TestMessage.instanceCreator)) + .toThrow(); + } else { + const mutableSubGropu = + accessor.getGroupOrNull(1, TestMessage.instanceCreator); + // The instance returned by getGroupOrNull is the exact same instance. + expect(mutableSubGropu).toBe(readOnly); + + // Serializing the subgroup does not write the changes + mutableSubGropu.setInt32(1, 0); + expect(accessor.serialize()).toEqual(intTwoBytes); + } + }); + + it('change hasFieldNumber after getGroupAttach', () => { + const accessor = Kernel.createEmpty(); + expect(accessor.hasFieldNumber(1)).toBe(false); + expect(accessor.getGroupAttach(1, TestMessage.instanceCreator)) + .not.toBe(null); + expect(accessor.hasFieldNumber(1)).toBe(true); + }); + + it('change hasFieldNumber after getGroupAttach when pivot is set', () => { + const accessor = Kernel.createEmpty(); + expect(accessor.hasFieldNumber(1)).toBe(false); + expect( + accessor.getGroupAttach(1, TestMessage.instanceCreator, /* pivot= */ 1)) + .not.toBe(null); + expect(accessor.hasFieldNumber(1)).toBe(true); + }); + + it('serialize subgroups made with getGroupAttach', () => { + const accessor = Kernel.createEmpty(); + const mutableSubGroup = + accessor.getGroupAttach(1, TestMessage.instanceCreator); + mutableSubGroup.setInt32(1, 10); + const intTenBytes = createArrayBuffer(0x0B, 0x08, 0x0A, 0x0C); + expect(accessor.serialize()).toEqual(intTenBytes); + }); + + it('serialize additions to subgroups using getMessageAttach', () => { + const intTwoBytes = createArrayBuffer(0x0B, 0x08, 0x02, 0x0C); + const accessor = Kernel.fromArrayBuffer(intTwoBytes); + const mutableSubGroup = + accessor.getGroupAttach(1, TestMessage.instanceCreator); + mutableSubGroup.setInt32(2, 3); + // Sub message contains the original field, plus the new one. + expect(accessor.serialize()) + .toEqual(createArrayBuffer(0x0B, 0x08, 0x02, 0x10, 0x03, 0x0C)); + }); + + it('fail with getGroupAttach if immutable message exist in cache', () => { + const intTwoBytes = createArrayBuffer(0x0B, 0x08, 0x02, 0x0C); + const accessor = Kernel.fromArrayBuffer(intTwoBytes); + + const readOnly = accessor.getGroup(1, TestMessage.instanceCreator); + if (CHECK_TYPE) { + expect(() => accessor.getGroupAttach(1, TestMessage.instanceCreator)) + .toThrow(); + } else { + const mutableSubGroup = + accessor.getGroupAttach(1, TestMessage.instanceCreator); + // The instance returned by getMessageOrNull is the exact same instance. + expect(mutableSubGroup).toBe(readOnly); + + // Serializing the submessage does not write the changes + mutableSubGroup.setInt32(1, 0); + expect(accessor.serialize()).toEqual(intTwoBytes); + } + }); + + it('read default group return empty group with getGroup', () => { + const bytes = new ArrayBuffer(0); + const accessor = Kernel.fromArrayBuffer(bytes); + expect(accessor.getGroup(1, TestMessage.instanceCreator)).toBeTruthy(); + expect(accessor.getGroup(1, TestMessage.instanceCreator).serialize()) + .toEqual(bytes); + }); + + it('read default group return null with getGroupOrNull', () => { + const bytes = new ArrayBuffer(0); + const accessor = Kernel.fromArrayBuffer(bytes); + expect(accessor.getGroupOrNull(1, TestMessage.instanceCreator)).toBe(null); + }); + + it('read group preserve reference equality', () => { + const bytes = createArrayBuffer(0x0B, 0x08, 0x02, 0x0C); + const accessor = Kernel.fromArrayBuffer(bytes); + const msg1 = accessor.getGroupOrNull(1, TestMessage.instanceCreator); + const msg2 = accessor.getGroupOrNull(1, TestMessage.instanceCreator); + const msg3 = accessor.getGroupAttach(1, TestMessage.instanceCreator); + expect(msg1).toBe(msg2); + expect(msg1).toBe(msg3); + }); + + it('fail when getting group with null instance constructor', () => { + const accessor = + Kernel.fromArrayBuffer(createArrayBuffer(0x0A, 0x02, 0x08, 0x01)); + const nullMessage = /** @type {function(!Kernel):!TestMessage} */ + (/** @type {*} */ (null)); + expect(() => accessor.getGroupOrNull(1, nullMessage)).toThrow(); + }); + + it('fail when setting group value with null value', () => { + const accessor = Kernel.fromArrayBuffer(new ArrayBuffer(0)); + const fakeMessage = /** @type {!TestMessage} */ (/** @type {*} */ (null)); + if (CHECK_CRITICAL_TYPE) { + expect(() => accessor.setGroup(1, fakeMessage)) + .toThrowError('Given value is not a message instance: null'); + } 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. + accessor.setMessage(1, fakeMessage); + expect(accessor.getGroupOrNull( + /* fieldNumber= */ 1, TestMessage.instanceCreator)) + .toBeNull(); + } + }); + + it('reads group in a longer buffer', () => { + const bytes = createArrayBuffer( + 0x12, 0x20, // 32 length delimited + 0x00, // random values for padding start + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, // random values for padding end + 0x0B, // Group tag + 0x08, 0x02, 0x0C); + const accessor = Kernel.fromArrayBuffer(bytes); + const msg1 = accessor.getGroupOrNull(1, TestMessage.instanceCreator); + const msg2 = accessor.getGroupOrNull(1, TestMessage.instanceCreator); + expect(msg1).toBe(msg2); + }); +}); diff --git a/js/experimental/runtime/kernel/tag.js b/js/experimental/runtime/kernel/tag.js new file mode 100644 index 0000000000..b288df3b8e --- /dev/null +++ b/js/experimental/runtime/kernel/tag.js @@ -0,0 +1,144 @@ +goog.module('protobuf.binary.tag'); + +const BufferDecoder = goog.require('protobuf.binary.BufferDecoder'); +const WireType = goog.require('protobuf.binary.WireType'); +const {checkCriticalElementIndex, checkCriticalState} = goog.require('protobuf.internal.checks'); + +/** + * Returns wire type stored in a tag. + * Protos store the wire type as the first 3 bit of a tag. + * @param {number} tag + * @return {!WireType} + */ +function tagToWireType(tag) { + return /** @type {!WireType} */ (tag & 0x07); +} + +/** + * Returns the field number stored in a tag. + * Protos store the field number in the upper 29 bits of a 32 bit number. + * @param {number} tag + * @return {number} + */ +function tagToFieldNumber(tag) { + return tag >>> 3; +} + +/** + * Combines wireType and fieldNumber into a tag. + * @param {!WireType} wireType + * @param {number} fieldNumber + * @return {number} + */ +function createTag(wireType, fieldNumber) { + return (fieldNumber << 3 | wireType) >>> 0; +} + +/** + * Returns the length, in bytes, of the field in the tag stream, less the tag + * itself. + * Note: This moves the cursor in the bufferDecoder. + * @param {!BufferDecoder} bufferDecoder + * @param {number} start + * @param {!WireType} wireType + * @param {number} fieldNumber + * @return {number} + * @private + */ +function getTagLength(bufferDecoder, start, wireType, fieldNumber) { + bufferDecoder.setCursor(start); + skipField(bufferDecoder, wireType, fieldNumber); + return bufferDecoder.cursor() - start; +} + +/** + * @param {number} value + * @return {number} + */ +function get32BitVarintLength(value) { + if (value < 0) { + return 5; + } + let size = 1; + while (value >= 128) { + size++; + value >>>= 7; + } + return size; +} + +/** + * Skips over a field. + * Note: If the field is a start group the entire group will be skipped, placing + * the cursor onto the next field. + * @param {!BufferDecoder} bufferDecoder + * @param {!WireType} wireType + * @param {number} fieldNumber + */ +function skipField(bufferDecoder, wireType, fieldNumber) { + switch (wireType) { + case WireType.VARINT: + checkCriticalElementIndex( + bufferDecoder.cursor(), bufferDecoder.endIndex()); + bufferDecoder.skipVarint(); + return; + case WireType.FIXED64: + bufferDecoder.skip(8); + return; + case WireType.DELIMITED: + checkCriticalElementIndex( + bufferDecoder.cursor(), bufferDecoder.endIndex()); + const length = bufferDecoder.getUnsignedVarint32(); + bufferDecoder.skip(length); + return; + case WireType.START_GROUP: + const foundGroup = skipGroup_(bufferDecoder, fieldNumber); + checkCriticalState(foundGroup, 'No end group found.'); + return; + case WireType.FIXED32: + bufferDecoder.skip(4); + return; + default: + throw new Error(`Unexpected wire type: ${wireType}`); + } +} + +/** + * Skips over fields until it finds the end of a given group consuming the stop + * group tag. + * @param {!BufferDecoder} bufferDecoder + * @param {number} groupFieldNumber + * @return {boolean} Whether the end group tag was found. + * @private + */ +function skipGroup_(bufferDecoder, groupFieldNumber) { + // On a start group we need to keep skipping fields until we find a + // corresponding stop group + // Note: Since we are calling skipField from here nested groups will be + // handled by recursion of this method and thus we will not see a nested + // STOP GROUP here unless there is something wrong with the input data. + while (bufferDecoder.hasNext()) { + const tag = bufferDecoder.getUnsignedVarint32(); + const wireType = tagToWireType(tag); + const fieldNumber = tagToFieldNumber(tag); + + if (wireType === WireType.END_GROUP) { + checkCriticalState( + groupFieldNumber === fieldNumber, + `Expected stop group for fieldnumber ${groupFieldNumber} not found.`); + return true; + } else { + skipField(bufferDecoder, wireType, fieldNumber); + } + } + return false; +} + +exports = { + createTag, + get32BitVarintLength, + getTagLength, + skipField, + tagToWireType, + tagToFieldNumber, +}; diff --git a/js/experimental/runtime/kernel/tag_test.js b/js/experimental/runtime/kernel/tag_test.js new file mode 100644 index 0000000000..04a6cb6668 --- /dev/null +++ b/js/experimental/runtime/kernel/tag_test.js @@ -0,0 +1,221 @@ +/** + * @fileoverview Tests for tag.js. + */ +goog.module('protobuf.binary.TagTests'); + +const BufferDecoder = goog.require('protobuf.binary.BufferDecoder'); +const WireType = goog.require('protobuf.binary.WireType'); +const {CHECK_CRITICAL_STATE} = goog.require('protobuf.internal.checks'); +const {createTag, get32BitVarintLength, skipField, tagToFieldNumber, tagToWireType} = goog.require('protobuf.binary.tag'); + + +goog.setTestOnly(); + +/** + * @param {...number} bytes + * @return {!ArrayBuffer} + */ +function createArrayBuffer(...bytes) { + return new Uint8Array(bytes).buffer; +} + +describe('skipField', () => { + it('skips varints', () => { + const bufferDecoder = + BufferDecoder.fromArrayBuffer(createArrayBuffer(0x80, 0x00)); + skipField(bufferDecoder, WireType.VARINT, 1); + expect(bufferDecoder.cursor()).toBe(2); + }); + + it('throws for out of bounds varints', () => { + const bufferDecoder = + BufferDecoder.fromArrayBuffer(createArrayBuffer(0x80, 0x00)); + bufferDecoder.setCursor(2); + if (CHECK_CRITICAL_STATE) { + expect(() => skipField(bufferDecoder, WireType.VARINT, 1)).toThrowError(); + } + }); + + it('skips fixed64', () => { + const bufferDecoder = BufferDecoder.fromArrayBuffer( + createArrayBuffer(0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)); + skipField(bufferDecoder, WireType.FIXED64, 1); + expect(bufferDecoder.cursor()).toBe(8); + }); + + it('throws for fixed64 if length is too short', () => { + const bufferDecoder = + BufferDecoder.fromArrayBuffer(createArrayBuffer(0x80, 0x00)); + if (CHECK_CRITICAL_STATE) { + expect(() => skipField(bufferDecoder, WireType.FIXED64, 1)) + .toThrowError(); + } + }); + + it('skips fixed32', () => { + const bufferDecoder = BufferDecoder.fromArrayBuffer( + createArrayBuffer(0x80, 0x00, 0x00, 0x00)); + skipField(bufferDecoder, WireType.FIXED32, 1); + expect(bufferDecoder.cursor()).toBe(4); + }); + + it('throws for fixed32 if length is too short', () => { + const bufferDecoder = + BufferDecoder.fromArrayBuffer(createArrayBuffer(0x80, 0x00)); + if (CHECK_CRITICAL_STATE) { + expect(() => skipField(bufferDecoder, WireType.FIXED32, 1)) + .toThrowError(); + } + }); + + + it('skips length delimited', () => { + const bufferDecoder = BufferDecoder.fromArrayBuffer( + createArrayBuffer(0x03, 0x00, 0x00, 0x00)); + skipField(bufferDecoder, WireType.DELIMITED, 1); + expect(bufferDecoder.cursor()).toBe(4); + }); + + it('throws for length delimited if length is too short', () => { + const bufferDecoder = + BufferDecoder.fromArrayBuffer(createArrayBuffer(0x03, 0x00, 0x00)); + if (CHECK_CRITICAL_STATE) { + expect(() => skipField(bufferDecoder, WireType.DELIMITED, 1)) + .toThrowError(); + } + }); + + it('skips groups', () => { + const bufferDecoder = BufferDecoder.fromArrayBuffer( + createArrayBuffer(0x0B, 0x08, 0x01, 0x0C)); + bufferDecoder.setCursor(1); + skipField(bufferDecoder, WireType.START_GROUP, 1); + expect(bufferDecoder.cursor()).toBe(4); + }); + + it('skips group in group', () => { + const buffer = createArrayBuffer( + 0x0B, // start outter + 0x10, 0x01, // field: 2, value: 1 + 0x0B, // start inner group + 0x10, 0x01, // payload inner group + 0x0C, // stop inner group + 0x0C // end outter + ); + const bufferDecoder = BufferDecoder.fromArrayBuffer(buffer); + bufferDecoder.setCursor(1); + skipField(bufferDecoder, WireType.START_GROUP, 1); + expect(bufferDecoder.cursor()).toBe(8); + }); + + it('throws for group if length is too short', () => { + // no closing group + const bufferDecoder = + BufferDecoder.fromArrayBuffer(createArrayBuffer(0x0B, 0x00, 0x00)); + if (CHECK_CRITICAL_STATE) { + expect(() => skipField(bufferDecoder, WireType.START_GROUP, 1)) + .toThrowError(); + } + }); +}); + + +describe('tagToWireType', () => { + it('decodes numbers ', () => { + // simple numbers + expect(tagToWireType(0x00)).toBe(WireType.VARINT); + expect(tagToWireType(0x01)).toBe(WireType.FIXED64); + expect(tagToWireType(0x02)).toBe(WireType.DELIMITED); + expect(tagToWireType(0x03)).toBe(WireType.START_GROUP); + expect(tagToWireType(0x04)).toBe(WireType.END_GROUP); + expect(tagToWireType(0x05)).toBe(WireType.FIXED32); + + // upper bits should not matter + expect(tagToWireType(0x08)).toBe(WireType.VARINT); + expect(tagToWireType(0x09)).toBe(WireType.FIXED64); + expect(tagToWireType(0x0A)).toBe(WireType.DELIMITED); + expect(tagToWireType(0x0B)).toBe(WireType.START_GROUP); + expect(tagToWireType(0x0C)).toBe(WireType.END_GROUP); + expect(tagToWireType(0x0D)).toBe(WireType.FIXED32); + + // upper bits should not matter + expect(tagToWireType(0xF8)).toBe(WireType.VARINT); + expect(tagToWireType(0xF9)).toBe(WireType.FIXED64); + expect(tagToWireType(0xFA)).toBe(WireType.DELIMITED); + expect(tagToWireType(0xFB)).toBe(WireType.START_GROUP); + expect(tagToWireType(0xFC)).toBe(WireType.END_GROUP); + expect(tagToWireType(0xFD)).toBe(WireType.FIXED32); + + // negative numbers work + expect(tagToWireType(-8)).toBe(WireType.VARINT); + expect(tagToWireType(-7)).toBe(WireType.FIXED64); + expect(tagToWireType(-6)).toBe(WireType.DELIMITED); + expect(tagToWireType(-5)).toBe(WireType.START_GROUP); + expect(tagToWireType(-4)).toBe(WireType.END_GROUP); + expect(tagToWireType(-3)).toBe(WireType.FIXED32); + }); +}); + +describe('tagToFieldNumber', () => { + it('returns fieldNumber', () => { + expect(tagToFieldNumber(0x08)).toBe(1); + expect(tagToFieldNumber(0x09)).toBe(1); + expect(tagToFieldNumber(0x10)).toBe(2); + expect(tagToFieldNumber(0x12)).toBe(2); + }); +}); + +describe('createTag', () => { + it('combines fieldNumber and wireType', () => { + expect(createTag(WireType.VARINT, 1)).toBe(0x08); + expect(createTag(WireType.FIXED64, 1)).toBe(0x09); + expect(createTag(WireType.DELIMITED, 1)).toBe(0x0A); + expect(createTag(WireType.START_GROUP, 1)).toBe(0x0B); + expect(createTag(WireType.END_GROUP, 1)).toBe(0x0C); + expect(createTag(WireType.FIXED32, 1)).toBe(0x0D); + + expect(createTag(WireType.VARINT, 2)).toBe(0x10); + expect(createTag(WireType.FIXED64, 2)).toBe(0x11); + expect(createTag(WireType.DELIMITED, 2)).toBe(0x12); + expect(createTag(WireType.START_GROUP, 2)).toBe(0x13); + expect(createTag(WireType.END_GROUP, 2)).toBe(0x14); + expect(createTag(WireType.FIXED32, 2)).toBe(0x15); + + expect(createTag(WireType.VARINT, 0x1FFFFFFF)).toBe(0xFFFFFFF8 >>> 0); + expect(createTag(WireType.FIXED64, 0x1FFFFFFF)).toBe(0xFFFFFFF9 >>> 0); + expect(createTag(WireType.DELIMITED, 0x1FFFFFFF)).toBe(0xFFFFFFFA >>> 0); + expect(createTag(WireType.START_GROUP, 0x1FFFFFFF)).toBe(0xFFFFFFFB >>> 0); + expect(createTag(WireType.END_GROUP, 0x1FFFFFFF)).toBe(0xFFFFFFFC >>> 0); + expect(createTag(WireType.FIXED32, 0x1FFFFFFF)).toBe(0xFFFFFFFD >>> 0); + }); +}); + +describe('get32BitVarintLength', () => { + it('length of tag', () => { + expect(get32BitVarintLength(0)).toBe(1); + expect(get32BitVarintLength(1)).toBe(1); + expect(get32BitVarintLength(1)).toBe(1); + + expect(get32BitVarintLength(Math.pow(2, 7) - 1)).toBe(1); + expect(get32BitVarintLength(Math.pow(2, 7))).toBe(2); + + expect(get32BitVarintLength(Math.pow(2, 14) - 1)).toBe(2); + expect(get32BitVarintLength(Math.pow(2, 14))).toBe(3); + + expect(get32BitVarintLength(Math.pow(2, 21) - 1)).toBe(3); + expect(get32BitVarintLength(Math.pow(2, 21))).toBe(4); + + expect(get32BitVarintLength(Math.pow(2, 28) - 1)).toBe(4); + expect(get32BitVarintLength(Math.pow(2, 28))).toBe(5); + + expect(get32BitVarintLength(Math.pow(2, 31) - 1)).toBe(5); + + expect(get32BitVarintLength(-1)).toBe(5); + expect(get32BitVarintLength(-Math.pow(2, 31))).toBe(5); + + expect(get32BitVarintLength(createTag(WireType.VARINT, 0x1fffffff))) + .toBe(5); + expect(get32BitVarintLength(createTag(WireType.FIXED32, 0x1fffffff))) + .toBe(5); + }); +}); diff --git a/js/experimental/runtime/kernel/writer.js b/js/experimental/runtime/kernel/writer.js index 8af7a06d0d..5b8b79b6c3 100644 --- a/js/experimental/runtime/kernel/writer.js +++ b/js/experimental/runtime/kernel/writer.js @@ -10,6 +10,7 @@ const Int64 = goog.require('protobuf.Int64'); const WireType = goog.require('protobuf.binary.WireType'); const {POLYFILL_TEXT_ENCODING, checkFieldNumber, checkTypeUnsignedInt32, checkWireType} = goog.require('protobuf.internal.checks'); const {concatenateByteArrays} = goog.require('protobuf.binary.uint8arrays'); +const {createTag, getTagLength} = goog.require('protobuf.binary.tag'); const {encode} = goog.require('protobuf.binary.textencoding'); /** @@ -92,8 +93,8 @@ class Writer { writeTag(fieldNumber, wireType) { checkFieldNumber(fieldNumber); checkWireType(wireType); - const tag = fieldNumber << 3 | wireType; - this.writeUnsignedVarint32_(tag >>> 0); + const tag = createTag(wireType, fieldNumber); + this.writeUnsignedVarint32_(tag); } /** @@ -299,6 +300,22 @@ class Writer { this.writeSfixed64Value_(value); } + /** + * Writes a sfixed64 value field to the buffer. + * @param {number} fieldNumber + */ + writeStartGroup(fieldNumber) { + this.writeTag(fieldNumber, WireType.START_GROUP); + } + + /** + * Writes a sfixed64 value field to the buffer. + * @param {number} fieldNumber + */ + writeEndGroup(fieldNumber) { + this.writeTag(fieldNumber, WireType.END_GROUP); + } + /** * Writes a uint32 value field to the buffer as a varint without tag. * @param {number} value @@ -430,67 +447,17 @@ class Writer { * @param {!BufferDecoder} bufferDecoder * @param {number} start * @param {!WireType} wireType + * @param {number} fieldNumber * @package */ - writeBufferDecoder(bufferDecoder, start, wireType) { + writeBufferDecoder(bufferDecoder, start, wireType, fieldNumber) { this.closeAndStartNewBuffer_(); - const dataLength = this.getLength_(bufferDecoder, start, wireType); + const dataLength = + getTagLength(bufferDecoder, start, wireType, fieldNumber); this.blocks_.push( bufferDecoder.subBufferDecoder(start, dataLength).asUint8Array()); } - /** - * Returns the length of the data to serialize. Returns -1 when a STOP GROUP - * is found. - * @param {!BufferDecoder} bufferDecoder - * @param {number} start - * @param {!WireType} wireType - * @return {number} - * @private - */ - getLength_(bufferDecoder, start, wireType) { - switch (wireType) { - case WireType.VARINT: - bufferDecoder.setCursor(start); - bufferDecoder.skipVarint(); - return bufferDecoder.cursor() - start; - case WireType.FIXED64: - return 8; - case WireType.DELIMITED: - const dataLength = bufferDecoder.getUnsignedVarint32At(start); - return dataLength + bufferDecoder.cursor() - start; - case WireType.START_GROUP: - return this.getGroupLength_(bufferDecoder, start); - case WireType.FIXED32: - return 4; - default: - throw new Error(`Invalid wire type: ${wireType}`); - } - } - - /** - * Skips over fields until it finds the end of a given group. - * @param {!BufferDecoder} bufferDecoder - * @param {number} start - * @return {number} - * @private - */ - getGroupLength_(bufferDecoder, start) { - // On a start group we need to keep skipping fields until we find a - // corresponding stop group - let cursor = start; - while (cursor < bufferDecoder.endIndex()) { - const tag = bufferDecoder.getUnsignedVarint32At(cursor); - const wireType = /** @type {!WireType} */ (tag & 0x07); - if (wireType === WireType.END_GROUP) { - return bufferDecoder.cursor() - start; - } - cursor = bufferDecoder.cursor() + - this.getLength_(bufferDecoder, bufferDecoder.cursor(), wireType); - } - throw new Error('No end group found'); - } - /** * Write the whole bytes as a length delimited field. * @param {number} fieldNumber diff --git a/js/experimental/runtime/kernel/writer_test.js b/js/experimental/runtime/kernel/writer_test.js index 331ab2b143..019ae1e18a 100644 --- a/js/experimental/runtime/kernel/writer_test.js +++ b/js/experimental/runtime/kernel/writer_test.js @@ -148,7 +148,7 @@ describe('Writer.writeBufferDecoder does', () => { const expected = createArrayBuffer( 0x08, /* varint start= */ 0xFF, /* varint end= */ 0x01, 0x08, 0x01); writer.writeBufferDecoder( - BufferDecoder.fromArrayBuffer(expected), 1, WireType.VARINT); + BufferDecoder.fromArrayBuffer(expected), 1, WireType.VARINT, 1); const result = writer.getAndResetResultBuffer(); expect(result).toEqual(arrayBufferSlice(expected, 1, 3)); }); @@ -159,7 +159,7 @@ describe('Writer.writeBufferDecoder does', () => { 0x09, /* fixed64 start= */ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, /* fixed64 end= */ 0x08, 0x08, 0x01); writer.writeBufferDecoder( - BufferDecoder.fromArrayBuffer(expected), 1, WireType.FIXED64); + BufferDecoder.fromArrayBuffer(expected), 1, WireType.FIXED64, 1); const result = writer.getAndResetResultBuffer(); expect(result).toEqual(arrayBufferSlice(expected, 1, 9)); }); @@ -170,7 +170,7 @@ describe('Writer.writeBufferDecoder does', () => { 0xA, /* length= */ 0x03, /* data start= */ 0x01, 0x02, /* data end= */ 0x03, 0x08, 0x01); writer.writeBufferDecoder( - BufferDecoder.fromArrayBuffer(expected), 1, WireType.DELIMITED); + BufferDecoder.fromArrayBuffer(expected), 1, WireType.DELIMITED, 1); const result = writer.getAndResetResultBuffer(); expect(result).toEqual(arrayBufferSlice(expected, 1, 5)); }); @@ -181,7 +181,7 @@ describe('Writer.writeBufferDecoder does', () => { 0xB, /* group start= */ 0x08, 0x01, /* nested group start= */ 0x0B, /* nested group end= */ 0x0C, /* group end= */ 0x0C, 0x08, 0x01); writer.writeBufferDecoder( - BufferDecoder.fromArrayBuffer(expected), 1, WireType.START_GROUP); + BufferDecoder.fromArrayBuffer(expected), 1, WireType.START_GROUP, 1); const result = writer.getAndResetResultBuffer(); expect(result).toEqual(arrayBufferSlice(expected, 1, 6)); }); @@ -192,7 +192,7 @@ describe('Writer.writeBufferDecoder does', () => { 0x09, /* fixed64 start= */ 0x01, 0x02, 0x03, /* fixed64 end= */ 0x04, 0x08, 0x01); writer.writeBufferDecoder( - BufferDecoder.fromArrayBuffer(expected), 1, WireType.FIXED32); + BufferDecoder.fromArrayBuffer(expected), 1, WireType.FIXED32, 1); const result = writer.getAndResetResultBuffer(); expect(result).toEqual(arrayBufferSlice(expected, 1, 5)); }); @@ -203,7 +203,7 @@ describe('Writer.writeBufferDecoder does', () => { const subBuffer = arrayBufferSlice(buffer, 0, 2); expect( () => writer.writeBufferDecoder( - BufferDecoder.fromArrayBuffer(subBuffer), 0, WireType.DELIMITED)) + BufferDecoder.fromArrayBuffer(subBuffer), 0, WireType.DELIMITED, 1)) .toThrow(); }); });