Merge pull request #1274 from bilawalsidhu/codex/fix-kml-root-scanner

fix(import): scan the KML root delimiter in bounded time
This commit is contained in:
Said Turksever authored and GitHub committed 2026-10-23 15:37:27 +01:00
commit f19a6da5b8
4 files changed
+197 -19

No files matched your search

+1
View File
@@ -0,0 +1 @@
- Scan the kml root delimiter in bounded time (asturksever, #1274).
+10 -1
View File
@@ -9,7 +9,16 @@ broader suffix variants. Follow-ups retain boundary provenance, and cancelled
lookups preserve the previous analyst cohort and boundary source. Navigation lookups preserve the previous analyst cohort and boundary source. Navigation
search keeps a successful geocode, with no search or view-based region recovery. search keeps a successful geocode, with no search or view-based region recovery.
The standalone military source no longer requests the unavailable, undocumented ADSB.lol trace endpoint. Tracking retains its locally accumulated trail, and custom sources may still supply optional getTrack history. The old HTTP route validates the identifier then reports 503 history_unavailable without any upstream request, rather than forwarding a misleading 403. The `get_aircraft_track` tool reports `unsupported` for a military feed without `getTrack` instead of a retry-later failure. This does not restore historical coverage or download bulk daily archives. KML namespace repair locates the opening token once and scans its quoted tag
delimiter once, skipping prolog and DOCTYPE comments and processing
instructions whole. The scanner is a boundary check only: an opening KML tag
that never closes, or one with raw markup starting inside it, fails before DOM
parsing, so a file of repeated incomplete `<kml` prefixes cannot trigger
quadratic regex rescans. All other XML grammar, including attribute syntax
inside a complete root tag, is left to DOMParser. Namespace declarations are
recognized only as whole root attributes, never inside quoted values; duplicate
namespace repair and missing xsi declarations remain supported. GPX continues
through its existing XML parsing path.
Terrain height requests accept at most 64 points, matching the browser service's Terrain height requests accept at most 64 points, matching the browser service's
existing batch size and one bounded upstream chunk. Larger requests return HTTP existing batch size and one bounded upstream chunk. Larger requests return HTTP
+95 -18
View File
@@ -1040,26 +1040,103 @@ export function detachImportedTime(dataSource) {
return dataSource; return dataSource;
} }
/** Repair only the first KML opening tag, scanning its delimiter once. */
export function repairKmlRootNamespaces(value, what = 'KML') {
const text = String(value);
// Skip XML comments and processing instructions without treating their
// literal '<kml' text as an opening root tag.
let start = 0;
for (;;) {
start = text.indexOf('<', start);
if (start < 0) return text;
if (text.slice(start, start + 9).toUpperCase() === '<!DOCTYPE') {
let quote = null;
let subsetDepth = 0;
let end = start + 9;
for (; end < text.length; end++) {
const char = text[end];
if (quote) {
if (char === quote) quote = null;
} else if (char === '"' || char === "'") quote = char;
else if (char === '<' && text.startsWith('<!--', end)) {
// Comments and processing instructions inside the internal subset
// carry no declaration syntax; jump past them as a whole.
const skip = text.indexOf('-->', end + 4);
if (skip < 0) return text;
end = skip + 2;
} else if (char === '<' && text.startsWith('<?', end)) {
const skip = text.indexOf('?>', end + 2);
if (skip < 0) return text;
end = skip + 1;
} else if (char === '[') subsetDepth++;
else if (char === ']') subsetDepth--;
else if (char === '>' && subsetDepth === 0) break;
}
if (end === text.length) return text;
start = end + 1;
continue;
}
const close = text.startsWith('<!--', start)
? '-->'
: text.startsWith('<?', start)
? '?>'
: null;
if (!close) break;
const end = text.indexOf(close, start + 2);
if (end < 0) return text; // The XML parser reports an unterminated preamble.
start = end + close.length;
}
const match = /^<kml\b/i.exec(text.slice(start, start + 5));
if (!match) return text;
let quote = null;
let end = -1;
for (let index = start + match[0].length; index < text.length; index++) {
const char = text[index];
if (char === '<') break; // Raw '<' is invalid even inside an XML attribute.
if (quote) {
if (char === quote) quote = null;
} else if (char === '"' || char === "'") {
quote = char;
} else if (char === '>') {
end = index + 1;
break;
}
}
if (end < 0)
throw new GeoImportError(
`That ${what} file is not valid XML.`,
msg('That {format} file is not valid XML.'),
{ format: what },
);
// Walk the root's attributes in order so quoted values are never read as
// declarations. Anything the walk cannot tokenize is kept verbatim for the
// XML parser to judge.
const seen = new Set();
const attribute = /(\s+)([^\s=/>"']+)\s*=\s*("[^"]*"|'[^']*')/y;
let position = start + match[0].length;
let root = text.slice(start, position);
for (;;) {
attribute.lastIndex = position;
const found = attribute.exec(text);
if (!found || attribute.lastIndex > end) break;
position = attribute.lastIndex;
const prefix = /^xmlns:([\w.-]+)$/.exec(found[2])?.[1];
if (prefix && seen.has(prefix)) continue;
if (prefix) seen.add(prefix);
root += found[0];
}
root += text.slice(position, end);
if (!seen.has('xsi') && /[<\s]xsi:/.test(text))
root = root.replace(
/(\/?)>$/,
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"$1>',
);
return text.slice(0, start) + root + text.slice(end);
}
function parseXml(text, what) { function parseXml(text, what) {
// Match the namespace repairs Cesium applies to ordinary exported KML. // Match the namespace repairs Cesium applies to ordinary exported KML.
if (what !== 'GPX') if (what !== 'GPX') text = repairKmlRootNamespaces(text, what);
text = String(text).replace(/<kml\b[^>]*>/i, (root) => {
const seen = new Set();
root = root.replace(
/\s+xmlns:([\w.-]+)\s*=\s*(["'])[^"']*\2/g,
(declaration, prefix) => {
if (seen.has(prefix)) return '';
seen.add(prefix);
return declaration;
},
);
if (!seen.has('xsi') && /[<\s]xsi:/.test(text))
root = root.replace(
/>$/,
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">',
);
return root;
});
const doc = new DOMParser().parseFromString( const doc = new DOMParser().parseFromString(
String(text).replace(/^/, ''), String(text).replace(/^/, ''),
'application/xml', 'application/xml',
+91
View File
@@ -1,9 +1,11 @@
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { deflateRawSync } from 'node:zlib'; import { deflateRawSync } from 'node:zlib';
import { spawnSync } from 'node:child_process';
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import { import {
GeoImportError, GeoImportError,
repairKmlRootNamespaces,
loadGeoFile, loadGeoFile,
MAX_IMPORT_POSITIONS, MAX_IMPORT_POSITIONS,
MAX_IMPORT_ENTITIES, MAX_IMPORT_ENTITIES,
@@ -1171,3 +1173,92 @@ test('a dated import exposes no clock and draws for all time', () => {
assert.equal(recorded.getValue(Cesium.JulianDate.now()).length, 2); assert.equal(recorded.getValue(Cesium.JulianDate.now()).length, 2);
assert.ok(track.polyline.clampToGround.getValue()); assert.ok(track.polyline.clampToGround.getValue());
}); });
test('KML root namespace repair respects quoted delimiters and preserves the document tail', () => {
const text =
'<KML note="a > b" xmlns:x="first" xmlns:x="second"><Placemark xsi:type="test"/></KML>';
const result = repairKmlRootNamespaces(text);
assert.match(result, /note="a > b"/);
assert.match(result, /xmlns:x="first"/);
assert.doesNotMatch(result, /second/);
assert.match(
result,
/xmlns:xsi="http:\/\/www.w3.org\/2001\/XMLSchema-instance"/,
);
assert.ok(result.endsWith('<Placemark xsi:type="test"/></KML>'));
assert.match(
repairKmlRootNamespaces('<kml xsi:type="test"/>'),
/XMLSchema-instance"\/>$/,
);
for (const malformed of ['<kml ', '<kml name="unterminated>', '<kml <kml >'])
assert.throws(() => repairKmlRootNamespaces(malformed), GeoImportError);
});
test('repeated incomplete KML roots finish within an isolated process deadline', () => {
const source = `import { repairKmlRootNamespaces } from ${JSON.stringify(new URL('./geoFileImport.js', import.meta.url).href)};
try { repairKmlRootNamespaces('<kml '.repeat(1000000)); process.exit(1); }
catch (error) { if (error.name !== 'GeoImportError') throw error; }
try { repairKmlRootNamespaces('<!DOCTYPE kml [' + ${JSON.stringify("<!-- ' --><?pi ' ?>")}.repeat(100000) + ']><kml '); process.exit(1); }
catch (error) { if (error.name !== 'GeoImportError') throw error; }
const wide = '<kml' + ${JSON.stringify(` xmlns:gx="a" note='xmlns:gx="b"'`)}.repeat(20000) + '/>';
if (repairKmlRootNamespaces(wide).split('xmlns:gx="a"').length !== 2) process.exit(1);`;
const child = spawnSync(
process.execPath,
['--input-type=module', '-e', source],
{ timeout: 5000, encoding: 'utf8' },
);
assert.ifError(child.error);
assert.equal(child.status, 0, child.stderr);
});
test('KML root repair ignores incomplete tag text inside preamble comments', () => {
const input =
'<?xml version="1.0"?><!-- example <kml incomplete -->\n<kml xmlns:gx="a" xmlns:gx="b"><Document/></kml>';
assert.equal(
repairKmlRootNamespaces(input),
input.replace(' xmlns:gx="b"', ''),
);
});
test('KML namespace repair follows a doctype with a quoted root-like entity', () => {
const input =
'<!DOCTYPE kml [<!ENTITY example "<kml incomplete >">]><kml xmlns:gx="a" xmlns:gx="b"/>';
assert.equal(
repairKmlRootNamespaces(input),
input.replace(' xmlns:gx="b"', ''),
);
});
test('KML namespace repair skips doctype comments and processing instructions whole', () => {
for (const prolog of [
"<!DOCTYPE kml [<!-- don't count this quote -->]>",
'<!DOCTYPE kml [<!-- a ] or > here --><!ENTITY x "y">]>',
"<!DOCTYPE kml [<?note it's ] > fine?>]>",
'<!DOCTYPE kml [<!ENTITY x "<!-- \' -->">]>',
]) {
const input = `${prolog}<kml xmlns:gx="a" xmlns:gx="b"><Placemark xsi:type="t"/></kml>`;
assert.equal(
repairKmlRootNamespaces(input),
input.replace(
' xmlns:gx="b">',
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">',
),
prolog,
);
}
const unterminated = '<!DOCTYPE kml [<!-- open <kml xmlns:gx="a">';
assert.equal(repairKmlRootNamespaces(unterminated), unterminated);
});
test('KML namespace repair ignores declaration text inside quoted attribute values', () => {
const input =
'<kml note=\'a > b xmlns:gx="fake"\' xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:gx="dup"><gx:Track/></kml>';
const result = repairKmlRootNamespaces(input);
assert.equal(result, input.replace(' xmlns:gx="dup"', ''));
assert.match(result, /\sxmlns:gx="http:\/\/www.google.com\/kml\/ext\/2.2"/);
const xsi = '<kml note="xmlns:xsi=\'x\'"><Placemark xsi:type="t"/></kml>';
assert.match(
repairKmlRootNamespaces(xsi),
/note="xmlns:xsi='x'" xmlns:xsi="http:\/\/www.w3.org\/2001\/XMLSchema-instance">/,
);
});