diff --git a/PowerEditor/src/MISC/Common/Common.cpp b/PowerEditor/src/MISC/Common/Common.cpp index e18b215aa..a1030a862 100644 --- a/PowerEditor/src/MISC/Common/Common.cpp +++ b/PowerEditor/src/MISC/Common/Common.cpp @@ -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 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 { diff --git a/PowerEditor/src/MISC/Common/Common.h b/PowerEditor/src/MISC/Common/Common.h index 2c673a506..c2cf072c2 100644 --- a/PowerEditor/src/MISC/Common/Common.h +++ b/PowerEditor/src/MISC/Common/Common.h @@ -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: diff --git a/PowerEditor/src/NppIO.cpp b/PowerEditor/src/NppIO.cpp index ee5e8a537..6f458fc2a 100644 --- a/PowerEditor/src/NppIO.cpp +++ b/PowerEditor/src/NppIO.cpp @@ -28,6 +28,7 @@ #include "fileBrowser.h" #include #include +#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 { diff --git a/PowerEditor/src/ScintillaComponent/Buffer.cpp b/PowerEditor/src/ScintillaComponent/Buffer.cpp index a6beebaa2..1b3eb46b5 100644 --- a/PowerEditor/src/ScintillaComponent/Buffer.cpp +++ b/PowerEditor/src/ScintillaComponent/Buffer.cpp @@ -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))