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

[インデックス 17094] ファイルの概要

このコミットは、Go言語の標準ライブラリ os パッケージにおけるファイルシステム操作、特にディレクトリの内容を読み取る Readdir メソッドの挙動を修正するものです。具体的には、src/pkg/os/export_test.gosrc/pkg/os/file_unix.gosrc/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 (シンボリックリンクの情報を取得するシステムコール) の呼び出しが失敗した場合、そのエントリに対応する FileInfonil になってしまう可能性がありました。これは、例えばファイルシステムが破損している場合や、アクセス権の問題がある場合に発生し得ます。

このような挙動は、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 がエラーを返した場合、fipnil になり、その 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.govar lstat = Lstat というグローバル変数を導入し、File.readdir 内で Lstat の代わりにこの lstat 変数を使用するように変更することで実現されています。そして、src/pkg/os/export_test.govar LstatP = &lstat をエクスポートすることで、テストコードがこの lstat 変数のポインタを介して Lstat の挙動を一時的に変更できるようになります。これにより、Lstat が特定のファイルに対してエラーを返すシナリオを簡単にシミュレートし、Readdir の修正が正しく機能するかを検証できるようになります。

新しいテストケース TestReaddirWithBadLstat は、このオーバーライド機能を利用して、特定のファイルに対して LstatErrInvalid を返すように設定し、その状況下で 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とは直接関連しないようです。)