1use std::fmt;
10#[cfg(unix)]
11use std::path::PathBuf;
12
13use super::error::{Error, Result};
14
15#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum ConnectionEndpoint {
23 Tcp {
25 host: String,
27 port: u16,
29 },
30
31 #[cfg(unix)]
33 DomainSocket {
34 directory: PathBuf,
36 name: String,
38 },
39 #[cfg(windows)]
43 NamedPipe {
44 host: String,
46 name: String,
48 },
49}
50
51impl ConnectionEndpoint {
52 pub fn tcp(host: impl Into<String>, port: u16) -> Self {
54 ConnectionEndpoint::Tcp {
55 host: host.into(),
56 port,
57 }
58 }
59
60 #[cfg(unix)]
62 pub fn domain_socket(directory: impl Into<PathBuf>, name: impl Into<String>) -> Self {
63 ConnectionEndpoint::DomainSocket {
64 directory: directory.into(),
65 name: name.into(),
66 }
67 }
68
69 #[cfg(windows)]
75 pub fn named_pipe(host: impl Into<String>, name: impl Into<String>) -> Self {
76 ConnectionEndpoint::NamedPipe {
77 host: host.into(),
78 name: name.into(),
79 }
80 }
81
82 pub fn parse(descriptor: &str) -> Result<Self> {
101 #[allow(unused_variables, reason = "`rest` is unused on non-unix platforms")]
103 if let Some(rest) = descriptor.strip_prefix("tab.domain://") {
104 #[cfg(unix)]
105 {
106 let idx = rest.find("/domain/").ok_or_else(|| {
107 Error::connection(format!(
108 "Invalid domain socket format: '{descriptor}'. Expected 'tab.domain://<dir>/domain/<name>'"
109 ))
110 })?;
111 let directory = &rest[..idx];
112 let name = &rest[idx + 8..]; if name.is_empty() {
115 return Err(Error::connection("Domain socket name cannot be empty"));
116 }
117
118 return Ok(ConnectionEndpoint::DomainSocket {
119 directory: PathBuf::from(directory),
120 name: name.to_string(),
121 });
122 }
123 #[cfg(not(unix))]
124 {
125 return Err(Error::connection(
126 "Unix domain sockets are not supported on this platform",
127 ));
128 }
129 }
130
131 #[allow(unused_variables, reason = "`rest` is unused on non-windows platforms")]
133 if let Some(rest) = descriptor.strip_prefix("tab.pipe://") {
134 #[cfg(windows)]
135 {
136 let idx = rest.find("/pipe/").ok_or_else(|| {
139 Error::connection(format!(
140 "Invalid named pipe format: '{descriptor}'. Expected 'tab.pipe://<host>/pipe/<name>'"
141 ))
142 })?;
143 let host = &rest[..idx];
144 let name = &rest[idx + 6..]; if name.is_empty() {
147 return Err(Error::connection("Named pipe name cannot be empty"));
148 }
149
150 return Ok(ConnectionEndpoint::NamedPipe {
151 host: host.to_string(),
152 name: name.to_string(),
153 });
154 }
155 #[cfg(not(windows))]
156 {
157 return Err(Error::connection(
158 "Named pipes are not supported on this platform",
159 ));
160 }
161 }
162
163 let tcp_part = descriptor
165 .strip_prefix("tab.tcp://")
166 .or_else(|| descriptor.strip_prefix("tcp.libpq://"))
167 .unwrap_or(descriptor);
168
169 Self::parse_tcp(tcp_part)
170 }
171
172 fn parse_tcp(s: &str) -> Result<Self> {
174 if s.starts_with('[') {
176 let end_bracket = s
177 .find(']')
178 .ok_or_else(|| Error::connection(format!("Invalid IPv6 address format: '{s}'")))?;
179 let host = &s[1..end_bracket];
180 let port_str = s[end_bracket + 1..]
181 .strip_prefix(':')
182 .ok_or_else(|| Error::connection(format!("Missing port in: '{s}'")))?;
183
184 let port = Self::parse_port(port_str)?;
185 return Ok(ConnectionEndpoint::Tcp {
186 host: host.to_string(),
187 port,
188 });
189 }
190
191 let colon_idx = s.rfind(':').ok_or_else(|| {
193 Error::connection(format!(
194 "Invalid endpoint format: '{s}'. Expected 'host:port'"
195 ))
196 })?;
197
198 let host = &s[..colon_idx];
199 let port_str = &s[colon_idx + 1..];
200
201 if host.is_empty() {
202 return Err(Error::connection("Host cannot be empty"));
203 }
204
205 let port = Self::parse_port(port_str)?;
206
207 Ok(ConnectionEndpoint::Tcp {
208 host: host.to_string(),
209 port,
210 })
211 }
212
213 fn parse_port(s: &str) -> Result<u16> {
215 if s == "auto" {
216 return Ok(0);
217 }
218 s.parse::<u16>()
219 .map_err(|_| Error::connection(format!("Invalid port number: '{s}'")))
220 }
221
222 #[must_use]
226 pub fn to_descriptor(&self) -> String {
227 match self {
228 ConnectionEndpoint::Tcp { host, port } => {
229 let port_str = if *port == 0 {
230 "auto".to_string()
231 } else {
232 port.to_string()
233 };
234 if host.contains(':') {
236 format!("tab.tcp://[{host}]:{port_str}")
237 } else {
238 format!("tab.tcp://{host}:{port_str}")
239 }
240 }
241 #[cfg(unix)]
242 ConnectionEndpoint::DomainSocket { directory, name } => {
243 format!("tab.domain://{}/domain/{}", directory.display(), name)
244 }
245 #[cfg(windows)]
246 ConnectionEndpoint::NamedPipe { host, name } => {
247 format!("tab.pipe://{host}/pipe/{name}")
248 }
249 }
250 }
251
252 #[cfg(unix)]
254 #[must_use]
255 pub fn socket_path(&self) -> Option<PathBuf> {
256 match self {
257 ConnectionEndpoint::DomainSocket { directory, name } => Some(directory.join(name)),
258 ConnectionEndpoint::Tcp { .. } => None,
259 }
260 }
261
262 #[cfg(windows)]
264 pub fn pipe_path(&self) -> Option<String> {
265 match self {
266 ConnectionEndpoint::NamedPipe { host, name } => {
267 Some(format!("\\\\{host}\\pipe\\{name}"))
268 }
269 ConnectionEndpoint::Tcp { .. } => None,
270 }
271 }
272
273 #[must_use]
275 pub fn is_tcp(&self) -> bool {
276 matches!(self, ConnectionEndpoint::Tcp { .. })
277 }
278
279 #[cfg(unix)]
281 #[must_use]
282 pub fn is_domain_socket(&self) -> bool {
283 matches!(self, ConnectionEndpoint::DomainSocket { .. })
284 }
285
286 #[cfg(windows)]
288 pub fn is_named_pipe(&self) -> bool {
289 matches!(self, ConnectionEndpoint::NamedPipe { .. })
290 }
291
292 #[must_use]
294 pub fn tcp_addr(&self) -> Option<(&str, u16)> {
295 match self {
296 ConnectionEndpoint::Tcp { host, port } => Some((host, *port)),
297 #[cfg(unix)]
298 ConnectionEndpoint::DomainSocket { .. } => None,
299 #[cfg(windows)]
300 ConnectionEndpoint::NamedPipe { .. } => None,
301 }
302 }
303}
304
305impl fmt::Display for ConnectionEndpoint {
306 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
307 match self {
308 ConnectionEndpoint::Tcp { host, port } => {
309 if host.contains(':') {
310 write!(f, "[{host}]:{port}")
311 } else {
312 write!(f, "{host}:{port}")
313 }
314 }
315 #[cfg(unix)]
316 ConnectionEndpoint::DomainSocket { directory, name } => {
317 write!(f, "{}/{}", directory.display(), name)
318 }
319 #[cfg(windows)]
320 ConnectionEndpoint::NamedPipe { host, name } => {
321 write!(f, "\\\\{host}\\pipe\\{name}")
322 }
323 }
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 #[test]
332 fn test_parse_tcp_simple() {
333 let ep = ConnectionEndpoint::parse("localhost:7483").unwrap();
334 assert_eq!(
335 ep,
336 ConnectionEndpoint::Tcp {
337 host: "localhost".to_string(),
338 port: 7483
339 }
340 );
341 }
342
343 #[test]
344 fn test_parse_tcp_with_scheme() {
345 let ep = ConnectionEndpoint::parse("tab.tcp://127.0.0.1:7483").unwrap();
346 assert_eq!(
347 ep,
348 ConnectionEndpoint::Tcp {
349 host: "127.0.0.1".to_string(),
350 port: 7483
351 }
352 );
353 }
354
355 #[test]
356 fn test_parse_tcp_auto_port() {
357 let ep = ConnectionEndpoint::parse("tab.tcp://localhost:auto").unwrap();
358 assert_eq!(
359 ep,
360 ConnectionEndpoint::Tcp {
361 host: "localhost".to_string(),
362 port: 0
363 }
364 );
365 }
366
367 #[test]
368 fn test_parse_tcp_ipv6() {
369 let ep = ConnectionEndpoint::parse("tab.tcp://[::1]:7483").unwrap();
370 assert_eq!(
371 ep,
372 ConnectionEndpoint::Tcp {
373 host: "::1".to_string(),
374 port: 7483
375 }
376 );
377 }
378
379 #[cfg(unix)]
380 #[test]
381 fn test_parse_domain_socket() {
382 let ep =
383 ConnectionEndpoint::parse("tab.domain:///tmp/hyper/domain/.s.PGSQL.12345").unwrap();
384 assert_eq!(
385 ep,
386 ConnectionEndpoint::DomainSocket {
387 directory: PathBuf::from("/tmp/hyper"),
388 name: ".s.PGSQL.12345".to_string()
389 }
390 );
391 }
392
393 #[test]
394 fn test_to_descriptor_tcp() {
395 let ep = ConnectionEndpoint::tcp("localhost", 7483);
396 assert_eq!(ep.to_descriptor(), "tab.tcp://localhost:7483");
397 }
398
399 #[test]
400 fn test_to_descriptor_tcp_auto() {
401 let ep = ConnectionEndpoint::tcp("localhost", 0);
402 assert_eq!(ep.to_descriptor(), "tab.tcp://localhost:auto");
403 }
404
405 #[cfg(unix)]
406 #[test]
407 fn test_to_descriptor_domain_socket() {
408 let ep = ConnectionEndpoint::domain_socket("/tmp/hyper", ".s.PGSQL.12345");
409 assert_eq!(
410 ep.to_descriptor(),
411 "tab.domain:///tmp/hyper/domain/.s.PGSQL.12345"
412 );
413 }
414
415 #[cfg(unix)]
416 #[test]
417 fn test_socket_path() {
418 let ep = ConnectionEndpoint::domain_socket("/tmp/hyper", ".s.PGSQL.12345");
419 assert_eq!(
420 ep.socket_path(),
421 Some(PathBuf::from("/tmp/hyper/.s.PGSQL.12345"))
422 );
423 }
424
425 #[test]
426 fn test_display_tcp() {
427 let ep = ConnectionEndpoint::tcp("localhost", 7483);
428 assert_eq!(format!("{ep}"), "localhost:7483");
429 }
430
431 #[cfg(unix)]
432 #[test]
433 fn test_display_domain_socket() {
434 let ep = ConnectionEndpoint::domain_socket("/tmp/hyper", ".s.PGSQL.12345");
435 assert_eq!(format!("{ep}"), "/tmp/hyper/.s.PGSQL.12345");
436 }
437
438 #[cfg(windows)]
439 #[test]
440 fn test_parse_named_pipe() {
441 let ep = ConnectionEndpoint::parse("tab.pipe://./pipe/hyper-12345").unwrap();
442 assert_eq!(
443 ep,
444 ConnectionEndpoint::NamedPipe {
445 host: ".".to_string(),
446 name: "hyper-12345".to_string()
447 }
448 );
449 }
450
451 #[cfg(windows)]
452 #[test]
453 fn test_parse_named_pipe_remote() {
454 let ep = ConnectionEndpoint::parse("tab.pipe://server1/pipe/hyper-db").unwrap();
455 assert_eq!(
456 ep,
457 ConnectionEndpoint::NamedPipe {
458 host: "server1".to_string(),
459 name: "hyper-db".to_string()
460 }
461 );
462 }
463
464 #[cfg(windows)]
465 #[test]
466 fn test_to_descriptor_named_pipe() {
467 let ep = ConnectionEndpoint::named_pipe(".", "hyper-12345");
468 assert_eq!(ep.to_descriptor(), "tab.pipe://./pipe/hyper-12345");
469 }
470
471 #[cfg(windows)]
472 #[test]
473 fn test_pipe_path() {
474 let ep = ConnectionEndpoint::named_pipe(".", "hyper-12345");
475 assert_eq!(ep.pipe_path(), Some(r"\\.\pipe\hyper-12345".to_string()));
476 }
477
478 #[cfg(windows)]
479 #[test]
480 fn test_display_named_pipe() {
481 let ep = ConnectionEndpoint::named_pipe(".", "hyper-12345");
482 assert_eq!(format!("{ep}"), r"\\.\pipe\hyper-12345");
483 }
484
485 #[cfg(windows)]
486 #[test]
487 fn test_named_pipe_is_methods() {
488 let ep = ConnectionEndpoint::named_pipe(".", "test");
489 assert!(!ep.is_tcp());
490 assert!(ep.is_named_pipe());
491 }
492}