changeset 684:41a3427cde1d

cbor: unpacking API This is an API to decode specific CBOR items. Signed-off-by: Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
author Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
date Mon, 06 Nov 2017 11:15:00 -0400
parents b3a4ac14a22d
children 09e338f3ebb2
files fmt_cbor.c include/jeffpc/cbor.h mapfile-vers tests/.hgignore tests/CMakeLists.txt tests/test_cbor_unpack.c
diffstat 6 files changed, 862 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/fmt_cbor.c	Thu Oct 11 14:20:08 2018 -0400
+++ b/fmt_cbor.c	Mon Nov 06 11:15:00 2017 -0400
@@ -20,6 +20,7 @@
  * SOFTWARE.
  */
 
+#include <jeffpc/mem.h>
 #include <jeffpc/cbor.h>
 #include <jeffpc/nvl.h>
 
@@ -359,3 +360,416 @@
 
 	return 0;
 }
+
+/*
+ * unpack
+ */
+
+static int read_cbor_type(struct buffer *buffer, enum major_type *type,
+			  uint8_t *extra)
+{
+	uint8_t byte;
+	ssize_t ret;
+
+	ret = buffer_read(buffer, &byte, 1);
+	if (ret < 1)
+		return ret ? ret : -EINVAL;
+
+	*type = byte >> 5;
+	*extra = byte & 0x1f;
+
+	return 0;
+}
+
+static int get_addl_bytes(struct buffer *buffer, uint8_t extra, uint64_t *out)
+{
+	const void *ptr;
+	ssize_t ret;
+	size_t size;
+
+	switch (extra) {
+		case ADDL_UINT8:
+			size = 1;
+			break;
+		case ADDL_UINT16:
+			size = 2;
+			break;
+		case ADDL_UINT32:
+			size = 4;
+			break;
+		case ADDL_UINT64:
+			size = 8;
+			break;
+		default:
+			if (extra > 23)
+				return -EINVAL;
+
+			size = 0;
+			break;
+	}
+
+	if (buffer_remain(buffer) < size)
+		return -EINVAL;
+
+	ptr = buffer_data_current(buffer);
+
+	switch (size) {
+		case 0:
+			*out = extra;
+			break;
+		case 1:
+			*out = be8_to_cpu_unaligned(ptr);
+			break;
+		case 2:
+			*out = be16_to_cpu_unaligned(ptr);
+			break;
+		case 4:
+			*out = be32_to_cpu_unaligned(ptr);
+			break;
+		case 8:
+			*out = be64_to_cpu_unaligned(ptr);
+			break;
+	}
+
+	ret = buffer_seek(buffer, size, SEEK_CUR);
+
+	return (ret < 0) ? ret : 0;
+}
+
+static int unpack_cbor_int(struct buffer *buffer, enum major_type expected_type,
+			   uint64_t *out)
+{
+	enum major_type type;
+	uint8_t extra;
+	int ret;
+
+	ret = read_cbor_type(buffer, &type, &extra);
+	if (ret)
+		return ret;
+
+	if (expected_type != type)
+		return -EILSEQ;
+
+	return get_addl_bytes(buffer, extra, out);
+}
+
+/* NOTE: the FLOAT major type is used for a *lot* of different things */
+static int unpack_cbor_float(struct buffer *buffer, uint8_t *extra)
+{
+	enum major_type type;
+	int ret;
+
+	ret = read_cbor_type(buffer, &type, extra);
+	if (ret)
+		return ret;
+
+	if (type != CMT_FLOAT)
+		return -EILSEQ;
+
+	switch (*extra) {
+		case ADDL_FLOAT_FALSE:
+		case ADDL_FLOAT_TRUE:
+		case ADDL_FLOAT_NULL:
+		case ADDL_FLOAT_BREAK:
+			return 0;
+	}
+
+	return -EILSEQ;
+}
+
+static int unpack_cbor_arraymap_start(struct buffer *buffer,
+				      enum major_type exp_type, uint8_t indef,
+				      uint64_t *len, bool *end_required)
+{
+	enum major_type type;
+	uint8_t extra;
+	int ret;
+
+	ret = read_cbor_type(buffer, &type, &extra);
+	if (ret)
+		return ret;
+
+	if (type != exp_type)
+		return -EILSEQ;
+
+	if (extra == indef) {
+		*end_required = true;
+		*len = 0;
+		return 0;
+	} else {
+		*end_required = false;
+		return get_addl_bytes(buffer, extra, len);
+	}
+}
+
+static int sync_buffers(struct buffer *orig, struct buffer *tmp)
+{
+	ssize_t ret;
+
+	ret = buffer_seek(orig, buffer_used(tmp), SEEK_CUR);
+
+	return (ret < 0) ? ret : 0;
+}
+
+int cbor_unpack_uint(struct buffer *buffer, uint64_t *v)
+{
+	struct buffer tmp;
+	int ret;
+
+	buffer_init_static(&tmp, buffer_data_current(buffer),
+			   buffer_remain(buffer), false);
+
+	ret = unpack_cbor_int(&tmp, CMT_UINT, v);
+	if (ret)
+		return ret;
+
+	return sync_buffers(buffer, &tmp);
+}
+
+int cbor_unpack_nint(struct buffer *buffer, uint64_t *v)
+{
+	struct buffer tmp;
+	int ret;
+
+	buffer_init_static(&tmp, buffer_data_current(buffer),
+			   buffer_remain(buffer), false);
+
+	ret = unpack_cbor_int(&tmp, CMT_NINT, v);
+	if (ret)
+		return ret;
+
+	return sync_buffers(buffer, &tmp);
+}
+
+int cbor_unpack_int(struct buffer *buffer, int64_t *v)
+{
+	struct buffer tmp;
+	uint64_t tmpv;
+	int ret;
+
+	/*
+	 * First, try unsigned ints
+	 */
+
+	buffer_init_static(&tmp, buffer_data_current(buffer),
+			   buffer_remain(buffer), false);
+
+	ret = cbor_unpack_uint(&tmp, &tmpv);
+	if (!ret) {
+		if (tmpv > INT64_MAX)
+			return -EOVERFLOW;
+
+		*v = tmpv;
+
+		return sync_buffers(buffer, &tmp);
+	}
+
+	/*
+	 * Second, try negative ints
+	 */
+
+	buffer_init_static(&tmp, buffer_data_current(buffer),
+			   buffer_remain(buffer), false);
+
+	ret = cbor_unpack_nint(&tmp, &tmpv);
+	if (!ret) {
+		/* 2's complement has one extra negative number */
+		if (tmpv > (((uint64_t) INT64_MAX) + 1))
+			return -EOVERFLOW;
+
+		*v = ((~tmpv) + 1);
+
+		return sync_buffers(buffer, &tmp);
+	}
+
+	return ret;
+}
+
+int cbor_unpack_blob(struct buffer *buffer, const void **data,
+		     size_t *size)
+{
+	return -ENOTSUP;
+}
+
+int cbor_unpack_cstr_len(struct buffer *buffer, char **str, size_t *len)
+{
+	uint64_t parsed_len;
+	struct buffer tmp;
+	ssize_t ret;
+	char *out;
+
+	buffer_init_static(&tmp, buffer_data_current(buffer),
+			   buffer_remain(buffer), false);
+
+	ret = unpack_cbor_int(&tmp, CMT_TEXT, &parsed_len);
+	if (ret)
+		return ret;
+
+	/* can't handle strings longer than what fits in memory */
+	if (parsed_len > SIZE_MAX)
+		return -EOVERFLOW;
+
+	out = malloc(parsed_len + 1);
+	if (!out)
+		return -ENOMEM;
+
+	ret = buffer_read(&tmp, out, parsed_len);
+	if (ret < 0)
+		goto err;
+
+	/* must read exactly the number of bytes */
+	if (ret != parsed_len) {
+		ret = -EILSEQ;
+		goto err;
+	}
+
+	out[parsed_len] = '\0';
+
+	*len = parsed_len;
+	*str = out;
+
+	return sync_buffers(buffer, &tmp);
+
+err:
+	free(out);
+
+	return ret;
+}
+
+int cbor_unpack_str(struct buffer *buffer, struct str **str)
+{
+	char *s;
+	size_t len;
+	int ret;
+
+	ret = cbor_unpack_cstr_len(buffer, &s, &len);
+	if (ret)
+		return ret;
+
+	*str = str_alloc(s);
+
+	return IS_ERR(*str) ? PTR_ERR(*str) : 0;
+}
+
+int cbor_unpack_bool(struct buffer *buffer, bool *b)
+{
+	struct buffer tmp;
+	uint8_t extra;
+	int ret;
+
+	buffer_init_static(&tmp, buffer_data_current(buffer),
+			   buffer_remain(buffer), false);
+
+	ret = unpack_cbor_float(&tmp, &extra);
+	if (ret)
+		return ret;
+
+	switch (extra) {
+		case ADDL_FLOAT_FALSE:
+			*b = false;
+			break;
+		case ADDL_FLOAT_TRUE:
+			*b = true;
+			break;
+		default:
+			return -EILSEQ;
+	}
+
+	return sync_buffers(buffer, &tmp);
+}
+
+int cbor_unpack_null(struct buffer *buffer)
+{
+	struct buffer tmp;
+	uint8_t extra;
+	int ret;
+
+	buffer_init_static(&tmp, buffer_data_current(buffer),
+			   buffer_remain(buffer), false);
+
+	ret = unpack_cbor_float(&tmp, &extra);
+	if (ret)
+		return ret;
+
+	switch (extra) {
+		case ADDL_FLOAT_NULL:
+			break;
+		default:
+			return -EILSEQ;
+	}
+
+	return sync_buffers(buffer, &tmp);
+}
+
+int cbor_unpack_break(struct buffer *buffer)
+{
+	struct buffer tmp;
+	uint8_t extra;
+	int ret;
+
+	buffer_init_static(&tmp, buffer_data_current(buffer),
+			   buffer_remain(buffer), false);
+
+	ret = unpack_cbor_float(&tmp, &extra);
+	if (ret)
+		return ret;
+
+	switch (extra) {
+		case ADDL_FLOAT_BREAK:
+			break;
+		default:
+			return -EILSEQ;
+	}
+
+	return sync_buffers(buffer, &tmp);
+}
+
+int cbor_unpack_map_start(struct buffer *buffer, uint64_t *npairs,
+			  bool *end_required)
+{
+	struct buffer tmp;
+	int ret;
+
+	buffer_init_static(&tmp, buffer_data_current(buffer),
+			   buffer_remain(buffer), false);
+
+	ret = unpack_cbor_arraymap_start(&tmp, CMT_MAP, ADDL_MAP_INDEF,
+					 npairs, end_required);
+	if (ret)
+		return ret;
+
+	return sync_buffers(buffer, &tmp);
+}
+
+int cbor_unpack_map_end(struct buffer *buffer, bool end_required)
+{
+	if (!end_required)
+		return 0;
+
+	return cbor_unpack_break(buffer);
+}
+
+int cbor_unpack_array_start(struct buffer *buffer, uint64_t *nelem,
+			    bool *end_required)
+{
+	struct buffer tmp;
+	int ret;
+
+	buffer_init_static(&tmp, buffer_data_current(buffer),
+			   buffer_remain(buffer), false);
+
+	ret = unpack_cbor_arraymap_start(&tmp, CMT_ARRAY, ADDL_ARRAY_INDEF,
+					 nelem, end_required);
+	if (ret)
+		return ret;
+
+	return sync_buffers(buffer, &tmp);
+}
+
+int cbor_unpack_array_end(struct buffer *buffer, bool end_required)
+{
+	if (!end_required)
+		return 0;
+
+	return cbor_unpack_break(buffer);
+}
--- a/include/jeffpc/cbor.h	Thu Oct 11 14:20:08 2018 -0400
+++ b/include/jeffpc/cbor.h	Mon Nov 06 11:15:00 2017 -0400
@@ -58,4 +58,26 @@
 
 extern int cbor_peek_type(struct buffer *buffer, enum val_type *type);
 
+/*
+ * On failure, the buffer state is unchanged.  On success, the buffer is
+ * updated to point to the first byte of the next data item.
+ */
+extern int cbor_unpack_uint(struct buffer *buffer, uint64_t *v);
+extern int cbor_unpack_nint(struct buffer *buffer, uint64_t *v);
+extern int cbor_unpack_int(struct buffer *buffer, int64_t *v);
+extern int cbor_unpack_blob(struct buffer *buffer, const void **data,
+			    size_t *size);
+extern int cbor_unpack_cstr_len(struct buffer *buffer, char **str,
+				size_t *len);
+extern int cbor_unpack_str(struct buffer *buffer, struct str **str);
+extern int cbor_unpack_bool(struct buffer *buffer, bool *b);
+extern int cbor_unpack_null(struct buffer *buffer);
+extern int cbor_unpack_break(struct buffer *buffer);
+extern int cbor_unpack_map_start(struct buffer *buffer, uint64_t *npairs,
+				 bool *end_required);
+extern int cbor_unpack_map_end(struct buffer *buffer, bool end_required);
+extern int cbor_unpack_array_start(struct buffer *buffer, uint64_t *nelem,
+				   bool *end_required);
+extern int cbor_unpack_array_end(struct buffer *buffer, bool end_required);
+
 #endif
--- a/mapfile-vers	Thu Oct 11 14:20:08 2018 -0400
+++ b/mapfile-vers	Mon Nov 06 11:15:00 2017 -0400
@@ -65,6 +65,19 @@
 		cbor_pack_uint;
 		cbor_pack_val;
 		cbor_peek_type;
+		cbor_unpack_array_end;
+		cbor_unpack_array_start;
+		cbor_unpack_blob;
+		cbor_unpack_break;
+		cbor_unpack_bool;
+		cbor_unpack_cstr_len;
+		cbor_unpack_int;
+		cbor_unpack_map_end;
+		cbor_unpack_map_start;
+		cbor_unpack_nint;
+		cbor_unpack_null;
+		cbor_unpack_str;
+		cbor_unpack_uint;
 
 		# cstr
 		strcpy_safe;
--- a/tests/.hgignore	Thu Oct 11 14:20:08 2018 -0400
+++ b/tests/.hgignore	Mon Nov 06 11:15:00 2017 -0400
@@ -7,6 +7,7 @@
 test_buffer
 test_cbor_pack
 test_cbor_peek_type
+test_cbor_unpack
 test_container_of
 test_endian
 test_errno
--- a/tests/CMakeLists.txt	Thu Oct 11 14:20:08 2018 -0400
+++ b/tests/CMakeLists.txt	Mon Nov 06 11:15:00 2017 -0400
@@ -23,6 +23,7 @@
 build_perf_bin(tree)
 
 build_test_bin_and_run_files(cbor_pack cbor-common/valid/*.lisp)
+build_test_bin_and_run_files(cbor_unpack cbor-common/valid/*.cbor)
 build_test_bin_and_run_files(nvl_pack nvl-pack/*.lisp)
 build_test_bin_and_run_files(qstring qstring-basic/*.qs)
 build_test_bin_and_run_files(sexpr_parser sexpr-parser/*.lisp)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_cbor_unpack.c	Mon Nov 06 11:15:00 2017 -0400
@@ -0,0 +1,411 @@
+/*
+ * Copyright (c) 2018-2019 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#include <jeffpc/hexdump.h>
+#include <jeffpc/buffer.h>
+#include <jeffpc/val.h>
+#include <jeffpc/sexpr.h>
+#include <jeffpc/io.h>
+#include <jeffpc/cbor.h>
+
+#include "test.c"
+
+static inline void dumpbuf(struct buffer *buf)
+{
+	const size_t len = buffer_used(buf);
+	char tmp[len * 2 + 1];
+
+	hexdumpz(tmp, buffer_data(buf), len, false);
+	fprintf(stderr, "%s", tmp);
+}
+
+#define RUN_ONE(fxn, exp_ret, alloc, in, exp)				\
+	do {								\
+		struct buffer tmp;					\
+		struct val *wrap;					\
+		int ret;						\
+									\
+		buffer_init_static(&tmp, buffer_data(in),		\
+				   buffer_used(in), false);		\
+									\
+		fprintf(stderr, "unpack via %s (should %s)...",		\
+			#fxn, (exp_ret) ? "fail" : "succeed");		\
+									\
+		ret = fxn;						\
+									\
+		check_rets((exp_ret), ret,				\
+			   "failed to unpack value directly");		\
+									\
+		if (ret) {						\
+			/* failed, so there is no value to compare */	\
+			fprintf(stderr, "ok.\n");			\
+			break;						\
+		}							\
+									\
+		wrap = alloc;						\
+		if (!sexpr_equal(wrap, val_getref(exp)))		\
+			fail("not equal");				\
+									\
+		/* TODO: unpack via cbor_unpack_val & compare value */	\
+		fprintf(stderr, "ok.\n");				\
+	} while (0)
+
+#define VINT(u)		VAL_ALLOC_INT(u)
+#define VSTR(s)		VAL_ALLOC_STR_STATIC(s)
+#define VBOOL(b)	VAL_ALLOC_BOOL(b)
+#define VNULL()		VAL_ALLOC_NULL()
+
+#define VSTRCAST(s)	str_getref_val(s)
+
+static void check_null(struct buffer *in, struct val *exp)
+{
+	uint64_t nelem, npairs;
+	bool end_required;
+	struct str *str;
+	uint64_t u;
+	int64_t i;
+	size_t ss;
+	char *s;
+	bool b;
+
+	RUN_ONE(cbor_unpack_uint(&tmp, &u), -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_nint(&tmp, &u), -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_int(&tmp, &i),  -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_cstr_len(&tmp, &s, &ss),
+		                            -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_str(&tmp, &str),-EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_bool(&tmp, &b), -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_null(&tmp),     0,       VNULL(), in, exp);
+
+	/* only need to check the starts & forced ends */
+	RUN_ONE(cbor_unpack_array_start(in, &nelem, &end_required),
+					    -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_array_end(in, true),
+					    -EILSEQ, NULL, in, exp);
+
+	/* only need to check the starts & forced ends */
+	RUN_ONE(cbor_unpack_map_start(in, &npairs, &end_required),
+					    -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_map_end(in, true),
+					    -EILSEQ, NULL, in, exp);
+}
+
+static void check_bool(struct buffer *in, struct val *exp)
+{
+	uint64_t nelem, npairs;
+	bool end_required;
+	struct str *str;
+	uint64_t u;
+	int64_t i;
+	size_t ss;
+	char *s;
+	bool b;
+
+	RUN_ONE(cbor_unpack_uint(&tmp, &u), -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_nint(&tmp, &u), -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_int(&tmp, &i),  -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_cstr_len(&tmp, &s, &ss),
+		                            -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_str(&tmp, &str),-EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_bool(&tmp, &b), 0,       VBOOL(b), in, exp);
+	RUN_ONE(cbor_unpack_null(&tmp),     -EILSEQ, NULL, in, exp);
+
+	/* only need to check the starts & forced ends */
+	RUN_ONE(cbor_unpack_array_start(in, &nelem, &end_required),
+					    -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_array_end(in, true),
+					    -EILSEQ, NULL, in, exp);
+
+	/* only need to check the starts & forced ends */
+	RUN_ONE(cbor_unpack_map_start(in, &npairs, &end_required),
+					    -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_map_end(in, true),
+					    -EILSEQ, NULL, in, exp);
+}
+
+static void check_int(struct buffer *in, struct val *exp)
+{
+	const int int_ret = (exp->i > INT64_MAX) ? -EOVERFLOW : 0;
+	uint64_t nelem, npairs;
+	bool end_required;
+	struct str *str;
+	uint64_t u;
+	int64_t i;
+	size_t ss;
+	char *s;
+	bool b;
+
+	RUN_ONE(cbor_unpack_uint(&tmp, &u), 0,       VINT(u), in, exp);
+	RUN_ONE(cbor_unpack_nint(&tmp, &u), -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_int(&tmp, &i),  int_ret, VINT(i), in, exp);
+	RUN_ONE(cbor_unpack_cstr_len(&tmp, &s, &ss),
+		                            -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_str(&tmp, &str),-EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_bool(&tmp, &b), -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_null(&tmp),     -EILSEQ, NULL, in, exp);
+
+	/* only need to check the starts & forced ends */
+	RUN_ONE(cbor_unpack_array_start(in, &nelem, &end_required),
+					    -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_array_end(in, true),
+					    -EILSEQ, NULL, in, exp);
+
+	/* only need to check the starts & forced ends */
+	RUN_ONE(cbor_unpack_map_start(in, &npairs, &end_required),
+					    -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_map_end(in, true),
+					    -EILSEQ, NULL, in, exp);
+}
+
+static void check_str(struct buffer *in, struct val *exp)
+{
+	uint64_t nelem, npairs;
+	bool end_required;
+	struct str *str;
+	uint64_t u;
+	int64_t i;
+	size_t ss;
+	char *s;
+	bool b;
+
+	RUN_ONE(cbor_unpack_uint(&tmp, &u), -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_nint(&tmp, &u), -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_int(&tmp, &i),  -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_cstr_len(&tmp, &s, &ss),
+		                            0,       VSTR(s), in, exp);
+	RUN_ONE(cbor_unpack_str(&tmp, &str),0,       VSTRCAST(str), in, exp);
+	RUN_ONE(cbor_unpack_bool(&tmp, &b), -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_null(&tmp),     -EILSEQ, NULL, in, exp);
+
+	/* only need to check the starts & forced ends */
+	RUN_ONE(cbor_unpack_array_start(in, &nelem, &end_required),
+					    -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_array_end(in, true),
+					    -EILSEQ, NULL, in, exp);
+
+	/* only need to check the starts & forced ends */
+	RUN_ONE(cbor_unpack_map_start(in, &npairs, &end_required),
+					    -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_map_end(in, true),
+					    -EILSEQ, NULL, in, exp);
+}
+
+static void check_arr(struct buffer *in, struct val *exp)
+{
+	uint64_t npairs;
+	bool end_required;
+	struct str *str;
+	uint64_t u;
+	int64_t i;
+	size_t ss;
+	char *s;
+	bool b;
+
+	RUN_ONE(cbor_unpack_uint(&tmp, &u), -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_nint(&tmp, &u), -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_int(&tmp, &i),  -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_cstr_len(&tmp, &s, &ss),
+		                            -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_str(&tmp, &str),-EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_bool(&tmp, &b), -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_null(&tmp),     -EILSEQ, NULL, in, exp);
+
+#if 0
+	/* FIXME: RUN_ONE resets the buffer state */
+	RUN_ONE(cbor_unpack_array_start(in, &npairs, &end_required),
+					    0,       X, in, exp);
+	/* TODO: unpack vals in a loop */
+	RUN_ONE(cbor_unpack_array_end(in, end_required),
+					    0,       X, in, exp);
+#endif
+
+	/* only need to check the starts & forced ends */
+	RUN_ONE(cbor_unpack_map_start(in, &npairs, &end_required),
+					    -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_map_end(in, true),
+					    -EILSEQ, NULL, in, exp);
+}
+
+static void check_nvl(struct buffer *in, struct val *exp)
+{
+	uint64_t nelem;
+	bool end_required;
+	struct str *str;
+	uint64_t u;
+	int64_t i;
+	size_t ss;
+	char *s;
+	bool b;
+
+	RUN_ONE(cbor_unpack_uint(&tmp, &u), -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_nint(&tmp, &u), -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_int(&tmp, &i),  -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_cstr_len(&tmp, &s, &ss),
+		                            -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_str(&tmp, &str),-EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_bool(&tmp, &b), -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_null(&tmp),     -EILSEQ, NULL, in, exp);
+
+	/* only need to check the starts & forced ends */
+	RUN_ONE(cbor_unpack_array_start(in, &nelem, &end_required),
+					    -EILSEQ, NULL, in, exp);
+	RUN_ONE(cbor_unpack_array_end(in, true),
+					    -EILSEQ, NULL, in, exp);
+
+#if 0
+	/* FIXME: RUN_ONE resets the buffer state */
+	RUN_ONE(cbor_unpack_map_start(in, &npairs, &end_required),
+					    0,       X, in, exp);
+	/* TODO: unpack vals in a loop */
+	RUN_ONE(cbor_unpack_map_end(in, end_required),
+					    0,       X, in, exp);
+#endif
+}
+
+static void check_cons(struct buffer *in, struct val *exp)
+{
+	enum val_type type;
+	int ret;
+
+	if (!sexpr_is_null(exp))
+		fail("non-empty cons is not supported");
+
+	ret = cbor_peek_type(in, &type);
+	check_rets(0, ret, "failed to peek into input buffer");
+
+	val_putref(exp);
+
+	switch (type) {
+		case VT_ARRAY:
+			check_arr(in, VAL_ALLOC_ARRAY_STATIC(NULL, 0));
+			break;
+		case VT_NVL:
+			check_nvl(in, VAL_ALLOC_NVL());
+			break;
+		case VT_NULL:
+		case VT_INT:
+		case VT_STR:
+		case VT_SYM:
+		case VT_BOOL:
+		case VT_CONS:
+		case VT_CHAR:
+		case VT_BLOB:
+			fail("peeked type doesn't match empty cons");
+	}
+}
+
+static void onefile(struct buffer *in, struct val *expected)
+{
+	struct val *orig_expected = expected;
+
+	fprintf(stderr, "input:  ");
+	dumpbuf(in);
+	fprintf(stderr, "\n");
+	fprintf(stderr, "expect:\n");
+	val_dump(expected, 1);
+
+	expected = sexpr_compact(expected);
+	ASSERT(!IS_ERR(expected));
+
+	if (expected != orig_expected) {
+		fprintf(stderr, "expect (compacted):\n");
+		val_dump(expected, 1);
+	}
+
+	switch (expected->type) {
+		case VT_NULL:
+			check_null(in, expected);
+			break;
+		case VT_INT:
+			check_int(in, expected);
+			break;
+		case VT_BOOL:
+			check_bool(in, expected);
+			break;
+		case VT_STR:
+			check_str(in, expected);
+			break;
+		case VT_ARRAY:
+			check_arr(in, expected);
+			break;
+		case VT_NVL:
+			check_nvl(in, expected);
+			break;
+		case VT_CONS:
+			/*
+			 * Empty nvlists and empty arrays look the same when
+			 * serialized as an sexpr.  Therefore, we need to
+			 * peek at the input buffer to know what to expect.
+			 */
+			check_cons(in, expected);
+			break;
+		case VT_SYM:
+		case VT_CHAR:
+		case VT_BLOB:
+			fail("Unsupported val type");
+			break;
+	}
+
+	val_putref(expected);
+}
+
+static struct val *get_expected_output(const char *fname)
+{
+	char expfname[FILENAME_MAX];
+	struct val *lv;
+	size_t len;
+	char *tmp;
+
+	VERIFY3U(strlen("lisp"), <=, strlen("cbor"));
+
+	/* replace .cbor with .lisp */
+	strcpy(expfname, fname);
+	strcpy(expfname + strlen(expfname) - 4, "lisp");
+
+	tmp = read_file_len(expfname, &len);
+	ASSERT(!IS_ERR(tmp));
+
+	lv = sexpr_parse(tmp, len);
+	if (IS_ERR(lv))
+		fail("failed to parse input: %s", xstrerror(PTR_ERR(lv)));
+
+	free(tmp);
+
+	return lv;
+}
+
+static void test(const char *fname)
+{
+	struct buffer input;
+	size_t len;
+	char *in;
+
+	in = read_file_len(fname, &len);
+	if (IS_ERR(in))
+		fail("failed to read input (%s)", xstrerror(PTR_ERR(in)));
+
+	buffer_init_static(&input, in, len, false);
+
+	onefile(&input, get_expected_output(fname));
+
+	free((void *) buffer_data(&input));
+}