[インデックス 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
パッケージの FileHeader
に SetModTime
メソッドを追加しました。
これにより、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
となる。
- 例: 2011年は
-
時刻 (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
に新しいヘルパー関数 timeToMsDosTime
と FileHeader
構造体のメソッド SetModTime
を追加したことです。
timeToMsDosTime
関数
この関数は、Goの time.Time
オブジェクトをMS-DOS形式の2つの uint16
値(日付と時刻)に変換します。
- UTCへの変換: 変換の前に、入力された
time.Time
オブジェクトをUTC(協定世界時)に変換します (t.In(time.UTC)
)。これは、タイムゾーンの差異による日付や時刻のずれを防ぎ、一貫した変換を保証するためです。 - 日付のエンコード:
t.Day()
: 日(1-31)をそのまま使用。int(t.Month())<<5
: 月(1-12)を5ビット左シフトして、日付の後に配置。(t.Year()-1980)<<9
: 年(1980年からのオフセット)を9ビット左シフトして、月の後に配置。 これらをビットOR演算で結合し、fDate
を生成します。
- 時刻のエンコード:
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.go
に TestModTime
という新しいテスト関数が追加されました。このテストは以下のことを確認します。
- 特定の
time.Time
オブジェクト (testTime
) を作成します。 - 新しい
FileHeader
オブジェクトを作成し、SetModTime
メソッドを使ってtestTime
を設定します。 FileHeader
のModTime()
メソッドを呼び出して、設定された日時をtime.Time
オブジェクトとして取得します (outTime
)。outTime
がtestTime
と等しいかどうかをEqual
メソッドで比較します。
このテストは、SetModTime
と ModTime
の両方が正しく機能し、MS-DOS形式への変換とそこからの復元が期待通りに行われることを検証します。ただし、MS-DOS形式の2秒精度のため、元の testTime
と outTime
が完全に一致するわけではなく、秒の端数が丸められる可能性があります。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
内部の ModifiedDate
と ModifiedTime
フィールドを更新します。
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形式の仕様によるものであり、このテストはそれを考慮した上で設計されています。
関連リンク
- Go Issue #2574: https://code.google.com/p/go/issues/detail?id=2574 (古いGoのIssueトラッカーのリンクですが、コミットメッセージに記載されています)
- Go CL 5494072: https://golang.org/cl/5494072 (Goのコードレビューシステムへのリンク)
参考にした情報源リンク
- MS-DOS Date/Time Format (MSDN): https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-dosdatetimetofiletime (コミット内のコメントに記載されているMSDNのリンク)
- ZIP File Format Specification: https://pkware.com/webdocs/casestudies/APPNOTE.TXT (ZIPファイルフォーマットの公式仕様)
- Go
time
package documentation: https://pkg.go.dev/time - Go
archive/zip
package documentation: https://pkg.go.dev/archive/zip - Go Issue Tracker (現在の): https://github.com/golang/go/issues (古いIssueリンクが機能しない場合のために)