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

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

このコミットは、Go言語の標準ライブラリ mime パッケージにおける FormatMediaType 関数のシグネチャ変更と、それに伴う内部ロジックの修正を扱っています。具体的には、FormatMediaType 関数がメディアタイプ(例: text/plain)を構成する際に、これまで typesubtype を別々の引数として受け取っていたものを、full type (例: text/plain のような完全な文字列) を単一の引数として受け取るように変更し、一貫性を向上させています。これにより、RFC 2045およびRFC 2616に準拠したメディアタイプのフォーマットがより直感的かつ堅牢になります。

コミット

  • Author: Brad Fitzpatrick bradfitz@golang.org
  • Date: Tue Jan 17 11:57:42 2012 -0800
  • Commit Message:
    mime: make FormatMediaType take full type for consistency
    
    Fixes #2405
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5539048
    

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

https://github.com/golang/go/commit/a00de45bbbd44bf592654dd0811fa4bcb883b1ae

元コミット内容

commit a00de45bbbd44bf592654dd0811fa4bcb883b1ae
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Tue Jan 17 11:57:42 2012 -0800

    mime: make FormatMediaType take full type for consistency
    
    Fixes #2405
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5539048
---
 src/pkg/mime/mediatype.go      | 17 +++++++++++------
 src/pkg/mime/mediatype_test.go | 21 +++++++++++++++++++++
 src/pkg/mime/type.go           | 15 ++++-----------
 3 files changed, 36 insertions(+), 17 deletions(-)

diff --git a/src/pkg/mime/mediatype.go b/src/pkg/mime/mediatype.go
index 2bf79788c7..41844c25f2 100644
--- a/src/pkg/mime/mediatype.go
+++ b/src/pkg/mime/mediatype.go
@@ -12,17 +12,22 @@ import (
 	"unicode"
 )
 
-// FormatMediaType serializes type t, subtype sub and the paramaters
-// param as a media type conform RFC 2045 and RFC 2616.
-// The type, subtype, and parameter names are written in lower-case.
+// FormatMediaType serializes mediatype t and the parameters
+// param as a media type conforming to RFC 2045 and RFC 2616.
+// The type and parameter names are written in lower-case.
 // When any of the arguments result in a standard violation then
 // FormatMediaType returns the empty string.
-func FormatMediaType(t, sub string, param map[string]string) string {
-	if !(IsToken(t) && IsToken(sub)) {
+func FormatMediaType(t string, param map[string]string) string {
+	slash := strings.Index(t, "/")
+	if slash == -1 {
+		return ""
+	}
+	major, sub := t[:slash], t[slash+1:]
+	if !IsToken(major) || !IsToken(sub) {
 		return ""
 	}
 	var b bytes.Buffer
-	b.WriteString(strings.ToLower(t))
+	b.WriteString(strings.ToLower(major))
 	b.WriteByte('/')
 	b.WriteString(strings.ToLower(sub))
 
diff --git a/src/pkg/mime/mediatype_test.go b/src/pkg/mime/mediatype_test.go
index c06f167ddc..64ab291341 100644
--- a/src/pkg/mime/mediatype_test.go
+++ b/src/pkg/mime/mediatype_test.go
@@ -253,3 +253,24 @@ func TestParseMediaTypeBogus(t *testing.T) {
 		t.Errorf("expected invalid media parameter; got error %q", err)
 	}
 }
+
+type formatTest struct {
+	typ    string
+	params map[string]string
+	want   string
+}
+
+var formatTests = []formatTest{
+	{"noslash", nil, ""},
+	{"foo/BAR", nil, "foo/bar"},
+	{"foo/BAR", map[string]string{"X": "Y"}, "foo/bar; x=Y"},
+}
+
+func TestFormatMediaType(t *testing.T) {
+	for i, tt := range formatTests {
+		got := FormatMediaType(tt.typ, tt.params)
+		if got != tt.want {
+			t.Errorf("%d. FormatMediaType(%q, %v) = %q; want %q", i, tt.typ, tt.params, got, tt.want)
+		}
+	}
+}
diff --git a/src/pkg/mime/type.go b/src/pkg/mime/type.go
index e3d968fb81..00cff263ba 100644
--- a/src/pkg/mime/type.go
+++ b/src/pkg/mime/type.go
@@ -62,21 +62,14 @@ func AddExtensionType(ext, typ string) error {
 }
 
 func setExtensionType(extension, mimeType string) error {
-	full, param, err := ParseMediaType(mimeType)
+	_, param, err := ParseMediaType(mimeType)
 	if err != nil {
 		return err
 	}
-	if split := strings.Index(full, "/"); split < 0 {
-		return fmt.Errorf(`mime: malformed MIME type "%s"`, mimeType)
-	} else {
-		main := full[:split]
-		sub := full[split+1:]
-		if main == "text" && param["charset"] == "" {
-			param["charset"] = "utf-8"
-		}
-		mimeType = FormatMediaType(main, sub, param)
+	if strings.HasPrefix(mimeType, "text/") && param["charset"] == "" {
+		param["charset"] = "utf-8"
+		mimeType = FormatMediaType(mimeType, param)
 	}
-
 	mimeLock.Lock()
 	mimeTypes[extension] = mimeType
 	mimeLock.Unlock()

変更の背景

このコミットの背景には、Go言語の mime パッケージにおける FormatMediaType 関数のAPI設計の一貫性の問題がありました。以前の FormatMediaType 関数は、メディアタイプを構成する際に、type (例: text) と subtype (例: plain) を別々の文字列引数として受け取っていました。しかし、メディアタイプは通常 type/subtype の形式で一つの文字列として扱われることが多く、ParseMediaType のような他の関連関数は完全なメディアタイプ文字列を引数として受け取っていました。

この不一致は、開発者が mime パッケージを使用する際に混乱を招く可能性があり、特に ParseMediaType で解析した結果を FormatMediaType に渡すようなシナリオで、typesubtype を手動で分割する必要があるなど、不必要な手間が発生していました。

コミットメッセージにある "Fixes #2405" は、この変更がIssue 2405を解決することを示しています。Issue 2405の内容は直接確認できませんが、おそらく FormatMediaType のAPIの一貫性に関する問題や使いにくさが報告されていたと推測されます。この変更は、APIの統一性を図り、開発者体験を向上させることを目的としています。

前提知識の解説

MIMEタイプ (Media Type)

MIMEタイプ(Multipurpose Internet Mail Extensions Type)は、インターネット上でやり取りされるデータの種類を識別するための標準的な方法です。HTTP通信、電子メール、ファイルシステムなどで広く利用されています。MIMEタイプは通常、type/subtype の形式で表現されます。

  • Type (Major Type): データの一般的なカテゴリを示します。例: text, image, audio, video, application
  • Subtype (Minor Type): 特定のデータ形式を示します。例: plain (text/plain), jpeg (image/jpeg), json (application/json)。
  • Parameters: オプションで、メディアタイプに関する追加情報を提供します。例: charset=utf-8 (text/plain; charset=utf-8)。

RFC 2045 (MIME Part One: Format of Internet Message Bodies)

RFC 2045は、MIMEの基本的な構造とヘッダーフィールドの定義を定めています。特に、Content-Type ヘッダーフィールドの構文と意味について詳細に記述されており、MIMEタイプの type/subtype 構造とパラメータの定義が含まれています。このRFCは、インターネットメッセージのボディがどのように構造化され、異なる種類のデータがどのように表現されるかを規定しています。

RFC 2616 (Hypertext Transfer Protocol -- HTTP/1.1)

RFC 2616は、HTTP/1.1プロトコルの仕様を定義しています。このRFCでは、HTTPヘッダーフィールドの一つである Content-Type の使用方法についても言及しており、MIMEタイプがHTTPメッセージのエンティティボディのメディアタイプを示すためにどのように使われるかを規定しています。FormatMediaType 関数が「RFC 2045およびRFC 2616に準拠」とあるのは、生成されるメディアタイプ文字列がこれらの標準で定義された構文と意味論に従うことを意味します。

Go言語の mime パッケージ

Go言語の標準ライブラリ mime パッケージは、MIMEメディアタイプを解析および生成するための機能を提供します。主な関数には以下のようなものがあります。

  • ParseMediaType(v string) (mediatype string, params map[string]string, err error): メディアタイプ文字列を解析し、主要なメディアタイプとパラメータのマップを返します。
  • FormatMediaType(mediatype string, params map[string]string) string: メディアタイプとパラメータのマップからメディアタイプ文字列を生成します。
  • AddExtensionType(ext, typ string) error: ファイル拡張子とMIMEタイプのマッピングを追加します。
  • TypeByExtension(ext string) string: ファイル拡張子に対応するMIMEタイプを返します。

このコミットは、特に FormatMediaType のAPIと内部実装に焦点を当てています。

技術的詳細

このコミットの主要な技術的変更点は、mime.FormatMediaType 関数のシグネチャ変更です。

変更前: func FormatMediaType(t, sub string, param map[string]string) string

  • t: メディアの主要タイプ (例: "text")
  • sub: メディアのサブタイプ (例: "plain")
  • param: パラメータのマップ

変更後: func FormatMediaType(t string, param map[string]string) string

  • t: 完全なメディアタイプ文字列 (例: "text/plain")
  • param: パラメータのマップ

この変更により、FormatMediaTypeParseMediaType と同様に、type/subtype 形式の完全なメディアタイプ文字列を単一の引数として受け取るようになりました。これにより、APIの一貫性が向上し、開発者はメディアタイプを扱う際に typesubtype を個別に管理する手間が省けます。

内部的には、新しい FormatMediaType 関数は、受け取った完全なメディアタイプ文字列 t/ で分割して major (主要タイプ) と sub (サブタイプ) を抽出し、それぞれが有効なトークンであるか (IsToken 関数でチェック) を検証します。この検証に失敗した場合や / が存在しない場合は、空文字列を返します。その後、majorsub を小文字に変換して / で結合し、パラメータを追加して最終的なメディアタイプ文字列を構築します。

また、src/pkg/mime/type.go 内の setExtensionType 関数も、この FormatMediaType のシグネチャ変更に合わせて修正されています。以前は ParseMediaType の結果から full (完全なメディアタイプ文字列) を取得し、それをさらに / で分割して mainsub を抽出し、FormatMediaType(main, sub, param) を呼び出していました。変更後は、ParseMediaType の結果から full を取得する必要がなくなり、直接 FormatMediaType(mimeType, param) を呼び出すことで、より簡潔なコードになっています。特に mimeTypetext/ で始まる場合に charset=utf-8 を追加するロジックも、新しい FormatMediaType の呼び出し方に合わせて調整されています。

この変更は、mime パッケージの内部的な整合性を高め、外部APIの使いやすさを向上させるための重要なリファクタリングと言えます。

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

このコミットでは、以下の3つのファイルが変更されています。

  1. src/pkg/mime/mediatype.go:

    • FormatMediaType 関数のシグネチャが変更されました。
      • 変更前: func FormatMediaType(t, sub string, param map[string]string) string
      • 変更後: func FormatMediaType(t string, param map[string]string) string
    • 関数内部で、新しい t (完全なメディアタイプ文字列) から majorsub を抽出し、それらのトークン検証を行うロジックが追加されました。
    • 文字列の結合部分で、strings.ToLower(t)strings.ToLower(major) に変更されました。
  2. src/pkg/mime/mediatype_test.go:

    • TestFormatMediaType という新しいテスト関数が追加されました。
    • formatTest という新しい構造体が定義され、テストケースを構造化しています。
    • formatTests というテストケースのスライスが定義され、FormatMediaType の新しいシグネチャに対応するテストデータが含まれています。これには、スラッシュがない場合の無効な入力、有効な入力、パラメータ付きの有効な入力などが含まれます。
  3. src/pkg/mime/type.go:

    • setExtensionType 関数内の FormatMediaType の呼び出し方が変更されました。
    • 変更前は FormatMediaType(main, sub, param) のように mainsub を個別に渡していましたが、変更後は FormatMediaType(mimeType, param) のように完全な mimeType 文字列を渡すようになりました。
    • ParseMediaType の戻り値で full 変数を受け取る必要がなくなり、_ で無視されるようになりました。
    • full から mainsub を抽出するロジックが削除されました。

コアとなるコードの解説

src/pkg/mime/mediatype.go の変更

// FormatMediaType serializes mediatype t and the parameters
// param as a media type conforming to RFC 2045 and RFC 2616.
// The type and parameter names are written in lower-case.
// When any of the arguments result in a standard violation then
// FormatMediaType returns the empty string.
func FormatMediaType(t string, param map[string]string) string {
	slash := strings.Index(t, "/") // (1) スラッシュの位置を検索
	if slash == -1 {               // (2) スラッシュがない場合は無効なメディアタイプとして空文字列を返す
		return ""
	}
	major, sub := t[:slash], t[slash+1:] // (3) スラッシュで主要タイプとサブタイプを分割
	if !IsToken(major) || !IsToken(sub) { // (4) 主要タイプまたはサブタイプが有効なトークンでない場合は空文字列を返す
		return ""
	}
	var b bytes.Buffer
	b.WriteString(strings.ToLower(major)) // (5) 主要タイプを小文字で書き込む
	b.WriteByte('/')                      // (6) スラッシュを書き込む
	b.WriteString(strings.ToLower(sub))   // (7) サブタイプを小文字で書き込む
	// ... (パラメータの処理は省略)
}
  1. strings.Index(t, "/"): 入力された完全なメディアタイプ文字列 t の中で、スラッシュ (/) の位置を探します。
  2. if slash == -1: スラッシュが見つからない場合、それは有効な type/subtype 形式ではないため、空文字列を返してエラーを示します。
  3. major, sub := t[:slash], t[slash+1:]: スラッシュの位置を基準に、文字列 tmajor (主要タイプ) と sub (サブタイプ) に分割します。
  4. if !IsToken(major) || !IsToken(sub): IsToken 関数は、RFCで定義された「トークン」の規則(特定の文字セットのみを含む)に文字列が準拠しているかを確認します。主要タイプまたはサブタイプが有効なトークンでない場合、これも無効なメディアタイプとして空文字列を返します。
  5. b.WriteString(strings.ToLower(major)): bytes.Buffer を使用して文字列を効率的に構築します。主要タイプを小文字に変換してバッファに書き込みます。RFCではメディアタイプの大文字・小文字は区別されないため、慣例的に小文字が使用されます。
  6. b.WriteByte('/'): スラッシュを書き込みます。
  7. b.WriteString(strings.ToLower(sub)): サブタイプを小文字に変換してバッファに書き込みます。

この変更により、FormatMediaType は入力の検証を強化し、より堅牢なメディアタイプ文字列の生成を保証します。

src/pkg/mime/type.go の変更

func setExtensionType(extension, mimeType string) error {
	_, param, err := ParseMediaType(mimeType) // (1) ParseMediaTypeの戻り値からfullを無視
	if err != nil {
		return err
	}
	// 変更前:
	// if split := strings.Index(full, "/"); split < 0 { ... } else {
	//     main := full[:split]
	//     sub := full[split+1:]
	//     if main == "text" && param["charset"] == "" { ... }
	//     mimeType = FormatMediaType(main, sub, param)
	// }

	// 変更後:
	if strings.HasPrefix(mimeType, "text/") && param["charset"] == "" { // (2) mimeTypeが"text/"で始まるかチェック
		param["charset"] = "utf-8"
		mimeType = FormatMediaType(mimeType, param) // (3) 新しいFormatMediaTypeのシグネチャで呼び出し
	}
	// ... (ロックとマップへの書き込みは省略)
}
  1. _, param, err := ParseMediaType(mimeType): ParseMediaType の最初の戻り値である完全なメディアタイプ文字列 (full) は、もはや直接使用されないため、_ で無視されます。必要なのはパラメータ param とエラー err のみです。
  2. if strings.HasPrefix(mimeType, "text/"): 以前は main == "text" とチェックしていましたが、新しい FormatMediaType のシグネチャに合わせて、入力された mimeTypetext/ で始まるかどうかを直接チェックするようになりました。これにより、text タイプのメディアに対して charset=utf-8 を自動的に追加するロジックが維持されます。
  3. mimeType = FormatMediaType(mimeType, param): ここが最も重要な変更点です。以前は ParseMediaType で分割した mainsub を個別に FormatMediaType に渡していましたが、新しい FormatMediaType のシグネチャに合わせて、元の完全な mimeType 文字列と param を直接渡すようになりました。これにより、コードが簡潔になり、APIの一貫性が保たれます。

これらの変更は、mime パッケージの内部的な整合性を高め、FormatMediaType のAPIをより直感的で使いやすいものにすることを目的としています。

関連リンク

  • Go CL (Code Review): https://golang.org/cl/5539048
  • Go Issue: Fixes #2405 (このコミットが解決したIssueの番号)

参考にした情報源リンク