/* inifile.c: * * vim:smartindent ts=8:sts=2:sta:et:ai:shiftwidth=2 **************************************************************** * Copyright (C) 2005 Canonical Limited * Authors: Robert Collins * * See the file "COPYING" for further information about * the copyright and warranty status of this work. */ #include "hackerlab/bugs/panic.h" #include "hackerlab/char/str.h" #include "hackerlab/mem/alloc-limits.h" #include "hackerlab/rx-posix/regex.h" #include "hackerlab/vu/safe.h" #include "libfsutils/file-contents.h" #include "libinifile/inifile.h" static t_uchar const *inifile_section_string = "^[[:space:]]*\\[([[:alnum:][:space:])]*)\\]"; static t_uchar const *inifile_non_comment = "^[[:space:]]*(;|#).*"; static t_uchar const *key_pattern_string = "^[[:space:]]*([[:alnum:]_)]+)[[:space:]]*=([^#;]*)(;(.*))?$"; /** * \brief load an inifile from disk * * initialises the inifile with the content of the file from disk. * \param inifile the inifile to be loaded into * \param filename the file to be loaded * \return 0 on success */ int inifile_load (inifile_t *inifile, t_uchar const *filename) { /* FIXME: check for file existence 20050308 RBC*/ t_uchar *content = file_contents (filename); inifile_process_text (inifile, content); lim_free (0, content); return 0; } /** * \brief save a inifile merge to disk * \param inifile the inifile to save * \param merge_index the merge to save * \param filename the file to save */ int inifile_save_merge (inifile_t *inifile, int merge_index, t_uchar const *filename) { /* FIXME use non 'safe' calls and possibly fail * 2000308 RBC */ t_uchar *content = inifile_get_text_merge (inifile, merge_index); file_set_contents (filename, content); lim_free (0, content); return 0; } /** * \brief initialise an inifile * * \param inifile the inifile to be loaded into * \return 0 on success */ void inifile_init (inifile_t *inifile) { inifile->content = NULL; } /** * \brief process the text in an inifile * * \param inifile the inifile to apply results to * \param content the text to process. * \return 0 on success */ int inifile_process_text (inifile_t *inifile, t_uchar const *content) { rel_table temp; temp = rel_nl_split (content); rel_add_records (&inifile->content, rel_make_record ("[]", 0), 0); rel_append_x (&inifile->content, temp); rel_free_table (temp); return 0; } /** * \brief get the text representation of an inifile * * \param inifile the inifile to retrieve * \param merge_index get the nth inifile from a merged inifile. * \return a heap allocated string */ t_uchar * inifile_get_text_merge (inifile_t *inifile, int merge_index) { int line; t_uchar *result=NULL; int copying = 0; int current_merge = 0; rel_for_each(inifile->content, line) { if (inifile_line_type (inifile->content[line][0]) == INIFILE_SECTION && !str_cmp("[]", inifile->content[line][0])) copying = current_merge++ == merge_index; else if (copying) result = str_replace (result, str_alloc_cat_many (0, result, inifile->content[line][0], "\n", str_end)); } return result; } /** * \brief get the text representation of an inifile * * \param inifile the inifile to retrieve * \return a heap allocated string */ t_uchar * inifile_get_text (inifile_t *inifile) { return inifile_get_text_merge (inifile, 0); } /** * \brief free the resources of inifile */ void inifile_finalise (inifile_t *inifile) { rel_free_table (inifile->content); inifile->content = NULL; } /** * \brief helper function for getting a match from a regex pattern * * \param pattern a cached regex pattern * \param pattern_string the regex string to compile if needed * \param input the string to match against * \param match the nth match to return */ static t_uchar * inifile_regex_get_match (regex_t **pattern, t_uchar const *pattern_string, t_uchar const *input, unsigned int match) { regmatch_t matches[match]; int regexres; if (!*pattern) { int re_error; *pattern = lim_malloc (0, sizeof (**pattern)); re_error = regcomp (*pattern, pattern_string, REG_EXTENDED); invariant (!re_error); } if ((regexres = regexec (*pattern, input, match, matches, 0))) { char buf[50]; if (regexres == REG_NOMATCH) return NULL; regerror (regexres, *pattern, buf, 50); safe_printfmt (2, "failed during regex match for section name: %d %s\n", regexres, buf); return NULL; } return str_save_n (0, input + matches[match - 1].rm_so, matches[match - 1].rm_eo - matches[match - 1].rm_so); } /** * \brief get the section name from a line * \param line the line * \return t_char * allocated on the heap, or NULL if not a section line. */ t_uchar * inifile_section_name (t_uchar const *line) { static regex_t * pattern = NULL; return inifile_regex_get_match (&pattern, inifile_section_string, line, 2); } /* the standard key patten */ static regex_t * key_pattern = NULL; /* group 2 is the keyname, */ /** * \brief get the key name from a line * * note that comment lines have a key of "" * \param line the line * \return t_char * allocated on the heap, or NULL if not a key line. */ t_uchar * inifile_key_name (t_uchar const *line) { static regex_t * comment = NULL; t_uchar *result; result = inifile_regex_get_match (&key_pattern, key_pattern_string, line, 2); if (!result) { result = inifile_regex_get_match (&comment, inifile_non_comment, line, 2); if (result) result = str_replace (result, str_save (0, "")); } return result; } /** * \brief get the value from a line * * note that comment lines have no value * \param line the line * \return t_char * allocated on the heap. */ t_uchar * inifile_value (t_uchar const *line) { static regex_t * comment = NULL; t_uchar *result; result = inifile_regex_get_match (&key_pattern, key_pattern_string, line, 3); if (!result) { result = inifile_regex_get_match (&comment, inifile_non_comment, line, 2); if (result) result = str_replace (result, str_save (0, "")); } return result; } /** * \brief get the comment from a line * * \param line the line * \return t_char * allocated on the heap. */ t_uchar * inifile_comment (t_uchar const *line) { static regex_t * comment = NULL; t_uchar *result; result = inifile_regex_get_match (&key_pattern, key_pattern_string, line, 5); if (!result) { result = inifile_regex_get_match (&comment, inifile_non_comment, line, 2); if (result) result = str_replace (result, str_save (0, line)); } return result; } /** * \brief get the list of keys a particular section has * * keys called "" are comment lines * \param inifile the inifile to query * \param section the section to return. "" is the top of the file * \return a rel_table containing the sections' key list. */ rel_table inifile_get_section (inifile_t *inifile, t_uchar const *section) { rel_table result = NULL; int line; t_uchar * current_section = str_save (0, ""); rel_for_each (inifile->content, line) { if (inifile_line_type (inifile->content[line][0]) == INIFILE_SECTION) { current_section = str_replace (current_section, inifile_section_name (inifile->content[line][0])); } else if (!str_casecmp(current_section, section)) { int index = 0; t_uchar *temp_key=inifile_key_name (inifile->content[line][0]); if (!temp_key) continue; while (index < rel_n_records (result) && str_casecmp (temp_key, result[index][0])) ++index; if (index == rel_n_records (result)) rel_add_records (&result, rel_make_record (temp_key, 0), 0); lim_free (0, temp_key); } } lim_free (0, current_section); return result; } /** * \brief get the list of values (and comments) for a keyname in a particular section * * to get just comments, use "" as the key * \param inifile the inifile to query * \param section the section to return. "" is the top of the file * \param key the key to query. * \return a rel_table containing the keys values in field 0 and comment in field 1. */ rel_table inifile_get_key_values (inifile_t *inifile, t_uchar const *section, t_uchar const *key) { rel_table result = NULL; int line; t_uchar * current_section = str_save (0, ""); rel_for_each (inifile->content, line) { if (inifile_line_type (inifile->content[line][0]) == INIFILE_SECTION) { current_section = str_replace (current_section, inifile_section_name (inifile->content[line][0])); } else if (!str_casecmp(current_section, section)) { t_uchar *temp_key=inifile_key_name (inifile->content[line][0]); t_uchar *temp_value; t_uchar *temp_comment; if (str_casecmp (temp_key, key)) { lim_free (0, temp_key); continue; } temp_value = inifile_value (inifile->content[line][0]); temp_comment = inifile_comment (inifile->content[line][0]); rel_add_records (&result, rel_make_record (temp_value, temp_comment/*, lineno */, 0), 0); lim_free (0, temp_key); lim_free (0, temp_value); lim_free (0, temp_comment); } } lim_free (0, current_section); return result; } /** * \brief identify the type of a line from an ini file * \param line a line to identify * \return inifile_line_type_t */ inifile_line_type_t inifile_line_type (t_uchar const *line) { int position; int looking_for = 0; t_uchar *temp; /* shortcuts by regex. stuff efficiency ini files are small */ if ((temp = inifile_section_name (line))) { lim_free (0, temp); return INIFILE_SECTION; } /* 0 -> ; # a-zA-Z * 1 -> = */ for (position = 0; position < str_length (line); ++position) { switch (looking_for) { case 0: if (line[position] == ';') return INIFILE_COMMENT; else if (line[position] == '#') return INIFILE_COMMENT; else if (('a' <= line[position] && line[position] <= 'z') || ('A' <= line[position] && line[position] <= 'Z')) looking_for = 1; break; case 1: if (line[position] == '=') return INIFILE_KEY; break; } } return INIFILE_COMMENT; } /** * \brief create a ini file line */ static rel_record inifile_make_line (t_uchar const *key, t_uchar const * value, t_uchar const * comment) { t_uchar * temp_line; rel_record result; if (!str_length (key)) temp_line = str_save (0, comment); else if (str_length (comment)) temp_line = str_alloc_cat_many (0, key, "=", value, ";", comment, str_end); else temp_line = str_alloc_cat_many (0, key, "=", value, str_end); result = rel_make_record (temp_line, 0); lim_free (0, temp_line); return result; } /** * \brief add a new key to an inifile. * * the key is added at the last line of the last instance of the section in belongs in. * \param inifile the file to update * \param section the section the key belongs in ("" for no section). * \param key the key name ("" for a literal comment) * \param value the value for the key * \param comment the comment to use * \return void */ void inifile_add_key (inifile_t *inifile, t_uchar const *section, t_uchar const *key, t_uchar const * value, t_uchar const * comment) { int candidate_insertion = 0; int line; t_uchar * current_section = str_save (0, ""); rel_for_each (inifile->content, line) { if (inifile_line_type (inifile->content[line][0]) == INIFILE_SECTION) { if (!str_casecmp (current_section, section)) candidate_insertion = line; current_section = str_replace (current_section, inifile_section_name (inifile->content[line][0])); } } if (!rel_n_records (inifile->content)) rel_add_records (&inifile->content, rel_make_record ("[]", 0), 0); if (!str_casecmp (current_section, section)) candidate_insertion = rel_n_records (inifile->content); if (str_length (section) && candidate_insertion == 0) { t_uchar * temp_section = str_alloc_cat_many (0, "[", section, "]", str_end); rel_insert_records (&inifile->content, rel_n_records (inifile->content), rel_make_record (temp_section, 0), 0); candidate_insertion = rel_n_records (inifile->content); lim_free (0, temp_section); } rel_insert_records (&inifile->content, candidate_insertion, inifile_make_line (key, value, comment), 0); lim_free (0, current_section); } /** * \brief update an existing key with new value and comment * \param inifile the inifile * \param the key we are changing * \param key the key to update * \param index the index (from-0) of the keys values to change * \param value the value to set * \param comment the comment to set */ void inifile_update_key (inifile_t *inifile, t_uchar const *section, t_uchar const *key, int index, t_uchar const * value, t_uchar const * comment) { int seen_keys = 0; int line; t_uchar * current_section = str_save (0, ""); rel_for_each (inifile->content, line) { if (inifile_line_type (inifile->content[line][0]) == INIFILE_SECTION) { current_section = str_replace (current_section, inifile_section_name (inifile->content[line][0])); } else if (!str_casecmp(current_section, section)) { t_uchar *temp_key=inifile_key_name (inifile->content[line][0]); if (str_casecmp (temp_key, key)) { lim_free (0, temp_key); continue; } /* matching key */ if (seen_keys++ == index) { /* assign it a value */ rel_replace_record (inifile->content, line,inifile_make_line (key, value, comment)); } lim_free (0, temp_key); } } lim_free (0, current_section); } /** * \brief remove a key from an inifile * \param inifile the inifile * \param section the section we are looking for the key in * \param key the key to remove * \param index the index (from-0) of the keys values to remove (-1 for all) */ void inifile_remove_key (inifile_t *inifile, t_uchar const *section, t_uchar const *key, int index) { int seen_keys = 0; int line; int line_to_remove = -1; t_uchar * current_section = str_save (0, ""); /* factoring this out needs a keys_iterator built */ rel_for_each (inifile->content, line) { if (inifile_line_type (inifile->content[line][0]) == INIFILE_SECTION) { current_section = str_replace (current_section, inifile_section_name (inifile->content[line][0])); } else if (!str_casecmp(current_section, section)) { t_uchar *temp_key=inifile_key_name (inifile->content[line][0]); if (str_casecmp (temp_key, key)) { lim_free (0, temp_key); continue; } /* matching key */ if (seen_keys++ == index) { line_to_remove = line; } lim_free (0, temp_key); } } lim_free (0, current_section); if (line_to_remove > -1) rel_remove_records (&inifile->content, line_to_remove, line_to_remove); } /** * \brief retrieve a single-valued key, with an optional default value */ t_uchar * inifile_get_single_string (inifile_t *inifile, t_uchar const * section, t_uchar const * key, t_uchar const * default_value) { rel_table values; t_uchar *result; values = inifile_get_key_values (inifile, section, key); if (!rel_n_records (values)) result = str_save (0, default_value); else result = str_save (0, values[rel_n_records (values) - 1][0]); rel_free_table (values); return result; } /** * \brief set a single-valued key, adding it if it doesn't exist */ void inifile_set_single_string (inifile_t *inifile, t_uchar const * section, t_uchar const * key, t_uchar const * value, t_uchar const * comment) { rel_table values; values = inifile_get_key_values (inifile, section, key); if (!rel_n_records (values)) inifile_add_key (inifile, section, key, value, comment); else inifile_update_key (inifile, section, key, rel_n_records (values) - 1, value, comment); rel_free_table (values); }