sscanf和scanset停止读取十六进制数字

5

我尝试验证一个UUID v4。我尝试使用sscanf完成此操作,如果UUID可以完全被sscanf读取(即读取的字符总数-36),则我认为这是一个正确的UUID。到目前为止我的代码如下:

#include <stdio.h>

int main()
{
    char uuid[ 37 ] = "da4dd6a0-5d4c-4dc6-a5e3-559a89aff639";
    int a = 0, b = 0, c = 0, d = 0, e = 0, g = 0;
    long long int f = 0;

    printf( "uuid >%s<, variables read: %d \n", uuid, sscanf( uuid, "%8x-%4x-4%3x-%1x%3x-%12llx%n", &a, &b, &c, &d, &e, &f, &g ) );
    printf( " a - %x, b - %x,  c - %x,  d - %x,  e - %x, f - %llx, total number of characters read - %d \n", a, b, c, d, e, f, g );

    return 0;
}

将返回以下输出

uuid >da4dd6a0-5d4c-4dc6-a5e3-559a89aff639<, variables read: 6 
 a - da4dd6a0, b - 5d4c,  c - dc6,  d - a,  e - 5e3, f - 559a89aff639, total number of characters read - 36 

目前为止,一切都好。 现在我想加入这样一个条件:第三个连字符后的第一个字符必须是 [89ab] 中的一个。因此,我将 %1x%3x 更改为 %1x[89ab]%3x。但是现在只有第一个字符被读取了,后面的字符没有被读取。

uuid >da4dd6a0-5d4c-4dc6-a5e3-559a89aff639<, variables read: 4 
a - da4dd6a0, b - 5d4c,  c - dc6,  d - a,  e - 0, f - 0, total number of characters read - 0 

我错过了什么?语法有什么问题?可以像这样阅读吗?我尝试了几种扫描集和格式指示符的组合,但没有任何效果。


那为什么不是 d == 0xa || d == 0x9 || d == 0xb || f == 0x8 呢? - KamilCuk
1
字符集格式只读取字符串。%1x[89ab]%3x 这将匹配一个1位十六进制数,然后是字面上的 [89ab] 字符串,最后是另一个3位十六进制数。 - dxiv
1
嗯,scanf() 的功能有限。你可能想学习正则表达式,通过 fgets() 读取用户输入并通过正则表达式进行检查。或者,编写自己的检查器... - the busybee
1
所以我把%1x%3x改成了%1x [89ab]%3x。是scanf文档中的某些内容提示这么做有意义吗? - David Schwartz
@David Schwartz,是的,当我读scanf的文档时,我没有看到任何限制。现在我在文档下面进一步看到了类型的备注。 - Simone
2个回答

2

为了完成这个任务,你可以编写一个简单的专用函数,而不是使用sscanf():

#include <ctype.h>
#include <string.h>

int check_UUID(const char *s) {
    int i;
    for (i = 0; s[i]; i++) {
        if (i == 8 || i == 13 || i == 18 || i == 23) {
            if (s[i] != '-')
                return 0;
        } else {
            if (!isxdigit((unsigned char)s[i])) {
                return 0;
        }
    }
    if (i != 36)
        return 0;

    // you can add further tests for specific characters:
    if (!strchr("89abAB", s[19]))
        return 0;

    return 1;
}

如果您坚持使用sscanf(),这里是简洁的实现:

#include <stdio.h>

int check_UUID(const char *s) {
    int n = 0;
    sscanf(s, "%*8[0-9a-fA-F]-%*4[0-9a-fA-F]-%*4[0-9a-fA-F]-%*4[0-9a-fA-F]-%*12[0-9a-fA-F]%n", &n);
    return n == 36 && s[n] == '\0';
}

如果你想对第三个连字符后的第一个字符进行精细化测试,可以添加另一个字符类:

#include <stdio.h>

int check_UUID(const char *s) {
    int n = 0;
    sscanf(s, "%*8[0-9a-fA-F]-%*4[0-9a-fA-F]-%*4[0-9a-fA-F]-%*1[89ab]%*3[0-9a-fA-F]-%*12[0-9a-fA-F]%n", &n);
    return n == 36 && s[n] == '\0';
}

注:

  • % 后的 * 意味着不要存储转换结果,跳过这些字符,而 1 表示最多消耗 1 个字符。
  • 为了使 sscanf 解析的字符数量达到 36,所有十六进制数字序列必须恰好符合指定的宽度。
  • %n 会导致 scanf 将当前已读取的字符数存储在下一个参数指向的 int 中。
  • 你的转换说明有助于获取实际 UUID 数字,但 %x 格式接受前导空格、可选符号以及可选的 0x0X 前缀,这些在 UUID 内部都是无效的。可以先验证 UUID,然后再根据需要将其转换为各个部分。

1
我喜欢这种好的使用“%n”来简化扫描成功。 - chux - Reinstate Monica

2
现在我想补充一点,即第三个连字符后的第一个字符必须是[89ab]之一。因此,我将%1x%3x更改为%1x[89ab]%3x
应该是"%1[89ab]%3x",然后保存到一个2个字符的字符串中。然后使用strtol(..., ..., 16)将该小字符串转换为十六进制值。
相反,我建议对通用唯一标识符(UUID)进行两步验证: 先检查语法,然后再读取值。
我会避免使用"%x",因为它允许前导空格、前导'+','-'和可选前导0x以及窄输入。
对于验证,也许代码中可以简单测试一下:
#include <ctype.h>
#include <stdio.h>

// byte lengths: 4-2-2-2-6
typedef struct {
  unsigned long time_low;
  unsigned time_mid;
  unsigned time_hi_and_version;
  unsigned clock_seq_hi_and_res_clock_seq_low;
  unsigned long long node;
} uuid_T;

uuid_T* validate_uuid(uuid_T *dest, const char *uuid_source) {
  static const char *uuid_pat = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
  const char *pat = uuid_pat;
  const unsigned char *u = (const unsigned char*) uuid_source;

  while (*u) {
    if ((*pat++ == 'x' && !isxdigit(*u)) || *u != '-') {
      return NULL;
    }
    u++;
  }
  if (*pat) {  // Too short
    return NULL;
  }
  sscanf(uuid_source, "%lx-%x-%x-%x-%llx", &dest->time_low,
      &dest->time_mid, &dest->time_hi_and_version,
      &dest->clock_seq_hi_and_res_clock_seq_low, &dest->node);
  return dest;
}

uunsigned char *u,因此只有在非负值的情况下调用 isxdigit(*u),从而避免了未定义行为。


1
不错的发现!(可选前导0x - chqrlie

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