diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index e1775ae2d..652a00515 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -10,10 +10,15 @@ lookups preserve the previous analyst cohort and boundary source. Navigation search keeps a successful geocode, with no search or view-based region recovery. 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 `', end + 4); + if (skip < 0) return text; + end = skip + 2; + } else if (char === '<' && text.startsWith('', end + 2); + if (skip < 0) return text; + end = skip + 1; + } else if (char === '[') subsetDepth++; else if (char === ']') subsetDepth--; else if (char === '>' && subsetDepth === 0) break; } @@ -1098,17 +1108,24 @@ export function repairKmlRootNamespaces(value, what = 'KML') { 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(); - 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; - }, - ); + 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( /(\/?)>$/, diff --git a/src/data/geoFileImport.test.mjs b/src/data/geoFileImport.test.mjs index 29545b0b6..9b147e1d2 100644 --- a/src/data/geoFileImport.test.mjs +++ b/src/data/geoFileImport.test.mjs @@ -1197,7 +1197,11 @@ test('KML root namespace repair respects quoted delimiters and preserves the doc 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('")}.repeat(100000) + ']>'; + if (repairKmlRootNamespaces(wide).split('xmlns:gx="a"').length !== 2) process.exit(1);`; const child = spawnSync( process.execPath, ['--input-type=module', '-e', source], @@ -1224,3 +1228,37 @@ test('KML namespace repair follows a doctype with a quoted root-like entity', () input.replace(' xmlns:gx="b"', ''), ); }); + +test('KML namespace repair skips doctype comments and processing instructions whole', () => { + for (const prolog of [ + "]>", + ' here -->]>', + " fine?>]>", + '">]>', + ]) { + const input = `${prolog}`; + assert.equal( + repairKmlRootNamespaces(input), + input.replace( + ' xmlns:gx="b">', + ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">', + ), + prolog, + ); + } + const unterminated = ''; + assert.equal(repairKmlRootNamespaces(unterminated), unterminated); +}); + +test('KML namespace repair ignores declaration text inside quoted attribute values', () => { + const input = + ' b xmlns:gx="fake"\' xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:gx="dup">'; + 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 = ''; + assert.match( + repairKmlRootNamespaces(xsi), + /note="xmlns:xsi='x'" xmlns:xsi="http:\/\/www.w3.org\/2001\/XMLSchema-instance">/, + ); +});