Notepad3/grepWinNP3/sktoolslib_mod/PathUtils.cpp
2023-06-19 13:45:09 +02:00

716 lines
25 KiB
C++

// sktoolslib - common files for SK tools
// Copyright (C) 2013-2015, 2017, 2020-2023 - Stefan Kueng
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software Foundation,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// Useful path info is found here:
// http://msdn.microsoft.com/en-us/library/aa365247.aspx#fully_qualified_vs._relative_paths
#include "stdafx.h"
#include "PathUtils.h"
#include "StringUtils.h"
#include <memory>
#include <algorithm>
#include <memory>
#include <assert.h>
#include <Shlwapi.h>
#include <Shldisp.h>
#include <Shlobj.h>
#include <comutil.h>
#pragma comment(lib, "Shlwapi.lib")
#pragma comment(lib, "version.lib")
#pragma comment(lib, "comsuppw.lib")
// New code should probably use filesystem V3 when it is standard.
namespace
{
// These variables are not exposed as any path name handling probably
// should be a function in here rather than be manipulating strings directly / inline.
constexpr wchar_t thisOsPathSeparator = L'\\';
constexpr wchar_t otherOsPathSeparator = L'/';
constexpr wchar_t DeviceSeparator = L':';
// Check if the character given is either type of folder separator.
// if we want to remove support for "other"separators we can just
// change this function and force callers to use NormalizeFolderSeparators on
// filenames first at first point of entry into a program.
inline bool IsFolderSeparator(wchar_t c)
{
return (c == thisOsPathSeparator || c == otherOsPathSeparator);
}
} // namespace
std::wstring CPathUtils::GetLongPathname(const std::wstring& path)
{
if (path.empty())
return path;
wchar_t pathBufCanonicalized[MAX_PATH]; // MAX_PATH ok.
DWORD ret = 0;
std::wstring sRet = path;
if (!PathIsURL(path.c_str()) && PathIsRelative(path.c_str()))
{
ret = GetFullPathName(path.c_str(), 0, nullptr, nullptr);
if (ret)
{
auto pathBuf = std::make_unique<wchar_t[]>(ret + 1);
if ((ret = GetFullPathName(path.c_str(), ret, pathBuf.get(), nullptr)) != 0)
{
sRet = std::wstring(pathBuf.get(), ret);
}
}
}
else if (PathCanonicalize(pathBufCanonicalized, path.c_str()))
{
ret = ::GetLongPathName(pathBufCanonicalized, nullptr, 0);
auto pathBuf = std::make_unique<wchar_t[]>(ret + 2);
ret = ::GetLongPathName(pathBufCanonicalized, pathBuf.get(), ret + 1);
// GetFullPathName() sometimes returns the full path with the wrong
// case. This is not a problem on Windows since its filesystem is
// case-insensitive. But for SVN that's a problem if the wrong case
// is inside a working copy: the svn wc database is case sensitive.
// To fix the casing of the path, we use a trick:
// convert the path to its short form, then back to its long form.
// That will fix the wrong casing of the path.
int shortRet = ::GetShortPathName(pathBuf.get(), nullptr, 0);
if (shortRet)
{
auto shortPath = std::make_unique<wchar_t[]>(shortRet + 2);
if (::GetShortPathName(pathBuf.get(), shortPath.get(), shortRet + 1))
{
int ret2 = ::GetLongPathName(shortPath.get(), pathBuf.get(), ret + 1);
if (ret2)
sRet = std::wstring(pathBuf.get(), ret2);
}
}
}
else
{
ret = ::GetLongPathName(path.c_str(), nullptr, 0);
auto pathBuf = std::make_unique<wchar_t[]>(ret + 2);
ret = ::GetLongPathName(path.c_str(), pathBuf.get(), ret + 1);
sRet = std::wstring(pathBuf.get(), ret);
// fix the wrong casing of the path. See above for details.
int shortRet = ::GetShortPathName(pathBuf.get(), nullptr, 0);
if (shortRet)
{
auto shortPath = std::make_unique<wchar_t[]>(shortRet + 2);
if (::GetShortPathName(pathBuf.get(), shortPath.get(), shortRet + 1))
{
int ret2 = ::GetLongPathName(shortPath.get(), pathBuf.get(), ret + 1);
if (ret2)
sRet = std::wstring(pathBuf.get(), ret2);
}
}
}
if (ret == 0)
return path;
return sRet;
}
std::wstring CPathUtils::AdjustForMaxPath(const std::wstring& path, bool force)
{
if (!force && path.size() < 248) // 248 instead of MAX_PATH because 248 is the limit for directories
return path;
if (path.substr(0, 4).compare(L"\\\\?\\") == 0)
return path;
return L"\\\\?\\" + path;
}
// Return the parent directory for a given path.
// Note if the path is just a device like "c:"
// or a device and a root like "c:\"
// or a server name like "\\my_server"
// then there is no parent, so "" is returned.
std::wstring CPathUtils::GetParentDirectory(const std::wstring& path)
{
static std::wstring noParent;
size_t pathLen = path.length();
size_t pos;
for (pos = pathLen; pos > 0;)
{
--pos;
if (IsFolderSeparator(path[pos]))
{
size_t fileNameLen = pathLen - (pos + 1);
// If the path in it's entirety is just a root, i.e. "\", it has no parent.
if (pos == 0 && fileNameLen == 0)
return noParent;
// If the path in it's entirety is server name, i.e. "\\x", it has no parent.
if (pos == 1 && IsFolderSeparator(path[0]) && IsFolderSeparator(path[1]) && fileNameLen > 0)
return noParent;
// If the parent begins with a device and root, i.e. "?:\" then
// include both in the parent.
if (pos == 2 && path[pos - 1] == DeviceSeparator)
{
// If the path is just a device i.e. not followed by a filename, it has no parent.
if (fileNameLen == 0)
return noParent;
++pos;
}
// In summary, return everything before the last "\" of a filename unless the
// whole path given is:
// a server name, a device name, a root directory, or
// a device followed by a root directory, in which case return "".
std::wstring parent = path.substr(0, pos);
return parent;
}
}
// The path doesn't have a directory separator, we must be looking at either:
// 1. just a name, like "apple"
// 2. just a device, like "c:"
// 3. a device followed by a name "c:apple"
// 1. and 2. specifically have no parent,
// For 3. the parent is the device including the separator.
// We'll return just the separator if that's all there is.
// It's an odd corner case but allow it through so the caller
// yields an error if it uses it concatenated with another name rather
// than something that might work.
pos = path.find_first_of(DeviceSeparator);
if (pos != std::wstring::npos)
{
// A device followed by a path. The device is the parent.
std::wstring parent = path.substr(0, pos + 1);
return parent;
}
return noParent;
}
// Finds the last "." after the last path separator and returns
// everything after it, NOT including the ".".
// Handles leading folders with dots.
// Example, if given: "c:\product version 1.0\test.txt"
// returns: "txt"
std::wstring CPathUtils::GetFileExtension(const std::wstring& path)
{
// Find the last dot after the first path separator as
// folders can have dots in them too.
// Start at the last character and work back stopping at the
// first . or path separator. If we find a dot take the rest
// after it as the extension.
for (size_t i = path.length(); i > 0;)
{
--i;
if (IsFolderSeparator(path[i]))
break;
if (path[i] == L'.')
{
std::wstring ext = path.substr(i + 1);
return ext;
}
}
return std::wstring();
}
// Finds the first "." after the last path separator and returns
// everything after it, NOT including the ".".
// Handles leading folders with dots.
// Example, if given: "c:\product version 1.0\test.aspx.cs"
// returns: "aspx.cs"
std::wstring CPathUtils::GetLongFileExtension(const std::wstring& path)
{
// Find the last dot after the first path separator as
// folders can have dots in them too.
// Start at the last character and work back stopping at the
// first . or path separator. If we find a dot take the rest
// after it as the extension.
size_t foundPos = static_cast<size_t>(-1);
bool found = false;
for (size_t i = path.length(); i > 0;)
{
--i;
if (IsFolderSeparator(path[i]))
break;
if (path[i] == L'.')
{
foundPos = i;
found = true;
}
}
if (found && foundPos > 0)
{
std::wstring ext = path.substr(foundPos + 1);
return ext;
}
return std::wstring();
}
// Given "c:\folder\test.txt", yields "test.txt".
// Isn't tripped up by path names having both separators
// as can sometimes happen like c:\folder/test.txt
// Handles c:test.txt as can sometimes appear too.
std::wstring CPathUtils::GetFileName(const std::wstring& path)
{
bool gotSep = false;
size_t sepPos = 0;
for (size_t i = path.length(); i > 0;)
{
--i;
if (IsFolderSeparator(path[i]) || path[i] == DeviceSeparator)
{
gotSep = true;
sepPos = i;
break;
}
}
size_t nameStart = gotSep ? sepPos + 1 : 0;
size_t nameLen = path.length() - nameStart;
std::wstring name = path.substr(nameStart, nameLen);
return name;
}
// Returns only the filename without extension, i.e. will not include a path.
std::wstring CPathUtils::GetFileNameWithoutExtension(const std::wstring& path)
{
return RemoveExtension(GetFileName(path));
}
// Returns only the filename without extension, i.e. will not include a path.
std::wstring CPathUtils::GetFileNameWithoutLongExtension(const std::wstring& path)
{
return RemoveLongExtension(GetFileName(path));
}
// Finds the last "." after the last path separator and returns
// everything before it.
// Does not include the dot. Handles leading folders with dots.
// Example, if given: "c:\product version 1.0\test.txt"
// returns: "c:\product version 1.0\test"
std::wstring CPathUtils::RemoveExtension(const std::wstring& path)
{
for (size_t i = path.length(); i > 0;)
{
--i;
if (IsFolderSeparator(path[i]))
break;
if (path[i] == L'.')
return path.substr(0, i);
}
return path;
}
// Finds the first "." after the last path separator and returns
// everything before it, NOT including the ".".
// Handles leading folders with dots.
// Example, if given: "c:\product version 1.0\test.aspx.cs"
// returns: "aspx.cs"
std::wstring CPathUtils::RemoveLongExtension(const std::wstring& path)
{
// Find the last dot after the first path separator as
// folders can have dots in them too.
// Start at the last character and work back stopping at the
// first . or path separator. If we find a dot take the rest
// after it as the extension.
size_t foundPos = static_cast<size_t>(-1);
bool found = false;
for (size_t i = path.length(); i > 0;)
{
--i;
if (IsFolderSeparator(path[i]))
break;
if (path[i] == L'.')
{
foundPos = i;
found = true;
}
}
if (found && foundPos > 0)
{
std::wstring pathWithoutExt = path.substr(0, foundPos);
return pathWithoutExt;
}
return path;
}
std::wstring CPathUtils::GetModulePath(HMODULE hMod /*= nullptr*/)
{
DWORD len = 0;
DWORD bufferLen = MAX_PATH; // MAX_PATH is not the limit here!
std::unique_ptr<wchar_t[]> path;
do
{
bufferLen += MAX_PATH; // MAX_PATH is not the limit here!
path = std::make_unique<wchar_t[]>(bufferLen);
len = GetModuleFileName(hMod, path.get(), bufferLen);
} while (len == bufferLen);
std::wstring sPath = path.get();
return sPath;
}
std::wstring CPathUtils::GetModuleDir(HMODULE hMod /*= nullptr*/)
{
return GetParentDirectory(GetModulePath(hMod));
}
// Append one path onto another such that "path" + "append" = "path\append"
// Aims to conform to C++ <filesystem> semantics.
// e.g: "c:" + "append" = "c:\append" not "c:append"
// note: "c:append" breaks many Windows APIs
std::wstring CPathUtils::Append(const std::wstring& path, const std::wstring& append)
{
std::wstring newPath(path);
size_t pathLen = path.length();
size_t appendLen = append.length();
if (pathLen == 0)
newPath += append;
else if (IsFolderSeparator(path[pathLen - 1]))
newPath += append;
else if (appendLen > 0)
{
if (IsFolderSeparator(append[0]))
newPath += append;
else
{
newPath += thisOsPathSeparator;
newPath += append;
}
}
return newPath;
}
std::wstring CPathUtils::GetTempFilePath()
{
DWORD len = ::GetTempPath(0, nullptr);
auto tempPath = std::make_unique<wchar_t[]>(len + 1);
auto tempF = std::make_unique<wchar_t[]>(len + 50);
::GetTempPath(len + 1, tempPath.get());
std::wstring tempFile;
::GetTempFileName(tempPath.get(), TEXT("cm_"), 0, tempF.get());
tempFile = std::wstring(tempF.get());
//now create the tempfile, so that subsequent calls to GetTempFile() return
//different filenames.
HANDLE hFile = CreateFile(tempFile.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, nullptr);
CloseHandle(hFile);
return tempFile;
}
std::wstring CPathUtils::GetVersionFromFile(const std::wstring& path)
{
struct Transarray
{
WORD wLanguageID;
WORD wCharacterSet;
};
std::wstring strReturn;
DWORD dwReserved = 0;
DWORD dwBufferSize = GetFileVersionInfoSize(const_cast<LPWSTR>(path.c_str()), &dwReserved);
dwReserved = 0;
if (dwBufferSize > 0)
{
auto pBuffer = std::make_unique<char[]>(dwBufferSize);
if (pBuffer)
{
UINT nInfoSize = 0,
nFixedLength = 0;
LPCWSTR lpVersion = nullptr;
VOID* lpFixedPointer;
std::wstring strLangProductVersion;
GetFileVersionInfo(path.c_str(),
dwReserved,
dwBufferSize,
pBuffer.get());
// Check the current language
VerQueryValue(pBuffer.get(),
L"\\VarFileInfo\\Translation",
&lpFixedPointer,
&nFixedLength);
Transarray* lpTransArray = static_cast<Transarray*>(lpFixedPointer);
strLangProductVersion = CStringUtils::Format(L"\\StringFileInfo\\%04x%04x\\ProductVersion",
lpTransArray[0].wLanguageID, lpTransArray[0].wCharacterSet);
VerQueryValue(pBuffer.get(),
const_cast<LPWSTR>(strLangProductVersion.c_str()),
reinterpret_cast<LPVOID*>(const_cast<LPWSTR*>(&lpVersion)),
&nInfoSize);
if (nInfoSize && lpVersion)
strReturn = lpVersion;
}
}
return strReturn;
}
std::wstring CPathUtils::GetAppDataPath(HMODULE hMod)
{
PWSTR path = nullptr;
if (SHGetKnownFolderPath(FOLDERID_RoamingAppData, KF_FLAG_CREATE, nullptr, &path) == S_OK)
{
std::wstring sPath = path;
CoTaskMemFree(path);
sPath += L"\\";
sPath += CPathUtils::GetFileNameWithoutExtension(CPathUtils::GetModulePath(hMod));
CreateDirectory(sPath.c_str(), nullptr);
return sPath;
}
return CPathUtils::GetModuleDir(hMod);
}
std::wstring CPathUtils::GetCWD()
{
// Getting the CWD is a little more complicated than it seems.
// The directory can change between asking for the name size
// and obtaining the name value. So we need to handle that.
// We also need to handle any error the first or second time we ask.
for (;;)
{
// Returned length already includes + 1 fo null.
auto estimatedLen = GetCurrentDirectory(0, nullptr);
if (estimatedLen <= 0) // Error, can't recover.
break;
auto cwd = std::make_unique<wchar_t[]>(estimatedLen);
auto actualLen = GetCurrentDirectory(estimatedLen, cwd.get());
if (actualLen <= 0) // Error Can't recover
break;
// Directory changed in mean time and got larger..
if (actualLen <= estimatedLen)
return std::wstring(cwd.get(), actualLen);
// If we reach here, the directory has changed between us
// asking for it's size and obtaining the value and the
// the size has increased, so loop around to try again.
}
return std::wstring();
}
// Change the path separators to ones appropriate for this OS.
void CPathUtils::NormalizeFolderSeparators(std::wstring& path)
{
std::replace(path.begin(), path.end(), otherOsPathSeparator, thisOsPathSeparator);
}
// Path names are case insensitive, using this function is clearer
// that the string involved is a path.
// In theory it can be case insensitive or not as needed for the OS too.
int CPathUtils::PathCompare(const std::wstring& path1, const std::wstring& path2)
{
return _wcsicmp(path1.c_str(), path2.c_str());
}
int CPathUtils::PathCompareN(const std::wstring& path1, const std::wstring& path2, size_t limit)
{
return _wcsnicmp(path1.c_str(), path2.c_str(), limit);
}
bool CPathUtils::Unzip2Folder(LPCWSTR lpZipFile, LPCWSTR lpFolder)
{
IShellDispatch* pIsd;
Folder* pZippedFile = nullptr;
Folder* pDestination = nullptr;
long filesCount = 0;
IDispatch* pItem = nullptr;
FolderItems* pFilesInside = nullptr;
VARIANT options, outFolder, inZipFile, item;
HRESULT hr = S_OK;
CoInitialize(nullptr);
try
{
if (CoCreateInstance(CLSID_Shell, nullptr, CLSCTX_INPROC_SERVER, IID_IShellDispatch, reinterpret_cast<void**>(&pIsd)) != S_OK)
return false;
inZipFile.vt = VT_BSTR;
_bstr_t bstr = lpZipFile; // back reading
inZipFile.bstrVal = bstr.Detach();
hr = pIsd->NameSpace(inZipFile, &pZippedFile);
if (FAILED(hr) || !pZippedFile)
{
pIsd->Release();
return false;
}
outFolder.vt = VT_BSTR;
bstr = lpFolder; // back reading
outFolder.bstrVal = bstr.Detach();
pIsd->NameSpace(outFolder, &pDestination);
if (!pDestination)
{
pZippedFile->Release();
pIsd->Release();
return false;
}
pZippedFile->Items(&pFilesInside);
if (!pFilesInside)
{
pDestination->Release();
pZippedFile->Release();
pIsd->Release();
return false;
}
pFilesInside->get_Count(&filesCount);
if (filesCount < 1)
{
pFilesInside->Release();
pDestination->Release();
pZippedFile->Release();
pIsd->Release();
return true;
}
pFilesInside->QueryInterface(IID_IDispatch, reinterpret_cast<void**>(&pItem));
item.vt = VT_DISPATCH;
item.pdispVal = pItem;
options.vt = VT_I4;
options.lVal = 1024 | 512 | 16 | 4; //http://msdn.microsoft.com/en-us/library/bb787866(VS.85).aspx
bool retval = pDestination->CopyHere(item, options) == S_OK;
pItem->Release();
pItem = nullptr;
pFilesInside->Release();
pFilesInside = nullptr;
pDestination->Release();
pDestination = nullptr;
pZippedFile->Release();
pZippedFile = nullptr;
pIsd->Release();
pIsd = nullptr;
return retval;
}
catch (std::exception&)
{
CoUninitialize();
}
return false;
}
bool CPathUtils::IsKnownExtension(const std::wstring& ext)
{
// an extension is considered as 'known' if it's registered
// in the registry with an associated application.
if (ext.empty())
return false; // no extension, assume 'not known'
LPCWSTR sExt = ext.c_str();
std::wstring sDotExt;
if (ext[0] != '.')
{
sDotExt = L"." + ext;
sExt = sDotExt.c_str();
}
HKEY hKey = nullptr;
if (RegOpenKeyEx(HKEY_CLASSES_ROOT, sExt, 0, KEY_QUERY_VALUE, &hKey) == ERROR_SUCCESS)
{
// key exists
RegCloseKey(hKey);
return true;
}
return false;
}
bool CPathUtils::PathIsChild(const std::wstring& parent, const std::wstring& child)
{
std::wstring sParent = GetLongPathname(parent);
std::wstring sChild = GetLongPathname(child);
if (sParent.size() >= sChild.size())
return false;
NormalizeFolderSeparators(sParent);
NormalizeFolderSeparators(sChild);
std::wstring sChildAsParent = sChild.substr(0, sParent.size());
if (sChildAsParent.empty())
return false;
if (PathCompare(sParent, sChildAsParent) != 0)
return false;
if (IsFolderSeparator(*sParent.rbegin()))
{
if (!IsFolderSeparator(*sChildAsParent.rbegin()))
return false;
}
else
{
if (!IsFolderSeparator(sChild[sParent.size()]))
return false;
}
return true;
}
bool CPathUtils::IsPathRelative(const std::wstring& path)
{
return PathIsRelative(path.c_str()) ? true : false;
}
bool CPathUtils::CreateRecursiveDirectory(const std::wstring& path)
{
if (path.empty() || PathIsRoot(path.c_str()))
return false;
auto ret = CreateDirectory(path.c_str(), nullptr);
if (ret == FALSE)
{
if (GetLastError() == ERROR_PATH_NOT_FOUND)
{
if (CPathUtils::CreateRecursiveDirectory(CPathUtils::GetParentDirectory(path)))
{
// some file systems (e.g. webdav mounted drives) take time until
// a dir is properly created. So we try a few times with a wait in between
// to create the sub dir after just having created the parent dir.
int retryCount = 5;
do
{
ret = CreateDirectory(path.c_str(), nullptr);
if (ret == FALSE)
Sleep(50);
} while (retryCount-- && (ret == FALSE));
}
}
}
return ret != FALSE;
}
// poor mans code tests
#ifdef _DEBUG
[[maybe_unused]] static class CPathTests
{
public:
CPathTests()
{
assert(CPathUtils::AdjustForMaxPath(L"c:\\", false) == L"c:\\");
assert(CPathUtils::AdjustForMaxPath(L"c:\\abcdefghijklmnopqrstuvwxyz\\abcdefghijklmnopqrstuvwxyz\\abcdefghijklmnopqrstuvwxyz\\abcdefghijklmnopqrstuvwxyz\\abcdefghijklmnopqrstuvwxyz\\abcdefghijklmnopqrstuvwxyz\\abcdefghijklmnopqrstuvwxyz\\abcdefghijklmnopqrstuvwxyz\\abcdefghijklmnopqrstuvwxyz\\abcdefghijklmnopqrstuvwxyz") == L"\\\\?\\c:\\abcdefghijklmnopqrstuvwxyz\\abcdefghijklmnopqrstuvwxyz\\abcdefghijklmnopqrstuvwxyz\\abcdefghijklmnopqrstuvwxyz\\abcdefghijklmnopqrstuvwxyz\\abcdefghijklmnopqrstuvwxyz\\abcdefghijklmnopqrstuvwxyz\\abcdefghijklmnopqrstuvwxyz\\abcdefghijklmnopqrstuvwxyz\\abcdefghijklmnopqrstuvwxyz");
assert(CPathUtils::GetParentDirectory(L"c:\\windows\\system32") == L"c:\\windows");
assert(CPathUtils::GetParentDirectory(L"c:\\") == L"");
assert(CPathUtils::GetParentDirectory(L"\\myserver") == L"");
assert(CPathUtils::GetFileExtension(L"d:\\test.file.ext1.ext2") == L"ext2");
assert(CPathUtils::GetLongFileExtension(L"d:\\test.file.ext1.ext2") == L"file.ext1.ext2");
assert(!CPathUtils::PathIsChild(L"c:\\windows\\", L"c:\\windows"));
assert(!CPathUtils::PathIsChild(L"c:\\windows\\", L"c:\\windows\\"));
assert(CPathUtils::PathIsChild(L"c:\\windows\\", L"c:\\windows\\child\\"));
assert(CPathUtils::PathIsChild(L"c:\\windows\\", L"c:\\windows\\child"));
assert(CPathUtils::PathIsChild(L"c:\\windows", L"c:\\windows\\child"));
assert(!CPathUtils::PathIsChild(L"c:\\windows", L"c:\\windowsnotachild"));
assert(!CPathUtils::PathIsChild(L"c:\\windows\\", L"c:\\windowsnotachild"));
}
} cPathTests;
#endif