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..7e6676bc4d0 --- /dev/null +++ b/packages/firestore/src/index/ordered_code_writer.ts @@ -0,0 +1,158 @@ +/** + * @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. */ +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 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 that has the following properties: + * 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..fe1aa188a00 --- /dev/null +++ b/packages/firestore/test/unit/index/ordered_code_writer.test.ts @@ -0,0 +1,217 @@ +/** + * @license + * Copyright 2017 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, + 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( + 0x08, + new Uint8Array([0x08, 0xc0, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + new Uint8Array([0xf7, 0x3f, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) + ), + new ValueTestCase( + 0x10, + new Uint8Array([0x08, 0xc0, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + new Uint8Array([0xf7, 0x3f, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) + ), + new ValueTestCase( + 0x20, + new Uint8Array([0x08, 0xc0, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + new Uint8Array([0xf7, 0x3f, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) + ), + new ValueTestCase( + 0x40, + new Uint8Array([0x08, 0xc0, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + new Uint8Array([0xf7, 0x3f, 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) + ), + new ValueTestCase( + 0x80, + new Uint8Array([0x08, 0xc0, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + new Uint8Array([0xf7, 0x3f, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) + ), + new ValueTestCase( + 0xff, + 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() }; +}