MySQLライブラリの削除

諏訪子 <suwako@076.moe>、2025-12-30 18:55
コミット 580ab9aabb411ac326f357ca107ae76ae4ed76e8 ・親 66e1b39 ・ファイルを見る

5個のファイル、+0 -1625

@@ -35,7 +35,6 @@
3535 define('ATOM_ENABLED', true);
3636 define('RSS_ENABLED', false);
3737 define('ACTIVITYPUB_ENABLED', false);
38-define('MYSQL_ENABLED', false);
3938 define('CSV_ENABLED', false);
4039 define('CURL_ENABLED', true);
4140 define('AUTH_ENABLED', false);
削除 src/Site/Lib/Mysql.php
@@ -1,1559 +0,0 @@
1-<?php
2-namespace Site\Lib;
3-
4-/**
5- * PHP純正のMySQLドライバ。php-mysql拡張モジュールが不要。
6- * TODO: execute()で他のMySQLタイプを処理 (MYSQL_TYPE_DATA, MYSQL_TYPE_BLOB)
7- * TODO: トランザクション管理
8- * TODO: 各種接続オプション
9- * TODO: プロトコルの完全なカバレッジ
10- */
11-class Mysql {
12- private $socket = null;
13- private bool $connected = false;
14- private bool $debug = false;
15- private array $packetLog = [];
16- private array $serverInfo = [];
17- public string $username;
18- private string $password;
19- public string $dbname;
20- public string $host;
21- private int $port;
22-
23- private array $prepared = [];
24-
25- /**
26- * コンストラクタ
27- *
28- * MySQL接続に必要な設定をDBINFOから初期化します。
29- */
30- public function __construct() {
31- if (!MYSQL_ENABLED) return;
32-
33- $this->host = DBINFO['host'];
34- $this->username = DBINFO['username'];
35- $this->password = DBINFO['password'];
36- $this->dbname = DBINFO['dbname'];
37- $this->port = DBINFO['port'];
38- $this->debug = DBINFO['debug'];
39- }
40-
41- /**
42- * デストラクタ
43- *
44- * オブジェクト破棄時に接続を閉じます。
45- */
46- public function __destruct() {
47- $this->close();
48- }
49-
50- /**
51- * デバッギングの有無
52- *
53- * @param bool $debug デバッグモードを有効にするかどうか
54- * @return Mysql 自身を返す(メソッドチェーン用)
55- */
56- public function setDebug(bool $debug): Mysql {
57- $this->debug = (bool)$debug;
58- return $this;
59- }
60-
61- /**
62- * パケットログの取得
63- *
64- * @return array 送信および受信したパケットのログ
65- */
66- public function getPacketLog(): array {
67- return $this->packetLog;
68- }
69-
70- /**
71- * MySQLサーバーに接続
72- *
73- * ソケットを作成し、サーバーに接続後、認証とデータベース選択を行います。
74- *
75- * @return bool 接続成功時はtrue
76- * @throws \Exception 接続または認証に失敗した場合
77- */
78- public function connect(): bool {
79- if (!MYSQL_ENABLED) return false;
80-
81- $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
82- if ($this->socket === false) {
83- $msg = 'ソケットの作成に失敗: '.socket_strerror(socket_last_error());
84- \logger(\LogType::MySQL, $msg);
85- throw new \Exception($msg);
86- }
87-
88- $res = socket_connect($this->socket, $this->host, $this->port);
89- if ($res === false) {
90- $msg = 'ソケットに接続に失敗: '
91- .socket_strerror(socket_last_error($this->socket));
92- \logger(\LogType::MySQL, $msg);
93- throw new \Exception($msg);
94- }
95-
96- $greeting = $this->readPacket();
97- $this->parseServerGreeting($greeting);
98- $this->authenticate();
99- $response = $this->readPacket();
100-
101- if (ord($response[0]) !== 0x00) {
102- $code = unpack('v', substr($response, 1, 2))[1];
103- $mes = substr($response, 3);
104- $msg = "認証応答に失敗: {$code} - {$mes}";
105- \logger(\LogType::MySQL, $msg);
106- throw new \Exception($msg);
107- }
108-
109- if (!empty($this->dbname)) {
110- $this->selectDatabase($this->dbname);
111- }
112-
113- $this->connected = true;
114- return true;
115- }
116-
117- /**
118- * 接続を閉じる
119- *
120- * COM_QUITコマンドを送信し、ソケットを閉じます。
121- *
122- * @return void
123- */
124- public function close(): void {
125- if (!MYSQL_ENABLED) return;
126-
127- if ($this->socket) {
128- $this->sendCommand(0x01); // COM_QUIT
129- socket_close($this->socket);
130- $this->socket = null;
131- $this->connected = false;
132- }
133- }
134-
135- /**
136- * 利用するデータベースを選択する
137- *
138- * COM_INIT_DBコマンドを使用してデータベースを選択します。
139- *
140- * @param string $database データベース名
141- * @return bool 成功時はtrue
142- * @throws \Exception データベース選択に失敗した場合
143- */
144- public function selectDatabase(string $database): bool {
145- if (!MYSQL_ENABLED) return false;
146-
147- $this->sendCommand(0x02, $database); // COM_INIT_DB
148- $res = $this->readPacket();
149-
150- if (ord($res[0]) === 0xFF) {
151- $code = unpack('v', substr($res, 1, 2))[1];
152- $mes = substr($res, 3);
153- $msg = "データベースの選択に失敗: {$code} - {$mes}";
154- \logger(\LogType::MySQL, $msg);
155- throw new \Exception($msg);
156- }
157-
158- $this->dbname = $database;
159-
160- return true;
161- }
162-
163- /**
164- * プリペアドステートメントの準備
165- *
166- * SQLクエリをプリペアドステートメントとして準備し、ステートメントIDを返します。
167- *
168- * @param string $query プレースホルダ付きSQLクエリ(例: "SELECT * FROM users WHERE id = ?")
169- * @return int 成功時はステートメントID
170- * @throws \Exception 準備に失敗した場合
171- */
172- public function prepare(string $query): int {
173- if (!$this->connected || !MYSQL_ENABLED) return false;
174-
175- $this->sendCommand(0x16, $query); // COM_STMT_PREPARE
176- $res = $this->readPacket();
177-
178- if (ord($res[0]) === 0xFF) {
179- $code = unpack('v', substr($res, 1, 2))[1];
180- $mes = substr($res, 3);
181- $msg = "準備に失敗: {$code} - {$mes}";
182- \logger(\LogType::MySQL, $msg);
183- throw new \Exception($msg);
184- }
185-
186- $pos = 0;
187- $statementId = unpack('V', substr($res, $pos + 1, 4))[1]; // ステートメントID
188- $pos += 5;
189- $numCols = unpack('v', substr($res, $pos, 2))[1]; // 列数
190- $pos += 2;
191- $numParam = unpack('v', substr($res, $pos, 2))[1]; // パラメートル数
192- $pos += 4;
193-
194- $this->prepared[$statementId] = [
195- 'num_params' => $numParam,
196- 'num_columns' => $numCols,
197- 'params' => [],
198- 'columns' => [],
199- ];
200-
201- if ($numParam > 0) {
202- for ($i = 0; $i < $numParam; $i++) {
203- $paramPacket = $this->readPacket();
204- $this->prepared[$statementId]['params'][] =
205- $this->parseFieldPacket($paramPacket);
206- }
207-
208- $this->readPacket();
209- }
210-
211- if ($numCols > 0) {
212- for ($i = 0; $i < $numCols; $i++) {
213- $columnPacket = $this->readPacket();
214- $this->prepared[$statementId]['columns'][] =
215- $this->parseFieldPacket($columnPacket);
216- }
217-
218- $this->readPacket();
219- }
220-
221- return $statementId;
222- }
223-
224- /**
225- * プリペアドステートメントの実行
226- *
227- * 指定されたステートメントIDとパラメータを使用してクエリを実行します。
228- *
229- * @param int $statementId プリペアドステートメントID
230- * @param array $params パラメータ値の配列
231- * @return array 結果セットまたはOKパケットデータ
232- * @throws \Exception 実行に失敗した場合
233- */
234- public function execute(int $statementId, array $params = []): array {
235- if (!MYSQL_ENABLED) return [];
236-
237- if (!isset($this->prepared[$statementId])) {
238- $msg = "不正なステートメントID: {$statementId}";
239- \logger(\LogType::MySQL, $msg);
240- throw new \Exception($msg);
241- }
242-
243- $stmtInfo = $this->prepared[$statementId];
244- if (count($params) != $stmtInfo['num_params']) {
245- $msg = "パラメータ数が一致しません: 期待 {$stmtInfo['num_params']}, 取得 ".count($params);
246- \logger(\LogType::MySQL, $msg);
247- throw new \Exception($msg);
248- }
249-
250- $data = chr(0x17); // COM_STMT_EXECUTE
251- $data .= pack('V', $statementId);
252- $data .= chr(0); // 0 = カーソルなし
253- $data .= pack('V', 1); // 繰り返し数(常に1)
254-
255- if ($stmtInfo['num_params'] > 0) {
256- // NULLビットマップ
257- $nullBitmap = str_repeat("\0", ceil($stmtInfo['num_params'] / 8));
258- foreach ($params as $k => $v) {
259- if ($v === NULL) {
260- $nullBitmap[$k >> 3] = chr(ord($nullBitmap[$k >> 3]) | (1 << ($k & 7)));
261- }
262- }
263-
264- $data .= $nullBitmap;
265-
266- $data .= chr(1); // 新パラメートルフラグ(1=はい)
267-
268- $paramTypes = '';
269- $paramValues = '';
270- foreach ($params as $param) {
271- /**
272- * MYSQL_TYPE_DECIMAL 0x00
273- * MYSQL_TYPE_TINY 0x01
274- * MYSQL_TYPE_SHORT 0x02
275- * MYSQL_TYPE_LONG 0x03
276- * MYSQL_TYPE_FLOAT 0x04
277- * MYSQL_TYPE_DOUBLE 0x05
278- * MYSQL_TYPE_NULL 0x06
279- * MYSQL_TYPE_TIMESTAMP 0x07
280- * MYSQL_TYPE_LONGLONG 0x08
281- * MYSQL_TYPE_INT24 0x09
282- * MYSQL_TYPE_DATE 0x0A
283- * MYSQL_TYPE_TIME 0x0B
284- * MYSQL_TYPE_DATETIME 0x0C
285- * MYSQL_TYPE_YEAR 0x0D
286- * MYSQL_TYPE_NEWDATE 0x0E
287- * MYSQL_TYPE_VARCHAR 0x0F
288- * MYSQL_TYPE_BIT 0x10
289- *
290- * MYSQL_TYPE_NEWDECIMAL 0xF6
291- * MYSQL_TYPE_ENUM 0xF7
292- * MYSQL_TYPE_SET 0xF8
293- * MYSQL_TYPE_TINY_BLOB 0xF9
294- * MYSQL_TYPE_MEDIUM_BLOB 0xFA
295- * MYSQL_TYPE_LONG_BLOB 0xFB
296- * MYSQL_TYPE_BLOB 0xFC
297- * MYSQL_TYPE_VAR_STRING 0xFD
298- * MYSQL_TYPE_STRING 0xFE
299- * MYSQL_TYPE_GEOMETRY 0xFF
300- */
301- if ($param === null) {
302- $paramType .= pack('v', 0x06); // MYSQL_TYPE_NULL
303- } else if (is_int($param)) {
304- $intLen = strlen((string)$param);
305- if ($intLen == 10) {
306- $paramTypes .= pack('v', 0x07); // MYSQL_TYPE_TIMESTAMP
307- } else if ($intLen >= -128 && $intLen < 127) {
308- $paramTypes .= pack('v', 0x01); // MYSQL_TYPE_TINY
309- } else if ($intLen >= -32768 && $intLen < 32767) {
310- $paramTypes .= pack('v', 0x02); // MYSQL_TYPE_SHORT
311- } else if ($intLen >= -8388608 && $intLen < 8388607) {
312- $paramTypes .= pack('v', 0x09); // MYSQL_TYPE_INT24
313- } else if ($intLen >= -2147483648 && $intLen < 2147483647) {
314- $paramTypes .= pack('v', 0x03); // MYSQL_TYPE_LONG
315- } else if ($intLen >= -9223372036854775808 && $intLen < 9223372036854775807) {
316- $paramTypes .= pack('v', 0x08); // MYSQL_TYPE_LONGLONG
317- }
318- $paramValues .= pack('V', $param);
319- } else if (is_float($param)) {
320- $decLen = strpos(strrev((string)$param), '.');
321- if ($decLen !== FALSE && $decLen < 25) {
322- $paramTypes .= pack('v', 0x04); // MYSQL_TYPE_FLOAT
323- } else if ($decLen !== FALSE && $decLen >= 25 && $decLen < 60) {
324- $paramTypes .= pack('v', 0x05); // MYSQL_TYPE_DOUBLE
325- }
326- $paramValues .= pack('d', $param);
327- } else {
328- $paramTypes .= pack('v', 0x0F); // MYSQL_TYPE_STRING
329- $len = strlen($param);
330- $paramValues .= $this->encodeLengthEncodedInteger($len).$param;
331- }
332- }
333-
334- $data .= $paramTypes.$paramValues;
335- }
336-
337- $this->sendPacket($data);
338- $res = $this->readPacket();
339-
340- if (ord($res[0]) === 0xFF) {
341- $code = unpack('v', substr($res, 1, 2))[1];
342- $mes = substr($res, 3);
343- $msg = "実行に失敗: {$code} - {$mes}";
344- \logger(\LogType::MySQL, $msg);
345- throw new \Exception($msg);
346- }
347-
348- if (ord($res[0]) === 0x00) {
349- return $this->parseOkPacket($res);
350- }
351-
352- return $this->parseResultSet($res);
353- }
354-
355- /**
356- * プリペアドステートメントの解放
357- *
358- * 指定されたステートメントIDを解放し、リソースをクリーンアップします。
359- *
360- * @param int $statementId プリペアドステートメントID
361- * @return bool 成功時はtrue
362- */
363- public function demolish(int $statementId): bool {
364- if (!MYSQL_ENABLED || !isset($this->prepared[$statementId])) return false;
365-
366- $data = chr(0x19).pack('V', $statementId); // COM_STMT_CLOSE
367- $this->sendPacket($data);
368-
369- unset($this->prepared[$statementId]);
370- return true;
371- }
372-
373- /**
374- * SQLクエリの実行
375- *
376- * COM_QUERYを使用してSQLクエリを実行し、結果を返します。
377- *
378- * @param string $query 実行するSQLクエリ
379- * @return array 結果セットまたはOKパケットデータ
380- * @throws \Exception クエリ実行に失敗した場合
381- */
382- public function query(string $query): array {
383- if (!MYSQL_ENABLED) return [];
384-
385- $this->sendCommand(0x03, $query); // COM_QUERY
386- $res = $this->readPacket();
387-
388- if (ord($res[0]) === 0xFF) {
389- $code = unpack('v', substr($res, 1, 2))[1];
390- $mes = substr($res, 3);
391- $msg = "クエリに失敗: {$code} - {$mes}";
392- \logger(\LogType::MySQL, $msg);
393- throw new \Exception($msg);
394- }
395-
396- // レスポンスは0x00で始まったら、OKパケットだ
397- if (ord($res[0]) === 0x00) {
398- return $this->parseOkPacket($res);
399- }
400-
401- // レスポンスは0xFBで始まったら、、 LOCAL INFILEリクエストだ
402- // @todo LOCAL INFILEリクエストの処理を実装
403- if (ord($res[0]) === 0xFB) {
404- $msg = "LOCAL INFOリクエストは未対応です";
405- \logger(\LogType::MySQL, $msg);
406- throw new \Exception($msg);
407- }
408-
409- return $this->parseResultSet($res);
410- }
411-
412- /**
413- * パケットログをファイルに保存する
414- *
415- * デバッグ用に収集したパケットログを指定されたファイルに保存します。
416- *
417- * @param string $filename 保存先ファイル名
418- * @return bool|int 成功時は書き込んだバイト数、失敗時はfalse
419- */
420- public function savePacketLogToFile(string $filename): bool|int {
421- if (!MYSQL_ENABLED) return 0;
422-
423- $output = '';
424-
425- foreach ($this->packetLog as $index => $packetInfo) {
426- $direction = $packetInfo['direction'];
427- $length = $packetInfo['length'];
428- $seqNum = $packetInfo['seqNum'];
429- $data = $packetInfo['data'];
430- $timestamp = date('Y-m-d H:i:s', (int)$packetInfo['timestamp']);
431-
432- $output .= "=== パケット #{$index} ({$timestamp}) {$direction}
433- $output .= (長さ: {$length}, シーケンス: {$seqNum}) ===\n";
434- $output .= "16進数: ".$this->hexDump($data)."\n";
435- $output .= "ASCII: ".$this->asciiDump($data)."\n";
436- $output .= "==========================================\n\n";
437- }
438-
439- return file_put_contents(ROOT.'/log/'.$filename, $output);
440- }
441-
442- // 機能性メソッド
443-
444- /**
445- * MySQLサーバーで認証する
446- *
447- * クライアント機能フラグと認証情報を送信してサーバー認証を行います。
448- *
449- * @return bool 認証成功時はtrue
450- * @throws \Exception 認証に失敗した場合
451- */
452- private function authenticate(): bool {
453- /**
454- * CLIENT_LONG_PASSWORD 0x00000001
455- * CLIENT_PROTOCOL_41 0x00000200
456- * CLIENT_SECURE_CONNECTION 0x00008000
457- * CLIENT_CONNECT_WITH_DB 0x00000800
458- *
459- * 0x00020D05 = CLIENT_LONG_PASSWORD | CLIENT_PROTOCOL_41 |
460- * CLIENT_SECURE_CONNECTION | CONNECT_WITH_DB
461- */
462- $data = '';
463- $data .= pack('L', 0x00020D05); // クライアント機能フラグ
464- $data .= pack('L', 16777216); // パケットサイズの大きさ
465- $data .= chr(33); // チャーセット(33 = utf8_general_ci)
466- $data .= str_repeat("\0", 23); // 予約バイト
467- $data .= $this->username."\0"; // ユーザー名
468-
469- // パスワード
470- if (empty($this->password)) {
471- $data .= "\0"; // 空
472- } else {
473- $pw = $this->scramblePassword($this->password, $this->serverInfo['scramble']);
474- $data .= chr(strlen($pw)).$pw;
475- }
476-
477- // データベース名
478- if (!empty($this->dbname)) {
479- $data .= $this->dbname."\0";
480- }
481-
482- // 認証パケットを送信する
483- $this->sendPacket($data, 1);
484-
485- // サーバー返事を送る
486- $res = $this->readPacket();
487-
488- if (ord($res[0]) === 0xFF) {
489- $code = unpack('v', substr($res, 1, 2))[1];
490- $mes = substr($res, 3);
491- $this->close();
492- $msg = "認証に失敗: {$code} - {$mes}";
493- \logger(\LogType::MySQL, $msg);
494- throw new \Exception($msg);
495- }
496-
497- return true;
498- }
499-
500- /**
501- * サーバーから結果セットを解析する
502- *
503- * クエリ結果のフィールドと行データを解析して返します。
504- *
505- * @param string $firstPacket 最初の結果セットパケット
506- * @return array フィールドと行データの配列
507- * @throws \Exception EOFパケットが期待通りに受信できない場合
508- */
509- private function parseResultSet(string $firstPacket): array {
510- $fieldCnt = ord($firstPacket[0]);
511-
512- $fields = [];
513- for ($i = 0; $i < $fieldCnt; $i++) {
514- $fieldPacket = $this->readPacket();
515- $fields[] = $this->parseFieldPacket($fieldPacket);
516- }
517-
518- $eofPacket = $this->readPacket();
519- if (ord($eofPacket[0]) !== 0xFE) {
520- $msg = "フィールド説明の後にEOFパケットが期待されます";
521- \logger(\LogType::MySQL, $msg);
522- throw new \Exception($msg);
523- }
524-
525- $rows = [];
526- while (true) {
527- $rowPacket = $this->readPacket();
528-
529- // 行データの終了を示すEOFパケットを確認
530- if (ord($rowPacket[0]) === 0xFE && strlen($rowPacket) < 9) break;
531- $rows[] = $this->parseRowPacket($rowPacket, $fields);
532- }
533-
534- return [
535- 'fields' => $fields,
536- 'rows' => $rows,
537- ];
538- }
539-
540- /**
541- * フィールドパケットを解析する
542- *
543- * フィールドのメタデータを解析して返します。
544- *
545- * @param string $packet フィールドパケット
546- * @return array フィールドのメタデータ
547- */
548- private function parseFieldPacket(string $packet): array {
549- $pos = 0;
550- $field = [];
551-
552- // カタログのスキップ(def等)
553- $len = $this->getLengthEncodedIntegerValue($packet, $pos);
554- $pos += $this->getLengthEncodedIntegerSize($len);
555- $field['catalog'] = substr($packet, $pos, $len);
556- $pos += 1 + $len;
557-
558- // データベース名
559- $len = $this->getLengthEncodedIntegerValue($packet, $pos);
560- $pos += $this->getLengthEncodedIntegerSize($len);
561- $field['db'] = substr($packet, $pos, $len);
562- $pos += $len;
563-
564- // テーブル名
565- $len = $this->getLengthEncodedIntegerValue($packet, $pos);
566- $pos += $this->getLengthEncodedIntegerSize($len);
567- $field['table'] = substr($packet, $pos, $len);
568- $pos += $len;
569-
570- // 元のテーブル名
571- $len = $this->getLengthEncodedIntegerValue($packet, $pos);
572- $pos += $this->getLengthEncodedIntegerSize($len);
573- $field['org_table'] = substr($packet, $pos, $len);
574- $pos += 1 + $len;
575-
576- // フィールド名
577- $len = $this->getLengthEncodedIntegerValue($packet, $pos);
578- $pos += $this->getLengthEncodedIntegerSize($len);
579- $field['name'] = substr($packet, $pos, $len);
580- $pos += $len;
581-
582- // 元のフィールド名
583- $len = $this->getLengthEncodedIntegerValue($packet, $pos);
584- $pos += $this->getLengthEncodedIntegerSize($len);
585- $field['org_name'] = substr($packet, $pos, $len);
586- $pos += $len;
587-
588- // フィルターバイトをスキップ(通常は0x0C)
589- $pos += 1;
590-
591- // 文字セット
592- $field['charset'] = unpack('v', substr($packet, $pos, 2))[1];
593- $pos += 2;
594-
595- // 列の長さ
596- $field['length'] = unpack('V', substr($packet, $pos, 4))[1];
597- $pos += 4;
598-
599- // フィールド種類
600- $field['type'] = ord($packet[$pos]);
601- $pos += 1;
602-
603- // フラグ
604- $field['flags'] = unpack('v', substr($packet, $pos, 2))[1];
605- $pos += 2;
606-
607- // 小数点以下の桁数
608- $field['decimals'] = ord($packet[$pos]);
609- $pos += 1;
610-
611- // フィルターバイトをスキップ
612- $pos += 2;
613-
614- // デフォルト値(存在する場合、長さエンコード文字列)
615- if ($pos < strlen($packet)) {
616- $len = $this->getLengthEncodedIntegerValue($packet, $pos);
617- $pos += $this->getLengthEncodedIntegerSize($len);
618- $field['default'] = substr($packet, $pos, $len);
619- }
620-
621- return $field;
622- }
623-
624- /**
625- * 行パケットを解析する
626- *
627- * 結果セットの行データを解析して返します。
628- *
629- * @param string $packet 行パケット
630- * @param array $fields フィールドメタデータの配列
631- * @return array 行データの連想配列
632- */
633- private function parseRowPacket(string $packet, array $fields): array {
634- $pos = 0;
635- $row = [];
636-
637- foreach ($fields as $field) {
638- // 0xFB = NULL
639- if (ord($packet[$pos]) === 0xFB) {
640- $row[$field['name']] = null;
641- $pos++;
642- continue;
643- }
644-
645- // 長さ
646- $len = ord($packet[$pos]);
647- $pos++;
648- $row[$field['name']] = substr($packet, $pos, $len);
649- $pos += $len;
650- }
651-
652- return $row;
653- }
654-
655- /**
656- * OKパケットを解析する
657- *
658- * OKパケットの内容を解析して影響を受けた行数や挿入IDなどを返します。
659- *
660- * @param string $packet OKパケット
661- * @return array OKパケットのデータ
662- * @throws \Exception パケットが不完全な場合
663- */
664- private function parseOkPacket(string $packet): array {
665- if (strlen($packet) < 2) {
666- $msg = "OKパケットが短すぎます: ".strlen($packet)."バイト";
667- \logger(\LogType::MySQL, $msg);
668- throw new \Exception($msg);
669- }
670-
671- $pos = 1; // ヘッダーバイト(0x00)をスキップする
672-
673- $affectedRows = $this->getLengthEncodedIntegerValue($packet, $pos);
674- $pos += $this->getLengthEncodedIntegerSize($affectedRows);
675-
676- $insertId = $this->getLengthEncodedIntegerValue($packet, $pos);
677- $pos += $this->getLengthEncodedIntegerSize($insertId);
678-
679- if (strlen($packet) < $pos + 2) {
680- $msg = "OKパケットにサーバーステータス用のデータが不足しています";
681- \logger(\LogType::MySQL, $msg);
682- throw new \Exception($msg);
683- }
684- $serverStatus = unpack('v', substr($packet, $pos, 2))[1];
685- $pos += 2;
686-
687- if (strlen($packet) < $pos + 2) {
688- $msg = "OKパケットに警告カウント用のデータが不足しています";
689- \logger(\LogType::MySQL, $msg);
690- throw new \Exception($msg);
691- }
692- $warningCount = unpack('v', substr($packet, $pos, 2))[1];
693-
694- return [
695- 'affectedRows' => $affectedRows,
696- 'insertId' => $insertId,
697- 'serverStatus' => $serverStatus,
698- 'warningCount' => $warningCount,
699- ];
700- }
701-
702- /**
703- * MySQLサーバーからパケットを読み込む
704- *
705- * ソケットからパケットを読み込み、完全なデータを受信するまで待機します。
706- *
707- * @return string 受信したパケットデータ
708- * @throws \Exception 読み込みに失敗した場合
709- */
710- private function readPacket(): string {
711- $header = ''; // パケットのヘッダー=4バイト
712- $bytesRead = socket_recv($this->socket, $header, 4, MSG_WAITALL);
713- if ($bytesRead !== 4) {
714- $msg = "パケットヘッダーの読み込みに失敗: 期待 4 バイト, 取得 {$bytesRead}";
715- \logger(\LogType::MySQL, $msg);
716- throw new \Exception($msg);
717- }
718-
719- // パケットの長さを最初3バイトからパーシングする
720- $len = ord($header[0]) + (ord($header[1]) << 8) + (ord($header[2]) << 16);
721-
722- // パケットの順序番号は第4目のバイト
723- $seqNum = ord($header[3]);
724-
725- // パケットの内容を読み込む
726- $data = '';
727- $remaining = $len;
728- $timeout = 5;
729- socket_set_option($this->socket, SOL_SOCKET, SO_RCVTIMEO, [
730- 'sec' => $timeout,
731- 'usec' => 0,
732- ]);
733-
734- while ($remaining > 0) {
735- $buffer = '';
736- $bytesRead = socket_recv($this->socket, $buffer, $remaining, 0);
737-
738- if ($bytesRead === false) {
739- $msg = "パケット内容の読み込みに失敗: エラー "
740- .socket_strerror(socket_last_error($this->socket));
741- \logger(\LogType::MySQL, $msg);
742- throw new \Exception($msg);
743- }
744-
745- if ($bytesRead === 0) {
746- usleep(10000);
747- continue;
748- }
749-
750- $data .= $buffer;
751- $remaining -= $bytesRead;
752- }
753-
754- if (ord($data[0]) === 0x00 && strlen($data) < 7) {
755- $extra = '';
756- $extraBytes = socket_recv($this->socket, $extra, 7 - strlen($data), 0);
757- if ($extraBytes !== false && $extraBytes > 0) {
758- $data .= $extra;
759- }
760- }
761-
762- // デバッグ
763- if ($this->debug) {
764- $packetInfo = [
765- 'direction' => 'RECV',
766- 'length' => $len,
767- 'seqNum' => $seqNum,
768- 'data' => $data,
769- 'timestamp' => microtime(true),
770- ];
771-
772- $this->logPacket($packetInfo);
773- }
774-
775- return $data;
776- }
777-
778- /**
779- * MySQLサーバーにパケットを送信する
780- *
781- * 指定されたデータとシーケンス番号でパケットを送信します。
782- *
783- * @param string $data 送信するデータ
784- * @param int $seqNum シーケンス番号(デフォルトは0)
785- * @return bool 成功時はtrue
786- * @throws \Exception 送信に失敗した場合
787- */
788- private function sendPacket(string $data, $seqNum = 0): bool {
789- $len = strlen($data);
790-
791- // パケットヘッダー:長さ=3バイト、順序番号=1バイト
792- $header = chr($len & 0xFF)
793- .chr(($len >> 8) & 0xFF)
794- .chr(($len >> 16) & 0xFF)
795- .chr($seqNum);
796-
797- // デバッグ
798- if ($this->debug) {
799- $packetInfo = [
800- 'direction' => 'SEND',
801- 'length' => $len,
802- 'seqNum' => $seqNum,
803- 'data' => $data,
804- 'timestamp' => microtime(true),
805- ];
806-
807- $this->logPacket($packetInfo);
808- }
809-
810- // ヘッダーの送信
811- $sent = socket_write($this->socket, $header, 4);
812- if ($sent !== 4) {
813- $msg = "パケットヘッダーの送信に失敗";
814- \logger(\LogType::MySQL, $msg);
815- throw new \Exception($msg);
816- }
817-
818- // データの送信
819- $sent = socket_write($this->socket, $data, $len);
820- if ($sent !== $len) {
821- $msg = "パケットデータの送信に失敗";
822- \logger(\LogType::MySQL, $msg);
823- throw new \Exception($msg);
824- }
825-
826- return true;
827- }
828-
829- /**
830- * MySQLサーバーにコマンドを送信する
831- *
832- * 指定されたコマンドとデータを送信します。
833- *
834- * @param string $command コマンド(例: 0x03 = COM_QUERY)
835- * @param string $data 付加データ(デフォルトは空)
836- * @return bool 成功時はtrue
837- * @throws \Exception 送信に失敗した場合
838- */
839- private function sendCommand(string $command, string $data = ''): bool {
840- $packet = chr($command).$data;
841- return $this->sendPacket($packet);
842- }
843-
844- /**
845- * サーバーの挨拶パケットを解析する
846- *
847- * サーバーからの初期挨拶パケットを解析し、サーバー情報を保存します。
848- *
849- * @param string $packet 挨拶パケット
850- * @return void
851- */
852- private function parseServerGreeting(string $packet): void {
853- $pos = 0;
854-
855- // プロトコールバージョン(1バイト)
856- $this->serverInfo['protocol'] = ord($packet[$pos]);
857- $pos++;
858-
859- // サーバーバージョン
860- $end = strpos($packet, "\0", $pos);
861- $this->serverInfo['version'] = substr($packet, $pos, $end - $pos);
862- $pos = $end + 1;
863-
864- // スレッドID(4バイト)
865- $this->serverInfo['threadId'] = unpack('V', substr($packet, $pos, 4))[1];
866- $pos += 4;
867-
868- // スクランブルバッファの最初の部分(8バイト)
869- $this->serverInfo['scramble'] = substr($packet, $pos, 8);
870- $pos += 8;
871-
872- // フィルターバイトをスキップする
873- $pos++;
874-
875- // サーバー機能(2バイト)
876- $this->serverInfo['capabilities'] = unpack('v', substr($packet, $pos, 2))[1];
877- $pos += 2;
878-
879- // サーバー言語(1バイト)
880- $this->serverInfo['language'] = ord($packet[$pos]);
881- $pos++;
882-
883- // サーバー状況(2バイト)
884- $this->serverInfo['status'] = unpack('v', substr($packet, $pos, 2))[1];
885- $pos += 2;
886-
887- // 13バイトのスキップ
888- $pos += 13;
889-
890- // その他(12バイト)
891- $this->serverInfo['scramble'] .= substr($packet, $pos, 12);
892- }
893-
894- /**
895- * パスワードをスクランブルする
896- *
897- * MySQL認証用のパスワードをスクランブルします。
898- *
899- * @param string $password プレーンテキストのパスワード
900- * @param string $scramble サーバーから提供されたスクランブル文字列
901- * @return string スクランブルされたパスワード(20バイト)
902- */
903- private function scramblePassword(string $password, string $scramble): string {
904- $stage1 = sha1($password, true);
905- $stage2 = sha1($stage1, true);
906- $stage3 = sha1($scramble.$stage2, true);
907-
908- // $stage1 XOR $stage3
909- $res = '';
910- for ($i = 0; $i < 20; $i++) {
911- $res .= chr(ord($stage1[$i]) ^ ord($stage3[$i]));
912- }
913-
914- return $res;
915- }
916-
917- /**
918- * デバッグのためにパケットをログする
919- *
920- * パケット情報をログに追加し、デバッグ出力を表示します。
921- *
922- * @param array $packetInfo パケット情報(方向、長さ、シーケンス番号、データ、タイムスタンプ)
923- * @return void
924- */
925- private function logPacket(array $packetInfo): void {
926- $this->packetLog[] = $packetInfo;
927-
928- $direction = $packetInfo['direction'];
929- $length = $packetInfo['length'];
930- $seqNum = $packetInfo['seqNum'];
931- $data = $packetInfo['data'];
932-
933- echo "=== {$direction} パケット (長さ: {$length}, シーケンス: $seqNum) ===\n";
934- echo $this->hexDumpWithAscii($data)."\n";
935-
936- $this->interpretPacket($data);
937-
938- echo "==========================================\n\n";
939- }
940-
941- /**
942- * バイナリデータを16進数で出力する
943- *
944- * デバッグ用にデータを16進数形式で表示します。
945- *
946- * @param string $data バイナリデータ
947- * @return string 16進数文字列
948- */
949- private function hexDump(string $data): string {
950- $res = '';
951- $len = strlen($data);
952-
953- for ($i = 0; $i < $len; $i++) {
954- $res .= sprintf('%02X ', ord($data[$i]));
955-
956- // 読み易さの為、各16バイトで新行列を入る
957- if (($i + 1) % 16 === 0 && $i !== $len - 1) {
958- $res .= "\n";
959- }
960- }
961-
962- return $res;
963- }
964-
965- /**
966- * バイナリデータをASCIIで出力する
967- *
968- * デバッグ用にデータをASCII形式で表示します。
969- *
970- * @param string $data バイナリデータ
971- * @return string ASCII文字列
972- */
973- private function asciiDump(string $data): string {
974- $res = '';
975- $len = strlen($data);
976-
977- for ($i = 0; $i < $len; $i++) {
978- $char = ord($data[$i]);
979-
980- // 表示出来るASCII文字だけを書き出す
981- if ($char >= 32 && $char <= 126) {
982- $res .= $data[$i];
983- } else {
984- $res .= '.';
985- }
986-
987- // 読み易さの為、各16バイトで新行列を入る
988- if (($i + 1) % 16 === 0 && $i !== $len - 1) {
989- $res .= "\n";
990- }
991- }
992-
993- return $res;
994- }
995-
996- /**
997- * バイナリデータを2進数で出力する
998- *
999- * デバッグ用にデータを2進数形式で表示します。
1000- *
1001- * @param string $data バイナリデータ
1002- * @return string 2進数文字列
1003- */
1004- private function binaryDump(string $data): string {
1005- $res = '';
1006- $len = strlen($data);
1007-
1008- for ($i = 0; $i < $len; $i++) {
1009- $res .= sprintf('%08b ', ord($data[$i]));
1010-
1011- // 読み易さの為、各88バイトで新行列を入る
1012- if (($i + 1) % 8 === 0 && $i !== $len - 1) {
1013- $res .= "\n";
1014- }
1015- }
1016-
1017- return $res;
1018- }
1019-
1020- /**
1021- * パケットを最初のバイトに基づいて解釈する
1022- *
1023- * パケットの種類を特定し、デバッグ情報を出力します。
1024- *
1025- * @param string $data パケットデータ
1026- * @return void
1027- */
1028- private function interpretPacket(string $data): void {
1029- if (empty($data)) {
1030- echo "解釈: 空パケット\n";
1031- return;
1032- }
1033-
1034- $firstByte = ord($data[0]);
1035-
1036- switch ($firstByte) {
1037- case 0x00:
1038- echo "解釈: OKパケット\n";
1039- $this->debugOkPacket($data);
1040- break;
1041- case 0x17:
1042- echo "解釈: COM_STMT_EXECUTEパケット\n";
1043- $this->debugStmtExecutePacket($data);
1044- break;
1045- case 0xFF:
1046- echo "解釈: エラーパケット\n";
1047- $this->debugErrorPacket($data);
1048- break;
1049- case 0xFE:
1050- echo "解釈: EOFパケット\n";
1051- break;
1052- case 0xFB:
1053- echo "解釈: LOCAL INFILEリクエスト\n";
1054- break;
1055- default:
1056- if ($firstByte === 3
1057- && $data[1] === 'd'
1058- && $data[2] === 'e'
1059- && $data[3] === 'f') {
1060- // フィールドパケットかどうかの確認
1061- echo "解釈: フィールド説明パケット\n";
1062- $this->debugFieldPacket($data);
1063- } else if ($firstByte > 0 && $firstByte < 251) {
1064- // 結果セットパケットかどうかの確認
1065- echo "解釈: 結果セットヘッダーパケット(フィールド数: {$firstByte})\n";
1066- } else {
1067- // 以上じゃないと、列データパケットでかもしん
1068- echo "解釈: 列データパケット又はその他のパケット種類\n";
1069- $this->debugLengthEncodedStrings($data);
1070- }
1071- break;
1072- }
1073- }
1074-
1075- /**
1076- * OKパケット構造をデバッグする
1077- *
1078- * OKパケットの内容を解析し、デバッグ情報を出力します。
1079- *
1080- * @param string $data OKパケットデータ
1081- * @return void
1082- */
1083- private function debugOkPacket(string $data): void {
1084- if (strlen($data) < 2) {
1085- echo ' Error: OK packet too short ('.strlen($data)." bytes)\n";
1086- return;
1087- }
1088-
1089- $pos = 1; // ヘッダーバイトをスキップ
1090-
1091- // 影響を受けた行数を取得
1092- $affectedRows = $this->getLengthEncodedIntegerValue($data, $pos);
1093- echo " 影響を受けた行数: {$affectedRows}\n";
1094- $pos += $this->getLengthEncodedIntegerSize($affectedRows);
1095-
1096- // 最後の挿入IDを取得
1097- $insertId = $this->getLengthEncodedIntegerValue($data, $pos);
1098- echo " 最後の挿入ID: {$insertId}\n";
1099- $pos += $this->getLengthEncodedIntegerSize($insertId);
1100-
1101- // サーバーステータス
1102- if (strlen($data) >= $pos + 2) {
1103- $serverStatus = unpack('v', substr($data, $pos, 2))[1];
1104- echo " サーバーステータス: ".sprintf('0x%04X', $serverStatus)."\n";
1105- $pos += 2;
1106- } else {
1107- echo " サーバーステータス: 利用不可\n";
1108- }
1109-
1110- // 警告カウント
1111- if (strlen($data) >= $pos + 2) {
1112- $warningCount = unpack('v', substr($data, $pos, 2))[1];
1113- echo " 警告カウント: {$warningCount}\n";
1114- $pos += 2;
1115- } else {
1116- echo " 警告カウント: 利用不可\n";
1117- }
1118-
1119- // サーバーメッセージ(存在する場合)
1120- if (strlen($data) > $pos) {
1121- $message = substr($data, $pos);
1122- echo " メッセージ: ".$this->safeString($message)."\n";
1123- }
1124- }
1125-
1126- /**
1127- * エラーパケット構造をデバッグする
1128- *
1129- * エラーパケットの内容を解析し、デバッグ情報を出力します。
1130- *
1131- * @param string $data エラーパケットデータ
1132- * @return void
1133- */
1134- private function debugErrorPacket(string $data): void {
1135- $errorCode = unpack('v', substr($data, 1, 2))[1];
1136- echo " エラーコード: {$errorCode}\n";
1137-
1138- // SQLステートマーカーをスキップ(#)
1139- $sqlState = substr($data, 4, 5);
1140- echo " SQLステート: {$sqlState}\n";
1141-
1142- $errorMessage = substr($data, 9);
1143- echo " エラーメッセージ: ".$this->safeString($errorMessage)."\n";
1144- }
1145-
1146- /**
1147- * フィールドパケット構造をデバッグする
1148- *
1149- * フィールドパケットの内容を解析し、デバッグ情報を出力します。
1150- *
1151- * @param string $data フィールドパケットデータ
1152- * @return void
1153- */
1154- private function debugFieldPacket(string $data): void {
1155- $pos = 0;
1156-
1157- // パケットから長さエンコード文字列を抽出
1158- echo " フィールドパケット構造:\n";
1159-
1160- // カタログを抽出
1161- $len = $this->getLengthEncodedIntegerValue($data, $pos);
1162- $pos += $this->getLengthEncodedIntegerSize($len);
1163- $catalog = substr($data, $pos, $len);
1164- echo " カタログ: ".$this->safeString($catalog)." (長さ: $len)\n";
1165- $pos += $len;
1166-
1167- // データベースを抽出
1168- $len = $this->getLengthEncodedIntegerValue($data, $pos);
1169- $pos += $this->getLengthEncodedIntegerSize($len);
1170- $database = substr($data, $pos, $len);
1171- echo " データベース: ".$this->safeString($database)." (長さ: $len)\n";
1172- $pos += $len;
1173-
1174- // テーブルを抽出
1175- $len = $this->getLengthEncodedIntegerValue($data, $pos);
1176- $pos += $this->getLengthEncodedIntegerSize($len);
1177- $table = substr($data, $pos, $len);
1178- echo " テーブル: ".$this->safeString($table)." (長さ: $len)\n";
1179- $pos += $len;
1180-
1181- // 元のテーブルを抽出
1182- $len = $this->getLengthEncodedIntegerValue($data, $pos);
1183- $pos += $this->getLengthEncodedIntegerSize($len);
1184- $orgTable = substr($data, $pos, $len);
1185- echo " 元のテーブル: ".$this->safeString($orgTable)." (長さ: $len)\n";
1186- $pos += $len;
1187-
1188- // 名前を抽出
1189- $len = $this->getLengthEncodedIntegerValue($data, $pos);
1190- $pos += $this->getLengthEncodedIntegerSize($len);
1191- $name = substr($data, $pos, $len);
1192- echo " 名前: ".$this->safeString($name)." (長さ: $len)\n";
1193- $pos += $len;
1194-
1195- // 元の名前を抽出
1196- $len = $this->getLengthEncodedIntegerValue($data, $pos);
1197- $pos += $this->getLengthEncodedIntegerSize($len);
1198- $orgName = substr($data, $pos, $len);
1199- echo " 元の名前: ".$this->safeString($orgName)." (長さ: $len)\n";
1200- $pos += $len;
1201-
1202- // 次の長さエンコード整数を抽出(固定フィールドの長さ、通常は0x0C)
1203- $fixedLength = $this->getLengthEncodedIntegerValue($data, $pos);
1204- echo " 固定フィールドの長さ: {$fixedLength}\n";
1205- $pos += $this->getLengthEncodedIntegerSize($fixedLength);
1206-
1207- // 文字セット
1208- $charSet = unpack('v', substr($data, $pos, 2))[1];
1209- echo " 文字セット: ".sprintf('0x%04X', $charSet)."\n";
1210- $pos += 2;
1211-
1212- // 列の長さ
1213- $columnLength = unpack('V', substr($data, $pos, 4))[1];
1214- echo " 列の長さ: {$columnLength}\n";
1215- $pos += 4;
1216-
1217- // 列の種類
1218- $columnType = ord($data[$pos]);
1219- echo " 列の種類: ".sprintf('0x%02X', $columnType)."\n";
1220- $pos++;
1221-
1222- // フラグ
1223- $flags = unpack('v', substr($data, $pos, 2))[1];
1224- echo " フラグ: ".sprintf('0x%04X', $flags)."\n";
1225- $pos += 2;
1226-
1227- // 小数点以下の桁数
1228- $decimals = ord($data[$pos]);
1229- echo " 小数点以下の桁数: {$decimals}\n";
1230- $pos++;
1231-
1232- // フィルター
1233- echo " フィルター: ".sprintf('0x%04X', unpack('v', substr($data, $pos, 2))[1])."\n";
1234- }
1235-
1236- /**
1237- * パケット内の長さエンコード文字列をデバッグする
1238- *
1239- * パケットからすべての長さエンコード文字列を抽出し、デバッグ情報を出力します。
1240- *
1241- * @param string $data パケットデータ
1242- * @return void
1243- */
1244- private function debugLengthEncodedStrings(string $data): void {
1245- $pos = 0;
1246- $length = strlen($data);
1247- $stringCount = 0;
1248-
1249- echo " 長さエンコード文字列:\n";
1250-
1251- while ($pos < $length) {
1252- // 長さエンコード文字列を特定
1253- if ($pos >= $length) break;
1254-
1255- $firstByte = ord($data[$pos]);
1256-
1257- // MySQLプロトコルに基づく長さエンコーディング
1258- if ($firstByte < 251) {
1259- // 1バイト長
1260- $len = $firstByte;
1261- $pos++;
1262-
1263- if ($pos + $len <= $length) {
1264- $value = substr($data, $pos, $len);
1265- echo " 文字列 ".(++$stringCount).": ".$this->safeString($value)
1266- ." (長さ: {$len})\n";
1267- $pos += $len;
1268- } else {
1269- echo " 位置 {$pos} での無効な長さエンコーディング\n";
1270- break;
1271- }
1272- } else if ($firstByte == 251) {
1273- // NULL値
1274- echo " 文字列 ".(++$stringCount).": NULL\n";
1275- $pos++;
1276- } else if ($firstByte == 252) {
1277- // 2バイト長
1278- if ($pos + 3 <= $length) {
1279- $len = unpack('v', substr($data, $pos + 1, 2))[1];
1280- $pos += 3;
1281-
1282- if ($pos + $len <= $length) {
1283- $value = substr($data, $pos, $len);
1284- echo " 文字列 ".(++$stringCount).": ".$this->safeString($value)
1285- ." (長さ: {$len})\n";
1286- $pos += $len;
1287- } else {
1288- echo " 位置 {$pos} での無効な長さエンコーディング\n";
1289- break;
1290- }
1291- } else {
1292- echo " 位置 {$pos} での不完全な2バイト長\n";
1293- break;
1294- }
1295- } else if ($firstByte == 253) {
1296- // 3バイト長
1297- if ($pos + 4 <= $length) {
1298- $len = unpack('V', substr($data, $pos + 1, 3)."\0")[1];
1299- $pos += 4;
1300-
1301- if ($pos + $len <= $length) {
1302- $value = substr($data, $pos, $len);
1303- echo " 文字列 ".(++$stringCount).": ".$this->safeString($value)
1304- ." (長さ: {$len})\n";
1305- $pos += $len;
1306- } else {
1307- echo " 位置 {$pos} での無効な長さエンコーディング\n";
1308- break;
1309- }
1310- } else {
1311- echo " 位置 {$pos} での不完全な3バイト長\n";
1312- break;
1313- }
1314- } else if ($firstByte == 254) {
1315- // 8バイト長
1316- if ($pos + 9 <= $length) {
1317- // PHPでは8バイト整数を完全に扱えないため、最初の4バイトのみ読み取る
1318- $len = unpack('V', substr($data, $pos + 1, 4))[1];
1319- $pos += 9;
1320-
1321- if ($pos + $len <= $length) {
1322- $value = substr($data, $pos, $len);
1323- echo " 文字列 ".(++$stringCount).": ".$this->safeString($value)
1324- ." (長さ: {$len})\n";
1325- $pos += $len;
1326- } else {
1327- echo " 位置 {$pos} での無効な長さエンコーディング\n";
1328- break;
1329- }
1330- } else {
1331- echo " 位置 {$pos} での不完全な8バイト長\n";
1332- break;
1333- }
1334- } else {
1335- // 長さエンコード文字列でない場合、次のバイトへ
1336- $pos++;
1337- }
1338- }
1339-
1340- if ($stringCount === 0) {
1341- echo " No length-encoded strings found\n";
1342- }
1343- }
1344-
1345- /**
1346- * 文字列を安全に出力用に変換する
1347- *
1348- * 表示可能な文字のみを含み、非表示文字は16進数で表現します。
1349- *
1350- * @param string $str 変換する文字列
1351- * @return string 安全な文字列
1352- */
1353- private function safeString(string $str): string {
1354- $result = '';
1355- $length = strlen($str);
1356-
1357- for ($i = 0; $i < $length; $i++) {
1358- $char = ord($str[$i]);
1359- if ($char >= 32 && $char <= 126) {
1360- $result .= $str[$i];
1361- } else {
1362- $result .= '\\x'.sprintf('%02X', $char);
1363- }
1364- }
1365-
1366- return $result;
1367- }
1368-
1369- /**
1370- * 指定位置の長さエンコード整数値を取得する
1371- *
1372- * パケット内の長さエンコード整数を読み取ります。
1373- *
1374- * @param string $data パケットデータ
1375- * @param int $pos 開始位置
1376- * @return mixed 整数値または0(不明な場合)
1377- */
1378- private function getLengthEncodedIntegerValue(string $data, int $pos): mixed {
1379- $firstByte = ord($data[$pos]);
1380-
1381- if ($firstByte < 251) {
1382- return $firstByte;
1383- } else if ($firstByte == 252) {
1384- return unpack('v', substr($data, $pos + 1, 2))[1];
1385- } else if ($firstByte == 253) {
1386- return unpack('V', substr($data, $pos + 1, 3)."\0")[1];
1387- } else if ($firstByte == 254) {
1388- // 簡略化のため、8バイト整数の最初の4バイトのみ読み取る
1389- return unpack('V', substr($data, $pos + 1, 4))[1];
1390- }
1391-
1392- return 0;
1393- }
1394-
1395- /**
1396- * 長さエンコード整数のサイズを取得する
1397- *
1398- * 値に基づいて長さエンコードに必要なバイト数を返します。
1399- *
1400- * @param int $value 整数値
1401- * @return int バイト数
1402- */
1403- private function getLengthEncodedIntegerSize(int $value): int {
1404- if ($value < 251) {
1405- return 1;
1406- } else if ($value < 65536) {
1407- return 3; // 0xFCマーカー1バイト + 値2バイト
1408- } else if ($value < 16777216) {
1409- return 4; // 0xFDマーカー1バイト + 値3バイト
1410- } else {
1411- return 9; // 0xFEマーカー1バイト + 値8バイト
1412- }
1413- }
1414-
1415- /**
1416- * 整数を長さエンコード形式に変換する
1417- *
1418- * MySQLプロトコルに基づいて整数を長さエンコードします。
1419- *
1420- * @param int $value 変換する整数
1421- * @return string 長さエンコードされた文字列
1422- */
1423- private function encodeLengthEncodedInteger(int $value): string {
1424- if ($value < 251) {
1425- return chr($value);
1426- } else if ($value < 65536) {
1427- return chr(0xFC).pack('v', $value);
1428- } else if ($value < 16777216) {
1429- return chr(0xFD).pack('V', $value & 0xFFFFFF);
1430- } else {
1431- return chr(0xFE).pack('P', $value);
1432- }
1433- }
1434-
1435- /**
1436- * STMTパケットをデバッグする
1437- *
1438- * COM_STMT_EXECUTEパケットの内容を解析し、デバッグ情報を出力します。
1439- *
1440- * @param string $data パケットデータ
1441- * @return void
1442- */
1443- private function debugStmtExecutePacket(string $data): void {
1444- $pos = 1; // コマンドバイトをスキップ
1445- $statementId = unpack('V', substr($data, $pos, 4))[1];
1446- echo " ステートメントID: {$statementId}\n";
1447- $pos += 5; // ステートメントIDとカーソルフラグをスキップ
1448- $iterationCount = unpack('V', substr($data, $pos, 4))[1];
1449- echo " 繰り返し数: {$iterationCount}\n";
1450- $pos += 4;
1451-
1452- $numParams = $this->prepared[$statementId]['num_params'] ?? 0;
1453- if ($numParams > 0) {
1454- $nullBitmapLen = ceil($numParams / 8);
1455- $nullBitmap = substr($data, $pos, $nullBitmapLen);
1456- echo " NULLビットマップ: ".$this->hexDump($nullBitmap)."\n";
1457- $pos += $nullBitmapLen;
1458-
1459- $newParamsFlag = ord($data[$pos]);
1460- echo " 新パラメータフラグ: {$newParamsFlag}\n";
1461- $pos++;
1462-
1463- if ($newParamsFlag) {
1464- echo " パラメータ種類:\n";
1465-
1466- for ($i = 0; $i < $numParams; $i++) {
1467- $type = unpack('v', substr($data, $pos, 2))[1];
1468- $typeName = $this->getMysqlTypeName($type);
1469- echo " パラメータ {$i}: ".sprintf("0x%02X", $type)." ({$typeName})\n";
1470- $pos += 2;
1471- }
1472-
1473- echo " パラメータ値: (生バイナリが続く)\n";
1474- }
1475- }
1476- }
1477-
1478- /**
1479- * 16進値をMySQLタイプ名に変換する
1480- *
1481- * MySQLのデータ型コードを対応する型名に変換します。
1482- *
1483- * @param int $type 16進数の型コード
1484- * @return string 型名(不明な場合は'UNKNOWN')
1485- */
1486- private function getMysqlTypeName(int $type): string {
1487- $types = [
1488- 0x00 => 'MYSQL_TYPE_DECIMAL',
1489- 0x01 => 'MYSQL_TYPE_TINY',
1490- 0x02 => 'MYSQL_TYPE_SHORT',
1491- 0x03 => 'MYSQL_TYPE_LONG',
1492- 0x04 => 'MYSQL_TYPE_FLOAT',
1493- 0x05 => 'MYSQL_TYPE_DOUBLE',
1494- 0x06 => 'MYSQL_TYPE_NULL',
1495- 0x07 => 'MYSQL_TYPE_TIMESTAMP',
1496- 0x08 => 'MYSQL_TYPE_LONGLONG',
1497- 0x09 => 'MYSQL_TYPE_INT24',
1498- 0x0A => 'MYSQL_TYPE_DATE',
1499- 0x0B => 'MYSQL_TYPE_TIME',
1500- 0x0C => 'MYSQL_TYPE_DATETIME',
1501- 0x0D => 'MYSQL_TYPE_YEAR',
1502- 0x0E => 'MYSQL_TYPE_NEWDATE',
1503- 0x0F => 'MYSQL_TYPE_VARCHAR',
1504- 0x10 => 'MYSQL_TYPE_BIT',
1505- 0xF6 => 'MYSQL_TYPE_NEWDECIMAL',
1506- 0xF7 => 'MYSQL_TYPE_ENUM',
1507- 0xF8 => 'MYSQL_TYPE_SET',
1508- 0xF9 => 'MYSQL_TYPE_TINY_BLOB',
1509- 0xFA => 'MYSQL_TYPE_MEDIUM_BLOB',
1510- 0xFB => 'MYSQL_TYPE_LONG_BLOB',
1511- 0xFC => 'MYSQL_TYPE_BLOB',
1512- 0xFD => 'MYSQL_TYPE_VAR_STRING',
1513- 0xFE => 'MYSQL_TYPE_STRING',
1514- 0xFF => 'MYSQL_TYPE_GEOMETRY',
1515- ];
1516-
1517- return $types[$type] ?? '不明';
1518- }
1519-
1520- /**
1521- * 16進数とASCII値を横に並べて出力する
1522- *
1523- * 16進エディタのように、16進数とASCII値を並べて表示します。
1524- *
1525- * @param string $data バイナリデータ
1526- * @return string フォーマットされた文字列
1527- */
1528- private function hexDumpWithAscii(string $data): string {
1529- $output = '';
1530- $len = strlen($data);
1531- $offset = 0;
1532-
1533- while ($offset < $len) {
1534- $hex = '';
1535- $ascii = '';
1536- $bytesInLine = min(16, $len - $offset); // 1行あたり16バイト
1537-
1538- // 16進数部分
1539- for ($i = 0; $i < 16; $i++) {
1540- if ($i < $bytesInLine) {
1541- $hex .= sprintf('%02X ', ord($data[$offset + $i]));
1542- } else {
1543- $hex .= ' '; // 揃えのためにスペースを埋める
1544- }
1545- }
1546-
1547- // ASCII部分
1548- for ($i = 0; $i < $bytesInLine; $i++) {
1549- $char = ord($data[$offset + $i]);
1550- $ascii .= ($char >= 32 && $char <= 126) ? chr($char) : '.';
1551- }
1552-
1553- $output .= sprintf("%08X %s |%s|\n", $offset, $hex, $ascii);
1554- $offset += 16;
1555- }
1556-
1557- return $output;
1558- }
1559-}
削除 src/Site/Test/LibMysql.php
@@ -1,60 +0,0 @@
1-<?php
2-namespace Site\Test;
3-
4-require_once __DIR__.'/../../../autoload.php';
5-
6-use Site\Lib\Tester;
7-use Site\Lib\Mysql;
8-
9-if (MYSQL_ENABLED) {
10- $test = new Tester([
11- 'colorOutput' => true,
12- 'verboseOutput' => true
13- ]);
14-
15- $test->describe('パケットのデバッグ', function($test): void {
16- $db = new Mysql();
17-
18- $db->setDebug(true);
19- $db->connect();
20-
21- $result = $db->query('SELECT * FROM user WHERE id = 1');
22-
23- foreach ($result['rows'] as $row) {
24- echo "ユーザー名: ".$row['nickname']."\n";
25- }
26-
27- $db->savePacketLogToFile('mysql_log.txt');
28- $db->close();
29- });
30-
31- $test->describe('プリペアドステートメント', function($test): void {
32- $db = new Mysql();
33- $db->connect();
34-
35- // データの入り
36- $stmt = $db->prepare('INSERT INTO users (name, age) VALUES (?, ?)');
37- $test->assertTrue($stmt);
38-
39- $db->execute($stmt, ['山田太郎', 25]);
40- // TODO: assert
41-
42- $close = $db->demolish($stmt);
43- $this->assertTrue($close);
44-
45- // データの受け取り
46- $stmt = $db->prepare('SELECT * FROM users WHERE age > ?');
47- $test->assertTrue($stmt);
48-
49- $res = $db->execute($stmt, [20]);
50- // TODO: assert
51- print_r($res);
52-
53- $close = $db->demolish($stmt);
54- $this->assertTrue($close);
55-
56- $db->close();
57- });
58-
59- $test->printSummary();
60-}
変更 util.php
@@ -6,7 +6,6 @@
66 case Auth;
77 case Csv;
88 case Mailer;
9- case MySQL;
109 }
1110
1211 class Result {
@@ -227,10 +226,6 @@
227226 break;
228227 case LogType::Mailer:
229228 $logfile .= 'mail_log.text';
230- $success = true;
231- break;
232- case LogType::MySQL:
233- $logfile .= 'mysql_log.text';
234229 $success = true;
235230 break;
236231 default: