diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index d3fc794754ab4b..c018ba56427fce 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -342,7 +342,7 @@ export function assetFileNamesToFileName( return hash case '[name]': - return name + return sanitizeFileName(name) } throw new Error( `invalid placeholder ${placeholder} in assetFileNames "${assetFileNames}"` @@ -353,6 +353,23 @@ export function assetFileNamesToFileName( return fileName } +// taken from https://github.com/rollup/rollup/blob/a8647dac0fe46c86183be8596ef7de25bc5b4e4b/src/utils/sanitizeFileName.ts +// https://datatracker.ietf.org/doc/html/rfc2396 +// eslint-disable-next-line no-control-regex +const INVALID_CHAR_REGEX = /[\x00-\x1F\x7F<>*#"{}|^[\]`;?:&=+$,]/g +const DRIVE_LETTER_REGEX = /^[a-z]:/i +function sanitizeFileName(name: string): string { + const match = DRIVE_LETTER_REGEX.exec(name) + const driveLetter = match ? match[0] : '' + + // A `:` is only allowed as part of a windows drive letter (ex: C:\foo) + // Otherwise, avoid them because they can refer to NTFS alternate data streams. + return ( + driveLetter + + name.substr(driveLetter.length).replace(INVALID_CHAR_REGEX, '_') + ) +} + export const publicAssetUrlCache = new WeakMap< ResolvedConfig, // hash -> url diff --git a/playground/assets-sanitize/+circle.svg b/playground/assets-sanitize/+circle.svg new file mode 100644 index 00000000000000..81ff39ee185e2e --- /dev/null +++ b/playground/assets-sanitize/+circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/playground/assets-sanitize/__tests__/assets-sanitize.spec.ts b/playground/assets-sanitize/__tests__/assets-sanitize.spec.ts new file mode 100644 index 00000000000000..fc9c1ad8c81a7c --- /dev/null +++ b/playground/assets-sanitize/__tests__/assets-sanitize.spec.ts @@ -0,0 +1,27 @@ +import { expect, test } from 'vitest' +import { getBg, isBuild, page, readManifest } from '~utils' + +if (!isBuild) { + test('importing asset with special char in filename works in dev', async () => { + expect(await getBg('.plus-circle')).toContain('+circle.svg') + expect(await page.textContent('.plus-circle')).toMatch('+circle.svg') + expect(await getBg('.underscore-circle')).toContain('_circle.svg') + expect(await page.textContent('.underscore-circle')).toMatch('_circle.svg') + }) +} else { + test('importing asset with special char in filename works in build', async () => { + const manifest = readManifest() + const plusCircleAsset = manifest['+circle.svg'].file + const underscoreCircleAsset = manifest['_circle.svg'].file + expect(await getBg('.plus-circle')).toMatch(plusCircleAsset) + expect(await page.textContent('.plus-circle')).toMatch(plusCircleAsset) + expect(await getBg('.underscore-circle')).toMatch(underscoreCircleAsset) + expect(await page.textContent('.underscore-circle')).toMatch( + underscoreCircleAsset + ) + expect(plusCircleAsset).toMatch('/_circle') + expect(underscoreCircleAsset).toMatch('/_circle') + expect(plusCircleAsset).not.toEqual(underscoreCircleAsset) + expect(Object.keys(manifest).length).toBe(3) // 2 svg, 1 index.js + }) +} diff --git a/playground/assets-sanitize/_circle.svg b/playground/assets-sanitize/_circle.svg new file mode 100644 index 00000000000000..f8e310c6148d42 --- /dev/null +++ b/playground/assets-sanitize/_circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/playground/assets-sanitize/index.html b/playground/assets-sanitize/index.html new file mode 100644 index 00000000000000..e4b4913ca7142c --- /dev/null +++ b/playground/assets-sanitize/index.html @@ -0,0 +1,11 @@ + + +

test elements below should show circles and their url

+
+
diff --git a/playground/assets-sanitize/index.js b/playground/assets-sanitize/index.js new file mode 100644 index 00000000000000..bac3b3b83e6b1d --- /dev/null +++ b/playground/assets-sanitize/index.js @@ -0,0 +1,9 @@ +import plusCircle from './+circle.svg' +import underscoreCircle from './_circle.svg' +function setData(classname, file) { + const el = document.body.querySelector(classname) + el.style.backgroundImage = `url(${file})` + el.textContent = file +} +setData('.plus-circle', plusCircle) +setData('.underscore-circle', underscoreCircle) diff --git a/playground/assets-sanitize/package.json b/playground/assets-sanitize/package.json new file mode 100644 index 00000000000000..3ade78a2bd33fe --- /dev/null +++ b/playground/assets-sanitize/package.json @@ -0,0 +1,11 @@ +{ + "name": "test-assets-sanitize", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "debug": "node --inspect-brk ../../packages/vite/bin/vite", + "preview": "vite preview" + } +} diff --git a/playground/assets-sanitize/vite.config.js b/playground/assets-sanitize/vite.config.js new file mode 100644 index 00000000000000..0e365a95383833 --- /dev/null +++ b/playground/assets-sanitize/vite.config.js @@ -0,0 +1,11 @@ +const { defineConfig } = require('vite') + +module.exports = defineConfig({ + build: { + //speed up build + minify: false, + target: 'esnext', + assetsInlineLimit: 0, + manifest: true + } +})