[インデックス 17343] ファイルの概要
このコミットは、Go言語の標準ライブラリであるarchive/tar
およびarchive/zip
パッケージにおいて、os.FileInfo
インターフェースの実装がGoの仕様に準拠していなかったバグを修正するものです。具体的には、os.FileInfo.Name()
メソッドがファイル名(ベース名)のみを返すという仕様に対し、これらのパッケージの実装ではフルパスを返してしまっていた問題を解決します。この変更はAPI互換性を損なう可能性がありましたが、重要なバグ修正として導入されました。
コミット
- コミットハッシュ:
9364868a07e9fe1cc58d963e4523bc5883201206
- Author: Rob Pike r@golang.org
- Date: Wed Aug 21 08:29:41 2013 +1000
- コミットメッセージの最初の行: archive/tar,zip: implement the os.FileInfo interface correctly.
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9364868a07e9fe1cc58d963e4523bc5883201206
元コミット内容
commit 9364868a07e9fe1cc58d963e4523bc5883201206
Author: Rob Pike <r@golang.org>
Date: Wed Aug 21 08:29:41 2013 +1000
archive/tar,zip: implement the os.FileInfo interface correctly.
This is potentially an API-breaking change, but it is an important bug fix.
The CL https://golang.org/cl/7305072/ added stuff to make
the tar file look more like a file system internally, including providing an
implementation of os.FileInfo for the file headers within the archive.
But the code is incorrect because FileInfo.Name is supposed to return
the base name only; this implementation returns the full path. A round
trip test added in the same shows this in action, as the slashes are
preserved as we create a header using the local implementation of
FileInfo.
The CL here changes the behavior of the tar (and zip) FileInfo to honor
the Go spec for that interface. It also clarifies that the FileInfoHeader
function, which takes a FileInfo as an argument, will therefore create
a header with only the base name of the file recorded, and that
subsequent adjustment may be necessary.
There may be code out there that depends on the broken behavior.
We can call out the risk in the release notes.
Fixes #6180.
R=golang-dev, dsymonds, adg, bradfitz
CC=golang-dev
https://golang.org/cl/13118043
変更の背景
この変更の背景には、archive/tar
およびarchive/zip
パッケージが提供するos.FileInfo
インターフェースの実装が、Go言語の標準ライブラリの仕様と異なる動作をしていたという問題があります。
元々、https://golang.org/cl/7305072/
という変更(CL: Change List)によって、archive/tar
パッケージの内部でtarファイルをファイルシステムのように扱えるようにするため、アーカイブ内のファイルヘッダーに対してos.FileInfo
の実装が追加されました。しかし、この実装にはバグがあり、os.FileInfo.Name()
メソッドがファイルの「ベース名(ファイル名のみ)」ではなく、「フルパス」を返していました。これはos.FileInfo.Name()
のGoの仕様に反する動作でした。
この誤った動作は、同じCLで追加されたラウンドトリップテストで顕在化しました。テストでは、ローカルのFileInfo
実装を使用してヘッダーを作成する際に、スラッシュ(パス区切り文字)がそのまま保持されてしまうことが示されました。
このコミットは、このバグを修正し、tar
およびzip
パッケージのFileInfo
がGoのインターフェース仕様に準拠するように動作を変更することを目的としています。また、FileInfoHeader
関数(FileInfo
を引数に取り、ヘッダーを作成する関数)が、ベース名のみを記録したヘッダーを作成するようになるため、必要に応じて後続の調整が必要になる可能性があることも明確にしています。
この修正は、既存の壊れた動作に依存している可能性のあるコードに影響を与える可能性があるため、API互換性を損なう変更としてリリースノートで注意喚起されるべきであるとされています。
前提知識の解説
os.FileInfo
インターフェース
Go言語のos
パッケージには、ファイルやディレクトリのメタデータ(名前、サイズ、パーミッション、更新時刻など)を抽象化するためのFileInfo
インターフェースが定義されています。このインターフェースは、ファイルシステム上の様々な種類のファイル(通常ファイル、ディレクトリ、シンボリックリンクなど)に対して統一的な情報アクセスを提供します。
os.FileInfo
インターフェースの主要なメソッドは以下の通りです。
Name() string
: ファイルのベース名(パスの最後の要素)を返します。例えば、/path/to/file.txt
の場合、file.txt
を返します。ディレクトリの場合も同様に、ディレクトリ名のみを返します。Size() int64
: ファイルのサイズをバイト単位で返します。Mode() FileMode
: ファイルのパーミッションとモードビットを返します。ModTime() time.Time
: ファイルの最終更新時刻を返します。IsDir() bool
: ディレクトリであればtrue
を返します。Sys() interface{}
: 基となるデータソースに依存するシステム固有の情報を返します。
このコミットで問題となったのは、Name()
メソッドの動作です。Goの仕様では、Name()
は常にベース名を返すことになっています。
archive/tar
パッケージ
archive/tar
パッケージは、TAR(Tape Archive)形式のアーカイブファイルを読み書きするためのGo言語の標準ライブラリです。TARファイルは、複数のファイルやディレクトリを一つのファイルにまとめるために使用されます。
このパッケージの主要な構造体と関数には以下のようなものがあります。
tar.Header
: TARアーカイブ内の各エントリ(ファイルやディレクトリ)のメタデータ(名前、サイズ、モードなど)を表現する構造体です。tar.Reader
: TARアーカイブからエントリを読み取るためのリーダーです。tar.Writer
: TARアーカイブにエントリを書き込むためのライターです。tar.FileInfoHeader(fi os.FileInfo, link string) (*Header, error)
:os.FileInfo
オブジェクトからtar.Header
を作成する関数です。
archive/zip
パッケージ
archive/zip
パッケージは、ZIP形式のアーカイブファイルを読み書きするためのGo言語の標準ライブラリです。ZIPファイルもTARファイルと同様に、複数のファイルやディレクトリを圧縮して一つのファイルにまとめるために使用されます。
このパッケージの主要な構造体と関数には以下のようなものがあります。
zip.FileHeader
: ZIPアーカイブ内の各エントリのメタデータを表現する構造体です。zip.Reader
: ZIPアーカイブからエントリを読み取るためのリーダーです。zip.Writer
: ZIPアーカイブにエントリを書き込むためのライターです。zip.FileInfoHeader(fi os.FileInfo) (*FileHeader, error)
:os.FileInfo
オブジェクトからzip.FileHeader
を作成する関数です。
ベース名とフルパス
- フルパス: ファイルシステム上のファイルやディレクトリの完全な位置を示すパスです。例えば、
/home/user/documents/report.txt
。 - ベース名: フルパスの最後の要素、つまりファイル名またはディレクトリ名そのものです。例えば、
/home/user/documents/report.txt
のベース名はreport.txt
です。ディレクトリ/home/user/documents/
のベース名はdocuments
です。
os.FileInfo.Name()
メソッドは、このベース名を返すことがGoの仕様で定められています。
技術的詳細
このコミットの技術的な核心は、archive/tar
とarchive/zip
パッケージが、os.FileInfo
インターフェースのName()
メソッドのGo仕様(ベース名を返す)に準拠していなかった点を修正することにあります。
archive/tar
パッケージの修正
archive/tar
パッケージでは、headerFileInfo
という内部構造体がos.FileInfo
インターフェースを実装していました。このheaderFileInfo
のName()
メソッドが、tar.Header
のName
フィールドをそのまま返していました。しかし、tar.Header.Name
はアーカイブ内のファイルのフルパス(または相対パス)を保持することが一般的です。
例えば、アーカイブ内にdir/file.txt
というエントリがある場合、tar.Header.Name
はdir/file.txt
となります。しかし、os.FileInfo.Name()
はfile.txt
を返す必要があります。
このコミットでは、headerFileInfo.Name()
の実装を修正し、path.Base()
関数を使用してtar.Header.Name
からベース名のみを抽出するように変更しました。
また、tar.FileInfoHeader
関数に関するドキュメントコメントも更新されました。この関数はos.FileInfo
を受け取り、tar.Header
を作成しますが、os.FileInfo.Name()
がベース名しか返さないため、作成されるtar.Header
のName
フィールドもベース名のみが設定されることになります。したがって、もしアーカイブ内でフルパスを保持したい場合は、FileInfoHeader
の呼び出し後にHeader.Name
フィールドを別途調整する必要があることを明記しました。
archive/zip
パッケージの修正
archive/zip
パッケージでも同様に、headerFileInfo
という内部構造体がos.FileInfo
インターフェースを実装しており、そのName()
メソッドがzip.FileHeader
のName
フィールドをそのまま返していました。zip.FileHeader.Name
もまた、ZIPアーカイブ内のファイルのフルパス(または相対パス)を保持します。
このコミットでは、headerFileInfo.Name()
の実装を修正し、path.Base()
関数を使用してzip.FileHeader.Name
からベース名のみを抽出するように変更しました。
zip.FileInfoHeader
関数に関するドキュメントコメントも更新され、tar
パッケージと同様に、os.FileInfo.Name()
がベース名しか返さないため、作成されるzip.FileHeader
のName
フィールドもベース名のみが設定されること、そして必要に応じてName
フィールドを調整する必要があることを明確にしました。
テストの追加と修正
archive/tar/tar_test.go
には、TestHeaderRoundTrip
というテストが修正されました。このテストは、os.FileInfo
からtar.Header
を作成し、そのHeader
から再度os.FileInfo
を生成する「ラウンドトリップ」の動作を検証するものです。
修正前は、h2.Name
(tar.Header
から生成されたHeader
のName)とg.h.Name
(元のtar.Header
のName)を直接比較していました。しかし、os.FileInfo.Name()
がベース名を返すように変更されたため、この比較は適切ではありません。
修正後では、fi.Name()
(os.FileInfo
のName)にスラッシュが含まれていないことを確認するアサーションが追加されました。また、h2.Name
と比較するwant
の値として、元のg.h.Name
からpath.Base()
を使ってベース名を抽出したものが使われるようになりました。これにより、os.FileInfo.Name()
の新しい正しい動作がテストで検証されるようになりました。
この変更は、Goの標準ライブラリのインターフェース仕様の一貫性を保つ上で非常に重要であり、ファイルアーカイブを扱うアプリケーションの堅牢性を向上させます。しかし、既存のコードがこの「壊れた」動作に依存していた場合、互換性の問題を引き起こす可能性があるため、リリースノートでの注意喚起が必要とされました。
コアとなるコードの変更箇所
src/pkg/archive/tar/common.go
--- a/src/pkg/archive/tar/common.go
+++ b/src/pkg/archive/tar/common.go
@@ -83,9 +83,9 @@ func (fi headerFileInfo) Sys() interface{} { return fi.h }\n // Name returns the base name of the file.\n func (fi headerFileInfo) Name() string {\n if fi.IsDir() {\n-\t\treturn path.Clean(fi.h.Name)\n+\t\treturn path.Base(path.Clean(fi.h.Name))\n }\n-\treturn fi.h.Name
+\treturn path.Base(fi.h.Name)\n }\n \n // Mode returns the permission and mode bits for the headerFileInfo.\n@@ -195,6 +195,9 @@ const (\n // FileInfoHeader creates a partially-populated Header from fi.\n // If fi describes a symlink, FileInfoHeader records link as the link target.\n // If fi describes a directory, a slash is appended to the name.\n+// Because os.FileInfo\'s Name method returns only the base name of\n+// the file it describes, it may be necessary to modify the Name field\n+// of the returned header to provide the full path name of the file.\n func FileInfoHeader(fi os.FileInfo, link string) (*Header, error) {\n if fi == nil {\n return nil, errors.New(\"tar: FileInfo is nil\")
src/pkg/archive/tar/tar_test.go
--- a/src/pkg/archive/tar/tar_test.go
+++ b/src/pkg/archive/tar/tar_test.go
@@ -8,7 +8,9 @@ import (\n \"bytes\"\n \"io/ioutil\"\n \"os\"\n+\t\"path\"\n \"reflect\"\n+\t\"strings\"\n \"testing\"\n \"time\"\n )\n@@ -249,7 +249,14 @@ func TestHeaderRoundTrip(t *testing.T) {\n \t\tt.Error(err)\n \t\t\tcontinue\n \t\t}\n-\t\tif got, want := h2.Name, g.h.Name; got != want {\n+\t\tif strings.Contains(fi.Name(), \"/\") {\n+\t\t\tt.Errorf(\"FileInfo of %q contains slash: %q\", g.h.Name, fi.Name())\n+\t\t}\n+\t\tname := path.Base(g.h.Name)\n+\t\tif fi.IsDir() {\n+\t\t\tname += \"/\"\n+\t\t}\n+\t\tif got, want := h2.Name, name; got != want {\n \t\t\tt.Errorf(\"i=%d: Name: got %v, want %v\", i, got, want)\n \t\t}\n \t\tif got, want := h2.Size, g.h.Size; got != want {\n```
### `src/pkg/archive/zip/struct.go`
```diff
--- a/src/pkg/archive/zip/struct.go
+++ b/src/pkg/archive/zip/struct.go
@@ -21,6 +21,7 @@ package zip\n \n import (\n \"os\"\n+\t\"path\"\n \"time\"\n )\n \n@@ -99,7 +100,7 @@ type headerFileInfo struct {\n fh *FileHeader\n }\n \n-func (fi headerFileInfo) Name() string { return fi.fh.Name }\n+func (fi headerFileInfo) Name() string { return path.Base(fi.fh.Name) }\n func (fi headerFileInfo) Size() int64 {\n if fi.fh.UncompressedSize64 > 0 {\n return int64(fi.fh.UncompressedSize64)\n@@ -113,6 +114,9 @@ func (fi headerFileInfo) Sys() interface{} { return fi.fh }\n \n // FileInfoHeader creates a partially-populated FileHeader from an\n // os.FileInfo.\n+// Because os.FileInfo\'s Name method returns only the base name of\n+// the file it describes, it may be necessary to modify the Name field\n+// of the returned header to provide the full path name of the file.\n func FileInfoHeader(fi os.FileInfo) (*FileHeader, error) {\n size := fi.Size()\n fh := &FileHeader{\n```
## コアとなるコードの解説
### `src/pkg/archive/tar/common.go` の変更
- **`func (fi headerFileInfo) Name() string` の修正**:
- 変更前: `fi.h.Name` をそのまま返していました。`fi.h.Name`はtarヘッダー内のファイルパスであり、フルパスを含む可能性がありました。
- 変更後: `path.Base(fi.h.Name)` を返すように変更されました。`path.Base()`関数は、与えられたパスの最後の要素(ベース名)を抽出します。これにより、`os.FileInfo.Name()`の仕様通り、ファイル名のみが返されるようになりました。ディレクトリの場合も同様に、`path.Clean`でパスを正規化した後、`path.Base`でベース名を取得しています。
- **`FileInfoHeader` 関数のコメント追加**:
- `os.FileInfo.Name`メソッドがファイルのベース名のみを返すため、`FileInfoHeader`関数によって作成されるヘッダーの`Name`フィールドもベース名のみが設定されることを明記しました。
- これにより、もしヘッダーにフルパスを記録したい場合は、この関数を呼び出した後に`Header.Name`フィールドを別途修正する必要があることが明確になりました。
### `src/pkg/archive/tar/tar_test.go` の変更
- **`TestHeaderRoundTrip` テストの修正**:
- `import`文に`"path"`と`"strings"`が追加されました。
- `if strings.Contains(fi.Name(), "/")` の追加: `os.FileInfo`から取得した`fi.Name()`にパス区切り文字(スラッシュ)が含まれていないことを確認するアサーションが追加されました。これは、`Name()`がベース名のみを返すという新しい(正しい)動作を検証するためのものです。
- `if got, want := h2.Name, g.h.Name; got != want` の比較ロジックの変更:
- 変更前は、`h2.Name`(`tar.Header`から生成された`Header`のName)と`g.h.Name`(元の`tar.Header`のName)を直接比較していました。
- 変更後では、`want`の値として`path.Base(g.h.Name)`が使用されるようになりました。これにより、`os.FileInfo.Name()`がベース名を返すようになったことに合わせて、テストの期待値もベース名に修正されました。ディレクトリの場合は末尾にスラッシュを追加する処理も含まれています。
### `src/pkg/archive/zip/struct.go` の変更
- **`func (fi headerFileInfo) Name() string` の修正**:
- 変更前: `fi.fh.Name` をそのまま返していました。`fi.fh.Name`はzipヘッダー内のファイルパスであり、フルパスを含む可能性がありました。
- 変更後: `path.Base(fi.fh.Name)` を返すように変更されました。これにより、`os.FileInfo.Name()`の仕様通り、ファイル名のみが返されるようになりました。
- **`FileInfoHeader` 関数のコメント追加**:
- `tar`パッケージと同様に、`os.FileInfo.Name`メソッドがファイルのベース名のみを返すため、`FileInfoHeader`関数によって作成されるヘッダーの`Name`フィールドもベース名のみが設定されることを明記しました。
- これにより、もしヘッダーにフルパスを記録したい場合は、この関数を呼び出した後に`FileHeader.Name`フィールドを別途修正する必要があることが明確になりました。
これらの変更により、`archive/tar`と`archive/zip`パッケージは、`os.FileInfo`インターフェースの`Name()`メソッドのGoの仕様に完全に準拠するようになりました。
## 関連リンク
- **関連するGo Issue**: `Fixes #6180.` (ただし、検索結果によると、このIssue番号はGoの公式リポジトリでは見つかりませんでした。コミットメッセージに記載されているため、内部的なトラッカーの可能性や、後に別のIssueに統合された可能性も考えられます。)
- **以前の変更リスト (CL)**: `https://golang.org/cl/7305072/` (このCLによって`os.FileInfo`の実装が追加されたが、バグがあった)
- **このコミットの変更リスト (CL)**: `https://golang.org/cl/13118043`
## 参考にした情報源リンク
- [Go os.FileInfo interface Name() specification - github.com](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHauDljJkCmArdwyKpCpJR7jE58Y3RIrnElN23XYal46ajdkN_ZJeqBPNcllmtvpReQzK6phoKbYjgvjpc1Ia6zXLXg2xe2HgBlVwnXElOTJb0xWMiAXHNXvG1CTLBLNW1E3iBk)
- [Go os.FileInfo interface Name() specification - golangbridge.org](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQE7XFB_iAJtuxwT2VSJtQzTUCOYSaU_hO9hLjxMyj4jzrqlJLmRnjlltf-KpBjBH8EilFrPpsg9rDXf4aXi7SlYjT11poM_XTt8nGjHGWgoaTvOkduVvTuyb12T-aSuL2ZQXEaf0qI9KatSNiP0JuMfmjlbDnIXkxN19OnARyiHvoCR1A==)
- [Go archive/tar package FileInfo - go.dev](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFhRWdXb-mcFsrQiURvNMUElqb5Jf4Mxd-StvyJpRe0138b2v_ZK-SFBm1nEeyF2rAlzRt91PgOesOYIxgScUwnYLquFOeD2bEQMWJrYPkQw4h708B_3DKRyA==)
- [Go archive/tar package FileInfo - arthurkoziel.com](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFve83HKEXSXPo7XDzwDdxRG6MUPdaXk-OHkPNMZ-XBI5MIQGylWlsBuLLRkaFFQlSPDAAZb-1cQyk1xir3DQC9x8zkZrCcIKDazHZ5sAyai2bmc132_03MYrUpbYGn51fh9yl6EGRGl-11-_q51UFj6LsN)
- [Go archive/zip package FileInfo - go.dev](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHLKNOUGGhAb81CWrFyXwbOiKKhErNlb7riuaz-891KNl1BuOg3U6qCGjPySPMNeB3Ud-wjAFRTQ1DPBzQU2089kaPqHkyVr1AikF9tC-FMZID1ZtrMikq71Q==)
- [Go archive/zip package FileInfo - gosamples.dev](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHH4JwQRGOFz78MTNeC4ULfeMIQG5duP9GohaDUeveoiy6c0dKMMab9uZJNkdaySd7jdMlgg4NggZj0XCrbePO2_2RvCWvQgDxKu0iqVia8cHHxb3scct1Y9aE=)
- [golang.org/cl/7305072 - appspot.com](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHRQEzAahqTRIiGXpe4XvKP0OKBwVTv4_l-ei5nVJiJFqKTWr1MPTxHvm0pn7zPs80pvifPra4hAgRqGNxxcEH34nrZUfZblwJYnZq5Dgv5GydvMwbQtIRzmN9dGQYfbmM=)
- [golang.org/cl/7305072 - github.com](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHFI0sEU586GR9tGgKAp2B7IAGVAk8sJxzoZ_dJ8zcsCxONouw-kueWORVkpEi-WgiiW2RdI_LI6ai6wpCKLTbuBzHYl3EwS14TybQMuhKCGc-lQAh_SwLUD_XOQo0i472Q==)
- [golang.org/cl/13118043 - web search result summary (not the CL itself)](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHHVTCH1Y-UZLAmJGeqpODnW97OzNOCvd-Xw07XDR7SOP45R_RkrAuMqFl5CG2WpMOqKfPlqVXmm-JZ0W1JkzjzHDjjIyLaiZ1hsoB9FJ0FoAn50xsC1RFB4U0DglDeNb9n2PPREGRRkdEnhL-ZxQ==)