From b23c98dd390db79e2243b0a7015d008b2f3f45f9 Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Thu, 22 Dec 2022 21:56:03 +0800 Subject: [PATCH] javascript: fix tile id overflow for z > 15 and error assertions in tests. --- js/index.ts | 27 ++++++++++++------- js/test/v3.test.ts | 64 ++++++++++++++++++++++++++++------------------ 2 files changed, 57 insertions(+), 34 deletions(-) diff --git a/js/index.ts b/js/index.ts index 57d2181..18338d2 100644 --- a/js/index.ts +++ b/js/index.ts @@ -70,43 +70,50 @@ function rotate(n: number, xy: number[], rx: number, ry: number): void { } function idOnLevel(z: number, pos: number): [number, number, number] { - const n = 1 << z; + const n = Math.pow(2, z); let rx = pos; let ry = pos; let t = pos; const xy = [0, 0]; let s = 1; while (s < n) { - rx = 1 & ((t / 2) >> 0); + rx = 1 & (t / 2); ry = 1 & (t ^ rx); rotate(s, xy, rx, ry); xy[0] += s * rx; xy[1] += s * ry; - t = (t / 4) >> 0; + t = t / 4; s *= 2; } return [z, xy[0], xy[1]]; } export function zxyToTileId(z: number, x: number, y: number): number { + if (z > 26) { + throw Error("Tile zoom level exceeds max safe number limit (26)"); + } + if (x > Math.pow(2, z) - 1 || y > Math.pow(2, z) - 1) { + throw Error("tile x/y outside zoom level bounds"); + } + let acc = 0; let tz = 0; while (tz < z) { - acc += (0x1 << tz) * (0x1 << tz); + acc += Math.pow(2, tz) * Math.pow(2, tz); tz++; } - const n = 1 << z; + const n = Math.pow(2, z); let rx = 0; let ry = 0; let d = 0; const xy = [x, y]; - let s = (n / 2) >> 0; + let s = n / 2; while (s > 0) { rx = (xy[0] & s) > 0 ? 1 : 0; ry = (xy[1] & s) > 0 ? 1 : 0; d += s * s * ((3 * rx) ^ ry); rotate(s, xy, rx, ry); - s = (s / 2) >> 0; + s = s / 2; } return acc + d; } @@ -114,14 +121,16 @@ export function zxyToTileId(z: number, x: number, y: number): number { export function tileIdToZxy(i: number): [number, number, number] { let acc = 0; let z = 0; - for (;;) { + + for (let z = 0; z < 27; z++) { const num_tiles = (0x1 << z) * (0x1 << z); if (acc + num_tiles > i) { return idOnLevel(z, i - acc); } acc += num_tiles; - z++; } + + throw Error("Tile zoom level exceeds max safe number limit (26)"); } export interface Entry { diff --git a/js/test/v3.test.ts b/js/test/v3.test.ts index 369afee..5a9e900 100644 --- a/js/test/v3.test.ts +++ b/js/test/v3.test.ts @@ -64,6 +64,34 @@ test("a lot of tiles", () => { } }); +test("tile extremes", () => { + for (var z = 0; z < 27; z++) { + const dim = Math.pow(2, z) - 1; + const tl = tileIdToZxy(zxyToTileId(z, 0, 0)); + assert.deepEqual([z, 0, 0], tl); + const tr = tileIdToZxy(zxyToTileId(z, dim, 0)); + assert.deepEqual([z, dim, 0], tr); + const bl = tileIdToZxy(zxyToTileId(z, 0, dim)); + assert.deepEqual([z, 0, dim], bl); + const br = tileIdToZxy(zxyToTileId(z, dim, dim)); + assert.deepEqual([z, dim, dim], br); + } +}); + +test("invalid tiles", () => { + assert.throws(() => { + tileIdToZxy(Number.MAX_SAFE_INTEGER); + }); + + assert.throws(() => { + zxyToTileId(27,0,0); + }); + + assert.throws(() => { + zxyToTileId(0,1,1); + }); +}); + test("tile search for missing entry", () => { const entries: Entry[] = []; assert.strictEqual(findTile(entries, 101), null); @@ -185,34 +213,25 @@ test("getUint64", async () => { test("cache check against empty", async () => { const source = new TestNodeFileSource("test/data/empty.pmtiles", "1"); const cache = new SharedPromiseCache(); - try { + assert.rejects(async () => { await cache.getHeader(source); - assert.fail("Should have thrown"); - } catch (e) { - assert.ok(e instanceof Error); - } + }); }); test("cache check magic number", async () => { const source = new TestNodeFileSource("test/data/invalid.pmtiles", "1"); const cache = new SharedPromiseCache(); - try { + assert.rejects(async () => { await cache.getHeader(source); - assert.fail("Should have thrown"); - } catch (e) { - assert.ok(e instanceof Error); - } + }); }); test("cache check future spec version", async () => { const source = new TestNodeFileSource("test/data/invalid_v4.pmtiles", "1"); const cache = new SharedPromiseCache(); - try { + assert.rejects(async () => { await cache.getHeader(source); - assert.fail("Should have thrown"); - } catch (e) { - assert.ok(e instanceof Error); - } + }); }); test("cache getDirectory", async () => { @@ -276,17 +295,15 @@ test("etags are part of key", async () => { source.etag = "etag_2"; - try { + assert.rejects(async () => { await cache.getDirectory( source, header.rootDirectoryOffset, header.rootDirectoryLength, header ); - assert.fail("Should have thrown"); - } catch (e) { - assert.ok(e instanceof EtagMismatch); - } + }) + cache.invalidate(source, "etag_2"); header = await cache.getHeader(source); assert.ok( @@ -311,17 +328,14 @@ test("soft failure on etag weirdness", async () => { source.etag = "etag_2"; - try { + assert.rejects(async () => { await cache.getDirectory( source, header.rootDirectoryOffset, header.rootDirectoryLength, header ); - assert.fail("Should have thrown"); - } catch (e) { - assert.ok(e instanceof EtagMismatch); - } + }) source.etag = "etag_1"; cache.invalidate(source, "etag_2");