2025-08-31 13:11:59 +02:00
|
|
|
|
using System.Text;
|
2025-08-30 20:09:10 +02:00
|
|
|
|
using FluentResults;
|
|
|
|
|
|
|
|
|
|
|
|
namespace pgLabII.PgUtils.ConnectionStrings;
|
|
|
|
|
|
|
|
|
|
|
|
public sealed class LibpqCodec : IConnectionStringCodec
|
|
|
|
|
|
{
|
|
|
|
|
|
public ConnStringFormat Format => ConnStringFormat.Libpq;
|
|
|
|
|
|
public string FormatName => "libpq";
|
|
|
|
|
|
|
|
|
|
|
|
public Result<ConnectionDescriptor> TryParse(string input)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2025-08-31 13:11:59 +02:00
|
|
|
|
Result<IDictionary<string, string>> kv = new PqConnectionStringParser(new PqConnectionStringTokenizer(input)).Parse();
|
|
|
|
|
|
if (kv.IsFailed)
|
|
|
|
|
|
return kv.ToResult();
|
2025-08-30 20:09:10 +02:00
|
|
|
|
|
|
|
|
|
|
// libpq keywords are case-insensitive; normalize to lower for lookup
|
|
|
|
|
|
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
2025-08-31 13:11:59 +02:00
|
|
|
|
foreach (var pair in kv.Value)
|
2025-08-30 20:09:10 +02:00
|
|
|
|
dict[pair.Key] = pair.Value;
|
|
|
|
|
|
|
|
|
|
|
|
var descriptor = new ConnectionDescriptorBuilder();
|
|
|
|
|
|
|
|
|
|
|
|
if (dict.TryGetValue("host", out var host))
|
|
|
|
|
|
{
|
|
|
|
|
|
// libpq supports host lists separated by commas
|
2025-08-31 13:11:59 +02:00
|
|
|
|
string[] hosts = CodecCommon.SplitHosts(host);
|
2025-08-30 20:09:10 +02:00
|
|
|
|
ushort? portForAll = null;
|
|
|
|
|
|
if (dict.TryGetValue("port", out var portStr) && ushort.TryParse(portStr, out var p))
|
|
|
|
|
|
portForAll = p;
|
|
|
|
|
|
foreach (var h in hosts)
|
|
|
|
|
|
{
|
|
|
|
|
|
descriptor.AddHost(h, portForAll);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-31 13:11:59 +02:00
|
|
|
|
if (dict.TryGetValue("hostaddr", out string? hostaddr) && !string.IsNullOrWhiteSpace(hostaddr))
|
2025-08-30 20:09:10 +02:00
|
|
|
|
{
|
2025-08-31 13:11:59 +02:00
|
|
|
|
// If hostaddr is provided without a host, include as host entries as well
|
|
|
|
|
|
string[] hosts = CodecCommon.SplitHosts(hostaddr);
|
2025-08-30 20:09:10 +02:00
|
|
|
|
ushort? portForAll = null;
|
|
|
|
|
|
if (dict.TryGetValue("port", out var portStr) && ushort.TryParse(portStr, out var p))
|
|
|
|
|
|
portForAll = p;
|
|
|
|
|
|
foreach (var h in hosts)
|
|
|
|
|
|
descriptor.AddHost(h, portForAll);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (dict.TryGetValue("dbname", out var db))
|
|
|
|
|
|
descriptor.Database = db;
|
|
|
|
|
|
if (dict.TryGetValue("user", out var user))
|
|
|
|
|
|
descriptor.Username = user;
|
|
|
|
|
|
else if (dict.TryGetValue("username", out var username))
|
|
|
|
|
|
descriptor.Username = username;
|
|
|
|
|
|
if (dict.TryGetValue("password", out var pass))
|
|
|
|
|
|
descriptor.Password = pass;
|
|
|
|
|
|
|
|
|
|
|
|
if (dict.TryGetValue("sslmode", out var sslStr))
|
2025-08-31 13:11:59 +02:00
|
|
|
|
descriptor.SslMode = CodecCommon.ParseSslModeLoose(sslStr);
|
2025-08-30 20:09:10 +02:00
|
|
|
|
if (dict.TryGetValue("application_name", out var app))
|
|
|
|
|
|
descriptor.ApplicationName = app;
|
|
|
|
|
|
if (dict.TryGetValue("connect_timeout", out var tout) && int.TryParse(tout, out var seconds))
|
|
|
|
|
|
descriptor.TimeoutSeconds = seconds;
|
|
|
|
|
|
|
|
|
|
|
|
// Remaining properties: store extras excluding mapped keys
|
|
|
|
|
|
var mapped = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
|
|
|
|
{
|
|
|
|
|
|
"host","hostaddr","port","dbname","user","username","password","sslmode","application_name","connect_timeout"
|
|
|
|
|
|
};
|
|
|
|
|
|
foreach (var (k,v) in dict)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!mapped.Contains(k))
|
|
|
|
|
|
descriptor.Properties[k] = v;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return Result.Ok(descriptor.Build());
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
return Result.Fail<ConnectionDescriptor>(ex.Message);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public Result<string> TryFormat(ConnectionDescriptor descriptor)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var parts = new List<string>();
|
|
|
|
|
|
|
|
|
|
|
|
// Hosts and port
|
2025-08-31 13:11:59 +02:00
|
|
|
|
if (descriptor.Hosts.Count > 0)
|
2025-08-30 20:09:10 +02:00
|
|
|
|
{
|
|
|
|
|
|
var hostList = string.Join(',', descriptor.Hosts.Select(h => h.Host));
|
|
|
|
|
|
parts.Add(FormatPair("host", hostList));
|
|
|
|
|
|
// If all ports are same and present, emit a single port
|
|
|
|
|
|
var ports = descriptor.Hosts.Select(h => h.Port).Where(p => p.HasValue).Select(p => p!.Value).Distinct().ToList();
|
|
|
|
|
|
if (ports.Count == 1)
|
|
|
|
|
|
parts.Add(FormatPair("port", ports[0].ToString()));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(descriptor.Database))
|
|
|
|
|
|
parts.Add(FormatPair("dbname", descriptor.Database));
|
|
|
|
|
|
if (!string.IsNullOrEmpty(descriptor.Username))
|
|
|
|
|
|
parts.Add(FormatPair("user", descriptor.Username));
|
|
|
|
|
|
if (!string.IsNullOrEmpty(descriptor.Password))
|
|
|
|
|
|
parts.Add(FormatPair("password", descriptor.Password));
|
|
|
|
|
|
if (descriptor.SslMode.HasValue)
|
2025-08-31 13:11:59 +02:00
|
|
|
|
parts.Add(FormatPair("sslmode", CodecCommon.FormatSslModeUrlLike(descriptor.SslMode.Value)));
|
2025-08-30 20:09:10 +02:00
|
|
|
|
if (!string.IsNullOrEmpty(descriptor.ApplicationName))
|
|
|
|
|
|
parts.Add(FormatPair("application_name", descriptor.ApplicationName));
|
|
|
|
|
|
if (descriptor.TimeoutSeconds.HasValue)
|
|
|
|
|
|
parts.Add(FormatPair("connect_timeout", descriptor.TimeoutSeconds.Value.ToString()));
|
|
|
|
|
|
|
|
|
|
|
|
// Extra properties (avoid duplicating keys we already emitted)
|
|
|
|
|
|
var emitted = new HashSet<string>(parts.Select(p => p.Split('=')[0]), StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
|
foreach (var kv in descriptor.Properties)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!emitted.Contains(kv.Key))
|
|
|
|
|
|
parts.Add(FormatPair(kv.Key, kv.Value));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return Result.Ok(string.Join(' ', parts));
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
return Result.Fail<string>(ex.Message);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static string FormatPair(string key, string? value)
|
|
|
|
|
|
{
|
|
|
|
|
|
value ??= string.Empty;
|
|
|
|
|
|
if (NeedsQuoting(value))
|
|
|
|
|
|
return key + "='" + EscapeValue(value) + "'";
|
|
|
|
|
|
return key + "=" + value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static bool NeedsQuoting(string value)
|
|
|
|
|
|
{
|
2025-08-31 13:11:59 +02:00
|
|
|
|
return value.Any(c => char.IsWhiteSpace(c) || c == '=' || c == '\'' || c == '\\');
|
2025-08-30 20:09:10 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static string EscapeValue(string value)
|
|
|
|
|
|
{
|
|
|
|
|
|
var sb = new StringBuilder();
|
2025-08-31 13:11:59 +02:00
|
|
|
|
foreach (char c in value)
|
2025-08-30 20:09:10 +02:00
|
|
|
|
{
|
|
|
|
|
|
if (c == '\'' || c == '\\') sb.Append('\\');
|
|
|
|
|
|
sb.Append(c);
|
|
|
|
|
|
}
|
|
|
|
|
|
return sb.ToString();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|