This is common operation within Graphviz. It is also a common source of bugs.
There have been numerous instances of ints being stringized into fixed sized
buffers that were not large enough to hold them. The result has been pernicious
memory safety issues that have been difficult to reproduce and root cause.¹
The hope is that this wrapper can provide a safe, efficient, and tested
alternative. Going forwards, the intent is to replace as many compatible
operations as possible with calls to this function.
¹ A user would report a crash that relied on their objects having large IDs or
their platform having a large int width. The example would run fine in a
different environment and mystify the maintainers.
agxbuf.h
cghdr.h
cgraph.h
+ itos.h
sprint.h
strcasecmp.h
AM_CPPFLAGS = -I$(top_srcdir)/lib -I$(top_srcdir)/lib/cdt
pkginclude_HEADERS = cgraph.h
-noinst_HEADERS = agxbuf.h cghdr.h sprint.h strcasecmp.h
+noinst_HEADERS = agxbuf.h cghdr.h itos.h sprint.h strcasecmp.h
noinst_LTLIBRARIES = libcgraph_C.la
lib_LTLIBRARIES = libcgraph.la
pkgconfig_DATA = libcgraph.pc
<ClInclude Include="agxbuf.h" />
<ClInclude Include="cghdr.h" />
<ClInclude Include="cgraph.h" />
+ <ClInclude Include="itos.h" />
<ClInclude Include="sprint.h" />
<ClInclude Include="strcasecmp.h" />
</ItemGroup>
<ClInclude Include="cgraph.h">
<Filter>Header Files</Filter>
</ClInclude>
+ <ClInclude Include="itos.h">
+ <Filter>Header Files</Filter>
+ </ClInclude>
<ClInclude Include="sprint.h">
<Filter>Header Files</Filter>
</ClInclude>
--- /dev/null
+#pragma once
+
+#include <stdio.h>
+
+// return type of itos below
+struct itos_ {
+ char str[12];
+};
+
+/** convert an integer to a string
+ *
+ * Intended usage is something like:
+ * agattr(g, AGEDGE, "hello", itos(42).str)
+ *
+ * @param i Integer to convert
+ * @return Stringized conversion wrapped in a struct
+ */
+static inline struct itos_ itos(int i) {
+ struct itos_ s;
+ (void)snprintf(s.str, sizeof(s.str), "%d", i);
+ return s;
+}
--- /dev/null
+// basic unit tester for itos
+
+#ifdef NDEBUG
+#error this is not intended to be compiled with assertions off
+#endif
+
+#include <assert.h>
+#include <cgraph/itos.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static void test_0(void) {
+ assert(strcmp(itos(0).str, "0") == 0);
+}
+
+static void test_1(void) {
+ assert(strcmp(itos(1).str, "1") == 0);
+}
+
+static void test_neg_1(void) {
+ assert(strcmp(itos(-1).str, "-1") == 0);
+}
+
+static void test_min(void) {
+
+ int r = snprintf(NULL, 0, "%d", INT_MIN);
+ assert(r > 0);
+
+ char *buffer = malloc(sizeof(char) * ((size_t)r + 1));
+ assert(buffer != NULL);
+
+ (void)sprintf(buffer, "%d", INT_MIN);
+
+ assert(strcmp(itos(INT_MIN).str, buffer) == 0);
+
+ free(buffer);
+}
+
+static void test_max(void) {
+
+ int r = snprintf(NULL, 0, "%d", INT_MAX);
+ assert(r > 0);
+
+ char *buffer = malloc(sizeof(char) * ((size_t)r + 1));
+ assert(buffer != NULL);
+
+ (void)sprintf(buffer, "%d", INT_MAX);
+
+ assert(strcmp(itos(INT_MAX).str, buffer) == 0);
+
+ free(buffer);
+}
+
+int main(void) {
+
+#define RUN(t) \
+ do { \
+ printf("running test_%s... ", #t); \
+ fflush(stdout); \
+ test_##t(); \
+ printf("OK\n"); \
+ } while (0)
+
+ RUN(0);
+ RUN(1);
+ RUN(neg_1);
+ RUN(min);
+ RUN(max);
+
+#undef RUN
+
+ return EXIT_SUCCESS;
+}
--- /dev/null
+"""test ../lib/cgraph/itos.h"""
+
+import os
+from pathlib import Path
+import sys
+
+sys.path.append(os.path.dirname(__file__))
+from gvtest import run_c
+
+def test_itos():
+ """run the itos unit tests"""
+
+ # locate the itos unit tests
+ src = Path(__file__).parent.resolve() / "../lib/cgraph/test_itos.c"
+ assert src.exists()
+
+ # locate lib directory that needs to be in the include path
+ lib = Path(__file__).parent.resolve() / "../lib"
+
+ # extra C flags this compilation needs
+ cflags = ['-I', lib]
+
+ ret, _, _ = run_c(src, cflags=cflags)
+
+ assert ret == 0