[インデックス 13665] ファイルの概要
このコミットは、Go言語の標準ライブラリ archive/zip
パッケージにZIP64形式のサポートを追加するものです。これにより、4GBを超えるファイルサイズや、65535個を超えるエントリを持つZIPアーカイブを扱うことが可能になります。
コミット
commit 2e6d0968e308b69b8be720f51a4177e90f41668f
Author: Joakim Sernbrant <serbaut@gmail.com>
Date: Wed Aug 22 11:05:24 2012 +1000
archive/zip: zip64 support
R=golang-dev, r, adg
CC=golang-dev
https://golang.org/cl/6463050
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2e6d0968e308b69b8be720f51a4177e90f41668f
元コミット内容
archive/zip: zip64 support
このコミットは、Go言語の標準ライブラリであるarchive/zip
パッケージにZIP64形式のサポートを導入します。これにより、従来のZIP形式が持つファイルサイズやエントリ数の制限(それぞれ4GB、65535個)を超えたアーカイブを適切に読み書きできるようになります。
変更の背景
従来のZIPファイル形式は、ファイルサイズやアーカイブ内のエントリ数に関する32ビットの制限を持っていました。具体的には、個々のファイルの圧縮/非圧縮サイズ、およびZIPアーカイブ全体のサイズが4GB(2^32バイト)を超えることができず、また、アーカイブ内のファイルエントリの総数が65535個(2^16個)を超えることもできませんでした。
しかし、データ量の増大に伴い、これらの制限は現実的な問題となっていました。特に、大容量のデータバックアップ、大規模なソフトウェア配布、または多数の小さなファイルを含むアーカイブなどでは、4GBや65535個という制限は容易に超えられてしまいます。
ZIP64は、これらの32ビット制限を64ビットに拡張することで、この問題を解決するために導入されたZIP形式の拡張仕様です。このコミットは、Goのarchive/zip
パッケージがこのZIP64形式をサポートし、現代のデータストレージ要件に対応できるようにすることを目的としています。
前提知識の解説
ZIPファイル形式の基本構造
ZIPファイルは、複数のファイルを圧縮して一つにまとめるためのアーカイブ形式です。その基本的な構造は以下の主要なセクションで構成されます。
- ローカルファイルヘッダ (Local File Header): 各ファイルデータの直前に配置され、ファイル名、圧縮方法、CRC-32チェックサム、圧縮/非圧縮サイズなどの情報を含みます。
- ファイルデータ (File Data): 実際のファイルの内容(圧縮されている場合もある)です。
- データディスクリプタ (Data Descriptor): オプションで、ファイルデータの後に配置されます。ローカルファイルヘッダにサイズやCRC-32情報がゼロで書き込まれた場合(ストリーミング書き込みなど)、ここに実際の情報が書き込まれます。
- セントラルディレクトリ (Central Directory): ZIPファイルの末尾に配置され、アーカイブ内のすべてのファイルのローカルファイルヘッダのコピーのような情報(ファイル名、圧縮方法、サイズ、オフセットなど)を一元的に管理します。これにより、ZIPファイル全体をスキャンせずに特定のファイルにアクセスできます。
- セントラルディレクトリエンドレコード (End of Central Directory Record - EOCD): ZIPファイルの最後のセクションで、セントラルディレクトリの開始位置、サイズ、エントリ数などの情報を含みます。ZIPファイルの読み込みはこのEOCDから開始されます。
32ビット制限とZIP64の必要性
従来のZIP形式では、上記のヘッダやレコード内のサイズやオフセットを示すフィールドが32ビット整数で定義されていました。
- ファイルサイズ (Compressed Size, Uncompressed Size): 32ビット (最大 4GB - 1バイト)
- セントラルディレクトリのサイズ: 32ビット (最大 4GB - 1バイト)
- セントラルディレクトリのオフセット: 32ビット (最大 4GB - 1バイト)
- エントリ数 (Number of entries): 16ビット (最大 65535個)
これらの制限により、4GBを超えるファイルや、65535個を超えるファイルを含むアーカイブを作成・展開することができませんでした。
ZIP64拡張
ZIP64は、これらの32ビット/16ビットの制限を克服するために導入された拡張仕様です。ZIP64では、以下の方法で64ビットの値をサポートします。
- Extra Field (拡張フィールド): ローカルファイルヘッダとセントラルディレクトリヘッダに「ZIP64 Extended Information Extra Field」という特別な拡張フィールドを追加します。このフィールド内に、64ビットの圧縮サイズ、非圧縮サイズ、ローカルヘッダのオフセットなどの情報が格納されます。元の32ビットフィールドには
0xFFFFFFFF
(または16ビットフィールドには0xFFFF
)が設定され、ZIP64拡張フィールドを参照する必要があることを示します。 - ZIP64セントラルディレクトリエンドレコード (ZIP64 End of Central Directory Record): 従来のEOCDレコードの前に、64ビットのサイズとオフセット情報を含む新しいレコードが追加されます。
- ZIP64セントラルディレクトリロケータ (ZIP64 End of Central Directory Locator): ZIP64 EOCDレコードの場所を示すためのロケータレコードが追加されます。
これらの拡張により、ZIP64対応のツールは、大容量ファイルや多数のエントリを持つアーカイブを正しく処理できるようになります。
技術的詳細
このコミットは、archive/zip
パッケージの以下の主要な部分にZIP64サポートを実装しています。
-
struct.go
の変更:FileHeader
構造体にCompressedSize64
とUncompressedSize64
というuint64
型のフィールドが追加されました。これにより、64ビットのファイルサイズを保持できるようになります。既存のCompressedSize
とUncompressedSize
はuint32
型のままで、ZIP64ファイルでは0xFFFFFFFF
が設定されることで、64ビットフィールドを参照する必要があることを示します。directoryEnd
構造体のdirectoryRecords
,directorySize
,directoryOffset
フィールドがuint32
からuint64
に変更され、diskNbr
,dirDiskNbr
,dirRecordsThisDisk
もuint16
からuint32
/uint64
に拡張されました。- ZIP64関連の新しいシグネチャ定数(
directory64LocSignature
,directory64EndSignature
)と、関連するヘッダの長さ定数(dataDescriptor64Len
,directory64LocLen
,directory64EndLen
)が追加されました。 zip64ExtraId
定数(0x0001
)が定義され、ZIP64拡張フィールドのIDとして使用されます。zipVersion45
定数(45
)が追加され、ZIP64をサポートするバージョンとして使用されます。FileHeader
にisZip64()
メソッドが追加され、ファイルがZIP64形式を必要とするかどうかを判断します。
-
reader.go
の変更:readDirectoryHeader
関数が、ファイルのExtra
フィールド内にZIP64拡張情報が存在するかどうかをチェックし、存在する場合はUncompressedSize64
,CompressedSize64
,headerOffset
を64ビット値で更新するように修正されました。readDirectoryEnd
関数が、従来のEOCDレコードの前にZIP64ロケータとZIP64 EOCDレコードを検索し、それらの情報を使用してdirectoryEnd
構造体の64ビットフィールドを更新するように拡張されました。findDirectory64End
とreadDirectory64End
という新しいヘルパー関数が追加され、ZIP64関連のレコードを読み込むロジックをカプセル化しています。readBuf
にuint64()
メソッドが追加され、バイトスライスからuint64
値を読み取れるようになりました。
-
writer.go
の変更:Writer.Close()
メソッドが、アーカイブ内のエントリ数、セントラルディレクトリのサイズ、オフセットが32ビット制限を超える場合に、ZIP64セントラルディレクトリエンドレコードとロケータを書き込むように修正されました。Writer.CreateHeader()
およびfileWriter.close()
メソッドが、ファイルサイズが32ビット制限を超える場合に、ローカルファイルヘッダとデータディスクリプタにZIP64拡張フィールドを適切に書き込むように変更されました。特に、32ビットのサイズフィールドにはuint32max
が設定され、64ビットのサイズは拡張フィールドに格納されます。writeBuf
にuint64()
メソッドが追加され、バイトスライスにuint64
値を書き込めるようになりました。
-
テストファイルの追加と修正:
src/pkg/archive/zip/testdata/zip64.zip
という新しいテスト用のZIP64ファイルが追加されました。reader_test.go
にzip64.zip
を読み込むテストケースが追加されました。zip_test.go
にTestFileHeaderRoundTrip64
とTestZip64
という新しいテスト関数が追加されました。TestZip64
は、4GBを超えるサイズのファイルを書き込み、それを読み戻すことでZIP64の書き込みと読み込みの機能が正しく動作することを確認します。
これらの変更により、Goのarchive/zip
パッケージは、大容量のZIPアーカイブを透過的に処理できるようになり、ユーザーはZIP64の複雑さを意識することなく、大きなファイルを扱うことができるようになります。
コアとなるコードの変更箇所
src/pkg/archive/zip/struct.go
type FileHeader struct {
Name string
CreatorVersion uint16
ReaderVersion uint16
Flags uint16
Method uint16
ModifiedTime uint16 // MS-DOS time
ModifiedDate uint16 // MS-DOS date
CRC32 uint32
CompressedSize uint32 // deprecated; use CompressedSize64
UncompressedSize uint32 // deprecated; use UncompressedSize64
CompressedSize64 uint64
UncompressedSize64 uint64
Extra []byte
ExternalAttrs uint32 // Meaning depends on CreatorVersion
Comment string
}
type directoryEnd struct {
diskNbr uint32 // unused
dirDiskNbr uint32 // unused
dirRecordsThisDisk uint64 // unused
directoryRecords uint64
directorySize uint64
directoryOffset uint64 // relative to file
commentLen uint16
comment string
}
// isZip64 returns true if the file size exceeds the 32 bit limit
func (fh *FileHeader) isZip64() bool {
return fh.CompressedSize64 > uint32max || fh.UncompressedSize64 > uint32max
}
src/pkg/archive/zip/reader.go
func readDirectoryHeader(f *File, r io.Reader) error {
// ... (既存のコード) ...
f.CompressedSize64 = uint64(f.CompressedSize)
f.UncompressedSize64 = uint64(f.UncompressedSize)
// ... (既存のコード) ...
if len(f.Extra) > 0 {
b := readBuf(f.Extra)
for len(b) > 0 {
tag := b.uint16()
size := b.uint16()
if tag == zip64ExtraId {
// update directory values from the zip64 extra block
eb := readBuf(b)
if len(eb) >= 8 {
f.UncompressedSize64 = eb.uint64()
}
if len(eb) >= 8 {
f.CompressedSize64 = eb.uint64()
}
if len(eb) >= 8 {
f.headerOffset = int64(eb.uint64())
}
}
b = b[size:]
}
}
return nil
}
func readDirectoryEnd(r io.ReaderAt, size int64) (dir *directoryEnd, err error) {
// ... (既存のコード) ...
d := &directoryEnd{
diskNbr: uint32(b.uint16()),
dirDiskNbr: uint32(b.uint16()),
dirRecordsThisDisk: uint64(b.uint16()),
directoryRecords: uint64(b.uint16()),
directorySize: uint64(b.uint32()),
directoryOffset: uint64(b.uint32()),
commentLen: b.uint16(),
}
// ... (既存のコード) ...
p, err := findDirectory64End(r, directoryEndOffset)
if err == nil && p >= 0 {
err = readDirectory64End(r, p, d)
}
if err != nil {
return nil, err
}
return d, nil
}
func (b *readBuf) uint64() uint64 {
v := binary.LittleEndian.Uint64(*b)
*b = (*b)[8:]
return v
}
src/pkg/archive/zip/writer.go
func (w *Writer) Close() error {
// ... (既存のコード - セントラルディレクトリの書き込み) ...
records := uint64(len(w.dir))
size := uint64(end - start)
offset := uint64(start)
if records > uint16max || size > uint32max || offset > uint32max {
var buf [directory64EndLen + directory64LocLen]byte
b := writeBuf(buf[:])
// zip64 end of central directory record
b.uint32(directory64EndSignature)
b.uint64(directory64EndLen)
b.uint16(zipVersion45) // version made by
b.uint16(zipVersion45) // version needed to extract
b.uint32(0) // number of this disk
b.uint32(0) // number of the disk with the start of the central directory
b.uint64(records) // total number of entries in the central directory on this disk
b.uint64(records) // total number of entries in the central directory
b.uint64(size) // size of the central directory
b.uint64(offset) // offset of start of central directory with respect to the starting disk number
// zip64 end of central directory locator
b.uint32(directory64LocSignature)
b.uint32(0) // number of the disk with the start of the zip64 end of central directory
b.uint64(uint64(end)) // relative offset of the zip64 end of central directory record
b.uint32(1) // total number of disks
if _, err := w.cw.Write(buf[:]); err != nil {
return err
}
// store max values in the regular end record to signal that
// that the zip64 values should be used instead
records = uint16max
size = uint32max
offset = uint32max
}
// write end record (従来のEOCD)
var buf [directoryEndLen]byte
b := writeBuf(buf[:])
b.uint32(uint32(directoryEndSignature))
b = b[4:] // skip over disk number and first disk number (2x uint16)
b.uint16(uint16(records)) // number of entries this disk
b.uint16(uint16(records)) // number of entries total
b.uint32(uint32(size)) // size of directory
b.uint32(uint32(offset)) // start of directory
// ... (既存のコード) ...
return nil
}
func (w *fileWriter) close() error {
// ... (既存のコード) ...
fh.CRC32 = w.crc32.Sum32()
fh.CompressedSize64 = uint64(w.compCount.count)
fh.UncompressedSize64 = uint64(w.rawCount.count)
if fh.isZip64() {
fh.CompressedSize = uint32max
fh.UncompressedSize = uint32max
fh.ReaderVersion = zipVersion45 // requires 4.5 - File uses ZIP64 format extensions
} else {
fh.CompressedSize = uint32(fh.CompressedSize64)
fh.UncompressedSize = uint32(fh.UncompressedSize64)
}
// Write data descriptor.
var buf []byte
if fh.isZip64() {
buf = make([]byte, dataDescriptor64Len)
} else {
buf = make([]byte, dataDescriptorLen)
}
b := writeBuf(buf)
b.uint32(dataDescriptorSignature) // de-facto standard, required by OS X
b.uint32(fh.CRC32)
if fh.isZip64() {
b.uint64(fh.CompressedSize64)
b.uint64(fh.UncompressedSize64)
} else {
b.uint32(fh.CompressedSize)
b.uint32(fh.UncompressedSize)
}
_, err := w.zipw.Write(buf)
return err
}
func (b *writeBuf) uint64(v uint64) {
binary.LittleEndian.PutUint64(*b, v)
*b = (*b)[8:]
}
コアとなるコードの解説
struct.go
の変更点
FileHeader
構造体へのCompressedSize64
とUncompressedSize64
の追加は、4GBを超えるファイルサイズを表現するための最も直接的な変更です。従来のuint32
フィールドは、ZIP64ファイルの場合にuint32max
(0xFFFFFFFF
)という特殊な値を持つことで、これらの64ビットフィールドを参照するようシグナルを送ります。directoryEnd
構造体のフィールドがuint64
に拡張されたのは、セントラルディレクトリのサイズやオフセット、エントリ数が4GBや65535個の制限を超える場合に対応するためです。isZip64()
メソッドは、ファイルがZIP64形式を必要とするかどうかを簡潔に判断するためのユーティリティ関数です。
reader.go
の変更点
readDirectoryHeader
におけるExtra
フィールドのパースロジックは、ZIP64拡張フィールド(zip64ExtraId
)を検出した場合に、その中に含まれる64ビットのサイズとオフセット情報をFileHeader
の対応する64ビットフィールドに適用します。これにより、リーダーはZIP64形式で保存された正確なファイルサイズとオフセットを認識できます。readDirectoryEnd
は、ZIPファイルの末尾からEOCDレコードを検索する際に、その手前にZIP64関連のレコード(directory64LocSignature
,directory64EndSignature
)が存在するかをチェックします。もし存在すれば、それらのレコードから64ビットのセントラルディレクトリ情報(サイズ、オフセット、エントリ数)を抽出し、directoryEnd
構造体を更新します。これにより、大容量のZIPファイルでもセントラルディレクトリを正しく見つけ、パースできるようになります。uint64()
メソッドは、Goのbinary
パッケージのLittleEndian.Uint64
を使用して、バイトスライスからリトルエンディアン形式の64ビット整数を効率的に読み取るためのヘルパーです。
writer.go
の変更点
Writer.Close()
におけるZIP64セントラルディレクトリ関連レコードの書き込みロジックは、アーカイブ全体のサイズやエントリ数が従来の制限を超える場合に発動します。この際、directory64EndSignature
とdirectory64LocSignature
を持つ新しいレコードが生成され、64ビットのサイズとオフセット情報が書き込まれます。これにより、ZIP64対応のリーダーがこのアーカイブを正しく解釈できるようになります。また、従来のEOCDレコードの対応するフィールドにはuint16max
やuint32max
が設定され、ZIP64レコードを参照するよう指示します。fileWriter.close()
におけるデータディスクリプタの書き込みロジックは、個々のファイルの圧縮/非圧縮サイズが4GBを超える場合に、dataDescriptor64Len
(24バイト)のデータディスクリプタを書き込み、その中に64ビットのサイズ情報を格納します。これにより、ストリーミング書き込みなどでローカルファイルヘッダにサイズ情報が書き込めなかった場合でも、正確なサイズを記録できます。uint64()
メソッドは、binary.LittleEndian.PutUint64
を使用して、64ビット整数をバイトスライスにリトルエンディアン形式で書き込むためのヘルパーです。
これらの変更は、ZIP64の仕様に厳密に従い、既存の32ビットZIP形式との後方互換性を保ちつつ、大容量ファイルや多数のエントリを持つアーカイブの読み書きを可能にしています。
関連リンク
- Go
archive/zip
package documentation - ZIP file format specification (PKWARE APPNOTE.TXT) - ZIPファイル形式の公式仕様書。ZIP64に関する詳細も含まれています。