それで肝心のパッチファイルを作る、つまり、バイナリ差分を作成するアルゴリズムはどのようになっているのでしょうか。
大きく分けて、二つの重要な技術的なポイントがあります。
- 新ファイルのある部分にマッチする旧ファイルの部分を高速に探す方法
- パッチファイルが小さくなるようなマッチング範囲の決定方法
一つ目は一種の全文検索の問題です。つまり、新ファイルのある場所から始まる文字列をキーに旧ファイル内を検索するわけです。それには接尾辞配列という索引を使い、その作成に接尾辞ソートを用います。接尾辞配列については Wikipedia に解説があります。ソートにはLarsson Sadakane法が使われているらしいです。
二つ目。新旧の対応関係が探せればあとは「旧データのどこから何バイトコピー、新データから何バイトコピー」といったデータを羅列していけば良いだけに思うかもしれませんが、話はそう簡単ではありません。私たちが欲しいのは最小のパッチファイルです。ある新旧のファイルがあったとき、旧から新を生成するパッチは一通りとは限りません。上述したように実行ファイルの変化は全体にまばらに広がっているので、下手にやると一致箇所と不一致箇所が細かく繰り返され、大変非効率なパッチになりかねません。上述したコントロール(ADD/INSERT操作)の回数が少ないほど余分なオーバーヘッドが小さくなりパッチファイルのサイズは小さくなるわけですが、そのような組み合わせをどのように求めたら良いでしょうか。全ての可能性を試すには膨大な時間が必要で現実的ではなさそうです。
BSDiffでは新旧が一致している部分(変更が無い部分)は旧から新へのベタCOPYではなくADD操作によって表現します。従って、1回のADD操作の中で多少の不一致部分が含まれていてもそれほど問題になりません。むしろ、一回のADDにまとめてしまった方が効率的な場合もあります。つまり、厳密な一致箇所・不一致箇所を求めるのでは無く、ある程度おおまかなマッチングを行います。これをBDiffでは近似一致(approximate matches)と呼んでいます。 つまり、新ファイルの先頭から末尾までスキャンしていきながら、旧ファイルとの間で近似的な一致範囲、ならびに同じく近似的な不一致範囲を求めていきます。そして近似一致をADD操作として、近似不一致をINSERT操作として出力していくことになります。
それでは bsdiff.c を見ていきましょう。(オンラインで本家のソースを見られるところがあまり無いので、Debianのbsdiffパッケージのソースへリンクしています)
main関数の流れは次のようになっています。
- 旧ファイルの読み込み(old, oldsize)
- 索引の作成(I) qsufsort(I,V,old,oldsize)呼び出し(Vはテンポラリ)
- 新ファイルの読み込み(new, newsize)
- ファイルヘッダーを仮出力(ブロックサイズはまだ分からないので0で仮出力)
- 差分を求める(db,eb,dblen,eblen)と同時にctrlブロックを書き出す
- 5で求めたdiffブロック(db)を書き出す
- 5で求めたextraブロック(eb)を書き出す
- ファイルヘッダーの書き直し(diffブロックとextraブロックの圧縮後サイズを書き直す)
- 終了
複雑なのは5の差分を求める部分です。
2と5を合わせてC++風にして余分なものを分離して分かりやすく変形してみました。
static void bsdiff(
u_char * const olddata, const off_t oldsize,
u_char * const newdata, const off_t newsize,
std::function<void(off_t, off_t, off_t)> writeCtrl,
std::vector<u_char> &db,
std::vector<u_char> &eb)
{
std::unique_ptr<off_t[]> I(new off_t[oldsize+1]);
{
std::unique_ptr<off_t[]> V(new off_t[oldsize+1]);
qsufsort(I.get(),V.get(),olddata,oldsize);
}
db.clear(); db.reserve(newsize+1);
eb.clear(); eb.reserve(newsize+1);
off_t scan=0;
off_t lastscan=0;
off_t lastpos=0;
off_t lastoffset=0;
while(lastscan < newsize){
off_t len=0;
off_t pos;
for(off_t scsc=scan, oldscore=0; scan<newsize;) {
len=search(I.get(),olddata,oldsize,newdata+scan,newsize-scan,
0,oldsize,&pos);
for(;scsc<scan+len;scsc++)
if((scsc+lastoffset<oldsize) &&
(olddata[scsc+lastoffset] == newdata[scsc]))
oldscore++;
if(len == oldscore && len != 0){
scan += len;
scsc = scan;
oldscore = 0;
continue;
}
if(len>oldscore+8) break;
if((scan+lastoffset<oldsize) &&
(olddata[scan+lastoffset] == newdata[scan]))
oldscore--;
++scan;
}
off_t lenf=0;
for(off_t i=0,s=0,Sf=0;(lastscan+i<scan)&&(lastpos+i<oldsize);) {
if(olddata[lastpos+i]==newdata[lastscan+i]) s++;
i++;
if(s*2-i>Sf*2-lenf) { Sf=s; lenf=i; };
};
off_t lenb=0;
if(scan<newsize) {
for(off_t i=1,s=0,Sb=0;(scan>=lastscan+i)&&(pos>=i);i++) {
if(olddata[pos-i]==newdata[scan-i]) s++;
if(s*2-i>Sb*2-lenb) { Sb=s; lenb=i; };
};
};
if(lastscan+lenf>scan-lenb) {
off_t overlap=(lastscan+lenf)-(scan-lenb);
off_t Ss=0,lens=0;
for(off_t i=0,s=0;i<overlap;i++) {
if(newdata[lastscan+lenf-overlap+i]==
olddata[lastpos+lenf-overlap+i]) s++;
if(newdata[scan-lenb+i]==
olddata[pos-lenb+i]) s--;
if(s>Ss) { Ss=s; lens=i+1; };
};
lenf+=lens-overlap;
lenb-=lens;
};
for(off_t i=0;i<lenf;i++)
db.push_back(newdata[lastscan+i]-olddata[lastpos+i]);
for(off_t i=0;i<(scan-lenb)-(lastscan+lenf);i++)
eb.push_back(newdata[lastscan+lenf+i]);
writeCtrl(
lenf,
(scan-lenb)-(lastscan+lenf),
(pos-lenb)-(lastpos+lenf));
lastscan=scan-lenb;
lastpos=pos-lenb;
lastoffset=pos-scan;
scan += len;
}
}
案外複雑ですね。ループを何回か追ってみないと何をしているのかよく分からないかもしれません。
新ファイルの内容が全て出力されるようなコントロール(ADD/INSERT操作列)を出力し終わったら一番外側のループは終了です。
一番外側のループ内では次のことをします。
-
大まかに一致・不一致範囲(領域の境目)を特定します。
まずnew[scan]から続くバイト列と一致するoldの位置と長さを求めます。
そしてoldの前回一致した領域の末尾(無ければ先頭)からその長さ分だけnewのscan位置と比較し、9バイト以上不一致がある場合、そこを大まかな境界線と考えて2へ。
9バイト以上不一致が無い、つまり、oldの前回一致した領域から続く列とnewのスキャン位置がそれほど違っていない場合、まだ前回からの一致が続いているものと考えて、scanを1進めて探索を繰り返します。
-
一致・不一致範囲を確定させます。
50%以上の一致率を目安に、 前回 一致した領域を前方に延ばした場合の長さを求めます(lenf)。
同じく50%以上の一致率を目安に、 今回 一致した領域を後方(backward:先頭へ戻る方向)に延ばした場合の長さを求めます(lenb)。
前回 一致した領域からlenfだけ前方へ延ばしたところが 今回 出力する近似一致領域の末尾です。
今回 一致した領域からlenbだけ後方へ延ばしたところが 次回 出力する近似一致領域の先頭です。
その間の一致率の低い領域が新領域だけに存在すると考える、今回出力する近似不一致領域です。
-
コントロールを出力します。
一致領域をADD操作としてdiffブロックへ、不一致領域をINSERT操作としてextraブロックへ、その長さと次の一致箇所への相対位置をctrlブロックへ出力します
かいつまんで言うと、新旧で完全に一致する領域を見つけていき、その前後にあるあまり変化していない(まばらに違いがある)領域をその一致領域に含めていく感じでしょうか。そして、その一致領域の間にある領域を不一致領域とします。
各変数の位置関係はおおむね次図のようになります。