fix(kml-import): address PR #488 review issues

- Strip BOM (U+FEFF) from 14 translation files injected by editor
- Guard KMZ unpack against zip-bomb: check entry.uncompressedSize against
  50 MB cap (KMZ_DECOMPRESSED_SIZE_LIMIT) before calling .buffer();
  limit is an exported constant so tests can override it
- Fix non-BMP HTML entity decoding: replace String.fromCharCode with
  String.fromCodePoint + 0x10FFFF bounds check so emoji like 😀
  round-trip correctly
- Switch KML namespace stripping from regex to fast-xml-parser's
  removeNSPrefix option; XMLValidator accepts namespaced XML natively,
  making the pre-strip step unnecessary
- Remove dead skippedCount overwrite after transaction; per-loop
  increment already tracks it alongside per-item error messages
- Type multer req.file as Express.Multer.File on both /import/gpx
  and /import/map routes instead of (req as any).file
- Add unit tests: emoji entity decoding (decimal + hex), KMZ zip-bomb
  rejection, KMZ-with-no-KML rejection
This commit is contained in:
jubnl
2026-04-15 05:16:47 +02:00
parent a1a7795945
commit 801ffbfb7b
19 changed files with 103 additions and 41 deletions
@@ -7,18 +7,9 @@ import {
parsePlacemarkNode,
resolveCategoryIdForFolder,
sanitizeKmlDescription,
stripXmlNamespaces,
} from '../../../src/services/kmlImport';
describe('kmlImportUtils', () => {
it('strips KML namespaces and prefixes', () => {
const xml = '<kml xmlns="http://www.opengis.net/kml/2.2"><kml:Document><kml:Placemark /></kml:Document></kml>';
const stripped = stripXmlNamespaces(xml);
expect(stripped).not.toContain('xmlns');
expect(stripped).toContain('<Document>');
expect(stripped).toContain('<Placemark');
});
it('sanitizes HTML descriptions with br to newline', () => {
const input = 'Line 1<br>Line <b>2</b> &amp; more';
const output = sanitizeKmlDescription(input);
@@ -64,6 +55,16 @@ describe('kmlImportUtils', () => {
expect(resolveCategoryIdForFolder('parks', lookup)).toBe(4);
});
it('decodes non-BMP decimal HTML entities (emoji)', () => {
// &#128512; = U+1F600 = 😀 — requires String.fromCodePoint, not fromCharCode
expect(sanitizeKmlDescription('&#128512;')).toBe('😀');
});
it('decodes non-BMP hex HTML entities (emoji)', () => {
// &#x1F600; = U+1F600 = 😀
expect(sanitizeKmlDescription('&#x1F600;')).toBe('😀');
});
it('returns warning for non-UTF8 payload', () => {
const buffer = Buffer.concat([
Buffer.from('<?xml version="1.0"?><kml><Document><Placemark><name>Caf'),