Skip to content

Commit

Permalink
fix: encode URLs correctly (fix #15298) (#15311)
Browse files Browse the repository at this point in the history
Co-authored-by: bluwy <bjornlu.dev@gmail.com>
  • Loading branch information
pzerelles and bluwy committed Mar 12, 2024
1 parent 57628dc commit b10d162
Show file tree
Hide file tree
Showing 12 changed files with 77 additions and 42 deletions.
2 changes: 2 additions & 0 deletions docs/guide/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,5 @@ experimental: {
}
}
```

Note that the `filename` passed is a decoded URL, and if the function returns a URL string, it should also be decoded. Vite will handle the encoding automatically when rendering the URLs. If an object with `runtime` is returned, encoding should be handled yourself where needed as the runtime code will be rendered as is.
15 changes: 11 additions & 4 deletions packages/vite/src/node/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
emptyDir,
joinUrlSegments,
normalizePath,
partialEncodeURI,
requireResolveFromRootWithFallback,
} from './utils'
import { manifestPlugin } from './plugins/manifest'
Expand Down Expand Up @@ -1092,7 +1093,7 @@ const getResolveUrl = (path: string, URL = 'URL') => `new ${URL}(${path}).href`

const getRelativeUrlFromDocument = (relativePath: string, umd = false) =>
getResolveUrl(
`'${escapeId(relativePath)}', ${
`'${escapeId(partialEncodeURI(relativePath))}', ${
umd ? `typeof document === 'undefined' ? location.href : ` : ''
}document.currentScript && document.currentScript.src || document.baseURI`,
)
Expand All @@ -1118,11 +1119,15 @@ const relativeUrlMechanisms: Record<
relativePath,
)} : ${getRelativeUrlFromDocument(relativePath)})`,
es: (relativePath) =>
getResolveUrl(`'${escapeId(relativePath)}', import.meta.url`),
getResolveUrl(
`'${escapeId(partialEncodeURI(relativePath))}', import.meta.url`,
),
iife: (relativePath) => getRelativeUrlFromDocument(relativePath),
// NOTE: make sure rollup generate `module` params
system: (relativePath) =>
getResolveUrl(`'${escapeId(relativePath)}', module.meta.url`),
getResolveUrl(
`'${escapeId(partialEncodeURI(relativePath))}', module.meta.url`,
),
umd: (relativePath) =>
`(typeof document === 'undefined' && typeof location === 'undefined' ? ${getFileUrlFromRelativePath(
relativePath,
Expand All @@ -1133,7 +1138,9 @@ const relativeUrlMechanisms: Record<
const customRelativeUrlMechanisms = {
...relativeUrlMechanisms,
'worker-iife': (relativePath) =>
getResolveUrl(`'${escapeId(relativePath)}', self.location.href`),
getResolveUrl(
`'${escapeId(partialEncodeURI(relativePath))}', self.location.href`,
),
} as const satisfies Record<string, (relativePath: string) => string>

export type RenderBuiltAssetUrl = (
Expand Down
8 changes: 5 additions & 3 deletions packages/vite/src/node/plugins/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export function renderAssetUrlInJS(
)
const replacementString =
typeof replacement === 'string'
? JSON.stringify(replacement).slice(1, -1)
? JSON.stringify(encodeURI(replacement)).slice(1, -1)
: `"+${replacement.runtime}+"`
s.update(match.index, match.index + full.length, replacementString)
}
Expand All @@ -123,7 +123,7 @@ export function renderAssetUrlInJS(
)
const replacementString =
typeof replacement === 'string'
? JSON.stringify(replacement).slice(1, -1)
? JSON.stringify(encodeURI(replacement)).slice(1, -1)
: `"+${replacement.runtime}+"`
s.update(match.index, match.index + full.length, replacementString)
}
Expand Down Expand Up @@ -206,7 +206,9 @@ export function assetPlugin(config: ResolvedConfig): Plugin {
}

return {
code: `export default ${JSON.stringify(url)}`,
code: `export default ${JSON.stringify(
url.startsWith('data:') ? url : encodeURI(url),
)}`,
// Force rollup to keep this module from being shared between other entry points if it's an entrypoint.
// If the resulting chunk is empty, it will be removed in generateBundle.
moduleSideEffects:
Expand Down
34 changes: 19 additions & 15 deletions packages/vite/src/node/plugins/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -593,13 +593,15 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
chunkCSS = chunkCSS.replace(assetUrlRE, (_, fileHash, postfix = '') => {
const filename = this.getFileName(fileHash) + postfix
chunk.viteMetadata!.importedAssets.add(cleanUrl(filename))
return toOutputFilePathInCss(
filename,
'asset',
cssAssetName,
'css',
config,
toRelative,
return encodeURI(
toOutputFilePathInCss(
filename,
'asset',
cssAssetName,
'css',
config,
toRelative,
),
)
})
// resolve public URL from CSS paths
Expand All @@ -610,13 +612,15 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
)
chunkCSS = chunkCSS.replace(publicAssetUrlRE, (_, hash) => {
const publicUrl = publicAssetUrlMap.get(hash)!.slice(1)
return toOutputFilePathInCss(
publicUrl,
'public',
cssAssetName,
'css',
config,
() => `${relativePathToPublicFromCSS}/${publicUrl}`,
return encodeURI(
toOutputFilePathInCss(
publicUrl,
'public',
cssAssetName,
'css',
config,
() => `${relativePathToPublicFromCSS}/${publicUrl}`,
),
)
})
}
Expand Down Expand Up @@ -711,7 +715,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
)
const replacementString =
typeof replacement === 'string'
? JSON.stringify(replacement).slice(1, -1)
? JSON.stringify(encodeURI(replacement)).slice(1, -1)
: `"+${replacement.runtime}+"`
s.update(start, end, replacementString)
}
Expand Down
35 changes: 22 additions & 13 deletions packages/vite/src/node/plugins/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
isDataUrl,
isExternalUrl,
normalizePath,
partialEncodeURI,
processSrcSet,
removeLeadingSlash,
urlCanParse,
Expand Down Expand Up @@ -436,7 +437,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
overwriteAttrValue(
s,
sourceCodeLocation!,
toOutputPublicFilePath(url),
partialEncodeURI(toOutputPublicFilePath(url)),
)
}

Expand Down Expand Up @@ -488,22 +489,24 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
if (attrKey === 'srcset') {
assetUrlsPromises.push(
(async () => {
const processedUrl = await processSrcSet(
const processedEncodedUrl = await processSrcSet(
p.value,
async ({ url }) => {
const decodedUrl = decodeURI(url)
if (!isExcludedUrl(decodedUrl)) {
const result = await processAssetUrl(url)
return result !== decodedUrl ? result : url
return result !== decodedUrl
? encodeURI(result)
: url
}
return url
},
)
if (processedUrl !== p.value) {
if (processedEncodedUrl !== p.value) {
overwriteAttrValue(
s,
getAttrSourceCodeLocation(node, attrKey),
processedUrl,
processedEncodedUrl,
)
}
})(),
Expand All @@ -514,7 +517,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
overwriteAttrValue(
s,
getAttrSourceCodeLocation(node, attrKey),
toOutputPublicFilePath(url),
partialEncodeURI(toOutputPublicFilePath(url)),
)
} else if (!isExcludedUrl(url)) {
if (
Expand Down Expand Up @@ -560,7 +563,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
overwriteAttrValue(
s,
getAttrSourceCodeLocation(node, attrKey),
processedUrl,
partialEncodeURI(processedUrl),
)
}
})(),
Expand Down Expand Up @@ -633,9 +636,13 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
// emit <script>import("./aaa")</script> asset
for (const { start, end, url } of scriptUrls) {
if (checkPublicFile(url, config)) {
s.update(start, end, toOutputPublicFilePath(url))
s.update(start, end, partialEncodeURI(toOutputPublicFilePath(url)))
} else if (!isExcludedUrl(url)) {
s.update(start, end, await urlToBuiltUrl(url, id, config, this))
s.update(
start,
end,
partialEncodeURI(await urlToBuiltUrl(url, id, config, this)),
)
}
}

Expand Down Expand Up @@ -897,17 +904,19 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
if (chunk) {
chunk.viteMetadata!.importedAssets.add(cleanUrl(file))
}
return toOutputAssetFilePath(file) + postfix
return encodeURI(toOutputAssetFilePath(file)) + postfix
})

result = result.replace(publicAssetUrlRE, (_, fileHash) => {
const publicAssetPath = toOutputPublicAssetFilePath(
getPublicAssetFilename(fileHash, config)!,
)

return urlCanParse(publicAssetPath)
? publicAssetPath
: normalizePath(publicAssetPath)
return encodeURI(
urlCanParse(publicAssetPath)
? publicAssetPath
: normalizePath(publicAssetPath),
)
})

if (chunk && canInlineEntry) {
Expand Down
3 changes: 2 additions & 1 deletion packages/vite/src/node/plugins/importAnalysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
joinUrlSegments,
moduleListContains,
normalizePath,
partialEncodeURI,
prettifyUrl,
removeImportQuery,
removeTimestampQuery,
Expand Down Expand Up @@ -591,7 +592,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
rewriteDone = true
}
if (!rewriteDone) {
const rewrittenUrl = JSON.stringify(url)
const rewrittenUrl = JSON.stringify(partialEncodeURI(url))
const s = isDynamicImport ? start : start - 1
const e = isDynamicImport ? end : end + 1
str().overwrite(s, e, rewrittenUrl, {
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/plugins/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin {
)
const replacementString =
typeof replacement === 'string'
? JSON.stringify(replacement).slice(1, -1)
? JSON.stringify(encodeURI(replacement)).slice(1, -1)
: `"+${replacement.runtime}+"`
s.update(match.index, match.index + full.length, replacementString)
}
Expand Down
10 changes: 10 additions & 0 deletions packages/vite/src/node/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,8 @@ function joinSrcset(ret: ImageCandidate[]) {
.join(', ')
}

// NOTE: The returned `url` should perhaps be decoded so all handled URLs within Vite are consistently decoded.
// However, this may also require a refactor for `cssReplacer` to accept decoded URLs instead.
function splitSrcSetDescriptor(srcs: string): ImageCandidate[] {
return splitSrcSet(srcs)
.map((s) => {
Expand Down Expand Up @@ -1407,3 +1409,11 @@ export function displayTime(time: number): string {
// display: {X}m {Y}s
return `${mins}m${seconds < 1 ? '' : ` ${seconds.toFixed(0)}s`}`
}

/**
* Like `encodeURI`, but only replacing `%` as `%25`. This is useful for environments
* that can handle un-encoded URIs, where `%` is the only ambiguous character.
*/
export function partialEncodeURI(uri: string): string {
return uri.replaceAll('%', '%25')
}
4 changes: 2 additions & 2 deletions playground/assets/__tests__/assets.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,11 +369,11 @@ test('?url import on css', async () => {

describe('unicode url', () => {
test('from js import', async () => {
const src = readFile('テスト-測試-white space.js')
const src = readFile('テスト-測試-white space%.js')
expect(await page.textContent('.unicode-url')).toMatch(
isBuild
? `data:text/javascript;base64,${Buffer.from(src).toString('base64')}`
: `/foo/bar/テスト-測試-white space.js`,
: encodeURI(`/foo/bar/テスト-測試-white space%.js`),
)
})
})
Expand Down
6 changes: 3 additions & 3 deletions playground/assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ <h2>CSS url references</h2>
<h2>Unicode URL</h2>
<div>
<code class="unicode-url"></code>
<img src="./nested/テスト-測試-white space.png" />
<img src="./nested/テスト-測試-white space%25.png" />
</div>

<h2>Filename including single quote</h2>
Expand All @@ -147,7 +147,7 @@ <h2>encodeURI for the address</h2>
<div>
<img
class="encodeURI"
src="./nested/%E3%83%86%E3%82%B9%E3%83%88-%E6%B8%AC%E8%A9%A6-white%20space.png"
src="./nested/%E3%83%86%E3%82%B9%E3%83%88-%E6%B8%AC%E8%A9%A6-white%20space%25.png"
/>
</div>

Expand Down Expand Up @@ -442,7 +442,7 @@ <h3>assets in noscript</h3>
import fooUrl from './foo.js?url'
text('.url', fooUrl)

import unicodeUrl from './テスト-測試-white space.js?url'
import unicodeUrl from './テスト-測試-white space%.js?url'
text('.unicode-url', unicodeUrl)

import filenameIncludingSingleQuoteUrl from "./nested/with-single'quote.png"
Expand Down

0 comments on commit b10d162

Please sign in to comment.