C ( 2/3 )のUTF - 8文字列



検証
我々が常に従わなければならない皮肉な規則は、それです:

any UTF-8 string must be validated before being used.


私は理由を掘りません、しかし、彼らは非常に重要です.
そこで、私は、「コード化されていないかのようにUTF - 8エンコードされた文字列を処理するかもしれない」と言ったとき、私は本当にあなたが100 %確かにストリングがすでにチェックされたか、それがどんな意味のある方法で使用される前に後でそれであるかどうか(それをDBに格納することを含む)であるならば、あなたがそれをすることができるということです.
疑わしい場合は、文字列を自分で検証する必要があります.
前の記事のテーブルを見るWikipedia ), バイト数列が有効なUTF - 8符号化でないことは明らかです.
例えば、バイト列C2 41 マルチバイトUTF - 8符号化の第2バイトが0 x 7 fより大きくなければならないので、明らかに無効です.
UTF - 8エンコーディングの検証方法はたくさんあります.
があるextremely fast, parallel algorithms しかし、ここでの私たちの目的は、どのようにバリデーションがどのように働くかを理解することです.

登場人物
文字列のシーケンスで最も基本的な操作は、次のいずれかを得ることです.
必要なのは文字列を取る関数で、最初のバイトが有効なUTF - 8エンコーディングであるかどうかをチェックし、場合によっては対応する文字を符号化したバイト数を返します.
また、無効なエンコーディングでどうするかを決定する必要があります.Unicode規格は無効な符号化を文字で置き換えるべきであることを再認識するU+FFFD : (疑問符を含んでいる損失)、しかしCプログラマとして、ストリングまたはエラーと全く無関係である何かを返すことは私にとって奇妙なようです.
バイトの無効なシーケンスを見つけるなら、最初のもの(すなわち、1の長さ)を返すでしょうerrno to EINVAL を返します.

文字表現
文字の内部表現には二つの選択肢があります.
  • CodePointをデコードし、Unicode値を使用します
  • 符号化形式を直接使用する
  • 最初の1つは論理的だが、それについて考えるなら、それはしばしば時間の無駄です.たとえば、2つの文字列を比較することを検討してください.コード化された形を比較することは、コードポイント値を比較することと全く同じです、しかし、それは復号化ステップを保存します.
    他方、第2のものはよりユーザーフレンドリーである.定数を使用する場合は、コードポイント値を手動でエンコードして使用する必要があります.
    しかし、私は主張すると主張するジとして30B8 ( Unicode CodePoint )あるいはE382B8 ( UTF - 8エンコード)は大きな違いはありません.
    任意の有効なUTF - 8エンコーディングは4バイトに収まるので、新しいタイプを定義しましょうu8chr_t :
    typedef uint32_t u8chr_t;
    

    エンコード長
    最初のバイトを参照するエンコーディングの長さを決定するには、配列のインデックスとしてバイトの上位4ビットを使用する配列とマクロを定義できます.
    static uint8_t const u8_length[] = {
    // 0 1 2 3 4 5 6 7 8 9 A B C D E F
       1,1,1,1,1,1,1,1,0,0,0,0,2,2,3,4
    } ;
    
    #define u8length(s) u8_length[(((uint8_t *)(s))[0] & 0xFF) >> 4];
    
    マクロは、符号化が長いと予想するバイト数を教えてくれます.
    マクロの値の例です.
    u8length("abc")    -> 1  ('a' is "\x41")
    u8length("èfg")    -> 2  ('è' is "\xC3\xA8")
    u8length("\x82HZ") -> 0  (invalid)
    u8length("\xC3")   -> 2  (invalid, but we don't know yet)
    
    最後の2つが無効な符号化であることに注意してください.最初のケースでは、我々はすぐにそれが迷ったバイトであると言うことができます.番目の1つでは、エンコードが無効であることを認識するために次のバイトを見なければなりません.

    エンコードの読み込み
    期待された長さを持っているので、その時点でエンコードを1バイト読み込むことができます(txt は文字列へのポインタです):
       u8chr_t encoding = 0
       int len = u8length(txt);
    
       for (int i=0; i<len && txt[i] != '\0'; i++) {
          encoding = (encoding << 8) | txt[i];
       }
    
    期待されたバイトより少ないバイトがあるならば、コードがストリングの終わりを過ぎて読まない点に注意してください.それが起こるならば、我々はループから終わります、そしてencoding 変数は、これまでに発見されたものを保ちます.

    検証!
    そして、現在コア部分:ロードされた符号化が有効なものであるかどうかチェックする機能を定義しましょう
    int u8chrisvalid(u8chr_t c)
    {
      if (c <= 0x7F) return 1;                    // [1]
    
      if (0xC280 <= c && c <= 0xDFBF)             // [2]
         return ((c & 0xE0C0) == 0xC080);
    
      if (0xEDA080 <= c && c <= 0xEDBFBF)         // [3]
         return 0; // Reject UTF-16 surrogates
    
      if (0xE0A080 <= c && c <= 0xEFBFBF)         // [4]
         return ((c & 0xF0C0C0) == 0xE08080);
    
      if (0xF0908080 <= c && c <= 0xF48FBFBF)     // [5]
         return ((c & 0xF8C0C0C0) == 0xF0808080);
    
      return 0;
    }
    
    その機能はわずか5だif 's
  • [1] ASCII値をチェックします.
  • [3] UTF - 16代用品を除外します.
  • 他の3つは値とビットパターンの両方を見て有効な符号化を保証します.
  • 詳しくチェックしましょう[2] (他の方法も同様である).
    これは範囲を覆っているU+0080 - U+07FF ( 11ビット)110xxxxx 10xxxxxx .
    ビットを置き換えるだけで、エンコードがU+0080 is 0xC280 ( 1100 0010 1000 0000 ) とU+07FF is 0xDFBF ( 1101 1111 1011 1111 ). Any encoding lower than 0xC280 無効でなければなりません(非最小の符号化から来ます).any encoding higher than 0xDFBF 他の場合はさらに検証されます.
    しかし、その範囲内のすべての値が有効であるというわけではありません1100 1010 1111 0011 バイナリで;番目のバイトは形式ではありません10xx xxxx .
    ' 0 xe 0 c 0 'で符号化をマスクすることは、ビットが彼らがそうであるはずであるものであるならば明らかになります.
    それが混乱しているようであるならば、ちょっとcodepointsのカップルをためしてください.私は、それがすぐにより明らかになると確信します.
    他のすべての場合は同じ方法で動作します.
    私は、より多くのdetalaid記述を書きましたStack Overflow 自己反応として.私はそれを使用する前に、このメソッドが正しいことを確認したいと私はその集団的な知恵をタップしました.

    すべてのトウ
    さて、文字列の次の文字の長さを返す初期関数を完成させることができます.
    int u8next(char *txt, u8chr_t *ch)
    {
       int len;
       u8chr_t encoding = 0;
    
       len = u8length(txt);
    
       for (int i=0; i<len && txt[i] != '\0'; i++) {
          encoding = (encoding << 8) | txt[i];
       }
    
       errno = 0;
       if (len == 0 || !u8chrisvalid(encoding)) {
          encoding = txt[0];
          len = 1;
          errno = EINVAL;
       }
    
       if (ch) *ch = encoding;
    
       return encoding ? len : 0 ;
    }
    
    
    最後に、別のチェックを追加しました\0 ' 長さも0です.この方法では、現在のポインターに戻り値を文字列に追加でき、文字列の末尾を越えないことを確認します.

    デコード/エンコード
    復号化と符号化文字は、興味のビットをマスキングし、整数でシフトするだけの問題です.
    この関数で何が起こっているのかを理解するには、テーブルの前にUTF - 8エンコーディングの構造を保持しておく必要があります.
    // from UTF-8 encoding to Unicode Codepoint
    uint32_t u8decode(u8chr_t c)
    {
      uint32_t mask;
    
      if (c > 0x7F) {
        mask = (c <= 0x00EFBFBF)? 0x000F0000 : 0x003F0000 ;
        c = ((c & 0x07000000) >> 6) |
            ((c & mask )      >> 4) |
            ((c & 0x00003F00) >> 2) |
             (c & 0x0000003F);
      }
    
      return c;
    }
    
    // From Unicode Codepoint to UTF-8 encoding
    u8chr_t u8encode(uint32_t codepoint)
    {
      u8chr_t c = codepoint;
    
      if (codepoint > 0x7F) {
        c =  (codepoint & 0x000003F) 
           | (codepoint & 0x0000FC0) << 2 
           | (codepoint & 0x003F000) << 4
           | (codepoint & 0x01C0000) << 6;
    
        if      (codepoint < 0x0000800) c |= 0x0000C080;
        else if (codepoint < 0x0010000) c |= 0x00E08080;
        else                            c |= 0xF0808080;
      }
      return c;
    }
    

    結論
    符号化、復号化、妥当性検査は、実際よりも複雑に思えるかもしれません.いくつかのビット操作は非常に簡単にトリックを行うことができます.何も本当に新しい.
    もう少し複雑なことは、通常の関数を文字に拡張することです.関数はどのようにしてisdigit() or tolower() ?
    我々はこれらの機能を必要とする最も些細なタスクを除いて、これはトピックです.