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

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

このコミットは、Go言語の標準ライブラリ archive/zip パッケージにおいて、ZIPアーカイブ内のファイルの最終更新日時を設定するための SetModTime メソッドを FileHeader 構造体に追加するものです。これにより、ZIPファイル内のエントリのタイムスタンプをより正確に制御できるようになります。

コミット

commit 0b28de9a05be8eea49f9a31325a0091d9fa8b191
Author: Andrew Gerrand <adg@golang.org>
Date:   Mon Dec 19 14:59:41 2011 +1100

    archive/zip: add SetModTime method to FileHeader
    
    Fixes #2574.
    
    R=golang-dev, bradfitz, adg, bradfitz
    CC=golang-dev
    https://golang.org/cl/5494072

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

https://github.com/golang/go/commit/0b28de9a05be8eea49f9a31325a0091d9fa8b191

元コミット内容

archive/zip パッケージの FileHeaderSetModTime メソッドを追加しました。 これにより、Issue #2574 が修正されます。

変更の背景

この変更は、Go言語のIssueトラッカーで報告されたIssue #2574に対応するものです。このIssueでは、archive/zip パッケージを使用してZIPアーカイブを作成する際に、ファイルの最終更新日時(ModTime)を適切に設定する手段がないことが指摘されていました。既存の FileHeader 構造体には ModTime() メソッドがありましたが、これはMS-DOS形式で格納された日時情報を time.Time 型に変換して返すものであり、逆に time.Time 型の値をMS-DOS形式に変換して設定する機能がありませんでした。

この機能がないため、ユーザーはZIPアーカイブ内のファイルのタイムスタンプをプログラム的に制御することができず、例えば元のファイルのタイムスタンプを保持したままアーカイブを作成するといった操作が困難でした。このコミットは、この欠落した機能を提供し、ZIPアーカイブの作成における柔軟性を向上させることを目的としています。

前提知識の解説

ZIPファイルフォーマットとタイムスタンプ

ZIPファイルフォーマットは、各エントリ(ファイルやディレクトリ)のメタデータの一部として、最終更新日時を格納します。この日時は、通常、MS-DOSのFATファイルシステムで使用される形式でエンコードされます。

MS-DOS 日時形式

MS-DOSの日時形式は、日付と時刻をそれぞれ16ビットの整数で表現します。

  • 日付 (Date):

    • ビット 0-4: 日 (1-31)
    • ビット 5-8: 月 (1-12)
    • ビット 9-15: 年 (1980年からのオフセット)
      • 例: 2011年は 2011 - 1980 = 31 となる。
  • 時刻 (Time):

    • ビット 0-4: 秒 (0-29, 2秒単位)
    • ビット 5-10: 分 (0-59)
    • ビット 11-15: 時 (0-23)

この形式の最大の特徴は、秒の精度が2秒単位であることです。つまり、奇数秒は表現できません。

Go言語の time.Time

Go言語の time パッケージは、日時を扱うための time.Time 構造体を提供します。これはナノ秒単位の精度を持ち、タイムゾーン情報も保持できます。MS-DOS形式との変換では、この高精度な time.Time を2秒精度のMS-DOS形式に丸める必要があります。

archive/zip パッケージ

archive/zip はGo言語の標準ライブラリで、ZIPアーカイブの読み書きをサポートします。FileHeader 構造体は、ZIPアーカイブ内の個々のファイルエントリのメタデータ(ファイル名、サイズ、圧縮方法、最終更新日時など)を表現します。

技術的詳細

このコミットの主要な変更点は、archive/zip/struct.go に新しいヘルパー関数 timeToMsDosTimeFileHeader 構造体のメソッド SetModTime を追加したことです。

timeToMsDosTime 関数

この関数は、Goの time.Time オブジェクトをMS-DOS形式の2つの uint16 値(日付と時刻)に変換します。

  1. UTCへの変換: 変換の前に、入力された time.Time オブジェクトをUTC(協定世界時)に変換します (t.In(time.UTC))。これは、タイムゾーンの差異による日付や時刻のずれを防ぎ、一貫した変換を保証するためです。
  2. 日付のエンコード:
    • t.Day(): 日(1-31)をそのまま使用。
    • int(t.Month())<<5: 月(1-12)を5ビット左シフトして、日付の後に配置。
    • (t.Year()-1980)<<9: 年(1980年からのオフセット)を9ビット左シフトして、月の後に配置。 これらをビットOR演算で結合し、fDate を生成します。
  3. 時刻のエンコード:
    • t.Second()/2: 秒を2で割って、2秒単位の精度に丸めます。
    • t.Minute()<<5: 分を5ビット左シフトして、秒の後に配置。
    • t.Hour()<<11: 時を11ビット左シフトして、分の後に配置。 これらをビットOR演算で結合し、fTime を生成します。

この変換ロジックは、MS-DOSの日時形式の仕様に厳密に従っています。

FileHeader.SetModTime メソッド

このメソッドは、FileHeader 構造体に新しい time.Time オブジェクトを受け取り、その ModifiedDate および ModifiedTime フィールドを更新します。内部的には、先ほど説明した timeToMsDosTime 関数を呼び出して、time.Time オブジェクトをMS-DOS形式に変換し、その結果を FileHeader の対応するフィールドに設定します。

これにより、ユーザーは FileHeader オブジェクトを作成または変更する際に、Goの time.Time オブジェクトを使って簡単に最終更新日時を設定できるようになります。

テストケース (zip_test.go)

zip_test.goTestModTime という新しいテスト関数が追加されました。このテストは以下のことを確認します。

  1. 特定の time.Time オブジェクト (testTime) を作成します。
  2. 新しい FileHeader オブジェクトを作成し、SetModTime メソッドを使って testTime を設定します。
  3. FileHeaderModTime() メソッドを呼び出して、設定された日時を time.Time オブジェクトとして取得します (outTime)。
  4. outTimetestTime と等しいかどうかを Equal メソッドで比較します。

このテストは、SetModTimeModTime の両方が正しく機能し、MS-DOS形式への変換とそこからの復元が期待通りに行われることを検証します。ただし、MS-DOS形式の2秒精度のため、元の testTimeoutTime が完全に一致するわけではなく、秒の端数が丸められる可能性があります。time.Time.Equal メソッドは、ナノ秒単位まで含めて厳密な比較を行うため、このテストは秒の端数による不一致を検出する可能性がありますが、この特定のテストケースでは秒が偶数であるため問題ありません。

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

src/pkg/archive/zip/struct.go

@@ -96,12 +96,28 @@ func msDosTimeToTime(dosDate, dosTime uint16) time.Time {
 	)
 }
 
+// timeToMsDosTime converts a time.Time to an MS-DOS date and time.
+// The resolution is 2s.
+// See: http://msdn.microsoft.com/en-us/library/ms724274(v=VS.85).aspx
+func timeToMsDosTime(t time.Time) (fDate uint16, fTime uint16) {
+	t = t.In(time.UTC)
+	fDate = uint16(t.Day() + int(t.Month())<<5 + (t.Year()-1980)<<9)
+	fTime = uint16(t.Second()/2 + t.Minute()<<5 + t.Hour()<<11)
+	return
+}
+
 // ModTime returns the modification time.
 // The resolution is 2s.
 func (h *FileHeader) ModTime() time.Time {
 	return msDosTimeToTime(h.ModifiedDate, h.ModifiedTime)
 }
 
+// SetModTime sets the ModifiedTime and ModifiedDate fields to the given time.
+// The resolution is 2s.
+func (h *FileHeader) SetModTime(t time.Time) {
+	h.ModifiedDate, h.ModifiedTime = timeToMsDosTime(t)
+}
+
 // traditional names for Unix constants
 const (
 	s_IFMT  = 0xf000

src/pkg/archive/zip/zip_test.go

@@ -11,6 +11,7 @@ import (
 	"fmt"
 	"io"
 	"testing"
+	"time"
 )
 
 type stringReaderAt string
@@ -55,3 +56,13 @@ func TestOver65kFiles(t *testing.T) {
 		}
 	}\n}\n+\n+func TestModTime(t *testing.T) {\n+\tvar testTime = time.Date(2009, time.November, 10, 23, 45, 58, 0, time.UTC)\n+\tfh := new(FileHeader)\n+\tfh.SetModTime(testTime)\n+\toutTime := fh.ModTime()\n+\tif !outTime.Equal(testTime) {\n+\t\tt.Errorf(\"times don\'t match: got %s, want %s\", outTime, testTime)\n+\t}\n+}\n```

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

### `timeToMsDosTime` 関数

この関数は、Goの `time.Time` 型の値をMS-DOS形式の `uint16` 型の日付と時刻に変換するロジックを実装しています。

```go
func timeToMsDosTime(t time.Time) (fDate uint16, fTime uint16) {
	t = t.In(time.UTC) // UTCに変換
	// 日付のエンコード
	fDate = uint16(t.Day() + int(t.Month())<<5 + (t.Year()-1980)<<9)
	// 時刻のエンコード (秒は2で割って2秒精度に丸める)
	fTime = uint16(t.Second()/2 + t.Minute()<<5 + t.Hour()<<11)
	return
}
  • t.In(time.UTC): time.Time オブジェクトをUTCに変換します。これは、ZIPファイルフォーマットがタイムゾーンに依存しない日時を格納するため、変換の一貫性を保つ上で重要です。
  • t.Day() + int(t.Month())<<5 + (t.Year()-1980)<<9: 日、月、年(1980年からのオフセット)をそれぞれ適切なビット位置にシフトし、ビットOR演算で結合して日付の uint16 を生成します。
  • t.Second()/2 + t.Minute()<<5 + t.Hour()<<11: 秒(2秒精度に丸める)、分、時をそれぞれ適切なビット位置にシフトし、ビットOR演算で結合して時刻の uint16 を生成します。

FileHeader.SetModTime メソッド

このメソッドは、FileHeader 構造体のレシーバーとして定義され、外部から time.Time オブジェクトを受け取って、FileHeader 内部の ModifiedDateModifiedTime フィールドを更新します。

func (h *FileHeader) SetModTime(t time.Time) {
	h.ModifiedDate, h.ModifiedTime = timeToMsDosTime(t)
}

このメソッドは、timeToMsDosTime 関数を呼び出すことで、Goの time.Time オブジェクトからMS-DOS形式への変換処理を抽象化し、FileHeader の利用者が簡単に最終更新日時を設定できるようにします。

TestModTime テスト関数

このテストは、SetModTime メソッドが正しく機能し、設定された日時が ModTime() メソッドで正確に取得できることを検証します。

func TestModTime(t *testing.T) {
	var testTime = time.Date(2009, time.November, 10, 23, 45, 58, 0, time.UTC)
	fh := new(FileHeader)
	fh.SetModTime(testTime)
	outTime := fh.ModTime()
	if !outTime.Equal(testTime) {
		t.Errorf("times don't match: got %s, want %s", outTime, testTime)
	}
}

このテストでは、秒が偶数である 58 秒を使用しているため、MS-DOS形式の2秒精度による丸め誤差が発生せず、outTime.Equal(testTime)true を返すことが期待されます。もし奇数秒を設定した場合、outTime は最も近い偶数秒に丸められるため、Equal メソッドは false を返す可能性があります。これはMS-DOS形式の仕様によるものであり、このテストはそれを考慮した上で設計されています。

関連リンク

参考にした情報源リンク