mirror of
https://github.com/Open-Shell/Open-Shell-Menu.git
synced 2026-04-12 01:47:24 +10:00
Windows started to add newer version of settings to settings description file. These have the same description as older settings, but use different parameters. Unfortunately old settings are still present. This causes our modern settings folder to contain duplicates. And also we tend to use older setting definitions that no longer work properly. Thus we will de-duplicate parsed settings and try to keep newer ones (that should work better). New settings tend to have numeric suffix, so we will keep those with biggest suffix. Fixes #1031
534 lines
12 KiB
C++
534 lines
12 KiB
C++
// Modern settings helper
|
|
|
|
// - parse modern settings definitions from %windir%\ImmersiveControlPanel\Settings\AllSystemSettings_{253E530E-387D-4BC2-959D-E6F86122E5F2}.xml
|
|
// - store cached data (parsed settings, localized strings) in %LOCALAPPDATA%\OpenShell\ModernSettings.dat
|
|
// - provide mapped view over cached data
|
|
|
|
#include "stdafx.h"
|
|
#include "ModernSettings.h"
|
|
#include "ResourceHelper.h"
|
|
#include <appmodel.h>
|
|
#include <Shlobj.h>
|
|
#include <Shlwapi.h>
|
|
#include <algorithm>
|
|
#include <functional>
|
|
#include <iterator>
|
|
#include <mutex>
|
|
|
|
enum class Id : uint32_t
|
|
{
|
|
Header = 'SMSO',
|
|
Undef = 0,
|
|
Blob,
|
|
FileName,
|
|
DeepLink,
|
|
Icon,
|
|
Glyph,
|
|
PageId,
|
|
HostId,
|
|
GroupId,
|
|
SettingId,
|
|
Description,
|
|
Keywords,
|
|
};
|
|
|
|
#pragma pack(1)
|
|
struct FileHdr
|
|
{
|
|
uint32_t openShellVersion = GetVersionEx(g_Instance);
|
|
uint32_t windowsVersion = GetVersionEx(GetModuleHandle(L"user32.dll"));
|
|
uint32_t userLanguageId = GetUserDefaultUILanguage();
|
|
|
|
bool operator==(const FileHdr& other) const
|
|
{
|
|
return (windowsVersion == other.windowsVersion) &&
|
|
(openShellVersion == other.openShellVersion) &&
|
|
(userLanguageId == other.userLanguageId);
|
|
}
|
|
};
|
|
|
|
struct ItemHdr
|
|
{
|
|
Id id;
|
|
uint32_t size;
|
|
|
|
const uint8_t* data() const
|
|
{
|
|
return (const uint8_t*)this + sizeof(*this);
|
|
}
|
|
|
|
const ItemHdr* next() const
|
|
{
|
|
return (const ItemHdr*)(data() + size);
|
|
}
|
|
|
|
std::wstring_view asString() const
|
|
{
|
|
std::wstring_view retval((const wchar_t*)data(), size / sizeof(wchar_t));
|
|
if (!retval.empty() && retval.back() == 0)
|
|
{
|
|
retval.remove_suffix(1);
|
|
return retval;
|
|
}
|
|
|
|
return {};
|
|
}
|
|
};
|
|
#pragma pack()
|
|
|
|
class AttributeWriter
|
|
{
|
|
public:
|
|
std::vector<uint8_t> buffer()
|
|
{
|
|
return std::move(m_buffer);
|
|
}
|
|
|
|
void addBlob(Id id, const void* data, size_t size)
|
|
{
|
|
ItemHdr hdr{ id, (uint32_t)size };
|
|
append(&hdr, sizeof(hdr));
|
|
append(data, size);
|
|
}
|
|
|
|
void addString(Id id, const std::wstring& str)
|
|
{
|
|
if (!str.empty())
|
|
addBlob(id, str.data(), (str.size() + 1) * sizeof(str[0]));
|
|
}
|
|
|
|
private:
|
|
void append(const void* data, size_t size)
|
|
{
|
|
m_buffer.insert(m_buffer.end(), (const uint8_t*)data, (const uint8_t*)data + size);
|
|
}
|
|
|
|
std::vector<uint8_t> m_buffer;
|
|
};
|
|
|
|
static void ProcessAttributes(const void* buffer, size_t size, std::function<void(const ItemHdr&)> callback)
|
|
{
|
|
if (size < sizeof(ItemHdr))
|
|
return;
|
|
|
|
auto item = (const ItemHdr*)buffer;
|
|
auto last = (const ItemHdr*)((const uint8_t*)buffer + size);
|
|
|
|
while (item < last)
|
|
{
|
|
auto next = item->next();
|
|
if (next <= item || next > last)
|
|
break;
|
|
|
|
callback(*item);
|
|
|
|
item = next;
|
|
}
|
|
}
|
|
|
|
///
|
|
|
|
static std::wstring GetPackageFullName(const wchar_t* packageFamily)
|
|
{
|
|
static auto pGetPackagesByPackageFamily = static_cast<decltype(&GetPackagesByPackageFamily)>((void*)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "GetPackagesByPackageFamily"));
|
|
if (pGetPackagesByPackageFamily)
|
|
{
|
|
UINT32 count = 0;
|
|
UINT32 bufferLength = 0;
|
|
|
|
if (pGetPackagesByPackageFamily(packageFamily, &count, nullptr, &bufferLength, nullptr) == ERROR_INSUFFICIENT_BUFFER && count > 0)
|
|
{
|
|
std::vector<PWSTR> names(count);
|
|
std::vector<WCHAR> buffer(bufferLength);
|
|
|
|
if (pGetPackagesByPackageFamily(packageFamily, &count, names.data(), &bufferLength, buffer.data()) == ERROR_SUCCESS && count > 0)
|
|
return names[0];
|
|
}
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
static std::pair<std::wstring_view, std::wstring_view> ParseResourceString(const wchar_t* resourceString)
|
|
{
|
|
std::wstring_view str = resourceString;
|
|
|
|
if (str[0] == '@' && str[1] == '{')
|
|
{
|
|
str.remove_prefix(2);
|
|
if (str.back() == '}')
|
|
str.remove_suffix(1);
|
|
|
|
auto pos = str.find('?');
|
|
if (pos != str.npos)
|
|
return { str.substr(0, pos), str.substr(pos + 1) };
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
static std::wstring FormatResourceString(const std::wstring_view& package, const std::wstring_view& resource)
|
|
{
|
|
std::wstring retval(L"@{");
|
|
|
|
retval += package;
|
|
retval += L"?";
|
|
retval += resource;
|
|
retval += L"}";
|
|
|
|
return retval;
|
|
}
|
|
|
|
|
|
static std::wstring TranslateIndirectString(const WCHAR* string)
|
|
{
|
|
std::wstring retval;
|
|
retval.resize(1024);
|
|
|
|
auto hr = ::SHLoadIndirectString(string, retval.data(), (UINT)retval.size(), nullptr);
|
|
|
|
if (hr == E_INVALIDARG)
|
|
{
|
|
auto [package, resource] = ParseResourceString(string);
|
|
if (!package.empty() && !resource.empty())
|
|
{
|
|
auto fullPackage = GetPackageFullName(std::wstring(package).c_str());
|
|
if (!fullPackage.empty())
|
|
{
|
|
auto fullStr = FormatResourceString(fullPackage, resource);
|
|
hr = ::SHLoadIndirectString(fullStr.c_str(), retval.data(), (UINT)retval.size(), nullptr);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
retval.resize(wcslen(retval.data()));
|
|
return retval;
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
static std::wstring TranslateIndirectMultiString(const WCHAR* string)
|
|
{
|
|
std::wstring retval;
|
|
std::wstring_view str(string);
|
|
|
|
// remove '@'
|
|
str.remove_prefix(1);
|
|
|
|
while (!str.empty())
|
|
{
|
|
auto len = str.find(L'@', 1);
|
|
if (len == std::wstring::npos)
|
|
len = str.length();
|
|
|
|
std::wstring tmp(str.substr(0, len));
|
|
retval += TranslateIndirectString(tmp.c_str());
|
|
|
|
str.remove_prefix(len);
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
static std::wstring GetTranslatedString(CComPtr<IXMLDOMNode>& parent, const WCHAR* name)
|
|
{
|
|
CComPtr<IXMLDOMNode> node;
|
|
if (parent->selectSingleNode(CComBSTR(name), &node) == S_OK)
|
|
{
|
|
CComBSTR value;
|
|
if (node->get_text(&value) == S_OK)
|
|
{
|
|
if (value[0] == L'@')
|
|
{
|
|
if (value[1] == L'@')
|
|
return TranslateIndirectMultiString(value);
|
|
else
|
|
return TranslateIndirectString(value);
|
|
}
|
|
else
|
|
{
|
|
return (LPWSTR)value;
|
|
}
|
|
}
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
static void ParseFileName(CComPtr<IXMLDOMNode>& parent, AttributeWriter& writer)
|
|
{
|
|
writer.addString(Id::FileName, GetTranslatedString(parent, L"Filename"));
|
|
}
|
|
|
|
static void ParseApplicationInformation(CComPtr<IXMLDOMNode>& parent, AttributeWriter& writer)
|
|
{
|
|
CComPtr<IXMLDOMNode> node;
|
|
if (parent->selectSingleNode(CComBSTR(L"ApplicationInformation"), &node) == S_OK)
|
|
{
|
|
writer.addString(Id::DeepLink, GetTranslatedString(node, L"DeepLink"));
|
|
writer.addString(Id::Icon, GetTranslatedString(node, L"Icon"));
|
|
writer.addString(Id::Glyph, GetTranslatedString(node, L"Glyph"));
|
|
}
|
|
}
|
|
|
|
static void ParseSettingIdentity(CComPtr<IXMLDOMNode>& parent, AttributeWriter& writer)
|
|
{
|
|
CComPtr<IXMLDOMNode> node;
|
|
if (parent->selectSingleNode(CComBSTR(L"SettingIdentity"), &node) == S_OK)
|
|
{
|
|
writer.addString(Id::PageId, GetTranslatedString(node, L"PageID"));
|
|
writer.addString(Id::HostId, GetTranslatedString(node, L"HostID"));
|
|
writer.addString(Id::GroupId, GetTranslatedString(node, L"GroupID"));
|
|
writer.addString(Id::SettingId, GetTranslatedString(node, L"SettingID"));
|
|
}
|
|
}
|
|
|
|
static void ParseSettingInformation(CComPtr<IXMLDOMNode>& parent, AttributeWriter& writer)
|
|
{
|
|
CComPtr<IXMLDOMNode> node;
|
|
if (parent->selectSingleNode(CComBSTR(L"SettingInformation"), &node) == S_OK)
|
|
{
|
|
auto description = GetTranslatedString(node, L"Description");
|
|
if (description.empty())
|
|
description = GetTranslatedString(node, L"Name");
|
|
|
|
writer.addString(Id::Description, description);
|
|
|
|
auto keywords = GetTranslatedString(node, L"HighKeywords");
|
|
keywords += GetTranslatedString(node, L"LowKeywords");
|
|
keywords += GetTranslatedString(node, L"Keywords");
|
|
|
|
writer.addString(Id::Keywords, keywords);
|
|
}
|
|
}
|
|
|
|
static std::vector<uint8_t> ParseSetting(CComPtr<IXMLDOMNode>& parent)
|
|
{
|
|
AttributeWriter writer;
|
|
|
|
ParseFileName(parent, writer);
|
|
ParseApplicationInformation(parent, writer);
|
|
ParseSettingIdentity(parent, writer);
|
|
ParseSettingInformation(parent, writer);
|
|
|
|
return writer.buffer();
|
|
}
|
|
|
|
static std::vector<std::vector<uint8_t>> ParseModernSettings()
|
|
{
|
|
std::vector<std::vector<uint8_t>> retval;
|
|
|
|
CComPtr<IXMLDOMDocument> doc;
|
|
if (SUCCEEDED(doc.CoCreateInstance(L"Msxml2.FreeThreadedDOMDocument")))
|
|
{
|
|
doc->put_async(VARIANT_FALSE);
|
|
|
|
wchar_t path[MAX_PATH]{};
|
|
wcscpy_s(path, LR"(%windir%\ImmersiveControlPanel\Settings\AllSystemSettings_{253E530E-387D-4BC2-959D-E6F86122E5F2}.xml)");
|
|
DoEnvironmentSubst(path, _countof(path));
|
|
|
|
VARIANT_BOOL loaded;
|
|
if (SUCCEEDED(doc->load(CComVariant(path), &loaded)) && loaded)
|
|
{
|
|
CComPtr<IXMLDOMNode> root;
|
|
if (doc->selectSingleNode(CComBSTR(L"PCSettings"), &root) == S_OK)
|
|
{
|
|
CComPtr<IXMLDOMNode> node;
|
|
root->get_firstChild(&node);
|
|
while (node)
|
|
{
|
|
auto buffer = ParseSetting(node);
|
|
if (!buffer.empty())
|
|
retval.push_back(std::move(buffer));
|
|
|
|
CComPtr<IXMLDOMNode> next;
|
|
if (FAILED(node->get_nextSibling(&next)))
|
|
break;
|
|
node = std::move(next);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
static std::vector<uint8_t> SerializeModernSettings(const std::vector<std::vector<uint8_t>>& settings)
|
|
{
|
|
AttributeWriter writer;
|
|
|
|
FileHdr hdr{};
|
|
writer.addBlob(Id::Header, &hdr, sizeof(hdr));
|
|
|
|
for (const auto& setting : settings)
|
|
writer.addBlob(Id::Blob, setting.data(), setting.size());
|
|
|
|
return writer.buffer();
|
|
}
|
|
|
|
ModernSettings::ModernSettings(const wchar_t* fname) : m_storage(fname)
|
|
{
|
|
if (m_storage)
|
|
{
|
|
bool valid = false;
|
|
auto s = m_storage.get();
|
|
ProcessAttributes(s.data, s.size, [&](const ItemHdr& item) {
|
|
switch (item.id)
|
|
{
|
|
case Id::Header:
|
|
if (item.size >= sizeof(FileHdr))
|
|
{
|
|
const auto hdr = (const FileHdr*)item.data();
|
|
if (FileHdr() == *hdr)
|
|
valid = true;
|
|
}
|
|
break;
|
|
case Id::Blob:
|
|
if (valid)
|
|
{
|
|
const Blob blob = { item.data(), item.size };
|
|
ModernSettings::Setting s(blob);
|
|
if (s)
|
|
m_settings.emplace(s.fileName, blob);
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
ModernSettings::Setting::Setting(const Blob& blob)
|
|
{
|
|
ProcessAttributes(blob.data, blob.size, [&](const ItemHdr& item) {
|
|
switch (item.id)
|
|
{
|
|
case Id::FileName:
|
|
fileName = item.asString();
|
|
break;
|
|
case Id::DeepLink:
|
|
deepLink = item.asString();
|
|
break;
|
|
case Id::Glyph:
|
|
glyph = item.asString();
|
|
break;
|
|
case Id::Icon:
|
|
icon = item.asString();
|
|
break;
|
|
case Id::PageId:
|
|
pageId = item.asString();
|
|
break;
|
|
case Id::HostId:
|
|
hostId = item.asString();
|
|
break;
|
|
case Id::GroupId:
|
|
groupId = item.asString();
|
|
break;
|
|
case Id::SettingId:
|
|
settingId = item.asString();
|
|
break;
|
|
case Id::Description:
|
|
description = item.asString();;
|
|
break;
|
|
case Id::Keywords:
|
|
keywords = item.asString();
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
std::vector<std::wstring_view> ModernSettings::enumerate() const
|
|
{
|
|
std::vector<std::wstring_view> retval;
|
|
retval.reserve(m_settings.size());
|
|
|
|
for (const auto& i : m_settings)
|
|
retval.emplace_back(i.first);
|
|
|
|
return retval;
|
|
}
|
|
|
|
ModernSettings::Setting ModernSettings::get(const std::wstring_view& name) const
|
|
{
|
|
auto it = m_settings.find(name);
|
|
if (it != m_settings.end())
|
|
return { (*it).second };
|
|
|
|
return {};
|
|
}
|
|
|
|
static std::mutex s_lock;
|
|
static std::shared_ptr<ModernSettings> s_settings;
|
|
|
|
std::wstring GetLocalAppData()
|
|
{
|
|
WCHAR path[MAX_PATH]{};
|
|
wcscpy_s(path, L"%LOCALAPPDATA%\\OpenShell");
|
|
DoEnvironmentSubst(path, _countof(path));
|
|
|
|
// make sure directory exists
|
|
SHCreateDirectory(nullptr, path);
|
|
|
|
return { path };
|
|
}
|
|
|
|
std::shared_ptr<ModernSettings> GetModernSettings()
|
|
{
|
|
std::unique_lock l(s_lock);
|
|
|
|
if (!s_settings)
|
|
{
|
|
auto path = GetLocalAppData();
|
|
path += L"\\ModernSettings.dat";
|
|
|
|
// try to open cached settings
|
|
s_settings = std::make_shared<ModernSettings>(path.c_str());
|
|
if (s_settings->size() == 0)
|
|
{
|
|
// file doesn't exist or wrong format
|
|
s_settings.reset();
|
|
|
|
// re-parse settings
|
|
auto settings = ParseModernSettings();
|
|
if (!settings.empty())
|
|
{
|
|
// sort by setting name (in reverse order)
|
|
// this way we will have newer settings (like SomeSetting-2) before older ones
|
|
std::stable_sort(settings.begin(), settings.end(), [](const auto& a, const auto& b) {
|
|
return ModernSettings::Setting(a).fileName > ModernSettings::Setting(b).fileName;
|
|
});
|
|
|
|
// now sort by description (strings presented to the user)
|
|
// and keep relative order of items with the same description
|
|
std::stable_sort(settings.begin(), settings.end(), [](const auto& a, const auto& b) {
|
|
return ModernSettings::Setting(a).description < ModernSettings::Setting(b).description;
|
|
});
|
|
|
|
// remove duplicates
|
|
settings.erase(std::unique(settings.begin(), settings.end(), [](const auto& a, const auto& b) {
|
|
return ModernSettings::Setting(a).description == ModernSettings::Setting(b).description;
|
|
}), settings.end());
|
|
|
|
// store to file
|
|
{
|
|
auto buffer = SerializeModernSettings(settings);
|
|
|
|
File f(path.c_str(), GENERIC_WRITE, 0, CREATE_ALWAYS);
|
|
if (f)
|
|
{
|
|
DWORD written;
|
|
::WriteFile(f, buffer.data(), (DWORD)buffer.size(), &written, nullptr);
|
|
}
|
|
}
|
|
|
|
// and try again
|
|
s_settings = std::make_shared<ModernSettings>(path.c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
return s_settings;
|
|
}
|