diff --git a/libmultipath/Makefile b/libmultipath/Makefile
index e7254f3..7f3921c 100644
--- a/libmultipath/Makefile
+++ b/libmultipath/Makefile
@@ -53,7 +53,7 @@ OBJS = memory.o parser.o vector.o devmapper.o callout.o \
 	log.o configure.o structs_vec.o sysfs.o prio.o checkers.o \
 	lock.o file.o wwids.o prioritizers/alua_rtpg.o prkey.o \
 	io_err_stat.o dm-generic.o generic.o foreign.o nvme-lib.o \
-	libsg.o valid.o
+	libsg.o valid.o strbuf.o
 
 all:	$(DEVLIB)
 
diff --git a/libmultipath/strbuf.c b/libmultipath/strbuf.c
new file mode 100644
index 0000000..a24a57d
--- /dev/null
+++ b/libmultipath/strbuf.c
@@ -0,0 +1,207 @@
+/*
+ * Copyright (c) 2021 SUSE LLC
+ * SPDX-License-Identifier: GPL-2.0-only
+ */
+#include <inttypes.h>
+#include <stdint.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <assert.h>
+#include "strbuf.h"
+
+static const char empty_str[] = "";
+
+const char *get_strbuf_str(const struct strbuf *buf)
+{
+	return buf->buf ? buf->buf : empty_str;
+}
+
+char *steal_strbuf_str(struct strbuf *buf)
+{
+	char *p = buf->buf;
+
+	buf->buf = NULL;
+	buf->size = buf->offs = 0;
+	return p;
+}
+
+size_t get_strbuf_len(const struct strbuf *buf)
+{
+	return buf->offs;
+}
+
+static bool strbuf_is_sane(const struct strbuf *buf)
+{
+	return buf && ((!buf->buf && !buf->size && !buf->offs) ||
+		       (buf->buf && buf->size && buf->size > buf->offs));
+}
+
+void reset_strbuf(struct strbuf *buf)
+{
+	free(buf->buf);
+	buf->buf = NULL;
+	buf->size = buf->offs = 0;
+}
+
+void free_strbuf(struct strbuf *buf)
+{
+	if (!buf)
+		return;
+	reset_strbuf(buf);
+	free(buf);
+}
+
+struct strbuf *new_strbuf(void)
+{
+	return calloc(1, sizeof(struct strbuf));
+}
+
+int truncate_strbuf(struct strbuf *buf, size_t offs)
+{
+	if (!buf->buf)
+		return -EFAULT;
+	if (offs > buf->offs)
+		return -ERANGE;
+
+	buf->offs = offs;
+	buf->buf[offs] = '\0';
+	return 0;
+}
+
+#define BUF_CHUNK 64
+
+static int expand_strbuf(struct strbuf *buf, int addsz)
+{
+	size_t add;
+	char *tmp;
+
+	assert(strbuf_is_sane(buf));
+	if (addsz < 0)
+		return -EINVAL;
+	if (buf->size - buf->offs >= (size_t)addsz + 1)
+		return 0;
+
+	add = ((addsz - (buf->size - buf->offs)) / BUF_CHUNK + 1)
+		* BUF_CHUNK;
+
+	if (buf->size >= SIZE_MAX - add) {
+		add = SIZE_MAX - buf->size;
+		if (add < (size_t)addsz + 1)
+			return -EOVERFLOW;
+	}
+
+	tmp = realloc(buf->buf, buf->size + add);
+	if (!tmp)
+		return -ENOMEM;
+
+	buf->buf = tmp;
+	buf->size += add;
+	buf->buf[buf->offs] = '\0';
+
+	return 0;
+}
+
+int __append_strbuf_str(struct strbuf *buf, const char *str, int slen)
+{
+	int ret;
+
+	if ((ret = expand_strbuf(buf, slen)) < 0)
+		return ret;
+
+	memcpy(buf->buf + buf->offs, str, slen);
+	buf->offs += slen;
+	buf->buf[buf->offs] = '\0';
+
+	return slen;
+}
+
+int append_strbuf_str(struct strbuf *buf, const char *str)
+{
+	size_t slen;
+
+	if (!str)
+		return -EINVAL;
+
+	slen = strlen(str);
+	if (slen > INT_MAX)
+		return -ERANGE;
+
+	return __append_strbuf_str(buf, str, slen);
+}
+
+int fill_strbuf(struct strbuf *buf, char c, int slen)
+{
+	int ret;
+
+	if ((ret = expand_strbuf(buf, slen)) < 0)
+		return ret;
+
+	memset(buf->buf + buf->offs, c, slen);
+	buf->offs += slen;
+	buf->buf[buf->offs] = '\0';
+
+	return slen;
+}
+
+int append_strbuf_quoted(struct strbuf *buff, const char *ptr)
+{
+	char *quoted, *q;
+	const char *p;
+	unsigned n_quotes, i;
+	size_t qlen;
+	int ret;
+
+	if (!ptr)
+		return -EINVAL;
+
+	for (n_quotes = 0, p = strchr(ptr, '"'); p; p = strchr(++p, '"'))
+		n_quotes++;
+
+	/* leading + trailing quote, 1 extra quote for every quote in ptr */
+	qlen = strlen(ptr) + 2 + n_quotes;
+	if (qlen > INT_MAX)
+		return -ERANGE;
+	if ((ret = expand_strbuf(buff, qlen)) < 0)
+		return ret;
+
+	quoted = &(buff->buf[buff->offs]);
+	*quoted++ = '"';
+	for (p = ptr, q = quoted, i = 0; i < n_quotes; i++) {
+		char *q1 = memccpy(q, p, '"', qlen - 2 - (q - quoted));
+
+		assert(q1 != NULL);
+		p += q1 - q;
+		*q1++ = '"';
+		q = q1;
+	}
+	q = mempcpy(q, p, qlen - 2 - (q - quoted));
+	*q++ = '"';
+	*q = '\0';
+	ret = q - &(buff->buf[buff->offs]);
+	buff->offs += ret;
+	return ret;
+}
+
+__attribute__((format(printf, 2, 3)))
+int print_strbuf(struct strbuf *buf, const char *fmt, ...)
+{
+	va_list ap;
+	int ret;
+	char *tail;
+
+	va_start(ap, fmt);
+	ret = vasprintf(&tail, fmt, ap);
+	va_end(ap);
+
+	if (ret < 0)
+		return -ENOMEM;
+
+	ret = __append_strbuf_str(buf, tail, ret);
+
+	free(tail);
+	return ret;
+}
diff --git a/libmultipath/strbuf.h b/libmultipath/strbuf.h
new file mode 100644
index 0000000..5903572
--- /dev/null
+++ b/libmultipath/strbuf.h
@@ -0,0 +1,168 @@
+/*
+ * Copyright (c) 2021 SUSE LLC
+ * SPDX-License-Identifier: GPL-2.0-only
+ */
+#ifndef _STRBUF_H
+#define _STRBUF_H
+#include <errno.h>
+#include <string.h>
+
+struct strbuf {
+	char *buf;
+	size_t size;
+	size_t offs;
+};
+
+/**
+ * reset_strbuf(): prepare strbuf for new content
+ * @param strbuf: string buffer to reset
+ *
+ * Frees internal buffer and resets size and offset to 0.
+ * Can be used to cleanup a struct strbuf on stack.
+ */
+void reset_strbuf(struct strbuf *buf);
+
+/**
+ * free_strbuf(): free resources
+ * @param strbuf: string buffer to discard
+ *
+ * Frees all memory occupied by a struct strbuf.
+ */
+void free_strbuf(struct strbuf *buf);
+
+/**
+ * macro: STRBUF_INIT
+ *
+ * Use this to initialize a local struct strbuf on the stack,
+ * or in a global/static variable.
+ */
+#define STRBUF_INIT { .buf = NULL, }
+
+/**
+ * macro: STRBUF_ON_STACK
+ *
+ * Define and initialize a local struct @strbuf to be cleaned up when
+ * the current scope is left
+ */
+#define STRBUF_ON_STACK(__x)						\
+	struct strbuf __attribute__((cleanup(reset_strbuf))) (__x) = STRBUF_INIT;
+
+/**
+ * new_strbuf(): allocate a struct strbuf on the heap
+ *
+ * @returns: pointer to allocated struct, or NULL in case of error.
+ */
+struct strbuf *new_strbuf(void);
+
+/**
+ * get_strbuf_str(): retrieve string from strbuf
+ * @param buf: a struct strbuf
+ * @returns: pointer to the string written to the strbuf so far.
+ *
+ * If @strbuf was never written to, the function returns a zero-
+ * length string. The return value of this function must not be
+ * free()d.
+ */
+const char *get_strbuf_str(const struct strbuf *buf);
+
+/**
+ * steal_strbuf_str(): retrieve string from strbuf and reset
+ * @param buf: a struct strbuf
+ * @returns: pointer to the string written to @strbuf, or NULL
+ *
+ * After calling this function, the @strbuf is empty as if freshly
+ * initialized. The caller is responsible to free() the returned pointer.
+ * If @strbuf was never written to (not even an empty string was appended),
+ * the function returns NULL.
+ */
+char *steal_strbuf_str(struct strbuf *buf);
+
+/**
+ * get_strbuf_len(): retrieve string length from strbuf
+ * @param buf: a struct strbuf
+ * @returns: the length of the string written to @strbuf so far.
+ */
+size_t get_strbuf_len(const struct strbuf *buf);
+
+/**
+ * truncate_strbuf(): shorten the buffer
+ * @param buf: struct strbuf to truncate
+ * @param offs: new buffer position / offset
+ * @returns: 0 on success, negative error code otherwise.
+ *
+ * If @strbuf is freshly allocated/reset (never written to), -EFAULT
+ * is returned. if @offs must be higher than the current offset as returned
+ * by get_strbuf_len(), -ERANGE is returned. The allocated size of the @strbuf
+ * remains unchanged.
+ */
+int truncate_strbuf(struct strbuf *buf, size_t offs);
+
+/**
+ * __append_strbuf_str(): append string of known length
+ * @param buf: the struct strbuf to write to
+ * @param str: the string to append, not necessarily 0-terminated
+ * @param slen: max number of characters to append, must be non-negative
+ * @returns: @slen = number of appended characters if successful (excluding
+ * terminating '\0'); negative error code otherwise.
+ *
+ * Notes: a 0-byte is always appended to the output buffer after @slen characters.
+ * 0-bytes possibly contained in the first @slen characters are copied into
+ * the output. If the function returns an error, @strbuf is unchanged.
+ */
+int __append_strbuf_str(struct strbuf *buf, const char *str, int slen);
+
+/**
+ * append_strbuf_str(): append string
+ * @param buf: the struct strbuf to write to
+ * @param str: the string to append, 0-terminated
+ * @returns: number of appended characters if successful (excluding
+ * terminating '\0'); negative error code otherwise
+ *
+ * Appends the given 0-terminated string to @strbuf, expanding @strbuf's size
+ * as necessary. If the function returns an error, @strbuf is unchanged.
+ */
+int append_strbuf_str(struct strbuf *buf, const char *str);
+
+/**
+ * fill_strbuf_str(): pad strbuf with a character
+ * @param buf: the struct strbuf to write to
+ * @param c: the character used for filling
+ * @param slen: max number of characters to append, must be non-negative
+ * @returns: number of appended characters if successful (excluding
+ * terminating '\0'); negative error code otherwise
+ *
+ * Appends the given character @slen times to @strbuf, expanding @strbuf's size
+ * as necessary. If the function returns an error, @strbuf is unchanged.
+ */
+int fill_strbuf(struct strbuf *buf, char c, int slen);
+
+/**
+ * append_strbuf_quoted(): append string in double quotes, escaping quotes in string
+ * @param buf: the struct strbuf to write to
+ * @param str: the string to append, 0-terminated
+ * @returns: number of appended characters if successful (excluding
+ * terminating '\0'); negative error code otherwise
+ *
+ * Appends the given string to @strbuf, with leading and trailing double
+ * quotes (") added, expanding @strbuf's size as necessary. Any double quote
+ * characters (") in the string are transformed to double double quotes ("").
+ * If the function returns an error, @strbuf is unchanged.
+ */
+int append_strbuf_quoted(struct strbuf *buf, const char *str);
+
+/**
+ * print_strbuf(): print to strbuf, formatted
+ * @param buf: the struct strbuf to print to
+ * @param fmt: printf()-like format string
+ * @returns: number of appended characters if successful, (excluding
+ * terminating '\0'); negative error code otherwise
+ *
+ * Appends the the arguments following @fmt, formatted as in printf(), to
+ * @strbuf, expanding @strbuf's size as necessary. The function makes sure that
+ * the output @strbuf is always 0-terminated.
+ * If the function returns an error, @strbuf is unchanged.
+ */
+__attribute__((format(printf, 2, 3)))
+int print_strbuf(struct strbuf *buf, const char *fmt, ...);
+
+#endif
diff --git a/tests/Makefile b/tests/Makefile
index e70c8ed..8cbc4b7 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -13,7 +13,7 @@ CFLAGS += $(BIN_CFLAGS) -I$(multipathdir) -I$(mpathcmddir) \
 LIBDEPS += -L. -L$(mpathcmddir) -lmultipath -lmpathcmd -lcmocka
 
 TESTS := uevent parser util dmevents hwtable blacklist unaligned vpd pgpolicy \
-	 alias directio valid devt mpathvalid
+	 alias directio valid devt mpathvalid strbuf
 HELPERS := test-lib.o test-log.o
 
 .SILENT: $(TESTS:%=%.o)
@@ -63,6 +63,7 @@ mpathvalid-test_OBJDEPS := ../libmpathvalid/mpath_valid.o
 ifneq ($(DIO_TEST_DEV),)
 directio-test_LIBDEPS := -laio
 endif
+strbuf-test_OBJDEPS := ../libmultipath/strbuf.o
 
 %.o: %.c
 	$(CC) $(CFLAGS) $($*-test_FLAGS) -c -o $@ $<
diff --git a/tests/strbuf.c b/tests/strbuf.c
new file mode 100644
index 0000000..43a477d
--- /dev/null
+++ b/tests/strbuf.c
@@ -0,0 +1,412 @@
+/*
+ * Copyright (c) 2021 SUSE LLC
+ * SPDX-License-Identifier: GPL-2.0-only
+ */
+
+#define _GNU_SOURCE
+#include <stdbool.h>
+#include <stdarg.h>
+#include <stddef.h>
+#include <setjmp.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <cmocka.h>
+#include <errno.h>
+#include "strbuf.h"
+#include "debug.h"
+#include "globals.c"
+
+void *__real_realloc(void *ptr, size_t size);
+
+static bool mock_realloc = false;
+void *__wrap_realloc(void *ptr, size_t size)
+{
+	void *p;
+	if (!mock_realloc)
+		return __real_realloc(ptr, size);
+
+	p = mock_ptr_type(void *);
+	condlog(4, "%s: %p, %zu -> %p", __func__, ptr, size, p);
+	return p;
+}
+
+static void test_strbuf_00(void **state)
+{
+	STRBUF_ON_STACK(buf);
+	char *p;
+
+	assert_ptr_equal(buf.buf, NULL);
+	assert_int_equal(buf.size, 0);
+	assert_int_equal(buf.offs, 0);
+	assert_int_equal(get_strbuf_len(&buf), 0);
+	assert_string_equal(get_strbuf_str(&buf), "");
+	p = steal_strbuf_str(&buf);
+	assert_ptr_equal(p, NULL);
+
+	assert_ptr_equal(buf.buf, NULL);
+	assert_int_equal(buf.size, 0);
+	assert_int_equal(buf.offs, 0);
+	assert_int_equal(get_strbuf_len(&buf), 0);
+	assert_string_equal(get_strbuf_str(&buf), "");
+
+	assert_int_equal(append_strbuf_str(&buf, "moin"), 4);
+	assert_int_equal(get_strbuf_len(&buf), 4);
+	assert_in_range(buf.size, 5, SIZE_MAX);
+	assert_string_equal(get_strbuf_str(&buf), "moin");
+	p = steal_strbuf_str(&buf);
+	assert_string_equal(p, "moin");
+	free(p);
+
+	assert_ptr_equal(buf.buf, NULL);
+	assert_int_equal(buf.size, 0);
+	assert_int_equal(buf.offs, 0);
+	assert_int_equal(get_strbuf_len(&buf), 0);
+	assert_string_equal(get_strbuf_str(&buf), "");
+
+	assert_int_equal(append_strbuf_str(&buf, NULL), -EINVAL);
+	assert_int_equal(buf.size, 0);
+	assert_int_equal(buf.offs, 0);
+	assert_int_equal(get_strbuf_len(&buf), 0);
+	assert_string_equal(get_strbuf_str(&buf), "");
+
+	assert_int_equal(append_strbuf_str(&buf, ""), 0);
+	/* appending a 0-length string allocates memory */
+	assert_in_range(buf.size, 1, SIZE_MAX);
+	assert_int_equal(buf.offs, 0);
+	assert_int_equal(get_strbuf_len(&buf), 0);
+	assert_string_equal(get_strbuf_str(&buf), "");
+	p = steal_strbuf_str(&buf);
+	assert_string_equal(p, "");
+	free(p);
+
+	assert_int_equal(__append_strbuf_str(&buf, "x", 0), 0);
+	/* appending a 0-length string allocates memory */
+	assert_in_range(buf.size, 1, SIZE_MAX);
+	assert_int_equal(buf.offs, 0);
+	assert_int_equal(get_strbuf_len(&buf), 0);
+	assert_string_equal(get_strbuf_str(&buf), "");
+}
+
+static void test_strbuf_alloc_err(void **state)
+{
+	STRBUF_ON_STACK(buf);
+	size_t sz, ofs;
+	int rc;
+
+	mock_realloc = true;
+	will_return(__wrap_realloc, NULL);
+	assert_int_equal(append_strbuf_str(&buf, "moin"), -ENOMEM);
+	assert_int_equal(buf.size, 0);
+	assert_int_equal(buf.offs, 0);
+	assert_int_equal(get_strbuf_len(&buf), 0);
+	assert_string_equal(get_strbuf_str(&buf), "");
+
+	mock_realloc = false;
+	assert_int_equal(append_strbuf_str(&buf, "moin"), 4);
+	sz = buf.size;
+	assert_in_range(sz, 5, SIZE_MAX);
+	assert_int_equal(buf.offs, 4);
+	assert_int_equal(get_strbuf_len(&buf), 4);
+	assert_string_equal(get_strbuf_str(&buf), "moin");
+
+	mock_realloc = true;
+	will_return(__wrap_realloc, NULL);
+	ofs = get_strbuf_len(&buf);
+	while ((rc = append_strbuf_str(&buf, " hello")) >= 0) {
+		condlog(3, "%s", get_strbuf_str(&buf));
+		assert_int_equal(rc, 6);
+		assert_int_equal(get_strbuf_len(&buf), ofs + 6);
+		assert_memory_equal(get_strbuf_str(&buf), "moin", 4);
+		assert_string_equal(get_strbuf_str(&buf) + ofs, " hello");
+		ofs = get_strbuf_len(&buf);
+	}
+	assert_int_equal(rc, -ENOMEM);
+	assert_int_equal(buf.size, sz);
+	assert_int_equal(get_strbuf_len(&buf), ofs);
+	assert_memory_equal(get_strbuf_str(&buf), "moin", 4);
+	assert_string_equal(get_strbuf_str(&buf) + ofs - 6, " hello");
+
+	reset_strbuf(&buf);
+	assert_ptr_equal(buf.buf, NULL);
+	assert_int_equal(buf.size, 0);
+	assert_int_equal(buf.offs, 0);
+	assert_int_equal(get_strbuf_len(&buf), 0);
+	assert_string_equal(get_strbuf_str(&buf), "");
+
+	mock_realloc = false;
+}
+
+static void test_strbuf_overflow(void **state)
+{
+	STRBUF_ON_STACK(buf);
+
+	assert_int_equal(append_strbuf_str(&buf, "x"), 1);
+	/* fake huge buffer */
+	buf.size = SIZE_MAX - 1;
+	buf.offs = buf.size - 1;
+	assert_int_equal(append_strbuf_str(&buf, "x"), -EOVERFLOW);
+}
+
+static void test_strbuf_big(void **state)
+{
+	STRBUF_ON_STACK(buf);
+	const char big[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\n";
+	char *bbig;
+	int i;
+
+	/* Under valgrind, 30000 iterations need ca. 30s on my laptop */
+	for (i = 0; i < 30000; i++) {
+		if (i % 1000 == 0)
+			condlog(4, "%d", i);
+		assert_int_equal(append_strbuf_str(&buf, big), sizeof(big) - 1);
+		assert_int_equal(get_strbuf_len(&buf), (sizeof(big) - 1) * (i + 1));
+		assert_memory_equal(get_strbuf_str(&buf), big, sizeof(big) - 1);
+		assert_string_equal(get_strbuf_str(&buf) + get_strbuf_len(&buf)
+				    - (sizeof(big) - 1), big);
+	};
+	bbig = steal_strbuf_str(&buf);
+
+	assert_ptr_equal(buf.buf, NULL);
+	assert_int_equal(buf.size, 0);
+	assert_int_equal(buf.offs, 0);
+	assert_int_equal(get_strbuf_len(&buf), 0);
+	assert_string_equal(get_strbuf_str(&buf), "");
+
+	assert_int_equal(strlen(bbig), i * (sizeof(big) - 1));
+	assert_memory_equal(bbig, big, sizeof(big) - 1);
+	free(bbig);
+}
+
+static void test_strbuf_nul(void **state)
+{
+	STRBUF_ON_STACK(buf);
+	char greet[] = "hello, sir!";
+
+	assert_int_equal(__append_strbuf_str(&buf, greet, 6), 6);
+	assert_string_equal(get_strbuf_str(&buf), "hello,");
+	assert_int_equal(__append_strbuf_str(&buf, greet, 6), 6);
+	assert_string_equal(get_strbuf_str(&buf), "hello,hello,");
+
+	/* overwrite comma with NUL; append_strbuf_str() stops at NUL byte */
+	greet[5] = '\0';
+	reset_strbuf(&buf);
+	assert_int_equal(append_strbuf_str(&buf, greet), 5);
+	assert_int_equal(get_strbuf_len(&buf), 5);
+	assert_string_equal(get_strbuf_str(&buf), "hello");
+	assert_int_equal(append_strbuf_str(&buf, greet), 5);
+	assert_int_equal(get_strbuf_len(&buf), 10);
+	assert_string_equal(get_strbuf_str(&buf), "hellohello");
+
+	/* __append_strbuf_str() appends full memory, including NUL bytes */
+	reset_strbuf(&buf);
+	assert_int_equal(__append_strbuf_str(&buf, greet, sizeof(greet) - 1),
+			 sizeof(greet) - 1);
+	assert_int_equal(get_strbuf_len(&buf), sizeof(greet) - 1);
+	assert_string_equal(get_strbuf_str(&buf), "hello");
+	assert_string_equal(get_strbuf_str(&buf) + get_strbuf_len(&buf) - 5, " sir!");
+	assert_int_equal(__append_strbuf_str(&buf, greet, sizeof(greet) - 1),
+			 sizeof(greet) - 1);
+	assert_string_equal(get_strbuf_str(&buf), "hello");
+	assert_int_equal(get_strbuf_len(&buf), 2 * (sizeof(greet) - 1));
+	assert_string_equal(get_strbuf_str(&buf) + get_strbuf_len(&buf) - 5, " sir!");
+}
+
+static void test_strbuf_quoted(void **state)
+{
+	STRBUF_ON_STACK(buf);
+	const char said[] = "She said ";
+	const char greet[] = "hi, man!";
+	char *p;
+	size_t n;
+
+	assert_int_equal(append_strbuf_str(&buf, said), sizeof(said) - 1);
+	assert_int_equal(append_strbuf_quoted(&buf, greet), sizeof(greet) + 1);
+	assert_string_equal(get_strbuf_str(&buf), "She said \"hi, man!\"");
+	n = get_strbuf_len(&buf);
+	p = steal_strbuf_str(&buf);
+	assert_int_equal(append_strbuf_str(&buf, said), sizeof(said) - 1);
+	assert_int_equal(append_strbuf_quoted(&buf, p), n + 4);
+	assert_string_equal(get_strbuf_str(&buf),
+			    "She said \"She said \"\"hi, man!\"\"\"");
+	free(p);
+	n = get_strbuf_len(&buf);
+	p = steal_strbuf_str(&buf);
+	assert_int_equal(append_strbuf_str(&buf, said), sizeof(said) - 1);
+	assert_int_equal(append_strbuf_quoted(&buf, p), n + 8);
+	assert_string_equal(get_strbuf_str(&buf),
+			    "She said \"She said \"\"She said \"\"\"\"hi, man!\"\"\"\"\"\"\"");
+	free(p);
+}
+
+static void test_strbuf_escaped(void **state)
+{
+	STRBUF_ON_STACK(buf);
+	const char said[] = "She said \"hi, man\"";
+
+	assert_int_equal(append_strbuf_quoted(&buf, said), sizeof(said) + 3);
+	assert_string_equal(get_strbuf_str(&buf),
+			    "\"She said \"\"hi, man\"\"\"");
+
+	reset_strbuf(&buf);
+	assert_int_equal(append_strbuf_quoted(&buf, "\""), 4);
+	assert_string_equal(get_strbuf_str(&buf), "\"\"\"\"");
+
+	reset_strbuf(&buf);
+	assert_int_equal(append_strbuf_quoted(&buf, "\"\""), 6);
+	assert_string_equal(get_strbuf_str(&buf), "\"\"\"\"\"\"");
+
+	reset_strbuf(&buf);
+	assert_int_equal(append_strbuf_quoted(&buf, "\"Hi\""), 8);
+	assert_string_equal(get_strbuf_str(&buf), "\"\"\"Hi\"\"\"");
+}
+
+#define SENTENCE "yields, preceded by itself, falsehood"
+static void test_print_strbuf(void **state)
+{
+	STRBUF_ON_STACK(buf);
+	char sentence[] = SENTENCE;
+
+	assert_int_equal(print_strbuf(&buf, "\"%s\" %s.", sentence, sentence),
+			 2 * (sizeof(sentence) - 1) + 4);
+	assert_string_equal(get_strbuf_str(&buf),
+			    "\"" SENTENCE "\" " SENTENCE ".");
+	condlog(3, "%s", get_strbuf_str(&buf));
+
+	reset_strbuf(&buf);
+	assert_int_equal(print_strbuf(&buf, "0x%08x", 0xdeadbeef), 10);
+	assert_string_equal(get_strbuf_str(&buf), "0xdeadbeef");
+
+	reset_strbuf(&buf);
+	assert_int_equal(print_strbuf(&buf, "%d%% of %d is %0.2f",
+				      5, 100, 0.05), 17);
+	assert_string_equal(get_strbuf_str(&buf), "5% of 100 is 0.05");
+}
+
+static void test_truncate_strbuf(void **state)
+{
+	STRBUF_ON_STACK(buf);
+	const char str[] = "hello my dear!\n";
+	size_t sz, sz1;
+
+	assert_int_equal(truncate_strbuf(&buf, 1), -EFAULT);
+	assert_int_equal(truncate_strbuf(&buf, 0), -EFAULT);
+
+	assert_int_equal(append_strbuf_str(&buf, str), sizeof(str) - 1);
+	assert_int_equal(get_strbuf_len(&buf), sizeof(str) - 1);
+	assert_string_equal(get_strbuf_str(&buf), str);
+
+	assert_int_equal(truncate_strbuf(&buf, sizeof(str)), -ERANGE);
+	assert_int_equal(get_strbuf_len(&buf), sizeof(str) - 1);
+	assert_string_equal(get_strbuf_str(&buf), str);
+
+	assert_int_equal(truncate_strbuf(&buf, sizeof(str) - 1), 0);
+	assert_int_equal(get_strbuf_len(&buf), sizeof(str) - 1);
+	assert_string_equal(get_strbuf_str(&buf), str);
+
+	assert_int_equal(truncate_strbuf(&buf, sizeof(str) - 2), 0);
+	assert_int_equal(get_strbuf_len(&buf), sizeof(str) - 2);
+	assert_string_not_equal(get_strbuf_str(&buf), str);
+	assert_memory_equal(get_strbuf_str(&buf), str, sizeof(str) - 2);
+
+	assert_int_equal(truncate_strbuf(&buf, 5), 0);
+	assert_int_equal(get_strbuf_len(&buf), 5);
+	assert_string_not_equal(get_strbuf_str(&buf), str);
+	assert_string_equal(get_strbuf_str(&buf), "hello");
+
+	reset_strbuf(&buf);
+	assert_int_equal(append_strbuf_str(&buf, str), sizeof(str) - 1);
+
+	sz = buf.size;
+	while (buf.size == sz)
+		assert_int_equal(append_strbuf_str(&buf, str), sizeof(str) - 1);
+
+	sz1  = buf.size;
+	assert_in_range(get_strbuf_len(&buf), sz + 1, SIZE_MAX);
+	assert_string_equal(get_strbuf_str(&buf) +
+			    get_strbuf_len(&buf) - (sizeof(str) - 1), str);
+	assert_int_equal(truncate_strbuf(&buf, get_strbuf_len(&buf) + 1),
+			 -ERANGE);
+	assert_int_equal(truncate_strbuf(&buf, get_strbuf_len(&buf)), 0);
+	assert_int_equal(truncate_strbuf(&buf, get_strbuf_len(&buf)
+					 - (sizeof(str) - 1)), 0);
+	assert_in_range(get_strbuf_len(&buf), 1, sz);
+	assert_string_equal(get_strbuf_str(&buf) +
+			    get_strbuf_len(&buf) - (sizeof(str) - 1), str);
+	assert_int_equal(buf.size, sz1);
+
+	assert_int_equal(truncate_strbuf(&buf, 5), 0);
+	assert_int_equal(get_strbuf_len(&buf), 5);
+	assert_string_equal(get_strbuf_str(&buf), "hello");
+	assert_int_equal(buf.size, sz1);
+
+	assert_int_equal(truncate_strbuf(&buf, 0), 0);
+	assert_int_equal(get_strbuf_len(&buf), 0);
+	assert_string_equal(get_strbuf_str(&buf), "");
+	assert_int_equal(buf.size, sz1);
+}
+
+static void test_fill_strbuf(void **state)
+{
+	STRBUF_ON_STACK(buf);
+	int i;
+	char *p;
+
+	assert_int_equal(fill_strbuf(&buf, '+', -5), -EINVAL);
+
+	assert_int_equal(fill_strbuf(&buf, '+', 0), 0);
+	assert_int_equal(get_strbuf_len(&buf), 0);
+	assert_string_equal(get_strbuf_str(&buf), "");
+
+	assert_int_equal(fill_strbuf(&buf, '+', 1), 1);
+	assert_int_equal(get_strbuf_len(&buf), 1);
+	assert_string_equal(get_strbuf_str(&buf), "+");
+
+	assert_int_equal(fill_strbuf(&buf, '-', 3), 3);
+	assert_int_equal(get_strbuf_len(&buf), 4);
+	assert_string_equal(get_strbuf_str(&buf), "+---");
+
+	assert_int_equal(fill_strbuf(&buf, '\0', 3), 3);
+	assert_int_equal(get_strbuf_len(&buf), 7);
+	assert_string_equal(get_strbuf_str(&buf), "+---");
+
+	truncate_strbuf(&buf, 4);
+	assert_int_equal(fill_strbuf(&buf, '+', 4), 4);
+	assert_int_equal(get_strbuf_len(&buf), 8);
+	assert_string_equal(get_strbuf_str(&buf), "+---++++");
+
+	reset_strbuf(&buf);
+	assert_int_equal(fill_strbuf(&buf, 'x', 30000), 30000);
+	assert_int_equal(get_strbuf_len(&buf), 30000);
+	p = steal_strbuf_str(&buf);
+	assert_int_equal(strlen(p), 30000);
+	for (i = 0; i < 30000; i++)
+		assert_int_equal(p[i], 'x');
+	free(p);
+}
+
+static int test_strbuf(void)
+{
+	const struct CMUnitTest tests[] = {
+		cmocka_unit_test(test_strbuf_00),
+		cmocka_unit_test(test_strbuf_alloc_err),
+		cmocka_unit_test(test_strbuf_overflow),
+		cmocka_unit_test(test_strbuf_big),
+		cmocka_unit_test(test_strbuf_nul),
+		cmocka_unit_test(test_strbuf_quoted),
+		cmocka_unit_test(test_strbuf_escaped),
+		cmocka_unit_test(test_print_strbuf),
+		cmocka_unit_test(test_truncate_strbuf),
+		cmocka_unit_test(test_fill_strbuf),
+	};
+
+	return cmocka_run_group_tests(tests, NULL, NULL);
+}
+
+int main(void)
+{
+	int ret = 0;
+
+	init_test_verbosity(-1);
+	ret += test_strbuf();
+	return ret;
+}
