[インデックス 12644] ファイルの概要
このコミットは、Go言語のos
パッケージにおいて、Windows環境で/dev/null
に相当するDevNull
ファイル(Windowsでは通常NUL
デバイス)に対してStat
システムコールを実行した際に、適切なファイル情報(FileInfo
)が返されない問題を修正するものです。具体的には、DevNull
に対するStat
呼び出しがエラーを返すか、不正確な情報を返すため、Goのテストやアプリケーションが予期せぬ動作をする可能性がありました。このコミットは、DevNull
に対してはWindows APIを介さずに、Goランタイムが「発明した」固定のFileInfo
を返すようにすることで、この問題を解決しています。
コミット
commit 4b872d61fe49ecd2ccca4dd8b285e4777d660932
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Thu Mar 15 16:33:45 2012 +1100
os: return some invented data from Stat(DevNull) on windows
Fixes #3321.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/5831043
---
src/pkg/os/os_test.go | 19 +++++++++++++++++++
src/pkg/os/stat_windows.go | 22 ++++++++++++++++++++++
2 files changed, 41 insertions(+)
diff --git a/src/pkg/os/os_test.go b/src/pkg/os/os_test.go
index d1e241f006..dec80cc091 100644
--- a/src/pkg/os/os_test.go
+++ b/src/pkg/os/os_test.go
@@ -1047,3 +1047,22 @@ func TestSameFile(t *testing.T) {
\tt.Errorf(\"files should be different\")
}\n }\n+\n+func TestDevNullFile(t *testing.T) {\n+\tf, err := Open(DevNull)\n+\tif err != nil {\n+\t\tt.Fatalf(\"Open(%s): %v\", DevNull, err)\n+\t}\n+\tdefer f.Close()\n+\tfi, err := f.Stat()\n+\tif err != nil {\n+\t\tt.Fatalf(\"Stat(%s): %v\", DevNull, err)\n+\t}\n+\tname := filepath.Base(DevNull)\n+\tif fi.Name() != name {\n+\t\tt.Fatalf(\"wrong file name have %v want %v\", fi.Name(), name)\n+\t}\n+\tif fi.Size() != 0 {\n+\t\tt.Fatalf(\"wrong file size have %d want 0\", fi.Size())\n+\t}\n+}\ndiff --git a/src/pkg/os/stat_windows.go b/src/pkg/os/stat_windows.go
index 6841748345..75351c805a 100644
--- a/src/pkg/os/stat_windows.go
+++ b/src/pkg/os/stat_windows.go
@@ -21,6 +21,9 @@ func (file *File) Stat() (fi FileInfo, err error) {\n \t\t// I don\'t know any better way to do that for directory\n \t\treturn Stat(file.name)\n \t}\n+\tif file.name == DevNull {\n+\t\treturn statDevNull()\n+\t}\n \tvar d syscall.ByHandleFileInformation\n \te := syscall.GetFileInformationByHandle(syscall.Handle(file.fd), &d)\n \tif e != nil {\n@@ -41,6 +44,9 @@ func Stat(name string) (fi FileInfo, err error) {\n \tif len(name) == 0 {\n \t\treturn nil, &PathError{\"Stat\", name, syscall.Errno(syscall.ERROR_PATH_NOT_FOUND)}\n \t}\n+\tif name == DevNull {\n+\t\treturn statDevNull()\n+\t}\n \tvar d syscall.Win32FileAttributeData\n \te := syscall.GetFileAttributesEx(syscall.StringToUTF16Ptr(name), syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&d)))\n \tif e != nil {\n@@ -69,6 +75,22 @@ func Lstat(name string) (fi FileInfo, err error) {\n \treturn Stat(name)\n }\n \n+// statDevNull return FileInfo structure describing DevNull file (\"NUL\").\n+// It creates invented data, since none of windows api will return\n+// that information.\n+func statDevNull() (fi FileInfo, err error) {\n+\treturn &fileStat{\n+\t\tname: DevNull,\n+\t\tmode: ModeDevice | ModeCharDevice | 0666,\n+\t\tsys: &winSys{\n+\t\t\t// hopefully this will work for SameFile\n+\t\t\tvol: 0,\n+\t\t\tidxhi: 0,\n+\t\t\tidxlo: 0,\n+\t\t},\n+\t}, nil\n+}\n+\n // basename removes trailing slashes and the leading\n // directory name and drive letter from path name.\n func basename(name string) string {\n```
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/4b872d61fe49ecd2ccca4dd8b285e4777d660932](https://github.com/golang/go/commit/4b872d61fe49ecd2ccca4dd8b285e4777d660932)
## 元コミット内容
os: return some invented data from Stat(DevNull) on windows
Fixes #3321.
R=golang-dev, bradfitz CC=golang-dev https://golang.org/cl/5831043
## 変更の背景
この変更は、Go言語の`os`パッケージがWindows環境で`DevNull`(`/dev/null`に相当する特殊なファイル)に対して`Stat`システムコールを実行した際に発生する問題を解決するために導入されました。
Goの`os`パッケージは、ファイルやディレクトリに関する情報(サイズ、パーミッション、更新日時など)を取得するために`Stat`関数を提供しています。これは内部的にOSのシステムコールを呼び出して情報を取得します。しかし、Windowsにおける`NUL`デバイス(`DevNull`)は、通常のファイルとは異なる特殊なデバイスであり、標準的なファイル情報取得API(`GetFileInformationByHandle`や`GetFileAttributesEx`など)では、その特性上、期待されるような完全なファイル情報を返しません。
具体的には、`NUL`デバイスはファイルシステム上の実体を持たないため、ファイルサイズやインデックスなどの情報が不定であったり、エラーを返したりすることがありました。これにより、`os.Stat(os.DevNull)`のような呼び出しが失敗したり、不正確な情報を返したりするため、Goのプログラムが`DevNull`の情報を必要とする場合に問題が生じていました。例えば、テストコードで`DevNull`のサイズが0であることを確認しようとすると、`Stat`がエラーを返すためにテストが失敗するといった事態が発生していました。
このコミットは、このようなWindows特有の`DevNull`の振る舞いに対応し、Goの`os`パッケージがクロスプラットフォームで一貫した動作をするようにするために必要とされました。
## 前提知識の解説
### `/dev/null` と `NUL` デバイス
* **`/dev/null` (Unix/Linux)**: Unix系OSにおける特殊なファイルで、「ビットバケツ」や「ブラックホール」とも呼ばれます。このファイルに書き込まれたデータはすべて破棄され、このファイルから読み込もうとすると常にEOF(End Of File)が返されます。これは、プログラムの出力を無視したり、空の入力を提供したりする際によく使用されます。
* **`NUL` デバイス (Windows)**: Windowsにおける`/dev/null`に相当する特殊なデバイスです。機能的には`/dev/null`とほぼ同じで、書き込まれたデータを破棄し、読み込み時にはEOFを返します。Windowsでは、ファイルパスとして`NUL`(大文字小文字を区別しない)を使用することでアクセスできます。Go言語の`os`パッケージでは、この`NUL`デバイスのパスを`os.DevNull`定数として提供しています。
### `Stat` システムコールと `FileInfo`
* **`Stat` (System Call)**: オペレーティングシステムが提供するシステムコールの一つで、指定されたファイルやディレクトリに関するメタデータ(ファイルサイズ、最終更新日時、パーミッション、所有者、デバイスID、inode番号など)を取得するために使用されます。Go言語の`os`パッケージの`os.Stat()`関数は、このシステムコールを抽象化したものです。
* **`FileInfo` (Go言語)**: Go言語の`os`パッケージで定義されているインターフェースで、`Stat`関数が返すファイル情報を表現します。このインターフェースは、ファイル名、サイズ、パーミッション、最終更新日時、ファイルの種類(ディレクトリ、通常ファイル、シンボリックリンクなど)、および基盤となるシステム固有の情報(`Sys()`メソッドで取得)を提供します。
### Windows API とファイル情報取得
Windowsでは、ファイルやデバイスの情報を取得するためにいくつかのAPIが提供されています。
* **`GetFileInformationByHandle`**: 開いているファイルハンドル(`HANDLE`)からファイル情報を取得します。
* **`GetFileAttributesEx`**: ファイルパスからファイル属性情報を取得します。
これらのAPIは通常のファイルに対しては適切に機能しますが、`NUL`デバイスのような特殊なデバイスに対しては、その特性上、期待通りの情報を返さないことがあります。例えば、`NUL`デバイスはディスク上の物理的な場所を持たないため、ファイルインデックス(`idxhi`, `idxlo`)やボリュームシリアル番号(`vol`)といった情報が意味を持たなかったり、APIがエラーを返したりすることがあります。
## 技術的詳細
このコミットの技術的な核心は、Windows環境における`DevNull`(`NUL`デバイス)の`Stat`処理を特別扱いすることにあります。通常のファイルやディレクトリに対する`Stat`はWindows API(`GetFileInformationByHandle`や`GetFileAttributesEx`)を呼び出して情報を取得しますが、`DevNull`に対してはこれらのAPIが適切に機能しないため、Goランタイムが「発明した」固定の`FileInfo`構造体を返すように変更されました。
具体的には、以下の点が重要です。
1. **`DevNull`の特別扱い**: `src/pkg/os/stat_windows.go`内の`(*File) Stat()`メソッドと`Stat(name string)`関数において、引数として渡されたファイル名が`DevNull`(つまり`NUL`)であるかどうかをチェックします。
2. **`statDevNull()`関数の導入**: `DevNull`であると判断された場合、新しく導入された`statDevNull()`関数が呼び出されます。この関数はWindows APIを呼び出す代わりに、`DevNull`に特化した`FileInfo`構造体(内部的には`fileStat`型)を生成して返します。
3. **「発明された」データ**: `statDevNull()`関数が生成する`FileInfo`には、以下の「発明された」データが含まれます。
* `name`: `DevNull`("NUL")
* `mode`: `ModeDevice | ModeCharDevice | 0666`
* `ModeDevice`: デバイスファイルであることを示すフラグ。
* `ModeCharDevice`: キャラクターデバイス(バイトストリームとしてアクセスされるデバイス)であることを示すフラグ。
* `0666`: 読み書き可能なパーミッション。
* `size`: `0` (常にサイズは0)
* `sys`: `winSys`構造体
* `vol`: `0`
* `idxhi`: `0`
* `idxlo`: `0`
これらの値は、`SameFile`関数(2つのファイルが同じファイルであるかを判断する関数)が`DevNull`に対して正しく機能するように、便宜的に設定されています。Windowsでは、ファイルの同一性はボリュームシリアル番号とファイルインデックスの組み合わせで判断されるため、これらの値を固定することで`DevNull`が常に自身と同一であると判断されるようにしています。
このアプローチにより、`os.Stat(os.DevNull)`はWindows上でもエラーを返さずに、期待される(サイズが0でデバイスファイルである)`FileInfo`を返すようになります。これにより、`DevNull`を扱うGoプログラムの堅牢性とクロスプラットフォーム互換性が向上します。
## コアとなるコードの変更箇所
このコミットでは、主に以下の2つのファイルが変更されています。
1. **`src/pkg/os/os_test.go`**:
* `TestDevNullFile`という新しいテスト関数が追加されました。このテストは、`os.DevNull`を開き、`Stat()`を呼び出し、返された`FileInfo`の名前が`DevNull`のベース名と一致し、サイズが0であることを確認します。これにより、`DevNull`に対する`Stat`の振る舞いが期待通りであることを保証します。
2. **`src/pkg/os/stat_windows.go`**:
* `(*File) Stat()`メソッドと`Stat(name string)`関数に、引数として渡されたファイル名が`DevNull`であるかをチェックする条件分岐が追加されました。
* もしファイル名が`DevNull`であれば、新しく定義された`statDevNull()`関数を呼び出し、その結果を返します。
* `statDevNull()`という新しい関数が追加されました。この関数は、Windows APIを呼び出すことなく、`DevNull`に特化した`FileInfo`構造体(`fileStat`型)を生成して返します。この`FileInfo`には、`DevNull`の名前、デバイスファイルを示すモード、サイズ0、そして`SameFile`関数が正しく機能するためのダミーのシステム情報(ボリュームIDとファイルインデックス)が含まれます。
## コアとなるコードの解説
### `src/pkg/os/os_test.go` の変更
```go
func TestDevNullFile(t *testing.T) {
f, err := Open(DevNull)
if err != nil {
t.Fatalf("Open(%s): %v", DevNull, err)
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
t.Fatalf("Stat(%s): %v", DevNull, err)
}
name := filepath.Base(DevNull)
if fi.Name() != name {
t.Fatalf("wrong file name have %v want %v", fi.Name(), name)
}
if fi.Size() != 0 {
t.Fatalf("wrong file size have %d want 0", fi.Size())
}
}
このテスト関数は、os.DevNull
に対して以下のことを確認します。
os.Open(DevNull)
がエラーなく成功すること。- 開いたファイルディスクリプタ
f
に対してf.Stat()
がエラーなく成功すること。 - 返された
FileInfo
のName()
がDevNull
のベース名(Windowsでは通常 "NUL")と一致すること。 - 返された
FileInfo
のSize()
が0
であること。
このテストの追加により、Windows環境でDevNull
に対するStat
の振る舞いが、Goの期待する仕様に合致していることが自動的に検証されるようになりました。
src/pkg/os/stat_windows.go
の変更
func (file *File) Stat() (fi FileInfo, err error) {
// ... 既存のコード ...
if file.name == DevNull {
return statDevNull()
}
// ... 既存のコード ...
}
func Stat(name string) (fi FileInfo, err error) {
// ... 既存のコード ...
if name == DevNull {
return statDevNull()
}
// ... 既存のコード ...
}
// statDevNull return FileInfo structure describing DevNull file ("NUL").
// It creates invented data, since none of windows api will return
// that information.
func statDevNull() (fi FileInfo, err error) {
return &fileStat{
name: DevNull,
mode: ModeDevice | ModeCharDevice | 0666,
sys: &winSys{
// hopefully this will work for SameFile
vol: 0,
idxhi: 0,
idxlo: 0,
},
}, nil
}
(*File) Stat()
とStat(name string)
の変更は非常にシンプルで、DevNull
に対する呼び出しをstatDevNull()
関数にリダイレクトしています。これにより、通常のファイルとは異なる特殊な処理パスが提供されます。statDevNull()
関数がこのコミットの核心です。name: DevNull
: ファイル名をDevNull
("NUL")に設定します。mode: ModeDevice | ModeCharDevice | 0666
:DevNull
がデバイスファイルであり、かつキャラクターデバイス(バイトストリームとして扱われる)であることを示し、読み書き可能なパーミッション(0666
)を設定します。これは、Unix系OSの/dev/null
のパーミッションと種類に合わせたものです。sys: &winSys{vol: 0, idxhi: 0, idxlo: 0}
:sys
フィールドは、OS固有の情報を格納するためのものです。WindowsではwinSys
構造体が使用されます。vol
(ボリュームシリアル番号)、idxhi
(ファイルインデックスの上位)、idxlo
(ファイルインデックスの下位)は、Windowsでファイルの同一性を識別するために使われる情報です。DevNull
は物理的なファイルではないため、これらの値は意味を持ちませんが、os.SameFile
関数がDevNull
に対して正しく機能するように、便宜的に0
に設定されています。これにより、SameFile
はDevNull
が常に自身と同一であると判断するようになります。
この変更により、Goのos
パッケージはWindows上でもDevNull
に対して一貫した、かつ期待されるFileInfo
を提供できるようになり、クロスプラットフォームでの動作の信頼性が向上しました。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/4b872d61fe49ecd2ccca4dd8b285e4777d660932
- Go CL (Change List): https://golang.org/cl/5831043
参考にした情報源リンク
- Go言語の
os
パッケージに関する公式ドキュメント - Windowsの
NUL
デバイスに関する情報 - Windows API (GetFileInformationByHandle, GetFileAttributesEx) に関するMicrosoftのドキュメント
- Go言語の
os.FileInfo
インターフェースに関する情報 - Go言語の
os.SameFile
関数に関する情報 - Go言語のIssueトラッカー (Issue #3321) - (ただし、今回の検索では直接的な情報は見つからず、コミットメッセージから推測)