add osmo_hexdump_buf() and test

Add osmo_hexdump_buf() as an all-purpose hexdump function, which all other
osmo_hexdump_*() implementations now call. It absorbs the static
_osmo_hexdump(). Add tests for osmo_hexdump_buf().

Rationale: recently during patch review, a situation came up where two hexdumps
in a single printf would have been useful. Now I've faced a similar situation
again, in ongoing development. So I decided it is time to provide this API.

The traditional osmo_hexdump() API returns a non-const char*, which should
probably have been a const instead. Particularly this new function may return a
string constant "" if the buf is NULL or empty, so return const char*. That is
why the older implementations calling osmo_hexdump_buf() separately return the
buffer instead of the const return value directly.

Change-Id: I590595567b218b24e53c9eb1fd8736c0324d371d
diff --git a/src/utils.c b/src/utils.c
index d1da4fa..0b2ed31 100644
--- a/src/utils.c
+++ b/src/utils.c
@@ -219,30 +219,55 @@
 static char hexd_buff[4096];
 static const char hex_chars[] = "0123456789abcdef";
 
-static char *_osmo_hexdump(const unsigned char *buf, int len, char *delim)
+/*! Convert binary sequence to hexadecimal ASCII string.
+ *  \param[out] out_buf  Output buffer to write the resulting string to.
+ *  \param[in] out_buf_size  sizeof(out_buf).
+ *  \param[in] buf  Input buffer, pointer to sequence of bytes.
+ *  \param[in] len  Length of input buf in number of bytes.
+ *  \param[in] delim  String to separate each byte; NULL or "" for no delim.
+ *  \param[in] delim_after_last  If true, end the string in delim (true: "1a:ef:d9:", false: "1a:ef:d9");
+ *                               if out_buf has insufficient space, the string will always end in a delim.
+ *  \returns out_buf, containing a zero-terminated string, or "" (empty string) if out_buf == NULL or out_buf_size < 1.
+ *
+ * This function will print a sequence of bytes as hexadecimal numbers, adding one delim between each byte (e.g. for
+ * delim passed as ":", return a string like "1a:ef:d9").
+ *
+ * The delim_after_last argument exists to be able to exactly show the original osmo_hexdump() behavior, which always
+ * ends the string with a delimiter.
+ */
+const char *osmo_hexdump_buf(char *out_buf, size_t out_buf_size, const unsigned char *buf, int len, const char *delim,
+			     bool delim_after_last)
 {
 	int i;
-	char *cur = hexd_buff;
+	char *cur = out_buf;
+	size_t delim_len;
 
-	hexd_buff[0] = 0;
+	if (!out_buf || !out_buf_size)
+		return "";
+
+	delim = delim ? : "";
+	delim_len = strlen(delim);
+
 	for (i = 0; i < len; i++) {
 		const char *delimp = delim;
-		int len_remain = sizeof(hexd_buff) - (cur - hexd_buff);
-		if (len_remain < 3)
+		int len_remain = out_buf_size - (cur - out_buf) - 1;
+		if (len_remain < (2 + delim_len)
+		    && !(!delim_after_last && i == (len - 1) && len_remain >= 2))
 			break;
 
 		*cur++ = hex_chars[buf[i] >> 4];
 		*cur++ = hex_chars[buf[i] & 0xf];
 
+		if (i == (len - 1) && !delim_after_last)
+			break;
+
 		while (len_remain > 1 && *delimp) {
 			*cur++ = *delimp++;
 			len_remain--;
 		}
-
-		*cur = 0;
 	}
-	hexd_buff[sizeof(hexd_buff)-1] = 0;
-	return hexd_buff;
+	*cur = '\0';
+	return out_buf;
 }
 
 /*! Convert a sequence of unpacked bits to ASCII string
@@ -292,7 +317,8 @@
  */
 char *osmo_hexdump(const unsigned char *buf, int len)
 {
-	return _osmo_hexdump(buf, len, " ");
+	osmo_hexdump_buf(hexd_buff, sizeof(hexd_buff), buf, len, " ", true);
+	return hexd_buff;
 }
 
 /*! Convert binary sequence to hexadecimal ASCII string
@@ -308,7 +334,8 @@
  */
 char *osmo_hexdump_nospc(const unsigned char *buf, int len)
 {
-	return _osmo_hexdump(buf, len, "");
+	osmo_hexdump_buf(hexd_buff, sizeof(hexd_buff), buf, len, "", true);
+	return hexd_buff;
 }
 
 /* Compat with previous typo to preserve abi */