Skip to content

Guard nil-group and out-of-bounds index in selectedToolbarItemGroupItem:#396

Merged
schuyler merged 3 commits into
mainfrom
claude/issue-394-toolbar-nil-guard-bUsbp
Apr 21, 2026
Merged

Guard nil-group and out-of-bounds index in selectedToolbarItemGroupItem:#396
schuyler merged 3 commits into
mainfrom
claude/issue-394-toolbar-nil-guard-bUsbp

Conversation

@schuyler

@schuyler schuyler commented Apr 21, 2026

Copy link
Copy Markdown
Owner

Summary

Adds nil-group and out-of-bounds guards to selectedToolbarItemGroupItem: in MPToolbarController.m.

The method previously did selectedGroup.subitems[selectedIndex] with no protection against:

  • sender.identifier not being present in toolbarItemIdentifierObjectDictionary (selectedGroup becomes nil)
  • selectedIndex being negative or >= selectedGroup.subitems.count (NSRangeException from objectAtIndexedSubscript:)

The bounds case is a latent NSRangeException crash. The nil case is benign today thanks to ObjC nil-messaging semantics, but is a correctness hazard if the downstream guards ever change.

Both paths now use NSAssert + an early return:

  • In Debug (CI default, ENABLE_NS_ASSERTIONS = YES) the asserts fire loudly with a message that names the offending identifier and the actual vs expected subitem count, so misconfigurations are diagnosed immediately.
  • In Release (ENABLE_NS_ASSERTIONS = NO) the asserts compile away and the if (…) return; keeps users out of the crash path.

Test coverage

Four new tests in MPToolbarControllerTests.m:

  • testSelectedToolbarItemGroupItemUnknownIdentifierAsserts
  • testSelectedToolbarItemGroupItemOutOfBoundsIndexAsserts
  • testSelectedToolbarItemGroupItemNegativeIndexAsserts (with a precondition check that NSSegmentedControl actually stores -1)
  • testSelectedToolbarItemGroupItemValidCallDoesNotCrash (regression guard for the happy path)

The three guard tests use XCTAssertThrowsSpecificNamed(…, NSInternalInconsistencyException) because the test target is built in Debug. A private category declaration exposes selectedToolbarItemGroupItem: to the test target so the call compiles without -Wundeclared-selector.

The Release-mode if … return; branches are defense-in-depth and not directly reachable under Debug; this is noted in a short comment above the new test section.

Related Issue

Related to #394

Manual Testing Plan

N/A — the XCTest suite covers the testable surface, and the guards only fire on a misconfigured toolbar that can't be produced through the normal UI (segmented controls are constructed in toolbarItemGroupWithIdentifier: with matching identifier and segmentCount == items.count, so the fault conditions are structurally impossible from user interaction).

claude added 3 commits April 21, 2026 04:49
…edToolbarItemGroupItem:

selectedToolbarItemGroupItem: previously did `selectedGroup.subitems[selectedIndex]`
with no protection against `selectedGroup` being nil (identifier missing from the
registration dictionary) or `selectedIndex` being out of range for subitems. The
bounds case is a latent NSRangeException crash; the nil case is benign today due
to ObjC nil-messaging but is a correctness hazard if any caller stops short-
circuiting on a nil item.

Add NSAssert + early-return guards for both cases. In Debug the asserts fire loudly
so misconfigurations are caught; in Release they compile away and the early return
protects users from the crash.

Also adds four XCTest cases: unknown identifier, out-of-bounds index, negative
index, and a valid-call regression guard.

Related to #394
- Declare selectedToolbarItemGroupItem: via a private category in the test
  file so the call compiles without -Wundeclared-selector
- Replace 99/50 magic numbers in the out-of-bounds test with 3/2 (one past
  the group's 2 subitems)
- Add a precondition check in the negative-index test to confirm AppKit
  doesn't clamp sender.selectedSegment = -1

Related to #394
@github-actions

github-actions Bot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Report

Current Coverage: 61.12%

Coverage Details (Summary)
Name                                                                                                                                   Coverage            
-------------------------------------------------------------------------------------------------------------------------------------- ------------------- 
MASPreferences.bundle                                                                                                                  0.00% (0/0)         
MacDown 3000.app                                                                                                                       62.07% (8793/14167) 
    /Users/runner/work/macdown3000/macdown3000/MacDown/Code/Extension/NSColor+HTML.m                                                   94.07% (333/354)    
        +[NSColor(HTML) colorWithHexString:]                                                                                           94.12% (16/17)      
        +[NSColor(HTML) colorWithHTMLName:]                                                                                            89.13% (164/184)    
        __35+[NSColor(HTML) colorWithHTMLName:]_block_invoke                                                                           100.00% (153/153)   
    /Users/runner/work/macdown3000/macdown3000/MacDown/Code/Document/MPAsset.m                                                         92.86% (104/112)    
        -[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/7)         
        -[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                                                  27.37% (26/95)      
        +[FileURLInlining inlineFromIterable:]                                                                                         0.00% (0/11)        
        -[FileURLInlining initWithURL:]                                                                                                0.00% (0/6)         
        -[FileURLInlining init]                                                                                                        100.00% (4/4)       
        -[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.5                                                                  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                                             85.93% (116/135)    
        hoedown_patch_reset_checkbox_index                                                                                             100.00% (3/3)       
        hoedown_patch_get_checkbox_index                                                                                               0.00% (0/3)         

... (3161 more lines truncated)

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

@schuyler

Copy link
Copy Markdown
Owner Author

thanks @yusufm as we caught this one while reviewing #393

@schuyler schuyler merged commit 2e4031b into main Apr 21, 2026
10 checks passed
@schuyler schuyler deleted the claude/issue-394-toolbar-nil-guard-bUsbp branch April 21, 2026 05:08
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