| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333 |
- 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<string, string> _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<string, string> 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<string, string>();
- }
- /**
- * 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;
- }
- }
- }
|