diff --git a/include/wx/filename.h b/include/wx/filename.h index 813d767b51..d0eb07b625 100644 --- a/include/wx/filename.h +++ b/include/wx/filename.h @@ -492,6 +492,10 @@ public: // is the char a path separator for this format? static bool IsPathSeparator(wxChar ch, wxPathFormat format = wxPATH_NATIVE); + // is this is a DOS path which begins with "\\?\"? + static bool IsMSWExtendedLengthPath(const wxString& path, + wxPathFormat format = wxPATH_NATIVE); + // is this is a DOS path which begins with a windows unique volume name // ('\\?\Volume{guid}\')? static bool IsMSWUniqueVolumeNamePath(const wxString& path, diff --git a/interface/wx/filename.h b/interface/wx/filename.h index 2f55e38375..379159a33d 100644 --- a/interface/wx/filename.h +++ b/interface/wx/filename.h @@ -870,8 +870,11 @@ public: - Just a single letter, for the usual drive letter volumes, e.g. @c C. - A share name preceded by a double backslash, e.g. `\\share`. - - A GUID volume preceded by a double backslash and a question mark, - e.g. `\\?\Volume{12345678-9abc-def0-1234-56789abcdef0}`. + - The first part of a so-called "Windows NT device" path, also called + "extended length" path, in the form of `\\.\X:` or a raw volume path, + e.g. `\\?\Volume{12345678-9abc-def0-1234-56789abcdef0}`. Such volumes + always start with a double backslash and a question mark. See also + IsMSWExtendedLengthPath(). */ wxString GetVolume() const; @@ -1033,6 +1036,20 @@ public: static bool IsPathSeparator(wxChar ch, wxPathFormat format = wxPATH_NATIVE); + /** + Returns @true if the path starts with a double backslash and a question + mark. + + Such paths are known as "Windows NT device" paths or "extended length" + paths and are passed directly to the file system, allowing to access + objects not accessible using the normal paths and avoiding the 260 + character path length restriction. + + @since 3.3.0 + */ + static bool IsMSWExtendedLengthPath(const wxString& path, + wxPathFormat format = wxPATH_NATIVE); + /** Returns @true if the volume part of the path is a unique volume name. diff --git a/src/common/filename.cpp b/src/common/filename.cpp index 2f1bed2596..df0d77dea6 100644 --- a/src/common/filename.cpp +++ b/src/common/filename.cpp @@ -134,8 +134,10 @@ namespace // private constants // ---------------------------------------------------------------------------- -// length of \\?\Volume{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}\ string -static const size_t wxMSWUniqueVolumePrefixLength = 49; +// Prefix of MSW extended-length paths. +static constexpr char wxMSW_EXTENDED_PATH_PREFIX[] = R"(\\?\)"; +static constexpr size_t + wxMSW_EXTENDED_PATH_PREFIX_LEN = WXSIZEOF(wxMSW_EXTENDED_PATH_PREFIX) - 1; // ---------------------------------------------------------------------------- // private classes @@ -300,14 +302,6 @@ static bool IsUNCPath(const wxString& path) !IsDOSPathSep(path[2u]); } -// return true if the string looks like a GUID volume path ("\\?\Volume{guid}\") -static bool IsVolumeGUIDPath(const wxString& path) -{ - return path.length() >= wxMSWUniqueVolumePrefixLength && - path.StartsWith(wxS("\\\\?\\Volume{")) && - path[wxMSWUniqueVolumePrefixLength - 1] == wxFILE_SEP_PATH_DOS; -} - // Under Unix-ish systems (basically everything except Windows but we can't // just test for non-__WIN32__ because Cygwin defines it, yet we want to use // lstat() under it, so test for all the rest explicitly) we may work either @@ -619,22 +613,44 @@ namespace void RemoveTrailingSeparatorsFromPath(wxString& strPath) { - // Windows fails to find directory named "c:\dir\" even if "c:\dir" exists, - // so remove all trailing backslashes from the path - but don't do this for - // the paths "d:\" (which are different from "d:"), for just "\" or for - // windows unique volume names ("\\?\Volume{GUID}\") - while ( wxEndsWithPathSeparator( strPath ) ) - { - size_t len = strPath.length(); - if ( len == 1 || (len == 3 && strPath[len - 2] == wxT(':')) || - (len == wxMSWUniqueVolumePrefixLength && - wxFileName::IsMSWUniqueVolumeNamePath(strPath))) - { - break; - } + // We should never have empty paths here, but skip them if we ever do. + if ( strPath.empty() ) + return; - strPath.Truncate(len - 1); + // Windows fails to find directory named "c:\dir\" even if "c:\dir" exists, + // so remove all trailing backslashes from the path - but don't do this if + // it is the last slash in the path to avoid turning "d:\" into "d:" (which + // is a different path), turning "\" into nothing or making extended length + // paths invalid. + const auto lastNonSeparator = strPath.find_last_not_of(R"(\/)"); + + const auto firstTrailingSeparator = + lastNonSeparator == wxString::npos ? 0 : lastNonSeparator + 1; + + if ( firstTrailingSeparator == strPath.length() ) + { + // The path doesn't end with a separator, nothing to do. + return; } + + // Check if there any separators would remain if we removed all trailing + // ones, ignoring those that are part of fixed wxMSW_EXTENDED_PATH_PREFIX + // or UNC path prefix. + const auto lastButOneSeparator = + strPath.find_last_of(R"(\/)", lastNonSeparator); + if ( lastButOneSeparator == wxString::npos || + (lastButOneSeparator == wxMSW_EXTENDED_PATH_PREFIX_LEN - 1 && + strPath.StartsWith(wxMSW_EXTENDED_PATH_PREFIX)) || + (lastButOneSeparator == 1 && IsUNCPath(strPath)) ) + { + // The path doesn't contain any other separators, so don't remove all + // of them. + strPath.erase(firstTrailingSeparator + 1); + return; + } + + // Remove all trailing separators. + strPath.erase(firstTrailingSeparator); } #endif // __WINDOWS_ @@ -1995,13 +2011,28 @@ bool wxFileName::IsPathSeparator(wxChar ch, wxPathFormat format) return ch != wxT('\0') && GetPathSeparators(format).Find(ch) != wxNOT_FOUND; } +/* static */ +bool +wxFileName::IsMSWExtendedLengthPath(const wxString& path, wxPathFormat format) +{ + return GetFormat(format) == wxPATH_DOS && + path.StartsWith(wxMSW_EXTENDED_PATH_PREFIX); +} + /* static */ bool wxFileName::IsMSWUniqueVolumeNamePath(const wxString& path, wxPathFormat format) { + // length of \\?\Volume{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}\ string + constexpr size_t wxMSWUniqueVolumePrefixLength = 49; + // return true if the format used is the DOS/Windows one and the string begins // with a Windows unique volume name ("\\?\Volume{guid}\") - return GetFormat(format) == wxPATH_DOS && IsVolumeGUIDPath(path); + return GetFormat(format) == wxPATH_DOS && + path.length() >= wxMSWUniqueVolumePrefixLength && + path.StartsWith(wxS("\\\\?\\Volume{")) && + path[wxMSWUniqueVolumePrefixLength - 1] == wxFILE_SEP_PATH_DOS; + } // ---------------------------------------------------------------------------- @@ -2367,18 +2398,33 @@ wxFileName::SplitVolume(const wxString& fullpath, switch ( format ) { case wxPATH_DOS: - // Deal with MSW UNC and volume GUID paths complications first. - if ( IsVolumeGUIDPath(fullpath) ) + // Deal with MSW complications first: first, the special case of + // extended-length paths. + if ( fullpath.StartsWith(wxMSW_EXTENDED_PATH_PREFIX) ) { + // Find the next path separator after this prefix. + // + // Note that such paths contain only backslashes, never slashes. + const auto posNextSep = + fullpath.find(wxFILE_SEP_PATH_DOS, + wxMSW_EXTENDED_PATH_PREFIX_LEN); + + // Note that this works even if posNextSep is npos. if ( pstrVolume ) - *pstrVolume = fullpath.Left(wxMSWUniqueVolumePrefixLength - 1); - - // Note: take the first slash here. - pathOnly = fullpath.Mid(wxMSWUniqueVolumePrefixLength - 1); + *pstrVolume = fullpath(0, posNextSep); + // Extended-length paths must have a backslash after the volume + // but if they ever don't, still pretend that there is one at + // the end because this is not going to be a normal path + // anyhow, so this seems like the least useless thing we can do. + if ( posNextSep != wxString::npos ) + pathOnly = fullpath.substr(posNextSep); + else + pathOnly = wxFILE_SEP_PATH_DOS; break; } + // Next check for UNC \\share\path syntax. if ( IsUNCPath(fullpath) ) { // Note that IsUNCPath() checks that 3rd character is not a diff --git a/tests/filename/filenametest.cpp b/tests/filename/filenametest.cpp index f7cb4ba324..328e3c5658 100644 --- a/tests/filename/filenametest.cpp +++ b/tests/filename/filenametest.cpp @@ -82,6 +82,7 @@ static struct TestFileNameInfo { "c:foo.bar", "c", "", "foo", "bar", false, wxPATH_DOS }, { "c:\\foo.bar", "c", "\\", "foo", "bar", true, wxPATH_DOS }, { "c:\\Windows\\command.com", "c", "\\Windows", "command", "com", true, wxPATH_DOS }, + { R"(\\?\c:\Windows\System32\cmd.exe)", R"(\\?\c:)", R"(\Windows\System32)", R"(cmd)", R"(exe)", true, wxPATH_DOS }, { "\\\\?\\Volume{8089d7d7-d0ac-11db-9dd0-806d6172696f}\\", "\\\\?\\Volume{8089d7d7-d0ac-11db-9dd0-806d6172696f}", "\\", "", "", true, wxPATH_DOS }, { "\\\\?\\Volume{8089d7d7-d0ac-11db-9dd0-806d6172696f}\\Program Files\\setup.exe",