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

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

このコミットは、Go言語の標準ライブラリ mime/multipart パッケージにおいて、Writer が使用するマルチパートの境界文字列(boundary string)を外部から設定できるようにする機能を追加するものです。これにより、ランダムに生成されるデフォルトの境界ではなく、ユーザーが指定した境界を使用できるようになります。

コミット

commit 575de93dd395481abeaf9427e04fc83b758dec0e
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Mon Dec 10 16:30:42 2012 -0500

    mime/multipart: allow setting the Writer boundary
    
    Fixes #4490
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6924044

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

https://github.com/golang/go/commit/575de93dd395481abeaf9427e04fc83b758dec0e

元コミット内容

mime/multipart: allow setting the Writer boundary

Fixes #4490

R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6924044

変更の背景

この変更の背景には、Go言語の mime/multipart パッケージが提供する Writer が、マルチパートメッセージの境界文字列を自動的にランダム生成していたという既存の挙動があります。通常、このランダム生成はセキュリティ上の理由(境界文字列がメッセージ内容と衝突する可能性を低減するため)から望ましいとされています。

しかし、特定のシナリオでは、ユーザーが境界文字列を明示的に制御する必要が生じます。例えば、以下のようなケースが考えられます。

  1. 特定のAPIとの互換性: 一部のレガシーなシステムや厳格な仕様を持つAPIでは、マルチパートリクエストの境界文字列が固定値であるか、特定のパターンに従うことを要求する場合があります。ランダムな境界ではこれらのシステムと連携できないため、手動で設定する機能が必要でした。
  2. デバッグとテストの容易性: デバッグ時やテストコードを書く際に、予測可能な固定の境界文字列を使用できると、生成されるマルチパートメッセージの内容を検証しやすくなります。ランダムな境界では、毎回異なる出力が生成されるため、テストの安定性やデバッグの効率が低下する可能性があります。
  3. パフォーマンス最適化: 非常に稀なケースですが、特定の環境下で境界文字列の生成オーバーヘッドを避けたい場合や、既知の短い境界文字列を使用することでわずかながらパフォーマンスを改善したいという要求があるかもしれません。

このコミットメッセージにある "Fixes #4490" は、GoのIssueトラッカーにおける特定のバグ報告や機能要望に対応していることを示しています。このIssue(Go issue 4490)の内容は、まさに上記のような理由から、mime/multipart.Writer の境界を設定可能にすることの必要性を訴えるものであったと推測されます。

前提知識の解説

MIME (Multipurpose Internet Mail Extensions)

MIMEは、電子メールがASCII文字しか扱えなかった時代に、非ASCII文字(日本語など)、画像、音声、動画などのバイナリデータ、複数のファイルなどを電子メールで送受信できるようにするために開発された標準です。HTTPなどの他のプロトコルでも広く利用されています。

multipart/form-data

multipart/form-data は、HTTPリクエストの Content-Type ヘッダで指定されるMIMEタイプの一つで、主にWebフォームからファイルアップロードを行う際に使用されます。このMIMEタイプは、単一のHTTPリクエストボディ内に複数の異なる種類のデータをカプセル化するために設計されています。

各データパートは、特定の「境界文字列(boundary string)」によって区切られます。この境界文字列は、リクエストボディ内でデータとデータの区切りを示すマーカーとして機能します。

マルチパートメッセージの構造

multipart/form-data メッセージの基本的な構造は以下のようになります。

Content-Type: multipart/form-data; boundary=--------------------------1234567890abcdef

--------------------------1234567890abcdef
Content-Disposition: form-data; name="field1"

value1
--------------------------1234567890abcdef
Content-Disposition: form-data; name="file1"; filename="example.txt"
Content-Type: text/plain

This is the content of example.txt.
--------------------------1234567890abcdef--
  • Content-Type ヘッダには、multipart/form-data と、その後に boundary パラメータが続きます。この boundary パラメータの値が、メッセージ内の各パートを区切る文字列になります。
  • 各パートは、-- と境界文字列で始まります。
  • 各パートには、そのパートのデータに関する情報(例: Content-DispositionContent-Type)を含むヘッダが続きます。
  • ヘッダの後に空行が続き、その後に実際のデータが続きます。
  • メッセージの最後のパートは、-- と境界文字列の後にさらに -- が付加された終端境界で閉じられます。

境界文字列(Boundary String)の役割と要件

境界文字列は、メッセージ内のどのデータとも衝突しないように、十分にユニークである必要があります。もし境界文字列がデータ内容の一部として現れてしまうと、受信側がメッセージの構造を正しく解析できなくなります。このため、通常はランダムな文字列が使用されます。

RFC 2046 (MIME Part Two: Media Types) のセクション 5.1.1 では、境界文字列に関する以下の要件が定義されています。

  • 境界文字列は、70文字以内である必要があります。
  • 境界文字列は、ASCII文字セットの特定のサブセットのみを含むことができます。具体的には、英数字(A-Z, a-z, 0-9)と、以下の特殊文字のみが許可されます: ' ( ) + _ , - . / : = ?
  • 境界文字列は、メッセージのどの部分にも出現しないことが保証されるべきです。

Goの mime/multipart パッケージの Writer は、これらの要件を満たすように境界文字列を生成・検証します。

技術的詳細

このコミットは、src/pkg/mime/multipart/writer.goSetBoundary メソッドを追加し、src/pkg/mime/multipart/writer_test.go にそのテストケースを追加しています。

SetBoundary メソッドの追加

Writer 構造体には、マルチパートメッセージの境界文字列を保持する boundary フィールドがあります。これまでは、NewWriter 関数が Writer を初期化する際に、この boundary フィールドにランダムな文字列を生成して設定していました。

追加された SetBoundary(boundary string) error メソッドは、以下のロジックで動作します。

  1. 書き込み後の呼び出しチェック: if w.lastpart != nil という条件で、既に何らかのパートが書き込まれているかどうかをチェックします。lastpart は、Writer が最後に書き込んだパートを追跡するための内部フィールドです。もし既にパートが書き込まれている場合、境界文字列を変更することはメッセージの整合性を破壊するため、errors.New("mime: SetBoundary called after write") というエラーを返します。これは、境界文字列がメッセージのヘッダ(Content-Type)とボディの両方で使用されるため、途中で変更できないというMIMEの仕様に則った重要な制約です。

  2. 境界文字列の長さ検証: if len(boundary) < 1 || len(boundary) > 69 という条件で、RFC 2046 で定義されている境界文字列の長さ要件を検証します。境界文字列は1文字以上69文字以下である必要があります。これに違反する場合、errors.New("mime: invalid boundary length") というエラーを返します。

  3. 境界文字列の文字検証: for _, b := range boundary ループで、入力された boundary 文字列の各文字がRFC 2046 で許可されている文字セットに含まれているかを検証します。

    • まず、英数字(A-Z, a-z, 0-9)であるかをチェックします。
    • 次に、' ( ) + _ , - . / : = ? のいずれかの特殊文字であるかを switch ステートメントでチェックします。
    • これらのいずれにも該当しない文字が含まれている場合、errors.New("mime: invalid boundary character") というエラーを返します。
  4. 境界文字列の設定: 上記のすべての検証をパスした場合、入力された boundary 文字列を w.boundary フィールドに設定し、nil エラーを返します。

Boundary() メソッドのコメント修正

既存の Boundary() メソッドのコメントが、"Boundary returns the Writer's randomly selected boundary string." から "Boundary returns the Writer's boundary." に修正されています。これは、SetBoundary メソッドの導入により、境界文字列が必ずしもランダムに選択されたものではなくなったことを反映するためです。

テストケースの追加 (writer_test.go)

TestWriterSetBoundary という新しいテスト関数が追加され、SetBoundary メソッドの挙動が検証されています。

  • 有効な境界と無効な境界のテスト: tests スライスには、有効な境界文字列(例: "abc", "my-separator", 69文字の'x'の繰り返し)と無効な境界文字列(例: "", "ungültig" (非ASCII文字), "!" (不正な特殊文字), 70文字の'x'の繰り返し, "bad!ascii!" (不正な特殊文字))のペアが定義されています。 各テストケースに対して SetBoundary を呼び出し、期待されるエラーの有無(tt.ok)と実際のエラーの有無(got)を比較しています。

  • 設定された境界の検証: tt.oktrue(つまり、SetBoundary が成功した場合)のテストケースでは、w.Boundary() を呼び出して、設定した境界文字列が正しく取得できるかどうかも検証しています。

  • 書き込み後の SetBoundary 呼び出しのテスト: テストコードには明示的に書かれていませんが、SetBoundaryw.lastpart != nil の場合にエラーを返すロジックは、既存の TestWriter のような、実際にパートを書き込むテストケースと組み合わせることで間接的に検証されるか、あるいはこのコミットの範囲外で別途テストされることが期待されます。

  • 最終的な出力の検証: テストの最後に、w.Close() を呼び出してマルチパートメッセージを完成させ、bytes.Buffer に書き込まれた内容が、設定した境界文字列(例: "my-separator")を含む終端境界(\r\n--my-separator--\r\n)を持っていることを strings.Contains で確認しています。これにより、SetBoundary で設定した境界が実際にメッセージの生成に使用されていることが検証されます。

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

src/pkg/mime/multipart/writer.go

--- a/src/pkg/mime/multipart/writer.go
+++ b/src/pkg/mime/multipart/writer.go
@@ -30,11 +30,38 @@ func NewWriter(w io.Writer) *Writer {
 	}
 }
 
-// Boundary returns the Writer's randomly selected boundary string.
+// Boundary returns the Writer's boundary.
 func (w *Writer) Boundary() string {
 	return w.boundary
 }
 
+// SetBoundary overrides the Writer's default randomly-generated
+// boundary separator with an explicit value.
+//
+// SetBoundary must be called before any parts are created, may only
+// contain certain ASCII characters, and must be 1-69 bytes long.
+func (w *Writer) SetBoundary(boundary string) error {
+	if w.lastpart != nil {
+		return errors.New("mime: SetBoundary called after write")
+	}
+	// rfc2046#section-5.1.1
+	if len(boundary) < 1 || len(boundary) > 69 {
+		return errors.New("mime: invalid boundary length")
+	}
+	for _, b := range boundary {
+		if 'A' <= b && b <= 'Z' || 'a' <= b && b <= 'z' || '0' <= b && b <= '9' {
+			continue
+		}
+		switch b {
+		case '\'', '(', ')', '+', '_', ',', '-', '.', '/', ':', '=', '?':
+			continue
+		}
+		return errors.New("mime: invalid boundary character")
+	}
+	w.boundary = boundary
+	return nil
+}
+
 // FormDataContentType returns the Content-Type for an HTTP
 // multipart/form-data with this Writer's Boundary.
 func (w *Writer) FormDataContentType() string {

src/pkg/mime/multipart/writer_test.go

--- a/src/pkg/mime/multipart/writer_test.go
+++ b/src/pkg/mime/multipart/writer_test.go
@@ -7,6 +7,7 @@ package multipart
 import (
 	"bytes"
 	"io/ioutil"
+	"strings"
 	"testing"
 )
 
@@ -76,3 +77,37 @@ func TestWriter(t *testing.T) {
 		t.Fatalf("expected end of parts; got %v, %v", part, err)
 	}
 }
+
+func TestWriterSetBoundary(t *testing.T) {
+	var b bytes.Buffer
+	w := NewWriter(&b)
+	tests := []struct {
+		b  string
+		ok bool
+	}{
+		{"abc", true},
+		{"", false},
+		{"ungültig", false},
+		{"!", false},
+		{strings.Repeat("x", 69), true},
+		{strings.Repeat("x", 70), false},
+		{"bad!ascii!", false},
+		{"my-separator", true},
+	}
+	for i, tt := range tests {
+		err := w.SetBoundary(tt.b)
+		got := err == nil
+		if got != tt.ok {
+			t.Errorf("%d. boundary %q = %v (%v); want %v", i, tt.b, got, err, tt.ok)
+		} else if tt.ok {
+			got := w.Boundary()
+			if got != tt.b {
+				t.Errorf("boundary = %q; want %q", got, tt.b)
+			}
+		}
+	}
+	w.Close()
+	if got := b.String(); !strings.Contains(got, "\r\n--my-separator--\r\n") {
+		t.Errorf("expected my-separator in output. got: %q", got)
+	}
+}

コアとなるコードの解説

writer.go の変更点

  1. Boundary() メソッドのコメント修正: Boundary() メソッドのドキュメンテーションコメントが変更されました。以前は「ランダムに選択された境界文字列を返す」と記述されていましたが、SetBoundary メソッドの導入により、境界がユーザーによって明示的に設定される可能性が生じたため、「Writerの境界を返す」というより一般的な表現に修正されました。これは、APIの振る舞いの変化を正確に反映するための重要なドキュメンテーションの更新です。

  2. SetBoundary メソッドの追加: このコミットの主要な変更点です。

    • シグネチャ: func (w *Writer) SetBoundary(boundary string) error Writer 型のメソッドとして定義され、設定したい境界文字列を string 型で受け取り、エラーを返します。
    • 事前条件チェック: if w.lastpart != nil このチェックは、SetBoundaryWriter を通じてデータが書き込まれる前に呼び出されることを保証します。一度でもパートが書き込まれると、Content-Type ヘッダが既に送信されている可能性があり、その後に境界を変更することはプロトコル違反となるため、エラーを返して早期に処理を終了させます。
    • RFC 2046 準拠のバリデーション: if len(boundary) < 1 || len(boundary) > 69 for _, b := range boundary これらのコードは、MIMEの仕様(RFC 2046, Section 5.1.1)に厳密に従い、境界文字列の長さと含まれる文字の有効性を検証します。
      • 長さは1文字以上69文字以下である必要があります。
      • 使用できる文字は、英数字と特定の記号(', (, ), +, _, ,, -, ., /, :, =, ?)に限定されます。 これらのバリデーションは、生成されるマルチパートメッセージがMIME標準に準拠し、他のシステムで正しく解析されることを保証するために不可欠です。無効な境界文字列が設定されることを防ぎ、潜在的な互換性問題やセキュリティリスクを回避します。
    • 境界の設定: w.boundary = boundary すべてのバリデーションをパスした場合、入力された boundary 文字列が Writer の内部 boundary フィールドに設定されます。これにより、以降のマルチパートメッセージ生成において、このカスタム境界が使用されるようになります。

writer_test.go の変更点

  1. strings パッケージのインポート: 新しいテスト関数 TestWriterSetBoundarystrings.Repeatstrings.Contains を使用するため、"strings" パッケージがインポートされています。

  2. TestWriterSetBoundary 関数の追加: このテスト関数は、SetBoundary メソッドの機能とエラーハンドリングを包括的に検証します。

    • テストケースの定義: tests スライスは、様々な有効および無効な境界文字列の組み合わせを定義しています。これにより、正常系だけでなく、長さ制限や文字制限に違反する異常系のケースも網羅的にテストされます。
    • ループによるテスト実行: 各テストケースに対して SetBoundary を呼び出し、返されたエラーが期待通りであるか(err == niltt.ok の比較)を検証します。
    • 成功時の境界値の検証: SetBoundary が成功した場合(tt.oktrue)、w.Boundary() を呼び出して、実際に設定された境界文字列が期待値と一致するかを確認します。これは、SetBoundary が単にエラーを返さないだけでなく、正しく内部状態を更新していることを保証します。
    • 最終的な出力の検証: テストの最後に、w.Close() を呼び出してマルチパートメッセージを終了させ、bytes.Buffer に書き込まれた最終的な出力文字列が、設定した境界文字列を含む終端境界(\r\n--my-separator--\r\n)を持っていることを確認します。これは、SetBoundary で設定された境界が、実際に生成されるマルチパートメッセージのフォーマットに反映されていることをエンドツーエンドで検証する重要なステップです。

これらの変更により、mime/multipart.Writer はより柔軟になり、特定の要件を持つアプリケーションやデバッグシナリオにおいて、マルチパートメッセージの境界文字列をより細かく制御できるようになりました。

関連リンク

参考にした情報源リンク