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

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

このコミットは、Go言語の archive/zip パッケージに、os.FileInfoFileHeader 間で変換を行うための新しい関数を追加するものです。これにより、ZIPアーカイブ内のファイル情報をGoの標準的なファイル情報インターフェースと相互運用できるようになります。

コミット

commit b62a5099e4bf2e87525792dd562c20894fff878c
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Thu Jan 26 15:31:09 2012 -0800

    archive/zip: add functions to convert between os.FileInfo & FileHeader
    
    Fixes #2186
    
    R=golang-dev, gri, adg
    CC=golang-dev
    https://golang.org/cl/5579044

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

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

元コミット内容

archive/zip: add functions to convert between os.FileInfo & FileHeader

Fixes #2186

R=golang-dev, gri, adg
CC=golang-dev
https://golang.org/cl/5579044

変更の背景

この変更の主な背景は、Goの標準ライブラリである archive/zip パッケージと、ファイルシステム操作に関連する os パッケージとの間の相互運用性を向上させることにあります。具体的には、ZIPアーカイブ内のエントリを表す FileHeader 構造体と、Goの一般的なファイル情報インターフェースである os.FileInfo との間で、容易に変換できるようにすることが目的です。

コミットメッセージにある "Fixes #2186" は、この変更が特定の課題を解決することを示唆していますが、このコミットが2012年のものであるため、当時のGoのIssueトラッカーにおける #2186 の具体的な内容は、現在の公開情報からは特定が困難です。しかし、一般的にこのような変換機能の追加は、以下のようなニーズから発生します。

  • APIの一貫性: Goの他のファイル操作API(例: os.Stat)が os.FileInfo を返すため、archive/zip パッケージも同様のインターフェースを提供することで、開発者が一貫した方法でファイル情報を扱えるようになります。
  • 利便性: os.FileInfo はファイル名、サイズ、更新時刻、パーミッションなどの一般的なファイル属性を抽象化しており、このインターフェースを介してZIPエントリの情報を取得できると、既存の os.FileInfo を受け入れる関数やライブラリと容易に連携できるようになります。
  • コードの簡素化: FileHeader から os.FileInfo への変換、またはその逆の変換を手動で行う必要がなくなるため、開発者のコードが簡素化され、エラーの可能性が減少します。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とパッケージに関する知識が必要です。

  • archive/zip パッケージ: Goの標準ライブラリの一部で、ZIPアーカイブの読み書きをサポートします。
    • FileHeader 構造体: ZIPアーカイブ内の個々のファイル(エントリ)のメタデータ(ファイル名、圧縮・非圧縮サイズ、更新日時、パーミッションなど)を保持する構造体です。ZIPファイルのセントラルディレクトリレコードに対応します。
    • File 構造体: ZIPアーカイブ内の個々のエントリを表す構造体で、FileHeader を含み、エントリのデータへのアクセスを提供します。
  • os パッケージ: Goの標準ライブラリの一部で、オペレーティングシステムとのインタラクション(ファイルシステム操作、プロセス管理など)を提供します。
    • os.FileInfo インターフェース: ファイルに関する抽象的な情報(ファイル名、サイズ、更新時刻、ファイルモード(パーミッションと種類)、ディレクトリかどうかなど)を提供するインターフェースです。os.Stat 関数などがこのインターフェースを実装した値を返します。
    • os.FileMode: ファイルのパーミッションビットとファイルの種類(ディレクトリ、シンボリックリンクなど)を表す型です。
  • インターフェースの実装: Goでは、ある型がインターフェースのすべてのメソッドを実装していれば、そのインターフェースを「実装している」とみなされます。明示的な宣言は不要です。

技術的詳細

このコミットでは、主に以下の2つの新しい機能が archive/zip パッケージに追加されています。

  1. FileHeader.FileInfo() os.FileInfo メソッドの追加:

    • FileHeader 構造体に FileInfo() メソッドが追加されました。このメソッドは、FileHeader の情報に基づいて os.FileInfo インターフェースを実装する新しい型 headerFileInfo のインスタンスを返します。
    • headerFileInfo は内部的に FileHeader へのポインタを持ち、os.FileInfo インターフェースが要求する Name(), Size(), IsDir(), ModTime(), Mode() メソッドを実装しています。これにより、FileHeader の情報を os.FileInfo として透過的に扱えるようになります。
    • IsDir() メソッドは、FileHeaderMode() メソッドの結果に基づいてディレクトリかどうかを判断します。
    • Size() メソッドは、FileHeaderUncompressedSizeint64 にキャストして返します。
  2. FileInfoHeader(fi os.FileInfo) (*FileHeader, error) 関数の追加:

    • os.FileInfo インターフェースを引数に取り、それに対応する FileHeader 構造体のポインタを返す新しいトップレベル関数です。
    • この関数は、os.FileInfo からファイル名、非圧縮サイズ、更新時刻、ファイルモードなどの情報を抽出し、新しい FileHeader インスタンスに設定します。
    • 特に、ファイルサイズがZIPフォーマットで許容される最大値(約4GB - 1バイト)を超える場合、エラーを返すようになっています。これは、ZIPフォーマットの制限(UncompressedSizeuint32 であるため)に対応するためです。
    • SetModTime()SetMode() メソッドは、FileHeader の内部表現(DOS日付/時刻形式や外部属性)に変換して設定するために使用されます。

また、既存の FileHeader.Mode() メソッドも変更されています。

  • 変更前は (mode os.FileMode, err error) のようにエラーを返していましたが、変更後は (mode os.FileMode) となり、エラーを返さなくなりました。これは、モード情報の取得が常に成功すると見なされるようになったためです。
  • これに伴い、reader_test.go 内の testFileMode 関数も、f.Mode() がエラーを返さなくなったことに合わせて修正されています。

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

src/pkg/archive/zip/struct.go

--- a/src/pkg/archive/zip/struct.go
+++ b/src/pkg/archive/zip/struct.go
@@ -12,6 +12,7 @@ This package does not support ZIP64 or disk spanning.
 package zip
 
 import (
+	"errors"
 	"os"
 	"time"
 )
@@ -55,6 +56,38 @@ type FileHeader struct {
 	Comment          string
 }
 
+// FileInfo returns an os.FileInfo for the FileHeader.
+func (fh *FileHeader) FileInfo() os.FileInfo {
+	return headerFileInfo{fh}
+}
+
+// headerFileInfo implements os.FileInfo.
+type headerFileInfo struct {
+	fh *FileHeader
+}
+
+func (fi headerFileInfo) Name() string       { return fi.fh.Name }
+func (fi headerFileInfo) Size() int64        { return int64(fi.fh.UncompressedSize) }
+func (fi headerFileInfo) IsDir() bool        { return fi.Mode().IsDir() }
+func (fi headerFileInfo) ModTime() time.Time { return fi.fh.ModTime() }
+func (fi headerFileInfo) Mode() os.FileMode  { return fi.fh.Mode() }
+
+// FileInfoHeader creates a partially-populated FileHeader from an
+// os.FileInfo.
+func FileInfoHeader(fi os.FileInfo) (*FileHeader, error) {
+	size := fi.Size()
+	if size > (1<<32 - 1) {
+		return nil, errors.New("zip: file over 4GB")
+	}
+	fh := &FileHeader{
+		Name:             fi.Name(),
+		UncompressedSize: uint32(size),
+	}
+	fh.SetModTime(fi.ModTime())
+	fh.SetMode(fi.Mode())
+	return fh, nil
+}
+
 type directoryEnd struct {
 	diskNbr            uint16 // unused
 	dirDiskNbr         uint16 // unused
@@ -131,8 +164,7 @@ const (
 )
 
 // Mode returns the permission and mode bits for the FileHeader.
-// An error is returned in case the information is not available.
-func (h *FileHeader) Mode() (mode os.FileMode, err error) {
+func (h *FileHeader) Mode() (mode os.FileMode) {
 	switch h.CreatorVersion >> 8 {
 	case creatorUnix, creatorMacOSX:
 		mode = unixModeToFileMode(h.ExternalAttrs >> 16)
@@ -142,7 +174,7 @@ func (h *FileHeader) Mode() (mode os.FileMode, err error) {
 	if len(h.Name) > 0 && h.Name[len(h.Name)-1] == '/' {
 		mode |= os.ModeDir
 	}
-	return mode, nil
+	return mode
 }
 
 // SetMode changes the permission and mode bits for the FileHeader.

src/pkg/archive/zip/reader_test.go

testFileMode 関数の変更。f.Mode() がエラーを返さなくなったため、エラーハンドリングが削除されました。

--- a/src/pkg/archive/zip/reader_test.go
+++ b/src/pkg/archive/zip/reader_test.go
@@ -250,13 +250,9 @@ func readTestFile(t *testing.T, ft ZipTestFile, f *File) {
 }
 
 func testFileMode(t *testing.T, f *File, want os.FileMode) {
-	mode, err := f.Mode()
+	mode := f.Mode()
 	if want == 0 {
-		if err == nil {
-			t.Errorf("%s mode: got %v, want none", f.Name, mode)
-		}
-	} else if err != nil {
-		t.Errorf("%s mode: %s", f.Name, err)
+		t.Errorf("%s mode: got %v, want none", f.Name, mode)
 	} else if mode != want {
 		t.Errorf("%s mode: want %v, got %v", f.Name, want, mode)
 	}

src/pkg/archive/zip/zip_test.go

新しい TestFileHeaderRoundTrip テストの追加。

--- a/src/pkg/archive/zip/zip_test.go
+++ b/src/pkg/archive/zip/zip_test.go
@@ -10,6 +10,7 @@ import (
 	"bytes"
 	"fmt"
 	"io"
+	"reflect"
 	"testing"
 	"time"
 )
@@ -66,3 +67,22 @@ func TestModTime(t *testing.T) {
 	\tt.Errorf("times don't match: got %s, want %s", outTime, testTime)\n \t}\n }\n+\n+func TestFileHeaderRoundTrip(t *testing.T) {\n+\tfh := &FileHeader{\n+\t\tName:             "foo.txt",\n+\t\tUncompressedSize: 987654321,\n+\t\tModifiedTime:     1234,\n+\t\tModifiedDate:     5678,\n+\t}\n+\tfi := fh.FileInfo()\n+\tfh2, err := FileInfoHeader(fi)\n+\n+\t// Ignore these fields:\n+\tfh2.CreatorVersion = 0\n+\tfh2.ExternalAttrs = 0\n+\n+\tif !reflect.DeepEqual(fh, fh2) {\n+\t\tt.Errorf("mismatch\\n input=%#v\\noutput=%#v\\nerr=%v", fh, fh2, err)\n+\t}\n+}\n```

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

### `FileHeader.FileInfo()` メソッド

このメソッドは、`FileHeader` のインスタンスから `os.FileInfo` インターフェースを実装する `headerFileInfo` 型の値を生成して返します。

```go
// FileInfo returns an os.FileInfo for the FileHeader.
func (fh *FileHeader) FileInfo() os.FileInfo {
	return headerFileInfo{fh}
}

// headerFileInfo implements os.FileInfo.
type headerFileInfo struct {
	fh *FileHeader
}

func (fi headerFileInfo) Name() string       { return fi.fh.Name }
func (fi headerFileInfo) Size() int64        { return int64(fi.fh.UncompressedSize) }
func (fi headerFileInfo) IsDir() bool        { return fi.Mode().IsDir() }
func (fi headerFileInfo) ModTime() time.Time { return fi.fh.ModTime() }
func (fi headerFileInfo) Mode() os.FileMode  { return fi.fh.Mode() }
  • headerFileInfoFileHeader へのポインタ fh を保持します。
  • os.FileInfo インターフェースの各メソッド(Name, Size, IsDir, ModTime, Mode)は、内部の FileHeader の対応するフィールドやメソッドを呼び出すことで実装されています。
  • Size()UncompressedSize (uint32) を int64 にキャストしています。
  • IsDir()FileHeaderMode() メソッドの結果を利用して、それがディレクトリモードであるかをチェックします。

この実装により、FileHeader を直接 os.FileInfo を期待する関数に渡すことはできませんが、fh.FileInfo() を呼び出すことで、os.FileInfo として扱うことができるようになります。

FileInfoHeader(fi os.FileInfo) (*FileHeader, error) 関数

この関数は、既存の os.FileInfo から新しい FileHeader を作成します。

// FileInfoHeader creates a partially-populated FileHeader from an
// os.FileInfo.
func FileInfoHeader(fi os.FileInfo) (*FileHeader, error) {
	size := fi.Size()
	if size > (1<<32 - 1) { // ZIPフォーマットのサイズ制限チェック (約4GB)
		return nil, errors.New("zip: file over 4GB")
	}
	fh := &FileHeader{
		Name:             fi.Name(),
		UncompressedSize: uint32(size),
	}
	fh.SetModTime(fi.ModTime()) // os.FileInfoの更新時刻をFileHeaderに設定
	fh.SetMode(fi.Mode())       // os.FileInfoのモードをFileHeaderに設定
	return fh, nil
}
  • os.FileInfo から Name()Size() を取得し、FileHeaderNameUncompressedSize に設定します。
  • ZIPフォーマットの UncompressedSize フィールドが uint32 であるため、ファイルサイズが (1<<32 - 1) (約4GB) を超える場合はエラーを返します。これはZIP64拡張をサポートしないこのパッケージの制約です。
  • SetModTime()SetMode() は、FileHeader の内部的な日付/時刻表現や外部属性に変換して設定するためのヘルパーメソッドです。

FileHeader.Mode() メソッドの変更

既存の FileHeader.Mode() メソッドは、エラーを返すシグネチャからエラーを返さないシグネチャに変更されました。

--- a/src/pkg/archive/zip/struct.go
+++ b/src/pkg/archive/zip/struct.go
@@ -131,8 +164,7 @@ const (
 )
 
 // Mode returns the permission and mode bits for the FileHeader.
-// An error is returned in case the information is not available.
-func (h *FileHeader) Mode() (mode os.FileMode, err error) {
+func (h *FileHeader) Mode() (mode os.FileMode) {
 	switch h.CreatorVersion >> 8 {
 	case creatorUnix, creatorMacOSX:
 		mode = unixModeToFileMode(h.ExternalAttrs >> 16)
@@ -142,7 +174,7 @@ func (h *FileHeader) Mode() (mode os.FileMode, err error) {
 	if len(h.Name) > 0 && h.Name[len(h.Name)-1] == '/' {
 		mode |= os.ModeDir
 	}
-	return mode, nil
+	return mode
 }

この変更は、FileHeader からモード情報を取得する際に、常に有効な os.FileMode が返されるという前提が確立されたことを示唆しています。これにより、呼び出し側でのエラーハンドリングが不要になり、コードが簡素化されます。

テストの変更

  • reader_test.gotestFileMode 関数は、FileHeader.Mode() がエラーを返さなくなったことに合わせて修正されました。
  • zip_test.goTestFileHeaderRoundTrip という新しいテストが追加されました。このテストは、FileHeader から os.FileInfo を作成し、その os.FileInfo から再度 FileHeader を作成するという「ラウンドトリップ」の変換が正しく行われることを検証します。CreatorVersionExternalAttrs フィールドは、変換プロセスで変更される可能性があるため、比較から除外されています。

これらの変更により、archive/zip パッケージは os.FileInfo との相互運用性が大幅に向上し、Goのファイルシステム関連APIとの連携がよりシームレスになりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント: archive/zip パッケージ, os パッケージ
  • Go言語のソースコード (このコミットの差分)