using System.Data.Common; using System.Globalization; using FluentResults; using Npgsql; namespace pgLabII.PgUtils.ConnectionStrings; /// /// Parser/formatter for Npgsql-style .NET connection strings. /// public sealed class NpgsqlCodec : IConnectionStringCodec { public ConnStringFormat Format => ConnStringFormat.Npgsql; public string FormatName => "Npgsql"; public Result 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)) { var rawHosts = SplitList(hostVal).ToList(); var hosts = new List(rawHosts.Count); var portsPerHost = new List(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. if (dict.TryGetValue("Port", out var portVal)) { var ports = SplitList(portVal).ToList(); if (ports.Count == 1 && ushort.TryParse(ports[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var singlePort)) { for (int i = 0; i < portsPerHost.Count; i++) if (!portsPerHost[i].HasValue) portsPerHost[i] = singlePort; } else if (ports.Count == hosts.Count) { for (int i = 0; i < ports.Count; i++) { if (!portsPerHost[i].HasValue && ushort.TryParse(ports[i], NumberStyles.Integer, CultureInfo.InvariantCulture, out var up)) portsPerHost[i] = up; } } } for (int i = 0; i < hosts.Count; i++) { descriptor.AddHost(hosts[i], i < portsPerHost.Count ? portsPerHost[i] : null); } } // 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(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(ex.Message); } } public Result TryFormat(ConnectionDescriptor descriptor) { try { var parts = new DbConnectionStringBuilder(); if (descriptor.Hosts != null && descriptor.Hosts.Count > 0) { var hostList = string.Join(',', descriptor.Hosts.Select(h => h.Host)); parts["Host"] = hostList; var ports = descriptor.Hosts.Select(h => h.Port).Where(p => p.HasValue).Select(p => p!.Value).Distinct().ToList(); if (ports.Count == 1) { parts["Port"] = ports[0].ToString(CultureInfo.InvariantCulture); } 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))) parts["Port"] = string.Join(',', perHost); } } if (!string.IsNullOrEmpty(descriptor.Database)) parts["Database"] = descriptor.Database; if (!string.IsNullOrEmpty(descriptor.Username)) parts["Username"] = descriptor.Username; if (!string.IsNullOrEmpty(descriptor.Password)) parts["Password"] = descriptor.Password; if (descriptor.SslMode.HasValue) parts["SSL Mode"] = FormatSslMode(descriptor.SslMode.Value); if (!string.IsNullOrEmpty(descriptor.ApplicationName)) parts["Application Name"] = descriptor.ApplicationName; if (descriptor.TimeoutSeconds.HasValue) parts["Timeout"] = descriptor.TimeoutSeconds.Value.ToString(CultureInfo.InvariantCulture); return Result.Ok(parts.ConnectionString); } catch (Exception ex) { return Result.Fail(ex.Message); } } private static IEnumerable SplitList(string s) { return s.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); } 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; } } } private static bool TryGetFirst(Dictionary 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; } private static SslMode ParseSslMode(string s) => CodecCommon.ParseSslModeLoose(s); private static string FormatSslMode(SslMode mode) => mode switch { SslMode.Disable => "Disable", SslMode.Allow => "Allow", SslMode.Prefer => "Prefer", SslMode.Require => "Require", SslMode.VerifyCA => "VerifyCA", SslMode.VerifyFull => "VerifyFull", _ => "Prefer" }; private static Dictionary Tokenize(string input) { DbConnectionStringBuilder db = new() { ConnectionString = input }; var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (string k in db.Keys) dict.Add(k, (string)db[k]); return dict; } }