/*================================================================== * SwamiConfig.c - Swami config variables loading/saving routines * * Swami * Copyright (C) 1999-2003 Josh Green * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * 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, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA * 02111-1307, USA or point your web browser to http://www.gnu.org. * * To contact the author of this program: * Email: Josh Green * Swami homepage: http://swami.sourceforge.net *==================================================================*/ #include "config.h" #include #include #include #include #include #include #include #include #include #include "SwamiConfig.h" #include "SwamiLog.h" #include "SwamiObject.h" /* FIXME: SWAMI_OK/SWAMI_FAIL */ #include "i18n.h" /* a config domain instance */ typedef struct _ConfigDomain { char *domain; /* domain key string */ SwamiConfigCategory category; /* domain category */ int var_count; /* count of variables in domain */ } ConfigDomain; /* a config variable instance */ typedef struct _ConfigVar { char *domain; /* domain key string */ char *id; /* variable identifier */ guint16 defined; /* TRUE if this variable has been defined */ guint16 pos; /* position in domain (preserves order) */ GTokenType type; /* variable type */ GTokenValue def; /* default value */ GTokenValue value; /* current value */ } ConfigVar; static const GScannerConfig scanner_config = { " \t\n\r", /* cset_skip_characters */ G_CSET_a_2_z "_" G_CSET_A_2_Z, /* cset_identifier_first */ G_CSET_a_2_z "_-0123456789" G_CSET_A_2_Z, /* cset_identifier_nth */ "#\n", /* cpair_comment_single */ FALSE, /* case_sensitive */ TRUE, /* skip_comment_multi */ TRUE, /* skip_comment_single */ TRUE, /* scan_comment_multi */ TRUE, /* scan_identifier */ FALSE, /* scan_identifier_1char */ FALSE, /* scan_identifier_NULL */ TRUE, /* scan_symbols */ TRUE, /* scan_binary */ TRUE, /* scan_octal */ TRUE, /* scan_float */ TRUE, /* scan_hex */ TRUE, /* scan_hex_dollar */ TRUE, /* scan_string_sq */ TRUE, /* scan_string_dq */ TRUE, /* numbers_2_int */ FALSE, /* int_2_float */ FALSE, /* identifier_2_string */ TRUE, /* char_2_token */ TRUE, /* symbol_2_token */ FALSE, /* scope_0_fallback */ }; static struct { char *file_name; /* name of file to store category in */ char *def_domain; /* default config domain for category */ gboolean up2date; /* variables are up2date? */ gboolean file_exists; /* if file exists */ } category_info[SWAMI_CONFIG_CATEGORY_COUNT] = { { "swami.cfg", "swami", TRUE, FALSE }, /* SWAMI_CONFIG_CATEGORY_MAIN */ { "swami_state.cfg", "swami-state", TRUE, FALSE }, /* .._STATE */ { "plugins.cfg", "plugins", TRUE, FALSE }, /* .._PLUGIN */ { "plugin_state.cfg", "plugin-state", TRUE, FALSE } /* .._PLUGIN_STATE */ }; static GHashTable *config_domains; /* variable domains (groups) */ static GHashTable *config_vars; /* variable table */ /* local prototypes */ static guint var_hash_func (gconstpointer v); static gint var_compare_func (gconstpointer v, gconstpointer v2); static ConfigDomain *config_make_domain (const char *domain); static ConfigVar *config_make_variable (const char *domain, const char *id, gboolean *created); static ConfigVar *config_get_variable (const char *domain, const char *id); static int swami_config_load_file (const char *fname, SwamiConfigCategory category); static void parse_error (GScanner *scanner, gchar *msg); static void next_line (GScanner *scanner); static int config_save_file (char *fname, SwamiConfigCategory category); static char *config_type_string (GTokenType type); static GList *config_get_category_domains (SwamiConfigCategory category); static void config_GHFunc_get_category_domains (gpointer key, gpointer value, gpointer user_data); static gint config_GCompareFunc_domain_id (gconstpointer a, gconstpointer b); static GList *config_get_domain_variables (const char *domain); static void config_GHFunc_get_domain_vars (gpointer key, gpointer value, gpointer user_data); static gint config_GCompareFunc_var_pos_counter (gconstpointer a, gconstpointer b); /* --- functions --- */ /** * swami_config_init: * * Initialize config system */ void swami_config_init (void) { GTokenValue val; int i; config_domains = g_hash_table_new (g_str_hash, g_str_equal); config_vars = g_hash_table_new ((GHashFunc)var_hash_func, (GCompareFunc)var_compare_func); val.v_string = VERSION; for (i=0; i < SWAMI_CONFIG_CATEGORY_COUNT; i++) { /* create default domains each with version variable */ swami_config_add_domain (category_info[i].def_domain, i); swami_config_add_variable (category_info[i].def_domain, "version", G_TOKEN_STRING, &val); } } static guint var_hash_func (gconstpointer v) { ConfigVar *var = (ConfigVar *)v; guint hashval; char *s; s = g_strconcat (var->id, "^", var->domain, NULL); hashval = g_str_hash (s); g_free (s); return (hashval); } static gint var_compare_func (gconstpointer v, gconstpointer v2) { ConfigVar *var = (ConfigVar *)v, *var2 = (ConfigVar *)v2; return (strcmp (var->id, var2->id) == 0 && strcmp (var->domain, var2->domain) == 0); } /** * swami_config_add_domain: * @domain: Domain identifier string * @category: Category of this domain (determines which file to save in) * * Creates a new configuration domain, which are used to group variables in * configuration files. */ void swami_config_add_domain (const char *domain, SwamiConfigCategory category) { ConfigDomain *dom; g_return_if_fail (domain != NULL); if (category < 0 || category >= SWAMI_CONFIG_CATEGORY_COUNT) { SWAMI_PARAM_ERROR ("category"); return; } /* create or lookup (if already existing) domain */ dom = config_make_domain (domain); dom->category = category; } static ConfigDomain * config_make_domain (const char *domain) { ConfigDomain *dom; dom = g_hash_table_lookup (config_domains, domain); if (!dom) { dom = g_malloc (sizeof (ConfigDomain)); dom->domain = g_strdup (domain); dom->category = SWAMI_CONFIG_CATEGORY_MAIN; dom->var_count = 0; g_hash_table_insert (config_domains, dom->domain, dom); } return (dom); } /** * swami_config_add_variable: * @domain: Domain name (groups variables) * @id: Variable identifier string * @type: Variable type (G_TOKEN_INT | G_TOKEN_FLOAT | G_TOKEN_STRING) * @def: Default value of this variable * * Add a config variable */ void swami_config_add_variable (const char *domain, const char *id, GTokenType type, const GTokenValue *def) { ConfigVar *var; gboolean created; g_return_if_fail (domain != NULL); g_return_if_fail (id != NULL); g_return_if_fail (def != NULL); var = config_make_variable (domain, id, &created); if (!var) return; if (!created) { if (var->defined) { g_warning ("Config variable \"%s\" of domain \"%s\" re-defined", var->id, var->domain); return; } if (type != var->type) { g_warning ("Value loaded for config variable \"%s\" of domain \"%s\"" " does not match defined type, expected \"%s\"", var->id, var->domain, config_type_string (type)); if (type == G_TOKEN_STRING) /* if string, duplicate default value */ var->value.v_string = g_strdup (def->v_string); else var->value = *def; /* set to default value */ } } else /* first time variable creation */ { if (type == G_TOKEN_STRING) /* if string, duplicate default value */ var->value.v_string = g_strdup (def->v_string); else var->value = *def; /* set to default value */ } var->defined = TRUE; var->type = type; if (type == G_TOKEN_STRING) /* if string duplicate default value */ var->def.v_string = g_strdup (def->v_string); else var->def = *def; } /** * swami_config_add_static_variables: * @vars: Pointer to an array of variable definitions * @count: Count of defined variables * * Add config variables in batches */ void swami_config_add_static_variables (const SwamiConfigStaticVars *vars, int count) { char *domain = "swami"; int i; for (i=0; i < count; i++) { if (vars[i].domain) domain = vars[i].domain; swami_config_add_variable (domain, vars[i].id, vars[i].type, &vars[i].def); } } /* create a configuration variable or fetch if already existing, if created is not NULL then it is set to TRUE if a variable was created */ static ConfigVar * config_make_variable (const char *domain, const char *id, gboolean *created) { ConfigVar *var, *lookup; ConfigDomain *dom; var = g_malloc0 (sizeof (ConfigVar)); var->domain = (char *)domain; var->id = (char *)id; /* see if variable already exists */ lookup = g_hash_table_lookup (config_vars, var); if (lookup) { if (created) *created = FALSE; g_free (var); return (lookup); } dom = g_hash_table_lookup (config_domains, domain); /* get domain */ if (!dom) { SWAMI_CRITICAL ("Config domain \"%s\" doesn't exist", domain); g_free (var); return (NULL); } dom->var_count++; /* increment domain variable count */ var->pos = dom->var_count; var->defined = FALSE; g_hash_table_insert (config_vars, var, var); if (created) *created = TRUE; return (var); } static ConfigVar * config_get_variable (const char *domain, const char *id) { ConfigVar *var, lookup; g_return_val_if_fail (domain != NULL, NULL); g_return_val_if_fail (id != NULL, NULL); lookup.domain = (char *)domain; lookup.id = (char *)id; var = g_hash_table_lookup (config_vars, &lookup); if (!var || !var->defined) g_critical ("Variable \"%s\" of domain \"%s\" has not been defined", id, domain); return (var); } /** * swami_config_get_value: * @domain: Config domain id string * @id: Variable id string * * Get the value of a config variable * NOTE: if value is a string, don't modify or free it. * * Returns: The value or NULL if not defined */ GTokenValue * swami_config_get_value (const char *domain, const char *id) { ConfigVar *var; var = config_get_variable (domain, id); if (!var) return (NULL); return (&var->value); } /** * swami_config_get_int: * @domain: Config domain id string * @id: Variable id string * @found: Set to TRUE if variable found, FALSE otherwise (can be NULL) * * Get an integer value from a config variable * * Returns: The integer value of the config variable (if found is TRUE) */ int swami_config_get_int (const char *domain, const char *id, gboolean *found) { ConfigVar *var; if (found) *found = FALSE; var = config_get_variable (domain, id); if (!var) return (0); if (var->type != G_TOKEN_INT) return (0); if (found) *found = TRUE; return (var->value.v_int); } /** * swami_config_get_float: * @domain: Config domain id string * @id: Variable id string * @found: Set to TRUE if variable found, FALSE otherwise (can be NULL) * * Get a float value from a config variable * * Returns: The float value of the config variable (if found is TRUE) */ float swami_config_get_float (const char *domain, const char *id, gboolean *found) { ConfigVar *var; if (found) *found = FALSE; var = config_get_variable (domain, id); if (!var) return (0.0); if (var->type != G_TOKEN_FLOAT) return (0.0); if (found) *found = TRUE; return (var->value.v_float); } /** * swami_config_get_string: * @domain: Config domain id string * @id: Variable id string * * Get a string value from a config variable * * Returns: The string value or NULL if not defined or not of string type. * Should \b NOT be modified or freed. */ char * swami_config_get_string (const char *domain, const char *id) { ConfigVar *var; var = config_get_variable (domain, id); if (!var) return (NULL); if (var->type != G_TOKEN_STRING) return (NULL); return (var->value.v_string); } /** * swami_config_set_value: * @domain: Config domain id string * @id: Config variable id string * @value: Value to set variable to * * Set configuration value */ void swami_config_set_value (const char *domain, const char *id, const GTokenValue *value) { ConfigVar *var; gboolean up2date = TRUE; g_return_if_fail (domain != NULL); g_return_if_fail (id != NULL); g_return_if_fail (value != NULL); var = config_get_variable (domain, id); if (!var) return; switch (var->type) { case G_TOKEN_STRING: /* string? */ /* current string value is not the same as new string? */ if (strcmp (var->value.v_string, value->v_string) != 0) { g_free (var->value.v_string); var->value.v_string = g_strdup (value->v_string); up2date = FALSE; } break; case G_TOKEN_INT: /* integer? */ if (var->value.v_int != value->v_int) /* value changed? */ { var->value.v_int = value->v_int; up2date = FALSE; } break; case G_TOKEN_FLOAT: /* float? */ if (var->value.v_float != value->v_float) /* value changed? */ { var->value.v_float = value->v_float; up2date = FALSE; } break; default: g_assert_not_reached (); break; } if (!up2date) { ConfigDomain *dom = g_hash_table_lookup (config_domains, domain); g_assert (dom != NULL); category_info[dom->category].up2date = FALSE; } } /** * swami_config_set_int: * @domain: Config domain id string * @id: Config variable id string * @value: Integer value to set variable to * * Set a configuration value of type integer */ void swami_config_set_int (const char *domain, const char *id, int value) { GTokenValue val; val.v_int = value; swami_config_set_value (domain, id, &val); } /** * swami_config_set_float: * @domain: Config domain id string * @id: Config variable id string * @value: Float value to set variable to * * Set a configuration value of type float */ void swami_config_set_float (const char *domain, const char *id, float value) { GTokenValue val; val.v_float = value; swami_config_set_value (domain, id, &val); } /** * swami_config_set_string: * @domain: Config domain id string * @id: Config variable id string * @value: String to set variable to * * Set a configuration value of type string */ void swami_config_set_string (const char *domain, const char *id, const char *value) { GTokenValue val; val.v_string = (char *)value; swami_config_set_value (domain, id, &val); } /** * swami_config_load: * * Load configuration files from config directory */ void swami_config_load (void) { char *dirstr; char *fname; int i; /* FIXME for other OSes (configuration directory) */ #ifndef MINGW32 dirstr = g_strconcat (g_get_home_dir (), G_DIR_SEPARATOR_S, ".swami", G_DIR_SEPARATOR_S, NULL); #else /* FIXME: Should use application directory */ dirstr = g_strconcat (".", G_DIR_SEPARATOR_S, NULL); #endif for (i = 0; i < SWAMI_CONFIG_CATEGORY_COUNT; i++) { fname = g_strconcat (dirstr, category_info[i].file_name, NULL); /* parse the file */ swami_config_load_file (fname, i); g_free (fname); } g_free (dirstr); } /* load a swami configuration file */ static int swami_config_load_file (const char *fname, SwamiConfigCategory category) { int fhandle; GScanner *scanner; ConfigVar *var; gboolean created; guint token; gchar *id, *domain, *s; if ((fhandle = open (fname, O_RDONLY)) == -1) { g_warning (_("Failed to open config file \"%s\": %s"), fname, g_strerror (errno)); return (SWAMI_FAIL); } g_message (_("Parsing config file \"%s\""), fname); category_info[category].file_exists = TRUE; /* indicate existence of file */ domain = category_info[category].def_domain; scanner = g_scanner_new ((GScannerConfig *) &scanner_config); g_scanner_input_file (scanner, fhandle); scanner->input_name = fname; scanner->user_data = 0; /* used to indicate parse error */ /* loop over each directive in file */ while (g_scanner_peek_next_token (scanner) != G_TOKEN_EOF) { /* get token */ token = g_scanner_get_next_token (scanner); /* domain identifier? */ if (token == G_TOKEN_LEFT_BRACE) { ConfigDomain *dom; token = g_scanner_get_next_token (scanner); if (token == G_TOKEN_EOF) { parse_error (scanner, _("Unexpected end of file")); break; } if (token != G_TOKEN_IDENTIFIER) { parse_error (scanner, _("Invalid configuration domain identifier")); next_line (scanner); continue; } s = g_strdup (scanner->value.v_string); token = g_scanner_get_next_token (scanner); if (token == G_TOKEN_EOF) { parse_error (scanner, _("Unexpected end of file")); g_free (s); break; } if (token != G_TOKEN_RIGHT_BRACE) { parse_error (scanner, _("Expected end of domain identifier" " character ']'")); next_line (scanner); g_free (s); continue; } dom = config_make_domain (s); dom->category = category; domain = dom->domain; g_free (s); continue; } if (token != G_TOKEN_IDENTIFIER) { parse_error (scanner, _("Invalid configuration identifier")); next_line (scanner); continue; } id = g_strdup (scanner->value.v_string); /* get equals token '=' */ token = g_scanner_get_next_token (scanner); if (token == G_TOKEN_EOF) { /* end of file reached? */ parse_error (scanner, _("Unexpected end of file")); break; } if (token != G_TOKEN_EQUAL_SIGN) { /* make sure its an '=' sign */ parse_error (scanner, _("Unexpected token while looking for '='")); next_line (scanner); continue; } /* get value */ token = g_scanner_get_next_token (scanner); if (token == G_TOKEN_EOF) { parse_error (scanner, _("Unexpected end of file")); break; /* end of file? */ } var = config_make_variable (domain, id, &created); g_assert (var != NULL); if (created || (var->defined && token == var->type)) { /* save the value */ var->type = token; switch (token) { case G_TOKEN_STRING: var->value.v_string = g_strdup (scanner->value.v_string); break; default: var->value = scanner->value; break; } } else if (var->defined) { g_warning ("Value loaded for config variable \"%s\" of domain \"%s\"" " does not match defined type, expected \"%s\"", var->id, var->domain, config_type_string (var->type)); } } if (scanner->user_data != 0) g_warning (_("Finished parsing \"%s\", some lines were discarded"), fname); g_scanner_destroy (scanner); close (fhandle); category_info[category].up2date = TRUE; return (SWAMI_OK); } static void parse_error (GScanner *scanner, gchar *msg) { scanner->user_data = GINT_TO_POINTER (1); /* indicate parse error */ g_warning (_("Parse error on line %d: %s"), scanner->line, msg); } /* advance to next line of input file */ static void next_line (GScanner *scanner) { gint line; line = g_scanner_cur_line (scanner); while ( g_scanner_peek_next_token (scanner) != G_TOKEN_EOF) { if (scanner->next_line > line) break; g_scanner_get_next_token (scanner); } } /** * swami_config_save: * @changed_only: TRUE to save changed only domains, FALSE to save all * * Save configuration files */ void swami_config_save (gboolean changed_only) { char *dirstr, *fname; int i; #ifndef MINGW32 struct stat st; dirstr = g_strconcat (g_get_home_dir (), G_DIR_SEPARATOR_S, ".swami", G_DIR_SEPARATOR_S, NULL); if (stat (dirstr, &st) == -1) /* check if config directory exists */ { /* nope: make the directory */ if (mkdir (dirstr, 0755) == -1) { g_critical (_("Failed to create config directory \"%s\": %s"), dirstr, g_strerror (errno)); g_free (dirstr); return; } } #else /* FIXME: Should use application directory */ dirstr = g_strconcat (".", G_DIR_SEPARATOR_S, NULL); #endif for (i = 0; i < SWAMI_CONFIG_CATEGORY_COUNT; i++) { /* determine if category should be saved (forced save, file no exist, or not up to date) */ if (!changed_only || !category_info[i].file_exists || !category_info[i].up2date) { /* save the file for this category */ fname = g_strconcat (dirstr, category_info[i].file_name, NULL); config_save_file (fname, i); g_free (fname); } } g_free (dirstr); } static int config_save_file (char *fname, SwamiConfigCategory category) { GList *domains, *vars, *p, *p2; FILE *fd; gchar *s, *s2; if (!(fd = fopen (fname, "w"))) /* open config file for writing */ { g_critical (_("Failed to open config file \"%s\" for writing: %s"), fname, g_strerror (errno)); return (SWAMI_FAIL); } /* This is a comment at the top of config files, leave '#'s intact */ if (fputs (_("#\n# Swami configuration file\n#\n"), fd) == EOF) { fclose (fd); g_critical (_("Failed to write to \"%s\": %s"), fname, g_strerror (errno)); return (SWAMI_FAIL); } /* set Swami version config var to current version */ swami_config_set_string ("swami", "version", VERSION); domains = config_get_category_domains (category); p = domains; while (p) /* loop over domains for this category */ { ConfigDomain *dom = (ConfigDomain *)(p->data); /* write [domain] for all but default domain */ if (g_list_previous (p)) s = g_strdup_printf ("\n[%s]\n", dom->domain); else s = g_strdup ("\n"); if (fputs (s, fd) == EOF) { g_critical (_("Failed to write to \"%s\": %s"), fname, g_strerror (errno)); g_free (s); g_list_free (domains); fclose (fd); return (SWAMI_FAIL); } g_free (s); vars = config_get_domain_variables (dom->domain); p2 = vars; while (p2) /* loop over variables in domain */ { ConfigVar *var = (ConfigVar *)(p2->data); switch (var->type) { case G_TOKEN_STRING: #if GLIB_MAJOR_VERSION == 1 s2 = g_strescape (var->value.v_string); #else s2 = g_strescape (var->value.v_string, NULL); #endif s = g_strdup_printf ("\"%s\"", s2); g_free (s2); break; case G_TOKEN_INT: s = g_strdup_printf ("%d", (int) var->value.v_int); break; case G_TOKEN_FLOAT: s = g_strdup_printf ("%f", var->value.v_float); break; default: p2 = g_list_next (p2); continue; } s2 = g_strdup_printf ("%s = %s\n", var->id, s); g_free (s); if (fputs (s2, fd) == EOF) { g_critical (_("Failed to write to \"%s\": %s"), fname, g_strerror (errno)); g_free (s2); g_list_free (vars); g_list_free (domains); fclose (fd); return (SWAMI_FAIL); } g_free (s2); p2 = g_list_next (p2); } g_list_free (vars); p = g_list_next (p); } g_list_free (domains); fclose (fd); return (SWAMI_OK); } /* get a descriptive type string for a given type */ static char * config_type_string (GTokenType type) { switch (type) { case G_TOKEN_STRING: return (_("string")); case G_TOKEN_INT: return (_("integer")); case G_TOKEN_FLOAT: return (_("float")); default: /* "Unknown" as refering to an unknown data type */ return (_("unknown")); } } struct TempStruct1 /* temporary structure */ { SwamiConfigCategory category; GList *list; ConfigDomain *def_dom; }; /* fetch domains for a category (list should be freed when done with) */ static GList * config_get_category_domains (SwamiConfigCategory category) { struct TempStruct1 temp; temp.category = category; temp.list = NULL; temp.def_dom = NULL; g_hash_table_foreach (config_domains, config_GHFunc_get_category_domains, &temp); /* prepend the default domain (was skipped in GHFunc) */ temp.list = g_list_prepend (temp.list, temp.def_dom); return (temp.list); } /* adds domains in hash matching a category to a GList */ static void config_GHFunc_get_category_domains (gpointer key, gpointer value, gpointer user_data) { struct TempStruct1 *temp = user_data; ConfigDomain *dom = value; if (temp->category == dom->category) { /* if its the default domain then don't add it, will be added later */ if (strcmp (dom->domain, category_info[temp->category].def_domain) == 0) temp->def_dom = dom; else temp->list = g_list_insert_sorted (temp->list, dom, config_GCompareFunc_domain_id); } } /* sorts ConfigDomains by their id name */ static gint config_GCompareFunc_domain_id (gconstpointer a, gconstpointer b) { ConfigDomain *dom = (ConfigDomain *)a, *dom2 = (ConfigDomain *)b; return (strcmp (dom->domain, dom2->domain)); } struct TempStruct2 /* temporary convenience structure */ { char *domain; GList *list; }; /* fetch variables for a domain (list should be freed when done with) */ static GList * config_get_domain_variables (const char *domain) { struct TempStruct2 temp; g_return_val_if_fail (domain != NULL, NULL); temp.domain = (char *)domain; temp.list = NULL; g_hash_table_foreach (config_vars, config_GHFunc_get_domain_vars, &temp); return (temp.list); } /* adds variables in hash matching a domain to a GList */ static void config_GHFunc_get_domain_vars (gpointer key, gpointer value, gpointer user_data) { struct TempStruct2 *temp = user_data; ConfigVar *var = value; if (strcmp (temp->domain, var->domain) == 0) temp->list = g_list_insert_sorted (temp->list, var, config_GCompareFunc_var_pos_counter); } /* sorts ConfigVars by their position field */ static gint config_GCompareFunc_var_pos_counter (gconstpointer a, gconstpointer b) { ConfigVar *var = (ConfigVar *)a, *var2 = (ConfigVar *)b; return (var->pos - var2->pos); }