Skip to content

Fix hasExt() falsely detecting dots in directory path as file extensions - #18540

Open
softmgr wants to merge 2 commits into
notepad-plus-plus:masterfrom
softmgr:Fixes_hasExt
Open

softmgr wants to merge 2 commits into
notepad-plus-plus:masterfrom
softmgr:Fixes_hasExt

Conversation

@softmgr

@softmgr softmgr commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

Fixes #18539

Problem

In CustomFileDialog.cpp, hasExt() used name.find_last_of('.') != wstring::npos to determine whether the user-entered file name has an extension.

If the input is an absolute or relative path where a directory name contains a dot (e.g., C:\app.v1.0\test or ../folder.d/notes), hasExt() matched the dot inside the directory name and returned true. This prevented Notepad++ from automatically appending the default extension or updating the extension when switching file types in the dialog.

Solution

Updated hasExt() to compare the position of the last dot (.) against the last directory separator (\ or /). A valid extension is now only recognized if the dot occurs after the last path separator.

Verification

  • Tested saving without an extension into a folder without dots (C:\test\abc) -> .txt appended as expected.
  • Tested saving without an extension into a folder with dots (C:\test.1.0\abc) -> .txt now correctly appended (previously missing).
  • Tested saving an explicit extension into a folder with dots (C:\test.1.0\abc.log) -> .log preserved properly.
  • Tested multi-file selection and relative paths -> no regression.

@vlakoff

vlakoff commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

There is another helper function in CustomFileDialog.cpp that suffers from the exact same logic flaw: replaceExt().

Beyond just code consistency, fixing replaceExt() is actually even more critical. If a user enters a path where a directory contains a dot (e.g., C:\version.1.0\myfile) and changes the file type filter in the dialog, changeExt() calls replaceExt(). Because replaceExt() currently uses an unconstrained find_last_of('.'), it matches the dot inside the directory name (version.1.0) and truncates the entire path right there (C:\version), completely corrupting and destroying the file path before appending the new extension.

To prevent this serious path corruption bug, replaceExt() should be updated using the same directory separator check.

Here is a suggested implementation snippet for replaceExt():

bool replaceExt(wstring& name, const wstring& ext)
{
    if (!name.empty() && !ext.empty())
    {
        // Remove an existing extension from the name only if the dot is after the last path separator
        size_t posNameExt = name.find_last_of(L'.');
        if (posNameExt != wstring::npos)
        {
            size_t lastSlash = name.find_last_of(L"\\/");
            if (lastSlash == wstring::npos || posNameExt > lastSlash)
            {
                name.erase(posNameExt);
            }
        }
        // Append a new extension.
        name += ext;
        return true;
    }
    return false;
}

@chcg chcg added the bug label Oct 23, 2026
@softmgr

softmgr commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

@vlakoff
Good catch! Thank you for pointing this out.

You are completely right. replaceExt() suffered from the exact same assumption that any dot belongs to a file extension. When the directory path contained a dot (e.g., C:\version.1.0\myfile), replaceExt() truncated the string at .0, completely corrupting the path into C:\version.ext.

I have updated the PR to apply the same path-separator boundary check to replaceExt():

  • If a dot exists after the last path separator, it is treated as a file extension and replaced.
  • If a dot exists only within directory names (or if there is no dot at all), no existing characters are erased, and the new extension is simply appended.

Test Cases Verified:

  • C:\version.1.0\myfile + .txt -> C:\version.1.0\myfile.txt (Path preserved, extension appended)
  • C:\version.1.0\myfile.bak + .txt -> C:\version.1.0\myfile.txt (Correctly replaced .bak with .txt)
  • myfile.bak + .txt -> myfile.txt (Standard filename replaced)
  • myfile + .txt -> myfile.txt (Appended without truncation)

Improve extension removal logic to handle paths correctly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] CustomFileDialog: hasExt() incorrectly treats dots in directory path as file extension

4 participants