309 lines
8.9 KiB
C++
309 lines
8.9 KiB
C++
#include "shellprv.h"
|
|
#include "ids.h"
|
|
#pragma hdrstop
|
|
|
|
#include "isproc.h"
|
|
|
|
// eventually expand this to do rename UI, right now it just picks a default name
|
|
// in the destination namespace
|
|
|
|
HRESULT QIThroughShellItem(IShellItem *psi, REFIID riid, void **ppv)
|
|
{
|
|
// todo: put this into shellitem
|
|
*ppv = NULL;
|
|
|
|
IShellFolder *psf;
|
|
HRESULT hr = psi->BindToHandler(NULL, BHID_SFObject, IID_PPV_ARG(IShellFolder, &psf));
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
hr = psf->QueryInterface(riid, ppv);
|
|
psf->Release();
|
|
}
|
|
return hr;
|
|
}
|
|
|
|
BOOL IsValidChar(WCHAR chTest, LPWSTR pszValid, LPWSTR pszInvalid)
|
|
{
|
|
return (!pszValid || StrChr(pszValid, chTest)) &&
|
|
(!pszInvalid || !StrChr(pszInvalid, chTest));
|
|
}
|
|
|
|
const WCHAR c_rgSubstitutes[] = { '_', ' ', '~' };
|
|
WCHAR GetValidSubstitute(LPWSTR pszValid, LPWSTR pszInvalid)
|
|
{
|
|
for (int i = 0; i < ARRAYSIZE(c_rgSubstitutes); i++)
|
|
{
|
|
if (IsValidChar(c_rgSubstitutes[i], pszValid, pszInvalid))
|
|
{
|
|
return c_rgSubstitutes[i];
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
HRESULT CheckCharsAndReplaceIfNecessary(IItemNameLimits *pinl, LPWSTR psz)
|
|
{
|
|
// returns S_OK if no chars replaced, S_FALSE otherwise
|
|
HRESULT hr = S_OK;
|
|
LPWSTR pszValid, pszInvalid;
|
|
if (SUCCEEDED(pinl->GetValidCharacters(&pszValid, &pszInvalid)))
|
|
{
|
|
WCHAR chSubs = GetValidSubstitute(pszValid, pszInvalid);
|
|
|
|
int iSrc = 0, iDest = 0;
|
|
while (psz[iSrc] != 0)
|
|
{
|
|
if (IsValidChar(psz[iSrc], pszValid, pszInvalid))
|
|
{
|
|
// use the char itself if it's valid
|
|
psz[iDest] = psz[iSrc];
|
|
iDest++;
|
|
}
|
|
else
|
|
{
|
|
// mark that we replaced a char
|
|
hr = S_FALSE;
|
|
if (chSubs)
|
|
{
|
|
// use a substitute if available
|
|
psz[iDest] = chSubs;
|
|
iDest++;
|
|
}
|
|
// else no valid char, just skip it
|
|
}
|
|
iSrc++;
|
|
}
|
|
psz[iDest] = 0;
|
|
|
|
if (pszValid)
|
|
CoTaskMemFree(pszValid);
|
|
if (pszInvalid)
|
|
CoTaskMemFree(pszInvalid);
|
|
}
|
|
return hr;
|
|
}
|
|
|
|
HRESULT BreakOutString(LPCWSTR psz, LPWSTR *ppszFilespec, LPWSTR *ppszExt)
|
|
{
|
|
// todo: detect the (2) in "New Text Document (2).txt" and reduce the filespec
|
|
// accordingly to prevent "(1) (1)" etc. in multiple copies
|
|
|
|
*ppszFilespec = NULL;
|
|
*ppszExt = NULL;
|
|
|
|
LPWSTR pszExt = PathFindExtension(psz);
|
|
// make an empty string if necessary. this makes our logic simpler later instead of having to
|
|
// handle the special case all the time.
|
|
HRESULT hr = SHStrDup(pszExt ? pszExt : L"", ppszExt);
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
int iLenExt = lstrlen(*ppszExt);
|
|
int cchBufFilespec = lstrlen(psz) - iLenExt + 1;
|
|
*ppszFilespec = (LPWSTR)CoTaskMemAlloc(cchBufFilespec * sizeof(WCHAR));
|
|
if (*ppszFilespec)
|
|
{
|
|
lstrcpyn(*ppszFilespec, psz, cchBufFilespec);
|
|
hr = S_OK;
|
|
}
|
|
else
|
|
{
|
|
hr = E_OUTOFMEMORY;
|
|
}
|
|
}
|
|
|
|
if (FAILED(hr) && *ppszExt)
|
|
{
|
|
CoTaskMemFree(*ppszExt);
|
|
*ppszExt = NULL;
|
|
}
|
|
ASSERT((SUCCEEDED(hr) && *ppszFilespec && *ppszExt) || (FAILED(hr) && !*ppszFilespec && !*ppszExt));
|
|
return hr;
|
|
}
|
|
|
|
BOOL ItemExists(LPCWSTR pszName, IShellItem *psiDest)
|
|
{
|
|
BOOL fRet = FALSE;
|
|
|
|
IBindCtx *pbc;
|
|
HRESULT hr = BindCtx_CreateWithMode(STGX_MODE_READ, &pbc);
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
ITransferDest *pitd;
|
|
hr = psiDest->BindToHandler(pbc, BHID_SFObject, IID_PPV_ARG(ITransferDest, &pitd));
|
|
if (FAILED(hr))
|
|
{
|
|
hr = CreateStg2StgExWrapper(psiDest, NULL, &pitd);
|
|
}
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
DWORD dwDummy;
|
|
IUnknown *punk;
|
|
hr = pitd->OpenElement(pszName, STGX_MODE_READ, &dwDummy, IID_PPV_ARG(IUnknown, &punk));
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
fRet = TRUE;
|
|
punk->Release();
|
|
}
|
|
pitd->Release();
|
|
}
|
|
pbc->Release();
|
|
}
|
|
return fRet;
|
|
}
|
|
|
|
HRESULT BuildName(LPCWSTR pszFilespec, LPCWSTR pszExt, int iOrd, int iMaxLen, LPWSTR *ppszName)
|
|
{
|
|
// some things are hardcoded here like the " (%d)" stuff. this limitation is equivalent to
|
|
// PathYetAnotherMakeUniqueName so we're okay.
|
|
|
|
WCHAR szOrd[10];
|
|
if (iOrd)
|
|
{
|
|
wnsprintf(szOrd, ARRAYSIZE(szOrd), L" (%d)", iOrd);
|
|
}
|
|
else
|
|
{
|
|
szOrd[0] = 0;
|
|
}
|
|
|
|
int iLenFilespecToUse = lstrlen(pszFilespec);
|
|
int iLenOrdToUse = lstrlen(szOrd);
|
|
int iLenExtToUse = lstrlen(pszExt);
|
|
int iLenTotal = iLenFilespecToUse + iLenOrdToUse + iLenExtToUse;
|
|
HRESULT hr = S_OK;
|
|
if (iLenTotal > iMaxLen)
|
|
{
|
|
// first reduce the filespec since its less important than the extension
|
|
iLenFilespecToUse = max(1, iLenFilespecToUse - (iLenTotal - iMaxLen));
|
|
iLenTotal = iLenFilespecToUse + iLenOrdToUse + iLenExtToUse;
|
|
if (iLenTotal > iMaxLen)
|
|
{
|
|
// next zap the extension.
|
|
iLenExtToUse = max(0, iLenExtToUse - (iLenTotal - iMaxLen));
|
|
iLenTotal = iLenFilespecToUse + iLenOrdToUse + iLenExtToUse;
|
|
if (iLenTotal > iMaxLen)
|
|
{
|
|
// now it's game over.
|
|
iLenOrdToUse = max(0, iLenOrdToUse - (iLenTotal - iMaxLen));
|
|
iLenTotal = iLenFilespecToUse + iLenOrdToUse + iLenExtToUse;
|
|
if (iLenTotal > iMaxLen)
|
|
{
|
|
hr = E_FAIL;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
int cchBuf = iLenTotal + 1;
|
|
*ppszName = (LPWSTR)CoTaskMemAlloc(cchBuf * sizeof(WCHAR));
|
|
if (*ppszName)
|
|
{
|
|
lstrcpyn(*ppszName, pszFilespec, iLenFilespecToUse + 1);
|
|
lstrcpyn(*ppszName + iLenFilespecToUse, szOrd, iLenOrdToUse + 1);
|
|
lstrcpyn(*ppszName + iLenFilespecToUse + iLenOrdToUse, pszExt, iLenExtToUse + 1);
|
|
hr = S_OK;
|
|
}
|
|
else
|
|
{
|
|
hr = E_OUTOFMEMORY;
|
|
}
|
|
}
|
|
return hr;
|
|
}
|
|
|
|
HRESULT FindUniqueName(LPCWSTR pszFilespec, LPCWSTR pszExt, int iMaxLen, IShellItem *psiDest, LPWSTR *ppszName)
|
|
{
|
|
*ppszName = NULL;
|
|
|
|
HRESULT hr = E_FAIL;
|
|
BOOL fFound = FALSE;
|
|
for (int i = 0; !fFound && (i < 1000); i++)
|
|
{
|
|
LPWSTR pszBuf;
|
|
if (SUCCEEDED(BuildName(pszFilespec, pszExt, i, iMaxLen, &pszBuf)))
|
|
{
|
|
if (!ItemExists(pszBuf, psiDest))
|
|
{
|
|
fFound = TRUE;
|
|
hr = S_OK;
|
|
*ppszName = pszBuf;
|
|
}
|
|
else
|
|
{
|
|
CoTaskMemFree(pszBuf);
|
|
}
|
|
}
|
|
}
|
|
|
|
ASSERT((SUCCEEDED(hr) && *ppszName) || (FAILED(hr) && !*ppszName));
|
|
return hr;
|
|
}
|
|
|
|
HRESULT AutoCreateName(IShellItem *psiDest, IShellItem *psi, LPWSTR *ppszName)
|
|
{
|
|
*ppszName = NULL;
|
|
|
|
LPWSTR pszOrigName;
|
|
HRESULT hr = psi->GetDisplayName(SIGDN_PARENTRELATIVEFORADDRESSBAR, &pszOrigName);
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
IItemNameLimits *pinl;
|
|
if (SUCCEEDED(QIThroughShellItem(psiDest, IID_PPV_ARG(IItemNameLimits, &pinl))))
|
|
{
|
|
int iMaxLen;
|
|
if (FAILED(pinl->GetMaxLength(pszOrigName, &iMaxLen)))
|
|
{
|
|
// assume this for now in case of failure
|
|
iMaxLen = MAX_PATH;
|
|
}
|
|
|
|
if (S_OK != CheckCharsAndReplaceIfNecessary(pinl, pszOrigName) ||
|
|
lstrlen(pszOrigName) > iMaxLen)
|
|
{
|
|
// only if it started as an illegal name do we retry and provide uniqueness.
|
|
// (if its legal then leave it as non-unique so callers can do their confirm overwrite code).
|
|
LPWSTR pszFilespec, pszExt;
|
|
hr = BreakOutString(pszOrigName, &pszFilespec, &pszExt);
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
hr = FindUniqueName(pszFilespec, pszExt, iMaxLen, psiDest, ppszName);
|
|
CoTaskMemFree(pszFilespec);
|
|
CoTaskMemFree(pszExt);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// the name is okay so let it go through
|
|
hr = S_OK;
|
|
*ppszName = pszOrigName;
|
|
pszOrigName = NULL;
|
|
}
|
|
pinl->Release();
|
|
}
|
|
else
|
|
{
|
|
// if the destination namespace doesn't have an IItemNameLimits then assume the
|
|
// name is all good. we're not going to probe or anything so this is the best
|
|
// we can do.
|
|
hr = S_OK;
|
|
*ppszName = pszOrigName;
|
|
pszOrigName = NULL;
|
|
}
|
|
if (pszOrigName)
|
|
{
|
|
CoTaskMemFree(pszOrigName);
|
|
}
|
|
}
|
|
|
|
if (FAILED(hr) && *ppszName)
|
|
{
|
|
CoTaskMemFree(*ppszName);
|
|
*ppszName = NULL;
|
|
}
|
|
|
|
ASSERT((SUCCEEDED(hr) && *ppszName) || (FAILED(hr) && !*ppszName));
|
|
return hr;
|
|
}
|