天天看点

开源项目cJSON具体实现4(转义字符串的解析)

参考文章:知乎

4. Unicode 的解析

文章目录

      • 4. Unicode 的解析
        • 4.1 ASCII、Unicode、UTF-8 介绍
        • 7.2 需求分析
        • 4.3 头文件设计
        • 4.3 测试代码
        • 4.5 实现解析器

4.1 ASCII、Unicode、UTF-8 介绍

我们知道在计算机中,所有的数据在存储和运算时都要使用二进制数表示(因为计算机用高电平和低电平分别表示1和0),例如,像a、b、c、d这样的字母以及0、1等数字还有一些常用的符号等,在计算机中存储时也要使用二进制数来表示。

而具体用哪些二进制数字表示哪个符号,当然每个人都可以约定自己的一套(这就叫编码)。大家如果要想互相通信而不造成混乱,那么大家就必须使用相同的编码规则,于是美国有关的标准化组织就出台了ASCII编码。

拓展: ASCII

它是一种字符编码,ASCII 码使用指定的7 位或8 位

二进制数

组合来表示128 或256 种可能的字符。标准ASCII 码使用7 位二进制数(剩下的1位二进制为0)来表示所有的大写和小写字母,数字0 到9、标点符号,以及在美式英语中使用的特殊控制字符

在美国,这128是够了,但是其他国家不够,ASCII是不够的。所以各地区制定了不同的编码系统,如中文主要用 GB 2312 和大五码、日文主要用 JIS 等。为了保持与ASCII码的兼容性,各地区制定了不同的编码系统,设定一般最高位为0时和原来的ASCII码相同,最高位为1的时候,各个国家自己给后面的位(1xxx xxxx)赋予他们国家的字符意义。但是这样一来又有问题出现了,不同国家对新增的128个数字赋予了不同的含义,比如说130在法语中代表了é,但是在希伯来语中却代表了字母Gimel,这样会造成很多不便。

于是,在这样的情景下:上世纪80年代末,Xerox、Apple 等公司开始研究,是否能制定一套多语言的统一编码系统。于是 Unicode 就诞生了。

拓展: Unicode

Unicode为世界上所有字符都分配了一个唯一的数字编号,这个编号范围(码点)从 0x000000 到 0x10FFFF(

十六进制

),有110多万,每个字符都有一个唯一的Unicode编号,这个编号一般写成16进制,在前面加上U+。Unicode就相当于一张表,建立了字符与编号之间的联系。

注意:Unicode本身只规定了每个字符的数字编号是多少,并没有规定这个编号如何存储。

UTF,Unicode 转换格式,它的作用在于制定了各种储存码点的方式,现时流行的 UTF 为 UTF-8、UTF-16 和 UTF-32。每种 UTF 会把一个码点储存为一至多个编码单元(code unit)。例如 UTF-8 的编码单元是 8 位的字节、UTF-16 为 16 位、UTF-32 为 32 位。除 UTF-32 外,UTF-8 和 UTF-16 都是可变长度编码。

拓展:UTF-8

UTF-8就是使用变长字节表示,意思就是使用的字节数可变,这个变化是根据 Unicode 编号的大小有关,编号小的使用的字节就少,编号大的使用的字节就多。这个编码方法的好处之一是,码点范围 U+0000 ~ U+007F 编码为一个字节,与 ASCII 编码兼容。这范围的 Unicode 码点也是和 ASCII 字符相同的。因此,一个 ASCII 文本也是一个 UTF-8 文本。

Unicode编号范围与对应的UTF-8二进制格式 :

开源项目cJSON具体实现4(转义字符串的解析)

对于一个具体的Unicode编号,具体进行UTF-8的编码的方法:

  • 首先找到该Unicode编号所在的编号范围,进而可以找到与之对应的二进制格式,然后将该Unicode编号转化为二进制数(去掉高位的0),最后将该二进制数

    从右向左

    依次填入二进制格式的X中,如果还有X未填,则设为0 。
  • 比如:“马”的Unicode编号是:0x9A6C,整数编号是39532,对应第三个范围,其格式为:1110XXXX 10XXXXXX 10XXXXXX,39532 对应的二进制是 1001 1010 0110 1100,将二进制填入进入就为: 11101001 10101001 10101100 。

对于这例子的范围,对应的 C 代码是这样的:

if (u >= 0x0800 && u <= 0xFFFF) {
    OutputByte(0xE0 | ((u >> 12) & 0xFF)); /* 0xE0 = 11100000 */
    OutputByte(0x80 | ((u >>  6) & 0x3F)); /* 0x80 = 10000000 */
    OutputByte(0x80 | ( u        & 0x3F)); /* 0x3F = 00111111 */
}
           

7.2 需求分析

首先由于 UTF-8 的普及性,我们的 JSON 库也只支持 UTF-8。又因为 C 标准库没有关于 Unicode 的处理功能(C++11 有),所以我们要实现 JSON 库所需的字符编码处理功能。

对于非转义的字符,只要它们不小于 32(0 ~ 31 是不合法的编码单元),我们可以直接复制结果。

对于转义字符,JSON字符串中的 \uXXXX 是以 16 进制表示的,码点范围为 U+0000 至 U+FFFF,所以我们需要做的是:

  1. 解析 4 位十六进制整数为码点;
  2. 我们要把这个码点编码成 UTF-8。

注意:4 位的 16 进制数字只能表示 0 至 0xFFFF,但 Unicode 的码点是从 0 至 0x10FFFF,那怎么表示多出来的码点呢?

规则是这样的:

首先,U+0000 至 U+FFFF 这组 Unicode 字符称为基本多文种平面(BMP)。

对于 BMP 以外的字符,JSON 会使用代理对的格式("\uXXXX\uYYYY")表示 。在 BMP 中,保留了 2048 个代理码点。如果第一个码点是 U+D800 至 U+DBFF,我们便知道它的代码对的高代理项(High surrogate),也就是\uXXXX部分;之后应该伴随一个 U+DC00 至 U+DFFF 的低代理项(Low surrogate),也就是\uYYYY部分。

如果我们知道了代理对 (H, L) ,可利用下面的公式将其变换成真实的码点:

codepoint = 0x10000 + (H − 0xD800) × 0x400 + (L − 0xDC00)

注意:
  • 如果只有高代理项而欠缺低代理项,或是低代理项不在合法码点范围,我们都返回 LEPT_PARSE_INVALID_UNICODE_SURROGATE 错误。
  • 如果 \u 后不是 4 位十六进位数字,则返回 LEPT_PARSE_INVALID_UNICODE_HEX 错误。

4.3 头文件设计

因为是还是字符串的解析,所以头文件的改动不大,只需添加两个新的错误码就行。

enum {
    /*  */

    LEPT_PARSE_INVALID_UNICODE_HEX,
    LEPT_PARSE_INVALID_UNICODE_SURROGATE
};
           

4.3 测试代码

因为新加两个错误码,而且我们在这部分打算测试Unicode,所以我们在test.c文件里面这么写:

static void test_parse_string() {
    TEST_STRING("", "\"\"");
    TEST_STRING("Hello", "\"Hello\"");
    TEST_STRING("Hello\nWorld", "\"Hello\\nWorld\"");
    TEST_STRING("\" \\ / \b \f \n \r \t", "\"\\\" \\\\ \\/ \\b \\f \\n \\r \\t\"");

#if 0
        TEST_STRING("Hello\0World", "\"Hello\\u0000World\"");
        TEST_STRING("\x24", "\"\\u0024\"");         /* Dollar sign U+0024 */
        TEST_STRING("\xC2\xA2", "\"\\u00A2\"");     /* Cents sign U+00A2 */
        TEST_STRING("\xE2\x82\xAC", "\"\\u20AC\""); /* Euro sign U+20AC */
        TEST_STRING("\xF0\x9D\x84\x9E", "\"\\uD834\\uDD1E\"");  /* G clef sign U+1D11E */
        TEST_STRING("\xF0\x9D\x84\x9E", "\"\\ud834\\udd1e\"");  /* G clef sign U+1D11E */
#endif
}

static void test_parse_invalid_unicode_hex() {
#if 0
        TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u\"");
        TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u0\"");
        TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u01\"");
        TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u012\"");
        TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u/000\"");
        TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\uG000\"");
        TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u0/00\"");
        TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u0G00\"");
        TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u00/0\"");
        TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u00G0\"");
        TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u000/\"");
        TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u000G\"");
#endif
}

static void test_parse_invalid_unicode_surrogate() {
#if 0
        TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_SURROGATE, "\"\\uD800\"");
        TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_SURROGATE, "\"\\uDBFF\"");
        TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_SURROGATE, "\"\\uD800\\\\\"");
        TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_SURROGATE, "\"\\uD800\\uDBFF\"");
        TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_SURROGATE, "\"\\uD800\\uE000\"");
#endif
}
           

4.5 实现解析器

还是那句话,因为我们是完善字符串的解析,所以改动的地方在于 lept_parse_string,主要是在转义字符串的处理处,得加上/uXXXX的处理方法。

#define STRING_ERROR(ret) do{c->top = head; return ret;} while (0);

/*
函数目的:解析字符串
参数:1. lept_context* c :要被解析的value
          2. lept_value* v :解析后要被保存的位置
*/
static int lept_parse_string(lept_context* c, lept_value* v) {
       /* */
    for (;;) {
        char ch = *p++;
        switch (ch) {
                       case '\\':             //转义字符的处理
                              switch (*p++){                            
                              case 'u':
                                      if (!(p = lept_parse_hex4(p, &num)))
                                             STRING_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX);//如果 \u 后不是 4 位十六进位数字
                                  /*  待完善 */
                                      lept_encode_utf8(c, num);
                                      break;                      
                              /*  */
                              }
              /*  */
        }
    }
}
           

接下来实现两个函数:

  • lept_parse_hex4(); 解析 4 位十六进数字,存储为码点 u。这个函数在成功时返回解析后的文本指针,失败返回 NULL。如果失败,就返回 LEPT_PARSE_INVALID_UNICODE_HEX 错误
  • lept_encode_utf8(); 把码点编码成 UTF-8,写进缓冲区。
/*
函数目的:解析4位16进制数字,并存储在num里面
步骤:
1. 参数p指向的是/u后面跟着的数字
2. num的初始值为0,然后将此时num的值左移四位
3. 在循环内判断ch的类型,并将char类型的ch转化为int类型的。
4. 因为ch数字范围在0~F之间,所以ch最多占的4个bit就可以保存,将ch与num按位或后再赋值给num.(按位或:有一个1就为1),相当于将ch的值赋到num的后4位上。
5. 一套循环下来,num就保存了4位16进制数字了。
*/
static const char* lept_parse_hex4(const char* p, unsigned* num)
{
        int i;
        *num = 0;
        for (i = 0; i < 4; i++)
        {

               char ch = *p++;

               *num = *num << 4;//向左移动四位

               if (ch >= '0' && ch <= '9') *num |= ch - '0';  //如果ch是数字,-'0'意味着将char 0 转换为 int 0
               else if (ch >= 'A' && ch <= 'F') *num |= ch - ('A' - 10);
               else if (ch >= 'a' && ch <= 'f')  *num |= ch - ('a' - 10);
               else return NULL;
               
        }
        return p;
}

/* 将码点编码成UTF-8,写进缓冲区  */
static void lept_encode_utf8(lept_context* c, unsigned u)
{
        if (u <= 0x7F)
               PUTC(c, u & 0xFF);  //按位与(&):一个为0就为0,为什么要&,主要是为了防止编译器的警告或者误判
        else if (u <= 0x7FF) {
               PUTC(c, 0xC0 | ((u >> 6) & 0xFF));  //0xC0 = 11000000.  FF = 11111111
               PUTC(c, 0x80 | (u & 0x3F)); //0x80 = 10000000  3F = 111111
        }
        else if (u <= 0xFFFF) {
               PUTC(c, 0xE0 | ((u >> 12) & 0xFF));
               PUTC(c, 0x80 | ((u >> 6) & 0x3F));
               PUTC(c, 0x80 | (u & 0x3F));
        }
        else {
               assert(u <= 0x10FFFF);
               PUTC(c, 0xF0 | ((u >> 18) & 0xFF));
               PUTC(c, 0x80 | ((u >> 12) & 0x3F));
               PUTC(c, 0x80 | ((u >> 6) & 0x3F));
               PUTC(c, 0x80 | (u & 0x3F));
        }
}
           

代码写到这里,剩下的事情就是完善lept_parse_string 函数,主要是对代理对的处理,遇到高代理项,就需要把低代理项 \uxxxx 也解析进来,然后用这两个项去计算出码点。

/*
函数目的:解析字符串
参数:1. lept_context* c :要被解析的value
          2. lept_value* v :解析后要被保存的位置
*/
static int lept_parse_string(lept_context* c, lept_value* v) {
        size_t head = c->top;//备份栈顶
        size_t len;
        unsigned num, num2;
    EXPECT(c, '\"');//判断c是否为string类型
        const char* p = c->json;
    for (;;) {
        char ch = *p++;
        switch (ch) {
                       case '\\':             //转义字符的处理
                              switch (*p++){
                              case '\"': PUTC(c, '\"'); break;
                              case '\\': PUTC(c, '\\'); break;
                              case '/':  PUTC(c, '/'); break;
                              case 'b':  PUTC(c, '\b'); break;
                              case 'f':  PUTC(c, '\f'); break;
                              case 'n':  PUTC(c, '\n'); break;
                              case 'r':  PUTC(c, '\r'); break;
                              case 't':  PUTC(c, '\t'); break;
                              case 'u':
                                      if (!(p = lept_parse_hex4(p, &num)))
                                            STRING_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX);//如果 \u 后不是 4 位十六进位数字
                                      if (num >= 0xD800 && num <= 0xDBFF)
                                      {
                                             if (*p++ != '\\')
                                                     STRING_ERROR(LEPT_PARSE_INVALID_UNICODE_SURROGATE);
                                             if (*p++ != 'u')
                                                     STRING_ERROR(LEPT_PARSE_INVALID_UNICODE_SURROGATE);
                                             if (!(p = lept_parse_hex4(p, &num2)))
                                                     STRING_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX);
                                             if (num2 < 0xDC00 || num2 > 0xDFFF)
                                                     STRING_ERROR(LEPT_PARSE_INVALID_UNICODE_SURROGATE);
                                             num = 0x10000 + (((num - 0xD800) << 10) | (num2 - 0xDC00));//右移10位 = *2^10 = *1024 = * 0x400
                                      }
                                      lept_encode_utf8(c, num);
                                      break;                        
                              default:
                                      c->top = head;
                                      return LEPT_PARSE_INVALID_STRING_ESCAPE;
                              }
                              break;
            case '\"':  //字符串结束标志
                len = c->top - head; 
                lept_set_string(v, (const char*)lept_context_pop(c, len), len);//将所有字符一次性弹出
                c->json = p;
                return LEPT_PARSE_OK;
            case '\0':
                c->top = head;
                return LEPT_PARSE_MISS_QUOTATION_MARK;
            default:
                              if ((unsigned char)ch < 0x20){  //剩余的情况下有不合法字符串的情况:%x00 至 %x1F
                                      c->top = head;
                                      return LEPT_PARSE_INVALID_STRING_CHAR;//不合法的字符串
                              }
                PUTC(c, ch); //把解析的字符串压栈
        }
    }
}