检查2个printf()
格式字符串是否兼容是格式解析的练习。
至少在C语言中,没有标准的运行时比较函数,例如:
int format_cmp(const char *f1, const char *f2); // Does not exist
如
"%d %f"
和
"%i %e"
格式显然是兼容的,因为两者都需要一个
int
和一个
float/double
。注意:由于
short
和
signed char
都会被提升为
int
,所以
float
也会被提升为
double
。
格式 "%*.*f"
和 "%i %d %e"
是兼容的,但不明显:两者都需要一个 int
,int
和一个 float/double
。
格式 "%hhd"
和 "%d"
都需要一个 int
,尽管在打印之前,第一个值将被转换为 signed char
。
格式 "%d"
和 "%u"
不兼容。虽然许多系统会按照期望的方式执行,但通常情况下,char
将被提升为 int
。
格式 "%d"
和 "%ld"
不严格兼容。在32位系统上它们是等效的,但在一般情况下是不等价的。当然,代码可以被修改来适应这一点。 另一方面,由于通常的参数提升将 float
提升为 double
,所以 "%lf"
和 "%f"
是兼容的。
格式 "%lu"
和 "%zu"
可能是兼容的,但这取决于 unsigned long
和 size_t
的实现。添加到代码中的内容可以允许这种或相关的等价。
某些修饰符和说明符的组合,例如 "%zp"
,是未定义的。以下内容并没有禁止这些神秘的组合,但进行了比较。
像 "$"
这样的修饰符是标准C的扩展,以下内容没有实现它们。
printf()
的兼容性测试与 scanf()
不同。
#include <ctype.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
typedef enum {
type_none,
type_int,
type_unsigned,
type_float,
type_charpointer,
type_voidpointer,
type_intpointer,
type_unknown,
type_type_N = 0xFFFFFF
} type_type;
typedef struct {
const char *format;
int int_queue;
type_type type;
} format_T;
static void format_init(format_T *state, const char *format);
static type_type format_get(format_T *state);
static void format_next(format_T *state);
void format_init(format_T *state, const char *format) {
state->format = format;
state->int_queue = 0;
state->type = type_none;
format_next(state);
}
type_type format_get(format_T *state) {
if (state->int_queue > 0) {
return type_int;
}
return state->type;
}
const char *seek_flag(const char *format) {
while (strchr("-+ #0", *format) != NULL)
format++;
return format;
}
const char *seek_width(const char *format, int *int_queue) {
*int_queue = 0;
if (*format == '*') {
format++;
(*int_queue)++;
} else {
while (isdigit((unsigned char ) *format))
format++;
}
if (*format == '.') {
if (*format == '*') {
format++;
(*int_queue)++;
} else {
while (isdigit((unsigned char ) *format))
format++;
}
}
return format;
}
const char *seek_mod(const char *format, int *mod) {
*mod = 0;
if (format[0] == 'h' && format[1] == 'h') {
format += 2;
} else if (format[0] == 'l' && format[1] == 'l') {
*mod = ('l' << CHAR_BIT) + 'l';
format += 2;
} else if (strchr("ljztL", *format)) {
*mod = *format;
format++;
} else if (strchr("h", *format)) {
format++;
}
return format;
}
const char *seek_specifier(const char *format, int mod, type_type *type) {
if (strchr("di", *format)) {
*type = type_int;
format++;
} else if (strchr("ouxX", *format)) {
*type = type_unsigned;
format++;
} else if (strchr("fFeEgGaA", *format)) {
if (mod == 'l') mod = 0;
*type = type_float;
format++;
} else if (strchr("c", *format)) {
*type = type_int;
format++;
} else if (strchr("s", *format)) {
*type = type_charpointer;
format++;
} else if (strchr("p", *format)) {
*type = type_voidpointer;
format++;
} else if (strchr("n", *format)) {
*type = type_intpointer;
format++;
} else {
*type = type_unknown;
exit(1);
}
*type |= mod << CHAR_BIT;
return format;
}
void format_next(format_T *state) {
if (state->int_queue > 0) {
state->int_queue--;
return;
}
while (*state->format) {
if (state->format[0] == '%') {
state->format++;
if (state->format[0] == '%') {
state->format++;
continue;
}
state->format = seek_flag(state->format);
state->format = seek_width(state->format, &state->int_queue);
int mod;
state->format = seek_mod(state->format, &mod);
state->format = seek_specifier(state->format, mod, &state->type);
return;
} else {
state->format++;
}
}
state->type = type_none;
}
int format_cmp(const char *f1, const char *f2) {
format_T state1;
format_init(&state1, f1);
format_T state2;
format_init(&state2, f2);
while (format_get(&state1) == format_get(&state2)) {
if (format_get(&state1) == type_none)
return 0;
if (format_get(&state1) == type_unknown)
return 2;
format_next(&state1);
format_next(&state2);
}
if (format_get(&state1) == type_unknown)
return 2;
if (format_get(&state2) == type_unknown)
return 2;
return 1;
}
注意:只进行了最少的测试。还有许多其他方面需要考虑。
已知不足之处:使用
n
时,
hh、h、l、ll、j、z、t
修饰符。在
s、c
中使用
l
。
[编辑]
OP关于安全问题的评论。这改变了帖子和比较的性质,从相等变成了安全性。我想其中一种模式(A)将是参考模式,下一个模式(B)将是测试。测试将是“B是否至少与A一样安全?”。例如:
A = "%.20s"
和
B1 = "%.19s"
、
B2 = "%.20s"
、
B3 = "%.21s"
。只有
B1
和
B2
通过了安全测试,因为它们都没有提取超过20个字符。
B3
存在问题,因为它超过了20个字符的参考限制。此外,任何非宽度限定符与
%s%[%] %c
都是一个安全问题-无论是在参考模式还是测试模式中。此答案的代码没有解决这个问题。
如上所述,代码尚未处理带有
"%n"
修饰符的情况。
[2018年编辑]
关于“格式
“%d”
和
“%u”
不兼容”的问题:这适用于通常要打印的值。对于
[0..INT_MAX]
范围内的值,两种格式都可能有效,根据C11dr §6.5.2.2 6。
NS_FORMAT_FUNCTION
来达到你的目的。查看这个 Stack Overflow 回答,以及 Clang 的__format__
文档。 - ravron"Something %d and %f" 和 "Something %2$f and %1$d"
中的$
不是 C 标准的一部分。这应该导致第三个答案:"不可比较"。 - chux - Reinstate Monica