/* name-value.c - Parser and writer for a name-value format. * Copyright (C) 2016, 2025 g10 Code GmbH * * This file is part of Libgpg-error. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This file is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, see . * SPDX-License-Identifier: LGPL-2.1-or-later * * This file was originally a part of GnuPG. */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include "gpgrt-int.h" struct _gpgrt_name_value_container { struct _gpgrt_name_value_entry *first; struct _gpgrt_name_value_entry *last; unsigned int wipe_on_free:1; unsigned int private_key_mode:1; unsigned int section_mode:1; unsigned int modified:1; }; struct _gpgrt_name_value_entry { struct _gpgrt_name_value_entry *prev; struct _gpgrt_name_value_entry *next; unsigned int wipe_on_free:1; /* Copied from the container. */ /* The length of the NAME (to save calling strlen). */ unsigned int namelen:8; /* The name. Comments and blank lines have NAME set to NULL. */ char *name; /* The value as stored in the file. We store it when we parse * a file so that we can reproduce it. */ gpgrt_strlist_t raw_value; /* The decoded value. */ char *value; }; /* This macro is a simple ascii only versions of the standard (and * locale affected) isspace(3). This macro does not consider \v and * \f as white space not any non-ascii character. */ #define ascii_isspace(a) ((a)==' ' || (a)=='\n' || (a)=='\r' || (a)=='\t') /* Other simple macros. */ #define spacep(p) (*(p) == ' ' || *(p) == '\t') #define digitp(p) (*(p) >= '0' && *(p) <= '9') #define alphap(p) ((*(p) >= 'A' && *(p) <= 'Z') \ || (*(p) >= 'a' && *(p) <= 'z')) #define alnump(p) (alphap (p) || digitp (p)) static GPGRT_INLINE int ascii_toupper (int c) { if (c >= 'a' && c <= 'z') c &= ~0x20; return c; } static int ascii_strcasecmp (const char *a, const char *b) { if (a == b) return 0; for (; *a && *b; a++, b++) { if (*a != *b && ascii_toupper(*a) != ascii_toupper(*b)) break; } return *a == *b? 0 : (ascii_toupper (*a) - ascii_toupper (*b)); } static int ascii_memcasecmp (const void *a_arg, const void *b_arg, size_t n ) { const char *a = a_arg; const char *b = b_arg; if (a == b) return 0; for ( ; n; n--, a++, b++ ) { if( *a != *b && ascii_toupper (*a) != ascii_toupper (*b) ) return *a == *b? 0 : (ascii_toupper (*a) - ascii_toupper (*b)); } return 0; } /* Return true if NAME of length NAMELEN is the same as STRING. The * comparison is case-insensitive for ascii characters. */ static int same_name_p (const char *name, size_t namelen, const char *string) { size_t stringlen = strlen (string); if (stringlen && string[stringlen-1] == ':') stringlen--; if (namelen != stringlen) return 0; return !ascii_memcasecmp (name, string, namelen); } /* Allocate a name value container structure. */ gpgrt_nvc_t _gpgrt_nvc_new (unsigned int flags) { gpgrt_nvc_t nvc; nvc = xtrycalloc (1, sizeof *nvc); if (!nvc) return NULL; nvc->modified = 1; if ((flags & GPGRT_NVC_PRIVKEY)) { nvc->wipe_on_free = 1; nvc->private_key_mode = 1; } else if ((flags & GPGRT_NVC_WIPE)) nvc->wipe_on_free = 1; nvc->section_mode = !!(flags & GPGRT_NVC_SECTION); return nvc; } /* Helper to free an entry. */ static void nve_release (gpgrt_nve_t entry, int with_wipe) { if (!entry) return; xfree (entry->name); if (entry->value && with_wipe) _gpgrt_wipememory (entry->value, strlen (entry->value)); xfree (entry->value); _gpgrt_strlist_free (entry->raw_value); xfree (entry); } /* Release a name value container. */ void _gpgrt_nvc_release (gpgrt_nvc_t cont) { gpgrt_nve_t e, next; if (!cont) return; for (e = cont->first; e; e = next) { next = e->next; nve_release (e, cont->wipe_on_free); } xfree (cont); } /* Return true if the specified flag is set. Only one bit should be * set in FLAGS. If the GPGRT_NVC_MODIFIED flag is requested and * CLEAR is set, that flag is cleared; note that this modified flag is * set for a new container and set with each update. */ int _gpgrt_nvc_get_flag (gpgrt_nvc_t cont, unsigned int flags, int clear) { int ret = 0; if (!cont) ; else if ((flags & GPGRT_NVC_MODIFIED)) { ret = cont->modified; if (clear) cont->modified = 0; } else if ((flags & GPGRT_NVC_PRIVKEY)) ret = cont->private_key_mode; else if ((flags & GPGRT_NVC_WIPE)) ret = cont->wipe_on_free; else if ((flags & GPGRT_NVC_SECTION)) ret = cont->section_mode; return !!ret; } /* Dealing with names and values. */ /* Check whether the given name is valid. Valid names start with a * letter, optionally end with a colon, and contain only alphanumeric * characters and the hyphen. Returns the length of the name sans the * optional colon if NAME is valid; returns 0 if the name is not * valid. The length of the name must be less than 255. */ static unsigned int valid_name (const char *name, int sectionmode) { size_t i; size_t len, extralen; const char *s; /* In section mode we skip over the first colon. We require that * after the colon at least one other characters is found. */ if (sectionmode && (s=strchr (name, ':')) && s[1] && s[1] != ':') { extralen = s + 1 - name; name = s+1; } else extralen = 0; len = strlen (name); if (!alphap (name) || !len || len > 255) return 0; if (name[len-1] == ':') /* The colon is optional. */ len--; if (!len) return 0; for (i = 1; i < len; i++) if (!alnump (name + i) && name[i] != '-') return 0; return len + extralen; } /* Makes sure that ENTRY has a RAW_VALUE. */ static gpg_err_code_t assert_raw_value (gpgrt_nve_t entry) { gpg_err_code_t err = 0; size_t len, offset; #define LINELEN 70 char buf[LINELEN+3]; if (entry->raw_value) return 0; len = strlen (entry->value); offset = 0; while (len) { size_t amount, linelen = LINELEN; /* On the first line we need to subtract space for the name. */ if (entry->raw_value == NULL && strlen (entry->name) < linelen) linelen -= strlen (entry->name); /* See if the rest of the value fits in this line. */ if (len <= linelen) amount = len; else { size_t i; /* Find a suitable space to break on. */ for (i = linelen - 1; linelen - i < 30; i--) if (ascii_isspace (entry->value[offset+i])) break; if (ascii_isspace (entry->value[offset+i])) { /* Found one. */ amount = i; } else { /* Just induce a hard break. */ amount = linelen; } } snprintf (buf, sizeof buf, " %.*s\n", (int) amount, &entry->value[offset]); if (!_gpgrt_strlist_add (&entry->raw_value, buf, (GPGRT_STRLIST_APPEND | (entry->wipe_on_free? GPGRT_STRLIST_WIPE:0)))) { err = _gpg_err_code_from_syserror (); goto leave; } offset += amount; len -= amount; } leave: if (err) { _gpgrt_strlist_free (entry->raw_value); entry->raw_value = NULL; } return err; #undef LINELEN } /* Computes the length of the value encoded as continuation. If * *SWALLOW_WS is set, all whitespace at the beginning of S is * swallowed. If START is given, a pointer to the beginning of the * value is stored there. */ static size_t continuation_length (const char *s, int *swallow_ws, const char **start) { size_t len; if (*swallow_ws) { /* The previous line was a blank line and we inserted a newline. Swallow all whitespace at the beginning of this line. */ while (ascii_isspace (*s)) s++; } else { /* Iff a continuation starts with more than one space, it encodes a space. */ if (ascii_isspace (*s)) s++; } /* Strip whitespace at the end. */ len = strlen (s); while (len > 0 && ascii_isspace (s[len-1])) len--; if (!len) { /* Blank lines encode newlines. */ len = 1; s = "\n"; *swallow_ws = 1; } else *swallow_ws = 0; if (start) *start = s; return len; } /* Makes sure that ENTRY has a VALUE. */ static gpg_err_code_t assert_value (gpgrt_nve_t entry) { size_t len; int swallow_ws; gpgrt_strlist_t s; char *p; if (entry->value) return 0; len = 0; swallow_ws = 0; for (s = entry->raw_value; s; s = s->next) len += continuation_length (s->d, &swallow_ws, NULL); /* Add one for the terminating zero. */ len += 1; entry->value = p = xtrymalloc (len); if (!entry->value) return _gpg_err_code_from_syserror (); swallow_ws = 0; for (s = entry->raw_value; s; s = s->next) { const char *start; size_t l = continuation_length (s->d, &swallow_ws, &start); memcpy (p, start, l); p += l; } *p++ = 0; gpgrt_assert (p - entry->value == len); return 0; } /* Get the name. */ const char * _gpgrt_nve_name (gpgrt_nve_t entry) { return entry->name; } /* Get the value. */ const char * _gpgrt_nve_value (gpgrt_nve_t entry) { if (assert_value (entry)) return NULL; return entry->value; } /* Adding and modifying values. */ /* Add (NAME, VALUE, RAW_VALUE) to CONT. NAME may be NULL for comments * and blank lines. At least one of VALUE and RAW_VALUE must be * given. If PRESERVE_ORDER is not given, entries with the same name * are grouped. NAME, VALUE and RAW_VALUE is consumed. */ static gpg_err_code_t do_nvc_add (gpgrt_nvc_t cont, char *name, char *value, gpgrt_strlist_t raw_value, int preserve_order) { gpg_err_code_t err = 0; gpgrt_nve_t e; unsigned int namelen; gpgrt_assert (value || raw_value); namelen = name ? valid_name (name, cont->section_mode) : 0; if (name && !namelen) { err = GPG_ERR_INV_NAME; goto leave; } if (name && cont->private_key_mode && same_name_p (name, namelen, "Key") && _gpgrt_nvc_lookup (cont, "Key")) { err = GPG_ERR_INV_NAME; goto leave; } e = xtrycalloc (1, sizeof *e); if (!e) { err = _gpg_err_code_from_syserror (); goto leave; } e->name = name; e->namelen = namelen; e->value = value; e->raw_value = raw_value; e->wipe_on_free = cont->wipe_on_free; if (cont->first) { gpgrt_nve_t last; if (preserve_order || !name) last = cont->last; else { /* See if there is already an entry with NAME. */ last = _gpgrt_nvc_lookup (cont, name); /* If so, find the last in that block. */ if (last) { while (last->next) { gpgrt_nve_t next = last->next; if (next->name && same_name_p (next->name, next->namelen, name)) last = next; else break; } } else /* Otherwise, just find the last entry. */ last = cont->last; } if (last->next) { e->prev = last; e->next = last->next; last->next = e; e->next->prev = e; } else { e->prev = last; last->next = e; cont->last = e; } } else cont->first = cont->last = e; cont->modified = 1; leave: if (err) { xfree (name); if (value && cont->wipe_on_free) _gpgrt_wipememory (value, strlen (value)); xfree (value); _gpgrt_strlist_free (raw_value); } return err; } /* Add (NAME, VALUE) to CONT. If an entry with NAME already exists, it * is not updated but the new entry is appended. */ gpg_err_code_t _gpgrt_nvc_add (gpgrt_nvc_t cont, const char *name, const char *value) { char *k, *v; k = xtrystrdup (name); if (!k) return _gpg_err_code_from_syserror (); v = xtrystrdup (value); if (!v) { xfree (k); return _gpg_err_code_from_syserror (); } return do_nvc_add (cont, k, v, NULL, 0); } /* Add (NAME, VALUE) to CONT. If an entry with NAME already exists, it * is updated with VALUE. If multiple entries with NAME exist, the * first entry is updated. */ gpg_err_code_t _gpgrt_nvc_set (gpgrt_nvc_t cont, const char *name, const char *value) { gpgrt_nve_t e; if (! valid_name (name, cont->section_mode)) return GPG_ERR_INV_NAME; e = _gpgrt_nvc_lookup (cont, name); if (e) return _gpgrt_nve_set (cont, e, value); else return _gpgrt_nvc_add (cont, name, value); } /* Update entry E to VALUE. CONT is optional; if given its modified * flag will be updated. */ gpg_err_code_t _gpgrt_nve_set (gpgrt_nvc_t cont, gpgrt_nve_t e, const char *value) { char *v; if (!e) return GPG_ERR_INV_ARG; if (e->value && value && !strcmp (e->value, value)) { /* Setting same value - ignore this call and don't set the * modified flag (if CONT is given). */ return 0; } v = xtrystrdup (value? value:""); if (!v) return _gpg_err_code_from_syserror (); _gpgrt_strlist_free (e->raw_value); e->raw_value = NULL; if (e->value) _gpgrt_wipememory (e->value, strlen (e->value)); xfree (e->value); e->value = v; if (cont) cont->modified = 1; return 0; } /* Delete the given ENTRY from the container CONT. */ static void do_nvc_delete (gpgrt_nvc_t cont, gpgrt_nve_t entry) { if (entry->prev) entry->prev->next = entry->next; else cont->first = entry->next; if (entry->next) entry->next->prev = entry->prev; else cont->last = entry->prev; nve_release (entry, cont->private_key_mode); cont->modified = 1; } /* Delete entries from the container CONT. Either ENTRY or NAME may * be given. IF Entry is given only this entry is removed. if NAME * is given all entries with this name are deleted. */ void _gpgrt_nvc_delete (gpgrt_nvc_t cont, gpgrt_nve_t entry, const char *name) { if (entry) do_nvc_delete (cont, entry); else if (valid_name (name, cont->section_mode)) { while ((entry = _gpgrt_nvc_lookup (cont, name))) do_nvc_delete (cont, entry); } } /* Lookup and iteration. */ /* Get the first entry with the given name. Return NULL if it does * not exist. If NAME is NULL the first non-comment entry is * returned. */ gpgrt_nve_t _gpgrt_nvc_lookup (gpgrt_nvc_t cont, const char *name) { gpgrt_nve_t entry; if (!cont) return NULL; if (!name) { for (entry = cont->first; entry; entry = entry->next) if (entry->name) return entry; return NULL; } for (entry = cont->first; entry; entry = entry->next) if (entry->name && same_name_p (entry->name, entry->namelen, name)) return entry; return NULL; } /* Get the next non-comment entry. If NAME is given the next entry * with that name is returned. */ gpgrt_nve_t _gpgrt_nve_next (gpgrt_nve_t entry, const char *name) { if (!entry) return NULL; if (name) { for (entry = entry->next; entry; entry = entry->next) if (entry->name && same_name_p (entry->name, entry->namelen, name)) return entry; } else { for (entry = entry->next; entry; entry = entry->next) if (entry->name) return entry; } return NULL; } /* Parsing and serialization. */ static gpg_err_code_t do_nvc_parse (gpgrt_nvc_t *result, int *errlinep, estream_t stream, unsigned int flags) { gpg_err_code_t err = 0; gpgrt_ssize_t len; char *buf = NULL; size_t buf_len = 0; char *name = NULL; char *section = NULL; gpgrt_strlist_t raw_value = NULL; unsigned int slflags; *result = _gpgrt_nvc_new (flags); if (!*result) return _gpg_err_code_from_syserror (); slflags = (GPGRT_STRLIST_APPEND | ((flags & GPGRT_NVC_WIPE)? GPGRT_STRLIST_WIPE:0)); if (errlinep) *errlinep = 0; while ((len = _gpgrt_read_line (stream, &buf, &buf_len, NULL)) > 0) { char *p, *p2; if (errlinep) *errlinep += 1; /* Skip any whitespace. */ for (p = buf; *p && ascii_isspace (*p); p++) /* Do nothing. */; if (name && (spacep (buf) || !*p)) { /* A continuation. */ if (!_gpgrt_strlist_add (&raw_value, buf, slflags)) { err = _gpg_err_code_from_syserror (); goto leave; } continue; } /* No continuation. Add the current entry if any. */ if (raw_value) { err = do_nvc_add (*result, name, NULL, raw_value, 1 /*preserve order*/); name = NULL; if (err) goto leave; } /* And prepare for the next one. */ name = NULL; raw_value = NULL; if ((flags & GPGRT_NVC_SECTION) && *p == '[' && (p2=strchr (p+1, ']'))) { /* This is a section header. Extract it so that we can * prepend all names with it. We allow a comment after the * section and spaces after the [ and before the ]. No * spaces inside the section name. We also limit the name * to 200 characters. */ _gpgrt_trim_spaces (p2+1); if (p == p2 || (p2[1] && p2[1] != '#')) { err = GPG_ERR_INV_VALUE; goto leave; } *p2 = 0; p++; _gpgrt_trim_spaces (p); if (!*p || strpbrk (p, " \t#:") || strlen (p) > 200) { err = GPG_ERR_INV_VALUE; /* No or invalid section name */ goto leave; } xfree (section); section = xtrystrdup (p); if (!section) { err = _gpg_err_code_from_syserror (); goto leave; } /* Map all backslashes to slashes. */ for (p2=section; *p2; p2++) if (*p2 == '\\') *p2 = '/'; continue; } if (*p && *p != '#') { char *colon, *value, tmp; colon = strchr (buf, ':'); if (!colon) { err = GPG_ERR_INV_VALUE; goto leave; } value = colon + 1; tmp = *value; *value = 0; if (section) name = _gpgrt_strconcat (section, ":", p, NULL); else name = xtrystrdup (p); *value = tmp; if (!name) { err = _gpg_err_code_from_syserror (); goto leave; } if (!_gpgrt_strlist_add (&raw_value, value, slflags)) { err = _gpg_err_code_from_syserror (); goto leave; } continue; } if (!_gpgrt_strlist_add (&raw_value, buf, slflags)) { err = _gpg_err_code_from_syserror (); goto leave; } } if (len < 0) { err = _gpg_err_code_from_syserror (); goto leave; } /* Add the final entry. */ if (raw_value) { err = do_nvc_add (*result, name, NULL, raw_value, 1 /*preserve order*/); name = NULL; } leave: xfree (section); xfree (name); xfree (buf); if (err) { _gpgrt_nvc_release (*result); *result = NULL; } return err; } /* Parse STREAM and return a newly allocated name value container * structure in RESULT. If ERRLINEP is given, the line number the * parser was last considering is stored there. For private key * containers the GPGRT_NVC_PRIVKEY bit should be set in FLAGS. */ gpg_err_code_t _gpgrt_nvc_parse (gpgrt_nvc_t *result, int *errlinep, estream_t stream, unsigned int flags) { return do_nvc_parse (result, errlinep, stream, flags); } /* Helper for nvc_write. */ static gpg_err_code_t write_one_entry (gpgrt_nve_t entry, estream_t stream) { gpg_err_code_t err; gpgrt_strlist_t sl; if (entry->name) _gpgrt_fputs (entry->name, stream); err = assert_raw_value (entry); if (err) return err; for (sl = entry->raw_value; sl; sl = sl->next) _gpgrt_fputs (sl->d, stream); if (_gpgrt_ferror (stream)) return _gpg_err_code_from_syserror (); return 0; } /* Write a representation of CONT to STREAM. */ gpg_err_code_t _gpgrt_nvc_write (gpgrt_nvc_t cont, estream_t stream) { gpg_err_code_t err = 0; gpgrt_nve_t entry; gpgrt_nve_t keyentry = NULL; /* We can't yet write out in section mode because we merge entries * from the same section and do not keep a raw representation. */ if (cont->section_mode) return GPG_ERR_NOT_IMPLEMENTED; for (entry = cont->first; entry; entry = entry->next) { if (cont->private_key_mode && entry->name && same_name_p (entry->name, entry->namelen, "Key")) { if (!keyentry) keyentry = entry; continue; } err = write_one_entry (entry, stream); if (err) return err; } /* In private key mode we write the Key always last. */ if (keyentry) err = write_one_entry (keyentry, stream); return err; } /* * Convenience functions. */ /* Return the string for the first entry in NVC with NAME. If an * entry with NAME is missing in NVC or its value is the empty string * NULL is returned. Note that the returned string is a pointer * into NVC. */ const char * _gpgrt_nvc_get_string (gpgrt_nvc_t nvc, const char *name) { gpgrt_nve_t item; if (!nvc) return NULL; item = _gpgrt_nvc_lookup (nvc, name); if (!item) return NULL; return _gpgrt_nve_value (item); } /* Return true (ie. a non-zero value) if NAME exists and its value is * true; that is either "yes", "true", or a decimal value unequal to 0. */ int _gpgrt_nvc_get_bool (gpgrt_nvc_t nvc, const char *name) { gpgrt_nve_t item; const char *s; int n; if (!nvc) return 0; item = _gpgrt_nvc_lookup (nvc, name); if (!item) return 0; s = _gpgrt_nve_value (item); if (!s) return 0; n = atoi (s); if (n) return n; if (!ascii_strcasecmp (s, "yes") || !ascii_strcasecmp (s, "true")) return 1; return 0; }