diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 55b7a63ab..40bddc651 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1,5 +1,11 @@ # 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 `' && subsetDepth === 0) break; + } + if (end === text.length) return text; + start = end + 1; + continue; + } + const close = text.startsWith('' + : text.startsWith('' + : 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 = /^') { + 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) { // Match the namespace repairs Cesium applies to ordinary exported KML. - if (what !== 'GPX') - text = String(text).replace(/]*>/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; - }); + if (what !== 'GPX') text = repairKmlRootNamespaces(text, what); const doc = new DOMParser().parseFromString( String(text).replace(/^/, ''), 'application/xml', diff --git a/src/data/geoFileImport.test.mjs b/src/data/geoFileImport.test.mjs index af02ec222..29545b0b6 100644 --- a/src/data/geoFileImport.test.mjs +++ b/src/data/geoFileImport.test.mjs @@ -1,9 +1,11 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { deflateRawSync } from 'node:zlib'; +import { spawnSync } from 'node:child_process'; import * as Cesium from 'cesium'; import { GeoImportError, + repairKmlRootNamespaces, loadGeoFile, MAX_IMPORT_POSITIONS, 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.ok(track.polyline.clampToGround.getValue()); }); + +test('KML root namespace repair respects quoted delimiters and preserves the document tail', () => { + const text = + ''; + 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('')); + assert.match( + repairKmlRootNamespaces(''), + /XMLSchema-instance"\/>$/, + ); + for (const malformed of ['\n'; + assert.equal( + repairKmlRootNamespaces(input), + input.replace(' xmlns:gx="b"', ''), + ); +}); + +test('KML namespace repair follows a doctype with a quoted root-like entity', () => { + const input = + '">]>'; + assert.equal( + repairKmlRootNamespaces(input), + input.replace(' xmlns:gx="b"', ''), + ); +});