From 53efccbc7f2d4a037617c7fdf191cc6233d5a367 Mon Sep 17 00:00:00 2001 From: skjnldsv Date: Thu, 10 Sep 2026 15:33:08 +0200 Subject: [PATCH] fix(images): encode the source handed to a media element `useViewerProps` builds an encoded source for exactly this reason, and three places went around it: the image element for a file with no preview, the video element of a live photo, and the editor's own `src`. A name holding a `#` cuts the URL short there and the file fails to load; the fallback of the first is a hand fetch, the other two just stay empty. Assisted-by: ClaudeCode:claude-opus-5 Signed-off-by: skjnldsv --- __tests__/component/imageEditor.spec.ts | 10 ++++++++++ __tests__/component/media.spec.ts | 19 +++++++++++++++++++ __tests__/utils.spec.ts | 8 ++++++++ lib/components/ImageEditor.vue | 2 +- lib/components/Images.vue | 5 +++-- lib/utils/previewUtils.ts | 4 +++- 6 files changed, 44 insertions(+), 4 deletions(-) diff --git a/__tests__/component/imageEditor.spec.ts b/__tests__/component/imageEditor.spec.ts index 9dbcc6e..b3a2acf 100644 --- a/__tests__/component/imageEditor.spec.ts +++ b/__tests__/component/imageEditor.spec.ts @@ -80,6 +80,16 @@ describe('ImageEditor wrapper', () => { expect(wrapper.emitted('close')).toBeUndefined() }) + // The editor loads the image from that URL, so a name holding a `#` or + // a `?` has to be encoded the same way the save request encodes it. + it('hands the editor the encoded source', async () => { + const file = makeFile({ basename: 'a#b c?.jpg', mime: 'image/jpeg' }) + const wrapper = mount(ImageEditor, { props: { file } }) + await flushPromises() + + expect(wrapper.findComponent({ name: 'LibImageEditor' }).attributes('src')).toBe(file.encodedSource) + }) + it('closes without saving on cancel', async () => { const { wrapper, editor } = mountEditor() editor.vm.$emit('cancel') diff --git a/__tests__/component/media.spec.ts b/__tests__/component/media.spec.ts index b67a117..3f61829 100644 --- a/__tests__/component/media.spec.ts +++ b/__tests__/component/media.spec.ts @@ -149,6 +149,25 @@ describe('Images.vue', () => { expect(preloadMediaMock).not.toHaveBeenCalled() }) + // The element's `src` is a URL: a name holding a `#` or a `?` cuts it + // short unless it is encoded, and the image then fails to load. + it('renders the encoded source of a file whose name needs it', async () => { + const file = makeFile({ basename: 'a#b c?.jpg', mime: 'image/jpeg' }) + const wrapper = mountImages({ file, files: [file] }) + await flushPromises() + + expect(wrapper.find('img').attributes('src')).toBe(file.encodedSource) + }) + + it('renders the encoded source of a live photo', async () => { + const photo = makeFile({ id: 1, basename: 'a#b.jpg', attributes: { 'metadata-files-live-photo': 2 } }) + const movie = makeFile({ id: 2, basename: 'a#b.mov', mime: 'video/quicktime' }) + const wrapper = mountImages({ file: photo, files: [photo, movie] }) + await flushPromises() + + expect(wrapper.find('video').attributes('src')).toBe(movie.encodedSource) + }) + it('shows the hand-fetched bytes when the source fails to load', async () => { const file = makeFile({ basename: 'broken.jpg' }) const wrapper = mountImages({ file, files: [file] }) diff --git a/__tests__/utils.spec.ts b/__tests__/utils.spec.ts index f1adfe0..7b1b2eb 100644 --- a/__tests__/utils.spec.ts +++ b/__tests__/utils.spec.ts @@ -121,6 +121,14 @@ describe('previewUtils.getPreviewIfAny', () => { const file = makeFileWithAttributes({ hasPreview: false }) expect(getPreviewIfAny(file)).toBe(file.source) }) + + // What comes back is handed to a media element as its `src`, so a name + // holding a `#` or a `?` has to be encoded or the URL is cut short. + it('encodes the fallback source', () => { + const file = makeFile({ basename: 'a#b c?.jpg', attributes: { hasPreview: false } }) + expect(getPreviewIfAny(file)).toBe(file.encodedSource) + expect(getPreviewIfAny(file)).not.toContain('#') + }) }) describe('livePhotoUtils.findLivePhotoPeerFromFileId', () => { diff --git a/lib/components/ImageEditor.vue b/lib/components/ImageEditor.vue index 424d43a..6e76900 100644 --- a/lib/components/ImageEditor.vue +++ b/lib/components/ImageEditor.vue @@ -4,7 +4,7 @@ -->