[インデックス 17060] ファイルの概要
このコミットは、Go言語の標準ライブラリ archive/zip パッケージにおける圧縮・解凍メカニズムを、ハードコードされた switch 文から、ユーザーが拡張可能な登録ベースのシステムへと変更するものです。これにより、Goの zip パッケージは、標準でサポートされていないカスタム圧縮方式にも対応できるようになり、より柔軟な利用が可能になります。
コミット
commit 911534592559239185200f73b214b9b11a62b848
Author: Dustin Sallings <dsallings@gmail.com>
Date: Tue Aug 6 12:03:38 2013 -0700
archive/zip: allow user-extensible compression methods
This change replaces the hard-coded switch on compression method
in zipfile reader and writer with a map into which users can
register compressors and decompressors in their init()s.
R=golang-dev, bradfitz, rsc
CC=golang-dev
https://golang.org/cl/12421043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/911534592559239185200f73b214b9b11a62b848
元コミット内容
このコミットの目的は、archive/zip パッケージがZIPファイルの読み書きを行う際に使用する圧縮方式の選択ロジックを改善することです。以前は、サポートされる圧縮方式(例: 無圧縮の Store、Deflate圧縮の Deflate)が reader.go と writer.go 内の switch 文で直接処理されていました。このアプローチでは、新しい圧縮方式を追加する際に、パッケージのソースコード自体を変更する必要がありました。
この変更により、switch 文は、ユーザーが init() 関数内でカスタムの圧縮器(compressor)と解凍器(decompressor)を登録できるマップベースのメカニズムに置き換えられます。これにより、パッケージの利用者が独自の圧縮方式を archive/zip パッケージに統合できるようになります。
変更の背景
ZIPファイルフォーマットは、様々な圧縮アルゴリズムをサポートするように設計されています。しかし、Goの archive/zip パッケージは、当初は最も一般的ないくつかの圧縮方式(主に Store と Deflate)のみを内部的にサポートしていました。
このハードコードされたアプローチにはいくつかの課題がありました。
- 拡張性の欠如: 新しい圧縮方式(例: Bzip2, LZMA, Zstandardなど)が普及したり、特定のアプリケーションで必要になったりした場合、
archive/zipパッケージ自体を修正し、Goのリリースサイクルを待つ必要がありました。これは、迅速な対応や特定のユースケースへの適応を妨げます。 - コードの重複と複雑性:
reader.goとwriter.goの両方で同様のswitch文が存在し、圧縮方式の追加ごとに両方のファイルを変更する必要がありました。 - 柔軟性の制限: ユーザーが独自の、あるいはニッチな圧縮アルゴリズムを使用したい場合、標準ライブラリの制約により、別のZIPライブラリを使用するか、
archive/zipパッケージをフォークするしかありませんでした。
このコミットは、これらの課題を解決し、archive/zip パッケージの柔軟性と拡張性を向上させることを目的としています。
前提知識の解説
このコミットを理解するためには、以下の概念が役立ちます。
- ZIPファイルフォーマット: ZIPファイルは、複数のファイルを一つのアーカイブにまとめるための一般的なファイルフォーマットです。各ファイルは個別に圧縮され、アーカイブ内に保存されます。ZIPフォーマットは、各エントリの圧縮方式を示す「圧縮メソッドID」を定義しています。
- 圧縮方式 (Compression Method): ZIPファイル内のデータがどのように圧縮されているかを示す識別子です。例えば、
0は無圧縮(Store)、8はDeflate圧縮を表します。 io.Readerとio.Writer: Go言語の標準ライブラリioパッケージで定義されているインターフェースです。io.Readerはデータを読み出すためのメソッドReadを持ち、io.Writerはデータを書き込むためのメソッドWriteを持ちます。これらはGoにおけるストリーム処理の基本です。io.ReadCloserとio.WriteCloser: それぞれio.Readerとio.WriterにClose()メソッドを追加したインターフェースです。リソースの解放が必要なストリーム(例: ファイル、ネットワーク接続、圧縮ストリーム)でよく使用されます。compress/flateパッケージ: Goの標準ライブラリで、Deflate圧縮アルゴリズムを実装しています。ZIPファイルのDeflate圧縮は、このパッケージによって処理されます。sync.RWMutex: Goのsyncパッケージで提供される読み書きロックです。複数のゴルーチンが共有リソース(この場合は圧縮器/解凍器のマップ)にアクセスする際に、データの競合を防ぐために使用されます。読み取り操作は並行して行えますが、書き込み操作は排他的に行われます。init()関数: Go言語の特殊な関数で、パッケージがインポートされた際に自動的に実行されます。通常、パッケージレベルの初期化処理(例: グローバル変数の設定、リソースの初期化、このコミットのように登録処理)に使用されます。
技術的詳細
このコミットの核心は、archive/zip パッケージが圧縮・解凍ロジックを動的に管理するための新しいメカニズムを導入した点にあります。
新しい register.go ファイル
このコミットで新しく追加された src/pkg/archive/zip/register.go ファイルは、以下の主要な要素を定義しています。
type Compressor func(io.Writer) (io.WriteCloser, error): この型は、圧縮器の関数シグネチャを定義します。io.Writerを受け取り、圧縮されたデータを書き込むためのio.WriteCloserを返します。エラーも返す可能性があります。type Decompressor func(io.Reader) io.ReadCloser: この型は、解凍器の関数シグネチャを定義します。io.Readerを受け取り、解凍されたデータを読み出すためのio.ReadCloserを返します。compressorsとdecompressorsマップ:map[uint16]Compressorとmap[uint16]Decompressor型のグローバルマップです。uint16はZIPファイルフォーマットで定義される圧縮メソッドIDを表します。これらのマップは、各圧縮メソッドIDに対応するCompressorまたはDecompressor関数を格納します。 初期状態では、Store(無圧縮) とDeflate(Deflate圧縮) の標準的な実装が登録されています。StoreのCompressor:func(w io.Writer) (io.WriteCloser, error) { return &nopCloser{w}, nil }これは、入力io.Writerをそのままio.WriteCloserとしてラップするnopCloserを返します。つまり、データは圧縮されずに直接書き込まれます。DeflateのCompressor:func(w io.Writer) (io.WriteCloser, error) { return flate.NewWriter(w, 5) }これはcompress/flateパッケージのNewWriterを使用して、Deflate圧縮を行うio.WriteCloserを返します。5は圧縮レベルを示します。StoreのDecompressor:ioutil.NopCloserこれはio.Readerをそのままio.ReadCloserとしてラップする関数です。データは解凍されずに直接読み込まれます。DeflateのDecompressor:flate.NewReaderこれはcompress/flateパッケージのNewReaderを使用して、Deflate解凍を行うio.ReadCloserを返します。
mu sync.RWMutex:compressorsとdecompressorsマップへの並行アクセスを安全に管理するための読み書きミューテックスです。マップの読み取り時には共有ロック(RLock)を、書き込み時には排他ロック(Lock)を使用します。RegisterDecompressor(method uint16, d Decompressor): 外部からカスタムの解凍器を登録するための公開関数です。指定されたmethodIDが既に登録されている場合はパニック(panic)を発生させます。これは、同じメソッドIDに対して複数の解凍器が登録されるのを防ぐためです。RegisterCompressor(method uint16, comp Compressor): 外部からカスタムの圧縮器を登録するための公開関数です。RegisterDecompressorと同様に、既に登録されている場合はパニックを発生させます。compressor(method uint16) Compressorとdecompressor(method uint16) Decompressor: 内部ヘルパー関数で、指定されたメソッドIDに対応する圧縮器または解凍器をマップから安全に取得します。これらの関数は読み取りロック(RLock)を使用するため、複数のゴルーチンから同時に呼び出されても安全です。
reader.go と writer.go の変更
reader.go と writer.go では、以前のハードコードされた switch 文が削除され、新しく導入された compressor および decompressor ヘルパー関数を介して、登録された圧縮器/解凍器を取得するロジックに置き換えられました。
-
reader.go:File.Open()メソッド内で、f.Methodに応じて解凍器を選択する部分が変更されました。// 変更前 switch f.Method { case Store: // (no compression) rc = ioutil.NopCloser(r) case Deflate: rc = flate.NewReader(r) default: err = ErrAlgorithm return } // 変更後 dcomp := decompressor(f.Method) // 登録された解凍器を取得 if dcomp == nil { err = ErrAlgorithm // 登録されていない場合はエラー return } rc = dcomp(r) // 取得した解凍器を使用これにより、
archive/zipパッケージは、未知の圧縮方式に遭遇した場合でも、ユーザーがその方式の解凍器を事前に登録していれば、適切に処理できるようになります。 -
writer.go:Writer.CreateHeader()メソッド内で、fh.Methodに応じて圧縮器を選択する部分が変更されました。// 変更前 switch fh.Method { case Store: fw.comp = nopCloser{fw.compCount} case Deflate: var err error fw.comp, err = flate.NewWriter(fw.compCount, 5) if err != nil { return nil, err } default: return nil, ErrAlgorithm } // 変更後 comp := compressor(fh.Method) // 登録された圧縮器を取得 if comp == nil { return nil, ErrAlgorithm // 登録されていない場合はエラー } var err error fw.comp, err = comp(fw.compCount) // 取得した圧縮器を使用 if err != nil { return nil, err }これにより、ZIPアーカイブを作成する際に、ユーザーが定義したカスタム圧縮方式を使用できるようになります。
ユーザーによる拡張の例
この変更により、ユーザーは以下のようにカスタム圧縮方式を登録できます(擬似コード)。
package mycompressor
import (
"archive/zip"
"io"
"fmt"
// 独自の圧縮アルゴリズムパッケージ
"github.com/example/mycustomalgo"
)
const MyCustomMethod uint16 = 99 // 未使用のメソッドIDを選択
func init() {
// カスタム圧縮器の登録
zip.RegisterCompressor(MyCustomMethod, func(w io.Writer) (io.WriteCloser, error) {
fmt.Println("Using MyCustomMethod compressor")
return mycustomalgo.NewCompressor(w), nil // 独自の圧縮器を返す
})
// カスタム解凍器の登録
zip.RegisterDecompressor(MyCustomMethod, func(r io.Reader) io.ReadCloser {
fmt.Println("Using MyCustomMethod decompressor")
return mycustomalgo.NewDecompressor(r) // 独自の解凍器を返す
})
}
この mycompressor パッケージをアプリケーションでインポートするだけで、archive/zip パッケージは MyCustomMethod を認識し、その圧縮・解凍ロジックを使用できるようになります。
コアとなるコードの変更箇所
src/pkg/archive/zip/reader.go
--- a/src/pkg/archive/zip/reader.go
+++ b/src/pkg/archive/zip/reader.go
@@ -6,13 +6,11 @@ package zip
import (
"bufio"
- "compress/flate"
"encoding/binary"
"errors"
"hash"
"hash/crc32"
"io"
- "io/ioutil"
"os"
)
@@ -125,15 +123,12 @@ func (f *File) Open() (rc io.ReadCloser, err error) {
}\n size := int64(f.CompressedSize64)
r := io.NewSectionReader(f.zipr, f.headerOffset+bodyOffset, size)
-\tswitch f.Method {\n-\tcase Store: // (no compression)\n-\t\trc = ioutil.NopCloser(r)\n-\tcase Deflate:\n-\t\trc = flate.NewReader(r)\n-\tdefault:\n+\tdcomp := decompressor(f.Method)\n+\tif dcomp == nil {\n \t\terr = ErrAlgorithm
\t\treturn
\t}
+\trc = dcomp(r)
\tvar desr io.Reader
\tif f.hasDataDescriptor() {
\t\tdesr = io.NewSectionReader(f.zipr, f.headerOffset+bodyOffset+size, dataDescriptorLen)
src/pkg/archive/zip/register.go (新規ファイル)
--- /dev/null
+++ b/src/pkg/archive/zip/register.go
@@ -0,0 +1,71 @@
+// Copyright 2010 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package zip
+
+import (
+ "compress/flate"
+ "io"
+ "io/ioutil"
+ "sync"
+)
+
+// A Compressor returns a compressing writer, writing to the
+// provided writer. On Close, any pending data should be flushed.
+type Compressor func(io.Writer) (io.WriteCloser, error)
+
+// Decompressor is a function that wraps a Reader with a decompressing Reader.
+// The decompressed ReadCloser is returned to callers who open files from
+// within the archive. These callers are responsible for closing this reader
+// when they're finished reading.
+type Decompressor func(io.Reader) io.ReadCloser
+
+var (
+ mu sync.RWMutex // guards compressor and decompressor maps
+
+ compressors = map[uint16]Compressor{
+ Store: func(w io.Writer) (io.WriteCloser, error) { return &nopCloser{w}, nil },
+ Deflate: func(w io.Writer) (io.WriteCloser, error) { return flate.NewWriter(w, 5) },
+ }
+
+ decompressors = map[uint16]Decompressor{
+ Store: ioutil.NopCloser,
+ Deflate: flate.NewReader,
+ }
+)
+
+// RegisterDecompressor allows custom decompressors for a specified method ID.
+func RegisterDecompressor(method uint16, d Decompressor) {
+ mu.Lock()
+ defer mu.Unlock()
+
+ if _, ok := decompressors[method]; ok {
+ panic("decompressor already registered")
+ }
+ decompressors[method] = d
+}
+
+// RegisterCompressor registers custom compressors for a specified method ID.
+// The common methods Store and Deflate are built in.
+func RegisterCompressor(method uint16, comp Compressor) {
+ mu.Lock()
+ defer mu.Unlock()
+
+ if _, ok := compressors[method]; ok {
+ panic("compressor already registered")
+ }
+ compressors[method] = comp
+}
+
+func compressor(method uint16) Compressor {
+ mu.RLock()
+ defer mu.RUnlock()
+ return compressors[method]
+}
+
+func decompressor(method uint16) Decompressor {
+ mu.RLock()
+ defer mu.RUnlock()
+ return decompressors[method]
+}
src/pkg/archive/zip/writer.go
--- a/src/pkg/archive/zip/writer.go
+++ b/src/pkg/archive/zip/writer.go
@@ -6,7 +6,6 @@ package zip
import (
"bufio"
- "compress/flate"
"encoding/binary"
"errors"
"hash"
@@ -198,18 +197,15 @@ func (w *Writer) CreateHeader(fh *FileHeader) (io.Writer, error) {
\t\tcompCount: &countWriter{w: w.cw},\n \t\tcrc32: crc32.NewIEEE(),
\t}
-\tswitch fh.Method {\n-\tcase Store:\n-\t\tfw.comp = nopCloser{fw.compCount}\n-\tcase Deflate:\n-\t\tvar err error\n-\t\tfw.comp, err = flate.NewWriter(fw.compCount, 5)\n-\t\tif err != nil {\n-\t\t\treturn nil, err\n-\t\t}\n-\tdefault:\n+\tcomp := compressor(fh.Method)\n+\tif comp == nil {\n \t\treturn nil, ErrAlgorithm
\t}
+\tvar err error
+\tfw.comp, err = comp(fw.compCount)
+\tif err != nil {
+\t\treturn nil, err
+\t}
\tfw.rawCount = &countWriter{w: fw.comp}
\n \th := &header{
コアとなるコードの解説
このコミットのコアとなる変更は、archive/zip パッケージが圧縮・解凍ロジックを管理する方法を根本的に変えた点にあります。
-
register.goの導入:- この新しいファイルは、圧縮器と解凍器の登録と管理を一元的に行います。
CompressorとDecompressorという関数型を定義し、これにより、任意の圧縮・解凍ロジックを標準のioインターフェースに適合させることができます。compressorsとdecompressorsというマップは、ZIPメソッドIDをキーとして、対応する圧縮・解凍関数を値として保持します。これにより、メソッドIDに基づいて動的に適切な関数を選択できるようになります。sync.RWMutexを使用することで、複数のゴルーチンが同時に圧縮器/解凍器のマップにアクセスしても、データの整合性が保たれるようにしています。特に、RegisterCompressorやRegisterDecompressorのような書き込み操作は排他的に行われ、compressorやdecompressorのような読み取り操作は並行して行えます。RegisterCompressorとRegisterDecompressorは、外部のパッケージがカスタムの圧縮・解凍ロジックをarchive/zipパッケージに「プラグイン」するための公開APIを提供します。これにより、標準ライブラリのコードを変更することなく、新しい圧縮方式をサポートできるようになります。
-
reader.goとwriter.goの抽象化:- 以前は、
reader.goのFile.Open()とwriter.goのWriter.CreateHeader()内に、圧縮方式に応じたswitch文が直接記述されていました。これは、新しい圧縮方式が追加されるたびにこれらのファイルを変更する必要があることを意味していました。 - このコミットでは、これらの
switch文が削除され、代わりにregister.goで定義されたcompressor()およびdecompressor()ヘルパー関数が呼び出されるようになりました。 - これらのヘルパー関数は、内部のマップから適切な圧縮器/解凍器関数を取得し、それを呼び出すことで実際の圧縮・解凍処理を行います。
- この変更により、
reader.goとwriter.goは、具体的な圧縮アルゴリズムの実装から切り離され、より汎用的なコードになりました。これにより、コードの保守性が向上し、将来的な拡張が容易になります。
- 以前は、
この変更は、Goの標準ライブラリが提供するモジュール性と拡張性の良い例です。ユーザーは、archive/zip パッケージの内部実装に触れることなく、自身のニーズに合わせて機能を拡張できるようになりました。
関連リンク
- Go言語
archive/zipパッケージのドキュメント: https://pkg.go.dev/archive/zip - Go言語
compress/flateパッケージのドキュメント: https://pkg.go.dev/compress/flate - Go言語
ioパッケージのドキュメント: https://pkg.go.dev/io - Go言語
syncパッケージのドキュメント: https://pkg.go.dev/sync
参考にした情報源リンク
- Goのコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Go Code Review (Gerrit): https://go-review.googlesource.com/c/go/+/12421043 (コミットメッセージに記載されているGerritの変更リストID)
- ZIPファイルフォーマットの仕様 (PKWARE): https://pkware.com/docs/casestudies/APPNOTE.TXT (圧縮メソッドIDなどの詳細情報)
- Go言語の
init()関数に関するドキュメントやチュートリアル。 - Go言語の
sync.RWMutexに関するドキュメントやチュートリアル。