[インデックス 14601] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/url
パッケージにおける ResolveReference
関数の相対パス処理、特に ..
(親ディレクトリ) の扱いに関するバグを修正するものです。具体的には、src/pkg/net/url/url.go
内の resolvePath
関数が修正され、それに対応するテストケースが src/pkg/net/url/url_test.go
に追加されています。
コミット
commit 2f45f2801da90350c2a2dbf7e36cd97f5fb7ce0f
Author: Rick Arnold <rickarnoldjr@gmail.com>
Date: Tue Dec 11 11:06:07 2012 -0500
net/url: fix handling of relative paths in ResolveReference.
Fixes #3560.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6886047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2f45f2801da90350c2a2dbf7e36cd97f5fb7ce0f
元コミット内容
net/url: fix handling of relative paths in ResolveReference.
Fixes #3560.
変更の背景
このコミットは、Go言語の net/url
パッケージが提供する ResolveReference
関数における、相対パスの解決に関する既存のバグ(Issue #3560)を修正するために行われました。
具体的には、ResolveReference
関数が .
(カレントディレクトリ) や ..
(親ディレクトリ) を含む相対パスを解決する際に、特にパスの中間に ..
が出現する場合に、RFC 3986で定義されているURI解決のルールに厳密に従わない挙動を示すことが問題でした。例えば、http://foo.com/bar/baz
をベースURIとして、quux/dotdot/../tail
のような相対パスを解決しようとすると、期待される結果 (http://foo.com/bar/quux/tail
) とは異なるURIが生成される可能性がありました。
この問題は、URIの正規化と解決の正確性に影響を与え、Webアプリケーションやネットワーク通信において予期せぬURIへのアクセスやリソースの誤認識を引き起こす可能性がありました。そのため、URI解決の仕様に完全に準拠するよう、resolvePath
関数のロジックを修正する必要がありました。
前提知識の解説
URI (Uniform Resource Identifier) と URL (Uniform Resource Locator)
URIは、リソースを一意に識別するための文字列です。URLはURIの一種であり、リソースの場所(ロケーション)を示すものです。Web上では、通常URLが使われます。
相対パスと絶対パス
- 絶対パス: リソースの完全な場所を最初から指定するパスです。例えば、
http://example.com/path/to/resource.html
。 - 相対パス: 現在のベースURIからの相対的な位置でリソースを指定するパスです。例えば、
../images/logo.png
やsubfolder/document.pdf
。
URI解決 (URI Resolution)
URI解決とは、ベースURIと相対URI参照(相対パス)から、完全な絶対URIを導出するプロセスです。これは、HTMLドキュメント内のリンクや、HTTPリダイレクトなどで広く利用されます。
RFC 3986: Uniform Resource Identifier (URI): Generic Syntax
このRFCは、URIの一般的な構文と、URI解決のアルゴリズムを定義しています。特に、セクション5.2「Reference Resolution Examples」では、相対URI参照の解決方法に関する詳細なルールと例が示されています。
URI解決の重要なルールの一つに、パスセグメントの処理があります。
.
(ドット): カレントディレクトリを示します。解決時には通常、無視されます。..
(ドットドット): 親ディレクトリを示します。解決時には、パスの最後のセグメントを削除し、その親ディレクトリに移動します。
これらのセグメントがパスの途中に複数回出現する場合や、連続して出現する場合の処理が複雑になることがあります。特に、a/b/../c
のようなパスは a/c
に解決されるべきですが、実装によっては a/b/c
のように誤って解釈されたり、a/c
になるべきところが a/b/c
になったりするバグが発生することがあります。
Go言語の net/url
パッケージ
net/url
パッケージは、Go言語でURLを解析、構築、操作するための機能を提供します。
url.URL
構造体: URLの各コンポーネント(スキーム、ホスト、パスなど)を表現します。Parse()
関数: 文字列からurl.URL
構造体を解析します。ResolveReference(ref *URL) *URL
メソッド: ベースURLに対して相対URL参照を解決し、新しい絶対URLを返します。このメソッドの内部で、パスの解決のためにresolvePath
関数が利用されます。
このコミットで修正されたのは、ResolveReference
の内部で呼び出される resolvePath
関数のロジックであり、..
の処理がRFC 3986に準拠していなかった点が問題でした。
技術的詳細
net/url
パッケージの resolvePath
関数は、ベースパスと参照パスを受け取り、それらを結合して正規化されたパスを生成します。この関数は、パスを /
で区切られたセグメントのリストとして扱い、.
や ..
といった特殊なセグメントを処理します。
修正前の resolvePath
関数には、..
セグメントがパスの中間に出現した場合の処理に問題がありました。具体的には、..
を処理する際に、常に base[len(base)-1] = ""
を実行して、base
スライスの最後の要素を空文字列に設定していました。これは、..
がパスの末尾にある場合には正しい動作ですが、パスの途中に ..
がある場合(例: a/b/../c
)には、b
が削除された後に a/
となり、その後に c
が追加されて a/c
となるべきです。しかし、常に最後の要素を空にするロジックでは、意図しないパスの短縮や、セグメントの誤った結合が発生する可能性がありました。
このコミットでは、rm
(remove) という新しいブール変数を導入することで、この問題を解決しています。rm
変数は、現在のパスセグメントの処理後に、base
スライスの最後の要素を空にする必要があるかどうかを制御します。
ref == "."
の場合:- もし
idx == 0
(参照パスの最初のセグメントが.
の場合) であれば、base
の最後の要素を空にし、rm = true
とします。これは、./foo
のような場合にfoo
となるべき挙動を保証します。 - そうでなければ、
rm = false
とし、最後の要素を空にしません。これは、a/./b
のような場合にa/b
となるべき挙動を保証します。
- もし
ref == ".."
の場合:base
スライスから最後のセグメントを削除します。rm
がtrue
の場合にのみ、base
の最後の要素を空にします。これにより、..
がパスの途中にあっても、不必要にパスが短縮されることを防ぎます。
ref
が通常のセグメントの場合:rm = false
とします。これは、通常のセグメントが追加された後は、最後の要素を空にする必要がないためです。
この変更により、resolvePath
関数は、RFC 3986で定義されているURI解決のルールに厳密に従い、..
や .
を含む複雑な相対パスも正しく解決できるようになりました。
コアとなるコードの変更箇所
src/pkg/net/url/url.go
の resolvePath
関数に以下の変更が加えられました。
diff --git a/src/pkg/net/url/url.go b/src/pkg/net/url/url.go
index 692a7fdc04..82db0367bc 100644
--- a/src/pkg/net/url/url.go
+++ b/src/pkg/net/url/url.go
@@ -572,23 +572,33 @@ func resolvePath(basepath string, refpath string) string {
if len(base) == 0 {
base = []string{""}
}
+
+ rm := true
for idx, ref := range refs {
switch {
case ref == ".":
- base[len(base)-1] = ""
+ if idx == 0 {
+ base[len(base)-1] = ""
+ rm = true
+ } else {
+ rm = false
+ }
case ref == "..":
newLen := len(base) - 1
if newLen < 1 {
newLen = 1
}
base = base[0:newLen]
- base[len(base)-1] = ""
+ if rm {
+ base[len(base)-1] = ""
+ }
default:
if idx == 0 || base[len(base)-1] == "" {
base[len(base)-1] = ref
} else {
base = append(base, ref)
}
+ rm = false
}
}
return strings.Join(base, "/")
また、src/pkg/net/url/url_test.go
には、Issue #3560に関連する複数の新しいテストケースが追加されました。
diff --git a/src/pkg/net/url/url_test.go b/src/pkg/net/url/url_test.go
index 64f1170027..4a09189403 100644
--- a/src/pkg/net/url/url.go
+++ b/src/pkg/net/url/url.go
@@ -536,6 +536,15 @@ var resolveReferenceTests = []struct {
{"http://foo.com/bar/baz", "../../../../../quux", "http://foo.com/quux"},
{"http://foo.com/bar", "..", "http://foo.com/"},
{"http://foo.com/bar/baz", "./..", "http://foo.com/"},
+ // ".." in the middle (issue 3560)
+ {"http://foo.com/bar/baz", "quux/dotdot/../tail", "http://foo.com/bar/quux/tail"},
+ {"http://foo.com/bar/baz", "quux/./dotdot/../tail", "http://foo.com/bar/quux/tail"},
+ {"http://foo.com/bar/baz", "quux/./dotdot/.././tail", "http://foo.com/bar/quux/tail"},
+ {"http://foo.com/bar/baz", "quux/./dotdot/./../tail", "http://foo.com/bar/quux/tail"},
+ {"http://foo.com/bar/baz", "quux/./dotdot/dotdot/././../../tail", "http://foo.com/bar/quux/tail"},
+ {"http://foo.com/bar/baz", "quux/./dotdot/dotdot/./.././../tail", "http://foo.com/bar/quux/tail"},
+ {"http://foo.com/bar/baz", "quux/./dotdot/dotdot/dotdot/./../../.././././tail", "http://foo.com/bar/quux/tail"},
+ {"http://foo.com/bar/baz", "quux/./dotdot/../dotdot/../dot/./tail/..", "http://foo.com/bar/quux/dot"},
// "." and ".." in the base aren't special
{"http://foo.com/dot/./dotdot/../foo/bar", "../baz", "http://foo.com/dot/./dotdot/../baz"},
コアとなるコードの解説
修正の核心は、resolvePath
関数内で導入された rm
(remove) というブール変数と、それを用いた条件分岐です。
-
rm
変数の導入と初期化:rm := true
rm
は、現在のパスセグメントの処理後に、base
スライスの最後の要素を空文字列 (""
) に設定する必要があるかどうかを示すフラグです。初期値はtrue
です。 -
.
(カレントディレクトリ) の処理:case ref == ".": if idx == 0 { // 参照パスの最初のセグメントが "." の場合 base[len(base)-1] = "" // 最後の要素を空にする rm = true // rmをtrueに維持 } else { // 参照パスの途中に "." がある場合 rm = false // rmをfalseにする }
- 参照パスの最初のセグメントが
.
の場合(例:./foo
)、ベースパスの最後のセグメントを空にし、rm
をtrue
に保ちます。これにより、http://example.com/dir/./file
がhttp://example.com/dir/file
となるように、dir/
の部分がdir
に正規化されます。 - 参照パスの途中に
.
がある場合(例:foo/./bar
)、rm
をfalse
に設定します。これにより、base[len(base)-1] = ""
が実行されなくなり、foo/bar
のように正しく解決されます。
- 参照パスの最初のセグメントが
-
..
(親ディレクトリ) の処理:case ref == "..": newLen := len(base) - 1 if newLen < 1 { newLen = 1 } base = base[0:newLen] // 最後のセグメントを削除 if rm { // rmがtrueの場合のみ base[len(base)-1] = "" // 最後の要素を空にする }
..
が出現した場合、まずbase
スライスから最後のセグメントを削除します。これは、親ディレクトリに移動する基本的な動作です。- 重要なのは、
if rm { base[len(base)-1] = "" }
という条件が追加された点です。これにより、rm
がtrue
の場合にのみ、削除後のbase
スライスの最後の要素が空文字列に設定されます。- 例えば、
http://foo.com/bar/baz
をベースに../quux
を解決する場合、base
は["foo.com", "bar", "baz"]
から["foo.com", "bar"]
になり、rm
がtrue
なので["foo.com", "bar", ""]
となり、最終的にhttp://foo.com/bar/quux
となります。 - しかし、
http://foo.com/bar/baz
をベースにquux/dotdot/../tail
を解決する場合、quux
が追加された時点でrm
はfalse
になります。次にdotdot
が追加され、その次に..
が処理されます。この時rm
はfalse
のままなので、base[len(base)-1] = ""
は実行されず、http://foo.com/bar/quux/tail
のように正しく解決されます。
- 例えば、
-
通常のセグメントの処理:
default: if idx == 0 || base[len(base)-1] == "" { base[len(base)-1] = ref } else { base = append(base, ref) } rm = false // 通常のセグメントが追加されたらrmをfalseにする
- 通常のパスセグメントが追加された後、
rm
をfalse
に設定します。これは、次に.
や..
が出現した場合に、不必要なパスの短縮を防ぐためです。
- 通常のパスセグメントが追加された後、
これらの変更により、resolvePath
関数は、RFC 3986のURI解決アルゴリズム、特にパスの正規化ルールに厳密に準拠するようになりました。これにより、net/url.ResolveReference
がより堅牢で正確なURI解決を提供できるようになりました。
関連リンク
- Go Issue #3560: https://github.com/golang/go/issues/3560
- Go Code Review 6886047: https://golang.org/cl/6886047
参考にした情報源リンク
- RFC 3986: Uniform Resource Identifier (URI): Generic Syntax - Section 5.2. Reference Resolution: https://datatracker.ietf.org/doc/html/rfc3986#section-5.2
- Go
net/url
package documentation: https://pkg.go.dev/net/url - Stack Overflow:
net/url.ResolveReference
function in Go: https://stackoverflow.com/questions/tagged/go+net-url+resolveref (一般的な情報源として)