[インデックス 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 := truermは、現在のパスセグメントの処理後に、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/urlpackage documentation: https://pkg.go.dev/net/url - Stack Overflow:
net/url.ResolveReferencefunction in Go: https://stackoverflow.com/questions/tagged/go+net-url+resolveref (一般的な情報源として)