[インデックス 18736] ファイルの概要
このコミットは、Go言語の標準ライブラリ path/filepath
パッケージにおける Glob
関数の挙動を修正するものです。具体的には、Glob
関数が壊れたシンボリックリンクを無視しないように変更され、関連するテストケースが追加されています。
コミット
commit 96c373f9e1eea8e13e1a8bcbfd1da8aada26fe90
Author: Kelsey Hightower <kelsey.hightower@gmail.com>
Date: Tue Mar 4 09:00:45 2014 -0800
path/filepath: ensure Glob does not ignore broken symlinks
Fixes #6463.
LGTM=bradfitz
R=golang-codereviews, bradfitz
CC=golang-codereviews
https://golang.org/cl/69870050
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/96c373f9e1eea8e13e1a8bcbfd1da8aada26fe90
元コミット内容
path/filepath: ensure Glob does not ignore broken symlinks
このコミットは、path/filepath
パッケージの Glob
関数が、壊れたシンボリックリンク(リンク先が存在しないシンボリックリンク)を適切に処理し、無視しないようにするための変更です。
変更の背景
この変更は、Go Issue #6463 を修正するために行われました。元の Glob
関数は、パターンにメタ文字(*
, ?
, []
など)が含まれていない場合、つまり単一のパスが指定された場合に、os.Stat
を使用してそのパスが存在するかどうかを確認していました。
os.Stat
は、シンボリックリンクの場合、そのリンクが指す実際のファイルやディレクトリの情報を取得しようとします。もしシンボリックリンクが壊れていて、指す先が存在しない場合、os.Stat
はエラーを返します。このエラーによって、Glob
関数は壊れたシンボリックリンクを「存在しない」ものとして扱い、結果としてマッチングから除外してしまっていました。
しかし、Glob
関数の期待される挙動としては、パターンにマッチするすべてのパスを返すことであり、シンボリックリンクが壊れているかどうかに関わらず、そのシンボリックリンク自体がパターンにマッチするならば、それを結果に含めるべきです。この不整合が問題となり、修正が必要とされました。
前提知識の解説
path/filepath
パッケージ
path/filepath
パッケージは、ファイルパスを操作するためのユーティリティ関数を提供します。これには、パスの結合、クリーンアップ、相対パスと絶対パスの変換、そしてファイル名パターンマッチング(グロビング)などが含まれます。
Glob
関数
Glob(pattern string) (matches []string, err error)
は、指定されたシェル形式のパターンにマッチするすべてのファイル名またはディレクトリ名を返します。パターンには、*
(任意の文字列にマッチ)、?
(任意の一文字にマッチ)、[]
(文字の範囲またはセットにマッチ)などのワイルドカード文字を含めることができます。
シンボリックリンク (Symbolic Link / Symlink)
シンボリックリンクは、ファイルシステム内の別のファイルやディレクトリへの参照(ポインタ)です。これは、WindowsのショートカットやUnix/Linuxのソフトリンクに似ています。シンボリックリンク自体は非常に小さなファイルで、その内容は参照先のパスを保持しています。
壊れたシンボリックリンク (Broken Symlink / Dangling Symlink)
壊れたシンボリックリンクとは、そのシンボリックリンクが指し示している元のファイルやディレクトリが、すでに存在しない場合に発生します。シンボリックリンク自体はファイルシステム上に存在しますが、その参照先が失われている状態です。
os.Stat
と os.Lstat
Go言語の os
パッケージには、ファイルやディレクトリの情報を取得するための関数がいくつかあります。
-
os.Stat(name string) (FileInfo, error)
: この関数は、指定されたname
のファイルまたはディレクトリのFileInfo
を返します。もしname
がシンボリックリンクである場合、os.Stat
はそのシンボリックリンクが指す実際のファイルまたはディレクトリの情報を取得しようとします。つまり、シンボリックリンクを「たどって」その先の情報を返します。もしシンボリックリンクの指す先が存在しない(壊れている)場合、os.Stat
はエラーを返します。 -
os.Lstat(name string) (FileInfo, error)
: この関数は、指定されたname
のファイルまたはディレクトリのFileInfo
を返します。os.Stat
とは異なり、os.Lstat
はname
がシンボリックリンクである場合、そのシンボリックリンク自体の情報を返します。シンボリックリンクの指す先が存在するかどうかは確認しません。これにより、壊れたシンボリックリンクであっても、そのシンボリックリンク自体の情報を取得し、エラーを発生させずに存在を確認することができます。
技術的詳細
このコミットの核心は、path/filepath/match.go
内の Glob
関数における os.Stat
の呼び出しを os.Lstat
に変更した点です。
変更前:
func Glob(pattern string) (matches []string, err error) {
if !hasMeta(pattern) {
if _, err = os.Stat(pattern); err != nil {
return nil, nil
}
return []string{pattern}, nil
変更後:
func Glob(pattern string) (matches []string, err error) {
if !hasMeta(pattern) {
if _, err = os.Lstat(pattern); err != nil {
return nil, nil
}
return []string{pattern}, nil
この変更は、Glob
関数がパターンにワイルドカードを含まない(つまり、単一のパスが指定された)場合にのみ影響します。このような場合、Glob
は指定されたパスが実際に存在するかどうかを確認する必要があります。
-
変更前 (
os.Stat
を使用): もしpattern
が壊れたシンボリックリンクであった場合、os.Stat(pattern)
はエラーを返します。このエラーによって、Glob
関数はnil, nil
を返し、壊れたシンボリックリンクをマッチング結果から除外してしまいます。これは、Glob
が「パターンにマッチするパス」を返すという期待に反します。壊れたシンボリックリンクであっても、そのパス自体は存在し、パターンにマッチするからです。 -
変更後 (
os.Lstat
を使用):os.Lstat(pattern)
は、pattern
がシンボリックリンクである場合、それが壊れていてもそのシンボリックリンク自体の情報を取得します。これにより、エラーが発生せず、Glob
関数は[]string{pattern}
を返すことができます。つまり、壊れたシンボリックリンクであっても、それがパターンにマッチする限り、結果に含まれるようになります。
この修正により、Glob
関数は、パターンにワイルドカードが含まれない場合でも、シンボリックリンクの健全性に関わらず、そのパスがファイルシステム上に存在するかどうかを正確に判断できるようになりました。
また、この変更を検証するために、src/pkg/path/filepath/match_test.go
に新しいテストケース TestGlobSymlink
が追加されました。このテストは、シンボリックリンク(正常なものと壊れたもの両方)が Glob
関数によって正しく処理されることを確認します。
テストの概要:
- 一時ディレクトリを作成します。
globSymlinkTests
という構造体スライスを定義し、テストケース(元のファイルパス、シンボリックリンクパス、壊れたリンクかどうか)を格納します。- 各テストケースについて、以下の操作を行います。
- 元のファイルを作成します。
- 元のファイルへのシンボリックリンクを作成します。
- もし
brokenLink
がtrue
の場合、元のファイルを削除してシンボリックリンクを壊します。 Glob
関数をシンボリックリンクパスに対して呼び出します。Glob
がエラーを返さないこと、および結果にシンボリックリンクパスが含まれていることをアサートします。
このテストは、WindowsやPlan 9などのシンボリックリンクをサポートしない、または挙動が異なるOSではスキップされます。
コアとなるコードの変更箇所
src/pkg/path/filepath/match.go
--- a/src/pkg/path/filepath/match.go
+++ b/src/pkg/path/filepath/match.go
@@ -230,7 +230,7 @@ func getEsc(chunk string) (r rune, nchunk string, err error) {
//
func Glob(pattern string) (matches []string, err error) {
if !hasMeta(pattern) {
- if _, err = os.Stat(pattern); err != nil {
+ if _, err = os.Lstat(pattern); err != nil {
return nil, nil
}
return []string{pattern}, nil
src/pkg/path/filepath/match_test.go
--- a/src/pkg/path/filepath/match_test.go
+++ b/src/pkg/path/filepath/match_test.go
@@ -2,9 +2,12 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package filepath
+package filepath_test
import (
+ "io/ioutil"
+ "os"
+ . "path/filepath"
"runtime"
"strings"
"testing"
@@ -153,3 +156,52 @@ func TestGlobError(t *testing.T) {
t.Error("expected error for bad pattern; got none")
}
}\n+\n+var globSymlinkTests = []struct {\n+\tpath, dest string\n+\tbrokenLink bool\n+}{\n+\t{\"test1\", \"link1\", false},\n+\t{\"test2\", \"link2\", true},\n+}\n+\n+func TestGlobSymlink(t *testing.T) {\n+\tswitch runtime.GOOS {\n+\tcase "windows", "plan9":\n+\t\t// The tests below are Unix specific so we skip plan9, which does not\n+\t\t// support symlinks, and windows.\n+\t\tt.Skipf("skipping test on %v", runtime.GOOS)\n+\t}\n+\ttmpDir, err := ioutil.TempDir("", "globsymlink")\n+\tif err != nil {\n+\t\tt.Fatal("creating temp dir:", err)\n+\t}\n+\tdefer os.RemoveAll(tmpDir)\n+\n+\tfor _, tt := range globSymlinkTests {\n+\t\tpath := Join(tmpDir, tt.path)\n+\t\tdest := Join(tmpDir, tt.dest)\n+\t\tf, err := os.Create(path)\n+\t\tif err != nil {\n+\t\t\tt.Fatal(err)\n+\t\t}\n+\t\tif err := f.Close(); err != nil {\n+\t\t\tt.Fatal(err)\n+\t\t}\n+\t\terr = os.Symlink(path, dest)\n+\t\tif err != nil {\n+\t\t\tt.Fatal(err)\n+\t\t}\n+\t\tif tt.brokenLink {\n+\t\t\t// Break the symlink.\n+\t\t\tos.Remove(path)\n+\t\t}\n+\t\tmatches, err := Glob(dest)\n+\t\tif err != nil {\n+\t\t\tt.Errorf("GlobSymlink error for %q: %s", dest, err)\n+\t\t}\n+\t\tif !contains(matches, dest) {\n+\t\t\tt.Errorf("Glob(%#q) = %#v want %v", dest, matches, dest)\n+\t\t}\n+\t}\n+}\n```
## コアとなるコードの解説
### `match.go` の変更
`Glob` 関数内で、パターンにメタ文字が含まれていない(`!hasMeta(pattern)` が `true`)場合のファイル存在チェックが `os.Stat` から `os.Lstat` に変更されました。
* `os.Stat(pattern)`: シンボリックリンクをたどって、その参照先のファイル情報を取得しようとします。参照先が存在しない(壊れたシンボリックリンク)場合、エラーを返します。
* `os.Lstat(pattern)`: シンボリックリンク自体に関する情報を取得します。参照先が存在するかどうかは確認しないため、壊れたシンボリックリンクであってもエラーを返しません。
この変更により、`Glob` 関数は、壊れたシンボリックリンクであっても、そのパス自体がパターンにマッチする限り、結果として返すことができるようになりました。
### `match_test.go` の変更
1. **パッケージ名の変更**: `package filepath` から `package filepath_test` に変更されました。これは、テストが `filepath` パッケージの外部から実行されることを示し、エクスポートされた関数のみをテストすることを意味します。これにより、テストがより現実的な使用シナリオを反映するようになります。
2. **新しいインポート**: `io/ioutil` と `os` が追加され、`path/filepath` パッケージ自体も `.` を使ってインポートされています (`. "path/filepath"`)。これにより、`filepath` パッケージのエクスポートされた関数をプレフィックスなしで直接呼び出すことができます(例: `Glob` の代わりに `Glob`)。
3. **`globSymlinkTests` 構造体**:
* `path`: シンボリックリンクの参照元となるファイル名。
* `dest`: 作成されるシンボリックリンクのファイル名。
* `brokenLink`: シンボリックリンクを壊すかどうかを示すブール値。
4. **`TestGlobSymlink` 関数**:
* **OSチェック**: WindowsとPlan 9ではシンボリックリンクの挙動が異なるため、これらのOSではテストをスキップします。
* **一時ディレクトリの作成**: `ioutil.TempDir` を使用して、テスト用のクリーンな一時ディレクトリを作成します。テスト終了時には `defer os.RemoveAll(tmpDir)` で確実に削除されます。
* **テストケースのループ**: `globSymlinkTests` の各要素に対してループ処理を行います。
* **ファイルとシンボリックリンクの作成**:
* `os.Create(path)` で元のファイルを作成します。
* `os.Symlink(path, dest)` で元のファイルへのシンボリックリンクを作成します。
* **シンボリックリンクの破壊**: `tt.brokenLink` が `true` の場合、`os.Remove(path)` で元のファイルを削除し、シンボリックリンクを壊します。
* **`Glob` の呼び出しと検証**:
* `Glob(dest)` を呼び出し、シンボリックリンクパスが正しくマッチングされることを確認します。
* `err != nil` でエラーが発生しないことを確認します。
* `!contains(matches, dest)` で、返されたマッチング結果にシンボリックリンクパスが含まれていることを確認します。`contains` 関数は、スライスが特定の文字列を含むかどうかをチェックするヘルパー関数です(このdiffには含まれていませんが、テストファイル内に存在すると仮定されます)。
この新しいテストは、`Glob` 関数が正常なシンボリックリンクと壊れたシンボリックリンクの両方を正しく処理し、期待される結果を返すことを保証します。
## 関連リンク
* Go Issue #6463: [https://github.com/golang/go/issues/6463](https://github.com/golang/go/issues/6463)
* Go CL 69870050: [https://golang.org/cl/69870050](https://golang.org/cl/69870050)
## 参考にした情報源リンク
* Go `os` package documentation: [https://pkg.go.dev/os](https://pkg.go.dev/os)
* Go `path/filepath` package documentation: [https://pkg.go.dev/path/filepath](https://pkg.go.dev/path/filepath)
* シンボリックリンク (Wikipedia): [https://ja.wikipedia.org/wiki/%E3%82%B7%E3%83%B3%E3%83%9C%E3%83%AA%E3%83%83%E3%82%AF%E3%83%AA%E3%83%B3%E3%82%AF](https://ja.wikipedia.org/wiki/%E3%82%B7%E3%83%B3%E3%83%9C%E3%83%AA%E3%83%83%E3%82%AF%E3%83%AA%E3%83%B3%E3%82%AF)
* `os.Stat` vs `os.Lstat` in Go: [https://stackoverflow.com/questions/21084678/os-stat-vs-os-lstat-in-go](https://stackoverflow.com/questions/21084678/os-stat-vs-os-lstat-in-go) (Stack Overflow, 一般的な情報源として)