Skip to content

Extension List exact matching fails when one extension is a prefix of another (e.g. .ps1 vs .ps1xml) #231

@vicomannen

Description

@vicomannen

Description

When using Match File → Extension (exact extension list), the menu is not shown if one extension is a prefix of another extension that appears earlier in the list.

Example: .ps1 fails to match when .ps1xml exists before it in AcceptExts.

Steps to reproduce

Create a custom menu item

Enable Match File

Set mode to Extension

Set AcceptExts to:

.ps1xml|.ps1

Right-click a .ps1 file

Expected behavior

The menu should be shown for .ps1 files, since .ps1 is explicitly listed.

Actual behavior

The menu is not shown for .ps1 files.

Minimal reproduction (logic simulation)

This reproduces the exact behavior in PowerShell, mimicking the current C++ logic:

function Test-CCM_ExtensionList {
param(
[string] $AcceptExts,
[string] $Ext
)

$pos = $AcceptExts.IndexOf($Ext)
if ($pos -lt 0) { return $false }

$isStart = ($pos -eq 0)
$isEnd   = ($pos + $Ext.Length -eq $AcceptExts.Length)

$prevOk = (-not $isStart) -and ($AcceptExts[$pos - 1] -eq '|')
$nextOk = (-not $isEnd)   -and ($AcceptExts[$pos + $Ext.Length] -eq '|')

return (($isStart -or $prevOk) -and ($isEnd -or $nextOk))

}

Test-CCM_ExtensionList ".ps1xml|.ps1" ".ps1" # False (bug)
Test-CCM_ExtensionList ".ps1|.ps1xml" ".ps1" # True

Root cause (analysis)

In CustomSubExplorerCommand::Accept(...), the Extension List logic:

Uses find(ext) to locate the first occurrence of the extension substring

Performs boundary checks (| before/after)

Returns false immediately if the first match fails

Does not search for subsequent valid matches

As a result, .ps1 matches the substring inside .ps1xml first, fails the boundary check, and the function exits without checking the correct .ps1 token later in the list.

Workaround

Reorder the list so shorter extensions appear before longer ones:

.ps1|.ps1xml

Proposed fixes
✅ Option A (recommended – simplest & most robust)

Tokenize the extension list and compare tokens exactly:

// Pseudocode
split acceptExts by '|'
trim whitespace
lowercase tokens and ext
if (token == ext) return true

Benefits:

True exact matching

No prefix issues

Robust against whitespace and case

Simpler logic

🔧 Option B (minimal patch)

Keep current logic, but iterate over all occurrences:

size_t pos = 0;
while ((pos = acceptExts.find(ext, pos)) != std::wstring::npos) {
bool isStart = (pos == 0);
bool isEnd = (pos + ext.size() == acceptExts.size());

bool prevOk = isStart || acceptExts[pos - 1] == L'|';
bool nextOk = isEnd   || acceptExts[pos + ext.size()] == L'|';

if (prevOk && nextOk) return true;
pos += 1;

}
return false;

Additional suggestions

Normalize acceptExts to lowercase before matching (currently ext is lowercased but acceptExts is not)

Trim whitespace around tokens

Notes

“Extension Like” works as expected since it uses substring matching

This issue only affects Extension (exact) mode

The bug becomes more likely with long extension lists

👍 Happy to help test or validate a fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions