2025-09-06 07:36:04 +02:00
|
|
|
|
using System.Collections;
|
|
|
|
|
|
using System.Data.Common;
|
|
|
|
|
|
using System.Globalization;
|
2025-08-30 20:21:36 +02:00
|
|
|
|
using System.Text;
|
|
|
|
|
|
using FluentResults;
|
|
|
|
|
|
using Npgsql;
|
|
|
|
|
|
|
|
|
|
|
|
namespace pgLabII.PgUtils.ConnectionStrings;
|
|
|
|
|
|
|
2025-08-31 06:49:37 +02:00
|
|
|
|
/// <summary>
|
2025-09-06 07:36:04 +02:00
|
|
|
|
/// Parser/formatter for Npgsql-style .NET connection strings.
|
2025-08-31 06:49:37 +02:00
|
|
|
|
/// </summary>
|
2025-08-30 20:21:36 +02:00
|
|
|
|
public sealed class NpgsqlCodec : IConnectionStringCodec
|
|
|
|
|
|
{
|
|
|
|
|
|
public ConnStringFormat Format => ConnStringFormat.Npgsql;
|
|
|
|
|
|
public string FormatName => "Npgsql";
|
|
|
|
|
|
|
|
|
|
|
|
public Result<ConnectionDescriptor> TryParse(string input)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var dict = Tokenize(input);
|
|
|
|
|
|
var descriptor = new ConnectionDescriptorBuilder();
|
|
|
|
|
|
|
|
|
|
|
|
// Hosts and Ports
|
|
|
|
|
|
if (dict.TryGetValue("Host", out var hostVal) || dict.TryGetValue("Server", out hostVal) || dict.TryGetValue("Servers", out hostVal))
|
|
|
|
|
|
{
|
2025-08-31 10:12:22 +02:00
|
|
|
|
var rawHosts = SplitList(hostVal).ToList();
|
|
|
|
|
|
var hosts = new List<string>(rawHosts.Count);
|
|
|
|
|
|
var portsPerHost = new List<ushort?>(rawHosts.Count);
|
|
|
|
|
|
|
|
|
|
|
|
// First, extract inline ports from each host entry (e.g., host:5432 or [::1]:5432)
|
|
|
|
|
|
foreach (var raw in rawHosts)
|
|
|
|
|
|
{
|
|
|
|
|
|
ParseHostPort(raw, out var hostOnly, out var inlinePort);
|
|
|
|
|
|
hosts.Add(hostOnly);
|
|
|
|
|
|
portsPerHost.Add(inlinePort);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Then, merge values from Port key: single port applies to all hosts missing a port;
|
|
|
|
|
|
// list of ports applies 1:1 for hosts that still miss a port. Inline ports take precedence.
|
2025-08-30 20:21:36 +02:00
|
|
|
|
if (dict.TryGetValue("Port", out var portVal))
|
|
|
|
|
|
{
|
|
|
|
|
|
var ports = SplitList(portVal).ToList();
|
2025-08-31 10:12:22 +02:00
|
|
|
|
if (ports.Count == 1 && ushort.TryParse(ports[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var singlePort))
|
2025-08-30 20:21:36 +02:00
|
|
|
|
{
|
2025-08-31 10:12:22 +02:00
|
|
|
|
for (int i = 0; i < portsPerHost.Count; i++)
|
|
|
|
|
|
if (!portsPerHost[i].HasValue)
|
|
|
|
|
|
portsPerHost[i] = singlePort;
|
2025-08-30 20:21:36 +02:00
|
|
|
|
}
|
|
|
|
|
|
else if (ports.Count == hosts.Count)
|
|
|
|
|
|
{
|
2025-08-31 10:12:22 +02:00
|
|
|
|
for (int i = 0; i < ports.Count; i++)
|
2025-08-30 20:21:36 +02:00
|
|
|
|
{
|
2025-08-31 10:12:22 +02:00
|
|
|
|
if (!portsPerHost[i].HasValue && ushort.TryParse(ports[i], NumberStyles.Integer, CultureInfo.InvariantCulture, out var up))
|
|
|
|
|
|
portsPerHost[i] = up;
|
2025-08-30 20:21:36 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-31 10:12:22 +02:00
|
|
|
|
|
2025-08-30 20:21:36 +02:00
|
|
|
|
for (int i = 0; i < hosts.Count; i++)
|
|
|
|
|
|
{
|
2025-08-31 10:12:22 +02:00
|
|
|
|
descriptor.AddHost(hosts[i], i < portsPerHost.Count ? portsPerHost[i] : null);
|
2025-08-30 20:21:36 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Standard fields
|
|
|
|
|
|
if (TryGetFirst(dict, out var db, "Database", "Db", "Initial Catalog", "dbname"))
|
|
|
|
|
|
descriptor.Database = db;
|
|
|
|
|
|
if (TryGetFirst(dict, out var user, "Username", "User ID", "User", "UID"))
|
|
|
|
|
|
descriptor.Username = user;
|
|
|
|
|
|
if (TryGetFirst(dict, out var pass, "Password", "PWD"))
|
|
|
|
|
|
descriptor.Password = pass;
|
|
|
|
|
|
if (TryGetFirst(dict, out var app, "Application Name", "ApplicationName"))
|
|
|
|
|
|
descriptor.ApplicationName = app;
|
|
|
|
|
|
if (TryGetFirst(dict, out var timeout, "Timeout", "Connect Timeout", "Connection Timeout"))
|
|
|
|
|
|
{
|
|
|
|
|
|
if (int.TryParse(timeout, NumberStyles.Integer, CultureInfo.InvariantCulture, out var t))
|
|
|
|
|
|
descriptor.TimeoutSeconds = t;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (TryGetFirst(dict, out var ssl, "SSL Mode", "SslMode", "SSLMode"))
|
|
|
|
|
|
descriptor.SslMode = ParseSslMode(ssl);
|
|
|
|
|
|
|
|
|
|
|
|
// Preserve extras (not mapped) into Properties
|
|
|
|
|
|
var mapped = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
|
|
|
|
{
|
|
|
|
|
|
"Host","Server","Servers","Port","Database","Db","Initial Catalog","dbname",
|
|
|
|
|
|
"Username","User ID","User","UID","Password","PWD","Application Name","ApplicationName",
|
|
|
|
|
|
"Timeout","Connect Timeout","Connection Timeout","SSL Mode","SslMode","SSLMode"
|
|
|
|
|
|
};
|
|
|
|
|
|
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
|
|
|
|
|
|
{
|
2025-09-06 07:36:04 +02:00
|
|
|
|
var parts = new DbConnectionStringBuilder();
|
2025-08-30 20:21:36 +02:00
|
|
|
|
|
|
|
|
|
|
if (descriptor.Hosts != null && descriptor.Hosts.Count > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
var hostList = string.Join(',', descriptor.Hosts.Select(h => h.Host));
|
2025-09-06 07:36:04 +02:00
|
|
|
|
parts["Host"] = hostList;
|
2025-08-30 20:21:36 +02:00
|
|
|
|
var ports = descriptor.Hosts.Select(h => h.Port).Where(p => p.HasValue).Select(p => p!.Value).Distinct().ToList();
|
|
|
|
|
|
if (ports.Count == 1)
|
|
|
|
|
|
{
|
2025-09-06 07:36:04 +02:00
|
|
|
|
parts["Port"] = ports[0].ToString(CultureInfo.InvariantCulture);
|
2025-08-30 20:21:36 +02:00
|
|
|
|
}
|
|
|
|
|
|
else if (ports.Count == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
// nothing
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// Per-host ports if provided 1:1
|
|
|
|
|
|
var perHost = descriptor.Hosts.Select(h => h.Port?.ToString(CultureInfo.InvariantCulture) ?? string.Empty).ToList();
|
|
|
|
|
|
if (perHost.All(s => !string.IsNullOrEmpty(s)))
|
2025-09-06 07:36:04 +02:00
|
|
|
|
parts["Port"] = string.Join(',', perHost);
|
2025-08-30 20:21:36 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(descriptor.Database))
|
2025-09-06 07:36:04 +02:00
|
|
|
|
parts["Database"] = descriptor.Database;
|
2025-08-30 20:21:36 +02:00
|
|
|
|
if (!string.IsNullOrEmpty(descriptor.Username))
|
2025-09-06 07:36:04 +02:00
|
|
|
|
parts["Username"] = descriptor.Username;
|
2025-08-30 20:21:36 +02:00
|
|
|
|
if (!string.IsNullOrEmpty(descriptor.Password))
|
2025-09-06 07:36:04 +02:00
|
|
|
|
parts["Password"] = descriptor.Password;
|
2025-08-30 20:21:36 +02:00
|
|
|
|
if (descriptor.SslMode.HasValue)
|
2025-09-06 07:36:04 +02:00
|
|
|
|
parts["SSL Mode"] = FormatSslMode(descriptor.SslMode.Value);
|
2025-08-30 20:21:36 +02:00
|
|
|
|
if (!string.IsNullOrEmpty(descriptor.ApplicationName))
|
2025-09-06 07:36:04 +02:00
|
|
|
|
parts["Application Name"] = descriptor.ApplicationName;
|
2025-08-30 20:21:36 +02:00
|
|
|
|
if (descriptor.TimeoutSeconds.HasValue)
|
2025-09-06 07:36:04 +02:00
|
|
|
|
parts["Timeout"] = descriptor.TimeoutSeconds.Value.ToString(CultureInfo.InvariantCulture);
|
2025-08-30 20:21:36 +02:00
|
|
|
|
|
2025-09-06 07:36:04 +02:00
|
|
|
|
return Result.Ok(parts.ConnectionString);
|
2025-08-30 20:21:36 +02:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
return Result.Fail<string>(ex.Message);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static IEnumerable<string> SplitList(string s)
|
|
|
|
|
|
{
|
|
|
|
|
|
return s.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-31 10:12:22 +02:00
|
|
|
|
private static void ParseHostPort(string hostPart, out string host, out ushort? port)
|
|
|
|
|
|
{
|
|
|
|
|
|
host = hostPart;
|
|
|
|
|
|
port = null;
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(hostPart)) return;
|
|
|
|
|
|
|
|
|
|
|
|
// IPv6 in brackets: [::1]:5432
|
|
|
|
|
|
if (hostPart[0] == '[')
|
|
|
|
|
|
{
|
|
|
|
|
|
int end = hostPart.IndexOf(']');
|
|
|
|
|
|
if (end > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
host = hostPart.Substring(1, end - 1);
|
|
|
|
|
|
if (end + 1 < hostPart.Length && hostPart[end + 1] == ':')
|
|
|
|
|
|
{
|
|
|
|
|
|
var ps = hostPart.Substring(end + 2);
|
|
|
|
|
|
if (ushort.TryParse(ps, NumberStyles.Integer, CultureInfo.InvariantCulture, out var p))
|
|
|
|
|
|
port = p;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Non-IPv6: split on last ':' and ensure right side is numeric
|
|
|
|
|
|
int colon = hostPart.LastIndexOf(':');
|
|
|
|
|
|
if (colon > 0 && colon < hostPart.Length - 1)
|
|
|
|
|
|
{
|
|
|
|
|
|
var ps = hostPart.Substring(colon + 1);
|
|
|
|
|
|
if (ushort.TryParse(ps, NumberStyles.Integer, CultureInfo.InvariantCulture, out var p))
|
|
|
|
|
|
{
|
|
|
|
|
|
host = hostPart.Substring(0, colon);
|
|
|
|
|
|
port = p;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-30 20:21:36 +02:00
|
|
|
|
private static bool TryGetFirst(Dictionary<string, string> dict, out string value, params string[] keys)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var k in keys)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (dict.TryGetValue(k, out value)) return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
value = string.Empty;
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-31 13:11:59 +02:00
|
|
|
|
private static SslMode ParseSslMode(string s) => CodecCommon.ParseSslModeLoose(s);
|
2025-08-30 20:21:36 +02:00
|
|
|
|
|
2025-08-31 13:11:59 +02:00
|
|
|
|
private static string FormatSslMode(SslMode mode) => mode switch
|
2025-08-30 20:21:36 +02:00
|
|
|
|
{
|
2025-08-31 13:11:59 +02:00
|
|
|
|
SslMode.Disable => "Disable",
|
|
|
|
|
|
SslMode.Allow => "Allow",
|
|
|
|
|
|
SslMode.Prefer => "Prefer",
|
|
|
|
|
|
SslMode.Require => "Require",
|
|
|
|
|
|
SslMode.VerifyCA => "VerifyCA",
|
|
|
|
|
|
SslMode.VerifyFull => "VerifyFull",
|
|
|
|
|
|
_ => "Prefer"
|
|
|
|
|
|
};
|
2025-08-30 20:21:36 +02:00
|
|
|
|
|
|
|
|
|
|
private static Dictionary<string, string> Tokenize(string input)
|
|
|
|
|
|
{
|
2025-09-06 07:36:04 +02:00
|
|
|
|
DbConnectionStringBuilder db = new() { ConnectionString = input };
|
2025-08-30 20:21:36 +02:00
|
|
|
|
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
2025-09-06 07:36:04 +02:00
|
|
|
|
foreach (string k in db.Keys)
|
|
|
|
|
|
dict.Add(k, (string)db[k]);
|
2025-08-30 20:21:36 +02:00
|
|
|
|
return dict;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|