Fix crash on Win32 Namespace prefixed file name

Implement support for Win32 Namespace prefixed file name in Notepad++.
(Ref: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces)

Support the Win32-filenames escaped by \\?\ or \\?\UNC\, possible globbing in filenames (\\?\C:\fil?.txt) and shell links (\\?\C:\file.txt.lnk) included.

Unsupported (temporarily - it needs further patches for Notepad++):
- any raw filename with length exceeding the MAX_PATH.
- any nonstandard Windows OS filename: with 'dot' or 'space' char(s) at the name end, WinOS reserved ones: AUX, CON, PRN, NUL, COM1-9, LPT1-9 and the ones with invalid ASCII chars in it (0-31, <, >, | , ").

Fix #12453, close #12613
pull/12641/head
xomx 2022-12-07 23:40:03 +01:00 committed by Don Ho
parent ee336b24c1
commit 4f1aa7b004
4 changed files with 202 additions and 23 deletions

View File

@ -1519,6 +1519,109 @@ HFONT createFont(const TCHAR* fontName, int fontSize, bool isBold, HWND hDestPar
return newFont;
}
// "For file I/O, the "\\?\" prefix to a path string tells the Windows APIs to disable all string parsing
// and to send the string that follows it straight to the file system..."
// Ref: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces
bool isWin32NamespacePrefixedFileName(const generic_string& fileName)
{
// TODO:
// ?! how to handle similar NT Object Manager path style prefix case \??\...
// (the \??\ prefix instructs the NT Object Manager to search in the caller's local device directory for an alias...)
// the following covers the \\?\... raw Win32-filenames or the \\?\UNC\... UNC equivalents
// and also its *nix like forward slash equivalents
return (fileName.starts_with(TEXT("\\\\?\\")) || fileName.starts_with(TEXT("//?/")));
}
bool isWin32NamespacePrefixedFileName(const TCHAR* szFileName)
{
const generic_string fileName = szFileName;
return isWin32NamespacePrefixedFileName(fileName);
}
bool isUnsupportedFileName(const generic_string& fileName)
{
bool isUnsupported = true;
// until the N++ (and its plugins) will not be prepared for filenames longer than the MAX_PATH,
// we have to limit also the maximum supported length below
if ((fileName.size() > 0) && (fileName.size() < MAX_PATH))
{
// possible raw filenames can contain space(s) or dot(s) at its end (e.g. "\\?\C:\file."), but the N++ advanced
// Open/SaveAs IFileOpenDialog/IFileSaveDialog COM-interface based dialogs currently do not handle this well
// (but e.g. direct N++ Ctrl+S works ok even with these filenames)
if (!fileName.ends_with(_T('.')) && !fileName.ends_with(_T(' ')))
{
bool invalidASCIIChar = false;
for (size_t pos = 0; pos < fileName.size(); ++pos)
{
TCHAR c = fileName.at(pos);
if (c <= 31)
{
invalidASCIIChar = true;
}
else
{
// as this could be also a complete filename with path and there could be also a globbing used,
// we tolerate here some other reserved Win32-filename chars: /, \, :, ?, *
switch (c)
{
case '<':
case '>':
case '"':
case '|':
invalidASCIIChar = true;
break;
}
}
if (invalidASCIIChar)
break;
}
if (!invalidASCIIChar)
{
// strip input string to a filename without a possible path and extension(s)
generic_string fileNameOnly;
size_t pos = fileName.find_first_of(TEXT("."));
if (pos != std::string::npos)
fileNameOnly = fileName.substr(0, pos);
else
fileNameOnly = fileName;
pos = fileNameOnly.find_last_of(TEXT("\\"));
if (pos == std::string::npos)
pos = fileNameOnly.find_last_of(TEXT("/"));
if (pos != std::string::npos)
fileNameOnly = fileNameOnly.substr(pos + 1);
const std::vector<generic_string> reservedWin32NamespaceDeviceList{
TEXT("CON"), TEXT("PRN"), TEXT("AUX"), TEXT("NUL"),
TEXT("COM1"), TEXT("COM2"), TEXT("COM3"), TEXT("COM4"), TEXT("COM5"), TEXT("COM6"), TEXT("COM7"), TEXT("COM8"), TEXT("COM9"),
TEXT("LPT1"), TEXT("LPT2"), TEXT("LPT3"), TEXT("LPT4"), TEXT("LPT5"), TEXT("LPT6"), TEXT("LPT7"), TEXT("LPT8"), TEXT("LPT9")
};
// last check is for all the old reserved Windows OS filenames
if (std::find(reservedWin32NamespaceDeviceList.begin(), reservedWin32NamespaceDeviceList.end(), fileNameOnly) == reservedWin32NamespaceDeviceList.end())
{
// ok, the current filename tested is not even on the blacklist
isUnsupported = false;
}
}
}
}
return isUnsupported;
}
bool isUnsupportedFileName(const TCHAR* szFileName)
{
const generic_string fileName = szFileName;
return isUnsupportedFileName(fileName);
}
Version::Version(const generic_string& versionStr)
{
try {

View File

@ -232,6 +232,11 @@ generic_string getDateTimeStrFrom(const generic_string& dateTimeFormat, const SY
HFONT createFont(const TCHAR* fontName, int fontSize, bool isBold, HWND hDestParent);
bool isWin32NamespacePrefixedFileName(const generic_string& fileName);
bool isWin32NamespacePrefixedFileName(const TCHAR* szFileName);
bool isUnsupportedFileName(const generic_string& fileName);
bool isUnsupportedFileName(const TCHAR* szFileName);
class Version final
{
public:

View File

@ -28,6 +28,7 @@
#include "fileBrowser.h"
#include <tchar.h>
#include <unordered_set>
#include "Common.h"
using namespace std;
@ -120,8 +121,10 @@ DWORD WINAPI Notepad_plus::monitorFileOnChange(void * params)
return EXIT_SUCCESS;
}
void resolveLinkFile(generic_string& linkFilePath)
bool resolveLinkFile(generic_string& linkFilePath)
{
bool isResolved = false;
IShellLink* psl;
WCHAR targetFilePath[MAX_PATH];
WIN32_FIND_DATA wfd = {};
@ -149,6 +152,7 @@ void resolveLinkFile(generic_string& linkFilePath)
if (SUCCEEDED(hres) && hres != S_FALSE)
{
linkFilePath = targetFilePath;
isResolved = true;
}
}
}
@ -158,6 +162,8 @@ void resolveLinkFile(generic_string& linkFilePath)
}
CoUninitialize();
}
return isResolved;
}
BufferID Notepad_plus::doOpen(const generic_string& fileName, bool isRecursive, bool isReadOnly, int encoding, const TCHAR *backupFileName, FILETIME fileNameTimestamp)
@ -167,30 +173,75 @@ BufferID Notepad_plus::doOpen(const generic_string& fileName, bool isRecursive,
return BUFFER_INVALID;
generic_string targetFileName = fileName;
resolveLinkFile(targetFileName);
bool isResolvedLinkFileName = resolveLinkFile(targetFileName);
bool isRawFileName;
if (isResolvedLinkFileName)
isRawFileName = false;
else
isRawFileName = isWin32NamespacePrefixedFileName(fileName);
if (isUnsupportedFileName(isResolvedLinkFileName ? targetFileName : fileName))
{
// TODO:
// for the raw filenames we can allow even the usually unsupported filenames in the future,
// but not now as it is not fully supported by the N++ COM IFileDialog based Open/SaveAs dialogs
//if (isRawFileName)
//{
// int answer = _nativeLangSpeaker.messageBox("OpenNonconformingWin32FileName",
// _pPublicInterface->getHSelf(),
// TEXT("You are about to open a file with unusual filename:\n\"$STR_REPLACE$\""),
// TEXT("Open Nonconforming Win32-Filename"),
// MB_OKCANCEL | MB_ICONWARNING | MB_APPLMODAL,
// 0,
// isResolvedLinkFileName ? targetFileName.c_str() : fileName.c_str());
// if (answer != IDOK)
// return BUFFER_INVALID; // aborted by user
//}
//else
//{
// unsupported, use the existing N++ file dialog to report
_nativeLangSpeaker.messageBox("OpenFileError",
_pPublicInterface->getHSelf(),
TEXT("Cannot open file \"$STR_REPLACE$\"."),
TEXT("ERROR"),
MB_OK,
0,
isResolvedLinkFileName ? targetFileName.c_str() : fileName.c_str());
return BUFFER_INVALID;
//}
}
//If [GetFullPathName] succeeds, the return value is the length, in TCHARs, of the string copied to lpBuffer, not including the terminating null character.
//If the lpBuffer buffer is too small to contain the path, the return value [of GetFullPathName] is the size, in TCHARs, of the buffer that is required to hold the path and the terminating null character.
//If [GetFullPathName] fails for any other reason, the return value is zero.
NppParameters& nppParam = NppParameters::getInstance();
TCHAR longFileName[longFileNameBufferSize];
WCHAR longFileName[longFileNameBufferSize] = { 0 };
const DWORD getFullPathNameResult = ::GetFullPathName(targetFileName.c_str(), longFileNameBufferSize, longFileName, NULL);
if (getFullPathNameResult == 0)
if (isRawFileName)
{
return BUFFER_INVALID;
// use directly the raw file name, skip the GetFullPathName WINAPI and alike...)
wcsncpy_s(longFileName, _countof(longFileName), fileName.c_str(), _TRUNCATE);
}
if (getFullPathNameResult > longFileNameBufferSize)
else
{
return BUFFER_INVALID;
}
assert(_tcslen(longFileName) == getFullPathNameResult);
const DWORD getFullPathNameResult = ::GetFullPathName(targetFileName.c_str(), longFileNameBufferSize, longFileName, NULL);
if (getFullPathNameResult == 0)
{
return BUFFER_INVALID;
}
if (getFullPathNameResult > longFileNameBufferSize)
{
return BUFFER_INVALID;
}
assert(wcslen(longFileName) == getFullPathNameResult);
if (_tcschr(longFileName, '~'))
{
// ignore the returned value of function due to win64 redirection system
::GetLongPathName(longFileName, longFileName, longFileNameBufferSize);
if (wcschr(longFileName, '~'))
{
// ignore the returned value of function due to win64 redirection system
::GetLongPathName(longFileName, longFileName, longFileNameBufferSize);
}
}
bool isSnapshotMode = backupFileName != NULL && PathFileExists(backupFileName);
@ -257,7 +308,11 @@ BufferID Notepad_plus::doOpen(const generic_string& fileName, bool isRecursive,
isWow64Off = true;
}
bool globbing = wcsrchr(longFileName, TCHAR('*')) || wcsrchr(longFileName, TCHAR('?'));
bool globbing;
if (isRawFileName)
globbing = (wcsrchr(longFileName, TCHAR('*')) || (abs(longFileName - wcsrchr(longFileName, TCHAR('?'))) > 3));
else
globbing = (wcsrchr(longFileName, TCHAR('*')) || wcsrchr(longFileName, TCHAR('?')));
if (!isSnapshotMode) // if not backup mode, or backupfile path is invalid
{

View File

@ -717,11 +717,19 @@ BufferID FileManager::loadFile(const TCHAR* filename, Document doc, int encoding
ownDoc = true;
}
TCHAR fullpath[MAX_PATH];
::GetFullPathName(filename, MAX_PATH, fullpath, NULL);
if (_tcschr(fullpath, '~'))
WCHAR fullpath[MAX_PATH] = { 0 };
if (isWin32NamespacePrefixedFileName(filename))
{
::GetLongPathName(fullpath, fullpath, MAX_PATH);
// use directly the raw file name, skip the GetFullPathName WINAPI
wcsncpy_s(fullpath, _countof(fullpath), filename, _TRUNCATE);
}
else
{
::GetFullPathName(filename, MAX_PATH, fullpath, NULL);
if (wcschr(fullpath, '~'))
{
::GetLongPathName(fullpath, fullpath, MAX_PATH);
}
}
bool isSnapshotMode = backupFileName != NULL && PathFileExists(backupFileName);
@ -1116,11 +1124,19 @@ SavingStatus FileManager::saveBuffer(BufferID id, const TCHAR * filename, bool i
bool isHiddenOrSys = false;
DWORD attrib = 0;
TCHAR fullpath[MAX_PATH];
::GetFullPathName(filename, MAX_PATH, fullpath, NULL);
if (_tcschr(fullpath, '~'))
WCHAR fullpath[MAX_PATH] = { 0 };
if (isWin32NamespacePrefixedFileName(filename))
{
::GetLongPathName(fullpath, fullpath, MAX_PATH);
// use directly the raw file name, skip the GetFullPathName WINAPI
wcsncpy_s(fullpath, _countof(fullpath), filename, _TRUNCATE);
}
else
{
::GetFullPathName(filename, MAX_PATH, fullpath, NULL);
if (wcschr(fullpath, '~'))
{
::GetLongPathName(fullpath, fullpath, MAX_PATH);
}
}
if (PathFileExists(fullpath))