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

[インデックス 12766] ファイルの概要

このコミットは、Go言語の path/filepath パッケージにおけるWindows環境でのシンボリックリンク評価(EvalSymlinks)の挙動を修正するものです。具体的には、Windowsパスのドライブレターの大文字・小文字の扱いに関する不整合を解消し、EvalSymlinks の結果が一意になるように改善しています。これにより、c:\aC:\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\Userc:\users\user は同じパスとして扱われます。しかし、Go言語の path/filepath パッケージの EvalSymlinks 関数がシンボリックリンクを評価する際に、内部的に使用されるWindows API関数 syscall.GetLongPathName がドライブレターのケースを変更しないという問題がありました。

この挙動により、例えば c:\aC:\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 Filesc:\program files は同じディレクトリを指します。
  • シンボリックリンク (Symbolic Link): ファイルシステム上の特殊なファイルの一種で、別のファイルやディレクトリへの参照(ポインタ)として機能します。シンボリックリンクをたどると、それが指し示す元のファイルやディレクトリにアクセスできます。Windowsでは、NTFSファイルシステムでサポートされています。
  • syscall.GetLongPathName: Windows API関数の一つで、短いパス名(8.3形式など)を長いパス名に変換するために使用されます。この関数は、パスの正規化に役立ちますが、ドライブレターのケースは変更しないという特性があります。
  • パスの一意性: プログラミングにおいて、同じファイルやディレクトリを指すパスは、常に同じ文字列として表現されることが望ましいです。これにより、パスの比較やキャッシュ、ハッシュ化などが正確に行えるようになります。

技術的詳細

Windows環境において、path/filepath パッケージの EvalSymlinks 関数は、内部で syscall.GetLongPathName を呼び出してパスの解決を行っていました。しかし、syscall.GetLongPathName は、パスのドライブレターのケース(大文字・小文字)を保持したままパスを返します。例えば、c:\foo\bar というパスが与えられた場合、syscall.GetLongPathNamec:\foo\bar を返す可能性があり、C:\foo\bar を返す可能性もあります。

この挙動は、Windowsファイルシステム自体がドライブレターのケースを区別しないため、通常の使用では問題になりません。しかし、Goの path/filepath パッケージが EvalSymlinks の結果として「一意な」パスを返すことを期待する場合、このケースの不整合が問題となります。例えば、os.Getwd()(現在の作業ディレクトリを取得する関数)は、ドライブレターを大文字で返すことが一般的です。そのため、EvalSymlinks が小文字のドライブレターを返すと、os.Getwd() の結果と EvalSymlinks の結果が、実質的に同じパスを指しているにもかかわらず、文字列としては異なるという状況が発生します。

このコミットでは、EvalSymlinks が返すパスのドライブレターが小文字である場合に、強制的に大文字に変換する処理を追加することで、この不整合を解消しています。これにより、EvalSymlinks は常に正規化された(ドライブレターが大文字の)パスを返すようになり、パスの一意性が保証されます。

コアとなるコードの変更箇所

変更は主に以下の2つのファイルで行われています。

  1. 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
     }
    
  2. 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

このコードブロックは、以下の条件をチェックします。

  1. len(s) >= 2: パス文字列の長さが少なくとも2文字以上であること(ドライブレターとコロンを含むため)。
  2. s[1] == ':': パスの2文字目がコロンであること(ドライブレターの形式 X: を確認)。
  3. '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 テストケースは、この修正が正しく機能することを確認するためのものです。

  1. runtime.GOOS != "windows": テストがWindows環境でのみ実行されるようにします。
  2. wd, _ := os.Getwd(): 現在の作業ディレクトリのパスを取得します。
  3. lp := strings.ToLower(wd)up := strings.ToUpper(wd): 取得したパスをそれぞれ完全に小文字と完全に大文字に変換します。
  4. filepath.EvalSymlinks(lp)filepath.EvalSymlinks(up): 小文字と大文字のパスそれぞれに対して EvalSymlinks を呼び出します。
  5. if flp != fup: EvalSymlinks の結果が異なる場合、テストは失敗します。これは、ドライブレターのケースが異なっていても、EvalSymlinks が同じ正規化されたパスを返すことを期待しているためです。

このテストは、修正が意図した通りに、ドライブレターのケースに関わらず EvalSymlinks が一貫した結果を返すことを保証します。

関連リンク

参考にした情報源リンク