Files
PowerToys/src/modules/powerrename/lib/Helpers.cpp
moooyo 70e1177a6a [PowerRename] Support using photo metadata to replace in the PowerRename (#41728)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
1. Introduce WIC for power rename and add new class WICMetadataExtractor
to use WIC to extract metadata.
2. Add some patterns for metadata extract.
3. Support XMP and EXIF metadata extract.
4. Add test data for xmp and exif extractor
5. Add attribution for the test data uploader.

UI:
<img width="2052" height="1415" alt="image"
src="https://github.com/user-attachments/assets/9051b12e-4e66-4fdc-a4d4-3bada661c235"
/>
<img width="284" height="170" alt="image"
src="https://github.com/user-attachments/assets/2fd67193-77a7-48f0-a5ac-08a69fe64e55"
/>
<img width="715" height="1160" alt="image"
src="https://github.com/user-attachments/assets/5fa68a8c-d129-44dd-b747-099dfbcded12"
/>

demo:


https://github.com/user-attachments/assets/e90bc206-62e5-4101-ada2-3187ee7e2039



<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #5612
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [x] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

---------

Co-authored-by: Yu Leng <yuleng@microsoft.com>
2025-11-04 09:27:16 +08:00

910 lines
32 KiB
C++

#include "pch.h"
#include "Helpers.h"
#include "MetadataTypes.h"
#include <regex>
#include <ShlGuid.h>
#include <cstring>
#include <filesystem>
#include <unordered_map>
#include <unordered_set>
#include <algorithm>
namespace fs = std::filesystem;
namespace
{
const int MAX_INPUT_STRING_LEN = 1024;
const wchar_t c_rootRegPath[] = L"Software\\Microsoft\\PowerRename";
// Helper function: Find the longest matching pattern starting at the given position
// Returns the matched pattern name, or empty string if no match found
std::wstring FindLongestPattern(
const std::wstring& input,
size_t startPos,
size_t maxPatternLength,
const std::unordered_set<std::wstring>& validPatterns)
{
const size_t remaining = input.length() - startPos;
const size_t searchLength = std::min(maxPatternLength, remaining);
// Try to match from longest to shortest to ensure greedy matching
// e.g., DATE_TAKEN_YYYY should be matched before DATE_TAKEN_YY
for (size_t len = searchLength; len > 0; --len)
{
std::wstring candidate = input.substr(startPos, len);
if (validPatterns.find(candidate) != validPatterns.end())
{
return candidate;
}
}
return L"";
}
// Helper function: Get the replacement value for a pattern
// Returns the actual metadata value if available; if not, returns the pattern name with $ prefix
std::wstring GetPatternValue(
const std::wstring& patternName,
const PowerRenameLib::MetadataPatternMap& patterns)
{
auto it = patterns.find(patternName);
// Return actual value if found and valid (non-empty)
if (it != patterns.end() && !it->second.empty())
{
return it->second;
}
// Return pattern name with $ prefix if value is unavailable
// This provides visual feedback that the field exists but has no data
return L"$" + patternName;
}
}
HRESULT GetTrimmedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source)
{
HRESULT hr = E_INVALIDARG;
if (source)
{
PWSTR newName = nullptr;
hr = SHStrDup(source, &newName);
if (SUCCEEDED(hr))
{
size_t firstValidIndex = 0, lastValidIndex = wcslen(newName) - 1;
while (firstValidIndex <= lastValidIndex && iswspace(newName[firstValidIndex]))
{
firstValidIndex++;
}
while (firstValidIndex <= lastValidIndex && (iswspace(newName[lastValidIndex]) || newName[lastValidIndex] == L'.'))
{
lastValidIndex--;
}
newName[lastValidIndex + 1] = '\0';
hr = StringCchCopy(result, cchMax, newName + firstValidIndex);
}
CoTaskMemFree(newName);
}
return hr;
}
HRESULT GetTransformedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, DWORD flags, bool isFolder)
{
std::locale::global(std::locale(""));
HRESULT hr = E_INVALIDARG;
auto contractionOrSingleQuotedWordCheck = [](std::wstring stem, size_t i) {
return !i || stem[i - 1] != '\'' || (i == 1 || iswpunct(stem[i - 2]) || iswspace(stem[i - 2]));
};
if (source && flags)
{
if (flags & Uppercase)
{
if (isFolder)
{
hr = StringCchCopy(result, cchMax, source);
if (SUCCEEDED(hr))
{
std::transform(result, result + wcslen(result), result, ::towupper);
}
}
else
{
if (flags & NameOnly)
{
std::wstring stem = fs::path(source).stem().wstring();
std::transform(stem.begin(), stem.end(), stem.begin(), ::towupper);
hr = StringCchPrintf(result, cchMax, L"%s%s", stem.c_str(), fs::path(source).extension().c_str());
}
else if (flags & ExtensionOnly)
{
std::wstring extension = fs::path(source).extension().wstring();
if (!extension.empty())
{
std::transform(extension.begin(), extension.end(), extension.begin(), ::towupper);
hr = StringCchPrintf(result, cchMax, L"%s%s", fs::path(source).stem().c_str(), extension.c_str());
}
else
{
hr = StringCchCopy(result, cchMax, source);
if (SUCCEEDED(hr))
{
std::transform(result, result + wcslen(result), result, ::towupper);
}
}
}
else
{
hr = StringCchCopy(result, cchMax, source);
if (SUCCEEDED(hr))
{
std::transform(result, result + wcslen(result), result, ::towupper);
}
}
}
}
else if (flags & Lowercase)
{
if (isFolder)
{
hr = StringCchCopy(result, cchMax, source);
if (SUCCEEDED(hr))
{
std::transform(result, result + wcslen(result), result, ::towlower);
}
}
else
{
if (flags & NameOnly)
{
std::wstring stem = fs::path(source).stem().wstring();
std::transform(stem.begin(), stem.end(), stem.begin(), ::towlower);
hr = StringCchPrintf(result, cchMax, L"%s%s", stem.c_str(), fs::path(source).extension().c_str());
}
else if (flags & ExtensionOnly)
{
std::wstring extension = fs::path(source).extension().wstring();
if (!extension.empty())
{
std::transform(extension.begin(), extension.end(), extension.begin(), ::towlower);
hr = StringCchPrintf(result, cchMax, L"%s%s", fs::path(source).stem().c_str(), extension.c_str());
}
else
{
hr = StringCchCopy(result, cchMax, source);
if (SUCCEEDED(hr))
{
std::transform(result, result + wcslen(result), result, ::towlower);
}
}
}
else
{
hr = StringCchCopy(result, cchMax, source);
if (SUCCEEDED(hr))
{
std::transform(result, result + wcslen(result), result, ::towlower);
}
}
}
}
else if (flags & Titlecase)
{
if (!(flags & ExtensionOnly))
{
std::vector<std::wstring> exceptions = { L"a", L"an", L"to", L"the", L"at", L"by", L"for", L"in", L"of", L"on", L"up", L"and", L"as", L"but", L"or", L"nor" };
std::wstring stem = isFolder ? source : fs::path(source).stem().wstring();
std::wstring extension = isFolder ? L"" : fs::path(source).extension().wstring();
size_t stemLength = stem.length();
bool isFirstWord = true;
while (stemLength > 0 && (iswspace(stem[stemLength - 1]) || iswpunct(stem[stemLength - 1])))
{
stemLength--;
}
for (size_t i = 0; i < stemLength; i++)
{
if (!i || iswspace(stem[i - 1]) || (iswpunct(stem[i - 1]) && contractionOrSingleQuotedWordCheck(stem, i)))
{
if (iswspace(stem[i]) || iswpunct(stem[i]))
{
continue;
}
size_t wordLength = 0;
while (i + wordLength < stemLength && !iswspace(stem[i + wordLength]) && !iswpunct(stem[i + wordLength]))
{
wordLength++;
}
auto subStr = stem.substr(i, wordLength);
std::transform(subStr.begin(), subStr.end(), subStr.begin(), ::towlower);
if (isFirstWord || i + wordLength == stemLength || std::find(exceptions.begin(), exceptions.end(), subStr) == exceptions.end())
{
stem[i] = towupper(stem[i]);
isFirstWord = false;
}
else
{
stem[i] = towlower(stem[i]);
}
}
else
{
stem[i] = towlower(stem[i]);
}
}
hr = StringCchPrintf(result, cchMax, L"%s%s", stem.c_str(), extension.c_str());
}
else
{
hr = StringCchCopy(result, cchMax, source);
}
}
else if (flags & Capitalized)
{
if (!(flags & ExtensionOnly))
{
std::wstring stem = isFolder ? source : fs::path(source).stem().wstring();
std::wstring extension = isFolder ? L"" : fs::path(source).extension().wstring();
size_t stemLength = stem.length();
while (stemLength > 0 && (iswspace(stem[stemLength - 1]) || iswpunct(stem[stemLength - 1])))
{
stemLength--;
}
for (size_t i = 0; i < stemLength; i++)
{
if (!i || iswspace(stem[i - 1]) || (iswpunct(stem[i - 1]) && contractionOrSingleQuotedWordCheck(stem, i)))
{
if (iswspace(stem[i]) || iswpunct(stem[i]))
{
continue;
}
else
{
stem[i] = towupper(stem[i]);
}
}
else
{
stem[i] = towlower(stem[i]);
}
}
hr = StringCchPrintf(result, cchMax, L"%s%s", stem.c_str(), extension.c_str());
}
else
{
hr = StringCchCopy(result, cchMax, source);
}
}
else
{
hr = StringCchCopy(result, cchMax, source);
}
}
return hr;
}
bool isFileTimeUsed(_In_ PCWSTR source)
{
bool used = false;
static const std::array patterns = {
std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$Y" },
std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$M" },
std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$D" },
std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$h" },
std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$m" },
std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$s" },
std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$f" },
std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$H" },
std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$T" },
std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$t" }
};
for (size_t i = 0; !used && i < patterns.size(); i++)
{
if (std::regex_search(source, patterns[i]))
{
used = true;
}
}
return used;
}
bool isMetadataUsed(_In_ PCWSTR source, PowerRenameLib::MetadataType metadataType, _In_opt_ PCWSTR filePath, bool isFolder)
{
if (!source) return false;
// Early exit: If file path is provided, check file type first (fastest checks)
// This avoids expensive pattern matching for files that don't support metadata
if (filePath != nullptr)
{
// Folders don't support metadata extraction
if (isFolder)
{
return false;
}
// Check if file path is valid
if (wcslen(filePath) == 0)
{
return false;
}
// Get file extension
std::wstring extension = fs::path(filePath).extension().wstring();
// Convert to lowercase for case-insensitive comparison
std::transform(extension.begin(), extension.end(), extension.begin(), ::towlower);
// According to the metadata support table, only these formats support metadata extraction:
// - JPEG (IFD, Exif, XMP, GPS, IPTC) - supports fast metadata encoding
// - TIFF (IFD, Exif, XMP, GPS, IPTC) - supports fast metadata encoding
// - PNG (text chunks)
static const std::unordered_set<std::wstring> supportedExtensions = {
L".jpg",
L".jpeg",
L".png",
L".tif",
L".tiff"
};
// If file type doesn't support metadata, no need to check patterns
if (supportedExtensions.find(extension) == supportedExtensions.end())
{
return false;
}
}
// Now check if any metadata pattern exists in the source string
// This is the most expensive check, so we do it last
std::wstring str(source);
// Get supported patterns for the specified metadata type
auto supportedPatterns = PowerRenameLib::MetadataPatternExtractor::GetSupportedPatterns(metadataType);
// Check if any metadata pattern exists in the source string
for (const auto& pattern : supportedPatterns)
{
std::wstring searchPattern = L"$" + pattern;
if (str.find(searchPattern) != std::wstring::npos)
{
return true;
}
}
// No metadata pattern found
return false;
}
HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SYSTEMTIME fileTime)
{
std::locale::global(std::locale(""));
HRESULT hr = E_INVALIDARG;
if (source && wcslen(source) > 0)
{
std::wstring res(source);
wchar_t replaceTerm[MAX_PATH] = { 0 };
wchar_t formattedDate[MAX_PATH] = { 0 };
wchar_t localeName[LOCALE_NAME_MAX_LENGTH];
if (GetUserDefaultLocaleName(localeName, LOCALE_NAME_MAX_LENGTH) == 0)
{
StringCchCopy(localeName, LOCALE_NAME_MAX_LENGTH, L"en_US");
}
int hour12 = (fileTime.wHour % 12);
if (hour12 == 0)
{
hour12 = 12;
}
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%04d"), L"$01", fileTime.wYear);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$YYYY"), replaceTerm);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", (fileTime.wYear % 100));
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$YY(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $YYY, $YYYY, or metadata patterns
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", (fileTime.wYear % 10));
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$Y(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $YY, $YYYY, or metadata patterns
GetDateFormatEx(localeName, NULL, &fileTime, L"MMMM", formattedDate, MAX_PATH, NULL);
formattedDate[0] = towupper(formattedDate[0]);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MMMM"), replaceTerm);
GetDateFormatEx(localeName, NULL, &fileTime, L"MMM", formattedDate, MAX_PATH, NULL);
formattedDate[0] = towupper(formattedDate[0]);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MMM(?!M)"), replaceTerm); // Negative lookahead prevents matching $MMMM
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMonth);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MM(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $MMM, $MMMM, or metadata patterns
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMonth);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$M(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $MM, $MMM, $MMMM, or metadata patterns
GetDateFormatEx(localeName, NULL, &fileTime, L"dddd", formattedDate, MAX_PATH, NULL);
formattedDate[0] = towupper(formattedDate[0]);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DDDD"), replaceTerm);
GetDateFormatEx(localeName, NULL, &fileTime, L"ddd", formattedDate, MAX_PATH, NULL);
formattedDate[0] = towupper(formattedDate[0]);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DDD(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $DDDD or metadata patterns
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wDay);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DD(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $DDD, $DDDD, or metadata patterns
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wDay);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$D(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $DD, $DDD, $DDDD, or metadata patterns like $DATE_TAKEN_YYYY
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", hour12);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$HH(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $HHH or metadata patterns
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", hour12);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$H(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $HH or metadata patterns
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", (fileTime.wHour < 12) ? L"AM" : L"PM");
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$TT"), replaceTerm);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", (fileTime.wHour < 12) ? L"am" : L"pm");
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$tt"), replaceTerm);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wHour);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$hh(?!h)"), replaceTerm); // Negative lookahead prevents matching $hhh
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wHour);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$h(?!h)"), replaceTerm); // Negative lookahead prevents matching $hh
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMinute);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$mm(?!m)"), replaceTerm); // Negative lookahead prevents matching $mmm
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMinute);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$m(?!m)"), replaceTerm); // Negative lookahead prevents matching $mm
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wSecond);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ss(?!s)"), replaceTerm); // Negative lookahead prevents matching $sss
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wSecond);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$s(?!s)"), replaceTerm); // Negative lookahead prevents matching $ss
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%03d"), L"$01", fileTime.wMilliseconds);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$fff(?!f)"), replaceTerm); // Negative lookahead prevents matching $ffff
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMilliseconds / 10);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ff(?!f)"), replaceTerm); // Negative lookahead prevents matching $fff
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMilliseconds / 100);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$f(?!f)"), replaceTerm); // Negative lookahead prevents matching $ff or $fff
hr = StringCchCopy(result, cchMax, res.c_str());
}
return hr;
}
HRESULT GetMetadataFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, const PowerRenameLib::MetadataPatternMap& patterns)
{
if (!source || wcslen(source) == 0)
{
return E_INVALIDARG;
}
std::wstring input(source);
std::wstring output;
output.reserve(input.length() * 2); // Reserve space to avoid frequent reallocations
// Build pattern lookup table for fast validation
// Using all possible patterns to recognize valid pattern names even when metadata is unavailable
auto allPatterns = PowerRenameLib::MetadataPatternExtractor::GetAllPossiblePatterns();
std::unordered_set<std::wstring> validPatterns;
validPatterns.reserve(allPatterns.size());
size_t maxPatternLength = 0;
for (const auto& pattern : allPatterns)
{
validPatterns.insert(pattern);
maxPatternLength = std::max(maxPatternLength, pattern.length());
}
size_t pos = 0;
while (pos < input.length())
{
// Handle regular characters
if (input[pos] != L'$')
{
output += input[pos];
pos++;
continue;
}
// Count consecutive dollar signs
size_t dollarCount = 0;
while (pos < input.length() && input[pos] == L'$')
{
dollarCount++;
pos++;
}
// Even number of dollars: all are escaped (e.g., $$ -> $, $$$$ -> $$)
if (dollarCount % 2 == 0)
{
output.append(dollarCount / 2, L'$');
continue;
}
// Odd number of dollars: pairs are escaped, last one might be a pattern prefix
// e.g., $ -> might be pattern, $$$ -> $ + might be pattern
size_t escapedDollars = dollarCount / 2;
// If no more characters, output all dollar signs
if (pos >= input.length())
{
output.append(dollarCount, L'$');
continue;
}
// Try to match a pattern (greedy matching for longest pattern)
std::wstring matchedPattern = FindLongestPattern(input, pos, maxPatternLength, validPatterns);
if (matchedPattern.empty())
{
// No pattern matched, output all dollar signs
output.append(dollarCount, L'$');
}
else
{
// Pattern matched
output.append(escapedDollars, L'$'); // Output escaped dollars first
// Replace pattern with its value or keep pattern name if value unavailable
std::wstring replacementValue = GetPatternValue(matchedPattern, patterns);
output += replacementValue;
pos += matchedPattern.length();
}
}
return StringCchCopy(result, cchMax, output.c_str());
}
HRESULT GetShellItemArrayFromDataObject(_In_ IUnknown* dataSource, _COM_Outptr_ IShellItemArray** items)
{
*items = nullptr;
CComPtr<IDataObject> dataObj;
HRESULT hr;
if (SUCCEEDED(dataSource->QueryInterface(IID_PPV_ARGS(&dataObj))))
{
hr = SHCreateShellItemArrayFromDataObject(dataObj, IID_PPV_ARGS(items));
}
else
{
hr = dataSource->QueryInterface(IID_PPV_ARGS(items));
}
return hr;
}
BOOL GetEnumeratedFileName(__out_ecount(cchMax) PWSTR pszUniqueName, UINT cchMax, __in PCWSTR pszTemplate, __in_opt PCWSTR pszDir, unsigned long ulMinLong, __inout unsigned long* pulNumUsed)
{
PWSTR pszName = nullptr;
HRESULT hr = S_OK;
BOOL fRet = FALSE;
int cchDir = 0;
if (0 != cchMax && pszUniqueName)
{
*pszUniqueName = 0;
if (pszDir)
{
hr = StringCchCopy(pszUniqueName, cchMax, pszDir);
if (SUCCEEDED(hr))
{
hr = PathCchAddBackslashEx(pszUniqueName, cchMax, &pszName, nullptr);
if (SUCCEEDED(hr))
{
cchDir = lstrlen(pszDir);
}
}
}
else
{
cchDir = 0;
pszName = pszUniqueName;
}
}
else
{
hr = E_INVALIDARG;
}
int cchTmp = 0;
int cchStem = 0;
PCWSTR pszStem = nullptr;
PCWSTR pszRest = nullptr;
wchar_t szFormat[MAX_PATH] = { 0 };
if (SUCCEEDED(hr))
{
pszStem = pszTemplate;
pszRest = StrChr(pszTemplate, L'(');
while (pszRest)
{
PCWSTR pszEndUniq = CharNext(pszRest);
while (*pszEndUniq && *pszEndUniq >= L'0' && *pszEndUniq <= L'9')
{
pszEndUniq++;
}
if (*pszEndUniq == L')' && (*CharNext(pszEndUniq) == L'\0' || CharNext(pszEndUniq) == PathFindExtension(pszTemplate)))
{
break;
}
pszRest = StrChr(CharNext(pszRest), L'(');
}
if (!pszRest)
{
pszRest = PathFindExtension(pszTemplate);
cchStem = static_cast<int>(pszRest - pszTemplate);
hr = StringCchCopy(szFormat, ARRAYSIZE(szFormat), L" (%lu)");
}
else
{
pszRest++;
cchStem = static_cast<int>(pszRest - pszTemplate);
while (*pszRest && *pszRest >= L'0' && *pszRest <= L'9')
{
pszRest++;
}
hr = StringCchCopy(szFormat, ARRAYSIZE(szFormat), L"%lu");
}
}
unsigned long ulMax = 0;
unsigned long ulMin = 0;
if (SUCCEEDED(hr))
{
int cchFormat = lstrlen(szFormat);
if (cchFormat < 3)
{
*pszUniqueName = L'\0';
return FALSE;
}
ulMin = ulMinLong;
cchTmp = cchMax - cchDir - cchStem - (cchFormat - 3);
switch (cchTmp)
{
case 1:
ulMax = 10;
break;
case 2:
ulMax = 100;
break;
case 3:
ulMax = 1000;
break;
case 4:
ulMax = 10000;
break;
case 5:
ulMax = 100000;
break;
default:
if (cchTmp <= 0)
{
ulMax = ulMin;
}
else
{
ulMax = 1000000;
}
break;
}
}
if (SUCCEEDED(hr))
{
hr = StringCchCopyN(pszName, pszUniqueName + cchMax - pszName, pszStem, cchStem);
if (SUCCEEDED(hr))
{
PWSTR pszDigit = pszName + cchStem;
for (unsigned long ul = ulMin; ((ul < ulMax) && (!fRet)); ul++)
{
wchar_t szTemp[MAX_PATH] = { 0 };
hr = StringCchPrintf(szTemp, ARRAYSIZE(szTemp), szFormat, ul);
if (SUCCEEDED(hr))
{
hr = StringCchCat(szTemp, ARRAYSIZE(szTemp), pszRest);
if (SUCCEEDED(hr))
{
hr = StringCchCopy(pszDigit, pszUniqueName + cchMax - pszDigit, szTemp);
if (SUCCEEDED(hr))
{
if (!PathFileExists(pszUniqueName))
{
(*pulNumUsed) = ul;
fRet = TRUE;
}
}
}
}
}
}
}
if (!fRet)
{
*pszUniqueName = L'\0';
}
return fRet;
}
// Iterate through the shell items array and checks if at least 1 item has SFGAO_CANRENAME.
// We do not enumerate child items - only items the user selected.
bool ShellItemArrayContainsRenamableItem(_In_ IShellItemArray* shellItemArray)
{
bool hasRenamable = false;
IEnumShellItems* spesi;
if (SUCCEEDED(shellItemArray->EnumItems(&spesi)))
{
ULONG celtFetched;
IShellItem* spsi;
while ((S_OK == spesi->Next(1, &spsi, &celtFetched)))
{
SFGAOF attrs;
if (SUCCEEDED(spsi->GetAttributes(SFGAO_CANRENAME, &attrs)) &&
attrs & SFGAO_CANRENAME)
{
hasRenamable = true;
break;
}
}
}
return hasRenamable;
}
// Iterate through the data source and checks if at least 1 item has SFGAO_CANRENAME.
// We do not enumerate child items - only items the user selected.
bool DataObjectContainsRenamableItem(_In_ IUnknown* dataSource)
{
bool hasRenamable = false;
CComPtr<IShellItemArray> spsia;
if (dataSource && SUCCEEDED(GetShellItemArrayFromDataObject(dataSource, &spsia)))
{
CComPtr<IEnumShellItems> spesi;
if (SUCCEEDED(spsia->EnumItems(&spesi)))
{
ULONG celtFetched;
CComPtr<IShellItem> spsi;
while ((S_OK == spesi->Next(1, &spsi, &celtFetched)))
{
SFGAOF attrs;
if (SUCCEEDED(spsi->GetAttributes(SFGAO_CANRENAME, &attrs)) &&
attrs & SFGAO_CANRENAME)
{
hasRenamable = true;
break;
}
}
}
}
return hasRenamable;
}
HWND CreateMsgWindow(_In_ HINSTANCE hInst, _In_ WNDPROC pfnWndProc, _In_ void* p)
{
WNDCLASS wc = { 0 };
PCWSTR wndClassName = L"MsgWindow";
wc.lpfnWndProc = DefWindowProc;
wc.cbWndExtra = sizeof(void*);
wc.hInstance = hInst;
wc.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_BTNFACE + 1);
wc.lpszClassName = wndClassName;
RegisterClass(&wc);
HWND hwnd = CreateWindowEx(
0, wndClassName, nullptr, 0, 0, 0, 0, 0, HWND_MESSAGE, 0, hInst, nullptr);
if (hwnd)
{
SetWindowLongPtr(hwnd, 0, reinterpret_cast<LONG_PTR>(p));
if (pfnWndProc)
{
SetWindowLongPtr(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(pfnWndProc));
}
}
return hwnd;
}
std::wstring GetRegString(const std::wstring& valueName, const std::wstring& subPath)
{
wchar_t value[MAX_INPUT_STRING_LEN];
value[0] = L'\0';
DWORD type = REG_SZ;
DWORD size = MAX_INPUT_STRING_LEN * sizeof(wchar_t);
std::wstring completePath = std::wstring(c_rootRegPath) + subPath;
if (SHGetValue(HKEY_CURRENT_USER, completePath.c_str(), valueName.c_str(), &type, value, &size) == ERROR_SUCCESS)
{
return std::wstring(value);
}
return std::wstring{};
}
unsigned int GetRegNumber(const std::wstring& valueName, unsigned int defaultValue)
{
DWORD type = REG_DWORD;
DWORD data = 0;
DWORD size = sizeof(DWORD);
if (SHGetValue(HKEY_CURRENT_USER, c_rootRegPath, valueName.c_str(), &type, &data, &size) == ERROR_SUCCESS)
{
return data;
}
return defaultValue;
}
void SetRegNumber(const std::wstring& valueName, unsigned int value)
{
SHSetValue(HKEY_CURRENT_USER, c_rootRegPath, valueName.c_str(), REG_DWORD, &value, sizeof(value));
}
bool GetRegBoolean(const std::wstring& valueName, bool defaultValue)
{
DWORD value = GetRegNumber(valueName.c_str(), defaultValue ? 1 : 0);
return (value == 0) ? false : true;
}
void SetRegBoolean(const std::wstring& valueName, bool value)
{
SetRegNumber(valueName, value ? 1 : 0);
}
bool LastModifiedTime(const std::wstring& filePath, FILETIME* lpFileTime)
{
WIN32_FILE_ATTRIBUTE_DATA attr{};
if (GetFileAttributesExW(filePath.c_str(), GetFileExInfoStandard, &attr))
{
*lpFileTime = attr.ftLastWriteTime;
return true;
}
return false;
}
std::wstring CreateGuidStringWithoutBrackets()
{
GUID guid;
if (CoCreateGuid(&guid) == S_OK)
{
OLECHAR* guidString;
if (StringFromCLSID(guid, &guidString) == S_OK)
{
std::wstring guidStr{ guidString };
CoTaskMemFree(guidString);
return guidStr.substr(1, guidStr.length() - 2);
}
}
return L"";
}