using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace Npgsql;
///
/// Represents a .pgpass file, which contains passwords for noninteractive connections
///
sealed class PgPassFile
{
#region Properties
///
/// File name being parsed for credentials
///
internal string FileName { get; }
#endregion
#region Construction
///
/// Initializes a new instance of the class
///
///
public PgPassFile(string fileName)
=> FileName = fileName;
#endregion
///
/// Parses file content and gets all credentials from the file
///
/// corresponding to all lines in the .pgpass file
internal IEnumerable Entries
{
get
{
var bytes = File.ReadAllBytes(FileName);
var mem = new MemoryStream(bytes);
using var reader = new StreamReader(mem);
while (reader.ReadLine() is { } l)
{
var line = l.Trim();
if (line.Length > 0 && line[0] != '#')
yield return Entry.Parse(line);
}
}
}
///
/// Searches queries loaded from .PGPASS file to find first entry matching the provided parameters.
///
/// Hostname to query. Use null to match any.
/// Port to query. Use null to match any.
/// Database to query. Use null to match any.
/// User name to query. Use null to match any.
/// Matching if match was found. Otherwise, returns null.
internal Entry? GetFirstMatchingEntry(string? host = null, int? port = null, string? database = null, string? username = null)
{
foreach (var entry in Entries)
if (entry.IsMatch(host, port, database, username))
return entry;
return null;
}
///
/// Represents a hostname, port, database, username, and password combination that has been retrieved from a .pgpass file
///
internal sealed class Entry
{
#region Fields and Properties
///
/// Hostname parsed from the .pgpass file
///
internal string? Host { get; }
///
/// Port parsed from the .pgpass file
///
internal int? Port { get; }
///
/// Database parsed from the .pgpass file
///
internal string? Database { get; }
///
/// User name parsed from the .pgpass file
///
internal string? Username { get; }
///
/// Password parsed from the .pgpass file
///
internal string? Password { get; }
#endregion
#region Construction / Initialization
///
/// This class represents an entry from the .pgpass file
///
/// Hostname parsed from the .pgpass file
/// Port parsed from the .pgpass file
/// Database parsed from the .pgpass file
/// User name parsed from the .pgpass file
/// Password parsed from the .pgpass file
Entry(string? host, int? port, string? database, string? username, string? password)
{
Host = host;
Port = port;
Database = database;
Username = username;
Password = password;
}
///
/// Creates new based on string in the format hostname:port:database:username:password. The : and \ characters should be escaped with a \.
///
/// string for the entry from the pgpass file
/// New instance of for the string
/// Entry is not formatted as hostname:port:database:username:password or non-wildcard port is not a number
internal static Entry Parse(string serializedEntry)
{
var parts = new List(5);
var builder = new StringBuilder();
for (var pos = 0; pos < serializedEntry.Length; pos++)
{
var c = serializedEntry[pos];
switch (c)
{
case '\\' when pos < serializedEntry.Length - 1:
// Strip backslash before colon or backslash, otherwise preserve it
c = serializedEntry[++pos];
if (c is not (':' or '\\'))
{
builder.Append('\\');
}
builder.Append(c);
continue;
case ':':
var part = builder.ToString();
parts.Add(part == "*" ? null : part);
builder.Clear();
continue;
default:
builder.Append(c);
continue;
}
}
var lastPart = builder.ToString();
parts.Add(lastPart == "*" ? null : lastPart);
if (parts.Count != 5)
throw new FormatException("pgpass entry was not well-formed. Please ensure all non-comment entries are formatted as hostname:port:database:username:password. If colon is included, it must be escaped like \\:.");
int? port = null;
if (parts[1] != null)
{
if (!int.TryParse(parts[1], out var tempPort))
throw new FormatException("pgpass entry was not formatted correctly. Port must be a valid integer.");
port = tempPort;
}
return new Entry(parts[0], port, parts[2], parts[3], parts[4]);
}
#endregion
///
/// Checks whether this matches the parameters supplied
///
/// Hostname to check against this entry
/// Port to check against this entry
/// Database to check against this entry
/// Username to check against this entry
/// True if the entry is a match. False otherwise.
internal bool IsMatch(string? host, int? port, string? database, string? username) =>
AreValuesMatched(host, Host) && AreValuesMatched(port, Port) && AreValuesMatched(database, Database) && AreValuesMatched(username, Username);
///
/// Checks if 2 strings are a match for a considering that either value can be a wildcard (*)
///
/// Value being searched
/// Value from the PGPASS entry
/// True if the values are a match. False otherwise.
bool AreValuesMatched(string? query, string? actual)
=> query == actual || actual == null || query == null;
bool AreValuesMatched(int? query, int? actual)
=> query == actual || actual == null || query == null;
}
}