[インデックス 12780] ファイルの概要
このコミットは、Go言語のpath/filepath
パッケージにおけるWindows環境でのシンボリックリンク評価(evalSymlinks
関数)の挙動を改善するものです。具体的には、Windows APIのGetShortPathName
関数を導入し、これを利用してGetLongPathName
関数が正しく動作するように強制することで、パスの正規化とシンボリックリンク解決の堅牢性を高めています。
コミット
commit 7a3965417426e4405a6ec81ce486668fa5c36e36
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Tue Mar 27 15:53:08 2012 +1100
path/filepath: use windows GetShortPathName api to force GetLongPathName to do its work
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5928043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7a3965417426e4405a6ec81ce486668fa5c36e36
元コミット内容
このコミットは、Go言語のpath/filepath
パッケージにおいて、Windows環境でのパス解決、特にシンボリックリンク(またはWindowsのジャンクションやディレクトリシンボリックリンク)の評価に関する問題を修正することを目的としています。既存のGetLongPathName
APIが特定の状況下で期待通りに動作しない場合があるため、GetShortPathName
APIを併用することで、この問題を回避し、より正確なパスの正規化を実現しています。
変更の背景
Windowsファイルシステムには、長いファイル名(Long File Name, LFN)と短いファイル名(Short File Name, SFN、または8.3形式ファイル名)という概念が存在します。GetLongPathName
関数は、与えられたパスの短い形式を長い形式に変換するために使用されます。しかし、この関数は、パスの一部がシンボリックリンクやジャンクションである場合、またはパスの途中に存在しない要素が含まれる場合など、特定の条件下で期待通りの完全な正規化されたパスを返さないことがありました。
この問題は、GoプログラムがWindows上でファイルパスを正確に解決し、シンボリックリンクを辿る際に不正確な結果を招く可能性がありました。特に、path/filepath
パッケージのEvalSymlinks
関数は、実際のファイルシステム上のパスを解決し、シンボリックリンクを解決した後の「真の」パスを返すことを目的としています。GetLongPathName
の挙動の不安定さが、この目的を達成する上での障害となっていました。
コミットメッセージにある「force GetLongPathName to do its work」という表現は、GetLongPathName
が本来の機能を果たすように、何らかの「きっかけ」を与える必要があることを示唆しています。その「きっかけ」として、一度パスを短い形式に変換するGetShortPathName
を利用するというアプローチが取られました。短いパスは、ファイルシステムが内部的に管理する別の形式であり、これを介することで、GetLongPathName
がより確実に完全な長いパスを解決できるようになるという仮説に基づいています。
前提知識の解説
Windowsのファイルパスと8.3形式ファイル名
Windowsのファイルシステム(NTFSなど)では、長いファイル名が標準ですが、MS-DOSとの互換性のために「8.3形式ファイル名」という短いファイル名も内部的に保持しています。これは、ファイル名の先頭8文字と拡張子の3文字からなる形式で、例えばProgram Files
はPROGRA~1
のようになることがあります。
GetLongPathName
API
Windows APIのGetLongPathName
関数は、指定されたパスの短い形式(8.3形式)を、その長い形式に変換するために使用されます。例えば、C:\PROGRA~1\MICROS~1
のようなパスをC:\Program Files\Microsoft Office
のような長いパスに変換します。
GetShortPathName
API
Windows APIのGetShortPathName
関数は、指定されたパスの長い形式を、その短い形式(8.3形式)に変換するために使用されます。例えば、C:\Program Files\Microsoft Office
のようなパスをC:\PROGRA~1\MICROS~1
のような短いパスに変換します。
シンボリックリンク、ジャンクション、ハードリンク (Windows)
- シンボリックリンク (Symbolic Link): ファイルまたはディレクトリへのポインタです。元のファイルやディレクトリが削除されると、シンボリックリンクは壊れます。Unix/Linuxのシンボリックリンクに似ています。
- ジャンクション (Junction): ディレクトリに特化したシンボリックリンクのようなもので、NTFSファイルシステム内の別のディレクトリを指します。主にボリューム内のディレクトリを別の場所にマウントするのに使われます。
- ハードリンク (Hard Link): 同じファイルデータへの複数のエントリです。元のファイルが削除されても、ハードリンクが存在する限りデータは残ります。
Goのpath/filepath.EvalSymlinks
は、これらの「再解析ポイント」(reparse point)を解決して、最終的な物理パスを特定しようとします。
Goの syscall
パッケージ
Go言語のsyscall
パッケージは、オペレーティングシステムが提供する低レベルなプリミティブ(システムコール)へのインターフェースを提供します。これにより、Goプログラムから直接OSの機能(ファイル操作、ネットワーク通信、プロセス管理など)を呼び出すことができます。Windowsの場合、syscall
パッケージはWindows API関数を呼び出すためのラッパーを提供します。
技術的詳細
このコミットの核心は、path/filepath
パッケージのevalSymlinks
関数におけるパス解決ロジックの変更です。以前は、直接syscall.GetLongPathName
を呼び出してパスを正規化しようとしていました。しかし、前述の通り、この関数は特定の条件下で期待通りに動作しないことがありました。
新しいアプローチでは、evalSymlinks
関数内で、まず入力パスをsyscall.GetShortPathName
を使って短いパス形式に変換します。この短いパスは、ファイルシステムがより確実に認識できる形式であるため、その後のsyscall.GetLongPathName
の呼び出しが、より正確な長いパスを返す可能性が高まります。
具体的な流れは以下のようになります。
-
toShort
関数の導入:- 入力された長いパスを
syscall.StringToUTF16
でUTF-16エンコードされたバイト列に変換します。 syscall.GetShortPathName
を呼び出し、このUTF-16パスの短い形式を取得します。- 取得した短いパスを
syscall.UTF16ToString
でGoの文字列に変換して返します。 - この関数は、
GetShortPathName
が返すバッファサイズが足りない場合に、バッファを再割り当てして再度呼び出すロジックを含んでいます。
- 入力された長いパスを
-
toLong
関数の導入:- これは既存の
evalSymlinks
関数内のGetLongPathName
呼び出しロジックを独立させたものです。 - 入力されたパスを
syscall.StringToUTF16
でUTF-16エンコードされたバイト列に変換します。 syscall.GetLongPathName
を呼び出し、このUTF-16パスの長い形式を取得します。- 取得した長いパスを
syscall.UTF16ToString
でGoの文字列に変換して返します。 - こちらも
GetLongPathName
が返すバッファサイズが足りない場合に、バッファを再割り当てして再度呼び出すロジックを含んでいます。
- これは既存の
-
evalSymlinks
関数の変更:evalSymlinks
関数は、まず入力パスを新しく導入されたtoShort
関数に渡して短いパスを取得します。- 次に、この短いパスを新しく導入された
toLong
関数に渡して、最終的な長いパスを取得します。 - これにより、
GetLongPathName
がより確実に動作することが期待されます。 - 最後に、ドライブレターを大文字に変換する既存のロジック(例:
c:\a
をC:\a
に)と、path/filepath.Clean
によるパスの正規化を適用して結果を返します。
この二段階の変換(長いパス -> 短いパス -> 長いパス)は、Windowsのファイルシステムが内部的にパスを解決する際の挙動を「リセット」または「再評価」させる効果があると考えられます。特に、シンボリックリンクやジャンクションが絡む複雑なパスにおいて、このアプローチがより正確な解決を導くことが期待されます。
コアとなるコードの変更箇所
このコミットでは、主に以下のファイルが変更されています。
api/go1.txt
: Go 1のAPI変更ログにsyscall.GetShortPathName
が追加されたことを記録しています。src/pkg/path/filepath/symlink_windows.go
:toShort
関数とtoLong
関数が新しく追加されました。- 既存の
evalSymlinks
関数が、これらの新しい関数を利用するように変更されました。
src/pkg/syscall/syscall_windows.go
:GetShortPathName
の外部関数宣言が追加されました。これにより、GoコードからWindows APIのGetShortPathNameW
を呼び出せるようになります。
src/pkg/syscall/zsyscall_windows_386.go
:- 32ビットWindows環境向けの
GetShortPathName
システムコールラッパーが追加されました。
- 32ビットWindows環境向けの
src/pkg/syscall/zsyscall_windows_amd64.go
:- 64ビットWindows環境向けの
GetShortPathName
システムコールラッパーが追加されました。
- 64ビットWindows環境向けの
コアとなるコードの解説
src/pkg/path/filepath/symlink_windows.go
// 新しく追加された toShort 関数
func toShort(path string) (string, error) {
p := syscall.StringToUTF16(path) // Go文字列をUTF-16に変換
b := p // GetShortPathNameはバッファの再利用が可能とされている
n, err := syscall.GetShortPathName(&p[0], &b[0], uint32(len(b))) // 短いパス名を取得
if err != nil {
return "", err
}
if n > uint32(len(b)) { // バッファが足りない場合
b = make([]uint16, n) // より大きなバッファを確保
n, err = syscall.GetShortPathName(&p[0], &b[0], uint32(len(b))) // 再度取得
if err != nil {
return "", err
}
}
return syscall.UTF16ToString(b), nil // UTF-16をGo文字列に変換して返す
}
// 新しく追加された toLong 関数 (既存のロジックを分離)
func toLong(path string) (string, error) {
p := syscall.StringToUTF16(path)
b := p // GetLongPathNameはバッファの再利用が可能とされている
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 syscall.UTF16ToString(b), nil
}
// evalSymlinks 関数の変更点
func evalSymlinks(path string) (string, error) {
p, err := toShort(path) // まず短いパスに変換
if err != nil {
return "", err
}
p, err = toLong(p) // 次に長いパスに変換
if err != nil {
return "", err
}
// ドライブレターを大文字にする既存のロジック
if len(p) >= 2 && p[1] == ':' && 'a' <= p[0] && p[0] <= 'z' {
p = string(p[0]+'A'-'a') + p[1:]
}
return Clean(p), nil // パスをクリーンアップして返す
}
src/pkg/syscall/syscall_windows.go
//sys GetShortPathName(longpath *uint16, shortpath *uint16, buflen uint32) (n uint32, err error) = kernel32.GetShortPathNameW
この行は、Goのsyscall
パッケージがWindowsのkernel32.dll
にあるGetShortPathNameW
関数を呼び出すための宣言です。//sys
ディレクティブは、Goのツールチェーンがこの宣言に基づいて、対応するシステムコールラッパーコード(zsyscall_windows_386.go
やzsyscall_windows_amd64.go
に生成されるもの)を自動生成するために使用されます。
src/pkg/syscall/zsyscall_windows_386.go
および src/pkg/syscall/zsyscall_windows_amd64.go
これらのファイルには、GetShortPathName
の実際のシステムコール呼び出しを行うためのGoコードが自動生成されています。例えば、32ビット版のGetShortPathName
関数は以下のようになります。
func GetShortPathName(longpath *uint16, shortpath *uint16, buflen uint32) (n uint32, err error) {
r0, _, e1 := Syscall(procGetShortPathNameW.Addr(), 3, uintptr(unsafe.Pointer(longpath)), uintptr(unsafe.Pointer(shortpath)), uintptr(buflen))
n = uint32(r0)
if n == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = EINVAL
}
}
return
}
これは、procGetShortPathNameW
(kernel32.GetShortPathNameW
へのポインタ)のアドレスを使って、Syscall
関数を呼び出しています。Syscall
は、指定されたアドレスのWindows API関数を、与えられた引数で実行します。返された値(r0
)は、関数の戻り値(ここでは書き込まれた文字数n
)として解釈され、エラーコード(e1
)はGoのエラーに変換されます。
関連リンク
- Go CL 5928043: https://golang.org/cl/5928043
参考にした情報源リンク
- GetLongPathName function (fileapi.h) - Win32 apps | Microsoft Learn
- GetShortPathName function (fileapi.h) - Win32 apps | Microsoft Learn
- Creating Hard Links, Junctions, and Symbolic Links - Win32 apps | Microsoft Learn
- Go言語のsyscallパッケージについて - Qiita (一般的なsyscallパッケージの解説として)
- GoのWindows API呼び出し - Qiita (GoからWindows APIを呼び出す一般的な方法として)