logmeow/config.cpp

228 lines
7.3 KiB
C++

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <cfloat>
#include <sys/stat.h>
#include <string>
#include <memory>
#include <system_error>
#include "log.h"
#include "config.h"
static FILE* fopen_or_raise(const char* path, const char* mode, bool ignore_enoent) {
FILE* file = fopen(path, mode);
if (file || (ignore_enoent && errno == ENOENT)) {
return file;
}
throw std::system_error(errno, std::generic_category(), std::string("fopen(") + quote(path) + ')');
}
static void fclose_and_log(FILE* file) {
if (!fclose(file)) {
return;
}
std::system_error e = std::system_error(errno, std::generic_category(), "fclose()");
log("Failed to close a file", std::move(e));
}
static bool write(const std::string& ptr, FILE* file) {
return fwrite(ptr.data(), sizeof(char), ptr.size(), file) == ptr.size() * sizeof(char);
}
static bool write(const char* ptr, FILE* file) {
return fwrite(ptr, 1, strlen(ptr), file) == strlen(ptr);
}
static inline float to_float(const char* str) {
char* endptr;
while (isspace(str[0])) {
str++;
}
if (str[0] == '\0') {
throw std::invalid_argument("float string is empty");
}
errno = 0;
float res = strtof(str, &endptr);
if (res == HUGE_VALF && errno == ERANGE) {
throw std::overflow_error(std::string(str) + " is bigger than HUGE_VALF");
} else if (res == FLT_MIN && errno == ERANGE) {
throw std::underflow_error(std::string(str) + " is smaller than FLT_MIN");
} else if (endptr[0] != '\0') {
throw std::invalid_argument(std::string(str) + " has trailing text");
}
return res;
}
static bool validate_font_size_or_log(float size) {
if (size <= 0.0f) {
log(std::string("Font size ") + std::to_string(size) + " is lower or equal to 0");
return false;
} else if (std::isnan(size)) {
log(std::string("Font size ") + std::to_string(size) + " is NaN");
return false;
} else if (std::isinf(size)) {
log(std::string("Font size ") + std::to_string(size) + " is infinity or negative infinity");
return false;
}
return true;
}
std::string get_config_folder() {
const char* path;
path = getenv("XDG_CONFIG_HOME");
if (path) {
return std::string(path) + "/logmeow";
}
path = getenv("HOME");
if (path) {
return std::string(path) + "/.config/logmeow";
}
throw std::runtime_error("cannot find suitable config folder, please set XDG_CONFIG_HOME or HOME");
}
std::string get_config_file_path() {
return get_config_folder() + "/config";
}
// tries to create a directory with an empty string if it's just "/" but who cares
void create_config_folders_if_necessary() {
std::string full_path = get_config_folder();
std::string path;
size_t pos = 0;
while (pos != std::string::npos) {
pos = full_path.find('/', pos);
if (pos != std::string::npos) {
pos++;
}
path = full_path.substr(0, pos);
if (!mkdir(path.c_str(), 0600)) {
continue;
}
if (errno == EEXIST) {
continue;
}
throw std::system_error(errno, std::generic_category(), std::string("mkdir(") + quote(path) + ')');
}
}
static inline void parse_config_line(Config& config, char* line) {
if (strncmp(line, "logcat_command=", 15 * sizeof(char)) == 0) {
config.logcat_command = &line[15];
} else if (strncmp(line, "normal_font_size=", 17 * sizeof(char)) == 0) {
float size = to_float(&line[17]);
if (validate_font_size_or_log(size)) {
config.normal_font_size = size;
}
} else if (strncmp(line, "monospace_font_size=", 20 * sizeof(char)) == 0) {
float size = to_float(&line[20]);
if (validate_font_size_or_log(size)) {
config.monospace_font_size = size;
}
} else if (line[0] != '\0') {
throw std::invalid_argument(std::string("unknown config line: ") + line);
}
}
static inline Config load_config(FILE* file) {
size_t line_capacity_size = 128 * sizeof(char);
char* line = static_cast<char*>(malloc(line_capacity_size));
if (line == nullptr) {
throw std::bad_alloc();
}
Config config;
while (true) {
errno = 0;
if (getline(&line, &line_capacity_size, file) < 0) {
int errsv = errno;
free(line);
if (errsv == ENOMEM) {
throw std::bad_alloc();
} else if (errsv != 0) {
throw std::system_error(errsv, std::generic_category(), "getline()");
} else {
break;
}
}
if (line_capacity_size == 0 || line[0] == '\0' || line[0] == '#') {
continue;
}
if (char* newline = strchr(line, '\n')) {
*newline = '\0';
}
try {
parse_config_line(config, line);
} catch (const std::exception& e) {
free(line);
throw;
}
}
return config;
}
Config load_config() {
std::string config_file_path = get_config_file_path();
std::unique_ptr<FILE, decltype(&fclose_and_log)> config_file(fopen_or_raise(config_file_path.c_str(), "r", true), fclose_and_log);
if (!config_file) {
return Config();
}
return load_config(config_file.get());
}
static inline void write_config(FILE* file, const Config& config) {
if (!write("# This is an auto-generated file, modifications will be lost\n"
"# This is a poor man's config file \"format\", there are only three legal lines:\n"
"# # a comment, only available at the start of a line\n"
"# (an empty line, no whitespace)\n"
"# key=value pairs, no spaces around the delimiter, and no unknown keys\n\n", file)) {
throw std::runtime_error("Failed to write info comment");
}
if (!write("logcat_command=", file)) {
throw std::runtime_error("Failed to write logcat command key");
}
if (!write(config.logcat_command, file)) {
throw std::runtime_error("Failed to write logcat command value");
}
if (!write("\nnormal_font_size=", file)) {
throw std::runtime_error("Failed to write normal font size key");
}
if (!write(std::to_string(config.normal_font_size), file)) {
throw std::runtime_error("Failed to write normal font size value");
}
if (!write("\nmonospace_font_size=", file)) {
throw std::runtime_error("Failed to write monospace font size key");
}
if (!write(std::to_string(config.monospace_font_size), file)) {
throw std::runtime_error("Failed to write monospace font size value");
}
}
void write_config(const Config& config) {
std::string config_file_path = get_config_file_path();
std::string tmp_config_file_path = config_file_path + ".tmp";
std::unique_ptr<FILE, decltype(&fclose_and_log)> config_file(fopen_or_raise(tmp_config_file_path.c_str(), "w", false), fclose_and_log);
write_config(config_file.get(), config);
config_file.reset();
if (!rename(tmp_config_file_path.c_str(), config_file_path.c_str())) {
return;
}
throw std::system_error(errno, std::generic_category(),
std::string("rename(") + quote(tmp_config_file_path) + ", " + quote(config_file_path) + ')');
}