mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Use a neutral compact switch codec and clarify provider size limits
This commit is contained in:
1 parent
fbe118aab9
commit
3ba8c6d61b
11 files changed
+127
-98
No files matched your search
@@ -1,8 +1,5 @@
|
||||
import { normalizeFilter } from './filter.js';
|
||||
import {
|
||||
encodeProviderSwitches,
|
||||
decodeProviderSwitches,
|
||||
} from './providerSwitches.js';
|
||||
import { encodeIdSwitches, decodeIdSwitches } from '../../data/idSwitches.js';
|
||||
|
||||
/**
|
||||
* Provider params plus the filter. Custom switches share a stable ID-based
|
||||
@@ -13,7 +10,9 @@ export function encodeParams({ providers, filter }) {
|
||||
const params = {};
|
||||
const entries = [...providers];
|
||||
for (const [id, on] of entries) params[id] = on === true;
|
||||
const switches = encodeProviderSwitches(entries);
|
||||
const switches = encodeIdSwitches(
|
||||
entries.filter(([id]) => id !== 'mapillary'),
|
||||
);
|
||||
if (switches) params.providerSwitches = switches;
|
||||
params.pano = filter.pano;
|
||||
params.sinceDays = filter.sinceDays;
|
||||
@@ -30,7 +29,8 @@ export function encodeParams({ providers, filter }) {
|
||||
export function decodeParams(params, { providerIds, filter }) {
|
||||
const providers = new Map();
|
||||
const source = params && typeof params === 'object' ? params : {};
|
||||
const switches = decodeProviderSwitches(source.providerSwitches);
|
||||
const switches = decodeIdSwitches(source.providerSwitches);
|
||||
switches.delete('mapillary');
|
||||
for (const id of providerIds) {
|
||||
if (switches.has(id)) providers.set(id, switches.get(id));
|
||||
else if (Object.hasOwn(source, id) && typeof source[id] === 'boolean')
|
||||
|
||||
@@ -14,7 +14,7 @@ test('encodeParams writes one boolean per provider plus the filter', () => {
|
||||
{
|
||||
mapillary: true,
|
||||
panoramax: false,
|
||||
providerSwitches: 'panoramax=0',
|
||||
providerSwitches: 'panoramax-0',
|
||||
pano: 'flat',
|
||||
sinceDays: 365,
|
||||
},
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
const MAX_SWITCHES_LENGTH = 256;
|
||||
const ENTRY = /^([a-z][a-z0-9-]*)=([01])$/;
|
||||
|
||||
/** Canonical, bounded provider switches; Mapillary retains its legacy token. */
|
||||
export function normalizeProviderSwitches(value) {
|
||||
if (typeof value !== 'string' || value.length > MAX_SWITCHES_LENGTH)
|
||||
return null;
|
||||
if (!value) return '';
|
||||
const entries = new Map();
|
||||
for (const field of value.split(',')) {
|
||||
const match = ENTRY.exec(field);
|
||||
if (!match || match[1] === 'mapillary' || entries.has(match[1]))
|
||||
return null;
|
||||
entries.set(match[1], match[2] === '1');
|
||||
}
|
||||
return [...entries]
|
||||
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
||||
.map(([id, on]) => `${id}=${on ? '1' : '0'}`)
|
||||
.join(',');
|
||||
}
|
||||
|
||||
/** Encode by stable IDs rather than registration order or generated tokens. */
|
||||
export function encodeProviderSwitches(providers) {
|
||||
const value = [...providers]
|
||||
.filter(([id]) => id !== 'mapillary')
|
||||
.map(([id, on]) => `${id}=${on === true ? '1' : '0'}`)
|
||||
.join(',');
|
||||
const normalized = normalizeProviderSwitches(value);
|
||||
if (normalized === null)
|
||||
throw new TypeError('Invalid or oversized Street Level provider switches');
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/** A Map keeps provider IDs separate from object prototype properties. */
|
||||
export function decodeProviderSwitches(value) {
|
||||
const normalized = normalizeProviderSwitches(value);
|
||||
return new Map(
|
||||
normalized
|
||||
? normalized.split(',').map((entry) => {
|
||||
const [id, on] = entry.split('=');
|
||||
return [id, on === '1'];
|
||||
})
|
||||
: [],
|
||||
);
|
||||
}
|
||||
@@ -2,9 +2,10 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { encodeParams, decodeParams } from './params.js';
|
||||
import {
|
||||
normalizeProviderSwitches,
|
||||
encodeProviderSwitches,
|
||||
} from './providerSwitches.js';
|
||||
normalizeIdSwitches,
|
||||
decodeIdSwitches,
|
||||
encodeIdSwitches,
|
||||
} from '../../data/idSwitches.js';
|
||||
import {
|
||||
createDefaultLayerState,
|
||||
encodeLayerStateParams,
|
||||
@@ -24,7 +25,7 @@ test('custom switches survive sharing and storage without depending on provider
|
||||
['mapillary', false],
|
||||
];
|
||||
const options = encodeParams({ providers, filter });
|
||||
assert.equal(options.providerSwitches, 'example=1,panoramax=0');
|
||||
assert.equal(options.providerSwitches, 'example-1*panoramax-0');
|
||||
assert.equal(
|
||||
encodeParams({ providers: [...providers].reverse(), filter })
|
||||
.providerSwitches,
|
||||
@@ -36,7 +37,11 @@ test('custom switches survive sharing and storage without depending on provider
|
||||
const params = new URLSearchParams({ v: '2' });
|
||||
encodeLayerStateParams(params, state);
|
||||
assert.match(params.get('lo'), /0\.m\.0/);
|
||||
assert.match(params.get('lo'), /0\.r\.example=1,panoramax=0/);
|
||||
assert.match(params.get('lo'), /0\.r\.example-1\*panoramax-0/);
|
||||
assert.ok(
|
||||
params.toString().includes('0.r.example-1*panoramax-0'),
|
||||
'custom switches need no percent escaping',
|
||||
);
|
||||
for (const restored of [
|
||||
decodeLayerStateParams(params),
|
||||
parseStoredLayerState(serializeStoredLayerState(state)),
|
||||
@@ -91,31 +96,44 @@ test('legacy links retain Mapillary semantics and leave custom defaults alone',
|
||||
|
||||
test('switches reject ambiguous or oversized fields and cannot override the legacy token', () => {
|
||||
for (const value of [
|
||||
'example=0,example=1',
|
||||
'mapillary=0',
|
||||
'__proto__=1',
|
||||
'a=2',
|
||||
'a=1,',
|
||||
'a.b=1',
|
||||
'a_b=0',
|
||||
`${'a'.repeat(255)}=1`,
|
||||
'example-0*example-1',
|
||||
'mapillary-0',
|
||||
'__proto__-1',
|
||||
'a-2',
|
||||
'a-1*',
|
||||
'a.b-1',
|
||||
'a_b-0',
|
||||
`${'a'.repeat(255)}-1`,
|
||||
null,
|
||||
{},
|
||||
]) {
|
||||
assert.equal(normalizeProviderSwitches(value), null);
|
||||
assert.equal(normalizeIdSwitches(value, ['mapillary']), null);
|
||||
}
|
||||
assert.equal(normalizeProviderSwitches('constructor=0'), 'constructor=0');
|
||||
assert.equal(normalizeIdSwitches('constructor-0'), 'constructor-0');
|
||||
assert.throws(
|
||||
() => encodeProviderSwitches([['a'.repeat(255), true]]),
|
||||
/oversized/,
|
||||
() => encodeIdSwitches([['a'.repeat(255), true]]),
|
||||
/share-link/,
|
||||
);
|
||||
assert.throws(
|
||||
() => validateProviders([fakeStreetLevelProvider({ id: 'a'.repeat(255) })]),
|
||||
/oversized/,
|
||||
/share-link/,
|
||||
);
|
||||
const decoded = decodeParams(
|
||||
{ providerSwitches: 'mapillary=0', mapillary: true },
|
||||
{ providerSwitches: 'mapillary-0', mapillary: true },
|
||||
{ providerIds: ['mapillary'], filter },
|
||||
);
|
||||
assert.equal(decoded.providers.get('mapillary'), true);
|
||||
});
|
||||
|
||||
test('neutral switch codec preserves hyphens and numeric suffixes in IDs', () => {
|
||||
const switches = [
|
||||
['provider-0', false],
|
||||
['provider-1', true],
|
||||
['a-b-c', true],
|
||||
];
|
||||
const encoded = encodeIdSwitches(switches);
|
||||
assert.equal(encoded, 'a-b-c-1*provider-0-0*provider-1-1');
|
||||
const decoded = decodeIdSwitches(encoded);
|
||||
for (const [id, on] of switches) assert.equal(decoded.get(id), on);
|
||||
assert.equal(decodeIdSwitches('valid-1*broken').size, 0);
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { encodeProviderSwitches } from './providerSwitches.js';
|
||||
import { encodeIdSwitches } from '../../data/idSwitches.js';
|
||||
|
||||
/**
|
||||
* The contract an imagery provider registers with the Street Level layer.
|
||||
@@ -128,7 +128,18 @@ export function validateProviders(providers) {
|
||||
prefixes.push(provider.pickPrefix);
|
||||
}
|
||||
// Validate the complete bounded share field once, before any instance exists.
|
||||
encodeProviderSwitches(providers.map(({ id }) => [id, true]));
|
||||
try {
|
||||
encodeIdSwitches(
|
||||
providers
|
||||
.filter(({ id }) => id !== 'mapillary')
|
||||
.map(({ id }) => [id, true]),
|
||||
);
|
||||
} catch (error) {
|
||||
if (!(error instanceof RangeError)) throw error;
|
||||
throw new RangeError(
|
||||
'Street Level provider IDs cannot fit in the 256-character share-link field; use shorter IDs or fewer providers',
|
||||
);
|
||||
}
|
||||
return Object.freeze([...providers]);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user