Skip to content

Commit

Permalink
Finish OrderedCode (#5821)
Browse files Browse the repository at this point in the history
  • Loading branch information
schmidt-sebastian committed Jan 4, 2022
1 parent 4f3662b commit be7ccb8
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 35 deletions.
90 changes: 59 additions & 31 deletions packages/firestore/src/index/ordered_code_writer.ts
Expand Up @@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { debugAssert, fail } from '../util/assert';
import { debugAssert } from '../util/assert';
import { ByteString } from '../util/byte_string';

/** These constants are taken from the backend. */
Expand All @@ -26,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;
Expand Down Expand Up @@ -111,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) {
Expand Down Expand Up @@ -183,6 +204,43 @@ 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);
}

/**
* Resets the buffer such that it is the same as when it was newly
* constructed.
*/
reset(): void {
this.position = 0;
}

seed(encodedBytes: Uint8Array): void {
this.ensureAvailable(encodedBytes.length);
this.buffer.set(encodedBytes, this.position);
this.position += encodedBytes.length;
}

/** Makes a copy of the encoded bytes in this buffer. */
encodedBytes(): Uint8Array {
return this.buffer.slice(0, this.position);
}

/**
* Encodes `val` into an encoding so that the order matches the IEEE 754
* floating-point comparison results with the following exceptions:
Expand All @@ -204,16 +262,6 @@ export class OrderedCodeWriter {
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);
}

/** Writes a single byte ascending to the buffer. */
private writeByteAscending(b: number): void {
const masked = b & 0xff;
Expand Down Expand Up @@ -262,26 +310,6 @@ export class OrderedCodeWriter {
this.buffer[this.position++] = ~b;
}

writeBytesAscending(value: ByteString): void {
fail('Not implemented');
}

writeBytesDescending(value: ByteString): void {
fail('Not implemented');
}

writeInfinityAscending(): void {
fail('Not implemented');
}

writeInfinityDescending(): void {
fail('Not implemented');
}

seed(encodedBytes: Uint8Array): void {
fail('Not implemented');
}

private ensureAvailable(bytes: number): void {
const minCapacity = bytes + this.position;
if (minCapacity <= this.buffer.length) {
Expand Down
13 changes: 13 additions & 0 deletions packages/firestore/src/util/byte_string.ts
Expand Up @@ -43,6 +43,19 @@ export class ByteString {
return new ByteString(binaryString);
}

[Symbol.iterator](): Iterator<number> {
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);
}
Expand Down
54 changes: 50 additions & 4 deletions packages/firestore/test/unit/index/ordered_code_writer.test.ts
Expand Up @@ -20,6 +20,8 @@ import {
numberOfLeadingZerosInByte,
OrderedCodeWriter
} from '../../../src/index/ordered_code_writer';
import { hardAssert } from '../../../src/util/assert';
import { ByteString } from '../../../src/util/byte_string';

class ValueTestCase<T> {
constructor(
Expand Down Expand Up @@ -108,6 +110,23 @@ const STRING_TEST_CASES: Array<ValueTestCase<string>> = [
)
];

const BYTES_TEST_CASES: Array<ValueTestCase<Uint8Array>> = [
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) {
Expand Down Expand Up @@ -139,6 +158,32 @@ 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<ValueTestCase<unknown>>): void {
for (let i = 0; i < testCases.length; ++i) {
const bytes = getBytes(testCases[i].val);
Expand All @@ -159,7 +204,6 @@ describe('Ordered Code Writer', () => {
const left = testCases[i].val;
const leftBytes = getBytes(left);
const right = testCases[j].val;

const rightBytes = getBytes(right);
expect(compare(leftBytes.asc, rightBytes.asc)).to.equal(
i === j ? 0 : -1,
Expand All @@ -176,8 +220,8 @@ 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);
for (let i = 0; i < hexString.length; i += 2) {
bytes[i / 2] = parseInt(hexString.substr(i, 2), 16);
}
return bytes;
}
Expand All @@ -204,7 +248,9 @@ function getBytes(val: unknown): { asc: Uint8Array; desc: Uint8Array } {
ascWriter.writeUtf8Ascending(val);
descWriter.writeUtf8Descending(val);
} else {
throw new Error('Encoding not yet supported for ' + val);
hardAssert(val instanceof Uint8Array);
ascWriter.writeBytesAscending(ByteString.fromUint8Array(val));
descWriter.writeBytesDescending(ByteString.fromUint8Array(val));
}
return { asc: ascWriter.encodedBytes(), desc: descWriter.encodedBytes() };
}

0 comments on commit be7ccb8

Please sign in to comment.