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

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

このコミットは、Go言語の標準ライブラリである net/url パッケージにおけるURLのパース処理のバグ修正に関するものです。具体的には、file スキームを持つURL(例: file:///foo)が正しくパースされず、Path フィールドに余分なスラッシュが含まれてしまう問題を解決します。これにより、URLのパースと文字列化の挙動がよりRFCに準拠したものになります。

コミット

  • Author: Andrew Gerrand adg@golang.org
  • Date: Wed Jan 23 11:37:06 2013 +1100
  • Commit Message:
    net/url: generate correct Path when hostname empty
    
    Parse("file:///foo") previously returned a URL with Scheme "file"
    and Path "///foo". Now it returns a URL with Path "/foo",
    such that
            &URL{Scheme: "file", Path: "/foo"}.String() == "file:///foo"
    
    This means that parsing and stringifying the URL "file:/foo"
    returns "file:///foo", technically a regression but one that only
    affects a corner case.
    
    Fixes #4189.
    
    R=bradfitz, rsc
    CC=golang-dev
    https://golang.org/cl/7135051
    

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

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

元コミット内容

net/url: generate correct Path when hostname empty

Parse("file:///foo") previously returned a URL with Scheme "file"
and Path "///foo". Now it returns a URL with Path "/foo",
such that
        &URL{Scheme: "file", Path: "/foo"}.String() == "file:///foo"

This means that parsing and stringifying the URL "file:/foo"
returns "file:///foo", technically a regression but one that only
affects a corner case.

Fixes #4189.

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

変更の背景

この変更は、Go言語の net/url パッケージが file スキームのURLを正しく処理できないというバグ(Issue #4189)を修正するために行われました。具体的には、file:///foo のような形式のURLをパースした際に、URL 構造体の Path フィールドが ///foo となってしまい、期待される /foo とは異なる値が設定される問題がありました。

RFC 3986(URI Generic Syntax)では、file スキームのURIは、ホスト名が省略された場合(つまり、ローカルファイルを参照する場合)、パスの前に3つのスラッシュ (///) が続く形式が一般的です。しかし、net/url パッケージの既存の実装では、この3つのスラッシュをパスの一部として誤って解釈していました。

この誤った解釈は、URLのパースと文字列化の間の不整合を引き起こし、特に file スキームを使用するアプリケーションで問題となる可能性がありました。このコミットは、この不整合を解消し、file スキームのURLがRFCの仕様に沿って正しく扱われるようにすることを目的としています。

コミットメッセージには、「file:/foo をパースして文字列化すると file:///foo になる」という「技術的にはリグレッションだが、ごく稀なケースにしか影響しない」という記述があります。これは、file: の後にスラッシュが1つしかない形式は、RFCでは非推奨または未定義の挙動とされており、通常は file:/// の形式が使われるため、実用上の影響は小さいと判断されたことを示唆しています。

前提知識の解説

URL (Uniform Resource Locator)

URLは、インターネット上のリソースの位置を示すための文字列です。一般的なURLの構造は以下のようになります。

scheme://[user:password@]host:port/path?query#fragment

  • Scheme (スキーム): リソースにアクセスするためのプロトコル(例: http, https, ftp, file)。
  • Authority (オーソリティ): ホスト名、ポート番号、ユーザー情報などを含む部分。
    • User Information (ユーザー情報): ユーザー名とパスワード(オプション)。
    • Host (ホスト): リソースが配置されているサーバーのドメイン名またはIPアドレス。
    • Port (ポート): サーバーのポート番号(オプション)。
  • Path (パス): サーバー上のリソースの場所を示す階層的な情報。
  • Query (クエリ): リソースに渡される追加のパラメータ(例: ?key=value)。
  • Fragment (フラグメント): リソース内の特定の部分を指す識別子(例: #section1)。

file スキーム

file スキームは、ローカルファイルシステム上のリソースを参照するために使用されます。その特徴は以下の通りです。

  • ホスト名: ローカルファイルを参照する場合、ホスト名は省略されるか、localhost が使用されます。ホスト名が省略される場合、パスの前に3つのスラッシュ (///) が続くのが一般的です。
    • 例: file:///path/to/file.txt (ホスト名なし)
    • 例: file://localhost/path/to/file.txt (ホスト名あり)
  • パス: ファイルシステム上の絶対パスが指定されます。

RFC 3986 (URI Generic Syntax)

RFC 3986は、URI(Uniform Resource Identifier)の一般的な構文を定義する標準です。URLはURIの一種です。このRFCは、URIの各コンポーネント(スキーム、オーソリティ、パスなど)の解釈方法や、それらの間の関係について詳細に記述しています。

特に、パスの解釈において、オーソリティ部分が存在しない場合のパスの開始方法について規定しています。file スキームのようにオーソリティが省略される場合、パスはスキームの後に続くスラッシュの数によって解釈が変わることがあります。

Go言語の net/url パッケージ

Go言語の net/url パッケージは、URLのパース、構築、エンコード、デコードを行うための機能を提供します。主要な型として url.URL 構造体があり、URLの各コンポーネント(Scheme, Opaque, User, Host, Path, RawPath, ForceQuery, RawQuery, Fragment, RawFragment)をフィールドとして持ちます。

  • url.Parse(rawurl string): 文字列形式のURLを url.URL 構造体にパースします。
  • url.URL.String(): url.URL 構造体を文字列形式のURLに変換します。

このパッケージは、Webアプリケーションやネットワークプログラミングにおいて、URLを安全かつ正確に扱うために不可欠です。

技術的詳細

このコミットの技術的詳細は、net/url パッケージ内のURLパースロジックと文字列化ロジックの変更に集約されます。

URLパースロジックの変更 (parse 関数)

変更前の parse 関数では、スキームが存在するか、またはリクエスト経由でない場合に、残りの文字列が // で始まり、かつ /// で始まらない場合にオーソリティ部分をパースしていました。

// 変更前
if (url.Scheme != "" || !viaRequest) && strings.HasPrefix(rest, "//") && !strings.HasPrefix(rest, "///") {
    // ... オーソリティのパース ...
}

この条件 !strings.HasPrefix(rest, "///") が問題でした。file:///foo のようなURLでは、rest///foo となるため、この条件が false となり、オーソリティ部分が正しくパースされず、結果として Path///foo が含まれてしまっていました。

変更後のコードでは、この !strings.HasPrefix(rest, "///") の条件が削除されました。

// 変更後
if (url.Scheme != "" || !viaRequest && !strings.HasPrefix(rest, "///")) && strings.HasPrefix(rest, "//") {
    // ... オーソリティのパース ...
}

この変更により、file:///foo のようなURLでも strings.HasPrefix(rest, "//")true となり、オーソリティのパースロジックに進むようになります。file スキームでホスト名が空の場合、parseAuthority 関数は空のホスト名を返し、残りの ///foo のうち最初のスラッシュがオーソリティの区切りとして消費され、結果的に Path/foo が設定されるようになります。

ただし、!viaRequest && !strings.HasPrefix(rest, "///") という条件が追加されています。これは、リクエスト経由でないURL(例: Parse("///foo") のような相対パス)で、かつ /// で始まる場合に、オーソリティとして解釈しないためのものです。これにより、file:///foo のような絶対パス形式のURLと、相対パス形式の ///foo を区別できるようになります。

URL文字列化ロジックの変更 (String メソッド)

変更前の String メソッドでは、Host または User が存在する場合にのみ // を追加していました。

// 変更前
if u.Host != "" || u.User != nil {
    result += "//"
    // ...
    result += u.Host
}

file:///foo のようなURLは、Host が空であり、User もないため、この条件が false となり、// が追加されませんでした。その結果、&URL{Scheme: "file", Path: "/foo"}.String()file:/foo のように出力されてしまい、期待される file:///foo とは異なっていました。

変更後のコードでは、Scheme が存在する場合にも // を追加する条件が追加されました。

// 変更後
if u.Scheme != "" || u.Host != "" || u.User != nil {
    result += "//"
    // ...
    if h := u.Host; h != "" {
        result += u.Host
    }
}

この変更により、file スキームを持つURLの場合、u.Scheme != ""true となるため、// が追加されるようになります。さらに、ホスト名が空の場合でも u.Host を追加しないように if h := u.Host; h != "" というチェックが追加されています。これにより、file:///foo のような形式が正しく生成されるようになります。

テストケースの変更

url_test.go には、mailto:/webmaster@golang.org のような「非オーソリティ」URLのテストケースがありました。変更前は、このURLの文字列化結果が空文字列 ("") となっていましたが、変更後は mailto:///webmaster@golang.org となるように修正されています。これは、mailto スキームも file スキームと同様にオーソリティを持たないが、パスの前に /// が付く形式がより適切であるという判断に基づいています。コミットメッセージにある「unfortunate compromise(残念な妥協)」とは、この mailto のケースで /// が付くようになることを指していると考えられます。これは、RFC 6068 (The 'mailto' URI Scheme) では mailto:user@example.com のようにスラッシュが一つも付かない形式が一般的であるため、/// が付くのは直感的ではないというニュアンスが含まれている可能性があります。しかし、net/url パッケージの内部的な整合性を保つためには、このような挙動が避けられないと判断されたのでしょう。

また、file:///home/adg/rabbits という新しいテストケースが追加され、このURLが正しくパースされ、Path/home/adg/rabbits となり、文字列化しても元の形式に戻ることを確認しています。

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

src/pkg/net/url/url.go

--- a/src/pkg/net/url/url.go
+++ b/src/pkg/net/url/url.go
@@ -386,7 +386,7 @@ func parse(rawurl string, viaRequest bool) (url *URL, err error) {
 		}
 	}
 
-	if (url.Scheme != "" || !viaRequest) && strings.HasPrefix(rest, "//") && !strings.HasPrefix(rest, "///") {
+	if (url.Scheme != "" || !viaRequest && !strings.HasPrefix(rest, "///")) && strings.HasPrefix(rest, "//") {
 		var authority string
 		authority, rest = split(rest[2:], '/', false)
 		url.User, url.Host, err = parseAuthority(authority)
@@ -442,12 +442,14 @@ func (u *URL) String() string {
 	if u.Opaque != "" {
 		result += u.Opaque
 	} else {
-		if u.Host != "" || u.User != nil {
+		if u.Scheme != "" || u.Host != "" || u.User != nil {
 			result += "//"
 			if u := u.User; u != nil {
 				result += u.String() + "@"
 			}
-			result += u.Host
+			if h := u.Host; h != "" {
+				result += u.Host
+			}
 		}
 		result += escape(u.Path, encodePath)
 	}

src/pkg/net/url/url_test.go

--- a/src/pkg/net/url/url_test.go
+++ b/src/pkg/net/url/url_test.go
@@ -122,14 +122,14 @@ var urltests = []URLTest{
 		},
 		"http:%2f%2fwww.google.com/?q=go+language",
 	},
-	// non-authority
+	// non-authority with path
 	{
 		"mailto:/webmaster@golang.org",
 		&URL{
 			Scheme: "mailto",
 			Path:   "/webmaster@golang.org",
 		},
-		"",
+		"mailto:///webmaster@golang.org", // unfortunate compromise
 	},
 	// non-authority
 	{
@@ -242,6 +242,15 @@ var urltests = []URLTest{
 		},
 		"http://www.google.com/?q=go+language#foo&bar",
 	},
+	{
+		"file:///home/adg/rabbits",
+		&URL{
+			Scheme: "file",
+			Host:   "",
+			Path:   "/home/adg/rabbits",
+		},
+		"file:///home/adg/rabbits",
+	},
 }
 
 // more useful string for debugging than fmt's struct printer

コアとなるコードの解説

src/pkg/net/url/url.go の変更点

  1. parse 関数の変更:

    • 変更前: if (url.Scheme != "" || !viaRequest) && strings.HasPrefix(rest, "//") && !strings.HasPrefix(rest, "///") {
      • この条件は、rest/// で始まる場合にオーソリティのパースをスキップしていました。これが file:///foo のようなURLで問題を引き起こしていました。
    • 変更後: if (url.Scheme != "" || !viaRequest && !strings.HasPrefix(rest, "///")) && strings.HasPrefix(rest, "//") {
      • !strings.HasPrefix(rest, "///") の位置が変更され、!viaRequest との論理積になりました。これにより、file:///foo のようなURL(viaRequestfalse で、rest/// で始まる)でも、strings.HasPrefix(rest, "//")true であればオーソリティのパースに進むようになります。
      • この変更により、file:///fooScheme: "file", Host: "", Path: "/foo" と正しくパースされるようになります。
  2. String メソッドの変更:

    • 変更前: if u.Host != "" || u.User != nil { result += "//" ... result += u.Host }
      • Host または User が存在しない場合(file スキームでホスト名が空の場合など)は // が追加されませんでした。
    • 変更後: if u.Scheme != "" || u.Host != "" || u.User != nil { result += "//" ... if h := u.Host; h != "" { result += u.Host } }
      • u.Scheme != "" という条件が追加されました。これにより、file スキームのようにホスト名が空でもスキームが存在すれば // が追加されるようになります。
      • result += u.Host の部分が if h := u.Host; h != "" { result += u.Host } に変更されました。これにより、ホスト名が空の場合は余分な空文字列が追加されるのを防ぎます。
      • これらの変更により、&URL{Scheme: "file", Path: "/foo"} のような URL 構造体が file:///foo と正しく文字列化されるようになります。

src/pkg/net/url/url_test.go の変更点

  1. mailto スキームのテストケースの修正:

    • mailto:/webmaster@golang.org のテストケースにおいて、期待される文字列化結果が "" から mailto:///webmaster@golang.org に変更されました。これは、file スキームと同様に、オーソリティを持たないスキームでもパスの前に /// が付く形式が内部的な整合性のため採用されたことを示しています。
  2. 新しい file スキームのテストケースの追加:

    • file:///home/adg/rabbits という新しいテストケースが追加されました。
    • このテストケースは、file スキームのURLが正しくパースされ(Scheme: "file", Host: "", Path: "/home/adg/rabbits")、そして正しく文字列化される(file:///home/adg/rabbits)ことを検証します。これは、このコミットの主要な修正内容を直接的にテストするものです。

これらの変更により、net/url パッケージは file スキームのURLをより正確に、RFCの意図に沿って処理できるようになりました。

関連リンク

参考にした情報源リンク

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

このコミットは、Go言語の標準ライブラリである net/url パッケージにおけるURLのパース処理のバグ修正に関するものです。具体的には、file スキームを持つURL(例: file:///foo)が正しくパースされず、Path フィールドに余分なスラッシュが含まれてしまう問題を解決します。これにより、URLのパースと文字列化の挙動がよりRFCに準拠したものになります。

コミット

  • Author: Andrew Gerrand adg@golang.org
  • Date: Wed Jan 23 11:37:06 2013 +1100
  • Commit Message:
    net/url: generate correct Path when hostname empty
    
    Parse("file:///foo") previously returned a URL with Scheme "file"
    and Path "///foo". Now it returns a URL with Path "/foo",
    such that
            &URL{Scheme: "file", Path: "/foo"}.String() == "file:///foo"
    
    This means that parsing and stringifying the URL "file:/foo"
    returns "file:///foo", technically a regression but one that only
    affects a corner case.
    
    Fixes #4189.
    
    R=bradfitz, rsc
    CC=golang-dev
    https://golang.org/cl/7135051
    

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

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

元コミット内容

net/url: generate correct Path when hostname empty

Parse("file:///foo") previously returned a URL with Scheme "file"
and Path "///foo". Now it returns a URL with Path "/foo",
such that
        &URL{Scheme: "file", Path: "/foo"}.String() == "file:///foo"

This means that parsing and stringifying the URL "file:/foo"
returns "file:///foo", technically a regression but one that only
affects a corner case.

Fixes #4189.

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

変更の背景

この変更は、Go言語の net/url パッケージが file スキームのURLを正しく処理できないというバグ(Issue #4189)を修正するために行われました。具体的には、file:///foo のような形式のURLをパースした際に、URL 構造体の Path フィールドが ///foo となってしまい、期待される /foo とは異なる値が設定される問題がありました。

RFC 3986(URI Generic Syntax)では、file スキームのURIは、ホスト名が省略された場合(つまり、ローカルファイルを参照する場合)、パスの前に3つのスラッシュ (///) が続く形式が一般的です。しかし、net/url パッケージの既存の実装では、この3つのスラッシュをパスの一部として誤って解釈していました。

この誤った解釈は、URLのパースと文字列化の間の不整合を引き起こし、特に file スキームを使用するアプリケーションで問題となる可能性がありました。このコミットは、この不整合を解消し、file スキームのURLがRFCの仕様に沿って正しく扱われるようにすることを目的としています。

コミットメッセージには、「file:/foo をパースして文字列化すると file:///foo になる」という「技術的にはリグレッションだが、ごく稀なケースにしか影響しない」という記述があります。これは、file: の後にスラッシュが1つしかない形式は、RFCでは非推奨または未定義の挙動とされており、通常は file:/// の形式が使われるため、実用上の影響は小さいと判断されたことを示唆しています。

前提知識の解説

URL (Uniform Resource Locator)

URLは、インターネット上のリソースの位置を示すための文字列です。一般的なURLの構造は以下のようになります。

scheme://[user:password@]host:port/path?query#fragment

  • Scheme (スキーム): リソースにアクセスするためのプロトコル(例: http, https, ftp, file)。
  • Authority (オーソリティ): ホスト名、ポート番号、ユーザー情報などを含む部分。
    • User Information (ユーザー情報): ユーザー名とパスワード(オプション)。
    • Host (ホスト): リソースが配置されているサーバーのドメイン名またはIPアドレス。
    • Port (ポート): サーバーのポート番号(オプション)。
  • Path (パス): サーバー上のリソースの場所を示す階層的な情報。
  • Query (クエリ): リソースに渡される追加のパラメータ(例: ?key=value)。
  • Fragment (フラグメント): リソース内の特定の部分を指す識別子(例: #section1)。

file スキーム

file スキームは、ローカルファイルシステム上のリソースを参照するために使用されます。その特徴は以下の通りです。

  • ホスト名: ローカルファイルを参照する場合、ホスト名は省略されるか、localhost が使用されます。ホスト名が省略される場合、パスの前に3つのスラッシュ (///) が続くのが一般的です。
    • 例: file:///path/to/file.txt (ホスト名なし)
    • 例: file://localhost/path/to/file.txt (ホスト名あり)
  • パス: ファイルシステム上の絶対パスが指定されます。

RFC 3986のセクション 3.3 "Path" およびセクション 3.2 "Authority" によると、URIのオーソリティコンポーネントはスキームの後に // が続くことで示されます。file スキームの場合、ホスト名が省略される(つまり、ローカルファイルを参照する)と、// の後にホスト名が続かず、すぐにパスが始まります。このとき、パスの最初のセグメントが空であることを示すために、さらにスラッシュが追加され、結果として /// となります。つまり、file:///path/to/file.txt は、file スキーム、空のホスト、そして /path/to/file.txt というパスを持つURIとして解釈されます。

RFC 3986 (URI Generic Syntax)

RFC 3986は、URI(Uniform Resource Identifier)の一般的な構文を定義する標準です。URLはURIの一種です。このRFCは、URIの各コンポーネント(スキーム、オーソリティ、パスなど)の解釈方法や、それらの間の関係について詳細に記述しています。

特に、パスの解釈において、オーソリティ部分が存在しない場合のパスの開始方法について規定しています。file スキームのようにオーソリティが省略される場合、パスはスキームの後に続くスラッシュの数によって解釈が変わることがあります。

Go言語の net/url パッケージ

Go言語の net/url パッケージは、URLのパース、構築、エンコード、デコードを行うための機能を提供します。主要な型として url.URL 構造体があり、URLの各コンポーネント(Scheme, Opaque, User, Host, RawPath, ForceQuery, RawQuery, Fragment, RawFragment)をフィールドとして持ちます。

  • url.Parse(rawurl string): 文字列形式のURLを url.URL 構造体にパースします。
  • url.URL.String(): url.URL 構造体を文字列形式のURLに変換します。

このパッケージは、Webアプリケーションやネットワークプログラミングにおいて、URLを安全かつ正確に扱うために不可欠です。

技術的詳細

このコミットの技術的詳細は、net/url パッケージ内のURLパースロジックと文字列化ロジックの変更に集約されます。

URLパースロジックの変更 (parse 関数)

変更前の parse 関数では、スキームが存在するか、またはリクエスト経由でない場合に、残りの文字列が // で始まり、かつ /// で始まらない場合にオーソリティ部分をパースしていました。

// 変更前
if (url.Scheme != "" || !viaRequest) && strings.HasPrefix(rest, "//") && !strings.HasPrefix(rest, "///") {
    // ... オーソリティのパース ...
}

この条件 !strings.HasPrefix(rest, "///") が問題でした。file:///foo のようなURLでは、rest///foo となるため、この条件が false となり、オーソリティ部分が正しくパースされず、結果として Path///foo が含まれてしまっていました。

変更後のコードでは、この !strings.HasPrefix(rest, "///") の条件が削除され、!viaRequest との論理積の形で strings.HasPrefix(rest, "///") が評価されるようになりました。

// 変更後
if (url.Scheme != "" || !viaRequest && !strings.HasPrefix(rest, "///")) && strings.HasPrefix(rest, "//") {
    // ... オーソリティのパース ...
}

この変更により、file:///foo のようなURLでも strings.HasPrefix(rest, "//")true となるため、オーソリティのパースロジックに進むようになります。file スキームでホスト名が空の場合、parseAuthority 関数は空のホスト名を返し、残りの ///foo のうち最初のスラッシュがオーソリティの区切りとして消費され、結果的に Path/foo が設定されるようになります。

ただし、!viaRequest && !strings.HasPrefix(rest, "///") という条件が追加されています。これは、リクエスト経由でないURL(例: Parse("///foo") のような相対パス)で、かつ /// で始まる場合に、オーソリティとして解釈しないためのものです。これにより、file:///foo のような絶対パス形式のURLと、相対パス形式の ///foo を区別できるようになります。

URL文字列化ロジックの変更 (String メソッド)

変更前の String メソッドでは、Host または User が存在する場合にのみ // を追加していました。

// 変更前
if u.Host != "" || u.User != nil {
    result += "//"
    // ...
    result += u.Host
}

file:///foo のようなURLは、Host が空であり、User もないため、この条件が false となり、// が追加されませんでした。その結果、&URL{Scheme: "file", Path: "/foo"}.String()file:/foo のように出力されてしまい、期待される file:///foo とは異なっていました。

変更後のコードでは、Scheme が存在する場合にも // を追加する条件が追加されました。

// 変更後
if u.Scheme != "" || u.Host != "" || u.User != nil {
    result += "//"
    // ...
    if h := u.Host; h != "" {
        result += u.Host
    }
}

この変更により、file スキームを持つURLの場合、u.Scheme != ""true となるため、// が追加されるようになります。さらに、ホスト名が空の場合でも u.Host を追加しないように if h := u.Host; h != "" というチェックが追加されています。これにより、file:///foo のような形式が正しく生成されるようになります。

テストケースの変更

url_test.go には、mailto:/webmaster@golang.org のような「非オーソリティ」URLのテストケースがありました。変更前は、このURLの文字列化結果が空文字列 ("") となっていましたが、変更後は mailto:///webmaster@golang.org となるように修正されています。これは、mailto スキームも file スキームと同様にオーソリティを持たないが、パスの前に /// が付く形式がより適切であるという判断に基づいています。コミットメッセージにある「unfortunate compromise(残念な妥協)」とは、この mailto のケースで /// が付くようになることを指していると考えられます。これは、RFC 6068 (The 'mailto' URI Scheme) では mailto:user@example.com のようにスラッシュが一つも付かない形式が一般的であるため、/// が付くのは直感的ではないというニュアンスが含まれている可能性があります。しかし、net/url パッケージの内部的な整合性を保つためには、このような挙動が避けられないと判断されたのでしょう。

また、file:///home/adg/rabbits という新しいテストケースが追加され、このURLが正しくパースされ、Path/home/adg/rabbits となり、文字列化しても元の形式に戻ることを確認しています。

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

src/pkg/net/url/url.go

--- a/src/pkg/net/url/url.go
+++ b/src/pkg/net/url/url.go
@@ -386,7 +386,7 @@ func parse(rawurl string, viaRequest bool) (url *URL, err error) {
 		}
 	}
 
-	if (url.Scheme != "" || !viaRequest) && strings.HasPrefix(rest, "//") && !strings.HasPrefix(rest, "///") {
+	if (url.Scheme != "" || !viaRequest && !strings.HasPrefix(rest, "///")) && strings.HasPrefix(rest, "//") {
 		var authority string
 		authority, rest = split(rest[2:], '/', false)
 		url.User, url.Host, err = parseAuthority(authority)
@@ -442,12 +442,14 @@ func (u *URL) String() string {
 	if u.Opaque != "" {
 		result += u.Opaque
 	} else {
-		if u.Host != "" || u.User != nil {
+		if u.Scheme != "" || u.Host != "" || u.User != nil {
 			result += "//"
 			if u := u.User; u != nil {
 				result += u.String() + "@"
 			}
-			result += u.Host
+			if h := u.Host; h != "" {
+				result += u.Host
+			}
 		}
 		result += escape(u.Path, encodePath)
 	}

src/pkg/net/url/url_test.go

--- a/src/pkg/net/url/url_test.go
+++ b/src/pkg/net/url/url_test.go
@@ -122,14 +122,14 @@ var urltests = []URLTest{
 		},
 		"http:%2f%2fwww.google.com/?q=go+language",
 	},
-	// non-authority
+	// non-authority with path
 	{
 		"mailto:/webmaster@golang.org",
 		&URL{
 			Scheme: "mailto",
 			Path:   "/webmaster@golang.org",
 		},
-		"",
+		"mailto:///webmaster@golang.org", // unfortunate compromise
 	},
 	// non-authority
 	{
@@ -242,6 +242,15 @@ var urltests = []URLTest{
 		},
 		"http://www.google.com/?q=go+language#foo&bar",
 	},
+	{
+		"file:///home/adg/rabbits",
+		&URL{
+			Scheme: "file",
+			Host:   "",
+			Path:   "/home/adg/rabbits",
+		},
+		"file:///home/adg/rabbits",
+	},
 }
 
 // more useful string for debugging than fmt's struct printer

コアとなるコードの解説

src/pkg/net/url/url.go の変更点

  1. parse 関数の変更:

    • 変更前: if (url.Scheme != "" || !viaRequest) && strings.HasPrefix(rest, "//") && !strings.HasPrefix(rest, "///") {
      • この条件は、rest/// で始まる場合にオーソリティのパースをスキップしていました。これが file:///foo のようなURLで問題を引き起こしていました。
    • 変更後: if (url.Scheme != "" || !viaRequest && !strings.HasPrefix(rest, "///")) && strings.HasPrefix(rest, "//") {
      • !strings.HasPrefix(rest, "///") の位置が変更され、!viaRequest との論理積になりました。これにより、file:///foo のようなURL(viaRequestfalse で、rest/// で始まる)でも、strings.HasPrefix(rest, "//")true であればオーソリティのパースに進むようになります。
      • この変更により、file:///fooScheme: "file", Host: "", Path: "/foo" と正しくパースされるようになります。
  2. String メソッドの変更:

    • 変更前: if u.Host != "" || u.User != nil { result += "//" ... result += u.Host }
      • Host または User が存在しない場合(file スキームでホスト名が空の場合など)は // が追加されませんでした。
    • 変更後: if u.Scheme != "" || u.Host != "" || u.User != nil { result += "//" ... if h := u.Host; h != "" { result += u.Host } }
      • u.Scheme != "" という条件が追加されました。これにより、file スキームのようにホスト名が空でもスキームが存在すれば // が追加されるようになります。
      • result += u.Host の部分が if h := u.Host; h != "" { result += u.Host } に変更されました。これにより、ホスト名が空の場合は余分な空文字列が追加されるのを防ぎます。
      • これらの変更により、&URL{Scheme: "file", Path: "/foo"} のような URL 構造体が file:///foo と正しく文字列化されるようになります。

src/pkg/net/url/url_test.go の変更点

  1. mailto スキームのテストケースの修正:

    • mailto:/webmaster@golang.org のテストケースにおいて、期待される文字列化結果が "" から mailto:///webmaster@golang.org に変更されました。これは、file スキームと同様に、オーソリティを持たないスキームでもパスの前に /// が付く形式が内部的な整合性のため採用されたことを示しています。
  2. 新しい file スキームのテストケースの追加:

    • file:///home/adg/rabbits という新しいテストケースが追加されました。
    • このテストケースは、file スキームのURLが正しくパースされ(Scheme: "file", Host: "", Path: "/home/adg/rabbits")、そして正しく文字列化される(file:///home/adg/rabbits)ことを検証します。これは、このコミットの主要な修正内容を直接的にテストするものです。

これらの変更により、net/url パッケージは file スキームのURLをより正確に、RFCの意図に沿って処理できるようになりました。

関連リンク

参考にした情報源リンク