天天看點

開源項目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); //把解析的字元串壓棧
        }
    }
}
           

繼續閱讀