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

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

コミット

  • コミットハッシュ: fc268acf05adf5d0437ef1bf80c6e919818fe6ff
  • 作者: Russ Cox rsc@golang.org
  • コミット日時: 2012年2月29日(水)16:37:40 -0500

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/fc268acf05adf5d0437ef1bf80c6e919818fe6ff

元コミット内容

path/filepath: steer people away from HasPrefix

The strikes against it are:

1. It does not take path boundaries into account.
2. It assumes that Windows==case-insensitive file system
and non-Windows==case-sensitive file system, neither of
which is always true.
3. Comparing ToLower against ToLower is not a correct
implementation of a case-insensitive string comparison.
4. If it returns true on Windows you still don't know how long
the matching prefix is in bytes, so you can't compute what
the suffix is.

R=golang-dev, r, dsymonds, r
CC=golang-dev
https://golang.org/cl/5712045

変更の背景

このコミットは、Go言語の標準ライブラリpath/filepathパッケージに含まれるHasPrefix関数の使用を非推奨にするための変更です。コミットメッセージには、この関数が抱える複数の問題点が具体的に挙げられており、それらがこの変更の主な背景となっています。

具体的には、以下の4つの問題点が指摘されています。

  1. パスの境界を考慮しない: HasPrefixは単なる文字列のプレフィックス比較を行うため、ファイルパスにおけるディレクトリの区切り(/\)といった論理的な境界を考慮しません。例えば、/foo/bar/fooで始まるかどうかは正しく判定できますが、/foobar/fooで始まるかどうかを判定する際に、/foobar/fooディレクトリ内のファイルやディレクトリではないにも関わらずtrueを返してしまう可能性があります。これは、ファイルパスの操作において予期せぬ結果を招く可能性があります。
  2. OSごとのファイルシステム特性の誤った仮定: HasPrefixは、Windowsファイルシステムは常に大文字・小文字を区別せず(case-insensitive)、非Windows(Unix系など)ファイルシステムは常に大文字・小文字を区別する(case-sensitive)という仮定に基づいていました。しかし、これは常に真ではありません。例えば、macOSのHFS+ファイルシステムはデフォルトで大文字・小文字を区別しませんし、Linuxでも特定のファイルシステム設定によっては大文字・小文字を区別しない場合があります。この誤った仮定は、クロスプラットフォームでの動作の不整合やバグの原因となります。
  3. 不正確な大文字・小文字を区別しない比較: 大文字・小文字を区別しない文字列比較を実装するために、両方の文字列を小文字に変換してから比較するという手法が用いられていました。しかし、これはUnicodeの複雑な文字変換ルール(特にトルコ語のiIのような特殊なケース)を考慮しておらず、正確な大文字・小文字を区別しない比較とは言えません。
  4. Windowsにおけるマッチングプレフィックスの長さの不明瞭さ: Windows環境でHasPrefixtrueを返した場合でも、マッチしたプレフィックスの正確なバイト長が不明瞭であるという問題がありました。これは、マッチしたプレフィックスを除いた残りのサフィックス部分を正確に計算することができないことを意味し、パス操作の柔軟性を損ないます。

これらの問題点から、HasPrefix関数はファイルパスの操作において信頼性が低く、誤用を招きやすいと判断され、その使用を避けるように促す変更が導入されました。

前提知識の解説

このコミットを理解するためには、以下の概念について理解しておく必要があります。

  • Go言語のpath/filepathパッケージ: Go言語の標準ライブラリの一部であり、オペレーティングシステムに依存しないパス操作機能を提供します。ファイルパスの結合、クリーンアップ、ディレクトリ名やファイル名の抽出、絶対パスへの変換など、様々なパス関連のユーティリティ関数が含まれています。このパッケージは、異なるOS(Unix、Windows、Plan 9など)のパス表現の違いを吸収し、統一的なインターフェースを提供することを目指しています。

  • strings.HasPrefix関数: Go言語の標準ライブラリstringsパッケージに含まれる関数で、ある文字列が指定されたプレフィックスで始まるかどうかを判定します。これは純粋な文字列比較であり、ファイルパスのセマンティクス(ディレクトリの区切りや大文字・小文字の区別など)は考慮しません。

  • ファイルシステムにおけるパスの概念: ファイルシステムは、ファイルやディレクトリを階層的に整理するための仕組みです。ファイルやディレクトリの場所は「パス」によって指定されます。パスの表現方法はオペレーティングシステムによって異なります。

    • Unix系(Linux, macOSなど): パス区切り文字は/(スラッシュ)です。大文字・小文字を区別するファイルシステムが一般的です(例: file.txtFile.txtは異なるファイル)。
    • Windows: パス区切り文字は\(バックスラッシュ)です。ドライブレター(例: C:)から始まる絶対パスが一般的です。ファイルシステムは通常、大文字・小文字を区別しません(例: file.txtFile.txtは同じファイルとみなされることが多い)。
    • Plan 9: Unix系に似ていますが、パスの概念やファイルシステムの設計思想に独自の特徴があります。
  • 大文字・小文字の区別(Case-sensitivity): ファイルシステムや文字列比較において、大文字と小文字を同じものとみなすか、異なるものとみなすかの特性です。

    • Case-sensitive(大文字・小文字を区別する): foo.txtFoo.txtは異なるファイルとして扱われます。
    • Case-insensitive(大文字・小文字を区別しない): foo.txtFoo.txtは同じファイルとして扱われます。
  • Unicodeと文字列比較: 現代のソフトウェアでは、様々な言語の文字を扱うためにUnicodeが広く用いられています。Unicodeには、同じ文字でも複数の表現形式があったり、大文字・小文字変換のルールが言語によって異なったりする複雑さがあります。単純なToLowerによる比較では、これらの複雑なケースを正確に処理できない場合があります。

技術的詳細

このコミットがHasPrefix関数の使用を非推奨にした技術的な理由は、前述の4つの問題点に深く関連しています。

  1. パスの境界の無視: path/filepathパッケージの目的は、OSに依存しない形でファイルパスを安全かつ正確に操作することです。しかし、HasPrefixは内部的にstrings.HasPrefixを呼び出すだけであり、これは単なる文字列の字面上の比較に過ぎません。ファイルパスにおいては、/\といったディレクトリセパレータが重要な意味を持ちます。例えば、/home/user/dataというパスと/home/user/databaseというパスがあった場合、strings.HasPrefix("/home/user/database", "/home/user/data")trueを返しますが、これは/home/user/database/home/user/dataディレクトリの「内部」にあることを意味しません。ファイルパスのプレフィックスを判定する際には、パスの構成要素(ディレクトリやファイル名)の境界を正確に認識し、それらが完全な要素として一致するかどうかを判断する必要があります。HasPrefixはこのセマンティクスを欠いていたため、誤ったパス操作を引き起こす可能性がありました。

  2. OSごとのファイルシステム特性の誤った仮定: Go言語はクロスプラットフォーム開発を強く意識しており、path/filepathパッケージもその哲学に基づいています。しかし、HasPrefixはWindowsでは大文字・小文字を区別しない比較を試み、Unix系では区別する比較を行うという、OSの一般的な特性に基づいた実装になっていました。このアプローチは、特定のOSのファイルシステムが常にその特性を持つという誤った仮定に依存しています。

    • Windows: 確かにWindowsのNTFSファイルシステムはデフォルトで大文字・小文字を区別しませんが、WSL (Windows Subsystem for Linux) 環境や特定の共有フォルダ設定では大文字・小文字を区別する場合があります。
    • Unix系: Linuxのext4やmacOSのAPFSはデフォルトで大文字・小文字を区別しますが、macOSのHFS+はデフォルトで大文字・小文字を区別しません(ただし、大文字・小文字を区別する設定も可能です)。 このような多様なファイルシステム特性を、単純なOS判定で一律に扱うことは、クロスプラットフォーム互換性を損ない、予期せぬバグを生み出す原因となります。
  3. 不正確な大文字・小文字を区別しない比較: HasPrefixのWindows実装では、大文字・小文字を区別しない比較のためにstrings.ToLowerを使用していました。しかし、Unicodeには大文字・小文字変換のルールが複雑な文字が存在します。例えば、トルコ語のi(点付きの小文字i)とI(点なしの大文字I)、そしてİ(点付きの大文字I)とı(点なしの小文字i)の関係は、英語のiIとは異なります。単純なToLowerでは、これらの言語固有のルールを正しく処理できず、国際化された環境でのパス比較において誤った結果を招く可能性があります。正確な大文字・小文字を区別しない比較には、Unicodeの正規化(Normalization)や言語固有のロケール設定を考慮した比較アルゴリズムが必要です。

  4. Windowsにおけるマッチングプレフィックスの長さの不明瞭さ: Windowsのファイルシステムでは、パスの正規化や大文字・小文字の扱いが複雑になることがあります。HasPrefixtrueを返した場合でも、実際にマッチしたプレフィックスが元の文字列のどの部分に対応するのか、そのバイト長が明確でないという問題がありました。これは、例えばTrimPrefixのような関数でプレフィックスを取り除いて残りのサフィックスを得る場合に、正確な操作ができないことを意味します。パス操作においては、文字列の長さだけでなく、そのセマンティクスに基づいた正確な位置情報が不可欠です。

これらの技術的な問題点から、HasPrefixpath/filepathパッケージの意図する「OSに依存しない安全なパス操作」という目標に合致しないと判断され、非推奨とされました。開発者には、よりセマンティクスを考慮したパス操作関数(例: filepath.Relfilepath.Walkなど、またはより低レベルなstringsパッケージの関数を適切に組み合わせる)を使用することが推奨されます。

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

このコミットでは、src/pkg/path/filepathディレクトリ内の以下の3つのファイルが変更されています。

  • src/pkg/path/filepath/path_plan9.go
  • src/pkg/path/filepath/path_unix.go
  • src/pkg/path/filepath/path_windows.go

それぞれのファイルで、HasPrefix関数のコメントが変更されています。

--- a/src/pkg/path/filepath/path_plan9.go
+++ b/src/pkg/path/filepath/path_plan9.go
@@ -17,7 +17,7 @@ func VolumeName(path string) string {
 	return ""
 }
 
-// HasPrefix tests whether the path p begins with prefix.
+// HasPrefix exists for historical compatibility and should not be used.
 func HasPrefix(p, prefix string) bool {
 	return strings.HasPrefix(p, prefix)
 }
--- a/src/pkg/path/filepath/path_unix.go
+++ b/src/pkg/path/filepath/path_unix.go
@@ -19,7 +19,7 @@ func VolumeName(path string) string {
 	return ""
 }
 
-// HasPrefix tests whether the path p begins with prefix.
+// HasPrefix exists for historical compatibility and should not be used.
 func HasPrefix(p, prefix string) bool {
 	return strings.HasPrefix(p, prefix)
 }
--- a/src/pkg/path/filepath/path_windows.go
+++ b/src/pkg/path/filepath/path_windows.go
@@ -67,8 +67,7 @@ func VolumeName(path string) (v string) {
 	return ""
 }
 
-// HasPrefix tests whether the path p begins with prefix.
-// It ignores case while comparing.
+// HasPrefix exists for historical compatibility and should not be used.
 func HasPrefix(p, prefix string) bool {
 	if strings.HasPrefix(p, prefix) {
 		return true

コアとなるコードの解説

変更は非常にシンプルで、HasPrefix関数のドキュメンテーションコメントを修正し、この関数が「歴史的な互換性のために存在し、使用すべきではない」ことを明確に示しています。

  • path_plan9.gopath_unix.go: これらのファイルでは、HasPrefix関数の元のコメント// HasPrefix tests whether the path p begins with prefix.が、// HasPrefix exists for historical compatibility and should not be used.に変更されています。これは、Unix系およびPlan 9環境におけるパス操作において、この関数が推奨されないことを示しています。これらの環境では、HasPrefixは単にstrings.HasPrefixを呼び出すだけであり、パスのセマンティクスを考慮していません。

  • path_windows.go: Windows環境では、ファイルシステムが大文字・小文字を区別しないことが多いため、HasPrefixの元のコメントには// It ignores case while comparing.という記述がありました。しかし、このコミットでは、この行も削除され、他のOSと同様に// HasPrefix exists for historical compatibility and should not be used.というコメントに統一されています。これは、Windowsにおける大文字・小文字を区別しない比較の実装が不正確であること、およびパスの境界を考慮しないという根本的な問題があるため、この関数自体が非推奨であることを強調しています。

この変更は、関数の実装自体を変更するものではなく、その使用方法に関するガイダンスを明確にすることで、開発者がこの関数を誤って使用することを防ぐことを目的としています。これにより、path/filepathパッケージの利用者が、より堅牢でクロスプラットフォーム互換性のあるパス操作を行うよう促されます。

関連リンク

  • Go CL 5712045: https://golang.org/cl/5712045 このコミットの元となったGoのコードレビューシステム(Gerrit)上のチェンジリストです。コミットメッセージに記載されているリンクであり、この変更に関する議論や詳細な経緯を確認できます。

参考にした情報源リンク

  • Go言語のpath/filepathパッケージのドキュメント: https://pkg.go.dev/path/filepath HasPrefix関数を含むpath/filepathパッケージの公式ドキュメントです。現在のバージョンでは、HasPrefixは非推奨であることが明記されています。

  • Go言語のstringsパッケージのドキュメント: https://pkg.go.dev/strings strings.HasPrefixstrings.ToLowerなど、文字列操作に関する基本的な関数が定義されているパッケージの公式ドキュメントです。

  • Unicodeの正規化と大文字・小文字変換に関する情報: (一般的な情報源として)

  • ファイルシステムの大文字・小文字の区別に関する情報: (一般的な情報源として)

    • Wikipedia: Filename case-sensitivity 異なるオペレーティングシステムやファイルシステムにおける大文字・小文字の区別の特性について概説されています。