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

[インデックス 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 FilesPROGRA~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の呼び出しが、より正確な長いパスを返す可能性が高まります。

具体的な流れは以下のようになります。

  1. toShort関数の導入:

    • 入力された長いパスをsyscall.StringToUTF16でUTF-16エンコードされたバイト列に変換します。
    • syscall.GetShortPathNameを呼び出し、このUTF-16パスの短い形式を取得します。
    • 取得した短いパスをsyscall.UTF16ToStringでGoの文字列に変換して返します。
    • この関数は、GetShortPathNameが返すバッファサイズが足りない場合に、バッファを再割り当てして再度呼び出すロジックを含んでいます。
  2. toLong関数の導入:

    • これは既存のevalSymlinks関数内のGetLongPathName呼び出しロジックを独立させたものです。
    • 入力されたパスをsyscall.StringToUTF16でUTF-16エンコードされたバイト列に変換します。
    • syscall.GetLongPathNameを呼び出し、このUTF-16パスの長い形式を取得します。
    • 取得した長いパスをsyscall.UTF16ToStringでGoの文字列に変換して返します。
    • こちらもGetLongPathNameが返すバッファサイズが足りない場合に、バッファを再割り当てして再度呼び出すロジックを含んでいます。
  3. evalSymlinks関数の変更:

    • evalSymlinks関数は、まず入力パスを新しく導入されたtoShort関数に渡して短いパスを取得します。
    • 次に、この短いパスを新しく導入されたtoLong関数に渡して、最終的な長いパスを取得します。
    • これにより、GetLongPathNameがより確実に動作することが期待されます。
    • 最後に、ドライブレターを大文字に変換する既存のロジック(例: c:\aC:\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システムコールラッパーが追加されました。
  • src/pkg/syscall/zsyscall_windows_amd64.go:
    • 64ビットWindows環境向けのGetShortPathNameシステムコールラッパーが追加されました。

コアとなるコードの解説

// 新しく追加された 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.gozsyscall_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
}

これは、procGetShortPathNameWkernel32.GetShortPathNameWへのポインタ)のアドレスを使って、Syscall関数を呼び出しています。Syscallは、指定されたアドレスのWindows API関数を、与えられた引数で実行します。返された値(r0)は、関数の戻り値(ここでは書き込まれた文字数n)として解釈され、エラーコード(e1)はGoのエラーに変換されます。

関連リンク

参考にした情報源リンク