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

[インデックス 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.gowriter.go 内の switch 文で直接処理されていました。このアプローチでは、新しい圧縮方式を追加する際に、パッケージのソースコード自体を変更する必要がありました。

この変更により、switch 文は、ユーザーが init() 関数内でカスタムの圧縮器(compressor)と解凍器(decompressor)を登録できるマップベースのメカニズムに置き換えられます。これにより、パッケージの利用者が独自の圧縮方式を archive/zip パッケージに統合できるようになります。

変更の背景

ZIPファイルフォーマットは、様々な圧縮アルゴリズムをサポートするように設計されています。しかし、Goの archive/zip パッケージは、当初は最も一般的ないくつかの圧縮方式(主に StoreDeflate)のみを内部的にサポートしていました。

このハードコードされたアプローチにはいくつかの課題がありました。

  1. 拡張性の欠如: 新しい圧縮方式(例: Bzip2, LZMA, Zstandardなど)が普及したり、特定のアプリケーションで必要になったりした場合、archive/zip パッケージ自体を修正し、Goのリリースサイクルを待つ必要がありました。これは、迅速な対応や特定のユースケースへの適応を妨げます。
  2. コードの重複と複雑性: reader.gowriter.go の両方で同様の switch 文が存在し、圧縮方式の追加ごとに両方のファイルを変更する必要がありました。
  3. 柔軟性の制限: ユーザーが独自の、あるいはニッチな圧縮アルゴリズムを使用したい場合、標準ライブラリの制約により、別のZIPライブラリを使用するか、archive/zip パッケージをフォークするしかありませんでした。

このコミットは、これらの課題を解決し、archive/zip パッケージの柔軟性と拡張性を向上させることを目的としています。

前提知識の解説

このコミットを理解するためには、以下の概念が役立ちます。

  1. ZIPファイルフォーマット: ZIPファイルは、複数のファイルを一つのアーカイブにまとめるための一般的なファイルフォーマットです。各ファイルは個別に圧縮され、アーカイブ内に保存されます。ZIPフォーマットは、各エントリの圧縮方式を示す「圧縮メソッドID」を定義しています。
  2. 圧縮方式 (Compression Method): ZIPファイル内のデータがどのように圧縮されているかを示す識別子です。例えば、0 は無圧縮(Store)、8 はDeflate圧縮を表します。
  3. io.Readerio.Writer: Go言語の標準ライブラリ io パッケージで定義されているインターフェースです。io.Reader はデータを読み出すためのメソッド Read を持ち、io.Writer はデータを書き込むためのメソッド Write を持ちます。これらはGoにおけるストリーム処理の基本です。
  4. io.ReadCloserio.WriteCloser: それぞれ io.Readerio.WriterClose() メソッドを追加したインターフェースです。リソースの解放が必要なストリーム(例: ファイル、ネットワーク接続、圧縮ストリーム)でよく使用されます。
  5. compress/flate パッケージ: Goの標準ライブラリで、Deflate圧縮アルゴリズムを実装しています。ZIPファイルのDeflate圧縮は、このパッケージによって処理されます。
  6. sync.RWMutex: Goの sync パッケージで提供される読み書きロックです。複数のゴルーチンが共有リソース(この場合は圧縮器/解凍器のマップ)にアクセスする際に、データの競合を防ぐために使用されます。読み取り操作は並行して行えますが、書き込み操作は排他的に行われます。
  7. 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 を返します。
  • compressorsdecompressors マップ: map[uint16]Compressormap[uint16]Decompressor 型のグローバルマップです。uint16 はZIPファイルフォーマットで定義される圧縮メソッドIDを表します。これらのマップは、各圧縮メソッドIDに対応する Compressor または Decompressor 関数を格納します。 初期状態では、Store (無圧縮) と Deflate (Deflate圧縮) の標準的な実装が登録されています。
    • StoreCompressor: func(w io.Writer) (io.WriteCloser, error) { return &nopCloser{w}, nil } これは、入力 io.Writer をそのまま io.WriteCloser としてラップする nopCloser を返します。つまり、データは圧縮されずに直接書き込まれます。
    • DeflateCompressor: func(w io.Writer) (io.WriteCloser, error) { return flate.NewWriter(w, 5) } これは compress/flate パッケージの NewWriter を使用して、Deflate圧縮を行う io.WriteCloser を返します。5 は圧縮レベルを示します。
    • StoreDecompressor: ioutil.NopCloser これは io.Reader をそのまま io.ReadCloser としてラップする関数です。データは解凍されずに直接読み込まれます。
    • DeflateDecompressor: flate.NewReader これは compress/flate パッケージの NewReader を使用して、Deflate解凍を行う io.ReadCloser を返します。
  • mu sync.RWMutex: compressorsdecompressors マップへの並行アクセスを安全に管理するための読み書きミューテックスです。マップの読み取り時には共有ロック(RLock)を、書き込み時には排他ロック(Lock)を使用します。
  • RegisterDecompressor(method uint16, d Decompressor): 外部からカスタムの解凍器を登録するための公開関数です。指定された method IDが既に登録されている場合はパニック(panic)を発生させます。これは、同じメソッドIDに対して複数の解凍器が登録されるのを防ぐためです。
  • RegisterCompressor(method uint16, comp Compressor): 外部からカスタムの圧縮器を登録するための公開関数です。RegisterDecompressor と同様に、既に登録されている場合はパニックを発生させます。
  • compressor(method uint16) Compressordecompressor(method uint16) Decompressor: 内部ヘルパー関数で、指定されたメソッドIDに対応する圧縮器または解凍器をマップから安全に取得します。これらの関数は読み取りロック(RLock)を使用するため、複数のゴルーチンから同時に呼び出されても安全です。

reader.gowriter.go の変更

reader.gowriter.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 パッケージが圧縮・解凍ロジックを管理する方法を根本的に変えた点にあります。

  1. register.go の導入:

    • この新しいファイルは、圧縮器と解凍器の登録と管理を一元的に行います。
    • CompressorDecompressor という関数型を定義し、これにより、任意の圧縮・解凍ロジックを標準の io インターフェースに適合させることができます。
    • compressorsdecompressors というマップは、ZIPメソッドIDをキーとして、対応する圧縮・解凍関数を値として保持します。これにより、メソッドIDに基づいて動的に適切な関数を選択できるようになります。
    • sync.RWMutex を使用することで、複数のゴルーチンが同時に圧縮器/解凍器のマップにアクセスしても、データの整合性が保たれるようにしています。特に、RegisterCompressorRegisterDecompressor のような書き込み操作は排他的に行われ、compressordecompressor のような読み取り操作は並行して行えます。
    • RegisterCompressorRegisterDecompressor は、外部のパッケージがカスタムの圧縮・解凍ロジックを archive/zip パッケージに「プラグイン」するための公開APIを提供します。これにより、標準ライブラリのコードを変更することなく、新しい圧縮方式をサポートできるようになります。
  2. reader.gowriter.go の抽象化:

    • 以前は、reader.goFile.Open()writer.goWriter.CreateHeader() 内に、圧縮方式に応じた switch 文が直接記述されていました。これは、新しい圧縮方式が追加されるたびにこれらのファイルを変更する必要があることを意味していました。
    • このコミットでは、これらの switch 文が削除され、代わりに register.go で定義された compressor() および decompressor() ヘルパー関数が呼び出されるようになりました。
    • これらのヘルパー関数は、内部のマップから適切な圧縮器/解凍器関数を取得し、それを呼び出すことで実際の圧縮・解凍処理を行います。
    • この変更により、reader.gowriter.go は、具体的な圧縮アルゴリズムの実装から切り離され、より汎用的なコードになりました。これにより、コードの保守性が向上し、将来的な拡張が容易になります。

この変更は、Goの標準ライブラリが提供するモジュール性と拡張性の良い例です。ユーザーは、archive/zip パッケージの内部実装に触れることなく、自身のニーズに合わせて機能を拡張できるようになりました。

関連リンク

参考にした情報源リンク