Skip to content

Commit 410dd18

Browse files
authored
fix(core): encode postgres usernames in connection urls (#40)
Encode saved Postgres usernames when building runtime connection URLs and decode usernames imported from pasted URLs. This keeps `@` from breaking URI parsing while preserving literal percent sequences.
1 parent 1c96852 commit 410dd18

2 files changed

Lines changed: 86 additions & 2 deletions

File tree

crates/tsql/src/config/connections.rs

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ impl ConnectionEntry {
416416
let mut url = "postgres://".to_string();
417417

418418
// Add user
419-
url.push_str(&self.user);
419+
url.push_str(&urlencoding::encode(&self.user));
420420

421421
// Add password if provided
422422
if let Some(pwd) = password {
@@ -528,7 +528,9 @@ impl ConnectionEntry {
528528
DbKind::Mongo => String::new(),
529529
}
530530
} else {
531-
url.username().to_string()
531+
urlencoding::decode(url.username())
532+
.map(|s| s.into_owned())
533+
.unwrap_or_else(|_| url.username().to_string())
532534
};
533535

534536
let password = url.password().map(|p| {
@@ -2088,6 +2090,51 @@ user = "me"
20882090
assert!(url.contains("p%40ss%3Aword%2F123"));
20892091
}
20902092

2093+
#[test]
2094+
fn test_connection_to_url_with_at_in_username() {
2095+
let entry = ConnectionEntry {
2096+
name: "test".to_string(),
2097+
host: "localhost".to_string(),
2098+
port: 5432,
2099+
database: "mydb".to_string(),
2100+
user: "user@domain.com".to_string(),
2101+
..Default::default()
2102+
};
2103+
2104+
let url = entry.to_url(None);
2105+
assert_eq!(url, "postgres://user%40domain.com@localhost/mydb");
2106+
}
2107+
2108+
#[test]
2109+
fn test_connection_to_url_encodes_reserved_username_chars() {
2110+
let entry = ConnectionEntry {
2111+
name: "test".to_string(),
2112+
host: "localhost".to_string(),
2113+
port: 5432,
2114+
database: "mydb".to_string(),
2115+
user: "user:name/role".to_string(),
2116+
..Default::default()
2117+
};
2118+
2119+
let url = entry.to_url(None);
2120+
assert_eq!(url, "postgres://user%3Aname%2Frole@localhost/mydb");
2121+
}
2122+
2123+
#[test]
2124+
fn test_connection_to_url_preserves_literal_percent_username() {
2125+
let entry = ConnectionEntry {
2126+
name: "test".to_string(),
2127+
host: "localhost".to_string(),
2128+
port: 5432,
2129+
database: "mydb".to_string(),
2130+
user: "user%40team".to_string(),
2131+
..Default::default()
2132+
};
2133+
2134+
let url = entry.to_url(None);
2135+
assert_eq!(url, "postgres://user%2540team@localhost/mydb");
2136+
}
2137+
20912138
#[test]
20922139
fn test_connection_to_url_non_default_port() {
20932140
let entry = ConnectionEntry {
@@ -2117,6 +2164,28 @@ user = "me"
21172164
assert!(entry.ssl_mode.is_none());
21182165
}
21192166

2167+
#[test]
2168+
fn test_connection_from_url_decodes_encoded_username() {
2169+
let (entry, password) =
2170+
ConnectionEntry::from_url("test", "postgres://user%40domain.com@localhost/mydb")
2171+
.unwrap();
2172+
2173+
assert_eq!(entry.user, "user@domain.com");
2174+
assert!(password.is_none());
2175+
}
2176+
2177+
#[test]
2178+
fn test_connection_url_round_trip_literal_percent_username() {
2179+
let (entry, password) =
2180+
ConnectionEntry::from_url("test", "postgres://user%2540team@localhost/mydb").unwrap();
2181+
2182+
assert_eq!(entry.user, "user%40team");
2183+
assert!(password.is_none());
2184+
2185+
let url = entry.to_url(None);
2186+
assert_eq!(url, "postgres://user%2540team@localhost/mydb");
2187+
}
2188+
21202189
#[test]
21212190
fn test_connection_from_url_with_password() {
21222191
let (entry, password) =

crates/tsql/src/ui/connection_form.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2133,6 +2133,21 @@ mod tests {
21332133
assert!(form.url_paste.is_empty());
21342134
}
21352135

2136+
#[test]
2137+
fn test_url_paste_decodes_postgres_username() {
2138+
let mut form = ConnectionFormModal::new();
2139+
form.focused = FormField::UrlPaste;
2140+
form.url_paste =
2141+
"postgres://user%40domain.com:secret@db.example.com:5433/production".to_string();
2142+
2143+
let action = form.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
2144+
assert!(matches!(action, ConnectionFormAction::StatusMessage(_)));
2145+
2146+
assert_eq!(form.user, "user@domain.com");
2147+
assert_eq!(form.password, "secret");
2148+
assert_eq!(form.kind, DbKind::Postgres);
2149+
}
2150+
21362151
#[test]
21372152
fn test_url_paste_mongodb_sets_kind_and_uri() {
21382153
let mut form = ConnectionFormModal::new();

0 commit comments

Comments
 (0)