如何检查两个格式字符串是否兼容?

39

示例:

"Something %d"        and "Something else %d"       // Compatible
"Something %d"        and "Something else %f"       // Not Compatible
"Something %d"        and "Something %d else %d"    // Not Compatible
"Something %d and %f" and "Something %2$f and %1$d" // Compatible

我认为应该有一些C函数可以做到这一点,但是我没有找到任何相关的搜索结果。我的意思是编译器正在检查格式字符串和参数是否匹配,因此检查此内容的代码已经编写好了。唯一的问题是我如何调用它。

我正在使用Objective-C,所以如果有Objective-C特定的解决方案也可以。


1
你可能能够利用 NS_FORMAT_FUNCTION 来达到你的目的。查看这个 Stack Overflow 回答,以及 Clang 的 __format__ 文档 - ravron
或者在这里查看GNU:http://www.gnu.org/software/libc/manual/html_node/Parsing-a-Template-String.html - Eugene Sh.
1
我认为问题在于苹果提供的libc不是glibc,但也许他们有类似的东西... - Erik B
1
@ErikB:如果你需要可用的代码,请联系我(请查看我的个人资料)。我有240多行C代码,专门用于比较格式字符串(还有几百行的测试代码/数据),然后是500行(预先存在的)格式解析代码,再加上300行的测试代码/数据,以及测试支持工具(也是预先存在的)。正如我所说的,这些代码总量远远超出了一个答案可以合理容纳的范围。 - Jonathan Leffler
1
"Something %d and %f" 和 "Something %2$f and %1$d" 中的 $ 不是 C 标准的一部分。这应该导致第三个答案:"不可比较"。 - chux - Reinstate Monica
显示剩余14条评论
2个回答

9

检查2个printf()格式字符串是否兼容是格式解析的练习。

至少在C语言中,没有标准的运行时比较函数,例如:

int format_cmp(const char *f1, const char *f2); // Does not exist

"%d %f""%i %e" 格式显然是兼容的,因为两者都需要一个 int 和一个 float/double。注意:由于 shortsigned char 都会被提升为 int,所以 float 也会被提升为 double

格式 "%*.*f""%i %d %e" 是兼容的,但不明显:两者都需要一个 intint 和一个 float/double

格式 "%hhd""%d" 都需要一个 int,尽管在打印之前,第一个值将被转换为 signed char

格式 "%d""%u" 不兼容。虽然许多系统会按照期望的方式执行,但通常情况下,char 将被提升为 int

格式 "%d""%ld" 不严格兼容。在32位系统上它们是等效的,但在一般情况下是不等价的。当然,代码可以被修改来适应这一点。 另一方面,由于通常的参数提升将 float 提升为 double,所以 "%lf""%f" 是兼容的。

格式 "%lu""%zu" 可能是兼容的,但这取决于 unsigned longsize_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; // Bring in modifier
  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;
}

// 0 Compatible
// 1 Not Compatible
// 2 Not Comparable
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"。只有B1B2通过了安全测试,因为它们都没有提取超过20个字符。B3存在问题,因为它超过了20个字符的参考限制。此外,任何非宽度限定符与%s%[%] %c都是一个安全问题-无论是在参考模式还是测试模式中。此答案的代码没有解决这个问题。
如上所述,代码尚未处理带有"%n"修饰符的情况。
[2018年编辑]
关于“格式“%d”“%u”不兼容”的问题:这适用于通常要打印的值。对于[0..INT_MAX]范围内的值,两种格式都可能有效,根据C11dr §6.5.2.2 6。

1
我真的很喜欢这个答案。如果楼主同意的话,你应该得到那个悬赏。 - Mekap
我还没有时间阅读、运行或测试代码,但看起来你已经理解了问题。我的主要关注点是安全性。我发现我们从一个可以从互联网下载的配置文件中获取格式字符串。这意味着攻击者可以注入格式字符串。我通过针对我们情况特定的实现来修复了这个问题,但我认为应该有一个通用的实现来检查格式字符串的兼容性。虽然你的实现可能很好用,但我不太舒服使用它,因为我想要一些经过充分测试和验证的东西。 - Erik B
@Erik B,我认为安全问题重要地改变了这个问题的焦点。我已经添加了一些内容来解决这个问题,但是安全确实是一个新问题。也许您可以发布一个新帖子,详细说明您的安全问题,特别是在字符串和"%n"方面。总之,我认为您不会找到现成的实现。 - chux - Reinstate Monica

-1

我理解你的意思是,你想要一个方法,可以查看两个字符串并检测它们是否具有相同类型的值。或者类似这样的功能... 如果是这样,那么可以尝试使用以下代码(或类似的代码):

-(int)checkCompatible:(NSString *)string_1 :(NSString *)string_2 {

    // Separate the string into single elements.
    NSArray *stringArray_1 = [string_1 componentsSeparatedByString:@" "];
    NSArray *stringArray_2 = [string_2 componentsSeparatedByString:@" "];

    // Store only the numbers for comparison in a new array.
    NSMutableArray *numbers_1 = [[NSMutableArray alloc] init];
    NSMutableArray *numbers_2 = [[NSMutableArray alloc] init];

    // Make sure the for loop below, runs for the appropriate
    // number of cycles depending on which array is bigger.
    int loopMax = 0;

    if ([stringArray_1 count] > [stringArray_2 count]) {
        loopMax = (int)[stringArray_1 count];
    } 

    else {
        loopMax = (int)[stringArray_2 count];
    }

    // Now go through the stringArray's and store only the 
    // numbers in the mutable array's. This will be used 
    // during the comparison stage.
    for (int loop = 0; loop < loopMax; loop++) {

        NSCharacterSet *notDigits = [[NSCharacterSet decimalDigitCharacterSet] invertedSet];

        if (loop < [stringArray_1 count]) {

            if ([[stringArray_1 objectAtindex:loop] rangeOfCharacterFromSet:notDigits].location == NSNotFound) {
                // String consists only of the digits 0 through 9.
                [numbers_1 addObject:[stringArray_1 objectAtindex:loop]];
            }
        }

        if (loop < [stringArray_2 count]) {

            if ([[stringArray_2 objectAtindex:loop] rangeOfCharacterFromSet:notDigits].location == NSNotFound) {
                // String consists only of the digits 0 through 9.
                [numbers_2 addObject:[stringArray_2 objectAtindex:loop]];
            }
        }
    }

    // Now look through the mutable array's
    // and perform the type comparison,.

    if ([numbers_1 count] != [numbers_2 count]) {

        // One of the two strings has more numbers 
        // than the other, so they are NOT compatible.
        return 1;
    }

    else {

        // Both string have the same number of  numbers
        // numbers so lets go through them to make 
        // sure the  numbers are of the same type.
        for (int loop = 0; loop < [numbers_1 count]; loop++) {

            // Check to see if the number in the current array index
            // is a float or an integer. All the numbers in the array have
            // to be the SAME type, in order for the strings to be compatible.
            BOOL check_float_1 = [[NSScanner scannerWithString:[numbers_1 objectAtindex:loop]] scanFloat:nil];
            BOOL check_int_1 = [[NSScanner scannerWithString:[numbers_1 objectAtindex:loop]] scanInt:nil];
            BOOL check_float_2 = [[NSScanner scannerWithString:[numbers_2 objectAtindex:loop]] scanFloat:nil];
            BOOL check_int_2 = [[NSScanner scannerWithString:[numbers_2 objectAtindex:loop]] scanInt:nil];

            if (check_float_1 == YES) {

                if (check_float_2 == NO) {
                    return 1;
                }
            }

            else if (check_int_1 == YES) {

                if (check_int_2 == NO) {
                    return 1;
                }
            }

            else {
                // Error of some sort......
                return 1;
            }
        }

        // All the numbers in the strings are of the same
        // type (otherwise we would NOT have reached
        // this point). Therefore the strings are compatible.
        return 0;
      }
}

1
第一个问题是通过空格分割字符串。为什么呢? - Sulthan
@Sulthan 我不是通过空格来分割它们的。我所做的是获取该字符串中所有单独字符串的副本,并将它们存储在单独的数组元素中。因此,假设该字符串为“hello 123”,那么在使用componentsSeparatedByString之后,它将被存储在数组中,"hello" [0] 和 "123" [1]。 - user4657588
1
“Something%d”和“Something %d”怎么样?根据问题,我猜它们被认为是“兼容”的。 - Yuchen
1
这完全没有回答我的问题。 "hello 123" 不是格式字符串。 我不认为您理解了问题。 - Erik B
@ErikB 噢,抱歉。也许你可以更好地向我们这些蠢人解释你的问题。 - user4657588

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