diff options
Diffstat (limited to 'src/app-profiles.c')
-rw-r--r-- | src/app-profiles.c | 2188 |
1 files changed, 2188 insertions, 0 deletions
diff --git a/src/app-profiles.c b/src/app-profiles.c new file mode 100644 index 0000000..4b37006 --- /dev/null +++ b/src/app-profiles.c @@ -0,0 +1,2188 @@ +/* + * nvidia-settings: A tool for configuring the NVIDIA X driver on Unix + * and Linux systems. + * + * Copyright (C) 2013 NVIDIA Corporation. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses>. + */ + +/* + * app-profiles.c - this source file contains functions for querying and + * assigning application profile settings, as well as parsing and saving + * application profile configuration files. + */ + +#define _GNU_SOURCE + +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <assert.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <errno.h> +#include <dirent.h> +#include <ctype.h> +#include <time.h> +#include "common-utils.h" +#include "app-profiles.h" +#include "msg.h" + +static char *slurp(FILE *fp) +{ + int eof = FALSE; + char *text = strdup(""); + char *new_text; + char *line = NULL; + + while (text && !eof) { + line = fget_next_line(fp, &eof); + if (!eof) { + new_text = nvstrcat(text, "\n", line, NULL); + free(text); + text = new_text; + } + } + + free(line); + + return text; +} + +static void splice_string(char **s, size_t b, size_t e, const char *replace) +{ + char *tail = strdup(*s + e); + *s = realloc(*s, b + strlen(replace) + strlen(tail) + 1); + if (!*s) { + return; + } + sprintf(*s + b, "%s%s", replace, tail); + free(tail); +} + +#define HEX_DIGITS "0123456789abcdefABCDEF" + +char *nv_app_profile_cfg_file_syntax_to_json(const char *orig_s) +{ + char *s = strdup(orig_s); + + int quoted = FALSE; + char *tok; + size_t start, end, size; + unsigned long long val; + char *old_substr = NULL; + char *endptr; + char *new_substr = NULL; + + tok = s; + while ((tok = strpbrk(tok, "\\\"#" HEX_DIGITS))) { + switch (*tok) { + case '\"': + // Quotation mark + quoted = !quoted; + tok++; + break; + case '\\': + // Escaped character + tok++; + if (*tok) { + tok++; + } + break; + case '#': + // Comment + if (!quoted) { + char *end_tok = nvstrchrnul(tok, '\n'); + start = tok - s; + end = end_tok - s; + splice_string(&s, start, end, ""); + if (!s) { + goto fail; + } + tok = s + start; + } else { + tok++; + } + break; + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': + // Numeric value + size = strspn(tok, "Xx." HEX_DIGITS); + if ((tok[0] == '0') && + (tok[1] == 'x' || tok[1] == 'X' || isdigit(tok[1])) && + !quoted) { + old_substr = nvstrndup(tok, size); + if (!old_substr) { + goto fail; + } + errno = 0; + val = strtoull(old_substr, &endptr, 0); + if (errno || (endptr - old_substr != strlen(old_substr))) { + // Invalid conversion, skip this string + tok += size; + free(old_substr); old_substr = NULL; + } else { + new_substr = nvasprintf("%llu", val); + if (!new_substr) { + goto fail; + } else { + start = tok - s; + end = tok - s + size; + splice_string(&s, start, end, new_substr); + free(new_substr); new_substr = NULL; + free(old_substr); old_substr = NULL; + tok = s + start; + } + } + } else { + // Not hex or octal; let the JSON parser deal with it + tok += size; + } + break; + default: + assert(!"Unhandled character"); + break; + } + } + + assert(!new_substr); + assert(!old_substr); + return s; + +fail: + free(old_substr); + free(new_substr); + free(s); + return NULL; +} + +static int open_and_stat(const char *filename, const char *perms, FILE **fp, struct stat *stat_buf) +{ + int ret; + *fp = fopen(filename, perms); + if (!*fp) { + if (errno != ENOENT) { + nv_error_msg("Could not open file %s (%s)", filename, strerror(errno)); + } + return -1; + } + + ret = fstat(fileno(*fp), stat_buf); + if (ret == -1) { + nv_error_msg("Could not stat file %s (%s)", filename, strerror(errno)); + fclose(*fp); + } + return ret; +} + +static char *nv_dirname(const char *path) +{ + char *last_slash = strrchr(path, '/'); + if (last_slash) { + return nvstrndup(path, last_slash - path); + } else { + return nvstrdup("."); + } +} + +static char *nv_basename(const char *path) +{ + char *last_slash = strrchr(path, '/'); + if (last_slash) { + return strdup(last_slash+1); + } else { + return strdup(path); + } +} + +static json_t *app_profile_config_insert_file_object(AppProfileConfig *config, json_t *new_file) +{ + json_t *json_filename, *json_new_filename; + char *dirname; + const char *filename, *new_filename; + json_t *file; + json_t *order, *file_order; + size_t new_file_major, new_file_minor, file_order_major, file_order_minor; + size_t i; + size_t num_files; + + json_new_filename = json_object_get(new_file, "filename"); + + assert(json_new_filename); + new_filename = json_string_value(json_new_filename); + + assert(nv_app_profile_config_check_valid_source_file(config, + new_filename, + NULL)); + + // Determine the correct location of the file in the search path + dirname = NULL; + + new_file_major = -1; + for (i = 0; i < config->search_path_count; i++) { + if (!strcmp(new_filename, config->search_path[i])) { + new_file_major = i; + break; + } else { + if (!dirname) { + dirname = nv_dirname(new_filename); + } + if (!strcmp(dirname, config->search_path[i])) { + new_file_major = i; + break; + } + } + } + free(dirname); + + new_file_minor = 0; + num_files = json_array_size(config->parsed_files); + + for (i = 0; i < num_files; i++) { + file = json_array_get(config->parsed_files, i); + file_order = json_object_get(file, "order"); + file_order_major = json_integer_value(json_object_get(file_order, "major")); + file_order_minor = json_integer_value(json_object_get(file_order, "minor")); + json_filename = json_object_get(file, "filename"); + assert(json_filename); + filename = json_string_value(json_filename); + if (file_order_major < new_file_major) { + } else if (file_order_major == new_file_major) { + if (strcoll(filename, new_filename) > 0) { + break; + } + new_file_minor++; + } else { + break; + } + } + + // Mark the order of the file + order = json_object_get(new_file, "order"); + json_object_set_new(order, "major", json_integer(new_file_major)); + json_object_set_new(order, "minor", json_integer(new_file_minor)); + + // Add the new file + json_array_insert(config->parsed_files, i, new_file); + + // Bump up minor for files after this one with the same major + num_files = json_array_size(config->parsed_files); + + for ( ; i < num_files; i++) { + file = json_array_get(config->parsed_files, i); + file_order = json_object_get(file, "order"); + file_order_major = json_integer_value(json_object_get(order, "major")); + file_order_minor = json_integer_value(json_object_get(order, "minor")); + if (file_order_major > new_file_major) { + break; + } + json_object_set_new(file_order, "minor", json_integer(file_order_minor+1)); + } + + return new_file; +} + + +/* + * Create a new empty file object and adds it to the configuration. + */ +static json_t *app_profile_config_new_file(AppProfileConfig *config, + const char *filename) +{ + json_t *new_file = json_object(); + + json_object_set_new(new_file, "filename", json_string(filename)); + json_object_set_new(new_file, "rules", json_array()); + json_object_set_new(new_file, "profiles", json_object()); + json_object_set_new(new_file, "dirty", json_false()); + json_object_set_new(new_file, "new", json_true()); + // order is set by app_profile_config_insert_file_object() below + + new_file = app_profile_config_insert_file_object(config, new_file); + + return new_file; +} + +static char *rule_id_to_key_string(int id) +{ + char *key; + key = nvasprintf("%d", id); + return key; +} + +/* + * Constructs a profile name that is guaranteed to be unique to this + * configuration. This is used to handle the case where there are multiple + * profiles with the same name (an invalid configuration). + */ +static char *app_profile_config_unique_profile_name(AppProfileConfig *config, + const char *orig_name, + const char *filename, + int do_warn, + int *needs_dirty) +{ + json_t *json_gold_filename = json_object_get(config->profile_locations, orig_name); + + if (json_gold_filename) { + int i = 0; + char *new_name = NULL; + do { + free(new_name); + new_name = nvasprintf("%s_duplicate_%d", orig_name, i++); + } while (new_name && json_object_get(config->profile_locations, new_name)); + if (do_warn) { + nv_error_msg("The profile \"%s\" in the file \"%s\" has the same name " + "as a profile defined in the file \"%s\", and will be renamed to \"%s\".", + orig_name, filename, json_string_value(json_gold_filename), new_name); + } + if (needs_dirty) { + *needs_dirty = TRUE; + } + return new_name; + } else { + return strdup(orig_name); + } +} + + +char *nv_app_profile_config_get_unused_profile_name(AppProfileConfig *config) +{ + char *temp_name, *unique_name; + int salt = rand(); + + temp_name = nvasprintf("profile_%x", salt); + unique_name = app_profile_config_unique_profile_name(config, + temp_name, + NULL, + FALSE, + NULL); + + free(temp_name); + return unique_name; +} + +static json_t *json_settings_parse(json_t *old_settings, const char *filename) +{ + int uses_setting_objects; + size_t i, size; + json_t *old_setting; + json_t *new_settings, *new_setting; + json_t *json_key, *json_value; + + if (!json_is_array(old_settings)) { + return NULL; + } + + new_settings = json_array(); + + uses_setting_objects = json_array_size(old_settings) && + json_is_object(json_array_get(old_settings, 0)); + + for (i = 0, size = json_array_size(old_settings); i < size; ) { + old_setting = json_array_get(old_settings, i++); + if (uses_setting_objects) { + json_key = json_object_get(old_setting, "key"); + if (!json_key) { + json_key = json_object_get(old_setting, "k"); + } + json_value = json_object_get(old_setting, "value"); + if (!json_value) { + json_value = json_object_get(old_setting, "v"); + } + } else { + if (i >= size) { + nv_error_msg("App profile parse error in %s: Key/value array of odd length\n", filename); + json_decref(new_settings); + return NULL; + } + json_key = old_setting; + json_value = json_array_get(old_settings, i++); + } + + if (!json_is_string(json_key)) { + nv_error_msg("App profile parse error in %s: Invalid key detected in settings array\n", filename); + json_decref(new_settings); + return NULL; + } + + if (!json_is_integer(json_value) && + !json_is_real(json_value) && + !json_is_true(json_value) && + !json_is_false(json_value) && + !json_is_string(json_value)) { + nv_error_msg("App profile parse error in %s: Invalid value detected in settings array\n", filename); + json_decref(new_settings); + return NULL; + } + new_setting = json_object(); + json_object_set(new_setting, "key", json_key); + json_object_set(new_setting, "value", json_value); + json_array_append_new(new_settings, new_setting); + } + + return new_settings; +} + +/* + * Load app profile settings from an already-open file. This operation is + * atomic: either all of the settings from the file are added to the + * configuration, or none are. + */ +static void app_profile_config_load_file(AppProfileConfig *config, + const char *filename, + struct stat *stat_buf, + FILE *fp) +{ + char *json_text = NULL; + char *orig_text = NULL; + size_t i, size; + json_error_t error; + json_t *orig_file = NULL; + json_t *orig_json_profiles, *orig_json_rules; + int next_free_rule_id = config->next_free_rule_id; + int dirty = FALSE; + json_t *new_file = NULL; + json_t *new_json_profiles = NULL; + json_t *new_json_rules = NULL; + + if (!S_ISREG(stat_buf->st_mode)) { + // Silently ignore all but regular files + goto done; + } + + orig_text = slurp(fp); + + if (!orig_text) { + nv_error_msg("Could not read from file %s", filename); + goto done; + } + + // Convert the file contents to JSON + json_text = nv_app_profile_cfg_file_syntax_to_json(orig_text); + + if (!json_text) { + nv_error_msg("App profile parse error in %s: text is not valid app profile configuration syntax", filename); + goto done; + } + + new_file = json_object(); + + json_object_set_new(new_file, "dirty", json_false()); + json_object_set_new(new_file, "filename", json_string(filename)); + + new_json_profiles = json_object(); + new_json_rules = json_array(); + + // Parse the resulting JSON + orig_file = json_loads(json_text, 0, &error); + + if (!orig_file) { + nv_error_msg("App profile parse error in %s: %s on %s, line %d\n", + filename, error.text, error.source, error.line); + goto done; + } + + if (!json_is_object(orig_file)) { + nv_error_msg("App profile parse error in %s: top-level config not an object!\n", filename); + goto done; + } + + orig_json_profiles = json_object_get(orig_file, "profiles"); + + if (orig_json_profiles) { + /* + * Note: we store profiles internally as members of an object, but the + * config file syntax uses an array to store profiles. + */ + if (!json_is_array(orig_json_profiles)) { + nv_error_msg("App profile parse error in %s: profiles value is not an array\n", filename); + goto done; + } + + size = json_array_size(orig_json_profiles); + for (i = 0; i < size; i++) { + const char *new_name; + json_t *orig_json_profile, *orig_json_name, *orig_json_settings; + json_t *new_json_profile, *new_json_settings; + + new_json_profile = json_object(); + + orig_json_profile = json_array_get(orig_json_profiles, i); + if (!json_is_object(orig_json_profile)) { + goto done; + } + + orig_json_name = json_object_get(orig_json_profile, "name"); + if (!json_is_string(orig_json_name)) { + goto done; + } + + orig_json_settings = json_object_get(orig_json_profile, "settings"); + new_json_settings = json_settings_parse(orig_json_settings, filename); + if (!new_json_settings) { + goto done; + } + + new_name = app_profile_config_unique_profile_name(config, + json_string_value(orig_json_name), + filename, + TRUE, + &dirty); + json_object_set_new(new_json_profile, "settings", new_json_settings); + + json_object_set_new(new_json_profiles, new_name, new_json_profile); + } + } + + orig_json_rules = json_object_get(orig_file, "rules"); + + if (orig_json_rules) { + if (!json_is_array(orig_json_rules)) { + nv_error_msg("App profile parse error in %s: rules value is not an array\n", filename); + goto done; + } + + size = json_array_size(orig_json_rules); + for (i = 0; i < size; i++) { + int new_id; + char *profile_name; + json_t *orig_json_rule, *orig_json_pattern, *orig_json_profile; + json_t *new_json_rule, *new_json_pattern; + orig_json_rule = json_array_get(orig_json_rules, i); + + if (!json_is_object(orig_json_rule)) { + goto done; + } + + new_id = next_free_rule_id++; + + new_json_rule = json_object(); + new_json_pattern = json_object(); + + orig_json_pattern = json_object_get(orig_json_rule, "pattern"); + if (json_is_object(orig_json_pattern)) { + // pattern object + json_t *orig_json_feature, *orig_json_matches; + orig_json_feature = json_object_get(orig_json_pattern, "feature"); + if (!json_is_string(orig_json_feature)) { + json_decref(new_json_rule); + json_decref(new_json_pattern); + goto done; + } + orig_json_matches = json_object_get(orig_json_pattern, "matches"); + if (!json_is_string(orig_json_matches)) { + json_decref(new_json_rule); + json_decref(new_json_pattern); + goto done; + } + json_object_set(new_json_pattern, "feature", orig_json_feature); + json_object_set(new_json_pattern, "matches", orig_json_matches); + } else if (json_is_string(orig_json_pattern)) { + // procname + json_object_set_new(new_json_pattern, "feature", json_string("procname")); + json_object_set(new_json_pattern, "matches", orig_json_pattern); + } else { + json_decref(new_json_rule); + json_decref(new_json_pattern); + goto done; + } + + json_object_set_new(new_json_rule, "pattern", new_json_pattern); + + orig_json_profile = json_object_get(orig_json_rule, "profile"); + if (json_is_object(orig_json_profile) || json_is_array(orig_json_profile)) { + // inline profile object + json_t *new_json_profile; + json_t *orig_json_settings, *orig_json_name; + json_t *new_json_settings; + + if (json_is_object(orig_json_profile)) { + orig_json_settings = json_object_get(orig_json_profile, "settings"); + orig_json_name = json_object_get(orig_json_profile, "name"); + } else { + // must be array + orig_json_settings = orig_json_profile; + orig_json_name = NULL; + } + + if (json_is_string(orig_json_name)) { + profile_name = app_profile_config_unique_profile_name(config, + json_string_value(orig_json_name), + filename, + TRUE, + &dirty); + } else if (!orig_json_name) { + char *profile_name_template; + profile_name_template = nvasprintf("inline_%d", new_id); + profile_name = app_profile_config_unique_profile_name(config, + profile_name_template, + filename, + FALSE, + &dirty); + free(profile_name_template); + } else { + json_decref(new_json_rule); + goto done; + } + + new_json_settings = json_settings_parse(orig_json_settings, filename); + + if (!profile_name || !new_json_settings) { + free(profile_name); + json_decref(new_json_settings); + json_decref(new_json_rule); + goto done; + } + + new_json_profile = json_object(); + json_object_set_new(new_json_profile, "settings", new_json_settings); + + json_object_set_new(new_json_profiles, profile_name, new_json_profile); + } else if (json_is_string(orig_json_profile)) { + // named profile + profile_name = strdup(json_string_value(orig_json_profile)); + } else { + json_decref(new_json_rule); + goto done; + } + + json_object_set_new(new_json_rule, "profile", json_string(profile_name)); + free(profile_name); + + json_object_set_new(new_json_rule, "id", json_integer(new_id)); + + json_array_append_new(new_json_rules, new_json_rule); + } + } + + json_object_set(new_file, "profiles", new_json_profiles); + json_object_set(new_file, "rules", new_json_rules); + json_object_set_new(new_file, "dirty", dirty ? json_true() : json_false()); + json_object_set_new(new_file, "new", json_false()); + + // Don't use the atime in the stat_buf; instead measure it here + json_object_set_new(new_file, "atime", json_integer(time(NULL))); + + // If we didn't fail anywhere above, add the file to our configuration + app_profile_config_insert_file_object(config, new_file); + + // Add the profiles in this file to the global profiles list + { + const char *key; + json_t *value; + json_object_foreach(new_json_profiles, key, value) { + json_object_set_new(config->profile_locations, key, json_string(filename)); + } + } + + // Add the rules in this file to the global rules list + size = json_array_size(new_json_rules); + for (i = 0; i < size; i++) { + char *key; + json_t *new_rule; + + new_rule = json_array_get(new_json_rules, i); + key = rule_id_to_key_string(json_integer_value(json_object_get(new_rule, "id"))); + json_object_set_new(config->rule_locations, key, json_string(filename)); + free(key); + } + config->next_free_rule_id = next_free_rule_id; + +done: + json_decref(orig_file); + json_decref(new_file); + json_decref(new_json_rules); + json_decref(new_json_profiles); + free(json_text); + free(orig_text); +} + +// Load app profile settings from a directory +static void app_profile_config_load_files_from_directory(AppProfileConfig *config, + const char *dirname) +{ + FILE *fp; + struct stat stat_buf; + struct dirent **namelist; + int i, n, ret; + + n = scandir(dirname, &namelist, NULL, alphasort); + + if (n < 0) { + nv_error_msg("Failed to open directory \"%s\"", dirname); + return; + } + + for (i = 0; i < n; i++) { + char *d_name = namelist[i]->d_name; + char *full_path; + + // Skip "." and ".." + if ((d_name[0] == '.') && + ((d_name[1] == '\0') || + ((d_name[1] == '.') && (d_name[2] == '\0')))) { + continue; + } + + full_path = nvstrcat(dirname, "/", d_name, NULL); + ret = open_and_stat(full_path, "r", &fp, &stat_buf); + + if (ret < 0) { + free(full_path); + free(namelist[i]); + continue; + } + + app_profile_config_load_file(config, + full_path, + &stat_buf, + fp); + fclose(fp); + free(full_path); + free(namelist[i]); + } + + free(namelist); +} + +static json_t *app_profile_config_load_global_options(const char *global_config_file) +{ + json_error_t error; + json_t *options = json_object(); + int ret; + FILE *fp; + struct stat stat_buf; + char *option_text; + json_t *options_from_file; + json_t *option; + + // By default, app profiles are disabled + json_object_set_new(options, "enabled", json_false()); + + if (!global_config_file) { + return options; + } + + ret = open_and_stat(global_config_file, "r", &fp, &stat_buf); + if ((ret < 0) || !S_ISREG(stat_buf.st_mode)) { + return options; + } + + option_text = slurp(fp); + fclose(fp); + + options_from_file = json_loads(option_text, 0, &error); + free(option_text); + + if (!options_from_file) { + nv_error_msg("App profile parse error in %s: %s on %s, line %d\n", + global_config_file, error.text, error.source, error.line); + return options; + } + + // Load the "enabled" option + option = json_object_get(options_from_file, "enabled"); + if (option && (json_is_true(option) || json_is_false(option))) { + json_object_set(options, "enabled", option); + } + + json_decref(options_from_file); + + return options; +} + +AppProfileConfig *nv_app_profile_config_load(const char *global_config_file, + char **search_path, + size_t search_path_count) +{ + size_t i; + AppProfileConfig *config = malloc(sizeof(AppProfileConfig)); + + if (!config) { + return NULL; + } + + // Initialize the config + config->next_free_rule_id = 0; + + config->parsed_files = json_array(); + config->profile_locations = json_object(); + config->rule_locations = json_object(); + + if (global_config_file) { + config->global_config_file = nvstrdup(global_config_file); + } else { + config->global_config_file = NULL; + } + + config->global_options = app_profile_config_load_global_options(global_config_file); + + config->search_path = malloc(sizeof(char *) * search_path_count); + config->search_path_count = search_path_count; + + for (i = 0; i < search_path_count; i++) { + config->search_path[i] = strdup(search_path[i]); + } + + for (i = 0; i < search_path_count; i++) { + int ret; + struct stat stat_buf; + const char *filename = search_path[i]; + FILE *fp; + + ret = open_and_stat(filename, "r", &fp, &stat_buf); + if (ret < 0) { + continue; + } + + if (S_ISDIR(stat_buf.st_mode)) { + // Parse files in the directory + fclose(fp); + app_profile_config_load_files_from_directory(config, filename); + } else { + // Load the individual file + app_profile_config_load_file(config, filename, &stat_buf, fp); + fclose(fp); + continue; + } + } + + return config; +} + +static int file_in_search_path(AppProfileConfig *config, const char *filename) +{ + size_t i; + for (i = 0; i < config->search_path_count; i++) { + if (!strcmp(filename, config->search_path[i])) { + return TRUE; + } + } + + return FALSE; +} + +/* + * Creates parent directories as needed, similarly to "mkdir -p" + */ +static int nv_mkdirp(const char *dirname) +{ + int ret = 0; + char *parent_name; + const char *cur, *next; + struct stat stat_buf; + cur = dirname; + + while (*cur && (next = strchr(cur + 1, '/'))) { + parent_name = nvstrndup(dirname, next - dirname); + ret = mkdir(parent_name, 0777); + if ((ret < 0) && (errno != EEXIST)) { + nv_error_msg("Could not create parent directory \"%s\" for full path \"%s\" (%s)", + parent_name, dirname, strerror(errno)); + free(parent_name); + return ret; + } + cur = next; + free(parent_name); + } + + ret = mkdir(dirname, 0777); + if (ret < 0) { + if (errno != EEXIST) { + nv_error_msg("Could not create directory \"%s\" (%s)", + dirname, strerror(errno)); + } else { + ret = stat(dirname, &stat_buf); + if (ret == 0) { + if (!S_ISDIR(stat_buf.st_mode)) { + nv_error_msg("Could not create directory \"%s\" (file " + "exists, but not as a directory)", + dirname); + ret = -1; + } + } + } + } + + return ret; +} + +char *nv_app_profile_config_get_backup_filename(AppProfileConfig *config, const char *filename) +{ + char *basename = NULL; + char *dirname = NULL; + char *backup_name = NULL; + + if ((config->global_config_file && + !strcmp(config->global_config_file, filename)) || + file_in_search_path(config, filename)) { + // Files in the top-level search path, and the global config file, can + // be renamed from "$FILE" to "$FILE.backup" without affecting the + // configuration + backup_name = nvasprintf("%s.backup", filename); + } else { + // Files in a search path directory *cannot* be renamed from "$FILE" to + // "$FILE.backup" without affecting the configuration due to the search + // path rules. Instead, attempt to move them to a subdirectory called + // ".backup". + dirname = nv_dirname(filename); + basename = nv_basename(filename); + assert(file_in_search_path(config, dirname)); + backup_name = nvasprintf("%s/.backup/%s", dirname, basename); + } + + free(dirname); + free(basename); + return backup_name; +} + +static int app_profile_config_backup_file(AppProfileConfig *config, + const char *filename) +{ + int ret; + char *backup_name = nv_app_profile_config_get_backup_filename(config, filename); + char *backup_dirname = nv_dirname(backup_name); + + ret = nv_mkdirp(backup_dirname); + if (ret < 0) { + nv_error_msg("Could not create backup directory \"%s\" (%s)", + backup_name, strerror(errno)); + goto done; + } + + ret = rename(filename, backup_name); + if (ret < 0) { + if (errno == ENOENT) { + // Clear the error; the file does not exist + ret = 0; + } else { + nv_error_msg("Could not rename file \"%s\" to \"%s\" for backup (%s)", + filename, backup_name, strerror(errno)); + } + } + + nv_info_msg("", "Backing up configuration file \"%s\" as \"%s\"\n", filename, backup_name); + +done: + free(backup_dirname); + free(backup_name); + return ret; +} + +static int app_profile_config_save_updates_to_file(AppProfileConfig *config, + const char *filename, + const char *update_text, + int backup) +{ + int file_is_new = FALSE; + struct stat stat_buf; + char *dirname = NULL; + FILE *fp; + int ret; + + ret = stat(filename, &stat_buf); + + if ((ret < 0) && (errno != ENOENT)) { + nv_error_msg("Could not stat file \"%s\" (%s)", filename, strerror(errno)); + goto done; + } else if ((ret < 0) && (errno == ENOENT)) { + file_is_new = TRUE; + // Check if the prefix is in the search path + dirname = nv_dirname(filename); + + if (file_in_search_path(config, dirname)) { + // This file is in a directory in the search path + ret = stat(dirname, &stat_buf); + if ((ret < 0) && (errno != ENOENT)) { + nv_error_msg("Could not stat file \"%s\" (%s)", dirname, strerror(errno)); + goto done; + } else if ((ret < 0) && errno == ENOENT) { + // Attempt to create the directory in the search path + ret = nv_mkdirp(dirname); + if (ret < 0) { + goto done; + } + } else if (S_ISREG(stat_buf.st_mode)) { + // If the search path entry is currently a regular file, + // unlink it and create a directory instead + if (backup) { + ret = app_profile_config_backup_file(config, dirname); + if (ret < 0) { + goto done; + } + } + ret = unlink(dirname); + if (ret < 0) { + nv_error_msg("Could not remove the file \"%s\" (%s)", dirname, strerror(errno)); + goto done; + } + ret = nv_mkdirp(dirname); + if (ret < 0) { + goto done; + } + } + } else { + // Attempt to create parent directories for this file + ret = nv_mkdirp(dirname); + if (ret < 0) { + goto done; + } + } + } else if (!S_ISREG(stat_buf.st_mode)) { + // XXX: if this is a directory, we could recursively remove files here, + // but that seems a little dangerous. Instead, complain and bail out + // here. + ret = -1; + nv_error_msg("Refusing to write to file \"%s\" since it is not a regular file", filename); + } + + if (!file_is_new && backup) { + ret = app_profile_config_backup_file(config, filename); + if (ret < 0) { + goto done; + } + } + ret = open_and_stat(filename, "w", &fp, &stat_buf); + if (ret < 0) { + nv_error_msg("Could not write to the file \"%s\" (%s)", filename, strerror(errno)); + goto done; + } + nv_info_msg("", "Writing to configuration file \"%s\"\n", filename); + fprintf(fp, "%s\n", update_text); + fclose(fp); + +done: + free(dirname); + return ret; +} + +int nv_app_profile_config_save_updates(AppProfileConfig *config, json_t *updates, int backup) +{ + json_t *update; + const char *filename; + const char *update_text; + size_t i, size; + int ret = 0; + + for (i = 0, size = json_array_size(updates); (ret == 0) && (i < size); i++) { + update = json_array_get(updates, i); + filename = json_string_value(json_object_get(update, "filename")); + update_text = json_string_value(json_object_get(update, "text")); + ret = app_profile_config_save_updates_to_file(config, filename, update_text, backup); + } + + assert(ret <= 0); + + return ret; +} + +AppProfileConfig *nv_app_profile_config_dup(AppProfileConfig *config) +{ + size_t i; + AppProfileConfig *new_config; + + new_config = malloc(sizeof(AppProfileConfig)); + new_config->parsed_files = json_deep_copy(config->parsed_files); + new_config->profile_locations = json_deep_copy(config->profile_locations); + new_config->rule_locations = json_deep_copy(config->rule_locations); + new_config->next_free_rule_id = config->next_free_rule_id; + + new_config->global_config_file = + config->global_config_file ? strdup(config->global_config_file) : NULL; + new_config->global_options = json_deep_copy(config->global_options); + + new_config->search_path = malloc(sizeof(char *) * config->search_path_count); + new_config->search_path_count = config->search_path_count; + + for (i = 0; i < config->search_path_count; i++) { + new_config->search_path[i] = strdup(config->search_path[i]); + } + + return new_config; +} + +void nv_app_profile_config_set_enabled(AppProfileConfig *config, + int enabled) +{ + json_t *global_options = config->global_options; + + json_object_set_new(global_options, "enabled", + enabled ? json_true() : json_false()); +} + +int nv_app_profile_config_get_enabled(AppProfileConfig *config) +{ + json_t *global_options = config->global_options; + json_t *enabled_json; + + enabled_json = json_object_get(global_options, "enabled"); + assert(enabled_json); + + return json_is_true(enabled_json); +} + +void nv_app_profile_config_free(AppProfileConfig *config) +{ + size_t i; + json_decref(config->global_options); + json_decref(config->parsed_files); + json_decref(config->profile_locations); + json_decref(config->rule_locations); + + for (i = 0; i < config->search_path_count; i++) { + free(config->search_path[i]); + } + + free(config->search_path); + free(config->global_config_file); + + free(config); +} + +static json_t *app_profile_config_lookup_file(AppProfileConfig *config, const char *filename) +{ + size_t i, size; + json_t *json_file, *json_filename; + + size = json_array_size(config->parsed_files); + + for (i = 0; i < size; i++) { + json_file = json_array_get(config->parsed_files, i); + json_filename = json_object_get(json_file, "filename"); + if (!strcmp(json_string_value(json_filename), filename)) { + return json_file; + } + } + + return NULL; +} + +static void app_profile_config_delete_file(AppProfileConfig *config, const char *filename) +{ + size_t i, size; + json_t *json_file, *json_filename; + + size = json_array_size(config->parsed_files); + + for (i = 0; i < size; i++) { + json_file = json_array_get(config->parsed_files, i); + json_filename = json_object_get(json_file, "filename"); + if (!strcmp(json_string_value(json_filename), filename)) { + json_array_remove(config->parsed_files, i); + return; + } + } +} + +static void app_profile_config_get_per_file_config(AppProfileConfig *config, + const char *filename, + json_t **file, + json_t **rules, + json_t **profiles) +{ + *file = app_profile_config_lookup_file(config, filename); + + if (!*file) { + *rules = NULL; + *profiles = NULL; + } else { + *rules = json_object_get(*file, "rules"); + *profiles = json_object_get(*file, "profiles"); + } +} + +/* + * Convert the internal representation of an application profile to a + * representation suitable for writing to disk. + */ +static json_t *app_profile_config_profile_output(const char *profile_name, const json_t *orig_profile) +{ + json_t *new_profile = json_object(); + + json_object_set_new(new_profile, "name", json_string(profile_name)); + json_object_set(new_profile, "settings", json_object_get(orig_profile, "settings")); + + return new_profile; +} + +static json_t *app_profile_config_rule_output(const json_t *orig_rule) +{ + json_t *new_rule = json_object(); + + json_object_set(new_rule, "pattern", json_object_get(orig_rule, "pattern")); + json_object_set(new_rule, "profile", json_object_get(orig_rule, "profile")); + + return new_rule; +} + +static char *config_to_cfg_file_syntax(json_t *old_rules, json_t *old_profiles) +{ + char *output = NULL; + const char *profile_name; + json_t *root, *rules_array, *profiles_array; + json_t *old_rule, *old_profile; + json_t *new_rule, *new_profile; + size_t i, size; + + root = json_object(); + if (!root) { + goto fail; + } + + rules_array = json_array(); + if (!rules_array) { + goto fail; + } + json_object_set_new(root, "rules", rules_array); + + profiles_array = json_array(); + if (!profiles_array) { + goto fail; + } + json_object_set_new(root, "profiles", profiles_array); + + if (old_rules) { + size = json_array_size(old_rules); + for (i = 0; i < size; i++) { + old_rule = json_array_get(old_rules, i); + new_rule = app_profile_config_rule_output(old_rule); + json_array_append_new(rules_array, new_rule); + } + } + + if (old_profiles) { + json_object_foreach(old_profiles, profile_name, old_profile) { + new_profile = app_profile_config_profile_output(profile_name, old_profile); + json_array_append_new(profiles_array, new_profile); + } + } + + output = json_dumps(root, JSON_ENSURE_ASCII | JSON_INDENT(4)); + +fail: + json_decref(root); + return output; +} + +static void add_files_from_config(AppProfileConfig *config, json_t *all_files, json_t *changed_files) +{ + json_t *file, *filename; + size_t i, size; + for (i = 0, size = json_array_size(config->parsed_files); i < size; i++) { + file = json_array_get(config->parsed_files, i); + filename = json_object_get(file, "filename"); + json_object_set_new(all_files, json_string_value(filename), json_true()); + if (json_is_true(json_object_get(file, "dirty"))) { + json_object_set_new(changed_files, json_string_value(filename), json_true()); + } + } +} + +static json_t *app_profile_config_validate_global_options(AppProfileConfig *new_config, + AppProfileConfig *old_config) +{ + json_t *update = NULL; + char *option_text; + + assert((!new_config->global_config_file && !old_config->global_config_file) || + (!strcmp(new_config->global_config_file, old_config->global_config_file))); + + if (new_config->global_config_file && + !json_equal(new_config->global_options, old_config->global_options)) { + update = json_object(); + json_object_set_new(update, "filename", json_string(new_config->global_config_file)); + option_text = json_dumps(new_config->global_options, JSON_ENSURE_ASCII | JSON_INDENT(4)); + json_object_set_new(update, "text", json_string(option_text)); + free(option_text); + } + + return update; +} + +json_t *nv_app_profile_config_validate(AppProfileConfig *new_config, + AppProfileConfig *old_config) +{ + json_t *all_files, *changed_files; + json_t *new_file, *new_rules, *old_rules; + json_t *old_file, *new_profiles, *old_profiles; + json_t *updates, *update; + const char *filename; + char *update_text; + json_t *unused; + + updates = json_array(); + + // Determine if the global config file needs to be updated + update = app_profile_config_validate_global_options(new_config, old_config); + if (update) { + json_array_append_new(updates, update); + } + + // Build a set of files to examine: this is the union of files specified + // by the old configuration and the new. + all_files = json_object(); + changed_files = json_object(); + add_files_from_config(new_config, all_files, changed_files); + add_files_from_config(old_config, all_files, changed_files); + + // For each file in the set, determine if it needs to be updated + json_object_foreach(all_files, filename, unused) { + app_profile_config_get_per_file_config(new_config, filename, &new_file, &new_rules, &new_profiles); + app_profile_config_get_per_file_config(old_config, filename, &old_file, &old_rules, &old_profiles); + + // Simply compare the JSON objects + if (!json_equal(old_rules, new_rules) || !json_equal(old_profiles, new_profiles)) { + json_object_set_new(changed_files, filename, json_true()); + } + } + + // For each file that changed, generate an update record with the new JSON + json_object_foreach(changed_files, filename, unused) { + update = json_object(); + + json_object_set_new(update, "filename", json_string(filename)); + app_profile_config_get_per_file_config(new_config, filename, &new_file, &new_rules, &new_profiles); + + update_text = config_to_cfg_file_syntax(new_rules, new_profiles); + json_object_set_new(update, "text", json_string(update_text)); + + json_array_append_new(updates, update); + free(update_text); + } + + json_decref(all_files); + json_decref(changed_files); + + return updates; +} + +static int file_object_is_empty(const json_t *file) +{ + const json_t *rules; + const json_t *profiles; + + rules = json_object_get(file, "rules"); + profiles = json_object_get(file, "profiles"); + + return (!json_array_size(rules) && !json_object_size(profiles)); +} + +/* + * Checks whether the given file is "empty" (contains no rules and profiles) + * and "new" (created in the configuration and not loaded from disk), and + * removes it from the configuration if both criteria are satisfied. + */ +static void app_profile_config_prune_empty_file(AppProfileConfig *config, const json_t *file) +{ + char *filename; + if (json_is_true(json_object_get(file, "new")) && file_object_is_empty(file)) { + filename = strdup(json_string_value(json_object_get(file, "filename"))); + app_profile_config_delete_file(config, filename); + free(filename); + } +} + +int nv_app_profile_config_update_profile(AppProfileConfig *config, + const char *filename, + const char *profile_name, + json_t *new_profile) +{ + json_t *file; + json_t *old_file = NULL; + json_t *file_profiles; + const char *old_filename; + + old_filename = json_string_value(json_object_get(config->profile_locations, profile_name)); + + if (old_filename) { + // Existing profile + old_file = app_profile_config_lookup_file(config, old_filename); + assert(old_file); + } + + // If there is an existing profile with a differing filename, delete it first + if (old_filename && (strcmp(filename, old_filename) != 0)) { + file = app_profile_config_lookup_file(config, old_filename); + file_profiles = json_object_get(file, "profiles"); + if (file) { + json_object_del(file_profiles, profile_name); + } + } + + file = app_profile_config_lookup_file(config, filename); + if (!file) { + file = app_profile_config_new_file(config, filename); + } + + file_profiles = json_object_get(file, "profiles"); + json_object_set(file_profiles, profile_name, new_profile); + json_object_set(config->profile_locations, profile_name, json_string(filename)); + + if (old_file) { + app_profile_config_prune_empty_file(config, old_file); + } + + return !old_filename; +} + +void nv_app_profile_config_delete_profile(AppProfileConfig *config, + const char *profile_name) +{ + json_t *file = NULL; + const char *filename = json_string_value(json_object_get(config->profile_locations, profile_name)); + + if (filename) { + file = app_profile_config_lookup_file(config, filename); + if (file) { + json_object_del(json_object_get(file, "profiles"), profile_name); + } + } + + json_object_del(config->profile_locations, profile_name); + + if (file) { + app_profile_config_prune_empty_file(config, file); + } +} + +int nv_app_profile_config_create_rule(AppProfileConfig *config, + const char *filename, + json_t *new_rule) +{ + char *key; + json_t *file, *file_rules; + json_t *new_rule_copy; + int new_id; + + file = app_profile_config_lookup_file(config, filename); + if (!file) { + file = app_profile_config_new_file(config, filename); + } + + file_rules = json_object_get(file, "rules"); + + // Add the rule to the head of the per-file list + json_array_append(file_rules, new_rule); + new_rule_copy = json_array_get(file_rules, json_array_size(file_rules) - 1); + + new_id = config->next_free_rule_id++; + json_object_set_new(new_rule_copy, "id", json_integer(new_id)); + + key = rule_id_to_key_string(new_id); + json_object_set(config->rule_locations, key, json_string(filename)); + free(key); + + return new_id; +} + +static int lookup_rule_index_in_array(json_t *rules, int id) +{ + json_t *rule, *rule_id; + size_t i, size; + for (i = 0, size = json_array_size(rules); i < size; i++) { + rule = json_array_get(rules, i); + rule_id = json_object_get(rule, "id"); + if (json_integer_value(rule_id) == id) { + return i; + } + } + + return -1; +} + +int nv_app_profile_config_update_rule(AppProfileConfig *config, + const char *filename, + int id, + json_t *new_rule) +{ + json_t *old_file, *new_file; + json_t *old_file_rules, *new_file_rules; + json_t *new_rule_copy; + const char *old_filename; + char *key; + int idx; + int rule_moved; + + key = rule_id_to_key_string(id); + old_filename = json_string_value(json_object_get(config->rule_locations, key)); + assert(old_filename); + + old_file = app_profile_config_lookup_file(config, old_filename); + assert(old_file); + + old_file_rules = json_object_get(old_file, "rules"); + + if (filename && (strcmp(filename, old_filename) != 0)) { + // If the rule has a new file, delete the rule and re-add it + new_file = app_profile_config_lookup_file(config, filename); + rule_moved = TRUE; + if (!new_file) { + new_file = app_profile_config_new_file(config, filename); + } + + new_file_rules = json_object_get(new_file, "rules"); + + idx = lookup_rule_index_in_array(old_file_rules, id); + if (idx != -1) { + json_array_remove(old_file_rules, idx); + } + json_array_insert(new_file_rules, 0, new_rule); + new_rule_copy = json_array_get(new_file_rules, 0); + json_object_set_new(new_rule_copy, "id", json_integer(id)); + + json_object_set_new(config->rule_locations, key, json_string(filename)); + } else { + // Otherwise, just edit the existing rule + rule_moved = FALSE; + idx = lookup_rule_index_in_array(old_file_rules, id); + if (idx != -1) { + json_array_set(old_file_rules, idx, new_rule); + new_rule_copy = json_array_get(old_file_rules, idx); + json_object_set_new(new_rule_copy, "id", json_integer(id)); + } + } + + free(key); + + app_profile_config_prune_empty_file(config, old_file); + + return rule_moved; +} + + +void nv_app_profile_config_delete_rule(AppProfileConfig *config, int id) +{ + json_t *file, *file_rules; + const char *filename; + char *key; + int idx; + + key = rule_id_to_key_string(id); + + filename = json_string_value(json_object_get(config->rule_locations, key)); + assert(filename); + + file = app_profile_config_lookup_file(config, filename); + assert(file); + + file_rules = json_object_get(file, "rules"); + + idx = lookup_rule_index_in_array(file_rules, id); + if (idx != -1) { + json_array_remove(file_rules, idx); + } + + json_object_del(config->rule_locations, key); + free(key); +} + +size_t nv_app_profile_config_count_rules(AppProfileConfig *config) +{ + return json_object_size(config->rule_locations); +} + +static size_t app_profile_config_count_rules_before(AppProfileConfig *config, const char *filename) +{ + size_t i, size; + size_t num_rules = 0; + json_t *cur_file, *cur_filename; + + for (i = 0, size = json_array_size(config->parsed_files); i < size; i++) { + cur_file = json_array_get(config->parsed_files, i); + cur_filename = json_object_get(cur_file, "filename"); + if (!strcmp(filename, json_string_value(cur_filename))) { + break; + } + num_rules += json_array_size(json_object_get(cur_file, "rules")); + } + + return num_rules; +} + +static void app_profile_config_insert_rule(AppProfileConfig *config, + json_t *rule, + size_t new_pri, + const char *old_filename) +{ + size_t i, j, size; + size_t num_rules = 0; + char *key; + const char *filename; + json_t *file, *file_rules; + json_t *target[2]; + size_t rules_before_target[2]; + + for (i = 0, j = 0, size = json_array_size(config->parsed_files); i < size; i++) { + file = json_array_get(config->parsed_files, i); + file_rules = json_object_get(file, "rules"); + if ((num_rules <= new_pri) && + (num_rules + json_array_size(file_rules) >= new_pri)) { + // Potential target file for this rule + rules_before_target[j] = num_rules; + target[j++] = file; + if (j >= 2) { + break; + } + } + num_rules += json_array_size(file_rules); + } + + assert((j > 0) && (j <= 2)); + + // If possible, we prefer to keep the rule in the same file as before + for (i = 0; i < j; i++) { + filename = json_string_value(json_object_get(target[i], "filename")); + if (!strcmp(filename, old_filename)) { + break; + } + } + i = (i == j) ? 0 : i; + + file_rules = json_object_get(target[i], "rules"); + json_array_insert_new(file_rules, new_pri - rules_before_target[i], rule); + // Update the hashtable to point to the new file + key = rule_id_to_key_string(json_integer_value(json_object_get(rule, "id"))); + filename = json_string_value(json_object_get(target[i], "filename")); + json_object_set_new(config->rule_locations, key, json_string(filename)); + free(key); +} + +size_t nv_app_profile_config_get_rule_priority(AppProfileConfig *config, + int id) +{ + json_t *file, *file_rules; + const char *filename; + int idx; + char *key; + + key = rule_id_to_key_string(id); + + filename = json_string_value(json_object_get(config->rule_locations, key)); + assert(filename); + + file = app_profile_config_lookup_file(config, filename); + assert(file); + + file_rules = json_object_get(file, "rules"); + + idx = lookup_rule_index_in_array(file_rules, id); + + free(key); + + return app_profile_config_count_rules_before(config, filename) + idx; +} + +static void app_profile_config_set_abs_rule_priority_internal(AppProfileConfig *config, + int id, + size_t new_pri, + size_t current_pri, + size_t lowest_pri) +{ + json_t *rule, *rule_copy; + json_t *file, *file_rules; + const char *filename; + int idx; + char *key; + + if (new_pri == current_pri) { + return; + } else if (new_pri >= lowest_pri) { + new_pri = lowest_pri - 1; + } + + key = rule_id_to_key_string(id); + + filename = json_string_value(json_object_get(config->rule_locations, key)); + assert(filename); + + file = app_profile_config_lookup_file(config, filename); + assert(file); + + file_rules = json_object_get(file, "rules"); + idx = lookup_rule_index_in_array(file_rules, id); + assert(idx >= 0); + rule = json_array_get(file_rules, idx); + + rule_copy = json_deep_copy(rule); + json_array_remove(file_rules, idx); + + app_profile_config_insert_rule(config, rule_copy, new_pri, filename); + + app_profile_config_prune_empty_file(config, file); + + free(key); +} + +void nv_app_profile_config_set_abs_rule_priority(AppProfileConfig *config, + int id, size_t new_pri) +{ + size_t current_pri = nv_app_profile_config_get_rule_priority(config, id); + size_t lowest_pri = nv_app_profile_config_count_rules(config); + app_profile_config_set_abs_rule_priority_internal(config, id, new_pri, current_pri, lowest_pri); +} + +void nv_app_profile_config_change_rule_priority(AppProfileConfig *config, + int id, + int delta) +{ + size_t lowest_pri = nv_app_profile_config_count_rules(config); + size_t current_pri = nv_app_profile_config_get_rule_priority(config, id); + size_t new_pri; + if ((delta < 0) && (((size_t)-delta) > current_pri)) { + new_pri = 0; + } else { + new_pri = current_pri + delta; + } + app_profile_config_set_abs_rule_priority_internal(config, id, new_pri, current_pri, lowest_pri); +} + +const json_t *nv_app_profile_config_get_profile(AppProfileConfig *config, + const char *profile_name) +{ + json_t *file, *file_profiles; + json_t *filename = json_object_get(config->profile_locations, profile_name); + + if (!filename) { + return NULL; + } + + file = app_profile_config_lookup_file(config, json_string_value(filename)); + file_profiles = json_object_get(file, "profiles"); + + return json_object_get(file_profiles, profile_name); +} + + +const json_t *nv_app_profile_config_get_rule(AppProfileConfig *config, + int id) +{ + char *key = rule_id_to_key_string(id); + json_t *file, *rule, *filename; + json_t *file_rules; + int idx; + + filename = json_object_get(config->rule_locations, key); + + if (!filename) { + free(key); + return NULL; + } + + file = app_profile_config_lookup_file(config, json_string_value(filename)); + file_rules = json_object_get(file, "rules"); + + idx = lookup_rule_index_in_array(file_rules, id); + if (idx != -1) { + rule = json_array_get(file_rules, idx); + } else { + assert(0); + rule = NULL; + } + + free(key); + return rule; +} + +struct AppProfileConfigProfileIterRec { + AppProfileConfig *config; + size_t file_idx; + json_t *profiles; + void *profile_iter; +}; + +AppProfileConfigProfileIter *nv_app_profile_config_profile_iter(AppProfileConfig *config) +{ + AppProfileConfigProfileIter *iter = malloc(sizeof(AppProfileConfigProfileIter)); + + iter->config = config; + iter->file_idx = 0; + iter->profile_iter = NULL; + + return nv_app_profile_config_profile_iter_next(iter); +} + +AppProfileConfigProfileIter *nv_app_profile_config_profile_iter_next(AppProfileConfigProfileIter *iter) +{ + AppProfileConfig *config = iter->config; + json_t *file; + int advance = TRUE; + size_t size; + + size = json_array_size(config->parsed_files); + while ((iter->file_idx < size) && + !iter->profile_iter) { + file = json_array_get(config->parsed_files, iter->file_idx); + iter->profiles = json_object_get(file, "profiles"); + iter->profile_iter = json_object_iter(iter->profiles); + iter->file_idx++; + advance = FALSE; + } + + if (!iter->profile_iter) { + free(iter); + return NULL; + } + + if (advance) { + iter->profile_iter = json_object_iter_next(iter->profiles, iter->profile_iter); + } + + while ((iter->file_idx < size) && + !iter->profile_iter) { + file = json_array_get(config->parsed_files, iter->file_idx); + iter->profiles = json_object_get(file, "profiles"); + iter->profile_iter = json_object_iter(iter->profiles); + iter->file_idx++; + } + + if (!iter->profile_iter) { + free(iter); + return NULL; + } + + return iter; +} + + +struct AppProfileConfigRuleIterRec { + AppProfileConfig *config; + size_t file_idx; + int rule_idx; + json_t *rules; +}; + +AppProfileConfigRuleIter *nv_app_profile_config_rule_iter(AppProfileConfig *config) +{ + AppProfileConfigRuleIter *iter = malloc(sizeof(AppProfileConfigRuleIter)); + + iter->file_idx = 0; + iter->rule_idx = -1; + iter->config = config; + + return nv_app_profile_config_rule_iter_next(iter); +} + +AppProfileConfigRuleIter *nv_app_profile_config_rule_iter_next(AppProfileConfigRuleIter *iter) +{ + AppProfileConfig *config = iter->config; + json_t *file; + int advance = TRUE; + size_t size; + + size = json_array_size(config->parsed_files); + while ((iter->file_idx < size) && + (iter->rule_idx == -1)) { + file = json_array_get(config->parsed_files, iter->file_idx); + iter->rules = json_object_get(file, "rules"); + if (json_array_size(iter->rules)) { + iter->rule_idx = 0; + } + iter->file_idx++; + advance = FALSE; + } + + if (iter->rule_idx == -1) { + free(iter); + return NULL; + } + + if (advance) { + iter->rule_idx++; + if (iter->rule_idx >= json_array_size(iter->rules)) { + iter->rule_idx = -1; + } + } + + while ((iter->file_idx < size) && + (iter->rule_idx == -1)) { + file = json_array_get(config->parsed_files, iter->file_idx); + iter->rules = json_object_get(file, "rules"); + if (json_array_size(iter->rules)) { + iter->rule_idx = 0; + } + iter->file_idx++; + } + + if (iter->rule_idx == -1) { + free(iter); + return NULL; + } + + return iter; +} + +const char *nv_app_profile_config_profile_iter_name(AppProfileConfigProfileIter *iter) +{ + return json_object_iter_key(iter->profile_iter); +} + +json_t *nv_app_profile_config_profile_iter_val(AppProfileConfigProfileIter *iter) +{ + return json_object_iter_value(iter->profile_iter); +} + +size_t nv_app_profile_config_rule_iter_pri(AppProfileConfigRuleIter *iter) +{ + json_t *rule = nv_app_profile_config_rule_iter_val(iter); + return nv_app_profile_config_get_rule_priority(iter->config, + json_integer_value(json_object_get(rule, "id"))); +} + +json_t *nv_app_profile_config_rule_iter_val(AppProfileConfigRuleIter *iter) +{ + return json_array_get(iter->rules, iter->rule_idx); +} + +const char *nv_app_profile_config_profile_iter_filename(AppProfileConfigProfileIter *iter) +{ + json_t *file = json_array_get(iter->config->parsed_files, iter->file_idx - 1); + return json_string_value(json_object_get(file, "filename")); +} + +const char *nv_app_profile_config_rule_iter_filename(AppProfileConfigRuleIter *iter) +{ + json_t *file = json_array_get(iter->config->parsed_files, iter->file_idx - 1); + return json_string_value(json_object_get(file, "filename")); +} + +const char *nv_app_profile_config_get_rule_filename(AppProfileConfig *config, + int id) +{ + const char *filename; + char *key; + + key = rule_id_to_key_string(id); + filename = json_string_value(json_object_get(config->rule_locations, key)); + free(key); + + return filename; +} + +const char *nv_app_profile_config_get_profile_filename(AppProfileConfig *config, + const char *profile_name) +{ + return json_string_value(json_object_get(config->profile_locations, profile_name)); +} + +static char *get_search_path_string(AppProfileConfig *config) +{ + size_t i; + char *new_s; + char *s = strdup(""); + for (i = 0; i < config->search_path_count; i++) { + new_s = nvasprintf("%s\t\"%s\"\n", + s, config->search_path[i]); + free(s); + s = new_s; + } + + return s; +} + +static int app_profile_config_check_is_prefix(const char *filename1, const char *filename2) +{ + char *dirname1, *dirname2; + int prefix_state; + dirname1 = nv_dirname(filename1); + dirname2 = nv_dirname(filename2); + + if (!strcmp(dirname1, filename2)) { + prefix_state = 1; + } else if (!strcmp(filename1, dirname2)) { + prefix_state = -1; + } else { + prefix_state = 0; + } + + free(dirname1); + free(dirname2); + + return prefix_state; +} + +int nv_app_profile_config_check_valid_source_file(AppProfileConfig *config, + const char *filename, + char **reason) +{ + const json_t *parsed_file; + size_t i, size; + const char *cur_filename; + char *dirname; + char *search_path_string; + int prefix_state; + + // Check if the source file can be found in the search path + dirname = NULL; + for (i = 0; i < config->search_path_count; i++) { + if (!strcmp(filename, config->search_path[i])) { + break; + } else { + if (!dirname) { + dirname = nv_dirname(filename); + } + if (!strcmp(dirname, config->search_path[i])) { + break; + } + } + } + free(dirname); + + if (i == config->search_path_count) { + search_path_string = get_search_path_string(config); + if (reason) { + *reason = nvasprintf("the filename is not valid in the search path:\n\n%s\n", + search_path_string); + } + free(search_path_string); + return FALSE; + } + + // Check if the source file is a prefix of some other file in the configuration, + // or vice versa + for (i = 0, size = json_array_size(config->parsed_files); i < size; i++) { + parsed_file = json_array_get(config->parsed_files, i); + cur_filename = json_string_value(json_object_get(parsed_file, "filename")); + + prefix_state = app_profile_config_check_is_prefix(filename, cur_filename); + + if (prefix_state) { + if (prefix_state > 0) { + if (reason) { + *reason = nvasprintf("the filename is a prefix of the existing file \"%s\".", + cur_filename); + } + } else if (reason) { + *reason = nvasprintf("the filename would be placed in the directory \"%s\", " + "but that is already a regular file in the configuration.", + cur_filename); + } + return FALSE; + } + } + + return TRUE; +} + +int nv_app_profile_config_check_backing_files(AppProfileConfig *config) +{ + json_t *file; + size_t i, size; + const char *filename; + FILE *fp; + time_t saved_atime; + struct stat stat_buf; + int ret; + int changed = FALSE; + for (i = 0, size = json_array_size(config->parsed_files); i < size; i++) { + file = json_array_get(config->parsed_files, i); + if (json_is_false(json_object_get(file, "new"))) { + // Stat the file and compare our saved atime to the file's mtime + filename = json_string_value(json_object_get(file, "filename")); + ret = open_and_stat(filename, "r", &fp, &stat_buf); + if (ret >= 0) { + fclose(fp); + saved_atime = (time_t)json_integer_value(json_object_get(file, "atime")); + if (stat_buf.st_mtime > saved_atime) { + json_object_set_new(file, "dirty", json_true()); + changed = TRUE; + } + } else { + // I/O errors: assume something changed + json_object_set_new(file, "dirty", json_true()); + changed = TRUE; + } + } + } + + return changed; +} + +/* + * Filenames in the search path ending in "*.d" are directories by convention, + * and should not be listed as valid default filenames. + */ +static inline int check_has_directory_suffix(const char *filename) +{ + size_t len = strlen(filename); + + return (len >= 2) && + (filename[len-2] == '.') && + (filename[len-1] == 'd'); +} + +json_t *nv_app_profile_config_get_source_filenames(AppProfileConfig *config) +{ + size_t i, j, size; + const char *filename; + json_t *filenames; + json_t *file; + int do_add_search_path_item; + + filenames = json_array(); + for (i = 0, size = json_array_size(config->parsed_files); i < size; i++) { + file = json_array_get(config->parsed_files, i); + json_array_append(filenames, json_object_get(file, "filename")); + } + + for (i = 0; i < config->search_path_count; i++) { + do_add_search_path_item = + !check_has_directory_suffix(config->search_path[i]); + for (j = 0; (j < size) && do_add_search_path_item; j++) { + file = json_array_get(config->parsed_files, j); + filename = json_string_value(json_object_get(file, "filename")); + if (!strcmp(config->search_path[i], filename) || + app_profile_config_check_is_prefix(config->search_path[i], + filename)) { + do_add_search_path_item = FALSE; + } + } + if (do_add_search_path_item) { + json_array_append_new(filenames, + json_string(config->search_path[i])); + } + } + + return filenames; +} + +int nv_app_profile_config_profile_name_change_fixup(AppProfileConfig *config, + const char *orig_name, + const char *new_name) +{ + int fixed_up = FALSE; + size_t i, j; + size_t num_files, num_rules; + json_t *file, *rules, *rule, *rule_profile; + const char *rule_profile_str; + + for (i = 0, num_files = json_array_size(config->parsed_files); i < num_files; i++) { + file = json_array_get(config->parsed_files, i); + rules = json_object_get(file, "rules"); + for (j = 0, num_rules = json_array_size(rules); j < num_rules; j++) { + rule = json_array_get(rules, j); + rule_profile = json_object_get(rule, "profile"); + assert(json_is_string(rule_profile)); + rule_profile_str = json_string_value(rule_profile); + if (!strcmp(rule_profile_str, orig_name)) { + json_object_set_new(rule, "profile", json_string(new_name)); + fixed_up = TRUE; + } + } + } + + return fixed_up; +} |