Skip to content

🐛 discard remote pastes with invalid content instead of writing them to the clipboard - #5234

Merged
guiyanakuang merged 3 commits into
mainfrom
fix/empty-remote-paste-clipboard
Oct 19, 2026
Merged

guiyanakuang merged 3 commits into
mainfrom
fix/empty-remote-paste-clipboard

Conversation

@guiyanakuang

@guiyanakuang guiyanakuang commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Problem

Received pastes are persisted as-is, so unlike a local capture they never go through
RemoveInvalidPlugin. A paste carrying an empty text flavor (e.g. a peer that captured
an empty clipboard) was stored, relayed, and handed to the clipboard, where the empty
value replaced whatever the user had copied: pbpaste prints nothing and pasting into
any app yields no content.

Fix

Treat invalid content as a receive policy, next to the existing
discardOversizedRemoteNonFilePaste / discardInvalidRemoteFilePaste:

  • PasteData.hasValidContent() extracts the content check out of isValid(), so the
    receiver can judge a paste without depending on its pasteState (isValid() keeps
    its behavior and now delegates to it for LOADED).

  • prepareRemotePasteReceive() calls discardInvalidRemotePasteContent(): a paste whose
    content is invalid is dropped before anything is stored, and its remote ID is settled so
    a reconnect does not pull it again. It is logged, not notified — nothing meaningful is
    lost.

  • releaseRemotePasteDataForPush() (HTTP push session for files/images) applies the same
    check right after its file-type check, before reserving the cursor or creating a row.
    validateFileTransferMetadata alone does not imply PasteFiles.isValid() (a zero-byte
    file or an empty hash passes it).

Every receive entry — WS push, pull, history pull and HTTP file push — is now checked
before anything is stored, so the invalid paste never becomes a hidden row, is never
relayed to other devices, and never reaches the clipboard. Finalize
(releaseRemotePasteDataWithFile) needs no extra guard: its rows only come from these
entries, and receives interrupted by an older build are discarded at startup by
discardInterruptedReceives().

Like the existing oversized discard, a discarded paste is reported back as rejected, so
the sender records one non-retried sync failure for it. This is intentional: it is a
receiver policy decision, not a transport error. PasteReleaseService
and PasteData are in commonMain, so this applies to desktop and mobile alike.

Tests

  • PasteReleaseServiceRemoteDiscardTest: an empty-text remote paste is discarded — no task
    submitted, no clipboard write, cursor settled, no notification. The "within limit" test
    now runs the submitted block and asserts the clipboard write actually happens.
  • File pastes whose metadata is valid but content is not (a zero-byte file): discarded on
    the pull path before any download (no task, cursor settled, no clipboard write), and
    rejected on the HTTP push path before the cursor is reserved or a row is created.

./gradlew app:desktopTest (fast tier) passes; ktlintFormat clean.

@guiyanakuang
guiyanakuang force-pushed the fix/empty-remote-paste-clipboard branch from 53d3f13 to 3f5a533 Compare October 19, 2026 08:04
@guiyanakuang guiyanakuang changed the title 🐛 never write an empty remote paste onto the clipboard 🐛 never write an invalid remote paste onto the clipboard Oct 19, 2026
@guiyanakuang
guiyanakuang force-pushed the fix/empty-remote-paste-clipboard branch from 3f5a533 to 2f1f2cc Compare October 19, 2026 08:09
Received pastes are persisted as-is, so unlike a local capture they never
went through RemoveInvalidPlugin. A paste carrying an empty text flavor
(e.g. a peer that captured an empty clipboard) was handed to the
clipboard, and the empty value replaced whatever the user had copied:
pbpaste prints nothing and paste yields no content.

Bind decoded wire pastes as PasteState.LOADED: the serializer defaults
them to LOADING, for which PasteData.isValid() short-circuits to true,
even though the sender had already finished the paste.

Then reuse PasteData.isValid() on both clipboard-write paths:

- releaseRemoteNonFilePaste keeps the paste in history (the UI filters
  invalid rows) but skips the clipboard write when it is invalid.
- releaseRemotePasteDataWithFile applies the same check when a downloaded
  file paste is finalized.

Valid remote pastes are written exactly as before.
@guiyanakuang
guiyanakuang force-pushed the fix/empty-remote-paste-clipboard branch from 2f1f2cc to 879d7d3 Compare October 19, 2026 08:17
Replace the previous approach (bind every wire paste as LOADED, store the
invalid row, then skip only the clipboard write) with a receive policy next
to the existing oversized/invalid-file discards:

- PasteData.hasValidContent() extracts the content check from isValid(),
  so the receiver can judge a paste without faking its pasteState.
- prepareRemotePasteReceive() drops a paste whose content is invalid and
  settles its remote ID. It is never stored, relayed or written to the
  clipboard, for file and non-file pastes alike.
@guiyanakuang guiyanakuang changed the title 🐛 never write an invalid remote paste onto the clipboard 🐛 discard remote pastes with invalid content instead of writing them to the clipboard Oct 19, 2026
releaseRemotePasteDataForPush only ran validateFileTransferMetadata, which
does not imply PasteFiles.isValid() (e.g. a zero-byte file or an empty
hash). Apply hasValidContent() there too, before reserving the cursor or
creating a row, and cover both file entry points in tests.
@guiyanakuang
guiyanakuang merged commit 230a5e4 into main Oct 19, 2026
7 checks passed
@guiyanakuang
guiyanakuang deleted the fix/empty-remote-paste-clipboard branch October 19, 2026 09:39
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