libpq-uri-v9.patch

application/octet-stream

Filename: libpq-uri-v9.patch
Type: application/octet-stream
Part: 0
Message: Re: Another review of URI for libpq, v7 submission

Patch

Same data as JSON: GET /api/v1/attachments/:id/patch the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes. API reference →
Format: context
Series: patch v9
File+
src/interfaces/libpq/fe-connect.c 658 0
src/interfaces/libpq/Makefile 5 0
src/interfaces/libpq/test/expected.out 37 0
src/interfaces/libpq/test/Makefile 11 0
src/interfaces/libpq/test/README 20 0
src/interfaces/libpq/test/regress.in 36 0
src/interfaces/libpq/test/regress.sh 38 0
*** a/src/interfaces/libpq/Makefile
--- b/src/interfaces/libpq/Makefile
***************
*** 121,126 **** install: all installdirs install-lib
--- 121,129 ----
  	$(INSTALL_DATA) $(srcdir)/pqexpbuffer.h '$(DESTDIR)$(includedir_internal)'
  	$(INSTALL_DATA) $(srcdir)/pg_service.conf.sample '$(DESTDIR)$(datadir)/pg_service.conf.sample'
  
+ check installcheck:
+ 	$(MAKE) -C test $@
+ 
  installdirs: installdirs-lib
  	$(MKDIR_P) '$(DESTDIR)$(includedir)' '$(DESTDIR)$(includedir_internal)'
  
***************
*** 132,137 **** uninstall: uninstall-lib
--- 135,141 ----
  	rm -f '$(DESTDIR)$(datadir)/pg_service.conf.sample'
  
  clean distclean: clean-lib
+ 	$(MAKE) -C test $@
  	rm -f $(OBJS) pthread.h libpq.rc
  # Might be left over from a Win32 client-only build
  	rm -f pg_config_paths.h
***************
*** 142,145 **** clean distclean: clean-lib
--- 146,150 ----
  	rm -f encnames.c wchar.c
  
  maintainer-clean: distclean maintainer-clean-lib
+ 	$(MAKE) -C test $@
  	rm -f libpq-dist.rc
*** a/src/interfaces/libpq/fe-connect.c
--- b/src/interfaces/libpq/fe-connect.c
***************
*** 282,287 **** static const PQEnvironmentOption EnvironmentOptions[] =
--- 282,290 ----
  	}
  };
  
+ /* The connection URI must start with either of the following designators: */
+ static const char uri_designator[] = "postgresql://";
+ static const char short_uri_designator[] = "postgres://";
  
  static bool connectOptions1(PGconn *conn, const char *conninfo);
  static bool connectOptions2(PGconn *conn);
***************
*** 293,298 **** static void fillPGconn(PGconn *conn, PQconninfoOption *connOptions);
--- 296,305 ----
  static void freePGconn(PGconn *conn);
  static void closePGconn(PGconn *conn);
  static PQconninfoOption *conninfo_init(PQExpBuffer errorMessage);
+ static PQconninfoOption *parse_connection_string(const char *conninfo,
+ 			   PQExpBuffer errorMessage, bool use_defaults);
+ static int uri_prefix_length(const char *connstr);
+ static bool recognized_connection_string(const char *connstr);
  static PQconninfoOption *conninfo_parse(const char *conninfo,
  			   PQExpBuffer errorMessage, bool use_defaults);
  static PQconninfoOption *conninfo_array_parse(const char *const * keywords,
***************
*** 300,306 **** static PQconninfoOption *conninfo_array_parse(const char *const * keywords,
  					 bool use_defaults, int expand_dbname);
  static bool conninfo_add_defaults(PQconninfoOption *options,
  					  PQExpBuffer errorMessage);
! static char *conninfo_getval(PQconninfoOption *connOptions,
  				const char *keyword);
  static void defaultNoticeReceiver(void *arg, const PGresult *res);
  static void defaultNoticeProcessor(void *arg, const char *message);
--- 307,332 ----
  					 bool use_defaults, int expand_dbname);
  static bool conninfo_add_defaults(PQconninfoOption *options,
  					  PQExpBuffer errorMessage);
! static PQconninfoOption *conninfo_uri_parse(const char *uri,
! 					  PQExpBuffer errorMessage, bool use_defaults);
! static bool conninfo_uri_parse_options(PQconninfoOption *options,
! 				const char *uri, char *buf,
! 				PQExpBuffer errorMessage);
! static bool conninfo_uri_parse_params(char *params,
! 				PQconninfoOption *connOptions,
! 				PQExpBuffer errorMessage);
! static char *conninfo_uri_decode(const char *str, PQExpBuffer errorMessage);
! static bool get_hexdigit(char digit, int *value);
! static const char *conninfo_getval(PQconninfoOption *connOptions,
! 				const char *keyword);
! static PQconninfoOption *conninfo_storeval(PQconninfoOption *connOptions,
! 				const char *keyword, const char *value,
! 				PQExpBuffer errorMessage, bool ignoreMissing);
! static PQconninfoOption *conninfo_store_uri_encoded_value(
! 				PQconninfoOption *connOptions,
! 				const char *keyword, const char *encoded_value,
! 				PQExpBuffer errorMessage, bool ignoreMissing);
! static PQconninfoOption *conninfo_find(PQconninfoOption *connOptions,
  				const char *keyword);
  static void defaultNoticeReceiver(void *arg, const PGresult *res);
  static void defaultNoticeProcessor(void *arg, const char *message);
***************
*** 583,589 **** PQconnectStart(const char *conninfo)
  static void
  fillPGconn(PGconn *conn, PQconninfoOption *connOptions)
  {
! 	char	   *tmp;
  
  	/*
  	 * Move option values into conn structure
--- 609,615 ----
  static void
  fillPGconn(PGconn *conn, PQconninfoOption *connOptions)
  {
! 	const char	   *tmp;
  
  	/*
  	 * Move option values into conn structure
***************
*** 680,686 **** connectOptions1(PGconn *conn, const char *conninfo)
  	/*
  	 * Parse the conninfo string
  	 */
! 	connOptions = conninfo_parse(conninfo, &conn->errorMessage, true);
  	if (connOptions == NULL)
  	{
  		conn->status = CONNECTION_BAD;
--- 706,712 ----
  	/*
  	 * Parse the conninfo string
  	 */
! 	connOptions = parse_connection_string(conninfo, &conn->errorMessage, true);
  	if (connOptions == NULL)
  	{
  		conn->status = CONNECTION_BAD;
***************
*** 881,889 **** PQsetdbLogin(const char *pghost, const char *pgport, const char *pgoptions,
  		return NULL;
  
  	/*
! 	 * If the dbName parameter contains '=', assume it's a conninfo string.
  	 */
! 	if (dbName && strchr(dbName, '='))
  	{
  		if (!connectOptions1(conn, dbName))
  			return conn;
--- 907,916 ----
  		return NULL;
  
  	/*
! 	 * If the dbName parameter contains a connection string, parse it into conn
! 	 * struct using connectOptions1.
  	 */
! 	if (dbName && recognized_connection_string(dbName))
  	{
  		if (!connectOptions1(conn, dbName))
  			return conn;
***************
*** 3756,3762 **** ldapServiceLookup(const char *purl, PQconninfoOption *options,
  static int
  parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
  {
! 	char	   *service = conninfo_getval(options, "service");
  	char		serviceFile[MAXPGPATH];
  	char	   *env;
  	bool		group_found = false;
--- 3783,3789 ----
  static int
  parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
  {
! 	const char *service = conninfo_getval(options, "service");
  	char		serviceFile[MAXPGPATH];
  	char	   *env;
  	bool		group_found = false;
***************
*** 3991,3997 **** PQconninfoParse(const char *conninfo, char **errmsg)
  	initPQExpBuffer(&errorBuf);
  	if (PQExpBufferDataBroken(errorBuf))
  		return NULL;			/* out of memory already :-( */
! 	connOptions = conninfo_parse(conninfo, &errorBuf, false);
  	if (connOptions == NULL && errmsg)
  		*errmsg = errorBuf.data;
  	else
--- 4018,4024 ----
  	initPQExpBuffer(&errorBuf);
  	if (PQExpBufferDataBroken(errorBuf))
  		return NULL;			/* out of memory already :-( */
! 	connOptions = parse_connection_string(conninfo, &errorBuf, false);
  	if (connOptions == NULL && errmsg)
  		*errmsg = errorBuf.data;
  	else
***************
*** 4019,4024 **** conninfo_init(PQExpBuffer errorMessage)
--- 4046,4097 ----
  }
  
  /*
+  * Connection string parser dispatcher
+  */
+ static PQconninfoOption *
+ parse_connection_string(const char *connstr, PQExpBuffer errorMessage,
+ 						bool use_defaults)
+ {
+ 	/* Parse as URI if connection string matches URI prefix */
+ 	if (uri_prefix_length(connstr) != 0)
+ 		return conninfo_uri_parse(connstr, errorMessage, use_defaults);
+ 
+ 	/* Parse as default otherwise */
+ 	return conninfo_parse(connstr, errorMessage, use_defaults);
+ }
+ 
+ /*
+  * Checks if connection string starts with either of the valid URI prefix
+  * designators.
+  *
+  * Returns the URI prefix length, 0 if the string doesn't contain a URI prefix.
+  */
+ static int
+ uri_prefix_length(const char *connstr)
+ {
+ 	if (strncmp(connstr, uri_designator,
+ 				sizeof(uri_designator) - 1) == 0)
+ 		return sizeof(uri_designator) - 1;
+ 
+ 	if (strncmp(connstr, short_uri_designator,
+ 				sizeof(short_uri_designator) - 1) == 0)
+ 		return sizeof(short_uri_designator) - 1;
+ 
+ 	return 0;
+ }
+ 
+ /*
+  * Recognized connection string either starts with a valid URI prefix or
+  * contains a "=" in it.
+  */
+ static bool
+ recognized_connection_string(const char *connstr)
+ {
+ 	return uri_prefix_length(connstr) != 0 ||
+ 		strchr(connstr, '=') != NULL;
+ }
+ 
+ /*
   * Conninfo parser routine
   *
   * If successful, a malloc'd PQconninfoOption array is returned.
***************
*** 4159,4191 **** conninfo_parse(const char *conninfo, PQExpBuffer errorMessage,
  		}
  
  		/*
! 		 * Now we have the name and the value. Search for the param record.
  		 */
! 		for (option = options; option->keyword != NULL; option++)
  		{
- 			if (strcmp(option->keyword, pname) == 0)
- 				break;
- 		}
- 		if (option->keyword == NULL)
- 		{
- 			printfPQExpBuffer(errorMessage,
- 						 libpq_gettext("invalid connection option \"%s\"\n"),
- 							  pname);
- 			PQconninfoFree(options);
- 			free(buf);
- 			return NULL;
- 		}
- 
- 		/*
- 		 * Store the value
- 		 */
- 		if (option->val)
- 			free(option->val);
- 		option->val = strdup(pval);
- 		if (!option->val)
- 		{
- 			printfPQExpBuffer(errorMessage,
- 							  libpq_gettext("out of memory\n"));
  			PQconninfoFree(options);
  			free(buf);
  			return NULL;
--- 4232,4241 ----
  		}
  
  		/*
! 		 * Now that we have the name and the value, store the record.
  		 */
! 		if (!conninfo_storeval(options, pname, pval, errorMessage, false))
  		{
  			PQconninfoFree(options);
  			free(buf);
  			return NULL;
***************
*** 4244,4260 **** conninfo_array_parse(const char *const * keywords, const char *const * values,
  		const char *pvalue = values[i];
  
  		/* first find "dbname" if any */
! 		if (strcmp(pname, "dbname") == 0)
  		{
! 			/* next look for "=" in the value */
! 			if (pvalue && strchr(pvalue, '='))
  			{
! 				/*
! 				 * Must be a conninfo string, so parse it, but do not use
! 				 * defaults here -- those get picked up later. We only want to
! 				 * override for those parameters actually passed.
! 				 */
! 				str_options = conninfo_parse(pvalue, errorMessage, false);
  				if (str_options == NULL)
  					return NULL;
  			}
--- 4294,4309 ----
  		const char *pvalue = values[i];
  
  		/* first find "dbname" if any */
! 		if (strcmp(pname, "dbname") == 0 && pvalue)
  		{
! 			/*
! 			 * If value is a connection string, parse it, but do not use defaults
! 			 * here -- those get picked up later. We only want to override for
! 			 * those parameters actually passed.
! 			 */
! 			if (recognized_connection_string(pvalue))
  			{
! 				str_options = parse_connection_string(pvalue, errorMessage, false);
  				if (str_options == NULL)
  					return NULL;
  			}
***************
*** 4442,4457 **** conninfo_add_defaults(PQconninfoOption *options, PQExpBuffer errorMessage)
  	return true;
  }
  
  static char *
  conninfo_getval(PQconninfoOption *connOptions,
  				const char *keyword)
  {
  	PQconninfoOption *option;
  
  	for (option = connOptions; option->keyword != NULL; option++)
  	{
  		if (strcmp(option->keyword, keyword) == 0)
! 			return option->val;
  	}
  
  	return NULL;
--- 4491,5073 ----
  	return true;
  }
  
+ /*
+  * Connection URI parser routine
+  *
+  * If successful, a malloc'd PQconninfoOption array is returned.
+  * If not successful, NULL is returned and an error message is
+  * left in errorMessage.
+  *
+  * Defaults are supplied (from a service file, environment variables, etc)
+  * for unspecified options, but only if use_defaults is TRUE.
+  *
+  * This is only a wrapper for conninfo_uri_parse_options, which does all of the
+  * heavy lifting, while this function handles proper (de-)allocation of memory
+  * buffers.
+  */
+ static PQconninfoOption *
+ conninfo_uri_parse(const char *uri, PQExpBuffer errorMessage,
+ 				   bool use_defaults)
+ {
+ 	PQconninfoOption *options;
+ 	char *buf;
+ 
+ 	resetPQExpBuffer(errorMessage);
+ 
+ 	/* Make a working copy of PQconninfoOptions */
+ 	options = conninfo_init(errorMessage);
+ 	if (options == NULL)
+ 		return NULL;
+ 
+ 	/* Make a writable copy of the URI buffer */
+ 	buf = strdup(uri);
+ 	if (buf == NULL)
+ 	{
+ 		printfPQExpBuffer(errorMessage, libpq_gettext("out of memory\n"));
+ 		PQconninfoFree(options);
+ 		return NULL;
+ 	}
+ 
+ 	if (!conninfo_uri_parse_options(options, uri, buf, errorMessage))
+ 	{
+ 		free(buf);
+ 		PQconninfoFree(options);
+ 		return NULL;
+ 	}
+ 	free(buf);
+ 
+ 	/*
+ 	 * Add in defaults if the caller wants that.
+ 	 */
+ 	if (use_defaults)
+ 	{
+ 		if (!conninfo_add_defaults(options, errorMessage))
+ 		{
+ 			PQconninfoFree(options);
+ 			return NULL;
+ 		}
+ 	}
+ 
+ 	return options;
+ }
+ 
+ /*
+  * Connection URI parser: actual parser routine
+  *
+  * If successful, returns true while the options array is filled with parsed
+  * options from the passed URI
+  * If not successful, returns false and fills errorMessage accordingly.
+  *
+  * Parses the connection URI string in 'buf' (while destructively modifying
+  * it,) according to the URI syntax below.  The 'uri' parameter is only used
+  * for error reporting and 'buf' should initially contain a writable copy of
+  * 'uri'.
+  *
+  * The general form for connection URI is the following:
+  *
+  *   postgresql://user:pw@host:port/database
+  *
+  * To specify a IPv6 host address, enclose the address in square brackets:
+  *
+  *   postgresql://[::1]/database
+  *
+  * Connection parameters may follow the base URI using this syntax:
+  *
+  *   postgresql://host/database?param1=value1&param2=value2&...
+  */
+ static bool
+ conninfo_uri_parse_options(PQconninfoOption *options, const char *uri,
+ 						   char *buf, PQExpBuffer errorMessage)
+ {
+ 	int prefix_len;
+ 	char *p;
+ 	char *start = buf;
+ 	char lastc = '\0';
+ 	char *user = NULL;
+ 
+ 	/* Skip the URI prefix */
+ 	prefix_len = uri_prefix_length(uri);
+ 	if (prefix_len == 0)
+ 	{
+ 		/* Should never happen */
+ 		printfPQExpBuffer(errorMessage,
+ 						  libpq_gettext("invalid URI \"%s\" propagated to internal parser routine"),
+ 						  uri);
+ 		return false;
+ 	}
+ 	start += prefix_len;
+ 	p = start;
+ 
+ 	/* Check for local unix socket dir at start of URI */
+ 	if (*p == '/')
+ 	{
+ 		/* Look for possible query parameters */
+ 		while (*p && *p != '?')
+ 			++p;
+ 		lastc = *p;
+ 		*p = '\0';
+ 
+ 		if (!conninfo_store_uri_encoded_value(options, "host", start,
+ 											  errorMessage, false))
+ 			return false;
+ 	}
+ 	else
+ 	{
+ 		/* Not a unix socket dir: parse as host name/address */
+ 		const char *host;
+ 
+ 		/* Look ahead for possible user credentials designator */
+ 		while (*p && *p != '@' && *p != '/')
+ 			++p;
+ 		if (*p == '@')
+ 		{
+ 			/*
+ 			 * Found username/password designator, so URI should be of the form
+ 			 * "scheme://user@netloc" or "scheme://user:password@netloc".
+ 			 */
+ 			user = start;
+ 
+ 			p = user;
+ 			while (*p != ':' && *p != '@')
+ 				++p;
+ 
+ 			/* Save last char and cut off at end of user name */
+ 			lastc = *p;
+ 			*p = '\0';
+ 
+ 			if (!*user)
+ 			{
+ 				printfPQExpBuffer(errorMessage,
+ 								  libpq_gettext("missing username specifier in URI: %s\n"),
+ 								  uri);
+ 				return false;
+ 			}
+ 			if (!conninfo_store_uri_encoded_value(options, "user", user,
+ 												  errorMessage, false))
+ 				return false;
+ 
+ 			if (lastc == ':')
+ 			{
+ 				const char *password = p + 1;
+ 
+ 				while (*p != '@')
+ 					++p;
+ 				*p = '\0';
+ 
+ 				if (!*password)
+ 				{
+ 					printfPQExpBuffer(errorMessage,
+ 									  libpq_gettext("missing password specifier in URI: %s\n"),
+ 									  uri);
+ 					return false;
+ 				}
+ 				if (!conninfo_store_uri_encoded_value(options, "password",
+ 													  password,
+ 													  errorMessage, false))
+ 					return false;
+ 			}
+ 
+ 			/* Advance past end of parsed user name or password token */
+ 			++p;
+ 		}
+ 		else
+ 		{
+ 			/*
+ 			 * No username/password designator found.
+ 			 *
+ 			 * Reset to start of URI and parse as "scheme://netloc/..."
+ 			 * instead.
+ 			 */
+ 			p = start;
+ 		}
+ 
+ 		/*
+ 		 * "p" has been incremented past optional URI credential information at
+ 		 * this point and now points at the "netloc" part of the URI.
+ 		 *
+ 		 * Look for IPv6 address 
+ 		 */
+ 		if (*p == '[')
+ 		{
+ 			host = ++p;
+ 			while (*p && *p != ']')
+ 				++p;
+ 			if (!*p)
+ 			{
+ 				printfPQExpBuffer(errorMessage,
+ 								  libpq_gettext("end of string reached when looking for matching ']' in IPv6 host address in URI: %s\n"),
+ 								  uri);
+ 				return false;
+ 			}
+ 			if (p == host)
+ 			{
+ 				printfPQExpBuffer(errorMessage,
+ 								  libpq_gettext("IPv6 host address may not be empty in URI: %s\n"),
+ 								  uri);
+ 				return false;
+ 			}
+ 
+ 			/* Cut off the bracket and advance */
+ 			*(p++) = '\0';
+ 
+ 			/* 
+ 			 * The address must be followed by a port specifier or a slash or a
+ 			 * query.
+ 			 */
+ 			if (*p && *p != ':' && *p != '/' && *p != '?')
+ 			{
+ 				printfPQExpBuffer(errorMessage,
+ 								  libpq_gettext("unexpected '%c' at position %td in URI (expecting ':' or '/'): %s\n"),
+ 								  *p, p - buf + 1, uri);
+ 				return false;
+ 			}
+ 		}
+ 		else
+ 		{
+ 			/* not an IPv6 address: DNS-named or IPv4 netloc */
+ 			host = p;
+ 
+ 			/*
+ 			 * Look for port specifier (colon) or end of host specifier
+ 			 * (slash), or query (question mark).
+ 			 */
+ 			while (*p && *p != ':' && *p != '/' && *p != '?')
+ 				++p;
+ 		}
+ 
+ 		/* Save the hostname terminator before we null it */
+ 		lastc = *p;
+ 		*p = '\0';
+ 
+ 		if (user && !*host)
+ 		{
+ 			printfPQExpBuffer(errorMessage,
+ 							  libpq_gettext("must specify host when using username specifier in URI: %s\n"),
+ 							  uri);
+ 			return false;
+ 		}
+ 		if (!conninfo_store_uri_encoded_value(options, "host", host,
+ 											  errorMessage, false))
+ 			return false;
+ 
+ 		if (lastc == ':')
+ 		{
+ 			const char *port = ++p; /* advance past host terminator */
+ 
+ 			while (*p && *p != '/' && *p != '?')
+ 				++p;
+ 
+ 			lastc = *p;
+ 			*p = '\0';
+ 
+ 			if (!*port)
+ 			{
+ 				printfPQExpBuffer(errorMessage,
+ 								  libpq_gettext("missing port specifier in URI: %s\n"),
+ 								  uri);
+ 				return false;
+ 			}
+ 			if (!conninfo_store_uri_encoded_value(options, "port", port,
+ 												  errorMessage, false))
+ 				return false;
+ 		}
+ 	}
+ 
+ 	if (lastc && lastc != '?')
+ 	{
+ 		const char *dbname = ++p; /* advance past host terminator */
+ 
+ 		/* Look for query parameters */
+ 		while (*p && *p != '?')
+ 			++p;
+ 
+ 		lastc = *p;
+ 		*p = '\0';
+ 
+ 		/*
+ 		 * Avoid setting dbname to an empty string, as it forces the default
+ 		 * value (username) and ignores $PGDATABASE, as opposed to not setting
+ 		 * it at all.
+ 		 */
+ 		if (*dbname &&
+ 			!conninfo_store_uri_encoded_value(options, "dbname", dbname,
+ 											  errorMessage, false))
+ 			return false;
+ 	}
+ 
+ 	if (lastc)
+ 	{
+ 		++p; /* advance past terminator */
+ 
+ 		if (!conninfo_uri_parse_params(p, options, errorMessage))
+ 			return false;
+ 	}
+ 
+ 	return true;
+ }
+ 
+ /*
+  * Connection URI parameters parser routine
+  *
+  * If successful, returns true while connOptions is filled with parsed
+  * parameters.
+  * If not successful, returns false and fills errorMessage accordingly.
+  *
+  * Destructively modifies 'params' buffer.
+  */
+ static bool
+ conninfo_uri_parse_params(char *params,
+ 						  PQconninfoOption *connOptions,
+ 						  PQExpBuffer errorMessage)
+ {
+ 	char *token;
+ 	char *savep;
+ 
+ 	while ((token = strtok_r(params, "&", &savep)))
+ 	{
+ 		const char *keyword;
+ 		const char *value;
+ 		char *p = strchr(token, '=');
+ 
+ 		if (p == NULL)
+ 		{
+ 			printfPQExpBuffer(errorMessage,
+ 							  libpq_gettext("missing key/value separator '=' in URI query parameter: %s\n"),
+ 							  token);
+ 			return false;
+ 		}
+ 
+ 		/* Cut off keyword and advance to value */
+ 		*(p++) = '\0';
+ 
+ 		keyword = token;
+ 		value = p;
+ 
+ 		/* Special keyword handling for improved JDBC compatibility */
+ 		if (strcmp(keyword, "ssl") == 0 &&
+ 			strcmp(value, "true") == 0)
+ 		{
+ 			keyword = "sslmode";
+ 			value = "require";
+ 		}
+ 
+ 		/*
+ 		 * Store the value if corresponding option was found -- ignore
+ 		 * otherwise.
+ 		 *
+ 		 * In theory, the keyword might be also percent-encoded, but hardly
+ 		 * that's ever needed by anyone.
+ 		 */
+ 		if (!conninfo_store_uri_encoded_value(connOptions, keyword, value,
+ 											  errorMessage, true))
+ 		{
+ 			fprintf(stderr,
+ 					libpq_gettext("WARNING: ignoring unrecognized URI query parameter: %s\n"),
+ 					keyword);
+ 		}
+ 
+ 		params = NULL; /* proceed to the next token with strtok_r */
+ 	}
+ 
+ 	return true;
+ }
+ 
+ /*
+  * Connection URI decoder routine
+  *
+  * If successful, returns the malloc'd decoded string.
+  * If not successful, returns NULL and fills errorMessage accordingly.
+  *
+  * The string is decoded by replacing any percent-encoded tokens with
+  * corresponding characters, while preserving any non-encoded characters.  A
+  * percent-encoded token is a character triplet: a percent sign, followed by a
+  * pair of hexadecimal digits (0-9A-F), where lower- and upper-case letters are
+  * treated identically.
+  */
  static char *
+ conninfo_uri_decode(const char *str, PQExpBuffer errorMessage)
+ {
+ 	char *buf = malloc(strlen(str) + 1);
+ 	char *p = buf;
+ 	const char *q = str;
+ 
+ 	if (buf == NULL)
+ 	{
+ 		printfPQExpBuffer(errorMessage, libpq_gettext("out of memory\n"));
+ 		return NULL;
+ 	}
+ 
+ 	for (;;)
+ 	{
+ 		if (*q != '%')
+ 		{
+ 			/* copy and check for NUL terminator */
+ 			if (!(*(p++) = *(q++)))
+ 				break;
+ 		}
+ 		else
+ 		{
+ 			int hi;
+ 			int lo;
+ 
+ 			++q; /* skip the percent sign itself */
+ 
+ 			/*
+ 			 * Possible EOL will be caught by the first call to get_hexdigit(),
+ 			 * so we never dereference an invalid q pointer.
+ 			 */
+ 			if (!(get_hexdigit(*q++, &hi) && get_hexdigit(*q++, &lo)))
+ 			{
+ 				printfPQExpBuffer(errorMessage,
+ 								  libpq_gettext("invalid percent-encoded token (a pair of hexadecimal digits expected after every '%%' symbol, use '%%25' to encode percent symbol itself): %s\n"),
+ 								  str);
+ 				free(buf);
+ 				return NULL;
+ 			}
+ 
+ 			*(p++) = (hi << 4) | lo;
+ 		}
+ 	}
+ 
+ 	return buf;
+ }
+ 
+ /*
+  * Convert hexadecimal digit character to it's integer value.
+  *
+  * If successful, returns true and value is filled with digit's base 16 value.
+  * If not successful, returns false.
+  *
+  * Lower- and upper-case letters in the range A-F are treated identically.
+  */
+ static bool
+ get_hexdigit(char digit, int *value)
+ {
+ 	if ('0' <= digit && digit <= '9')
+ 	{
+ 		*value = digit - '0';
+ 	}
+ 	else if ('A' <= digit && digit <= 'F')
+ 	{
+ 		*value = digit - 'A' + 10;
+ 	}
+ 	else if ('a' <= digit && digit <= 'f')
+ 	{
+ 		*value = digit - 'a' + 10;
+ 	}
+ 	else
+ 	{
+ 		return false;
+ 	}
+ 
+ 	return true;
+ }
+ 
+ /*
+  * Find an option value corresponding to the keyword in the connOptions array.
+  *
+  * If successful, returns a pointer to the corresponding option's value.
+  * If not successful, returns NULL.
+  */
+ static const char *
  conninfo_getval(PQconninfoOption *connOptions,
  				const char *keyword)
  {
+ 	PQconninfoOption *option = conninfo_find(connOptions, keyword);
+ 
+ 	return option ? option->val : NULL;
+ }
+ 
+ /*
+  * Store a (new) value for an option corresponding to the keyword in
+  * connOptions array.
+  *
+  * If successful, returns a pointer to the corresponding PQconninfoOption,
+  * which value is replaced with a strdup'd copy of the passed value string.
+  * The existing value for the option is free'd before replacing, if any.
+  *
+  * If not successful, returns NULL and fills errorMessage accordingly.
+  */
+ static PQconninfoOption *
+ conninfo_storeval(PQconninfoOption *connOptions,
+ 				  const char *keyword, const char *value,
+ 				  PQExpBuffer errorMessage, bool ignoreMissing)
+ {
+ 	PQconninfoOption *option = conninfo_find(connOptions, keyword);
+ 	char *value_copy;
+ 
+ 	if (option == NULL)
+ 	{
+ 		if (!ignoreMissing)
+ 			printfPQExpBuffer(errorMessage,
+ 							  libpq_gettext("invalid connection option \"%s\"\n"),
+ 							  keyword);
+ 		return NULL;
+ 	}
+ 
+ 	value_copy = strdup(value);
+ 	if (value_copy == NULL)
+ 	{
+ 		printfPQExpBuffer(errorMessage, libpq_gettext("out of memory\n"));
+ 		return NULL;
+ 	}
+ 
+ 	if (option->val)
+ 		free(option->val);
+ 	option->val = value_copy;
+ 
+ 	return option;
+ }
+ 
+ /*
+  * Store a (new) possibly URI-encoded value for an option corresponding to the
+  * keyword in connOptions array.
+  *
+  * If successful, returns a pointer to the corresponding PQconninfoOption,
+  * which value is replaced with a URI-decoded copy of the passed value string.
+  * The existing value for the option is free'd before replacing, if any.
+  *
+  * If not successful, returns NULL and fills errorMessage accordingly.  If the
+  * corresponding option was not found, but ignoreMissing is true: doesn't fill
+  * errorMessage.
+  *
+  * See also conninfo_storeval.
+  */
+ static PQconninfoOption *
+ conninfo_store_uri_encoded_value(PQconninfoOption *connOptions,
+ 								 const char *keyword, const char *encoded_value,
+ 								 PQExpBuffer errorMessage, bool ignoreMissing)
+ {
+ 	PQconninfoOption *option;
+ 	char *decoded_value = conninfo_uri_decode(encoded_value, errorMessage);
+ 
+ 	if (decoded_value == NULL)
+ 		return NULL;
+ 
+ 	option = conninfo_storeval(connOptions, keyword, decoded_value,
+ 							   errorMessage, ignoreMissing);
+ 	free(decoded_value);
+ 
+ 	return option;
+ }
+ 
+ /*
+  * Find a PQconninfoOption option corresponding to the keyword in the
+  * connOptions array.
+  *
+  * If successful, returns a pointer to the corresponding PQconninfoOption
+  * structure.
+  * If not successful, returns NULL.
+  */
+ static PQconninfoOption *
+ conninfo_find(PQconninfoOption *connOptions, const char *keyword)
+ {
  	PQconninfoOption *option;
  
  	for (option = connOptions; option->keyword != NULL; option++)
  	{
  		if (strcmp(option->keyword, keyword) == 0)
! 			return option;
  	}
  
  	return NULL;
*** /dev/null
--- b/src/interfaces/libpq/test/Makefile
***************
*** 0 ****
--- 1,11 ----
+ subdir = src/interfaces/libpq/test
+ top_builddir = ../../../..
+ include $(top_builddir)/src/Makefile.global
+ 
+ all: installcheck
+ 
+ installcheck:
+ 	BINDIR='$(bindir)' SUBDIR='$(subdir)' $(SHELL) ./regress.sh
+ 
+ clean distclean maintainer-clean:
+ 	rm -f regress.out regress.diff
*** /dev/null
--- b/src/interfaces/libpq/test/README
***************
*** 0 ****
--- 1,20 ----
+ This is a testsuite for testing libpq URI connection string syntax.
+ 
+ To run the suite, use 'make installcheck' command.  It works by
+ running regress.sh from this directory with appropriate environment
+ set up.
+ 
+ The test itself works by running the psql command installed by 'make
+ install' with different -d option arguments and collecting the output,
+ then comparing that to the expected 'correct' output.
+ 
+ The test script assumes that the server is running on localhost and
+ accepting local and IPv4,6 connections on port $PGPORT.  Attempts to
+ connect are made from user $PGUSER (which defaults to $USER,) to the
+ database specified in $PGDATABASE (which defaults to "regression".)
+ The database is created by the script prior to running the actual
+ tests.
+ 
+ The connection URI lines to test are read from the file 'regress.in',
+ and are eval-d before passing them to psql (like `eval echo $line`,)
+ thus ${variable} substitutions are carried out.
*** /dev/null
--- b/src/interfaces/libpq/test/expected.out
***************
*** 0 ****
--- 1,37 ----
+ postgresql://${PGUSER}@localhost:${PGPORT}/${PGDATABASE}
+ postgresql://${PGUSER}@localhost/${PGDATABASE}
+ postgresql://localhost:${PGPORT}/${PGDATABASE}
+ postgresql://localhost/${PGDATABASE}
+ postgresql://${PGUSER}@localhost:${PGPORT}/
+ postgresql://${PGUSER}@localhost/
+ postgresql://localhost:${PGPORT}/
+ postgresql://localhost:${PGPORT}
+ postgresql://localhost/${PGDATABASE}
+ postgresql://localhost/
+ postgresql://localhost
+ postgresql://
+ postgresql://%6Cocalhost/
+ postgresql://localhost/${PGDATABASE}?user=${PGUSER}
+ postgresql://localhost/${PGDATABASE}?user=${PGUSER}&port=${PGPORT}
+ postgresql://localhost/${PGDATABASE}?user=${PGUSER}&port=${PGPORT}
+ postgresql://localhost:${PGPORT}?user=${PGUSER}
+ postgresql://localhost?user=${PGUSER}
+ postgresql://localhost?
+ postgresql://[::1]:${PGPORT}/${PGDATABASE}
+ postgresql://[::1]/${PGDATABASE}
+ postgresql://[::1]/
+ postgresql://[::1]
+ postgres://
+ postgres:///tmp
+ WARNING: ignoring unrecognized URI query parameter: uzer
+ postgresql://localhost?uzer=
+ psql: FATAL:  database "postgre://" does not exist
+ psql: end of string reached when looking for matching ']' in IPv6 host address in URI: postgres://[::1
+ psql: IPv6 host address may not be empty in URI: postgres://[]
+ psql: unexpected 'z' at position 17 in URI (expecting ':' or '/'): postgres://[::1]z
+ psql: missing key/value separator '=' in URI query parameter: zzz
+ psql: invalid percent-encoded token (a pair of hexadecimal digits expected after every '%' symbol, use '%25' to encode percent symbol itself): %zz
+ psql: invalid percent-encoded token (a pair of hexadecimal digits expected after every '%' symbol, use '%25' to encode percent symbol itself): %1
+ psql: invalid percent-encoded token (a pair of hexadecimal digits expected after every '%' symbol, use '%25' to encode percent symbol itself): %
+ psql: missing username specifier in URI: postgres://@localhost
+ psql: missing port specifier in URI: postgres://localhost:/
*** /dev/null
--- b/src/interfaces/libpq/test/regress.in
***************
*** 0 ****
--- 1,36 ----
+ postgresql://${PGUSER}@localhost:${PGPORT}/${PGDATABASE}
+ postgresql://${PGUSER}@localhost/${PGDATABASE}
+ postgresql://localhost:${PGPORT}/${PGDATABASE}
+ postgresql://localhost/${PGDATABASE}
+ postgresql://${PGUSER}@localhost:${PGPORT}/
+ postgresql://${PGUSER}@localhost/
+ postgresql://localhost:${PGPORT}/
+ postgresql://localhost:${PGPORT}
+ postgresql://localhost/${PGDATABASE}
+ postgresql://localhost/
+ postgresql://localhost
+ postgresql://
+ postgresql://%6Cocalhost/
+ postgresql://localhost/${PGDATABASE}?user=${PGUSER}
+ postgresql://localhost/${PGDATABASE}?user=${PGUSER}&port=${PGPORT}
+ postgresql://localhost/${PGDATABASE}?user=${PGUSER}&port=${PGPORT}
+ postgresql://localhost:${PGPORT}?user=${PGUSER}
+ postgresql://localhost?user=${PGUSER}
+ postgresql://localhost?
+ postgresql://[::1]:${PGPORT}/${PGDATABASE}
+ postgresql://[::1]/${PGDATABASE}
+ postgresql://[::1]/
+ postgresql://[::1]
+ postgres://
+ postgres:///tmp
+ postgresql://localhost?uzer=
+ postgre://
+ postgres://[::1
+ postgres://[]
+ postgres://[::1]z
+ postgresql://localhost?zzz
+ postgresql://%zz
+ postgresql://%1
+ postgresql://%
+ postgres://@localhost
+ postgres://localhost:/
*** /dev/null
--- b/src/interfaces/libpq/test/regress.sh
***************
*** 0 ****
--- 1,38 ----
+ #!/bin/sh
+ if [ -z "$PGUSER" ]; then
+ PGUSER=$USER
+ fi
+ if [ -z "$PGPORT" ]; then
+ PGPORT=5432
+ fi
+ if [ -z "$PGDATABASE" ]; then
+ PGDATABASE=regression
+ fi
+ export PGUSER PGPORT PGDATABASE
+ 
+ "${BINDIR}/createdb" "${PGDATABASE}"
+ 
+ echo "Running libpq URI support test..."
+ 
+ while read line
+ do
+     # Frist, expand variables in the test URI line.
+     uri=$(eval echo "$line")
+ 
+     # But SELECT the original line, so test result doesn't depend on
+     # the substituted values.
+     "${BINDIR}/psql" -d "$uri" -At -c "SELECT '$line'"
+ done <regress.in >regress.out 2>&1
+ 
+ if diff -c expected.out regress.out >regress.diff; then
+     echo "========================================"
+     echo "All tests passed"
+     exit 0
+ else
+     echo "========================================"
+     echo "FAILED: the test result differs from the expected output"
+     echo
+     echo "Review the difference in ${SUBDIR}/regress.diff"
+     echo "========================================"
+     exit 1
+ fi