Skip to content

Commit f0ee7d9

Browse files
committed
Fix issues
1 parent ec93fcc commit f0ee7d9

3 files changed

Lines changed: 135 additions & 34 deletions

File tree

lib/src/platform/selection/platform_selection_backend.dart

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -430,10 +430,7 @@ final class PlatformSelectionBackend
430430
diagnosticCode: 'selection_lease_missing',
431431
);
432432
}
433-
final beforeRead = await _platform.getClipboardRevision();
434-
final afterRead = await _platform.getClipboardRevision();
435-
if (beforeRead != lease.clipboardRevision ||
436-
afterRead != lease.clipboardRevision ||
433+
if (!await _clipboardStillOwned(lease.clipboardRevision, replacement) ||
437434
!await _matchesTarget(identity) ||
438435
await _platform.getForegroundWindow() != identity.handle) {
439436
throw const SelectionBackendException(
@@ -453,25 +450,58 @@ final class PlatformSelectionBackend
453450
return CommitVerification.unverified;
454451
}
455452

453+
static bool _sameClipboardText(String left, String right) {
454+
if (left == right) return true;
455+
String normalize(String value) => value.replaceAll('\r\n', '\n');
456+
return normalize(left) == normalize(right);
457+
}
458+
459+
Future<String?> _readClipboardTextSafely() async {
460+
for (var attempt = 0; attempt < 2; attempt++) {
461+
try {
462+
return await _clipboard.readText();
463+
} catch (_) {
464+
if (attempt == 0) {
465+
await Future<void>.delayed(const Duration(milliseconds: 30));
466+
}
467+
}
468+
}
469+
return null;
470+
}
471+
472+
Future<bool> _clipboardStillOwned(int revision, String expectedText) async {
473+
final currentRevision = await _platform.getClipboardRevision();
474+
if (currentRevision == revision) return true;
475+
final currentText = await _readClipboardTextSafely();
476+
if (currentText == null ||
477+
!_sameClipboardText(currentText, expectedText) ||
478+
await _platform.getClipboardRevision() != currentRevision) {
479+
return false;
480+
}
481+
_ownedClipboardText[currentRevision] = expectedText;
482+
return true;
483+
}
484+
456485
@override
457486
Future<ClipboardRestoreResult> restoreClipboard(
458487
ClipboardSnapshot snapshot, {
459488
required int expectedRevision,
460489
bool restoreOriginal = true,
461490
}) async {
462491
try {
463-
final currentRevision = await _platform.getClipboardRevision();
464492
final expectedText = _ownedClipboardText[expectedRevision];
465-
if (currentRevision != expectedRevision || expectedText == null) {
493+
if (expectedText == null ||
494+
!await _clipboardStillOwned(expectedRevision, expectedText)) {
466495
return ClipboardRestoreResult.skippedExternalChange;
467496
}
497+
final currentRevision = await _platform.getClipboardRevision();
468498
final supportsAtomicTransactions =
469499
_platform.supportsAtomicTextClipboardTransactions();
470500
if (!supportsAtomicTransactions) {
471501
final currentText = await _clipboard.readText();
472502
final revisionAfterRead = await _platform.getClipboardRevision();
473-
if (revisionAfterRead != expectedRevision ||
474-
currentText != expectedText) {
503+
if (revisionAfterRead != currentRevision ||
504+
!_sameClipboardText(currentText, expectedText)) {
475505
return ClipboardRestoreResult.skippedExternalChange;
476506
}
477507
}
@@ -488,7 +518,7 @@ final class PlatformSelectionBackend
488518
final originalData = original!;
489519
final usesNativeSnapshot = originalData is PlatformClipboardSnapshot;
490520
if (supportsAtomicTransactions || usesNativeSnapshot) {
491-
var recoveryRevision = expectedRevision;
521+
var recoveryRevision = currentRevision;
492522
var recoveryText = expectedText;
493523
for (var attempt = 0; attempt < 3; attempt++) {
494524
try {
@@ -559,7 +589,7 @@ final class PlatformSelectionBackend
559589
}
560590
}
561591
} else {
562-
if (await _platform.getClipboardRevision() != expectedRevision) {
592+
if (await _platform.getClipboardRevision() != currentRevision) {
563593
return ClipboardRestoreResult.skippedExternalChange;
564594
}
565595
if (originalData is! String) {

lib/src/platform/windows/windows_platform_gateway.dart

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -646,42 +646,50 @@ class WindowsPlatformGateway
646646
retryable: true,
647647
);
648648
}
649+
var revisionMatched = false;
650+
var desiredWritten = false;
651+
var rollbackWritten = false;
649652
try {
650-
if (GetClipboardSequenceNumber() != expectedRevision) return null;
651-
if (GetClipboardSequenceNumber() != expectedRevision) return null;
652-
if (EmptyClipboard() == 0) {
653-
throw const ClipboardTransactionException(
654-
code: 'clipboard_empty_failed',
655-
retryable: true,
656-
);
657-
}
658-
if (SetClipboardData(
659-
CLIPBOARD_FORMAT.CF_UNICODETEXT,
660-
desiredMemory.address,
661-
) ==
662-
0) {
653+
if (GetClipboardSequenceNumber() == expectedRevision) {
654+
revisionMatched = true;
655+
if (EmptyClipboard() == 0) {
656+
throw const ClipboardTransactionException(
657+
code: 'clipboard_empty_failed',
658+
retryable: true,
659+
);
660+
}
663661
if (SetClipboardData(
662+
CLIPBOARD_FORMAT.CF_UNICODETEXT,
663+
desiredMemory.address,
664+
) !=
665+
0) {
666+
desiredTransferred = true;
667+
desiredWritten = true;
668+
_excludeStagedContentFromClipboardMonitors();
669+
} else if (SetClipboardData(
664670
CLIPBOARD_FORMAT.CF_UNICODETEXT,
665671
rollbackMemory.address,
666672
) !=
667673
0) {
668674
rollbackTransferred = true;
669-
throw AtomicClipboardMutationException(
670-
revision: GetClipboardSequenceNumber(),
671-
currentText: rollbackText,
672-
);
675+
rollbackWritten = true;
673676
}
674-
throw AtomicClipboardMutationException(
675-
revision: GetClipboardSequenceNumber(),
676-
currentText: '',
677-
);
678677
}
679-
desiredTransferred = true;
680-
_excludeStagedContentFromClipboardMonitors();
681-
return GetClipboardSequenceNumber();
682678
} finally {
683679
CloseClipboard();
684680
}
681+
if (!revisionMatched) return null;
682+
if (desiredWritten) return GetClipboardSequenceNumber();
683+
if (rollbackWritten) {
684+
throw AtomicClipboardMutationException(
685+
revision: GetClipboardSequenceNumber(),
686+
currentText: rollbackText,
687+
);
688+
}
689+
throw AtomicClipboardMutationException(
690+
revision: GetClipboardSequenceNumber(),
691+
currentText: '',
692+
);
685693
} finally {
686694
if (!desiredTransferred) GlobalFree(desiredMemory);
687695
if (!rollbackTransferred && rollbackMemory != null) {

test/platform/selection/platform_selection_backend_test.dart

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ void main() {
112112
rollbackText: copy.text,
113113
);
114114
platform.revision++;
115+
clipboard.text = 'external';
115116

116117
expect(
117118
await backend.restoreClipboard(
@@ -645,6 +646,68 @@ void main() {
645646
);
646647
});
647648

649+
test('commits and restores across a phantom revision bump', () async {
650+
final platform = _FakeSelectionPlatformGateway(atomicTransactions: true);
651+
final clipboard = _FakeClipboard(platform, 'original');
652+
final backend = PlatformSelectionBackend(
653+
platform: platform,
654+
clipboard: clipboard,
655+
);
656+
final target = await backend.captureTarget();
657+
final snapshot = await backend.snapshotClipboard();
658+
await backend.focus(target);
659+
final lease = await backend.stageReplacement(
660+
target,
661+
'improved',
662+
expectedRevision: platform.revision,
663+
rollbackText: 'original',
664+
);
665+
platform.revision++;
666+
667+
final verification = await backend.commitReplacement(lease);
668+
final restore = await backend.restoreClipboard(
669+
snapshot,
670+
expectedRevision: lease.clipboardRevision,
671+
);
672+
673+
expect(verification, CommitVerification.unverified);
674+
expect(platform.replacement, 'improved');
675+
expect(restore, ClipboardRestoreResult.restored);
676+
expect(clipboard.text, 'original');
677+
});
678+
679+
test('a phantom bump with foreign clipboard text still loses the lease',
680+
() async {
681+
final platform = _FakeSelectionPlatformGateway(atomicTransactions: true);
682+
final clipboard = _FakeClipboard(platform, 'original');
683+
final backend = PlatformSelectionBackend(
684+
platform: platform,
685+
clipboard: clipboard,
686+
);
687+
final target = await backend.captureTarget();
688+
await backend.focus(target);
689+
final lease = await backend.stageReplacement(
690+
target,
691+
'improved',
692+
expectedRevision: platform.revision,
693+
rollbackText: 'original',
694+
);
695+
platform.revision++;
696+
clipboard.text = 'external';
697+
698+
await expectLater(
699+
backend.commitReplacement(lease),
700+
throwsA(
701+
isA<SelectionBackendException>().having(
702+
(error) => error.diagnosticCode,
703+
'diagnosticCode',
704+
'selection_stage_lease_lost',
705+
),
706+
),
707+
);
708+
expect(platform.replacement, isNull);
709+
});
710+
648711
test('rejects a stale copy instead of reusing clipboard contents', () async {
649712
final platform = _FakeSelectionPlatformGateway(copyChangesRevision: false);
650713
final clipboard = _FakeClipboard(platform, 'original');

0 commit comments

Comments
 (0)