#include #include #include #include #include #include #include #include #include "log.h" #include "misc.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 make_system_error(std::string("fopen(") + quote(path) + ')'); } static void fclose_and_log(FILE* file) { if (!fclose(file)) { return; } log(std::string("Failed to close a file: ") + make_system_error("fclose()").what()); } 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 make_system_error(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(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 make_system_error(errsv, "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 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 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 make_system_error(std::string("rename(") + quote(tmp_config_file_path) + ", " + quote(config_file_path) + ')'); }