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

[インデックス 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::QuotedPrintableqprint コマンドラインツールなど)の挙動に合わせることで、より多くの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= =WorldHelloWorld とデコードされます。
  • ハード改行 (Hard Line Break): エンコードされたデータの一部として改行を含める場合は、\r\n (CRLF) をそのまま使用します。これはエスケープされません。

bufio.Readerio.Reader

  • io.Reader: Go言語における基本的な入力インターフェースです。Read(p []byte) (n int, err error) メソッドを持ち、データをバイトスライス p に読み込み、読み込んだバイト数 n とエラー err を返します。
  • bufio.Reader: io.Reader をラップし、バッファリング機能を追加することで、より効率的な読み込みを可能にする構造体です。特に、ReadSliceReadLine のような行指向の読み込みメソッドを提供し、基になる io.Reader からの読み込み回数を減らすことでパフォーマンスを向上させます。

Go言語のエラーハンドリング

Go言語では、エラーは戻り値として明示的に扱われます。関数は通常、最後の戻り値として error 型の値を返します。nil はエラーがないことを意味し、非nil の値はエラーが発生したことを示します。

技術的詳細

このコミットは、src/pkg/mime/multipart/quotedprintable.go 内の qpReader 構造体とその Read メソッドに焦点を当てています。主な変更点は以下の通りです。

  1. qpReader 構造体の変更:

    • skipWhite bool フィールドが追加されました。これは、行の先頭で空白文字(スペースやタブ)をスキップするかどうかを制御するために使用されます。
  2. newQuotedPrintableReader 関数の変更:

    • qpReader の初期化時に skipWhitetrue に設定するようになりました。これにより、デコード処理の開始時に先頭の空白がスキップされるようになります。
  3. isQPSkipWhiteByte 関数の追加:

    • byte がスペース ( ) またはタブ (\t) であるかを判定するヘルパー関数が追加されました。
  4. 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 の変更は、qpReaderRead メソッドが Quoted-Printable エンコードされたデータをどのように処理するかを根本的に変更しています。

  1. qpReader 構造体の拡張: skipWhite フィールドの追加は、デコード処理の柔軟性を高めるためのものです。行の先頭にある空白文字を条件付きでスキップすることで、より多様な入力形式に対応できます。

  2. ソフト改行の処理:

    • 以前は if len(q.line) == 1 && q.line[0] == '=' という非常に厳密な条件でソフト改行を検出していました。これは、= の直後に改行が続く場合にのみソフト改行と見なすものでした。
    • 新しいロジックでは、bytes.HasSuffix(q.line, softSuffix) (行が = で終わる) をチェックし、さらに rightStripped (行末の = の後の部分) が lf (LF) または crlf (CRLF) で始まらない場合にエラーを発生させることで、より堅牢なソフト改行の検出を行います。特に =\n をソフト改行として許容するようになった点が重要です。
  3. エスケープされていない改行の通過:

    • 最も重要な変更は、switch 文における case b == '\t' || b == '\r' || b == '\n': break の追加です。これにより、タブ、キャリッジリターン、ラインフィードがエスケープされていない場合でも、それらを不正なバイトとしてエラーにするのではなく、そのまま出力ストリームに渡すようになりました。
    • これはRFC 2045の厳密な解釈からは逸脱しますが、現実世界の多くのQuoted-Printableエンコーダがこのような非標準的な改行を生成するため、Goのデコーダがより実用的なものになります。

これらの変更は、GoのQuoted-Printableデコーダを「より寛容な」デコーダにすることで、より多くの既存のQuoted-Printableエンコードされたコンテンツを処理できるようにすることを目的としています。これは、特に電子メールのMIMEパートを扱う際に、異なるシステム間の互換性を確保するために重要です。

関連リンク

参考にした情報源リンク