[インデックス 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)
: 外部からカスタムの解凍器を登録するための公開関数です。指定されたmethod
IDが既に登録されている場合はパニック(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
に関するドキュメントやチュートリアル。