diff options
Diffstat (limited to 'src/core/settings/plugins/ifcfg-rh/shvar.c')
| -rw-r--r-- | src/core/settings/plugins/ifcfg-rh/shvar.c | 1585 |
1 files changed, 1585 insertions, 0 deletions
diff --git a/src/core/settings/plugins/ifcfg-rh/shvar.c b/src/core/settings/plugins/ifcfg-rh/shvar.c new file mode 100644 index 00000000..386bcac7 --- /dev/null +++ b/src/core/settings/plugins/ifcfg-rh/shvar.c @@ -0,0 +1,1585 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 1999, 2000 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "shvar.h" + +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> + +#include "nm-core-internal.h" +#include "nm-core-utils.h" +#include "nm-glib-aux/nm-enum-utils.h" +#include "nm-glib-aux/nm-io-utils.h" +#include "c-list/src/c-list.h" +#include "nms-ifcfg-rh-utils.h" + +/*****************************************************************************/ + +struct _shvarLine { + const char *key; + + CList lst; + + /* We index variables by their key in shvarFile.lst_idx. One shell variable might + * occur multiple times in a file (in which case the last occurrence wins). + * Hence, we need to keep a list of all the same keys. + * + * This is a pointer to the next shadowed line. */ + struct _shvarLine *prev_shadowed; + + /* There are three cases: + * + * 1) the line is not a valid variable assignment (that is, it doesn't + * start with a "FOO=" with possible whitespace prefix). + * In that case, @key and @key_with_prefix are %NULL, and the entire + * original line is in @line. Such entries are ignored for the most part. + * + * 2) if the line can be parsed with a "FOO=" assignment, then @line contains + * the part after '=', @key_with_prefix contains the key "FOO" with possible + * whitespace prefix, and @key points into @key_with_prefix skipping over the + * whitespace. + * + * 3) like 2, but if the value was deleted via svSetValue(), the entry is not removed, + * but only marked for deletion. That is done by clearing @line but preserving + * @key/@key_with_prefix. + * */ + char *line; + char *key_with_prefix; + + /* svSetValue() will clear the dirty flag. */ + bool dirty : 1; +}; + +typedef struct _shvarLine shvarLine; + +struct _shvarFile { + char * fileName; + CList lst_head; + GHashTable *lst_idx; + int fd; + bool modified : 1; +}; + +/*****************************************************************************/ + +static void _line_link_parse(shvarFile *s, const char *value, gsize len); + +/*****************************************************************************/ + +#define ASSERT_key_is_well_known(key) \ + nm_assert(({ \ + const char *_key = (key); \ + gboolean _is_wellknown = TRUE; \ + \ + if (!nms_ifcfg_rh_utils_is_well_known_key(_key)) { \ + _is_wellknown = FALSE; \ + g_critical("ifcfg-rh key \"%s\" is not well-known", _key); \ + } \ + \ + _is_wellknown; \ + })) + +/** + * svParseBoolean: + * @value: the input string + * @fallback: the fallback value + * + * Parses a string and returns the boolean value it contains or, + * in case no valid value is found, the fallback value. Valid values + * are: "yes", "true", "t", "y", "1" and "no", "false", "f", "n", "0". + * + * Returns: the parsed boolean value or @fallback. + */ +int +svParseBoolean(const char *value, int fallback) +{ + if (!value) + return fallback; + + if (!g_ascii_strcasecmp("yes", value) || !g_ascii_strcasecmp("true", value) + || !g_ascii_strcasecmp("t", value) || !g_ascii_strcasecmp("y", value) + || !g_ascii_strcasecmp("1", value)) + return TRUE; + else if (!g_ascii_strcasecmp("no", value) || !g_ascii_strcasecmp("false", value) + || !g_ascii_strcasecmp("f", value) || !g_ascii_strcasecmp("n", value) + || !g_ascii_strcasecmp("0", value)) + return FALSE; + + return fallback; +} + +/*****************************************************************************/ + +static gboolean +_shell_is_name(const char *key, gssize len) +{ + gssize i; + + /* whether @key is a valid identifier (name). */ + if (!key || len == 0) + return FALSE; + if (!g_ascii_isalpha(key[0]) && key[0] != '_') + return FALSE; + for (i = 1; TRUE; i++) { + if (len < 0) { + if (!key[i]) + return TRUE; + } else { + if (i >= len) + return TRUE; + } + if (!g_ascii_isalnum(key[i]) && key[i] != '_') + return FALSE; + } +} + +/*****************************************************************************/ + +/* like g_strescape(), except that it also escapes '\''' *sigh*. + * + * While at it, add $''. */ +static char * +_escape_ansic(const char *source) +{ + const char *p; + char * dest; + char * q; + + nm_assert(source); + + p = (const char *) source; + /* Each source byte needs maximally four destination chars (\777) */ + q = dest = g_malloc(strlen(source) * 4 + 1 + 3); + + *q++ = '$'; + *q++ = '\''; + + while (*p) { + switch (*p) { + case '\b': + *q++ = '\\'; + *q++ = 'b'; + break; + case '\f': + *q++ = '\\'; + *q++ = 'f'; + break; + case '\n': + *q++ = '\\'; + *q++ = 'n'; + break; + case '\r': + *q++ = '\\'; + *q++ = 'r'; + break; + case '\t': + *q++ = '\\'; + *q++ = 't'; + break; + case '\v': + *q++ = '\\'; + *q++ = 'v'; + break; + case '\\': + case '"': + case '\'': + *q++ = '\\'; + *q++ = *p; + break; + default: + if ((*p < ' ') || (*p >= 0177)) { + *q++ = '\\'; + *q++ = '0' + (((*p) >> 6) & 07); + *q++ = '0' + (((*p) >> 3) & 07); + *q++ = '0' + ((*p) & 07); + } else + *q++ = *p; + break; + } + p++; + } + *q++ = '\''; + *q++ = '\0'; + + nm_assert(q - dest <= strlen(source) * 4 + 1 + 3); + + return dest; +} + +/*****************************************************************************/ + +#define _char_req_escape(ch) NM_IN_SET(ch, '"', '\\', '$', '`') +#define _char_req_escape_old(ch) NM_IN_SET(ch, '"', '\\', '\'', '$', '`', '~') +#define _char_req_quotes(ch) NM_IN_SET(ch, ' ', '\'', '~', '\t', '|', '&', ';', '(', ')', '<', '>') + +const char * +svEscape(const char *s, char **to_free) +{ + char *new; + gsize mangle = 0; + gboolean requires_quotes = FALSE; + int newlen; + size_t i, j, slen; + + for (slen = 0; s[slen]; slen++) { + if (_char_req_escape(s[slen])) + mangle++; + else if (_char_req_quotes(s[slen])) + requires_quotes = TRUE; + else if (s[slen] < ' ') { + /* if the string contains newline we can only express it using ANSI C quotation + * (as we don't support line continuation). + * Additionally, ANSI control characters look odd with regular quotation, so handle + * them too. */ + return (*to_free = _escape_ansic(s)); + } + } + if (!mangle && !requires_quotes) { + *to_free = NULL; + return s; + } + + newlen = slen + mangle + 3; /* 3 is extra ""\0 */ + new = g_malloc(newlen); + + j = 0; + new[j++] = '"'; + for (i = 0; i < slen; i++) { + if (_char_req_escape(s[i])) + new[j++] = '\\'; + new[j++] = s[i]; + } + new[j++] = '"'; + new[j++] = '\0'; + + nm_assert(j == slen + mangle + 3); + + *to_free = new; + return new; +} + +static gboolean +_looks_like_old_svescaped(const char *value) +{ + gsize k; + + if (value[0] != '"') + return FALSE; + + for (k = 1;; k++) { + if (value[k] == '\0') + return FALSE; + if (!_char_req_escape_old(value[k])) + continue; + + if (value[k] == '"') + return (value[k + 1] == '\0'); + else if (value[k] == '\\') { + k++; + if (!_char_req_escape_old(value[k])) + return FALSE; + } else + return FALSE; + } +} + +static gboolean +_ch_octal_is(char ch) +{ + return ch >= '0' && ch < '8'; +} + +static guint8 +_ch_octal_get(char ch) +{ + nm_assert(_ch_octal_is(ch)); + return (ch - '0'); +} + +static gboolean +_ch_hex_is(char ch) +{ + return g_ascii_isxdigit(ch); +} + +static guint8 +_ch_hex_get(char ch) +{ + nm_assert(_ch_hex_is(ch)); + return ch <= '9' ? ch - '0' : (ch & 0x4F) - 'A' + 10; +} + +static void +_gstr_init(GString **str, const char *value, gsize i) +{ + nm_assert(str); + nm_assert(value); + + if (!(*str)) { + /* if @str is not yet initialized, it allocates + * a new GString and copies @i characters from + * @value over. + * + * Unescaping usually does not extend the length of a string, + * so we might be tempted to allocate a fixed buffer of length + * (strlen(value)+CONST). + * However, due to $'\Ux' escapes, the maximum length is some + * (FACTOR*strlen(value) + CONST), which is non trivial to get + * right in all cases. Also, we would have to provision for the + * very unlikely extreme case. + * Instead, use a GString buffer which can grow as needed. But for an + * initial guess, strlen(value) is a good start */ + *str = g_string_new_len(NULL, strlen(value) + 3); + if (i) + g_string_append_len(*str, value, i); + } +} + +const char * +svUnescape(const char *value, char **to_free) +{ + gsize i, j; + GString *str = NULL; + int looks_like_old_svescaped = -1; + + /* we handle bash syntax here (note that ifup has #!/bin/bash. + * Thus, see https://www.gnu.org/software/bash/manual/html_node/Quoting.html#Quoting */ + + /* @value shall start with the first character after "FOO=" */ + + nm_assert(value); + nm_assert(to_free); + + /* we don't expect any newlines. They must be filtered out before-hand. + * We also don't support line continuation. */ + nm_assert(!NM_STRCHAR_ANY(value, ch, ch == '\n')); + + i = 0; + while (TRUE) { + if (value[i] == '\0') + goto out_value; + + if (g_ascii_isspace(value[i]) || value[i] == ';') { + gboolean has_semicolon = (value[i] == ';'); + + /* starting with space is only allowed, if the entire + * string consists of spaces (possibly terminated by a comment). + * This disallows for example + * LANG=C ls -1 + * LANG= ls -1 + * but allows + * LANG= #comment + * + * As a special case, we also allow one trailing semicolon, as long + * it is only followed by whitespace or a #-comment. + * FOO=; + * FOO=a; + * FOO=b ; #hallo + */ + j = i + 1; + while (g_ascii_isspace(value[j]) + || (!has_semicolon && (has_semicolon = (value[j] == ';')))) + j++; + if (!NM_IN_SET(value[j], '\0', '#')) + goto out_error; + goto out_value; + } + + if (value[i] == '\\') { + /* backslash escape */ + _gstr_init(&str, value, i); + i++; + if (G_UNLIKELY(value[i] == '\0')) { + /* we don't support line continuation */ + goto out_error; + } + g_string_append_c(str, value[i]); + i++; + goto loop1_next; + } + + if (value[i] == '\'') { + /* single quotes */ + _gstr_init(&str, value, i); + i++; + j = i; + while (TRUE) { + if (value[j] == '\0') { + /* unterminated single quote. We don't support line continuation */ + goto out_error; + } + if (value[j] == '\'') + break; + j++; + } + g_string_append_len(str, &value[i], j - i); + i = j + 1; + goto loop1_next; + } + + if (value[i] == '"') { + /* double quotes */ + _gstr_init(&str, value, i); + i++; + while (TRUE) { + if (value[i] == '"') { + i++; + break; + } + if (value[i] == '\0') { + /* unterminated double quote. We don't support line continuation. */ + goto out_error; + } + if (NM_IN_SET(value[i], '`', '$')) { + /* we don't support shell expansion. */ + goto out_error; + } + if (value[i] == '\\') { + i++; + if (value[i] == '\0') { + /* we don't support line continuation */ + goto out_error; + } + if (NM_IN_SET(value[i], '$', '`', '"', '\\')) { + /* Drop the backslash. */ + } else if (NM_IN_SET(value[i], '\'', '~')) { + /* '\'' and '~' in double quotes are not handled special by shell. + * However, old versions of svEscape() would wrongly use double-quoting + * with backslash escaping for these characters (expecting svUnescape() + * to remove the backslash). + * + * In order to preserve previous behavior, we continue to read such + * strings different then shell does. */ + + /* Actually, we can relax this. Old svEscape() escaped the entire value + * in a particular way with double quotes. + * If the value doesn't exactly look like something as created by svEscape(), + * don't do the compat hack and preserve the backslash. */ + if (looks_like_old_svescaped < 0) + looks_like_old_svescaped = _looks_like_old_svescaped(value); + if (!looks_like_old_svescaped) + g_string_append_c(str, '\\'); + } else + g_string_append_c(str, '\\'); + } + g_string_append_c(str, value[i]); + i++; + } + goto loop1_next; + } + + if (value[i] == '$' && value[i + 1] == '\'') { + /* ANSI-C Quoting */ + _gstr_init(&str, value, i); + i += 2; + while (TRUE) { + char ch; + + if (value[i] == '\'') { + i++; + break; + } + if (value[i] == '\0') { + /* unterminated double quote. We don't support line continuation. */ + goto out_error; + } + if (value[i] == '\\') { + i++; + if (value[i] == '\0') { + /* we don't support line continuation */ + goto out_error; + } + switch (value[i]) { + case 'a': + ch = '\a'; + break; + case 'b': + ch = '\b'; + break; + case 'e': + ch = '\e'; + break; + case 'E': + ch = '\E'; + break; + case 'f': + ch = '\f'; + break; + case 'n': + ch = '\n'; + break; + case 'r': + ch = '\r'; + break; + case 't': + ch = '\t'; + break; + case 'v': + ch = '\v'; + break; + case '?': + ch = '\?'; + break; + case '"': + ch = '"'; + break; + case '\\': + ch = '\\'; + break; + case '\'': + ch = '\''; + break; + default: + if (_ch_octal_is(value[i])) { + guint v; + + v = _ch_octal_get(value[i]); + i++; + if (_ch_octal_is(value[i])) { + v = (v * 8) + _ch_octal_get(value[i]); + i++; + if (_ch_octal_is(value[i])) { + v = (v * 8) + _ch_octal_get(value[i]); + i++; + } + } + /* like bash, we cut too large numbers off. E.g. A=$'\772' becomes 0xfa */ + g_string_append_c(str, (guint8) v); + } else if (NM_IN_SET(value[i], 'x', 'u', 'U')) { + const char escape_type = value[i]; + int max_digits = escape_type == 'x' ? 2 : escape_type == 'u' ? 4 : 8; + guint64 v; + + i++; + if (!_ch_hex_is(value[i])) { + /* missing hex value after "\x" escape. This is treated like no escaping. */ + g_string_append_c(str, '\\'); + g_string_append_c(str, escape_type); + } else { + v = _ch_hex_get(value[i]); + i++; + + while (--max_digits > 0) { + if (!_ch_hex_is(value[i])) + break; + v = v * 16 + _ch_hex_get(value[i]); + i++; + } + if (escape_type == 'x') + g_string_append_c(str, v); + else { + /* we treat the unicode escapes as utf-8 encoded values. */ + g_string_append_unichar(str, v); + } + } + } else { + g_string_append_c(str, '\\'); + g_string_append_c(str, value[i]); + i++; + } + goto loop_ansic_next; + } + } else + ch = value[i]; + g_string_append_c(str, ch); + i++; +loop_ansic_next:; + } + goto loop1_next; + } + + if (NM_IN_SET(value[i], '|', '&', '(', ')', '<', '>')) { + /* shell metacharacters are not supported without quoting. + * Note that ';' is already handled above. */ + goto out_error; + } + + /* an unquoted, regular character. Just consume it directly. */ + if (str) + g_string_append_c(str, value[i]); + i++; + +loop1_next:; + } + + nm_assert_not_reached(); + +out_value: + if (i == 0) { + nm_assert(!str); + *to_free = NULL; + return ""; + } + + if (str) { + if (str->len == 0 || str->str[0] == '\0') { + g_string_free(str, TRUE); + *to_free = NULL; + return ""; + } else { + *to_free = g_string_free(str, FALSE); + return *to_free; + } + } + + if (value[i] != '\0') { + *to_free = g_strndup(value, i); + return *to_free; + } + + *to_free = NULL; + return value; + +out_error: + if (str) + g_string_free(str, TRUE); + *to_free = NULL; + return NULL; +} + +/*****************************************************************************/ + +shvarFile * +svFile_new(const char *name, int fd, const char *content) +{ + shvarFile * s; + const char *p; + const char *q; + + nm_assert(name); + nm_assert(fd >= -1); + + s = g_slice_new(shvarFile); + *s = (shvarFile){ + .fileName = g_strdup(name), + .fd = fd, + .lst_head = C_LIST_INIT(s->lst_head), + .lst_idx = g_hash_table_new(nm_pstr_hash, nm_pstr_equal), + }; + + if (content) { + for (p = content; (q = strchr(p, '\n')) != NULL; p = q + 1) + _line_link_parse(s, p, q - p); + if (p[0]) + _line_link_parse(s, p, strlen(p)); + } + + return s; +} + +const char * +svFileGetName(const shvarFile *s) +{ + nm_assert(s); + + return s->fileName; +} + +void +_nmtst_svFileSetName(shvarFile *s, const char *fileName) +{ + /* changing the file name is not supported for regular + * operation. Only allowed to use in tests, otherwise, + * the filename is immutable. */ + g_free(s->fileName); + s->fileName = g_strdup(fileName); +} + +void +_nmtst_svFileSetModified(shvarFile *s) +{ + /* marking a file as modified is only for testing. */ + s->modified = TRUE; +} + +/*****************************************************************************/ + +static void +ASSERT_shvarLine(const shvarLine *line) +{ +#if NM_MORE_ASSERTS > 5 + const char *s, *s2; + + nm_assert(line); + if (!line->key) { + nm_assert(line->line); + nm_assert(!line->key_with_prefix); + s = nm_str_skip_leading_spaces(line->line); + s2 = strchr(s, '='); + nm_assert(!s2 || !_shell_is_name(s, s2 - s)); + } else { + nm_assert(line->key_with_prefix); + nm_assert(line->key == nm_str_skip_leading_spaces(line->key_with_prefix)); + nm_assert(_shell_is_name(line->key, -1)); + } +#endif +} + +static shvarLine * +line_new_parse(const char *value, gsize len) +{ + shvarLine *line; + gsize k, e; + + nm_assert(value); + + line = g_slice_new(shvarLine); + *line = (shvarLine){ + .lst = C_LIST_INIT(line->lst), + .dirty = TRUE, + }; + + for (k = 0; k < len; k++) { + if (g_ascii_isspace(value[k])) + continue; + + if (g_ascii_isalpha(value[k]) || value[k] == '_') { + for (e = k + 1; e < len; e++) { + if (value[e] == '=') { + nm_assert(_shell_is_name(&value[k], e - k)); + line->line = g_strndup(&value[e + 1], len - e - 1); + line->key_with_prefix = g_strndup(value, e); + line->key = &line->key_with_prefix[k]; + ASSERT_shvarLine(line); + return line; + } + if (!g_ascii_isalnum(value[e]) && value[e] != '_') + break; + } + } + break; + } + line->line = g_strndup(value, len); + ASSERT_shvarLine(line); + return line; +} + +static shvarLine * +line_new_build(const char *key, const char *value) +{ + char * value_escaped = NULL; + shvarLine *line; + char * new_key; + + value = svEscape(value, &value_escaped); + + line = g_slice_new(shvarLine); + new_key = g_strdup(key), *line = (shvarLine){ + .lst = C_LIST_INIT(line->lst), + .line = value_escaped ?: g_strdup(value), + .key_with_prefix = new_key, + .key = new_key, + .dirty = FALSE, + }; + ASSERT_shvarLine(line); + return line; +} + +static gboolean +line_set(shvarLine *line, const char *value) +{ + char * value_escaped = NULL; + gboolean changed = FALSE; + + ASSERT_shvarLine(line); + nm_assert(line->key); + + line->dirty = FALSE; + + if (line->key != line->key_with_prefix) { + memmove(line->key_with_prefix, line->key, strlen(line->key) + 1); + line->key = line->key_with_prefix; + changed = TRUE; + ASSERT_shvarLine(line); + } + + value = svEscape(value, &value_escaped); + + if (line->line) { + if (nm_streq(value, line->line)) { + g_free(value_escaped); + return changed; + } + g_free(line->line); + } + + line->line = value_escaped ?: g_strdup(value); + ASSERT_shvarLine(line); + return TRUE; +} + +static void +line_free(shvarLine *line) +{ + ASSERT_shvarLine(line); + c_list_unlink_stale(&line->lst); + g_free(line->line); + g_free(line->key_with_prefix); + g_slice_free(shvarLine, line); +} + +/*****************************************************************************/ + +static void +_line_link_parse(shvarFile *s, const char *value, gsize len) +{ + shvarLine *line; + + line = line_new_parse(value, len); + if (!line->key) + goto do_link; + + if (G_UNLIKELY(!g_hash_table_insert(s->lst_idx, line, line))) { + shvarLine *existing_key; + shvarLine *existing_val; + + /* Slow-path: we have duplicate keys. Fix the mess we created. + * Unfortunately, g_hash_table_insert() now had to allocate an extra + * array to track the keys/values differently. I wish there was an + * GHashTable API to add a key only if it does not exist yet. */ + + if (!g_hash_table_lookup_extended(s->lst_idx, + line, + (gpointer *) &existing_key, + (gpointer *) &existing_val)) + nm_assert_not_reached(); + + nm_assert(existing_val == line); + nm_assert(existing_key != line); + line->prev_shadowed = existing_key; + g_hash_table_replace(s->lst_idx, line, line); + } + +do_link: + c_list_link_tail(&s->lst_head, &line->lst); +} + +/*****************************************************************************/ + +/* Open the file <name>, returning a shvarFile on success and NULL on failure. + * Add a wrinkle to let the caller specify whether or not to create the file + * (actually, return a structure anyway) if it doesn't exist. + */ +static shvarFile * +svOpenFileInternal(const char *name, gboolean create, GError **error) +{ + gboolean closefd = FALSE; + int errsv = 0; + gs_free char *content = NULL; + gs_free_error GError *local = NULL; + nm_auto_close int fd = -1; + + if (create) + fd = open(name, O_RDWR | O_CLOEXEC); /* NOT O_CREAT */ + if (fd < 0) { + /* try read-only */ + fd = open(name, O_RDONLY | O_CLOEXEC); /* NOT O_CREAT */ + if (fd < 0) + errsv = errno; + else + closefd = TRUE; + } + + if (fd < 0) { + if (create) + return svFile_new(name, -1, NULL); + + g_set_error(error, + G_FILE_ERROR, + g_file_error_from_errno(errsv), + "Could not read file '%s': %s", + name, + nm_strerror_native(errsv)); + return NULL; + } + + if (!nm_utils_fd_get_contents(closefd ? nm_steal_fd(&fd) : fd, + closefd, + 10 * 1024 * 1024, + NM_UTILS_FILE_GET_CONTENTS_FLAG_NONE, + &content, + NULL, + NULL, + &local)) { + if (create) + return svFile_new(name, -1, NULL); + + g_set_error(error, + G_FILE_ERROR, + local->domain == G_FILE_ERROR ? local->code : G_FILE_ERROR_FAILED, + "Could not read file '%s': %s", + name, + local->message); + return NULL; + } + + /* closefd is set if we opened the file read-only, so go ahead and + * close it, because we can't write to it anyway */ + nm_assert(closefd || fd >= 0); + return svFile_new(name, !closefd ? nm_steal_fd(&fd) : -1, content); +} + +/* Open the file <name>, return shvarFile on success, NULL on failure */ +shvarFile * +svOpenFile(const char *name, GError **error) +{ + return svOpenFileInternal(name, FALSE, error); +} + +/* Create a new file structure, returning actual data if the file exists, + * and a suitable starting point if it doesn't. + */ +shvarFile * +svCreateFile(const char *name) +{ + return svOpenFileInternal(name, TRUE, NULL); +} + +/*****************************************************************************/ + +static gboolean +_svKeyMatchesType(const char *key, SvKeyType match_key_type) +{ + if (NM_FLAGS_HAS(match_key_type, SV_KEY_TYPE_ANY)) + return TRUE; + +#define _IS_NUMBERED(key, tag) \ + ({ \ + gint64 _idx; \ + \ + NMS_IFCFG_RH_UTIL_IS_NUMBERED_TAG(key, tag, &_idx) \ + &&_idx >= 0; \ + }) + + if (NM_FLAGS_HAS(match_key_type, SV_KEY_TYPE_ROUTE_SVFORMAT)) { + if (_IS_NUMBERED(key, "ADDRESS") || _IS_NUMBERED(key, "NETMASK") + || _IS_NUMBERED(key, "GATEWAY") || _IS_NUMBERED(key, "METRIC") + || _IS_NUMBERED(key, "OPTIONS")) + return TRUE; + } + if (NM_FLAGS_HAS(match_key_type, SV_KEY_TYPE_IP4_ADDRESS)) { + if (_IS_NUMBERED(key, "IPADDR") || _IS_NUMBERED(key, "PREFIX") + || _IS_NUMBERED(key, "NETMASK") || _IS_NUMBERED(key, "GATEWAY")) + return TRUE; + } + if (NM_FLAGS_HAS(match_key_type, SV_KEY_TYPE_USER)) { + if (g_str_has_prefix(key, "NM_USER_")) + return TRUE; + } + if (NM_FLAGS_HAS(match_key_type, SV_KEY_TYPE_TC)) { + if (_IS_NUMBERED(key, "QDISC") || _IS_NUMBERED(key, "FILTER")) + return TRUE; + } + if (NM_FLAGS_HAS(match_key_type, SV_KEY_TYPE_SRIOV_VF)) { + if (_IS_NUMBERED(key, "SRIOV_VF")) + return TRUE; + } + if (NM_FLAGS_HAS(match_key_type, SV_KEY_TYPE_ROUTING_RULE4)) { + if (_IS_NUMBERED(key, "ROUTING_RULE_")) + return TRUE; + } + if (NM_FLAGS_HAS(match_key_type, SV_KEY_TYPE_ROUTING_RULE6)) { + if (_IS_NUMBERED(key, "ROUTING_RULE6_")) + return TRUE; + } + + return FALSE; +} + +gint64 +svNumberedParseKey(const char *key) +{ + gint64 idx; + + if (NMS_IFCFG_RH_UTIL_IS_NUMBERED_TAG(key, "ROUTING_RULE_", &idx) + || NMS_IFCFG_RH_UTIL_IS_NUMBERED_TAG(key, "ROUTING_RULE6_", &idx)) + return idx; + return -1; +} + +GHashTable * +svGetKeys(shvarFile *s, SvKeyType match_key_type) +{ + GHashTable * keys = NULL; + CList * current; + const shvarLine *line; + + nm_assert(s); + + c_list_for_each (current, &s->lst_head) { + line = c_list_entry(current, shvarLine, lst); + if (line->key && line->line && _svKeyMatchesType(line->key, match_key_type)) { + /* we don't clone the keys. The keys are only valid + * until @s gets modified. */ + if (!keys) + keys = g_hash_table_new_full(nm_str_hash, g_str_equal, NULL, NULL); + g_hash_table_add(keys, (gpointer) line->key); + } + } + return keys; +} + +static int +_get_keys_sorted_cmp(gconstpointer a, gconstpointer b, gpointer user_data) +{ + const char *k_a = *((const char *const *) a); + const char *k_b = *((const char *const *) b); + gint64 n_a; + gint64 n_b; + + n_a = svNumberedParseKey(k_a); + n_b = svNumberedParseKey(k_b); + NM_CMP_DIRECT(n_a, n_b); + NM_CMP_RETURN(strcmp(k_a, k_b)); + nm_assert_not_reached(); + return 0; +} + +const char ** +svGetKeysSorted(shvarFile *s, SvKeyType match_key_type, guint *out_len) +{ + gs_unref_hashtable GHashTable *keys_hash = NULL; + + keys_hash = svGetKeys(s, match_key_type); + if (!keys_hash) { + NM_SET_OUT(out_len, 0); + return NULL; + } + return ( + const char **) nm_utils_hash_keys_to_array(keys_hash, _get_keys_sorted_cmp, NULL, out_len); +} + +/*****************************************************************************/ + +const char * +svFindFirstNumberedKey(shvarFile *s, const char *key_prefix) +{ + const shvarLine *line; + + g_return_val_if_fail(s, NULL); + g_return_val_if_fail(key_prefix, NULL); + + c_list_for_each_entry (line, &s->lst_head, lst) { + if (line->key && line->line + && nms_ifcfg_rh_utils_is_numbered_tag(line->key, key_prefix, NULL)) + return line->key; + } + + return NULL; +} + +/*****************************************************************************/ + +static const char * +_svGetValue(shvarFile *s, const char *key, char **to_free) +{ + const shvarLine *line; + const char * v; + + nm_assert(s); + nm_assert(_shell_is_name(key, -1)); + nm_assert(to_free); + + ASSERT_key_is_well_known(key); + + line = g_hash_table_lookup(s->lst_idx, &key); + + if (line && line->line) { + v = svUnescape(line->line, to_free); + if (!v) { + /* a wrongly quoted value is treated like the empty string. + * See also svWriteFile(), which handles unparsable values + * that way. */ + nm_assert(!*to_free); + return ""; + } + return v; + } + *to_free = NULL; + return NULL; +} + +/* Returns the value for key. The value is either owned by @s + * or returned as to_free. This aims to avoid cloning the string. + * + * - like svGetValue_cp(), but avoids cloning the value if possible. + * - like svGetValueStr(), but does not ignore empty string values. + */ +const char * +svGetValue(shvarFile *s, const char *key, char **to_free) +{ + g_return_val_if_fail(s, NULL); + g_return_val_if_fail(key, NULL); + g_return_val_if_fail(to_free, NULL); + + return _svGetValue(s, key, to_free); +} + +/* Returns the value for key. The value is either owned by @s + * or returned as to_free. This aims to avoid cloning the string. + * + * - like svGetValue(), but does not return an empty string. + * - like svGetValueStr_cp(), but avoids cloning the value if possible. + */ +const char * +svGetValueStr(shvarFile *s, const char *key, char **to_free) +{ + const char *value; + + g_return_val_if_fail(s, NULL); + g_return_val_if_fail(key, NULL); + g_return_val_if_fail(to_free, NULL); + + value = _svGetValue(s, key, to_free); + if (!value || !value[0]) { + nm_assert(!*to_free); + return NULL; + } + return value; +} + +/* Returns the value for key. The returned value must be freed + * by the caller. + * + * - like svGetValue(), but always returns a copy of the value. + * - like svGetValueStr_cp(), but does not ignore an empty string. + */ +char * +svGetValue_cp(shvarFile *s, const char *key) +{ + char * to_free; + const char *value; + + g_return_val_if_fail(s, NULL); + g_return_val_if_fail(key, NULL); + + value = _svGetValue(s, key, &to_free); + if (!value) { + nm_assert(!to_free); + return NULL; + } + return to_free ?: g_strdup(value); +} + +/* Returns the value for key. The returned value must be freed + * by the caller. + * If the key is unset or the value an empty string, NULL is returned. + * + * - like svGetValueStr(), but always returns a copy of the value. + * - like svGetValue_cp(), but returns NULL instead of an empty string. + */ +char * +svGetValueStr_cp(shvarFile *s, const char *key) +{ + char * to_free; + const char *value; + + g_return_val_if_fail(s, NULL); + g_return_val_if_fail(key, NULL); + + value = _svGetValue(s, key, &to_free); + if (!value || !value[0]) { + nm_assert(!to_free); + return NULL; + } + return to_free ?: g_strdup(value); +} + +/* svGetValueBoolean: + * @s: fhe file + * @key: the name of the key to read + * @fallback: the fallback value in any error case + * + * Reads a value @key and converts it to a boolean using svParseBoolean(). + * + * Returns: the parsed boolean value or @fallback. + */ +int +svGetValueBoolean(shvarFile *s, const char *key, int fallback) +{ + gs_free char *to_free = NULL; + const char * value; + + value = _svGetValue(s, key, &to_free); + return svParseBoolean(value, fallback); +} + +/* svGetValueTernary: + * @s: fhe file + * @key: the name of the key to read + * + * Reads a value @key and converts it to a NMTernary value. + * + * Returns: the parsed NMTernary + */ +NMTernary +svGetValueTernary(shvarFile *s, const char *key) +{ + return svGetValueBoolean(s, key, NM_TERNARY_DEFAULT); +} + +/* svGetValueInt64: + * @s: fhe file + * @key: the name of the key to read + * @base: the numeric base (usually 10). Setting to 0 means "auto". Usually you want 10. + * @min: the minimum for range-check + * @max: the maximum for range-check + * @fallback: the fallback value in any error case + * + * Reads a value @key and converts it to an integer using _nm_utils_ascii_str_to_int64(). + * In case of error, @errno will be set and @fallback returned. */ +gint64 +svGetValueInt64(shvarFile *s, const char *key, guint base, gint64 min, gint64 max, gint64 fallback) +{ + char * to_free; + const char *value; + gint64 result; + int errsv; + + value = _svGetValue(s, key, &to_free); + if (!value) { + nm_assert(!to_free); + /* indicate that the key does not exist (or has a syntax error + * and svUnescape() failed). */ + errno = ENOKEY; + return fallback; + } + + result = _nm_utils_ascii_str_to_int64(value, base, min, max, fallback); + if (to_free) { + errsv = errno; + g_free(to_free); + errno = errsv; + } + return result; +} + +gboolean +svGetValueEnum(shvarFile *s, const char *key, GType gtype, int *out_value, GError **error) +{ + gs_free char *to_free = NULL; + const char * svalue; + gs_free char *err_token = NULL; + int value; + + svalue = _svGetValue(s, key, &to_free); + if (!svalue) { + /* don't touch out_value. The caller is supposed + * to initialize it with the default value. */ + return TRUE; + } + + if (!nm_utils_enum_from_str(gtype, svalue, &value, &err_token)) { + g_set_error(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + "Invalid token \"%s\" in \"%s\" for %s", + err_token, + svalue, + key); + return FALSE; + } + + NM_SET_OUT(out_value, value); + return TRUE; +} + +/*****************************************************************************/ + +gboolean +svUnsetAll(shvarFile *s, SvKeyType match_key_type) +{ + shvarLine *line; + gboolean changed = FALSE; + + g_return_val_if_fail(s, FALSE); + + c_list_for_each_entry (line, &s->lst_head, lst) { + ASSERT_shvarLine(line); + if (line->key && _svKeyMatchesType(line->key, match_key_type)) { + if (nm_clear_g_free(&line->line)) { + ASSERT_shvarLine(line); + changed = TRUE; + } + } + } + + if (changed) + s->modified = TRUE; + return changed; +} + +gboolean +svUnsetDirtyWellknown(shvarFile *s, NMTernary new_dirty_value) +{ + shvarLine *line; + gboolean changed = FALSE; + + g_return_val_if_fail(s, FALSE); + + c_list_for_each_entry (line, &s->lst_head, lst) { + const NMSIfcfgKeyTypeInfo *ti; + + ASSERT_shvarLine(line); + + if (line->dirty && line->key && line->line + && (ti = nms_ifcfg_rh_utils_is_well_known_key(line->key)) + && !NM_FLAGS_HAS(ti->key_flags, NMS_IFCFG_KEY_TYPE_KEEP_WHEN_DIRTY)) { + if (nm_clear_g_free(&line->line)) { + ASSERT_shvarLine(line); + changed = TRUE; + } + } + + if (new_dirty_value != NM_TERNARY_DEFAULT) + line->dirty = (new_dirty_value != NM_TERNARY_FALSE); + } + + if (changed) + s->modified = TRUE; + return changed; +} + +/* Same as svSetValueStr() but it preserves empty @value -- contrary to + * svSetValueStr() for which "" effectively means to remove the value. */ +gboolean +svSetValue(shvarFile *s, const char *key, const char *value) +{ + shvarLine *line; + shvarLine *l_shadowed; + gboolean changed = FALSE; + + g_return_val_if_fail(s, FALSE); + g_return_val_if_fail(key, FALSE); + + nm_assert(_shell_is_name(key, -1)); + + ASSERT_key_is_well_known(key); + + line = g_hash_table_lookup(s->lst_idx, &key); + if (line && (l_shadowed = line->prev_shadowed)) { + /* if we find multiple entries for the same key, we can + * delete the shadowed ones. */ + line->prev_shadowed = NULL; + changed = TRUE; + do { + shvarLine *l = l_shadowed; + + l_shadowed = l_shadowed->prev_shadowed; + line_free(l); + } while (l_shadowed); + } + + if (!value) { + if (line) { + /* We only clear the value, but leave the line entry. This way, if we + * happen to re-add the value, we write it to the same line again. */ + if (nm_clear_g_free(&line->line)) { + changed = TRUE; + } + } + } else { + if (!line) { + line = line_new_build(key, value); + if (!g_hash_table_add(s->lst_idx, line)) + nm_assert_not_reached(); + c_list_link_tail(&s->lst_head, &line->lst); + changed = TRUE; + } else { + if (line_set(line, value)) + changed = TRUE; + } + } + + if (changed) + s->modified = TRUE; + return changed; +} + +/* Set the variable <key> equal to the value <value>. + * If <key> does not exist, and the <current> pointer is set, append + * the key=value pair after that line. Otherwise, append the pair + * to the bottom of the file. + */ +gboolean +svSetValueStr(shvarFile *s, const char *key, const char *value) +{ + return svSetValue(s, key, value && value[0] ? value : NULL); +} + +gboolean +svSetValueInt64(shvarFile *s, const char *key, gint64 value) +{ + char buf[NM_DECIMAL_STR_MAX(value)]; + + return svSetValue(s, key, nm_sprintf_buf(buf, "%" G_GINT64_FORMAT, value)); +} + +gboolean +svSetValueInt64_cond(shvarFile *s, const char *key, gboolean do_set, gint64 value) +{ + if (do_set) + return svSetValueInt64(s, key, value); + else + return svUnsetValue(s, key); +} + +gboolean +svSetValueBoolean(shvarFile *s, const char *key, gboolean value) +{ + return svSetValue(s, key, value ? "yes" : "no"); +} + +gboolean +svSetValueTernary(shvarFile *s, const char *key, NMTernary value) +{ + if (NM_IN_SET(value, NM_TERNARY_TRUE, NM_TERNARY_FALSE)) + return svSetValueBoolean(s, key, (gboolean) value); + else + return svUnsetValue(s, key); +} + +gboolean +svSetValueBoolean_cond_true(shvarFile *s, const char *key, gboolean value) +{ + return svSetValue(s, key, value ? "yes" : NULL); +} + +gboolean +svSetValueEnum(shvarFile *s, const char *key, GType gtype, int value) +{ + gs_free char *v = NULL; + + v = _nm_utils_enum_to_str_full(gtype, value, " ", NULL); + return svSetValueStr(s, key, v); +} + +gboolean +svUnsetValue(shvarFile *s, const char *key) +{ + return svSetValue(s, key, NULL); +} + +/*****************************************************************************/ + +/* Write the current contents iff modified. Returns FALSE on error + * and TRUE on success. Do not write if no values have been modified. + * The mode argument is only used if creating the file, not if + * re-writing an existing file, and is passed unchanged to the + * open() syscall. + */ +gboolean +svWriteFile(shvarFile *s, int mode, GError **error) +{ + FILE * f; + int tmpfd; + CList *current; + int errsv; + + if (s->modified) { + if (s->fd == -1) + s->fd = open(s->fileName, O_WRONLY | O_CREAT | O_CLOEXEC, mode); + if (s->fd == -1) { + errsv = errno; + g_set_error(error, + G_FILE_ERROR, + g_file_error_from_errno(errsv), + "Could not open file '%s' for writing: %s", + s->fileName, + nm_strerror_native(errsv)); + return FALSE; + } + if (ftruncate(s->fd, 0) < 0) { + errsv = errno; + g_set_error(error, + G_FILE_ERROR, + g_file_error_from_errno(errsv), + "Could not overwrite file '%s': %s", + s->fileName, + nm_strerror_native(errsv)); + return FALSE; + } + + tmpfd = fcntl(s->fd, F_DUPFD_CLOEXEC, 0); + if (tmpfd == -1) { + errsv = errno; + g_set_error(error, + G_FILE_ERROR, + g_file_error_from_errno(errsv), + "Internal error writing file '%s': %s", + s->fileName, + nm_strerror_native(errsv)); + return FALSE; + } + f = fdopen(tmpfd, "w"); + if (!f) { + errsv = errno; + g_set_error(error, + G_FILE_ERROR, + g_file_error_from_errno(errsv), + "Internal error writing file '%s': %s", + s->fileName, + nm_strerror_native(errsv)); + return FALSE; + } + fseek(f, 0, SEEK_SET); + c_list_for_each (current, &s->lst_head) { + const shvarLine *line = c_list_entry(current, shvarLine, lst); + const char * str; + char * s_tmp; + gboolean valid_value; + + ASSERT_shvarLine(line); + + if (!line->key) { + str = nm_str_skip_leading_spaces(line->line); + if (NM_IN_SET(str[0], '\0', '#')) + fprintf(f, "%s\n", line->line); + else + fprintf(f, "#NM: %s\n", line->line); + continue; + } + + if (!line->line) + continue; + + /* we check that the assignment can be properly unescaped. */ + valid_value = !!svUnescape(line->line, &s_tmp); + g_free(s_tmp); + + if (valid_value) + fprintf(f, "%s=%s\n", line->key_with_prefix, line->line); + else { + fprintf(f, "%s=\n", line->key); + fprintf(f, "#NM: %s=%s\n", line->key_with_prefix, line->line); + } + } + fclose(f); + } + + return TRUE; +} + +void +svCloseFile(shvarFile *s) +{ + shvarLine *line; + + g_return_if_fail(s != NULL); + + if (s->fd >= 0) + nm_close(s->fd); + g_free(s->fileName); + g_hash_table_destroy(s->lst_idx); + while ((line = c_list_first_entry(&s->lst_head, shvarLine, lst))) + line_free(line); + g_slice_free(shvarFile, s); +} |