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

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

このコミットは、Go言語のコマンドラインツール go におけるリモートパッケージ発見機能のバグ修正に関するものです。具体的には、go get コマンドがリモートリポジトリからパッケージ情報を取得する際に、HTMLレスポンスのパース処理が特定の条件下で誤ってエラーを返す問題を解決しています。

コミット

commit 441c4bb939666555f697c1d5abf30b2f78528962
Author: Russ Cox <rsc@golang.org>
Date:   Tue Feb 25 11:22:22 2014 -0500

    cmd/go: fix bug in remote package discovery
    
    The parser was assuming it would find <body> or </head>.
    If the entire response is just <meta> tags, it finds EOF and
    treats that as an error. It's not.
    
    LGTM=bradfitz
    R=bradfitz
    CC=golang-codereviews
    https://golang.org/cl/68520044

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

https://github.com/golang/go/commit/441c4bb939666555f697c1d5abf30b2f78528962

元コミット内容

cmd/go: fix bug in remote package discovery

このコミットは、go コマンドのリモートパッケージ発見機能におけるバグを修正します。 パース処理は <body> または </head> タグを見つけることを前提としていました。 もしレスポンス全体が <meta> タグのみで構成されている場合、パーサーはEOF(ファイルの終端)に到達し、それをエラーとして扱っていました。しかし、これはエラーではありません。

変更の背景

Go言語の go get コマンドは、指定されたインポートパスに対応するソースコードをリモートリポジトリから取得する機能を提供します。この際、go get はまずHTTPリクエストを送信し、そのレスポンスのHTMLヘッダ内に埋め込まれた <meta name="go-import" ...> タグを解析することで、実際のバージョン管理システム(VCS)のリポジトリURLを特定します。

このコミット以前の go get の実装では、HTMLレスポンスを解析する際に、パーサーが <body> タグまたは </head> タグのいずれかを見つけることを期待していました。しかし、一部のウェブサーバーやコンテンツでは、HTMLドキュメントが非常にシンプルで、<meta> タグのみを含み、<body></head> タグが存在しない場合があります。このような場合、パーサーは期待するタグを見つけられずにファイルの終端(EOF)に到達し、これをパースエラーとして扱っていました。

結果として、go get は有効な go-import メタタグが存在するにもかかわらず、パッケージの取得に失敗するというバグが発生していました。このコミットは、この誤ったエラー処理を修正し、go get がより堅牢にリモートパッケージを発見できるようにすることを目的としています。

前提知識の解説

go get コマンドとリモートパッケージ発見

go get はGoのモジュール管理において重要なコマンドです。go get example.com/repo/pkg のように実行すると、以下の手順でパッケージを取得します。

  1. HTTPリクエスト: example.com に対してHTTPリクエストを送信します。
  2. HTMLレスポンス解析: レスポンスのHTMLボディ(特に <head> セクション)を解析し、<meta name="go-import" content="importpath vcs repo-url"> 形式のタグを探します。
    • importpath: Goのインポートパス(例: example.com/repo
    • vcs: バージョン管理システム(例: git, hg, svn, bzr
    • repo-url: 実際のVCSリポジトリのURL(例: https://github.com/example/repo.git
  3. VCSクローン: 取得した vcsrepo-url を使用して、指定されたリポジトリをローカルにクローンまたはフェッチします。

このメカニズムにより、Goのインポートパスは実際のVCSリポジトリの場所から抽象化され、カスタムドメインを使用できるようになります。

XML/HTMLパーサーとEOF

XMLやHTMLを解析する際には、パーサーはドキュメントの構造をトークン(タグ、属性、テキストなど)に分解していきます。通常、ドキュメントの終わりに達するとEOF(End Of File)が通知されます。 一般的なパーサーでは、予期しないEOFは構文エラーとして扱われることが多いです。しかし、このケースでは、go-import メタタグはHTMLドキュメントの冒頭、特に <head> セクションに配置されることが期待されます。パーサーがこれらのメタタグを読み取った後、<body></head> タグが見つからずにEOFに達した場合でも、必要な情報は既に取得されているため、それをエラーとして扱うのは不適切です。

io.Readerio.EOF

Go言語の io パッケージは、I/O操作のための基本的なインターフェースを提供します。 io.Reader はデータを読み取るためのインターフェースで、Read メソッドを持ちます。 Read メソッドは、読み取ったバイト数とエラーを返します。データがこれ以上ない場合、io.EOF という特別なエラーを返します。これは、エラー状態を示すものではなく、単にストリームの終端に達したことを示すシグナルとして扱われます。

技術的詳細

このバグは、src/cmd/go/discovery.go 内の parseMetaGoImports 関数に存在していました。この関数は、go get がリモートパッケージのメタ情報を取得するためにHTMLレスポンスを解析する主要なロジックを含んでいます。

元の実装では、xml.Decoder を使用してHTMLストリームをトークン化していました。このデコーダは、ストリームの終端に達すると io.EOF エラーを返します。parseMetaGoImports 関数は、<body> または </head> タグが見つかるまでトークンを読み進めるループを持っていました。

問題は、HTMLレスポンスが <meta> タグのみで構成され、<body></head> タグが全く存在しない場合に発生しました。このシナリオでは、パーサーは必要なメタタグを正常に読み取った後、ループ内で次のトークンを読み込もうとした際に io.EOF に到達します。元のコードでは、この io.EOF を一般的なエラーとして扱っていたため、go get はパッケージの発見に失敗していました。

修正は非常にシンプルかつ効果的です。parseMetaGoImports 関数内でトークンを読み取るループにおいて、io.EOF が返された場合に、それをエラーとして処理するのではなく、nil エラーとして扱うように変更されました。これにより、パーサーは必要なメタタグを読み取った後、ドキュメントの終端に達しても正常に処理を完了し、取得したメタ情報を返すことができるようになりました。

また、この修正を検証するために、src/cmd/go/pkg_test.go に新しいテストケースが追加されました。これらのテストケースは、go-import メタタグのみを含むHTMLスニペットや、<head> タグ内にメタタグを含むスニペットなど、様々なシナリオを網羅しており、修正が正しく機能することを確認しています。

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

src/cmd/go/discovery.go

--- a/src/cmd/go/discovery.go
+++ b/src/cmd/go/discovery.go
@@ -43,6 +43,9 @@ func parseMetaGoImports(r io.Reader) (imports []metaImport, err error) {
 	for {
 		t, err = d.Token()
 		if err != nil {
+			if err == io.EOF {
+				err = nil
+			}
 			return
 		}
 		if e, ok := t.(xml.StartElement); ok && strings.EqualFold(e.Name.Local, "body") {

src/cmd/go/pkg_test.go

--- a/src/cmd/go/pkg_test.go
+++ b/src/cmd/go/pkg_test.go
@@ -4,7 +4,11 @@
 
 package main
 
-import "testing"
+import (
+	"reflect"
+	"strings"
+	"testing"
+)
 
 var foldDupTests = []struct {
 	list   []string
@@ -25,3 +29,45 @@ func TestFoldDup(t *testing.T) {
 		}
 	}\n}\n+\n+var parseMetaGoImportsTests = []struct {\n+\tin  string\n+\tout []metaImport\n+}{\n+\t{\n+\t\t`<meta name=\"go-import\" content=\"foo/bar git https://github.com/rsc/foo/bar\">`,\n+\t\t[]metaImport{{\"foo/bar\", "git", "https://github.com/rsc/foo/bar"}},\n+\t},\n+\t{\n+\t\t`<meta name=\"go-import\" content=\"foo/bar git https://github.com/rsc/foo/bar\">\n+\t\t<meta name=\"go-import\" content=\"baz/quux git http://github.com/rsc/baz/quux\">`,\n+\t\t[]metaImport{\n+\t\t\t{"foo/bar", "git", "https://github.com/rsc/foo/bar"},\n+\t\t\t{"baz/quux", "git", "http://github.com/rsc/baz/quux"},\n+\t\t},\n+\t},\n+\t{\n+\t\t`<head>\n+\t\t<meta name=\"go-import\" content=\"foo/bar git https://github.com/rsc/foo/bar\">\n+\t\t</head>`,\n+\t\t[]metaImport{{\"foo/bar", "git", "https://github.com/rsc/foo/bar"}},\n+\t},\n+\t{\n+\t\t`<meta name=\"go-import\" content=\"foo/bar git https://github.com/rsc/foo/bar\">\n+\t\t<body>`,\n+\t\t[]metaImport{{\"foo/bar", "git", "https://github.com/rsc/foo/bar"}},\n+\t},\n+}\n+\n+func TestParseMetaGoImports(t *testing.T) {\n+\tfor i, tt := range parseMetaGoImportsTests {\n+\t\tout, err := parseMetaGoImports(strings.NewReader(tt.in))\n+\t\tif err != nil {\n+\t\t\tt.Errorf("test#%d: %v", i, err)\n+\t\t\tcontinue\n+\t\t}\n+\t\tif !reflect.DeepEqual(out, tt.out) {\n+\t\t\tt.Errorf("test#%d:\\n\\thave %q\\n\\twant %q", i, out, tt.out)\n+\t\t}\n+\t}\n+}\n```

## コアとなるコードの解説

### `src/cmd/go/discovery.go` の変更

`parseMetaGoImports` 関数は、`io.Reader` からHTMLコンテンツを読み込み、`go-import` メタタグを解析して `metaImport` スライスの形式で返します。

変更前は、`d.Token()` から返される `err` が `nil` でない場合、すぐに `return` していました。これにより、`io.EOF` もエラーとして扱われ、関数が早期に終了していました。

変更後は、`if err != nil` のブロック内に `if err == io.EOF { err = nil }` という条件が追加されました。
これは、もしエラーが `io.EOF` であった場合、`err` 変数を `nil` に上書きするという意味です。
これにより、`io.EOF` が発生しても、それは正常なストリームの終端として扱われ、関数はエラーを返さずにこれまでにパースした `metaImport` 情報を返すことができるようになります。

この修正は、パーサーが `<body>` や `</head>` タグを見つける前にドキュメントの終端に達した場合でも、既に読み取った `go-import` メタタグの情報を失うことなく処理を完了させることを保証します。

### `src/cmd/go/pkg_test.go` の変更

このファイルには、`parseMetaGoImports` 関数の動作を検証するための新しいテストが追加されました。

1.  **新しいインポート**: `reflect` と `strings` パッケージがインポートされました。
    *   `reflect.DeepEqual` は、パース結果の `metaImport` スライスが期待される出力と完全に一致するかを比較するために使用されます。
    *   `strings.NewReader` は、テスト用のHTML文字列を `io.Reader` インターフェースに変換するために使用されます。
2.  **`parseMetaGoImportsTests` 変数**:
    この変数は、`parseMetaGoImports` 関数に与える入力HTML文字列 (`in`) と、それに対応する期待される `metaImport` スライス (`out`) のペアを定義する構造体のスライスです。
    注目すべきテストケースは以下の通りです。
    *   `<meta name="go-import" ...>` のみが含まれるケース: これは、`<body>` や `</head>` が存在しない場合のシナリオを直接テストします。
    *   複数の `<meta>` タグが含まれるケース。
    *   `<head>` タグ内に `<meta>` タグが含まれるケース。
    *   `<meta>` タグの後に `<body>` タグが続くケース。
3.  **`TestParseMetaGoImports` 関数**:
    この関数は、`parseMetaGoImportsTests` の各テストケースをループで実行します。
    *   `parseMetaGoImports` を呼び出し、返されたエラーと結果をチェックします。
    *   エラーが発生した場合(修正前は `io.EOF` でエラーになっていたケース)、`t.Errorf` でテスト失敗を報告します。
    *   `reflect.DeepEqual` を使用して、実際の出力と期待される出力が一致するかを厳密に比較します。

これらのテストケースの追加により、`parseMetaGoImports` 関数が様々な有効なHTML構造(特に `<body>` や `</head>` が存在しない、または遅れて出現するケース)を正しく処理できることが保証されます。

## 関連リンク

*   Go Modules Reference: [https://go.dev/ref/mod](https://go.dev/ref/mod)
*   `go get` command documentation: [https://go.dev/cmd/go/#hdr-Download_and_install_packages_and_dependencies](https://go.dev/cmd/go/#hdr-Download_and_install_packages_and_dependencies)
*   `go-import` meta tag specification: [https://go.dev/cmd/go/#hdr-Remote_import_paths](https://go.dev/cmd/go/#hdr-Remote_import_paths)

## 参考にした情報源リンク

*   Go言語の公式ドキュメント
*   Go言語のソースコード(特に `cmd/go` ディレクトリ)
*   `io` パッケージのドキュメント
*   `encoding/xml` パッケージのドキュメント
*   Goのテストに関するドキュメント