master
ブランチ master
LittleBeast / src / Std / Test / Helper / mysql-server.php
36.3 KB、968行 生 Blame 履歴
1<?php
2/*************************************************************
3# 076 License
4
5Copyright (c) テクニカル諏訪子
6
7Permission is hereby granted to any person obtaining a copy of the software
8Little Beast (the "Software") to use, modify, merge, copy, publish, distribute,
9sublicense, 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
31THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
32INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
33PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
34HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
35CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
36OR 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
56require_once __DIR__.'/../../../../autoload.php';
57
58use Std\Lib\Crypto\Rsa;
59use 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
66if (!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);
72if (!$server) {
73 fwrite(STDOUT, "error {$errstr}\n");
74 exit(1);
75}
76
77$my = new MockMysql($defaultPlugin, $certFile, $keyFile);
78fwrite(STDOUT, "ready\n");
79fflush(STDOUT);
80$my->run($server);
81
82class 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}