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

[インデックス 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.Openos.ReadDirは、ディレクトリの内容を列挙するためにWindows APIのFindFirstFile関数を使用します。しかし、FindFirstFileは、指定されたパターンに一致するファイルが見つからない場合にERROR_FILE_NOT_FOUNDエラーを返します。通常のファイル検索ではこのエラーは「ファイルが見つからない」ことを意味しますが、ディレクトリを列挙する際には、そのディレクトリが単に空であるだけで、ディレクトリ自体は存在するという状況がありえます。

このバグにより、空のルートディレクトリに対してos.Openos.ReadDirを実行すると、ディレクトリが存在するにもかかわらず、ERROR_FILE_NOT_FOUNDが返され、Goのosパッケージはこれを「ディレクトリが存在しない」かのようなエラーとして扱ってしまっていました。結果として、ユーザーは空のルートディレクトリを正しく操作できず、アプリケーションの動作に支障をきたしていました。

このコミットは、FindFirstFileERROR_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: FindFirstFileFindNextFileが返すファイル/ディレクトリの情報を格納する構造体です。
  • 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構造体、および関連するヘルパー関数の修正にあります。

  1. newFile関数の導入とNewFileの変更:

    • 既存のNewFile関数は、syscall.InvalidHandleをチェックし、無効な場合はnilを返していました。
    • 新しくnewFileという内部関数が導入され、これはsyscall.InvalidHandleのチェックを行いません。
    • NewFilenewFileを呼び出すように変更され、syscall.InvalidHandleのチェックはNewFile内で行われるようになりました。
    • この変更の目的は、openDir関数内でFindFirstFileERROR_FILE_NOT_FOUNDを返した場合でも、有効なハンドルではないがディレクトリとして扱いたいケースに対応するためです。
  2. dirInfo構造体へのisemptyフィールドの追加:

    • dirInfo構造体は、ディレクトリに関する補助情報を保持します。
    • isemptyというブール型のフィールドが追加されました。これは、FindFirstFileERROR_FILE_NOT_FOUNDを返した場合にtrueに設定されます。このフラグは、ディレクトリが空であることを示し、後続のReaddirClose操作で特殊な処理を行うために使用されます。
  3. openDir関数のロジック変更:

    • openDir関数は、ディレクトリを開く際にsyscall.FindFirstFileを呼び出します。
    • 重要な変更点: FindFirstFileがエラーを返した場合、そのエラーがsyscall.ERROR_FILE_NOT_FOUNDであるかどうかをチェックします。
      • もしERROR_FILE_NOT_FOUNDであれば、それはディレクトリ内にファイルが見つからなかったことを意味する可能性があります。この場合、すぐにエラーを返すのではなく、そのパスが実際にディレクトリであるかどうかをsyscall.GetFileAttributesExを使って確認します。
      • GetFileAttributesExでパスの属性を取得し、FILE_ATTRIBUTE_DIRECTORYフラグが設定されていることを確認します。これにより、パスがディレクトリであることが保証されます。
      • もしパスがディレクトリであれば、dirInfo.isemptytrueに設定し、FindFirstFileが返した無効なハンドル(r)を使ってnewFileを呼び出します。これにより、空のディレクトリでもFileオブジェクトが作成され、後続の操作が可能になります。
      • FindFirstFileERROR_FILE_NOT_FOUND以外のエラーを返した場合、またはGetFileAttributesExでパスがディレクトリではないと判断された場合は、従来通りエラーを返します。
    • この修正により、空のディレクトリであっても、FindFirstFileERROR_FILE_NOT_FOUNDを返した際に、それがディレクトリの存在自体を否定するものではないと正しく判断できるようになりました。
  4. CloseおよびReaddir関数の変更:

    • Filecloseメソッド(内部関数)では、file.isdir() && file.dirinfo.isemptyの場合に、syscall.InvalidHandleであってもエラーを返さずにnilを返すように変更されました。これは、空のディレクトリに対してFindFirstFileが有効なハンドルを返さないため、そのハンドルを閉じようとするとエラーになるのを防ぐためです。
    • Filereaddirメソッドでは、!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内でFindFirstFileERROR_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フィールドが追加され、FindFirstFileERROR_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
    }
    
    この部分が修正の核心です。
    1. syscall.UTF16PtrFromStringsyscall.Openからのエラーが直接返されるようになりました(以前はPathErrorでラップされていた)。これは一貫性のための変更です。
    2. FindFirstFileがエラーを返した場合、そのエラーがsyscall.ERROR_FILE_NOT_FOUNDであるかをチェックします。
    3. もしERROR_FILE_NOT_FOUNDであれば、syscall.GetFileAttributesExを使って、実際にそのパスがディレクトリであるかを確認します。
    4. GetFileAttributesExが成功し、かつFILE_ATTRIBUTE_DIRECTORY属性が設定されていれば、そのパスは空のディレクトリであると判断し、d.isempty = trueを設定します。この場合、FindFirstFileが返した無効なハンドルr(通常はsyscall.InvalidHandle)を使ってnewFile(r, name)を呼び出し、Fileオブジェクトを作成します。これにより、空のディレクトリでもFileオブジェクトが生成され、後続の操作が可能になります。
    5. それ以外の場合(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
     	}
    

    filenilの場合のチェックが分離され、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 {
    
    1. !file.dirinfo.isempty && file.fd == syscall.InvalidHandleの場合にsyscall.EINVALを返すチェックが追加されました。これは、空ではないディレクトリにもかかわらず無効なハンドルを持っているという矛盾した状態を検出するためです。
    2. forループの条件に&& !file.dirinfo.isemptyが追加されました。これにより、ディレクトリが空であるとマークされている場合(isemptytrueの場合)は、FindNextFileの呼び出しをスキップし、不必要なAPI呼び出しやエラーを防ぎます。

これらの変更により、Windowsのファイルシステムにおける空のディレクトリの特殊な挙動が適切に処理され、osパッケージがより堅牢になりました。

関連リンク

参考にした情報源リンク

[インデックス 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.Openos.ReadDirは、ディレクトリの内容を列挙するためにWindows APIのFindFirstFile関数を使用します。しかし、FindFirstFileは、指定されたパターンに一致するファイルが見つからない場合にERROR_FILE_NOT_FOUNDエラーを返します。通常のファイル検索ではこのエラーは「ファイルが見つからない」ことを意味しますが、ディレクトリを列挙する際には、そのディレクトリが単に空であるだけで、ディレクトリ自体は存在するという状況がありえます。

このバグにより、空のルートディレクトリに対してos.Openos.ReadDirを実行すると、ディレクトリが存在するにもかかわらず、ERROR_FILE_NOT_FOUNDが返され、Goのosパッケージはこれを「ディレクトリが存在しない」かのようなエラーとして扱ってしまっていました。結果として、ユーザーは空のルートディレクトリを正しく操作できず、アプリケーションの動作に支障をきたしていました。

このコミットは、FindFirstFileERROR_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: FindFirstFileFindNextFileが返すファイル/ディレクトリの情報を格納する構造体です。
  • 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構造体、および関連するヘルパー関数の修正にあります。

  1. newFile関数の導入とNewFileの変更:

    • 既存のNewFile関数は、syscall.InvalidHandleをチェックし、無効な場合はnilを返していました。
    • 新しくnewFileという内部関数が導入され、これはsyscall.InvalidHandleのチェックを行いません。
    • NewFilenewFileを呼び出すように変更され、syscall.InvalidHandleのチェックはNewFile内で行われるようになりました。
    • この変更の目的は、openDir関数内でFindFirstFileERROR_FILE_NOT_FOUNDを返した場合でも、有効なハンドルではないがディレクトリとして扱いたいケースに対応するためです。
  2. dirInfo構造体へのisemptyフィールドの追加:

    • dirInfo構造体は、ディレクトリに関する補助情報を保持します。
    • isemptyというブール型のフィールドが追加されました。これは、FindFirstFileERROR_FILE_NOT_FOUNDを返した場合にtrueに設定されます。このフラグは、ディレクトリが空であることを示し、後続のReaddirClose操作で特殊な処理を行うために使用されます。
  3. openDir関数のロジック変更:

    • openDir関数は、ディレクトリを開く際にsyscall.FindFirstFileを呼び出します。
    • 重要な変更点: FindFirstFileがエラーを返した場合、そのエラーがsyscall.ERROR_FILE_NOT_FOUNDであるかどうかをチェックします。
      • もしERROR_FILE_NOT_FOUNDであれば、それはディレクトリ内にファイルが見つからなかったことを意味する可能性があります。この場合、すぐにエラーを返すのではなく、そのパスが実際にディレクトリであるかどうかをsyscall.GetFileAttributesExを使って確認します。
      • GetFileAttributesExでパスの属性を取得し、FILE_ATTRIBUTE_DIRECTORYフラグが設定されていることを確認します。これにより、パスがディレクトリであることが保証されます。
      • もしパスがディレクトリであれば、dirInfo.isemptytrueに設定し、FindFirstFileが返した無効なハンドル(r)を使ってnewFileを呼び出します。これにより、空のディレクトリでもFileオブジェクトが作成され、後続の操作が可能になります。
      • FindFirstFileERROR_FILE_NOT_FOUND以外のエラーを返した場合、またはGetFileAttributesExでパスがディレクトリではないと判断された場合は、従来通りエラーを返します。
    • この修正により、空のディレクトリであっても、FindFirstFileERROR_FILE_NOT_FOUNDを返した際に、それがディレクトリの存在自体を否定するものではないと正しく判断できるようになりました。
  4. CloseおよびReaddir関数の変更:

    • Filecloseメソッド(内部関数)では、file.isdir() && file.dirinfo.isemptyの場合に、syscall.InvalidHandleであってもエラーを返さずにnilを返すように変更されました。これは、空のディレクトリに対してFindFirstFileが有効なハンドルを返さないため、そのハンドルを閉じようとするとエラーになるのを防ぐためです。
    • Filereaddirメソッドでは、!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内でFindFirstFileERROR_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フィールドが追加され、FindFirstFileERROR_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
    }
    
    この部分が修正の核心です。
    1. syscall.UTF16PtrFromStringsyscall.Openからのエラーが直接返されるようになりました(以前はPathErrorでラップされていた)。これは一貫性のための変更です。
    2. FindFirstFileがエラーを返した場合、そのエラーがsyscall.ERROR_FILE_NOT_FOUNDであるかをチェックします。
    3. もしERROR_FILE_NOT_FOUNDであれば、syscall.GetFileAttributesExを使って、実際にそのパスがディレクトリであるかを確認します。
    4. GetFileAttributesExが成功し、かつFILE_ATTRIBUTE_DIRECTORY属性が設定されていれば、そのパスは空のディレクトリであると判断し、d.isempty = trueを設定します。この場合、FindFirstFileが返した無効なハンドルr(通常はsyscall.InvalidHandle)を使ってnewFile(r, name)を呼び出し、Fileオブジェクトを作成します。これにより、空のディレクトリでもFileオブジェクトが生成され、後続の操作が可能になります。
    5. それ以外の場合(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
     	}
    

    filenilの場合のチェックが分離され、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 {
    
    1. !file.dirinfo.isempty && file.fd == syscall.InvalidHandleの場合にsyscall.EINVALを返すチェックが追加されました。これは、空ではないディレクトリにもかかわらず無効なハンドルを持っているという矛盾した状態を検出するためです。
    2. forループの条件に&& !file.dirinfo.isemptyが追加されました。これにより、ディレクトリが空であるとマークされている場合(isemptytrueの場合)は、FindNextFileの呼び出しをスキップし、不必要なAPI呼び出しやエラーを防ぎます。

これらの変更により、Windowsのファイルシステムにおける空のディレクトリの特殊な挙動が適切に処理され、osパッケージがより堅牢になりました。

関連リンク

参考にした情報源リンク