}> */ public function rows(string $path, string $sourceDatabase = 'acore_auth'): Generator { if (! is_file($path) || ! is_readable($path)) { throw new RuntimeException('The auth backup file is not readable.'); } $stream = fopen($path, 'rb'); if ($stream === false) { throw new RuntimeException('Could not open the auth backup.'); } $compressed = fread($stream, 2) === "\x1f\x8b"; if ($compressed) { fclose($stream); $stream = fopen('compress.zlib://'.$path, 'rb'); } else { rewind($stream); } if ($stream === false) { throw new RuntimeException('Could not open the auth backup.'); } $database = null; $schemas = []; $pending = ''; $kind = $table = null; try { while (($line = fgets($stream)) !== false) { if ($pending === '') { if (preg_match('/^\s*USE\s+`([^`]+)`\s*;/i', $line, $match)) { $database = $match[1]; continue; } if (! preg_match('/^\s*(CREATE TABLE(?: IF NOT EXISTS)?|INSERT(?: IGNORE)? INTO)\s+(?:`([^`]+)`\.)?`(account|account_access)`(?=\s|\()/i', $line, $match)) { continue; } $statementDatabase = $match[2] !== '' ? $match[2] : $database; if ($statementDatabase !== null && $statementDatabase !== $sourceDatabase) { continue; } $kind = str_starts_with(strtoupper($match[1]), 'CREATE') ? 'create' : 'insert'; $table = strtolower($match[3]); } $pending .= $line; if (! preg_match('/;\s*$/D', $line)) { continue; } if ($kind === 'create') { preg_match_all('/(?:\(\s*|,\s*)`([A-Za-z0-9_]+)`\s+[A-Za-z]/', $pending, $columns); $schemas[$table] = $columns[1]; } else { foreach ($this->insertRows($pending, $schemas[$table] ?? []) as $row) { yield ['table' => $table, 'row' => $row]; } } $pending = ''; } if ($pending !== '') { throw new RuntimeException('The auth backup has a truncated account SQL statement.'); } } finally { fclose($stream); } } /** @return Generator> */ private function insertRows(string $sql, array $schema): Generator { if (! preg_match('/^\s*INSERT(?: IGNORE)? INTO\s+(?:`[^`]+`\.)?`(?:account|account_access)`\s*(?:\(([^)]*)\))?\s*VALUES\s*/i', $sql, $header)) { throw new RuntimeException('Unsupported account INSERT syntax in the auth backup.'); } $columns = $schema; if (isset($header[1]) && trim($header[1]) !== '') { $columns = array_map(fn (string $value): string => trim($value, " \t\r\n`"), explode(',', $header[1])); } if ($columns === [] || count($columns) !== count(array_unique($columns))) { throw new RuntimeException('The auth backup must include account table definitions or explicit INSERT column names.'); } $offset = strlen($header[0]); $length = strlen($sql); while ($offset < $length) { $this->skipWhitespace($sql, $offset); if (($sql[$offset] ?? '') === ';') { $offset++; $this->skipWhitespace($sql, $offset); if ($offset !== $length) { throw new RuntimeException('Unexpected SQL after the account INSERT.'); } return; } if (($sql[$offset] ?? '') !== '(') { throw new RuntimeException('Malformed account tuple in the auth backup.'); } $offset++; $values = []; while (true) { $values[] = $this->value($sql, $offset); $this->skipWhitespace($sql, $offset); $delimiter = $sql[$offset++] ?? ''; if ($delimiter === ')') { break; } if ($delimiter !== ',') { throw new RuntimeException('Malformed account value in the auth backup.'); } } if (count($values) !== count($columns)) { throw new RuntimeException('Account column count differs from the auth backup schema.'); } yield array_combine($columns, $values); $this->skipWhitespace($sql, $offset); if (($sql[$offset] ?? '') === ',') { $offset++; } elseif (($sql[$offset] ?? '') !== ';') { throw new RuntimeException('Malformed account INSERT ending in the auth backup.'); } } throw new RuntimeException('Unterminated account INSERT in the auth backup.'); } private function value(string $sql, int &$offset): ?string { $this->skipWhitespace($sql, $offset); if (preg_match('/\G(?:_binary|_utf8mb4|_utf8)\s*/i', $sql, $prefix, 0, $offset)) { $offset += strlen($prefix[0]); } $hexQuoted = strtolower($sql[$offset] ?? '') === 'x' && ($sql[$offset + 1] ?? '') === "'"; if ($hexQuoted) { $offset++; } $quote = $sql[$offset] ?? ''; if ($quote === "'" || $quote === '"') { $offset++; $result = ''; while ($offset < strlen($sql)) { $character = $sql[$offset++]; if ($character === $quote) { if (($sql[$offset] ?? '') === $quote) { $result .= $quote; $offset++; continue; } if ($hexQuoted) { if (strlen($result) % 2 !== 0 || ($result !== '' && ! ctype_xdigit($result))) { throw new RuntimeException('Invalid quoted binary literal in the auth backup.'); } return hex2bin($result); } return $result; } if ($character === '\\') { $escaped = $sql[$offset++] ?? ''; $result .= match ($escaped) { '0' => "\0", 'n' => "\n", 'r' => "\r", 't' => "\t", 'b' => "\x08", 'Z' => "\x1a", '%', '_' => '\\'.$escaped, default => $escaped, }; } else { $result .= $character; } } throw new RuntimeException('Unterminated account string in the auth backup.'); } $start = $offset; while ($offset < strlen($sql) && $sql[$offset] !== ',' && $sql[$offset] !== ')') { $offset++; } $token = trim(substr($sql, $start, $offset - $start)); if (strcasecmp($token, 'NULL') === 0) { return null; } if (preg_match('/^0x([0-9a-f]+)$/Di', $token, $match) && strlen($match[1]) % 2 === 0) { return hex2bin($match[1]); } if (preg_match('/^-?\d+(?:\.\d+)?$/D', $token)) { return $token; } throw new RuntimeException('Unsupported account value literal in the auth backup.'); } private function skipWhitespace(string $sql, int &$offset): void { while ($offset < strlen($sql) && ctype_space($sql[$offset])) { $offset++; } } }