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

[インデックス 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) は、指定されたシェル形式のパターンにマッチするすべてのファイル名またはディレクトリ名を返します。パターンには、*(任意の文字列にマッチ)、?(任意の一文字にマッチ)、[](文字の範囲またはセットにマッチ)などのワイルドカード文字を含めることができます。

シンボリックリンクは、ファイルシステム内の別のファイルやディレクトリへの参照(ポインタ)です。これは、WindowsのショートカットやUnix/Linuxのソフトリンクに似ています。シンボリックリンク自体は非常に小さなファイルで、その内容は参照先のパスを保持しています。

壊れたシンボリックリンクとは、そのシンボリックリンクが指し示している元のファイルやディレクトリが、すでに存在しない場合に発生します。シンボリックリンク自体はファイルシステム上に存在しますが、その参照先が失われている状態です。

os.Statos.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.Lstatname がシンボリックリンクである場合、そのシンボリックリンク自体の情報を返します。シンボリックリンクの指す先が存在するかどうかは確認しません。これにより、壊れたシンボリックリンクであっても、そのシンボリックリンク自体の情報を取得し、エラーを発生させずに存在を確認することができます。

技術的詳細

このコミットの核心は、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 関数によって正しく処理されることを確認します。

テストの概要:

  1. 一時ディレクトリを作成します。
  2. globSymlinkTests という構造体スライスを定義し、テストケース(元のファイルパス、シンボリックリンクパス、壊れたリンクかどうか)を格納します。
  3. 各テストケースについて、以下の操作を行います。
    • 元のファイルを作成します。
    • 元のファイルへのシンボリックリンクを作成します。
    • もし brokenLinktrue の場合、元のファイルを削除してシンボリックリンクを壊します。
    • 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, 一般的な情報源として)