diff --git a/__tests__/files/node.spec.ts b/__tests__/files/node.spec.ts index 029937a6..136b7ce0 100644 --- a/__tests__/files/node.spec.ts +++ b/__tests__/files/node.spec.ts @@ -740,16 +740,66 @@ describe('Undefined properties are allowed', () => { }) describe('Encoded source is handled properly', () => { - test('File', () => { + test('File with special characters', () => { const file = new File({ - source: 'https://cloud.domain.com/remote.php/dav/files/em ma!/Photos~⛰️ shot of a $[big} mountain/realy #1\'s.md', + source: 'https://cloud.domain.com/remote.php/dav/files/em ma!/Photos~⛰️ shot of a $[big} mountain/really #1\'s.md', owner: 'em ma!', id: 123456, mime: 'image/jpeg', root: '/files/em ma!', }) - expect(file.encodedSource).toBe('https://cloud.domain.com/remote.php/dav/files/em%20ma!/Photos~%E2%9B%B0%EF%B8%8F%20shot%20of%20a%20%24%5Bbig%7D%20mountain/realy%20%231\'s.md') + expect(file.encodedSource).toBe('https://cloud.domain.com/remote.php/dav/files/em%20ma!/Photos~%E2%9B%B0%EF%B8%8F%20shot%20of%20a%20%24%5Bbig%7D%20mountain/really%20%231\'s.md') + expect(file.path).toBe('/Photos~⛰️ shot of a $[big} mountain/really #1\'s.md') + expect(file.basename).toBe('really #1\'s.md') + }) + + test('Folder with question mark', () => { + const folder = new Folder({ + source: 'https://cloud.domain.com/remote.php/dav/files/emma?/Photos?', + owner: 'emma?', + root: '/files/emma?', + }) + + expect(folder.encodedSource).toBe('https://cloud.domain.com/remote.php/dav/files/emma%3F/Photos%3F') + expect(folder.path).toBe('/Photos?') + expect(folder.basename).toBe('Photos?') + }) + + test('Folder with percent characters', () => { + const folder = new Folder({ + source: 'https://cloud.domain.com/remote.php/dav/files/emma%/Ph%otos/rea%lly%', + owner: 'emma%', + root: '/files/emma%', + }) + + expect(folder.encodedSource).toBe('https://cloud.domain.com/remote.php/dav/files/emma%25/Ph%25otos/rea%25lly%25') + expect(folder.path).toBe('/Ph%otos/rea%lly%') + expect(folder.basename).toBe('rea%lly%') + }) + + test('Unicode combining marks', () => { + const file = new File({ + source: 'https://cloud.domain.com/remote.php/dav/files/emma/cre\u0301me', + owner: 'emma', + root: '/files/emma', + }) + + expect(file.encodedSource).toBe('https://cloud.domain.com/remote.php/dav/files/emma/cre%CC%81me') + expect(file.path).toBe('/créme') + expect(file.basename).toBe('créme') + }) + + test('Plus signs in filename', () => { + const folder = new Folder({ + source: 'https://cloud.domain.com/remote.php/dav/files/emma/My+Folder/fi+le+.txt', + owner: 'emma', + root: '/files/emma', + }) + + expect(folder.encodedSource).toBe('https://cloud.domain.com/remote.php/dav/files/emma/My%2BFolder/fi%2Ble%2B.txt') + expect(folder.path).toBe('/My+Folder/fi+le+.txt') + expect(folder.basename).toBe('fi+le+.txt') }) }) diff --git a/lib/node/node.ts b/lib/node/node.ts index 54da8c10..c6047ed7 100644 --- a/lib/node/node.ts +++ b/lib/node/node.ts @@ -270,8 +270,21 @@ export abstract class Node { * Get the absolute path of this object relative to the root */ get path(): string { - const url = new URL(this.source) - let source = decodeURI(url.pathname) + // Extract the path part from the source URL + // e.g. https://cloud.domain.com/remote.php/dav/files/username/Path/To/File.txt + const idx = this.source.indexOf('://') + const protocol = this.source.slice(0, idx) // e.g. https + const remainder = this.source.slice(idx + 3) // e.g. cloud.domain.com/remote.php/dav/files/username/Path/To/File.txt + + const slashIndex = remainder.indexOf('/') + const host = remainder.slice(0, slashIndex) // e.g. cloud.domain.com + const rawPath = remainder.slice(slashIndex) // e.g. /remote.php/dav/files/username/Path/To/File.txt + + // Rebuild a safe URL with encoded path + const safeUrl = `${protocol}://${host}${encodePath(rawPath)}` + const url = new URL(safeUrl) + + let source = decodeURIComponent(url.pathname) if (this.isDavResource) { // ensure we only work on the real path in case root is not distinct