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

[インデックス 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ファイルは、複数のファイルを圧縮して一つにまとめるためのアーカイブ形式です。その基本的な構造は以下の主要なセクションで構成されます。

  1. ローカルファイルヘッダ (Local File Header): 各ファイルデータの直前に配置され、ファイル名、圧縮方法、CRC-32チェックサム、圧縮/非圧縮サイズなどの情報を含みます。
  2. ファイルデータ (File Data): 実際のファイルの内容(圧縮されている場合もある)です。
  3. データディスクリプタ (Data Descriptor): オプションで、ファイルデータの後に配置されます。ローカルファイルヘッダにサイズやCRC-32情報がゼロで書き込まれた場合(ストリーミング書き込みなど)、ここに実際の情報が書き込まれます。
  4. セントラルディレクトリ (Central Directory): ZIPファイルの末尾に配置され、アーカイブ内のすべてのファイルのローカルファイルヘッダのコピーのような情報(ファイル名、圧縮方法、サイズ、オフセットなど)を一元的に管理します。これにより、ZIPファイル全体をスキャンせずに特定のファイルにアクセスできます。
  5. セントラルディレクトリエンドレコード (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サポートを実装しています。

  1. struct.goの変更:

    • FileHeader構造体にCompressedSize64UncompressedSize64というuint64型のフィールドが追加されました。これにより、64ビットのファイルサイズを保持できるようになります。既存のCompressedSizeUncompressedSizeuint32型のままで、ZIP64ファイルでは0xFFFFFFFFが設定されることで、64ビットフィールドを参照する必要があることを示します。
    • directoryEnd構造体のdirectoryRecords, directorySize, directoryOffsetフィールドがuint32からuint64に変更され、diskNbr, dirDiskNbr, dirRecordsThisDiskuint16からuint32/uint64に拡張されました。
    • ZIP64関連の新しいシグネチャ定数(directory64LocSignature, directory64EndSignature)と、関連するヘッダの長さ定数(dataDescriptor64Len, directory64LocLen, directory64EndLen)が追加されました。
    • zip64ExtraId定数(0x0001)が定義され、ZIP64拡張フィールドのIDとして使用されます。
    • zipVersion45定数(45)が追加され、ZIP64をサポートするバージョンとして使用されます。
    • FileHeaderisZip64()メソッドが追加され、ファイルがZIP64形式を必要とするかどうかを判断します。
  2. reader.goの変更:

    • readDirectoryHeader関数が、ファイルのExtraフィールド内にZIP64拡張情報が存在するかどうかをチェックし、存在する場合はUncompressedSize64, CompressedSize64, headerOffsetを64ビット値で更新するように修正されました。
    • readDirectoryEnd関数が、従来のEOCDレコードの前にZIP64ロケータとZIP64 EOCDレコードを検索し、それらの情報を使用してdirectoryEnd構造体の64ビットフィールドを更新するように拡張されました。
    • findDirectory64EndreadDirectory64Endという新しいヘルパー関数が追加され、ZIP64関連のレコードを読み込むロジックをカプセル化しています。
    • readBufuint64()メソッドが追加され、バイトスライスからuint64値を読み取れるようになりました。
  3. writer.goの変更:

    • Writer.Close()メソッドが、アーカイブ内のエントリ数、セントラルディレクトリのサイズ、オフセットが32ビット制限を超える場合に、ZIP64セントラルディレクトリエンドレコードとロケータを書き込むように修正されました。
    • Writer.CreateHeader()およびfileWriter.close()メソッドが、ファイルサイズが32ビット制限を超える場合に、ローカルファイルヘッダとデータディスクリプタにZIP64拡張フィールドを適切に書き込むように変更されました。特に、32ビットのサイズフィールドにはuint32maxが設定され、64ビットのサイズは拡張フィールドに格納されます。
    • writeBufuint64()メソッドが追加され、バイトスライスにuint64値を書き込めるようになりました。
  4. テストファイルの追加と修正:

    • src/pkg/archive/zip/testdata/zip64.zipという新しいテスト用のZIP64ファイルが追加されました。
    • reader_test.gozip64.zipを読み込むテストケースが追加されました。
    • zip_test.goTestFileHeaderRoundTrip64TestZip64という新しいテスト関数が追加されました。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構造体へのCompressedSize64UncompressedSize64の追加は、4GBを超えるファイルサイズを表現するための最も直接的な変更です。従来のuint32フィールドは、ZIP64ファイルの場合にuint32max0xFFFFFFFF)という特殊な値を持つことで、これらの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セントラルディレクトリ関連レコードの書き込みロジックは、アーカイブ全体のサイズやエントリ数が従来の制限を超える場合に発動します。この際、directory64EndSignaturedirectory64LocSignatureを持つ新しいレコードが生成され、64ビットのサイズとオフセット情報が書き込まれます。これにより、ZIP64対応のリーダーがこのアーカイブを正しく解釈できるようになります。また、従来のEOCDレコードの対応するフィールドにはuint16maxuint32maxが設定され、ZIP64レコードを参照するよう指示します。
  • fileWriter.close()におけるデータディスクリプタの書き込みロジックは、個々のファイルの圧縮/非圧縮サイズが4GBを超える場合に、dataDescriptor64Len(24バイト)のデータディスクリプタを書き込み、その中に64ビットのサイズ情報を格納します。これにより、ストリーミング書き込みなどでローカルファイルヘッダにサイズ情報が書き込めなかった場合でも、正確なサイズを記録できます。
  • uint64()メソッドは、binary.LittleEndian.PutUint64を使用して、64ビット整数をバイトスライスにリトルエンディアン形式で書き込むためのヘルパーです。

これらの変更は、ZIP64の仕様に厳密に従い、既存の32ビットZIP形式との後方互換性を保ちつつ、大容量ファイルや多数のエントリを持つアーカイブの読み書きを可能にしています。

関連リンク

参考にした情報源リンク