summaryrefslogtreecommitdiff
path: root/src/app-profiles.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/app-profiles.c')
-rw-r--r--src/app-profiles.c2188
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;
+}