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

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

このコミットは、Go言語の標準ライブラリ mime/multipart パッケージ内の quotedprintable デコーダにおけるバグ修正を目的としています。具体的には、Quoted-Printableエンコーディングされたテキストをデコードする際に、行頭のスペースやタブが誤って除去されてしまう問題を解決します。この問題は、Go 1.0からGo 1.1への移行期に導入された回帰バグであり、RFC 2045で定義されているQuoted-Printableの仕様に準拠していない動作でした。

コミット

commit 24555c7b8cfc11f47b0df7ab2add827c64ba3d19
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Wed Apr 17 20:04:58 2013 -0700

    mime/multipart: don't strip leading space/tab in quoted-printable decoding
    
    Late bug fix, but this is arguably a regression from Go 1.0,
    since we added this transparent decoding since then. Without
    this fix, Go 1.0 users could decode this correctly, but Go 1.1
    users would not be able to.
    
    The newly added test is from the RFC itself.
    
    The updated tests had the wrong "want" values before. They
    were there to test \r\n vs \n equivalence (which is
    unchanged), not leading whitespace.
    
    The skipWhite decoder struct field was added in the battles of
    Issue 4771 in revision b3bb265bfecf. It was just a wrong
    strategy, from an earlier round of attempts in
    https://golang.org/cl/7300092/
    
    Update #4771
    Fixes #5295
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/8536045

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

https://github.com/golang/go/commit/24555c7b8cfc11f47b0df7ab2add827c64ba3d19

元コミット内容

このコミットは、mime/multipart: don't strip leading space/tab in quoted-printable decoding というタイトルで、Quoted-Printableデコード時に行頭のスペース/タブを除去しないようにする修正です。これはGo 1.0からの回帰バグであり、Go 1.1でこの問題が発生していました。RFC 2045の例に基づいた新しいテストが追加され、既存のテストも修正されています。skipWhite フィールドが誤った戦略であったことが指摘されており、関連するIssue #4771と#5295を修正しています。

変更の背景

この変更の背景には、Go言語の mime/multipart パッケージにおけるQuoted-Printableエンコーディングのデコード処理の不正確さがありました。コミットメッセージによると、この問題はGo 1.0からGo 1.1への移行期に導入された「回帰バグ」であるとされています。Go 1.0では正しくデコードできていたものが、Go 1.1ではできなくなっていたという状況です。

具体的には、Quoted-Printableエンコーディングの仕様(RFC 2045)では、エンコードされたテキストの行頭にスペースやタブが存在する場合、それらはそのまま保持されるべきです。しかし、Go 1.1のデコーダでは、これらの行頭の空白文字が誤って除去されてしまう動作になっていました。

このバグは、GoのIssue #5295として報告され、その根本原因は以前のIssue #4771の修正に関連して導入された skipWhite というデコーダのフィールドにありました。skipWhite は、Quoted-Printableのデコード処理において、行頭の空白文字をスキップするためのフラグとして導入されましたが、これがRFCの仕様に反する動作を引き起こしていました。コミットメッセージでは、この skipWhite フィールドが「間違った戦略」であったと明言されています。

この修正は、Quoted-Printableエンコーディングの標準への準拠を回復し、Go 1.1におけるデコードの正確性を向上させるために行われました。

前提知識の解説

Quoted-Printableエンコーディング

Quoted-Printable (QP) は、MIME (Multipurpose Internet Mail Extensions) で使用されるコンテンツ転送エンコーディングの一つです。主に、7ビットのASCII文字セットで表現できない8ビットのデータ(日本語などの多バイト文字やバイナリデータ)を、7ビットのASCII文字のみで安全に転送するために設計されています。電子メールの本文などでよく利用されます。

主な特徴とルール:

  • 印字可能なASCII文字の直接表現: ほとんどの印字可能なASCII文字(スペース、タブを含む)は、そのまま表現されます。
  • 特殊文字のエスケープ:
    • ASCII文字セット外の文字(8ビット文字)は、=XX の形式でエスケープされます。XX はその文字の16進数表現です。例えば、日本語の「あ」のような文字は、UTF-8エンコーディングされたバイト列が E3 81 82 であれば、=E3=81=82 と表現されます。
    • Quoted-Printableエンコーディング自体で使用される = 文字も、=3D とエスケープされます。
    • 行末のスペースやタブは、=20=09 とエスケープされることがあります。これは、一部のメールシステムが行末の空白文字を削除する可能性があるためです。
  • ソフト改行 (Soft Line Break):
    • Quoted-Printableエンコードされた行は、76文字(または75文字)を超えないようにすることが推奨されています。行が長すぎる場合、= の後に改行(CRLF)を挿入することで、論理的な行を分割せずに物理的な行を分割できます。これを「ソフト改行」と呼びます。デコード時には、この =CRLF は無視されます。
    • 例: Now's the time = + for all folk to come= + to the aid of their country. は、デコードすると Now's the time for all folk to come to the aid of their country. となります。
  • ハード改行 (Hard Line Break):
    • 元のテキストに含まれる改行(CRLF)は、そのまま CRLF として表現されます。

RFC 2045

RFC 2045は「MIME (Multipurpose Internet Mail Extensions) Part One: Format of Internet Message Bodies」という仕様書の一部であり、MIMEメッセージの基本的な構造とヘッダフィールド、そしてコンテンツ転送エンコーディング(Quoted-PrintableやBase64など)について定義しています。

このコミットで特に重要となるのは、RFC 2045のセクション6.7「Quoted-Printable Content-Transfer-Encoding」です。このセクションには、Quoted-Printableエンコーディングの具体的なルールと、デコード時の振る舞いが詳細に記述されています。コミットメッセージで「The newly added test is from the RFC itself.」とあるように、RFC 2045に記載されているQuoted-Printableの例がテストケースとして採用されています。これは、実装がRFCの仕様に厳密に準拠していることを確認するための重要なステップです。

Go言語の mime/multipart パッケージ

Go言語の標準ライブラリ mime/multipart パッケージは、MIME multipartメッセージの解析と生成をサポートします。これは、HTTPの multipart/form-data や電子メールの添付ファイルなど、複数の異なるデータタイプを一つのメッセージにまとめる際に使用されます。このパッケージは、Quoted-PrintableやBase64といったコンテンツ転送エンコーディングのデコード機能も内部的に利用しています。

このコミットで修正された quotedprintable.go は、mime/multipart パッケージの一部として、Quoted-Printableエンコードされたデータを読み取り、デコードするためのロジックを提供しています。

技術的詳細

このコミットの技術的詳細は、mime/multipart/quotedprintable.go ファイル内の qpReader 構造体と、その Read メソッドの変更に集約されます。

qpReader 構造体の変更

修正前、qpReader 構造体には skipWhite というブール型のフィールドが含まれていました。

type qpReader struct {
	br        *bufio.Reader
	skipWhite bool // 修正前: 行頭の空白をスキップするかどうか
	rerr      error  // last read error
	line      []byte // to be consumed before more of br
}

この skipWhite フィールドは、newQuotedPrintableReader 関数で true に初期化され、Read メソッド内で使用されていました。

修正後、skipWhite フィールドは qpReader 構造体から完全に削除されました。

type qpReader struct {
	br   *bufio.Reader
	rerr error  // last read error
	line []byte // to be consumed before more of br
}

この変更は、行頭の空白文字をスキップするという「戦略」自体が誤りであったという認識に基づいています。Quoted-Printableの仕様では、行頭の空白文字はデータの一部として扱われるべきであり、デコード時に除去されるべきではありません。

isQPSkipWhiteByte 関数の削除

skipWhite フィールドの削除に伴い、isQPSkipWhiteByte というヘルパー関数も削除されました。この関数は、バイトがスペース (' ') またはタブ ('\t') であるかを判定するために使用されていました。

// 修正前:
func isQPSkipWhiteByte(b byte) bool {
	return b == ' ' || b == '\t'
}

この関数の削除は、行頭の空白文字を特別扱いする必要がなくなったことを意味します。

Read メソッドの変更

qpReaderRead メソッドは、Quoted-Printableエンコードされた入力ストリームからバイトを読み取り、デコードして出力バッファ p に書き込む主要なロジックを含んでいます。

修正前、Read メソッド内には skipWhite フィールドと isQPSkipWhiteByte 関数を利用して行頭の空白文字をスキップするロジックが存在しました。

// 修正前: Read メソッド内の関連部分
// ...
// q.skipWhite = true // 新しい行を読み込む際にリセット
// q.line, q.rerr = q.br.ReadSlice('\n')
// ...
// if q.skipWhite && isQPSkipWhiteByte(b) {
//     q.line = q.line[1:] // 先頭の空白をスキップ
//     continue
// }
// q.skipWhite = false // 空白以外の文字を読んだらスキップを停止
// ...

このロジックは、新しい行を読み込むたびに skipWhitetrue に設定し、行頭の文字がスペースまたはタブであればそれをスキップし、skipWhitefalse に設定して通常のデコード処理に進むというものでした。

修正後、この skipWhite に関連するすべてのロジックが Read メソッドから削除されました。

// 修正後: Read メソッド内の関連部分
// ...
// q.line, q.rerr = q.br.ReadSlice('\n') // skipWhite の設定が削除された
// ...
// (skipWhite と isQPSkipWhiteByte を使用した条件分岐が削除された)
// ...

これにより、qpReader は行頭のスペースやタブを特別扱いせず、他の印字可能なASCII文字と同様にデコード処理の一部として扱うようになりました。結果として、RFC 2045の仕様に準拠し、行頭の空白文字が保持されるようになりました。

テストケースの更新

src/pkg/mime/multipart/quotedprintable_test.go ファイルでは、既存のテストケースが更新され、RFC 2045の例に基づいた新しいテストケースが追加されました。

特に注目すべきは、以下のテストケースの変更です。

--- a/src/pkg/mime/multipart/quotedprintable_test.go
+++ b/src/pkg/mime/multipart/quotedprintable_test.go
@@ -32,8 +32,9 @@ func TestQuotedPrintable(t *testing.T) {
 		{in: "foo bar=0", want: "foo bar", err: io.ErrUnexpectedEOF},
 		{in: "foo bar=ab", want: "foo bar", err: "multipart: invalid quoted-printable hex byte 0x61"},
 		{in: "foo bar=0D=0A", want: "foo bar\r\n"},
-		{in: " A B =\r\n C ", want: "A B C"},
-		{in: " A B =\n C ", want: "A B C"}, // lax. treating LF as CRLF
+		{in: " A B        \r\n C ", want: " A B\r\n C"},
+		{in: " A B =\r\n C ", want: " A B  C"},
+		{in: " A B =\n C ", want: " A B  C"}, // lax. treating LF as CRLF
 		{in: "foo=\nbar", want: "foobar"},
 		{in: "foo\x00bar", want: "foo", err: "multipart: invalid unescaped byte 0x00 in quoted-printable body"},
 		{in: "foo bar\xff", want: "foo bar", err: "multipart: invalid unescaped byte 0xff in quoted-printable body"},
@@ -57,6 +58,10 @@ func TestQuotedPrintable(t *testing.T) {
 		{in: "foo=\nbar", want: "foobar"},
 		{in: "foo=\rbar", want: "foo", err: "multipart: invalid quoted-printable hex byte 0x0d"},
 		{in: "foo=\r\r\r \nbar", want: "foo", err: `multipart: invalid bytes after =: "\r\r\r \n"`},
++
++		// Example from RFC 2045:
++		{in: "Now's the time =\n" + "for all folk to come=\n" + " to the aid of their country.",
++		        want: "Now's the time for all folk to come to the aid of their country."},
 	}
 	for _, tt := range tests {
 		var buf bytes.Buffer
  • {in: " A B =\r\n C ", want: "A B C"}{in: " A B =\r\n C ", want: " A B C"} に変更されています。これは、ソフト改行 (=CRLF) の前後の空白が保持されるべきであることを示しています。元のテストでは、行頭のスペースが除去され、ソフト改行後のスペースも結合されてしまっていました。
  • {in: " A B \r\n C ", want: " A B\r\n C"} という新しいテストケースが追加されています。これは、ソフト改行ではない通常の改行 (\r\n) の前後の空白が保持されることを確認しています。
  • RFC 2045からの例 (Now's the time =...) が追加され、ソフト改行が正しく処理され、結果として空白が適切に結合されることを検証しています。

これらのテストケースの変更は、デコーダがRFC 2045の仕様に厳密に準拠し、特に空白文字の扱いで誤った挙動をしないことを保証するために不可欠です。

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

このコミットにおけるコアとなるコードの変更箇所は、以下の2つのファイルに集中しています。

  1. src/pkg/mime/multipart/quotedprintable.go

    • qpReader 構造体から skipWhite フィールドの削除。
    • newQuotedPrintableReader 関数における skipWhite の初期化の削除。
    • isQPSkipWhiteByte ヘルパー関数の削除。
    • qpReader.Read メソッド内での skipWhite フィールドと isQPSkipWhiteByte 関数の使用箇所の削除。
  2. src/pkg/mime/multipart/quotedprintable_test.go

    • 既存のテストケースの want (期待値) の修正。
    • RFC 2045の例に基づいた新しいテストケースの追加。

コアとなるコードの解説

src/pkg/mime/multipart/quotedprintable.go

このファイルは、Quoted-Printableエンコーディングのデコードロジックを実装しています。

変更前:

type qpReader struct {
	br        *bufio.Reader
	skipWhite bool // このフィールドが問題の原因
	rerr      error
	line      []byte
}

func newQuotedPrintableReader(r io.Reader) io.Reader {
	return &qpReader{
		br:        bufio.NewReader(r),
		skipWhite: true, // ここで true に初期化される
	}
}

func isQPSkipWhiteByte(b byte) bool {
	return b == ' ' || b == '\t'
}

func (q *qpReader) Read(p []byte) (n int, err error) {
	// ...
	// 新しい行を読み込む際に skipWhite を true にリセット
	// q.skipWhite = true
	// q.line, q.rerr = q.br.ReadSlice('\n')
	// ...
	// 行頭の空白をスキップするロジック
	// if q.skipWhite && isQPSkipWhiteByte(b) {
	//     q.line = q.line[1:]
	//     continue
	// }
	// q.skipWhite = false
	// ...
}

skipWhite フィールドは、qpReader が新しい行の先頭にいるときに true に設定され、行頭のスペースやタブをスキップするために使用されていました。これは、Quoted-Printableの仕様に反して、行頭の空白文字をデータの一部として扱わないという誤った仮定に基づいていました。

変更後:

type qpReader struct {
	br   *bufio.Reader
	rerr error
	line []byte
}

func newQuotedPrintableReader(r io.Reader) io.Reader {
	return &qpReader{
		br: bufio.NewReader(r),
		// skipWhite の初期化が削除された
	}
}

// isQPSkipWhiteByte 関数が完全に削除された

func (q *qpReader) Read(p []byte) (n int, err error) {
	// ...
	// skipWhite に関連するすべてのロジックが削除された
	// q.line, q.rerr = q.br.ReadSlice('\n')
	// ...
	// 行頭の空白をスキップする条件分岐が削除された
	// ...
}

skipWhite フィールドとその関連ロジックが削除されたことで、qpReader は入力ストリームから読み込んだすべての文字を、Quoted-Printableのデコードルールに従って処理するようになりました。これにより、行頭のスペースやタブも正しくデコードされ、出力に反映されるようになります。

src/pkg/mime/multipart/quotedprintable_test.go

このファイルは、quotedprintable パッケージのテストケースを含んでいます。

変更前:

既存のテストケースの一部では、行頭の空白がデコード時に除去されることを期待する want (期待値) が設定されていました。これは、バグのある実装の動作に合わせてテストが書かれていたためです。

変更後:

// ...
{in: " A B        \r\n C ", want: " A B\r\n C"}, // 新規追加
{in: " A B =\r\n C ", want: " A B  C"}, // 期待値が変更された
{in: " A B =\n C ", want: " A B  C"}, // 期待値が変更された
// ...
// RFC 2045からの例が追加された
{in: "Now's the time =\n" + "for all folk to come=\n" + " to the aid of their country.",
        want: "Now's the time for all folk to come to the aid of their country."},
// ...

テストケースの want 値が修正され、行頭の空白が保持されること、およびソフト改行の前後で空白が適切に処理されることが期待されるようになりました。RFC 2045の例が追加されたことで、標準仕様への準拠がより厳密に検証されるようになりました。これらのテストの変更は、修正されたデコードロジックが正しく機能していることを確認するために不可欠です。

関連リンク

参考にした情報源リンク