Skip to content

Commit d4cac28

Browse files
committed
Add robust DSN parsing and CLI improvements
- Added support for --server1-url and --server2-url flags in CLIGetter, allowing direct DSN URLs as alternatives to legacy --server1/--server2 flags. - Enhanced DsnParser to handle DSNs with unencoded special characters in credentials: if parse_url fails, the user:pass portion is percent-encoded and re-parsed. - Switched from urldecode to rawurldecode for user and password fields to correctly handle '+' and other special characters. - All relevant DsnParser tests (including edge cases for unencoded credentials) now pass in Podman.
1 parent 504d548 commit d4cac28

3 files changed

Lines changed: 50 additions & 5 deletions

File tree

src/Migration/Config/DsnParser.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,29 @@ public static function parse(string $url): array
6464
];
6565
}
6666

67+
6768
$parsed = parse_url($url);
6869

70+
// If parse_url fails (commonly because credentials contain unencoded
71+
// characters such as '@'), attempt a best-effort fix by percent-encoding
72+
// the userinfo (user:pass) portion and re-parse. This allows callers to
73+
// pass DSNs containing unencoded special characters and still succeed.
74+
if ($parsed === false || empty($parsed['scheme'])) {
75+
// Try to percent-encode user:pass if present
76+
if (preg_match('#^([a-z0-9+.-]+)://([^@/]+)@([^/]+)(/.*)?$#i', $url, $m)) {
77+
$scheme = $m[1];
78+
$userinfo = $m[2];
79+
$hostpart = $m[3];
80+
$rest = $m[4] ?? '';
81+
// Split userinfo into user:pass
82+
$userpass = explode(':', $userinfo, 2);
83+
$user = rawurlencode($userpass[0]);
84+
$pass = isset($userpass[1]) ? rawurlencode($userpass[1]) : '';
85+
$encodedUserinfo = $user . ($pass !== '' ? ":$pass" : '');
86+
$fixedUrl = "$scheme://$encodedUserinfo@$hostpart$rest";
87+
$parsed = parse_url($fixedUrl);
88+
}
89+
}
6990
if ($parsed === false || empty($parsed['scheme'])) {
7091
throw new \InvalidArgumentException("Cannot parse database URL: {$url}");
7192
}
@@ -82,8 +103,8 @@ public static function parse(string $url): array
82103

83104
// ── MySQL / Postgres ──────────────────────────────────────────────────
84105
$host = $parsed['host'] ?? 'localhost';
85-
$user = isset($parsed['user']) ? urldecode($parsed['user']) : '';
86-
$password = isset($parsed['pass']) ? urldecode($parsed['pass']) : '';
106+
$user = isset($parsed['user']) ? rawurldecode($parsed['user']) : '';
107+
$password = isset($parsed['pass']) ? rawurldecode($parsed['pass']) : '';
87108
$dbName = ltrim($parsed['path'] ?? '', '/');
88109

89110
// Default ports

src/Params/CLIGetter.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public function getParams() {
1515
$stdio = $cliFactory->newStdio();
1616

1717
$getopt = $context->getopt([
18-
'server1::', 'server2::', 'format::',
18+
'server1::', 'server2::', 'server1-url::', 'server2-url::', 'format::',
1919
'template::', 'type::', 'include::',
2020
'nocomments::', 'config::', 'output::', 'debug::',
2121
'driver::', 'supabase::'
@@ -26,10 +26,15 @@ public function getParams() {
2626
$params->input = $this->parseInput($input);
2727
} else throw new CLIException("Missing input");
2828

29-
if ($getopt->get('--server1')) {
29+
// Prefer --server1-url/--server2-url if present, else fallback to --server1/--server2
30+
if ($getopt->get('--server1-url')) {
31+
$params->server1 = $getopt->get('--server1-url');
32+
} elseif ($getopt->get('--server1')) {
3033
$params->server1 = $this->parseServer($getopt->get('--server1'));
3134
}
32-
if ($getopt->get('--server2')) {
35+
if ($getopt->get('--server2-url')) {
36+
$params->server2 = $getopt->get('--server2-url');
37+
} elseif ($getopt->get('--server2')) {
3338
$params->server2 = $this->parseServer($getopt->get('--server2'));
3439
}
3540
if ($getopt->get('--format')) {

tests/Migration/Config/DsnParserTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,25 @@ public function testUrlEncodedUser(): void
221221
$this->assertSame('my@user', $r['user']);
222222
}
223223

224+
/**
225+
* Unencoded credentials containing special characters (e.g. '@' and '+')
226+
* should be accepted by the parser via the fallback encoding behaviour.
227+
*/
228+
public function testUnencodedPasswordWithAtAndPlus(): void
229+
{
230+
$r = DsnParser::parse('postgresql://postgres:p@ss+xx@db.example.com:5432/postgres');
231+
$this->assertSame('postgres', $r['user']);
232+
$this->assertSame('p@ss+xx', $r['password']);
233+
$this->assertSame('db.example.com', $r['host']);
234+
}
235+
236+
public function testUnencodedPasswordWithExclamation(): void
237+
{
238+
$r = DsnParser::parse('postgresql://alice:p@ss!word@host:5432/db');
239+
$this->assertSame('alice', $r['user']);
240+
$this->assertSame('p@ss!word', $r['password']);
241+
}
242+
224243
// ── toServerAndDb() ───────────────────────────────────────────────────────
225244

226245
public function testToServerAndDbShape(): void

0 commit comments

Comments
 (0)