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

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

コミット

commit b4237b599708b7f8d7d967806e577692db30a4f1
Author: Philip K. Warren <pkwarren@gmail.com>
Date:   Tue Mar 12 01:50:10 2013 -0400

    encoding/base32, encoding/base64: fix issues with decoder whitespace handling
    
    Adds a new reader to filter newlines, which fixes errors seen in the
    decoder chunking code. Found additional issues with whitespace handling
    after the first padding character.
    Fixes #4779.
    
    R=minux.ma, rsc, bradfitz
    CC=golang-dev
    https://golang.org/cl/7311069

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

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

元コミット内容

encoding/base32 および encoding/base64 パッケージにおけるデコーダの空白文字(特に改行)処理に関する問題を修正するコミットです。具体的には、改行文字をフィルタリングするための新しいリーダーを導入し、デコーダのチャンク処理で発生していたエラーを修正しています。また、最初のパディング文字の後に続く空白文字の処理に関する追加の問題も解決しています。この変更は、GoのIssue #4779 を修正するものです。

変更の背景

このコミットは、Go言語の標準ライブラリである encoding/base32 および encoding/base64 パッケージにおけるデコード処理の堅牢性を向上させることを目的としています。特に、Base32/Base64エンコードされたデータに改行文字やその他の空白文字が含まれている場合に、デコーダが正しく動作しないという問題が報告されていました(Issue #4779)。

Base32やBase64エンコーディングは、バイナリデータをASCII文字列に変換するために広く使用されます。しかし、これらのエンコードされたデータがファイルやネットワーク経由で転送される際、可読性や特定のプロトコルの要件のために、改行文字が挿入されることがあります。例えば、MIME (Multipurpose Internet Mail Extensions) では、Base64エンコードされた行の長さに制限があり、長いデータは改行で区切られます。

従来のGoのデコーダは、このような改行文字の扱いに不備があり、デコードエラーや予期せぬ結果を引き起こす可能性がありました。具体的には、デコーダが入力ストリームからデータを読み込む際に、改行文字を適切にスキップせず、デコード処理の途中で不正な文字として扱ってしまうことが問題でした。また、パディング文字(=)の後に続く空白文字の処理にも問題があり、これもデコードエラーの原因となっていました。

このコミットは、これらの問題を解決し、Base32/Base64デコーダがより柔軟に、そして正確に、空白文字を含む入力データを処理できるようにすることで、ライブラリの信頼性と使いやすさを向上させています。

前提知識の解説

Base32エンコーディング

Base32は、バイナリデータを32種類のASCII文字(A-Zと2-7)で表現するエンコーディング方式です。5ビットのグループでデータを表現し、8バイトのバイナリデータを13文字のBase32文字列に変換します。データの最後にパディング文字として=が使用されることがあります。主に、DNSSEC (DNS Security Extensions) や、人間が読みやすく、かつ大文字小文字を区別しない環境でのデータ転送に利用されます。

Base64エンコーディング

Base64は、バイナリデータを64種類のASCII文字(A-Z, a-z, 0-9, +, /)で表現するエンコーディング方式です。6ビットのグループでデータを表現し、3バイトのバイナリデータを4文字のBase64文字列に変換します。Base32と同様に、データの最後にパディング文字として=が使用されることがあります。電子メールのMIMEや、HTTPのBasic認証、JSONデータ内の画像埋め込みなど、インターネット上でバイナリデータをテキスト形式で安全に転送する際に広く利用されます。

デコーダのチャンク処理

Base32やBase64のデコーダは、入力ストリームからデータを一定の「チャンク」(塊)で読み込み、それをデコード処理します。例えば、Base64では4文字の入力が3バイトの出力に対応します。このチャンク処理中に、予期せぬ文字(例えば改行)が混入すると、デコーダは正しいチャンクを形成できなくなり、エラーを発生させたり、誤ったデコード結果を返したりする可能性があります。

ストリーム処理と io.Reader

Go言語では、データの読み書きは io.Reader および io.Writer インターフェースを通じて抽象化されています。io.ReaderRead(p []byte) (n int, err error) メソッドを持ち、データをバイトスライス p に読み込み、読み込んだバイト数 n とエラー err を返します。デコーダは通常、この io.Reader を入力として受け取り、ストリームとしてデータを処理します。

パディング文字 (=) の扱い

Base32およびBase64エンコーディングでは、元のバイナリデータの長さがエンコーディングのブロックサイズ(Base32は5ビット、Base64は6ビット)の倍数でない場合、出力の最後にパディング文字=が追加されます。デコーダは、このパディング文字を認識し、それ以降のデータ(もしあれば)をエラーとして扱う必要があります。

技術的詳細

このコミットの主要な技術的変更点は、encoding/base32encoding/base64 パッケージの両方に newlineFilteringReader という新しい io.Reader の実装を導入したことです。

newlineFilteringReader の役割

newlineFilteringReader は、既存の io.Reader をラップし、その Read メソッドが呼び出された際に、読み込んだバイトデータから改行文字 (\r および \n) を透過的に除去します。これにより、下層のリーダーから読み込まれたデータがデコーダに渡される前に、不要な改行文字が取り除かれるため、デコーダはクリーンな入力データを受け取ることができます。

newlineFilteringReaderRead メソッドの動作は以下の通りです。

  1. ラップしている io.Reader からデータを読み込みます。
  2. 読み込んだバイトスライス p の中を走査し、改行文字でないバイトのみを p の先頭から詰めていきます。
  3. もし読み込んだデータが全て改行文字であった場合、再度下層のリーダーからデータを読み込み直します。これは、デコーダが有効なデータを受け取るまで繰り返されます。
  4. 改行文字を除去した後の有効なバイト数と、下層のリーダーから返されたエラーを返します。

デコーダの変更点

  1. decode メソッドからの改行処理の削除: 以前の decode メソッド(Base32とBase64の両方)では、入力バイトを1バイトずつ処理するループ内で、if in == '\r' || in == '\n' { continue } という形で改行文字をスキップしていました。このコミットでは、このロジックが削除されました。これは、newlineFilteringReader がデコード処理の前に改行文字をフィルタリングするため、decode メソッド内で再度処理する必要がなくなったためです。これにより、decode メソッドのロジックが簡素化され、本来のデコード処理に集中できるようになりました。

  2. Decode および DecodeString メソッドの変更:

    • Decode(dst, src []byte) メソッドでは、入力 srcbytes.Map(removeNewlinesMapper, src) を使って処理するようになりました。removeNewlinesMapper は、\r または \n-1 にマッピングすることで、これらの文字を実質的に削除します。これにより、decode メソッドに渡される前に、入力バイトスライスから改行文字が除去されます。
    • DecodeString(s string) メソッドでは、入力文字列 sstrings.Map(removeNewlinesMapper, s) を使って処理するようになりました。同様に、デコード処理の前に文字列から改行文字が除去されます。
  3. NewDecoder 関数の変更: NewDecoder(enc *Encoding, r io.Reader) 関数は、これまでの &decoder{enc: enc, r: r} の代わりに、&decoder{enc: enc, r: &newlineFilteringReader{r}} を返すようになりました。これにより、NewDecoder を通じて作成されるすべてのデコーダは、自動的に newlineFilteringReader を介して入力データを読み込むようになり、改行文字の透過的なフィルタリングが保証されます。

パディング文字後の空白文字処理の修正

コミットメッセージに「Found additional issues with whitespace handling after the first padding character.」とあるように、パディング文字=の後に続く空白文字の処理にも問題がありました。decode メソッド内のパディング文字検出ロジックにおいて、len(src)+j < 8-1 (Base32) や len(src)+j < 4-1 (Base64) の条件が修正され、パディング文字の後に続くデータが不正であるかどうかのチェックがより厳密になりました。これにより、パディング文字の後に改行などの空白文字が続く場合でも、正しくエラーとして扱われるようになりました。

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

src/pkg/encoding/base32/base32.go および src/pkg/encoding/base64/base64.go

  • removeNewlinesMapper 関数(runerune にマッピング)の追加。
  • decode メソッドから、入力バイトの改行文字スキップロジックを削除。
  • Decode メソッドで、bytes.Map(removeNewlinesMapper, src) を使って入力 src から改行文字を除去する処理を追加。
  • DecodeString メソッドで、strings.Map(removeNewlinesMapper, s) を使って入力 s から改行文字を除去する処理を追加。
  • newlineFilteringReader 構造体と、その Read メソッドの実装を追加。
  • NewDecoder 関数が newlineFilteringReader をラップして返すように変更。

src/pkg/encoding/base32/base32_test.go および src/pkg/encoding/base64/base64_test.go

  • TestNewLineCharacters テストケースの拡張。様々な改行文字の組み合わせを含む入力文字列が正しくデコードされることを確認。
  • Base32のテストでは、testStringEncoding ヘルパー関数が追加され、複数の入力例に対してデコード結果を検証するようになりました。
  • Base64のテストでは、パディング文字の後に改行が続くケースが追加されました。
  • TestDecoderIssue4779 テストケースの追加。これは、Issue #4779 で報告された具体的な長いエンコード文字列(改行を含む)が、改行なしの同じ文字列と同じ結果にデコードされることを検証します。これにより、newlineFilteringReader の効果と、デコーダが改行を正しく無視できることが確認されます。

コアとなるコードの解説

newlineFilteringReader の実装 (例: base32.go)

type newlineFilteringReader struct {
	wrapped io.Reader
}

func (r *newlineFilteringReader) Read(p []byte) (int, error) {
	n, err := r.wrapped.Read(p) // 下層のリーダーからデータを読み込む
	for n > 0 { // 読み込んだデータがある限りループ
		offset := 0
		for i, b := range p[0:n] { // 読み込んだバイトを走査
			if b != '\r' && b != '\n' { // 改行文字でなければ
				if i != offset { // 既に移動済みでなければ
					p[offset] = b // 先頭に詰める
				}
				offset++ // 次の有効バイトの書き込み位置を更新
			}
		}
		if offset > 0 { // 有効なバイトが残っていれば
			return offset, err // そのバイト数とエラーを返す
		}
		// Previous buffer entirely whitespace, read again
		// 読み込んだデータが全て空白文字だった場合、再度読み込みを試みる
		n, err = r.wrapped.Read(p)
	}
	return n, err // 読み込んだバイトがないか、エラーが発生した場合
}

この Read メソッドは、io.Reader インターフェースを実装しており、内部でラップしている io.Reader からデータを読み込みます。読み込んだデータの中から \r (キャリッジリターン) と \n (ラインフィード) を取り除き、残りの有効なバイトを p の先頭に詰めて返します。これにより、デコーダは改行文字を含まないクリーンなデータを受け取ることができます。

NewDecoder 関数の変更 (例: base32.go)

// NewDecoder constructs a new base32 stream decoder.
func NewDecoder(enc *Encoding, r io.Reader) io.Reader {
	// 以前: return &decoder{enc: enc, r: r}
	return &decoder{enc: enc, r: &newlineFilteringReader{r}} // newlineFilteringReader でラップ
}

この変更により、NewDecoder を使って作成されるすべてのデコーダは、自動的に newlineFilteringReader を介して入力データを読み込むようになります。これにより、ストリームとしてBase32/Base64データをデコードする際に、入力に含まれる改行文字が透過的に処理されるようになります。

Decode および DecodeString メソッドの変更 (例: base32.go)

// New line characters (\r and \n) are ignored.
func (enc *Encoding) Decode(dst, src []byte) (n int, err error) {
	src = bytes.Map(removeNewlinesMapper, src) // 入力バイトスライスから改行文字を除去
	n, _, err = enc.decode(dst, src)
	return
}

// DecodeString returns the bytes represented by the base32 string s.
func (enc *Encoding) DecodeString(s string) ([]byte, error) {
	s = strings.Map(removeNewlinesMapper, s) // 入力文字列から改行文字を除去
	dbuf := make([]byte, enc.DecodedLen(len(s)))
	n, err := enc.Decode(dbuf, []byte(s))
	return dbuf[:n], err
}

これらのメソッドは、bytes.Map または strings.Map を使用して、デコード処理の前に直接入力データから改行文字を削除します。これにより、decode メソッドは改行文字を考慮する必要がなくなり、ロジックが簡素化されます。

関連リンク

参考にした情報源リンク

  • Base32 - Wikipedia: https://ja.wikipedia.org/wiki/Base32
  • Base64 - Wikipedia: https://ja.wikipedia.org/wiki/Base64
  • Go言語の io.Reader インターフェースに関するドキュメント
  • Go言語の bytes および strings パッケージの Map 関数に関するドキュメント
  • MIME (Multipurpose Internet Mail Extensions) におけるBase64エンコーディングの利用に関する情報
  • Go言語の encoding/base32 および encoding/base64 パッケージのソースコード (コミット前後の比較)
  • Go言語のテストコードの慣習に関する情報