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

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

このコミットは、Go言語の標準ライブラリ archive/zip パッケージ内のテストコード reader_test.go における、ZIPファイルの非圧縮サイズ(UncompressedSize)の取り扱いに関するバグ修正とテストの改善を目的としています。特に、32ビット環境でのビルドと動作の正確性を保証するために、ファイルサイズの比較ロジックが修正されています。

コミット

commit c4b279ba5ac2226a36434a6d715f2d7713e987e1
Author: Andrew Gerrand <adg@golang.org>
Date:   Tue Feb 11 16:27:14 2014 +1100

    archive/zip: use correct test, fix 32-bit build

    LGTM=dsymonds
    R=dsymonds
    CC=golang-codereviews
    https://golang.org/cl/61070047

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

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

元コミット内容

--- a/src/pkg/archive/zip/reader_test.go
+++ b/src/pkg/archive/zip/reader_test.go
@@ -371,11 +371,11 @@ func readTestFile(t *testing.T, zt ZipTest, ft ZipTestFile, f *File) {
 	}\n 	r.Close()\n \n-\tsize := int(f.UncompressedSize)\n-\tif size == 1<<32-1 {\n-\t\tsize = int(f.UncompressedSize64)\n+\tsize := uint64(f.UncompressedSize)\n+\tif size == uint32max {\n+\t\tsize = f.UncompressedSize64\n \t}\n-\tif g := b.Len(); g != size {\n+\tif g := uint64(b.Len()); g != size {\n \t\tt.Errorf("%v: read %v bytes but f.UncompressedSize == %v", f.Name, g, size)\n \t}\n \n```

## 変更の背景

このコミットの背景には、Go言語の `archive/zip` パッケージがZIP64形式で圧縮された大容量ファイルを正しく処理できない問題、特に32ビットシステム上での数値オーバーフローの問題がありました。

標準のZIPファイルフォーマットでは、ファイルサイズは32ビットの符号なし整数で表現されます。これにより、最大で約4GB(2^32 - 1バイト)までのファイルサイズしか表現できません。これを超えるサイズのファイルを扱うために、ZIP64という拡張フォーマットが導入されました。ZIP64では、ファイルサイズやオフセットを64ビットの符号なし整数で表現します。

`archive/zip` パッケージの `File` 構造体には、通常 `UncompressedSize` (32ビット) と、ZIP64対応のために `UncompressedSize64` (64ビット) の両方のフィールドが存在します。ZIP64が使用されている場合、`UncompressedSize` フィールドは `0xFFFFFFFF` (つまり `2^32 - 1`) という特殊な値を取り、実際のサイズは `UncompressedSize64` に格納されます。

元のテストコードでは、このサイズ判定と値の取得において、`int` 型へのキャストが多用されていました。32ビットシステムでは `int` 型が32ビット幅であるため、`uint32` や `uint64` の値を `int` にキャストすると、値が `math.MaxInt32` (約2GB) を超えた場合にオーバーフローや値の切り捨てが発生する可能性がありました。これにより、テストが誤って失敗したり、実際のファイルサイズと読み取られたバイト数が一致しないという問題が生じていました。

このコミットは、このような32ビット環境での潜在的なバグを修正し、ZIP64で表現される大容量ファイルサイズを正確に扱うためのテストロジックを改善することを目的としています。

## 前提知識の解説

1.  **ZIPファイルフォーマットとZIP64**:
    *   **ZIPファイル**: 複数のファイルを一つにまとめるための一般的なアーカイブフォーマットです。各ファイルのエントリには、ファイル名、圧縮・非圧縮サイズ、CRC32チェックサムなどのメタデータが含まれます。
    *   **32ビット制限**: 標準のZIPフォーマットでは、ファイルサイズやアーカイブ内のオフセットは32ビットの数値で表現されます。これにより、単一ファイルのサイズやアーカイブ全体のサイズが4GBに制限されます。
    *   **ZIP64**: 4GBを超えるファイルを扱うために導入されたZIPフォーマットの拡張です。ZIP64では、サイズやオフセットを64ビットの数値で格納するための追加フィールドが導入されます。標準の32ビットサイズフィールドが `0xFFFFFFFF` の場合、そのエントリはZIP64形式であり、実際のサイズはZIP64拡張フィールドに格納されていることを示します。

2.  **Go言語の数値型と型変換**:
    *   **`int` と `uint`**: Go言語の `int` および `uint` 型は、実行環境のCPUアーキテクチャに依存してサイズが変わります。32ビットシステムでは32ビット幅、64ビットシステムでは64ビット幅になります。
    *   **`int32`, `uint32`, `int64`, `uint64`**: これらは固定幅の整数型です。`uint32` は32ビットの符号なし整数(0から2^32-1)、`uint64` は64ビットの符号なし整数(0から2^64-1)です。
    *   **型変換の注意点**: 異なる型の間で変換を行う場合、特に大きい型から小さい型へ変換する際には、値が収まらない場合にオーバーフローや切り捨てが発生する可能性があります。例えば、`uint64` の値を `int` にキャストすると、`int` の最大値を超える部分が失われることがあります。

3.  **`archive/zip` パッケージ**:
    *   Go言語の標準ライブラリの一部で、ZIPファイルの読み書きをサポートします。
    *   `zip.File` 構造体には、`UncompressedSize` (通常 `uint32`) と `UncompressedSize64` (通常 `uint64`) のフィールドがあります。`UncompressedSize` が `0xFFFFFFFF` の場合、`UncompressedSize64` に実際の非圧縮サイズが格納されています。

4.  **`uint32max`**:
    *   このコミットで導入された、または参照されている定数です。Go言語の `math` パッケージには `MaxUint32` という定数があり、これは `2^32 - 1` (つまり `0xFFFFFFFF`) を表します。この値は、ZIP64が使用されているかどうかを判断するためのマーカーとして機能します。

## 技術的詳細

このコミットは、`archive/zip` パッケージの `reader_test.go` 内の `readTestFile` 関数における、非圧縮ファイルサイズの検証ロジックを修正しています。

元のコードでは、非圧縮サイズ `f.UncompressedSize` を `int` 型にキャストして `size` 変数に格納していました。そして、この `size` が `1<<32-1` (つまり `0xFFFFFFFF`) と等しい場合に、`f.UncompressedSize64` を `int` 型にキャストして `size` に再代入していました。最後に、読み込んだバイト数 `b.Len()` と `size` を比較していました。

このアプローチには以下の問題がありました。

1.  **`int` 型へのキャストによるオーバーフロー**:
    *   `f.UncompressedSize` は `uint32` 型であり、最大値は `2^32 - 1` です。32ビットシステムでは `int` 型の最大値は `2^31 - 1` (約2GB) であるため、`f.UncompressedSize` が `2^31 - 1` を超える場合、`int(f.UncompressedSize)` は負の値になったり、誤った値になったりする可能性があります。
    *   同様に、`f.UncompressedSize64` は `uint64` 型であり、非常に大きな値を持ち得ます。これを `int` にキャストすると、ほとんどの場合でオーバーフローが発生し、正しいサイズが取得できません。

2.  **`1<<32-1` の表現**:
    *   `1<<32-1` は `0xFFFFFFFF` を表すビット演算ですが、Go言語では `1<<32` は `int` 型の範囲を超えるため、コンパイルエラーになるか、予期せぬ結果を生む可能性があります(Goの仕様では、シフト演算の結果は左オペランドの型に依存し、オーバーフローは許可されません)。`uint32max` のような定数を使用する方が、より安全で意図が明確です。

3.  **比較の一貫性**:
    *   `b.Len()` は `int` を返します。`size` が `int` であれば問題ありませんが、ZIP64対応で `uint64` の値が使われる可能性があるため、比較対象の型を統一する必要があります。

新しいコードでは、これらの問題を解決するために以下の変更が行われました。

1.  **`size` 変数を `uint64` で初期化**:
    *   `size := uint64(f.UncompressedSize)` とすることで、`f.UncompressedSize` の値が `uint32` の最大値であっても、正確に `uint64` 型として `size` に格納されます。これにより、32ビットシステムでのオーバーフローが回避されます。

2.  **`uint32max` の使用**:
    *   `if size == uint32max` とすることで、ZIP64のマーカー値 (`0xFFFFFFFF`) との比較がより明確かつ安全に行われます。`uint32max` はおそらく `math.MaxUint32` のような定数として定義されており、`uint32` の最大値を正確に表します。

3.  **`f.UncompressedSize64` の直接使用**:
    *   `size = f.UncompressedSize64` とすることで、`uint64` 型である `f.UncompressedSize64` の値をそのまま `uint64` 型の `size` に代入します。これにより、不必要な `int` へのキャストによる情報損失がなくなります。

4.  **`b.Len()` の `uint64` へのキャスト**:
    *   `if g := uint64(b.Len()); g != size` とすることで、`b.Len()` が返す `int` 値を `uint64` に変換してから `size` (これも `uint64`) と比較します。これにより、比較される両方の値が同じ型になり、正確な比較が可能になります。

これらの変更により、`archive/zip` パッケージは32ビットシステム上でもZIP64形式の大容量ファイルを正しく読み込み、そのサイズを正確に検証できるようになりました。

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

変更は `src/pkg/archive/zip/reader_test.go` ファイルの `readTestFile` 関数内、374行目から377行目にかけての4行です。

```diff
--- a/src/pkg/archive/zip/reader_test.go
+++ b/src/pkg/archive/zip/reader_test.go
@@ -371,11 +371,11 @@ func readTestFile(t *testing.T, zt ZipTest, ft ZipTestFile, f *File) {
 	}\n 	r.Close()\n \n-\tsize := int(f.UncompressedSize)\n-\tif size == 1<<32-1 {\n-\t\tsize = int(f.UncompressedSize64)\n+\tsize := uint64(f.UncompressedSize)\n+\tif size == uint32max {\n+\t\tsize = f.UncompressedSize64\n \t}\n-\tif g := b.Len(); g != size {\n+\tif g := uint64(b.Len()); g != size {\n \t\tt.Errorf("%v: read %v bytes but f.UncompressedSize == %v", f.Name, g, size)\n \t}\n \n```

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

変更された行は、ZIPエントリの非圧縮サイズを決定し、実際に読み込まれたバイト数と比較するロジックです。

**変更前:**

```go
	size := int(f.UncompressedSize) // f.UncompressedSize (uint32) を int にキャスト
	if size == 1<<32-1 {            // size が 0xFFFFFFFF と等しいかチェック
		size = int(f.UncompressedSize64) // f.UncompressedSize64 (uint64) を int にキャスト
	}
	if g := b.Len(); g != size { // b.Len() (int) と size (int) を比較
		t.Errorf("%v: read %v bytes but f.UncompressedSize == %v", f.Name, g, size)
	}
  • f.UncompressedSize はZIPヘッダの32ビット非圧縮サイズフィールドに対応します。
  • f.UncompressedSize64 はZIP64拡張ヘッダの64ビット非圧縮サイズフィールドに対応します。
  • b.Len() は、テスト中に実際に読み込まれたバイト数を返します。

このコードの問題点は、int 型のサイズがシステムに依存することです。32ビットシステムでは int が32ビット幅であるため、uint32uint64 の値を int にキャストすると、値が 2^31-1 (約2GB) を超えた場合にオーバーフローが発生し、正しいサイズが取得できませんでした。また、1<<32-1 のような表現は、int の範囲を超えるため問題を引き起こす可能性がありました。

変更後:

	size := uint64(f.UncompressedSize) // f.UncompressedSize (uint32) を uint64 にキャスト
	if size == uint32max {             // size が uint32max (0xFFFFFFFF) と等しいかチェック
		size = f.UncompressedSize64    // f.UncompressedSize64 (uint64) をそのまま代入
	}
	if g := uint64(b.Len()); g != size { // b.Len() (int) を uint64 にキャストし、size (uint64) と比較
		t.Errorf("%v: read %v bytes but f.UncompressedSize == %v", f.Name, g, size)
	}
  • size 変数を最初から uint64 型として宣言し、f.UncompressedSizeuint64 にキャストして代入することで、値のオーバーフローを防ぎます。
  • 1<<32-1 の代わりに uint32max という定数を使用することで、コードの意図が明確になり、より安全な比較が可能になります。uint32maxmath.MaxUint32 と同義であると推測されます。
  • f.UncompressedSize64size に代入する際に int() へのキャストを削除し、uint64 の値をそのまま保持するようにしました。
  • b.Len() の戻り値も uint64 にキャストしてから size と比較することで、両方のオペランドが同じ型になり、正確なサイズ比較が保証されます。

これらの変更により、ZIP64形式でエンコードされた大容量ファイルであっても、その非圧縮サイズが正確に取得され、テストで正しく検証されるようになりました。特に32ビット環境でのビルドと実行において、この修正は重要です。

関連リンク

参考にした情報源リンク

  • Go言語のコミット履歴とコードレビューシステム (Gerrit): https://go.googlesource.com/go/+/c4b279ba5ac2226a36434a6d715f2d7713e987e1
  • Go言語の型変換に関する公式ドキュメントやチュートリアル (Go言語の公式ウェブサイト)
  • 32ビット/64ビットシステムにおける整数型の挙動に関する一般的なプログラミング知識