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

[インデックス 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関数の実装を、ファイルのボリュームシリアル番号とファイルインデックスに基づいて行うように修正したことです。

  1. fileStat構造体の拡張とwinSys構造体の導入:

    • 以前はFileInfoSys()メソッドがwinTimesという構造体を返していましたが、このコミットではwinSysという新しい構造体が導入されました。
    • winSys構造体は、ファイルのパス、アクセス時刻、作成時刻に加えて、vol (ボリュームシリアル番号)、idxhi (ファイルインデックスの上位32ビット)、idxlo (ファイルインデックスの下位32ビット) を保持するように設計されています。
    • fileStat構造体(FileInfoインターフェースの実装)のsysフィールドが、このwinSys型を指すように変更されました。
  2. ファイル識別子の取得ロジックの追加:

    • mkSysFromFI関数が新しく追加され、syscall.ByHandleFileInformation構造体からwinSys構造体を生成する際に、ボリュームシリアル番号とファイルインデックスを抽出して設定します。
    • winSys構造体にはloadFileId()というメソッドが追加されました。このメソッドは、必要に応じてファイルのパスからCreateFileGetFileInformationByHandleを呼び出し、ファイルのボリュームシリアル番号とファイルインデックスを取得してwinSys構造体のフィールドを更新します。これにより、FileInfoが作成された時点ではファイル識別子を持っていなくても、SameFileが呼び出されたときに遅延ロードされるようになります。
  3. sameFile関数の修正:

    • sameFile関数(os.SameFileの実体)は、引数として渡された2つのFileInfoSys()メソッドが返すwinSys構造体を取り出します。
    • それぞれのwinSys構造体に対してloadFileId()を呼び出し、ファイル識別子を確実にロードします。
    • 最終的に、2つのwinSys構造体のvolidxhiidxloの各フィールドを比較し、すべてが一致する場合にのみtrueを返します。これにより、Windows上でのファイルの同一性が正確に判断されるようになります。
  4. パスの正規化と絶対パスへの変換:

    • Stat関数やopenDir関数内で、渡されたパスが相対パスである場合に、Getwd()(現在の作業ディレクトリを取得)と組み合わせて絶対パスに変換するロジックが追加されました。これは、GetFileInformationByHandleなどのWindows APIが正確なファイル情報を取得するために絶対パスを必要とするためです。
    • isAbsvolumeNameといったヘルパー関数が追加され、Windowsパスの絶対性やボリューム名の解析をサポートします。
  5. テストケースの追加:

    • src/pkg/os/os_test.goTestSameFileという新しいテスト関数が追加されました。このテストは、同じファイル(作成後に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)