[インデックス 10514] ファイルの概要
このコミットは、Go言語の標準ライブラリpath/filepath
パッケージにおけるRel
関数のバッファサイズ計算の不具合を修正するものです。具体的には、相対パスを計算する際に内部で使用されるバッファのサイズが適切でなかったために発生する可能性のある問題を解決しています。
コミット
commit a620865639d4e8c159c563c05b6cd7b50596273c
Author: Gustavo Niemeyer <gustavo@niemeyer.net>
Date: Sun Nov 27 21:28:52 2011 -0500
filepath/path: fix Rel buffer sizing
Fixes #2493.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/5433079
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a620865639d4e8c159c563c05b6cd7b50596273c
元コミット内容
filepath/path: fix Rel buffer sizing
このコミットは、filepath
パッケージのRel
関数におけるバッファサイズ計算の不具合を修正します。
これはIssue #2493を修正するものです。
変更の背景
この変更は、Go言語のIssue #2493「path/filepath: Rel
can return incorrect path if targpath
is shorter than basepath
」に対応するものです。このIssueでは、filepath.Rel
関数が、ターゲットパス(targpath
)がベースパス(basepath
)よりも短い場合に、誤った相対パスを返す可能性があることが報告されていました。
具体的には、Rel
関数が内部で相対パスを構築するために使用するバイトスライス(バッファ)の初期サイズ計算に問題がありました。ベースパスからターゲットパスへの相対パスを計算する際、共通のプレフィックスを特定し、ベースパスの残りの部分を「../
」で遡り、その後ターゲットパスの残りの部分を連結するというロジックが用いられます。このとき、バッファのサイズが不適切だと、結果として生成されるパスが切り詰められたり、正しくない内容になったりする可能性がありました。
このバッファサイズの問題は、特にベースパスがターゲットパスよりも深く、かつターゲットパスがベースパスの親ディレクトリに相当する場合に顕在化しました。例えば、/a/b/c/d
から/a/b
への相対パスを計算する際に、../../
という結果が期待されますが、バッファサイズが不適切だと、この「../..
」が正しく生成されない、あるいは後続のパス要素が追加される際に問題が生じる可能性がありました。
前提知識の解説
path/filepath
パッケージ
path/filepath
パッケージは、Go言語においてファイルパスを操作するためのユーティリティを提供します。これは、オペレーティングシステムに依存しないパス操作(path
パッケージ)とは異なり、現在のOSのパス区切り文字(Windowsでは\
、Unix系では/
)やパスの規則(絶対パス、相対パスなど)を考慮した操作を行います。ファイルシステムのパスを扱うアプリケーションでは、このパッケージが不可欠です。
filepath.Rel
関数
func Rel(basepath, targpath string) (string, error)
filepath.Rel
関数は、basepath
からtargpath
への相対パスを計算します。例えば、basepath
が/a/b
でtargpath
が/a/b/c/d
の場合、戻り値はc/d
となります。また、basepath
が/a/b/c
でtargpath
が/a/d
の場合、戻り値は../d
となります。この関数は、ファイルシステム上の2つのパス間の関係を表現する際に非常に便利です。
バッファリングとバイトスライス
Go言語では、文字列操作やデータ処理において、[]byte
型のバイトスライスをバッファとして使用することが一般的です。特に、最終的な文字列の長さを事前に見積もることができる場合、make([]byte, size)
のように適切なサイズのバッファを事前に確保することで、メモリの再割り当て(reallocation)を減らし、パフォーマンスを向上させることができます。しかし、このバッファサイズの計算が誤っていると、確保したバッファが小さすぎてデータが収まらない(結果が切り詰められる、パニックが発生する)か、大きすぎて無駄なメモリを消費する(パフォーマンスに影響はないが効率が悪い)といった問題が発生します。
strings.Count
関数
strings.Count(s, sep string) int
この関数は、文字列s
内にsep
が何回出現するかを数えます。このコミットの文脈では、base[b0:bl]
(ベースパスの共通プレフィックス以降の部分)に含まれるパス区切り文字(Separator
)の数を数えることで、ベースパスを遡るために必要な「../
」の数を計算するために使用されています。
技術的詳細
このコミットの核心は、filepath.Rel
関数内で相対パスを構築するためのバイトスライスbuf
のサイズ計算ロジックの改善にあります。
変更前のコードでは、バッファサイズは以下のように計算されていました。
buf := make([]byte, 3+seps*3+tl-t0)
ここで、
seps
は、ベースパスの共通プレフィックス以降の部分に含まれるパス区切り文字の数です。これは、../
を何回繰り返す必要があるかを示します。3+seps*3
は、../
の繰り返し部分の長さを概算しています。..
が2バイト、/
が1バイトで合計3バイトなので、seps
回繰り返すとseps*3
バイトになります。最初の..
のために3
が加算されています。tl-t0
は、ターゲットパスの共通プレフィックス以降の部分の長さです。
この計算式には、特定のケースで問題がありました。特に、ターゲットパスがベースパスの親ディレクトリに相当する場合(例: /a/b/c/d
から/a/b
への相対パスは../../
)、tl-t0
が非常に小さくなるか、ゼロになることがあります。この場合、buf
のサイズが不足し、結果として生成されるパスが切り詰められる可能性がありました。
修正後のコードでは、バッファサイズは以下のように計算されます。
size := 2 + seps*3
if tl != t0 {
size += 1 + tl - t0
}
buf := make([]byte, size)
変更点と意図は以下の通りです。
-
size := 2 + seps*3
:- まず、ベースパスを遡る部分の最小限のサイズを計算します。
2
は、最初の..
(2バイト)を考慮しています。seps*3
は、残りのseps
個の../
(それぞれ3バイト)を考慮しています。- これにより、ベースパスを遡るために必要な「
../
」の合計長がより正確に計算されます。例えば、seps
が0の場合(basepath
がtargpath
の親ディレクトリの場合)、2
となり、..
のスペースが確保されます。
-
if tl != t0 { size += 1 + tl - t0 }
:- この条件は、ターゲットパスに共通プレフィックス以降の残りの部分があるかどうかをチェックします。
tl != t0
は、ターゲットパスの残りの部分(targpath[t0:tl]
)が空でないことを意味します。- もしターゲットパスにまだ要素が残っている場合、その要素の長さ
tl-t0
に加えて、その要素の前に来るパス区切り文字(/
)のための1
バイトを追加します。 - この条件分岐により、ターゲットパスの残りの部分がない場合(例:
/a/b/c/d
から/a/b
への相対パスが../../
で終わる場合)に、不要な1 + tl - t0
の加算が行われなくなり、バッファサイズがより正確になります。
この修正により、Rel
関数が生成する相対パスのバッファサイズが、あらゆるケースで適切に確保されるようになり、Issue #2493で報告されたような、パスが切り詰められる問題が解決されました。
また、path_test.go
には、この修正が正しく機能することを確認するための新しいテストケースが追加されています。特に、{"a/b/c/d", "a/b", "../.."}
のような、ベースパスがターゲットパスよりも深く、ターゲットパスがベースパスの親ディレクトリであるようなケースが追加されており、これが修正の意図を明確に示しています。
コアとなるコードの変更箇所
src/pkg/path/filepath/path.go
--- a/src/pkg/path/filepath/path.go
+++ b/src/pkg/path/filepath/path.go
@@ -312,7 +312,11 @@ func Rel(basepath, targpath string) (string, error) {
if b0 != bl {
// Base elements left. Must go up before going down.
seps := strings.Count(base[b0:bl], string(Separator))
- buf := make([]byte, 3+seps*3+tl-t0)
+ size := 2 + seps*3
+ if tl != t0 {
+ size += 1 + tl - t0
+ }
+ buf := make([]byte, size)
n := copy(buf, "..")
for i := 0; i < seps; i++ {
buf[n] = Separator
src/pkg/path/filepath/path_test.go
--- a/src/pkg/path/filepath/path_test.go
+++ b/src/pkg/path/filepath/path_test.go
@@ -629,6 +629,10 @@ var reltests = []RelTests{
{"a/b/../c", "a/b", "../b"},
{"a/b/c", "a/c/d", "../../c/d"},
{"a/b", "c/d", "../../c/d"},
+ {"a/b/c/d", "a/b", "../.."},
+ {"a/b/c/d", "a/b/", "../.."},
+ {"a/b/c/d/", "a/b", "../.."},
+ {"a/b/c/d/", "a/b/", "../.."},
{"../../a/b", "../../a/b/c/d", "c/d"},
{"/a/b", "/a/b", "."},
{"/a/b/.", "/a/b", "."},
@@ -640,6 +644,10 @@ var reltests = []RelTests{
{"/a/b/../c", "/a/b", "../b"},
{"/a/b/c", "/a/c/d", "../../c/d"},
{"/a/b", "/c/d", "../../c/d"},
+ {"/a/b/c/d", "/a/b", "../.."},
+ {"/a/b/c/d", "/a/b/", "../.."},
+ {"/a/b/c/d/", "/a/b", "../.."},
+ {"/a/b/c/d/", "/a/b/", "../.."},
{"/../../a/b", "/../../a/b/c/d", "c/d"},
{".", "a/b", "a/b"},
{".", "..", ".."},
コアとなるコードの解説
src/pkg/path/filepath/path.go
の変更
このファイルでは、Rel
関数の内部で、相対パスを格納するためのバイトスライスbuf
をmake
する際のサイズ計算ロジックが変更されています。
-
変更前:
buf := make([]byte, 3+seps*3+tl-t0)
- この計算式は、ベースパスを遡る部分(
../
の繰り返し)とターゲットパスの残りの部分の長さを単純に合計していました。 3+seps*3
は、最初の..
とそれに続くseps
個の../
の長さを意図していましたが、特にseps
が0の場合や、ターゲットパスの残りの部分がない場合に、バッファサイズが不足する可能性がありました。
- この計算式は、ベースパスを遡る部分(
-
変更後:
size := 2 + seps*3 if tl != t0 { size += 1 + tl - t0 } buf := make([]byte, size)
- 新しい
size
計算では、まず2 + seps*3
で、ベースパスを遡るために必要な../
の合計長をより正確に計算します。2
は最初の..
の長さ、seps*3
は残りのseps
個の../
の長さです。 - 次に、
if tl != t0
という条件分岐が追加されました。これは、ターゲットパスに共通プレフィックス以降の要素が残っている場合にのみ、その要素の長さ(tl-t0
)と、その要素の前に必要となるパス区切り文字(/
)のための1
バイトをsize
に加算します。 - この条件分岐により、ターゲットパスの残りの部分がない場合(例:
/a/b/c/d
から/a/b
への相対パスが../../
で終わる場合)に、不要な1 + tl - t0
の加算が行われなくなり、バッファサイズが過剰になったり不足したりするのを防ぎます。 - 結果として、
buf
のサイズがより正確に計算されるようになり、相対パスの生成時にバッファオーバーフローや切り詰めが発生する可能性がなくなりました。
- 新しい
src/pkg/path/filepath/path_test.go
の変更
このファイルでは、reltests
というテストケースのスライスに、新しいテストエントリが追加されています。
- 追加されたテストケースは、特に
basepath
がtargpath
よりも深く、targpath
がbasepath
の親ディレクトリであるようなシナリオをカバーしています。{"a/b/c/d", "a/b", "../.."}
{"a/b/c/d", "a/b/", "../.."}
{"a/b/c/d/", "a/b", "../.."}
{"a/b/c/d/", "a/b/", "../.."}
- 同様に、絶対パスのバージョンも追加されています。
これらのテストケースは、修正前のコードでは正しくない結果を返す可能性があったシナリオを明示的に検証するために追加されました。例えば、/a/b/c/d
から/a/b
への相対パスは../../
となるべきですが、修正前のバッファ計算ではこれが正しく生成されないことがありました。これらのテストの追加により、修正が意図通りに機能し、将来のリグレッションを防ぐための安全網が強化されました。
関連リンク
- Go Issue 2493: https://github.com/golang/go/issues/2493
- Go CL 5433079: https://golang.org/cl/5433079
参考にした情報源リンク
- Go言語の公式ドキュメント:
path/filepath
パッケージ - Go言語のソースコード(
src/pkg/path/filepath/path.go
およびsrc/pkg/path/filepath/path_test.go
) - GitHubのGoリポジトリのIssueトラッカー
- Go言語のコードレビューシステム (Gerrit)