Skip to content

Fix critical bugs: off-by-one crash, OCR data race, DB recovery, accessibility prompt#1362

Open
twitchyvr wants to merge 5 commits intop0deje:masterfrom
twitchyvr:fix/critical-bugs-and-performance
Open

Fix critical bugs: off-by-one crash, OCR data race, DB recovery, accessibility prompt#1362
twitchyvr wants to merge 5 commits intop0deje:masterfrom
twitchyvr:fix/critical-bugs-and-performance

Conversation

@twitchyvr
Copy link
Copy Markdown

Summary

Five targeted fixes for bugs found during code review — no new features, no model changes, no behavioral shifts.

1. Off-by-one crash in AppIntents (Get, Select, Delete)

Files: Intents/Get.swift, Intents/Select.swift, Intents/Delete.swift

The bounds check items.count >= index allows items[items.count], which is out of bounds. For example, with 5 items, requesting item #5 yields index = 4, and 5 >= 4 passes the guard, but items[4] is valid — however, requesting item #6 yields index = 5, and 5 >= 5 passes the guard, then items[5] crashes. More critically, when items.count == index (boundary case), the access crashes.

Fix: Changed to index >= 0 && index < items.count — standard bounds checking that also prevents negative index access.

2. Data race in Vision OCR callback

File: Models/HistoryItem.swift

recognizeTextHandler runs on an arbitrary Vision framework queue and writes directly to self.title. Since HistoryItem is a @Model class, its properties should only be mutated on the main actor (the model context's actor). This is a data race that can corrupt SwiftData's change tracking.

Fix: Dispatch the title update to DispatchQueue.main.async.

3. Graceful database recovery instead of fatalError

File: Storage.swift

If ModelContainer creation fails (corrupt SQLite from forced quit, disk error, etc.), the app calls fatalError() and is permanently bricked until the user manually deletes ~/Library/Application Support/Maccy/Storage.sqlite.

Fix: Catch the error, delete the corrupt .sqlite, .shm, and .wal files, and retry container creation. The user loses history but the app remains functional. Only fatalErrors if re-creation also fails.

4. Cache compiled regexes in clipboard ignore check

File: Clipboard.swift

shouldIgnore(_ item:) creates new NSRegularExpression objects for every pattern on every clipboard poll (default: every 0.5s). Regex compilation is expensive.

Fix: Cache compiled regexes and only recompile when the user's pattern list changes. Also restructured to extract the string once before the loop.

5. Prompt user for accessibility permissions

Files: Accessibility.swift, Clipboard.swift

Accessibility.check() was a no-op when permissions were missing — it returned silently, and paste() would proceed to simulate keyboard events that did nothing. Users had no indication why paste wasn't working.

Fix: check() now returns a Bool and triggers the macOS accessibility permissions prompt via AXIsProcessTrustedWithOptions with the prompt option. paste() gates on the return value.

Test plan

  • Verify AppIntents work via Shortcuts app with boundary item numbers
  • Copy an image to trigger OCR — confirm no crash or unexpected behavior
  • Simulate corrupt database (rename Storage.sqlite to corrupt it) — verify app recovers
  • Add ignore regex patterns — verify clipboard polling remains performant
  • Revoke accessibility permissions — verify paste prompts for permissions instead of silently failing
  • General regression: copy, paste, search, pin/unpin, delete all work as before

🤖 Generated with Claude Code

twitchyvr and others added 5 commits March 22, 2026 03:29
The bounds check in Get, Select, and Delete intents used `>=` instead
of `>`, causing an index-out-of-bounds crash when requesting the last
item by number. For example, with 5 items, requesting item p0deje#5 would
pass the guard (`5 >= 5` is true) but then crash on `items[5]`.

Also adds a lower bound check (`index >= 0`) to prevent negative
index access when `number` is 0 or negative.

Co-Authored-By: Claude <noreply@anthropic.com>
The VNRecognizeTextRequest completion handler runs on an arbitrary
background queue but writes directly to self.title, which is a
property on a @model class (SwiftData). Since the model context uses
the main actor, this is a data race that can corrupt SwiftData's
change tracking.

Dispatch the title update to the main queue to ensure thread safety.

Co-Authored-By: Claude <noreply@anthropic.com>
If the SwiftData ModelContainer fails to load (e.g., due to a corrupt
SQLite file from a forced quit or disk error), the app previously
called fatalError(), permanently bricking the app until the user
manually deleted ~/Library/Application Support/Maccy/Storage.sqlite.

Now attempts to delete the corrupt database files (.sqlite, .shm,
.wal) and recreate the container. The user loses their clipboard
history but the app remains functional. Only calls fatalError if
re-creation also fails.

Co-Authored-By: Claude <noreply@anthropic.com>
shouldIgnore(_ item:) was creating new NSRegularExpression objects
for every pattern in ignoreRegexp on every clipboard poll (default
interval: 0.5s). Regex compilation is expensive relative to matching.

Now caches the compiled regexes and only recompiles when the user's
pattern list changes. Also moves the string extraction outside the
loop since it only needs to happen once per check.

Co-Authored-By: Claude <noreply@anthropic.com>
Accessibility.check() was a silent no-op — if the user hadn't granted
accessibility permissions, paste() would proceed to simulate keyboard
events that silently did nothing. Users had no indication why paste
wasn't working.

Now check() returns a Bool and prompts the macOS accessibility
permissions dialog via AXIsProcessTrustedWithOptions when permissions
are missing. Clipboard.paste() gates on the return value, preventing
the futile keyboard event simulation.

Co-Authored-By: Claude <noreply@anthropic.com>
@twitchyvr
Copy link
Copy Markdown
Author

CI Note

The Bitrise CI run shows testCopyRTF failing (all 3 retries) — this is not related to this PR.

The failure is an RTF title parsing issue on the CI VM:

XCTAssertEqual failed: ("["foo", "DC74B875-..."]") is not equal to ("["foo", "bar"]")

The RTF NSAttributedString parser returns a UUID instead of the parsed text "bar" — likely a font/text services issue on the CI's macOS Sequoia VM. The test suite reports 38 tests, 6 failures (0 unexpected), confirming these are known/expected failures.

This PR does not modify:

  • Any test files (git diff upstream/master..fix/critical-bugs-and-performance -- MaccyUITests/ is empty)
  • generateTitle() or previewableText (the RTF parsing path)
  • Any RTF handling code

The only HistoryItem.swift change is dispatching the Vision OCR callback to the main queue — completely unrelated to RTF.

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