UTF-8 編碼及檢查其完整性

hsy0發表於2019-01-17

為什麼需要檢查 UTF-8 編碼

根據 WebSocket 協議的要求 5.6 資料幀,如果 Frame 的 Opcode 是 0x1 的話,則表示這是一個文字幀,即其 “Application Data” 是使用 UTF-8 編碼的字串。不過由於訊息也可以使用多個 Frame 進行分片傳輸,所以在驗證文字訊息的編碼時,需要收集到訊息的所有 Frames 後,提取所有的 Frame 中的 “Application Data” 組成一個大的 “Application Data”,然後驗證這個大的 “Application Data” 中的位元組是不是合法的 UTF-8 編碼。

既然協議中要求了文字訊息必須使用 UTF-8 編碼,那麼反過來,驗證編碼是否是 UTF-8就可以一定程度上確定訊息的完整性。

Unicode

簡單的說 Unicode 就是一種字元的編碼方式,此編碼方式一般使用兩個位元組(UCS-2)去表示一個字元,比如“漢”這個中文字元,其 unicode 編碼的十六進位制表示就是 0x6c49

UTF-8

UTF-8 的全稱是 8-bit Unicode Transformation Format 中文就是 “8 位的 unicode 轉換格式”。UTF-8 是具體的 Unicode 實現方式中的一種,套用 wiki 上的一段話:

但是在實際傳輸過程中,由於不同系統平臺的設計不一定一致,以及出於節省空間的目的,對Unicode編碼的實現方式有所不同。Unicode的實現方式稱為Unicode轉換格式(Unicode Transformation Format,簡稱為UTF)

UTF-8 的編碼方式

UTF-8使用一至六個位元組為每個字元編碼(儘管如此,2003年11月UTF-8被RFC 3629重新規範,只能使用原來Unicode定義的區域,U+0000到U+10FFFF,也就是說最多四個位元組)

   Char. number range  |        UTF-8 octet sequence
      (hexadecimal)    |              (binary)
   --------------------+---------------------------------------------
   0000 0000-0000 007F | 0xxxxxxx
   0000 0080-0000 07FF | 110xxxxx 10xxxxxx
   0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
   0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
複製程式碼
  • 對於 UTF-8 編碼中的任意位元組B,如果B的第一位為0,則B為ASCII碼,並且B獨立的表示一個字元;
  • 如果B的第一位為1,第二位為0,則B為一個非ASCII字元(該字元由多個位元組表示)中的一個位元組,並且不是字元的第一個位元組編碼;
  • 如果B的前兩位為1,第三位為0,則B為一個非ASCII字元(該字元由多個位元組表示)中的第一個位元組,並且該字元由兩個位元組表示;
  • 如果B的前三位為1,第四位為0,則B為一個非ASCII字元(該字元由多個位元組表示)中的第一個位元組,並且該字元由三個位元組表示;
  • 如果B的前四位為1,第五位為0,則B為一個非ASCII字元(該字元由多個位元組表示)中的第一個位元組,並且該字元由四個位元組表示;

所以我們只需要相容最新的標準即可。如果你還沒有明白 UTF-8 編碼的含義,我們可以看一個具體的例子,比如中文的 “漢”,其 Unicode 編碼的十六進位制表示是 0x6c49,那麼很明顯,它必然落在 0x00000800 – 0x0000FFFF 這個區間內,而這個區間的字元必須使用 3 個位元組的 UTF-8 編碼,表示成 1110xxxx 10xxxxxx 10xxxxxx 的形式。

所以對於 0x6c49 要轉成 UTF-8 編碼:

  1. 0x6c49 右移 12 位,取出最高的 4 位,然後或上 11100000(即 0xE0),得到第一個位元組 0xE6
  2. 0x6c49 與上 0000111111000000(即 0xFC0) 後、右移 6 位,這樣得到中間的 6 位,然後或上 10000000(即 0x80) 得到第二個位元組 0xB1
  3. 0x6c49 與上 0000000000111111(即 0x3F) 後,或上 10000000(即 0x80) 得到第三個位元組 0x89

於是中文字元 “漢” 的 UTF-8 編碼就是 0xE6 0xB1 0x89,是不是 so easy。

現在,假設現在我們得到一串資料,它包含 3 個位元組,其內容是 0xE6 0xB1 0x89,並且我們知道這串資料採用的是 UTF-8 編碼,我們怎麼得知其對應的 unicode 編碼是什麼呢?一種一種的情況試啊!

  1. 取出第一個位元組,檢查其最高位是不是 0,如果是0,那麼當前位元組即表示一個字元,如果不是,進行下一步
  2. 檢查最高 3 位是不是 110,如果是的話,那麼接下來的一個位元組和當前位元組合起來表示一個字元,如果不是,進行下一步
  3. 檢查最高 4 位是不是 1110,如果是的話,那麼接下來的兩個位元組和當前位元組合起來表示一個字元,如果不是,進行下一步
  4. 檢查最高 5 位是不是 11110,如果是的話,那麼接下來的三個位元組和當前位元組合起來表示一個字元,如果不是,進行下一步
  5. 根據最新的標準,UTF-8 編碼最多隻使用四個位元組去表示一個字元,所以到了這一步就說明編碼錯誤了
  6. 另外除了表示剩餘位元組數的那個位元組外,其餘位元組的最高兩位都必須是 10
  7. U+0000 不可以使用兩個位元組進行編碼
  8. U+D800~U+DFFF (左右邊界不可取)是保留段,不可以使用

那麼看看剛才的例子,我們取出第一個位元組 0xE6(即 1110 0110),我要逐一的嘗試每一種情況,最後我們發現它的最高 4 位是 1110 那麼它之後的兩個位元組和它一起表示一個字元。

  1. 首先我們先將第一個位元組與上 0xF,這樣可以得到實際的 4 位
  2. 取出緊隨的第二個位元組,將其與上 0x3F,這樣可以得到實際的 6 位
  3. 取出緊隨的第三個位元組,將其與上 0x3F,這樣可以得到實際的 6 位

最後將這 16 個數位按照取出的順序從左往右放存放到三個位元組中。

程式碼

先放 javascript 的,注意這裡使用了 ES6 中的 String.prototype.codePointAt 方法,因為在 ES5 中對於超過了 0xFFFF 的字元使用 String.prototype.charCodeAt 並不能正確的獲取其 unicode 編碼:

"use strict";

console.assert(typeof String.prototype.codePointAt == 'function', "Current env doesn't support ECMAScript 6!");

Array.prototype.equal = function (b) {
    return this.every(function (e, i) {
        return e === b[i];
    });
};

var unicode2utf8 = function (unicode) {
    unicode = typeof unicode == 'string' ? unicode.codePointAt(0) : unicode;

    if (unicode <= 0x7F) {
        return [unicode]
    } else if (unicode >= 0x80 && unicode <= 0x7FF) {
        return [
            unicode >> 6 | 0xC0,
            unicode & 0x3F | 0x80
        ];
    } else if (unicode >= 0x800 && unicode <= 0xFFFF) {
        return [
            unicode >> 12 | 0xE0,
            (unicode & 0xFC0) >> 6 | 0x80,
            unicode & 0x3F | 0x80
        ];
    } else if (unicode >= 0x10000 && unicode <= 0x10FFFF) {
        return [
            unicode >> 18 | 0xF0,
            (unicode & 0x3F000) >> 12 | 0x80,
            (unicode & 0xFC0) >> 6 | 0x80,
            unicode & 0x3F | 0x80
        ];
    } else {
        throw new Error('deformed unicode: ' + unicode);
    }
};

console.assert(unicode2utf8('u').equal([0x75]), "unicode2utf8 not pass 'u'");
console.assert(unicode2utf8('©').equal([0xC2, 0xA9]), "unicode2utf8 not pass '©'");
console.assert(unicode2utf8('漢').equal([0xE6, 0xB1, 0x89]), "unicode2utf8 not pass '漢'");
console.assert(unicode2utf8('?').equal([0xF0, 0x9F, 0x98, 0x84]), "unicode2utf8 not pass '?'");

var utf82unicode = function (utf8) {
    var ul = utf8.length, byte = utf8[0];

    if (ul == 0) {
        throw new Error('empty utf8');
    }

    if (byte <= 127) {
        return byte;
    } else if (byte >> 5 == 0x6 && ul == 2) {
        return ((byte & 0x1F) << 6) |
                utf8[1] & 0x3F;
    } else if (byte >> 4 == 0xE && ul == 3) {
        return ((byte & 0xF) << 12) |
                ((utf8[1] & 0x3F) << 6) |
                (utf8[2] & 0x3F)
    } else if (byte >> 3 == 0x1E && ul == 4) {
        return ((byte & 0x7) << 18) |
                ((utf8[1] & 0x3F) << 12) |
                ((utf8[2] & 0x3F) << 6) |
                (utf8[3] & 0x3F)
    } else {
        throw new Error('deformed utf8: ' + utf8);
    }
};

console.assert(utf82unicode([0x75]) == 'u'.codePointAt(0), "utf82unicode not pass 'u'");
console.assert(utf82unicode([0xC2, 0xA9]) == '©'.codePointAt(0), "utf82unicode not pass '©'");
console.assert(utf82unicode([0xE6, 0xB1, 0x89]) == '漢'.codePointAt(0), "utf82unicode not pass '漢'");
console.assert(utf82unicode([0xF0, 0x9F, 0x98, 0x84]) == '?'.codePointAt(0), "utf82unicode not pass '?'");
複製程式碼

接下來是 golang 的,其中的 IsIntactUtf8 函式就是本文討論的主題 - 檢查UTF-8編碼的完整性:

func Unicode2utf8(u uint32) (u8 []byte, err error) {
	if u <= 0x7F {
		return []byte{byte(u)}, nil
	} else if u >= 0x80 && u <= 0x7FF {
		return []byte{
			byte(u>>6 | 0xC0),
			byte(u&0x3F | 0x80),
		}, nil
	} else if u >= 0x800 && u <= 0xFFFF {
		return []byte{
			byte(u>>12 | 0xE0),
			byte((u&0xFC0)>>6 | 0x80),
			byte(u&0x3F | 0x80),
		}, nil
	} else if u >= 0x10000 && u <= 0x10FFFF {
		return []byte{
			byte(u>>18 | 0xF0),
			byte((u&0x3F000)>>12 | 0x80),
			byte((u&0xFC0)>>6 | 0x80),
			byte(u&0x3F | 0x80),
		}, nil
	}

	return nil, errors.New(fmt.Sprintf("deformed unicode: %d", u))
}

func TestUnicode2utf8(t *testing.T) {
	u8, _ := Unicode2utf8(0x75)
	if !reflect.DeepEqual(u8, []byte{0x75}) {
		t.Fatal("not pass 'u'")
	}

	u8, _ = Unicode2utf8(0xA9)
	if !reflect.DeepEqual(u8, []byte{0xC2, 0xA9}) {
		t.Fatal("not pass '©'")
	}

	u8, _ = Unicode2utf8(0x6C49)
	if !reflect.DeepEqual(u8, []byte{0xE6, 0xB1, 0x89}) {
		t.Fatal("not pass '漢'")
	}

	u8, _ = Unicode2utf8(0x1F604)
	if !reflect.DeepEqual(u8, []byte{0xF0, 0x9F, 0x98, 0x84}) {
		t.Fatal("not pass '?'")
	}
}

func Utf82unicode(u8 []byte) (u uint32, err error) {
	u8l := len(u8)

	if u8l == 0 {
		return 0, errors.New("empty utf8")
	}

	b1 := u8[0]
	if b1 <= 0x7F {
		return uint32(b1), nil
	} else if b1>>5 == 0x6 && u8l == 2 {
		return uint32(b1&0x1F)<<6 |
			uint32(u8[1]&0x3F), nil
	} else if b1>>4 == 0xE && u8l == 3 {
		return uint32(b1&0xF)<<12 |
			uint32(u8[1]&0x3F)<<6 |
			uint32(u8[2]&0x3F), nil

	} else if b1>>3 == 0x1E && u8l == 4 {
		return uint32(b1&0x7)<<18 |
			uint32(u8[1]&0x3F)<<12 |
			uint32(u8[2]&0x3F)<<6 |
			uint32(u8[3]&0x3F), nil
	}

	return 0, errors.New(fmt.Sprintf("deformed utf8: %d", u8))
}

func TestUtf82unicode(t *testing.T) {
	u, _ := Utf82unicode([]byte{0x75})
	if u != 0x75 {
		t.Fatal("not pass 'u'")
	}

	u, _ = Utf82unicode([]byte{0xC2, 0xA9})
	if u != 0xA9 {
		t.Fatal("not pass '©'")
	}

	u, _ = Utf82unicode([]byte{0xE6, 0xB1, 0x89})
	if u != 0x6C49 {
		t.Fatalf("not pass '漢': %x", u)
	}

	u, _ = Utf82unicode([]byte{0xF0, 0x9F, 0x98, 0x84})
	if u != 0x1F604 {
		t.Fatal("not pass '?'")
	}
}

func IsIntactUtf8(u8 []byte) bool {
	i := 0
	u8l := len(u8)

	for {
		if i == u8l {
			break
		}

		b1 := u8[i]
		var tu uint32

		switch {
		case b1 <= 0x7F:
		case b1>>5 == 0x6:
			if u8l-i >= 2 &&
				u8[i+1]&0xC0 == 0x80 &&
				// U+0000 encoded in two bytes: incorrect
				(u8[i] > 0xC0 || u8[i+1] > 0x80) {
				i++
			} else {
				return false
			}
		case b1>>4 == 0xE:
			if u8l-i >= 3 {
				tu = uint32(b1&0xF)<<12 |
					uint32(u8[i+1]&0x3F)<<6 |
					uint32(u8[i+2]&0x3F)

				// UTF-8 prohibits encoding character numbers between U+D800 and U+DFFF
				if tu >= 0x800 && tu <= 0xFFFF && !(tu >= 0xD800 && tu <= 0xDFFF) {
					i += 2
				} else {
					return false
				}
			} else {
				return false
			}
		case b1>>3 == 0x1E:
			if u8l-i >= 4 &&
				u8[i]&0x7 <= 0x4 &&
				u8[i+1]&0xC0 == 0x80 && u8[i+1]&0x3F <= 0xF &&
				u8[i+2]&0xC0 == 0x80 &&
				u8[i+3]&0xC0 == 0x80 {
				i += 3
			} else {
				return false
			}
		default:
			return false
		}
		i++
	}

	return i == u8l
}

type ValidTest struct {
	in  string
	out bool
}

var validTests = []ValidTest{
	{"", true},
	{"a", true},
	{"abc", true},
	{"Ж", true},
	{"ЖЖ", true},
	{"брэд-ЛГТМ", true},
	{"☺☻☹", true},
	{string([]byte{66, 250}), false},
	{string([]byte{66, 250, 67}), false},
	{"a\uFFFDb", true},
	{string("\xF4\x8F\xBF\xBF"), true},      // U+10FFFF
	{string("\xF4\x90\x80\x80"), false},     // U+10FFFF+1; out of range
	{string("\xF7\xBF\xBF\xBF"), false},     // 0x1FFFFF; out of range
	{string("\xFB\xBF\xBF\xBF\xBF"), false}, // 0x3FFFFFF; out of range
	{string("\xc0\x80"), false},             // U+0000 encoded in two bytes: incorrect
	{string("\xed\xa0\x80"), false},         // U+D800 high surrogate (sic)
	{string("\xed\xbf\xbf"), false},         // U+DFFF low surrogate (sic)
}

func TestIsIntactUtf8(t *testing.T) {
	for i, tt := range validTests {
		if IsIntactUtf8([]byte(tt.in)) != tt.out {
			t.Fatalf("[CASE %d] IsIntactUtf8(%q) = %v; want %v", i, tt.in, !tt.out, tt.out)
		}
	}
}
複製程式碼

原理以及程式碼都給出來了,應該會對 UTF-8 以及 UTF-8 與 Unicode 之間的關係不明瞭的同學有些幫助吧。

相關文章