diff --git a/packages/firestore/src/index/ordered_code_writer.ts b/packages/firestore/src/index/ordered_code_writer.ts index f94056df9d8..68b29c598ef 100644 --- a/packages/firestore/src/index/ordered_code_writer.ts +++ b/packages/firestore/src/index/ordered_code_writer.ts @@ -15,6 +15,7 @@ * limitations under the License. */ import { debugAssert } from '../util/assert'; +import { ByteString } from '../util/byte_string'; /** These constants are taken from the backend. */ const MIN_SURROGATE = '\uD800'; @@ -25,6 +26,7 @@ const NULL_BYTE = 0xff; // Combined with ESCAPE1 const SEPARATOR = 0x01; // Combined with ESCAPE1 const ESCAPE2 = 0xff; +const INFINITY = 0xff; // Combined with ESCAPE2 const FF_BYTE = 0x00; // Combined with ESCAPE2 const LONG_SIZE = 64; @@ -110,6 +112,26 @@ export class OrderedCodeWriter { buffer = new Uint8Array(DEFAULT_BUFFER_SIZE); position = 0; + writeBytesAscending(value: ByteString): void { + const it = value[Symbol.iterator](); + let byte = it.next(); + while (!byte.done) { + this.writeByteAscending(byte.value); + byte = it.next(); + } + this.writeSeparatorAscending(); + } + + writeBytesDescending(value: ByteString): void { + const it = value[Symbol.iterator](); + let byte = it.next(); + while (!byte.done) { + this.writeByteDescending(byte.value); + byte = it.next(); + } + this.writeSeparatorDescending(); + } + /** Writes utf8 bytes into this byte sequence, ascending. */ writeUtf8Ascending(sequence: string): void { for (const c of sequence) { @@ -182,6 +204,24 @@ export class OrderedCodeWriter { } } + /** + * Writes the "infinity" byte sequence that sorts after all other byte + * sequences written in ascending order. + */ + writeInfinityAscending(): void { + this.writeEscapedByteAscending(ESCAPE2); + this.writeEscapedByteAscending(INFINITY); + } + + /** + * Writes the "infinity" byte sequence that sorts before all other byte + * sequences written in descending order. + */ + writeInfinityDescending(): void { + this.writeEscapedByteDescending(ESCAPE2); + this.writeEscapedByteDescending(INFINITY); + } + /** * Encodes `val` into an encoding so that the order matches the IEEE 754 * floating-point comparison results with the following exceptions: @@ -277,4 +317,10 @@ export class OrderedCodeWriter { newBuffer.set(this.buffer); // copy old data this.buffer = newBuffer; } + + seed(encodedBytes: Uint8Array): void { + this.ensureAvailable(encodedBytes.length); + this.buffer.set(encodedBytes, this.position); + this.position += encodedBytes.length; + } } diff --git a/packages/firestore/src/util/byte_string.ts b/packages/firestore/src/util/byte_string.ts index f34efb44927..73b5cd1fa1a 100644 --- a/packages/firestore/src/util/byte_string.ts +++ b/packages/firestore/src/util/byte_string.ts @@ -43,6 +43,19 @@ export class ByteString { return new ByteString(binaryString); } + [Symbol.iterator](): Iterator { + let i = 0; + return { + next: () => { + if (i < this.binaryString.length) { + return { value: this.binaryString.charCodeAt(i++), done: false }; + } else { + return { value: undefined, done: true }; + } + } + }; + } + toBase64(): string { return encodeBase64(this.binaryString); } diff --git a/packages/firestore/test/unit/index/ordered_code_writer.test.ts b/packages/firestore/test/unit/index/ordered_code_writer.test.ts index 5da6b78d0c7..823173ead14 100644 --- a/packages/firestore/test/unit/index/ordered_code_writer.test.ts +++ b/packages/firestore/test/unit/index/ordered_code_writer.test.ts @@ -20,6 +20,7 @@ import { numberOfLeadingZerosInByte, OrderedCodeWriter } from '../../../src/index/ordered_code_writer'; +import { ByteString } from '../../../src/util/byte_string'; class ValueTestCase { constructor( @@ -108,6 +109,23 @@ const STRING_TEST_CASES: Array> = [ ) ]; +const BYTES_TEST_CASES: Array> = [ + new ValueTestCase(fromHex(''), '0001', 'fffe'), + new ValueTestCase(fromHex('00'), '00ff0001', 'ff00fffe'), + new ValueTestCase(fromHex('0000'), '00ff00ff0001', 'ff00ff00fffe'), + new ValueTestCase(fromHex('0001'), '00ff010001', 'ff00fefffe'), + new ValueTestCase(fromHex('0041'), '00ff410001', 'ff00befffe'), + new ValueTestCase(fromHex('00ff'), '00ffff000001', 'ff0000fffffe'), + new ValueTestCase(fromHex('01'), '010001', 'fefffe'), + new ValueTestCase(fromHex('0100'), '0100ff0001', 'feff00fffe'), + new ValueTestCase(fromHex('6f776c'), '6f776c0001', '908893fffe'), + new ValueTestCase(fromHex('ff'), 'ff000001', '00fffffe'), + new ValueTestCase(fromHex('ff00'), 'ff0000ff0001', '00ffff00fffe'), + new ValueTestCase(fromHex('ff01'), 'ff00010001', '00fffefffe'), + new ValueTestCase(fromHex('ffff'), 'ff00ff000001', '00ff00fffffe'), + new ValueTestCase(fromHex('ffffff'), 'ff00ff00ff000001', '00ff00ff00fffffe') +]; + describe('Ordered Code Writer', () => { it('computes number of leading zeros', () => { for (let i = 0; i < 0xff; ++i) { @@ -139,6 +157,34 @@ describe('Ordered Code Writer', () => { verifyOrdering(STRING_TEST_CASES); }); + it('converts bytes to bits', () => { + verifyEncoding(BYTES_TEST_CASES); + }); + + it('orders bytes correctly', () => { + verifyOrdering(BYTES_TEST_CASES); + }); + + it('encodes infinity', () => { + const writer = new OrderedCodeWriter(); + writer.writeInfinityAscending(); + expect(writer.encodedBytes()).to.deep.equal(fromHex("ffff")); + + writer.reset(); + writer.writeInfinityDescending(); + expect(writer.encodedBytes()).to.deep.equal(fromHex("0000")); + }); + + it('seeds bytes', () => { + const writer = new OrderedCodeWriter(); + writer.seed(fromHex("01")); + writer.writeInfinityAscending(); + writer.seed(fromHex("02")); + expect(writer.encodedBytes()).to.deep.equal( + fromHex("01ffff02") + ); + }); + function verifyEncoding(testCases: Array>): void { for (let i = 0; i < testCases.length; ++i) { const bytes = getBytes(testCases[i].val); @@ -202,8 +248,9 @@ function getBytes(val: unknown): { asc: Uint8Array; desc: Uint8Array } { } else if (typeof val === 'string') { ascWriter.writeUtf8Ascending(val); descWriter.writeUtf8Descending(val); - } else { - throw new Error('Encoding not yet supported for ' + val); + } else if (val instanceof Uint8Array) { + ascWriter.writeBytesAscending(ByteString.fromUint8Array(val)); + descWriter.writeBytesDescending(ByteString.fromUint8Array(val)); } return { asc: ascWriter.encodedBytes(), desc: descWriter.encodedBytes() }; }