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

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

このコミットは、Go言語の標準ライブラリ archive/zip パッケージのテストデータに関する変更です。具体的には、テストで使用されていた r.zip というファイルがディスク上から削除され、その内容が reader_test.go 内のGoのソースコードに直接埋め込まれるように変更されました。

コミット

commit c959ebe4d85c24a17b74120fa9b6abe27427d9ed
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Wed Mar 14 14:41:06 2012 -0700

    archive/zip: move r.zip off disk, into reader_test.go
    
    Makes certain virus scanners happier.
    
    R=golang-dev, rsc, adg
    CC=golang-dev
    https://golang.org/cl/5823053

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

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

元コミット内容

archive/zip: move r.zip off disk, into reader_test.go Makes certain virus scanners happier.

このコミットの目的は、r.zip ファイルをディスク上から reader_test.go 内に移動させることです。これにより、特定のウイルススキャナーが誤検知する問題を解消します。

変更の背景

この変更の主な背景は、Go言語のテストスイートを実行する際に、一部のウイルススキャナーが r.zip ファイルをマルウェアとして誤検知し、テストの実行を妨げたり、警告を発したりする問題が発生していたためです。

r.zip は、再帰的に自身を含むような特殊な構造を持つZIPファイルであり、ZIPファイルのパース処理の堅牢性をテストするために使用されていました。このような特殊なファイル構造は、悪意のあるソフトウェアがファイルシステムを混乱させるために利用されることがあるため、一部のウイルススキャナーが過敏に反応することがあります。

開発環境やCI/CD環境でウイルススキャナーが誤検知を起こすと、開発者は不必要な警告に悩まされたり、ビルドプロセスが中断されたりする可能性があります。これを避けるため、テストデータをディスク上のファイルとしてではなく、Goのソースコード内に直接埋め込むことで、ファイルシステムのスキャン対象から外すという解決策が採用されました。これにより、ウイルススキャナーの誤検知を回避し、開発体験を向上させることが目的です。

前提知識の解説

Go言語の archive/zip パッケージ

archive/zip パッケージは、Go言語でZIPアーカイブの読み書きを行うための標準ライブラリです。このパッケージは、ZIPファイルの圧縮・解凍、ファイル情報の取得、ディレクトリ構造の操作など、ZIPアーカイブに関する基本的な機能を提供します。テストコードでは、このパッケージが正しく機能するかどうかを検証するために、様々な種類のZIPファイルがテストデータとして使用されます。

io.ReaderAt インターフェース

io.ReaderAt はGo言語の標準ライブラリ io パッケージで定義されているインターフェースです。これは、任意のオフセットからデータを読み込む機能を提供します。

type ReaderAt interface {
    ReadAt(p []byte, off int64) (n int, err error)
}

ReadAt メソッドは、off で指定されたオフセットから p の長さ分のデータを読み込み、p に書き込みます。ZIPファイルの読み込みでは、ファイル内の特定の位置にシークしてデータを読み込む必要があるため、io.ReaderAt は非常に重要な役割を果たします。このコミットでは、メモリ上のバイトスライスを bytes.NewReader を使って io.ReaderAt として扱うことで、ディスク上のファイルと同様にZIPファイルを読み込めるようにしています。

encoding/hex パッケージと16進数エンコード

encoding/hex パッケージは、バイナリデータを16進数文字列にエンコード/デコードするためのGo言語の標準ライブラリです。このコミットでは、r.zip のバイナリデータを16進数文字列としてGoのソースコード内に埋め込み、実行時に hex.DecodeString 関数を使って元のバイナリデータにデコードしています。

例えば、hex.DecodeString("48656c6c6f")[]byte("Hello") を返します。これにより、バイナリファイルを直接ソースコードに埋め込むことが可能になります。

再帰的なZIPファイル (r.zip)

r.zip は、それ自身の中に r.zip という名前のファイルを含む、再帰的な構造を持つZIPファイルです。このようなファイルは、ZIPパーサーが無限ループに陥ったり、リソースを過剰に消費したりしないことを確認するためのエッジケーステストとして非常に有用です。しかし、その特殊な構造ゆえに、一部のウイルススキャナーが異常なファイルとして検知してしまうことがあります。

技術的詳細

このコミットの技術的な核心は、ディスク上のバイナリファイル (r.zip) をGoのソースコード内のバイトスライスとして表現し、実行時にそれをデコードして利用する点にあります。

  1. バイナリデータの16進数エンコード: r.zip のバイナリデータは、16進数表現に変換され、Goの文字列リテラルとして rZipBytes 関数内に埋め込まれました。元の r.zip ファイルは440バイトのバイナリデータでしたが、これが16進数文字列として表現されています。

  2. 文字列の整形とデコード: rZipBytes 関数内では、埋め込まれた16進数文字列から不要な空白や行番号のようなメタデータが正規表現 (regexp.MustCompile) を使って除去されます。その後、encoding/hex.DecodeString 関数を用いて、整形された16進数文字列が元のバイナリデータ ([]byte) にデコードされます。

  3. io.ReaderAt としての提供: デコードされたバイトスライスは、bytes.NewReader 関数に渡され、io.ReaderAt インターフェースを満たす *bytes.Reader 型のインスタンスが生成されます。これにより、archive/zip パッケージのテストコードは、ディスク上のファイルから読み込む場合と同様に、メモリ上のデータからZIPファイルを読み込むことができるようになります。

  4. テストデータの参照変更: reader_test.go 内の tests 変数で定義されている r.zip のテストエントリが変更されました。以前は File: "r.zip" のようにファイルパスを参照していましたが、変更後は Content: rZipBytes(), のように、rZipBytes 関数が返すバイトスライスを直接参照するようになりました。

このアプローチにより、r.zip ファイルがファイルシステム上に存在する必要がなくなり、ウイルススキャナーがファイルシステムをスキャンする際に誤検知する可能性がなくなります。テストの実行はメモリ上で行われるため、パフォーマンスへの影響も最小限に抑えられます。

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

このコミットで変更された主要なファイルは以下の2つです。

  1. src/pkg/archive/zip/reader_test.go:

    • encoding/hexregexp パッケージがインポートに追加されました。
    • tests 変数内の r.zip のエントリが変更され、Source: returnRecursiveZip,Content: rZipBytes(), が追加されました。
    • rZipBytes() 関数が追加されました。この関数は、r.zip のバイナリデータを16進数文字列として埋め込み、それをデコードしてバイトスライスとして返します。
    • returnRecursiveZip() 関数が追加されました。この関数は rZipBytes() が返すバイトスライスを io.ReaderAt としてラップして返します。
  2. src/pkg/archive/zip/testdata/r.zip:

    • このファイルは完全に削除されました。

src/pkg/archive/zip/reader_test.go の変更差分

--- a/src/pkg/archive/zip/reader_test.go
+++ b/src/pkg/archive/zip/reader_test.go
@@ -7,10 +7,12 @@ package zip
 import (
 	"bytes"
 	"encoding/binary"
+	"encoding/hex"
 	"io"
 	"io/ioutil"
 	"os"
 	"path/filepath"
+	"regexp"
 	"testing"
 	"time"
 )
@@ -62,13 +64,14 @@ var tests = []ZipTest{
 		},
 	},
 	{
-		Name: "r.zip",
+		Name:   "r.zip",
+		Source: returnRecursiveZip,
 		File: []ZipTestFile{
 			{
-				Name:  "r/r.zip",
-				File:  "r.zip",
-				Mtime: "03-04-10 00:24:16",
-				Mode:  0666,
+				Name:    "r/r.zip",
+				Content: rZipBytes(),
+				Mtime:   "03-04-10 00:24:16",
+				Mode:    0666,
 			},
 		},
 	},
@@ -415,3 +418,49 @@ func returnCorruptNotStreamedZip() (r io.ReaderAt, size int64) {
 		// is what matters.
 	})\n}\n+\n+// rZipBytes returns the bytes of a recursive zip file, without
+// putting it on disk and triggering certain virus scanners.
+func rZipBytes() []byte {
+	s := `
+0000000 50 4b 03 04 14 00 00 00 08 00 08 03 64 3c f9 f4
+0000010 89 64 48 01 00 00 b8 01 00 00 07 00 00 00 72 2f
+0000020 72 2e 7a 69 70 00 25 00 da ff 50 4b 03 04 14 00
+0000030 00 00 08 00 08 03 64 3c f9 f4 89 64 48 01 00 00
+0000040 b8 01 00 00 07 00 00 00 72 2f 72 2e 7a 69 70 00
+0000050 2f 00 d0 ff 00 25 00 da ff 50 4b 03 04 14 00 00
+0000060 00 08 00 08 03 64 3c f9 f4 89 64 48 01 00 00 b8
+0000070 01 00 00 07 00 00 00 72 2f 72 2e 7a 69 70 00 2f
+0000080 00 d0 ff c2 54 8e 57 39 00 05 00 fa ff c2 54 8e
+0000090 57 39 00 05 00 fa ff 00 05 00 fa ff 00 14 00 eb
+00000a0 ff c2 54 8e 57 39 00 05 00 fa ff 00 05 00 fa ff
+00000b0 00 14 00 eb ff 42 88 21 c4 00 00 14 00 eb ff 42
+00000c0 88 21 c4 00 00 14 00 eb ff 42 88 21 c4 00 00 14
+00000d0 00 eb ff 42 88 21 c4 00 00 14 00 eb ff 42 88 21
+00000e0 c4 00 00 00 00 ff ff 00 00 00 ff ff 00 34 00 cb
+00000f0 ff 42 88 21 c4 00 00 00 00 ff ff 00 00 00 ff ff
+0000100 00 34 00 cb ff 42 e8 21 5e 0f 00 00 00 ff ff 0a
+0000110 f0 66 64 12 61 c0 15 dc e8 a0 48 bf 48 af 2a b3
+0000120 20 c0 9b 95 0d c4 67 04 42 53 06 06 06 40 00 06
+0000130 00 f9 ff 6d 01 00 00 00 00 42 e8 21 5e 0f 00 00
+0000140 00 ff ff 0a f0 66 64 12 61 c0 15 dc e8 a0 48 bf
+0000150 48 af 2a b3 20 c0 9b 95 0d c4 67 04 42 53 06 06
+0000160 06 40 00 06 00 f9 ff 6d 01 00 00 00 00 50 4b 01
+0000170 02 14 00 14 00 00 00 08 00 08 03 64 3c f9 f4 89
+0000180 64 48 01 00 00 b8 01 00 00 07 00 00 00 00 00 00
+0000190 00 00 00 00 00 00 00 00 00 00 00 72 2f 72 2e 7a
+00001a0 69 70 50 4b 05 06 00 00 00 00 01 00 01 00 35 00
+00001b0 00 00 6d 01 00 00 00 00`
+	s = regexp.MustCompile(`[0-9a-f]{7}`).ReplaceAllString(s, "")
+	s = regexp.MustCompile(`\s+`).ReplaceAllString(s, "")
+	b, err := hex.DecodeString(s)
+	if err != nil {
+		panic(err)
+	}
+	return b
+}
+
+func returnRecursiveZip() (r io.ReaderAt, size int64) {
+	b := rZipBytes()
+	return bytes.NewReader(b), int64(len(b))
+}

src/pkg/archive/zip/testdata/r.zip の変更差分

--- a/src/pkg/archive/zip/testdata/r.zip
+++ /dev/null
@@ -1 +0,0 @@
-Binary files a/src/pkg/archive/zip/testdata/r.zip and /dev/null differ

この差分は、r.zip ファイルが完全に削除されたことを示しています。

コアとなるコードの解説

rZipBytes() 関数

func rZipBytes() []byte {
	s := `
// ... 16進数データ ...
`
	s = regexp.MustCompile(`[0-9a-f]{7}`).ReplaceAllString(s, "") // 行番号のようなものを除去
	s = regexp.MustCompile(`\s+`).ReplaceAllString(s, "")         // 空白を除去
	b, err := hex.DecodeString(s)                                 // 16進数文字列をバイトスライスにデコード
	if err != nil {
		panic(err) // エラーが発生した場合はパニック
	}
	return b
}

この関数は、r.zip ファイルのバイナリ内容をGoのソースコード内に埋め込むための中心的な役割を担っています。

  1. s 変数には、r.zip のバイナリデータが16進数形式で記述された複数行の文字列リテラルが代入されています。この形式は、hexdump -C コマンドの出力に似ており、各行の先頭にはオフセット、その後に16進数バイト、そしてASCII表現が続くのが一般的です。
  2. regexp.MustCompile を使用して、まず [0-9a-f]{7} (7桁の16進数、おそらく行のオフセット部分) を空文字列に置換し、次に \s+ (1つ以上の空白文字) を空文字列に置換しています。これにより、元の16進数データのみが連続した文字列として抽出されます。
  3. hex.DecodeString(s) は、整形された16進数文字列を実際のバイナリバイトスライス ([]byte) に変換します。
  4. デコード中にエラーが発生した場合(例えば、無効な16進数文字が含まれていた場合)、panic を発生させます。これはテストコードであるため、このようなエラーは致命的と判断されます。
  5. 最終的に、r.zip のバイナリ内容を表すバイトスライスが返されます。

returnRecursiveZip() 関数

func returnRecursiveZip() (r io.ReaderAt, size int64) {
	b := rZipBytes() // r.zip のバイトスライスを取得
	return bytes.NewReader(b), int64(len(b)) // バイトスライスを io.ReaderAt としてラップして返す
}

この関数は、rZipBytes() が返すバイトスライスを io.ReaderAt インターフェースを満たすオブジェクトとして提供します。

  1. rZipBytes() を呼び出して、r.zip のバイナリデータを取得します。
  2. bytes.NewReader(b) は、与えられたバイトスライス b を読み込むための *bytes.Reader を作成します。*bytes.Readerio.ReaderAt インターフェースを実装しているため、ZIPリーダーがファイルのようにランダムアクセスでデータを読み込むことができます。
  3. int64(len(b)) は、ZIPファイルのサイズをバイト数で返します。これは io.ReaderAt インターフェースを使用する際に、ファイルの総サイズが必要となる場合があるためです。

これらの変更により、r.zip はディスク上のファイルではなく、Goのコンパイルされたバイナリの一部として扱われるようになり、ウイルススキャナーの誤検知を回避しつつ、テストの機能性を維持しています。

関連リンク

参考にした情報源リンク

  • Goの公式GitHubリポジトリ: https://github.com/golang/go
  • このコミットのChange-ID: https://golang.org/cl/5823053 (GoのコードレビューシステムGerritのリンク)
  • ZIPファイルフォーマットの仕様 (PKWARE): https://pkware.com/docs/casestudies/APPNOTE.TXT (ZIPファイルの構造に関する詳細な情報)
  • ウイルススキャナーの誤検知に関する一般的な情報 (例: テストデータや特殊なファイル形式): 一般的なセキュリティブログやフォーラムで議論されることがあります。