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

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

このコミットは、Go言語の実験的なUnicode正規化ライブラリ exp/norm における2つの重要なバグ修正と、パフォーマンスの改善を目的としています。具体的には、String メソッドにおける出力バッファの長さの誤り、および patchTail 関数における文字の損失バグが修正されました。これらのバグは、Unicode文字列の正規化処理において、予期せぬ結果やデータ破損を引き起こす可能性がありました。

コミット

  • コミットハッシュ: cadbd3ea4986a43eebb1be3cacdce346513d537f
  • 作者: Marcel van Lohuizen mpvl@golang.org
  • 日付: Fri Dec 23 18:21:26 2011 +0100
  • コミットメッセージ:
    exp/norm: fixed two unrelated bugs in normalization library.
    1) incorrect length given for out buffer in String.
    2) patchTail bug that could cause characters to be lost
       when crossing into the out-buffer boundary.
    
    Added tests to expose these bugs. Also slightly improved
    performance of Bytes() and String() by sharing the reorderBuffer
    across operations.
    
    Fixes #2567.
    
    R=r
    CC=golang-dev
    https://golang.org/cl/5502069
    

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

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

元コミット内容

exp/norm: fixed two unrelated bugs in normalization library.
1) incorrect length given for out buffer in String.
2) patchTail bug that could cause characters to be lost
   when crossing into the out-buffer boundary.

Added tests to expose these bugs. Also slightly improved
performance of Bytes() and String() by sharing the reorderBuffer
across operations.

Fixes #2567.

R=r
CC=golang-dev
https://golang.org/cl/5502069

変更の背景

このコミットは、Go言語のUnicode正規化ライブラリ exp/norm における既知の不具合を修正するために行われました。主な背景は以下の2点です。

  1. String メソッドのバッファ長計算の誤り: String メソッドが正規化された文字列を格納するための出力バッファの長さを誤って計算していたため、結果として不正確な文字列が生成される可能性がありました。
  2. patchTail 関数における文字損失バグ: patchTail 関数は、不正なUTF-8シーケンスや結合文字の処理に関連するもので、特定の条件下で文字が失われる可能性のあるバグを抱えていました。これは、特にバッファ境界をまたぐ場合に顕著でした。

これらのバグは、Unicode文字列の正確な正規化を妨げ、アプリケーションの国際化対応において問題を引き起こす可能性があったため、修正が急務でした。また、これらのバグを露呈させるためのテストが追加され、同時に Bytes() および String() メソッドのパフォーマンス改善も図られました。

前提知識の解説

Unicode正規化 (Unicode Normalization)

Unicodeは、世界中の多様な文字体系を表現するための文字コード標準です。しかし、同じ文字や文字の組み合わせが複数の異なるバイトシーケンスで表現されることがあります。例えば、アクセント付きの文字「é」は、単一のコードポイント(U+00E9)で表現することもできますし、「e」と結合文字の「´」(U+0065 U+0301)の組み合わせで表現することもできます。

このような複数の表現が存在すると、文字列の比較や検索が困難になります。この問題を解決するために、Unicode標準では「正規化 (Normalization)」というプロセスが定義されています。正規化は、論理的に等価な文字列が常に同じバイナリ表現を持つように変換するプロセスです。

Unicodeには主に4つの正規化形式があります。

  • NFC (Normalization Form C - 結合済み正規化形式): 可能な限り結合文字をプリコンポーズド文字(単一のコードポイントで表現される文字)に変換します。例えば、「e」と「´」は「é」に変換されます。
  • NFD (Normalization Form D - 分解済み正規化形式): 可能な限りプリコンポーズド文字を分解し、基本文字と結合文字のシーケンスに変換します。例えば、「é」は「e」と「´」に変換されます。
  • NFKC (Normalization Form KC - 互換性結合済み正規化形式): NFCと同様に結合文字をプリコンポーズド文字に変換しますが、さらに互換性分解も行います。これにより、見た目は異なるが意味的に等価な文字(例: 全角数字と半角数字)も同じ表現に正規化されます。
  • NFKD (Normalization Form KD - 互換性分解済み正規化形式): NFDと同様に分解を行いますが、NFKCと同様に互換性分解も行います。

Go言語の exp/norm (現在の golang.org/x/text/unicode/norm) パッケージは、これらのUnicode正規化形式をGoプログラムで扱うための機能を提供します。

UTF-8

UTF-8は、Unicode文字をバイトシーケンスにエンコードするための可変長エンコーディング方式です。ASCII文字は1バイトで表現され、それ以外の文字は2バイトから4バイトで表現されます。UTF-8は、Unicodeの全ての文字を表現でき、ASCIIとの互換性があるため、ウェブや多くのシステムで広く利用されています。

Go言語の exp パッケージ

Go言語の標準ライブラリには、実験的な機能やまだ安定版ではない機能が exp (experimental) パッケージとして提供されることがあります。このコミットで言及されている exp/norm は、Unicode正規化に関する初期の実験的な実装であったと考えられます。後に、この機能は golang.org/x/text/unicode/norm としてGoの公式エクステンションライブラリの一部となりました。

技術的詳細

このコミットは、exp/norm パッケージ内のUnicode正規化処理の正確性と堅牢性を向上させるためのものです。

  1. String メソッドのバッファ長修正:

    • Form.String(s string) string メソッドは、入力文字列 s を正規化し、その結果を新しい文字列として返します。
    • 元の実装では、このメソッドが内部的に使用する出力バッファの初期サイズ計算に誤りがありました。これにより、正規化後の文字列が元の文字列よりも長くなる場合に、バッファが不足し、不正確な結果やパニックを引き起こす可能性がありました。
    • 修正では、make([]byte, n, len(s)) の部分で、len(s) を容量として指定することで、元の文字列の長さを考慮した適切なバッファ容量を確保するように変更されました。これにより、バッファの再割り当てが減り、パフォーマンスも向上します。
  2. patchTail 関数のバグ修正:

    • patchTail 関数は、正規化処理中に発生する可能性のある、不正なUTF-8シーケンスや、結合文字がバッファの末尾で途切れるようなエッジケースを処理するために使用されます。
    • 元の実装では、この関数がバッファ境界をまたぐ文字(特に結合文字)を処理する際に、一部の文字が誤って失われる可能性がありました。これは、doAppend 関数内で patchTail が呼び出される際のロジックに起因していました。
    • 修正では、patchTail の戻り値が ([]byte, int) から ([]byte, bool) に変更され、文字が失われたかどうかではなく、末尾に不正な継続バイトがあったかどうかを示すようになりました。また、不正なUTF-8シーケンスが検出された場合に、その部分を適切に処理し、残りのバッファと結合するロジックが追加されました。これにより、文字の損失が防止され、不正な入力に対してもより堅牢になりました。
  3. reorderBuffer の共有によるパフォーマンス改善:

    • Bytes() および String() メソッド内で、reorderBuffer という内部バッファが各操作で新しく初期化されていました。
    • 修正では、これらのメソッドが reorderBuffer を共有するように変更されました。これにより、オブジェクトの生成とガベージコレクションのオーバーヘッドが削減され、特に頻繁に正規化操作が行われる場合にパフォーマンスが向上します。

これらの変更は、Unicode正規化の正確性を保証し、Go言語で国際化対応アプリケーションを開発する際の信頼性を高める上で非常に重要です。

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

このコミットでは、主に以下のファイルが変更されています。

  • src/pkg/exp/norm/input.go: input インターフェースの skipNonStarter メソッドのシグネチャが変更され、p int 引数が追加されました。これにより、非スターターバイトのスキップ開始位置を指定できるようになりました。
  • src/pkg/exp/norm/normalize.go:
    • Form.Bytes() および Form.String() メソッドで reorderBuffer の初期化方法が変更され、共有されるようになりました。
    • Form.String() メソッドの出力バッファの初期容量計算が修正されました。
    • patchTail 関数のシグネチャと内部ロジックが変更され、文字損失バグが修正されました。
    • doAppend 関数のシグネチャと内部ロジックが変更され、patchTail の変更に対応し、非スターターバイトの処理が改善されました。
    • Form.QuickSpan() メソッドの戻り値が n を直接返すように変更されました。
  • src/pkg/exp/norm/normalize_test.go:
    • appendTests に新しいテストケースが追加され、patchTail のバグを露呈させるためのテストが強化されました。
    • TestBytesTestString のテスト関数が追加され、Bytes()String() メソッドの動作が検証されるようになりました。
  • src/pkg/exp/norm/readwriter.go: doAppend の呼び出しに 0 が追加され、doAppend のシグネチャ変更に対応しました。

具体的なコードの変更内容については、GitHubのコミットページで差分を確認できます。

コアとなるコードの解説

このコミットの核となる変更は、Unicode正規化処理の正確性と効率性を向上させるためのものです。

  1. Form.String() のバッファ容量修正:

    // normalize.go
    // 変更前:
    // out := make([]byte, 0, len(s))
    // 変更後:
    out := make([]byte, n, len(s)) // nはQuickSpanで計算された、正規化不要なプレフィックスの長さ
    

    この変更は、String() メソッドが正規化された文字列を格納するためのバイトスライス out を作成する際に、その初期容量をより適切に設定するようにします。nQuickSpan によって計算された、正規化が不要な文字列のプレフィックスの長さです。これにより、正規化によって文字列が長くなる場合でも、初期段階で十分な容量が確保され、不要な再割り当てが減り、パフォーマンスが向上します。

  2. patchTail 関数の修正:

    // normalize.go
    // 変更前:
    // func patchTail(rb *reorderBuffer, buf []byte) ([]byte, int) { ... }
    // 変更後:
    func patchTail(rb *reorderBuffer, buf []byte) ([]byte, bool) {
        // ...
        if extra > 0 {
            // Potentially allocating memory. However, this only
            // happens with ill-formed UTF-8.
            x := make([]byte, 0)
            x = append(x, buf[len(buf)-extra:]...)
            buf = decomposeToLastBoundary(rb, buf[:end])
            if rb.f.composing {
                rb.compose()
            }
            buf = rb.flush(buf)
            return append(buf, x...), true // 不正な継続バイトがあったことを示す
        }
        return buf, false
    }
    

    patchTail は、入力の末尾に不正なUTF-8シーケンスや、結合文字が途中で切れているような場合に、その部分を適切に処理するための関数です。変更前は、失われたバイト数を示す int を返していましたが、変更後は不正な継続バイトがあったかどうかを示す bool を返すようになりました。これにより、doAppend 関数でのエラーハンドリングがより明確になります。特に、extra > 0 のブロックでは、不正な継続バイトを一時的に保持し、正規化処理後に元のバッファに結合することで、文字の損失を防いでいます。

  3. doAppend 関数の変更:

    // normalize.go
    // 変更前:
    // func doAppend(rb *reorderBuffer, out []byte) []byte { ... }
    // 変更後:
    func doAppend(rb *reorderBuffer, out []byte, p int) []byte {
        // ...
        if q := src.skipNonStarter(p); q > p {
            out = src.appendSlice(out, p, q)
            buf, endsInError := patchTail(rb, out)
            if endsInError {
                out = buf
                doMerge = false // no need to merge, ends with illegal UTF-8
            } else {
                out = decomposeToLastBoundary(rb, buf) // force decomposition
            }
            p = q
        }
        // ...
    }
    

    doAppend は、正規化の主要なロジックを含む関数です。この変更では、patchTail の戻り値の変更に対応し、endsInError フラグに基づいて処理を分岐させています。もし patchTailtrue を返した場合(不正な継続バイトがあった場合)、そのバッファをそのまま使用し、それ以上のマージは行いません。これにより、不正な入力に対する堅牢性が向上します。また、skipNonStarterp 引数が追加され、スキップ開始位置をより細かく制御できるようになりました。

これらの変更は、Go言語のUnicode正規化ライブラリが、より正確で堅牢、そして効率的に動作するようにするための重要な改善です。

関連リンク

  • Go Gerrit Change: https://golang.org/cl/5502069
  • 関連するGo Issue: #2567 (このコミットは、当時のGoのIssue TrackerにおけるIssue 2567を修正したものです。2011年当時のIssue Trackerは現在のGitHubとは異なるプラットフォームであった可能性が高く、直接的なリンクは提供できませんが、コミットメッセージに明記されています。)

参考にした情報源リンク