From ba2e33a0c92d97d35711825d930fbaa3aa695880 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Wed, 15 Dec 2021 14:47:58 -0700 Subject: [PATCH 1/4] Add double to bit encoding for OrderedCode --- .../src/index/ordered_code_writer.ts | 162 +++++++++++++ .../unit/index/ordered_code_writer.test.ts | 222 ++++++++++++++++++ 2 files changed, 384 insertions(+) create mode 100644 packages/firestore/src/index/ordered_code_writer.ts create mode 100644 packages/firestore/test/unit/index/ordered_code_writer.test.ts diff --git a/packages/firestore/src/index/ordered_code_writer.ts b/packages/firestore/src/index/ordered_code_writer.ts new file mode 100644 index 00000000000..2026987e3a5 --- /dev/null +++ b/packages/firestore/src/index/ordered_code_writer.ts @@ -0,0 +1,162 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law | agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES | CONDITIONS OF ANY KIND, either express | implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const LONG_SIZE = 64; +const BYTE_SIZE = 8; + +/** + * The default size of the buffer. This is arbitrary, but likely larger than + * most index values so that less copies of the underlying buffer will be made. + * For large values, a single copy will made to double the buffer length. + */ +const DEFAULT_BUFFER_SIZE = 1024; + +/** + * Converts a JavaScript number to a byte array (using little endian + * encoding). + */ +function doubleToLongBits(value: number): Uint8Array { + const dv = new DataView(new ArrayBuffer(8)); + dv.setFloat64(0, value, false); + return new Uint8Array(dv.buffer); +} + +/** + * Counts the number of zeros in a byte. + * + * Visible for testing. + */ +export function numberOfLeadingZerosInByte(x: number): number { + if (x === 0) { + return 8; + } + + let zeros = 0; + if (x >> 4 === 0) { + // Test if the first four bits are zero. + zeros += 4; + x = x << 4; + } + if (x >> 6 === 0) { + // Test if the first two (or next two) bits are zero. + zeros += 2; + x = x << 2; + } + if (x >> 7 === 0) { + // Test if the remaining bit is zero. + zeros += 1; + } + return zeros; +} + +/** Counts the number of leading zeros in the given byte array. */ +function numberOfLeadingZeros(bytes: Uint8Array): number { + let leadingZeros = 0; + for (let i = 0; i < bytes.length; ++i) { + const zeros = numberOfLeadingZerosInByte(bytes[i] & 0xff); + leadingZeros += zeros; + if (zeros !== 8) { + break; + } + } + return leadingZeros; +} + +/** + * Returns the number of bytes required to store "value". Leading zero bytes + * are skipped. + */ +function unsignedNumLength(value: Uint8Array): number { + // This is just the number of bytes for the unsigned representation of the number. + const numBits = LONG_SIZE - numberOfLeadingZeros(value); + return Math.ceil(numBits / BYTE_SIZE); +} + +/** + * OrderedCodeWriter is a minimal-allocation implementation of the writing + * behavior defined by the backend. + * + * The code is ported from its Java counterpart. + */ +export class OrderedCodeWriter { + buffer = new Uint8Array(DEFAULT_BUFFER_SIZE); + position = 0; + + writeNumberAscending(val: number): void { + const value = this.toOrderedBits(val); + const len = unsignedNumLength(value); + this.ensureAvailable(1 + len); + this.buffer[this.position++] = len & 0xff; // Write the length + for (let i = value.length - len; i < value.length; ++i) { + this.buffer[this.position++] = value[i] & 0xff; + } + } + + writeNumberDescending(val: number): void { + const value = this.toOrderedBits(val); + const len = unsignedNumLength(value); + this.ensureAvailable(1 + len); + this.buffer[this.position++] = ~(len & 0xff); // Write the length + for (let i = value.length - len; i < value.length; ++i) { + this.buffer[this.position++] = ~(value[i] & 0xff); + } + } + + /** + * Encodes `val` into an encoding so that the order matches the IEEE 754 + * floating-point comparison results with the following exceptions: + * -0.0 < 0.0 + * all non-NaN < NaN + * NaN = NaN + */ + private toOrderedBits(val: number): Uint8Array { + const value = doubleToLongBits(val); + const isNegative = (value[0] & 0x80) !== 0; + value[0] ^= isNegative ? 0xff : 0x80; + for (let i = 1; i < value.length; ++i) { + value[i] ^= isNegative ? 0xff : 0x00; + } + return value; + } + + /** Resets the buffer such that it is the same as when it was newly constructed. */ + reset(): void { + this.position = 0; + } + + /** Makes a copy of the encoded bytes in this buffer. */ + encodedBytes(): Uint8Array { + return this.buffer.slice(0, this.position); + } + + private ensureAvailable(bytes: number): void { + const minCapacity = bytes + this.position; + if (minCapacity <= this.buffer.length) { + return; + } + // Try doubling. + let newLength = this.buffer.length * 2; + // Still not big enough? Just allocate the right size. + if (newLength < minCapacity) { + newLength = minCapacity; + } + // Create the new buffer. + const newBuffer = new Uint8Array(newLength); + newBuffer.set(this.buffer); // copy old data + this.buffer = newBuffer; + } +} diff --git a/packages/firestore/test/unit/index/ordered_code_writer.test.ts b/packages/firestore/test/unit/index/ordered_code_writer.test.ts new file mode 100644 index 00000000000..0da4c4dbf08 --- /dev/null +++ b/packages/firestore/test/unit/index/ordered_code_writer.test.ts @@ -0,0 +1,222 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0x00 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0x00 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect } from 'chai'; + +import { + numberOfLeadingZerosInByte, + OrderedCodeWriter +} from '../../../src/index/ordered_code_writer'; + +class ValueTestCase { + constructor( + readonly val: T, + readonly ascEncoding: Uint8Array, + readonly descEncoding: Uint8Array + ) {} +} + +const NUMBER_TEST_CASES: Array> = [ + new ValueTestCase( + Number.NEGATIVE_INFINITY, + // Note: This values are taken from the Android reference implementation + new Uint8Array([0x07, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), + new Uint8Array([0xf8, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + ), + new ValueTestCase( + Number.MIN_SAFE_INTEGER, + new Uint8Array([0x08, 0x3c, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + new Uint8Array([0xf7, 0xc3, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) + ), + new ValueTestCase( + -2, + new Uint8Array([0x08, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), + new Uint8Array([0xf7, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + ), + new ValueTestCase( + -1, + new Uint8Array([0x08, 0x40, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), + new Uint8Array([0xf7, 0xbf, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + ), + new ValueTestCase( + -0.1, + new Uint8Array([0x08, 0x40, 0x46, 0x66, 0x66, 0x66, 0x66, 0x66, 0x65]), + new Uint8Array([0xf7, 0xbf, 0xb9, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9a]) + ), + new ValueTestCase( + -0.0, + new Uint8Array([0x08, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), + new Uint8Array([0xf7, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + ), + new ValueTestCase( + 0, + new Uint8Array([0x08, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + new Uint8Array([0xf7, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) + ), + new ValueTestCase( + Number.MIN_VALUE, + new Uint8Array([0x08, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]), + new Uint8Array([0xf7, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe]) + ), + new ValueTestCase( + 0.1, + new Uint8Array([0x08, 0xbf, 0xb9, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9a]), + new Uint8Array([0xf7, 0x40, 0x46, 0x66, 0x66, 0x66, 0x66, 0x66, 0x65]) + ), + new ValueTestCase( + 1, + new Uint8Array([0x08, 0xbf, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + new Uint8Array([0xf7, 0x40, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) + ), + new ValueTestCase( + 2, + new Uint8Array([0x08, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + new Uint8Array([0xf7, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) + ), + new ValueTestCase( + 4, + new Uint8Array([0x08, 0xc0, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + new Uint8Array([0xf7, 0x3f, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) + ), + new ValueTestCase( + 8, + new Uint8Array([0x08, 0xc0, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + new Uint8Array([0xf7, 0x3f, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) + ), + new ValueTestCase( + 16, + new Uint8Array([0x08, 0xc0, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + new Uint8Array([0xf7, 0x3f, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) + ), + new ValueTestCase( + 32, + new Uint8Array([0x08, 0xc0, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + new Uint8Array([0xf7, 0x3f, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) + ), + new ValueTestCase( + 64, + new Uint8Array([0x08, 0xc0, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + new Uint8Array([0xf7, 0x3f, 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) + ), + new ValueTestCase( + 128, + new Uint8Array([0x08, 0xc0, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + new Uint8Array([0xf7, 0x3f, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) + ), + new ValueTestCase( + 255, + new Uint8Array([0x08, 0xc0, 0x6f, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00]), + new Uint8Array([0xf7, 0x3f, 0x90, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff]) + ), + new ValueTestCase( + 256, + new Uint8Array([0x08, 0xc0, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + new Uint8Array([0xf7, 0x3f, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) + ), + new ValueTestCase( + 257, + new Uint8Array([0x08, 0xc0, 0x70, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00]), + new Uint8Array([0xf7, 0x3f, 0x8f, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff]) + ), + new ValueTestCase( + Number.MAX_SAFE_INTEGER, + new Uint8Array([0x08, 0xc3, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), + new Uint8Array([0xf7, 0x3c, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + ), + new ValueTestCase( + Number.POSITIVE_INFINITY, + new Uint8Array([0x08, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + new Uint8Array([0xf7, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) + ), + new ValueTestCase( + Number.NaN, + new Uint8Array([0x08, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + new Uint8Array([0xf7, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) + ) +]; + +describe('Ordered Code Writer', () => { + it('computes number of leading zeros', () => { + for (let i = 0; i < 0xff; ++i) { + let zeros = 0; + for (let bit = 7; bit >= 0; --bit) { + if ((i & (1 << bit)) === 0) { + ++zeros; + } else { + break; + } + } + expect(numberOfLeadingZerosInByte(i)).to.equal(zeros, `for number ${i}`); + } + }); + + it('converts numbers to bits', () => { + for (let i = 0; i < NUMBER_TEST_CASES.length; ++i) { + const bytes = getBytes(NUMBER_TEST_CASES[i].val); + expect(bytes.asc).to.deep.equal( + NUMBER_TEST_CASES[i].ascEncoding, + 'Ascending for ' + NUMBER_TEST_CASES[i].val + ); + expect(bytes.desc).to.deep.equal( + NUMBER_TEST_CASES[i].descEncoding, + 'Descending for ' + NUMBER_TEST_CASES[i].val + ); + } + }); + + it('orders numbers correctly', () => { + for (let i = 0; i < NUMBER_TEST_CASES.length; ++i) { + for (let j = i; j < NUMBER_TEST_CASES.length; ++j) { + const left = NUMBER_TEST_CASES[i].val; + const leftBytes = getBytes(left); + const right = NUMBER_TEST_CASES[j].val; + const rightBytes = getBytes(right); + expect(compare(leftBytes.asc, rightBytes.asc)).to.equal( + i === j ? 0 : -1, + `Ascending order: ${left} vs ${right}` + ); + expect(compare(leftBytes.desc, rightBytes.desc)).to.equal( + i === j ? 0 : 1, + `Descending order: ${left} vs ${right}` + ); + } + } + }); +}); + +function compare(left: Uint8Array, right: Uint8Array): number { + for (let i = 0; i < Math.min(left.length, right.length); ++i) { + if (left[i] < right[i]) { + return -1; + } + if (left[i] > right[i]) { + return 1; + } + } + return left.length - right.length; +} + +function getBytes(val: unknown): { asc: Uint8Array; desc: Uint8Array } { + const ascWriter = new OrderedCodeWriter(); + const descWriter = new OrderedCodeWriter(); + if (typeof val === 'number') { + ascWriter.writeNumberAscending(val); + descWriter.writeNumberDescending(val); + } else { + throw new Error('Encoding not yet supported for ' + val); + } + return { asc: ascWriter.encodedBytes(), desc: descWriter.encodedBytes() }; +} From 92a8b8ef7a7bc906a39a01f1862b2e3d54391e31 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Thu, 16 Dec 2021 13:24:49 -0700 Subject: [PATCH 2/4] Comments --- .../src/index/ordered_code_writer.ts | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/firestore/src/index/ordered_code_writer.ts b/packages/firestore/src/index/ordered_code_writer.ts index 2026987e3a5..0368c45e8d2 100644 --- a/packages/firestore/src/index/ordered_code_writer.ts +++ b/packages/firestore/src/index/ordered_code_writer.ts @@ -14,6 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { debugAssert } from '../util/assert'; const LONG_SIZE = 64; const BYTE_SIZE = 8; @@ -25,13 +26,10 @@ const BYTE_SIZE = 8; */ const DEFAULT_BUFFER_SIZE = 1024; -/** - * Converts a JavaScript number to a byte array (using little endian - * encoding). - */ +/** Converts a JavaScript number to a byte array (using big endian encoding). */ function doubleToLongBits(value: number): Uint8Array { const dv = new DataView(new ArrayBuffer(8)); - dv.setFloat64(0, value, false); + dv.setFloat64(0, value, /* littleEndian= */ false); return new Uint8Array(dv.buffer); } @@ -41,6 +39,7 @@ function doubleToLongBits(value: number): Uint8Array { * Visible for testing. */ export function numberOfLeadingZerosInByte(x: number): number { + debugAssert(x < 256, 'Provided value is not a byte: ' + x); if (x === 0) { return 8; } @@ -65,8 +64,12 @@ export function numberOfLeadingZerosInByte(x: number): number { /** Counts the number of leading zeros in the given byte array. */ function numberOfLeadingZeros(bytes: Uint8Array): number { + debugAssert( + bytes.length == 8, + 'Can only count leading zeros in 64-bit numbers' + ); let leadingZeros = 0; - for (let i = 0; i < bytes.length; ++i) { + for (let i = 0; i < 8; ++i) { const zeros = numberOfLeadingZerosInByte(bytes[i] & 0xff); leadingZeros += zeros; if (zeros !== 8) { @@ -97,6 +100,8 @@ export class OrderedCodeWriter { position = 0; writeNumberAscending(val: number): void { + // Values are encoded with a single byte length prefix, followed by the + // actual value in big-endian format with leading 0 bytes dropped. const value = this.toOrderedBits(val); const len = unsignedNumLength(value); this.ensureAvailable(1 + len); @@ -107,6 +112,8 @@ export class OrderedCodeWriter { } writeNumberDescending(val: number): void { + // Values are encoded with a single byte length prefix, followed by the + // inverted value in big-endian format with leading 0 bytes dropped. const value = this.toOrderedBits(val); const len = unsignedNumLength(value); this.ensureAvailable(1 + len); @@ -125,7 +132,11 @@ export class OrderedCodeWriter { */ private toOrderedBits(val: number): Uint8Array { const value = doubleToLongBits(val); + // Check if the first bit is set. We use a bit mask since value[0] is + // encoded as a number from 0 to 255. const isNegative = (value[0] & 0x80) !== 0; + + // Revert the two complement to get natural ordering value[0] ^= isNegative ? 0xff : 0x80; for (let i = 1; i < value.length; ++i) { value[i] ^= isNegative ? 0xff : 0x00; From 98b861a57f77b63e4ac88f0ae3e498dce4110200 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Thu, 16 Dec 2021 15:27:50 -0700 Subject: [PATCH 3/4] Use Hex in tests --- .../src/index/ordered_code_writer.ts | 2 +- .../unit/index/ordered_code_writer.test.ts | 146 +++++------------- 2 files changed, 42 insertions(+), 106 deletions(-) diff --git a/packages/firestore/src/index/ordered_code_writer.ts b/packages/firestore/src/index/ordered_code_writer.ts index 0368c45e8d2..af634fb5d00 100644 --- a/packages/firestore/src/index/ordered_code_writer.ts +++ b/packages/firestore/src/index/ordered_code_writer.ts @@ -65,7 +65,7 @@ export function numberOfLeadingZerosInByte(x: number): number { /** Counts the number of leading zeros in the given byte array. */ function numberOfLeadingZeros(bytes: Uint8Array): number { debugAssert( - bytes.length == 8, + bytes.length === 8, 'Can only count leading zeros in 64-bit numbers' ); let leadingZeros = 0; 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 0da4c4dbf08..0ff46b373ce 100644 --- a/packages/firestore/test/unit/index/ordered_code_writer.test.ts +++ b/packages/firestore/test/unit/index/ordered_code_writer.test.ts @@ -24,8 +24,8 @@ import { class ValueTestCase { constructor( readonly val: T, - readonly ascEncoding: Uint8Array, - readonly descEncoding: Uint8Array + readonly ascString: string, + readonly descString: string ) {} } @@ -33,119 +33,47 @@ const NUMBER_TEST_CASES: Array> = [ new ValueTestCase( Number.NEGATIVE_INFINITY, // Note: This values are taken from the Android reference implementation - new Uint8Array([0x07, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), - new Uint8Array([0xf8, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + '070fffffffffffff', + 'f8f0000000000000' ), new ValueTestCase( Number.MIN_SAFE_INTEGER, - new Uint8Array([0x08, 0x3c, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - new Uint8Array([0xf7, 0xc3, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) - ), - new ValueTestCase( - -2, - new Uint8Array([0x08, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), - new Uint8Array([0xf7, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) - ), - new ValueTestCase( - -1, - new Uint8Array([0x08, 0x40, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), - new Uint8Array([0xf7, 0xbf, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) - ), - new ValueTestCase( - -0.1, - new Uint8Array([0x08, 0x40, 0x46, 0x66, 0x66, 0x66, 0x66, 0x66, 0x65]), - new Uint8Array([0xf7, 0xbf, 0xb9, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9a]) - ), - new ValueTestCase( - -0.0, - new Uint8Array([0x08, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), - new Uint8Array([0xf7, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) - ), - new ValueTestCase( - 0, - new Uint8Array([0x08, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - new Uint8Array([0xf7, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) + '083cc0000000000000', + 'f7c33fffffffffffff' ), + new ValueTestCase(-2, '083fffffffffffffff', 'f7c000000000000000'), + new ValueTestCase(-1, '08400fffffffffffff', 'f7bff0000000000000'), + new ValueTestCase(-0.1, '084046666666666665', 'f7bfb999999999999a'), + new ValueTestCase(-0.0, '087fffffffffffffff', 'f78000000000000000'), + new ValueTestCase(0, '088000000000000000', 'f77fffffffffffffff'), new ValueTestCase( Number.MIN_VALUE, - new Uint8Array([0x08, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]), - new Uint8Array([0xf7, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe]) - ), - new ValueTestCase( - 0.1, - new Uint8Array([0x08, 0xbf, 0xb9, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9a]), - new Uint8Array([0xf7, 0x40, 0x46, 0x66, 0x66, 0x66, 0x66, 0x66, 0x65]) - ), - new ValueTestCase( - 1, - new Uint8Array([0x08, 0xbf, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - new Uint8Array([0xf7, 0x40, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) - ), - new ValueTestCase( - 2, - new Uint8Array([0x08, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - new Uint8Array([0xf7, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) - ), - new ValueTestCase( - 4, - new Uint8Array([0x08, 0xc0, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - new Uint8Array([0xf7, 0x3f, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) - ), - new ValueTestCase( - 8, - new Uint8Array([0x08, 0xc0, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - new Uint8Array([0xf7, 0x3f, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) - ), - new ValueTestCase( - 16, - new Uint8Array([0x08, 0xc0, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - new Uint8Array([0xf7, 0x3f, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) - ), - new ValueTestCase( - 32, - new Uint8Array([0x08, 0xc0, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - new Uint8Array([0xf7, 0x3f, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) - ), - new ValueTestCase( - 64, - new Uint8Array([0x08, 0xc0, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - new Uint8Array([0xf7, 0x3f, 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) - ), - new ValueTestCase( - 128, - new Uint8Array([0x08, 0xc0, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - new Uint8Array([0xf7, 0x3f, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) - ), - new ValueTestCase( - 255, - new Uint8Array([0x08, 0xc0, 0x6f, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00]), - new Uint8Array([0xf7, 0x3f, 0x90, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff]) - ), - new ValueTestCase( - 256, - new Uint8Array([0x08, 0xc0, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - new Uint8Array([0xf7, 0x3f, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) - ), - new ValueTestCase( - 257, - new Uint8Array([0x08, 0xc0, 0x70, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00]), - new Uint8Array([0xf7, 0x3f, 0x8f, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff]) - ), + '088000000000000001', + 'f77ffffffffffffffe' + ), + new ValueTestCase(0.1, '08bfb999999999999a', 'f74046666666666665'), + new ValueTestCase(1, '08bff0000000000000', 'f7400fffffffffffff'), + new ValueTestCase(2, '08c000000000000000', 'f73fffffffffffffff'), + new ValueTestCase(4, '08c010000000000000', 'f73fefffffffffffff'), + new ValueTestCase(8, '08c020000000000000', 'f73fdfffffffffffff'), + new ValueTestCase(16, '08c030000000000000', 'f73fcfffffffffffff'), + new ValueTestCase(32, '08c040000000000000', 'f73fbfffffffffffff'), + new ValueTestCase(64, '08c050000000000000', 'f73fafffffffffffff'), + new ValueTestCase(128, '08c060000000000000', 'f73f9fffffffffffff'), + new ValueTestCase(255, '08c06fe00000000000', 'f73f901fffffffffff'), + new ValueTestCase(256, '08c070000000000000', 'f73f8fffffffffffff'), + new ValueTestCase(257, '08c070100000000000', 'f73f8fefffffffffff'), new ValueTestCase( Number.MAX_SAFE_INTEGER, - new Uint8Array([0x08, 0xc3, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), - new Uint8Array([0xf7, 0x3c, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + '08c33fffffffffffff', + 'f73cc0000000000000' ), new ValueTestCase( Number.POSITIVE_INFINITY, - new Uint8Array([0x08, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - new Uint8Array([0xf7, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) + '08fff0000000000000', + 'f7000fffffffffffff' ), - new ValueTestCase( - Number.NaN, - new Uint8Array([0x08, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - new Uint8Array([0xf7, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) - ) + new ValueTestCase(Number.NaN, '08fff8000000000000', 'f70007ffffffffffff') ]; describe('Ordered Code Writer', () => { @@ -167,11 +95,11 @@ describe('Ordered Code Writer', () => { for (let i = 0; i < NUMBER_TEST_CASES.length; ++i) { const bytes = getBytes(NUMBER_TEST_CASES[i].val); expect(bytes.asc).to.deep.equal( - NUMBER_TEST_CASES[i].ascEncoding, + fromHex(NUMBER_TEST_CASES[i].ascString), 'Ascending for ' + NUMBER_TEST_CASES[i].val ); expect(bytes.desc).to.deep.equal( - NUMBER_TEST_CASES[i].descEncoding, + fromHex(NUMBER_TEST_CASES[i].descString), 'Descending for ' + NUMBER_TEST_CASES[i].val ); } @@ -197,6 +125,14 @@ describe('Ordered Code Writer', () => { }); }); +function fromHex(hexString: string): Uint8Array { + const bytes = new Uint8Array(hexString.length / 2); + for (let c = 0; c < hexString.length; c += 2) { + bytes[c / 2] = parseInt(hexString.substr(c, 2), 16); + } + return bytes; +} + function compare(left: Uint8Array, right: Uint8Array): number { for (let i = 0; i < Math.min(left.length, right.length); ++i) { if (left[i] < right[i]) { From 51d278493740e7d585b7cd34de6541ffd5917625 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Tue, 4 Jan 2022 11:12:30 -0700 Subject: [PATCH 4/4] Format --- packages/firestore/src/index/ordered_code_writer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/firestore/src/index/ordered_code_writer.ts b/packages/firestore/src/index/ordered_code_writer.ts index baece1a6e9d..9f32c2d4fef 100644 --- a/packages/firestore/src/index/ordered_code_writer.ts +++ b/packages/firestore/src/index/ordered_code_writer.ts @@ -14,8 +14,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {debugAssert, fail} from '../util/assert'; -import {ByteString} from "../util/byte_string"; +import { debugAssert, fail } from '../util/assert'; +import { ByteString } from '../util/byte_string'; const LONG_SIZE = 64; const BYTE_SIZE = 8;