[インデックス 12766] ファイルの概要
このコミットは、Go言語の path/filepath
パッケージにおけるWindows環境でのシンボリックリンク評価(EvalSymlinks
)の挙動を修正するものです。具体的には、Windowsパスのドライブレターの大文字・小文字の扱いに関する不整合を解消し、EvalSymlinks
の結果が一意になるように改善しています。これにより、c:\a
と C:\a
のようにドライブレターのケースが異なるパスに対しても、EvalSymlinks
が同じ結果を返すようになります。
コミット
commit cf13bd3fab523931c3555c82c3d2fe896d2935c9
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Tue Mar 27 12:56:56 2012 +1100
path/filepath: convert drive letter to upper case in windows EvalSymlinks
Fixes #3347.
R=golang-dev, aram, r, rsc
CC=golang-dev
https://golang.org/cl/5918043
---
src/pkg/path/filepath/path_test.go | 23 +++++++++++++++++++++++
src/pkg/path/filepath/symlink_windows.go | 10 +++++++++-
2 files changed, 32 insertions(+), 1 deletion(-)
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/cf13bd3fab523931c3555c82c3d2fe896d2935c9
元コミット内容
path/filepath
: Windowsの EvalSymlinks
においてドライブレターを大文字に変換する。
Issue #3347 を修正。
変更の背景
Windowsのファイルシステムは、パスにおけるドライブレターの大文字・小文字を区別しません。例えば、C:\Users\User
と c:\users\user
は同じパスとして扱われます。しかし、Go言語の path/filepath
パッケージの EvalSymlinks
関数がシンボリックリンクを評価する際に、内部的に使用されるWindows API関数 syscall.GetLongPathName
がドライブレターのケースを変更しないという問題がありました。
この挙動により、例えば c:\a
と C:\a
のようにドライブレターのケースが異なる同じパスに対して EvalSymlinks
を呼び出した場合、異なる結果が返される可能性がありました。これは、パスの一意性を保証する必要があるGoの path/filepath
パッケージの設計思想に反し、予期せぬバグや不整合を引き起こす可能性がありました。
このコミットは、この不整合を解消し、EvalSymlinks
が常に一意で正規化されたパス(具体的にはドライブレターを大文字にしたパス)を返すようにすることで、Windows環境でのパス処理の堅牢性を向上させることを目的としています。コミットメッセージにある Fixes #3347
は、この問題がGoの内部バグトラッカーで追跡されていたことを示しています。
前提知識の解説
path/filepath
パッケージ: Go言語の標準ライブラリの一部で、ファイルパスの操作(結合、分割、クリーンアップ、絶対パスへの変換など)や、ファイルシステム上のパスに関する情報(シンボリックリンクの解決など)を提供するパッケージです。OS固有のパス区切り文字や慣習を抽象化し、クロスプラットフォームなパス操作を可能にします。EvalSymlinks
関数:path/filepath
パッケージに含まれる関数で、与えられたパスがシンボリックリンクである場合、そのリンクが指し示す最終的な物理パスを再帰的に評価して返します。シンボリックリンクの連鎖を解決し、実際のファイルやディレクトリのパスを取得するために使用されます。- Windowsのパス慣習: Windowsでは、パスは通常ドライブレター(例:
C:
)から始まり、その後にディレクトリとファイル名が続きます。Windowsのファイルシステムは、パスの大文字・小文字を区別しない(case-insensitive)という特徴があります。つまり、C:\Program Files
とc:\program files
は同じディレクトリを指します。 - シンボリックリンク (Symbolic Link): ファイルシステム上の特殊なファイルの一種で、別のファイルやディレクトリへの参照(ポインタ)として機能します。シンボリックリンクをたどると、それが指し示す元のファイルやディレクトリにアクセスできます。Windowsでは、NTFSファイルシステムでサポートされています。
syscall.GetLongPathName
: Windows API関数の一つで、短いパス名(8.3形式など)を長いパス名に変換するために使用されます。この関数は、パスの正規化に役立ちますが、ドライブレターのケースは変更しないという特性があります。- パスの一意性: プログラミングにおいて、同じファイルやディレクトリを指すパスは、常に同じ文字列として表現されることが望ましいです。これにより、パスの比較やキャッシュ、ハッシュ化などが正確に行えるようになります。
技術的詳細
Windows環境において、path/filepath
パッケージの EvalSymlinks
関数は、内部で syscall.GetLongPathName
を呼び出してパスの解決を行っていました。しかし、syscall.GetLongPathName
は、パスのドライブレターのケース(大文字・小文字)を保持したままパスを返します。例えば、c:\foo\bar
というパスが与えられた場合、syscall.GetLongPathName
は c:\foo\bar
を返す可能性があり、C:\foo\bar
を返す可能性もあります。
この挙動は、Windowsファイルシステム自体がドライブレターのケースを区別しないため、通常の使用では問題になりません。しかし、Goの path/filepath
パッケージが EvalSymlinks
の結果として「一意な」パスを返すことを期待する場合、このケースの不整合が問題となります。例えば、os.Getwd()
(現在の作業ディレクトリを取得する関数)は、ドライブレターを大文字で返すことが一般的です。そのため、EvalSymlinks
が小文字のドライブレターを返すと、os.Getwd()
の結果と EvalSymlinks
の結果が、実質的に同じパスを指しているにもかかわらず、文字列としては異なるという状況が発生します。
このコミットでは、EvalSymlinks
が返すパスのドライブレターが小文字である場合に、強制的に大文字に変換する処理を追加することで、この不整合を解消しています。これにより、EvalSymlinks
は常に正規化された(ドライブレターが大文字の)パスを返すようになり、パスの一意性が保証されます。
コアとなるコードの変更箇所
変更は主に以下の2つのファイルで行われています。
-
src/pkg/path/filepath/symlink_windows.go
:evalSymlinks
関数内で、syscall.UTF16ToString(b)
で得られたパス文字列s
に対して、ドライブレターが大文字に変換されるロジックが追加されました。--- a/src/pkg/path/filepath/symlink_windows.go +++ b/src/pkg/path/filepath/symlink_windows.go @@ -23,5 +23,13 @@ func evalSymlinks(path string) (string, error) { } } b = b[:n] - return Clean(syscall.UTF16ToString(b)), nil + s := syscall.UTF16ToString(b) + // syscall.GetLongPathName does not change the case of the drive letter, + // but the result of EvalSymlinks must be unique, so we have + // EvalSymlinks(`c:\a`) == EvalSymlinks(`C:\a`). + // Make drive letter upper case. This matches what os.Getwd returns. + if len(s) >= 2 && s[1] == ':' && 'a' <= s[0] && s[0] <= 'z' { + s = string(s[0]+'A'-'a') + s[1:] + } + return Clean(s), nil }
-
src/pkg/path/filepath/path_test.go
:TestDriveLetterInEvalSymlinks
という新しいテストケースが追加されました。このテストは、現在の作業ディレクトリのパスを小文字と大文字の両方でEvalSymlinks
に渡し、その結果が一致することを確認します。--- a/src/pkg/path/filepath/path_test.go +++ b/src/pkg/path/filepath/path_test.go @@ -846,3 +846,26 @@ func TestVolumeName(t *testing.T) { }\n \t}\n }\n+\n+func TestDriveLetterInEvalSymlinks(t *testing.T) {\n+\tif runtime.GOOS != \"windows\" {\n+\t\treturn\n+\t}\n+\twd, _ := os.Getwd()\n+\tif len(wd) < 3 {\n+\t\tt.Errorf(\"Current directory path %q is too short\", wd)\n+\t}\n+\tlp := strings.ToLower(wd)\n+\tup := strings.ToUpper(wd)\n+\tflp, err := filepath.EvalSymlinks(lp)\n+\tif err != nil {\n+\t\tt.Fatalf(\"EvalSymlinks(%q) failed: %q\", lp, err)\n+\t}\n+\tfup, err := filepath.EvalSymlinks(up)\n+\tif err != nil {\n+\t\tt.Fatalf(\"EvalSymlinks(%q) failed: %q\", up, err)\n+\t}\n+\tif flp != fup {\n+\t\tt.Errorf(\"Results of EvalSymlinks do not match: %q and %q\", flp, fup)\n+\t}\n+}\n ```
コアとなるコードの解説
symlink_windows.go
の変更は、evalSymlinks
関数が syscall.GetLongPathName
から受け取ったパス文字列 s
を処理する部分にあります。
s := syscall.UTF16ToString(b)
// syscall.GetLongPathName does not change the case of the drive letter,
// but the result of EvalSymlinks must be unique, so we have
// EvalSymlinks(`c:\a`) == EvalSymlinks(`C:\a`).
// Make drive letter upper case. This matches what os.Getwd returns.
if len(s) >= 2 && s[1] == ':' && 'a' <= s[0] && s[0] <= 'z' {
s = string(s[0]+'A'-'a') + s[1:]
}
return Clean(s), nil
このコードブロックは、以下の条件をチェックします。
len(s) >= 2
: パス文字列の長さが少なくとも2文字以上であること(ドライブレターとコロンを含むため)。s[1] == ':'
: パスの2文字目がコロンであること(ドライブレターの形式X:
を確認)。'a' <= s[0] && s[0] <= 'z'
: パスの1文字目(ドライブレター)が小文字のアルファベットであること。
これらの条件がすべて満たされた場合、つまりパスが小文字のドライブレターで始まるWindowsパスであると判断された場合、以下の処理が行われます。
s = string(s[0]+'A'-'a') + s[1:]
この行は、小文字のドライブレターを大文字に変換しています。例えば、'c'
は 'c' + ('A' - 'a')
によって 'C'
に変換されます。変換された大文字のドライブレターと、元のパスの2文字目以降(コロンと残りのパス)を結合し、新しいパス文字列 s
を生成します。
最後に、Clean(s)
を呼び出してパスを正規化し、その結果を返します。Clean
関数は、パスの冗長な要素(例: .
や ..
、重複するパス区切り文字)を削除し、標準的な形式に整える役割があります。
path_test.go
に追加された TestDriveLetterInEvalSymlinks
テストケースは、この修正が正しく機能することを確認するためのものです。
runtime.GOOS != "windows"
: テストがWindows環境でのみ実行されるようにします。wd, _ := os.Getwd()
: 現在の作業ディレクトリのパスを取得します。lp := strings.ToLower(wd)
とup := strings.ToUpper(wd)
: 取得したパスをそれぞれ完全に小文字と完全に大文字に変換します。filepath.EvalSymlinks(lp)
とfilepath.EvalSymlinks(up)
: 小文字と大文字のパスそれぞれに対してEvalSymlinks
を呼び出します。if flp != fup
:EvalSymlinks
の結果が異なる場合、テストは失敗します。これは、ドライブレターのケースが異なっていても、EvalSymlinks
が同じ正規化されたパスを返すことを期待しているためです。
このテストは、修正が意図した通りに、ドライブレターのケースに関わらず EvalSymlinks
が一貫した結果を返すことを保証します。
関連リンク
参考にした情報源リンク
- Go言語
path/filepath
パッケージ公式ドキュメント: https://pkg.go.dev/path/filepath - Go言語
syscall
パッケージ公式ドキュメント: https://pkg.go.dev/syscall - Windows API
GetLongPathName
(Microsoft Learn): https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getlongpathnamea - コミットメッセージに記載されている
Fixes #3347
は、Goプロジェクトの内部バグトラッカーのIssue番号を指していると考えられます。公開されているGitHub Issuesでは直接対応するIssueは見つかりませんでしたが、これは当時のGoプロジェクトのワークフローによるものです。