libdxfrwのDWGパース時の整数アンダーフロー修正

libdxfrwは、DXF/DWGファイルの読み書きを行うC++ライブラリです。FessでDWGファイルのテキスト抽出に利用しているのですが、特定のDWGファイルをパースする際に、メモリを80GB以上消費して実質的に無限ループに陥るという問題が発生しました。

原因

問題の原因は、DWGファイルのクラス情報をパースする処理における符号なし整数のアンダーフローでした。

dwgreader18.cppにある元のコードは以下のようになっていました。

duint32 endDataPos = maxClassNum - 499;

DWGフォーマットでは、クラス番号0〜499は予約済みの組み込みクラスで、カスタムクラスは500番から始まります。そのため、maxClassNumが499以下の場合、カスタムクラスは存在しません。

しかし、duint32(符号なし32ビット整数)でmaxClassNum - 499を計算すると、maxClassNumが499以下の場合にアンダーフローが発生します。例えば、DWG TrueViewで作成されたAC1032形式のファイルではmaxClassNum = 236となることがあり、この場合:

  • 元のコード: 236 - 499 = 4294967033(符号なし整数のラップアラウンド)

この巨大な値がループ回数として使われるため、メモリの大量消費と実質的な無限ループが発生していました。

修正内容

修正はシンプルで、減算前に境界チェックを追加しました。

// Classes 0-499 are built-in; custom classes start at 500.
// If maxClassNum <= 499, there are no custom classes to parse.
duint32 endDataPos = (maxClassNum > 499) ? (maxClassNum - 499) : 0;

maxClassNumが499以下の場合はendDataPos = 0となり、カスタムクラスのパースをスキップします。この修正はdwgreader18.cppdwgreader21.cppの両方に適用しました。

修正後の計算結果:

  • maxClassNum = 236: endDataPos = 0(カスタムクラスなし)
  • maxClassNum = 499: endDataPos = 0(カスタムクラスなし)
  • maxClassNum = 500: endDataPos = 1(カスタムクラス1つ)
  • maxClassNum = 1000: endDataPos = 501(カスタムクラス501個)

テスト

今回の修正に合わせて、ユニットテストとインテグレーションテストも追加しました。境界値のテストや、メモリ安全性の確認を行っています。

符号なし整数のアンダーフローは、C/C++ではよくあるバグですが、今回のようにメモリ消費が爆発するケースは影響が大きいので、早めに対処できてよかったです。

関連リンク

Fess Crawlerのエクストラクターにweight設定を追加

Fess Crawlerのエクストラクターにweightを指定できるようにしました。これにより、同じMIMEタイプに対して複数のエクストラクターが登録されている場合に、優先度を制御できるようになります。

背景

Fess Crawlerでは、ドキュメントからテキストを抽出するためにエクストラクター(Extractor)を利用しています。エクストラクターはMIMEタイプに基づいて選択されますが、同じMIMEタイプに対して複数のエクストラクターが存在する場合、どちらを優先するかを制御する仕組みがありませんでした。

変更内容

ExtractorインターフェースにgetWeight()メソッドをデフォルトメソッドとして定義し、AbstractExtractor基底クラスにweightフィールドを追加しました。

Extractorインターフェースでは、デフォルトのweightとして1を返すようになっています。

public interface Extractor {
    ExtractData getText(InputStream in, Map<String, String> params);

    default int getWeight() {
        return 1;
    }
}

AbstractExtractorでは、weightフィールドとsetter/getterを実装しています。

public abstract class AbstractExtractor implements Extractor {
    protected int weight = 1;

    @Override
    public int getWeight() {
        return weight;
    }

    public void setWeight(final int weight) {
        this.weight = weight;
    }
}

設定方法

Fessの設定ファイル(XML)で、エクストラクターのweightを指定できます。weightの値が大きいエクストラクターが優先的に使用されます。

<component name="tikaExtractor" class="org.codelibs.fess.crawler.extractor.impl.TikaExtractor">
    <property name="weight">10</property>
</component>

デフォルトのweightは1なので、特に設定しなければ従来と同じ動作になります。

まとめ

この変更により、エクストラクターの優先度をweight値で柔軟に制御できるようになりました。カスタムエクストラクターを追加する際に、既存のエクストラクターとの優先順位を設定ファイルで簡単に調整できます。

fess-crawlerにPostScriptテキスト抽出機能を追加

fess-crawlerに、PostScript(.ps)ファイルからテキストを抽出するPsExtractorを追加しました。これにより、Fessのクロール対象としてPostScriptファイルも扱えるようになります。

PostScriptとは

PostScriptはAdobe Systemsが開発したページ記述言語で、印刷やDTPの分野で広く使われてきたフォーマットです。PostScriptファイルにはテキスト描画命令が含まれていますが、プログラミング言語としての側面もあり、テキストの抽出は単純ではありません。

PsExtractorの仕組み

PsExtractorは、PostScriptのshow系オペレータを解析してテキストを抽出します。対応しているオペレータは以下の通りです。

オペレータ説明
show基本的なテキスト描画
ashow文字間隔調整付きテキスト描画
widthshow特定文字の幅調整付きテキスト描画
awidthshowashow + widthshow の組み合わせ
kshowカーニングプロシージャ付きテキスト描画
xshow個別X座標指定のテキスト描画
yshow個別Y座標指定のテキスト描画
xyshow個別XY座標指定のテキスト描画

文字列リテラルとしては、括弧形式の文字列((Hello World))と16進文字列(<48656C6C6F>)の両方に対応しています。括弧形式の文字列では、エスケープシーケンス(\n\t、8進数など)やネストされた括弧も正しく処理されます。

DI設定

extractor.xmlpsExtractorコンポーネントを登録し、application/postscript MIMEタイプにマッピングしています。

制限事項

現在の実装では以下のケースには対応していません。

  • ループやプロシージャによる動的なテキスト生成
  • フォントエンコーディングの再定義
  • バイナリエンコードされたPostScriptファイル

静的にshow系オペレータで描画されるテキストの抽出に特化した実装となっています。

テスト

12件のテストケースを作成し、基本的なテキスト抽出、16進文字列、エスケープシーケンス、ネストされた括弧、空コンテンツ、各種show系オペレータなどの動作を検証しています。

変更の詳細はPR #140を参照してください。