[インデックス 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)では、ヘッダー内の文字列フィールド(FNAME
やFCOMMENT
など)はISO 8859-1 (Latin-1)エンコーディングで格納されると規定されています。しかし、Go言語の文字列は内部的にUTF-8エンコーディングを使用します。
この不一致のため、以前のcompress/gzip
パッケージでは、Latin-1の範囲外の文字(例えば、ウムラウトやアクセント記号など)を含む文字列がGZIPヘッダーに書き込まれた場合、または読み込まれた場合に、文字化けやエラーが発生する可能性がありました。特に、gzip.go
のwriteString
関数には「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
)に達した際、needconv
がtrue
であれば、読み込んだバイト列をrune
スライスに変換し、それをstring
にキャストすることでUTF-8文字列を生成します。rune
はGoにおけるUnicodeコードポイントを表す型であり、string(rune_slice)
はUTF-8エンコードされた文字列を生成します。 needconv
がfalse
であれば(つまり、文字列が純粋な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
ではなくバイトスライスへの変換が必要であることを示します。- ループ後、
needconv
がtrue
であれば、入力文字列s
の各rune
をbyte
にキャストしてバイトスライスb
を構築し、z.w.Write(b)
で書き込みます。Goにおいて、byte(rune)
はrune
が0-255の範囲内であればその値をbyte
に変換します。これにより、UTF-8文字列のLatin-1部分がLatin-1バイト列として抽出されます。 needconv
がfalse
であれば、文字列は純粋な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.go
の readString
関数
needconv := false
: 新しく導入されたフラグで、Latin-1からUTF-8への変換が必要かどうかを示します。if z.buf[i] > 0x7f { needconv = true }
: 読み込んだバイトがASCII範囲外(0x7Fより大きい)であれば、変換が必要と判断します。if z.buf[i] == 0 { ... }
: ヌル文字(文字列の終端)に到達した際の処理です。if needconv { ... }
:needconv
がtrue
の場合、つまり非ASCII文字が含まれていた場合、以下の変換ロジックが実行されます。s := make([]rune, 0, i)
: 読み込んだ文字数分のrune
スライスを初期化します。for _, v := range z.buf[0:i] { s = append(s, rune(v)) }
: 読み込んだバイト列の各バイトv
をrune(v)
としてrune
スライスに追加します。これにより、Latin-1の各バイトが対応するUnicodeコードポイントに変換されます。return string(s), nil
:rune
スライスをstring
に変換することで、UTF-8エンコードされた文字列が生成されます。
return string(z.buf[0:i]), nil
:needconv
がfalse
の場合(ASCII文字のみの場合)、変換は不要なので、バイトスライスを直接string
にキャストして返します。
gzip.go
の writeString
関数
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 { ... }
:needconv
がtrue
の場合、つまり非ASCII文字が含まれていた場合、以下の変換ロジックが実行されます。b := make([]byte, 0, len(s))
: 入力文字列の長さ分のバイトスライスを初期化します。for _, v := range s { b = append(b, byte(v)) }
: 入力文字列の各rune
をbyte
にキャストしてバイトスライスb
に追加します。これにより、UTF-8文字列のLatin-1部分がLatin-1バイト列として抽出されます。_, err = z.w.Write(b)
: 変換されたバイトスライスをライターに書き込みます。
else { _, err = io.WriteString(z.w, s) }
:needconv
がfalse
の場合(ASCII文字のみの場合)、変換は不要なので、io.WriteString
を直接呼び出して文字列を書き込みます。
gzip_test.go
の TestLatin1
関数
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 - GZIP file format specification version 4.3
- ISO/IEC 8859-1 - Wikipedia
- UTF-8 - Wikipedia
- Go言語における文字列とバイト列 - Qiita (Go言語の文字列とバイト列の扱いに関する一般的な情報)
参考にした情報源リンク
- 上記のRFC 1952の仕様書
- ISO 8859-1およびUTF-8の文字エンコーディングに関する一般的な知識
- Go言語の
string
、[]byte
、rune
の型変換とエンコーディングに関するGo公式ドキュメントやチュートリアル - コミットメッセージとコードの差分