前回記事の続きです。筋悪であろうともsqlite_escape_string()の自前実装をすべく、調べます。

SQLite2モジュール

sqlite_escape_string()

https://github.com/php/php-src/blob/PHP-5.3.29/ext/sqlite/sqlite.c#L3153

  1. 空文字列なら空文字列を返す
  2. 「先頭が\x01」または「\0を含んでいる」ならsqlite_encode_binary()を呼び出す。エンコード済み文字列の前に\x01を付与してreturnする。
  3. 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

  1. 「適切な数値」e を選ぶ
  2. 文字列のすべてのバイトに対して数値eを引く(ただし8ビットなのでmod 256)
  3. 次のルールで各バイトを置換する。
    0x00 -> 0x01 0x01
    0x01 -> 0x01 0x02
    0x27 -> 0x01 0x28
  4. 置換済み文字列の前に数値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

  1. 入力0バイト目から数値eを取得する
  2. 入力1バイト目以降を次のルールで置換する
    0x01 0x01 -> 0x00
    0x01 0x02 -> 0x01
    0x01 0x28 -> 0x27
  3. 置換後の各バイトに数値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. 空文字列なら空文字列を返す
  2. 1に当てはまらないならsqlite3_mprintf(“%q”)を呼び出す。

sqlite_escape_string()からバイナリ向け処理が間引かれています。そのためエンコード済みフラグである\x01も存在しなくなり、fetchからもなくなっています。

これにより、SQLite2から3に.dump経由でデータ移行した場合、PHPから見るとBLOBに互換性がない(エンコード後文字列がそのまま見えてしまう)ということになります。移行時にデータ破壊が起きたように見えたのはこれが原因のようです。

やるべきことが見えてきた

sqlite_escape_string()をSQLite3で再現するには

  1. エンコード時は上記sqlite_escape_string()と同じ処理をする
  2. fetch時にエンコード済みフラグがあればデコードする

ということになります。
fetch部分にも手を入れないといけないのがあいたたたですが、全部プリペアドステートメントに置き換えるよりは現実的な工数になるでしょう。

2015年10月5日追記

実装しました。

https://github.com/noldor/kino2/commit/8059e70df9d592305f7d6a1a5e1364b9e49a043a