Files
Gitree/src/managers/user_data.cpp

554 lines
23 KiB
C++

#include "user_data.h"
#include <izo/Paths.hpp>
extern "C"
{
#include <ikv.h>
}
#include <algorithm>
#include <fstream>
#include <iomanip>
#include <optional>
#include <sstream>
#include <utility>
#ifdef _WIN32
#include <windows.h>
#include <wincrypt.h>
#endif
namespace
{
std::filesystem::path roaming_directory()
{
if (const auto config = izo::GetKnownPath(izo::KnownPath::Config); !config.empty())
return config;
if (const auto temporary = izo::GetKnownPath(izo::KnownPath::Temporary); !temporary.empty())
return temporary;
return std::filesystem::temp_directory_path();
}
const ikv_node_t *object_value(const ikv_node_t *object, const char *key, ikv_type_t type)
{
const ikv_node_t *value = object ? ikv_object_get(object, key) : nullptr;
return value && ikv_node_type(value) == type ? value : nullptr;
}
std::optional<std::pair<std::string, std::string>> splitCredentialPayload(const std::string &payload)
{
const size_t separator = payload.find('\n');
if (separator == std::string::npos)
return std::nullopt;
return std::pair<std::string, std::string>{
payload.substr(0, separator),
payload.substr(separator + 1)};
}
#ifdef _WIN32
std::string hexEncode(const std::string &value)
{
static constexpr char digits[] = "0123456789abcdef";
std::string encoded;
encoded.reserve(value.size() * 2);
for (const unsigned char byte : value)
{
encoded.push_back(digits[(byte >> 4) & 0x0F]);
encoded.push_back(digits[byte & 0x0F]);
}
return encoded;
}
std::optional<std::string> hexDecode(const std::string &value)
{
if (value.size() % 2 != 0)
return std::nullopt;
auto decode_nibble = [](char character) -> int
{
if (character >= '0' && character <= '9')
return character - '0';
if (character >= 'a' && character <= 'f')
return 10 + (character - 'a');
if (character >= 'A' && character <= 'F')
return 10 + (character - 'A');
return -1;
};
std::string decoded;
decoded.reserve(value.size() / 2);
for (size_t index = 0; index < value.size(); index += 2)
{
const int high = decode_nibble(value[index]);
const int low = decode_nibble(value[index + 1]);
if (high < 0 || low < 0)
return std::nullopt;
decoded.push_back(static_cast<char>((high << 4) | low));
}
return decoded;
}
std::optional<std::string> protectCredentialString(const std::string &value)
{
DATA_BLOB input{};
input.pbData = reinterpret_cast<BYTE *>(const_cast<char *>(value.data()));
input.cbData = static_cast<DWORD>(value.size());
DATA_BLOB output{};
if (!CryptProtectData(&input, L"Gitree Git Credential", nullptr, nullptr, nullptr,
CRYPTPROTECT_UI_FORBIDDEN, &output))
return std::nullopt;
std::string protected_value(reinterpret_cast<const char *>(output.pbData), output.cbData);
LocalFree(output.pbData);
return hexEncode(protected_value);
}
std::optional<std::string> unprotectCredentialString(const std::string &value)
{
const auto binary = hexDecode(value);
if (!binary)
return std::nullopt;
DATA_BLOB input{};
input.pbData = reinterpret_cast<BYTE *>(const_cast<char *>(binary->data()));
input.cbData = static_cast<DWORD>(binary->size());
DATA_BLOB output{};
if (!CryptUnprotectData(&input, nullptr, nullptr, nullptr, nullptr,
CRYPTPROTECT_UI_FORBIDDEN, &output))
return std::nullopt;
std::string plain(reinterpret_cast<const char *>(output.pbData), output.cbData);
LocalFree(output.pbData);
return plain;
}
#endif
}
UserData::UserData()
{
directory_ = roaming_directory() / "Identity" / "Gitree";
std::filesystem::create_directories(directory_);
load();
}
UserData::~UserData()
{
save();
}
std::string UserData::credentialScope(const std::string &remote_url)
{
const size_t scheme = remote_url.find("://");
if (scheme == std::string::npos)
return remote_url;
const size_t authority_begin = scheme + 3;
const size_t authority_end = remote_url.find_first_of("/?#", authority_begin);
return remote_url.substr(0, authority_end == std::string::npos ? remote_url.size() : authority_end);
}
std::filesystem::path UserData::dataPath() const
{
return directory_ / "user_data.ikv2b";
}
void UserData::removeLegacyFiles() const
{
std::error_code error;
for (const char *name : {"imgui.ini", "settings.ini", "history.txt", "session.txt", "user_data.ikv"})
std::filesystem::remove(directory_ / name, error);
}
void UserData::load()
{
const std::filesystem::path binary_path = dataPath();
if (ikv_node_t *root = ikvb_parse_file(binary_path.string().c_str()))
{
if (const ikv_node_t *settings = object_value(root, "settings", IKV_OBJECT))
{
if (const ikv_node_t *value = object_value(settings, "sidebar_width", IKV_FLOAT))
sidebar_width_ = static_cast<float>(ikv_as_float(value));
if (const ikv_node_t *value = object_value(settings, "details_width", IKV_FLOAT))
details_width_ = static_cast<float>(ikv_as_float(value));
if (const ikv_node_t *value = object_value(settings, "sidebar_collapsed", IKV_BOOL))
sidebar_collapsed_ = ikv_as_bool(value) != 0;
if (const ikv_node_t *value = object_value(settings, "pull_mode", IKV_INT))
pull_mode_ = static_cast<int>(ikv_as_int(value));
if (const ikv_node_t *value = object_value(settings, "zoom_percent", IKV_INT))
zoom_percent_ = static_cast<int>(ikv_as_int(value));
if (const ikv_node_t *heights = object_value(settings, "sidebar_sections", IKV_ARRAY))
{
const uint32_t count = std::min<uint32_t>(
ikv_array_size(heights), static_cast<uint32_t>(sidebar_section_heights_.size()));
for (uint32_t index = 0; index < count; ++index)
sidebar_section_heights_[index] = static_cast<float>(ikv_as_float(ikv_array_get(heights, index)));
}
if (const ikv_node_t *open = object_value(settings, "sidebar_section_open", IKV_ARRAY))
{
const uint32_t count = std::min<uint32_t>(
ikv_array_size(open), static_cast<uint32_t>(sidebar_section_open_.size()));
for (uint32_t index = 0; index < count; ++index)
sidebar_section_open_[index] = ikv_as_bool(ikv_array_get(open, index)) != 0;
}
if (const ikv_node_t *widths = object_value(settings, "commit_table_columns", IKV_ARRAY))
{
const uint32_t count = std::min<uint32_t>(
ikv_array_size(widths), static_cast<uint32_t>(commit_table_column_widths_.size()));
for (uint32_t index = 0; index < count; ++index)
commit_table_column_widths_[index] = static_cast<float>(ikv_as_float(ikv_array_get(widths, index)));
}
}
if (const ikv_node_t *history = object_value(root, "recently_closed", IKV_ARRAY))
{
const uint32_t count = std::min<uint32_t>(ikv_array_size(history), 100);
for (uint32_t index = 0; index < count; ++index)
{
const char *path = ikv_as_string(ikv_array_get(history, index));
if (path && *path)
recently_closed_.emplace_back(path);
}
}
if (const ikv_node_t *recent = object_value(root, "recent_repositories", IKV_ARRAY))
{
const uint32_t count = std::min<uint32_t>(ikv_array_size(recent), 100);
for (uint32_t index = 0; index < count; ++index)
{
const char *path = ikv_as_string(ikv_array_get(recent, index));
if (path && *path)
recent_repositories_.emplace_back(path);
}
}
if (const ikv_node_t *session = object_value(root, "session", IKV_OBJECT))
{
if (const ikv_node_t *active = object_value(session, "active_tab", IKV_INT))
active_repository_ = static_cast<size_t>(std::max<int64_t>(0, ikv_as_int(active)));
if (const ikv_node_t *tabs = object_value(session, "tabs", IKV_ARRAY))
{
for (uint32_t index = 0; index < ikv_array_size(tabs); ++index)
{
const char *path = ikv_as_string(ikv_array_get(tabs, index));
open_repositories_.emplace_back(path ? path : "");
}
}
}
if (const ikv_node_t *credentials = object_value(root, "credentials", IKV_ARRAY))
{
for (uint32_t index = 0; index < ikv_array_size(credentials); ++index)
{
const ikv_node_t *entry = ikv_array_get(credentials, index);
if (ikv_node_type(entry) != IKV_OBJECT)
continue;
const ikv_node_t *scope = object_value(entry, "scope", IKV_STRING);
const ikv_node_t *secret = object_value(entry, "secret", IKV_STRING);
if (!scope || !secret)
continue;
const char *scope_value = ikv_as_string(scope);
const char *secret_value = ikv_as_string(secret);
if (!scope_value || !secret_value || !*scope_value || !*secret_value)
continue;
encrypted_credentials_[scope_value] = secret_value;
}
}
ikv_free(root);
sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
details_width_ = std::clamp(details_width_, 280.0f, 650.0f);
pull_mode_ = std::clamp(pull_mode_, 0, 3);
zoom_percent_ = std::clamp(zoom_percent_, 80, 200);
for (float &height : sidebar_section_heights_)
height = std::clamp(height, 42.0f, 500.0f);
for (float &width : commit_table_column_widths_)
width = std::clamp(width, 0.0f, 1200.0f);
if (open_repositories_.empty())
active_repository_ = 0;
else
active_repository_ = std::min(active_repository_, open_repositories_.size() - 1);
return;
}
const std::filesystem::path text_data_path = directory_ / "user_data.ikv";
if (ikv_node_t *root = ikv_parse_file(text_data_path.string().c_str()))
{
if (const ikv_node_t *settings = object_value(root, "settings", IKV_OBJECT))
{
if (const ikv_node_t *value = object_value(settings, "sidebar_width", IKV_FLOAT))
sidebar_width_ = static_cast<float>(ikv_as_float(value));
if (const ikv_node_t *value = object_value(settings, "details_width", IKV_FLOAT))
details_width_ = static_cast<float>(ikv_as_float(value));
if (const ikv_node_t *value = object_value(settings, "sidebar_collapsed", IKV_BOOL))
sidebar_collapsed_ = ikv_as_bool(value) != 0;
if (const ikv_node_t *value = object_value(settings, "pull_mode", IKV_INT))
pull_mode_ = static_cast<int>(ikv_as_int(value));
if (const ikv_node_t *value = object_value(settings, "zoom_percent", IKV_INT))
zoom_percent_ = static_cast<int>(ikv_as_int(value));
if (const ikv_node_t *heights = object_value(settings, "sidebar_sections", IKV_ARRAY))
{
const uint32_t count = std::min<uint32_t>(
ikv_array_size(heights), static_cast<uint32_t>(sidebar_section_heights_.size()));
for (uint32_t index = 0; index < count; ++index)
sidebar_section_heights_[index] = static_cast<float>(ikv_as_float(ikv_array_get(heights, index)));
}
if (const ikv_node_t *open = object_value(settings, "sidebar_section_open", IKV_ARRAY))
{
const uint32_t count = std::min<uint32_t>(
ikv_array_size(open), static_cast<uint32_t>(sidebar_section_open_.size()));
for (uint32_t index = 0; index < count; ++index)
sidebar_section_open_[index] = ikv_as_bool(ikv_array_get(open, index)) != 0;
}
if (const ikv_node_t *widths = object_value(settings, "commit_table_columns", IKV_ARRAY))
{
const uint32_t count = std::min<uint32_t>(
ikv_array_size(widths), static_cast<uint32_t>(commit_table_column_widths_.size()));
for (uint32_t index = 0; index < count; ++index)
commit_table_column_widths_[index] = static_cast<float>(ikv_as_float(ikv_array_get(widths, index)));
}
}
if (const ikv_node_t *history = object_value(root, "recently_closed", IKV_ARRAY))
{
const uint32_t count = std::min<uint32_t>(ikv_array_size(history), 100);
for (uint32_t index = 0; index < count; ++index)
{
const char *path = ikv_as_string(ikv_array_get(history, index));
if (path && *path)
recently_closed_.emplace_back(path);
}
}
if (const ikv_node_t *recent = object_value(root, "recent_repositories", IKV_ARRAY))
{
const uint32_t count = std::min<uint32_t>(ikv_array_size(recent), 100);
for (uint32_t index = 0; index < count; ++index)
{
const char *path = ikv_as_string(ikv_array_get(recent, index));
if (path && *path)
recent_repositories_.emplace_back(path);
}
}
if (const ikv_node_t *session = object_value(root, "session", IKV_OBJECT))
{
if (const ikv_node_t *active = object_value(session, "active_tab", IKV_INT))
active_repository_ = static_cast<size_t>(std::max<int64_t>(0, ikv_as_int(active)));
if (const ikv_node_t *tabs = object_value(session, "tabs", IKV_ARRAY))
{
for (uint32_t index = 0; index < ikv_array_size(tabs); ++index)
{
const char *path = ikv_as_string(ikv_array_get(tabs, index));
open_repositories_.emplace_back(path ? path : "");
}
}
}
if (const ikv_node_t *credentials = object_value(root, "credentials", IKV_ARRAY))
{
for (uint32_t index = 0; index < ikv_array_size(credentials); ++index)
{
const ikv_node_t *entry = ikv_array_get(credentials, index);
if (ikv_node_type(entry) != IKV_OBJECT)
continue;
const ikv_node_t *scope = object_value(entry, "scope", IKV_STRING);
const ikv_node_t *secret = object_value(entry, "secret", IKV_STRING);
if (!scope || !secret)
continue;
const char *scope_value = ikv_as_string(scope);
const char *secret_value = ikv_as_string(secret);
if (!scope_value || !secret_value || !*scope_value || !*secret_value)
continue;
encrypted_credentials_[scope_value] = secret_value;
}
}
ikv_free(root);
sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
details_width_ = std::clamp(details_width_, 280.0f, 650.0f);
pull_mode_ = std::clamp(pull_mode_, 0, 3);
zoom_percent_ = std::clamp(zoom_percent_, 80, 200);
for (float &height : sidebar_section_heights_)
height = std::clamp(height, 42.0f, 500.0f);
for (float &width : commit_table_column_widths_)
width = std::clamp(width, 0.0f, 1200.0f);
if (open_repositories_.empty())
active_repository_ = 0;
else
active_repository_ = std::min(active_repository_, open_repositories_.size() - 1);
save();
return;
}
// Import the original files once when upgrading an existing installation.
std::ifstream settings(directory_ / "settings.ini");
std::string key;
while (settings >> key)
{
if (key == "sidebar_width")
settings >> sidebar_width_;
else if (key == "details_width")
settings >> details_width_;
else if (key == "pull_mode")
settings >> pull_mode_;
else if (key == "zoom_percent")
settings >> zoom_percent_;
else if (key.rfind("sidebar_section_", 0) == 0)
{
const size_t index = static_cast<size_t>(std::stoul(key.substr(16)));
float height = 0.0f;
settings >> height;
if (index < sidebar_section_heights_.size())
sidebar_section_heights_[index] = height;
}
}
sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
details_width_ = std::clamp(details_width_, 280.0f, 650.0f);
pull_mode_ = std::clamp(pull_mode_, 0, 3);
zoom_percent_ = std::clamp(zoom_percent_, 80, 200);
for (float &height : sidebar_section_heights_)
height = std::clamp(height, 42.0f, 500.0f);
std::ifstream history(directory_ / "history.txt");
std::string path;
while (history >> std::quoted(path))
{
if (!path.empty())
recently_closed_.push_back(path);
if (recently_closed_.size() == 100)
break;
}
recent_repositories_ = recently_closed_;
std::ifstream session(directory_ / "session.txt");
session >> active_repository_;
while (session >> std::quoted(path))
open_repositories_.push_back(path);
if (open_repositories_.empty())
active_repository_ = 0;
else
active_repository_ = std::min(active_repository_, open_repositories_.size() - 1);
save();
}
std::optional<SavedGitCredential> UserData::remoteCredential(const std::string &remote_url) const
{
const auto iterator = encrypted_credentials_.find(credentialScope(remote_url));
if (iterator == encrypted_credentials_.end())
return std::nullopt;
#ifdef _WIN32
const auto plain = unprotectCredentialString(iterator->second);
if (!plain)
return std::nullopt;
const auto split = splitCredentialPayload(*plain);
if (!split)
return std::nullopt;
return SavedGitCredential{remote_url, split->first, split->second};
#else
return std::nullopt;
#endif
}
void UserData::addRecentRepository(const std::string &path)
{
if (path.empty())
return;
std::erase(recent_repositories_, path);
recent_repositories_.insert(recent_repositories_.begin(), path);
if (recent_repositories_.size() > 100)
recent_repositories_.resize(100);
save();
}
void UserData::addRecentlyClosed(const std::string &path)
{
if (path.empty())
return;
std::erase(recent_repositories_, path);
recent_repositories_.insert(recent_repositories_.begin(), path);
if (recent_repositories_.size() > 100)
recent_repositories_.resize(100);
std::erase(recently_closed_, path);
recently_closed_.insert(recently_closed_.begin(), path);
if (recently_closed_.size() > 100)
recently_closed_.resize(100);
save();
}
std::string UserData::takeRecentlyClosed()
{
if (recently_closed_.empty())
return {};
std::string path = std::move(recently_closed_.front());
recently_closed_.erase(recently_closed_.begin());
save();
return path;
}
void UserData::setRepositorySession(std::vector<std::string> paths, size_t active_repository)
{
open_repositories_ = std::move(paths);
active_repository_ = open_repositories_.empty()
? 0
: std::min(active_repository, open_repositories_.size() - 1);
save();
}
void UserData::storeRemoteCredential(const std::string &remote_url, const std::string &username,
const std::string &password)
{
const std::string scope = credentialScope(remote_url);
if (scope.empty() || username.empty())
return;
#ifdef _WIN32
const auto protected_value = protectCredentialString(username + "\n" + password);
if (!protected_value)
return;
encrypted_credentials_[scope] = *protected_value;
save();
#else
(void)password;
#endif
}
void UserData::clearRemoteCredential(const std::string &remote_url)
{
encrypted_credentials_.erase(credentialScope(remote_url));
save();
}
void UserData::save() const
{
std::filesystem::create_directories(directory_);
ikv_node_t *root = ikv_create_object("gitree");
if (!root)
return;
ikv_node_t *settings = ikv_object_add_object(root, "settings");
ikv_object_set_float(settings, "sidebar_width", sidebar_width_);
ikv_object_set_float(settings, "details_width", details_width_);
ikv_object_set_bool(settings, "sidebar_collapsed", sidebar_collapsed_);
ikv_object_set_int(settings, "pull_mode", pull_mode_);
ikv_object_set_int(settings, "zoom_percent", zoom_percent_);
ikv_node_t *heights = ikv_object_add_array(settings, "sidebar_sections", IKV_FLOAT);
for (const float height : sidebar_section_heights_)
ikv_array_add_float(heights, height);
ikv_node_t *open = ikv_object_add_array(settings, "sidebar_section_open", IKV_BOOL);
for (const bool state : sidebar_section_open_)
ikv_array_add_bool(open, state);
ikv_node_t *widths = ikv_object_add_array(settings, "commit_table_columns", IKV_FLOAT);
for (const float width : commit_table_column_widths_)
ikv_array_add_float(widths, width);
ikv_node_t *history = ikv_object_add_array(root, "recently_closed", IKV_STRING);
for (const auto &path : recently_closed_)
ikv_array_add_string(history, path.c_str());
ikv_node_t *recent = ikv_object_add_array(root, "recent_repositories", IKV_STRING);
for (const auto &path : recent_repositories_)
ikv_array_add_string(recent, path.c_str());
ikv_node_t *session = ikv_object_add_object(root, "session");
ikv_object_set_int(session, "active_tab", static_cast<int64_t>(active_repository_));
ikv_node_t *tabs = ikv_object_add_array(session, "tabs", IKV_STRING);
for (const auto &path : open_repositories_)
ikv_array_add_string(tabs, path.c_str());
ikv_node_t *credentials = ikv_object_add_array(root, "credentials", IKV_OBJECT);
for (const auto &[scope, secret] : encrypted_credentials_)
{
ikv_node_t *entry = ikv_array_add_object(credentials);
ikv_object_set_string(entry, "scope", scope.c_str());
ikv_object_set_string(entry, "secret", secret.c_str());
}
ikvb_write_file_version(dataPath().string().c_str(), root, IKV_VERSION_2);
ikv_free(root);
removeLegacyFiles();
}