terminal/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp

408 lines
15 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include <argb.h>
#include "CascadiaSettings.h"
#include "AppKeyBindingsSerialization.h"
#include "../../types/inc/utils.hpp"
#include <appmodel.h>
#include <shlobj.h>
using namespace ::TerminalApp;
using namespace winrt::Microsoft::Terminal::TerminalControl;
using namespace winrt::TerminalApp;
using namespace winrt::Windows::Data::Json;
using namespace winrt::Windows::Storage;
using namespace winrt::Windows::Storage::Streams;
using namespace ::Microsoft::Console;
static constexpr std::wstring_view FILENAME{ L"profiles.json" };
static constexpr std::wstring_view SETTINGS_FOLDER_NAME{ L"\\Microsoft\\Windows Terminal\\" };
static constexpr std::string_view ProfilesKey{ "profiles" };
static constexpr std::string_view KeybindingsKey{ "keybindings" };
static constexpr std::string_view GlobalsKey{ "globals" };
static constexpr std::string_view SchemesKey{ "schemes" };
// Method Description:
// - Creates a CascadiaSettings from whatever's saved on disk, or instantiates
// a new one with the default values. If we're running as a packaged app,
// it will load the settings from our packaged localappdata. If we're
// running as an unpackaged application, it will read it from the path
// we've set under localappdata.
// Arguments:
// - saveOnLoad: If true, we'll write the settings back out after we load them,
// to make sure the schema is updated.
// Return Value:
// - a unique_ptr containing a new CascadiaSettings object.
std::unique_ptr<CascadiaSettings> CascadiaSettings::LoadAll(const bool saveOnLoad)
{
std::unique_ptr<CascadiaSettings> resultPtr;
std::optional<std::string> fileData = _IsPackaged() ?
_LoadAsPackagedApp() :
_LoadAsUnpackagedApp();
const bool foundFile = fileData.has_value();
if (foundFile)
{
const auto actualData = fileData.value();
// Parse the json data.
Json::Value root;
std::unique_ptr<Json::CharReader> reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() };
std::string errs; // This string will recieve any error text from failing to parse.
// `parse` will return false if it fails.
if (!reader->parse(actualData.c_str(), actualData.c_str() + actualData.size(), &root, &errs))
{
// TODO:GH#990 display this exception text to the user, in a
// copy-pasteable way.
throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs));
}
resultPtr = FromJson(root);
if (saveOnLoad)
{
// Logically compare the json we've parsed from the file to what
// we'd serialize at runtime. If the values are different, then
// write the updated schema back out.
const Json::Value reserialized = resultPtr->ToJson();
if (reserialized != root)
{
resultPtr->SaveAll();
}
}
}
else
{
resultPtr = std::make_unique<CascadiaSettings>();
resultPtr->CreateDefaults();
// The settings file does not exist. Let's commit one.
resultPtr->SaveAll();
}
return resultPtr;
}
// Method Description:
// - Serialize this settings structure, and save it to a file. The location of
// the file changes depending whether we're running as a packaged
// application or not.
// Arguments:
// - <none>
// Return Value:
// - <none>
void CascadiaSettings::SaveAll() const
{
const auto json = ToJson();
Json::StreamWriterBuilder wbuilder;
// Use 4 spaces to indent instead of \t
wbuilder.settings_["indentation"] = " ";
const auto serializedString = Json::writeString(wbuilder, json);
if (_IsPackaged())
{
_SaveAsPackagedApp(serializedString);
}
else
{
_SaveAsUnpackagedApp(serializedString);
}
}
// Method Description:
// - Serialize this object to a JsonObject.
// Arguments:
// - <none>
// Return Value:
// - a JsonObject which is an equivalent serialization of this object.
Json::Value CascadiaSettings::ToJson() const
{
Json::Value root;
Json::Value profilesArray;
for (const auto& profile : _profiles)
{
profilesArray.append(profile.ToJson());
}
Json::Value schemesArray;
const auto& colorSchemes = _globals.GetColorSchemes();
for (auto& scheme : colorSchemes)
{
schemesArray.append(scheme.ToJson());
}
root[GlobalsKey.data()] = _globals.ToJson();
root[ProfilesKey.data()] = profilesArray;
root[SchemesKey.data()] = schemesArray;
return root;
}
// Method Description:
// - Create a new instance of this class from a serialized JsonObject.
// Arguments:
// - json: an object which should be a serialization of a CascadiaSettings object.
// Return Value:
// - a new CascadiaSettings instance created from the values in `json`
std::unique_ptr<CascadiaSettings> CascadiaSettings::FromJson(const Json::Value& json)
{
std::unique_ptr<CascadiaSettings> resultPtr = std::make_unique<CascadiaSettings>();
if (auto globals{ json[GlobalsKey.data()] })
{
if (globals.isObject())
{
resultPtr->_globals = GlobalAppSettings::FromJson(globals);
}
}
else
{
// If there's no globals key in the root object, then try looking at the
// root object for those properties instead, to gracefully upgrade.
// This will attempt to do the legacy keybindings loading too
resultPtr->_globals = GlobalAppSettings::FromJson(json);
// If we didn't find keybindings in the legacy path, then they probably
// don't exist in the file. Create the default keybindings if we
// couldn't find any keybindings.
auto keybindings{ json[KeybindingsKey.data()] };
if (!keybindings)
{
resultPtr->_CreateDefaultKeybindings();
}
}
// TODO:MSFT:20737698 - Display an error if we failed to parse settings
// What should we do here if these keys aren't found?For default profile,
// we could always pick the first profile and just set that as the default.
// Finding no schemes is probably fine, unless of course one profile
// references a scheme. We could fail with come error saying the
// profiles file is corrupted.
// Not having any profiles is also bad - should we say the file is corrupted?
// Or should we just recreate the default profiles?
auto& resultSchemes = resultPtr->_globals.GetColorSchemes();
if (auto schemes{ json[SchemesKey.data()] })
{
for (auto schemeJson : schemes)
{
if (schemeJson.isObject())
{
auto scheme = ColorScheme::FromJson(schemeJson);
resultSchemes.emplace_back(std::move(scheme));
}
}
}
if (auto profiles{ json[ProfilesKey.data()] })
{
for (auto profileJson : profiles)
{
if (profileJson.isObject())
{
auto profile = Profile::FromJson(profileJson);
resultPtr->_profiles.emplace_back(profile);
}
}
}
return resultPtr;
}
// Function Description:
// - Returns true if we're running in a packaged context. If we are, then we
// have to use the Windows.Storage API's to save/load our files. If we're
// not, then we won't be able to use those API's.
// Arguments:
// - <none>
// Return Value:
// - true iff we're running in a packaged context.
bool CascadiaSettings::_IsPackaged()
{
UINT32 length = 0;
LONG rc = GetCurrentPackageFullName(&length, NULL);
return rc != APPMODEL_ERROR_NO_PACKAGE;
}
// Method Description:
// - Writes the given content to our settings file as UTF-8 encoded using the Windows.Storage
// APIS's. This will only work within the context of an application with
// package identity, so make sure to call _IsPackaged before calling this method.
// Will overwrite any existing content in the file.
// Arguments:
// - content: the given string of content to write to the file.
// Return Value:
// - <none>
void CascadiaSettings::_SaveAsPackagedApp(const std::string& content)
{
auto curr = ApplicationData::Current();
auto folder = curr.RoamingFolder();
auto file_async = folder.CreateFileAsync(FILENAME,
CreationCollisionOption::ReplaceExisting);
auto file = file_async.get();
DataWriter dw = DataWriter();
const char* firstChar = content.c_str();
const char* lastChar = firstChar + content.size();
const uint8_t* firstByte = reinterpret_cast<const uint8_t*>(firstChar);
const uint8_t* lastByte = reinterpret_cast<const uint8_t*>(lastChar);
winrt::array_view<const uint8_t> bytes{ firstByte, lastByte };
dw.WriteBytes(bytes);
FileIO::WriteBufferAsync(file, dw.DetachBuffer()).get();
}
// Method Description:
// - Writes the given content in UTF-8 to our settings file using the Win32 APIS's.
// Will overwrite any existing content in the file.
// Arguments:
// - content: the given string of content to write to the file.
// Return Value:
// - <none>
// This can throw an exception if we fail to open the file for writing, or we
// fail to write the file
void CascadiaSettings::_SaveAsUnpackagedApp(const std::string& content)
{
// Get path to output file
// In this scenario, the settings file will end up under e.g. C:\Users\admin\AppData\Roaming\Microsoft\Windows Terminal\profiles.json
std::wstring pathToSettingsFile = CascadiaSettings::_GetFullPathToUnpackagedSettingsFile();
auto hOut = CreateFileW(pathToSettingsFile.c_str(), GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hOut == INVALID_HANDLE_VALUE)
{
THROW_LAST_ERROR();
}
THROW_LAST_ERROR_IF(!WriteFile(hOut, content.c_str(), gsl::narrow<DWORD>(content.length()), 0, 0));
CloseHandle(hOut);
}
// Method Description:
// - Computes the path to the settings file if the app is run unpackaged.
// Will create any intermediate directories if they don't exist.
// The file will end up under e.g. C:\Users\admin\AppData\Roaming\Microsoft\Windows Terminal\profiles.json
// Arguments:
// - <none>
// Return Value:
// - A string containing the path to the unpackaged settings file
// This can throw an exception if it fails to get the roaming app data folder.
std::wstring CascadiaSettings::_GetFullPathToUnpackagedSettingsFile()
{
wil::unique_cotaskmem_string roamingAppDataFolder;
if (FAILED(SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, 0, &roamingAppDataFolder)))
{
THROW_LAST_ERROR();
}
std::wstring parentDirectoryForSettingsFile(roamingAppDataFolder.get());
parentDirectoryForSettingsFile.append(SETTINGS_FOLDER_NAME);
// Create the directory if it doesn't exist
wil::CreateDirectoryDeep(parentDirectoryForSettingsFile.c_str());
std::wstring pathToSettingsFile(parentDirectoryForSettingsFile);
pathToSettingsFile.append(FILENAME);
return pathToSettingsFile;
}
// Method Description:
// - Reads the content of our settings file using the Windows.Storage
// APIS's. This will only work within the context of an application with
// package identity, so make sure to call _IsPackaged before calling this method.
// Arguments:
// - <none>
// Return Value:
// - an optional with the content of the file if we were able to open it,
// otherwise the optional will be empty
std::optional<std::string> CascadiaSettings::_LoadAsPackagedApp()
{
auto curr = ApplicationData::Current();
auto folder = curr.RoamingFolder();
auto file_async = folder.TryGetItemAsync(FILENAME);
auto file = file_async.get();
if (file == nullptr)
{
return std::nullopt;
}
const auto storageFile = file.as<StorageFile>();
// settings file is UTF-8 without BOM
auto buffer = FileIO::ReadBufferAsync(storageFile).get();
auto bufferData = buffer.data();
std::vector<uint8_t> bytes{ bufferData, bufferData + buffer.Length() };
std::string resultString{ bytes.begin(), bytes.end() };
return { resultString };
}
// Method Description:
// - Reads the content in UTF-8 enconding of our settings file using the Win32 APIs
// Arguments:
// - <none>
// Return Value:
// - an optional with the content of the file if we were able to open it,
// otherwise the optional will be empty.
// If the file exists, but we fail to read it, this can throw an exception
// from reading the file
std::optional<std::string> CascadiaSettings::_LoadAsUnpackagedApp()
{
std::wstring pathToSettingsFile = CascadiaSettings::_GetFullPathToUnpackagedSettingsFile();
const auto hFile = CreateFileW(pathToSettingsFile.c_str(), GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
// If the file doesn't exist, that's fine. Just log the error and return
// nullopt - we'll create the defaults.
LOG_LAST_ERROR();
return std::nullopt;
}
// fileSize is in bytes
const auto fileSize = GetFileSize(hFile, nullptr);
THROW_LAST_ERROR_IF(fileSize == INVALID_FILE_SIZE);
auto utf8buffer = std::make_unique<char[]>(fileSize);
DWORD bytesRead = 0;
THROW_LAST_ERROR_IF(!ReadFile(hFile, utf8buffer.get(), fileSize, &bytesRead, nullptr));
CloseHandle(hFile);
// convert buffer to UTF-8 string
std::string utf8string(utf8buffer.get(), fileSize);
return { utf8string };
}
// function Description:
// - Returns the full path to the settings file, either within the application
// package, or in its unpackaged location.
// Arguments:
// - <none>
// Return Value:
// - the full path to the settings file
winrt::hstring CascadiaSettings::GetSettingsPath()
{
return _IsPackaged() ? CascadiaSettings::_GetPackagedSettingsPath() :
winrt::hstring{ CascadiaSettings::_GetFullPathToUnpackagedSettingsFile() };
}
// Function Description:
// - Get the full path to settings file in its packaged location.
// Arguments:
// - <none>
// Return Value:
// - the full path to the packaged settings file.
winrt::hstring CascadiaSettings::_GetPackagedSettingsPath()
{
const auto curr = ApplicationData::Current();
const auto folder = curr.RoamingFolder();
const auto file_async = folder.TryGetItemAsync(FILENAME);
const auto file = file_async.get();
return file.Path();
}