[インデックス 15377] ファイルの概要
このコミットは、Go言語の mime/multipart
パッケージにおける Quoted-Printable デコーダの挙動を修正し、より一般的な実装との互換性を高めるものです。具体的には、Quoted-Printable エンコーディングにおいて、エスケープされていない改行文字(\r
や \n
)の扱いを改善し、他の一般的なデコーダの挙動に合わせることで、既存の「壊れた」エンコーダによって生成されたコンテンツのデコードを可能にします。
コミット
commit 9c7aa5fea983fe58d126542013861a022adefa70
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Fri Feb 22 10:40:23 2013 -0800
mime/multipart: allow unescaped newlines through in quoted-printable
This makes Go's quoted-printable decoder more like other
popular ones, allowing through a bare \r or \n, and also
passes through \r\n which looked like a real bug before.
Fixes #4771
R=minux.ma
CC=golang-dev
https://golang.org/cl/7300092
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9c7aa5fea983fe58d126542013861a022adefa70
元コミット内容
mime/multipart: allow unescaped newlines through in quoted-printable
このコミットは、GoのQuoted-Printableデコーダを他の一般的なデコーダに近づけ、エスケープされていない \r
や \n
を通過させるようにします。また、以前はバグのように見えた \r\n
も通過させます。
Fixes #4771
R=minux.ma CC=golang-dev https://golang.org/cl/7300092
変更の背景
この変更の背景には、Quoted-Printable エンコーディングの実際の運用における「非標準的な」挙動への対応があります。RFC 2045で定義されているQuoted-Printableエンコーディングでは、改行は通常、ソフト改行(=
の後に改行が続く)またはハード改行(エンコードされていないCRLF)として扱われます。しかし、一部のエンコーダは、RFCの厳密な解釈から逸脱し、エスケープされていない単独の \r
や \n
を含んだコンテンツを生成することがありました。
Goの mime/multipart
パッケージのQuoted-Printableデコーダは、以前はこれらの非標準的な改行を適切に処理できず、デコードエラーや予期せぬ結果を引き起こしていました。特に、Issue #4771で報告された問題は、このデコーダが \r\n
を含むコンテンツを正しく扱えないというものでした。
このコミットは、Goのデコーダをより堅牢にし、広く使われている他のデコーダ(例えば、Perlの MIME::QuotedPrintable
や qprint
コマンドラインツールなど)の挙動に合わせることで、より多くのQuoted-Printableエンコードされたコンテンツを正しく処理できるようにすることを目的としています。これにより、Goアプリケーションが様々なソースから受信するメールやMIMEパートのデコードにおける互換性が向上します。
前提知識の解説
Quoted-Printable エンコーディング (RFC 2045)
Quoted-Printable (QP) は、MIME (Multipurpose Internet Mail Extensions) で使用されるコンテンツ転送エンコーディングの一つで、主にテキストデータ(特に8ビットデータや非ASCII文字)を7ビットのASCII文字セットで安全に転送するために設計されています。電子メールシステムは歴史的に7ビットのデータしか扱えないことが多かったため、このエンコーディングが必要とされました。
QPエンコーディングの主な特徴は以下の通りです。
- 可読性: ほとんどのASCII文字(英数字、記号など)はそのままエンコードされずに転送されるため、エンコードされたテキストは比較的読みやすいです。
- エスケープシーケンス: ASCII文字セット外の文字や、QPエンコーディングで特別な意味を持つ文字(
=
、\t
、\r
、\n
)は、=XX
の形式でエスケープされます。ここでXX
は、その文字のバイト値を表す2桁の16進数です。- 例:
é
(U+00E9) はE9
のバイト値を持つため、=E9
とエンコードされます。 - スペース (
\t
) は、行末にある場合に='20'
や='09'
のようにエスケープされることがあります。これは、一部のメールシステムが行末の空白を削除する可能性があるためです。
- 例:
- ソフト改行 (Soft Line Break): QPエンコードされた行は、76文字(または75文字)の長さに制限されることが推奨されています。長い行を分割するために、行末に
=
を置くことで、その改行がエンコードされたデータの一部ではないことを示します。デコード時には、=
とそれに続く改行(CRLF)は削除され、論理的な行が再構築されます。- 例:
Hello= =World
はHelloWorld
とデコードされます。
- 例:
- ハード改行 (Hard Line Break): エンコードされたデータの一部として改行を含める場合は、
\r\n
(CRLF) をそのまま使用します。これはエスケープされません。
bufio.Reader
と io.Reader
io.Reader
: Go言語における基本的な入力インターフェースです。Read(p []byte) (n int, err error)
メソッドを持ち、データをバイトスライスp
に読み込み、読み込んだバイト数n
とエラーerr
を返します。bufio.Reader
:io.Reader
をラップし、バッファリング機能を追加することで、より効率的な読み込みを可能にする構造体です。特に、ReadSlice
やReadLine
のような行指向の読み込みメソッドを提供し、基になるio.Reader
からの読み込み回数を減らすことでパフォーマンスを向上させます。
Go言語のエラーハンドリング
Go言語では、エラーは戻り値として明示的に扱われます。関数は通常、最後の戻り値として error
型の値を返します。nil
はエラーがないことを意味し、非nil
の値はエラーが発生したことを示します。
技術的詳細
このコミットは、src/pkg/mime/multipart/quotedprintable.go
内の qpReader
構造体とその Read
メソッドに焦点を当てています。主な変更点は以下の通りです。
-
qpReader
構造体の変更:skipWhite bool
フィールドが追加されました。これは、行の先頭で空白文字(スペースやタブ)をスキップするかどうかを制御するために使用されます。
-
newQuotedPrintableReader
関数の変更:qpReader
の初期化時にskipWhite
をtrue
に設定するようになりました。これにより、デコード処理の開始時に先頭の空白がスキップされるようになります。
-
isQPSkipWhiteByte
関数の追加:byte
がスペース (\t
) であるかを判定するヘルパー関数が追加されました。
-
qpReader.Read
メソッドのロジック変更:- ソフト改行の処理の緩和: 以前は
q.line
が[]byte{'='}
の場合にのみソフト改行として扱われていましたが、新しいロジックでは、行がbytes.HasSuffix(q.line, softSuffix)
(つまり=
で終わる) かどうかをチェックします。 =\n
のサポート: RFC 2045ではソフト改行は=\r\n
と定義されていますが、この変更により=\n
もソフト改行として扱われるようになりました。これは、他の一般的なデコーダの挙動に合わせたものです。- エスケープされていない
\r
および\n
の通過:- 以前のコードでは、
b != '\t' && (b < ' ' || b > '~')
の条件で、タブ以外の制御文字や非ASCII文字を不正なバイトとしてエラーにしていました。 - 新しいコードでは、
b == '\t' || b == '\r' || b == '\n'
のケースが追加され、これらの文字はエスケープされていない場合でもエラーとせず、そのまま通過させるようになりました。これにより、一部の「壊れた」エンコーダが生成するコンテンツとの互換性が向上します。
- 以前のコードでは、
- 行末の空白処理の改善:
bytes.TrimRightFunc(wholeLine, isQPDiscardWhitespace)
を使用して、行末の空白をトリムするようになりました。これにより、行末の空白が正しく処理されます。 skipWhite
フラグの利用:q.skipWhite
フラグとisQPSkipWhiteByte
関数を使用して、行の先頭の空白をスキップするロジックが追加されました。
- ソフト改行の処理の緩和: 以前は
これらの変更により、qpReader
はより柔軟になり、RFC 2045の厳密な解釈から逸脱したQuoted-Printableエンコードされたデータも、より多くのケースで正しくデコードできるようになりました。
コアとなるコードの変更箇所
src/pkg/mime/multipart/quotedprintable.go
--- a/src/pkg/mime/multipart/quotedprintable.go
+++ b/src/pkg/mime/multipart/quotedprintable.go
@@ -3,6 +3,10 @@
// license that can be found in the LICENSE file.
// The file define a quoted-printable decoder, as specified in RFC 2045.
+// Deviations:
+// 1. in addition to "=\r\n", "=\n" is also treated as soft line break.
+// 2. it will pass through a '\r' or '\n' not preceded by '=', consistent
+// with other broken QP encoders & decoders.
package multipart
@@ -14,14 +18,16 @@ import (
)
type qpReader struct {
- br *bufio.Reader
- rerr error // last read error
- line []byte // to be consumed before more of br
+ br *bufio.Reader
+ skipWhite bool
+ rerr error // last read error
+ line []byte // to be consumed before more of br
}
func newQuotedPrintableReader(r io.Reader) io.Reader {
return &qpReader{
- br: bufio.NewReader(r),
+ br: bufio.NewReader(r),
+ skipWhite: true,
}
}
@@ -49,6 +55,10 @@ func (q *qpReader) readHexByte(v []byte) (b byte, err error) {
return hb<<4 | lb, nil
}
+func isQPSkipWhiteByte(b byte) bool {
+ return b == ' ' || b == '\t'
+}
+
func isQPDiscardWhitespace(r rune) bool {
switch r {
case '\n', '\r', ' ', '\t':
@@ -57,22 +67,48 @@ func isQPDiscardWhitespace(r rune) bool {
return false
}
+var (
+ crlf = []byte("\r\n")
+ lf = []byte("\n")
+ softSuffix = []byte("=")
+)
+
func (q *qpReader) Read(p []byte) (n int, err error) {
for len(p) > 0 {
if len(q.line) == 0 {
if q.rerr != nil {
return n, q.rerr
}
+ q.skipWhite = true
q.line, q.rerr = q.br.ReadSlice('\n')
- q.line = bytes.TrimRightFunc(q.line, isQPDiscardWhitespace)
+
+ // Does the line end in CRLF instead of just LF?
+ hasLF := bytes.HasSuffix(q.line, lf)
+ hasCR := bytes.HasSuffix(q.line, crlf)
+ wholeLine := q.line
+ q.line = bytes.TrimRightFunc(wholeLine, isQPDiscardWhitespace)
+ if bytes.HasSuffix(q.line, softSuffix) {
+ rightStripped := wholeLine[len(q.line):]
+ q.line = q.line[:len(q.line)-1]
+ if !bytes.HasPrefix(rightStripped, lf) && !bytes.HasPrefix(rightStripped, crlf) {
+ q.rerr = fmt.Errorf("multipart: invalid bytes after =: %q", rightStripped)
+ }
+ } else if hasLF {
+ if hasCR {
+ q.line = append(q.line, '\r', '\n')
+ } else {
+ q.line = append(q.line, '\n')
+ }
+ }
continue
}
- if len(q.line) == 1 && q.line[0] == '=' {
- // Soft newline; skipped.
- q.line = nil
+ b := q.line[0]
+ if q.skipWhite && isQPSkipWhiteByte(b) {
+ q.line = q.line[1:]
continue
}
- b := q.line[0]
+ q.skipWhite = false
+
switch {
case b == '=':
b, err = q.readHexByte(q.line[1:])
@@ -80,7 +116,9 @@ func (q *qpReader) Read(p []byte) (n int, err error) {
return n, err
}\n \t\t\tq.line = q.line[2:] // 2 of the 3; other 1 is done below
-\t\tcase b != '\\t' && (b < ' ' || b > '~'):
+\t\tcase b == '\\t' || b == '\\r' || b == '\\n':
+\t\t\tbreak
+\t\tcase b < ' ' || b > '~':
\t\t\treturn n, fmt.Errorf(\"multipart: invalid unescaped byte 0x%02x in quoted-printable body\", b)\n \t\t}\n \t\tp[0] = b
src/pkg/mime/multipart/quotedprintable_test.go
テストファイルには、新しい挙動を検証するための多数のテストケースが追加されています。
- 空文字列、通常の文字列、エスケープされた文字列のテスト。
=\n
をソフト改行として扱うテスト。- エスケープされていない
\n
や\r
がそのまま通過するテスト。 - 行末の空白文字の処理に関するテスト。
=
の後に不正なバイトが続く場合のテスト。TestQPExhaustive
という網羅的なテストが追加され、qprint
コマンドラインツールとの比較も行われています。これは、様々な組み合わせの入力に対してデコーダの挙動を検証し、既存のツールとの互換性を確認するためのものです。
コアとなるコードの解説
quotedprintable.go
の変更は、qpReader
の Read
メソッドが Quoted-Printable エンコードされたデータをどのように処理するかを根本的に変更しています。
-
qpReader
構造体の拡張:skipWhite
フィールドの追加は、デコード処理の柔軟性を高めるためのものです。行の先頭にある空白文字を条件付きでスキップすることで、より多様な入力形式に対応できます。 -
ソフト改行の処理:
- 以前は
if len(q.line) == 1 && q.line[0] == '='
という非常に厳密な条件でソフト改行を検出していました。これは、=
の直後に改行が続く場合にのみソフト改行と見なすものでした。 - 新しいロジックでは、
bytes.HasSuffix(q.line, softSuffix)
(行が=
で終わる) をチェックし、さらにrightStripped
(行末の=
の後の部分) がlf
(LF) またはcrlf
(CRLF) で始まらない場合にエラーを発生させることで、より堅牢なソフト改行の検出を行います。特に=\n
をソフト改行として許容するようになった点が重要です。
- 以前は
-
エスケープされていない改行の通過:
- 最も重要な変更は、
switch
文におけるcase b == '\t' || b == '\r' || b == '\n': break
の追加です。これにより、タブ、キャリッジリターン、ラインフィードがエスケープされていない場合でも、それらを不正なバイトとしてエラーにするのではなく、そのまま出力ストリームに渡すようになりました。 - これはRFC 2045の厳密な解釈からは逸脱しますが、現実世界の多くのQuoted-Printableエンコーダがこのような非標準的な改行を生成するため、Goのデコーダがより実用的なものになります。
- 最も重要な変更は、
これらの変更は、GoのQuoted-Printableデコーダを「より寛容な」デコーダにすることで、より多くの既存のQuoted-Printableエンコードされたコンテンツを処理できるようにすることを目的としています。これは、特に電子メールのMIMEパートを扱う際に、異なるシステム間の互換性を確保するために重要です。
関連リンク
- Go Issue #4771: https://github.com/golang/go/issues/4771 - このコミットが修正したバグの報告。
- Go CL 7300092: https://golang.org/cl/7300092 - このコミットに対応するGoのコードレビューシステム (Gerrit) のチェンジリスト。
参考にした情報源リンク
- RFC 2045 - Multipurpose Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies: https://datatracker.ietf.org/doc/html/rfc2045 - Quoted-Printableエンコーディングの公式仕様。
- Quoted-Printable - Wikipedia: https://en.wikipedia.org/wiki/Quoted-printable - Quoted-Printableエンコーディングに関する一般的な情報。
- Go言語の
mime/multipart
パッケージのドキュメント: https://pkg.go.dev/mime/multipart - Go言語の公式ドキュメント。 - Go言語の
bufio
パッケージのドキュメント: https://pkg.go.dev/bufio - Go言語の公式ドキュメント。 - Go言語の
io
パッケージのドキュメント: https://pkg.go.dev/io - Go言語の公式ドキュメント。 - Go言語の
bytes
パッケージのドキュメント: https://pkg.go.dev/bytes - Go言語の公式ドキュメント。 - Perl
MIME::QuotedPrintable
module: https://metacpan.org/pod/MIME::QuotedPrintable - 他の言語でのQuoted-Printable実装の例。 qprint
command-line tool: Quoted-Printableエンコーディング/デコーディングを行う一般的なUNIXコマンドラインツール。