| 1 | <?php |
| 2 | /************************************************************* |
| 3 | # 076 License |
| 4 | |
| 5 | Copyright (c) テクニカル諏訪子 |
| 6 | |
| 7 | Permission is hereby granted to any person obtaining a copy of the software |
| 8 | Little Beast (the "Software") to use, modify, merge, copy, publish, distribute, |
| 9 | sublicense, and/or sell copies of the Software, subject to the following conditions: |
| 10 | |
| 11 | 1. **Origin Attribution**: |
| 12 | - You must not misrepresent the origin of the Software; you must not claim |
| 13 | you created the original Software. |
| 14 | - If the Software is used in a product, you must either: |
| 15 | a. Provide clear attribution in the product's documentation, user interface, |
| 16 | or other visible areas, **OR** |
| 17 | b. Pay the original developers a fee they specify in writing. |
| 18 | 2. **Usage Restriction**: |
| 19 | - The Software, or any derivative works, dependencies, or libraries |
| 20 | incorporating it, must not be used for censorship or to suppress freedom of |
| 21 | speech, expression, or creativity. Prohibited uses include, but are not |
| 22 | limited to: |
| 23 | - Censorship of so-called "hate speech", visuals, non-mainstream opinions, |
| 24 | ideas, or objective reality. |
| 25 | - Tools or systems designed to restrict access to information or |
| 26 | artistic works. |
| 27 | 3. **Notice Preservation**: |
| 28 | - This license and the above copyright notice must remain intact in all copies |
| 29 | of the source code. |
| 30 | |
| 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, |
| 32 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A |
| 33 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT |
| 34 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF |
| 35 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE |
| 36 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 37 | */ |
| 38 | // テスト用の偽物のMySQLサーバー(MockServerが起動する)。 |
| 39 | // 使い方:php mysql-server.php ポート [既定の認証プラグイン] [証明書のPEM] [秘密鍵のPEM] |
| 40 | // SQLはメモリーのSQLiteで実行し、結果をMySQLのプロトコル(テキスト・バイナリ)で返す(pdo_sqliteが必要)。 |
| 41 | // 証明書と秘密鍵を渡すと、TLSとRSA(caching_sha2_password・sha256_passwordの完全な認証)に対応する。 |
| 42 | // |
| 43 | // ユーザー(パスワード、認証プラグイン): |
| 44 | // lb(ひみつ、mysql_native_password)、nopw(なし、mysql_native_password)、 |
| 45 | // sha2(sha2ひみつ、caching_sha2_password)、sha256(sha256ひみつ、sha256_password)、 |
| 46 | // clear(clearひみつ、mysql_clear_password、TLSのみ) |
| 47 | // データベース:「nodb」以外は何でも存在する事にする |
| 48 | // 特別なSQL: |
| 49 | // SELECT LB_DISCONNECT() 返事をせずに接続を切る |
| 50 | // SELECT LB_SLEEP(秒) 返事を遅らせる |
| 51 | // CALL lb_multi() 結果を2つ返す |
| 52 | // SHOW FULL TABLES、SHOW CREATE TABLE(このサーバーで作ったテーブルの元のSQL) |
| 53 | // @@lb_tls・@@lb_plugin・@@lb_full_auth・@@lb_caps・@@character_set_client・@@version、 |
| 54 | // DATABASE()・CONNECTION_ID()・VERSION() |
| 55 | |
| 56 | require_once __DIR__.'/../../../../autoload.php'; |
| 57 | |
| 58 | use Std\Lib\Crypto\Rsa; |
| 59 | use Std\Lib\Tls; |
| 60 | |
| 61 | $port = (int)($argv[1] ?? 0); |
| 62 | $defaultPlugin = $argv[2] ?? 'mysql_native_password'; |
| 63 | $certFile = $argv[3] ?? ''; |
| 64 | $keyFile = $argv[4] ?? ''; |
| 65 | |
| 66 | if (!class_exists('PDO') || !in_array('sqlite', PDO::getAvailableDrivers(), true)) { |
| 67 | fwrite(STDOUT, "error pdo_sqlite\n"); |
| 68 | exit(1); |
| 69 | } |
| 70 | |
| 71 | $server = @stream_socket_server("tcp://127.0.0.1:{$port}", $errno, $errstr); |
| 72 | if (!$server) { |
| 73 | fwrite(STDOUT, "error {$errstr}\n"); |
| 74 | exit(1); |
| 75 | } |
| 76 | |
| 77 | $my = new MockMysql($defaultPlugin, $certFile, $keyFile); |
| 78 | fwrite(STDOUT, "ready\n"); |
| 79 | fflush(STDOUT); |
| 80 | $my->run($server); |
| 81 | |
| 82 | class MockMysql { |
| 83 | private const VERSION = '8.4.0-lbmock'; |
| 84 | |
| 85 | // 機能フラグ |
| 86 | private const CLIENT_CONNECT_WITH_DB = 0x00000008; |
| 87 | private const CLIENT_PROTOCOL_41 = 0x00000200; |
| 88 | private const CLIENT_SSL = 0x00000800; |
| 89 | private const CLIENT_SECURE_CONNECTION = 0x00008000; |
| 90 | private const CLIENT_PLUGIN_AUTH = 0x00080000; |
| 91 | private const CLIENT_CONNECT_ATTRS = 0x00100000; |
| 92 | private const CLIENT_PLUGIN_AUTH_LENENC = 0x00200000; |
| 93 | private const SERVER_CAPS = 0x00000001 | 0x00000002 | 0x00000004 | 0x00000008 | 0x00000200 | 0x00002000 |
| 94 | | 0x00008000 | 0x00020000 | 0x00040000 | 0x00080000 | 0x00100000 | 0x00200000; |
| 95 | |
| 96 | // 状況 |
| 97 | private const STATUS_IN_TRANS = 0x0001; |
| 98 | private const STATUS_AUTOCOMMIT = 0x0002; |
| 99 | private const STATUS_MORE_RESULTS = 0x0008; |
| 100 | |
| 101 | // 種類 |
| 102 | private const T_DECIMAL = 0xF6, T_TINY = 0x01, T_SHORT = 0x02, T_LONG = 0x03, T_FLOAT = 0x04, T_DOUBLE = 0x05, |
| 103 | T_NULL = 0x06, T_TIMESTAMP = 0x07, T_LONGLONG = 0x08, T_INT24 = 0x09, T_DATE = 0x0A, T_TIME = 0x0B, |
| 104 | T_DATETIME = 0x0C, T_YEAR = 0x0D, T_JSON = 0xF5, T_BLOB = 0xFC, T_VAR_STRING = 0xFD, T_STRING = 0xFE; |
| 105 | |
| 106 | private const USERS = [ |
| 107 | 'lb' => ['ひみつ', 'mysql_native_password'], |
| 108 | 'nopw' => ['', 'mysql_native_password'], |
| 109 | 'sha2' => ['sha2ひみつ', 'caching_sha2_password'], |
| 110 | 'sha256' => ['sha256ひみつ', 'sha256_password'], |
| 111 | 'clear' => ['clearひみつ', 'mysql_clear_password'], |
| 112 | ]; |
| 113 | |
| 114 | private const CHARSETS = [45 => 'utf8mb4', 33 => 'utf8mb3', 8 => 'latin1', 11 => 'ascii', 63 => 'binary', 12 => 'ujis', 13 => 'sjis', 95 => 'cp932', 97 => 'eucjpms', 255 => 'utf8mb4']; |
| 115 | |
| 116 | private \PDO $pdo; |
| 117 | private array $conns = []; |
| 118 | /** テーブル => 元のCREATE TABLE(SHOW CREATE TABLE用) */ |
| 119 | private array $ddl = []; |
| 120 | private int $nextId = 1; |
| 121 | private int $nextStmt = 1; |
| 122 | private array $sha2Cache = []; |
| 123 | private $privKey = null; |
| 124 | private string $pubPem = ''; |
| 125 | |
| 126 | public function __construct(private string $defaultPlugin, private string $certFile, private string $keyFile) { |
| 127 | $this->pdo = new \PDO('sqlite::memory:'); |
| 128 | $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); |
| 129 | if ($keyFile !== '') { |
| 130 | $this->privKey = Rsa::fromPem((string)file_get_contents($keyFile)); |
| 131 | $this->pubPem = $this->privKey->publicPem(); |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | public function run($server): void { |
| 136 | while (true) { |
| 137 | // TLSで既に復号されたデータが残っている接続を先に |
| 138 | $pending = []; |
| 139 | foreach ($this->conns as $id => $cn) { |
| 140 | if ($cn['tls'] && Tls::pending($cn['sock'])) $pending[] = $id; |
| 141 | } |
| 142 | |
| 143 | if (empty($pending)) { |
| 144 | $r = [$server]; |
| 145 | foreach ($this->conns as $cn) $r[] = $cn['sock']; |
| 146 | $w = $e = null; |
| 147 | $n = @stream_select($r, $w, $e, 120); |
| 148 | if ($n === false || $n === 0) return; |
| 149 | |
| 150 | foreach ($r as $s) { |
| 151 | if ($s === $server) { |
| 152 | $c = @stream_socket_accept($server, 5); |
| 153 | if ($c) $this->accept($c); |
| 154 | continue; |
| 155 | } |
| 156 | foreach ($this->conns as $id => $cn) { |
| 157 | if ($cn['sock'] === $s) $pending[] = $id; |
| 158 | } |
| 159 | } |
| 160 | } |
| 161 | |
| 162 | foreach ($pending as $id) { |
| 163 | if (!isset($this->conns[$id])) continue; |
| 164 | $res = $this->handle($id); |
| 165 | if ($res === 'quit') return; |
| 166 | if ($res === false) $this->drop($id); |
| 167 | } |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | /////////////// |
| 172 | // 接続・認証 |
| 173 | |
| 174 | private function accept($c): void { |
| 175 | stream_set_blocking($c, true); |
| 176 | stream_set_timeout($c, 30); |
| 177 | // 読み込みのバッファを無くす(SSLリクエストと一緒にTLSのClientHelloまで読んでしまうと、TLSを開始出来ない) |
| 178 | stream_set_read_buffer($c, 0); |
| 179 | $id = $this->nextId++; |
| 180 | $scramble = ''; |
| 181 | for ($i = 0; $i < 20; $i++) $scramble .= chr(random_int(33, 126)); |
| 182 | |
| 183 | $caps = self::SERVER_CAPS | ($this->certFile !== '' ? self::CLIENT_SSL : 0); |
| 184 | $this->conns[$id] = [ |
| 185 | 'sock' => $c, 'id' => $id, 'state' => 'new', 'seq' => 0, 'tls' => false, 'caps' => 0, |
| 186 | 'user' => '', 'plugin' => '', 'scramble' => $scramble, 'db' => '', 'charset' => 45, |
| 187 | 'fullAuth' => false, 'stmts' => [], 'inTrans' => false, |
| 188 | ]; |
| 189 | |
| 190 | $g = chr(10).self::VERSION."\0".pack('V', $id).substr($scramble, 0, 8)."\0" |
| 191 | .pack('v', $caps & 0xFFFF).chr(255).pack('v', self::STATUS_AUTOCOMMIT).pack('v', $caps >> 16) |
| 192 | .chr(21).str_repeat("\0", 10).substr($scramble, 8)."\0".$this->defaultPlugin."\0"; |
| 193 | $this->send($id, $g); |
| 194 | } |
| 195 | |
| 196 | private function drop(int $id): void { |
| 197 | @fclose($this->conns[$id]['sock']); |
| 198 | unset($this->conns[$id]); |
| 199 | } |
| 200 | |
| 201 | /** |
| 202 | * 1つのパケットを処理する |
| 203 | * |
| 204 | * @return bool|string false=接続を切る、'quit'=サーバーを止める |
| 205 | */ |
| 206 | private function handle(int $id): bool|string { |
| 207 | $cn = &$this->conns[$id]; |
| 208 | |
| 209 | if ($cn['state'] === 'new') { |
| 210 | // MockServer::stop()の終了のリクエスト |
| 211 | $head = $this->readRaw($id, 4); |
| 212 | if ($head === null) return false; |
| 213 | if ($head === 'QUIT') return 'quit'; |
| 214 | $pkt = $this->readRest($id, $head); |
| 215 | if ($pkt === null) return false; |
| 216 | return $this->handshake($id, $pkt); |
| 217 | } |
| 218 | |
| 219 | $pkt = $this->recv($id); |
| 220 | if ($pkt === null || $pkt === '') return false; |
| 221 | return $this->command($id, $pkt); |
| 222 | } |
| 223 | |
| 224 | private function handshake(int $id, string $pkt): bool { |
| 225 | $cn = &$this->conns[$id]; |
| 226 | $caps = unpack('V', $pkt)[1]; |
| 227 | |
| 228 | // SSLリクエスト(32バイト) |
| 229 | if (($caps & self::CLIENT_SSL) && strlen($pkt) === 32) { |
| 230 | if ($this->certFile === '') return false; |
| 231 | try { |
| 232 | $cn['sock'] = Tls::accept($cn['sock'], ['cert' => $this->certFile, 'key' => $this->keyFile]); |
| 233 | } catch (\Throwable $e) { |
| 234 | return false; |
| 235 | } |
| 236 | $cn['tls'] = true; |
| 237 | $pkt = $this->recv($id); |
| 238 | if ($pkt === null) return false; |
| 239 | $caps = unpack('V', $pkt)[1]; |
| 240 | } |
| 241 | |
| 242 | if (!($caps & self::CLIENT_PROTOCOL_41)) { |
| 243 | $this->err($id, 1251, '08004', 'Client does not support authentication protocol requested by server'); |
| 244 | return false; |
| 245 | } |
| 246 | |
| 247 | $cn['caps'] = $caps; |
| 248 | $cn['charset'] = ord($pkt[8]); |
| 249 | $pos = 32; |
| 250 | $end = strpos($pkt, "\0", $pos); |
| 251 | $user = substr($pkt, $pos, $end - $pos); |
| 252 | $pos = $end + 1; |
| 253 | if ($caps & self::CLIENT_PLUGIN_AUTH_LENENC) { |
| 254 | $auth = $this->lenencStr($pkt, $pos); |
| 255 | } else if ($caps & self::CLIENT_SECURE_CONNECTION) { |
| 256 | $len = ord($pkt[$pos]); |
| 257 | $auth = substr($pkt, $pos + 1, $len); |
| 258 | $pos += 1 + $len; |
| 259 | } else { |
| 260 | $end = strpos($pkt, "\0", $pos); |
| 261 | $auth = substr($pkt, $pos, $end - $pos); |
| 262 | $pos = $end + 1; |
| 263 | } |
| 264 | $db = ''; |
| 265 | if ($caps & self::CLIENT_CONNECT_WITH_DB) { |
| 266 | $end = strpos($pkt, "\0", $pos); |
| 267 | $db = substr($pkt, $pos, ($end === false ? strlen($pkt) : $end) - $pos); |
| 268 | $pos = $end === false ? strlen($pkt) : $end + 1; |
| 269 | } |
| 270 | $clientPlugin = ''; |
| 271 | if ($caps & self::CLIENT_PLUGIN_AUTH) { |
| 272 | $end = strpos($pkt, "\0", $pos); |
| 273 | $clientPlugin = substr($pkt, $pos, ($end === false ? strlen($pkt) : $end) - $pos); |
| 274 | } |
| 275 | |
| 276 | $cn['user'] = $user; |
| 277 | $known = self::USERS[$user] ?? null; |
| 278 | $using = $auth === '' || $auth === "\0" ? 'NO' : 'YES'; |
| 279 | if (!$known) { |
| 280 | $this->err($id, 1045, '28000', "Access denied for user '{$user}'@'localhost' (using password: {$using})"); |
| 281 | return false; |
| 282 | } |
| 283 | [$pw, $plugin] = $known; |
| 284 | $cn['plugin'] = $plugin; |
| 285 | |
| 286 | // クライアントのプラグインが違えば、切り替えを求める |
| 287 | if ($clientPlugin !== $plugin) { |
| 288 | for ($i = 0, $cn['scramble'] = ''; $i < 20; $i++) $cn['scramble'] .= chr(random_int(33, 126)); |
| 289 | $this->send($id, "\xFE{$plugin}\0{$cn['scramble']}\0"); |
| 290 | $auth = $this->recv($id); |
| 291 | if ($auth === null) return false; |
| 292 | } |
| 293 | |
| 294 | if (!$this->verify($id, $plugin, $pw, $auth)) { |
| 295 | $this->err($id, 1045, '28000', "Access denied for user '{$user}'@'localhost' (using password: {$using})"); |
| 296 | return false; |
| 297 | } |
| 298 | |
| 299 | if ($db === 'nodb') { |
| 300 | $this->err($id, 1049, '42000', "Unknown database '{$db}'"); |
| 301 | return false; |
| 302 | } |
| 303 | $cn['db'] = $db; |
| 304 | $cn['state'] = 'cmd'; |
| 305 | $this->ok($id); |
| 306 | return true; |
| 307 | } |
| 308 | |
| 309 | private function verify(int $id, string $plugin, string $pw, ?string $auth): bool { |
| 310 | $cn = &$this->conns[$id]; |
| 311 | $scramble = $cn['scramble']; |
| 312 | if ($auth === null) return false; |
| 313 | |
| 314 | switch ($plugin) { |
| 315 | case 'mysql_native_password': |
| 316 | if ($pw === '') return $auth === ''; |
| 317 | $s1 = sha1($pw, true); |
| 318 | return $auth === ($s1 ^ sha1($scramble.sha1($s1, true), true)); |
| 319 | |
| 320 | case 'caching_sha2_password': |
| 321 | if ($pw === '') return $auth === ''; |
| 322 | $s1 = hash('sha256', $pw, true); |
| 323 | $fast = $s1 ^ hash('sha256', hash('sha256', $s1, true).$scramble, true); |
| 324 | if (isset($this->sha2Cache[$cn['user']])) { |
| 325 | if ($auth !== $fast) return false; |
| 326 | $this->send($id, "\x01\x03"); |
| 327 | return true; |
| 328 | } |
| 329 | // キャッシュに無い:完全な認証 |
| 330 | $this->send($id, "\x01\x04"); |
| 331 | $res = $this->recv($id); |
| 332 | if ($res === null) return false; |
| 333 | if ($cn['tls']) { |
| 334 | $ok = $res === $pw."\0"; |
| 335 | } else { |
| 336 | if ($res === "\x02") { |
| 337 | if ($this->pubPem === '') return false; |
| 338 | $this->send($id, "\x01".$this->pubPem); |
| 339 | $res = $this->recv($id); |
| 340 | } |
| 341 | $ok = $this->decrypt((string)$res, $scramble) === $pw."\0"; |
| 342 | } |
| 343 | if ($ok) { |
| 344 | $this->sha2Cache[$cn['user']] = true; |
| 345 | $cn['fullAuth'] = true; |
| 346 | } |
| 347 | return $ok; |
| 348 | |
| 349 | case 'sha256_password': |
| 350 | if ($pw === '') return $auth === "\0" || $auth === ''; |
| 351 | if ($cn['tls']) return $auth === $pw."\0"; |
| 352 | if ($auth === "\x01") { |
| 353 | if ($this->pubPem === '') return false; |
| 354 | $this->send($id, "\x01".$this->pubPem); |
| 355 | $auth = $this->recv($id); |
| 356 | } |
| 357 | $cn['fullAuth'] = true; |
| 358 | return $this->decrypt((string)$auth, $scramble) === $pw."\0"; |
| 359 | |
| 360 | case 'mysql_clear_password': |
| 361 | return $cn['tls'] && $auth === $pw."\0"; |
| 362 | } |
| 363 | |
| 364 | return false; |
| 365 | } |
| 366 | |
| 367 | private function decrypt(string $data, string $scramble): ?string { |
| 368 | $out = $this->privKey?->decrypt($data, 'sha1'); |
| 369 | if ($out === null) return null; |
| 370 | $res = ''; |
| 371 | for ($i = 0; $i < strlen($out); $i++) $res .= $out[$i] ^ $scramble[$i % strlen($scramble)]; |
| 372 | return $res; |
| 373 | } |
| 374 | |
| 375 | /////////////// |
| 376 | // コマンド |
| 377 | |
| 378 | private function command(int $id, string $pkt): bool { |
| 379 | $cn = &$this->conns[$id]; |
| 380 | $cmd = ord($pkt[0]); |
| 381 | $data = (string)substr($pkt, 1); |
| 382 | |
| 383 | switch ($cmd) { |
| 384 | case 0x01: // COM_QUIT |
| 385 | return false; |
| 386 | case 0x02: // COM_INIT_DB |
| 387 | if ($data === 'nodb') { |
| 388 | $this->err($id, 1049, '42000', "Unknown database '{$data}'"); |
| 389 | } else { |
| 390 | $cn['db'] = $data; |
| 391 | $this->ok($id); |
| 392 | } |
| 393 | return true; |
| 394 | case 0x03: // COM_QUERY |
| 395 | return $this->query($id, $data); |
| 396 | case 0x0E: // COM_PING |
| 397 | $this->ok($id); |
| 398 | return true; |
| 399 | case 0x16: // COM_STMT_PREPARE |
| 400 | $this->prepare($id, $data); |
| 401 | return true; |
| 402 | case 0x17: // COM_STMT_EXECUTE |
| 403 | $this->execute($id, $data); |
| 404 | return true; |
| 405 | case 0x19: // COM_STMT_CLOSE(返事なし) |
| 406 | unset($cn['stmts'][unpack('V', $data)[1]]); |
| 407 | return true; |
| 408 | case 0x1A: // COM_STMT_RESET |
| 409 | $this->ok($id); |
| 410 | return true; |
| 411 | } |
| 412 | |
| 413 | $this->err($id, 1047, '08S01', 'Unknown command'); |
| 414 | return true; |
| 415 | } |
| 416 | |
| 417 | private function query(int $id, string $sql): bool { |
| 418 | $cn = &$this->conns[$id]; |
| 419 | $sql = rtrim(trim($sql), ';'); |
| 420 | |
| 421 | if (preg_match('/^SELECT\s+LB_DISCONNECT\(\)/i', $sql)) return false; |
| 422 | if (preg_match('/LB_SLEEP\(([\d.]+)\)/i', $sql, $m)) { |
| 423 | usleep((int)((float)$m[1] * 1000000)); |
| 424 | $sql = preg_replace('/LB_SLEEP\([\d.]+\)/i', '0', $sql); |
| 425 | } |
| 426 | |
| 427 | if (preg_match('/^CALL\s+lb_multi\(\)$/i', $sql)) { |
| 428 | $this->resultSet($id, $this->pdo->query("SELECT 1 AS a"), false, self::STATUS_MORE_RESULTS); |
| 429 | $this->resultSet($id, $this->pdo->query("SELECT '二' AS b"), false, self::STATUS_MORE_RESULTS); |
| 430 | $this->ok($id); |
| 431 | return true; |
| 432 | } |
| 433 | |
| 434 | if (preg_match('/^(START\s+TRANSACTION|BEGIN)$/i', $sql)) { |
| 435 | if ($this->pdo->inTransaction()) $this->pdo->commit(); |
| 436 | $this->pdo->beginTransaction(); |
| 437 | $cn['inTrans'] = true; |
| 438 | $this->ok($id); |
| 439 | return true; |
| 440 | } |
| 441 | if (preg_match('/^(COMMIT|ROLLBACK)$/i', $sql, $m)) { |
| 442 | if ($this->pdo->inTransaction()) strtoupper($m[1]) === 'COMMIT' ? $this->pdo->commit() : $this->pdo->rollBack(); |
| 443 | $cn['inTrans'] = false; |
| 444 | $this->ok($id); |
| 445 | return true; |
| 446 | } |
| 447 | if (preg_match('/^SET\s+NAMES\s+\'?(\w+)/i', $sql, $m)) { |
| 448 | $cs = array_search(strtolower($m[1]), self::CHARSETS, true); |
| 449 | if ($cs !== false) $cn['charset'] = $cs; |
| 450 | $this->ok($id); |
| 451 | return true; |
| 452 | } |
| 453 | if (preg_match('/^SET\s/i', $sql)) { |
| 454 | $this->ok($id); |
| 455 | return true; |
| 456 | } |
| 457 | if (preg_match('/^USE\s+`?(\w+)`?$/i', $sql, $m)) { |
| 458 | return $this->command($id, "\x02".$m[1]); |
| 459 | } |
| 460 | // 変換(Std\Lib\Db\Convert)用:テーブルの一覧と元のCREATE TABLE |
| 461 | if (preg_match('/^SHOW\s+FULL\s+TABLES$/i', $sql)) { |
| 462 | $db = $cn['db'] === '' ? 'lb' : $cn['db']; |
| 463 | $st = $this->pdo->query("SELECT name AS ".$this->pdo->quote('Tables_in_'.$db).", 'BASE TABLE' AS Table_type FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name"); |
| 464 | $this->resultSet($id, $st, false); |
| 465 | return true; |
| 466 | } |
| 467 | if (preg_match('/^SHOW\s+CREATE\s+TABLE\s+`?([^`]+)`?$/i', $sql, $m)) { |
| 468 | $ddl = $this->ddl[strtolower($m[1])] ?? null; |
| 469 | if ($ddl === null) { |
| 470 | $this->err($id, 1146, '42S02', "Table '{$cn['db']}.{$m[1]}' doesn't exist"); |
| 471 | return true; |
| 472 | } |
| 473 | $st = $this->pdo->prepare('SELECT ? AS "Table", ? AS "Create Table"'); |
| 474 | $st->execute([$m[1], $ddl]); |
| 475 | $this->resultSet($id, $st, false); |
| 476 | return true; |
| 477 | } |
| 478 | |
| 479 | try { |
| 480 | $st = $this->pdo->prepare($this->rewrite($id, $sql)); |
| 481 | $st->execute(); |
| 482 | } catch (\PDOException $e) { |
| 483 | $this->sqlError($id, $e); |
| 484 | return true; |
| 485 | } |
| 486 | if (preg_match('/^CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?`?([^`\s(]+)`?/i', $sql, $m)) $this->ddl[strtolower($m[1])] = $sql; |
| 487 | if (preg_match('/^DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?`?([^`\s]+)`?/i', $sql, $m)) unset($this->ddl[strtolower($m[1])]); |
| 488 | |
| 489 | $this->finish($id, $st, $sql, false); |
| 490 | return true; |
| 491 | } |
| 492 | |
| 493 | private function prepare(int $id, string $sql): void { |
| 494 | $cn = &$this->conns[$id]; |
| 495 | $sql = rtrim(trim($sql), ';'); |
| 496 | [$n, $marks] = $this->placeholders($sql); |
| 497 | |
| 498 | try { |
| 499 | $st = $this->pdo->prepare($this->rewrite($id, $sql)); |
| 500 | $cols = []; |
| 501 | if (preg_match('/^\s*(SELECT|WITH|VALUES|PRAGMA)/i', $sql)) { |
| 502 | $st->execute(array_fill(0, $n, null)); |
| 503 | $cols = $this->columns($st, []); |
| 504 | $st->closeCursor(); |
| 505 | } |
| 506 | } catch (\PDOException $e) { |
| 507 | $this->sqlError($id, $e); |
| 508 | return; |
| 509 | } |
| 510 | |
| 511 | $sid = $this->nextStmt++; |
| 512 | $cn['stmts'][$sid] = ['sql' => $sql, 'n' => $n, 'marks' => $marks, 'types' => array_fill(0, $n, [self::T_VAR_STRING, 0])]; |
| 513 | |
| 514 | $this->send($id, "\x00".pack('V', $sid).pack('v', count($cols)).pack('v', $n)."\x00".pack('v', 0)); |
| 515 | if ($n > 0) { |
| 516 | for ($i = 0; $i < $n; $i++) $this->send($id, $this->colDef(['name' => '?', 'table' => '', 'type' => self::T_VAR_STRING, 'flags' => 0, 'decimals' => 0, 'charset' => 63])); |
| 517 | $this->eof($id); |
| 518 | } |
| 519 | if ($cols) { |
| 520 | foreach ($cols as $c) $this->send($id, $this->colDef($c)); |
| 521 | $this->eof($id); |
| 522 | } |
| 523 | } |
| 524 | |
| 525 | private function execute(int $id, string $data): void { |
| 526 | $cn = &$this->conns[$id]; |
| 527 | $sid = unpack('V', $data)[1]; |
| 528 | if (!isset($cn['stmts'][$sid])) { |
| 529 | $this->err($id, 1243, 'HY000', "Unknown prepared statement handler ({$sid}) given to mysqld_stmt_execute"); |
| 530 | return; |
| 531 | } |
| 532 | $stmt = &$cn['stmts'][$sid]; |
| 533 | $n = $stmt['n']; |
| 534 | $pos = 9; |
| 535 | $values = []; |
| 536 | |
| 537 | if ($n > 0) { |
| 538 | $bitmap = substr($data, $pos, intdiv($n + 7, 8)); |
| 539 | $pos += strlen($bitmap); |
| 540 | $bound = ord($data[$pos]); |
| 541 | $pos++; |
| 542 | if ($bound) { |
| 543 | for ($i = 0; $i < $n; $i++) { |
| 544 | $stmt['types'][$i] = [ord($data[$pos]), ord($data[$pos + 1])]; |
| 545 | $pos += 2; |
| 546 | } |
| 547 | } |
| 548 | for ($i = 0; $i < $n; $i++) { |
| 549 | if (ord($bitmap[$i >> 3]) & (1 << ($i & 7))) { |
| 550 | $values[] = null; |
| 551 | continue; |
| 552 | } |
| 553 | $values[] = $this->readParam($data, $pos, $stmt['types'][$i][0], ($stmt['types'][$i][1] & 0x80) !== 0); |
| 554 | } |
| 555 | } |
| 556 | |
| 557 | // 浮動小数点数はSQLiteにREALとして渡す |
| 558 | $sql = $stmt['sql']; |
| 559 | foreach (array_reverse(array_keys($values)) as $i) { |
| 560 | if (is_float($values[$i])) $sql = substr_replace($sql, 'CAST(? AS REAL)', $stmt['marks'][$i], 1); |
| 561 | } |
| 562 | |
| 563 | try { |
| 564 | $st = $this->pdo->prepare($this->rewrite($id, $sql)); |
| 565 | foreach ($values as $i => $v) { |
| 566 | $type = match (true) { |
| 567 | $v === null => \PDO::PARAM_NULL, |
| 568 | is_int($v) => \PDO::PARAM_INT, |
| 569 | is_string($v) && !mb_check_encoding($v, 'UTF-8') => \PDO::PARAM_LOB, |
| 570 | default => \PDO::PARAM_STR, |
| 571 | }; |
| 572 | $st->bindValue($i + 1, is_float($v) ? var_export($v, true) : $v, $type); |
| 573 | } |
| 574 | $st->execute(); |
| 575 | } catch (\PDOException $e) { |
| 576 | $this->sqlError($id, $e); |
| 577 | return; |
| 578 | } |
| 579 | |
| 580 | $this->finish($id, $st, $stmt['sql'], true); |
| 581 | } |
| 582 | |
| 583 | private function readParam(string $d, int &$pos, int $type, bool $unsigned): mixed { |
| 584 | switch ($type) { |
| 585 | case self::T_TINY: |
| 586 | $v = ord($d[$pos]); |
| 587 | $pos += 1; |
| 588 | return $unsigned || $v < 0x80 ? $v : $v - 0x100; |
| 589 | case self::T_SHORT: |
| 590 | case self::T_YEAR: |
| 591 | $v = unpack('v', $d, $pos)[1]; |
| 592 | $pos += 2; |
| 593 | return $unsigned || $v < 0x8000 ? $v : $v - 0x10000; |
| 594 | case self::T_LONG: |
| 595 | case self::T_INT24: |
| 596 | $v = unpack('V', $d, $pos)[1]; |
| 597 | $pos += 4; |
| 598 | return $unsigned || $v < 0x80000000 ? $v : $v - 0x100000000; |
| 599 | case self::T_LONGLONG: |
| 600 | $v = unpack('P', $d, $pos)[1]; |
| 601 | $pos += 8; |
| 602 | return $unsigned && $v < 0 ? sprintf('%u', $v) : $v; |
| 603 | case self::T_FLOAT: |
| 604 | $v = unpack('g', $d, $pos)[1]; |
| 605 | $pos += 4; |
| 606 | return $v; |
| 607 | case self::T_DOUBLE: |
| 608 | $v = unpack('e', $d, $pos)[1]; |
| 609 | $pos += 8; |
| 610 | return $v; |
| 611 | case self::T_NULL: |
| 612 | return null; |
| 613 | case self::T_DATE: |
| 614 | case self::T_DATETIME: |
| 615 | case self::T_TIMESTAMP: |
| 616 | $len = ord($d[$pos]); |
| 617 | $b = substr($d, $pos + 1, $len).str_repeat("\0", 11); |
| 618 | $pos += 1 + $len; |
| 619 | $s = sprintf('%04d-%02d-%02d', unpack('v', $b)[1], ord($b[2]), ord($b[3])); |
| 620 | if ($type === self::T_DATE) return $s; |
| 621 | $us = unpack('V', $b, 7)[1]; |
| 622 | return $s.sprintf(' %02d:%02d:%02d', ord($b[4]), ord($b[5]), ord($b[6])).($us ? sprintf('.%06d', $us) : ''); |
| 623 | case self::T_TIME: |
| 624 | $len = ord($d[$pos]); |
| 625 | $b = substr($d, $pos + 1, $len).str_repeat("\0", 12); |
| 626 | $pos += 1 + $len; |
| 627 | $us = unpack('V', $b, 8)[1]; |
| 628 | return (ord($b[0]) ? '-' : '').sprintf('%02d:%02d:%02d', unpack('V', $b, 1)[1] * 24 + ord($b[5]), ord($b[6]), ord($b[7])).($us ? sprintf('.%06d', $us) : ''); |
| 629 | } |
| 630 | |
| 631 | return $this->lenencStr($d, $pos); |
| 632 | } |
| 633 | |
| 634 | /** 結果セット又はOK */ |
| 635 | private function finish(int $id, \PDOStatement $st, string $sql, bool $binary): void { |
| 636 | if ($st->columnCount() > 0) { |
| 637 | $this->resultSet($id, $st, $binary); |
| 638 | return; |
| 639 | } |
| 640 | $insertId = preg_match('/^\s*(INSERT|REPLACE)\b/i', $sql) ? (int)$this->pdo->lastInsertId() : 0; |
| 641 | $this->ok($id, $st->rowCount(), $insertId); |
| 642 | } |
| 643 | |
| 644 | /////////////// |
| 645 | // SQLite → MySQL |
| 646 | |
| 647 | /** MySQLの書き方をSQLiteにする、特別な変数を値にする */ |
| 648 | private function rewrite(int $id, string $sql): string { |
| 649 | $cn = $this->conns[$id]; |
| 650 | $q = fn(string $s): string => $this->pdo->quote($s); |
| 651 | $auto = 0; |
| 652 | $sql = preg_replace('/\b(?:TINY|SMALL|MEDIUM|BIG)?INT(?:EGER)?(?:\(\d+\))?(?:\s+UNSIGNED)?(?:\s+NOT\s+NULL)?\s+AUTO_INCREMENT(?:\s+PRIMARY\s+KEY)?/i', 'INTEGER PRIMARY KEY AUTOINCREMENT', $sql, -1, $auto); |
| 653 | $sql = preg_replace('/\)\s*(ENGINE|DEFAULT\s+CHARSET|CHARSET|COLLATE)\s*=.*$/is', ')', $sql); |
| 654 | if (preg_match('/^\s*CREATE\s+TABLE/i', $sql)) { |
| 655 | // SQLiteに無い物(照合順序、文字セット、DATETIME(6)のDEFAULT)、AUTO_INCREMENTの別の主キー |
| 656 | $sql = preg_replace(['/\s+COLLATE\s+\w+/i', '/\s+CHARACTER\s+SET\s+\w+/i', '/CURRENT_TIMESTAMP\(\d*\)/i'], ['', '', 'CURRENT_TIMESTAMP'], $sql); |
| 657 | if ($auto) $sql = preg_replace('/,\s*PRIMARY\s+KEY\s*\([^)]*\)/i', '', $sql); |
| 658 | } |
| 659 | |
| 660 | return strtr($sql, [ |
| 661 | 'DATABASE()' => $cn['db'] === '' ? 'NULL' : $q($cn['db']), |
| 662 | 'CONNECTION_ID()' => (string)$cn['id'], |
| 663 | 'VERSION()' => $q(self::VERSION), |
| 664 | '@@version' => $q(self::VERSION), |
| 665 | '@@lb_tls' => $cn['tls'] ? '1' : '0', |
| 666 | '@@lb_plugin' => $q($cn['plugin']), |
| 667 | '@@lb_full_auth' => $cn['fullAuth'] ? '1' : '0', |
| 668 | '@@lb_caps' => (string)$cn['caps'], |
| 669 | '@@character_set_client' => $q(self::CHARSETS[$cn['charset']] ?? (string)$cn['charset']), |
| 670 | ]); |
| 671 | } |
| 672 | |
| 673 | /** 「?」の数と位置(文字列・識別子・コメントの中は数えない) */ |
| 674 | private function placeholders(string $sql): array { |
| 675 | $marks = []; |
| 676 | $len = strlen($sql); |
| 677 | for ($i = 0; $i < $len; $i++) { |
| 678 | $c = $sql[$i]; |
| 679 | if ($c === "'" || $c === '"' || $c === '`') { |
| 680 | for ($i++; $i < $len && $sql[$i] !== $c; $i++) if ($sql[$i] === '\\') $i++; |
| 681 | } else if ($c === '-' && ($sql[$i + 1] ?? '') === '-') { |
| 682 | while ($i < $len && $sql[$i] !== "\n") $i++; |
| 683 | } else if ($c === '/' && ($sql[$i + 1] ?? '') === '*') { |
| 684 | $end = strpos($sql, '*/', $i + 2); |
| 685 | $i = $end === false ? $len : $end + 1; |
| 686 | } else if ($c === '?') { |
| 687 | $marks[] = $i; |
| 688 | } |
| 689 | } |
| 690 | return [count($marks), $marks]; |
| 691 | } |
| 692 | |
| 693 | /** 列の説明(宣言された型から、無ければ最初の値から) */ |
| 694 | private function columns(\PDOStatement $st, array $rows): array { |
| 695 | $cols = []; |
| 696 | for ($i = 0; $i < $st->columnCount(); $i++) { |
| 697 | $meta = $st->getColumnMeta($i) ?: []; |
| 698 | $decl = strtoupper((string)($meta['sqlite:decl_type'] ?? '')); |
| 699 | $col = ['name' => (string)($meta['name'] ?? ''), 'table' => (string)($meta['table'] ?? ''), 'flags' => 0, 'decimals' => 0, 'charset' => 45]; |
| 700 | |
| 701 | if (str_contains($decl, 'UNSIGNED')) $col['flags'] |= 0x20; |
| 702 | $col['type'] = match (true) { |
| 703 | $decl === '' => null, |
| 704 | str_starts_with($decl, 'TINYINT') || str_starts_with($decl, 'BOOL') => self::T_TINY, |
| 705 | str_starts_with($decl, 'SMALLINT') => self::T_SHORT, |
| 706 | str_starts_with($decl, 'MEDIUMINT') => self::T_INT24, |
| 707 | str_starts_with($decl, 'BIGINT') => self::T_LONGLONG, |
| 708 | str_starts_with($decl, 'INT') => self::T_LONG, |
| 709 | str_starts_with($decl, 'FLOAT') => self::T_FLOAT, |
| 710 | str_starts_with($decl, 'DOUBLE') || str_starts_with($decl, 'REAL') => self::T_DOUBLE, |
| 711 | str_starts_with($decl, 'DECIMAL') || str_starts_with($decl, 'NUMERIC') => self::T_DECIMAL, |
| 712 | str_starts_with($decl, 'DATETIME') => self::T_DATETIME, |
| 713 | str_starts_with($decl, 'TIMESTAMP') => self::T_TIMESTAMP, |
| 714 | str_starts_with($decl, 'DATE') => self::T_DATE, |
| 715 | str_starts_with($decl, 'TIME') => self::T_TIME, |
| 716 | str_starts_with($decl, 'YEAR') => self::T_YEAR, |
| 717 | str_starts_with($decl, 'JSON') => self::T_JSON, |
| 718 | str_contains($decl, 'BLOB') || str_contains($decl, 'BINARY') => self::T_BLOB, |
| 719 | str_contains($decl, 'TEXT') => self::T_BLOB, |
| 720 | str_starts_with($decl, 'CHAR') => self::T_STRING, |
| 721 | default => self::T_VAR_STRING, |
| 722 | }; |
| 723 | if (str_contains($decl, 'BLOB') || str_contains($decl, 'BINARY')) { |
| 724 | $col['charset'] = 63; |
| 725 | $col['flags'] |= 0x80; |
| 726 | } |
| 727 | if (in_array($col['type'], [self::T_FLOAT, self::T_DOUBLE], true)) $col['decimals'] = 31; |
| 728 | if (preg_match('/\((\d+)(?:,\s*(\d+))?\)/', $decl, $m)) { |
| 729 | if ($col['type'] === self::T_DECIMAL) $col['decimals'] = (int)($m[2] ?? 0); |
| 730 | if (in_array($col['type'], [self::T_FLOAT, self::T_DOUBLE], true) && isset($m[2])) $col['decimals'] = (int)$m[2]; |
| 731 | if (in_array($col['type'], [self::T_DATETIME, self::T_TIMESTAMP, self::T_TIME], true)) $col['decimals'] = (int)$m[1]; |
| 732 | } |
| 733 | |
| 734 | if ($col['type'] === null) { |
| 735 | $col['type'] = self::T_NULL; |
| 736 | foreach ($rows as $r) { |
| 737 | if ($r[$i] === null) continue; |
| 738 | $col['type'] = is_int($r[$i]) ? self::T_LONGLONG : (is_float($r[$i]) ? self::T_DOUBLE : self::T_VAR_STRING); |
| 739 | if ($col['type'] === self::T_DOUBLE) $col['decimals'] = 31; |
| 740 | break; |
| 741 | } |
| 742 | if ($col['type'] === self::T_NULL && empty($rows)) $col['type'] = self::T_VAR_STRING; |
| 743 | } |
| 744 | $cols[] = $col; |
| 745 | } |
| 746 | return $cols; |
| 747 | } |
| 748 | |
| 749 | private function resultSet(int $id, \PDOStatement $st, bool $binary, int $extra = 0): void { |
| 750 | $rows = $st->fetchAll(\PDO::FETCH_NUM); |
| 751 | $cols = $this->columns($st, $rows); |
| 752 | |
| 753 | $this->send($id, $this->lenenc(count($cols))); |
| 754 | foreach ($cols as $c) $this->send($id, $this->colDef($c)); |
| 755 | $this->eof($id); |
| 756 | |
| 757 | foreach ($rows as $r) { |
| 758 | if ($binary) { |
| 759 | $bitmap = str_repeat("\0", intdiv(count($cols) + 9, 8)); |
| 760 | $vals = ''; |
| 761 | foreach ($cols as $i => $c) { |
| 762 | if ($r[$i] === null) { |
| 763 | $b = $i + 2; |
| 764 | $bitmap[$b >> 3] = chr(ord($bitmap[$b >> 3]) | (1 << ($b & 7))); |
| 765 | continue; |
| 766 | } |
| 767 | $vals .= $this->binValue($r[$i], $c); |
| 768 | } |
| 769 | $this->send($id, "\x00".$bitmap.$vals); |
| 770 | } else { |
| 771 | $out = ''; |
| 772 | foreach ($cols as $i => $c) { |
| 773 | $out .= $r[$i] === null ? "\xFB" : $this->lenencStrOut($this->textValue($r[$i], $c)); |
| 774 | } |
| 775 | $this->send($id, $out); |
| 776 | } |
| 777 | } |
| 778 | $this->eof($id, $extra); |
| 779 | } |
| 780 | |
| 781 | private function textValue(mixed $v, array $c): string { |
| 782 | switch ($c['type']) { |
| 783 | case self::T_DECIMAL: |
| 784 | return sprintf('%.'.$c['decimals'].'F', (float)$v); |
| 785 | case self::T_FLOAT: |
| 786 | return (string)(float)sprintf('%.6g', (float)$v); |
| 787 | case self::T_DOUBLE: |
| 788 | return (string)(float)$v; |
| 789 | case self::T_DATETIME: |
| 790 | case self::T_TIMESTAMP: |
| 791 | $t = $this->splitDateTime((string)$v); |
| 792 | return sprintf('%04d-%02d-%02d %02d:%02d:%02d', ...array_slice($t, 0, 6)).$this->frac($t[6], $c['decimals']); |
| 793 | case self::T_TIME: |
| 794 | $t = $this->splitTime((string)$v); |
| 795 | return ($t[0] ? '-' : '').sprintf('%02d:%02d:%02d', $t[1], $t[2], $t[3]).$this->frac($t[4], $c['decimals']); |
| 796 | } |
| 797 | return (string)$v; |
| 798 | } |
| 799 | |
| 800 | private function binValue(mixed $v, array $c): string { |
| 801 | switch ($c['type']) { |
| 802 | case self::T_TINY: |
| 803 | return chr((int)$v & 0xFF); |
| 804 | case self::T_SHORT: |
| 805 | case self::T_YEAR: |
| 806 | return pack('v', (int)$v & 0xFFFF); |
| 807 | case self::T_LONG: |
| 808 | case self::T_INT24: |
| 809 | return pack('V', (int)$v & 0xFFFFFFFF); |
| 810 | case self::T_LONGLONG: |
| 811 | return pack('P', (int)$v); |
| 812 | case self::T_FLOAT: |
| 813 | return pack('g', (float)$v); |
| 814 | case self::T_DOUBLE: |
| 815 | return pack('e', (float)$v); |
| 816 | case self::T_DATE: |
| 817 | case self::T_DATETIME: |
| 818 | case self::T_TIMESTAMP: |
| 819 | $t = $this->splitDateTime((string)$v); |
| 820 | if ($c['type'] === self::T_DATE || ($t[3] === 0 && $t[4] === 0 && $t[5] === 0 && $t[6] === 0)) { |
| 821 | if ($t[0] === 0 && $t[1] === 0 && $t[2] === 0) return "\x00"; |
| 822 | return "\x04".pack('v', $t[0]).chr($t[1]).chr($t[2]); |
| 823 | } |
| 824 | if ($t[6] === 0) return "\x07".pack('v', $t[0]).chr($t[1]).chr($t[2]).chr($t[3]).chr($t[4]).chr($t[5]); |
| 825 | return "\x0B".pack('v', $t[0]).chr($t[1]).chr($t[2]).chr($t[3]).chr($t[4]).chr($t[5]).pack('V', $t[6]); |
| 826 | case self::T_TIME: |
| 827 | $t = $this->splitTime((string)$v); |
| 828 | $days = intdiv($t[1], 24); |
| 829 | $body = chr($t[0]).pack('V', $days).chr($t[1] % 24).chr($t[2]).chr($t[3]); |
| 830 | if ($t[4] === 0) return ($t[1] === 0 && $t[2] === 0 && $t[3] === 0) ? "\x00" : "\x08".$body; |
| 831 | return "\x0C".$body.pack('V', $t[4]); |
| 832 | } |
| 833 | return $this->lenencStrOut($this->textValue($v, $c)); |
| 834 | } |
| 835 | |
| 836 | /** [年, 月, 日, 時, 分, 秒, マイクロ秒] */ |
| 837 | private function splitDateTime(string $v): array { |
| 838 | preg_match('/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?)?/', $v, $m); |
| 839 | return [(int)($m[1] ?? 0), (int)($m[2] ?? 0), (int)($m[3] ?? 0), (int)($m[4] ?? 0), (int)($m[5] ?? 0), (int)($m[6] ?? 0), (int)str_pad($m[7] ?? '0', 6, '0')]; |
| 840 | } |
| 841 | |
| 842 | /** [負数, 時, 分, 秒, マイクロ秒] */ |
| 843 | private function splitTime(string $v): array { |
| 844 | preg_match('/^(-)?(\d+):(\d{2}):(\d{2})(?:\.(\d{1,6}))?/', $v, $m); |
| 845 | return [!empty($m[1]) ? 1 : 0, (int)($m[2] ?? 0), (int)($m[3] ?? 0), (int)($m[4] ?? 0), (int)str_pad($m[5] ?? '0', 6, '0')]; |
| 846 | } |
| 847 | |
| 848 | private function frac(int $us, int $dec): string { |
| 849 | return $dec > 0 && $dec <= 6 ? '.'.substr(sprintf('%06d', $us), 0, $dec) : ''; |
| 850 | } |
| 851 | |
| 852 | private function sqlError(int $id, \PDOException $e): void { |
| 853 | $msg = $e->getMessage(); |
| 854 | $db = $this->conns[$id]['db']; |
| 855 | if (preg_match('/no such table: (\S+)/', $msg, $m)) { |
| 856 | $this->err($id, 1146, '42S02', "Table '{$db}.{$m[1]}' doesn't exist"); |
| 857 | } else if (preg_match('/UNIQUE constraint failed: (\S+)/', $msg, $m)) { |
| 858 | $this->err($id, 1062, '23000', "Duplicate entry for key '{$m[1]}'"); |
| 859 | } else if (preg_match('/NOT NULL constraint failed: \w+\.(\w+)/', $msg, $m)) { |
| 860 | $this->err($id, 1048, '23000', "Column '{$m[1]}' cannot be null"); |
| 861 | } else if (preg_match('/no such column: (\S+)/', $msg, $m)) { |
| 862 | $this->err($id, 1054, '42S22', "Unknown column '{$m[1]}' in 'field list'"); |
| 863 | } else if (str_contains($msg, 'syntax error')) { |
| 864 | $this->err($id, 1064, '42000', 'You have an error in your SQL syntax; '.preg_replace('/^.*?: \d+ /', '', $msg)); |
| 865 | } else { |
| 866 | $this->err($id, 1105, 'HY000', preg_replace('/^SQLSTATE\[\w+\]: [^:]*: \d+ /', '', $msg)); |
| 867 | } |
| 868 | } |
| 869 | |
| 870 | /////////////// |
| 871 | // パケット |
| 872 | |
| 873 | private function status(int $id, int $extra = 0): int { |
| 874 | return self::STATUS_AUTOCOMMIT | ($this->conns[$id]['inTrans'] ? self::STATUS_IN_TRANS : 0) | $extra; |
| 875 | } |
| 876 | |
| 877 | private function ok(int $id, int $affected = 0, int $insertId = 0): void { |
| 878 | $this->send($id, "\x00".$this->lenenc($affected).$this->lenenc($insertId).pack('v', $this->status($id)).pack('v', 0)); |
| 879 | } |
| 880 | |
| 881 | private function eof(int $id, int $extra = 0): void { |
| 882 | $this->send($id, "\xFE".pack('v', 0).pack('v', $this->status($id, $extra))); |
| 883 | } |
| 884 | |
| 885 | private function err(int $id, int $code, string $state, string $msg): void { |
| 886 | $this->send($id, "\xFF".pack('v', $code).'#'.$state.$msg); |
| 887 | } |
| 888 | |
| 889 | private function colDef(array $c): string { |
| 890 | return $this->lenencStrOut('def').$this->lenencStrOut($c['table'] === '' ? '' : 'lb').$this->lenencStrOut($c['table']) |
| 891 | .$this->lenencStrOut($c['table']).$this->lenencStrOut($c['name']).$this->lenencStrOut($c['name']) |
| 892 | ."\x0C".pack('v', $c['charset']).pack('V', 255).chr($c['type']).pack('v', $c['flags']).chr($c['decimals'])."\0\0"; |
| 893 | } |
| 894 | |
| 895 | private function lenenc(int $v): string { |
| 896 | if ($v < 251) return chr($v); |
| 897 | if ($v < 65536) return "\xFC".pack('v', $v); |
| 898 | if ($v < 16777216) return "\xFD".substr(pack('V', $v), 0, 3); |
| 899 | return "\xFE".pack('P', $v); |
| 900 | } |
| 901 | |
| 902 | private function lenencStrOut(string $s): string { |
| 903 | return $this->lenenc(strlen($s)).$s; |
| 904 | } |
| 905 | |
| 906 | private function lenencStr(string $d, int &$pos): string { |
| 907 | $f = ord($d[$pos] ?? "\0"); |
| 908 | $pos++; |
| 909 | if ($f < 251) { |
| 910 | $len = $f; |
| 911 | } else { |
| 912 | $size = [0xFC => 2, 0xFD => 3, 0xFE => 8][$f] ?? 0; |
| 913 | $len = unpack('P', str_pad(substr($d, $pos, $size), 8, "\0"))[1]; |
| 914 | $pos += $size; |
| 915 | } |
| 916 | $s = (string)substr($d, $pos, $len); |
| 917 | $pos += $len; |
| 918 | return $s; |
| 919 | } |
| 920 | |
| 921 | private function send(int $id, string $data): void { |
| 922 | $cn = &$this->conns[$id]; |
| 923 | $pos = 0; |
| 924 | $total = strlen($data); |
| 925 | do { |
| 926 | $len = min($total - $pos, 0xFFFFFF); |
| 927 | $out = substr(pack('V', $len), 0, 3).chr($cn['seq']).substr($data, $pos, $len); |
| 928 | $pos += $len; |
| 929 | $cn['seq'] = ($cn['seq'] + 1) & 0xFF; |
| 930 | for ($w = 0; $w < strlen($out);) { |
| 931 | $n = @fwrite($cn['sock'], substr($out, $w, 1048576)); |
| 932 | if ($n === false || $n === 0) return; |
| 933 | $w += $n; |
| 934 | } |
| 935 | } while ($len === 0xFFFFFF); |
| 936 | } |
| 937 | |
| 938 | private function readRaw(int $id, int $n): ?string { |
| 939 | $buf = ''; |
| 940 | while (strlen($buf) < $n) { |
| 941 | $x = @fread($this->conns[$id]['sock'], $n - strlen($buf)); |
| 942 | if ($x === false || $x === '') return null; |
| 943 | $buf .= $x; |
| 944 | } |
| 945 | return $buf; |
| 946 | } |
| 947 | |
| 948 | private function readRest(int $id, string $head): ?string { |
| 949 | $data = ''; |
| 950 | while (true) { |
| 951 | $len = unpack('V', substr($head, 0, 3)."\0")[1]; |
| 952 | $this->conns[$id]['seq'] = (ord($head[3]) + 1) & 0xFF; |
| 953 | if ($len > 0) { |
| 954 | $chunk = $this->readRaw($id, $len); |
| 955 | if ($chunk === null) return null; |
| 956 | $data .= $chunk; |
| 957 | } |
| 958 | if ($len !== 0xFFFFFF) return $data; |
| 959 | $head = $this->readRaw($id, 4); |
| 960 | if ($head === null) return null; |
| 961 | } |
| 962 | } |
| 963 | |
| 964 | private function recv(int $id): ?string { |
| 965 | $head = $this->readRaw($id, 4); |
| 966 | return $head === null ? null : $this->readRest($id, $head); |
| 967 | } |
| 968 | } |