Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 10797] ファイルの概要

このコミットは、Go言語のcompress/gzipパッケージにおいて、GZIPヘッダー内の文字列(コメントやファイル名など)のエンコーディング処理を改善するものです。具体的には、GZIP仕様で規定されているISO 8859-1 (Latin-1)エンコーディングと、Go言語が内部的に使用するUTF-8エンコーディング間の変換を適切に行うように修正されています。これにより、非ASCII文字を含むGZIPヘッダーが正しく処理されるようになります。

コミット

commit 8fbeb945dbe9532218110a42ceccd07860128673
Author: Vadim Vygonets <unixdj@gmail.com>
Date:   Wed Dec 14 17:17:40 2011 -0500

    gzip: Convert between Latin-1 and Unicode
    
    I realize I didn't send the tests in last time.  Anyway, I added
    a test that knows too much about the package's internal structure,
    and I'm not sure whether it's the right thing to do.
    
    Vadik.
    
    R=bradfitz, rsc, go.peter.90
    CC=golang-dev
    https://golang.org/cl/5450073

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/8fbeb945dbe9532218110a42ceccd07860128673

元コミット内容

gzip: Convert between Latin-1 and Unicode

I realize I didn't send the tests in last time.  Anyway, I added
a test that knows too much about the package's internal structure,
and I'm not sure whether it's the right thing to do.

Vadik.

R=bradfitz, rsc, go.peter.90
CC=golang-dev
https://golang.org/cl/5450073

変更の背景

GZIPファイルフォーマット(RFC 1952)では、ヘッダー内の文字列フィールド(FNAMEFCOMMENTなど)はISO 8859-1 (Latin-1)エンコーディングで格納されると規定されています。しかし、Go言語の文字列は内部的にUTF-8エンコーディングを使用します。

この不一致のため、以前のcompress/gzipパッケージでは、Latin-1の範囲外の文字(例えば、ウムラウトやアクセント記号など)を含む文字列がGZIPヘッダーに書き込まれた場合、または読み込まれた場合に、文字化けやエラーが発生する可能性がありました。特に、gzip.gowriteString関数には「TODO(nigeltao): Convert from UTF-8 to ISO 8859-1 (Latin-1).」というコメントがあり、この問題が認識されていました。

このコミットは、このTODOコメントに対応し、GZIP仕様に準拠しつつ、Go言語のUTF-8文字列との間で透過的な変換を行うことで、国際化された文字列がGZIPヘッダーで正しく扱えるようにすることを目的としています。

前提知識の解説

GZIPファイルフォーマット (RFC 1952)

GZIPは、データ圧縮とファイルフォーマットを組み合わせたものです。RFC 1952でその仕様が定義されており、主に以下のセクションで構成されます。

  • ヘッダー (Header): ファイルのメタデータを含みます。これには、マジックナンバー、圧縮方式、フラグ、変更時刻、追加フィールド、ファイル名、コメントなどが含まれます。
  • 圧縮データ (Compressed Data): 実際の圧縮されたデータです。
  • フッター (Footer): CRC32チェックサムと元のデータのサイズを含みます。

このコミットで関連するのはヘッダー部分、特にFNAME (ファイル名) と FCOMMENT (コメント) フラグが設定された場合に現れる文字列フィールドです。RFC 1952の2.3.1節「Member header and trailer」には、「All strings are stored in ISO 8859-1 (Latin-1) encoding.」と明記されています。

文字エンコーディング: ISO 8859-1 (Latin-1) と UTF-8 (Unicode)

  • ISO 8859-1 (Latin-1):

    • 1バイト(8ビット)で1文字を表現する文字エンコーディングです。
    • 0x00から0xFFまでの256種類の文字を定義します。
    • 0x00-0x7FはASCIIと同じです。
    • 0x80-0xFFは西ヨーロッパ言語で使われるアクセント付き文字や特殊記号を含みます。
    • 日本語や中国語のような多バイト文字は表現できません。
  • UTF-8 (Unicode Transformation Format - 8-bit):

    • Unicode文字セットを可変長バイトでエンコードする方式です。
    • 1文字を1バイトから4バイトで表現します。
    • ASCII文字(0x00-0x7F)は1バイトで表現され、Latin-1のASCII部分と互換性があります。
    • Latin-1の0x80-0xFFの文字は、UTF-8では2バイトで表現されます。
    • 世界中のほとんどの言語の文字を表現できます。

なぜ変換が必要か

GZIPヘッダーはLatin-1でなければならないという仕様があるため、GoプログラムがUTF-8で扱っている文字列をGZIPヘッダーに書き込む際にはLatin-1に変換する必要があります。逆に、GZIPヘッダーからLatin-1で読み込んだ文字列をGoプログラムで扱う際にはUTF-8に変換する必要があります。この変換を適切に行わないと、非ASCII文字が正しく表示されなかったり、データが破損したりする可能性があります。

技術的詳細

このコミットは、compress/gzipパッケージ内のDecompressor.readString()Compressor.writeString()の2つの主要な関数に変更を加えています。

Decompressor.readString()の変更

  • 目的: GZIPヘッダーからLatin-1エンコードされた文字列を読み込み、Goの内部表現であるUTF-8文字列に変換します。
  • 変更点:
    • needconvというブール変数を導入し、読み込んだバイト列に0x7F(ASCIIの最大値)を超える値が含まれているかどうかをチェックします。
    • もし0x7Fを超える値が含まれていれば、その文字列はLatin-1の非ASCII文字を含んでいると判断し、UTF-8への変換が必要であるとマークします。
    • 文字列の終端(ヌル文字0)に達した際、needconvtrueであれば、読み込んだバイト列をruneスライスに変換し、それをstringにキャストすることでUTF-8文字列を生成します。runeはGoにおけるUnicodeコードポイントを表す型であり、string(rune_slice)はUTF-8エンコードされた文字列を生成します。
    • needconvfalseであれば(つまり、文字列が純粋なASCII文字のみで構成されていれば)、変換は不要なので、元のバイトスライスを直接stringにキャストします。これはASCII文字がLatin-1とUTF-8で同じ表現を持つためです。

Compressor.writeString()の変更

  • 目的: GoのUTF-8文字列をGZIPヘッダーに書き込むためにLatin-1エンコードされたバイト列に変換します。
  • 変更点:
    • 入力文字列sの各rune(Unicodeコードポイント)をループで処理します。
    • v == 0 || v > 0xffのチェックを追加し、ヌル文字(GZIP文字列の終端文字)またはLatin-1の範囲外(0xFFを超える)の文字が含まれていないかを検証します。もしそのような文字があれば、"gzip.Write: non-Latin-1 header string"というエラーを返します。これは、GZIPヘッダーがLatin-1に限定されているため、Latin-1で表現できない文字は書き込めないという仕様上の制約を強制するものです。
    • v > 0x7fのチェックでneedconvフラグを設定します。これは、入力文字列にASCII範囲外の文字が含まれている場合にio.WriteStringではなくバイトスライスへの変換が必要であることを示します。
    • ループ後、needconvtrueであれば、入力文字列sの各runebyteにキャストしてバイトスライスbを構築し、z.w.Write(b)で書き込みます。Goにおいて、byte(rune)runeが0-255の範囲内であればその値をbyteに変換します。これにより、UTF-8文字列のLatin-1部分がLatin-1バイト列として抽出されます。
    • needconvfalseであれば、文字列は純粋なASCIIなので、io.WriteString(z.w, s)を直接呼び出します。

テストの追加 (gzip_test.go)

  • TestLatin1という新しいテスト関数が追加されました。
  • このテストは、Latin-1エンコードされたバイト列(例: ÄußerungのLatin-1表現)をDecompressor.readString()で読み込み、それが正しいUTF-8文字列に変換されることを検証します。
  • また、UTF-8文字列(例: Äußerung)をCompressor.writeString()で書き込み、それが正しいLatin-1バイト列として出力されることを検証します。
  • 既存のTestWriter関数も修正され、compressor.Comment"Äußerung"という非ASCII文字を含む文字列を設定し、それが正しく読み書きされることを確認しています。

コアとなるコードの変更箇所

src/pkg/compress/gzip/gunzip.go

--- a/src/pkg/compress/gzip/gunzip.go
+++ b/src/pkg/compress/gzip/gunzip.go
@@ -96,6 +96,7 @@ func get4(p []byte) uint32 {
 
 func (z *Decompressor) readString() (string, error) {
 	var err error
+	needconv := false
 	for i := 0; ; i++ {
 		if i >= len(z.buf) {
 			return "", HeaderError
@@ -104,9 +105,18 @@ func (z *Decompressor) readString() (string, error) {
 		if err != nil {
 			return "", err
 		}
+		if z.buf[i] > 0x7f {
+			needconv = true
+		}
 		if z.buf[i] == 0 {
 			// GZIP (RFC 1952) specifies that strings are NUL-terminated ISO 8859-1 (Latin-1).
 			// TODO(nigeltao): Convert from ISO 8859-1 (Latin-1) to UTF-8.
+			if needconv {
+				s := make([]rune, 0, i)
+				for _, v := range z.buf[0:i] {
+					s = append(s, rune(v))
+				}
+				return string(s), nil
+			}
 			return string(z.buf[0:i]), nil
 		}
 	}

src/pkg/compress/gzip/gzip.go

--- a/src/pkg/compress/gzip/gzip.go
+++ b/src/pkg/compress/gzip/gzip.go
@@ -86,13 +86,25 @@ func (z *Compressor) writeBytes(b []byte) error {
 // writeString writes a string (in ISO 8859-1 (Latin-1) format) to z.w.
 func (z *Compressor) writeString(s string) error {
 	// GZIP (RFC 1952) specifies that strings are NUL-terminated ISO 8859-1 (Latin-1).
 	// TODO(nigeltao): Convert from UTF-8 to ISO 8859-1 (Latin-1).
+	var err error
+	needconv := false
 	for _, v := range s {
-		if v == 0 || v > 0x7f {
-			return errors.New("gzip.Write: non-ASCII header string")
+		if v == 0 || v > 0xff {
+			return errors.New("gzip.Write: non-Latin-1 header string")
 		}
+		if v > 0x7f {
+			needconv = true
+		}
+	}
+	if needconv {
+		b := make([]byte, 0, len(s))
+		for _, v := range s {
+			b = append(b, byte(v))
+		}
+		_, err = z.w.Write(b)
+	} else {
+		_, err = io.WriteString(z.w, s)
 	}
-	_, err := io.WriteString(z.w, s)
 	if err != nil {
 		return err
 	}

src/pkg/compress/gzip/gzip_test.go

--- a/src/pkg/compress/gzip/gzip_test.go
+++ b/src/pkg/compress/gzip/gzip_test.go
@@ -5,6 +5,8 @@
 package gzip
 
 import (
+	"bufio"
+	"bytes"
 	"io"
 	"io/ioutil"
 	"testing"
@@ -52,7 +54,8 @@ func TestEmpty(t *testing.T) {
 func TestWriter(t *testing.T) {
 	pipe(t,
 		func(compressor *Compressor) {
-			compressor.Comment = "comment"
+			compressor.Comment = "Äußerung"
+			//compressor.Comment = "comment"
 			compressor.Extra = []byte("extra")
 			compressor.ModTime = time.Unix(1e8, 0)
 			compressor.Name = "name"
@@ -69,8 +72,8 @@ func TestWriter(t *testing.T) {
 			if string(b) != "payload" {
 				t.Fatalf("payload is %q, want %q", string(b), "payload")
 			}
-			if decompressor.Comment != "comment" {
-				t.Fatalf("comment is %q, want %q", decompressor.Comment, "comment")
+			if decompressor.Comment != "Äußerung" {
+				t.Fatalf("comment is %q, want %q", decompressor.Comment, "Äußerung")
 			}
 			if string(decompressor.Extra) != "extra" {
 				t.Fatalf("extra is %q, want %q", decompressor.Extra, "extra")
@@ -83,3 +86,29 @@ func TestWriter(t *testing.T) {
 			}
 		})
 }
+
+func TestLatin1(t *testing.T) {
+	latin1 := []byte{0xc4, 'u', 0xdf, 'e', 'r', 'u', 'n', 'g', 0}
+	utf8 := "Äußerung"
+	z := Decompressor{r: bufio.NewReader(bytes.NewBuffer(latin1))}
+	s, err := z.readString()
+	if err != nil {
+		t.Fatalf("%v", err)
+	}
+	if s != utf8 {
+		t.Fatalf("string is %q, want %q", s, utf8)
+	}
+
+	buf := bytes.NewBuffer(make([]byte, 0, len(latin1)))
+	c := Compressor{w: buf}
+	if err = c.writeString(utf8); err != nil {
+		t.Fatalf("%v", err)
+	}
+	s = buf.String()
+	if s != string(latin1) {
+		t.Fatalf("string is %v, want %v", s, latin1)
+	}
+	//if s, err = buf.ReadString(0); err != nil {
+	//t.Fatalf("%v", err)
+	//}
+}

コアとなるコードの解説

gunzip.goreadString 関数

  • needconv := false: 新しく導入されたフラグで、Latin-1からUTF-8への変換が必要かどうかを示します。
  • if z.buf[i] > 0x7f { needconv = true }: 読み込んだバイトがASCII範囲外(0x7Fより大きい)であれば、変換が必要と判断します。
  • if z.buf[i] == 0 { ... }: ヌル文字(文字列の終端)に到達した際の処理です。
    • if needconv { ... }: needconvtrueの場合、つまり非ASCII文字が含まれていた場合、以下の変換ロジックが実行されます。
      • s := make([]rune, 0, i): 読み込んだ文字数分のruneスライスを初期化します。
      • for _, v := range z.buf[0:i] { s = append(s, rune(v)) }: 読み込んだバイト列の各バイトvrune(v)としてruneスライスに追加します。これにより、Latin-1の各バイトが対応するUnicodeコードポイントに変換されます。
      • return string(s), nil: runeスライスをstringに変換することで、UTF-8エンコードされた文字列が生成されます。
    • return string(z.buf[0:i]), nil: needconvfalseの場合(ASCII文字のみの場合)、変換は不要なので、バイトスライスを直接stringにキャストして返します。

gzip.gowriteString 関数

  • needconv := false: 新しく導入されたフラグで、UTF-8からLatin-1への変換が必要かどうかを示します。
  • for _, v := range s { ... }: 入力文字列sの各rune(Unicodeコードポイント)をループで処理します。
  • if v == 0 || v > 0xff { return errors.New("gzip.Write: non-Latin-1 header string") }:
    • v == 0: ヌル文字はGZIP文字列の終端として使用されるため、文字列の途中に現れることは許されません。
    • v > 0xff: Latin-1は0xFFまでの文字しか表現できないため、それ以上のUnicodeコードポイントを持つ文字は書き込めません。これらの条件に合致する場合、エラーを返します。
  • if v > 0x7f { needconv = true }: runeがASCII範囲外(0x7Fより大きい)であれば、変換が必要と判断します。
  • if needconv { ... }: needconvtrueの場合、つまり非ASCII文字が含まれていた場合、以下の変換ロジックが実行されます。
    • b := make([]byte, 0, len(s)): 入力文字列の長さ分のバイトスライスを初期化します。
    • for _, v := range s { b = append(b, byte(v)) }: 入力文字列の各runebyteにキャストしてバイトスライスbに追加します。これにより、UTF-8文字列のLatin-1部分がLatin-1バイト列として抽出されます。
    • _, err = z.w.Write(b): 変換されたバイトスライスをライターに書き込みます。
  • else { _, err = io.WriteString(z.w, s) }: needconvfalseの場合(ASCII文字のみの場合)、変換は不要なので、io.WriteStringを直接呼び出して文字列を書き込みます。

gzip_test.goTestLatin1 関数

  • latin1 := []byte{0xc4, 'u', 0xdf, 'e', 'r', 'u', 'n', 'g', 0}: Äußerungというドイツ語の単語のLatin-1エンコーディングとヌル終端を表すバイト列です。0xc4Ä0xdfßのLatin-1コードです。
  • utf8 := "Äußerung": 同じ単語のUTF-8エンコーディングです。
  • 読み込みテスト:
    • Decompressorを作成し、latin1バイト列を読み込ませます。
    • z.readString()を呼び出し、返された文字列sが期待されるutf8文字列と一致するかを検証します。
  • 書き込みテスト:
    • Compressorを作成し、utf8文字列を書き込ませます。
    • c.writeString(utf8)を呼び出し、書き込まれたバイト列が期待されるlatin1バイト列と一致するかを検証します。

関連リンク

参考にした情報源リンク

  • 上記のRFC 1952の仕様書
  • ISO 8859-1およびUTF-8の文字エンコーディングに関する一般的な知識
  • Go言語のstring[]byteruneの型変換とエンコーディングに関するGo公式ドキュメントやチュートリアル
  • コミットメッセージとコードの差分