diff --git a/strings.c b/strings.c index 2dd7bed..79e6df4 100644 --- a/strings.c +++ b/strings.c @@ -50,7 +50,7 @@ size_t str_strlcpy(char *dst, const char *src, size_t len) register size_t n = len; assert(src); - assert(dst); + assert(dst || len==0); /* Copy as many bytes as will fit */ if (n != 0 && --n != 0) { do { @@ -430,6 +430,101 @@ const char *str_escape_ex(const char *str, char *buffer, size_t size, const char return buffer; } +size_t str_format(char *dst, size_t size, const char *format, const char *params[], size_t nparams) +{ + const char *pos, *copy = format; + char *write = dst; + + size_t space = size; + size_t space_needed = 0; + + for (pos = format; *pos; ++pos) { + pos = strpbrk(pos, "{\\"); + if (!pos) { + /* no more tokens, copy remainder of format string to output */ + space_needed += str_strlcpy(write, copy, space); + break; + } + else { + size_t bytes = pos - copy; + /* found the start of a new token, copy `bytes` of lead text from [copy, pos) */ + space_needed += bytes; + if (space > 0) { + if (bytes >= space) { + bytes = space - 1; + } + str_strlcpy(write, copy, bytes + 1); + write += bytes; + space -= bytes; + } + } + if (*pos == '{') { + const char *token = pos + 1; + + copy = strchr(pos, '}'); + if (!copy) { + /* syntax error: unterminated token */ + return 0; + } + else { + /* found a token in the [token, copy) interval */ + size_t keylen = copy - token; + size_t n; + const char *val = NULL; + for (n = 0; n != nparams; ++n) { + const char *key = params[n]; + if (0 == strncmp(key, token, keylen)) { + /* param matches token? */ + if (key[keylen] == '\0') { + val = key + keylen + 1; + break; + } + } + } + if (val) { + size_t slen = str_strlcpy(write, val, space); + space_needed += slen; + if (slen < space) { + space -= slen; + write += slen; + } + else { + /* buffer is full */ + write += space - 1; + space = 0; + } + } + else { + /* token not found, copy [pos, copy] to output instead */ + size_t bytes = keylen + 2; + space_needed += bytes; + if (bytes >= space) { + bytes = space - 1; + } + str_strlcpy(write, pos, bytes + 1); + write += bytes; + space -= bytes; + } + pos = ++copy; + } + } + else { + /** backslash escapes the next character */ + ++space_needed; + if (pos[1]) ++pos; + if (space > 1) { + *write++ = *pos; + --space; + } + copy = pos + 1; + } + } + if (copy < pos) { + space_needed += str_strlcpy(write, copy, space); + } + return space_needed; +} + const char *str_escape(const char *str, char *buffer, size_t size) { return str_escape_ex(str, buffer, size, "\n\t\r\'\"\\"); } diff --git a/strings.h b/strings.h index d0f94cb..1daf684 100644 --- a/strings.h +++ b/strings.h @@ -10,6 +10,7 @@ size_t str_strlcpy(char *dst, const char *src, size_t len); size_t str_strlcat(char *dst, const char *src, size_t len); char *str_strdup(const char *s); +size_t str_format(char *dst, size_t size, const char *format, const char *params[], size_t nparams); const char *str_escape(const char *str, char *buffer, size_t size); const char *str_escape_ex(const char *str, char *buffer, size_t size, const char *chars); char *str_unescape(char *str); diff --git a/test_strings.c b/test_strings.c index 05b13c5..2445dcb 100644 --- a/test_strings.c +++ b/test_strings.c @@ -117,6 +117,7 @@ static void test_str_strlcpy(CuTest * tc) memset(buffer, 0x7f, sizeof(buffer)); + CuAssertIntEquals(tc, 8, (int)str_strlcpy(NULL, "herpderp", 0)); CuAssertIntEquals(tc, 4, (int)str_strlcpy(buffer, "herp", 8)); CuAssertStrEquals(tc, "herp", buffer); CuAssertIntEquals(tc, 0x7f, buffer[5]); @@ -143,6 +144,85 @@ static void test_str_itoa(CuTest * tc) CuAssertStrEquals(tc, "-1234567890", str_itoa(-1234567890)); } +static void test_str_format(CuTest *tc) +{ + const char *params[] = { + "name\0Enno", + "time\0now" + }; + char buffer[64]; + /* no tokens */ + memset(buffer, 0, sizeof(buffer)); + CuAssertIntEquals(tc, 11, (int)str_format(NULL, 0, "Hello World", NULL, 0)); + CuAssertIntEquals(tc, 11, (int) str_format(buffer, 64, "Hello World", NULL, 0)); + CuAssertStrEquals(tc, "Hello World", buffer); + memset(buffer, 0, sizeof(buffer)); + CuAssertIntEquals(tc, 11, (int)str_format(buffer, 12, "Hello World", NULL, 0)); + CuAssertStrEquals(tc, "Hello World", buffer); + memset(buffer, 0, sizeof(buffer)); + CuAssertIntEquals(tc, 11, (int)str_format(buffer, 11, "Hello World", NULL, 0)); + CuAssertStrEquals(tc, "Hello Worl", buffer); + memset(buffer, 0, sizeof(buffer)); + CuAssertIntEquals(tc, 11, (int)str_format(buffer, 64, "Hello World", params, 2)); + CuAssertStrEquals(tc, "Hello World", buffer); + + /* unterminated token error */ + memset(buffer, 0, sizeof(buffer)); + CuAssertIntEquals(tc, 0, (int)str_format(buffer, 64, "Hello {world", NULL, 0)); + + /* token not found */ + memset(buffer, 0, sizeof(buffer)); + CuAssertIntEquals(tc, 13, (int)str_format(buffer, 64, "Hello {world}", NULL, 0)); + CuAssertStrEquals(tc, "Hello {world}", buffer); + memset(buffer, 0, sizeof(buffer)); + CuAssertIntEquals(tc, 13, (int)str_format(buffer, 64, "Hello {world}", params, 2)); + CuAssertStrEquals(tc, "Hello {world}", buffer); + + /* happy case, find a token and replace it */ + memset(buffer, 0, sizeof(buffer)); + CuAssertIntEquals(tc, 10, (int)str_format(buffer, 64, "Hello {name}", params, 2)); + CuAssertStrEquals(tc, "Hello Enno", buffer); + + /* repeat tokens */ + memset(buffer, 0, sizeof(buffer)); + CuAssertIntEquals(tc, 12, (int)str_format(buffer, 64, "{name} is {name}", params, 2)); + CuAssertStrEquals(tc, "Enno is Enno", buffer); + + /* multiple tokens */ + memset(buffer, 0, sizeof(buffer)); + CuAssertIntEquals(tc, 28, (int)str_format(buffer, 29, "Hello {name}, the time is {time}.", params, 2)); + CuAssertStrEquals(tc, "Hello Enno, the time is now.", buffer); + + /* escaping braces */ + memset(buffer, 0, sizeof(buffer)); + CuAssertIntEquals(tc, 12, (int)str_format(buffer, 64, "Hello \\{name\\}", params, 2)); + CuAssertStrEquals(tc, "Hello {name}", buffer); + + /* escape other characters */ + memset(buffer, 0, sizeof(buffer)); + CuAssertIntEquals(tc, 13, (int)str_format(buffer, 64, "Hello \\\\\\World\\", params, 2)); + CuAssertStrEquals(tc, "Hello \\World\\", buffer); + + +#if 0 + + /* buffer too short */ + CuAssertStrEquals(tc, "Hello", str_format(buffer, 6, "Hello {name}", params, 2)); + + /* escaped braces */ + CuAssertStrEquals(tc, "Hello {name}!", str_format(buffer, 64, "Hello \\{name\\}!", params, 2)); + + /* escaped backslashes */ + CuAssertStrEquals(tc, "Hello \\Enno!", str_format(buffer, 64, "Hello \\\\{name}!", params, 2)); + + /* ignore garbage single backslash */ + CuAssertStrEquals(tc, "", str_format(buffer, 64, "\\", params, 2)); + + /* syntax error (missing }) */ + CuAssertPtrEquals(tc, NULL, str_format(buffer, 64, "Hello {name", params, 2)); +#endif +} + static void test_sbstring(CuTest * tc) { char buffer[16]; @@ -293,6 +373,7 @@ void add_suite_strings(CuSuite *suite) SUITE_ADD_TEST(suite, test_str_strlcat); SUITE_ADD_TEST(suite, test_str_strlcpy); SUITE_ADD_TEST(suite, test_str_itoa); + SUITE_ADD_TEST(suite, test_str_format); SUITE_ADD_TEST(suite, test_sbstring); SUITE_ADD_TEST(suite, test_sbs_strcat); SUITE_ADD_TEST(suite, test_sbs_substr);