[インデックス 12222] ファイルの概要
このコミットは、Go言語の標準ライブラリos
パッケージにおいて、Windows環境でのSameFile
関数の実装を追加し、既存のバグ(Issue 2511)を修正するものです。具体的には、ファイルの同一性を正確に判断するために、Windows固有のファイル識別子(ボリュームシリアル番号とファイルインデックス)を利用するように変更が加えられています。
コミット
commit c7482b919619b459cb68e3a0c681afa1c3425dc4
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Mon Feb 27 12:29:33 2012 +1100
os: implement sameFile on windows
Fixes #2511.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5687072
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c7482b919619b459cb68e3a0c681afa1c3425dc4
元コミット内容
このコミットは、Go言語のos
パッケージにおけるSameFile
関数のWindows実装に関するものです。以前のSameFile
関数は、Windows環境では常にtrue
を返しており、異なるファイルであっても同一であると誤って判断してしまうバグがありました。このコミットは、Windowsのファイルシステムが提供する一意のファイル識別子(ボリュームシリアル番号とファイルインデックス)を利用して、この問題を修正し、ファイルの同一性を正確に判定できるようにします。
変更の背景
Go言語のos
パッケージには、SameFile
という関数が存在します。この関数は、2つのFileInfo
インターフェースが参照するファイルが、同じファイルシステム上の同じファイルであるかどうかを判断するために使用されます。Unix系システムでは、ファイルのデバイスIDとinode番号を比較することでファイルの同一性を判断できます。しかし、Windowsシステムでは、これらの概念が直接的に適用できるわけではありませんでした。
コミットメッセージにあるFixes #2511
は、この変更がGoのIssue 2511を解決することを示しています。Issue 2511は、「os.SameFile
がWindowsで常にtrue
を返す」というバグ報告でした。これは、シンボリックリンクやハードリンク、あるいは単に異なるパスから同じファイルを参照している場合に、SameFile
が正しく機能しないことを意味していました。このバグは、ファイル操作の正確性や信頼性に影響を与えるため、修正が急務でした。
前提知識の解説
os.SameFile
関数
os.SameFile(fi1, fi2 FileInfo) bool
は、Go言語のos
パッケージに存在する関数で、2つのFileInfo
インターフェースが記述するファイルが、同じファイルシステム上の同じファイルであるかどうかを判定します。これは、ファイルパスが異なっていても、実体が同じファイルである場合にtrue
を返すことを目的としています。例えば、ハードリンクやシンボリックリンク、あるいは異なる相対パスで同じファイルを参照している場合などに有用です。
FileInfo
インターフェース
FileInfo
インターフェースは、ファイルに関する情報(ファイル名、サイズ、更新時刻、パーミッション、ディレクトリかどうかなど)を提供するGoの標準インターフェースです。Stat()
関数やLstat()
関数によって返されます。このインターフェースには、Sys()
というメソッドがあり、これは基盤となるシステム固有の情報を返すために使用されます。SameFile
関数は、このSys()
メソッドが返すシステム固有の情報を利用してファイルの同一性を判断します。
Windowsファイルシステムにおけるファイルの識別
Unix系システムでは、ファイルはデバイスIDとinode番号の組み合わせで一意に識別されます。しかし、WindowsのNTFSファイルシステムでは、これに相当する概念として「ボリュームシリアル番号 (Volume Serial Number)」と「ファイルインデックス (File Index)」の組み合わせが使用されます。
- ボリュームシリアル番号 (Volume Serial Number): ファイルが存在する論理ドライブ(ボリューム)を一意に識別する番号です。
- ファイルインデックス (File Index): ボリューム内でファイルを一意に識別する番号です。これは、ファイルが作成されたときに割り当てられ、ファイルが移動しても同じボリューム内であれば変更されません。
これらの情報は、Windows APIのGetFileInformationByHandle
関数やGetFileAttributesEx
関数などを用いて取得できます。SameFile
をWindowsで正しく実装するためには、これらのシステム固有の識別子を比較する必要があります。
syscall
パッケージ
Go言語のsyscall
パッケージは、オペレーティングシステムが提供する低レベルのプリミティブ(システムコール)へのアクセスを提供します。Windows固有のファイルシステム情報を取得するためには、このパッケージを通じてWindows APIを呼び出す必要があります。
技術的詳細
このコミットの主要な変更点は、Windows環境におけるos.SameFile
関数の実装を、ファイルのボリュームシリアル番号とファイルインデックスに基づいて行うように修正したことです。
-
fileStat
構造体の拡張とwinSys
構造体の導入:- 以前は
FileInfo
のSys()
メソッドがwinTimes
という構造体を返していましたが、このコミットではwinSys
という新しい構造体が導入されました。 winSys
構造体は、ファイルのパス、アクセス時刻、作成時刻に加えて、vol
(ボリュームシリアル番号)、idxhi
(ファイルインデックスの上位32ビット)、idxlo
(ファイルインデックスの下位32ビット) を保持するように設計されています。fileStat
構造体(FileInfo
インターフェースの実装)のsys
フィールドが、このwinSys
型を指すように変更されました。
- 以前は
-
ファイル識別子の取得ロジックの追加:
mkSysFromFI
関数が新しく追加され、syscall.ByHandleFileInformation
構造体からwinSys
構造体を生成する際に、ボリュームシリアル番号とファイルインデックスを抽出して設定します。winSys
構造体にはloadFileId()
というメソッドが追加されました。このメソッドは、必要に応じてファイルのパスからCreateFile
とGetFileInformationByHandle
を呼び出し、ファイルのボリュームシリアル番号とファイルインデックスを取得してwinSys
構造体のフィールドを更新します。これにより、FileInfo
が作成された時点ではファイル識別子を持っていなくても、SameFile
が呼び出されたときに遅延ロードされるようになります。
-
sameFile
関数の修正:sameFile
関数(os.SameFile
の実体)は、引数として渡された2つのFileInfo
のSys()
メソッドが返すwinSys
構造体を取り出します。- それぞれの
winSys
構造体に対してloadFileId()
を呼び出し、ファイル識別子を確実にロードします。 - 最終的に、2つの
winSys
構造体のvol
、idxhi
、idxlo
の各フィールドを比較し、すべてが一致する場合にのみtrue
を返します。これにより、Windows上でのファイルの同一性が正確に判断されるようになります。
-
パスの正規化と絶対パスへの変換:
Stat
関数やopenDir
関数内で、渡されたパスが相対パスである場合に、Getwd()
(現在の作業ディレクトリを取得)と組み合わせて絶対パスに変換するロジックが追加されました。これは、GetFileInformationByHandle
などのWindows APIが正確なファイル情報を取得するために絶対パスを必要とするためです。isAbs
やvolumeName
といったヘルパー関数が追加され、Windowsパスの絶対性やボリューム名の解析をサポートします。
-
テストケースの追加:
src/pkg/os/os_test.go
にTestSameFile
という新しいテスト関数が追加されました。このテストは、同じファイル(作成後にStat
を2回呼び出す)と異なるファイル(異なる名前で作成)に対してSameFile
が期待通りに動作するかを確認します。
コアとなるコードの変更箇所
src/pkg/os/file_windows.go
--- a/src/pkg/os/file_windows.go
+++ b/src/pkg/os/file_windows.go
@@ -52,6 +52,7 @@ func NewFile(fd uintptr, name string) *File {
type dirInfo struct {
data syscall.Win32finddata
needdata bool
+ path string
}
const DevNull = "NUL"
@@ -79,6 +80,11 @@ func openDir(name string) (file *File, err error) {
if e != nil {
return nil, &PathError{"open", name, e}
}\n+\td.path = name
+\tif !isAbs(d.path) {
+\t\tcwd, _ := Getwd()
+\t\td.path = cwd + `\` + d.path
+\t}
f := NewFile(uintptr(r), name)
f.dirinfo = d
return f, nil
@@ -171,7 +177,13 @@ func (file *File) readdir(n int) (fi []FileInfo, err error) {
if name == "." || name == ".." { // Useless names
continue
}
-\t\tf := toFileInfo(name, d.FileAttributes, d.FileSizeHigh, d.FileSizeLow, d.CreationTime, d.LastAccessTime, d.LastWriteTime)
+\t\tf := &fileStat{
+\t\t\tname: name,
+\t\t\tsize: mkSize(d.FileSizeHigh, d.FileSizeLow),
+\t\t\tmodTime: mkModTime(d.LastWriteTime),
+\t\t\tmode: mkMode(d.FileAttributes),
+\t\t\tsys: mkSys(file.dirinfo.path+`\`+name, d.LastAccessTime, d.CreationTime),
+\t\t}
n--
fi = append(fi, f)
}
src/pkg/os/os_test.go
--- a/src/pkg/os/os_test.go
+++ b/src/pkg/os/os_test.go
@@ -1014,3 +1014,38 @@ func TestNilProcessStateString(t *testing.T) {
\tt.Errorf("(*ProcessState)(nil).String() = %q, want %q", s, "<nil>")
}\n}\n+\n+func TestSameFile(t *testing.T) {
+\tfa, err := Create("a")
+\tif err != nil {
+\t\tt.Fatalf("Create(a): %v", err)
+\t}
+\tdefer Remove(fa.Name())
+\tfa.Close()
+\tfb, err := Create("b")
+\tif err != nil {
+\t\tt.Fatalf("Create(b): %v", err)
+\t}
+\tdefer Remove(fb.Name())
+\tfb.Close()
+\n+\tia1, err := Stat("a")
+\tif err != nil {
+\t\tt.Fatalf("Stat(a): %v", err)
+\t}
+\tia2, err := Stat("a")
+\tif err != nil {
+\t\tt.Fatalf("Stat(a): %v", err)
+\t}
+\tif !SameFile(ia1, ia2) {
+\t\tt.Errorf("files should be same")
+\t}
+\n+\tib, err := Stat("b")
+\tif err != nil {
+\t\tt.Fatalf("Stat(b): %v", err)
+\t}
+\tif SameFile(ia1, ib) {
+\t\tt.Errorf("files should be different")
+\t}
+}\n```
### `src/pkg/os/stat_windows.go`
```diff
--- a/src/pkg/os/stat_windows.go
+++ b/src/pkg/os/stat_windows.go
@@ -5,6 +5,7 @@
package os
import (
+\t"sync"
"syscall"
"time"
"unsafe"
@@ -25,7 +26,13 @@ func (file *File) Stat() (fi FileInfo, err error) {
if e != nil {
return nil, &PathError{"GetFileInformationByHandle", file.name, e}
}\n-\treturn toFileInfo(basename(file.name), d.FileAttributes, d.FileSizeHigh, d.FileSizeLow, d.CreationTime, d.LastAccessTime, d.LastWriteTime), nil
+\treturn &fileStat{
+\t\tname: basename(file.name),
+\t\tsize: mkSize(d.FileSizeHigh, d.FileSizeLow),
+\t\tmodTime: mkModTime(d.LastWriteTime),
+\t\tmode: mkMode(d.FileAttributes),
+\t\tsys: mkSysFromFI(&d),
+\t}, nil
}\n \n // Stat returns a FileInfo structure describing the named file.\n@@ -39,7 +46,18 @@ func Stat(name string) (fi FileInfo, err error) {
if e != nil {
return nil, &PathError{"GetFileAttributesEx", name, e}
}\n-\treturn toFileInfo(basename(name), d.FileAttributes, d.FileSizeHigh, d.FileSizeLow, d.CreationTime, d.LastAccessTime, d.LastWriteTime), nil
+\tpath := name
+\tif !isAbs(path) {
+\t\tcwd, _ := Getwd()
+\t\tpath = cwd + `\` + path
+\t}
+\treturn &fileStat{
+\t\tname: basename(name),
+\t\tsize: mkSize(d.FileSizeHigh, d.FileSizeLow),
+\t\tmodTime: mkModTime(d.LastWriteTime),
+\t\tmode: mkMode(d.FileAttributes),
+\t\tsys: mkSys(path, d.LastAccessTime, d.CreationTime),
+\t}, nil
}\n \n // Lstat returns the FileInfo structure describing the named file.\n@@ -75,37 +93,144 @@ func basename(name string) string {
return name
}\n \n-type winTimes struct {
-\tatime, ctime syscall.Filetime
+func isSlash(c uint8) bool {
+\treturn c == '\\' || c == '/'
+}\n+\n+func isAbs(path string) (b bool) {
+\tv := volumeName(path)
+\tif v == "" {
+\t\treturn false
+\t}
+\tpath = path[len(v):]
+\tif path == "" {
+\t\treturn false
+\t}
+\treturn isSlash(path[0])
}\n \n-func toFileInfo(name string, fa, sizehi, sizelo uint32, ctime, atime, mtime syscall.Filetime) FileInfo {
-\tfs := &fileStat{
-\t\tname: name,
-\t\tsize: int64(sizehi)<<32 + int64(sizelo),
-\t\tmodTime: time.Unix(0, mtime.Nanoseconds()),
-\t\tsys: &winTimes{atime, ctime},
+func volumeName(path string) (v string) {
+\tif len(path) < 2 {
+\t\treturn ""
}
+\t// with drive letter
+\tc := path[0]
+\tif path[1] == ':' &&
+\t\t('0' <= c && c <= '9' || 'a' <= c && c <= 'z' ||
+\t\t\t'A' <= c && c <= 'Z') {
+\t\treturn path[:2]
+\t}
+\t// is it UNC
+\tif l := len(path); l >= 5 && isSlash(path[0]) && isSlash(path[1]) &&
+\t\t!isSlash(path[2]) && path[2] != '.' {
+\t\t// first, leading `\\` and next shouldn't be `\`. its server name.
+\t\tfor n := 3; n < l-1; n++ {
+\t\t\t// second, next '\' shouldn't be repeated.
+\t\t\tif isSlash(path[n]) {
+\t\t\t\tn++
+\t\t\t\t// third, following something characters. its share name.
+\t\t\t\tif !isSlash(path[n]) {
+\t\t\t\t\tif path[n] == '.' {
+\t\t\t\t\t\tbreak
+\t\t\t\t\t}
+\t\t\t\t\tfor ; n < l; n++ {
+\t\t\t\t\t\tif isSlash(path[n]) {
+\t\t\t\t\t\t\tbreak
+\t\t\t\t\t\t}
+\t\t\t\t\t}
+\t\t\t\t\treturn path[:n]
+\t\t\t\t}
+\t\t\t\tbreak
+\t\t\t}
+\t\t}
+\t}
+\treturn ""
+}\n+\n+type winSys struct {
+\tsync.Mutex
+\tpath string
+\tatime, ctime syscall.Filetime
+\tvol, idxhi, idxlo uint32
+}\n+\n+func mkSize(hi, lo uint32) int64 {
+\treturn int64(hi)<<32 + int64(lo)
+}\n+\n+func mkModTime(mtime syscall.Filetime) time.Time {
+\treturn time.Unix(0, mtime.Nanoseconds())
+}\n+\n+func mkMode(fa uint32) (m FileMode) {
\tif fa&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 {
-\t\tfs.mode |= ModeDir
+\t\tm |= ModeDir
}
\tif fa&syscall.FILE_ATTRIBUTE_READONLY != 0 {
-\t\tfs.mode |= 0444
+\t\tm |= 0444
} else {
-\t\tfs.mode |= 0666
+\t\tm |= 0666
}
-\treturn fs
+\treturn m
+}\n+\n+func mkSys(path string, atime, ctime syscall.Filetime) *winSys {
+\treturn &winSys{
+\t\tpath: path,
+\t\tatime: atime,
+\t\tctime: ctime,
+\t}
+}\n+\n+func mkSysFromFI(i *syscall.ByHandleFileInformation) *winSys {
+\treturn &winSys{
+\t\tatime: i.LastAccessTime,
+\t\tctime: i.CreationTime,
+\t\tvol: i.VolumeSerialNumber,
+\t\tidxhi: i.FileIndexHigh,
+\t\tidxlo: i.FileIndexLow,
+\t}
+}\n+\n+func (s *winSys) loadFileId() error {
+\tif s.path == "" {
+\t\t// already done
+\t\treturn nil
+\t}
+\ts.Lock()
+\tdefer s.Unlock()
+\th, e := syscall.CreateFile(syscall.StringToUTF16Ptr(s.path), syscall.GENERIC_READ, syscall.FILE_SHARE_READ, nil, syscall.OPEN_EXISTING, 0, 0)
+\tif e != nil {
+\t\treturn e
+\t}
+\tdefer syscall.CloseHandle(h)
+\tvar i syscall.ByHandleFileInformation
+\te = syscall.GetFileInformationByHandle(syscall.Handle(h), &i)
+\tif e != nil {
+\t\treturn e
+\t}
+\ts.path = ""
+\ts.vol = i.VolumeSerialNumber
+\ts.idxhi = i.FileIndexHigh
+\ts.idxlo = i.FileIndexLow
+\treturn nil
}\n \n func sameFile(sys1, sys2 interface{}) bool {
-\t// TODO(rsc): Do better than this, but this matches what
-\t// used to happen when code compared .Dev and .Ino,\n-\t// which were both always zero. Obviously not all files\n-\t// are the same.\n-\treturn true
+\ts1 := sys1.(*winSys)
+\ts2 := sys2.(*winSys)
+\te := s1.loadFileId()
+\tif e != nil {
+\t\tpanic(e)
+\t}
+\te = s2.loadFileId()
+\tif e != nil {
+\t\tpanic(e)
+\t}
+\treturn s1.vol == s2.vol && s1.idxhi == s2.idxhi && s1.idxlo == s2.idxlo
}\n \n // For testing.\n func atime(fi FileInfo) time.Time {
-\treturn time.Unix(0, fi.Sys().(*winTimes).atime.Nanoseconds())
+\treturn time.Unix(0, fi.Sys().(*winSys).atime.Nanoseconds())
}\n```
## コアとなるコードの解説
### `src/pkg/os/stat_windows.go`
このファイルが変更の核心です。
* **`import "sync"`**: `winSys`構造体でミューテックスを使用するために`sync`パッケージがインポートされています。これは、`loadFileId`がファイル識別子を遅延ロードする際に、複数のゴルーチンからの同時アクセスを防ぐためです。
* **`type winSys struct { ... }`**:
* `path string`: ファイルの絶対パスを保持します。`loadFileId`がファイル識別子をロードする際に使用されます。
* `atime, ctime syscall.Filetime`: アクセス時刻と作成時刻を保持します。
* `vol, idxhi, idxlo uint32`: Windows固有のファイル識別子であるボリュームシリアル番号とファイルインデックス(上位/下位)を保持します。
* **`mkSize`, `mkModTime`, `mkMode`**: これらのヘルパー関数は、`syscall.Win32finddata`や`syscall.ByHandleFileInformation`から`fileStat`の対応するフィールド(サイズ、更新時刻、モード)を生成するために使用されます。これらは以前の`toFileInfo`関数で行われていた処理を分割し、より明確にしています。
* **`mkSys(path string, atime, ctime syscall.Filetime) *winSys`**:
* `Stat`関数や`readdir`関数から呼び出され、`winSys`構造体を初期化します。この時点ではファイル識別子(`vol`, `idxhi`, `idxlo`)は設定されず、`path`のみが設定されます。これは、ファイル識別子の取得が比較的コストの高い操作であるため、必要な時(`SameFile`が呼び出された時)まで遅延させるためです。
* **`mkSysFromFI(i *syscall.ByHandleFileInformation) *winSys`**:
* `File.Stat()`関数から呼び出され、`syscall.ByHandleFileInformation`構造体から直接`winSys`構造体を生成します。この場合、`GetFileInformationByHandle`が既に呼び出されているため、ファイル識別子も同時に設定されます。
* **`func (s *winSys) loadFileId() error`**:
* このメソッドは、`winSys`構造体にファイル識別子(`vol`, `idxhi`, `idxlo`)がまだロードされていない場合に、それをロードする責任を負います。
* `s.path`が空でない場合(つまり、まだロードされていない場合)、`syscall.CreateFile`でファイルハンドルを取得し、`syscall.GetFileInformationByHandle`を呼び出してファイル情報を取得します。
* 取得した情報から`vol`, `idxhi`, `idxlo`を抽出し、`winSys`構造体に格納します。
* `sync.Mutex`を使用して、複数のゴルーチンからの同時ロードを防ぎます。
* **`func sameFile(sys1, sys2 interface{}) bool`**:
* `os.SameFile`関数から呼び出される実際の比較ロジックです。
* 引数の`sys1`, `sys2`を`*winSys`型に型アサートします。
* それぞれの`winSys`に対して`loadFileId()`を呼び出し、ファイル識別子が確実にロードされていることを保証します。エラーが発生した場合は`panic`します(これは通常、ファイルが存在しないなどの致命的なエラーを示します)。
* 最後に、`s1.vol == s2.vol && s1.idxhi == s2.idxhi && s1.idxlo == s2.idxlo`という条件で、ボリュームシリアル番号とファイルインデックスがすべて一致するかどうかを比較します。これにより、Windows上でのファイルの同一性が正確に判断されます。
* **`isSlash`, `isAbs`, `volumeName`**: これらのヘルパー関数は、Windowsパスの解析と正規化を助けます。特に`isAbs`と`volumeName`は、パスが絶対パスであるか、UNCパスであるかなどを判断し、`Stat`関数などで正確なファイル情報を取得するために使用されます。
### `src/pkg/os/file_windows.go`
* **`dirInfo`構造体への`path`フィールド追加**: ディレクトリのパスを保持するために`dirInfo`に`path`フィールドが追加されました。これは、`readdir`内で`fileStat`を生成する際に、完全なファイルパスを`mkSys`に渡すために使用されます。
* **`openDir`でのパスの絶対化**: `openDir`関数内で、開こうとしているディレクトリのパスが相対パスの場合、`Getwd()`を使って絶対パスに変換するロジックが追加されました。
* **`readdir`での`fileStat`生成の変更**: `readdir`関数内で、ディレクトリ内の各エントリの`FileInfo`を生成する際に、以前の`toFileInfo`の代わりに新しい`fileStat`構造体を直接初期化し、`mkSys`を使って`sys`フィールドを設定するように変更されました。これにより、`SameFile`が正しく機能するために必要なパス情報が`sys`フィールドに渡されるようになります。
### `src/pkg/os/os_test.go`
* **`TestSameFile`**: このテストは、`SameFile`関数の動作を検証します。
* 同じ名前のファイルを2回`Stat`して、`SameFile`が`true`を返すことを確認します。
* 異なる名前のファイルを`Stat`して、`SameFile`が`false`を返すことを確認します。
* これにより、Windows上での`SameFile`の正確な動作が保証されます。
## 関連リンク
* Go Issue 2511: [https://github.com/golang/go/issues/2511](https://github.com/golang/go/issues/2511)
* Go CL 5687072: [https://golang.org/cl/5687072](https://golang.org/cl/5687072)
## 参考にした情報源リンク
* Windows API `GetFileInformationByHandle` documentation: [https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfileinformationbyhandle](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfileinformationbyhandle)
* Windows API `CreateFile` documentation: [https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew)
* Windows API `GetFileAttributesEx` documentation: [https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfileattributesexw](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfileattributesexw)
* Go `os` package documentation: [https://pkg.go.dev/os](https://pkg.go.dev/os)
* Go `syscall` package documentation: [https://pkg.go.dev/syscall](https://pkg.go.dev/syscall)
* Go `sync` package documentation: [https://pkg.go.dev/sync](https://pkg.go.dev/sync)