Blob Blame History Raw
// SPDX-License-Identifier: GPL-2.0+
/*
 * Copyright (C) 1999, 2000 Red Hat, Inc.
 */

#include "nm-default.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);
}

/* 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
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);
}