[インデックス 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
が、マルチパートメッセージの境界文字列を自動的にランダム生成していたという既存の挙動があります。通常、このランダム生成はセキュリティ上の理由(境界文字列がメッセージ内容と衝突する可能性を低減するため)から望ましいとされています。
しかし、特定のシナリオでは、ユーザーが境界文字列を明示的に制御する必要が生じます。例えば、以下のようなケースが考えられます。
- 特定のAPIとの互換性: 一部のレガシーなシステムや厳格な仕様を持つAPIでは、マルチパートリクエストの境界文字列が固定値であるか、特定のパターンに従うことを要求する場合があります。ランダムな境界ではこれらのシステムと連携できないため、手動で設定する機能が必要でした。
- デバッグとテストの容易性: デバッグ時やテストコードを書く際に、予測可能な固定の境界文字列を使用できると、生成されるマルチパートメッセージの内容を検証しやすくなります。ランダムな境界では、毎回異なる出力が生成されるため、テストの安定性やデバッグの効率が低下する可能性があります。
- パフォーマンス最適化: 非常に稀なケースですが、特定の環境下で境界文字列の生成オーバーヘッドを避けたい場合や、既知の短い境界文字列を使用することでわずかながらパフォーマンスを改善したいという要求があるかもしれません。
このコミットメッセージにある "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-Disposition
、Content-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.go
に SetBoundary
メソッドを追加し、src/pkg/mime/multipart/writer_test.go
にそのテストケースを追加しています。
SetBoundary
メソッドの追加
Writer
構造体には、マルチパートメッセージの境界文字列を保持する boundary
フィールドがあります。これまでは、NewWriter
関数が Writer
を初期化する際に、この boundary
フィールドにランダムな文字列を生成して設定していました。
追加された SetBoundary(boundary string) error
メソッドは、以下のロジックで動作します。
-
書き込み後の呼び出しチェック:
if w.lastpart != nil
という条件で、既に何らかのパートが書き込まれているかどうかをチェックします。lastpart
は、Writer
が最後に書き込んだパートを追跡するための内部フィールドです。もし既にパートが書き込まれている場合、境界文字列を変更することはメッセージの整合性を破壊するため、errors.New("mime: SetBoundary called after write")
というエラーを返します。これは、境界文字列がメッセージのヘッダ(Content-Type
)とボディの両方で使用されるため、途中で変更できないというMIMEの仕様に則った重要な制約です。 -
境界文字列の長さ検証:
if len(boundary) < 1 || len(boundary) > 69
という条件で、RFC 2046 で定義されている境界文字列の長さ要件を検証します。境界文字列は1文字以上69文字以下である必要があります。これに違反する場合、errors.New("mime: invalid boundary length")
というエラーを返します。 -
境界文字列の文字検証:
for _, b := range boundary
ループで、入力されたboundary
文字列の各文字がRFC 2046 で許可されている文字セットに含まれているかを検証します。- まず、英数字(
A-Z
,a-z
,0-9
)であるかをチェックします。 - 次に、
'
(
)
+
_
,
-
.
/
:
=
?
のいずれかの特殊文字であるかをswitch
ステートメントでチェックします。 - これらのいずれにも該当しない文字が含まれている場合、
errors.New("mime: invalid boundary character")
というエラーを返します。
- まず、英数字(
-
境界文字列の設定: 上記のすべての検証をパスした場合、入力された
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.ok
がtrue
(つまり、SetBoundary
が成功した場合)のテストケースでは、w.Boundary()
を呼び出して、設定した境界文字列が正しく取得できるかどうかも検証しています。 -
書き込み後の
SetBoundary
呼び出しのテスト: テストコードには明示的に書かれていませんが、SetBoundary
がw.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
の変更点
-
Boundary()
メソッドのコメント修正:Boundary()
メソッドのドキュメンテーションコメントが変更されました。以前は「ランダムに選択された境界文字列を返す」と記述されていましたが、SetBoundary
メソッドの導入により、境界がユーザーによって明示的に設定される可能性が生じたため、「Writerの境界を返す」というより一般的な表現に修正されました。これは、APIの振る舞いの変化を正確に反映するための重要なドキュメンテーションの更新です。 -
SetBoundary
メソッドの追加: このコミットの主要な変更点です。- シグネチャ:
func (w *Writer) SetBoundary(boundary string) error
Writer
型のメソッドとして定義され、設定したい境界文字列をstring
型で受け取り、エラーを返します。 - 事前条件チェック:
if w.lastpart != nil
このチェックは、SetBoundary
がWriter
を通じてデータが書き込まれる前に呼び出されることを保証します。一度でもパートが書き込まれると、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
の変更点
-
strings
パッケージのインポート: 新しいテスト関数TestWriterSetBoundary
でstrings.Repeat
やstrings.Contains
を使用するため、"strings"
パッケージがインポートされています。 -
TestWriterSetBoundary
関数の追加: このテスト関数は、SetBoundary
メソッドの機能とエラーハンドリングを包括的に検証します。- テストケースの定義:
tests
スライスは、様々な有効および無効な境界文字列の組み合わせを定義しています。これにより、正常系だけでなく、長さ制限や文字制限に違反する異常系のケースも網羅的にテストされます。 - ループによるテスト実行: 各テストケースに対して
SetBoundary
を呼び出し、返されたエラーが期待通りであるか(err == nil
とtt.ok
の比較)を検証します。 - 成功時の境界値の検証:
SetBoundary
が成功した場合(tt.ok
がtrue
)、w.Boundary()
を呼び出して、実際に設定された境界文字列が期待値と一致するかを確認します。これは、SetBoundary
が単にエラーを返さないだけでなく、正しく内部状態を更新していることを保証します。 - 最終的な出力の検証: テストの最後に、
w.Close()
を呼び出してマルチパートメッセージを終了させ、bytes.Buffer
に書き込まれた最終的な出力文字列が、設定した境界文字列を含む終端境界(\r\n--my-separator--\r\n
)を持っていることを確認します。これは、SetBoundary
で設定された境界が、実際に生成されるマルチパートメッセージのフォーマットに反映されていることをエンドツーエンドで検証する重要なステップです。
- テストケースの定義:
これらの変更により、mime/multipart.Writer
はより柔軟になり、特定の要件を持つアプリケーションやデバッグシナリオにおいて、マルチパートメッセージの境界文字列をより細かく制御できるようになりました。
関連リンク
- Go CL 6924044: https://golang.org/cl/6924044
- Go Issue 4490: https://github.com/golang/go/issues/4490 (コミットメッセージで参照されているIssue)
参考にした情報源リンク
- RFC 2046 - MIME Part Two: Media Types: https://datatracker.ietf.org/doc/html/rfc2046#section-5.1.1 (Boundaryに関する仕様)
- Go Documentation -
mime/multipart
package: https://pkg.go.dev/mime/multipart - MDN Web Docs -
Content-Type: multipart/form-data
: https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Type/multipart/form-data