snprintf usage for making up formatted strings

Viewed 713

I'm trying to learn about the snprintf and found this answer with the example:

char buf[20] = "";
char *cur = buf, * const end = buf + sizeof buf;
cur += snprintf(cur, end-cur, "%s", "foo");
printf("%s\n", buf);
if (cur < end) {
    cur += snprintf(cur, end-cur, "%s", " bar");
}
printf("%s\n", buf);
free(str);

The thing that is not clear to me is that we allocate a fixed hardcoded buffer size which seems suffer from buffer overflow. In the N1570 I found that (7.21.6.5)

1

#include <stdio.h>
int snprintf(char * restrict s, size_t n,
const char * restrict format, ...);

2 The snprintf function is equivalent to fprintf, except that the output is written into an array (specified by argument s) rather than to a stream. If n is zero, nothing is written, and s may be a null pointer.

So to me it appears as the idiomatic usage would be as follows:

int need_space = snprintf(NULL, 0, "abs %s", "fgh") + 1; //How much to allocate?
char *const str = malloc(need_space * sizeof(char)); //allocate
int written = snprintf(str, need_space, "abs %s", "fgh"); //do format
printf("Need space = %d, written = %d\n", need_space, written);
printf("%s\n", str);

Or this is not common and has another problem?

1 Answers

The 2x call to snprintf() is a common idiom.

... has another problem?

Problems lie in the corners

Format maintenance

The below suffers from a repeated format - prone to breaking as code ages and and only one line is changed.

// Oops!
int need_space = snprintf(NULL, 0, "abs %s", "fgh") + 1;
char *const str = malloc(need_space * sizeof(char));
int written = snprintf(str, need_space, "abs %s ", "fgh");

Did you notice the difference?

Better to state the format once.

#define FMT_ABS_S "abs %s"
int need_space = snprintf(NULL, 0, FMT_ABS_S, "fgh");
char *const str = malloc(sizeof *str * (need_space + 1u));
int written = snprintf(str, need_space, FMT_ABS_S, "fgh");

Volatility

Code needs to insure the values do not change (non-volatile) between the 2 calls and special concern arise in multi-threaded applications.

Error checking

Code lacked checks. Yet even with checks, how to even handle unexpected results - perhaps just bail?

static const char *fmt_s = "abs %s";
int needed_space = snprintf(NULL, 0, fmt_s, "fgh");
if (needed_space < 0) {
  Handle_EncodingError();
}
char * const str = malloc(sizeof *str * (needed_space + 1u));
if (str == NULL) {
  Handle_OutOfMemory();
}
int written = snprintf(str, needed_space, fmt_s, "fgh");
if (written < 0 || written > needed_space) {
  Handle_Error();
}

Going twice though

2 calls can be wasteful in select situations, yet commonly the impact is less than thought. @Jonathan Leffler

Yet for me, the "what to do if allocation fails" is a show stopper anyways and code reports the error and maybe exit.

Selectively, once is enough

When the s*printf() format is tightly controlled, I see 1 call as sufficient.

// int to string

#define LOG2_N 28
#define LOG2_D 93
// Number of char needed for a string of INT_MIN is log10(bit width) + 3
#define INT_SIZE ((sizeof(int)*CHAR_BIT-1)*LOG2_N/LOG2_D + 3)

char s[INT_SIZE * 2];  // I like to use 2x to handle locale issues, no need to be stingy here.

int len = snprintf(s, sizeof s, "%d", i);

if (len < 0 || (unsigned) len >= sizeof s) {
  Handle_VeryUnusualFailure();
}
Related