mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
fix(import): scan the KML root delimiter in bounded time
This commit is contained in:
1 parent
de2138e84c
commit
6552a89ddd
3 files changed
+137
-18
No files matched your search
@@ -1,5 +1,11 @@
|
|||||||
# God's Eye View Current State
|
# God's Eye View Current State
|
||||||
|
|
||||||
|
KML namespace repair locates the opening token once and scans its quoted tag
|
||||||
|
delimiter once. Malformed or unterminated tags fail before DOM parsing, so a file
|
||||||
|
of repeated incomplete `<kml` prefixes cannot trigger quadratic regex rescans.
|
||||||
|
Duplicate namespace repair and missing xsi declarations remain supported; GPX
|
||||||
|
continues through its existing XML parsing path.
|
||||||
|
|
||||||
Shared distance checks use `src/geoDistance.js` with `{ lat, lon }` points and
|
Shared distance checks use `src/geoDistance.js` with `{ lat, lon }` points and
|
||||||
the IUGG mean spherical Earth radius, 6371.0088 km. `greatCircleKm` is
|
the IUGG mean spherical Earth radius, 6371.0088 km. `greatCircleKm` is
|
||||||
canonical; `greatCircleMeters` is its exact metre conversion. Missing,
|
canonical; `greatCircleMeters` is its exact metre conversion. Missing,
|
||||||
|
|||||||
+78
-18
@@ -1040,26 +1040,86 @@ 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 === '[') 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 },
|
||||||
|
);
|
||||||
|
const seen = new Set();
|
||||||
|
let root = text
|
||||||
|
.slice(start, end)
|
||||||
|
.replace(
|
||||||
|
/\sxmlns:([\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"$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',
|
||||||
|
|||||||
@@ -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,54 @@ 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; }`;
|
||||||
|
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"', ''),
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user