mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-08-17 23:55:04 +00:00
289 lines
9.3 KiB
C++
289 lines
9.3 KiB
C++
// SPDX-License-Identifier: Apache-2.0
|
|
#include <tactility/preferences.h>
|
|
|
|
#include <tactility/log.h>
|
|
#include <tactility/paths.h>
|
|
#include <tactility/properties_file.h>
|
|
|
|
#include <cerrno>
|
|
#include <cinttypes>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <new>
|
|
#include <string>
|
|
#include <sys/stat.h>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
constexpr auto* TAG = "preferences";
|
|
|
|
// Escapes '\\' and '\n' so a string value can never break properties_file's one-entry-per-line
|
|
// on-disk format, regardless of its content.
|
|
std::string escape(const std::string& value) {
|
|
std::string result;
|
|
result.reserve(value.size());
|
|
for (char c : value) {
|
|
if (c == '\\') {
|
|
result += "\\\\";
|
|
} else if (c == '\n') {
|
|
result += "\\n";
|
|
} else {
|
|
result += c;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::string unescape(const std::string& value) {
|
|
std::string result;
|
|
result.reserve(value.size());
|
|
for (size_t i = 0; i < value.size(); i++) {
|
|
if (value[i] == '\\' && i + 1 < value.size()) {
|
|
i++;
|
|
result += (value[i] == 'n') ? '\n' : value[i];
|
|
} else {
|
|
result += value[i];
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Splits a tagged value ("b:1", "i32:42", "i64:123", "s:escaped text") into its type tag and
|
|
// raw payload. Returns false if there's no ':' separator (malformed/missing).
|
|
bool split_tag(const std::string& tagged_value, std::string& tag, std::string& raw_value) {
|
|
size_t colon = tagged_value.find(':');
|
|
if (colon == std::string::npos) {
|
|
return false;
|
|
}
|
|
tag = tagged_value.substr(0, colon);
|
|
raw_value = tagged_value.substr(colon + 1);
|
|
return true;
|
|
}
|
|
|
|
// Rejects anything but an exact "0" or "1" - a manually edited or corrupted properties file
|
|
// could otherwise have e.g. "b:garbage" silently read back as false.
|
|
bool parse_bool(const std::string& raw_value, bool& out) {
|
|
if (raw_value == "0") {
|
|
out = false;
|
|
return true;
|
|
}
|
|
if (raw_value == "1") {
|
|
out = true;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// strtol()'s own error signaling (errno/end pointer) is easy to get wrong by omission: called
|
|
// naively, it silently accepts trailing garbage ("42abc"), out-of-range input (clamped to
|
|
// LONG_MIN/LONG_MAX instead of failing), and - since `long` can be wider than int32_t (e.g. on
|
|
// the posix simulator, where `long` is 64-bit) - a value that overflows int32_t but not `long`
|
|
// would silently truncate on the narrowing cast instead of being rejected.
|
|
bool parse_int32(const std::string& raw_value, int32_t& out) {
|
|
if (raw_value.empty()) {
|
|
return false;
|
|
}
|
|
errno = 0;
|
|
char* end = nullptr;
|
|
long parsed = std::strtol(raw_value.c_str(), &end, 10);
|
|
if (errno == ERANGE || end != raw_value.c_str() + raw_value.size()) {
|
|
return false;
|
|
}
|
|
if (parsed < INT32_MIN || parsed > INT32_MAX) {
|
|
return false;
|
|
}
|
|
out = static_cast<int32_t>(parsed);
|
|
return true;
|
|
}
|
|
|
|
// See parse_int32() - same reasoning, with strtoll()/`long long`/int64_t.
|
|
bool parse_int64(const std::string& raw_value, int64_t& out) {
|
|
if (raw_value.empty()) {
|
|
return false;
|
|
}
|
|
errno = 0;
|
|
char* end = nullptr;
|
|
long long parsed = std::strtoll(raw_value.c_str(), &end, 10);
|
|
if (errno == ERANGE || end != raw_value.c_str() + raw_value.size()) {
|
|
return false;
|
|
}
|
|
if (parsed < INT64_MIN || parsed > INT64_MAX) {
|
|
return false;
|
|
}
|
|
out = static_cast<int64_t>(parsed);
|
|
return true;
|
|
}
|
|
|
|
bool ensure_directory(const std::string& path) {
|
|
struct stat info {};
|
|
if (stat(path.c_str(), &info) == 0) {
|
|
return (info.st_mode & S_IFMT) == S_IFDIR;
|
|
}
|
|
return mkdir(path.c_str(), 0777) == 0;
|
|
}
|
|
|
|
// mkdir -p.
|
|
bool ensure_directory_recursive(const std::string& path) {
|
|
for (size_t index = path.find('/', 1); index != std::string::npos; index = path.find('/', index + 1)) {
|
|
if (!ensure_directory(path.substr(0, index))) {
|
|
return false;
|
|
}
|
|
}
|
|
return ensure_directory(path);
|
|
}
|
|
|
|
// "" if @a path has no directory component (e.g. a bare filename) - nothing to create in that
|
|
// case, the current/root directory already exists.
|
|
std::string parent_directory(const std::string& path) {
|
|
size_t slash = path.find_last_of('/');
|
|
if (slash == std::string::npos) {
|
|
return "";
|
|
}
|
|
return path.substr(0, slash);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// Definition of the opaque handle declared in tactility/preferences.h - C callers only ever
|
|
// see it through a Preferences* pointer, never its members. Backed by a PropertiesFile
|
|
// (tactility/properties_file.h) rather than its own file I/O - each value is stored as a
|
|
// tagged string ("b:1", "i32:42", "i64:123", "s:escaped text") since PropertiesFile only knows
|
|
// about plain strings.
|
|
struct Preferences {
|
|
PropertiesFile* file;
|
|
};
|
|
|
|
namespace {
|
|
|
|
// Grow-and-retry: properties_file_get() needs a bounded buffer, and a string preference's
|
|
// value (unlike bool/int32/int64's short encodings) can be arbitrarily long.
|
|
bool try_get_tagged(const PropertiesFile* file, const char* key, std::string& tag, std::string& raw_value) {
|
|
std::vector<char> buffer(32);
|
|
while (true) {
|
|
error_t error = properties_file_get(file, key, buffer.data(), buffer.size());
|
|
if (error == ERROR_NONE) {
|
|
return split_tag(std::string(buffer.data()), tag, raw_value);
|
|
}
|
|
if (error == ERROR_NOT_FOUND) {
|
|
return false;
|
|
}
|
|
buffer.resize(buffer.size() * 2);
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
extern "C" {
|
|
|
|
Preferences* preferences_open(const char* path) {
|
|
std::string directory = parent_directory(path);
|
|
if (!directory.empty() && !ensure_directory_recursive(directory)) {
|
|
LOG_E(TAG, "Directory not found: %s", directory.c_str());
|
|
return nullptr;
|
|
}
|
|
|
|
PropertiesFile* file = properties_file_open(path);
|
|
if (file == nullptr) {
|
|
LOG_E(TAG, "Failed to open %s", path);
|
|
return nullptr;
|
|
}
|
|
|
|
auto* preferences = new (std::nothrow) Preferences { file };
|
|
if (preferences == nullptr) {
|
|
LOG_E(TAG, "Out of memory");
|
|
properties_file_close(file);
|
|
return nullptr;
|
|
}
|
|
return preferences;
|
|
}
|
|
|
|
void preferences_close(Preferences* preferences) {
|
|
properties_file_close(preferences->file);
|
|
delete preferences;
|
|
}
|
|
|
|
bool preferences_has_bool(const Preferences* preferences, const char* key) {
|
|
std::string tag, raw_value;
|
|
bool value;
|
|
return try_get_tagged(preferences->file, key, tag, raw_value) && tag == "b" && parse_bool(raw_value, value);
|
|
}
|
|
|
|
bool preferences_has_int32(const Preferences* preferences, const char* key) {
|
|
std::string tag, raw_value;
|
|
int32_t value;
|
|
return try_get_tagged(preferences->file, key, tag, raw_value) && tag == "i32" && parse_int32(raw_value, value);
|
|
}
|
|
|
|
bool preferences_has_int64(const Preferences* preferences, const char* key) {
|
|
std::string tag, raw_value;
|
|
int64_t value;
|
|
return try_get_tagged(preferences->file, key, tag, raw_value) && tag == "i64" && parse_int64(raw_value, value);
|
|
}
|
|
|
|
bool preferences_has_string(const Preferences* preferences, const char* key) {
|
|
std::string tag, raw_value;
|
|
return try_get_tagged(preferences->file, key, tag, raw_value) && tag == "s";
|
|
}
|
|
|
|
bool preferences_opt_bool(const Preferences* preferences, const char* key, bool* out_value) {
|
|
std::string tag, raw_value;
|
|
if (!try_get_tagged(preferences->file, key, tag, raw_value) || tag != "b") {
|
|
return false;
|
|
}
|
|
return parse_bool(raw_value, *out_value);
|
|
}
|
|
|
|
bool preferences_opt_int32(const Preferences* preferences, const char* key, int32_t* out_value) {
|
|
std::string tag, raw_value;
|
|
if (!try_get_tagged(preferences->file, key, tag, raw_value) || tag != "i32") {
|
|
return false;
|
|
}
|
|
return parse_int32(raw_value, *out_value);
|
|
}
|
|
|
|
bool preferences_opt_int64(const Preferences* preferences, const char* key, int64_t* out_value) {
|
|
std::string tag, raw_value;
|
|
if (!try_get_tagged(preferences->file, key, tag, raw_value) || tag != "i64") {
|
|
return false;
|
|
}
|
|
return parse_int64(raw_value, *out_value);
|
|
}
|
|
|
|
error_t preferences_opt_string(const Preferences* preferences, const char* key, char* out_value, size_t out_value_size) {
|
|
std::string tag, raw_value;
|
|
if (!try_get_tagged(preferences->file, key, tag, raw_value) || tag != "s") {
|
|
return ERROR_NOT_FOUND;
|
|
}
|
|
std::string value = unescape(raw_value);
|
|
if (value.size() + 1 > out_value_size) {
|
|
return ERROR_BUFFER_OVERFLOW;
|
|
}
|
|
std::memcpy(out_value, value.c_str(), value.size() + 1);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
void preferences_put_bool(Preferences* preferences, const char* key, bool value) {
|
|
properties_file_set(preferences->file, key, value ? "b:1" : "b:0");
|
|
}
|
|
|
|
void preferences_put_int32(Preferences* preferences, const char* key, int32_t value) {
|
|
char buffer[32];
|
|
std::snprintf(buffer, sizeof(buffer), "i32:%" PRId32, value);
|
|
properties_file_set(preferences->file, key, buffer);
|
|
}
|
|
|
|
void preferences_put_int64(Preferences* preferences, const char* key, int64_t value) {
|
|
char buffer[40];
|
|
std::snprintf(buffer, sizeof(buffer), "i64:%" PRId64, value);
|
|
properties_file_set(preferences->file, key, buffer);
|
|
}
|
|
|
|
void preferences_put_string(Preferences* preferences, const char* key, const char* value) {
|
|
std::string tagged = "s:" + escape(value);
|
|
properties_file_set(preferences->file, key, tagged.c_str());
|
|
}
|
|
|
|
} // extern "C"
|