前回記事の続きです。筋悪であろうともsqlite_escape_string()の自前実装をすべく、調べます。
SQLite2モジュール
sqlite_escape_string()
https://github.com/php/php-src/blob/PHP-5.3.29/ext/sqlite/sqlite.c#L3153
- 空文字列なら空文字列を返す
- 「先頭が\x01」または「\0を含んでいる」ならsqlite_encode_binary()を呼び出す。エンコード済み文字列の前に\x01を付与してreturnする。
- 1と2に当てはまらないならsqlite_mprintf(“%q”)を呼び出す。
つまり、PHPはエンコード済みフラグとして「\x01が0バイト目にあるかどうか」を用いています。fetchする部分でフラグをみてデコードが必要かどうかを判定しています。このフラグはPHP独自のもので、SQLiteのものではありません。
「先頭が\x01」のときにもsqlite_encode_binary()を呼び出すのは、2重エンコードとなった時に常に2重エンコードするためです。2重エンコード時にsqlite_mprintf(“%q”)に流れてしまうと、2重デコードができなくなります。
sqlite_encode_binary()
http://www.sqlite.org/cgi/src/artifact/fc8c51f0b61bc803
- 「適切な数値」e を選ぶ
- 文字列のすべてのバイトに対して数値eを引く(ただし8ビットなのでmod 256)
- 次のルールで各バイトを置換する。
0x00 -> 0x01 0x01
0x01 -> 0x01 0x02
0x27 -> 0x01 0x28
- 置換済み文字列の前に数値eを付与してreturnする
エンコードした結果には\0も’も含まれていないので、”で囲えばリテラル文字列として安全に使用できます。
つまり、SQLite2におけるBLOBとは「巧妙にエスケープされているstring」ということになります。
ソースコードのコメントには数値eを「足す」とあるのですが、コード上は引いています。正確には「オフセットを足す」とあるので、計算上は引き算となるのかもしれません。
トリッキーな処理である「数値eを引く」理由は容量効率のためです。
例えば\0が100個並ぶ100バイトの文字列を単純にデコードすると200バイトに膨れ上がりますが、e = 1を各バイトから引けば\xFFが100個並ぶ文字列となり、エンコード後は100+1バイトで済みます。
数値eはエンコード後の容量が一番小さくなるように計算されます。
sqlite_decode_binary()
http://www.sqlite.org/cgi/src/artifact/fc8c51f0b61bc803
- 入力0バイト目から数値eを取得する
- 入力1バイト目以降を次のルールで置換する
0x01 0x01 -> 0x00
0x01 0x02 -> 0x01
0x01 0x28 -> 0x27
- 置換後の各バイトに数値eを足す(ただし8ビットなのでmod 256)
単にsqlite_encode_binary()の逆算です。
置換ルールは3種類あるように見せて、その実「\x01があったら\x01そのものは読み飛ばし、次の文字から1を引く」というルールになっています。このためデコードの計算量は極限まで抑えられています。
SQLite3モジュール
SQLite3::escapeString()
https://github.com/php/php-src/blob/PHP-5.6.13/ext/sqlite3/sqlite3.c#L441
- 空文字列なら空文字列を返す
- 1に当てはまらないならsqlite3_mprintf(“%q”)を呼び出す。
sqlite_escape_string()からバイナリ向け処理が間引かれています。そのためエンコード済みフラグである\x01も存在しなくなり、fetchからもなくなっています。
これにより、SQLite2から3に.dump経由でデータ移行した場合、PHPから見るとBLOBに互換性がない(エンコード後文字列がそのまま見えてしまう)ということになります。移行時にデータ破壊が起きたように見えたのはこれが原因のようです。
やるべきことが見えてきた
sqlite_escape_string()をSQLite3で再現するには
- エンコード時は上記sqlite_escape_string()と同じ処理をする
- fetch時にエンコード済みフラグがあればデコードする
ということになります。
fetch部分にも手を入れないといけないのがあいたたたですが、全部プリペアドステートメントに置き換えるよりは現実的な工数になるでしょう。
2015年10月5日追記
実装しました。
https://github.com/noldor/kino2/commit/8059e70df9d592305f7d6a1a5e1364b9e49a043a
最近のコメント