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

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

コミット

commit bfcd2d1e800d78a9da9b9ab24f624c4621875ae3
Author: Andrew Gerrand <adg@golang.org>
Date:   Mon Apr 8 15:38:06 2013 +1000

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

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

元コミット内容

archive/zip: handle trailing data after the end of directory header

The spec doesn't explicitly say that trailing data is okay, but a lot
of people do this and most unzippers will handle it just fine. In any
case, this makes the package more useful, and led me to make the
directory parsing code marginally more robust.

Fixes #5228.

R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/8504044

変更の背景

このコミットは、Go言語のarchive/zipパッケージが、ZIPファイルの「End of Central Directory Record (EOCD)」ヘッダの後に続く余分なデータ(trailing data)を適切に処理できるようにするためのものです。ZIPファイル形式の仕様(PKWARE社の.ZIP File Format Specificationなど)では、EOCDの後にデータが存在することについて明示的に言及していません。しかし、現実世界では多くのツールやシステムが、ZIPファイルの末尾にデジタル署名、コメント、あるいは単なるパディングなどの目的で追加データを付加することがあります。

このような非標準的な、しかし広く行われている慣習に対応できない場合、Goのarchive/zipパッケージは、これらのZIPファイルを不正な形式として扱い、読み込みに失敗する可能性がありました。これは、ユーザーが作成したZIPファイルや、他のツールで生成されたZIPファイルをGoアプリケーションで処理しようとした際に互換性の問題を引き起こします。

この変更は、具体的にはGo issue #5228を修正するために行われました。この問題は、EOCDの後にデータが存在するZIPファイルをarchive/zipパッケージが正しく読み込めないというバグ報告でした。この修正により、パッケージの堅牢性が向上し、より多くの種類のZIPファイルを問題なく扱えるようになります。

前提知識の解説

ZIPファイル形式の基本構造

ZIPファイルは、複数のファイルやディレクトリを単一のアーカイブにまとめるための一般的な形式です。その構造は、主に以下の要素で構成されます。

  1. ローカルファイルヘッダ (Local File Header): 各ファイルのエントリの先頭に位置し、ファイル名、圧縮方法、圧縮サイズ、非圧縮サイズなどのメタデータを含みます。
  2. ファイルデータ (File Data): 圧縮された実際のファイルデータです。
  3. データ記述子 (Data Descriptor): オプションで、ファイルデータの後ろに配置され、CRC-32チェックサム、圧縮サイズ、非圧縮サイズなどの情報を含みます。これは、ローカルファイルヘッダでこれらの情報が不明な場合(例:ストリーミング圧縮)に使用されます。
  4. セントラルディレクトリファイルヘッダ (Central Directory File Header): ZIPファイル内のすべてのファイルエントリに関する情報(ファイル名、圧縮方法、ファイルサイズ、ローカルファイルヘッダへのオフセットなど)を集中管理するディレクトリです。各ファイルエントリに対して1つ存在します。
  5. セントラルディレクトリの終わりレコード (End of Central Directory Record, EOCD): ZIPファイルの末尾に位置し、セントラルディレクトリの開始オフセット、セントラルディレクトリのサイズ、ZIPファイルコメントの長さなどの重要な情報を含みます。ZIPリーダーは通常、このEOCDレコードをファイルの末尾から逆方向に検索することで、セントラルディレクトリの場所を特定し、そこからファイル構造全体を解析します。

EOCDとZIPファイルの読み込み

ZIPファイルを読み込む際、最も重要なステップの一つはEOCDレコードを見つけることです。EOCDは常にZIPファイルの末尾に存在し、特定のシグネチャ(0x06054b50、リトルエンディアンでPK\x05\x06)で始まります。ZIPリーダーは通常、ファイルの末尾からこのシグネチャを逆方向にスキャンし、EOCDを見つけます。EOCDが見つかると、そこに含まれる情報(特にセントラルディレクトリのオフセット)を使って、ファイル内のすべてのエントリを効率的に読み込むことができます。

トレーリングデータ (Trailing Data) の問題

ZIPファイル形式の仕様では、EOCDレコードがファイルの最後の構造体であると暗黙的に想定されています。しかし、前述の通り、一部のツールはEOCDの後に任意のデータを追加することがあります。この「トレーリングデータ」が存在する場合、単純にファイルの末尾からEOCDシグネチャを検索するだけでは問題が発生する可能性があります。

従来のZIPリーダーは、EOCDシグネチャを見つけた後、そのシグネチャからEOCDレコードの全長(コメント長を含む)を計算し、その計算された長さがファイルの残りの部分と完全に一致することを期待していました。もしトレーリングデータが存在すると、計算されたEOCDの長さがファイルの残りの部分よりも短くなり、リーダーは「ファイルが破損している」と判断してしまうことがありました。

このコミットの目的は、この厳密すぎるチェックを緩和し、EOCDの後に余分なデータがあってもZIPファイルを正しく解析できるようにすることです。

技術的詳細

このコミットの技術的な核心は、archive/zipパッケージがEOCDレコードを検索し、その有効性を検証する方法の変更にあります。

ZIPファイルのEOCDレコードは、以下の構造を持っています(簡略化):

[EOCDシグネチャ (4 bytes)]
[ディスク番号 (2 bytes)]
[セントラルディレクトリ開始ディスク番号 (2 bytes)]
[このディスク上のセントラルディレクトリのエントリ数 (2 bytes)]
[セントラルディレクトリの総エントリ数 (2 bytes)]
[セントラルディレクトリのサイズ (4 bytes)]
[セントラルディレクトリの開始オフセット (4 bytes)]
[ZIPファイルコメントの長さ (2 bytes)]
[ZIPファイルコメント (可変長)]

EOCDの固定長部分は22バイトです。これにZIPファイルコメントの長さが加わります。

archive/zipパッケージでは、findSignatureInBlock関数が、与えられたバイトスライス内でEOCDシグネチャ(PK\x05\x06)を検索します。この関数は、シグネチャが見つかった場合、そのシグネチャがEOCDレコードの開始点であり、かつそのEOCDレコードがバイトスライスの末尾で終わることを確認していました。

具体的には、以前のコードでは以下の条件でEOCDの有効性をチェックしていました。

if n+directoryEndLen+i == len(b) {
    return i
}

ここで、

  • n はZIPファイルコメントの長さ。
  • directoryEndLen はEOCDの固定長部分(22バイト)。
  • i はバイトスライスb内でのEOCDシグネチャの開始インデックス。
  • len(b) は検索対象のバイトスライスの全長。

この条件は、「EOCDシグネチャの開始位置 (i) からEOCDレコードの全長 (n + directoryEndLen) を足したものが、バイトスライスの厳密な末尾 (len(b)) と一致しなければならない」ということを意味していました。つまり、EOCDの後に1バイトでもデータがあると、この条件は偽となり、EOCDが見つからないと判断されていました。

このコミットでは、この条件を以下のように変更しました。

if n+directoryEndLen+i <= len(b) {
    return i
}

この変更により、EOCDシグネチャの開始位置からEOCDレコードの全長を足したものが、バイトスライスの全長以下であれば良い、と解釈されるようになりました。これにより、EOCDの後に余分なデータが存在しても、findSignatureInBlock関数はEOCDを正しく識別できるようになります。

また、readDirectoryEnd関数には、directoryOffset(セントラルディレクトリの開始オフセット)がファイルの有効な範囲内にあることを確認する追加のチェックが導入されました。これは、EOCDの解析がより堅牢になった結果、不正なオフセットが読み込まれる可能性を考慮した防御的なプログラミングです。

if o := int64(d.directoryOffset); o < 0 || o >= size {
    return nil, ErrFormat
}

このチェックは、directoryOffsetが負の値であるか、またはファイルサイズを超えている場合にエラーを返すことで、不正なZIPファイルや破損したZIPファイルに対する耐性を高めます。

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

src/pkg/archive/zip/reader.go

--- a/src/pkg/archive/zip/reader.go
+++ b/src/pkg/archive/zip/reader.go
@@ -353,6 +353,11 @@ func readDirectoryEnd(r io.ReaderAt, size int64) (dir *directoryEnd, err error)\n 	if err != nil {\n 		return nil, err\n 	}\n+\n+\t// Make sure directoryOffset points to somewhere in our file.\n+\tif o := int64(d.directoryOffset); o < 0 || o >= size {\n+\t\treturn nil, ErrFormat\n+\t}\n 	return d, nil\n }\
 \n@@ -407,7 +412,7 @@ func findSignatureInBlock(b []byte) int {\n 	if b[i] == 'P' && b[i+1] == 'K' && b[i+2] == 0x05 && b[i+3] == 0x06 {\n 		// n is length of comment\n 		n := int(b[i+directoryEndLen-2]) | int(b[i+directoryEndLen-1])<<8\n-\t\tif n+directoryEndLen+i == len(b) {\n+\t\tif n+directoryEndLen+i <= len(b) {\n 			return i\n 		}\n 	}\

src/pkg/archive/zip/reader_test.go

--- a/src/pkg/archive/zip/reader_test.go
+++ b/src/pkg/archive/zip/reader_test.go
@@ -63,6 +63,24 @@ var tests = []ZipTest{\n 			},\n 		},\n 	},\n+\t{\n+\t\tName:    "test-trailing-junk.zip",\n+\t\tComment: "This is a zipfile comment.",\n+\t\tFile: []ZipTestFile{\n+\t\t\t{\n+\t\t\t\tName:    "test.txt",\n+\t\t\t\tContent: []byte("This is a test text file.\\n"),\n+\t\t\t\tMtime:   "09-05-10 12:12:02",\n+\t\t\t\tMode:    0644,\n+\t\t\t},\n+\t\t\t{\n+\t\t\t\tName:  "gophercolor16x16.png",\n+\t\t\t\tFile:  "gophercolor16x16.png",\n+\t\t\t\tMtime: "09-05-10 15:52:58",\n+\t\t\t\tMode:  0644,\n+\t\t\t},\n+\t\t},\n+\t},\
 \t{\n \t\tName:   "r.zip",\n \t\tSource: returnRecursiveZip,\n@@ -262,7 +280,7 @@ func readTestZip(t *testing.T, zt ZipTest) {\n \t\t}\n \t}\n \tif err != zt.Error {\n-\t\tt.Errorf("error=%v, want %v", err, zt.Error)\n+\t\tt.Errorf("%s: error=%v, want %v", zt.Name, err, zt.Error)\n \t\treturn\n \t}\

src/pkg/archive/zip/testdata/test-trailing-junk.zip

このコミットでは、EOCDの後にトレーリングデータを含む新しいテスト用ZIPファイルが追加されました。これはバイナリファイルであり、変更差分にはその内容が直接表示されませんが、ファイルが追加されたことが示されています。

コアとなるコードの解説

src/pkg/archive/zip/reader.go

  1. func readDirectoryEnd(r io.ReaderAt, size int64) (dir *directoryEnd, err error) 内の変更:

    • 追加されたコード:
      // Make sure directoryOffset points to somewhere in our file.
      if o := int64(d.directoryOffset); o < 0 || o >= size {
          return nil, ErrFormat
      }
      
    • このコードは、EOCDから読み取られたセントラルディレクトリの開始オフセット(d.directoryOffset)が、ファイルの有効な範囲内にあることを検証します。オフセットが負の値であるか、またはファイル全体のサイズ(size)以上である場合、それは不正なオフセットであり、ErrFormat(ZIPファイル形式エラー)を返します。これは、ZIPファイルの破損や、EOCDが誤って解釈された場合に備えた堅牢性向上策です。
  2. func findSignatureInBlock(b []byte) int 内の変更:

    • 変更前の行: if n+directoryEndLen+i == len(b) {
    • 変更後の行: if n+directoryEndLen+i <= len(b) {
    • この変更が、トレーリングデータを許容するための最も重要な部分です。
      • n: ZIPファイルコメントの長さ。
      • directoryEndLen: EOCDレコードの固定長(22バイト)。
      • i: b(検索対象のバイトスライス)内でのEOCDシグネチャの開始インデックス。
      • len(b): bの全長。
    • 変更前は、EOCDレコードがバイトスライスの厳密な末尾で終わることを要求していました。つまり、iからEOCDの全長を足した値がlen(b)と完全に一致しなければなりませんでした。
    • 変更後は、EOCDレコードがバイトスライスの末尾以前で終わることを許容します。つまり、iからEOCDの全長を足した値がlen(b)以下であれば良い、と解釈されます。これにより、EOCDの後に余分なデータ(トレーリングデータ)が存在しても、findSignatureInBlock関数はEOCDを正しく見つけることができるようになります。

src/pkg/archive/zip/reader_test.go

  1. 新しいZipTestケースの追加:

    • Name: "test-trailing-junk.zip"という新しいテストケースが追加されました。
    • このテストケースは、EOCDの後に余分なデータを含むZIPファイルを読み込むシナリオをシミュレートします。
    • このZIPファイルには、test.txtgophercolor16x16.pngという2つのファイルが含まれています。
    • このテストの目的は、archive/zipパッケージがトレーリングデータが存在するZIPファイルをエラーなく正常に読み込めることを検証することです。
  2. readTestZip関数内のエラーメッセージの改善:

    • 変更前の行: t.Errorf("error=%v, want %v", err, zt.Error)
    • 変更後の行: t.Errorf("%s: error=%v, want %v", zt.Name, err, zt.Error)
    • エラーメッセージにテストケースの名前(zt.Name)が追加されました。これにより、テストが失敗した場合に、どのZIPファイルテストでエラーが発生したのかがより明確になり、デバッグが容易になります。

関連リンク

参考にした情報源リンク

  • PKWARE, Inc. .ZIP File Format Specification: https://pkware.com/documents/casestudies/APPNOTE.TXT (ZIPファイル形式の公式仕様)
  • Go archive/zipパッケージのドキュメント: https://pkg.go.dev/archive/zip
  • Stack Overflowや技術ブログ記事("ZIP file trailing data", "End of Central Directory Record" などのキーワードで検索)
    • (具体的なURLは省略しますが、これらのキーワードで検索することで、ZIPファイルの構造やトレーリングデータに関する詳細な解説が見つかります。)