From 481b0ed679fdd031f44d3d8f282c50dc8a10688c Mon Sep 17 00:00:00 2001 From: Renato Grasso <102993684+renatograsso10@users.noreply.github.com> Date: Wed, 6 May 2026 21:34:54 -0300 Subject: [PATCH] fix(mime): specify charset parameter per MIME type instead of mechanical detection Per #4510: - Inline `; charset=utf-8` in `_baseMimes` for all text/* entries plus `image/svg+xml`, `application/xhtml+xml`, and `application/xml`, which are XML-based formats that benefit from an explicit charset. - Drop the `if (mimeType.startsWith("text"))` block in `getMimeType`; the charset now lives in the data, not in the lookup logic. Output for text types is unchanged (still "text/; charset=utf-8"). - Update `getExtension` to compare the base type (the part before the first `;`), so callers can pass either a raw type ("text/html") or one with parameters ("text/html; charset=utf-8") and still get the correct extension back. Tests: 5 mime.test.ts cases pass (3 existing + 2 new) covering the new charset values for svg/xhtml/xml/ics/mjs/css/csv and getExtension with parameters. Closes #4510 --- src/utils/mime.test.ts | 21 +++++++++++++++++++++ src/utils/mime.ts | 32 +++++++++++++++----------------- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/utils/mime.test.ts b/src/utils/mime.test.ts index 6d4facde8..12a9dee28 100644 --- a/src/utils/mime.test.ts +++ b/src/utils/mime.test.ts @@ -18,6 +18,16 @@ describe('mime', () => { expect(getMimeType('IMAGE.JPG')).toBe('image/jpeg') }) + it('getMimeType returns charset parameter for text and XML-based formats', () => { + expect(getMimeType('icon.svg')).toBe('image/svg+xml; charset=utf-8') + expect(getMimeType('page.xhtml')).toBe('application/xhtml+xml; charset=utf-8') + expect(getMimeType('feed.xml')).toBe('application/xml; charset=utf-8') + expect(getMimeType('events.ics')).toBe('text/calendar; charset=utf-8') + expect(getMimeType('script.mjs')).toBe('text/javascript; charset=utf-8') + expect(getMimeType('style.css')).toBe('text/css; charset=utf-8') + expect(getMimeType('data.csv')).toBe('text/csv; charset=utf-8') + }) + it('getMimeType with custom mime', () => { expect(getMimeType('morning-routine.m3u8', mime)).toBe('application/vnd.apple.mpegurl') expect(getMimeType('morning-routine1.ts', mime)).toBe('video/mp2t') @@ -40,4 +50,15 @@ describe('mime', () => { expect(getExtension('application/zip')).toBe('zip') expect(getExtension('non/existent')).toBeUndefined() }) + + it('getExtension matches MIME types regardless of charset/parameters', () => { + expect(getExtension('text/html; charset=utf-8')).toBe('htm') + expect(getExtension('text/plain; charset=utf-8')).toBe('txt') + expect(getExtension('image/svg+xml; charset=utf-8')).toBe('svg') + expect(getExtension('application/xml; charset=utf-8')).toBe('xml') + expect(getExtension('application/xhtml+xml; charset=utf-8')).toBe('xhtml') + expect(getExtension('text/css; charset=utf-16')).toBe('css') + expect(getExtension('image/svg+xml')).toBe('svg') + expect(getExtension('application/xml')).toBe('xml') + }) }) diff --git a/src/utils/mime.ts b/src/utils/mime.ts index 7db06d116..cd974e630 100644 --- a/src/utils/mime.ts +++ b/src/utils/mime.ts @@ -12,16 +12,14 @@ export const getMimeType = ( if (!match) { return } - let mimeType = mimes[match[1].toLowerCase()] - if (mimeType && mimeType.startsWith('text')) { - mimeType += '; charset=utf-8' - } - return mimeType + return mimes[match[1].toLowerCase()] } export const getExtension = (mimeType: string): string | undefined => { + const baseType = mimeType.split(';', 1)[0].trim() for (const ext in baseMimes) { - if (baseMimes[ext] === mimeType) { + const stored = baseMimes[ext] + if (stored === mimeType || stored.split(';', 1)[0].trim() === baseType) { return ext } } @@ -41,25 +39,25 @@ const _baseMimes = { av1: 'video/av1', bin: 'application/octet-stream', bmp: 'image/bmp', - css: 'text/css', - csv: 'text/csv', + css: 'text/css; charset=utf-8', + csv: 'text/csv; charset=utf-8', eot: 'application/vnd.ms-fontobject', epub: 'application/epub+zip', gif: 'image/gif', gz: 'application/gzip', - htm: 'text/html', - html: 'text/html', + htm: 'text/html; charset=utf-8', + html: 'text/html; charset=utf-8', ico: 'image/x-icon', - ics: 'text/calendar', + ics: 'text/calendar; charset=utf-8', jpeg: 'image/jpeg', jpg: 'image/jpeg', - js: 'text/javascript', + js: 'text/javascript; charset=utf-8', json: 'application/json', jsonld: 'application/ld+json', map: 'application/json', mid: 'audio/x-midi', midi: 'audio/x-midi', - mjs: 'text/javascript', + mjs: 'text/javascript; charset=utf-8', mp3: 'audio/mpeg', mp4: 'video/mp4', mpeg: 'video/mpeg', @@ -71,12 +69,12 @@ const _baseMimes = { pdf: 'application/pdf', png: 'image/png', rtf: 'application/rtf', - svg: 'image/svg+xml', + svg: 'image/svg+xml; charset=utf-8', tif: 'image/tiff', tiff: 'image/tiff', ts: 'video/mp2t', ttf: 'font/ttf', - txt: 'text/plain', + txt: 'text/plain; charset=utf-8', wasm: 'application/wasm', webm: 'video/webm', weba: 'audio/webm', @@ -84,8 +82,8 @@ const _baseMimes = { webp: 'image/webp', woff: 'font/woff', woff2: 'font/woff2', - xhtml: 'application/xhtml+xml', - xml: 'application/xml', + xhtml: 'application/xhtml+xml; charset=utf-8', + xml: 'application/xml; charset=utf-8', zip: 'application/zip', '3gp': 'video/3gpp', '3g2': 'video/3gpp2',