diff mbox series

[V2] mount.nfs4: Add support for nfs://-URLs

Message ID 20241210122846.821199-1-roland.mainz@nrubsig.org (mailing list archive)
State Handled Elsewhere
Headers show
Series [V2] mount.nfs4: Add support for nfs://-URLs | expand

Commit Message

Roland Mainz Dec. 10, 2024, 12:28 p.m. UTC
Add support for RFC 2224-style nfs://-URLs as alternative to the
traditional hostname:/path+-o port=<tcp-port> notation,
providing standardised, extensible, single-string, crossplatform,
portable, Character-Encoding independent (e.g. mount point with
Japanese, Chinese, French etc. characters) and ASCII-compatible
descriptions of NFSv4 server resources (exports).

Reviewed-by: Martin Wege <martin.l.wege@gmail.com>
Signed-off-by: Marvin Wenzel <marvin.wenzel@rovema.de>
Signed-off-by: Cedric Blancher <cedric.blancher@gmail.com>
---
 utils/mount/Makefile.am  |   3 +-
 utils/mount/mount.c      |   3 +
 utils/mount/nfs4mount.c  |  69 +++++++-
 utils/mount/nfsmount.c   |  93 ++++++++--
 utils/mount/parse_dev.c  |  67 ++++++--
 utils/mount/stropts.c    |  96 ++++++++++-
 utils/mount/urlparser1.c | 358 +++++++++++++++++++++++++++++++++++++++
 utils/mount/urlparser1.h |  60 +++++++
 utils/mount/utils.c      | 155 +++++++++++++++++
 utils/mount/utils.h      |  23 +++
 10 files changed, 890 insertions(+), 37 deletions(-)
 create mode 100644 utils/mount/urlparser1.c
 create mode 100644 utils/mount/urlparser1.h

Comments

Steve Dickson Dec. 10, 2024, 12:37 p.m. UTC | #1
On 12/10/24 7:28 AM, Roland Mainz wrote:
> Add support for RFC 2224-style nfs://-URLs as alternative to the
> traditional hostname:/path+-o port=<tcp-port> notation,
> providing standardised, extensible, single-string, crossplatform,
> portable, Character-Encoding independent (e.g. mount point with
> Japanese, Chinese, French etc. characters) and ASCII-compatible
> descriptions of NFSv4 server resources (exports).
> 
> Reviewed-by: Martin Wege <martin.l.wege@gmail.com>
> Signed-off-by: Marvin Wenzel <marvin.wenzel@rovema.de>
> Signed-off-by: Cedric Blancher <cedric.blancher@gmail.com>
> ---
>   utils/mount/Makefile.am  |   3 +-
>   utils/mount/mount.c      |   3 +
>   utils/mount/nfs4mount.c  |  69 +++++++-
>   utils/mount/nfsmount.c   |  93 ++++++++--
>   utils/mount/parse_dev.c  |  67 ++++++--
>   utils/mount/stropts.c    |  96 ++++++++++-
>   utils/mount/urlparser1.c | 358 +++++++++++++++++++++++++++++++++++++++
>   utils/mount/urlparser1.h |  60 +++++++
>   utils/mount/utils.c      | 155 +++++++++++++++++
>   utils/mount/utils.h      |  23 +++
>   10 files changed, 890 insertions(+), 37 deletions(-)
>   create mode 100644 utils/mount/urlparser1.c
>   create mode 100644 utils/mount/urlparser1.h
I would like to see a man page update explaining how
to use the URLs.

steved.

> 
> diff --git a/utils/mount/Makefile.am b/utils/mount/Makefile.am
> index 83a8ee1c..0e4cab3e 100644
> --- a/utils/mount/Makefile.am
> +++ b/utils/mount/Makefile.am
> @@ -13,7 +13,8 @@ sbin_PROGRAMS	= mount.nfs
>   EXTRA_DIST = nfsmount.conf $(man8_MANS) $(man5_MANS)
>   mount_common = error.c network.c token.c \
>   		    parse_opt.c parse_dev.c \
> -		    nfsmount.c nfs4mount.c stropts.c\
> +		    nfsmount.c nfs4mount.c \
> +		    urlparser1.c urlparser1.h stropts.c \
>   		    mount_constants.h error.h network.h token.h \
>   		    parse_opt.h parse_dev.h \
>   		    nfs4_mount.h stropts.h version.h \
> diff --git a/utils/mount/mount.c b/utils/mount/mount.c
> index b98f9e00..2ce6209d 100644
> --- a/utils/mount/mount.c
> +++ b/utils/mount/mount.c
> @@ -29,6 +29,7 @@
>   #include <string.h>
>   #include <errno.h>
>   #include <fcntl.h>
> +#include <locale.h>
>   #include <sys/mount.h>
>   #include <getopt.h>
>   #include <mntent.h>
> @@ -386,6 +387,8 @@ int main(int argc, char *argv[])
>   	char *extra_opts = NULL, *mount_opts = NULL;
>   	uid_t uid = getuid();
>   
> +	(void)setlocale(LC_ALL, "");
> +
>   	progname = basename(argv[0]);
>   
>   	nfs_mount_data_version = discover_nfs_mount_data_version(&string);
> diff --git a/utils/mount/nfs4mount.c b/utils/mount/nfs4mount.c
> index 0fe142a7..8e4fbf30 100644
> --- a/utils/mount/nfs4mount.c
> +++ b/utils/mount/nfs4mount.c
> @@ -50,8 +50,10 @@
>   #include "mount_constants.h"
>   #include "nfs4_mount.h"
>   #include "nfs_mount.h"
> +#include "urlparser1.h"
>   #include "error.h"
>   #include "network.h"
> +#include "utils.h"
>   
>   #if defined(VAR_LOCK_DIR)
>   #define DEFAULT_DIR VAR_LOCK_DIR
> @@ -182,7 +184,7 @@ int nfs4mount(const char *spec, const char *node, int flags,
>   	int num_flavour = 0;
>   	int ip_addr_in_opts = 0;
>   
> -	char *hostname, *dirname, *old_opts;
> +	char *hostname, *dirname, *mb_dirname = NULL, *old_opts;
>   	char new_opts[1024];
>   	char *opt, *opteq;
>   	char *s;
> @@ -192,15 +194,66 @@ int nfs4mount(const char *spec, const char *node, int flags,
>   	int retry;
>   	int retval = EX_FAIL;
>   	time_t timeout, t;
> +	int nfs_port = NFS_PORT;
> +	parsed_nfs_url pnu;
> +
> +	(void)memset(&pnu, 0, sizeof(parsed_nfs_url));
>   
>   	if (strlen(spec) >= sizeof(hostdir)) {
>   		nfs_error(_("%s: excessively long host:dir argument\n"),
>   				progname);
>   		goto fail;
>   	}
> -	strcpy(hostdir, spec);
> -	if (parse_devname(hostdir, &hostname, &dirname))
> -		goto fail;
> +
> +	/*
> +	 * Support nfs://-URLS per RFC 2224 ("NFS URL
> +	 * SCHEME", see https://www.rfc-editor.org/rfc/rfc2224.html),
> +	 * including custom port (nfs://hostname@port/path/...)
> +	 * and URL parameter (e.g. nfs://.../?param1=val1&param2=val2
> +	 * support
> +	 */
> +	if (is_spec_nfs_url(spec)) {
> +		if (!mount_parse_nfs_url(spec, &pnu)) {
> +			goto fail;
> +		}
> +
> +		/*
> +		 * |pnu.uctx->path| is in UTF-8, but we need the data
> +		 * in the current local locale's encoding, as mount(2)
> +		 * does not have something like a |MS_UTF8_SPEC| flag
> +		 * to indicate that the input path is in UTF-8,
> +		 * independently of the current locale
> +		 */
> +		hostname = pnu.uctx->hostport.hostname;
> +		dirname = mb_dirname = utf8str2mbstr(pnu.uctx->path);
> +
> +		(void)snprintf(hostdir, sizeof(hostdir), "%s:/%s",
> +			hostname, dirname);
> +		spec = hostdir;
> +
> +		if (pnu.uctx->hostport.port != -1) {
> +			nfs_port = pnu.uctx->hostport.port;
> +		}
> +
> +		/*
> +		 * Values added here based on URL parameters
> +		 * should be added the front of the list of options,
> +		 * so users can override the nfs://-URL given default.
> +		 *
> +		 * FIXME: We do not do that here for |MS_RDONLY|!
> +		 */
> +		if (pnu.mount_params.read_only != TRIS_BOOL_NOT_SET) {
> +			if (pnu.mount_params.read_only)
> +				flags |= MS_RDONLY;
> +			else
> +				flags &= ~MS_RDONLY;
> +		}
> +        } else {
> +		(void)strcpy(hostdir, spec);
> +
> +		if (parse_devname(hostdir, &hostname, &dirname))
> +			goto fail;
> +	}
>   
>   	if (fill_ipv4_sockaddr(hostname, &server_addr))
>   		goto fail;
> @@ -247,7 +300,7 @@ int nfs4mount(const char *spec, const char *node, int flags,
>   	/*
>   	 * NFSv4 specifies that the default port should be 2049
>   	 */
> -	server_addr.sin_port = htons(NFS_PORT);
> +	server_addr.sin_port = htons(nfs_port);
>   
>   	/* parse options */
>   
> @@ -474,8 +527,14 @@ int nfs4mount(const char *spec, const char *node, int flags,
>   		}
>   	}
>   
> +	mount_free_parse_nfs_url(&pnu);
> +	free(mb_dirname);
> +
>   	return EX_SUCCESS;
>   
>   fail:
> +	mount_free_parse_nfs_url(&pnu);
> +	free(mb_dirname);
> +
>   	return retval;
>   }
> diff --git a/utils/mount/nfsmount.c b/utils/mount/nfsmount.c
> index a1c92fe8..e61d718a 100644
> --- a/utils/mount/nfsmount.c
> +++ b/utils/mount/nfsmount.c
> @@ -63,11 +63,13 @@
>   #include "xcommon.h"
>   #include "mount.h"
>   #include "nfs_mount.h"
> +#include "urlparser1.h"
>   #include "mount_constants.h"
>   #include "nls.h"
>   #include "error.h"
>   #include "network.h"
>   #include "version.h"
> +#include "utils.h"
>   
>   #ifdef HAVE_RPCSVC_NFS_PROT_H
>   #include <rpcsvc/nfs_prot.h>
> @@ -493,7 +495,7 @@ nfsmount(const char *spec, const char *node, int flags,
>   	 char **extra_opts, int fake, int running_bg)
>   {
>   	char hostdir[1024];
> -	char *hostname, *dirname, *old_opts, *mounthost = NULL;
> +	char *hostname, *dirname, *mb_dirname = NULL, *old_opts, *mounthost = NULL;
>   	char new_opts[1024], cbuf[1024];
>   	static struct nfs_mount_data data;
>   	int val;
> @@ -521,29 +523,79 @@ nfsmount(const char *spec, const char *node, int flags,
>   	time_t t;
>   	time_t prevt;
>   	time_t timeout;
> +	int nfsurl_port = -1;
> +	parsed_nfs_url pnu;
> +
> +	(void)memset(&pnu, 0, sizeof(parsed_nfs_url));
>   
>   	if (strlen(spec) >= sizeof(hostdir)) {
>   		nfs_error(_("%s: excessively long host:dir argument"),
>   				progname);
>   		goto fail;
>   	}
> -	strcpy(hostdir, spec);
> -	if ((s = strchr(hostdir, ':'))) {
> -		hostname = hostdir;
> -		dirname = s + 1;
> -		*s = '\0';
> -		/* Ignore all but first hostname in replicated mounts
> -		   until they can be fully supported. (mack@sgi.com) */
> -		if ((s = strchr(hostdir, ','))) {
> +
> +	/*
> +	 * Support nfs://-URLS per RFC 2224 ("NFS URL
> +	 * SCHEME", see https://www.rfc-editor.org/rfc/rfc2224.html),
> +	 * including custom port (nfs://hostname@port/path/...)
> +	 * and URL parameter (e.g. nfs://.../?param1=val1&param2=val2
> +	 * support
> +	 */
> +	if (is_spec_nfs_url(spec)) {
> +		if (!mount_parse_nfs_url(spec, &pnu)) {
> +			goto fail;
> +		}
> +
> +		/*
> +		 * |pnu.uctx->path| is in UTF-8, but we need the data
> +		 * in the current local locale's encoding, as mount(2)
> +		 * does not have something like a |MS_UTF8_SPEC| flag
> +		 * to indicate that the input path is in UTF-8,
> +		 * independently of the current locale
> +		 */
> +		hostname = pnu.uctx->hostport.hostname;
> +		dirname = mb_dirname = utf8str2mbstr(pnu.uctx->path);
> +
> +		(void)snprintf(hostdir, sizeof(hostdir), "%s:/%s",
> +			hostname, dirname);
> +		spec = hostdir;
> +
> +		if (pnu.uctx->hostport.port != -1) {
> +			nfsurl_port = pnu.uctx->hostport.port;
> +		}
> +
> +		/*
> +		 * Values added here based on URL parameters
> +		 * should be added the front of the list of options,
> +		 * so users can override the nfs://-URL given default.
> +		 *
> +		 * FIXME: We do not do that here for |MS_RDONLY|!
> +		 */
> +		if (pnu.mount_params.read_only != TRIS_BOOL_NOT_SET) {
> +			if (pnu.mount_params.read_only)
> +				flags |= MS_RDONLY;
> +			else
> +				flags &= ~MS_RDONLY;
> +		}
> +        } else {
> +		(void)strcpy(hostdir, spec);
> +		if ((s = strchr(hostdir, ':'))) {
> +			hostname = hostdir;
> +			dirname = s + 1;
>   			*s = '\0';
> -			nfs_error(_("%s: warning: "
> -				  "multiple hostnames not supported"),
> +			/* Ignore all but first hostname in replicated mounts
> +			   until they can be fully supported. (mack@sgi.com) */
> +			if ((s = strchr(hostdir, ','))) {
> +				*s = '\0';
> +				nfs_error(_("%s: warning: "
> +					"multiple hostnames not supported"),
>   					progname);
> -		}
> -	} else {
> -		nfs_error(_("%s: directory to mount not in host:dir format"),
> +			}
> +		} else {
> +			nfs_error(_("%s: directory to mount not in host:dir format"),
>   				progname);
> -		goto fail;
> +			goto fail;
> +		}
>   	}
>   
>   	if (!nfs_gethostbyname(hostname, nfs_saddr))
> @@ -579,6 +631,14 @@ nfsmount(const char *spec, const char *node, int flags,
>   	memset(nfs_pmap, 0, sizeof(*nfs_pmap));
>   	nfs_pmap->pm_prog = NFS_PROGRAM;
>   
> +	if (nfsurl_port != -1) {
> +		/*
> +		 * Set custom TCP port defined by a nfs://-URL here,
> +		 * so $ mount -o port ... # can be used to override
> +		 */
> +		nfs_pmap->pm_port = nfsurl_port;
> +	}
> +
>   	/* parse options */
>   	new_opts[0] = 0;
>   	if (!parse_options(old_opts, &data, &bg, &retry, &mnt_server, &nfs_server,
> @@ -863,10 +923,13 @@ noauth_flavors:
>   		}
>   	}
>   
> +	mount_free_parse_nfs_url(&pnu);
> +
>   	return EX_SUCCESS;
>   
>   	/* abort */
>    fail:
> +	mount_free_parse_nfs_url(&pnu);
>   	if (fsock != -1)
>   		close(fsock);
>   	return retval;
> diff --git a/utils/mount/parse_dev.c b/utils/mount/parse_dev.c
> index 2ade5d5d..d9f8cf59 100644
> --- a/utils/mount/parse_dev.c
> +++ b/utils/mount/parse_dev.c
> @@ -27,6 +27,8 @@
>   #include "xcommon.h"
>   #include "nls.h"
>   #include "parse_dev.h"
> +#include "urlparser1.h"
> +#include "utils.h"
>   
>   #ifndef NFS_MAXHOSTNAME
>   #define NFS_MAXHOSTNAME		(255)
> @@ -179,17 +181,62 @@ static int nfs_parse_square_bracket(const char *dev,
>   }
>   
>   /*
> - * RFC 2224 says an NFS client must grok "public file handles" to
> - * support NFS URLs.  Linux doesn't do that yet.  Print a somewhat
> - * helpful error message in this case instead of pressing forward
> - * with the mount request and failing with a cryptic error message
> - * later.
> + * Support nfs://-URLS per RFC 2224 ("NFS URL
> + * SCHEME", see https://www.rfc-editor.org/rfc/rfc2224.html),
> + * including port support (nfs://hostname@port/path/...)
>    */
> -static int nfs_parse_nfs_url(__attribute__((unused)) const char *dev,
> -			     __attribute__((unused)) char **hostname,
> -			     __attribute__((unused)) char **pathname)
> +static int nfs_parse_nfs_url(const char *dev,
> +			     char **out_hostname,
> +			     char **out_pathname)
>   {
> -	nfs_error(_("%s: NFS URLs are not supported"), progname);
> +	parsed_nfs_url pnu;
> +
> +	if (out_hostname)
> +		*out_hostname = NULL;
> +	if (out_pathname)
> +		*out_pathname = NULL;
> +
> +	/*
> +	 * Support nfs://-URLS per RFC 2224 ("NFS URL
> +	 * SCHEME", see https://www.rfc-editor.org/rfc/rfc2224.html),
> +	 * including custom port (nfs://hostname@port/path/...)
> +	 * and URL parameter (e.g. nfs://.../?param1=val1&param2=val2
> +	 * support
> +	 */
> +	if (!mount_parse_nfs_url(dev, &pnu)) {
> +		goto fail;
> +	}
> +
> +	if (pnu.uctx->hostport.port != -1) {
> +		/* NOP here, unless we switch from hostname to hostport */
> +	}
> +
> +	if (out_hostname)
> +		*out_hostname = strdup(pnu.uctx->hostport.hostname);
> +	if (out_pathname)
> +		*out_pathname = utf8str2mbstr(pnu.uctx->path);
> +
> +	if (((out_hostname)?(*out_hostname == NULL):0) ||
> +		((out_pathname)?(*out_pathname == NULL):0)) {
> +		nfs_error(_("%s: out of memory"),
> +			progname);
> +		goto fail;
> +	}
> +
> +	mount_free_parse_nfs_url(&pnu);
> +
> +	return 1;
> +
> +fail:
> +	mount_free_parse_nfs_url(&pnu);
> +	if (out_hostname) {
> +		free(*out_hostname);
> +		*out_hostname = NULL;
> +	}
> +	if (out_pathname) {
> +		free(*out_pathname);
> +		*out_pathname = NULL;
> +	}
>   	return 0;
>   }
>   
> @@ -223,7 +270,7 @@ int nfs_parse_devname(const char *devname,
>   		return nfs_pdn_nomem_err();
>   	if (*dev == '[')
>   		result = nfs_parse_square_bracket(dev, hostname, pathname);
> -	else if (strncmp(dev, "nfs://", 6) == 0)
> +	else if (is_spec_nfs_url(dev))
>   		result = nfs_parse_nfs_url(dev, hostname, pathname);
>   	else
>   		result = nfs_parse_simple_hostname(dev, hostname, pathname);
> diff --git a/utils/mount/stropts.c b/utils/mount/stropts.c
> index 23f0a8c0..ad92ab78 100644
> --- a/utils/mount/stropts.c
> +++ b/utils/mount/stropts.c
> @@ -42,6 +42,7 @@
>   #include "nls.h"
>   #include "nfsrpc.h"
>   #include "mount_constants.h"
> +#include "urlparser1.h"
>   #include "stropts.h"
>   #include "error.h"
>   #include "network.h"
> @@ -50,6 +51,7 @@
>   #include "parse_dev.h"
>   #include "conffile.h"
>   #include "misc.h"
> +#include "utils.h"
>   
>   #ifndef NFS_PROGRAM
>   #define NFS_PROGRAM	(100003)
> @@ -643,24 +645,106 @@ static int nfs_sys_mount(struct nfsmount_info *mi, struct mount_options *opts)
>   {
>   	char *options = NULL;
>   	int result;
> +	int nfs_port = 2049;
>   
>   	if (mi->fake)
>   		return 1;
>   
> -	if (po_join(opts, &options) == PO_FAILED) {
> -		errno = EIO;
> -		return 0;
> -	}
> +	/*
> +	 * Support nfs://-URLS per RFC 2224 ("NFS URL
> +	 * SCHEME", see https://www.rfc-editor.org/rfc/rfc2224.html),
> +	 * including custom port (nfs://hostname@port/path/...)
> +	 * and URL parameter (e.g. nfs://.../?param1=val1&param2=val2
> +	 * support
> +	 */
> +	if (is_spec_nfs_url(mi->spec)) {
> +		parsed_nfs_url pnu;
> +		char *mb_path;
> +		char mount_source[1024];
> +
> +		if (!mount_parse_nfs_url(mi->spec, &pnu)) {
> +			result = 1;
> +			errno = EINVAL;
> +			goto done;
> +		}
> +
> +		/*
> +		 * |pnu.uctx->path| is in UTF-8, but we need the data
> +		 * in the current local locale's encoding, as mount(2)
> +		 * does not have something like a |MS_UTF8_SPEC| flag
> +		 * to indicate that the input path is in UTF-8,
> +		 * independently of the current locale
> +		 */
> +		mb_path = utf8str2mbstr(pnu.uctx->path);
> +		if (!mb_path) {
> +			nfs_error(_("%s: Could not convert path to local encoding."),
> +				progname);
> +			mount_free_parse_nfs_url(&pnu);
> +			result = 1;
> +			errno = EINVAL;
> +			goto done;
> +		}
> +
> +		(void)snprintf(mount_source, sizeof(mount_source),
> +			"%s:/%s",
> +			pnu.uctx->hostport.hostname,
> +			mb_path);
> +		free(mb_path);
> +
> +		if (pnu.uctx->hostport.port != -1) {
> +			nfs_port = pnu.uctx->hostport.port;
> +		}
>   
> -	result = mount(mi->spec, mi->node, mi->type,
> +		/*
> +		 * Insert "port=" option with the value from the nfs://
> +		 * URL at the beginning of the list of options, so
> +		 * users can override it with $ mount.nfs4 -o port= #,
> +		 * e.g.
> +		 * $ mount.nfs4 -o port=1234 nfs://10.49.202.230:400//bigdisk /mnt4 #
> +		 * should use port 1234, and not port 400 as specified
> +		 * in the URL.
> +		 */
> +		char portoptbuf[5+32+1];
> +		(void)snprintf(portoptbuf, sizeof(portoptbuf), "port=%d", nfs_port);
> +		(void)po_insert(opts, portoptbuf);
> +
> +		if (pnu.mount_params.read_only != TRIS_BOOL_NOT_SET) {
> +			if (pnu.mount_params.read_only)
> +				mi->flags |= MS_RDONLY;
> +			else
> +				mi->flags &= ~MS_RDONLY;
> +		}
> +
> +		mount_free_parse_nfs_url(&pnu);
> +
> +		if (po_join(opts, &options) == PO_FAILED) {
> +			errno = EIO;
> +			result = 1;
> +			goto done;
> +		}
> +
> +		result = mount(mount_source, mi->node, mi->type,
> +			mi->flags & ~(MS_USER|MS_USERS), options);
> +		free(options);
> +	} else {
> +		if (po_join(opts, &options) == PO_FAILED) {
> +			errno = EIO;
> +			result = 1;
> +			goto done;
> +		}
> +
> +		result = mount(mi->spec, mi->node, mi->type,
>   			mi->flags & ~(MS_USER|MS_USERS), options);
> -	free(options);
> +		free(options);
> +	}
>   
>   	if (verbose && result) {
>   		int save = errno;
>   		nfs_error(_("%s: mount(2): %s"), progname, strerror(save));
>   		errno = save;
>   	}
> +
> +done:
>   	return !result;
>   }
>   
> diff --git a/utils/mount/urlparser1.c b/utils/mount/urlparser1.c
> new file mode 100644
> index 00000000..d4c6f339
> --- /dev/null
> +++ b/utils/mount/urlparser1.c
> @@ -0,0 +1,358 @@
> +/*
> + * MIT License
> + *
> + * Copyright (c) 2024 Roland Mainz <roland.mainz@nrubsig.org>
> + *
> + * Permission is hereby granted, free of charge, to any person obtaining a copy
> + * of this software and associated documentation files (the "Software"), to deal
> + * in the Software without restriction, including without limitation the rights
> + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> + * copies of the Software, and to permit persons to whom the Software is
> + * furnished to do so, subject to the following conditions:
> + *
> + * The above copyright notice and this permission notice shall be included in all
> + * copies or substantial portions of the Software.
> + *
> + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
> + * SOFTWARE.
> + */
> +
> +/* urlparser1.c - simple URL parser */
> +
> +#include <stdlib.h>
> +#include <stdbool.h>
> +#include <string.h>
> +#include <ctype.h>
> +#include <stdio.h>
> +
> +#ifdef DBG_USE_WIDECHAR
> +#include <wchar.h>
> +#include <locale.h>
> +#include <io.h>
> +#include <fcntl.h>
> +#endif /* DBG_USE_WIDECHAR */
> +
> +#include "urlparser1.h"
> +
> +typedef struct _url_parser_context_private {
> +	url_parser_context c;
> +
> +	/* Private data */
> +	char *parameter_string_buff;
> +} url_parser_context_private;
> +
> +#define MAX_URL_PARAMETERS 256
> +
> +/*
> + * Original extended regular expression:
> + *
> + * "^"
> + * "(.+?)"				// scheme
> + * "://"				// '://'
> + * "("					// login
> + *	"(?:"
> + *	"(.+?)"				// user (optional)
> + *		"(?::(.+))?"		// password (optional)
> + *		"@"
> + *	")?"
> + *	"("				// hostport
> + *		"(.+?)"			// host
> + *		"(?::([[:digit:]]+))?"	// port (optional)
> + *	")"
> + * ")"
> + * "(?:/(.*?))?"			// path (optional)
> + * "(?:\?(.*?))?"			// URL parameters (optional)
> + * "$"
> + */
> +
> +#define DBGNULLSTR(s) (((s)!=NULL)?(s):"<NULL>")
> +#if 0 || defined(TEST_URLPARSER)
> +#define D(x) x
> +#else
> +#define D(x)
> +#endif
> +
> +#ifdef DBG_USE_WIDECHAR
> +/*
> + * Use wide-char APIs on WIN32, otherwise we cannot output
> + * Japanese/Chinese/etc correctly
> + */
> +#define DBG_PUTS(str, fp)		fputws(L"" str, (fp))
> +#define DBG_PUTC(c, fp)			fputwc(btowc(c), (fp))
> +#define DBG_PRINTF(fp, fmt, ...)	fwprintf((fp), L"" fmt, __VA_ARGS__)
> +#else
> +#define DBG_PUTS(str, fp)		fputs((str), (fp))
> +#define DBG_PUTC(c, fp)			fputc((c), (fp))
> +#define DBG_PRINTF(fp, fmt, ...)	fprintf((fp), fmt, __VA_ARGS__)
> +#endif /* DBG_USE_WIDECHAR */
> +
> +static
> +void urldecodestr(char *outbuff, const char *buffer, size_t len)
> +{
> +	size_t i, j;
> +
> +	for (i = j = 0 ; i < len ; ) {
> +		switch (buffer[i]) {
> +			case '%':
> +				if ((i + 2) < len) {
> +					if (isxdigit((int)buffer[i+1]) && isxdigit((int)buffer[i+2])) {
> +						const char hexstr[3] = {
> +							buffer[i+1],
> +							buffer[i+2],
> +							'\0'
> +						};
> +						outbuff[j++] = (unsigned char)strtol(hexstr, NULL, 16);
> +						i += 3;
> +					} else {
> +						/* invalid hex digit */
> +						outbuff[j++] = buffer[i];
> +						i++;
> +					}
> +				} else {
> +					/* incomplete hex digit */
> +					outbuff[j++] = buffer[i];
> +					i++;
> +				}
> +				break;
> +			case '+':
> +				outbuff[j++] = ' ';
> +				i++;
> +				break;
> +			default:
> +				outbuff[j++] = buffer[i++];
> +				break;
> +		}
> +	}
> +
> +	outbuff[j] = '\0';
> +}
> +
> +url_parser_context *url_parser_create_context(const char *in_url, unsigned int flags)
> +{
> +	url_parser_context_private *uctx;
> +	char *s;
> +	size_t in_url_len;
> +	size_t context_len;
> +
> +	/* |flags| is for future extensions */
> +	(void)flags;
> +
> +	if (!in_url)
> +		return NULL;
> +
> +	in_url_len = strlen(in_url);
> +
> +	context_len = sizeof(url_parser_context_private) +
> +		((in_url_len+1)*6) +
> +		(sizeof(url_parser_name_value)*MAX_URL_PARAMETERS)+sizeof(void*);
> +	uctx = malloc(context_len);
> +	if (!uctx)
> +		return NULL;
> +
> +	s = (void *)(uctx+1);
> +	uctx->c.in_url = s;		s+= in_url_len+1;
> +	(void)strcpy(uctx->c.in_url, in_url);
> +	uctx->c.scheme = s;		s+= in_url_len+1;
> +	uctx->c.login.username = s;	s+= in_url_len+1;
> +	uctx->c.hostport.hostname = s;	s+= in_url_len+1;
> +	uctx->c.path = s;		s+= in_url_len+1;
> +	uctx->c.hostport.port = -1;
> +	uctx->c.num_parameters = -1;
> +	uctx->c.parameters = (void *)s;		s+= (sizeof(url_parser_name_value)*MAX_URL_PARAMETERS)+sizeof(void*);
> +	uctx->parameter_string_buff = s;	s+= in_url_len+1;
> +
> +	return &uctx->c;
> +}
> +
> +int url_parser_parse(url_parser_context *ctx)
> +{
> +	url_parser_context_private *uctx = (url_parser_context_private *)ctx;
> +
> +	D((void)DBG_PRINTF(stderr, "## parser in_url='%s'\n", uctx->c.in_url));
> +
> +	char *s;
> +	const char *urlstr = uctx->c.in_url;
> +	size_t slen;
> +
> +	s = strstr(urlstr, "://");
> +	if (!s) {
> +		D((void)DBG_PUTS("url_parser: Not an URL\n", stderr));
> +		return -1;
> +	}
> +
> +	slen = s-urlstr;
> +	(void)memcpy(uctx->c.scheme, urlstr, slen);
> +	uctx->c.scheme[slen] = '\0';
> +	urlstr += slen + 3;
> +
> +	D((void)DBG_PRINTF(stdout, "scheme='%s', rest='%s'\n", uctx->c.scheme, urlstr));
> +
> +	s = strstr(urlstr, "@");
> +	if (s) {
> +		/* URL has user/password */
> +		slen = s-urlstr;
> +		urldecodestr(uctx->c.login.username, urlstr, slen);
> +		urlstr += slen + 1;
> +
> +		s = strstr(uctx->c.login.username, ":");
> +		if (s) {
> +			/* found passwd */
> +			uctx->c.login.passwd = s+1;
> +			*s = '\0';
> +		}
> +		else {
> +			uctx->c.login.passwd = NULL;
> +		}
> +
> +		/* catch password-only URLs */
> +		if (uctx->c.login.username[0] == '\0')
> +			uctx->c.login.username = NULL;
> +	}
> +	else {
> +		uctx->c.login.username = NULL;
> +		uctx->c.login.passwd = NULL;
> +	}
> +
> +	D((void)DBG_PRINTF(stdout, "login='%s', passwd='%s', rest='%s'\n",
> +		DBGNULLSTR(uctx->c.login.username),
> +		DBGNULLSTR(uctx->c.login.passwd),
> +		DBGNULLSTR(urlstr)));
> +
> +	char *raw_parameters;
> +
> +	uctx->c.num_parameters = 0;
> +	raw_parameters = strstr(urlstr, "?");
> +	/* Do we have a non-empty parameter string ? */
> +	if (raw_parameters && (raw_parameters[1] != '\0')) {
> +		*raw_parameters++ = '\0';
> +		D((void)DBG_PRINTF(stdout, "raw parameters = '%s'\n", raw_parameters));
> +
> +		char *ps = raw_parameters;
> +		char *pv; /* parameter value */
> +		char *na; /* next '&' */
> +		char *pb = uctx->parameter_string_buff;
> +		char *pname;
> +		char *pvalue;
> +		ssize_t pi;
> +
> +		for (pi = 0; pi < MAX_URL_PARAMETERS ; pi++) {
> +			pname = ps;
> +
> +			/*
> +			 * Handle parameters without value,
> +			 * e.g. "path?name1&name2=value2"
> +			 */
> +			na = strstr(ps, "&");
> +			pv = strstr(ps, "=");
> +			if (pv && (na?(na > pv):true)) {
> +				*pv++ = '\0';
> +				pvalue = pv;
> +				ps = pv;
> +			}
> +			else {
> +				pvalue = NULL;
> +			}
> +
> +			if (na) {
> +				*na++ = '\0';
> +			}
> +
> +			/* URLDecode parameter name */
> +			urldecodestr(pb, pname, strlen(pname));
> +			uctx->c.parameters[pi].name = pb;
> +			pb += strlen(uctx->c.parameters[pi].name)+1;
> +
> +			/* URLDecode parameter value */
> +			if (pvalue) {
> +				urldecodestr(pb, pvalue, strlen(pvalue));
> +				uctx->c.parameters[pi].value = pb;
> +				pb += strlen(uctx->c.parameters[pi].value)+1;
> +			}
> +			else {
> +				uctx->c.parameters[pi].value = NULL;
> +			}
> +
> +			/* Next '&' ? */
> +			if (!na)
> +				break;
> +
> +			ps = na;
> +		}
> +
> +		uctx->c.num_parameters = pi+1;
> +	}
> +
> +	s = strstr(urlstr, "/");
> +	if (s) {
> +		/* URL has hostport */
> +		slen = s-urlstr;
> +		urldecodestr(uctx->c.hostport.hostname, urlstr, slen);
> +		urlstr += slen + 1;
> +
> +		/*
> +		 * check for addresses within '[' and ']', like
> +		 * IPv6 addresses
> +		 */
> +		s = uctx->c.hostport.hostname;
> +		if (s[0] == '[')
> +			s = strstr(s, "]");
> +
> +		if (s == NULL) {
> +			D((void)DBG_PUTS("url_parser: Unmatched '[' in hostname\n", stderr));
> +			return -1;
> +		}
> +
> +		s = strstr(s, ":");
> +		if (s) {
> +			/* found port number */
> +			uctx->c.hostport.port = atoi(s+1);
> +			*s = '\0';
> +		}
> +	}
> +	else {
> +		(void)strcpy(uctx->c.hostport.hostname, urlstr);
> +		uctx->c.path = NULL;
> +		urlstr = NULL;
> +	}
> +
> +	D(
> +		(void)DBG_PRINTF(stdout,
> +			"hostport='%s', port=%d, rest='%s', num_parameters=%d\n",
> +			DBGNULLSTR(uctx->c.hostport.hostname),
> +			uctx->c.hostport.port,
> +			DBGNULLSTR(urlstr),
> +			(int)uctx->c.num_parameters);
> +	);
> +
> +
> +	D(
> +		ssize_t dpi;
> +		for (dpi = 0 ; dpi < uctx->c.num_parameters ; dpi++) {
> +			(void)DBG_PRINTF(stdout,
> +				"param[%d]: name='%s'/value='%s'\n",
> +				(int)dpi,
> +				uctx->c.parameters[dpi].name,
> +				DBGNULLSTR(uctx->c.parameters[dpi].value));
> +		}
> +	);
> +
> +	if (!urlstr) {
> +		goto done;
> +	}
> +
> +	urldecodestr(uctx->c.path, urlstr, strlen(urlstr));
> +	D((void)DBG_PRINTF(stdout, "path='%s'\n", uctx->c.path));
> +
> +done:
> +	return 0;
> +}
> +
> +void url_parser_free_context(url_parser_context *c)
> +{
> +	free(c);
> +}
> diff --git a/utils/mount/urlparser1.h b/utils/mount/urlparser1.h
> new file mode 100644
> index 00000000..515eea9d
> --- /dev/null
> +++ b/utils/mount/urlparser1.h
> @@ -0,0 +1,60 @@
> +/*
> + * MIT License
> + *
> + * Copyright (c) 2024 Roland Mainz <roland.mainz@nrubsig.org>
> + *
> + * Permission is hereby granted, free of charge, to any person obtaining a copy
> + * of this software and associated documentation files (the "Software"), to deal
> + * in the Software without restriction, including without limitation the rights
> + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> + * copies of the Software, and to permit persons to whom the Software is
> + * furnished to do so, subject to the following conditions:
> + *
> + * The above copyright notice and this permission notice shall be included in all
> + * copies or substantial portions of the Software.
> + *
> + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
> + * SOFTWARE.
> + */
> +
> +/* urlparser1.h - header for simple URL parser */
> +
> +#ifndef __URLPARSER1_H__
> +#define __URLPARSER1_H__ 1
> +
> +#include <stdlib.h>
> +
> +typedef struct _url_parser_name_value {
> +	char *name;
> +	char *value;
> +} url_parser_name_value;
> +
> +typedef struct _url_parser_context {
> +	char *in_url;
> +
> +	char *scheme;
> +	struct {
> +		char *username;
> +		char *passwd;
> +	} login;
> +	struct {
> +		char *hostname;
> +		signed int port;
> +	} hostport;
> +	char *path;
> +
> +	ssize_t num_parameters;
> +	url_parser_name_value *parameters;
> +} url_parser_context;
> +
> +/* Prototypes */
> +url_parser_context *url_parser_create_context(const char *in_url, unsigned int flags);
> +int url_parser_parse(url_parser_context *uctx);
> +void url_parser_free_context(url_parser_context *c);
> +
> +#endif /* !__URLPARSER1_H__ */
> diff --git a/utils/mount/utils.c b/utils/mount/utils.c
> index b7562a47..3d55e997 100644
> --- a/utils/mount/utils.c
> +++ b/utils/mount/utils.c
> @@ -28,6 +28,7 @@
>   #include <unistd.h>
>   #include <sys/types.h>
>   #include <sys/stat.h>
> +#include <iconv.h>
>   
>   #include "sockaddr.h"
>   #include "nfs_mount.h"
> @@ -173,3 +174,157 @@ int nfs_umount23(const char *devname, char *string)
>   	free(dirname);
>   	return result;
>   }
> +
> +/* Convert UTF-8 string to multibyte string in the current locale */
> +char *utf8str2mbstr(const char *utf8_str)
> +{
> +	iconv_t cd;
> +
> +	cd = iconv_open("", "UTF-8");
> +	if (cd == (iconv_t)-1) {
> +		perror("utf8str2mbstr: iconv_open failed");
> +		return NULL;
> +	}
> +
> +	size_t inbytesleft = strlen(utf8_str);
> +	char *inbuf = (char *)utf8_str;
> +	size_t outbytesleft = inbytesleft*4+1;
> +	char *outbuf = malloc(outbytesleft);
> +	char *outbuf_orig = outbuf;
> +
> +	if (!outbuf) {
> +		perror("utf8str2mbstr: out of memory");
> +		(void)iconv_close(cd);
> +		return NULL;
> +	}
> +
> +	int ret = iconv(cd, &inbuf, &inbytesleft, &outbuf, &outbytesleft);
> +	if (ret == -1) {
> +		perror("utf8str2mbstr: iconv() failed");
> +		free(outbuf_orig);
> +		(void)iconv_close(cd);
> +		return NULL;
> +	}
> +
> +	*outbuf = '\0';
> +
> +	(void)iconv_close(cd);
> +	return outbuf_orig;
> +}
> +
> +/* fixme: We should use |bool|! */
> +int is_spec_nfs_url(const char *spec)
> +{
> +	return (!strncmp(spec, "nfs://", 6));
> +}
> +
> +int mount_parse_nfs_url(const char *spec, parsed_nfs_url *pnu)
> +{
> +	int result = 1;
> +	url_parser_context *uctx = NULL;
> +
> +	(void)memset(pnu, 0, sizeof(parsed_nfs_url));
> +	pnu->mount_params.read_only = TRIS_BOOL_NOT_SET;
> +
> +	uctx = url_parser_create_context(spec, 0);
> +	if (!uctx) {
> +		nfs_error(_("%s: out of memory"),
> +			progname);
> +		result = 1;
> +		goto done;
> +	}
> +
> +	if (url_parser_parse(uctx) < 0) {
> +		nfs_error(_("%s: Error parsing nfs://-URL."),
> +			progname);
> +		result = 1;
> +		goto done;
> +	}
> +	if (uctx->login.username || uctx->login.passwd) {
> +		nfs_error(_("%s: Username/Password are not defined for nfs://-URL."),
> +			progname);
> +		result = 1;
> +		goto done;
> +	}
> +	if (!uctx->path) {
> +		nfs_error(_("%s: Path missing in nfs://-URL."),
> +			progname);
> +		result = 1;
> +		goto done;
> +	}
> +	if (uctx->path[0] != '/') {
> +		nfs_error(_("%s: Relative nfs://-URLs are not supported."),
> +			progname);
> +		result = 1;
> +		goto done;
> +	}
> +
> +	if (uctx->num_parameters > 0) {
> +		int pi;
> +		const char *pname;
> +		const char *pvalue;
> +
> +		/*
> +		 * Values added here based on URL parameters
> +		 * should be added the front of the list of options,
> +		 * so users can override the nfs://-URL given default.
> +		 */
> +		for (pi = 0; pi < uctx->num_parameters ; pi++) {
> +			pname = uctx->parameters[pi].name;
> +			pvalue = uctx->parameters[pi].value;
> +
> +			if (!strcmp(pname, "rw")) {
> +				if ((pvalue == NULL) || (!strcmp(pvalue, "1"))) {
> +					pnu->mount_params.read_only = TRIS_BOOL_FALSE;
> +				}
> +				else if (!strcmp(pvalue, "0")) {
> +					pnu->mount_params.read_only = TRIS_BOOL_TRUE;
> +				}
> +				else {
> +					nfs_error(_("%s: Unsupported nfs://-URL "
> +						"parameter '%s' value '%s'."),
> +						progname, pname, pvalue);
> +					result = 1;
> +					goto done;
> +				}
> +			}
> +			else if (!strcmp(pname, "ro")) {
> +				if ((pvalue == NULL) || (!strcmp(pvalue, "1"))) {
> +					pnu->mount_params.read_only = TRIS_BOOL_TRUE;
> +				}
> +				else if (!strcmp(pvalue, "0")) {
> +					pnu->mount_params.read_only = TRIS_BOOL_FALSE;
> +				}
> +				else {
> +					nfs_error(_("%s: Unsupported nfs://-URL "
> +						"parameter '%s' value '%s'."),
> +						progname, pname, pvalue);
> +					result = 1;
> +					goto done;
> +				}
> +			}
> +			else {
> +				nfs_error(_("%s: Unsupported nfs://-URL "
> +						"parameter '%s'."),
> +					progname, pname);
> +				result = 1;
> +				goto done;
> +			}
> +		}
> +	}
> +
> +	result = 0;
> +done:
> +	if (result != 0) {
> +		url_parser_free_context(uctx);
> +		return 0;
> +	}
> +
> +	pnu->uctx = uctx;
> +	return 1;
> +}
> +
> +void mount_free_parse_nfs_url(parsed_nfs_url *pnu)
> +{
> +	url_parser_free_context(pnu->uctx);
> +}
> diff --git a/utils/mount/utils.h b/utils/mount/utils.h
> index 224918ae..465c0a5e 100644
> --- a/utils/mount/utils.h
> +++ b/utils/mount/utils.h
> @@ -24,13 +24,36 @@
>   #define _NFS_UTILS_MOUNT_UTILS_H
>   
>   #include "parse_opt.h"
> +#include "urlparser1.h"
>   
> +/* Boolean with three states: { not_set, false, true */
> +typedef signed char tristate_bool;
> +#define TRIS_BOOL_NOT_SET (-1)
> +#define TRIS_BOOL_TRUE (1)
> +#define TRIS_BOOL_FALSE (0)
> +
> +#define TRIS_BOOL_GET_VAL(tsb, tsb_default) \
> +	(((tsb)!=TRIS_BOOL_NOT_SET)?(tsb):(tsb_default))
> +
> +typedef struct _parsed_nfs_url {
> +	url_parser_context *uctx;
> +	struct {
> +		tristate_bool read_only;
> +	} mount_params;
> +} parsed_nfs_url;
> +
> +/* Prototypes */
>   int discover_nfs_mount_data_version(int *string_ver);
>   void print_one(char *spec, char *node, char *type, char *opts);
>   void mount_usage(void);
>   void umount_usage(void);
>   int chk_mountpoint(const char *mount_point);
> +char *utf8str2mbstr(const char *utf8_str);
> +int is_spec_nfs_url(const char *spec);
>   
>   int nfs_umount23(const char *devname, char *string);
>   
> +int mount_parse_nfs_url(const char *spec, parsed_nfs_url *pnu);
> +void mount_free_parse_nfs_url(parsed_nfs_url *pnu);
> +
>   #endif	/* !_NFS_UTILS_MOUNT_UTILS_H */
Chuck Lever Dec. 10, 2024, 5:54 p.m. UTC | #2
On 12/10/24 7:28 AM, Roland Mainz wrote:
> Add support for RFC 2224-style nfs://-URLs as alternative to the
> traditional hostname:/path+-o port=<tcp-port> notation,
> providing standardised, extensible, single-string, crossplatform,
> portable, Character-Encoding independent (e.g. mount point with
> Japanese, Chinese, French etc. characters) and ASCII-compatible
> descriptions of NFSv4 server resources (exports).
> 
> Reviewed-by: Martin Wege <martin.l.wege@gmail.com>
> Signed-off-by: Marvin Wenzel <marvin.wenzel@rovema.de>
> Signed-off-by: Cedric Blancher <cedric.blancher@gmail.com>
> ---
>   utils/mount/Makefile.am  |   3 +-
>   utils/mount/mount.c      |   3 +
>   utils/mount/nfs4mount.c  |  69 +++++++-
>   utils/mount/nfsmount.c   |  93 ++++++++--
>   utils/mount/parse_dev.c  |  67 ++++++--
>   utils/mount/stropts.c    |  96 ++++++++++-
>   utils/mount/urlparser1.c | 358 +++++++++++++++++++++++++++++++++++++++
>   utils/mount/urlparser1.h |  60 +++++++
>   utils/mount/utils.c      | 155 +++++++++++++++++
>   utils/mount/utils.h      |  23 +++
>   10 files changed, 890 insertions(+), 37 deletions(-)
>   create mode 100644 utils/mount/urlparser1.c
>   create mode 100644 utils/mount/urlparser1.h
> 
> diff --git a/utils/mount/Makefile.am b/utils/mount/Makefile.am
> index 83a8ee1c..0e4cab3e 100644
> --- a/utils/mount/Makefile.am
> +++ b/utils/mount/Makefile.am
> @@ -13,7 +13,8 @@ sbin_PROGRAMS	= mount.nfs
>   EXTRA_DIST = nfsmount.conf $(man8_MANS) $(man5_MANS)
>   mount_common = error.c network.c token.c \
>   		    parse_opt.c parse_dev.c \
> -		    nfsmount.c nfs4mount.c stropts.c\
> +		    nfsmount.c nfs4mount.c \
> +		    urlparser1.c urlparser1.h stropts.c \
>   		    mount_constants.h error.h network.h token.h \
>   		    parse_opt.h parse_dev.h \
>   		    nfs4_mount.h stropts.h version.h \
> diff --git a/utils/mount/mount.c b/utils/mount/mount.c
> index b98f9e00..2ce6209d 100644
> --- a/utils/mount/mount.c
> +++ b/utils/mount/mount.c
> @@ -29,6 +29,7 @@
>   #include <string.h>
>   #include <errno.h>
>   #include <fcntl.h>
> +#include <locale.h>
>   #include <sys/mount.h>
>   #include <getopt.h>
>   #include <mntent.h>
> @@ -386,6 +387,8 @@ int main(int argc, char *argv[])
>   	char *extra_opts = NULL, *mount_opts = NULL;
>   	uid_t uid = getuid();
>   
> +	(void)setlocale(LC_ALL, "");
> +
>   	progname = basename(argv[0]);
>   
>   	nfs_mount_data_version = discover_nfs_mount_data_version(&string);
> diff --git a/utils/mount/nfs4mount.c b/utils/mount/nfs4mount.c
> index 0fe142a7..8e4fbf30 100644
> --- a/utils/mount/nfs4mount.c
> +++ b/utils/mount/nfs4mount.c
> @@ -50,8 +50,10 @@
>   #include "mount_constants.h"
>   #include "nfs4_mount.h"
>   #include "nfs_mount.h"
> +#include "urlparser1.h"
>   #include "error.h"
>   #include "network.h"
> +#include "utils.h"
>   
>   #if defined(VAR_LOCK_DIR)
>   #define DEFAULT_DIR VAR_LOCK_DIR
> @@ -182,7 +184,7 @@ int nfs4mount(const char *spec, const char *node, int flags,
>   	int num_flavour = 0;
>   	int ip_addr_in_opts = 0;
>   
> -	char *hostname, *dirname, *old_opts;
> +	char *hostname, *dirname, *mb_dirname = NULL, *old_opts;
>   	char new_opts[1024];
>   	char *opt, *opteq;
>   	char *s;
> @@ -192,15 +194,66 @@ int nfs4mount(const char *spec, const char *node, int flags,
>   	int retry;
>   	int retval = EX_FAIL;
>   	time_t timeout, t;
> +	int nfs_port = NFS_PORT;
> +	parsed_nfs_url pnu;
> +
> +	(void)memset(&pnu, 0, sizeof(parsed_nfs_url));
>   
>   	if (strlen(spec) >= sizeof(hostdir)) {
>   		nfs_error(_("%s: excessively long host:dir argument\n"),
>   				progname);
>   		goto fail;
>   	}
> -	strcpy(hostdir, spec);
> -	if (parse_devname(hostdir, &hostname, &dirname))
> -		goto fail;
> +
> +	/*
> +	 * Support nfs://-URLS per RFC 2224 ("NFS URL
> +	 * SCHEME", see https://www.rfc-editor.org/rfc/rfc2224.html),
> +	 * including custom port (nfs://hostname@port/path/...)
> +	 * and URL parameter (e.g. nfs://.../?param1=val1&param2=val2
> +	 * support
> +	 */
> +	if (is_spec_nfs_url(spec)) {
> +		if (!mount_parse_nfs_url(spec, &pnu)) {
> +			goto fail;
> +		}
> +
> +		/*
> +		 * |pnu.uctx->path| is in UTF-8, but we need the data
> +		 * in the current local locale's encoding, as mount(2)
> +		 * does not have something like a |MS_UTF8_SPEC| flag
> +		 * to indicate that the input path is in UTF-8,
> +		 * independently of the current locale
> +		 */
> +		hostname = pnu.uctx->hostport.hostname;
> +		dirname = mb_dirname = utf8str2mbstr(pnu.uctx->path);
> +
> +		(void)snprintf(hostdir, sizeof(hostdir), "%s:/%s",
> +			hostname, dirname);
> +		spec = hostdir;
> +
> +		if (pnu.uctx->hostport.port != -1) {
> +			nfs_port = pnu.uctx->hostport.port;
> +		}
> +
> +		/*
> +		 * Values added here based on URL parameters
> +		 * should be added the front of the list of options,
> +		 * so users can override the nfs://-URL given default.
> +		 *
> +		 * FIXME: We do not do that here for |MS_RDONLY|!
> +		 */
> +		if (pnu.mount_params.read_only != TRIS_BOOL_NOT_SET) {
> +			if (pnu.mount_params.read_only)
> +				flags |= MS_RDONLY;
> +			else
> +				flags &= ~MS_RDONLY;
> +		}
> +        } else {
> +		(void)strcpy(hostdir, spec);
> +
> +		if (parse_devname(hostdir, &hostname, &dirname))
> +			goto fail;
> +	}
>   
>   	if (fill_ipv4_sockaddr(hostname, &server_addr))
>   		goto fail;
> @@ -247,7 +300,7 @@ int nfs4mount(const char *spec, const char *node, int flags,
>   	/*
>   	 * NFSv4 specifies that the default port should be 2049
>   	 */
> -	server_addr.sin_port = htons(NFS_PORT);
> +	server_addr.sin_port = htons(nfs_port);
>   
>   	/* parse options */
>   
> @@ -474,8 +527,14 @@ int nfs4mount(const char *spec, const char *node, int flags,
>   		}
>   	}
>   
> +	mount_free_parse_nfs_url(&pnu);
> +	free(mb_dirname);
> +
>   	return EX_SUCCESS;
>   
>   fail:
> +	mount_free_parse_nfs_url(&pnu);
> +	free(mb_dirname);
> +
>   	return retval;
>   }
> diff --git a/utils/mount/nfsmount.c b/utils/mount/nfsmount.c
> index a1c92fe8..e61d718a 100644
> --- a/utils/mount/nfsmount.c
> +++ b/utils/mount/nfsmount.c
> @@ -63,11 +63,13 @@
>   #include "xcommon.h"
>   #include "mount.h"
>   #include "nfs_mount.h"
> +#include "urlparser1.h"
>   #include "mount_constants.h"
>   #include "nls.h"
>   #include "error.h"
>   #include "network.h"
>   #include "version.h"
> +#include "utils.h"
>   
>   #ifdef HAVE_RPCSVC_NFS_PROT_H
>   #include <rpcsvc/nfs_prot.h>
> @@ -493,7 +495,7 @@ nfsmount(const char *spec, const char *node, int flags,
>   	 char **extra_opts, int fake, int running_bg)
>   {
>   	char hostdir[1024];
> -	char *hostname, *dirname, *old_opts, *mounthost = NULL;
> +	char *hostname, *dirname, *mb_dirname = NULL, *old_opts, *mounthost = NULL;
>   	char new_opts[1024], cbuf[1024];
>   	static struct nfs_mount_data data;
>   	int val;
> @@ -521,29 +523,79 @@ nfsmount(const char *spec, const char *node, int flags,
>   	time_t t;
>   	time_t prevt;
>   	time_t timeout;
> +	int nfsurl_port = -1;
> +	parsed_nfs_url pnu;
> +
> +	(void)memset(&pnu, 0, sizeof(parsed_nfs_url));
>   
>   	if (strlen(spec) >= sizeof(hostdir)) {
>   		nfs_error(_("%s: excessively long host:dir argument"),
>   				progname);
>   		goto fail;
>   	}
> -	strcpy(hostdir, spec);
> -	if ((s = strchr(hostdir, ':'))) {
> -		hostname = hostdir;
> -		dirname = s + 1;
> -		*s = '\0';
> -		/* Ignore all but first hostname in replicated mounts
> -		   until they can be fully supported. (mack@sgi.com) */
> -		if ((s = strchr(hostdir, ','))) {
> +
> +	/*
> +	 * Support nfs://-URLS per RFC 2224 ("NFS URL
> +	 * SCHEME", see https://www.rfc-editor.org/rfc/rfc2224.html),
> +	 * including custom port (nfs://hostname@port/path/...)
> +	 * and URL parameter (e.g. nfs://.../?param1=val1&param2=val2
> +	 * support
> +	 */
> +	if (is_spec_nfs_url(spec)) {
> +		if (!mount_parse_nfs_url(spec, &pnu)) {
> +			goto fail;
> +		}
> +
> +		/*
> +		 * |pnu.uctx->path| is in UTF-8, but we need the data
> +		 * in the current local locale's encoding, as mount(2)
> +		 * does not have something like a |MS_UTF8_SPEC| flag
> +		 * to indicate that the input path is in UTF-8,
> +		 * independently of the current locale
> +		 */
> +		hostname = pnu.uctx->hostport.hostname;
> +		dirname = mb_dirname = utf8str2mbstr(pnu.uctx->path);
> +
> +		(void)snprintf(hostdir, sizeof(hostdir), "%s:/%s",
> +			hostname, dirname);
> +		spec = hostdir;
> +
> +		if (pnu.uctx->hostport.port != -1) {
> +			nfsurl_port = pnu.uctx->hostport.port;
> +		}
> +
> +		/*
> +		 * Values added here based on URL parameters
> +		 * should be added the front of the list of options,
> +		 * so users can override the nfs://-URL given default.
> +		 *
> +		 * FIXME: We do not do that here for |MS_RDONLY|!
> +		 */
> +		if (pnu.mount_params.read_only != TRIS_BOOL_NOT_SET) {
> +			if (pnu.mount_params.read_only)
> +				flags |= MS_RDONLY;
> +			else
> +				flags &= ~MS_RDONLY;
> +		}
> +        } else {
> +		(void)strcpy(hostdir, spec);
> +		if ((s = strchr(hostdir, ':'))) {
> +			hostname = hostdir;
> +			dirname = s + 1;
>   			*s = '\0';
> -			nfs_error(_("%s: warning: "
> -				  "multiple hostnames not supported"),
> +			/* Ignore all but first hostname in replicated mounts
> +			   until they can be fully supported. (mack@sgi.com) */
> +			if ((s = strchr(hostdir, ','))) {
> +				*s = '\0';
> +				nfs_error(_("%s: warning: "
> +					"multiple hostnames not supported"),
>   					progname);
> -		}
> -	} else {
> -		nfs_error(_("%s: directory to mount not in host:dir format"),
> +			}
> +		} else {
> +			nfs_error(_("%s: directory to mount not in host:dir format"),
>   				progname);
> -		goto fail;
> +			goto fail;
> +		}
>   	}
>   
>   	if (!nfs_gethostbyname(hostname, nfs_saddr))
> @@ -579,6 +631,14 @@ nfsmount(const char *spec, const char *node, int flags,
>   	memset(nfs_pmap, 0, sizeof(*nfs_pmap));
>   	nfs_pmap->pm_prog = NFS_PROGRAM;
>   
> +	if (nfsurl_port != -1) {
> +		/*
> +		 * Set custom TCP port defined by a nfs://-URL here,
> +		 * so $ mount -o port ... # can be used to override
> +		 */
> +		nfs_pmap->pm_port = nfsurl_port;
> +	}
> +
>   	/* parse options */
>   	new_opts[0] = 0;
>   	if (!parse_options(old_opts, &data, &bg, &retry, &mnt_server, &nfs_server,
> @@ -863,10 +923,13 @@ noauth_flavors:
>   		}
>   	}
>   
> +	mount_free_parse_nfs_url(&pnu);
> +
>   	return EX_SUCCESS;
>   
>   	/* abort */
>    fail:
> +	mount_free_parse_nfs_url(&pnu);
>   	if (fsock != -1)
>   		close(fsock);
>   	return retval;
> diff --git a/utils/mount/parse_dev.c b/utils/mount/parse_dev.c
> index 2ade5d5d..d9f8cf59 100644
> --- a/utils/mount/parse_dev.c
> +++ b/utils/mount/parse_dev.c
> @@ -27,6 +27,8 @@
>   #include "xcommon.h"
>   #include "nls.h"
>   #include "parse_dev.h"
> +#include "urlparser1.h"
> +#include "utils.h"
>   
>   #ifndef NFS_MAXHOSTNAME
>   #define NFS_MAXHOSTNAME		(255)
> @@ -179,17 +181,62 @@ static int nfs_parse_square_bracket(const char *dev,
>   }
>   
>   /*
> - * RFC 2224 says an NFS client must grok "public file handles" to
> - * support NFS URLs.  Linux doesn't do that yet.  Print a somewhat
> - * helpful error message in this case instead of pressing forward
> - * with the mount request and failing with a cryptic error message
> - * later.
> + * Support nfs://-URLS per RFC 2224 ("NFS URL
> + * SCHEME", see https://www.rfc-editor.org/rfc/rfc2224.html),
> + * including port support (nfs://hostname@port/path/...)
>    */
> -static int nfs_parse_nfs_url(__attribute__((unused)) const char *dev,
> -			     __attribute__((unused)) char **hostname,
> -			     __attribute__((unused)) char **pathname)
> +static int nfs_parse_nfs_url(const char *dev,
> +			     char **out_hostname,
> +			     char **out_pathname)
>   {
> -	nfs_error(_("%s: NFS URLs are not supported"), progname);
> +	parsed_nfs_url pnu;
> +
> +	if (out_hostname)
> +		*out_hostname = NULL;
> +	if (out_pathname)
> +		*out_pathname = NULL;
> +
> +	/*
> +	 * Support nfs://-URLS per RFC 2224 ("NFS URL
> +	 * SCHEME", see https://www.rfc-editor.org/rfc/rfc2224.html),
> +	 * including custom port (nfs://hostname@port/path/...)
> +	 * and URL parameter (e.g. nfs://.../?param1=val1&param2=val2
> +	 * support
> +	 */
> +	if (!mount_parse_nfs_url(dev, &pnu)) {
> +		goto fail;
> +	}
> +
> +	if (pnu.uctx->hostport.port != -1) {
> +		/* NOP here, unless we switch from hostname to hostport */
> +	}
> +
> +	if (out_hostname)
> +		*out_hostname = strdup(pnu.uctx->hostport.hostname);
> +	if (out_pathname)
> +		*out_pathname = utf8str2mbstr(pnu.uctx->path);
> +
> +	if (((out_hostname)?(*out_hostname == NULL):0) ||
> +		((out_pathname)?(*out_pathname == NULL):0)) {
> +		nfs_error(_("%s: out of memory"),
> +			progname);
> +		goto fail;
> +	}
> +
> +	mount_free_parse_nfs_url(&pnu);
> +
> +	return 1;
> +
> +fail:
> +	mount_free_parse_nfs_url(&pnu);
> +	if (out_hostname) {
> +		free(*out_hostname);
> +		*out_hostname = NULL;
> +	}
> +	if (out_pathname) {
> +		free(*out_pathname);
> +		*out_pathname = NULL;
> +	}
>   	return 0;
>   }
>   
> @@ -223,7 +270,7 @@ int nfs_parse_devname(const char *devname,
>   		return nfs_pdn_nomem_err();
>   	if (*dev == '[')
>   		result = nfs_parse_square_bracket(dev, hostname, pathname);
> -	else if (strncmp(dev, "nfs://", 6) == 0)
> +	else if (is_spec_nfs_url(dev))
>   		result = nfs_parse_nfs_url(dev, hostname, pathname);
>   	else
>   		result = nfs_parse_simple_hostname(dev, hostname, pathname);
> diff --git a/utils/mount/stropts.c b/utils/mount/stropts.c
> index 23f0a8c0..ad92ab78 100644
> --- a/utils/mount/stropts.c
> +++ b/utils/mount/stropts.c
> @@ -42,6 +42,7 @@
>   #include "nls.h"
>   #include "nfsrpc.h"
>   #include "mount_constants.h"
> +#include "urlparser1.h"
>   #include "stropts.h"
>   #include "error.h"
>   #include "network.h"
> @@ -50,6 +51,7 @@
>   #include "parse_dev.h"
>   #include "conffile.h"
>   #include "misc.h"
> +#include "utils.h"
>   
>   #ifndef NFS_PROGRAM
>   #define NFS_PROGRAM	(100003)
> @@ -643,24 +645,106 @@ static int nfs_sys_mount(struct nfsmount_info *mi, struct mount_options *opts)
>   {
>   	char *options = NULL;
>   	int result;
> +	int nfs_port = 2049;
>   
>   	if (mi->fake)
>   		return 1;
>   
> -	if (po_join(opts, &options) == PO_FAILED) {
> -		errno = EIO;
> -		return 0;
> -	}
> +	/*
> +	 * Support nfs://-URLS per RFC 2224 ("NFS URL
> +	 * SCHEME", see https://www.rfc-editor.org/rfc/rfc2224.html),
> +	 * including custom port (nfs://hostname@port/path/...)
> +	 * and URL parameter (e.g. nfs://.../?param1=val1&param2=val2
> +	 * support
> +	 */
> +	if (is_spec_nfs_url(mi->spec)) {
> +		parsed_nfs_url pnu;
> +		char *mb_path;
> +		char mount_source[1024];
> +
> +		if (!mount_parse_nfs_url(mi->spec, &pnu)) {
> +			result = 1;
> +			errno = EINVAL;
> +			goto done;
> +		}
> +
> +		/*
> +		 * |pnu.uctx->path| is in UTF-8, but we need the data
> +		 * in the current local locale's encoding, as mount(2)
> +		 * does not have something like a |MS_UTF8_SPEC| flag
> +		 * to indicate that the input path is in UTF-8,
> +		 * independently of the current locale
> +		 */
> +		mb_path = utf8str2mbstr(pnu.uctx->path);
> +		if (!mb_path) {
> +			nfs_error(_("%s: Could not convert path to local encoding."),
> +				progname);
> +			mount_free_parse_nfs_url(&pnu);
> +			result = 1;
> +			errno = EINVAL;
> +			goto done;
> +		}
> +
> +		(void)snprintf(mount_source, sizeof(mount_source),
> +			"%s:/%s",
> +			pnu.uctx->hostport.hostname,
> +			mb_path);
> +		free(mb_path);
> +
> +		if (pnu.uctx->hostport.port != -1) {
> +			nfs_port = pnu.uctx->hostport.port;
> +		}
>   
> -	result = mount(mi->spec, mi->node, mi->type,
> +		/*
> +		 * Insert "port=" option with the value from the nfs://
> +		 * URL at the beginning of the list of options, so
> +		 * users can override it with $ mount.nfs4 -o port= #,
> +		 * e.g.
> +		 * $ mount.nfs4 -o port=1234 nfs://10.49.202.230:400//bigdisk /mnt4 #
> +		 * should use port 1234, and not port 400 as specified
> +		 * in the URL.
> +		 */
> +		char portoptbuf[5+32+1];
> +		(void)snprintf(portoptbuf, sizeof(portoptbuf), "port=%d", nfs_port);
> +		(void)po_insert(opts, portoptbuf);
> +
> +		if (pnu.mount_params.read_only != TRIS_BOOL_NOT_SET) {
> +			if (pnu.mount_params.read_only)
> +				mi->flags |= MS_RDONLY;
> +			else
> +				mi->flags &= ~MS_RDONLY;
> +		}
> +
> +		mount_free_parse_nfs_url(&pnu);
> +
> +		if (po_join(opts, &options) == PO_FAILED) {
> +			errno = EIO;
> +			result = 1;
> +			goto done;
> +		}
> +
> +		result = mount(mount_source, mi->node, mi->type,
> +			mi->flags & ~(MS_USER|MS_USERS), options);
> +		free(options);
> +	} else {
> +		if (po_join(opts, &options) == PO_FAILED) {
> +			errno = EIO;
> +			result = 1;
> +			goto done;
> +		}
> +
> +		result = mount(mi->spec, mi->node, mi->type,
>   			mi->flags & ~(MS_USER|MS_USERS), options);
> -	free(options);
> +		free(options);
> +	}
>   
>   	if (verbose && result) {
>   		int save = errno;
>   		nfs_error(_("%s: mount(2): %s"), progname, strerror(save));
>   		errno = save;
>   	}
> +
> +done:
>   	return !result;
>   }
>   
> diff --git a/utils/mount/urlparser1.c b/utils/mount/urlparser1.c
> new file mode 100644
> index 00000000..d4c6f339
> --- /dev/null
> +++ b/utils/mount/urlparser1.c
> @@ -0,0 +1,358 @@
> +/*
> + * MIT License
> + *
> + * Copyright (c) 2024 Roland Mainz <roland.mainz@nrubsig.org>
> + *
> + * Permission is hereby granted, free of charge, to any person obtaining a copy
> + * of this software and associated documentation files (the "Software"), to deal
> + * in the Software without restriction, including without limitation the rights
> + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> + * copies of the Software, and to permit persons to whom the Software is
> + * furnished to do so, subject to the following conditions:
> + *
> + * The above copyright notice and this permission notice shall be included in all
> + * copies or substantial portions of the Software.
> + *
> + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
> + * SOFTWARE.
> + */
> +
> +/* urlparser1.c - simple URL parser */
> +
> +#include <stdlib.h>
> +#include <stdbool.h>
> +#include <string.h>
> +#include <ctype.h>
> +#include <stdio.h>
> +
> +#ifdef DBG_USE_WIDECHAR
> +#include <wchar.h>
> +#include <locale.h>
> +#include <io.h>
> +#include <fcntl.h>
> +#endif /* DBG_USE_WIDECHAR */
> +
> +#include "urlparser1.h"
> +
> +typedef struct _url_parser_context_private {
> +	url_parser_context c;
> +
> +	/* Private data */
> +	char *parameter_string_buff;
> +} url_parser_context_private;
> +
> +#define MAX_URL_PARAMETERS 256
> +
> +/*
> + * Original extended regular expression:
> + *
> + * "^"
> + * "(.+?)"				// scheme
> + * "://"				// '://'
> + * "("					// login
> + *	"(?:"
> + *	"(.+?)"				// user (optional)
> + *		"(?::(.+))?"		// password (optional)
> + *		"@"
> + *	")?"
> + *	"("				// hostport
> + *		"(.+?)"			// host
> + *		"(?::([[:digit:]]+))?"	// port (optional)
> + *	")"
> + * ")"
> + * "(?:/(.*?))?"			// path (optional)
> + * "(?:\?(.*?))?"			// URL parameters (optional)
> + * "$"
> + */
> +
> +#define DBGNULLSTR(s) (((s)!=NULL)?(s):"<NULL>")
> +#if 0 || defined(TEST_URLPARSER)
> +#define D(x) x
> +#else
> +#define D(x)
> +#endif
> +
> +#ifdef DBG_USE_WIDECHAR
> +/*
> + * Use wide-char APIs on WIN32, otherwise we cannot output
> + * Japanese/Chinese/etc correctly
> + */
> +#define DBG_PUTS(str, fp)		fputws(L"" str, (fp))
> +#define DBG_PUTC(c, fp)			fputwc(btowc(c), (fp))
> +#define DBG_PRINTF(fp, fmt, ...)	fwprintf((fp), L"" fmt, __VA_ARGS__)
> +#else
> +#define DBG_PUTS(str, fp)		fputs((str), (fp))
> +#define DBG_PUTC(c, fp)			fputc((c), (fp))
> +#define DBG_PRINTF(fp, fmt, ...)	fprintf((fp), fmt, __VA_ARGS__)
> +#endif /* DBG_USE_WIDECHAR */
> +
> +static
> +void urldecodestr(char *outbuff, const char *buffer, size_t len)
> +{
> +	size_t i, j;
> +
> +	for (i = j = 0 ; i < len ; ) {
> +		switch (buffer[i]) {
> +			case '%':
> +				if ((i + 2) < len) {
> +					if (isxdigit((int)buffer[i+1]) && isxdigit((int)buffer[i+2])) {
> +						const char hexstr[3] = {
> +							buffer[i+1],
> +							buffer[i+2],
> +							'\0'
> +						};
> +						outbuff[j++] = (unsigned char)strtol(hexstr, NULL, 16);
> +						i += 3;
> +					} else {
> +						/* invalid hex digit */
> +						outbuff[j++] = buffer[i];
> +						i++;
> +					}
> +				} else {
> +					/* incomplete hex digit */
> +					outbuff[j++] = buffer[i];
> +					i++;
> +				}
> +				break;
> +			case '+':
> +				outbuff[j++] = ' ';
> +				i++;
> +				break;
> +			default:
> +				outbuff[j++] = buffer[i++];
> +				break;
> +		}
> +	}
> +
> +	outbuff[j] = '\0';
> +}
> +
> +url_parser_context *url_parser_create_context(const char *in_url, unsigned int flags)
> +{
> +	url_parser_context_private *uctx;
> +	char *s;
> +	size_t in_url_len;
> +	size_t context_len;
> +
> +	/* |flags| is for future extensions */
> +	(void)flags;
> +
> +	if (!in_url)
> +		return NULL;
> +
> +	in_url_len = strlen(in_url);
> +
> +	context_len = sizeof(url_parser_context_private) +
> +		((in_url_len+1)*6) +
> +		(sizeof(url_parser_name_value)*MAX_URL_PARAMETERS)+sizeof(void*);
> +	uctx = malloc(context_len);
> +	if (!uctx)
> +		return NULL;
> +
> +	s = (void *)(uctx+1);
> +	uctx->c.in_url = s;		s+= in_url_len+1;
> +	(void)strcpy(uctx->c.in_url, in_url);
> +	uctx->c.scheme = s;		s+= in_url_len+1;
> +	uctx->c.login.username = s;	s+= in_url_len+1;
> +	uctx->c.hostport.hostname = s;	s+= in_url_len+1;
> +	uctx->c.path = s;		s+= in_url_len+1;
> +	uctx->c.hostport.port = -1;
> +	uctx->c.num_parameters = -1;
> +	uctx->c.parameters = (void *)s;		s+= (sizeof(url_parser_name_value)*MAX_URL_PARAMETERS)+sizeof(void*);
> +	uctx->parameter_string_buff = s;	s+= in_url_len+1;
> +
> +	return &uctx->c;
> +}
> +
> +int url_parser_parse(url_parser_context *ctx)
> +{
> +	url_parser_context_private *uctx = (url_parser_context_private *)ctx;
> +
> +	D((void)DBG_PRINTF(stderr, "## parser in_url='%s'\n", uctx->c.in_url));
> +
> +	char *s;
> +	const char *urlstr = uctx->c.in_url;
> +	size_t slen;
> +
> +	s = strstr(urlstr, "://");
> +	if (!s) {
> +		D((void)DBG_PUTS("url_parser: Not an URL\n", stderr));
> +		return -1;
> +	}
> +
> +	slen = s-urlstr;
> +	(void)memcpy(uctx->c.scheme, urlstr, slen);
> +	uctx->c.scheme[slen] = '\0';
> +	urlstr += slen + 3;
> +
> +	D((void)DBG_PRINTF(stdout, "scheme='%s', rest='%s'\n", uctx->c.scheme, urlstr));
> +
> +	s = strstr(urlstr, "@");
> +	if (s) {
> +		/* URL has user/password */
> +		slen = s-urlstr;
> +		urldecodestr(uctx->c.login.username, urlstr, slen);
> +		urlstr += slen + 1;
> +
> +		s = strstr(uctx->c.login.username, ":");
> +		if (s) {
> +			/* found passwd */
> +			uctx->c.login.passwd = s+1;
> +			*s = '\0';
> +		}
> +		else {
> +			uctx->c.login.passwd = NULL;
> +		}
> +
> +		/* catch password-only URLs */
> +		if (uctx->c.login.username[0] == '\0')
> +			uctx->c.login.username = NULL;
> +	}
> +	else {
> +		uctx->c.login.username = NULL;
> +		uctx->c.login.passwd = NULL;
> +	}
> +
> +	D((void)DBG_PRINTF(stdout, "login='%s', passwd='%s', rest='%s'\n",
> +		DBGNULLSTR(uctx->c.login.username),
> +		DBGNULLSTR(uctx->c.login.passwd),
> +		DBGNULLSTR(urlstr)));
> +
> +	char *raw_parameters;
> +
> +	uctx->c.num_parameters = 0;
> +	raw_parameters = strstr(urlstr, "?");
> +	/* Do we have a non-empty parameter string ? */
> +	if (raw_parameters && (raw_parameters[1] != '\0')) {
> +		*raw_parameters++ = '\0';
> +		D((void)DBG_PRINTF(stdout, "raw parameters = '%s'\n", raw_parameters));
> +
> +		char *ps = raw_parameters;
> +		char *pv; /* parameter value */
> +		char *na; /* next '&' */
> +		char *pb = uctx->parameter_string_buff;
> +		char *pname;
> +		char *pvalue;
> +		ssize_t pi;
> +
> +		for (pi = 0; pi < MAX_URL_PARAMETERS ; pi++) {
> +			pname = ps;
> +
> +			/*
> +			 * Handle parameters without value,
> +			 * e.g. "path?name1&name2=value2"
> +			 */
> +			na = strstr(ps, "&");
> +			pv = strstr(ps, "=");
> +			if (pv && (na?(na > pv):true)) {
> +				*pv++ = '\0';
> +				pvalue = pv;
> +				ps = pv;
> +			}
> +			else {
> +				pvalue = NULL;
> +			}
> +
> +			if (na) {
> +				*na++ = '\0';
> +			}
> +
> +			/* URLDecode parameter name */
> +			urldecodestr(pb, pname, strlen(pname));
> +			uctx->c.parameters[pi].name = pb;
> +			pb += strlen(uctx->c.parameters[pi].name)+1;
> +
> +			/* URLDecode parameter value */
> +			if (pvalue) {
> +				urldecodestr(pb, pvalue, strlen(pvalue));
> +				uctx->c.parameters[pi].value = pb;
> +				pb += strlen(uctx->c.parameters[pi].value)+1;
> +			}
> +			else {
> +				uctx->c.parameters[pi].value = NULL;
> +			}
> +
> +			/* Next '&' ? */
> +			if (!na)
> +				break;
> +
> +			ps = na;
> +		}
> +
> +		uctx->c.num_parameters = pi+1;
> +	}
> +
> +	s = strstr(urlstr, "/");
> +	if (s) {
> +		/* URL has hostport */
> +		slen = s-urlstr;
> +		urldecodestr(uctx->c.hostport.hostname, urlstr, slen);
> +		urlstr += slen + 1;
> +
> +		/*
> +		 * check for addresses within '[' and ']', like
> +		 * IPv6 addresses
> +		 */
> +		s = uctx->c.hostport.hostname;
> +		if (s[0] == '[')
> +			s = strstr(s, "]");
> +
> +		if (s == NULL) {
> +			D((void)DBG_PUTS("url_parser: Unmatched '[' in hostname\n", stderr));
> +			return -1;
> +		}
> +
> +		s = strstr(s, ":");
> +		if (s) {
> +			/* found port number */
> +			uctx->c.hostport.port = atoi(s+1);
> +			*s = '\0';
> +		}
> +	}
> +	else {
> +		(void)strcpy(uctx->c.hostport.hostname, urlstr);
> +		uctx->c.path = NULL;
> +		urlstr = NULL;
> +	}
> +
> +	D(
> +		(void)DBG_PRINTF(stdout,
> +			"hostport='%s', port=%d, rest='%s', num_parameters=%d\n",
> +			DBGNULLSTR(uctx->c.hostport.hostname),
> +			uctx->c.hostport.port,
> +			DBGNULLSTR(urlstr),
> +			(int)uctx->c.num_parameters);
> +	);
> +
> +
> +	D(
> +		ssize_t dpi;
> +		for (dpi = 0 ; dpi < uctx->c.num_parameters ; dpi++) {
> +			(void)DBG_PRINTF(stdout,
> +				"param[%d]: name='%s'/value='%s'\n",
> +				(int)dpi,
> +				uctx->c.parameters[dpi].name,
> +				DBGNULLSTR(uctx->c.parameters[dpi].value));
> +		}
> +	);
> +
> +	if (!urlstr) {
> +		goto done;
> +	}
> +
> +	urldecodestr(uctx->c.path, urlstr, strlen(urlstr));
> +	D((void)DBG_PRINTF(stdout, "path='%s'\n", uctx->c.path));
> +
> +done:
> +	return 0;
> +}
> +
> +void url_parser_free_context(url_parser_context *c)
> +{
> +	free(c);
> +}
> diff --git a/utils/mount/urlparser1.h b/utils/mount/urlparser1.h
> new file mode 100644
> index 00000000..515eea9d
> --- /dev/null
> +++ b/utils/mount/urlparser1.h
> @@ -0,0 +1,60 @@
> +/*
> + * MIT License
> + *
> + * Copyright (c) 2024 Roland Mainz <roland.mainz@nrubsig.org>
> + *
> + * Permission is hereby granted, free of charge, to any person obtaining a copy
> + * of this software and associated documentation files (the "Software"), to deal
> + * in the Software without restriction, including without limitation the rights
> + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> + * copies of the Software, and to permit persons to whom the Software is
> + * furnished to do so, subject to the following conditions:
> + *
> + * The above copyright notice and this permission notice shall be included in all
> + * copies or substantial portions of the Software.
> + *
> + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
> + * SOFTWARE.
> + */
> +
> +/* urlparser1.h - header for simple URL parser */
> +
> +#ifndef __URLPARSER1_H__
> +#define __URLPARSER1_H__ 1
> +
> +#include <stdlib.h>
> +
> +typedef struct _url_parser_name_value {
> +	char *name;
> +	char *value;
> +} url_parser_name_value;
> +
> +typedef struct _url_parser_context {
> +	char *in_url;
> +
> +	char *scheme;
> +	struct {
> +		char *username;
> +		char *passwd;
> +	} login;
> +	struct {
> +		char *hostname;
> +		signed int port;
> +	} hostport;
> +	char *path;
> +
> +	ssize_t num_parameters;
> +	url_parser_name_value *parameters;
> +} url_parser_context;
> +
> +/* Prototypes */
> +url_parser_context *url_parser_create_context(const char *in_url, unsigned int flags);
> +int url_parser_parse(url_parser_context *uctx);
> +void url_parser_free_context(url_parser_context *c);
> +
> +#endif /* !__URLPARSER1_H__ */
> diff --git a/utils/mount/utils.c b/utils/mount/utils.c
> index b7562a47..3d55e997 100644
> --- a/utils/mount/utils.c
> +++ b/utils/mount/utils.c
> @@ -28,6 +28,7 @@
>   #include <unistd.h>
>   #include <sys/types.h>
>   #include <sys/stat.h>
> +#include <iconv.h>
>   
>   #include "sockaddr.h"
>   #include "nfs_mount.h"
> @@ -173,3 +174,157 @@ int nfs_umount23(const char *devname, char *string)
>   	free(dirname);
>   	return result;
>   }
> +
> +/* Convert UTF-8 string to multibyte string in the current locale */
> +char *utf8str2mbstr(const char *utf8_str)
> +{
> +	iconv_t cd;
> +
> +	cd = iconv_open("", "UTF-8");
> +	if (cd == (iconv_t)-1) {
> +		perror("utf8str2mbstr: iconv_open failed");
> +		return NULL;
> +	}
> +
> +	size_t inbytesleft = strlen(utf8_str);
> +	char *inbuf = (char *)utf8_str;
> +	size_t outbytesleft = inbytesleft*4+1;
> +	char *outbuf = malloc(outbytesleft);
> +	char *outbuf_orig = outbuf;
> +
> +	if (!outbuf) {
> +		perror("utf8str2mbstr: out of memory");
> +		(void)iconv_close(cd);
> +		return NULL;
> +	}
> +
> +	int ret = iconv(cd, &inbuf, &inbytesleft, &outbuf, &outbytesleft);
> +	if (ret == -1) {
> +		perror("utf8str2mbstr: iconv() failed");
> +		free(outbuf_orig);
> +		(void)iconv_close(cd);
> +		return NULL;
> +	}
> +
> +	*outbuf = '\0';
> +
> +	(void)iconv_close(cd);
> +	return outbuf_orig;
> +}
> +
> +/* fixme: We should use |bool|! */
> +int is_spec_nfs_url(const char *spec)
> +{
> +	return (!strncmp(spec, "nfs://", 6));
> +}
> +
> +int mount_parse_nfs_url(const char *spec, parsed_nfs_url *pnu)
> +{
> +	int result = 1;
> +	url_parser_context *uctx = NULL;
> +
> +	(void)memset(pnu, 0, sizeof(parsed_nfs_url));
> +	pnu->mount_params.read_only = TRIS_BOOL_NOT_SET;
> +
> +	uctx = url_parser_create_context(spec, 0);
> +	if (!uctx) {
> +		nfs_error(_("%s: out of memory"),
> +			progname);
> +		result = 1;
> +		goto done;
> +	}
> +
> +	if (url_parser_parse(uctx) < 0) {
> +		nfs_error(_("%s: Error parsing nfs://-URL."),
> +			progname);
> +		result = 1;
> +		goto done;
> +	}
> +	if (uctx->login.username || uctx->login.passwd) {
> +		nfs_error(_("%s: Username/Password are not defined for nfs://-URL."),
> +			progname);
> +		result = 1;
> +		goto done;
> +	}
> +	if (!uctx->path) {
> +		nfs_error(_("%s: Path missing in nfs://-URL."),
> +			progname);
> +		result = 1;
> +		goto done;
> +	}
> +	if (uctx->path[0] != '/') {
> +		nfs_error(_("%s: Relative nfs://-URLs are not supported."),
> +			progname);
> +		result = 1;
> +		goto done;
> +	}
> +
> +	if (uctx->num_parameters > 0) {
> +		int pi;
> +		const char *pname;
> +		const char *pvalue;
> +
> +		/*
> +		 * Values added here based on URL parameters
> +		 * should be added the front of the list of options,
> +		 * so users can override the nfs://-URL given default.
> +		 */
> +		for (pi = 0; pi < uctx->num_parameters ; pi++) {
> +			pname = uctx->parameters[pi].name;
> +			pvalue = uctx->parameters[pi].value;
> +
> +			if (!strcmp(pname, "rw")) {
> +				if ((pvalue == NULL) || (!strcmp(pvalue, "1"))) {
> +					pnu->mount_params.read_only = TRIS_BOOL_FALSE;
> +				}
> +				else if (!strcmp(pvalue, "0")) {
> +					pnu->mount_params.read_only = TRIS_BOOL_TRUE;
> +				}
> +				else {
> +					nfs_error(_("%s: Unsupported nfs://-URL "
> +						"parameter '%s' value '%s'."),
> +						progname, pname, pvalue);
> +					result = 1;
> +					goto done;
> +				}
> +			}
> +			else if (!strcmp(pname, "ro")) {
> +				if ((pvalue == NULL) || (!strcmp(pvalue, "1"))) {
> +					pnu->mount_params.read_only = TRIS_BOOL_TRUE;
> +				}
> +				else if (!strcmp(pvalue, "0")) {
> +					pnu->mount_params.read_only = TRIS_BOOL_FALSE;
> +				}
> +				else {
> +					nfs_error(_("%s: Unsupported nfs://-URL "
> +						"parameter '%s' value '%s'."),
> +						progname, pname, pvalue);
> +					result = 1;
> +					goto done;
> +				}
> +			}
> +			else {
> +				nfs_error(_("%s: Unsupported nfs://-URL "
> +						"parameter '%s'."),
> +					progname, pname);
> +				result = 1;
> +				goto done;
> +			}
> +		}
> +	}
> +
> +	result = 0;
> +done:
> +	if (result != 0) {
> +		url_parser_free_context(uctx);
> +		return 0;
> +	}
> +
> +	pnu->uctx = uctx;
> +	return 1;
> +}
> +
> +void mount_free_parse_nfs_url(parsed_nfs_url *pnu)
> +{
> +	url_parser_free_context(pnu->uctx);
> +}
> diff --git a/utils/mount/utils.h b/utils/mount/utils.h
> index 224918ae..465c0a5e 100644
> --- a/utils/mount/utils.h
> +++ b/utils/mount/utils.h
> @@ -24,13 +24,36 @@
>   #define _NFS_UTILS_MOUNT_UTILS_H
>   
>   #include "parse_opt.h"
> +#include "urlparser1.h"
>   
> +/* Boolean with three states: { not_set, false, true */
> +typedef signed char tristate_bool;
> +#define TRIS_BOOL_NOT_SET (-1)
> +#define TRIS_BOOL_TRUE (1)
> +#define TRIS_BOOL_FALSE (0)
> +
> +#define TRIS_BOOL_GET_VAL(tsb, tsb_default) \
> +	(((tsb)!=TRIS_BOOL_NOT_SET)?(tsb):(tsb_default))
> +
> +typedef struct _parsed_nfs_url {
> +	url_parser_context *uctx;
> +	struct {
> +		tristate_bool read_only;
> +	} mount_params;
> +} parsed_nfs_url;
> +
> +/* Prototypes */
>   int discover_nfs_mount_data_version(int *string_ver);
>   void print_one(char *spec, char *node, char *type, char *opts);
>   void mount_usage(void);
>   void umount_usage(void);
>   int chk_mountpoint(const char *mount_point);
> +char *utf8str2mbstr(const char *utf8_str);
> +int is_spec_nfs_url(const char *spec);
>   
>   int nfs_umount23(const char *devname, char *string);
>   
> +int mount_parse_nfs_url(const char *spec, parsed_nfs_url *pnu);
> +void mount_free_parse_nfs_url(parsed_nfs_url *pnu);
> +
>   #endif	/* !_NFS_UTILS_MOUNT_UTILS_H */

Roland, thank you for working out your email issues. The white space
looks much better; folks should be able to apply this and try it out,
and it is now in a form that can be properly merged.

There is still a lot going on here that needs to be addressed. In an
effort to turn down the heat, my comments in this email focus on only 
one aspect.

The issue about how URL parsing is handled still concerns me. Code
that parses human (or network) input is how the majority of security
bugs are introduced. I believe this mechanism needs to be as hardened
as we can make it.

This is why I strongly recommended the use of a popular and actively
maintained URL parsing library rather than rolling it ourselves. Yes,
as Neil said, it is easy enough to write one. But after it is written,
code has to be properly maintained, and that is where the cost of long
term maintenance actually resides.

Bonus points if the library can be statically linked when mount.nfs
is included in initramfs or other resource-constrained environments.

Note that other components of NFS will need to parse URLs as well.
Thus placing the URL parser in utils/mount rather than using a library
or adding it under support/ seems like a mistake.
Jeff Layton Dec. 12, 2024, 5:05 p.m. UTC | #3
On Tue, 2024-12-10 at 07:28 -0500, Roland Mainz wrote:
> Add support for RFC 2224-style nfs://-URLs as alternative to the
> traditional hostname:/path+-o port=<tcp-port> notation,
> providing standardised, extensible, single-string, crossplatform,
> portable, Character-Encoding independent (e.g. mount point with
> Japanese, Chinese, French etc. characters) and ASCII-compatible
> descriptions of NFSv4 server resources (exports).
> 
> Reviewed-by: Martin Wege <martin.l.wege@gmail.com>
> Signed-off-by: Marvin Wenzel <marvin.wenzel@rovema.de>
> Signed-off-by: Cedric Blancher <cedric.blancher@gmail.com>
> ---
>  utils/mount/Makefile.am  |   3 +-
>  utils/mount/mount.c      |   3 +
>  utils/mount/nfs4mount.c  |  69 +++++++-
>  utils/mount/nfsmount.c   |  93 ++++++++--
>  utils/mount/parse_dev.c  |  67 ++++++--
>  utils/mount/stropts.c    |  96 ++++++++++-
>  utils/mount/urlparser1.c | 358 +++++++++++++++++++++++++++++++++++++++
>  utils/mount/urlparser1.h |  60 +++++++
>  utils/mount/utils.c      | 155 +++++++++++++++++
>  utils/mount/utils.h      |  23 +++
>  10 files changed, 890 insertions(+), 37 deletions(-)
>  create mode 100644 utils/mount/urlparser1.c
>  create mode 100644 utils/mount/urlparser1.h
> 
> diff --git a/utils/mount/Makefile.am b/utils/mount/Makefile.am
> index 83a8ee1c..0e4cab3e 100644
> --- a/utils/mount/Makefile.am
> +++ b/utils/mount/Makefile.am
> @@ -13,7 +13,8 @@ sbin_PROGRAMS	= mount.nfs
>  EXTRA_DIST = nfsmount.conf $(man8_MANS) $(man5_MANS)
>  mount_common = error.c network.c token.c \
>  		    parse_opt.c parse_dev.c \
> -		    nfsmount.c nfs4mount.c stropts.c\
> +		    nfsmount.c nfs4mount.c \
> +		    urlparser1.c urlparser1.h stropts.c \
>  		    mount_constants.h error.h network.h token.h \
>  		    parse_opt.h parse_dev.h \
>  		    nfs4_mount.h stropts.h version.h \
> diff --git a/utils/mount/mount.c b/utils/mount/mount.c
> index b98f9e00..2ce6209d 100644
> --- a/utils/mount/mount.c
> +++ b/utils/mount/mount.c
> @@ -29,6 +29,7 @@
>  #include <string.h>
>  #include <errno.h>
>  #include <fcntl.h>
> +#include <locale.h>
>  #include <sys/mount.h>
>  #include <getopt.h>
>  #include <mntent.h>
> @@ -386,6 +387,8 @@ int main(int argc, char *argv[])
>  	char *extra_opts = NULL, *mount_opts = NULL;
>  	uid_t uid = getuid();
>  
> +	(void)setlocale(LC_ALL, "");
> +
>  	progname = basename(argv[0]);
>  
>  	nfs_mount_data_version = discover_nfs_mount_data_version(&string);
> diff --git a/utils/mount/nfs4mount.c b/utils/mount/nfs4mount.c
> index 0fe142a7..8e4fbf30 100644
> --- a/utils/mount/nfs4mount.c
> +++ b/utils/mount/nfs4mount.c
> @@ -50,8 +50,10 @@
>  #include "mount_constants.h"
>  #include "nfs4_mount.h"
>  #include "nfs_mount.h"
> +#include "urlparser1.h"
>  #include "error.h"
>  #include "network.h"
> +#include "utils.h"
>  
>  #if defined(VAR_LOCK_DIR)
>  #define DEFAULT_DIR VAR_LOCK_DIR
> @@ -182,7 +184,7 @@ int nfs4mount(const char *spec, const char *node, int flags,
>  	int num_flavour = 0;
>  	int ip_addr_in_opts = 0;
>  
> -	char *hostname, *dirname, *old_opts;
> +	char *hostname, *dirname, *mb_dirname = NULL, *old_opts;
>  	char new_opts[1024];
>  	char *opt, *opteq;
>  	char *s;
> @@ -192,15 +194,66 @@ int nfs4mount(const char *spec, const char *node, int flags,
>  	int retry;
>  	int retval = EX_FAIL;
>  	time_t timeout, t;
> +	int nfs_port = NFS_PORT;
> +	parsed_nfs_url pnu;
> +
> +	(void)memset(&pnu, 0, sizeof(parsed_nfs_url));
>  
>  	if (strlen(spec) >= sizeof(hostdir)) {
>  		nfs_error(_("%s: excessively long host:dir argument\n"),
>  				progname);
>  		goto fail;
>  	}
> -	strcpy(hostdir, spec);
> -	if (parse_devname(hostdir, &hostname, &dirname))
> -		goto fail;
> +
> +	/*
> +	 * Support nfs://-URLS per RFC 2224 ("NFS URL
> +	 * SCHEME", see https://www.rfc-editor.org/rfc/rfc2224.html),
> +	 * including custom port (nfs://hostname@port/path/...)
> +	 * and URL parameter (e.g. nfs://.../?param1=val1&param2=val2

This part seems ill-advised:

While 2224 does define an nfs:// URL, I don't recall seeing anything
about parameters being passed in the above fashion. Where are these
parameters defined? Are they just mount options?

If so, while we do strive to mirror option names with (e.g.) BSD or
Solaris, it's not always possible and different operating systems have
mount options that are not recognized by others.

Is there a plan to deal with that? If not, then maybe it'd be best to
leave mount option handling out of this for now.

> +	 * support
> +	 */
> +	if (is_spec_nfs_url(spec)) {
> +		if (!mount_parse_nfs_url(spec, &pnu)) {
> +			goto fail;
> +		}
> +
> +		/*
> +		 * |pnu.uctx->path| is in UTF-8, but we need the data
> +		 * in the current local locale's encoding, as mount(2)
> +		 * does not have something like a |MS_UTF8_SPEC| flag
> +		 * to indicate that the input path is in UTF-8,
> +		 * independently of the current locale
> +		 */
> +		hostname = pnu.uctx->hostport.hostname;
> +		dirname = mb_dirname = utf8str2mbstr(pnu.uctx->path);
> +
> +		(void)snprintf(hostdir, sizeof(hostdir), "%s:/%s",
> +			hostname, dirname);
> +		spec = hostdir;
> +
> +		if (pnu.uctx->hostport.port != -1) {
> +			nfs_port = pnu.uctx->hostport.port;
> +		}
> +
> +		/*
> +		 * Values added here based on URL parameters
> +		 * should be added the front of the list of options,
> +		 * so users can override the nfs://-URL given default.
> +		 *
> +		 * FIXME: We do not do that here for |MS_RDONLY|!
> +		 */
> +		if (pnu.mount_params.read_only != TRIS_BOOL_NOT_SET) {
> +			if (pnu.mount_params.read_only)
> +				flags |= MS_RDONLY;
> +			else
> +				flags &= ~MS_RDONLY;
> +		}
> +        } else {
> +		(void)strcpy(hostdir, spec);
> +
> +		if (parse_devname(hostdir, &hostname, &dirname))
> +			goto fail;
> +	}
>  
>  	if (fill_ipv4_sockaddr(hostname, &server_addr))
>  		goto fail;
> @@ -247,7 +300,7 @@ int nfs4mount(const char *spec, const char *node, int flags,
>  	/*
>  	 * NFSv4 specifies that the default port should be 2049
>  	 */
> -	server_addr.sin_port = htons(NFS_PORT);
> +	server_addr.sin_port = htons(nfs_port);
>  
>  	/* parse options */
>  
> @@ -474,8 +527,14 @@ int nfs4mount(const char *spec, const char *node, int flags,
>  		}
>  	}
>  
> +	mount_free_parse_nfs_url(&pnu);
> +	free(mb_dirname);
> +
>  	return EX_SUCCESS;
>  
>  fail:
> +	mount_free_parse_nfs_url(&pnu);
> +	free(mb_dirname);
> +
>  	return retval;
>  }
> diff --git a/utils/mount/nfsmount.c b/utils/mount/nfsmount.c
> index a1c92fe8..e61d718a 100644
> --- a/utils/mount/nfsmount.c
> +++ b/utils/mount/nfsmount.c
> @@ -63,11 +63,13 @@
>  #include "xcommon.h"
>  #include "mount.h"
>  #include "nfs_mount.h"
> +#include "urlparser1.h"
>  #include "mount_constants.h"
>  #include "nls.h"
>  #include "error.h"
>  #include "network.h"
>  #include "version.h"
> +#include "utils.h"
>  
>  #ifdef HAVE_RPCSVC_NFS_PROT_H
>  #include <rpcsvc/nfs_prot.h>
> @@ -493,7 +495,7 @@ nfsmount(const char *spec, const char *node, int flags,
>  	 char **extra_opts, int fake, int running_bg)
>  {
>  	char hostdir[1024];
> -	char *hostname, *dirname, *old_opts, *mounthost = NULL;
> +	char *hostname, *dirname, *mb_dirname = NULL, *old_opts, *mounthost = NULL;
>  	char new_opts[1024], cbuf[1024];
>  	static struct nfs_mount_data data;
>  	int val;
> @@ -521,29 +523,79 @@ nfsmount(const char *spec, const char *node, int flags,
>  	time_t t;
>  	time_t prevt;
>  	time_t timeout;
> +	int nfsurl_port = -1;
> +	parsed_nfs_url pnu;
> +
> +	(void)memset(&pnu, 0, sizeof(parsed_nfs_url));
>  
>  	if (strlen(spec) >= sizeof(hostdir)) {
>  		nfs_error(_("%s: excessively long host:dir argument"),
>  				progname);
>  		goto fail;
>  	}
> -	strcpy(hostdir, spec);
> -	if ((s = strchr(hostdir, ':'))) {
> -		hostname = hostdir;
> -		dirname = s + 1;
> -		*s = '\0';
> -		/* Ignore all but first hostname in replicated mounts
> -		   until they can be fully supported. (mack@sgi.com) */
> -		if ((s = strchr(hostdir, ','))) {
> +
> +	/*
> +	 * Support nfs://-URLS per RFC 2224 ("NFS URL
> +	 * SCHEME", see https://www.rfc-editor.org/rfc/rfc2224.html),
> +	 * including custom port (nfs://hostname@port/path/...)
> +	 * and URL parameter (e.g. nfs://.../?param1=val1&param2=val2
> +	 * support
> +	 */
> +	if (is_spec_nfs_url(spec)) {
> +		if (!mount_parse_nfs_url(spec, &pnu)) {
> +			goto fail;
> +		}
> +
> +		/*
> +		 * |pnu.uctx->path| is in UTF-8, but we need the data
> +		 * in the current local locale's encoding, as mount(2)
> +		 * does not have something like a |MS_UTF8_SPEC| flag
> +		 * to indicate that the input path is in UTF-8,
> +		 * independently of the current locale
> +		 */
> +		hostname = pnu.uctx->hostport.hostname;
> +		dirname = mb_dirname = utf8str2mbstr(pnu.uctx->path);
> +
> +		(void)snprintf(hostdir, sizeof(hostdir), "%s:/%s",
> +			hostname, dirname);
> +		spec = hostdir;
> +
> +		if (pnu.uctx->hostport.port != -1) {
> +			nfsurl_port = pnu.uctx->hostport.port;
> +		}
> +
> +		/*
> +		 * Values added here based on URL parameters
> +		 * should be added the front of the list of options,
> +		 * so users can override the nfs://-URL given default.
> +		 *
> +		 * FIXME: We do not do that here for |MS_RDONLY|!
> +		 */
> +		if (pnu.mount_params.read_only != TRIS_BOOL_NOT_SET) {
> +			if (pnu.mount_params.read_only)
> +				flags |= MS_RDONLY;
> +			else
> +				flags &= ~MS_RDONLY;
> +		}
> +        } else {
> +		(void)strcpy(hostdir, spec);
> +		if ((s = strchr(hostdir, ':'))) {
> +			hostname = hostdir;
> +			dirname = s + 1;
>  			*s = '\0';
> -			nfs_error(_("%s: warning: "
> -				  "multiple hostnames not supported"),
> +			/* Ignore all but first hostname in replicated mounts
> +			   until they can be fully supported. (mack@sgi.com) */
> +			if ((s = strchr(hostdir, ','))) {
> +				*s = '\0';
> +				nfs_error(_("%s: warning: "
> +					"multiple hostnames not supported"),
>  					progname);
> -		}
> -	} else {
> -		nfs_error(_("%s: directory to mount not in host:dir format"),
> +			}
> +		} else {
> +			nfs_error(_("%s: directory to mount not in host:dir format"),
>  				progname);
> -		goto fail;
> +			goto fail;
> +		}
>  	}
>  
>  	if (!nfs_gethostbyname(hostname, nfs_saddr))
> @@ -579,6 +631,14 @@ nfsmount(const char *spec, const char *node, int flags,
>  	memset(nfs_pmap, 0, sizeof(*nfs_pmap));
>  	nfs_pmap->pm_prog = NFS_PROGRAM;
>  
> +	if (nfsurl_port != -1) {
> +		/*
> +		 * Set custom TCP port defined by a nfs://-URL here,
> +		 * so $ mount -o port ... # can be used to override
> +		 */
> +		nfs_pmap->pm_port = nfsurl_port;
> +	}
> +
>  	/* parse options */
>  	new_opts[0] = 0;
>  	if (!parse_options(old_opts, &data, &bg, &retry, &mnt_server, &nfs_server,
> @@ -863,10 +923,13 @@ noauth_flavors:
>  		}
>  	}
>  
> +	mount_free_parse_nfs_url(&pnu);
> +
>  	return EX_SUCCESS;
>  
>  	/* abort */
>   fail:
> +	mount_free_parse_nfs_url(&pnu);
>  	if (fsock != -1)
>  		close(fsock);
>  	return retval;
> diff --git a/utils/mount/parse_dev.c b/utils/mount/parse_dev.c
> index 2ade5d5d..d9f8cf59 100644
> --- a/utils/mount/parse_dev.c
> +++ b/utils/mount/parse_dev.c
> @@ -27,6 +27,8 @@
>  #include "xcommon.h"
>  #include "nls.h"
>  #include "parse_dev.h"
> +#include "urlparser1.h"
> +#include "utils.h"
>  
>  #ifndef NFS_MAXHOSTNAME
>  #define NFS_MAXHOSTNAME		(255)
> @@ -179,17 +181,62 @@ static int nfs_parse_square_bracket(const char *dev,
>  }
>  
>  /*
> - * RFC 2224 says an NFS client must grok "public file handles" to
> - * support NFS URLs.  Linux doesn't do that yet.  Print a somewhat
> - * helpful error message in this case instead of pressing forward
> - * with the mount request and failing with a cryptic error message
> - * later.
> + * Support nfs://-URLS per RFC 2224 ("NFS URL
> + * SCHEME", see https://www.rfc-editor.org/rfc/rfc2224.html),
> + * including port support (nfs://hostname@port/path/...)
>   */
> -static int nfs_parse_nfs_url(__attribute__((unused)) const char *dev,
> -			     __attribute__((unused)) char **hostname,
> -			     __attribute__((unused)) char **pathname)
> +static int nfs_parse_nfs_url(const char *dev,
> +			     char **out_hostname,
> +			     char **out_pathname)
>  {
> -	nfs_error(_("%s: NFS URLs are not supported"), progname);
> +	parsed_nfs_url pnu;
> +
> +	if (out_hostname)
> +		*out_hostname = NULL;
> +	if (out_pathname)
> +		*out_pathname = NULL;
> +
> +	/*
> +	 * Support nfs://-URLS per RFC 2224 ("NFS URL
> +	 * SCHEME", see https://www.rfc-editor.org/rfc/rfc2224.html),
> +	 * including custom port (nfs://hostname@port/path/...)
> +	 * and URL parameter (e.g. nfs://.../?param1=val1&param2=val2
> +	 * support
> +	 */
> +	if (!mount_parse_nfs_url(dev, &pnu)) {
> +		goto fail;
> +	}
> +
> +	if (pnu.uctx->hostport.port != -1) {
> +		/* NOP here, unless we switch from hostname to hostport */
> +	}
> +
> +	if (out_hostname)
> +		*out_hostname = strdup(pnu.uctx->hostport.hostname);
> +	if (out_pathname)
> +		*out_pathname = utf8str2mbstr(pnu.uctx->path);
> +
> +	if (((out_hostname)?(*out_hostname == NULL):0) ||
> +		((out_pathname)?(*out_pathname == NULL):0)) {
> +		nfs_error(_("%s: out of memory"),
> +			progname);
> +		goto fail;
> +	}
> +
> +	mount_free_parse_nfs_url(&pnu);
> +
> +	return 1;
> +
> +fail:
> +	mount_free_parse_nfs_url(&pnu);
> +	if (out_hostname) {
> +		free(*out_hostname);
> +		*out_hostname = NULL;
> +	}
> +	if (out_pathname) {
> +		free(*out_pathname);
> +		*out_pathname = NULL;
> +	}
>  	return 0;
>  }
>  
> @@ -223,7 +270,7 @@ int nfs_parse_devname(const char *devname,
>  		return nfs_pdn_nomem_err();
>  	if (*dev == '[')
>  		result = nfs_parse_square_bracket(dev, hostname, pathname);
> -	else if (strncmp(dev, "nfs://", 6) == 0)
> +	else if (is_spec_nfs_url(dev))
>  		result = nfs_parse_nfs_url(dev, hostname, pathname);
>  	else
>  		result = nfs_parse_simple_hostname(dev, hostname, pathname);
> diff --git a/utils/mount/stropts.c b/utils/mount/stropts.c
> index 23f0a8c0..ad92ab78 100644
> --- a/utils/mount/stropts.c
> +++ b/utils/mount/stropts.c
> @@ -42,6 +42,7 @@
>  #include "nls.h"
>  #include "nfsrpc.h"
>  #include "mount_constants.h"
> +#include "urlparser1.h"
>  #include "stropts.h"
>  #include "error.h"
>  #include "network.h"
> @@ -50,6 +51,7 @@
>  #include "parse_dev.h"
>  #include "conffile.h"
>  #include "misc.h"
> +#include "utils.h"
>  
>  #ifndef NFS_PROGRAM
>  #define NFS_PROGRAM	(100003)
> @@ -643,24 +645,106 @@ static int nfs_sys_mount(struct nfsmount_info *mi, struct mount_options *opts)
>  {
>  	char *options = NULL;
>  	int result;
> +	int nfs_port = 2049;
>  
>  	if (mi->fake)
>  		return 1;
>  
> -	if (po_join(opts, &options) == PO_FAILED) {
> -		errno = EIO;
> -		return 0;
> -	}
> +	/*
> +	 * Support nfs://-URLS per RFC 2224 ("NFS URL
> +	 * SCHEME", see https://www.rfc-editor.org/rfc/rfc2224.html),
> +	 * including custom port (nfs://hostname@port/path/...)
> +	 * and URL parameter (e.g. nfs://.../?param1=val1&param2=val2
> +	 * support
> +	 */
> +	if (is_spec_nfs_url(mi->spec)) {
> +		parsed_nfs_url pnu;
> +		char *mb_path;
> +		char mount_source[1024];
> +
> +		if (!mount_parse_nfs_url(mi->spec, &pnu)) {
> +			result = 1;
> +			errno = EINVAL;
> +			goto done;
> +		}
> +
> +		/*
> +		 * |pnu.uctx->path| is in UTF-8, but we need the data
> +		 * in the current local locale's encoding, as mount(2)
> +		 * does not have something like a |MS_UTF8_SPEC| flag
> +		 * to indicate that the input path is in UTF-8,
> +		 * independently of the current locale
> +		 */
> +		mb_path = utf8str2mbstr(pnu.uctx->path);
> +		if (!mb_path) {
> +			nfs_error(_("%s: Could not convert path to local encoding."),
> +				progname);
> +			mount_free_parse_nfs_url(&pnu);
> +			result = 1;
> +			errno = EINVAL;
> +			goto done;
> +		}
> +
> +		(void)snprintf(mount_source, sizeof(mount_source),
> +			"%s:/%s",
> +			pnu.uctx->hostport.hostname,
> +			mb_path);
> +		free(mb_path);
> +
> +		if (pnu.uctx->hostport.port != -1) {
> +			nfs_port = pnu.uctx->hostport.port;
> +		}
>  
> -	result = mount(mi->spec, mi->node, mi->type,
> +		/*
> +		 * Insert "port=" option with the value from the nfs://
> +		 * URL at the beginning of the list of options, so
> +		 * users can override it with $ mount.nfs4 -o port= #,
> +		 * e.g.
> +		 * $ mount.nfs4 -o port=1234 nfs://10.49.202.230:400//bigdisk /mnt4 #
> +		 * should use port 1234, and not port 400 as specified
> +		 * in the URL.
> +		 */
> +		char portoptbuf[5+32+1];
> +		(void)snprintf(portoptbuf, sizeof(portoptbuf), "port=%d", nfs_port);
> +		(void)po_insert(opts, portoptbuf);
> +
> +		if (pnu.mount_params.read_only != TRIS_BOOL_NOT_SET) {
> +			if (pnu.mount_params.read_only)
> +				mi->flags |= MS_RDONLY;
> +			else
> +				mi->flags &= ~MS_RDONLY;
> +		}
> +
> +		mount_free_parse_nfs_url(&pnu);
> +
> +		if (po_join(opts, &options) == PO_FAILED) {
> +			errno = EIO;
> +			result = 1;
> +			goto done;
> +		}
> +
> +		result = mount(mount_source, mi->node, mi->type,
> +			mi->flags & ~(MS_USER|MS_USERS), options);
> +		free(options);
> +	} else {
> +		if (po_join(opts, &options) == PO_FAILED) {
> +			errno = EIO;
> +			result = 1;
> +			goto done;
> +		}
> +
> +		result = mount(mi->spec, mi->node, mi->type,
>  			mi->flags & ~(MS_USER|MS_USERS), options);
> -	free(options);
> +		free(options);
> +	}
>  
>  	if (verbose && result) {
>  		int save = errno;
>  		nfs_error(_("%s: mount(2): %s"), progname, strerror(save));
>  		errno = save;
>  	}
> +
> +done:
>  	return !result;
>  }
>  
> diff --git a/utils/mount/urlparser1.c b/utils/mount/urlparser1.c
> new file mode 100644
> index 00000000..d4c6f339
> --- /dev/null
> +++ b/utils/mount/urlparser1.c
> @@ -0,0 +1,358 @@
> +/*
> + * MIT License
> + *
> + * Copyright (c) 2024 Roland Mainz <roland.mainz@nrubsig.org>
> + *
> + * Permission is hereby granted, free of charge, to any person obtaining a copy
> + * of this software and associated documentation files (the "Software"), to deal
> + * in the Software without restriction, including without limitation the rights
> + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> + * copies of the Software, and to permit persons to whom the Software is
> + * furnished to do so, subject to the following conditions:
> + *
> + * The above copyright notice and this permission notice shall be included in all
> + * copies or substantial portions of the Software.
> + *
> + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
> + * SOFTWARE.
> + */
> +
> +/* urlparser1.c - simple URL parser */
> +
> +#include <stdlib.h>
> +#include <stdbool.h>
> +#include <string.h>
> +#include <ctype.h>
> +#include <stdio.h>
> +
> +#ifdef DBG_USE_WIDECHAR
> +#include <wchar.h>
> +#include <locale.h>
> +#include <io.h>
> +#include <fcntl.h>
> +#endif /* DBG_USE_WIDECHAR */
> +
> +#include "urlparser1.h"
> +
> +typedef struct _url_parser_context_private {
> +	url_parser_context c;
> +
> +	/* Private data */
> +	char *parameter_string_buff;
> +} url_parser_context_private;
> +
> +#define MAX_URL_PARAMETERS 256
> +
> +/*
> + * Original extended regular expression:
> + *
> + * "^"
> + * "(.+?)"				// scheme
> + * "://"				// '://'
> + * "("					// login
> + *	"(?:"
> + *	"(.+?)"				// user (optional)
> + *		"(?::(.+))?"		// password (optional)
> + *		"@"
> + *	")?"
> + *	"("				// hostport
> + *		"(.+?)"			// host
> + *		"(?::([[:digit:]]+))?"	// port (optional)
> + *	")"
> + * ")"
> + * "(?:/(.*?))?"			// path (optional)
> + * "(?:\?(.*?))?"			// URL parameters (optional)
> + * "$"
> + */
> +
> +#define DBGNULLSTR(s) (((s)!=NULL)?(s):"<NULL>")
> +#if 0 || defined(TEST_URLPARSER)
> +#define D(x) x
> +#else
> +#define D(x)
> +#endif
> +
> +#ifdef DBG_USE_WIDECHAR
> +/*
> + * Use wide-char APIs on WIN32, otherwise we cannot output
> + * Japanese/Chinese/etc correctly
> + */
> +#define DBG_PUTS(str, fp)		fputws(L"" str, (fp))
> +#define DBG_PUTC(c, fp)			fputwc(btowc(c), (fp))
> +#define DBG_PRINTF(fp, fmt, ...)	fwprintf((fp), L"" fmt, __VA_ARGS__)
> +#else
> +#define DBG_PUTS(str, fp)		fputs((str), (fp))
> +#define DBG_PUTC(c, fp)			fputc((c), (fp))
> +#define DBG_PRINTF(fp, fmt, ...)	fprintf((fp), fmt, __VA_ARGS__)
> +#endif /* DBG_USE_WIDECHAR */
> +
> +static
> +void urldecodestr(char *outbuff, const char *buffer, size_t len)
> +{
> +	size_t i, j;
> +
> +	for (i = j = 0 ; i < len ; ) {
> +		switch (buffer[i]) {
> +			case '%':
> +				if ((i + 2) < len) {
> +					if (isxdigit((int)buffer[i+1]) && isxdigit((int)buffer[i+2])) {
> +						const char hexstr[3] = {
> +							buffer[i+1],
> +							buffer[i+2],
> +							'\0'
> +						};
> +						outbuff[j++] = (unsigned char)strtol(hexstr, NULL, 16);
> +						i += 3;
> +					} else {
> +						/* invalid hex digit */
> +						outbuff[j++] = buffer[i];
> +						i++;
> +					}
> +				} else {
> +					/* incomplete hex digit */
> +					outbuff[j++] = buffer[i];
> +					i++;
> +				}
> +				break;
> +			case '+':
> +				outbuff[j++] = ' ';
> +				i++;
> +				break;
> +			default:
> +				outbuff[j++] = buffer[i++];
> +				break;
> +		}
> +	}
> +
> +	outbuff[j] = '\0';
> +}
> +
> +url_parser_context *url_parser_create_context(const char *in_url, unsigned int flags)
> +{
> +	url_parser_context_private *uctx;
> +	char *s;
> +	size_t in_url_len;
> +	size_t context_len;
> +
> +	/* |flags| is for future extensions */
> +	(void)flags;
> +
> +	if (!in_url)
> +		return NULL;
> +
> +	in_url_len = strlen(in_url);
> +
> +	context_len = sizeof(url_parser_context_private) +
> +		((in_url_len+1)*6) +
> +		(sizeof(url_parser_name_value)*MAX_URL_PARAMETERS)+sizeof(void*);
> +	uctx = malloc(context_len);
> +	if (!uctx)
> +		return NULL;
> +
> +	s = (void *)(uctx+1);
> +	uctx->c.in_url = s;		s+= in_url_len+1;
> +	(void)strcpy(uctx->c.in_url, in_url);
> +	uctx->c.scheme = s;		s+= in_url_len+1;
> +	uctx->c.login.username = s;	s+= in_url_len+1;
> +	uctx->c.hostport.hostname = s;	s+= in_url_len+1;
> +	uctx->c.path = s;		s+= in_url_len+1;
> +	uctx->c.hostport.port = -1;
> +	uctx->c.num_parameters = -1;
> +	uctx->c.parameters = (void *)s;		s+= (sizeof(url_parser_name_value)*MAX_URL_PARAMETERS)+sizeof(void*);
> +	uctx->parameter_string_buff = s;	s+= in_url_len+1;
> +
> +	return &uctx->c;
> +}
> +
> +int url_parser_parse(url_parser_context *ctx)
> +{
> +	url_parser_context_private *uctx = (url_parser_context_private *)ctx;
> +
> +	D((void)DBG_PRINTF(stderr, "## parser in_url='%s'\n", uctx->c.in_url));
> +
> +	char *s;
> +	const char *urlstr = uctx->c.in_url;
> +	size_t slen;
> +
> +	s = strstr(urlstr, "://");
> +	if (!s) {
> +		D((void)DBG_PUTS("url_parser: Not an URL\n", stderr));
> +		return -1;
> +	}
> +
> +	slen = s-urlstr;
> +	(void)memcpy(uctx->c.scheme, urlstr, slen);
> +	uctx->c.scheme[slen] = '\0';
> +	urlstr += slen + 3;
> +
> +	D((void)DBG_PRINTF(stdout, "scheme='%s', rest='%s'\n", uctx->c.scheme, urlstr));
> +
> +	s = strstr(urlstr, "@");
> +	if (s) {
> +		/* URL has user/password */
> +		slen = s-urlstr;
> +		urldecodestr(uctx->c.login.username, urlstr, slen);
> +		urlstr += slen + 1;
> +
> +		s = strstr(uctx->c.login.username, ":");
> +		if (s) {
> +			/* found passwd */
> +			uctx->c.login.passwd = s+1;
> +			*s = '\0';
> +		}
> +		else {
> +			uctx->c.login.passwd = NULL;
> +		}
> +
> +		/* catch password-only URLs */
> +		if (uctx->c.login.username[0] == '\0')
> +			uctx->c.login.username = NULL;
> +	}
> +	else {
> +		uctx->c.login.username = NULL;
> +		uctx->c.login.passwd = NULL;
> +	}
> +
> +	D((void)DBG_PRINTF(stdout, "login='%s', passwd='%s', rest='%s'\n",
> +		DBGNULLSTR(uctx->c.login.username),
> +		DBGNULLSTR(uctx->c.login.passwd),
> +		DBGNULLSTR(urlstr)));
> +
> +	char *raw_parameters;
> +
> +	uctx->c.num_parameters = 0;
> +	raw_parameters = strstr(urlstr, "?");
> +	/* Do we have a non-empty parameter string ? */
> +	if (raw_parameters && (raw_parameters[1] != '\0')) {
> +		*raw_parameters++ = '\0';
> +		D((void)DBG_PRINTF(stdout, "raw parameters = '%s'\n", raw_parameters));
> +
> +		char *ps = raw_parameters;
> +		char *pv; /* parameter value */
> +		char *na; /* next '&' */
> +		char *pb = uctx->parameter_string_buff;
> +		char *pname;
> +		char *pvalue;
> +		ssize_t pi;
> +
> +		for (pi = 0; pi < MAX_URL_PARAMETERS ; pi++) {
> +			pname = ps;
> +
> +			/*
> +			 * Handle parameters without value,
> +			 * e.g. "path?name1&name2=value2"
> +			 */
> +			na = strstr(ps, "&");
> +			pv = strstr(ps, "=");
> +			if (pv && (na?(na > pv):true)) {
> +				*pv++ = '\0';
> +				pvalue = pv;
> +				ps = pv;
> +			}
> +			else {
> +				pvalue = NULL;
> +			}
> +
> +			if (na) {
> +				*na++ = '\0';
> +			}
> +
> +			/* URLDecode parameter name */
> +			urldecodestr(pb, pname, strlen(pname));
> +			uctx->c.parameters[pi].name = pb;
> +			pb += strlen(uctx->c.parameters[pi].name)+1;
> +
> +			/* URLDecode parameter value */
> +			if (pvalue) {
> +				urldecodestr(pb, pvalue, strlen(pvalue));
> +				uctx->c.parameters[pi].value = pb;
> +				pb += strlen(uctx->c.parameters[pi].value)+1;
> +			}
> +			else {
> +				uctx->c.parameters[pi].value = NULL;
> +			}
> +
> +			/* Next '&' ? */
> +			if (!na)
> +				break;
> +
> +			ps = na;
> +		}
> +
> +		uctx->c.num_parameters = pi+1;
> +	}
> +
> +	s = strstr(urlstr, "/");
> +	if (s) {
> +		/* URL has hostport */
> +		slen = s-urlstr;
> +		urldecodestr(uctx->c.hostport.hostname, urlstr, slen);
> +		urlstr += slen + 1;
> +
> +		/*
> +		 * check for addresses within '[' and ']', like
> +		 * IPv6 addresses
> +		 */
> +		s = uctx->c.hostport.hostname;
> +		if (s[0] == '[')
> +			s = strstr(s, "]");
> +
> +		if (s == NULL) {
> +			D((void)DBG_PUTS("url_parser: Unmatched '[' in hostname\n", stderr));
> +			return -1;
> +		}
> +
> +		s = strstr(s, ":");
> +		if (s) {
> +			/* found port number */
> +			uctx->c.hostport.port = atoi(s+1);
> +			*s = '\0';
> +		}
> +	}
> +	else {
> +		(void)strcpy(uctx->c.hostport.hostname, urlstr);
> +		uctx->c.path = NULL;
> +		urlstr = NULL;
> +	}
> +
> +	D(
> +		(void)DBG_PRINTF(stdout,
> +			"hostport='%s', port=%d, rest='%s', num_parameters=%d\n",
> +			DBGNULLSTR(uctx->c.hostport.hostname),
> +			uctx->c.hostport.port,
> +			DBGNULLSTR(urlstr),
> +			(int)uctx->c.num_parameters);
> +	);
> +
> +
> +	D(
> +		ssize_t dpi;
> +		for (dpi = 0 ; dpi < uctx->c.num_parameters ; dpi++) {
> +			(void)DBG_PRINTF(stdout,
> +				"param[%d]: name='%s'/value='%s'\n",
> +				(int)dpi,
> +				uctx->c.parameters[dpi].name,
> +				DBGNULLSTR(uctx->c.parameters[dpi].value));
> +		}
> +	);
> +
> +	if (!urlstr) {
> +		goto done;
> +	}
> +
> +	urldecodestr(uctx->c.path, urlstr, strlen(urlstr));
> +	D((void)DBG_PRINTF(stdout, "path='%s'\n", uctx->c.path));
> +
> +done:
> +	return 0;
> +}
> +
> +void url_parser_free_context(url_parser_context *c)
> +{
> +	free(c);
> +}
> diff --git a/utils/mount/urlparser1.h b/utils/mount/urlparser1.h
> new file mode 100644
> index 00000000..515eea9d
> --- /dev/null
> +++ b/utils/mount/urlparser1.h
> @@ -0,0 +1,60 @@
> +/*
> + * MIT License
> + *
> + * Copyright (c) 2024 Roland Mainz <roland.mainz@nrubsig.org>
> + *
> + * Permission is hereby granted, free of charge, to any person obtaining a copy
> + * of this software and associated documentation files (the "Software"), to deal
> + * in the Software without restriction, including without limitation the rights
> + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> + * copies of the Software, and to permit persons to whom the Software is
> + * furnished to do so, subject to the following conditions:
> + *
> + * The above copyright notice and this permission notice shall be included in all
> + * copies or substantial portions of the Software.
> + *
> + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
> + * SOFTWARE.
> + */
> +
> +/* urlparser1.h - header for simple URL parser */
> +
> +#ifndef __URLPARSER1_H__
> +#define __URLPARSER1_H__ 1
> +
> +#include <stdlib.h>
> +
> +typedef struct _url_parser_name_value {
> +	char *name;
> +	char *value;
> +} url_parser_name_value;
> +
> +typedef struct _url_parser_context {
> +	char *in_url;
> +
> +	char *scheme;
> +	struct {
> +		char *username;
> +		char *passwd;
> +	} login;
> +	struct {
> +		char *hostname;
> +		signed int port;
> +	} hostport;
> +	char *path;
> +
> +	ssize_t num_parameters;
> +	url_parser_name_value *parameters;
> +} url_parser_context;
> +
> +/* Prototypes */
> +url_parser_context *url_parser_create_context(const char *in_url, unsigned int flags);
> +int url_parser_parse(url_parser_context *uctx);
> +void url_parser_free_context(url_parser_context *c);
> +
> +#endif /* !__URLPARSER1_H__ */
> diff --git a/utils/mount/utils.c b/utils/mount/utils.c
> index b7562a47..3d55e997 100644
> --- a/utils/mount/utils.c
> +++ b/utils/mount/utils.c
> @@ -28,6 +28,7 @@
>  #include <unistd.h>
>  #include <sys/types.h>
>  #include <sys/stat.h>
> +#include <iconv.h>
>  
>  #include "sockaddr.h"
>  #include "nfs_mount.h"
> @@ -173,3 +174,157 @@ int nfs_umount23(const char *devname, char *string)
>  	free(dirname);
>  	return result;
>  }
> +
> +/* Convert UTF-8 string to multibyte string in the current locale */
> +char *utf8str2mbstr(const char *utf8_str)
> +{
> +	iconv_t cd;
> +
> +	cd = iconv_open("", "UTF-8");
> +	if (cd == (iconv_t)-1) {
> +		perror("utf8str2mbstr: iconv_open failed");
> +		return NULL;
> +	}
> +
> +	size_t inbytesleft = strlen(utf8_str);
> +	char *inbuf = (char *)utf8_str;
> +	size_t outbytesleft = inbytesleft*4+1;
> +	char *outbuf = malloc(outbytesleft);
> +	char *outbuf_orig = outbuf;
> +
> +	if (!outbuf) {
> +		perror("utf8str2mbstr: out of memory");
> +		(void)iconv_close(cd);
> +		return NULL;
> +	}
> +
> +	int ret = iconv(cd, &inbuf, &inbytesleft, &outbuf, &outbytesleft);
> +	if (ret == -1) {
> +		perror("utf8str2mbstr: iconv() failed");
> +		free(outbuf_orig);
> +		(void)iconv_close(cd);
> +		return NULL;
> +	}
> +
> +	*outbuf = '\0';
> +
> +	(void)iconv_close(cd);
> +	return outbuf_orig;
> +}
> +
> +/* fixme: We should use |bool|! */
> +int is_spec_nfs_url(const char *spec)
> +{
> +	return (!strncmp(spec, "nfs://", 6));
> +}
> +
> +int mount_parse_nfs_url(const char *spec, parsed_nfs_url *pnu)
> +{
> +	int result = 1;
> +	url_parser_context *uctx = NULL;
> +
> +	(void)memset(pnu, 0, sizeof(parsed_nfs_url));
> +	pnu->mount_params.read_only = TRIS_BOOL_NOT_SET;
> +
> +	uctx = url_parser_create_context(spec, 0);
> +	if (!uctx) {
> +		nfs_error(_("%s: out of memory"),
> +			progname);
> +		result = 1;
> +		goto done;
> +	}
> +
> +	if (url_parser_parse(uctx) < 0) {
> +		nfs_error(_("%s: Error parsing nfs://-URL."),
> +			progname);
> +		result = 1;
> +		goto done;
> +	}
> +	if (uctx->login.username || uctx->login.passwd) {
> +		nfs_error(_("%s: Username/Password are not defined for nfs://-URL."),
> +			progname);
> +		result = 1;
> +		goto done;
> +	}
> +	if (!uctx->path) {
> +		nfs_error(_("%s: Path missing in nfs://-URL."),
> +			progname);
> +		result = 1;
> +		goto done;
> +	}
> +	if (uctx->path[0] != '/') {
> +		nfs_error(_("%s: Relative nfs://-URLs are not supported."),
> +			progname);
> +		result = 1;
> +		goto done;
> +	}
> +
> +	if (uctx->num_parameters > 0) {
> +		int pi;
> +		const char *pname;
> +		const char *pvalue;
> +
> +		/*
> +		 * Values added here based on URL parameters
> +		 * should be added the front of the list of options,
> +		 * so users can override the nfs://-URL given default.
> +		 */
> +		for (pi = 0; pi < uctx->num_parameters ; pi++) {
> +			pname = uctx->parameters[pi].name;
> +			pvalue = uctx->parameters[pi].value;
> +
> +			if (!strcmp(pname, "rw")) {
> +				if ((pvalue == NULL) || (!strcmp(pvalue, "1"))) {
> +					pnu->mount_params.read_only = TRIS_BOOL_FALSE;
> +				}
> +				else if (!strcmp(pvalue, "0")) {
> +					pnu->mount_params.read_only = TRIS_BOOL_TRUE;
> +				}
> +				else {
> +					nfs_error(_("%s: Unsupported nfs://-URL "
> +						"parameter '%s' value '%s'."),
> +						progname, pname, pvalue);
> +					result = 1;
> +					goto done;
> +				}
> +			}
> +			else if (!strcmp(pname, "ro")) {
> +				if ((pvalue == NULL) || (!strcmp(pvalue, "1"))) {
> +					pnu->mount_params.read_only = TRIS_BOOL_TRUE;
> +				}
> +				else if (!strcmp(pvalue, "0")) {
> +					pnu->mount_params.read_only = TRIS_BOOL_FALSE;
> +				}
> +				else {
> +					nfs_error(_("%s: Unsupported nfs://-URL "
> +						"parameter '%s' value '%s'."),
> +						progname, pname, pvalue);
> +					result = 1;
> +					goto done;
> +				}
> +			}
> +			else {
> +				nfs_error(_("%s: Unsupported nfs://-URL "
> +						"parameter '%s'."),
> +					progname, pname);
> +				result = 1;
> +				goto done;
> +			}
> +		}
> +	}
> +
> +	result = 0;
> +done:
> +	if (result != 0) {
> +		url_parser_free_context(uctx);
> +		return 0;
> +	}
> +
> +	pnu->uctx = uctx;
> +	return 1;
> +}
> +
> +void mount_free_parse_nfs_url(parsed_nfs_url *pnu)
> +{
> +	url_parser_free_context(pnu->uctx);
> +}
> diff --git a/utils/mount/utils.h b/utils/mount/utils.h
> index 224918ae..465c0a5e 100644
> --- a/utils/mount/utils.h
> +++ b/utils/mount/utils.h
> @@ -24,13 +24,36 @@
>  #define _NFS_UTILS_MOUNT_UTILS_H
>  
>  #include "parse_opt.h"
> +#include "urlparser1.h"
>  
> +/* Boolean with three states: { not_set, false, true */
> +typedef signed char tristate_bool;
> +#define TRIS_BOOL_NOT_SET (-1)
> +#define TRIS_BOOL_TRUE (1)
> +#define TRIS_BOOL_FALSE (0)
> +
> +#define TRIS_BOOL_GET_VAL(tsb, tsb_default) \
> +	(((tsb)!=TRIS_BOOL_NOT_SET)?(tsb):(tsb_default))
> +
> +typedef struct _parsed_nfs_url {
> +	url_parser_context *uctx;
> +	struct {
> +		tristate_bool read_only;
> +	} mount_params;
> +} parsed_nfs_url;
> +
> +/* Prototypes */
>  int discover_nfs_mount_data_version(int *string_ver);
>  void print_one(char *spec, char *node, char *type, char *opts);
>  void mount_usage(void);
>  void umount_usage(void);
>  int chk_mountpoint(const char *mount_point);
> +char *utf8str2mbstr(const char *utf8_str);
> +int is_spec_nfs_url(const char *spec);
>  
>  int nfs_umount23(const char *devname, char *string);
>  
> +int mount_parse_nfs_url(const char *spec, parsed_nfs_url *pnu);
> +void mount_free_parse_nfs_url(parsed_nfs_url *pnu);
> +
>  #endif	/* !_NFS_UTILS_MOUNT_UTILS_H */
diff mbox series

Patch

diff --git a/utils/mount/Makefile.am b/utils/mount/Makefile.am
index 83a8ee1c..0e4cab3e 100644
--- a/utils/mount/Makefile.am
+++ b/utils/mount/Makefile.am
@@ -13,7 +13,8 @@  sbin_PROGRAMS	= mount.nfs
 EXTRA_DIST = nfsmount.conf $(man8_MANS) $(man5_MANS)
 mount_common = error.c network.c token.c \
 		    parse_opt.c parse_dev.c \
-		    nfsmount.c nfs4mount.c stropts.c\
+		    nfsmount.c nfs4mount.c \
+		    urlparser1.c urlparser1.h stropts.c \
 		    mount_constants.h error.h network.h token.h \
 		    parse_opt.h parse_dev.h \
 		    nfs4_mount.h stropts.h version.h \
diff --git a/utils/mount/mount.c b/utils/mount/mount.c
index b98f9e00..2ce6209d 100644
--- a/utils/mount/mount.c
+++ b/utils/mount/mount.c
@@ -29,6 +29,7 @@ 
 #include <string.h>
 #include <errno.h>
 #include <fcntl.h>
+#include <locale.h>
 #include <sys/mount.h>
 #include <getopt.h>
 #include <mntent.h>
@@ -386,6 +387,8 @@  int main(int argc, char *argv[])
 	char *extra_opts = NULL, *mount_opts = NULL;
 	uid_t uid = getuid();
 
+	(void)setlocale(LC_ALL, "");
+
 	progname = basename(argv[0]);
 
 	nfs_mount_data_version = discover_nfs_mount_data_version(&string);
diff --git a/utils/mount/nfs4mount.c b/utils/mount/nfs4mount.c
index 0fe142a7..8e4fbf30 100644
--- a/utils/mount/nfs4mount.c
+++ b/utils/mount/nfs4mount.c
@@ -50,8 +50,10 @@ 
 #include "mount_constants.h"
 #include "nfs4_mount.h"
 #include "nfs_mount.h"
+#include "urlparser1.h"
 #include "error.h"
 #include "network.h"
+#include "utils.h"
 
 #if defined(VAR_LOCK_DIR)
 #define DEFAULT_DIR VAR_LOCK_DIR
@@ -182,7 +184,7 @@  int nfs4mount(const char *spec, const char *node, int flags,
 	int num_flavour = 0;
 	int ip_addr_in_opts = 0;
 
-	char *hostname, *dirname, *old_opts;
+	char *hostname, *dirname, *mb_dirname = NULL, *old_opts;
 	char new_opts[1024];
 	char *opt, *opteq;
 	char *s;
@@ -192,15 +194,66 @@  int nfs4mount(const char *spec, const char *node, int flags,
 	int retry;
 	int retval = EX_FAIL;
 	time_t timeout, t;
+	int nfs_port = NFS_PORT;
+	parsed_nfs_url pnu;
+
+	(void)memset(&pnu, 0, sizeof(parsed_nfs_url));
 
 	if (strlen(spec) >= sizeof(hostdir)) {
 		nfs_error(_("%s: excessively long host:dir argument\n"),
 				progname);
 		goto fail;
 	}
-	strcpy(hostdir, spec);
-	if (parse_devname(hostdir, &hostname, &dirname))
-		goto fail;
+
+	/*
+	 * Support nfs://-URLS per RFC 2224 ("NFS URL
+	 * SCHEME", see https://www.rfc-editor.org/rfc/rfc2224.html),
+	 * including custom port (nfs://hostname@port/path/...)
+	 * and URL parameter (e.g. nfs://.../?param1=val1&param2=val2
+	 * support
+	 */
+	if (is_spec_nfs_url(spec)) {
+		if (!mount_parse_nfs_url(spec, &pnu)) {
+			goto fail;
+		}
+
+		/*
+		 * |pnu.uctx->path| is in UTF-8, but we need the data
+		 * in the current local locale's encoding, as mount(2)
+		 * does not have something like a |MS_UTF8_SPEC| flag
+		 * to indicate that the input path is in UTF-8,
+		 * independently of the current locale
+		 */
+		hostname = pnu.uctx->hostport.hostname;
+		dirname = mb_dirname = utf8str2mbstr(pnu.uctx->path);
+
+		(void)snprintf(hostdir, sizeof(hostdir), "%s:/%s",
+			hostname, dirname);
+		spec = hostdir;
+
+		if (pnu.uctx->hostport.port != -1) {
+			nfs_port = pnu.uctx->hostport.port;
+		}
+
+		/*
+		 * Values added here based on URL parameters
+		 * should be added the front of the list of options,
+		 * so users can override the nfs://-URL given default.
+		 *
+		 * FIXME: We do not do that here for |MS_RDONLY|!
+		 */
+		if (pnu.mount_params.read_only != TRIS_BOOL_NOT_SET) {
+			if (pnu.mount_params.read_only)
+				flags |= MS_RDONLY;
+			else
+				flags &= ~MS_RDONLY;
+		}
+        } else {
+		(void)strcpy(hostdir, spec);
+
+		if (parse_devname(hostdir, &hostname, &dirname))
+			goto fail;
+	}
 
 	if (fill_ipv4_sockaddr(hostname, &server_addr))
 		goto fail;
@@ -247,7 +300,7 @@  int nfs4mount(const char *spec, const char *node, int flags,
 	/*
 	 * NFSv4 specifies that the default port should be 2049
 	 */
-	server_addr.sin_port = htons(NFS_PORT);
+	server_addr.sin_port = htons(nfs_port);
 
 	/* parse options */
 
@@ -474,8 +527,14 @@  int nfs4mount(const char *spec, const char *node, int flags,
 		}
 	}
 
+	mount_free_parse_nfs_url(&pnu);
+	free(mb_dirname);
+
 	return EX_SUCCESS;
 
 fail:
+	mount_free_parse_nfs_url(&pnu);
+	free(mb_dirname);
+
 	return retval;
 }
diff --git a/utils/mount/nfsmount.c b/utils/mount/nfsmount.c
index a1c92fe8..e61d718a 100644
--- a/utils/mount/nfsmount.c
+++ b/utils/mount/nfsmount.c
@@ -63,11 +63,13 @@ 
 #include "xcommon.h"
 #include "mount.h"
 #include "nfs_mount.h"
+#include "urlparser1.h"
 #include "mount_constants.h"
 #include "nls.h"
 #include "error.h"
 #include "network.h"
 #include "version.h"
+#include "utils.h"
 
 #ifdef HAVE_RPCSVC_NFS_PROT_H
 #include <rpcsvc/nfs_prot.h>
@@ -493,7 +495,7 @@  nfsmount(const char *spec, const char *node, int flags,
 	 char **extra_opts, int fake, int running_bg)
 {
 	char hostdir[1024];
-	char *hostname, *dirname, *old_opts, *mounthost = NULL;
+	char *hostname, *dirname, *mb_dirname = NULL, *old_opts, *mounthost = NULL;
 	char new_opts[1024], cbuf[1024];
 	static struct nfs_mount_data data;
 	int val;
@@ -521,29 +523,79 @@  nfsmount(const char *spec, const char *node, int flags,
 	time_t t;
 	time_t prevt;
 	time_t timeout;
+	int nfsurl_port = -1;
+	parsed_nfs_url pnu;
+
+	(void)memset(&pnu, 0, sizeof(parsed_nfs_url));
 
 	if (strlen(spec) >= sizeof(hostdir)) {
 		nfs_error(_("%s: excessively long host:dir argument"),
 				progname);
 		goto fail;
 	}
-	strcpy(hostdir, spec);
-	if ((s = strchr(hostdir, ':'))) {
-		hostname = hostdir;
-		dirname = s + 1;
-		*s = '\0';
-		/* Ignore all but first hostname in replicated mounts
-		   until they can be fully supported. (mack@sgi.com) */
-		if ((s = strchr(hostdir, ','))) {
+
+	/*
+	 * Support nfs://-URLS per RFC 2224 ("NFS URL
+	 * SCHEME", see https://www.rfc-editor.org/rfc/rfc2224.html),
+	 * including custom port (nfs://hostname@port/path/...)
+	 * and URL parameter (e.g. nfs://.../?param1=val1&param2=val2
+	 * support
+	 */
+	if (is_spec_nfs_url(spec)) {
+		if (!mount_parse_nfs_url(spec, &pnu)) {
+			goto fail;
+		}
+
+		/*
+		 * |pnu.uctx->path| is in UTF-8, but we need the data
+		 * in the current local locale's encoding, as mount(2)
+		 * does not have something like a |MS_UTF8_SPEC| flag
+		 * to indicate that the input path is in UTF-8,
+		 * independently of the current locale
+		 */
+		hostname = pnu.uctx->hostport.hostname;
+		dirname = mb_dirname = utf8str2mbstr(pnu.uctx->path);
+
+		(void)snprintf(hostdir, sizeof(hostdir), "%s:/%s",
+			hostname, dirname);
+		spec = hostdir;
+
+		if (pnu.uctx->hostport.port != -1) {
+			nfsurl_port = pnu.uctx->hostport.port;
+		}
+
+		/*
+		 * Values added here based on URL parameters
+		 * should be added the front of the list of options,
+		 * so users can override the nfs://-URL given default.
+		 *
+		 * FIXME: We do not do that here for |MS_RDONLY|!
+		 */
+		if (pnu.mount_params.read_only != TRIS_BOOL_NOT_SET) {
+			if (pnu.mount_params.read_only)
+				flags |= MS_RDONLY;
+			else
+				flags &= ~MS_RDONLY;
+		}
+        } else {
+		(void)strcpy(hostdir, spec);
+		if ((s = strchr(hostdir, ':'))) {
+			hostname = hostdir;
+			dirname = s + 1;
 			*s = '\0';
-			nfs_error(_("%s: warning: "
-				  "multiple hostnames not supported"),
+			/* Ignore all but first hostname in replicated mounts
+			   until they can be fully supported. (mack@sgi.com) */
+			if ((s = strchr(hostdir, ','))) {
+				*s = '\0';
+				nfs_error(_("%s: warning: "
+					"multiple hostnames not supported"),
 					progname);
-		}
-	} else {
-		nfs_error(_("%s: directory to mount not in host:dir format"),
+			}
+		} else {
+			nfs_error(_("%s: directory to mount not in host:dir format"),
 				progname);
-		goto fail;
+			goto fail;
+		}
 	}
 
 	if (!nfs_gethostbyname(hostname, nfs_saddr))
@@ -579,6 +631,14 @@  nfsmount(const char *spec, const char *node, int flags,
 	memset(nfs_pmap, 0, sizeof(*nfs_pmap));
 	nfs_pmap->pm_prog = NFS_PROGRAM;
 
+	if (nfsurl_port != -1) {
+		/*
+		 * Set custom TCP port defined by a nfs://-URL here,
+		 * so $ mount -o port ... # can be used to override
+		 */
+		nfs_pmap->pm_port = nfsurl_port;
+	}
+
 	/* parse options */
 	new_opts[0] = 0;
 	if (!parse_options(old_opts, &data, &bg, &retry, &mnt_server, &nfs_server,
@@ -863,10 +923,13 @@  noauth_flavors:
 		}
 	}
 
+	mount_free_parse_nfs_url(&pnu);
+
 	return EX_SUCCESS;
 
 	/* abort */
  fail:
+	mount_free_parse_nfs_url(&pnu);
 	if (fsock != -1)
 		close(fsock);
 	return retval;
diff --git a/utils/mount/parse_dev.c b/utils/mount/parse_dev.c
index 2ade5d5d..d9f8cf59 100644
--- a/utils/mount/parse_dev.c
+++ b/utils/mount/parse_dev.c
@@ -27,6 +27,8 @@ 
 #include "xcommon.h"
 #include "nls.h"
 #include "parse_dev.h"
+#include "urlparser1.h"
+#include "utils.h"
 
 #ifndef NFS_MAXHOSTNAME
 #define NFS_MAXHOSTNAME		(255)
@@ -179,17 +181,62 @@  static int nfs_parse_square_bracket(const char *dev,
 }
 
 /*
- * RFC 2224 says an NFS client must grok "public file handles" to
- * support NFS URLs.  Linux doesn't do that yet.  Print a somewhat
- * helpful error message in this case instead of pressing forward
- * with the mount request and failing with a cryptic error message
- * later.
+ * Support nfs://-URLS per RFC 2224 ("NFS URL
+ * SCHEME", see https://www.rfc-editor.org/rfc/rfc2224.html),
+ * including port support (nfs://hostname@port/path/...)
  */
-static int nfs_parse_nfs_url(__attribute__((unused)) const char *dev,
-			     __attribute__((unused)) char **hostname,
-			     __attribute__((unused)) char **pathname)
+static int nfs_parse_nfs_url(const char *dev,
+			     char **out_hostname,
+			     char **out_pathname)
 {
-	nfs_error(_("%s: NFS URLs are not supported"), progname);
+	parsed_nfs_url pnu;
+
+	if (out_hostname)
+		*out_hostname = NULL;
+	if (out_pathname)
+		*out_pathname = NULL;
+
+	/*
+	 * Support nfs://-URLS per RFC 2224 ("NFS URL
+	 * SCHEME", see https://www.rfc-editor.org/rfc/rfc2224.html),
+	 * including custom port (nfs://hostname@port/path/...)
+	 * and URL parameter (e.g. nfs://.../?param1=val1&param2=val2
+	 * support
+	 */
+	if (!mount_parse_nfs_url(dev, &pnu)) {
+		goto fail;
+	}
+
+	if (pnu.uctx->hostport.port != -1) {
+		/* NOP here, unless we switch from hostname to hostport */
+	}
+
+	if (out_hostname)
+		*out_hostname = strdup(pnu.uctx->hostport.hostname);
+	if (out_pathname)
+		*out_pathname = utf8str2mbstr(pnu.uctx->path);
+
+	if (((out_hostname)?(*out_hostname == NULL):0) ||
+		((out_pathname)?(*out_pathname == NULL):0)) {
+		nfs_error(_("%s: out of memory"),
+			progname);
+		goto fail;
+	}
+
+	mount_free_parse_nfs_url(&pnu);
+
+	return 1;
+
+fail:
+	mount_free_parse_nfs_url(&pnu);
+	if (out_hostname) {
+		free(*out_hostname);
+		*out_hostname = NULL;
+	}
+	if (out_pathname) {
+		free(*out_pathname);
+		*out_pathname = NULL;
+	}
 	return 0;
 }
 
@@ -223,7 +270,7 @@  int nfs_parse_devname(const char *devname,
 		return nfs_pdn_nomem_err();
 	if (*dev == '[')
 		result = nfs_parse_square_bracket(dev, hostname, pathname);
-	else if (strncmp(dev, "nfs://", 6) == 0)
+	else if (is_spec_nfs_url(dev))
 		result = nfs_parse_nfs_url(dev, hostname, pathname);
 	else
 		result = nfs_parse_simple_hostname(dev, hostname, pathname);
diff --git a/utils/mount/stropts.c b/utils/mount/stropts.c
index 23f0a8c0..ad92ab78 100644
--- a/utils/mount/stropts.c
+++ b/utils/mount/stropts.c
@@ -42,6 +42,7 @@ 
 #include "nls.h"
 #include "nfsrpc.h"
 #include "mount_constants.h"
+#include "urlparser1.h"
 #include "stropts.h"
 #include "error.h"
 #include "network.h"
@@ -50,6 +51,7 @@ 
 #include "parse_dev.h"
 #include "conffile.h"
 #include "misc.h"
+#include "utils.h"
 
 #ifndef NFS_PROGRAM
 #define NFS_PROGRAM	(100003)
@@ -643,24 +645,106 @@  static int nfs_sys_mount(struct nfsmount_info *mi, struct mount_options *opts)
 {
 	char *options = NULL;
 	int result;
+	int nfs_port = 2049;
 
 	if (mi->fake)
 		return 1;
 
-	if (po_join(opts, &options) == PO_FAILED) {
-		errno = EIO;
-		return 0;
-	}
+	/*
+	 * Support nfs://-URLS per RFC 2224 ("NFS URL
+	 * SCHEME", see https://www.rfc-editor.org/rfc/rfc2224.html),
+	 * including custom port (nfs://hostname@port/path/...)
+	 * and URL parameter (e.g. nfs://.../?param1=val1&param2=val2
+	 * support
+	 */
+	if (is_spec_nfs_url(mi->spec)) {
+		parsed_nfs_url pnu;
+		char *mb_path;
+		char mount_source[1024];
+
+		if (!mount_parse_nfs_url(mi->spec, &pnu)) {
+			result = 1;
+			errno = EINVAL;
+			goto done;
+		}
+
+		/*
+		 * |pnu.uctx->path| is in UTF-8, but we need the data
+		 * in the current local locale's encoding, as mount(2)
+		 * does not have something like a |MS_UTF8_SPEC| flag
+		 * to indicate that the input path is in UTF-8,
+		 * independently of the current locale
+		 */
+		mb_path = utf8str2mbstr(pnu.uctx->path);
+		if (!mb_path) {
+			nfs_error(_("%s: Could not convert path to local encoding."),
+				progname);
+			mount_free_parse_nfs_url(&pnu);
+			result = 1;
+			errno = EINVAL;
+			goto done;
+		}
+
+		(void)snprintf(mount_source, sizeof(mount_source),
+			"%s:/%s",
+			pnu.uctx->hostport.hostname,
+			mb_path);
+		free(mb_path);
+
+		if (pnu.uctx->hostport.port != -1) {
+			nfs_port = pnu.uctx->hostport.port;
+		}
 
-	result = mount(mi->spec, mi->node, mi->type,
+		/*
+		 * Insert "port=" option with the value from the nfs://
+		 * URL at the beginning of the list of options, so
+		 * users can override it with $ mount.nfs4 -o port= #,
+		 * e.g.
+		 * $ mount.nfs4 -o port=1234 nfs://10.49.202.230:400//bigdisk /mnt4 #
+		 * should use port 1234, and not port 400 as specified
+		 * in the URL.
+		 */
+		char portoptbuf[5+32+1];
+		(void)snprintf(portoptbuf, sizeof(portoptbuf), "port=%d", nfs_port);
+		(void)po_insert(opts, portoptbuf);
+
+		if (pnu.mount_params.read_only != TRIS_BOOL_NOT_SET) {
+			if (pnu.mount_params.read_only)
+				mi->flags |= MS_RDONLY;
+			else
+				mi->flags &= ~MS_RDONLY;
+		}
+
+		mount_free_parse_nfs_url(&pnu);
+
+		if (po_join(opts, &options) == PO_FAILED) {
+			errno = EIO;
+			result = 1;
+			goto done;
+		}
+
+		result = mount(mount_source, mi->node, mi->type,
+			mi->flags & ~(MS_USER|MS_USERS), options);
+		free(options);
+	} else {
+		if (po_join(opts, &options) == PO_FAILED) {
+			errno = EIO;
+			result = 1;
+			goto done;
+		}
+
+		result = mount(mi->spec, mi->node, mi->type,
 			mi->flags & ~(MS_USER|MS_USERS), options);
-	free(options);
+		free(options);
+	}
 
 	if (verbose && result) {
 		int save = errno;
 		nfs_error(_("%s: mount(2): %s"), progname, strerror(save));
 		errno = save;
 	}
+
+done:
 	return !result;
 }
 
diff --git a/utils/mount/urlparser1.c b/utils/mount/urlparser1.c
new file mode 100644
index 00000000..d4c6f339
--- /dev/null
+++ b/utils/mount/urlparser1.c
@@ -0,0 +1,358 @@ 
+/*
+ * MIT License
+ *
+ * Copyright (c) 2024 Roland Mainz <roland.mainz@nrubsig.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+/* urlparser1.c - simple URL parser */
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <stdio.h>
+
+#ifdef DBG_USE_WIDECHAR
+#include <wchar.h>
+#include <locale.h>
+#include <io.h>
+#include <fcntl.h>
+#endif /* DBG_USE_WIDECHAR */
+
+#include "urlparser1.h"
+
+typedef struct _url_parser_context_private {
+	url_parser_context c;
+
+	/* Private data */
+	char *parameter_string_buff;
+} url_parser_context_private;
+
+#define MAX_URL_PARAMETERS 256
+
+/*
+ * Original extended regular expression:
+ *
+ * "^"
+ * "(.+?)"				// scheme
+ * "://"				// '://'
+ * "("					// login
+ *	"(?:"
+ *	"(.+?)"				// user (optional)
+ *		"(?::(.+))?"		// password (optional)
+ *		"@"
+ *	")?"
+ *	"("				// hostport
+ *		"(.+?)"			// host
+ *		"(?::([[:digit:]]+))?"	// port (optional)
+ *	")"
+ * ")"
+ * "(?:/(.*?))?"			// path (optional)
+ * "(?:\?(.*?))?"			// URL parameters (optional)
+ * "$"
+ */
+
+#define DBGNULLSTR(s) (((s)!=NULL)?(s):"<NULL>")
+#if 0 || defined(TEST_URLPARSER)
+#define D(x) x
+#else
+#define D(x)
+#endif
+
+#ifdef DBG_USE_WIDECHAR
+/*
+ * Use wide-char APIs on WIN32, otherwise we cannot output
+ * Japanese/Chinese/etc correctly
+ */
+#define DBG_PUTS(str, fp)		fputws(L"" str, (fp))
+#define DBG_PUTC(c, fp)			fputwc(btowc(c), (fp))
+#define DBG_PRINTF(fp, fmt, ...)	fwprintf((fp), L"" fmt, __VA_ARGS__)
+#else
+#define DBG_PUTS(str, fp)		fputs((str), (fp))
+#define DBG_PUTC(c, fp)			fputc((c), (fp))
+#define DBG_PRINTF(fp, fmt, ...)	fprintf((fp), fmt, __VA_ARGS__)
+#endif /* DBG_USE_WIDECHAR */
+
+static
+void urldecodestr(char *outbuff, const char *buffer, size_t len)
+{
+	size_t i, j;
+
+	for (i = j = 0 ; i < len ; ) {
+		switch (buffer[i]) {
+			case '%':
+				if ((i + 2) < len) {
+					if (isxdigit((int)buffer[i+1]) && isxdigit((int)buffer[i+2])) {
+						const char hexstr[3] = {
+							buffer[i+1],
+							buffer[i+2],
+							'\0'
+						};
+						outbuff[j++] = (unsigned char)strtol(hexstr, NULL, 16);
+						i += 3;
+					} else {
+						/* invalid hex digit */
+						outbuff[j++] = buffer[i];
+						i++;
+					}
+				} else {
+					/* incomplete hex digit */
+					outbuff[j++] = buffer[i];
+					i++;
+				}
+				break;
+			case '+':
+				outbuff[j++] = ' ';
+				i++;
+				break;
+			default:
+				outbuff[j++] = buffer[i++];
+				break;
+		}
+	}
+
+	outbuff[j] = '\0';
+}
+
+url_parser_context *url_parser_create_context(const char *in_url, unsigned int flags)
+{
+	url_parser_context_private *uctx;
+	char *s;
+	size_t in_url_len;
+	size_t context_len;
+
+	/* |flags| is for future extensions */
+	(void)flags;
+
+	if (!in_url)
+		return NULL;
+
+	in_url_len = strlen(in_url);
+
+	context_len = sizeof(url_parser_context_private) +
+		((in_url_len+1)*6) +
+		(sizeof(url_parser_name_value)*MAX_URL_PARAMETERS)+sizeof(void*);
+	uctx = malloc(context_len);
+	if (!uctx)
+		return NULL;
+
+	s = (void *)(uctx+1);
+	uctx->c.in_url = s;		s+= in_url_len+1;
+	(void)strcpy(uctx->c.in_url, in_url);
+	uctx->c.scheme = s;		s+= in_url_len+1;
+	uctx->c.login.username = s;	s+= in_url_len+1;
+	uctx->c.hostport.hostname = s;	s+= in_url_len+1;
+	uctx->c.path = s;		s+= in_url_len+1;
+	uctx->c.hostport.port = -1;
+	uctx->c.num_parameters = -1;
+	uctx->c.parameters = (void *)s;		s+= (sizeof(url_parser_name_value)*MAX_URL_PARAMETERS)+sizeof(void*);
+	uctx->parameter_string_buff = s;	s+= in_url_len+1;
+
+	return &uctx->c;
+}
+
+int url_parser_parse(url_parser_context *ctx)
+{
+	url_parser_context_private *uctx = (url_parser_context_private *)ctx;
+
+	D((void)DBG_PRINTF(stderr, "## parser in_url='%s'\n", uctx->c.in_url));
+
+	char *s;
+	const char *urlstr = uctx->c.in_url;
+	size_t slen;
+
+	s = strstr(urlstr, "://");
+	if (!s) {
+		D((void)DBG_PUTS("url_parser: Not an URL\n", stderr));
+		return -1;
+	}
+
+	slen = s-urlstr;
+	(void)memcpy(uctx->c.scheme, urlstr, slen);
+	uctx->c.scheme[slen] = '\0';
+	urlstr += slen + 3;
+
+	D((void)DBG_PRINTF(stdout, "scheme='%s', rest='%s'\n", uctx->c.scheme, urlstr));
+
+	s = strstr(urlstr, "@");
+	if (s) {
+		/* URL has user/password */
+		slen = s-urlstr;
+		urldecodestr(uctx->c.login.username, urlstr, slen);
+		urlstr += slen + 1;
+
+		s = strstr(uctx->c.login.username, ":");
+		if (s) {
+			/* found passwd */
+			uctx->c.login.passwd = s+1;
+			*s = '\0';
+		}
+		else {
+			uctx->c.login.passwd = NULL;
+		}
+
+		/* catch password-only URLs */
+		if (uctx->c.login.username[0] == '\0')
+			uctx->c.login.username = NULL;
+	}
+	else {
+		uctx->c.login.username = NULL;
+		uctx->c.login.passwd = NULL;
+	}
+
+	D((void)DBG_PRINTF(stdout, "login='%s', passwd='%s', rest='%s'\n",
+		DBGNULLSTR(uctx->c.login.username),
+		DBGNULLSTR(uctx->c.login.passwd),
+		DBGNULLSTR(urlstr)));
+
+	char *raw_parameters;
+
+	uctx->c.num_parameters = 0;
+	raw_parameters = strstr(urlstr, "?");
+	/* Do we have a non-empty parameter string ? */
+	if (raw_parameters && (raw_parameters[1] != '\0')) {
+		*raw_parameters++ = '\0';
+		D((void)DBG_PRINTF(stdout, "raw parameters = '%s'\n", raw_parameters));
+
+		char *ps = raw_parameters;
+		char *pv; /* parameter value */
+		char *na; /* next '&' */
+		char *pb = uctx->parameter_string_buff;
+		char *pname;
+		char *pvalue;
+		ssize_t pi;
+
+		for (pi = 0; pi < MAX_URL_PARAMETERS ; pi++) {
+			pname = ps;
+
+			/*
+			 * Handle parameters without value,
+			 * e.g. "path?name1&name2=value2"
+			 */
+			na = strstr(ps, "&");
+			pv = strstr(ps, "=");
+			if (pv && (na?(na > pv):true)) {
+				*pv++ = '\0';
+				pvalue = pv;
+				ps = pv;
+			}
+			else {
+				pvalue = NULL;
+			}
+
+			if (na) {
+				*na++ = '\0';
+			}
+
+			/* URLDecode parameter name */
+			urldecodestr(pb, pname, strlen(pname));
+			uctx->c.parameters[pi].name = pb;
+			pb += strlen(uctx->c.parameters[pi].name)+1;
+
+			/* URLDecode parameter value */
+			if (pvalue) {
+				urldecodestr(pb, pvalue, strlen(pvalue));
+				uctx->c.parameters[pi].value = pb;
+				pb += strlen(uctx->c.parameters[pi].value)+1;
+			}
+			else {
+				uctx->c.parameters[pi].value = NULL;
+			}
+
+			/* Next '&' ? */
+			if (!na)
+				break;
+
+			ps = na;
+		}
+
+		uctx->c.num_parameters = pi+1;
+	}
+
+	s = strstr(urlstr, "/");
+	if (s) {
+		/* URL has hostport */
+		slen = s-urlstr;
+		urldecodestr(uctx->c.hostport.hostname, urlstr, slen);
+		urlstr += slen + 1;
+
+		/*
+		 * check for addresses within '[' and ']', like
+		 * IPv6 addresses
+		 */
+		s = uctx->c.hostport.hostname;
+		if (s[0] == '[')
+			s = strstr(s, "]");
+
+		if (s == NULL) {
+			D((void)DBG_PUTS("url_parser: Unmatched '[' in hostname\n", stderr));
+			return -1;
+		}
+
+		s = strstr(s, ":");
+		if (s) {
+			/* found port number */
+			uctx->c.hostport.port = atoi(s+1);
+			*s = '\0';
+		}
+	}
+	else {
+		(void)strcpy(uctx->c.hostport.hostname, urlstr);
+		uctx->c.path = NULL;
+		urlstr = NULL;
+	}
+
+	D(
+		(void)DBG_PRINTF(stdout,
+			"hostport='%s', port=%d, rest='%s', num_parameters=%d\n",
+			DBGNULLSTR(uctx->c.hostport.hostname),
+			uctx->c.hostport.port,
+			DBGNULLSTR(urlstr),
+			(int)uctx->c.num_parameters);
+	);
+
+
+	D(
+		ssize_t dpi;
+		for (dpi = 0 ; dpi < uctx->c.num_parameters ; dpi++) {
+			(void)DBG_PRINTF(stdout,
+				"param[%d]: name='%s'/value='%s'\n",
+				(int)dpi,
+				uctx->c.parameters[dpi].name,
+				DBGNULLSTR(uctx->c.parameters[dpi].value));
+		}
+	);
+
+	if (!urlstr) {
+		goto done;
+	}
+
+	urldecodestr(uctx->c.path, urlstr, strlen(urlstr));
+	D((void)DBG_PRINTF(stdout, "path='%s'\n", uctx->c.path));
+
+done:
+	return 0;
+}
+
+void url_parser_free_context(url_parser_context *c)
+{
+	free(c);
+}
diff --git a/utils/mount/urlparser1.h b/utils/mount/urlparser1.h
new file mode 100644
index 00000000..515eea9d
--- /dev/null
+++ b/utils/mount/urlparser1.h
@@ -0,0 +1,60 @@ 
+/*
+ * MIT License
+ *
+ * Copyright (c) 2024 Roland Mainz <roland.mainz@nrubsig.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+/* urlparser1.h - header for simple URL parser */
+
+#ifndef __URLPARSER1_H__
+#define __URLPARSER1_H__ 1
+
+#include <stdlib.h>
+
+typedef struct _url_parser_name_value {
+	char *name;
+	char *value;
+} url_parser_name_value;
+
+typedef struct _url_parser_context {
+	char *in_url;
+
+	char *scheme;
+	struct {
+		char *username;
+		char *passwd;
+	} login;
+	struct {
+		char *hostname;
+		signed int port;
+	} hostport;
+	char *path;
+
+	ssize_t num_parameters;
+	url_parser_name_value *parameters;
+} url_parser_context;
+
+/* Prototypes */
+url_parser_context *url_parser_create_context(const char *in_url, unsigned int flags);
+int url_parser_parse(url_parser_context *uctx);
+void url_parser_free_context(url_parser_context *c);
+
+#endif /* !__URLPARSER1_H__ */
diff --git a/utils/mount/utils.c b/utils/mount/utils.c
index b7562a47..3d55e997 100644
--- a/utils/mount/utils.c
+++ b/utils/mount/utils.c
@@ -28,6 +28,7 @@ 
 #include <unistd.h>
 #include <sys/types.h>
 #include <sys/stat.h>
+#include <iconv.h>
 
 #include "sockaddr.h"
 #include "nfs_mount.h"
@@ -173,3 +174,157 @@  int nfs_umount23(const char *devname, char *string)
 	free(dirname);
 	return result;
 }
+
+/* Convert UTF-8 string to multibyte string in the current locale */
+char *utf8str2mbstr(const char *utf8_str)
+{
+	iconv_t cd;
+
+	cd = iconv_open("", "UTF-8");
+	if (cd == (iconv_t)-1) {
+		perror("utf8str2mbstr: iconv_open failed");
+		return NULL;
+	}
+
+	size_t inbytesleft = strlen(utf8_str);
+	char *inbuf = (char *)utf8_str;
+	size_t outbytesleft = inbytesleft*4+1;
+	char *outbuf = malloc(outbytesleft);
+	char *outbuf_orig = outbuf;
+
+	if (!outbuf) {
+		perror("utf8str2mbstr: out of memory");
+		(void)iconv_close(cd);
+		return NULL;
+	}
+
+	int ret = iconv(cd, &inbuf, &inbytesleft, &outbuf, &outbytesleft);
+	if (ret == -1) {
+		perror("utf8str2mbstr: iconv() failed");
+		free(outbuf_orig);
+		(void)iconv_close(cd);
+		return NULL;
+	}
+
+	*outbuf = '\0';
+
+	(void)iconv_close(cd);
+	return outbuf_orig;
+}
+
+/* fixme: We should use |bool|! */
+int is_spec_nfs_url(const char *spec)
+{
+	return (!strncmp(spec, "nfs://", 6));
+}
+
+int mount_parse_nfs_url(const char *spec, parsed_nfs_url *pnu)
+{
+	int result = 1;
+	url_parser_context *uctx = NULL;
+
+	(void)memset(pnu, 0, sizeof(parsed_nfs_url));
+	pnu->mount_params.read_only = TRIS_BOOL_NOT_SET;
+
+	uctx = url_parser_create_context(spec, 0);
+	if (!uctx) {
+		nfs_error(_("%s: out of memory"),
+			progname);
+		result = 1;
+		goto done;
+	}
+
+	if (url_parser_parse(uctx) < 0) {
+		nfs_error(_("%s: Error parsing nfs://-URL."),
+			progname);
+		result = 1;
+		goto done;
+	}
+	if (uctx->login.username || uctx->login.passwd) {
+		nfs_error(_("%s: Username/Password are not defined for nfs://-URL."),
+			progname);
+		result = 1;
+		goto done;
+	}
+	if (!uctx->path) {
+		nfs_error(_("%s: Path missing in nfs://-URL."),
+			progname);
+		result = 1;
+		goto done;
+	}
+	if (uctx->path[0] != '/') {
+		nfs_error(_("%s: Relative nfs://-URLs are not supported."),
+			progname);
+		result = 1;
+		goto done;
+	}
+
+	if (uctx->num_parameters > 0) {
+		int pi;
+		const char *pname;
+		const char *pvalue;
+
+		/*
+		 * Values added here based on URL parameters
+		 * should be added the front of the list of options,
+		 * so users can override the nfs://-URL given default.
+		 */
+		for (pi = 0; pi < uctx->num_parameters ; pi++) {
+			pname = uctx->parameters[pi].name;
+			pvalue = uctx->parameters[pi].value;
+
+			if (!strcmp(pname, "rw")) {
+				if ((pvalue == NULL) || (!strcmp(pvalue, "1"))) {
+					pnu->mount_params.read_only = TRIS_BOOL_FALSE;
+				}
+				else if (!strcmp(pvalue, "0")) {
+					pnu->mount_params.read_only = TRIS_BOOL_TRUE;
+				}
+				else {
+					nfs_error(_("%s: Unsupported nfs://-URL "
+						"parameter '%s' value '%s'."),
+						progname, pname, pvalue);
+					result = 1;
+					goto done;
+				}
+			}
+			else if (!strcmp(pname, "ro")) {
+				if ((pvalue == NULL) || (!strcmp(pvalue, "1"))) {
+					pnu->mount_params.read_only = TRIS_BOOL_TRUE;
+				}
+				else if (!strcmp(pvalue, "0")) {
+					pnu->mount_params.read_only = TRIS_BOOL_FALSE;
+				}
+				else {
+					nfs_error(_("%s: Unsupported nfs://-URL "
+						"parameter '%s' value '%s'."),
+						progname, pname, pvalue);
+					result = 1;
+					goto done;
+				}
+			}
+			else {
+				nfs_error(_("%s: Unsupported nfs://-URL "
+						"parameter '%s'."),
+					progname, pname);
+				result = 1;
+				goto done;
+			}
+		}
+	}
+
+	result = 0;
+done:
+	if (result != 0) {
+		url_parser_free_context(uctx);
+		return 0;
+	}
+
+	pnu->uctx = uctx;
+	return 1;
+}
+
+void mount_free_parse_nfs_url(parsed_nfs_url *pnu)
+{
+	url_parser_free_context(pnu->uctx);
+}
diff --git a/utils/mount/utils.h b/utils/mount/utils.h
index 224918ae..465c0a5e 100644
--- a/utils/mount/utils.h
+++ b/utils/mount/utils.h
@@ -24,13 +24,36 @@ 
 #define _NFS_UTILS_MOUNT_UTILS_H
 
 #include "parse_opt.h"
+#include "urlparser1.h"
 
+/* Boolean with three states: { not_set, false, true */
+typedef signed char tristate_bool;
+#define TRIS_BOOL_NOT_SET (-1)
+#define TRIS_BOOL_TRUE (1)
+#define TRIS_BOOL_FALSE (0)
+
+#define TRIS_BOOL_GET_VAL(tsb, tsb_default) \
+	(((tsb)!=TRIS_BOOL_NOT_SET)?(tsb):(tsb_default))
+
+typedef struct _parsed_nfs_url {
+	url_parser_context *uctx;
+	struct {
+		tristate_bool read_only;
+	} mount_params;
+} parsed_nfs_url;
+
+/* Prototypes */
 int discover_nfs_mount_data_version(int *string_ver);
 void print_one(char *spec, char *node, char *type, char *opts);
 void mount_usage(void);
 void umount_usage(void);
 int chk_mountpoint(const char *mount_point);
+char *utf8str2mbstr(const char *utf8_str);
+int is_spec_nfs_url(const char *spec);
 
 int nfs_umount23(const char *devname, char *string);
 
+int mount_parse_nfs_url(const char *spec, parsed_nfs_url *pnu);
+void mount_free_parse_nfs_url(parsed_nfs_url *pnu);
+
 #endif	/* !_NFS_UTILS_MOUNT_UTILS_H */