如何检测 `snprintf` 错误?

8
int snprintf(char * restrict s, size_t n, const char * restrict format, ...);

snprintf() 函数可以有效地避免超出目标字符串 s 的长度。但当目标字符串不足以容纳完整的结果时,如何检测并处理这种情况?

下面的方法是否足够可靠?

char buf[11 + 10 + 1];
if (snprintf(buf, sizeof buf, "Random int %d", rand()) >= sizeof buf) {
  fprintf(stderr, "Buffer too small");  // Maybe `int` was 64-bit?
  exit (EXIT_FAILURE);
}

我认为你的意思是溢出而不是覆盖。您可以使用asprintf来彻底解决此类问题,它会在堆上分配字符串。您仍然需要检查它是否有效(底层的malloc可能会失败),但您无需担心缓冲区不足的问题。 缺点:您需要释放构建的字符串,另外需要时间来分配和释放空间。 - Perette
1
@PeretteBarella 注意:asprintf()不是标准的C库函数,尽管它经常出现在各种扩展库中。 - chux - Reinstate Monica
经过测试,即使使用GCC -O1标志也会显示错误。如果未指定,则不会出现错误。 - jian
1个回答

16

这是我可以回答自己的问题吗?的一部分。欢迎提出其他答案。

如何在C语言中检测snprintf错误?

简短回答

记得snprintf()返回一个int用于指示将要写入的字符串长度或负值表示编码错误。

if ((size_t) snprintf(... ) >= sizeof buf) {
  error();
}

严谨地说,使用更宽的size_tunsigned转换,所以最好是:
int length_needed = snprintf(... );
if (length_needed < 0 || (unsigned) length_needed >= sizeof buf) {
  error();
}

测试截断

有时候从snprintf()检测到截断的字符串非常重要。缺乏测试可能会导致麻烦:

char buf[13];
char *command = "format_drive";
char *sub_command = "cancel";  
snprintf(buf, sizeof buf, "%s %s", command, sub_command);
system(buf); // system("format_drive") leads to bye-bye data

OK 代码

只需单独测试返回值是否符合或超过目标数组的大小几乎足够了。

char buf[20];
if (snprintf(buf, sizeof buf, "Random int %d", rand()) >= sizeof buf) {
  fprintf(stderr, "Buffer too small");
  exit(EXIT_FAILURE);
}

负值

snprintf函数返回将被写入的字符数,如果n足够大,则不计算终止的空字符,或者如果发生编码错误则返回负值。因此,只有当返回值为非负且小于n时,才完全写入了以空字符结尾的输出。C11dr §7.21.6.5 3

健壮的代码应直接检查是否存在罕见的编码错误的负值。不幸的是,if (some_int <= some_size_t)并不足够,因为int会被转换为size_t。然后,一个int的负返回值就变成了一个很大的正size_t。这通常远大于数组的大小,但没有规定必须如此。

// Pedantic check for negative values 
int length_needed = snprintf(... as above ...);
if (length_needed < 0 || length_needed >= sizeof buf) {
  fprintf(stderr, "Buffer too small (or encoding error)");
  exit(EXIT_FAILURE);
}

类型不匹配

一些编译器警告会抱怨比较不同类型的整数,例如gcc的-Wsign-compareintsize_t。将其转换为size_t似乎是合理的。

警告:有符号和无符号整数表达式之间的比较[-Wsign-compare]

// Quiet different sign-ness warnings
int length_needed = snprintf(... as above ...);
if (length_needed < 0 || (size_t) length_needed >= sizeof buf) {
  fprintf(stderr, "Buffer too small (or encoding error)");
  exit(EXIT_FAILURE);
}

追求严谨

C语言没有规定int类型的正值是size_t类型的子范围。 size_t可以是unsigned short,然后SIZE_MAX < INT_MAX。(我不知道是否有这样的实现。)因此,将some_int强制转换为(size_t)可能会改变其值。相反,将正返回值强制转换为unsignedINT_MAX <= UINT_MAX始终成立)将不会改变其值,并确保使用unsignedsize_t之间最宽的无符号类型进行比较。

// Quiet different sign-ness warnings
int length_needed = snprintf(... as above ...);
if (length_needed < 0 || (unsigned) length_needed >= sizeof buf) {
  fprintf(stderr, "Buffer too small (or encoding error)");
  exit(EXIT_FAILURE);
}

我认为“size_t可以是unsigned short”的评论应该明确提到暗示的假设,即unsigned shortunsigned int不同。也就是说,SIZE_MAXUSHRT_MAXUINT_MAX都相等并不奇怪,但是一个假想的病态实现的奇怪情况是SIZE_MAX < UINT_MAX - jamesdlin
@jamesdlin 根据您的建议修改了答案。 - chux - Reinstate Monica
这看起来可能很有用:https://linux.die.net/man/3/explain_snprintf_or_die - Den-Jason

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接