summary refs log tree commit diff
path: root/src/settings/plugins/ifcfg-rh/shvar.c
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2017-01-17 20:25:09 +0100
committerMichael Biebl <biebl@debian.org>2017-01-17 20:25:09 +0100
commit58f8be580039b0575b197b9573a1c92745d96d30 (patch)
tree2c226233f623a0dcb529be0eb8cdf97e4a2ae0c0 /src/settings/plugins/ifcfg-rh/shvar.c
parent45cb5bb3c0e6edb887cf69b417fcaf7053814a9b (diff)
New upstream version 1.5.90 upstream/1.5.90
Diffstat (limited to 'src/settings/plugins/ifcfg-rh/shvar.c')
-rw-r--r--src/settings/plugins/ifcfg-rh/shvar.c1218
1 files changed, 932 insertions, 286 deletions
diff --git a/src/settings/plugins/ifcfg-rh/shvar.c b/src/settings/plugins/ifcfg-rh/shvar.c
index a865c7df..65bcc0d9 100644
--- a/src/settings/plugins/ifcfg-rh/shvar.c
+++ b/src/settings/plugins/ifcfg-rh/shvar.c
@@ -37,292 +37,901 @@
 #include <unistd.h>
 
 #include "nm-core-internal.h"
+#include "nm-core-utils.h"
+
+/*****************************************************************************/
+
+typedef struct {
+	/* 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;
+	const char *key;
+	char *key_with_prefix;
+} shvarLine;
 
-/* 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.
+struct _shvarFile {
+	char      *fileName;
+	int        fd;
+	GList     *lineList;
+	gboolean   modified;
+};
+
+/*****************************************************************************/
+
+/**
+ * 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.
  */
-static shvarFile *
-svOpenFileInternal (const char *name, gboolean create, GError **error)
+gint
+svParseBoolean (const char *value, gint fallback)
 {
-	shvarFile *s = NULL;
-	gboolean closefd = FALSE;
-	int errsv = 0;
+	if (!value)
+		return fallback;
 
-	s = g_slice_new0 (shvarFile);
+	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;
 
-	s->fd = -1;
-	if (create)
-		s->fd = open (name, O_RDWR | O_CLOEXEC); /* NOT O_CREAT */
+	return fallback;
+}
 
-	if (!create || s->fd == -1) {
-		/* try read-only */
-		s->fd = open (name, O_RDONLY | O_CLOEXEC); /* NOT O_CREAT */
-		if (s->fd == -1)
-			errsv = errno;
-		else
-			closefd = TRUE;
-	}
-	s->fileName = g_strdup (name);
+/*****************************************************************************/
 
-	if (s->fd != -1) {
-		struct stat buf;
-		char *arena, *p, *q;
-		ssize_t nread, total = 0;
+static gboolean
+_shell_is_name (const char *key, gssize len)
+{
+	gssize i;
 
-		if (fstat (s->fd, &buf) < 0) {
-			errsv = errno;
-			goto bail;
+	/* 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;
 		}
-		arena = g_malloc (buf.st_size + 1);
-		arena[buf.st_size] = '\0';
+		if (   !g_ascii_isalnum (key[i])
+		    && key[i] != '_')
+			return FALSE;
+	}
+}
 
-		while (total < buf.st_size) {
-			nread = read (s->fd, arena + total, buf.st_size - total);
-			if (nread == -1 && errno == EINTR)
-				continue;
-			if (nread <= 0) {
-				errsv = errno;
-				g_free (arena);
-				goto bail;
-			}
-			total += nread;
-		}
+/*****************************************************************************/
+
+/* like g_strescape(), except that it also escapes '\''' *sigh*.
+ *
+ * While at it, add $''. */
+static char *
+_escape_ansic (const char *source)
+{
+	const char *p;
+	gchar *dest;
+	gchar *q;
+
+	nm_assert (source);
 
-		/* we'd use g_strsplit() here, but we want a list, not an array */
-		for (p = arena; (q = strchr (p, '\n')) != NULL; p = q + 1)
-			s->lineList = g_list_append (s->lineList, g_strndup (p, q - p));
-		g_free (arena);
-
-		/* closefd is set if we opened the file read-only, so go ahead and
-		 * close it, because we can't write to it anyway
-		 */
-		if (closefd) {
-			close (s->fd);
-			s->fd = -1;
+	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;
 
+	slen = strlen (s);
+
+	for (i = 0; i < slen; i++) {
+		if (_char_req_escape (s[i]))
+			mangle++;
+		else if (_char_req_quotes (s[i]))
+			requires_quotes = TRUE;
+		else if (s[i] < ' ') {
+			/* 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;
 	}
 
-	if (create)
-		return s;
+	newlen = slen + mangle + 3; /* 3 is extra ""\0 */
+	new = g_malloc (newlen);
 
- bail:
-	if (s->fd != -1)
-		close (s->fd);
-	g_free (s->fileName);
-	g_slice_free (shvarFile, s);
+	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';
 
-	g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errsv),
-	             "Could not read file '%s': %s",
-	             name, errsv ? strerror (errsv) : "Unknown error");
-	return NULL;
+	nm_assert (j == slen + mangle + 3);
+
+	*to_free = new;
+	return new;
 }
 
-/* Open the file <name>, return shvarFile on success, NULL on failure */
-shvarFile *
-svOpenFile (const char *name, GError **error)
+static gboolean
+_looks_like_old_svescaped (const char *value)
 {
-	return svOpenFileInternal (name, FALSE, error);
+	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;
+	}
 }
 
-/* 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)
+static gboolean
+_ch_octal_is (char ch)
 {
-	return svOpenFileInternal (name, TRUE, NULL);
+	return ch >= '0' && ch < '8';
 }
 
-/* remove escaped characters in place */
-void
-svUnescape (char *s)
+static guint8
+_ch_octal_get (char ch)
 {
-	size_t len, idx_rd = 0, idx_wr = 0;
-	char c;
+	nm_assert (_ch_octal_is (ch));
+	return (ch - '0');
+}
 
-	len = strlen (s);
-	if (len < 2) {
-		if (s[0] == '\\')
-			s[0] = '\0';
-		return;
+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 maxium 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 ((s[0] == '"' || s[0] == '\'') && s[0] == s[len-1]) {
-		if (len == 2) {
-			s[0] = '\0';
-			return;
+		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 (len == 3) {
-			if (s[1] == '\\') {
-				s[0] = '\0';
-			} else {
-				s[0] = s[1];
-				s[1] = '\0';
+
+		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++;
 			}
-			return;
+			g_string_append_len (str, &value[i], j - i);
+			i = j + 1;
+			goto loop1_next;
 		}
-		s[--len] = '\0';
-		idx_rd = 1;
-	} else {
-		/* seek for the first escape... */
-		char *p = strchr (s, '\\');
-
-		if (!p)
-			return;
-		if (p[1] == '\0') {
-			p[0] = '\0';
-			return;
+
+		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 qoutes 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;
 		}
-		idx_wr = idx_rd = (p - s);
-	}
 
-	/* idx_rd points to the first escape. Walk the string and shift the
-	 * characters from idx_rd to idx_wr.
-	 */
-	while ((c = s[idx_rd++])) {
-		if (c == '\\') {
-			if (s[idx_rd] == '\0') {
-				s[idx_wr] = '\0';
-				return;
+		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: ;
 			}
-			s[idx_wr++] = s[idx_rd++];
-			continue;
+			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;
 		}
-		s[idx_wr++] = c;
 	}
-	s[idx_wr] = '\0';
+
+	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;
 }
 
-/* create a new string with all necessary characters escaped.
- * caller must free returned string
- */
-static const char escapees[] = "\"'\\$~`";		/* must be escaped */
-static const char spaces[] = " \t|&;()<>";		/* only require "" */
-static const char newlines[] = "\n\r";			/* will be removed */
+/*****************************************************************************/
+
+static shvarFile *
+svFile_new (const char *name)
+{
+	shvarFile *s;
+
+	s = g_slice_new0 (shvarFile);
+	s->fd = -1;
+	s->fileName = g_strdup (name);
+	return s;
+}
 
 const char *
-svEscape (const char *s, char **to_free)
+svFileGetName (const shvarFile *s)
 {
-	char *new;
-	int i, j, mangle = 0, space = 0, newline = 0;
-	int newlen, slen;
+	nm_assert (s);
 
-	slen = strlen (s);
+	return s->fileName;
+}
 
-	for (i = 0; i < slen; i++) {
-		if (strchr (escapees, s[i]))
-			mangle++;
-		if (strchr (spaces, s[i]))
-			space++;
-		if (strchr (newlines, s[i]))
-			newline++;
-	}
-	if (!mangle && !space && !newline) {
-		*to_free = NULL;
-		return s;
+void
+svFileSetName (shvarFile *s, const char *fileName)
+{
+	g_free (s->fileName);
+	s->fileName = g_strdup (fileName);
+}
+
+void
+svFileSetModified (shvarFile *s)
+{
+	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
+}
 
-	newlen = slen + mangle - newline + 3;	/* 3 is extra ""\0 */
-	new = g_malloc (newlen);
+static shvarLine *
+line_new_parse (const char *value, gsize len)
+{
+	shvarLine *line;
+	gsize k, e;
 
-	j = 0;
-	new[j++] = '"';
-	for (i = 0; i < slen; i++) {
-		if (strchr (newlines, s[i]))
+	nm_assert (value);
+
+	line = g_slice_new0 (shvarLine);
+
+	for (k = 0; k < len; k++) {
+		if (g_ascii_isspace (value[k]))
 			continue;
-		if (strchr (escapees, s[i])) {
-			new[j++] = '\\';
+
+		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;
+			}
 		}
-		new[j++] = s[i];
+		break;
 	}
-	new[j++] = '"';
-	new[j++] = '\0';
-	g_assert (j == slen + mangle - newline + 3);
+	line->line = g_strndup (value, len);
+	ASSERT_shvarLine (line);
+	return line;
+}
 
-	*to_free = new;
-	return new;
+static shvarLine *
+line_new_build (const char *key, const char *value)
+{
+	char *value_escaped = NULL;
+	shvarLine *line;
+
+	value = svEscape (value, &value_escaped);
+
+	line = g_slice_new (shvarLine);
+	line->line = value_escaped ?: g_strdup (value);
+	line->key_with_prefix = g_strdup (key);
+	line->key = line->key_with_prefix;
+	ASSERT_shvarLine (line);
+	return line;
 }
 
-/* Get the value associated with the key, and leave the current pointer
- * pointing at the line containing the value.  The char* returned MUST
- * be freed by the caller.
+static gboolean
+line_set (shvarLine *line, const char *value)
+{
+	char *value_escaped = NULL;
+	gboolean changed = FALSE;
+
+	ASSERT_shvarLine (line);
+	nm_assert (line->key);
+
+	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);
+	g_free (line->line);
+	g_free (line->key_with_prefix);
+	g_slice_free (shvarLine, line);
+}
+
+/*****************************************************************************/
+
+/* 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.
  */
-char *
-svGetValue (shvarFile *s, const char *key, gboolean verbatim)
+static shvarFile *
+svOpenFileInternal (const char *name, gboolean create, GError **error)
 {
-	char *value;
+	shvarFile *s;
+	gboolean closefd = FALSE;
+	int errsv = 0;
+	char *arena;
+	const char *p, *q;
+	GError *local = NULL;
+	nm_auto_close int fd = -1;
+	GList *lineList = NULL;
+
+	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;
+	}
 
-	value = svGetValueFull (s, key, verbatim);
-	if (value && !*value) {
-		g_free (value);
+	if (fd < 0) {
+		if (create)
+			return svFile_new (name);
+
+		g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errsv),
+		             "Could not read file '%s': %s",
+		             name, strerror (errsv));
 		return NULL;
 	}
-	return value;
+
+	if (nm_utils_fd_get_contents (fd,
+	                              10 * 1024 * 1024,
+	                              &arena,
+	                              NULL,
+	                              &local) < 0) {
+		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);
+		g_error_free (local);
+		return NULL;
+	}
+
+	for (p = arena; (q = strchr (p, '\n')) != NULL; p = q + 1)
+		lineList = g_list_prepend (lineList, line_new_parse (p, q - p));
+	if (p[0])
+		lineList = g_list_prepend (lineList, line_new_parse (p, strlen (p)));
+	g_free (arena);
+	lineList = g_list_reverse (lineList);
+
+	s = svFile_new (name);
+	s->lineList = lineList;
+
+	/* closefd is set if we opened the file read-only, so go ahead and
+	 * close it, because we can't write to it anyway */
+	if (!closefd) {
+		s->fd = fd;
+		fd = -1;
+	}
+
+	return s;
 }
 
-/* svGetValueFull() is identical to svGetValue() except that
- * svGetValue() will never return an empty value (but %NULL instead).
- * svGetValueFull() will return empty values if that is the value for the @key. */
-char *
-svGetValueFull (shvarFile *s, const char *key, gboolean verbatim)
+/* Open the file <name>, return shvarFile on success, NULL on failure */
+shvarFile *
+svOpenFile (const char *name, GError **error)
 {
-	char *value = NULL;
-	char *line;
-	guint len;
+	return svOpenFileInternal (name, FALSE, error);
+}
 
-	g_return_val_if_fail (s != NULL, NULL);
-	g_return_val_if_fail (key != NULL, NULL);
+/* 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);
+}
 
-	len = strlen (key);
+/*****************************************************************************/
 
-	for (s->current = s->lineList; s->current; s->current = s->current->next) {
-		line = s->current->data;
-		if (!strncmp (key, line, len) && line[len] == '=') {
-			/* Strip trailing spaces before unescaping to preserve spaces quoted whitespace */
-			value = g_strchomp (g_strdup (line + len + 1));
-			if (!verbatim)
-				svUnescape (value);
-			break;
-		}
+static const GList *
+shlist_find (const GList *current, const char *key)
+{
+	nm_assert (_shell_is_name (key, -1));
+
+	if (current) {
+		do {
+			shvarLine *line = current->data;
+
+			ASSERT_shvarLine (line);
+			if (line->key && nm_streq (line->key, key))
+				return current;
+			current = current->next;
+		} while (current);
+	}
+	return NULL;
+}
+
+/*****************************************************************************/
+
+static const char *
+_svGetValue (shvarFile *s, const char *key, char **to_free)
+{
+	const GList *current, *last;
+	const shvarLine *line;
+
+	nm_assert (s);
+	nm_assert (_shell_is_name (key, -1));
+	nm_assert (to_free);
+
+	last = NULL;
+	current = s->lineList;
+	while ((current = shlist_find (current, key))) {
+		last = current;
+		current = current->next;
+	}
+	if (last) {
+		line = last->data;
+		if (line->line)
+			return svUnescape (line->line, to_free);
 	}
+	*to_free = NULL;
+	return NULL;
+}
 
-	return value;
+const char *
+svGetValue (shvarFile *s, const char *key, char **to_free)
+{
+	g_return_val_if_fail (s != NULL, NULL);
+	g_return_val_if_fail (key != NULL, NULL);
+	g_return_val_if_fail (to_free, NULL);
+
+	return _svGetValue (s, key, to_free);
 }
 
-/**
- * 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.
+/* Get the value associated with the key, and leave the current pointer
+ * pointing at the line containing the value.  The char* returned MUST
+ * be freed by the caller.
  */
-gint
-svParseBoolean (const char *value, gint fallback)
+char *
+svGetValueString (shvarFile *s, const char *key)
 {
-	if (!value)
-		return fallback;
+	char *to_free;
+	const char *value;
 
-	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;
+	value = _svGetValue (s, key, &to_free);
+	if (!value) {
+		nm_assert (!to_free);
+		return NULL;
+	}
+	if (!value[0]) {
+		nm_assert (!to_free);
+		return NULL;
+	}
+	return to_free ?: g_strdup (value);
 }
 
 /* svGetValueBoolean:
@@ -337,10 +946,11 @@ svParseBoolean (const char *value, gint fallback)
 gint
 svGetValueBoolean (shvarFile *s, const char *key, gint fallback)
 {
-	gs_free char *tmp = NULL;
+	gs_free char *to_free = NULL;
+	const char *value;
 
-	tmp = svGetValue (s, key, FALSE);
-	return svParseBoolean (tmp, fallback);
+	value = _svGetValue (s, key, &to_free);
+	return svParseBoolean (value, fallback);
 }
 
 /* svGetValueInt64:
@@ -356,97 +966,108 @@ svGetValueBoolean (shvarFile *s, const char *key, gint fallback)
 gint64
 svGetValueInt64 (shvarFile *s, const char *key, guint base, gint64 min, gint64 max, gint64 fallback)
 {
-	char *tmp;
+	char *to_free;
+	const char *value;
 	gint64 result;
 	int errsv;
 
-	tmp = svGetValueFull (s, key, FALSE);
-	if (!tmp) {
-		errno = 0;
+	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 (tmp, base, min, max, fallback);
-	errsv = errno;
-
-	g_free (tmp);
-
-	errno = errsv;
+	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;
 }
 
-/* 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.
- */
-void
-svSetValue (shvarFile *s, const char *key, const char *value, gboolean verbatim)
-{
-	svSetValueFull (s, key, value && value[0] ? value : NULL, verbatim);
-}
+/*****************************************************************************/
 
-/* Same as svSetValue() but it preserves empty @value -- contrary to
- * svSetValue() for which "" effectively means to remove the value. */
+/* Same as svSetValueString() but it preserves empty @value -- contrary to
+ * svSetValueString() for which "" effectively means to remove the value. */
 void
-svSetValueFull (shvarFile *s, const char *key, const char *value, gboolean verbatim)
+svSetValue (shvarFile *s, const char *key, const char *value)
 {
-	gs_free char *newval_free = NULL;
-	gs_free char *oldval = NULL;
-	const char *newval;
-	char *keyValue;
+	GList *current, *last;
 
 	g_return_if_fail (s != NULL);
 	g_return_if_fail (key != NULL);
-	/* value may be NULL */
-
-	if (!value || verbatim)
-		newval = value;
-	else
-		newval = svEscape (value, &newval_free);
-	oldval = svGetValueFull (s, key, FALSE);
-
-	if (!newval) {
-		/* delete value */
-		if (oldval) {
-			/* delete line */
-			s->lineList = g_list_remove_link (s->lineList, s->current);
-			g_free (s->current->data);
-			g_list_free_1 (s->current);
+
+	nm_assert (_shell_is_name (key, -1));
+
+	last = NULL;
+	current = s->lineList;
+	while ((current = (GList *) shlist_find (current, key))) {
+		if (last) {
+			/* if we find multiple entries for the same key, we can
+			 * delete all but the last. */
+			line_free (last->data);
+			s->lineList = g_list_delete_link (s->lineList, last);
 			s->modified = TRUE;
 		}
-		return;
+		last = current;
+		current = current->next;
 	}
 
-	keyValue = g_strdup_printf ("%s=%s", key, newval);
-	if (!oldval) {
-		/* append line */
-		s->lineList = g_list_append (s->lineList, keyValue);
-		s->modified = TRUE;
-		return;
+	if (!value) {
+		if (last) {
+			shvarLine *line = last->data;
+
+			if (nm_clear_g_free (&line->line))
+				s->modified = TRUE;
+		}
+	} else {
+		if (!last) {
+			s->lineList = g_list_append (s->lineList, line_new_build (key, value));
+			s->modified = TRUE;
+		} else {
+			if (line_set (last->data, value))
+				s->modified = TRUE;
+		}
 	}
+}
 
-	if (strcmp (oldval, newval) != 0) {
-		/* change line */
-		if (s->current) {
-			g_free (s->current->data);
-			s->current->data = keyValue;
-		} else
-			s->lineList = g_list_append (s->lineList, keyValue);
-		s->modified = TRUE;
-	} else
-		g_free (keyValue);
+/* 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.
+ */
+void
+svSetValueString (shvarFile *s, const char *key, const char *value)
+{
+	svSetValue (s, key, value && value[0] ? value : NULL);
 }
 
 void
 svSetValueInt64 (shvarFile *s, const char *key, gint64 value)
 {
-	gs_free char *v = NULL;
+	char buf[NM_DECIMAL_STR_MAX (value)];
+
+	svSetValue (s, key, nm_sprintf_buf (buf, "%"G_GINT64_FORMAT, value));
+}
 
-	v = g_strdup_printf ("%"G_GINT64_FORMAT, value);
-	svSetValueFull (s, key, v, TRUE);
+void
+svSetValueBoolean (shvarFile *s, const char *key, gboolean value)
+{
+	svSetValue (s, key, value ? "yes" : "no");
 }
 
+void
+svUnsetValue (shvarFile *s, const char *key)
+{
+	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
@@ -458,6 +1079,7 @@ svWriteFile (shvarFile *s, int mode, GError **error)
 {
 	FILE *f;
 	int tmpfd;
+	const GList *current;
 
 	if (s->modified) {
 		if (s->fd == -1)
@@ -490,9 +1112,36 @@ svWriteFile (shvarFile *s, int mode, GError **error)
 		}
 		f = fdopen (tmpfd, "w");
 		fseek (f, 0, SEEK_SET);
-		for (s->current = s->lineList; s->current; s->current = s->current->next) {
-			char *line = s->current->data;
-			fprintf (f, "%s\n", line);
+		for (current = s->lineList; current; current = current->next) {
+			const shvarLine *line = current->data;
+			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);
 	}
@@ -500,8 +1149,6 @@ svWriteFile (shvarFile *s, int mode, GError **error)
 	return TRUE;
 }
 
-
-/* Close the file descriptor (if open) and free the shvarFile. */
 void
 svCloseFile (shvarFile *s)
 {
@@ -509,8 +1156,7 @@ svCloseFile (shvarFile *s)
 
 	if (s->fd != -1)
 		close (s->fd);
-
 	g_free (s->fileName);
-	g_list_free_full (s->lineList, g_free); /* implicitly frees s->current */
+	g_list_free_full (s->lineList, (GDestroyNotify) line_free);
 	g_slice_free (shvarFile, s);
 }