Notepad3/grepWinNP3/sktoolslib_mod/PathUtils.cpp
2020-04-17 14:19:38 +02:00

713 lines
24 KiB
C++

// sktoolslib - common files for SK tools
// Copyright (C) 2013-2015, 2017 - 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 <vector>
#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.
const wchar_t ThisOSPathSeparator = L'\\';
const wchar_t OtherOSPathSeparator = L'/';
const 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);
}
}
std::wstring CPathUtils::GetLongPathname(const std::wstring& path)
{
if (path.empty())
return path;
TCHAR 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<TCHAR[]>(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<TCHAR[]>(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<TCHAR[]>(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<TCHAR[]>(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<TCHAR[]>(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)
{
if (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 no_parent;
size_t filenameLen;
size_t pathLen = path.length();
size_t pos;
for (pos = pathLen; pos > 0; )
{
--pos;
if (IsFolderSeparator(path[pos]))
{
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 no_parent;
// 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 no_parent;
// 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 no_parent;
++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 no_parent;
}
// 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 = 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 = 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<TCHAR[]>(len+1);
auto tempF = std::make_unique<TCHAR[]>(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((LPTSTR)(LPCTSTR)path.c_str(),&dwReserved);
dwReserved = 0;
if (dwBufferSize > 0)
{
auto pBuffer = std::make_unique<char[]>(dwBufferSize);
if (pBuffer)
{
UINT nInfoSize = 0,
nFixedLength = 0;
LPSTR lpVersion = nullptr;
VOID* lpFixedPointer;
TRANSARRAY* lpTransArray;
std::wstring strLangProduktVersion;
GetFileVersionInfo(path.c_str(),
dwReserved,
dwBufferSize,
pBuffer.get());
// Check the current language
VerQueryValue(pBuffer.get(),
L"\\VarFileInfo\\Translation",
&lpFixedPointer,
&nFixedLength);
lpTransArray = (TRANSARRAY*) lpFixedPointer;
strLangProduktVersion = CStringUtils::Format(L"\\StringFileInfo\\%04x%04x\\ProductVersion",
lpTransArray[0].wLanguageID, lpTransArray[0].wCharacterSet);
VerQueryValue(pBuffer.get(),
(LPTSTR)(LPCTSTR)strLangProduktVersion.c_str(),
(LPVOID *)&lpVersion,
&nInfoSize);
if (nInfoSize && lpVersion)
strReturn = (LPCTSTR)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<TCHAR[]>(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 = 0L;
Folder *pDestination = 0L;
long FilesCount = 0;
IDispatch* pItem = 0L;
FolderItems *pFilesInside = 0L;
VARIANT Options, OutFolder, InZipFile, Item;
HRESULT hr = S_OK;
CoInitialize(nullptr);
try
{
if (CoCreateInstance(CLSID_Shell, nullptr, CLSCTX_INPROC_SERVER, IID_IShellDispatch, (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, (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 = 0L;
pFilesInside->Release(); pFilesInside = 0L;
pDestination->Release(); pDestination = 0L;
pZippedFile->Release(); pZippedFile = 0L;
pISD->Release(); pISD = 0L;
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 = 0;
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
static class CPathTests
{
public:
CPathTests()
{
assert(CPathUtils::AdjustForMaxPath(L"c:\\") == 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