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

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

このコミットは、Go言語の標準ライブラリである archive/zip パッケージにおける、ZIPアーカイブの読み書き処理の改善を目的としています。具体的には、バイナリデータの読み書きに encoding/binary パッケージを再導入し、reader.goreadBuf ヘルパーを導入することで、コードの可読性、保守性、そして堅牢性を向上させています。

コミット

commit ce51e1074995cf7f1369b26d059d5a86e7b82e98
Author: Andrew Gerrand <adg@golang.org>
Date:   Tue Feb 28 09:41:30 2012 +1100

    archive/zip: use encoding/binary again, add readBuf helper
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/5699097
---
 src/pkg/archive/zip/reader.go | 136 ++++++++++++++++++++++++------------------
 src/pkg/archive/zip/writer.go |  12 +---\n 2 files changed, 81 insertions(+), 67 deletions(-)

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

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

元コミット内容

archive/zip: use encoding/binary again, add readBuf helper

変更の背景

このコミットが行われた2012年2月時点のGo言語では、encoding/binary パッケージの利用に関して、特にパフォーマンス面での考慮が必要な時期がありました。初期のGo言語では、encoding/binary.Write のような関数がリフレクションを使用しており、これが特定のシナリオでオーバーヘッドとなる可能性がありました。そのため、archive/zip パッケージのような、バイナリデータを頻繁に扱う部分では、手動でのバイト操作(ビットシフトなど)が採用されていた時期がありました。

しかし、手動でのバイト操作は、コードが冗長になりがちで、バグの温床となる可能性も秘めています。特に、エンディアンネス(バイト順序)の扱いは非常にデリケートであり、手動で実装するとミスが発生しやすくなります。

このコミットの背景には、encoding/binary パッケージの成熟、あるいは特定の関数(binary.LittleEndian.Uint16, binary.LittleEndian.PutUint16 など)がリフレクションを使用せず、効率的に実装されるようになったという状況変化があったと考えられます。これにより、手動でのバイト操作から、より標準的で安全な encoding/binary パッケージへの回帰が可能となり、コードの品質向上を図ることが目的とされました。

前提知識の解説

ZIPファイルフォーマット

ZIPファイルは、複数のファイルを圧縮して一つにまとめるための一般的なアーカイブフォーマットです。その構造は、ファイルヘッダ、データ、セントラルディレクトリ、エンドオブセントラルディレクトリレコードなど、複数のセクションから構成されています。これらのセクションには、ファイル名、圧縮・非圧縮サイズ、CRC32チェックサム、ファイル属性などのメタデータがバイナリ形式で格納されています。

ZIPファイルフォーマットの重要な特徴の一つは、数値データがリトルエンディアンで格納されることです。リトルエンディアンとは、複数バイトで構成される数値の最下位バイトが、メモリの最も小さいアドレスに格納される方式です。例えば、0x12345678 という32ビットの数値は、リトルエンディアンでは 78 56 34 12 の順で格納されます。

エンディアンネス (Endianness)

エンディアンネスは、コンピュータのメモリ上で複数バイトのデータをどのように並べるかを定義する方式です。

  • リトルエンディアン (Little-endian): 最下位バイトが先頭(最も小さいアドレス)に格納されます。Intel x86アーキテクチャのCPUはリトルエンディアンを採用しています。
  • ビッグエンディアン (Big-endian): 最上位バイトが先頭(最も小さいアドレス)に格納されます。ネットワークプロトコルやJavaの仮想マシンなどでよく用いられます。

Go言語の encoding/binary パッケージは、このエンディアンネスを意識したバイナリデータの読み書きをサポートします。

encoding/binary パッケージ

Go言語の encoding/binary パッケージは、Goのプリミティブ型(整数、浮動小数点数など)とバイトスライスとの間でバイナリデータを変換するための機能を提供します。このパッケージは、特定のバイト順序(リトルエンディアンまたはビッグエンディアン)を指定してデータの読み書きを行うことができます。

主要な関数としては、以下のようなものがあります。

  • binary.Read(r io.Reader, order ByteOrder, data interface{}) error: io.Reader からデータを読み込み、指定されたバイト順序でGoのデータ構造にデコードします。
  • binary.Write(w io.Writer, order ByteOrder, data interface{}) error: Goのデータ構造を、指定されたバイト順序で io.Writer にエンコードして書き込みます。
  • binary.LittleEndian.Uint16(b []byte) uint16: バイトスライス b の先頭2バイトをリトルエンディアンの uint16 として解釈し、その値を返します。
  • binary.LittleEndian.PutUint16(b []byte, v uint16): uint16 の値 v をリトルエンディアンでバイトスライス b の先頭2バイトに書き込みます。
  • binary.LittleEndian.Uint32(b []byte) uint32: バイトスライス b の先頭4バイトをリトルエンディアンの uint32 として解釈し、その値を返します。
  • binary.LittleEndian.PutUint32(b []byte, v uint32): uint32 の値 v をリトルエンディアンでバイトスライス b の先頭4バイトに書き込みます。

このコミットでは、特に Uint16, Uint32, PutUint16, PutUint32 といった、リフレクションを使用しない直接的な変換関数が活用されています。

技術的詳細

このコミットの技術的な核心は、archive/zip パッケージがZIPファイルフォーマットのバイナリデータを扱う方法を、手動のビット操作から encoding/binary パッケージの利用へと移行した点にあります。

reader.go の変更点

  1. encoding/binary のインポート:

    import (
        // ...
        "encoding/binary"
        // ...
    )
    

    これにより、encoding/binary パッケージの機能が利用可能になります。

  2. readBuf 型とヘルパーメソッドの導入:

    type readBuf []byte
    
    func (b *readBuf) uint16() uint16 {
        v := binary.LittleEndian.Uint16(*b)
        *b = (*b)[2:] // 読み取った分だけスライスを進める
        return v
    }
    
    func (b *readBuf) uint32() uint32 {
        v := binary.LittleEndian.Uint32(*b)
        *b = (*b)[4:] // 読み取った分だけスライスを進める
        return v
    }
    

    readBuf はバイトスライス []byte のエイリアス型です。この型に uint16()uint32() というメソッドが追加されました。これらのメソッドは、binary.LittleEndian.Uint16 および binary.LittleEndian.Uint32 を使用して、それぞれ2バイトまたは4バイトをリトルエンディアンの uint16 または uint32 として読み取ります。 特筆すべきは、読み取り後に *b = (*b)[2:]*b = (*b)[4:] のように、レシーバのバイトスライス自体を更新している点です。これにより、一度 readBuf を作成すれば、連続してフィールドを読み取っていく際に、自動的に読み取り位置が進むようになり、コードが非常に簡潔になります。

  3. 手動変換関数の削除と readBuf への置き換え: 以前は toUint16toUint32 といった手動でビットシフトを行う関数が使われていました。

    // 変更前 (例: reader.go)
    // func toUint16(b []byte) uint16 { return uint16(b[0]) | uint16(b[1])<<8 }
    // func toUint32(b []byte) uint32 { return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24 }
    
    // 変更前 (例: readFileHeader)
    // f.ReaderVersion = toUint16(b[4:])
    // f.Flags = toUint16(b[6:])
    // ...
    

    これが、readBuf を使用する形に置き換えられました。

    // 変更後 (例: readFileHeader)
    // var buf [fileHeaderLen]byte
    // if _, err := io.ReadFull(r, buf[:]); err != nil { ... }
    // b := readBuf(buf[:]) // readBufを作成
    // if sig := b.uint32(); sig != fileHeaderSignature { ... } // uint32を読み取り、readBufは4バイト進む
    // f.ReaderVersion = b.uint16() // uint16を読み取り、readBufは2バイト進む
    // f.Flags = b.uint16()         // uint16を読み取り、readBufは2バイト進む
    // ...
    

    この変更により、各フィールドの読み取りが b.uint16()b.uint32() のように直感的になり、オフセット計算のミスが減り、コードの可読性が大幅に向上しました。

  4. readDirectoryEnd におけるコメント長の検証追加:

    // ...
    d.commentLen = b.uint16()
    // ...
    l := int(d.commentLen)
    if l > len(b) { // 新しく追加された検証
        return nil, errors.New("zip: invalid comment length")
    }
    d.comment = string(b[:l])
    

    readDirectoryEnd 関数で、コメントの長さを読み取った後、実際に残っているバッファの長さと比較する検証が追加されました。これにより、不正なZIPファイルが与えられた場合に、パニック(インデックス範囲外アクセス)を防ぎ、より安全なエラーハンドリングが可能になりました。

writer.go の変更点

  1. encoding/binary のインポート: reader.go と同様に、encoding/binary パッケージがインポートされました。

  2. writeBuf メソッドの encoding/binary 利用への変更: 以前は writeBufuint16uint32 メソッドも手動でバイトを書き込んでいました。

    // 変更前 (例: writer.go)
    // func (b *writeBuf) uint16(v uint16) {
    //  (*b)[0] = byte(v)
    //  (*b)[1] = byte(v >> 8)
    //  *b = (*b)[2:]
    // }
    // func (b *writeBuf) uint32(v uint32) {
    //  (*b)[0] = byte(v)
    //  (*b)[1] = byte(v >> 8)
    //  (*b)[2] = byte(v >> 16)
    //  (*b)[3] = byte(v >> 24)
    //  *b = (*b)[4:]
    // }
    

    これが encoding/binary.LittleEndian.PutUint16 および PutUint32 を使用する形に置き換えられました。

    // 変更後 (例: writer.go)
    func (b *writeBuf) uint16(v uint16) {
        binary.LittleEndian.PutUint16(*b, v)
        *b = (*b)[2:]
    }
    
    func (b *writeBuf) uint32(v uint32) {
        binary.LittleEndian.PutUint32(*b, v)
        *b = (*b)[4:]
    }
    

    これにより、書き込み処理も標準ライブラリの機能を利用するようになり、コードの信頼性が向上しました。

  3. 不要なコメントの削除: writer.go にあった以下のコメントが削除されました。

    // We use this helper instead of encoding/binary's Write to avoid reflection.
    // It's easy enough, anyway.
    

    このコメントは、以前 encoding/binary.Write がリフレクションを使用していたために手動実装を選択した理由を説明していましたが、今回の変更で encoding/binary の直接的な PutUintX 関数を使用するようになったため、このコメントはもはや適切ではなくなりました。

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

src/pkg/archive/zip/reader.go

--- a/src/pkg/archive/zip/reader.go
+++ b/src/pkg/archive/zip/reader.go
@@ -7,6 +7,7 @@ package zip
 import (
 	"bufio"
 	"compress/flate"
+	"encoding/binary" // 追加
 	"errors"
 	"hash"
 	"hash/crc32"
@@ -169,23 +170,24 @@ func (r *checksumReader) Read(b []byte) (n int, err error) {
 func (r *checksumReader) Close() error { return r.rc.Close() }
 
 func readFileHeader(f *File, r io.Reader) error {
-	var b [fileHeaderLen]byte
-	if _, err := io.ReadFull(r, b[:]); err != nil {
+	var buf [fileHeaderLen]byte // 変数名をbからbufに変更
+	if _, err := io.ReadFull(r, buf[:]); err != nil {
 		return err
 	}
-	if sig := toUint32(b[:]); sig != fileHeaderSignature {
+	b := readBuf(buf[:]) // readBufの作成
+	if sig := b.uint32(); sig != fileHeaderSignature { // readBufメソッドの利用
 		return ErrFormat
 	}
-	f.ReaderVersion = toUint16(b[4:])
-	f.Flags = toUint16(b[6:])
-	f.Method = toUint16(b[8:])
-	f.ModifiedTime = toUint16(b[10:])
-	f.ModifiedDate = toUint16(b[12:])
-	f.CRC32 = toUint32(b[14:])
-	f.CompressedSize = toUint32(b[18:])
-	f.UncompressedSize = toUint32(b[22:])
-	filenameLen := int(toUint16(b[26:]))
-	extraLen := int(toUint16(b[28:]))
+	f.ReaderVersion = b.uint16() // readBufメソッドの利用
+	f.Flags = b.uint16()
+	f.Method = b.uint16()
+	f.ModifiedTime = b.uint16()
+	f.ModifiedDate = b.uint16()
+	f.CRC32 = b.uint32()
+	f.CompressedSize = b.uint32()
+	f.UncompressedSize = b.uint32()
+	filenameLen := int(b.uint16())
+	extraLen := int(b.uint16())
 	d := make([]byte, filenameLen+extraLen)
 	if _, err := io.ReadFull(r, d); err != nil {
 		return err
@@ -199,15 +201,17 @@ func readFileHeader(f *File, r io.Reader) error {
 // and returns the file body offset.
 func (f *File) findBodyOffset() (int64, error) {
 	r := io.NewSectionReader(f.zipr, f.headerOffset, f.zipsize-f.headerOffset)
-	var b [fileHeaderLen]byte
-	if _, err := io.ReadFull(r, b[:]); err != nil {
+	var buf [fileHeaderLen]byte // 変数名をbからbufに変更
+	if _, err := io.ReadFull(r, buf[:]); err != nil {
 		return 0, err
 	}
-	if sig := toUint32(b[:4]); sig != fileHeaderSignature {
+	b := readBuf(buf[:]) // readBufの作成
+	if sig := b.uint32(); sig != fileHeaderSignature { // readBufメソッドの利用
 		return 0, ErrFormat
 	}
-	filenameLen := int(toUint16(b[26:28]))
-	extraLen := int(toUint16(b[28:30]))
+	b = b[22:] // skip over most of the header // readBufをスキップ
+	filenameLen := int(b.uint16()) // readBufメソッドの利用
+	extraLen := int(b.uint16())
 	return int64(fileHeaderLen + filenameLen + extraLen), nil
 }
 
@@ -215,28 +219,29 @@ func (f *File) findBodyOffset() (int64, error) {
 // It returns io.ErrUnexpectedEOF if it cannot read a complete header,\n // and ErrFormat if it doesn\'t find a valid header signature.\n func readDirectoryHeader(f *File, r io.Reader) error {
-	var b [directoryHeaderLen]byte
-	if _, err := io.ReadFull(r, b[:]); err != nil {
+	var buf [directoryHeaderLen]byte // 変数名をbからbufに変更
+	if _, err := io.ReadFull(r, buf[:]); err != nil {
 		return err
 	}
-	if sig := toUint32(b[:]); sig != directoryHeaderSignature {
+	b := readBuf(buf[:]) // readBufの作成
+	if sig := b.uint32(); sig != directoryHeaderSignature { // readBufメソッドの利用
 		return ErrFormat
 	}
-	f.CreatorVersion = toUint16(b[4:])
-	f.ReaderVersion = toUint16(b[6:])
-	f.Flags = toUint16(b[8:])
-	f.Method = toUint16(b[10:])
-	f.ModifiedTime = toUint16(b[12:])
-	f.ModifiedDate = toUint16(b[14:])
-	f.CRC32 = toUint32(b[16:])
-	f.CompressedSize = toUint32(b[20:])
-	f.UncompressedSize = toUint32(b[24:])
-	filenameLen := int(toUint16(b[28:]))
-	extraLen := int(toUint16(b[30:32]))
-	commentLen := int(toUint16(b[32:]))
-	// skipped start disk number and internal attributes (2x uint16)
-	f.ExternalAttrs = toUint32(b[38:])
-	f.headerOffset = int64(toUint32(b[42:]))
+	f.CreatorVersion = b.uint16() // readBufメソッドの利用
+	f.ReaderVersion = b.uint16()
+	f.Flags = b.uint16()
+	f.Method = b.uint16()
+	f.ModifiedTime = b.uint16()
+	f.ModifiedDate = b.uint16()
+	f.CRC32 = b.uint32()
+	f.CompressedSize = b.uint32()
+	f.UncompressedSize = b.uint32()
+	filenameLen := int(b.uint16())
+	extraLen := int(b.uint16())
+	commentLen := int(b.uint16())
+	b = b[4:] // skipped start disk number and internal attributes (2x uint16) // readBufをスキップ
+	f.ExternalAttrs = b.uint32()
+	f.headerOffset = int64(b.uint32())
 	d := make([]byte, filenameLen+extraLen+commentLen)
 	if _, err := io.ReadFull(r, d); err != nil {
 		return err
@@ -248,29 +253,30 @@ func readDirectoryHeader(f *File, r io.Reader) error {
 }\n \n func readDataDescriptor(r io.Reader, f *File) error {
-	var b [dataDescriptorLen]byte
-	if _, err := io.ReadFull(r, b[:]); err != nil {
+	var buf [dataDescriptorLen]byte // 変数名をbからbufに変更
+	if _, err := io.ReadFull(r, buf[:]); err != nil {
 		return err
 	}
-	f.CRC32 = toUint32(b[:4])
-	f.CompressedSize = toUint32(b[4:8])
-	f.UncompressedSize = toUint32(b[8:12])
+	b := readBuf(buf[:]) // readBufの作成
+	f.CRC32 = b.uint32() // readBufメソッドの利用
+	f.CompressedSize = b.uint32()
+	f.UncompressedSize = b.uint32()
 	return nil
 }\n \n func readDirectoryEnd(r io.ReaderAt, size int64) (dir *directoryEnd, err error) {
 	// look for directoryEndSignature in the last 1k, then in the last 65k
-	var b []byte
+	var buf []byte // 変数名をbからbufに変更
 	for i, bLen := range []int64{1024, 65 * 1024} {
 		if bLen > size {
 			bLen = size
 		}
-		b = make([]byte, int(bLen))
-		if _, err := r.ReadAt(b, size-bLen); err != nil && err != io.EOF {
+		buf = make([]byte, int(bLen)) // 変数名をbからbufに変更
+		if _, err := r.ReadAt(buf, size-bLen); err != nil && err != io.EOF {
 			return nil, err
 		}\n-		if p := findSignatureInBlock(b); p >= 0 {\n-			b = b[p:]
+		if p := findSignatureInBlock(buf); p >= 0 { // 変数名をbからbufに変更
+			buf = buf[p:] // 変数名をbからbufに変更
 			break
 		}
 		if i == 1 || bLen == size {
@@ -279,15 +285,21 @@ func readDirectoryEnd(r io.ReaderAt, size int64) (dir *directoryEnd, err error) {
 	}\n \n 	// read header into struct
-	d := new(directoryEnd)
-	d.diskNbr = toUint16(b[4:])
-	d.dirDiskNbr = toUint16(b[6:])
-	d.dirRecordsThisDisk = toUint16(b[8:])
-	d.directoryRecords = toUint16(b[10:])
-	d.directorySize = toUint32(b[12:])
-	d.directoryOffset = toUint32(b[16:])
-	d.commentLen = toUint16(b[20:])
-	d.comment = string(b[22 : 22+int(d.commentLen)])
+	b := readBuf(buf[4:]) // skip signature // readBufの作成
+	d := &directoryEnd{ // 構造体初期化の変更
+		diskNbr:            b.uint16(),
+		dirDiskNbr:         b.uint16(),
+		dirRecordsThisDisk: b.uint16(),
+		directoryRecords:   b.uint16(),
+		directorySize:      b.uint32(),
+		directoryOffset:    b.uint32(),
+		commentLen:         b.uint16(),
+	}
+	l := int(d.commentLen)
+	if l > len(b) { // コメント長の検証を追加
+		return nil, errors.New("zip: invalid comment length")
+	}
+	d.comment = string(b[:l]) // コメント読み取りの変更
 	return d, nil
 }\n \n@@ -305,8 +317,16 @@ func findSignatureInBlock(b []byte) int {
 	return -1
 }\n \n-func toUint16(b []byte) uint16 { return uint16(b[0]) | uint16(b[1])<<8 } // 削除
+type readBuf []byte // readBuf型の定義を追加
+\n+func (b *readBuf) uint16() uint16 { // readBuf.uint16メソッドの追加
+\tv := binary.LittleEndian.Uint16(*b)
+\t*b = (*b)[2:]
+\treturn v
+}\n \n-func toUint32(b []byte) uint32 { // 削除
-\treturn uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
+func (b *readBuf) uint32() uint32 { // readBuf.uint32メソッドの追加
+\tv := binary.LittleEndian.Uint32(*b)
+\t*b = (*b)[4:]
+\treturn v
 }\n```

### `src/pkg/archive/zip/writer.go`

```diff
--- a/src/pkg/archive/zip/writer.go
+++ b/src/pkg/archive/zip/writer.go
@@ -7,6 +7,7 @@ package zip
 import (
 	"bufio"
 	"compress/flate"
+	"encoding/binary" // 追加
 	"errors"
 	"hash"
 	"hash/crc32"
@@ -249,21 +250,14 @@ func (w nopCloser) Close() error {
 	return nil
 }\n \n-// We use this helper instead of encoding/binary's Write to avoid reflection. // 削除
-// It's easy enough, anyway. // 削除
-\n type writeBuf []byte
 \n func (b *writeBuf) uint16(v uint16) {
-\t(*b)[0] = byte(v)
-\t(*b)[1] = byte(v >> 8)
+	binary.LittleEndian.PutUint16(*b, v) // encoding/binaryの利用
 	*b = (*b)[2:]
 }\n \n func (b *writeBuf) uint32(v uint32) {
-\t(*b)[0] = byte(v)
-\t(*b)[1] = byte(v >> 8)
-\t(*b)[2] = byte(v >> 16)
-\t(*b)[3] = byte(v >> 24)
+	binary.LittleEndian.PutUint32(*b, v) // encoding/binaryの利用
 	*b = (*b)[4:]
 }\n```

## コアとなるコードの解説

### `reader.go` の変更解説

`reader.go` では、ZIPファイルのヘッダ情報を読み取るための複数の関数(`readFileHeader`, `findBodyOffset`, `readDirectoryHeader`, `readDataDescriptor`, `readDirectoryEnd`)が変更されています。

1.  **`encoding/binary` のインポート**: これは、`encoding/binary` パッケージの `LittleEndian` インターフェースとそのメソッド(`Uint16`, `Uint32`)を使用するための前提です。

2.  **`readBuf` 型の導入とメソッド**:
    `type readBuf []byte` は、バイトスライスをラップする新しい型を定義しています。この型にアタッチされた `uint16()` と `uint32()` メソッドがこのコミットの肝です。
    ```go
    func (b *readBuf) uint16() uint16 {
        v := binary.LittleEndian.Uint16(*b)
        *b = (*b)[2:]
        return v
    }
    ```
    この `uint16()` メソッドは、以下の処理を行います。
    *   `binary.LittleEndian.Uint16(*b)`: 現在の `readBuf` が指すバイトスライスの先頭2バイトをリトルエンディアンの `uint16` として解釈し、その値を `v` に格納します。
    *   `*b = (*b)[2:]`: ここが重要です。レシーバ `b` はポインタなので、`*b` は元のバイトスライスを指します。この行は、元のバイトスライスを先頭から2バイト分進めた新しいスライスに更新します。これにより、次に `b.uint16()` や `b.uint32()` を呼び出すと、自動的に次のフィールドから読み取りが開始されます。
    `uint32()` メソッドも同様に、4バイトを読み取り、スライスを4バイト進めます。

3.  **ヘッダ読み取り関数の簡素化**:
    例えば `readFileHeader` 関数では、以前は `toUint32(b[:])` や `toUint16(b[4:])` のように、手動でオフセットを指定してバイトスライスを切り出し、変換関数に渡していました。
    変更後は、まず `readBuf(buf[:])` で `readBuf` インスタンスを作成し、あとは `b.uint32()` や `b.uint16()` を連続して呼び出すだけで、各フィールドの値を順番に読み取れるようになりました。これにより、オフセット計算のミスが大幅に減り、コードが非常に読みやすくなっています。

4.  **`readDirectoryEnd` の堅牢性向上**:
    `readDirectoryEnd` 関数では、ZIPファイルのセントラルディレクトリの終端レコードを読み取ります。このレコードにはコメントの長さが含まれています。以前のコードでは、コメントの長さを読み取った後、その長さに基づいてバイトスライスを切り出していましたが、もし不正なコメント長が指定されていた場合(例えば、残りのバッファ長よりも長い場合)、`panic` を引き起こす可能性がありました。
    ```go
    l := int(d.commentLen)
    if l > len(b) {
        return nil, errors.New("zip: invalid comment length")
    }
    ```
    このコミットでは、`if l > len(b)` というチェックが追加され、コメント長が残りのバッファ長を超えていないかを確認するようになりました。これにより、不正なZIPファイルに対する耐性が向上し、より安全なエラーハンドリングが可能になりました。

### `writer.go` の変更解説

`writer.go` では、ZIPアーカイブにデータを書き込む際のバイナリ変換処理が変更されています。

1.  **`encoding/binary` のインポート**: `reader.go` と同様に、`encoding/binary` パッケージの `PutUint16` や `PutUint32` メソッドを使用するためにインポートされています。

2.  **`writeBuf` メソッドの変更**:
    `writeBuf` 型の `uint16()` と `uint32()` メソッドは、以前は手動でバイトを書き込んでいました。
    ```go
    func (b *writeBuf) uint16(v uint16) {
        binary.LittleEndian.PutUint16(*b, v)
        *b = (*b)[2:]
    }
    ```
    この変更により、`binary.LittleEndian.PutUint16(*b, v)` を使用して、`uint16` の値 `v` をリトルエンディアンでバイトスライス `*b` の先頭に書き込むようになりました。書き込み後、`*b = (*b)[2:]` でスライスを2バイト進めるのは読み取り時と同様です。`uint32()` メソッドも同様に `PutUint32` を使用します。
    これにより、書き込み処理も標準ライブラリの堅牢な実装に依存するようになり、手動でのバイト操作に起因する潜在的なバグが排除されました。

3.  **コメントの削除**:
    `writer.go` にあった「`encoding/binary` の `Write` がリフレクションを使うため、手動で実装している」というコメントは、今回の変更で `encoding/binary` の直接的な `PutUintX` 関数を使うようになったため、その理由が解消され、削除されました。これは、Go言語の進化に伴い、特定のAPIのパフォーマンス特性が改善されたことを示唆しています。

## 関連リンク

*   Go言語 `archive/zip` パッケージのドキュメント: [https://pkg.go.dev/archive/zip](https://pkg.go.dev/archive/zip)
*   Go言語 `encoding/binary` パッケージのドキュメント: [https://pkg.go.dev/encoding/binary](https://pkg.go.dev/encoding/binary)
*   Go言語のコードレビューシステム (Gerrit): [https://go-review.googlesource.com/](https://go-review.googlesource.com/) (コミットメッセージにある `https://golang.org/cl/5699097` は、このGerritの変更リストへのリンクです)

## 参考にした情報源リンク

*   ZIPファイルフォーマットの仕様 (PKWARE): [https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT](https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT)
*   エンディアンネスに関するWikipediaの記事: [https://ja.wikipedia.org/wiki/%E3%82%A8%E3%83%B3%E3%83%87%E3%82%A3%E3%82%A2%E3%83%B3](https://ja.wikipedia.org/wiki/%E3%82%A8%E3%83%B3%E3%83%87%E3%82%A3%E3%82%A2%E3%83%B3)
*   Go言語の `encoding/binary` パッケージに関する解説記事 (例: Go by Example - Binary Encoding): [https://gobyexample.com/binary-encoding](https://gobyexample.com/binary-encoding) (これは一般的な例であり、このコミットに直接関連するものではありませんが、`encoding/binary` の理解に役立ちます)
*   Go言語の歴史と進化に関する情報 (一般的なGoのブログやドキュメント)