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

[インデックス 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/btargpath/a/b/c/dの場合、戻り値はc/dとなります。また、basepath/a/b/ctargpath/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)

変更点と意図は以下の通りです。

  1. size := 2 + seps*3:

    • まず、ベースパスを遡る部分の最小限のサイズを計算します。
    • 2は、最初の..(2バイト)を考慮しています。
    • seps*3は、残りのseps個の../(それぞれ3バイト)を考慮しています。
    • これにより、ベースパスを遡るために必要な「../」の合計長がより正確に計算されます。例えば、sepsが0の場合(basepathtargpathの親ディレクトリの場合)、2となり、..のスペースが確保されます。
  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関数の内部で、相対パスを格納するためのバイトスライスbufmakeする際のサイズ計算ロジックが変更されています。

  • 変更前: 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というテストケースのスライスに、新しいテストエントリが追加されています。

  • 追加されたテストケースは、特にbasepathtargpathよりも深く、targpathbasepathの親ディレクトリであるようなシナリオをカバーしています。
    • {"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言語の公式ドキュメント: path/filepathパッケージ
  • Go言語のソースコード(src/pkg/path/filepath/path.go および src/pkg/path/filepath/path_test.go
  • GitHubのGoリポジトリのIssueトラッカー
  • Go言語のコードレビューシステム (Gerrit)