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

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

このコミットは、godoc ツールにおける絶対パスから相対パスへのマッピング処理のバグを修正するものです。具体的には、strings.HasPrefix の誤用によって発生していた、意図しないパスのマッチングを防ぐための改善が行われています。

コミット

commit 7b22e4628296518691a8ce6e4e4065ff4aeb69d8
Author: Robert Griesemer <gri@golang.org>
Date:   Tue Feb 21 18:12:37 2012 -0800

    godoc: fix absolute->relative mapping
    
    Fixes #3096.
    
    R=golang-dev, dsymonds
    CC=golang-dev
    https://golang.org/cl/5690063

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

https://github.com/golang/go/commit/7b22e4628296518691a8ce6e4e4065ff4aeb69d8

元コミット内容

 src/cmd/godoc/mapping.go | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/cmd/godoc/mapping.go b/src/cmd/godoc/mapping.go
index 89e531e2f3..1a0da15bfc 100644
--- a/src/cmd/godoc/mapping.go
+++ b/cmd/godoc/mapping.go
@@ -178,7 +178,8 @@ func (m *Mapping) ToAbsolute(spath string) string {
 //
 func (m *Mapping) ToRelative(fpath string) string {
 	for _, e := range m.list {
-		if strings.HasPrefix(fpath, e.path) {
+		// if fpath has prefix e.path, the next character must be a separator (was issue 3096)
+		if strings.HasPrefix(fpath, e.path) && fpath[len(e.path)] == filepath.Separator {
 			spath := filepath.ToSlash(fpath)
 			// /absolute/prefix/foo -> prefix/foo
 			return path.Join(e.prefix, spath[len(e.path):]) // Join will remove a trailing '/'

変更の背景

このコミットは、Go言語のドキュメンテーションツールである godoc における、パスのマッピングに関するバグ(Issue 3096)を修正するために行われました。

元のコードでは、絶対ファイルパスを相対パスに変換する ToRelative 関数内で、strings.HasPrefix(fpath, e.path) という条件を使用していました。この条件は、fpathe.path で始まるかどうかを単純にチェックします。しかし、このチェックだけでは不十分なケースがありました。

例えば、e.path/usr/local/go/src/pkg/net で、fpath/usr/local/go/src/pkg/net/http のような場合、strings.HasPrefix は正しく true を返します。しかし、問題となるのは、e.path/usr/local/go/src/pkg/net で、fpath/usr/local/go/src/pkg/network のようなケースです。この場合も strings.HasPrefixtrue を返してしまいますが、これらは異なるディレクトリであり、networknet のサブディレクトリであるかのように誤って扱われてしまう可能性がありました。

この誤ったマッピングは、godoc がドキュメントを生成する際に、間違ったパス解決を行い、結果としてリンク切れや不正確なドキュメント表示を引き起こす原因となっていました。Issue 3096 はこの問題点を指摘しており、このコミットはその根本原因を解決することを目的としています。

前提知識の解説

godoc

godoc は、Go言語のソースコードからドキュメンテーションを生成し、表示するためのツールです。Goのコードには、関数、型、変数、パッケージなどに対してコメント形式でドキュメントを記述する慣習があり、godoc はこれらのコメントを解析して、Webブラウザで閲覧可能な形式で表示したり、プレーンテキストで出力したりします。開発者がGoの標準ライブラリやサードパーティのパッケージのドキュメントを参照する際に広く利用されます。

パスとファイルシステム

  • 絶対パス (Absolute Path): ファイルシステム上のルートディレクトリから始まる完全なパスです。例えば、Linux/macOSでは /home/user/documents/file.txt、Windowsでは C:\Users\user\Documents\file.txt のようになります。
  • 相対パス (Relative Path): 現在の作業ディレクトリを基準としたパスです。例えば、現在のディレクトリが /home/user であれば、documents/file.txt/home/user/documents/file.txt を指します。
  • パスセパレータ (Path Separator): ディレクトリ名を区切る文字です。Linux/macOSでは / (スラッシュ)、Windowsでは \ (バックスラッシュ) が一般的です。Go言語の filepath パッケージは、OSに依存しないパス操作を提供し、filepath.Separator は現在のOSのパスセパレータを表す定数です。

strings.HasPrefix

Go言語の標準ライブラリ strings パッケージに含まれる関数で、strings.HasPrefix(s, prefix string) bool の形式で使用します。これは、文字列 s が指定された prefix で始まる場合に true を返し、そうでない場合に false を返します。

filepath.Separator

Go言語の標準ライブラリ path/filepath パッケージに含まれる定数で、現在のオペレーティングシステムで使用されるパスセパレータ文字を表します。例えば、Unix系システムでは /、Windowsでは \ となります。この定数を使用することで、OSに依存しないパス操作が可能になります。

技術的詳細

このコミットの技術的な核心は、godocMapping 構造体の ToRelative メソッドにおけるパスの比較ロジックの改善です。

元のコードでは、for _, e := range m.list ループ内で、各マッピングエントリ ee.path (絶対パスのプレフィックス) と入力ファイルパス fpath を比較する際に、strings.HasPrefix(fpath, e.path) のみを使用していました。

この単純なプレフィックスチェックの問題点は、例えば以下のようなケースで顕在化します。

  • e.path = /a/b
  • fpath = /a/bc

この場合、strings.HasPrefix("/a/bc", "/a/b")true を返します。しかし、/a/bc/a/b ディレクトリのサブパスではなく、/a/b とは異なる /a/bc というファイルまたはディレクトリを指しています。godoc のパスマッピングにおいては、/a/b/a/bc の親ディレクトリであるかのように誤って解釈されてしまう可能性がありました。

このコミットでは、この問題を解決するために、strings.HasPrefix の結果に加えて、さらに厳密な条件を追加しています。

if strings.HasPrefix(fpath, e.path) && fpath[len(e.path)] == filepath.Separator {

この変更により、以下の2つの条件が同時に満たされる場合にのみ、パスがマッチしたと判断されます。

  1. fpathe.path で始まる (strings.HasPrefix(fpath, e.path))。
  2. e.path の直後の fpath の文字が、パスセパレータ (filepath.Separator) である。

この追加された fpath[len(e.path)] == filepath.Separator という条件が非常に重要です。

  • len(e.path)e.path の文字列長です。
  • fpath[len(e.path)] は、fpath の中で e.path の直後に続く文字を指します。

例えば、

  • e.path = /usr/local/go/src/pkg/net (長さ: 26)
  • fpath = /usr/local/go/src/pkg/net/http

この場合、fpath[26]/ となり、filepath.Separator と一致するため、条件は true となります。これは正しいマッピングです。

一方、

  • e.path = /usr/local/go/src/pkg/net (長さ: 26)
  • fpath = /usr/local/go/src/pkg/network

この場合、fpath[26]w となり、filepath.Separator (/) と一致しません。したがって、この条件は false となり、誤ったマッピングが回避されます。

この修正により、godoc はパスをより正確に識別し、絶対パスから相対パスへの変換が意図した通りに行われるようになります。これは、特にGoの標準ライブラリのように、多くのパッケージが階層的に配置されている場合に、ドキュメントの正確性を保つ上で不可欠な改善です。

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

--- a/src/cmd/godoc/mapping.go
+++ b/src/cmd/godoc/mapping.go
@@ -178,7 +178,8 @@ func (m *Mapping) ToAbsolute(spath string) string {
 //
 func (m *Mapping) ToRelative(fpath string) string {
 	for _, e := range m.list {
-		if strings.HasPrefix(fpath, e.path) {
+		// if fpath has prefix e.path, the next character must be a separator (was issue 3096)
+		if strings.HasPrefix(fpath, e.path) && fpath[len(e.path)] == filepath.Separator {
 			spath := filepath.ToSlash(fpath)
 			// /absolute/prefix/foo -> prefix/foo
 			return path.Join(e.prefix, spath[len(e.path):]) // Join will remove a trailing '/'

コアとなるコードの解説

変更は src/cmd/godoc/mapping.go ファイルの ToRelative 関数内で行われています。

func (m *Mapping) ToRelative(fpath string) string {
	for _, e := range m.list {
		// if fpath has prefix e.path, the next character must be a separator (was issue 3096)
		if strings.HasPrefix(fpath, e.path) && fpath[len(e.path)] == filepath.Separator {
			spath := filepath.ToSlash(fpath)
			// /absolute/prefix/foo -> prefix/foo
			return path.Join(e.prefix, spath[len(e.path):]) // Join will remove a trailing '/'
		}
	}
	// ... (以降のコードは変更なし)
}
  • for _, e := range m.list: Mapping 構造体が持つマッピングエントリのリストをイテレートしています。各 e は、絶対パスのプレフィックス (e.path) とそれに対応する相対パスのプレフィックス (e.prefix) を持っています。
  • - if strings.HasPrefix(fpath, e.path) {: 変更前のコードです。fpathe.path で始まるかどうかだけをチェックしていました。
  • + // if fpath has prefix e.path, the next character must be a separator (was issue 3096): 追加されたコメントです。Issue 3096 で指摘された問題の解決策として、次の文字がセパレータである必要があることを示しています。
  • + if strings.HasPrefix(fpath, e.path) && fpath[len(e.path)] == filepath.Separator {: 変更後のコードです。
    • strings.HasPrefix(fpath, e.path): これは以前と同じく、fpathe.path で始まることを確認します。
    • &&: 論理AND演算子です。両方の条件が true の場合にのみ、全体の条件が true となります。
    • fpath[len(e.path)] == filepath.Separator: この部分が追加された重要な条件です。
      • len(e.path): e.path の文字列長を取得します。
      • fpath[len(e.path)]: fpath 文字列の len(e.path) インデックスにある文字(つまり、e.path の直後の文字)を取得します。
      • == filepath.Separator: 取得した文字が、現在のOSのパスセパレータ(例: / または \)と等しいかどうかをチェックします。

この追加された条件により、fpathe.path の「真の」サブパスである場合にのみマッチングが成功するようになり、"/a/b""/a/bc" のような誤ったマッチングが排除されます。

  • spath := filepath.ToSlash(fpath): fpath をスラッシュ区切りのパスに変換します。これは、Windows環境など、バックスラッシュがパスセパレータとして使われる場合でも、内部的に統一された形式でパスを扱うためです。
  • return path.Join(e.prefix, spath[len(e.path):]):
    • spath[len(e.path):]: spath の中で e.path の部分を除いた残りの部分(相対パスの残りの部分)を抽出します。
    • path.Join(e.prefix, ...): e.prefix (マッピングされた相対パスのプレフィックス) と抽出した相対パスの残りの部分を結合して、最終的な相対パスを生成します。path.Join は、必要に応じて余分なスラッシュを削除するなどの正規化を行います。

この修正は、godoc のパス解決の堅牢性を高め、より正確なドキュメント生成に貢献しています。

関連リンク

参考にした情報源リンク