Skip to content

Commit

Permalink
add a str_format function for formatting strings, like {fmt}
Browse files Browse the repository at this point in the history
  • Loading branch information
ennorehling committed Jan 11, 2025
1 parent 2c34e58 commit 867ad47
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 1 deletion.
97 changes: 96 additions & 1 deletion strings.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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\'\"\\");
}
1 change: 1 addition & 0 deletions strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
81 changes: 81 additions & 0 deletions test_strings.c
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand All @@ -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];
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 867ad47

Please sign in to comment.