@@ -516,30 +516,47 @@ export function resolveProjectRelativeSrc(
516516) : string {
517517 const qIdx = src . indexOf ( "?" ) ;
518518 const cleanSrc = qIdx >= 0 ? src . slice ( 0 , qIdx ) : src ;
519- const fromCompiled = compiledDir ? join ( compiledDir , cleanSrc ) : null ;
520- const fromBase = join ( baseDir , cleanSrc ) ;
521519 const candidates : string [ ] = [ ] ;
522- if ( fromCompiled ) candidates . push ( fromCompiled ) ;
523- candidates . push ( fromBase ) ;
524- // If the joined result escapes the project root (either via leading `..`
525- // or mid-path traversal that path.join collapsed past baseDir), retry
526- // with the basename re-anchored at the project root. This mirrors the
527- // browser URL clamp without relying on a particular `..` shape.
528- const baseAbs = resolve ( baseDir ) ;
529- const fromBaseAbs = resolve ( fromBase ) ;
530- if ( ! fromBaseAbs . startsWith ( baseAbs + sep ) && fromBaseAbs !== baseAbs ) {
531- // Normalize first (`assets/../../assets/foo.mp4` → `../assets/foo.mp4`)
532- // then strip any remaining leading `..` segments. Stripping `..` from the
533- // raw input would leave dangling siblings (`assets/../../assets/foo`
534- // would become `assets/assets/foo` instead of `assets/foo`).
535- const normalized = posix . normalize ( cleanSrc . replace ( / \\ / g, "/" ) ) ;
536- const stripped = normalized . replace ( / ^ ( \. \. \/ ) + / , "" ) ;
537- if ( stripped && stripped !== src && ! stripped . startsWith ( ".." ) ) {
538- if ( compiledDir ) candidates . push ( join ( compiledDir , stripped ) ) ;
539- candidates . push ( join ( baseDir , stripped ) ) ;
520+
521+ const srcVariants = [ cleanSrc ] ;
522+ try {
523+ const decodedSrc = decodeURIComponent ( cleanSrc ) ;
524+ if ( decodedSrc !== cleanSrc ) srcVariants . unshift ( decodedSrc ) ;
525+ } catch {
526+ // Keep malformed percent sequences as literal filenames.
527+ }
528+
529+ const addCandidate = ( candidate : string ) : void => {
530+ if ( ! candidates . includes ( candidate ) ) candidates . push ( candidate ) ;
531+ } ;
532+
533+ for ( const variant of srcVariants ) {
534+ const fromCompiled = compiledDir ? join ( compiledDir , variant ) : null ;
535+ const fromBase = join ( baseDir , variant ) ;
536+
537+ // If the joined result escapes the project root (either via leading `..`
538+ // or mid-path traversal that path.join collapsed past baseDir), retry
539+ // with the basename re-anchored at the project root. This mirrors the
540+ // browser URL clamp without relying on a particular `..` shape.
541+ const baseAbs = resolve ( baseDir ) ;
542+ const fromBaseAbs = resolve ( fromBase ) ;
543+ if ( ! fromBaseAbs . startsWith ( baseAbs + sep ) && fromBaseAbs !== baseAbs ) {
544+ // Normalize first (`assets/../../assets/foo.mp4` → `../assets/foo.mp4`)
545+ // then strip any remaining leading `..` segments. Stripping `..` from the
546+ // raw input would leave dangling siblings (`assets/../../assets/foo`
547+ // would become `assets/assets/foo` instead of `assets/foo`).
548+ const normalized = posix . normalize ( variant . replace ( / \\ / g, "/" ) ) ;
549+ const stripped = normalized . replace ( / ^ ( \. \. \/ ) + / , "" ) ;
550+ if ( stripped && stripped !== variant && ! stripped . startsWith ( ".." ) ) {
551+ if ( compiledDir ) addCandidate ( join ( compiledDir , stripped ) ) ;
552+ addCandidate ( join ( baseDir , stripped ) ) ;
553+ }
540554 }
555+
556+ if ( fromCompiled ) addCandidate ( fromCompiled ) ;
557+ addCandidate ( fromBase ) ;
541558 }
542- return candidates . find ( existsSync ) ?? fromBase ;
559+ return candidates . find ( existsSync ) ?? join ( baseDir , cleanSrc ) ;
543560}
544561
545562export async function extractAllVideoFrames (
0 commit comments