Skip to content

fix(export): inline local images on Windows - #363

Merged
PathGao merged 1 commit into
sftwrdotdev:masterfrom
PathGao:fix/inline-local-images-on-windows
Aug 2, 2026
Merged

fix(export): inline local images on Windows#363
PathGao merged 1 commit into
sftwrdotdev:masterfrom
PathGao:fix/inline-local-images-on-windows

Conversation

@PathGao

@PathGao PathGao commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

convertFileSrc does not return the same shape on every platform: Windows (and Android) get http://asset.localhost/<encoded path>, everywhere else gets asset://localhost/<encoded path>. The app's own CSP lists both forms, but normalizeAssetPath only recognised the second.

Worse, the remote-scheme bail-out ran first. Because the Windows asset URL is an http: URL, it was discarded before it could be recognised — so the image was neither inlined nor counted in missingImages. A Windows export silently shipped http://asset.localhost/... references that are dead the moment the file leaves the app, and the "some images could not be inlined" toast never fired.

This recognises both shapes and checks for them before the remote bail-out. The host match is anchored, so asset.localhost.evil.test and asset.localhostx stay remote.

Reviewer note

The existing tests passed only because they used asset://localhost/C:/… — a shape convertFileSrc never actually produces (it percent-encodes the path). The new cases use the real encoded output for both platforms, plus look-alike hosts as negatives.

Validation

  • npm run check — 0 errors, 0 warnings
  • npm test — 156/156

`convertFileSrc` does not return the same shape on every platform: Windows
(and Android) get `http://asset.localhost/<encoded path>`, everywhere else
gets `asset://localhost/<encoded path>`. The app's own CSP lists both forms,
but the export only recognised the second.

Worse, the remote-scheme bail-out ran first. Because the Windows asset URL
*is* an `http:` URL, it was discarded before it could be recognised — so the
image was neither inlined nor counted in `missingImages`. A Windows export
silently shipped `http://asset.localhost/...` links that are dead the moment
the file leaves the app, with no warning toast.

Recognise both shapes and check for them before the remote bail-out. The
host match is anchored so `asset.localhost.evil.test` and `asset.localhostx`
stay remote.

The existing tests only covered `asset://localhost/C:/…`, a shape
`convertFileSrc` never actually produces; the new ones use the real encoded
output for both platforms.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@PathGao
PathGao merged commit 3e05a22 into sftwrdotdev:master Aug 2, 2026
5 checks passed
@PathGao
PathGao deleted the fix/inline-local-images-on-windows branch August 2, 2026 18:18
PathGao pushed a commit that referenced this pull request Aug 2, 2026
`[data](./data.csv)` was handed to `openUrl(anchor.href)`, and
`anchor.href` is what the DOM resolved against the webview origin, not a
path on disk. The two platforms then failed differently: on macOS and
Linux the origin is `tauri://localhost`, which the opener scope refuses,
so the click did nothing and left an uncaught promise rejection; on
Windows it is `http://tauri.localhost`, which matches `http://*`, so the
browser really opened onto a dead link.

`resolveLocalFileLinkPath` reuses `resolveExportImagePath` from #363 for
the whole scheme, drive-letter, UNC and query-suffix decision table, and
adds two rules a link needs that an image does not: `//host/path` is a web
address, matching what `getMarkdownLinkTarget` already assumes, and a
relative link in an unsaved buffer resolves to nothing rather than to
something relative to the process working directory.

Both OS calls are now inside try/catch with a toast, which removes the
uncaught rejection and makes the macOS failure visible.

The file still will not open until the opener path scope is decided
(#399, #403); everything above is an improvement regardless.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PathGao pushed a commit that referenced this pull request Aug 2, 2026
`[data](./data.csv)` was handed to `openUrl(anchor.href)`, and
`anchor.href` is what the DOM resolved against the webview origin, not a
path on disk. The two platforms then failed differently: on macOS and
Linux the origin is `tauri://localhost`, which the opener scope refuses,
so the click did nothing and left an uncaught promise rejection; on
Windows it is `http://tauri.localhost`, which matches `http://*`, so the
browser really opened onto a dead link.

`resolveLocalFileLinkPath` reuses `resolveExportImagePath` from #363 for
the whole scheme, drive-letter, UNC and query-suffix decision table, and
adds two rules a link needs that an image does not: `//host/path` is a web
address, matching what `getMarkdownLinkTarget` already assumes, and a
relative link in an unsaved buffer resolves to nothing rather than to
something relative to the process working directory.

Both OS calls are now inside try/catch with a toast, which removes the
uncaught rejection and makes the macOS failure visible.

The file still will not open until the opener path scope is decided
(#399, #403); everything above is an improvement regardless.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PathGao pushed a commit that referenced this pull request Aug 2, 2026
`[data](./data.csv)` was handed to `openUrl(anchor.href)`, and
`anchor.href` is what the DOM resolved against the webview origin, not a
path on disk. The two platforms then failed differently: on macOS and
Linux the origin is `tauri://localhost`, which the opener scope refuses,
so the click did nothing and left an uncaught promise rejection; on
Windows it is `http://tauri.localhost`, which matches `http://*`, so the
browser really opened onto a dead link.

`resolveLocalFileLinkPath` reuses `resolveExportImagePath` from #363 for
the whole scheme, drive-letter, UNC and query-suffix decision table, and
adds two rules a link needs that an image does not: `//host/path` is a web
address, matching what `getMarkdownLinkTarget` already assumes, and a
relative link in an unsaved buffer resolves to nothing rather than to
something relative to the process working directory.

Both OS calls are now inside try/catch with a toast, which removes the
uncaught rejection and makes the macOS failure visible.

The file still will not open until the opener path scope is decided
(#399, #403); everything above is an improvement regardless.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PathGao added a commit that referenced this pull request Aug 3, 2026
…nestly for remote ones (#408)

The remote branch fetched the image bytes with `fetch(src)`, which
`connect-src 'self'` refuses - so it was unreachable on every platform,
not merely failing. Windows fell into that same unreachable branch for
*local* images too, because the local test was `src.startsWith('asset:')`
while Windows asset URLs are `http://asset.localhost/...`.

Reuses `normalizeAssetPath` from #363 rather than another hand-rolled
prefix test. It handles both URL shapes and rejects
`http://asset.localhost.evil.test/...`, which a `startsWith` check accepts
and would have handed to `copy_file`.

The dead fetch path is deleted. The remote case now bails before opening a
save dialog it cannot honour, and says why, instead of leaving code that
reads like a feature. Supporting it needs a download command in Rust.

Co-authored-by: PathGao <gaoyanbo@gaoyanbodeMacBook-Air.local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
PathGao added a commit that referenced this pull request Aug 3, 2026
…ent (#409)

`[data](./data.csv)` was handed to `openUrl(anchor.href)`, and
`anchor.href` is what the DOM resolved against the webview origin, not a
path on disk. The two platforms then failed differently: on macOS and
Linux the origin is `tauri://localhost`, which the opener scope refuses,
so the click did nothing and left an uncaught promise rejection; on
Windows it is `http://tauri.localhost`, which matches `http://*`, so the
browser really opened onto a dead link.

`resolveLocalFileLinkPath` reuses `resolveExportImagePath` from #363 for
the whole scheme, drive-letter, UNC and query-suffix decision table, and
adds two rules a link needs that an image does not: `//host/path` is a web
address, matching what `getMarkdownLinkTarget` already assumes, and a
relative link in an unsaved buffer resolves to nothing rather than to
something relative to the process working directory.

Both OS calls are now inside try/catch with a toast, which removes the
uncaught rejection and makes the macOS failure visible.

The file still will not open until the opener path scope is decided
(#399, #403); everything above is an improvement regardless.

Co-authored-by: PathGao <gaoyanbo@gaoyanbodeMacBook-Air.local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant