[インデックス 14811] ファイルの概要
このコミットは、Go言語のos
パッケージにおけるWindows環境でのディレクトリ操作に関するバグ修正です。具体的には、空のルートディレクトリ(例: C:\
)をos.Open
またはos.ReadDir
で開こうとした際に発生するエラーを修正し、正しくディレクトリとして認識・処理できるようにします。
コミット
commit 20e976073de3922257e727f4137090a2a817fd8e
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Mon Jan 7 12:48:32 2013 +1100
os: fix Open for empty root directories on windows
Fixes #4601.
R=golang-dev, rsc, bradfitz, kardianos
CC=golang-dev
https://golang.org/cl/7033046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/20e976073de3922257e727f4137090a2a817fd8e
元コミット内容
os: fix Open for empty root directories on windows
Fixes #4601.
R=golang-dev, rsc, bradfitz, kardianos
CC=golang-dev
https://golang.org/cl/7033046
変更の背景
このコミットは、Go言語のos
パッケージがWindows環境で空のルートディレクトリ(例えば、ファイルやサブディレクトリが一つも存在しないC:\
のようなドライブのルート)を正しく扱えないという問題(Issue #4601)を修正するために行われました。
従来のos.Open
やos.ReadDir
は、ディレクトリの内容を列挙するためにWindows APIのFindFirstFile
関数を使用します。しかし、FindFirstFile
は、指定されたパターンに一致するファイルが見つからない場合にERROR_FILE_NOT_FOUND
エラーを返します。通常のファイル検索ではこのエラーは「ファイルが見つからない」ことを意味しますが、ディレクトリを列挙する際には、そのディレクトリが単に空であるだけで、ディレクトリ自体は存在するという状況がありえます。
このバグにより、空のルートディレクトリに対してos.Open
やos.ReadDir
を実行すると、ディレクトリが存在するにもかかわらず、ERROR_FILE_NOT_FOUND
が返され、Goのos
パッケージはこれを「ディレクトリが存在しない」かのようなエラーとして扱ってしまっていました。結果として、ユーザーは空のルートディレクトリを正しく操作できず、アプリケーションの動作に支障をきたしていました。
このコミットは、FindFirstFile
がERROR_FILE_NOT_FOUND
を返した場合でも、そのパスが実際にディレクトリであるかどうかを別途確認するロジックを追加することで、この問題を解決します。
前提知識の解説
Go言語のos
パッケージ
os
パッケージは、オペレーティングシステム(OS)の機能へのプラットフォームに依存しないインターフェースを提供します。ファイル操作、ディレクトリ操作、プロセス管理、環境変数へのアクセスなどが含まれます。
os.File
: 開かれたファイルまたはディレクトリを表す構造体です。os.Open(name string)
: 指定された名前のファイルまたはディレクトリを開きます。os.ReadDir(name string)
: 指定されたディレクトリの内容を読み取ります。os.PathError
: パス操作中に発生したエラーを表す構造体で、操作名、パス、および元のエラーを含みます。
Windows APIとsyscall
パッケージ
Go言語のsyscall
パッケージは、低レベルのOSプリミティブへのアクセスを提供します。Windows環境では、Win32 APIの関数を呼び出すために使用されます。
syscall.Handle
: Windowsのオブジェクトハンドルを表す型です。ファイル、ディレクトリ、プロセスなどのオブジェクトを識別するために使用されます。syscall.InvalidHandle
: 無効なハンドルを表す定数です。syscall.FindFirstFile(lpFileName *uint16, lpFindFileData *Win32finddata)
: 指定されたディレクトリ内のファイルまたはサブディレクトリを検索し、最初に見つかったファイル/ディレクトリの情報を取得します。検索が成功すると検索ハンドルを返し、失敗するとsyscall.InvalidHandle
を返します。syscall.FindNextFile(hFindFile syscall.Handle, lpFindFileData *Win32finddata)
:FindFirstFile
で開始された検索を続行し、次に見つかったファイル/ディレクトリの情報を取得します。syscall.GetFileAttributesEx(lpFileName *uint16, fInfoLevelId uint32, lpFileInformation *byte)
: 指定されたファイルまたはディレクトリの属性情報を取得します。syscall.Win32finddata
:FindFirstFile
やFindNextFile
が返すファイル/ディレクトリの情報を格納する構造体です。syscall.Win32FileAttributeData
:GetFileAttributesEx
が返すファイル/ディレクトリの属性情報を格納する構造体です。syscall.FILE_ATTRIBUTE_DIRECTORY
: ファイル属性の一つで、対象がディレクトリであることを示します。syscall.ERROR_FILE_NOT_FOUND
: Windows APIのエラーコードの一つで、指定されたファイルまたはディレクトリが見つからなかったことを示します。syscall.UTF16PtrFromString(s string)
: Goの文字列をUTF-16エンコードされたNULL終端文字列のポインタに変換します。Windows APIは通常、UTF-16文字列を期待するため、この関数が使用されます。
ディレクトリの扱い
Windowsでは、ディレクトリもファイルシステム上の「オブジェクト」として扱われます。FindFirstFile
は、ディレクトリ内のエントリ(ファイルやサブディレクトリ)を列挙するための関数であり、ディレクトリ自体が存在するかどうかを確認する主要な手段ではありません。ディレクトリ自体の存在と属性を確認するには、GetFileAttributesEx
のような別のAPIがより適切です。
技術的詳細
このコミットの主要な変更点は、src/pkg/os/file_windows.go
内のopenDir
関数とFile
構造体、および関連するヘルパー関数の修正にあります。
-
newFile
関数の導入とNewFile
の変更:- 既存の
NewFile
関数は、syscall.InvalidHandle
をチェックし、無効な場合はnil
を返していました。 - 新しく
newFile
という内部関数が導入され、これはsyscall.InvalidHandle
のチェックを行いません。 NewFile
はnewFile
を呼び出すように変更され、syscall.InvalidHandle
のチェックはNewFile
内で行われるようになりました。- この変更の目的は、
openDir
関数内でFindFirstFile
がERROR_FILE_NOT_FOUND
を返した場合でも、有効なハンドルではないがディレクトリとして扱いたいケースに対応するためです。
- 既存の
-
dirInfo
構造体へのisempty
フィールドの追加:dirInfo
構造体は、ディレクトリに関する補助情報を保持します。isempty
というブール型のフィールドが追加されました。これは、FindFirstFile
がERROR_FILE_NOT_FOUND
を返した場合にtrue
に設定されます。このフラグは、ディレクトリが空であることを示し、後続のReaddir
やClose
操作で特殊な処理を行うために使用されます。
-
openDir
関数のロジック変更:openDir
関数は、ディレクトリを開く際にsyscall.FindFirstFile
を呼び出します。- 重要な変更点:
FindFirstFile
がエラーを返した場合、そのエラーがsyscall.ERROR_FILE_NOT_FOUND
であるかどうかをチェックします。- もし
ERROR_FILE_NOT_FOUND
であれば、それはディレクトリ内にファイルが見つからなかったことを意味する可能性があります。この場合、すぐにエラーを返すのではなく、そのパスが実際にディレクトリであるかどうかをsyscall.GetFileAttributesEx
を使って確認します。 GetFileAttributesEx
でパスの属性を取得し、FILE_ATTRIBUTE_DIRECTORY
フラグが設定されていることを確認します。これにより、パスがディレクトリであることが保証されます。- もしパスがディレクトリであれば、
dirInfo.isempty
をtrue
に設定し、FindFirstFile
が返した無効なハンドル(r
)を使ってnewFile
を呼び出します。これにより、空のディレクトリでもFile
オブジェクトが作成され、後続の操作が可能になります。 FindFirstFile
がERROR_FILE_NOT_FOUND
以外のエラーを返した場合、またはGetFileAttributesEx
でパスがディレクトリではないと判断された場合は、従来通りエラーを返します。
- もし
- この修正により、空のディレクトリであっても、
FindFirstFile
がERROR_FILE_NOT_FOUND
を返した際に、それがディレクトリの存在自体を否定するものではないと正しく判断できるようになりました。
-
Close
およびReaddir
関数の変更:File
のclose
メソッド(内部関数)では、file.isdir() && file.dirinfo.isempty
の場合に、syscall.InvalidHandle
であってもエラーを返さずにnil
を返すように変更されました。これは、空のディレクトリに対してFindFirstFile
が有効なハンドルを返さないため、そのハンドルを閉じようとするとエラーになるのを防ぐためです。File
のreaddir
メソッドでは、!file.dirinfo.isempty && file.fd == syscall.InvalidHandle
の場合にsyscall.EINVAL
を返すチェックが追加されました。これは、空ではないディレクトリで無効なハンドルを持つ場合にエラーを発生させるためです。- また、
readdir
のループ条件に&& !file.dirinfo.isempty
が追加され、空のディレクトリの場合はFindNextFile
を呼び出さないように変更されました。これにより、空のディレクトリに対して不必要なAPI呼び出しが行われるのを防ぎます。
これらの変更により、Windows環境で空のルートディレクトリを扱う際のos
パッケージの堅牢性が向上しました。
コアとなるコードの変更箇所
src/pkg/os/file_windows.go
--- a/src/pkg/os/file_windows.go
+++ b/src/pkg/os/file_windows.go
@@ -11,6 +11,7 @@ import (
"syscall"
"unicode/utf16"
"unicode/utf8"
+ "unsafe"
)
// File represents an open file descriptor.
@@ -41,12 +42,9 @@ func (file *File) Fd() uintptr {
return uintptr(file.fd)
}
-// NewFile returns a new File with the given file descriptor and name.
-func NewFile(fd uintptr, name string) *File {
- h := syscall.Handle(fd)
- if h == syscall.InvalidHandle {
- return nil
- }
+// newFile returns a new File with the given file handle and name.
+// Unlike NewFile, it does not check that h is syscall.InvalidHandle.
+func newFile(h syscall.Handle, name string) *File {
f := &File{&file{fd: h, name: name}}\
var m uint32
if syscall.GetConsoleMode(f.fd, &m) == nil {
@@ -56,11 +54,21 @@ func NewFile(fd uintptr, name string) *File {
return f
}
+// NewFile returns a new File with the given file descriptor and name.
+func NewFile(fd uintptr, name string) *File {
+ h := syscall.Handle(fd)
+ if h == syscall.InvalidHandle {
+ return nil
+ }
+ return newFile(h, name)
+}
+
// Auxiliary information if the File describes a directory
type dirInfo struct {
data syscall.Win32finddata
needdata bool
path string
+ isempty bool // set if FindFirstFile returns ERROR_FILE_NOT_FOUND
}\
func epipecheck(file *File, e error) {
@@ -73,7 +81,7 @@ func (f *file) isdir() bool { return f != nil && f.dirinfo != nil }\
func openFile(name string, flag int, perm FileMode) (file *File, err error) {
r, e := syscall.Open(name, flag|syscall.O_CLOEXEC, syscallMode(perm))
if e != nil {
- return nil, &PathError{"open", name, e}
+ return nil, e
}
return NewFile(uintptr(r), name), nil
}
@@ -81,19 +89,37 @@ func openFile(name string, flag int, perm FileMode) (file *File, err error) {
func openDir(name string) (file *File, err error) {
maskp, e := syscall.UTF16PtrFromString(name + `\*`)
if e != nil {
- return nil, &PathError{"open", name, e}
+ return nil, e
}
d := new(dirInfo)
r, e := syscall.FindFirstFile(maskp, &d.data)
if e != nil {
- return nil, &PathError{"open", name, e}
+ // FindFirstFile returns ERROR_FILE_NOT_FOUND when
+ // no matching files can be found. Then, if directory
+ // exists, we should proceed.
+ if e != syscall.ERROR_FILE_NOT_FOUND {
+ return nil, e
+ }
+ var fa syscall.Win32FileAttributeData
+ namep, e := syscall.UTF16PtrFromString(name)
+ if e != nil {
+ return nil, e
+ }
+ e = syscall.GetFileAttributesEx(namep, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fa)))
+ if e != nil {
+ return nil, e
+ }
+ if fa.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY == 0 {
+ return nil, e
+ }
+ d.isempty = true
}
d.path = name
if !isAbs(d.path) {
cwd, _ := Getwd()
d.path = cwd + `\` + d.path
}
- f := NewFile(uintptr(r), name)
+ f := newFile(r, name)
f.dirinfo = d
return f, nil
}
@@ -101,7 +127,7 @@ func OpenFile(name string, flag int, perm FileMode) (file *File, err error) {
if e == nil {
return r, nil
}
- return nil, e
+ return nil, &PathError{"open", name, e}
}
// Close closes the File, rendering it unusable for I/O.
@@ -111,7 +137,14 @@ func (file *File) Close() error {
}
func (file *file) close() error {
- if file == nil || file.fd == syscall.InvalidHandle {
+ if file == nil {
+ return syscall.EINVAL
+ }
+ if file.isdir() && file.dirinfo.isempty {
+ // "special" empty directories
+ return nil
+ }
+ if file.fd == syscall.InvalidHandle {
return syscall.EINVAL
}
var e error
@@ -132,12 +165,15 @@ func (file *file) close() error {
}
func (file *File) readdir(n int) (fi []FileInfo, err error) {
- if file == nil || file.fd == syscall.InvalidHandle {
+ if file == nil {
return nil, syscall.EINVAL
}
if !file.isdir() {
return nil, &PathError{"Readdir", file.name, syscall.ENOTDIR}
}
+ if !file.dirinfo.isempty && file.fd == syscall.InvalidHandle {
+ return nil, syscall.EINVAL
+ }
wantAll := n <= 0
size := n
if wantAll {
size = 100 // arbitrary initial size
}
fi = make([]FileInfo, 0, size) // Empty with room to grow.
d := &file.dirinfo.data
- for n != 0 {
+ for n != 0 && !file.dirinfo.isempty {
if file.dirinfo.needdata {
e := syscall.FindNextFile(syscall.Handle(file.fd), d)
if e != nil {
コアとなるコードの解説
newFile
関数の導入とNewFile
の変更
- 変更前:
func NewFile(fd uintptr, name string) *File { h := syscall.Handle(fd) if h == syscall.InvalidHandle { return nil } // ... }
- 変更後:
func newFile(h syscall.Handle, name string) *File { f := &File{&file{fd: h, name: name}} // ... return f } func NewFile(fd uintptr, name string) *File { h := syscall.Handle(fd) if h == syscall.InvalidHandle { return nil } return newFile(h, name) }
newFile
は、ハンドルがsyscall.InvalidHandle
であるかどうかのチェックを行わない、より低レベルなファイルオブジェクト作成関数として導入されました。これにより、openDir
内でFindFirstFile
がERROR_FILE_NOT_FOUND
を返した場合でも、そのハンドル(この場合は無効なハンドル)を使ってFile
オブジェクトを作成し、isempty
フラグでその状態を管理できるようになります。NewFile
は引き続き外部からの呼び出しに対して安全なインターフェースを提供します。
dirInfo
構造体へのisempty
フィールドの追加
- 変更前:
type dirInfo struct { data syscall.Win32finddata needdata bool path string }
- 変更後:
type dirInfo struct { data syscall.Win32finddata needdata bool path string isempty bool // set if FindFirstFile returns ERROR_FILE_NOT_FOUND }
isempty
フィールドが追加され、FindFirstFile
がERROR_FILE_NOT_FOUND
を返した際にtrue
に設定されます。これは、ディレクトリが空であることを示す内部状態フラグとして機能します。
openDir
関数のロジック変更
- 変更前:
func openDir(name string) (file *File, err error) { maskp, e := syscall.UTF16PtrFromString(name + `\*`) if e != nil { return nil, &PathError{"open", name, e} } d := new(dirInfo) r, e := syscall.FindFirstFile(maskp, &d.data) if e != nil { return nil, &PathError{"open", name, e} } // ... f := NewFile(uintptr(r), name) f.dirinfo = d return f, nil }
- 変更後:
この部分が修正の核心です。func openDir(name string) (file *File, err error) { maskp, e := syscall.UTF16PtrFromString(name + `\*`) if e != nil { return nil, e // PathErrorから直接エラーを返すように変更 } d := new(dirInfo) r, e := syscall.FindFirstFile(maskp, &d.data) if e != nil { // FindFirstFile returns ERROR_FILE_NOT_FOUND when // no matching files can be found. Then, if directory // exists, we should proceed. if e != syscall.ERROR_FILE_NOT_FOUND { return nil, e } var fa syscall.Win32FileAttributeData namep, e := syscall.UTF16PtrFromString(name) if e != nil { return nil, e } e = syscall.GetFileAttributesEx(namep, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fa))) if e != nil { return nil, e } if fa.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY == 0 { return nil, e } d.isempty = true // ディレクトリが空であることをマーク } d.path = name // ... f := newFile(r, name) // NewFileではなくnewFileを呼び出す f.dirinfo = d return f, nil }
syscall.UTF16PtrFromString
やsyscall.Open
からのエラーが直接返されるようになりました(以前はPathError
でラップされていた)。これは一貫性のための変更です。FindFirstFile
がエラーを返した場合、そのエラーがsyscall.ERROR_FILE_NOT_FOUND
であるかをチェックします。- もし
ERROR_FILE_NOT_FOUND
であれば、syscall.GetFileAttributesEx
を使って、実際にそのパスがディレクトリであるかを確認します。 GetFileAttributesEx
が成功し、かつFILE_ATTRIBUTE_DIRECTORY
属性が設定されていれば、そのパスは空のディレクトリであると判断し、d.isempty = true
を設定します。この場合、FindFirstFile
が返した無効なハンドルr
(通常はsyscall.InvalidHandle
)を使ってnewFile(r, name)
を呼び出し、File
オブジェクトを作成します。これにより、空のディレクトリでもFile
オブジェクトが生成され、後続の操作が可能になります。- それ以外の場合(
ERROR_FILE_NOT_FOUND
以外のエラー、またはパスがディレクトリではない場合)は、従来通りエラーを返します。
Close
およびReaddir
関数の変更
-
file.close()
:- if file == nil || file.fd == syscall.InvalidHandle { + if file == nil { + return syscall.EINVAL + } + if file.isdir() && file.dirinfo.isempty { + // "special" empty directories + return nil + } + if file.fd == syscall.InvalidHandle { return syscall.EINVAL }
file
がnil
の場合のチェックが分離され、file.isdir() && file.dirinfo.isempty
の場合(つまり、空のディレクトリで、FindFirstFile
が有効なハンドルを返さなかった場合)は、syscall.InvalidHandle
であってもエラーを返さずにnil
を返すようになりました。これにより、無効なハンドルを閉じようとして発生するエラーを防ぎます。 -
File.readdir()
:- if file == nil || file.fd == syscall.InvalidHandle { + if file == nil { return nil, syscall.EINVAL } if !file.isdir() { return nil, &PathError{"Readdir", file.name, syscall.ENOTDIR} } + if !file.dirinfo.isempty && file.fd == syscall.InvalidHandle { + return nil, syscall.EINVAL + } wantAll := n <= 0 size := n // ... d := &file.dirinfo.data - for n != 0 { + for n != 0 && !file.dirinfo.isempty { if file.dirinfo.needdata { e := syscall.FindNextFile(syscall.Handle(file.fd), d) if e != nil {
!file.dirinfo.isempty && file.fd == syscall.InvalidHandle
の場合にsyscall.EINVAL
を返すチェックが追加されました。これは、空ではないディレクトリにもかかわらず無効なハンドルを持っているという矛盾した状態を検出するためです。for
ループの条件に&& !file.dirinfo.isempty
が追加されました。これにより、ディレクトリが空であるとマークされている場合(isempty
がtrue
の場合)は、FindNextFile
の呼び出しをスキップし、不必要なAPI呼び出しやエラーを防ぎます。
これらの変更により、Windowsのファイルシステムにおける空のディレクトリの特殊な挙動が適切に処理され、os
パッケージがより堅牢になりました。
関連リンク
- Go Issue #4601: https://github.com/golang/go/issues/4601 (Web検索結果から推測される関連Issue)
- Go Change List 7033046: https://golang.org/cl/7033046
参考にした情報源リンク
- https://github.com/golang/go/commit/20e976073de3922257e727f4137090a2a817fd8e
- https://golang.org/cl/7033046
- https://github.com/golang/go/issues/4601
- Microsoft Learn: FindFirstFileW function
- Microsoft Learn: GetFileAttributesExW function
- Microsoft Learn: File Attribute Constants
- Microsoft Learn: System Error Codes (0-499) (for
ERROR_FILE_NOT_FOUND
)```markdown
[インデックス 14811] ファイルの概要
このコミットは、Go言語のos
パッケージにおけるWindows環境でのディレクトリ操作に関するバグ修正です。具体的には、空のルートディレクトリ(例: C:\
)をos.Open
またはos.ReadDir
で開こうとした際に発生するエラーを修正し、正しくディレクトリとして認識・処理できるようにします。
コミット
commit 20e976073de3922257e727f4137090a2a817fd8e
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Mon Jan 7 12:48:32 2013 +1100
os: fix Open for empty root directories on windows
Fixes #4601.
R=golang-dev, rsc, bradfitz, kardianos
CC=golang-dev
https://golang.org/cl/7033046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/20e976073de3922257e727f4137090a2a817fd8e
元コミット内容
os: fix Open for empty root directories on windows
Fixes #4601.
R=golang-dev, rsc, bradfitz, kardianos
CC=golang-dev
https://golang.org/cl/7033046
変更の背景
このコミットは、Go言語のos
パッケージがWindows環境で空のルートディレクトリ(例えば、ファイルやサブディレクトリが一つも存在しないC:\
のようなドライブのルート)を正しく扱えないという問題(Issue #4601)を修正するために行われました。
従来のos.Open
やos.ReadDir
は、ディレクトリの内容を列挙するためにWindows APIのFindFirstFile
関数を使用します。しかし、FindFirstFile
は、指定されたパターンに一致するファイルが見つからない場合にERROR_FILE_NOT_FOUND
エラーを返します。通常のファイル検索ではこのエラーは「ファイルが見つからない」ことを意味しますが、ディレクトリを列挙する際には、そのディレクトリが単に空であるだけで、ディレクトリ自体は存在するという状況がありえます。
このバグにより、空のルートディレクトリに対してos.Open
やos.ReadDir
を実行すると、ディレクトリが存在するにもかかわらず、ERROR_FILE_NOT_FOUND
が返され、Goのos
パッケージはこれを「ディレクトリが存在しない」かのようなエラーとして扱ってしまっていました。結果として、ユーザーは空のルートディレクトリを正しく操作できず、アプリケーションの動作に支障をきたしていました。
このコミットは、FindFirstFile
がERROR_FILE_NOT_FOUND
を返した場合でも、そのパスが実際にディレクトリであるかどうかを別途確認するロジックを追加することで、この問題を解決します。
前提知識の解説
Go言語のos
パッケージ
os
パッケージは、オペレーティングシステム(OS)の機能へのプラットフォームに依存しないインターフェースを提供します。ファイル操作、ディレクトリ操作、プロセス管理、環境変数へのアクセスなどが含まれます。
os.File
: 開かれたファイルまたはディレクトリを表す構造体です。os.Open(name string)
: 指定された名前のファイルまたはディレクトリを開きます。os.ReadDir(name string)
: 指定されたディレクトリの内容を読み取ります。os.PathError
: パス操作中に発生したエラーを表す構造体で、操作名、パス、および元のエラーを含みます。
Windows APIとsyscall
パッケージ
Go言語のsyscall
パッケージは、低レベルのOSプリミティブへのアクセスを提供します。Windows環境では、Win32 APIの関数を呼び出すために使用されます。
syscall.Handle
: Windowsのオブジェクトハンドルを表す型です。ファイル、ディレクトリ、プロセスなどのオブジェクトを識別するために使用されます。syscall.InvalidHandle
: 無効なハンドルを表す定数です。syscall.FindFirstFile(lpFileName *uint16, lpFindFileData *Win32finddata)
: 指定されたディレクトリ内のファイルまたはサブディレクトリを検索し、最初に見つかったファイル/ディレクトリの情報を取得します。検索が成功すると検索ハンドルを返し、失敗するとsyscall.InvalidHandle
を返します。syscall.FindNextFile(hFindFile syscall.Handle, lpFindFileData *Win32finddata)
:FindFirstFile
で開始された検索を続行し、次に見つかったファイル/ディレクトリの情報を取得します。syscall.GetFileAttributesEx(lpFileName *uint16, fInfoLevelId uint32, lpFileInformation *byte)
: 指定されたファイルまたはディレクトリの属性情報を取得します。syscall.Win32finddata
:FindFirstFile
やFindNextFile
が返すファイル/ディレクトリの情報を格納する構造体です。syscall.Win32FileAttributeData
:GetFileAttributesEx
が返すファイル/ディレクトリの属性情報を格納する構造体です。syscall.FILE_ATTRIBUTE_DIRECTORY
: ファイル属性の一つで、対象がディレクトリであることを示します。syscall.ERROR_FILE_NOT_FOUND
: Windows APIのエラーコードの一つで、指定されたファイルまたはディレクトリが見つからなかったことを示します。syscall.UTF16PtrFromString(s string)
: Goの文字列をUTF-16エンコードされたNULL終端文字列のポインタに変換します。Windows APIは通常、UTF-16文字列を期待するため、この関数が使用されます。
ディレクトリの扱い
Windowsでは、ディレクトリもファイルシステム上の「オブジェクト」として扱われます。FindFirstFile
は、ディレクトリ内のエントリ(ファイルやサブディレクトリ)を列挙するための関数であり、ディレクトリ自体が存在するかどうかを確認する主要な手段ではありません。ディレクトリ自体の存在と属性を確認するには、GetFileAttributesEx
のような別のAPIがより適切です。
技術的詳細
このコミットの主要な変更点は、src/pkg/os/file_windows.go
内のopenDir
関数とFile
構造体、および関連するヘルパー関数の修正にあります。
-
newFile
関数の導入とNewFile
の変更:- 既存の
NewFile
関数は、syscall.InvalidHandle
をチェックし、無効な場合はnil
を返していました。 - 新しく
newFile
という内部関数が導入され、これはsyscall.InvalidHandle
のチェックを行いません。 NewFile
はnewFile
を呼び出すように変更され、syscall.InvalidHandle
のチェックはNewFile
内で行われるようになりました。- この変更の目的は、
openDir
関数内でFindFirstFile
がERROR_FILE_NOT_FOUND
を返した場合でも、有効なハンドルではないがディレクトリとして扱いたいケースに対応するためです。
- 既存の
-
dirInfo
構造体へのisempty
フィールドの追加:dirInfo
構造体は、ディレクトリに関する補助情報を保持します。isempty
というブール型のフィールドが追加されました。これは、FindFirstFile
がERROR_FILE_NOT_FOUND
を返した場合にtrue
に設定されます。このフラグは、ディレクトリが空であることを示し、後続のReaddir
やClose
操作で特殊な処理を行うために使用されます。
-
openDir
関数のロジック変更:openDir
関数は、ディレクトリを開く際にsyscall.FindFirstFile
を呼び出します。- 重要な変更点:
FindFirstFile
がエラーを返した場合、そのエラーがsyscall.ERROR_FILE_NOT_FOUND
であるかどうかをチェックします。- もし
ERROR_FILE_NOT_FOUND
であれば、それはディレクトリ内にファイルが見つからなかったことを意味する可能性があります。この場合、すぐにエラーを返すのではなく、そのパスが実際にディレクトリであるかどうかをsyscall.GetFileAttributesEx
を使って確認します。 GetFileAttributesEx
でパスの属性を取得し、FILE_ATTRIBUTE_DIRECTORY
フラグが設定されていることを確認します。これにより、パスがディレクトリであることが保証されます。- もしパスがディレクトリであれば、
dirInfo.isempty
をtrue
に設定し、FindFirstFile
が返した無効なハンドル(r
)を使ってnewFile
を呼び出します。これにより、空のディレクトリでもFile
オブジェクトが作成され、後続の操作が可能になります。 FindFirstFile
がERROR_FILE_NOT_FOUND
以外のエラーを返した場合、またはGetFileAttributesEx
でパスがディレクトリではないと判断された場合は、従来通りエラーを返します。
- もし
- この修正により、空のディレクトリであっても、
FindFirstFile
がERROR_FILE_NOT_FOUND
を返した際に、それがディレクトリの存在自体を否定するものではないと正しく判断できるようになりました。
-
Close
およびReaddir
関数の変更:File
のclose
メソッド(内部関数)では、file.isdir() && file.dirinfo.isempty
の場合に、syscall.InvalidHandle
であってもエラーを返さずにnil
を返すように変更されました。これは、空のディレクトリに対してFindFirstFile
が有効なハンドルを返さないため、そのハンドルを閉じようとするとエラーになるのを防ぐためです。File
のreaddir
メソッドでは、!file.dirinfo.isempty && file.fd == syscall.InvalidHandle
の場合にsyscall.EINVAL
を返すチェックが追加されました。これは、空ではないディレクトリで無効なハンドルを持つ場合にエラーを発生させるためです。- また、
readdir
のループ条件に&& !file.dirinfo.isempty
が追加され、空のディレクトリの場合はFindNextFile
を呼び出さないように変更されました。これにより、空のディレクトリに対して不必要なAPI呼び出しが行われるのを防ぎます。
これらの変更により、Windows環境で空のルートディレクトリを扱う際のos
パッケージの堅牢性が向上しました。
コアとなるコードの変更箇所
src/pkg/os/file_windows.go
--- a/src/pkg/os/file_windows.go
+++ b/src/pkg/os/file_windows.go
@@ -11,6 +11,7 @@ import (
"syscall"
"unicode/utf16"
"unicode/utf8"
+ "unsafe"
)
// File represents an open file descriptor.
@@ -41,12 +42,9 @@ func (file *File) Fd() uintptr {
return uintptr(file.fd)
}
-// NewFile returns a new File with the given file descriptor and name.\
-func NewFile(fd uintptr, name string) *File {
- h := syscall.Handle(fd)
- if h == syscall.InvalidHandle {
- return nil
- }
+// newFile returns a new File with the given file handle and name.
+// Unlike NewFile, it does not check that h is syscall.InvalidHandle.
+func newFile(h syscall.Handle, name string) *File {
f := &File{&file{fd: h, name: name}}\
var m uint32
if syscall.GetConsoleMode(f.fd, &m) == nil {
@@ -56,11 +54,21 @@ func NewFile(fd uintptr, name string) *File {
return f
}
+// NewFile returns a new File with the given file descriptor and name.
+func NewFile(fd uintptr, name string) *File {
+ h := syscall.Handle(fd)
+ if h == syscall.InvalidHandle {
+ return nil
+ }
+ return newFile(h, name)
+}
+
// Auxiliary information if the File describes a directory
type dirInfo struct {
data syscall.Win32finddata
needdata bool
path string
+ isempty bool // set if FindFirstFile returns ERROR_FILE_NOT_FOUND
}\
func epipecheck(file *File, e error) {
@@ -73,7 +81,7 @@ func (f *file) isdir() bool { return f != nil && f.dirinfo != nil }\
func openFile(name string, flag int, perm FileMode) (file *File, err error) {
r, e := syscall.Open(name, flag|syscall.O_CLOEXEC, syscallMode(perm))
if e != nil {
- return nil, &PathError{"open", name, e}
+ return nil, e
}
return NewFile(uintptr(r), name), nil
}
@@ -81,19 +89,37 @@ func openFile(name string, flag int, perm FileMode) (file *File, err error) {
func openDir(name string) (file *File, err error) {
maskp, e := syscall.UTF16PtrFromString(name + `\*`)
if e != nil {
- return nil, &PathError{"open", name, e}
+ return nil, e
}
d := new(dirInfo)
r, e := syscall.FindFirstFile(maskp, &d.data)
if e != nil {
- return nil, &PathError{"open", name, e}
+ // FindFirstFile returns ERROR_FILE_NOT_FOUND when
+ // no matching files can be found. Then, if directory
+ // exists, we should proceed.
+ if e != syscall.ERROR_FILE_NOT_FOUND {
+ return nil, e
+ }
+ var fa syscall.Win32FileAttributeData
+ namep, e := syscall.UTF16PtrFromString(name)
+ if e != nil {
+ return nil, e
+ }
+ e = syscall.GetFileAttributesEx(namep, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fa)))
+ if e != nil {
+ return nil, e
+ }
+ if fa.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY == 0 {
+ return nil, e
+ }
+ d.isempty = true
}
d.path = name
if !isAbs(d.path) {
cwd, _ := Getwd()
d.path = cwd + `\` + d.path
}
- f := NewFile(uintptr(r), name)
+ f := newFile(r, name)
f.dirinfo = d
return f, nil
}
@@ -101,7 +127,7 @@ func OpenFile(name string, flag int, perm FileMode) (file *File, err error) {
if e == nil {
return r, nil
}
- return nil, e
+ return nil, &PathError{"open", name, e}
}
// Close closes the File, rendering it unusable for I/O.
@@ -111,7 +137,14 @@ func (file *File) Close() error {
}
func (file *file) close() error {
- if file == nil || file.fd == syscall.InvalidHandle {
+ if file == nil {
+ return syscall.EINVAL
+ }
+ if file.isdir() && file.dirinfo.isempty {
+ // "special" empty directories
+ return nil
+ }
+ if file.fd == syscall.InvalidHandle {
return syscall.EINVAL
}
var e error
@@ -132,12 +165,15 @@ func (file *file) close() error {
}
func (file *File) readdir(n int) (fi []FileInfo, err error) {
- if file == nil || file.fd == syscall.InvalidHandle {
+ if file == nil {
return nil, syscall.EINVAL
}
if !file.isdir() {
return nil, &PathError{"Readdir", file.name, syscall.ENOTDIR}
}
+ if !file.dirinfo.isempty && file.fd == syscall.InvalidHandle {
+ return nil, syscall.EINVAL
+ }
wantAll := n <= 0
size := n
if wantAll {
size = 100 // arbitrary initial size
}
fi = make([]FileInfo, 0, size) // Empty with room to grow.
d := &file.dirinfo.data
- for n != 0 {
+ for n != 0 && !file.dirinfo.isempty {
if file.dirinfo.needdata {
e := syscall.FindNextFile(syscall.Handle(file.fd), d)
if e != nil {
コアとなるコードの解説
newFile
関数の導入とNewFile
の変更
- 変更前:
func NewFile(fd uintptr, name string) *File { h := syscall.Handle(fd) if h == syscall.InvalidHandle { return nil } // ... }
- 変更後:
func newFile(h syscall.Handle, name string) *File { f := &File{&file{fd: h, name: name}} // ... return f } func NewFile(fd uintptr, name string) *File { h := syscall.Handle(fd) if h == syscall.InvalidHandle { return nil } return newFile(h, name) }
newFile
は、ハンドルがsyscall.InvalidHandle
であるかどうかのチェックを行わない、より低レベルなファイルオブジェクト作成関数として導入されました。これにより、openDir
内でFindFirstFile
がERROR_FILE_NOT_FOUND
を返した場合でも、そのハンドル(この場合は無効なハンドル)を使ってFile
オブジェクトを作成し、isempty
フラグでその状態を管理できるようになります。NewFile
は引き続き外部からの呼び出しに対して安全なインターフェースを提供します。
dirInfo
構造体へのisempty
フィールドの追加
- 変更前:
type dirInfo struct { data syscall.Win32finddata needdata bool path string }
- 変更後:
type dirInfo struct { data syscall.Win32finddata needdata bool path string isempty bool // set if FindFirstFile returns ERROR_FILE_NOT_FOUND }
isempty
フィールドが追加され、FindFirstFile
がERROR_FILE_NOT_FOUND
を返した際にtrue
に設定されます。これは、ディレクトリが空であることを示す内部状態フラグとして機能します。
openDir
関数のロジック変更
- 変更前:
func openDir(name string) (file *File, err error) { maskp, e := syscall.UTF16PtrFromString(name + `\*`) if e != nil { return nil, &PathError{"open", name, e} } d := new(dirInfo) r, e := syscall.FindFirstFile(maskp, &d.data) if e != nil { return nil, &PathError{"open", name, e} } // ... f := NewFile(uintptr(r), name) f.dirinfo = d return f, nil }
- 変更後:
この部分が修正の核心です。func openDir(name string) (file *File, err error) { maskp, e := syscall.UTF16PtrFromString(name + `\*`) if e != nil { return nil, e // PathErrorから直接エラーを返すように変更 } d := new(dirInfo) r, e := syscall.FindFirstFile(maskp, &d.data) if e != nil { // FindFirstFile returns ERROR_FILE_NOT_FOUND when // no matching files can be found. Then, if directory // exists, we should proceed. if e != syscall.ERROR_FILE_NOT_FOUND { return nil, e } var fa syscall.Win32FileAttributeData namep, e := syscall.UTF16PtrFromString(name) if e != nil { return nil, e } e = syscall.GetFileAttributesEx(namep, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fa))) if e != nil { return nil, e } if fa.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY == 0 { return nil, e } d.isempty = true // ディレクトリが空であることをマーク } d.path = name // ... f := newFile(r, name) // NewFileではなくnewFileを呼び出す f.dirinfo = d return f, nil }
syscall.UTF16PtrFromString
やsyscall.Open
からのエラーが直接返されるようになりました(以前はPathError
でラップされていた)。これは一貫性のための変更です。FindFirstFile
がエラーを返した場合、そのエラーがsyscall.ERROR_FILE_NOT_FOUND
であるかをチェックします。- もし
ERROR_FILE_NOT_FOUND
であれば、syscall.GetFileAttributesEx
を使って、実際にそのパスがディレクトリであるかを確認します。 GetFileAttributesEx
が成功し、かつFILE_ATTRIBUTE_DIRECTORY
属性が設定されていれば、そのパスは空のディレクトリであると判断し、d.isempty = true
を設定します。この場合、FindFirstFile
が返した無効なハンドルr
(通常はsyscall.InvalidHandle
)を使ってnewFile(r, name)
を呼び出し、File
オブジェクトを作成します。これにより、空のディレクトリでもFile
オブジェクトが生成され、後続の操作が可能になります。- それ以外の場合(
ERROR_FILE_NOT_FOUND
以外のエラー、またはパスがディレクトリではない場合)は、従来通りエラーを返します。
Close
およびReaddir
関数の変更
-
file.close()
:- if file == nil || file.fd == syscall.InvalidHandle { + if file == nil { + return syscall.EINVAL + } + if file.isdir() && file.dirinfo.isempty { + // "special" empty directories + return nil + } + if file.fd == syscall.InvalidHandle { return syscall.EINVAL }
file
がnil
の場合のチェックが分離され、file.isdir() && file.dirinfo.isempty
の場合(つまり、空のディレクトリで、FindFirstFile
が有効なハンドルを返さなかった場合)は、syscall.InvalidHandle
であってもエラーを返さずにnil
を返すようになりました。これにより、無効なハンドルを閉じようとして発生するエラーを防ぎます。 -
File.readdir()
:- if file == nil || file.fd == syscall.InvalidHandle { + if file == nil { return nil, syscall.EINVAL } if !file.isdir() { return nil, &PathError{"Readdir", file.name, syscall.ENOTDIR} } + if !file.dirinfo.isempty && file.fd == syscall.InvalidHandle { + return nil, syscall.EINVAL + } wantAll := n <= 0 size := n // ... d := &file.dirinfo.data - for n != 0 { + for n != 0 && !file.dirinfo.isempty { if file.dirinfo.needdata { e := syscall.FindNextFile(syscall.Handle(file.fd), d) if e != nil {
!file.dirinfo.isempty && file.fd == syscall.InvalidHandle
の場合にsyscall.EINVAL
を返すチェックが追加されました。これは、空ではないディレクトリにもかかわらず無効なハンドルを持っているという矛盾した状態を検出するためです。for
ループの条件に&& !file.dirinfo.isempty
が追加されました。これにより、ディレクトリが空であるとマークされている場合(isempty
がtrue
の場合)は、FindNextFile
の呼び出しをスキップし、不必要なAPI呼び出しやエラーを防ぎます。
これらの変更により、Windowsのファイルシステムにおける空のディレクトリの特殊な挙動が適切に処理され、os
パッケージがより堅牢になりました。
関連リンク
- Go Issue #4601: https://github.com/golang/go/issues/4601 (Web検索結果から推測される関連Issue)
- Go Change List 7033046: https://golang.org/cl/7033046
参考にした情報源リンク
- https://github.com/golang/go/commit/20e976073de3922257e727f4137090a2a817fd8e
- https://golang.org/cl/7033046
- https://github.com/golang/go/issues/4601
- Microsoft Learn: FindFirstFileW function
- Microsoft Learn: GetFileAttributesExW function
- Microsoft Learn: File Attribute Constants
- Microsoft Learn: System Error Codes (0-499) (for
ERROR_FILE_NOT_FOUND
)