[インデックス 17094] ファイルの概要
このコミットは、Go言語の標準ライブラリ os
パッケージにおけるファイルシステム操作、特にディレクトリの内容を読み取る Readdir
メソッドの挙動を修正するものです。具体的には、src/pkg/os/export_test.go
、src/pkg/os/file_unix.go
、src/pkg/os/os_unix_test.go
の3つのファイルが変更されています。
src/pkg/os/export_test.go
: テストのために内部変数をエクスポートするファイル。lstat
関数をテストからオーバーライドできるようにするための変更が含まれます。src/pkg/os/file_unix.go
: Unix系システムにおけるファイル操作の実装が含まれるファイル。File.Readdir
メソッドのロジックが修正されています。src/pkg/os/os_unix_test.go
: Unix系システムにおけるos
パッケージのテストファイル。Readdir
の新しいテストケースが追加されています。
コミット
commit bdbd5418f4c2dc87fa18e6b39e4ead21c6d87bbe
Author: Pieter Droogendijk <pieter@binky.org.uk>
Date: Thu Aug 8 10:44:01 2013 -0700
os: make Readdir work as documented
Readdir's result should never contain a nil.
Fixes #5960.
R=golang-dev, rsc, bradfitz
CC=golang-dev
https://golang.org/cl/12261043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/bdbd5418f4c2dc87fa18e6b39e4ead21c6d87bbe
元コミット内容
os: make Readdir work as documented
Readdir's result should never contain a nil.
Fixes #5960.
このコミットは、os
パッケージの Readdir
メソッドがドキュメント通りに動作するように修正することを目的としています。具体的には、Readdir
が返す []FileInfo
スライスに nil
が含まれるべきではないという仕様に準拠させるための変更です。これは内部的な問題追跡システムにおける Issue #5960 を修正します。
変更の背景
Go言語の os
パッケージには、ディレクトリの内容を読み取るための File.Readdir
メソッドがあります。このメソッドは、ディレクトリ内のファイルやサブディレクトリの情報を []FileInfo
型のスライスとして返します。FileInfo
インターフェースは、ファイル名、サイズ、パーミッション、最終更新時刻などのファイルメタデータを提供します。
Readdir
のドキュメントには、返される []FileInfo
スライスには nil
が含まれないことが明記されていました。しかし、実際の動作では、ディレクトリ内のエントリに対して Lstat
(シンボリックリンクの情報を取得するシステムコール) の呼び出しが失敗した場合、そのエントリに対応する FileInfo
が nil
になってしまう可能性がありました。これは、例えばファイルシステムが破損している場合や、アクセス権の問題がある場合に発生し得ます。
このような挙動は、Readdir
の結果を処理するアプリケーションにとって予期せぬ nil
ポインタ参照を引き起こす可能性があり、プログラムのクラッシュや誤動作の原因となります。このコミットは、このドキュメントと実装の乖離を解消し、Readdir
が常に有効な FileInfo
オブジェクト(たとえエラーが発生したエントリであっても、名前だけは持つ FileInfo
オブジェクト)を返すようにすることで、より堅牢なファイルシステム操作を保証することを目的としています。
前提知識の解説
os.File.Readdir(n int) ([]FileInfo, error)
このメソッドは、ディレクトリの内容を読み取り、最大 n
個の FileInfo
オブジェクトのスライスを返します。n
が0以下の場合、すべてのディレクトリの内容を読み取ります。返される FileInfo
スライスは、ディレクトリ内の各エントリ(ファイルやサブディレクトリ)のメタデータを含みます。
os.File.Readdirnames(n int) ([]string, error)
このメソッドは Readdir
と似ていますが、FileInfo
オブジェクトの代わりに、ディレクトリ内のエントリの名前(文字列)のスライスを返します。Readdir
は内部的に Readdirnames
を呼び出し、その後各名前に対して Lstat
を実行して FileInfo
を取得します。
os.Lstat(name string) (FileInfo, error)
この関数は、指定されたパスのファイルまたはディレクトリの FileInfo
を返します。Stat
とは異なり、Lstat
はシンボリックリンク自体に関する情報を返し、リンクが指す先のファイルの情報は返しません。ファイルが存在しない、またはアクセスできない場合、エラーを返します。
os.FileInfo
インターフェース
FileInfo
は、ファイルシステムオブジェクト(ファイル、ディレクトリ、シンボリックリンクなど)のメタデータを提供するインターフェースです。主なメソッドには Name() string
(ベース名)、Size() int64
(バイト単位のサイズ)、Mode() FileMode
(パーミッションと種類)、ModTime() time.Time
(最終更新時刻)、IsDir() bool
(ディレクトリかどうか)、Sys() interface{}
(基盤となるデータソースに依存する追加情報) などがあります。
Goにおける nil
とスライス
Go言語では、スライスは基盤となる配列への参照です。[]FileInfo
のようなスライスは、FileInfo
インターフェースを実装するオブジェクトのポインタを要素として持ちます。もし Lstat
のような操作が失敗した場合、その要素が nil
ポインタになる可能性があります。このコミットの目的は、Readdir
が返すスライス内の要素が nil
にならないようにすることです。つまり、FileInfo
オブジェクト自体は常に存在し、たとえエラーが発生したエントリであっても、その FileInfo
オブジェクトは少なくとも Name()
メソッドでファイル名を返すことができるようにします。
技術的詳細
このコミットの核心は、File.readdir
メソッド(Readdir
の内部実装)における Lstat
の呼び出しとエラーハンドリングの改善にあります。
変更前のコードでは、Readdirnames
で取得した各ファイル名に対して Lstat
を呼び出し、その結果を fip
に格納していました。もし Lstat
がエラーを返した場合、fip
は nil
になり、その nil
がそのまま fi[i]
に代入されていました。これにより、Readdir
が返す []FileInfo
スライスの中に nil
要素が含まれる可能性がありました。
変更後のコードでは、Lstat
がエラーを返した場合でも、fi[i]
には &fileStat{name: filename}
という新しい FileInfo
オブジェクトが代入されるようになりました。この fileStat
は、ファイル名だけを持つ最小限の FileInfo
実装であり、Sys()
メソッドは nil
を返しますが、Name()
メソッドは有効なファイル名を返します。これにより、Readdir
が返すスライス内のすべての要素が非 nil
であることが保証されます。
また、テスト容易性を向上させるために、os.Lstat
関数をテストからオーバーライドできるようにする変更も導入されています。これは、src/pkg/os/file_unix.go
で var lstat = Lstat
というグローバル変数を導入し、File.readdir
内で Lstat
の代わりにこの lstat
変数を使用するように変更することで実現されています。そして、src/pkg/os/export_test.go
で var LstatP = &lstat
をエクスポートすることで、テストコードがこの lstat
変数のポインタを介して Lstat
の挙動を一時的に変更できるようになります。これにより、Lstat
が特定のファイルに対してエラーを返すシナリオを簡単にシミュレートし、Readdir
の修正が正しく機能するかを検証できるようになります。
新しいテストケース TestReaddirWithBadLstat
は、このオーバーライド機能を利用して、特定のファイルに対して Lstat
が ErrInvalid
を返すように設定し、その状況下で Readdir
が期待通りに動作し、nil
ではない FileInfo
オブジェクトを返すことを確認しています。
コアとなるコードの変更箇所
src/pkg/os/export_test.go
--- a/src/pkg/os/export_test.go
+++ b/src/pkg/os/export_test.go
@@ -7,3 +7,4 @@ package os
// Export for testing.
var Atime = atime
+var LstatP = &lstat
src/pkg/os/file_unix.go
--- a/src/pkg/os/file_unix.go
+++ b/src/pkg/os/file_unix.go
@@ -149,6 +149,9 @@ func Lstat(name string) (fi FileInfo, err error) {
return fileInfoFromStat(&stat, name), nil
}
+// lstat is overridden in tests.
+var lstat = Lstat
+
func (f *File) readdir(n int) (fi []FileInfo, err error) {
dirname := f.name
if dirname == "" {
@@ -158,12 +161,14 @@ func (f *File) readdir(n int) (fi []FileInfo, err error) {
names, err := f.Readdirnames(n)
fi = make([]FileInfo, len(names))
for i, filename := range names {
-\t\tfip, lerr := Lstat(dirname + filename)
-\t\tif err == nil {
+\t\tfip, lerr := lstat(dirname + filename)
+\t\tif lerr == nil {
\t\t\tfi[i] = fip
-\t\t\terr = lerr
\t\t} else {
\t\t\tfi[i] = &fileStat{name: filename}
+\t\t\tif err == nil {
+\t\t\t\terr = lerr
+\t\t\t}\n \t\t}\n \t}\n \treturn fi, err
src/pkg/os/os_unix_test.go
--- a/src/pkg/os/os_unix_test.go
+++ b/src/pkg/os/os_unix_test.go
@@ -28,7 +28,7 @@ func checkUidGid(t *testing.T, path string, uid, gid int) {
}
func TestChown(t *testing.T) {\n-\t// Chown is not supported under windows or Plan 9.\n+\t// Chown is not supported under windows os Plan 9.\n \t// Plan9 provides a native ChownPlan9 version instead.\n \tif runtime.GOOS == "windows" || runtime.GOOS == "plan9" {\n \t\treturn\n@@ -74,3 +74,41 @@ func TestChown(t *testing.T) {\n \t\tcheckUidGid(t, f.Name(), int(sys.Uid), gid)\n \t}\n }\n+\n+func TestReaddirWithBadLstat(t *testing.T) {\n+\thandle, err := Open(sfdir)\n+\tfailfile := sfdir + "/" + sfname\n+\tif err != nil {\n+\t\tt.Fatalf("Couldn't open %s: %s", sfdir, err)\n+\t}\n+\n+\t*LstatP = func(file string) (FileInfo, error) {\n+\t\tif file == failfile {\n+\t\t\tvar fi FileInfo\n+\t\t\treturn fi, ErrInvalid\n+\t\t}\n+\t\treturn Lstat(file)\n+\t}\n+\tdefer func() { *LstatP = Lstat }()\n+\n+\tdirs, err := handle.Readdir(-1)\n+\tif err != ErrInvalid {\n+\t\tt.Fatalf("Expected Readdir to return ErrInvalid, got %v", err)\n+\t}\n+\tfoundfail := false\n+\tfor _, dir := range dirs {\n+\t\tif dir.Name() == sfname {\n+\t\t\tfoundfail = true\n+\t\t\tif dir.Sys() != nil {\n+\t\t\t\tt.Errorf("Expected Readdir for %s should not contain Sys", failfile)\n+\t\t\t}\n+\t\t} else {\n+\t\t\tif dir.Sys() == nil {\n+\t\t\t\tt.Errorf("Readdir for every file other than %s should contain Sys, but %s/%s didn't either", failfile, sfdir, dir.Name())\n+\t\t\t}\n+\t\t}\n+\t}\n+\tif !foundfail {\n+\t\tt.Fatalf("Expected %s from Readdir, but didn't find it", failfile)\n+\t}\n+}\n```
## コアとなるコードの解説
### `src/pkg/os/export_test.go` の変更
`var LstatP = &lstat` の追加は、テストコードが `os` パッケージの内部変数 `lstat` にアクセスし、その値を変更できるようにするためのものです。これにより、`Lstat` 関数の挙動をテスト時にモック(模擬)することが可能になり、ファイルシステムエラーなどの特定のシナリオを再現して `Readdir` の動作を検証できます。
### `src/pkg/os/file_unix.go` の変更
1. **`var lstat = Lstat` の導入**:
`Lstat` 関数への直接呼び出しを `lstat` 変数への呼び出しに置き換えることで、テスト時に `lstat` の値を別の関数に設定し、`Lstat` の実際の動作をシミュレートできるようになります。これは、Go言語でプライベートな関数をテストからオーバーライドする一般的なパターンです。
2. **`for` ループ内のエラーハンドリングの修正**:
変更前:
```go
fip, lerr := Lstat(dirname + filename)
if err == nil {
fi[i] = fip
err = lerr
} else {
fi[i] = &fileStat{name: filename}
}
```
変更後:
```go
fip, lerr := lstat(dirname + filename)
if lerr == nil {
fi[i] = fip
} else {
fi[i] = &fileStat{name: filename}
if err == nil {
err = lerr
}
}
```
この変更が最も重要です。
- `Lstat` の代わりに `lstat` 変数を使用するように変更されています。
- `lerr == nil` の条件が追加され、`Lstat` の呼び出しが成功した場合(`lerr` が `nil` の場合)にのみ、取得した `fip` (FileInfo) を `fi[i]` に代入します。
- `Lstat` の呼び出しが失敗した場合(`lerr` が `nil` でない場合)、`fi[i]` には `&fileStat{name: filename}` という新しい `FileInfo` オブジェクトが代入されます。この `fileStat` は、ファイル名だけを持つ最小限の `FileInfo` 実装であり、`nil` ではありません。これにより、`Readdir` が返すスライス内のすべての要素が非 `nil` であることが保証されます。
- エラーの伝播ロジックも修正されています。以前は、最初の `Lstat` エラーが発生した場合にのみ `err` 変数に代入されていましたが、新しいロジックでは、`Readdir` 全体で最初のエラーを保持しつつ、個々の `Lstat` エラーが発生しても `FileInfo` スライスに `nil` を含めないようにしています。
### `src/pkg/os/os_unix_test.go` の変更
`TestReaddirWithBadLstat` という新しいテスト関数が追加されています。
このテストは以下の手順を実行します。
1. テスト用のディレクトリを開きます。
2. `*LstatP` をオーバーライドし、特定のファイル (`failfile`) に対して `Lstat` が `ErrInvalid` エラーを返すように設定します。他のファイルに対しては通常の `Lstat` を呼び出します。
3. `defer` ステートメントを使用して、テスト終了後に `LstatP` を元の `Lstat` に戻すように設定し、テスト後のクリーンアップを保証します。
4. `handle.Readdir(-1)` を呼び出し、すべてのディレクトリ内容を読み取ります。
5. `Readdir` が `ErrInvalid` を返すことを確認します。これは、`failfile` に対する `Lstat` がエラーを返したためです。
6. 返された `dirs` スライスをイテレートし、以下のことを確認します。
- `failfile` に対応する `FileInfo` オブジェクトが存在し、その `Name()` が正しいことを確認します。
- `failfile` に対応する `FileInfo` の `Sys()` メソッドが `nil` であることを確認します。これは、エラーが発生したエントリに対してはシステム固有の情報が取得できないためです。
- `failfile` 以外のファイルに対応する `FileInfo` オブジェクトの `Sys()` メソッドが `nil` でないことを確認します。これは、それらのファイルに対しては `Lstat` が成功し、完全な `FileInfo` が取得されているためです。
7. `failfile` が `Readdir` の結果に含まれていることを確認します。
このテストは、`Readdir` がエラーが発生したエントリに対しても `nil` ではない `FileInfo` オブジェクトを返し、その `FileInfo` が少なくともファイル名を持つことを保証するという、このコミットの主要な目的を効果的に検証しています。
## 関連リンク
- Go CL (Change List): [https://golang.org/cl/12261043](https://golang.org/cl/12261043)
## 参考にした情報源リンク
- Go CL (Change List): [https://golang.org/cl/12261043](https://golang.org/cl/12261043)
- Go言語の `os` パッケージのドキュメント (当時のバージョン): `os.File.Readdir` のドキュメントは、返されるスライスに `nil` が含まれないことを明記しています。
(注: `Fixes #5960` は、Goプロジェクトの内部的なIssueトラッカーの番号である可能性が高く、GitHubの公開Issueとは直接関連しないようです。)