diff --git a/newIDE/app/src/ResourcesList/ResourceUtils.js b/newIDE/app/src/ResourcesList/ResourceUtils.js index f98922d7dff7..f3e54a3530e4 100644 --- a/newIDE/app/src/ResourcesList/ResourceUtils.js +++ b/newIDE/app/src/ResourcesList/ResourceUtils.js @@ -48,7 +48,12 @@ export const getLocalResourceFullPath = ( resourcePath.lastIndexOf('?cache=') ); } - return resourcePath; + + try { + return decodeURIComponent(resourcePath); + } catch (error) { + return resourcePath; + } }; export const isPathInProjectFolder = ( diff --git a/newIDE/app/src/ResourcesList/ResourceUtils.spec.js b/newIDE/app/src/ResourcesList/ResourceUtils.spec.js index 23cbabe8c320..ad62be7844d1 100644 --- a/newIDE/app/src/ResourcesList/ResourceUtils.spec.js +++ b/newIDE/app/src/ResourcesList/ResourceUtils.spec.js @@ -1,9 +1,11 @@ // @flow import { + getLocalResourceFullPath, parseLocalFilePathOrExtensionFromMetadata, renameResourcesInProject, updateResourceJsonMetadata, } from './ResourceUtils'; +import ResourcesLoader from '../ResourcesLoader'; const gd: libGDevelop = global.gd; const addNewAnimationWithImageToSpriteObject = ( @@ -22,6 +24,20 @@ const addNewAnimationWithImageToSpriteObject = ( }; describe('ResourceUtils', () => { + it('decodes local resource paths from file URLs', () => { + const getResourceFullUrlSpy = jest + .spyOn(ResourcesLoader, 'getResourceFullUrl') + .mockReturnValue( + 'file:///Users/me/Game%20Jam%20%231/sprite%20%231.png?cache=123' + ); + + expect(getLocalResourceFullPath((null: any), 'sprite #1.png')).toBe( + '/Users/me/Game Jam #1/sprite #1.png' + ); + + getResourceFullUrlSpy.mockRestore(); + }); + it('can rename a resource in the whole project', () => { const project = gd.ProjectHelper.createNewGDJSProject(); diff --git a/newIDE/app/src/ResourcesLoader/index.js b/newIDE/app/src/ResourcesLoader/index.js index 43f2d79d6d80..611374a0cf7b 100644 --- a/newIDE/app/src/ResourcesLoader/index.js +++ b/newIDE/app/src/ResourcesLoader/index.js @@ -4,6 +4,16 @@ import optionalRequire from '../Utils/OptionalRequire'; const electron = optionalRequire('electron'); const path = optionalRequire('path'); +export const getFileUrlFromPath = (filePath: string): string => { + const normalizedFilePath = filePath.replace(/\\/g, '/'); + return ( + 'file://' + + encodeURI(normalizedFilePath) + .replace(/#/g, '%23') + .replace(/\?/g, '%3F') + ); +}; + class UrlsCache { projectCache: { [number]: { [string]: string } } = {}; @@ -132,15 +142,13 @@ export default class ResourcesLoader { // Support local filesystem with Electron const file = project.getProjectFile(); const projectPath = path.dirname(file); - const resourceAbsolutePath = path - .resolve(projectPath, urlOrFilename) - .replace(/\\/g, '/'); + const resourceAbsolutePath = path.resolve(projectPath, urlOrFilename); console.info('Caching resolved local filename:', resourceAbsolutePath); return this._cache.cacheLocalFileUrl( project, urlOrFilename, - 'file://' + resourceAbsolutePath, + getFileUrlFromPath(resourceAbsolutePath), !!disableCacheBurst ); } diff --git a/newIDE/app/src/ResourcesLoader/index.spec.js b/newIDE/app/src/ResourcesLoader/index.spec.js new file mode 100644 index 000000000000..d561175ff292 --- /dev/null +++ b/newIDE/app/src/ResourcesLoader/index.spec.js @@ -0,0 +1,16 @@ +// @flow +import { getFileUrlFromPath } from './index'; + +describe('ResourcesLoader', () => { + it('escapes hash characters in local file URLs', () => { + expect(getFileUrlFromPath('/Users/me/Game Jam #1/sprite #1.png')).toBe( + 'file:///Users/me/Game%20Jam%20%231/sprite%20%231.png' + ); + }); + + it('escapes question marks before cache-busting parameters are appended', () => { + expect(getFileUrlFromPath('/tmp/project/image?draft.png')).toBe( + 'file:///tmp/project/image%3Fdraft.png' + ); + }); +});