connection-string.vala 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. using Invercargill.DataStructures;
  2. namespace InvercargillSql {
  3. /**
  4. * Immutable representation of a parsed connection string.
  5. *
  6. * Supports standard URI format: `scheme://[user[:password]@]host[:port]/database[?options]`
  7. *
  8. * SQLite special cases:
  9. * - `sqlite:///absolute/path/to/db.sqlite` - Absolute path
  10. * - `sqlite://./relative/path.db` - Relative path
  11. * - `sqlite::memory:` or `sqlite://:memory:` - In-memory database
  12. * - `sqlite:///db.sqlite?mode=ro` - With options
  13. */
  14. public class ConnectionString : Object {
  15. private Dictionary<string, string> _options;
  16. /**
  17. * The database scheme (e.g., "sqlite", "postgresql", "mysql").
  18. */
  19. public string scheme { get; private set; }
  20. /**
  21. * The host name or IP address. May be empty for file-based databases.
  22. */
  23. public string host { get; private set; }
  24. /**
  25. * The port number. 0 if not specified.
  26. */
  27. public uint16 port { get; private set; }
  28. /**
  29. * The database name or file path.
  30. */
  31. public string database { get; private set; }
  32. /**
  33. * The user name for authentication. May be empty.
  34. */
  35. public string user { get; private set; }
  36. /**
  37. * The password for authentication. May be empty.
  38. */
  39. public string password { get; private set; }
  40. /**
  41. * Query string options as key-value pairs.
  42. */
  43. public Dictionary<string, string> options {
  44. get { return _options; }
  45. }
  46. /**
  47. * The original connection string.
  48. */
  49. public string original_string { get; private set; }
  50. /**
  51. * Private constructor - use parse() to create instances.
  52. */
  53. private ConnectionString() {
  54. _options = new Dictionary<string, string>();
  55. }
  56. /**
  57. * Parses a connection string into its components.
  58. *
  59. * @param connection_string The connection string to parse
  60. * @return A new ConnectionString instance
  61. * @throws SqlError.INVALID_CONNECTION_STRING if the format is invalid
  62. */
  63. public static ConnectionString parse(string connection_string) throws SqlError {
  64. var cs = new ConnectionString();
  65. cs.original_string = connection_string;
  66. string remaining = connection_string.strip();
  67. // Extract scheme
  68. int scheme_end = remaining.index_of(":");
  69. if (scheme_end < 1) {
  70. throw new SqlError.INVALID_CONNECTION_STRING(
  71. "Missing scheme in connection string: %s".printf(connection_string)
  72. );
  73. }
  74. cs.scheme = remaining.substring(0, scheme_end).down();
  75. remaining = remaining.substring(scheme_end);
  76. // Handle SQLite special cases
  77. if (cs.scheme == "sqlite") {
  78. cs.parse_sqlite(remaining);
  79. return cs;
  80. }
  81. // Standard URI parsing for other databases
  82. cs.parse_standard_uri(remaining);
  83. return cs;
  84. }
  85. /**
  86. * Parses SQLite-specific connection string formats.
  87. */
  88. private void parse_sqlite(string uri_part) throws SqlError {
  89. // Case 1: sqlite::memory: (no slashes)
  90. if (uri_part == "::memory:") {
  91. database = ":memory:";
  92. host = "";
  93. return;
  94. }
  95. // Case 2: sqlite://... (with slashes)
  96. if (!uri_part.has_prefix("://")) {
  97. throw new SqlError.INVALID_CONNECTION_STRING(
  98. "Invalid SQLite connection string format: %s".printf(original_string)
  99. );
  100. }
  101. string path_part = uri_part.substring(3); // Remove "://"
  102. // Case 3: sqlite://:memory:
  103. if (path_part == ":memory:") {
  104. database = ":memory:";
  105. host = "";
  106. return;
  107. }
  108. // Split off query string if present
  109. string query_string = "";
  110. int query_pos = path_part.index_of("?");
  111. if (query_pos >= 0) {
  112. query_string = path_part.substring(query_pos + 1);
  113. path_part = path_part.substring(0, query_pos);
  114. }
  115. // For SQLite, the entire path is the database (file path)
  116. // The "host" portion is not used - everything after :// is the path
  117. host = "";
  118. database = path_part;
  119. // Parse query options
  120. parse_query_string(query_string);
  121. }
  122. /**
  123. * Parses standard URI format: //[user[:password]@]host[:port]/database[?options]
  124. */
  125. private void parse_standard_uri(string uri_part) throws SqlError {
  126. if (!uri_part.has_prefix("://")) {
  127. throw new SqlError.INVALID_CONNECTION_STRING(
  128. "Invalid connection string format (expected scheme://): %s".printf(original_string)
  129. );
  130. }
  131. string remaining = uri_part.substring(3); // Remove "://"
  132. // Extract user:password@ if present
  133. int at_pos = remaining.index_of("@");
  134. if (at_pos >= 0) {
  135. string auth_part = remaining.substring(0, at_pos);
  136. remaining = remaining.substring(at_pos + 1);
  137. int colon_pos = auth_part.index_of(":");
  138. if (colon_pos >= 0) {
  139. user = auth_part.substring(0, colon_pos);
  140. password = auth_part.substring(colon_pos + 1);
  141. } else {
  142. user = auth_part;
  143. password = "";
  144. }
  145. } else {
  146. user = "";
  147. password = "";
  148. }
  149. // Split off query string if present
  150. string query_string = "";
  151. int query_pos = remaining.index_of("?");
  152. if (query_pos >= 0) {
  153. query_string = remaining.substring(query_pos + 1);
  154. remaining = remaining.substring(0, query_pos);
  155. }
  156. // Find the / that separates host:port from database
  157. int slash_pos = remaining.index_of("/");
  158. if (slash_pos < 0) {
  159. throw new SqlError.INVALID_CONNECTION_STRING(
  160. "Missing database name in connection string: %s".printf(original_string)
  161. );
  162. }
  163. string host_port = remaining.substring(0, slash_pos);
  164. database = remaining.substring(slash_pos + 1);
  165. // Parse host:port
  166. int port_colon = host_port.last_index_of(":");
  167. if (port_colon >= 0) {
  168. // Check if it's an IPv6 address (contains brackets)
  169. if (host_port.contains("[") && host_port.contains("]")) {
  170. // IPv6 format: [::1]:5432
  171. int bracket_close = host_port.index_of("]");
  172. if (bracket_close >= 0 && bracket_close + 1 < host_port.length && host_port[bracket_close + 1] == ':') {
  173. host = host_port.substring(1, bracket_close - 1);
  174. string port_str = host_port.substring(bracket_close + 2);
  175. port = parse_port(port_str);
  176. } else {
  177. host = host_port.substring(1, bracket_close - 1);
  178. port = 0;
  179. }
  180. } else {
  181. // IPv4 or hostname: host:port
  182. host = host_port.substring(0, port_colon);
  183. string port_str = host_port.substring(port_colon + 1);
  184. port = parse_port(port_str);
  185. }
  186. } else {
  187. host = host_port;
  188. port = 0;
  189. }
  190. // Parse query options
  191. parse_query_string(query_string);
  192. }
  193. /**
  194. * Parses a port string into a uint16.
  195. */
  196. private uint16 parse_port(string port_str) throws SqlError {
  197. if (port_str.length == 0) {
  198. return 0;
  199. }
  200. int64 port_value = 0;
  201. if (!int64.try_parse(port_str, out port_value)) {
  202. throw new SqlError.INVALID_CONNECTION_STRING(
  203. "Invalid port number: %s".printf(port_str)
  204. );
  205. }
  206. if (port_value < 0 || port_value > uint16.MAX) {
  207. throw new SqlError.INVALID_CONNECTION_STRING(
  208. "Port number out of range: %s".printf(port_str)
  209. );
  210. }
  211. return (uint16)port_value;
  212. }
  213. /**
  214. * Parses query string options into the options dictionary.
  215. */
  216. private void parse_query_string(string query_string) {
  217. if (query_string.length == 0) {
  218. return;
  219. }
  220. string[] pairs = query_string.split("&");
  221. foreach (string pair in pairs) {
  222. string trimmed = pair.strip();
  223. if (trimmed.length == 0) {
  224. continue;
  225. }
  226. int eq_pos = trimmed.index_of("=");
  227. if (eq_pos >= 0) {
  228. string key = Uri.unescape_string(trimmed.substring(0, eq_pos)) ?? trimmed.substring(0, eq_pos);
  229. string val = Uri.unescape_string(trimmed.substring(eq_pos + 1)) ?? trimmed.substring(eq_pos + 1);
  230. _options[key] = val;
  231. } else {
  232. _options[Uri.unescape_string(trimmed) ?? trimmed] = "";
  233. }
  234. }
  235. }
  236. /**
  237. * Gets an option value by key.
  238. *
  239. * @param key The option key
  240. * @param default_value The default value if key is not found
  241. * @return The option value or default_value
  242. */
  243. public string? get_option(string key, string? default_value = null) {
  244. if (has_option(key)) {
  245. return _options.get(key);
  246. }
  247. return default_value;
  248. }
  249. /**
  250. * Checks if an option exists.
  251. *
  252. * @param key The option key
  253. * @return true if the option exists
  254. */
  255. public bool has_option(string key) {
  256. return _options.has(key);
  257. }
  258. /**
  259. * Returns a string representation of this connection string.
  260. * Note: Password is masked for security.
  261. */
  262. public string to_safe_string() {
  263. var parts = new StringBuilder();
  264. parts.append(scheme);
  265. parts.append("://");
  266. if (user.length > 0) {
  267. parts.append(user);
  268. if (password.length > 0) {
  269. parts.append(":***");
  270. }
  271. parts.append("@");
  272. }
  273. if (host.length > 0) {
  274. parts.append(host);
  275. if (port > 0) {
  276. parts.append(":");
  277. parts.append(port.to_string());
  278. }
  279. parts.append("/");
  280. }
  281. parts.append(database);
  282. return parts.str;
  283. }
  284. }
  285. }