From b26ac01e42b7cef87c4e170d151cb278eb012c3c Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Wed, 4 Nov 2026 21:01:57 +0900 Subject: [PATCH] hexdump: add hexdump_memcmp() Add a helper producing a hexdump(3)-like side-by-side dump of two memory regions, marking the bytes that differ. Meant for assertion failure messages. --- src/shared/hexdump.c | 76 +++++++++++++++++++++++++++++++++++++++++ src/shared/hexdump.h | 4 +++ src/test/test-hexdump.c | 24 +++++++++++++ 3 files changed, 104 insertions(+) diff --git a/src/shared/hexdump.c b/src/shared/hexdump.c index 489e5fb94c6..3ad0be3794d 100644 --- a/src/shared/hexdump.c +++ b/src/shared/hexdump.c @@ -5,6 +5,7 @@ #include #include "hexdump.h" +#include "memstream-util.h" void hexdump(FILE *f, const void *p, size_t s) { const uint8_t *b = p; @@ -54,3 +55,78 @@ void hexdump(FILE *f, const void *p, size_t s) { s -= 16; } } + +static void dump_hex_line(FILE *f, char tag, const uint8_t *p, size_t size, size_t off) { + assert(f); + + fprintf(f, "%c %04zx ", tag, off); + + for (size_t i = 0; i < 16; i++) { + if (off + i < size) + fprintf(f, "%02x ", p[off + i]); + else + fputs(" ", f); + + if (i == 7) + fputc(' ', f); + } + + fputs(" |", f); + for (size_t i = 0; i < 16; i++) { + if (off + i < size) { + uint8_t c = p[off + i]; + fputc(isprint(c) ? (char) c : '.', f); + } else + fputc(' ', f); + } + fputs("|\n", f); +} + +char* hexdump_memcmp(const void *m1, size_t l1, const void *m2, size_t l2) { + _cleanup_(memstream_done) MemStream m = {}; + const uint8_t *a = m1, *b = m2; + size_t mx = MAX(l1, l2); + FILE *f; + + /* Returns a hexdump(3)-like side-by-side representation of two memory regions. For each 16 byte + * chunk that differs a marker line pointing at the diverging bytes ('<' is the first region, '>' the + * second) is appended. */ + + f = memstream_init(&m); + if (!f) + return NULL; + + for (size_t off = 0; off < mx; off += 16) { + dump_hex_line(f, '<', a, l1, off); + dump_hex_line(f, '>', b, l2, off); + + /* Figure out whether this chunk differs at all, and only then emit the marker line. */ + bool any = false; + for (size_t i = 0; i < 16 && off + i < mx; i++) { + bool pa = off + i < l1, pb = off + i < l2; + if (pa != pb || (pa && pb && a[off + i] != b[off + i])) { + any = true; + break; + } + } + if (!any) + continue; + + fputs(" ", f); /* align with the 8-char "%c %04zx " prefix: tag + space + 4 hex + 2 spaces */ + for (size_t i = 0; i < 16; i++) { + bool pa = off + i < l1, pb = off + i < l2; + bool diff = pa != pb || (pa && pb && a[off + i] != b[off + i]); + fputs(diff ? "^^ " : " ", f); + + if (i == 7) + fputc(' ', f); + } + fputc('\n', f); + } + + char *ret; + if (memstream_finalize(&m, &ret, NULL) < 0) + return NULL; + + return ret; +} diff --git a/src/shared/hexdump.h b/src/shared/hexdump.h index f129a4a57b0..daac65225da 100644 --- a/src/shared/hexdump.h +++ b/src/shared/hexdump.h @@ -4,3 +4,7 @@ #include "forward.h" void hexdump(FILE *f, const void *p, size_t s) _nonnull_if_nonzero_(2, 3); + +/* Returns a hexdump(3)-like side-by-side dump of two memory regions, marking the differing bytes. Returns + * NULL on OOM. Meant for assertion failure messages, see ASSERT_MEMEQ() and friends. */ +char* hexdump_memcmp(const void *m1, size_t l1, const void *m2, size_t l2); diff --git a/src/test/test-hexdump.c b/src/test/test-hexdump.c index dd668491a63..46a639a5150 100644 --- a/src/test/test-hexdump.c +++ b/src/test/test-hexdump.c @@ -1,5 +1,6 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include "alloc-util.h" #include "hexdump.h" #include "tests.h" @@ -26,4 +27,27 @@ TEST(hexdump) { hexdump(stdout, data, sizeof(data)); } +TEST(hexdump_memcmp) { + static const uint8_t a[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11 }; + static const uint8_t b[] = { 0x00, 0x01, 0xff, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10 }; + + /* Equal regions: no marker lines, just the two sides. */ + _cleanup_free_ char *s = ASSERT_NOT_NULL(hexdump_memcmp(a, sizeof(a), a, sizeof(a))); + log_info("equal:\n%s", s); + + /* Differing byte and differing length. */ + free(s); + s = ASSERT_NOT_NULL(hexdump_memcmp(a, sizeof(a), b, sizeof(b))); + log_info("differ:\n%s", s); + + /* Empty regions are fine. */ + free(s); + s = ASSERT_NOT_NULL(hexdump_memcmp(NULL, 0, NULL, 0)); + ASSERT_STREQ(s, ""); +} + DEFINE_TEST_MAIN(LOG_INFO);