using Invercargill.DataStructures; namespace InvercargillSql { /** * Immutable representation of a parsed connection string. * * Supports standard URI format: `scheme://[user[:password]@]host[:port]/database[?options]` * * SQLite special cases: * - `sqlite:///absolute/path/to/db.sqlite` - Absolute path * - `sqlite://./relative/path.db` - Relative path * - `sqlite::memory:` or `sqlite://:memory:` - In-memory database * - `sqlite:///db.sqlite?mode=ro` - With options */ public class ConnectionString : Object { private Dictionary _options; /** * The database scheme (e.g., "sqlite", "postgresql", "mysql"). */ public string scheme { get; private set; } /** * The host name or IP address. May be empty for file-based databases. */ public string host { get; private set; } /** * The port number. 0 if not specified. */ public uint16 port { get; private set; } /** * The database name or file path. */ public string database { get; private set; } /** * The user name for authentication. May be empty. */ public string user { get; private set; } /** * The password for authentication. May be empty. */ public string password { get; private set; } /** * Query string options as key-value pairs. */ public Dictionary options { get { return _options; } } /** * The original connection string. */ public string original_string { get; private set; } /** * Private constructor - use parse() to create instances. */ private ConnectionString() { _options = new Dictionary(); } /** * Parses a connection string into its components. * * @param connection_string The connection string to parse * @return A new ConnectionString instance * @throws SqlError.INVALID_CONNECTION_STRING if the format is invalid */ public static ConnectionString parse(string connection_string) throws SqlError { var cs = new ConnectionString(); cs.original_string = connection_string; string remaining = connection_string.strip(); // Extract scheme int scheme_end = remaining.index_of(":"); if (scheme_end < 1) { throw new SqlError.INVALID_CONNECTION_STRING( "Missing scheme in connection string: %s".printf(connection_string) ); } cs.scheme = remaining.substring(0, scheme_end).down(); remaining = remaining.substring(scheme_end); // Handle SQLite special cases if (cs.scheme == "sqlite") { cs.parse_sqlite(remaining); return cs; } // Standard URI parsing for other databases cs.parse_standard_uri(remaining); return cs; } /** * Parses SQLite-specific connection string formats. */ private void parse_sqlite(string uri_part) throws SqlError { // Case 1: sqlite::memory: (no slashes) if (uri_part == "::memory:") { database = ":memory:"; host = ""; return; } // Case 2: sqlite://... (with slashes) if (!uri_part.has_prefix("://")) { throw new SqlError.INVALID_CONNECTION_STRING( "Invalid SQLite connection string format: %s".printf(original_string) ); } string path_part = uri_part.substring(3); // Remove "://" // Case 3: sqlite://:memory: if (path_part == ":memory:") { database = ":memory:"; host = ""; return; } // Split off query string if present string query_string = ""; int query_pos = path_part.index_of("?"); if (query_pos >= 0) { query_string = path_part.substring(query_pos + 1); path_part = path_part.substring(0, query_pos); } // For SQLite, the entire path is the database (file path) // The "host" portion is not used - everything after :// is the path host = ""; database = path_part; // Parse query options parse_query_string(query_string); } /** * Parses standard URI format: //[user[:password]@]host[:port]/database[?options] */ private void parse_standard_uri(string uri_part) throws SqlError { if (!uri_part.has_prefix("://")) { throw new SqlError.INVALID_CONNECTION_STRING( "Invalid connection string format (expected scheme://): %s".printf(original_string) ); } string remaining = uri_part.substring(3); // Remove "://" // Extract user:password@ if present int at_pos = remaining.index_of("@"); if (at_pos >= 0) { string auth_part = remaining.substring(0, at_pos); remaining = remaining.substring(at_pos + 1); int colon_pos = auth_part.index_of(":"); if (colon_pos >= 0) { user = auth_part.substring(0, colon_pos); password = auth_part.substring(colon_pos + 1); } else { user = auth_part; password = ""; } } else { user = ""; password = ""; } // Split off query string if present string query_string = ""; int query_pos = remaining.index_of("?"); if (query_pos >= 0) { query_string = remaining.substring(query_pos + 1); remaining = remaining.substring(0, query_pos); } // Find the / that separates host:port from database int slash_pos = remaining.index_of("/"); if (slash_pos < 0) { throw new SqlError.INVALID_CONNECTION_STRING( "Missing database name in connection string: %s".printf(original_string) ); } string host_port = remaining.substring(0, slash_pos); database = remaining.substring(slash_pos + 1); // Parse host:port int port_colon = host_port.last_index_of(":"); if (port_colon >= 0) { // Check if it's an IPv6 address (contains brackets) if (host_port.contains("[") && host_port.contains("]")) { // IPv6 format: [::1]:5432 int bracket_close = host_port.index_of("]"); if (bracket_close >= 0 && bracket_close + 1 < host_port.length && host_port[bracket_close + 1] == ':') { host = host_port.substring(1, bracket_close - 1); string port_str = host_port.substring(bracket_close + 2); port = parse_port(port_str); } else { host = host_port.substring(1, bracket_close - 1); port = 0; } } else { // IPv4 or hostname: host:port host = host_port.substring(0, port_colon); string port_str = host_port.substring(port_colon + 1); port = parse_port(port_str); } } else { host = host_port; port = 0; } // Parse query options parse_query_string(query_string); } /** * Parses a port string into a uint16. */ private uint16 parse_port(string port_str) throws SqlError { if (port_str.length == 0) { return 0; } int64 port_value = 0; if (!int64.try_parse(port_str, out port_value)) { throw new SqlError.INVALID_CONNECTION_STRING( "Invalid port number: %s".printf(port_str) ); } if (port_value < 0 || port_value > uint16.MAX) { throw new SqlError.INVALID_CONNECTION_STRING( "Port number out of range: %s".printf(port_str) ); } return (uint16)port_value; } /** * Parses query string options into the options dictionary. */ private void parse_query_string(string query_string) { if (query_string.length == 0) { return; } string[] pairs = query_string.split("&"); foreach (string pair in pairs) { string trimmed = pair.strip(); if (trimmed.length == 0) { continue; } int eq_pos = trimmed.index_of("="); if (eq_pos >= 0) { string key = Uri.unescape_string(trimmed.substring(0, eq_pos)) ?? trimmed.substring(0, eq_pos); string val = Uri.unescape_string(trimmed.substring(eq_pos + 1)) ?? trimmed.substring(eq_pos + 1); _options[key] = val; } else { _options[Uri.unescape_string(trimmed) ?? trimmed] = ""; } } } /** * Gets an option value by key. * * @param key The option key * @param default_value The default value if key is not found * @return The option value or default_value */ public string? get_option(string key, string? default_value = null) { if (has_option(key)) { return _options.get(key); } return default_value; } /** * Checks if an option exists. * * @param key The option key * @return true if the option exists */ public bool has_option(string key) { return _options.has(key); } /** * Returns a string representation of this connection string. * Note: Password is masked for security. */ public string to_safe_string() { var parts = new StringBuilder(); parts.append(scheme); parts.append("://"); if (user.length > 0) { parts.append(user); if (password.length > 0) { parts.append(":***"); } parts.append("@"); } if (host.length > 0) { parts.append(host); if (port > 0) { parts.append(":"); parts.append(port.to_string()); } parts.append("/"); } parts.append(database); return parts.str; } } }