summary refs log tree commit diff
path: root/src/core/settings/plugins/ifcfg-rh/shvar.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/core/settings/plugins/ifcfg-rh/shvar.c')
-rw-r--r--src/core/settings/plugins/ifcfg-rh/shvar.c1585
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);
+}