[インデックス 12495] ファイルの概要
このコミットは、Go言語の標準ライブラリpath/filepathパッケージにおけるWindows環境でのシンボリックリンク評価(EvalSymlinks関数)の挙動を改善するものです。具体的には、Windowsのファイルシステムが持つ多様なパス表現(ショートネーム、ロングネーム、シンボリックリンク、ジャンクションポイントなど)に対応するため、Windows APIのGetLongPathNameW関数を利用するように変更されています。これにより、EvalSymlinksがWindows上でより正確な「実ファイル名」を取得できるようになります。
コミット
commit 0029b0d20744392260997441c1158dcb1b23a734
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Thu Mar 8 10:00:25 2012 +1100
path/filepath: retrieve real file name in windows EvalSymlinks
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5756049
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0029b0d20744392260997441c1158dcb1b23a734
元コミット内容
path/filepath: retrieve real file name in windows EvalSymlinks
このコミットは、Windows環境におけるpath/filepathパッケージのEvalSymlinks関数が、シンボリックリンクやその他のパス表現を評価する際に、より正確な実ファイル名(ロングパス名)を取得するように修正するものです。
変更の背景
Go言語のpath/filepath.EvalSymlinks関数は、与えられたパスが指すシンボリックリンクを解決し、最終的な実パスを返すことを目的としています。しかし、Windowsのファイルシステムは、UNIX系システムとは異なるパス解決のメカニズムを持っています。特に、ショートファイル名(8.3形式)や、シンボリックリンク、ジャンクションポイント、マウントポイントなど、様々な種類の「再解析ポイント」が存在します。
従来のEvalSymlinksの実装では、Windows環境においてこれらの複雑なパス表現を適切に解決できていませんでした。特に、シンボリックリンクが指す先の「実ファイル名」を取得する際に、Windowsが内部的に使用するロングパス名(完全なファイル名)ではなく、ショートパス名や不完全なパスが返される可能性がありました。これは、ファイル操作の正確性や、パスの正規化において問題を引き起こす可能性がありました。
このコミットの背景には、Windows環境でのEvalSymlinksの信頼性と正確性を向上させ、GoプログラムがWindowsファイルシステムとより適切に連携できるようにするという目的があります。
前提知識の解説
シンボリックリンク (Symbolic Link)
シンボリックリンク(またはソフトリンク)は、ファイルシステム上の別のファイルやディレクトリへの参照(ポインタ)を含む特殊なファイルです。シンボリックリンクを操作すると、通常はそれが指す元のファイルやディレクトリが操作されます。UNIX系システムでは広く使われていますが、WindowsでもWindows 2000以降、NTFSファイルシステムでサポートされています。
Windowsのパス表現
Windowsのファイルシステムは、UNIX系システムとは異なり、パスの表現にいくつかの特徴があります。
- ドライブレター:
C:\,D:\のようにドライブレターで始まる絶対パス。 - UNCパス:
\\Server\Share\Pathのようにネットワーク上のリソースを指すパス。 - ショートファイル名 (8.3形式): DOS時代からの互換性のために存在する、ファイル名が8文字、拡張子が3文字に制限された形式。例:
PROGRA~1はProgram Filesのショートファイル名。 - ロングファイル名: 通常の、スペースや特殊文字を含むファイル名。
- ジャンクションポイント (Junction Point): NTFSファイルシステムにおけるディレクトリへのシンボリックリンクに似た機能。ボリューム内の別のディレクトリを指します。
- ディレクトリシンボリックリンク: Windows Vista以降で導入された、ディレクトリへのシンボリックリンク。
- ファイルシンボリックリンク: Windows Vista以降で導入された、ファイルへのシンボリックリンク。
これらの多様なパス表現を正確に解決し、最終的な実パス(ロングパス名)を取得することは、Windowsプログラミングにおいて重要です。
GetLongPathNameW Windows API
GetLongPathNameWは、Windows APIの一つで、指定されたパスのロングパス形式を取得するために使用されます。この関数は、ショートパス名、シンボリックリンク、ジャンクションポイントなどを解決し、そのパスが指す実際のファイルまたはディレクトリの完全なロングパス名を返します。Wサフィックスは、ワイド文字(UTF-16)を使用するバージョンであることを示します。
技術的詳細
このコミットの主要な技術的変更点は、path/filepath.EvalSymlinks関数のWindows固有の処理を、Windows APIのGetLongPathNameWを利用するように変更した点です。
-
プラットフォーム固有の実装の分離:
- 従来の
path/filepath/path.goにあったEvalSymlinks関数内のWindows固有の処理が削除されました。 - 代わりに、
evalSymlinksという内部関数が導入され、この関数がプラットフォーム固有の挙動をカプセル化するように設計されました。 src/pkg/path/filepath/symlink.goが新しく作成され、UNIX系システム(!windowsビルドタグ)向けのevalSymlinksの実装が格納されました。この実装は、従来のEvalSymlinksのUNIX系システム向けのロジックをそのまま引き継いでいます。src/pkg/path/filepath/symlink_windows.goが新しく作成され、Windows向けのevalSymlinksの実装が格納されました。このファイルはwindowsビルドタグによってWindowsでのみコンパイルされます。
- 従来の
-
Windowsでの
GetLongPathNameWの利用:symlink_windows.go内のevalSymlinks関数は、syscall.GetLongPathNameを呼び出すことで、入力パスのロングパス名を取得します。GetLongPathNameは、UTF-16エンコーディングのパスを受け取り、UTF-16エンコーディングのロングパス名を返します。Goのsyscallパッケージは、これらのUTF-16文字列とGoの文字列(UTF-8)間の変換を処理します。GetLongPathNameは、バッファのサイズが足りない場合に、必要なバッファサイズを返すため、そのケースをハンドリングしてバッファを再割り当てし、再度呼び出すロジックが含まれています。
-
syscallパッケージの拡張:src/pkg/syscall/syscall_windows.go、src/pkg/syscall/zsyscall_windows_386.go、src/pkg/syscall/zsyscall_windows_amd64.goが変更され、GetLongPathNameWWindows APIをGoのsyscallパッケージから呼び出せるように、その定義と実装が追加されました。これにより、Goのコードから直接このAPIを利用できるようになります。
-
依存関係の更新:
src/pkg/go/build/deps_test.goが更新され、path/filepathパッケージがsyscallパッケージに依存するようになったことが反映されています。これは、Windows固有のevalSymlinks実装がsyscallパッケージを利用するためです。
この変更により、Windows環境でのEvalSymlinksは、ショートパス名やシンボリックリンク、ジャンクションポイントなど、Windowsがサポートするあらゆるパス表現を正確に解決し、その実ファイル名(ロングパス名)を返すことができるようになりました。
コアとなるコードの変更箇所
src/pkg/path/filepath/path.go
EvalSymlinks関数からWindows固有のロジックが削除され、プラットフォーム固有のevalSymlinks関数を呼び出すように変更されました。
--- a/src/pkg/path/filepath/path.go
+++ b/src/pkg/path/filepath/path.go
@@ -191,64 +189,7 @@ func Ext(path string) string {
// If path is relative the result will be relative to the current directory,
// unless one of the components is an absolute symbolic link.
func EvalSymlinks(path string) (string, error) {
- if runtime.GOOS == "windows" {
- // Symlinks are not supported under windows.
- _, err := os.Lstat(path)
- if err != nil {
- return "", err
- }
- return Clean(path), nil
- }
- const maxIter = 255
- originalPath := path
- // consume path by taking each frontmost path element,
- // expanding it if it's a symlink, and appending it to b
- var b bytes.Buffer
- for n := 0; path != ""; n++ {
- if n > maxIter {
- return "", errors.New("EvalSymlinks: too many links in " + originalPath)
- }
-
- // find next path component, p
- i := strings.IndexRune(path, Separator)
- var p string
- if i == -1 {
- p, path = path, ""
- } else {
- p, path = path[:i], path[i+1:]
- }
-
- if p == "" {
- if b.Len() == 0 {
- // must be absolute path
- b.WriteRune(Separator)
- }
- continue
- }
-
- fi, err := os.Lstat(b.String() + p)
- if err != nil {
- return "", err
- }
- if fi.Mode()&os.ModeSymlink == 0 {
- b.WriteString(p)
- if path != "" {
- b.WriteRune(Separator)
- }
- continue
- }
-
- // it's a symlink, put it at the front of path
- dest, err := os.Readlink(b.String() + p)
- if err != nil {
- return "", err
- }
- if IsAbs(dest) {
- b.Reset()
- }
- path = dest + string(Separator) + path
- }
- return Clean(b.String()), nil
+ return evalSymlinks(path)
}
src/pkg/path/filepath/symlink_windows.go (新規ファイル)
Windows環境でのevalSymlinksの実装。syscall.GetLongPathNameを利用してロングパス名を取得します。
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package filepath
import (
"syscall"
)
func evalSymlinks(path string) (string, error) {
p := syscall.StringToUTF16(path)
b := p // GetLongPathName says we can reuse buffer
n, err := syscall.GetLongPathName(&p[0], &b[0], uint32(len(b)))
if err != nil {
return "", err
}
if n > uint32(len(b)) {
b = make([]uint16, n)
n, err = syscall.GetLongPathName(&p[0], &b[0], uint32(len(b)))
if err != nil {
return "", err
}
}
b = b[:n]
return Clean(syscall.UTF16ToString(b)), nil
}
src/pkg/syscall/syscall_windows.go
GetLongPathName関数の宣言が追加されました。
--- a/src/pkg/syscall/syscall_windows.go
+++ b/src/pkg/syscall/syscall_windows.go
@@ -174,6 +174,7 @@ func NewCallback(fn interface{}) uintptr
//sys SetHandleInformation(handle Handle, mask uint32, flags uint32) (err error)
//sys FlushFileBuffers(handle Handle) (err error)
//sys GetFullPathName(path *uint16, buflen uint32, buf *uint16, fname **uint16) (n uint32, err error) = kernel32.GetFullPathNameW
+//sys GetLongPathName(path *uint16, buf *uint16, buflen uint32) (n uint32, err error) = kernel32.GetLongPathNameW
//sys CreateFileMapping(fhandle Handle, sa *SecurityAttributes, prot uint32, maxSizeHigh uint32, maxSizeLow uint32, name *uint16) (handle Handle, err error) = kernel32.CreateFileMappingW
//sys MapViewOfFile(handle Handle, access uint32, offsetHigh uint32, offsetLow uint32, length uintptr) (addr uintptr, err error)
//sys UnmapViewOfFile(addr uintptr) (err error)
src/pkg/syscall/zsyscall_windows_386.go および src/pkg/syscall/zsyscall_windows_amd64.go
GetLongPathName関数の実装が追加されました。これは、Windows APIのGetLongPathNameWを呼び出すためのGoのラッパーです。
--- a/src/pkg/syscall/zsyscall_windows_386.go
+++ b/src/pkg/syscall/zsyscall_windows_386.go
@@ -890,6 +891,19 @@ func GetFullPathName(path *uint16, buflen uint32, buf *uint16, fname **uint16) (
return
}
+func GetLongPathName(path *uint16, buf *uint16, buflen uint32) (n uint32, err error) {
+ r0, _, e1 := Syscall(procGetLongPathNameW.Addr(), 3, uintptr(unsafe.Pointer(path)), uintptr(unsafe.Pointer(buf)), uintptr(buflen))
+ n = uint32(r0)
+ if n == 0 {
+ if e1 != 0 {
+ err = error(e1)
+ } else {
+ err = EINVAL
+ }
+ }
+ return
+}
+
func CreateFileMapping(fhandle Handle, sa *SecurityAttributes, prot uint32, maxSizeHigh uint32, maxSizeLow uint32, name *uint16) (handle Handle, err error) {
r0, _, e1 := Syscall6(procCreateFileMappingW.Addr(), 6, uintptr(fhandle), uintptr(unsafe.Pointer(sa)), uintptr(prot), uintptr(maxSizeHigh), uintptr(maxSizeLow), uintptr(unsafe.Pointer(name)))
handle = Handle(r0)
コアとなるコードの解説
このコミットの核心は、Windowsにおけるパス解決の複雑性に対応するため、Goのpath/filepath.EvalSymlinks関数がWindows APIのGetLongPathNameWを利用するように変更された点です。
-
path/filepath/path.goの変更:EvalSymlinks関数は、もはやプラットフォーム固有のロジックを直接含んでいません。これは、Goの標準ライブラリにおける一般的な設計パターンであり、プラットフォーム固有のコードを分離することで、コードの可読性と保守性を向上させます。return evalSymlinks(path)というシンプルな呼び出しに置き換えられたことで、EvalSymlinksは、コンパイル時に選択されたプラットフォーム固有のevalSymlinks実装に処理を委譲するようになりました。
-
symlink_windows.goの新規追加とevalSymlinksの実装:- このファイルは、
// +build windowsタグによってWindows環境でのみコンパイルされます。 evalSymlinks関数は、入力パスpathをsyscall.StringToUTF16でUTF-16エンコーディングに変換します。Windows APIは通常UTF-16を使用するため、これは必須のステップです。syscall.GetLongPathNameを呼び出し、変換されたパスを渡します。この関数は、ショートパス名、シンボリックリンク、ジャンクションポイントなどを解決し、そのパスが指す実際のファイルまたはディレクトリの完全なロングパス名をUTF-16形式でbバッファに書き込みます。GetLongPathNameは、必要なバッファサイズが現在のバッファサイズを超える場合、その必要なサイズを返します。この場合、コードは新しい、より大きなバッファをmake([]uint16, n)で作成し、再度GetLongPathNameを呼び出して、完全なロングパス名を取得します。これは、Windows APIを安全に利用するための一般的なパターンです。- 最後に、取得したUTF-16形式のロングパス名を
syscall.UTF16ToStringでGoの文字列(UTF-8)に変換し、filepath.Cleanでパスを正規化して返します。Clean関数は、余分なスラッシュの削除や、.、..の解決などを行います。
- このファイルは、
-
syscallパッケージの拡張:syscallパッケージは、Goプログラムがオペレーティングシステムの低レベルな機能(システムコール)にアクセスするためのインターフェースを提供します。syscall_windows.goでは、GetLongPathNameというGoの関数が、Windows APIのGetLongPathNameWに対応するように宣言されています。//sysディレクティブは、Goのツールチェーンがこの宣言に基づいて、対応するシステムコールを自動的に生成することを示します。zsyscall_windows_386.goとzsyscall_windows_amd64.goは、それぞれ32ビットおよび64ビットWindowsアーキテクチャ向けの自動生成されたシステムコールラッパーファイルです。このコミットでは、GetLongPathName関数がprocGetLongPathNameW(kernel32.dllからロードされるGetLongPathNameW関数のアドレス)を呼び出すように実装が追加されています。これにより、Goのコードから直接Windows APIを呼び出すことが可能になります。
この一連の変更により、Goのpath/filepath.EvalSymlinksは、Windows環境において、より堅牢で正確なパス解決能力を獲得しました。
関連リンク
- Go言語の
path/filepathパッケージ: https://pkg.go.dev/path/filepath - Go言語の
syscallパッケージ: https://pkg.go.dev/syscall - Windows API
GetLongPathNameW(Microsoft Learn): https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getlongpathname
参考にした情報源リンク
- Go言語のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Go言語のコードレビューシステム (Gerrit): https://go.dev/cl/5756049 (コミットメッセージに記載されているCLリンク)
- Microsoft Learn -
GetLongPathNameWdocumentation: https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getlongpathname - Goのビルドタグに関するドキュメント: https://go.dev/cmd/go/#hdr-Build_constraints
- Goの
syscallパッケージの利用方法に関する一般的な情報 (Goのドキュメントやブログ記事など)