Skip to content

Fix blank preview for documents with execute bit set#454

Merged
schuyler merged 1 commit into
mainfrom
claude/github-tickets-review-9hyn9l
Jun 22, 2026
Merged

Fix blank preview for documents with execute bit set#454
schuyler merged 1 commit into
mainfrom
claude/github-tickets-review-9hyn9l

Conversation

@schuyler

Copy link
Copy Markdown
Owner

Summary

Fixes an issue where Markdown previews render blank when the document file has the execute bit set. Some sync clients (notably OneDrive) automatically set the execute bit (0700) on synced files, causing WebKit to silently refuse to load the preview content.

Changes

  • MPDocument.m: Added previewSafeBaseURL: method that detects executable files and substitutes a non-existent sentinel URL in the same directory. This allows WebKit to render the preview while preserving all path resolution and security scope behavior.

    • Updated renderer:didProduceHTMLOutput: to use the safe base URL
    • Updated rendererBaseURL: to use the safe base URL
  • MPDocumentIOTests.m: Added two test cases to verify the fix:

    • testPreviewSafeBaseURLLeavesNonExecutableFileUnchanged: Ensures normal files are used as-is
    • testPreviewSafeBaseURLRewritesExecutableFile: Verifies executable files are replaced with a sentinel in the same directory
  • CHANGELOG.md: Documented the fix

Implementation Details

The solution works by:

  1. Checking if the base URL points to an executable file using MPURLSecurityPolicy
  2. If executable, replacing it with a non-existent sentinel file (.macdown-preview-base) in the same directory
  3. This keeps WebKit happy (no executable base resource) while maintaining all existing behavior since relative resource resolution, security scope checks, and other features depend on the directory, not the specific file

Related to #431, #405

https://claude.ai/code/session_01R7hAw42Bbyo9kMrtUGxyN2

On macOS 26, WebKit silently refuses to load file:// preview content
whose base resource is an executable file. Some sync clients (notably
OneDrive) set the execute bit (0700) on every synced file and revert any
manual `chmod -x`, so externally-originated documents render blank in the
preview pane even though the editor reads them fine.

Add -[MPDocument previewSafeBaseURL:], which substitutes a non-existent
sentinel in the same directory when the document file is executable. The
preview WebView no longer sees an executable base resource, while relative
resource resolution and the MPURLSecurityPolicy scope check — both keyed
off the base URL's parent directory — are unchanged.

Related to #405.

Claude-Session: https://claude.ai/code/session_01R7hAw42Bbyo9kMrtUGxyN2
@schuyler schuyler merged commit 90d17dd into main Jun 22, 2026
10 checks passed
@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Report

Current Coverage: 60.46%

Coverage Details (Summary)
Name                                                                                                                                   Coverage            
-------------------------------------------------------------------------------------------------------------------------------------- ------------------- 
MASPreferences.bundle                                                                                                                  0.00% (0/0)         
MacDown 3000.app                                                                                                                       62.03% (8726/14068) 
    /Users/runner/work/macdown3000/macdown3000/MacDown/Code/Extension/NSColor+HTML.m                                                   94.05% (332/353)    
        +[NSColor(HTML) colorWithHexString:]                                                                                           94.12% (16/17)      
        +[NSColor(HTML) colorWithHTMLName:]                                                                                            89.13% (164/184)    
        __35+[NSColor(HTML) colorWithHTMLName:]_block_invoke                                                                           100.00% (152/152)   
    /Users/runner/work/macdown3000/macdown3000/MacDown/Code/Document/MPAsset.m                                                         93.69% (104/111)    
        -[MPAsset typeName]                                                                                                            100.00% (3/3)       
        -[MPAsset defaultTypeName]                                                                                                     100.00% (3/3)       
        +[MPAsset assetWithURL:andType:]                                                                                               100.00% (3/3)       
        -[MPAsset initWithURL:andType:]                                                                                                87.50% (7/8)        
        -[MPAsset init]                                                                                                                100.00% (3/3)       
        -[MPAsset templateForOption:]                                                                                                  0.00% (0/6)         
        -[MPAsset htmlForOption:]                                                                                                      100.00% (28/28)     
        -[MPStyleSheet defaultTypeName]                                                                                                100.00% (3/3)       
        +[MPStyleSheet CSSWithURL:]                                                                                                    100.00% (3/3)       
        -[MPStyleSheet templateForOption:]                                                                                             100.00% (20/20)     
        -[MPScript defaultTypeName]                                                                                                    100.00% (3/3)       
        +[MPScript javaScriptWithURL:]                                                                                                 100.00% (3/3)       
        -[MPScript templateForOption:]                                                                                                 100.00% (20/20)     
        -[MPEmbeddedScript htmlForOption:]                                                                                             100.00% (5/5)       
    /Users/runner/work/macdown3000/macdown3000/MacDown/Code/Utility/FileURLInlining.m                                                  26.60% (25/94)      
        +[FileURLInlining inlineFromIterable:]                                                                                         0.00% (0/11)        
        -[FileURLInlining initWithURL:]                                                                                                0.00% (0/6)         
        -[FileURLInlining init]                                                                                                        100.00% (3/3)       
        -[FileURLInlining inlineContent]                                                                                               0.00% (0/9)         
        -[FileURLInlining imageContent]                                                                                                0.00% (0/12)        
        +[FileURLInlining mimeTypeForFilePath:]                                                                                        100.00% (22/22)     
        -[FileURLInlining clippingContent]                                                                                             0.00% (0/4)         
        +[FileURLInlining isTextClippingAtPath:]                                                                                       0.00% (0/12)        
        +[FileURLInlining textContentFromClipping:]                                                                                    0.00% (0/15)        
    /Users/runner/work/macdown3000/macdown3000/Dependency/peg-markdown-highlight/HGMarkdownHighlightingStyle.m                         87.69% (57/65)      
        +[HGMarkdownHighlightingStyle colorFromARGBColor:]                                                                             100.00% (6/6)       
        -[HGMarkdownHighlightingStyle initWithType:attributesToAdd:toRemove:fontTraitsToAdd:]                                          88.89% (8/9)        
        -[HGMarkdownHighlightingStyle initWithStyleAttributes:baseFont:]                                                               86.00% (43/50)      
    /Users/runner/work/macdown3000/macdown3000/MacDown/Code/Utility/MPResourceWatcherSet.m                                             93.62% (88/94)      
        -[MPResourceWatcherSet init]                                                                                                   100.00% (6/6)       
        -[MPResourceWatcherSet dealloc]                                                                                                0.00% (0/3)         
        -[MPResourceWatcherSet watchedPaths]                                                                                           100.00% (3/3)       
        -[MPResourceWatcherSet updateWatchedPaths:]                                                                                    100.00% (16/16)     
        -[MPResourceWatcherSet addWatcherForPath:]                                                                                     100.00% (32/32)     
        __42-[MPResourceWatcherSet addWatcherForPath:]_block_invoke                                                                    100.00% (6/6)       
        __42-[MPResourceWatcherSet addWatcherForPath:]_block_invoke.37                                                                 93.33% (14/15)      
        __42-[MPResourceWatcherSet addWatcherForPath:]_block_invoke_2                                                                  75.00% (6/8)        
        -[MPResourceWatcherSet stopAll]                                                                                                100.00% (5/5)       
    /Users/runner/work/macdown3000/macdown3000/MacDown/Code/Extension/hoedown_html_patch.c                                             86.47% (115/133)    
        hoedown_patch_reset_checkbox_index                                                                                             100.00% (3/3)       
        hoedown_patch_get_checkbox_index                                                                                               0.00% (0/3)         

... (3171 more lines truncated)

📊 **Full coverage report available in workflow artifacts**

schuyler added a commit that referenced this pull request Jun 22, 2026
…RL (#456)

## Summary

A **new** document previews fine, but **opening a saved `.md` file**
leaves the preview empty while the editor works. On macOS 26, WebKit
silently refuses to load the `file://` preview content when its *base
resource* is the real document file, based on metadata WebKit inspects
but the editor (which reads bytes via `NSDocument`) does not — e.g. the
execute bit set by sync clients like OneDrive (#431/#454), or stale
TCC/provenance state carried by a file's inode across its history
(#431). There's no signal in the document bytes, so there is no reliable
runtime detector.

This generalizes the previously execute-bit-only `previewSafeBaseURL:`
so the **real document file is never handed to WebKit as the base
resource**. Whenever the base URL points at a real file, a non-existent
sentinel in the *same directory* is substituted. The base URL is only
ever needed for the document's directory — relative resource resolution,
`MPLocalFilePathsInHTML`, cache busting, and the `MPURLSecurityPolicy`
scope check (which keys off the parent directory) — so all of that is
unchanged, while the broader blank-preview class is covered.

### Changes
- **`MPDocument.m`** — `previewSafeBaseURL:` now substitutes the
sentinel for any real file base URL (not just executable ones);
directories, non-existent paths, and non-file URLs pass through. Updated
the explanatory comment. Single-point change; both preview load paths
already route through this method.
- **`MPDocumentIOTests.m`** — unit tests for regular file, executable
file, directory, non-existent path, non-file URL, and nil.
- **`MPDocumentLifecycleTests.m`** — opened-file test asserting
`rendererBaseURL:` never returns the real document file when a `fileURL`
is set.
- **`CHANGELOG.md`** — updated the entry to describe the generalized
fix.

## Related Issue

Related to #405 (and the same root cause as #431).

## Manual Testing Plan

On macOS 26 with a sync client (or any file that previously rendered
blank on open):
1. Create a new document, type Markdown, confirm the preview renders.
2. Save it as a `.md` file, close the window, reopen the file → the
preview should now render (previously empty).
3. Set the execute bit on a saved `.md` file (`chmod +x file.md`) and
open it → preview should render.
4. Open a document that references a relative local image
(`[](image.png)` alongside the file) → the image should still load,
confirming relative resource resolution is unaffected by the sentinel
base URL.
5. Edit an opened document → the DOM-replacement fast path should keep
working (base URL is deterministic across renders).

## Review Notes

- The key assumption — relative resources still load with a non-existent
same-directory sentinel base URL — is already proven and shipped by #454
for executable files; this PR applies the same mechanism to all files.
- `MPURLSecurityPolicy.isExecutableOrAppBundleAtURL:` is still used by
the resource security checks, so it remains in use.
- WebView rendering can't be asserted in headless CI, so coverage is at
the base-URL / delegate level.

Co-authored-by: Claude <[email protected]>
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.

2 participants