mirror of https://github.com/Gnucash/gnucash
commit
287c802d8c
@ -1,429 +0,0 @@
|
||||
/*
|
||||
* gnc-uri-utils.c -- utility functions to convert uri in separate
|
||||
* components and back.
|
||||
*
|
||||
* Copyright (C) 2010 Geert Janssens <janssens.geert@telenet.be>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, contact:
|
||||
*
|
||||
* Free Software Foundation Voice: +1-617-542-5942
|
||||
* 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652
|
||||
* Boston, MA 02110-1301, USA gnu@gnu.org
|
||||
*/
|
||||
|
||||
#include <glib.h>
|
||||
#include "gnc-uri-utils.h"
|
||||
#include "gnc-filepath-utils.h"
|
||||
#include "qofsession.h"
|
||||
|
||||
/* Checks if the given uri is a valid uri
|
||||
*/
|
||||
gboolean gnc_uri_is_uri (const gchar *uri)
|
||||
{
|
||||
|
||||
gchar *scheme = NULL, *hostname = NULL;
|
||||
gchar *username = NULL, *password = NULL;
|
||||
gchar *path = NULL;
|
||||
gint port = 0;
|
||||
gboolean is_uri = FALSE;
|
||||
|
||||
gnc_uri_get_components ( uri, &scheme, &hostname, &port,
|
||||
&username, &password, &path );
|
||||
|
||||
/* For gnucash to consider a uri valid the following must be true:
|
||||
* - scheme and path must not be NULL
|
||||
* - for anything but local filesystem uris, hostname must be valid as well */
|
||||
is_uri = (scheme && path && (gnc_uri_is_file_scheme(scheme) || hostname));
|
||||
|
||||
g_free (scheme);
|
||||
g_free (hostname);
|
||||
g_free (username);
|
||||
g_free (password);
|
||||
g_free (path);
|
||||
|
||||
return is_uri;
|
||||
}
|
||||
|
||||
/* Checks if the given scheme is used to refer to a file
|
||||
* (as opposed to a network service)
|
||||
*/
|
||||
gboolean gnc_uri_is_known_scheme (const gchar *scheme)
|
||||
{
|
||||
gboolean is_known_scheme = FALSE;
|
||||
GList *node;
|
||||
GList *known_scheme_list = qof_backend_get_registered_access_method_list();
|
||||
|
||||
for ( node = known_scheme_list; node != NULL; node = node->next )
|
||||
{
|
||||
gchar *known_scheme = node->data;
|
||||
if ( !g_ascii_strcasecmp (scheme, known_scheme) )
|
||||
{
|
||||
is_known_scheme = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
g_list_free (known_scheme_list);
|
||||
return is_known_scheme;
|
||||
}
|
||||
|
||||
/* Checks if the given scheme is used to refer to a file
|
||||
* (as opposed to a network service)
|
||||
* Note unknown schemes are always considered network schemes.
|
||||
*
|
||||
* *Compatibility note:*
|
||||
* This used to be the other way around before gnucash 3.4. Before
|
||||
* that unknown schemes were always considered local file system
|
||||
* uri schemes.
|
||||
*/
|
||||
gboolean gnc_uri_is_file_scheme (const gchar *scheme)
|
||||
{
|
||||
return (scheme &&
|
||||
(!g_ascii_strcasecmp (scheme, "file") ||
|
||||
!g_ascii_strcasecmp (scheme, "xml") ||
|
||||
!g_ascii_strcasecmp (scheme, "sqlite3")));
|
||||
}
|
||||
|
||||
/* Checks if the given uri defines a file
|
||||
* (as opposed to a network service)
|
||||
*/
|
||||
gboolean gnc_uri_is_file_uri (const gchar *uri)
|
||||
{
|
||||
gchar *scheme = gnc_uri_get_scheme ( uri );
|
||||
gboolean result = gnc_uri_is_file_scheme ( scheme );
|
||||
|
||||
g_free ( scheme );
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Checks if the given uri is a valid uri
|
||||
*/
|
||||
gboolean gnc_uri_targets_local_fs (const gchar *uri)
|
||||
{
|
||||
|
||||
gchar *scheme = NULL, *hostname = NULL;
|
||||
gchar *username = NULL, *password = NULL;
|
||||
gchar *path = NULL;
|
||||
gint port = 0;
|
||||
gboolean is_local_fs = FALSE;
|
||||
|
||||
gnc_uri_get_components ( uri, &scheme, &hostname, &port,
|
||||
&username, &password, &path );
|
||||
|
||||
/* For gnucash to consider a uri to target the local fs:
|
||||
* path must not be NULL
|
||||
* AND
|
||||
* scheme should be NULL
|
||||
* OR
|
||||
* scheme must be file type scheme (file, xml, sqlite) */
|
||||
is_local_fs = (path && (!scheme || gnc_uri_is_file_scheme(scheme)));
|
||||
|
||||
g_free (scheme);
|
||||
g_free (hostname);
|
||||
g_free (username);
|
||||
g_free (password);
|
||||
g_free (path);
|
||||
|
||||
return is_local_fs;
|
||||
}
|
||||
|
||||
/* Splits a uri into its separate components */
|
||||
void gnc_uri_get_components (const gchar *uri,
|
||||
gchar **scheme,
|
||||
gchar **hostname,
|
||||
gint32 *port,
|
||||
gchar **username,
|
||||
gchar **password,
|
||||
gchar **path)
|
||||
{
|
||||
gchar **splituri;
|
||||
gchar *url = NULL, *tmpusername = NULL, *tmphostname = NULL;
|
||||
gchar *delimiter = NULL;
|
||||
|
||||
*scheme = NULL;
|
||||
*hostname = NULL;
|
||||
*port = 0;
|
||||
*username = NULL;
|
||||
*password = NULL;
|
||||
*path = NULL;
|
||||
|
||||
g_return_if_fail( uri != NULL && strlen (uri) > 0);
|
||||
|
||||
splituri = g_strsplit ( uri, "://", 2 );
|
||||
if ( splituri[1] == NULL )
|
||||
{
|
||||
/* No scheme means simple file path.
|
||||
Set path to copy of the input. */
|
||||
*path = g_strdup ( uri );
|
||||
g_strfreev ( splituri );
|
||||
return;
|
||||
}
|
||||
|
||||
/* At least a scheme was found, set it here */
|
||||
*scheme = g_strdup ( splituri[0] );
|
||||
|
||||
if ( gnc_uri_is_file_scheme ( *scheme ) )
|
||||
{
|
||||
/* a true file uri on windows can start file:///N:/
|
||||
so we come here with /N:/, it could also be /N:\
|
||||
*/
|
||||
if (g_str_has_prefix (splituri[1], "/") &&
|
||||
((g_strstr_len (splituri[1], -1, ":/") != NULL) || (g_strstr_len (splituri[1], -1, ":\\") != NULL)))
|
||||
{
|
||||
gchar *ptr = splituri[1];
|
||||
*path = gnc_resolve_file_path ( ptr + 1 );
|
||||
}
|
||||
else
|
||||
*path = gnc_resolve_file_path ( splituri[1] );
|
||||
g_strfreev ( splituri );
|
||||
return;
|
||||
}
|
||||
|
||||
/* Protocol indicates full network style uri, let's see if it
|
||||
* has a username and/or password
|
||||
*/
|
||||
url = g_strdup (splituri[1]);
|
||||
g_strfreev ( splituri );
|
||||
|
||||
/* Check for "@" sign, but start from the end - the password may contain
|
||||
* this sign as well
|
||||
*/
|
||||
delimiter = g_strrstr ( url, "@" );
|
||||
if ( delimiter != NULL )
|
||||
{
|
||||
/* There is at least a username in the url */
|
||||
delimiter[0] = '\0';
|
||||
tmpusername = url;
|
||||
tmphostname = delimiter + 1;
|
||||
|
||||
/* Check if there's a password too by looking for a :
|
||||
* Start from the beginning this time to avoid possible :
|
||||
* in the password */
|
||||
delimiter = g_strstr_len ( tmpusername, -1, ":" );
|
||||
if ( delimiter != NULL )
|
||||
{
|
||||
/* There is password in the url */
|
||||
delimiter[0] = '\0';
|
||||
*password = g_strdup ( (const gchar*)(delimiter + 1) );
|
||||
}
|
||||
*username = g_strdup ( (const gchar*)tmpusername );
|
||||
}
|
||||
else
|
||||
{
|
||||
/* No username and password were given */
|
||||
tmphostname = url;
|
||||
}
|
||||
|
||||
/* Find the path part */
|
||||
delimiter = g_strstr_len ( tmphostname, -1, "/" );
|
||||
if ( delimiter != NULL )
|
||||
{
|
||||
delimiter[0] = '\0';
|
||||
if ( gnc_uri_is_file_scheme ( *scheme ) ) /* always return absolute file paths */
|
||||
*path = gnc_resolve_file_path ( (const gchar*)(delimiter + 1) );
|
||||
else /* path is no file path, so copy it as is */
|
||||
*path = g_strdup ( (const gchar*)(delimiter + 1) );
|
||||
}
|
||||
|
||||
/* Check for a port specifier */
|
||||
delimiter = g_strstr_len ( tmphostname, -1, ":" );
|
||||
if ( delimiter != NULL )
|
||||
{
|
||||
delimiter[0] = '\0';
|
||||
*port = g_ascii_strtoll ( delimiter + 1, NULL, 0 );
|
||||
}
|
||||
|
||||
*hostname = g_strdup ( (const gchar*)tmphostname );
|
||||
|
||||
g_free ( url );
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
gchar *gnc_uri_get_scheme (const gchar *uri)
|
||||
{
|
||||
gchar *scheme = NULL;
|
||||
gchar *hostname = NULL;
|
||||
gint32 port = 0;
|
||||
gchar *username = NULL;
|
||||
gchar *password = NULL;
|
||||
gchar *path = NULL;
|
||||
|
||||
gnc_uri_get_components ( uri, &scheme, &hostname, &port,
|
||||
&username, &password, &path );
|
||||
|
||||
g_free (hostname);
|
||||
g_free (username);
|
||||
g_free (password);
|
||||
g_free (path);
|
||||
|
||||
return scheme;
|
||||
}
|
||||
|
||||
gchar *gnc_uri_get_path (const gchar *uri)
|
||||
{
|
||||
gchar *scheme = NULL;
|
||||
gchar *hostname = NULL;
|
||||
gint32 port = 0;
|
||||
gchar *username = NULL;
|
||||
gchar *password = NULL;
|
||||
gchar *path = NULL;
|
||||
|
||||
gnc_uri_get_components ( uri, &scheme, &hostname, &port,
|
||||
&username, &password, &path );
|
||||
|
||||
g_free (scheme);
|
||||
g_free (hostname);
|
||||
g_free (username);
|
||||
g_free (password);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/* Generates a normalized uri from the separate components */
|
||||
gchar *gnc_uri_create_uri (const gchar *scheme,
|
||||
const gchar *hostname,
|
||||
gint32 port,
|
||||
const gchar *username,
|
||||
const gchar *password,
|
||||
const gchar *path)
|
||||
{
|
||||
gchar *userpass = NULL, *portstr = NULL, *uri = NULL;
|
||||
|
||||
g_return_val_if_fail( path != 0, NULL );
|
||||
|
||||
if (!scheme || gnc_uri_is_file_scheme (scheme))
|
||||
{
|
||||
/* Compose a file based uri, which means ignore everything but
|
||||
* the scheme and the path
|
||||
* We return an absolute pathname if the scheme is known or
|
||||
* no scheme was given. For an unknown scheme, we return the
|
||||
* path info as is.
|
||||
*/
|
||||
gchar *abs_path;
|
||||
gchar *uri_scheme;
|
||||
if (scheme && (!gnc_uri_is_known_scheme (scheme)) )
|
||||
abs_path = g_strdup ( path );
|
||||
else
|
||||
abs_path = gnc_resolve_file_path ( path );
|
||||
|
||||
if (!scheme)
|
||||
uri_scheme = g_strdup ("file");
|
||||
else
|
||||
uri_scheme = g_strdup (scheme);
|
||||
|
||||
/* Arrive here with...
|
||||
*
|
||||
* /my/path/to/file with space.txt
|
||||
* becomes file:///my/path/to/file with space.txt
|
||||
*
|
||||
* c:\my\path\to\file with space.txt
|
||||
* becomes file:///c:\my\path\to\file with space.txt
|
||||
*
|
||||
* \\myserver\share\path\to\file with space.txt
|
||||
* becomes file://\\myserver\share\path\to\file with space.txt
|
||||
*
|
||||
* probably they should all be forward slashes and spaces escaped
|
||||
* also with UNC it could be file://myserver/share/path/to/file with space.txt
|
||||
*/
|
||||
|
||||
if (g_str_has_prefix (abs_path, "/") || g_str_has_prefix (abs_path, "\\"))
|
||||
uri = g_strdup_printf ( "%s://%s", uri_scheme, abs_path );
|
||||
else // for windows add an extra "/"
|
||||
uri = g_strdup_printf ( "%s:///%s", uri_scheme, abs_path );
|
||||
|
||||
g_free (uri_scheme);
|
||||
g_free (abs_path);
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
/* Not a file based uri, we need to setup all components that are not NULL
|
||||
* For this scenario, hostname is mandatory.
|
||||
*/
|
||||
g_return_val_if_fail( hostname != 0, NULL );
|
||||
|
||||
if ( username != NULL && *username )
|
||||
{
|
||||
if ( password != NULL && *password )
|
||||
userpass = g_strdup_printf ( "%s:%s@", username, password );
|
||||
else
|
||||
userpass = g_strdup_printf ( "%s@", username );
|
||||
}
|
||||
else
|
||||
userpass = g_strdup ( "" );
|
||||
|
||||
if ( port != 0 )
|
||||
portstr = g_strdup_printf ( ":%d", port );
|
||||
else
|
||||
portstr = g_strdup ( "" );
|
||||
|
||||
// XXX Do I have to add the slash always or are there situations
|
||||
// it is in the path already ?
|
||||
uri = g_strconcat ( scheme, "://", userpass, hostname, portstr, "/", path, NULL );
|
||||
|
||||
g_free ( userpass );
|
||||
g_free ( portstr );
|
||||
|
||||
return uri;
|
||||
|
||||
}
|
||||
|
||||
gchar *gnc_uri_normalize_uri (const gchar *uri, gboolean allow_password)
|
||||
{
|
||||
gchar *scheme = NULL;
|
||||
gchar *hostname = NULL;
|
||||
gint32 port = 0;
|
||||
gchar *username = NULL;
|
||||
gchar *password = NULL;
|
||||
gchar *path = NULL;
|
||||
gchar *newuri = NULL;
|
||||
|
||||
gnc_uri_get_components ( uri, &scheme, &hostname, &port,
|
||||
&username, &password, &path );
|
||||
if (allow_password)
|
||||
newuri = gnc_uri_create_uri ( scheme, hostname, port,
|
||||
username, password, path);
|
||||
else
|
||||
newuri = gnc_uri_create_uri ( scheme, hostname, port,
|
||||
username, /* no password */ NULL, path);
|
||||
|
||||
g_free (scheme);
|
||||
g_free (hostname);
|
||||
g_free (username);
|
||||
g_free (password);
|
||||
g_free (path);
|
||||
|
||||
return newuri;
|
||||
}
|
||||
|
||||
gchar *gnc_uri_add_extension ( const gchar *uri, const gchar *extension )
|
||||
{
|
||||
g_return_val_if_fail( uri != 0, NULL );
|
||||
|
||||
/* Only add extension if the user provided the extension and the uri is
|
||||
* file based.
|
||||
*/
|
||||
if ( !extension || !gnc_uri_is_file_uri( uri ) )
|
||||
return g_strdup( uri );
|
||||
|
||||
/* Don't add extension if it's already there */
|
||||
if ( g_str_has_suffix( uri, extension ) )
|
||||
return g_strdup( uri );
|
||||
|
||||
/* Ok, all tests passed, let's add the extension */
|
||||
return g_strconcat( uri, extension, NULL );
|
||||
}
|
||||
@ -0,0 +1,408 @@
|
||||
/*
|
||||
* gnc-uri.cpp -- utility functions to convert uri in separate
|
||||
* components and back.
|
||||
*
|
||||
* Copyright (C) 2010 Geert Janssens <janssens.geert@telenet.be>
|
||||
* Copyright (C) 2026 Brent McBride <mcbridebt@hotmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, contact:
|
||||
*
|
||||
* Free Software Foundation Voice: +1-617-542-5942
|
||||
* 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652
|
||||
* Boston, MA 02110-1301, USA gnu@gnu.org
|
||||
*/
|
||||
|
||||
#include <glib.h>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include "gnc-uri-utils.h"
|
||||
#include "gnc-uri.hpp"
|
||||
#include "gnc-filepath-utils.h"
|
||||
#include "qofsession.h"
|
||||
|
||||
/* Duplicates an optional component into a freshly allocated C string, or
|
||||
* returns nullptr when the component is absent. This preserves the public C
|
||||
* API's contract that returned strings (or NULL) are released with g_free(). */
|
||||
static gchar *
|
||||
dup_or_null (const std::optional<std::string>& component)
|
||||
{
|
||||
return component ? g_strdup (component->c_str()) : nullptr;
|
||||
}
|
||||
|
||||
/* Wraps a possibly-null C string as an optional, mapping NULL to nullopt so
|
||||
* the historical "absent vs empty" distinction is preserved when crossing
|
||||
* from the C veneers into the GncUri class. */
|
||||
static std::optional<std::string>
|
||||
to_opt (const gchar *s)
|
||||
{
|
||||
return s ? std::optional<std::string> { s } : std::nullopt;
|
||||
}
|
||||
|
||||
/* True when the scheme is one of the registered backend access methods. */
|
||||
static bool
|
||||
scheme_is_known (const gchar *scheme)
|
||||
{
|
||||
bool is_known_scheme = false;
|
||||
GList *known_scheme_list = qof_backend_get_registered_access_method_list();
|
||||
|
||||
for ( GList *node = known_scheme_list; node != nullptr; node = node->next )
|
||||
{
|
||||
gchar *known_scheme = static_cast<gchar*>(node->data);
|
||||
if ( !g_ascii_strcasecmp (scheme, known_scheme) )
|
||||
{
|
||||
is_known_scheme = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
g_list_free (known_scheme_list);
|
||||
return is_known_scheme;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* GncUri - C++ interface
|
||||
*
|
||||
* The class below carries all of the parsing and composition logic. The
|
||||
* extern "C" functions further down are thin veneers over it, preserving the
|
||||
* historical gchar* / g_free contract for C (and not-yet-migrated C++)
|
||||
* callers.
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
GncUri::GncUri (std::optional<std::string> scheme,
|
||||
std::optional<std::string> hostname,
|
||||
int32_t port,
|
||||
std::optional<std::string> username,
|
||||
std::optional<std::string> password,
|
||||
std::optional<std::string> path)
|
||||
: m_scheme (std::move (scheme))
|
||||
, m_hostname (std::move (hostname))
|
||||
, m_username (std::move (username))
|
||||
, m_password (std::move (password))
|
||||
, m_path (std::move (path))
|
||||
, m_port (port)
|
||||
{
|
||||
/* A GncUri built from components must be able to form a valid uri: it
|
||||
* needs a path, and a non-file scheme also needs a hostname. Failing here
|
||||
* keeps every constructed GncUri valid, so str()/try_str() never have to
|
||||
* re-check. (The parsing ctor below is deliberately permissive instead.) */
|
||||
if (!m_path)
|
||||
throw std::invalid_argument ("GncUri: a path is required");
|
||||
if (m_scheme && !scheme_is_file (*m_scheme) && !m_hostname)
|
||||
throw std::invalid_argument (
|
||||
"GncUri: a hostname is required for a non-file scheme");
|
||||
}
|
||||
|
||||
GncUri::GncUri (const std::string& uri)
|
||||
{
|
||||
if (uri.empty())
|
||||
return;
|
||||
|
||||
auto sep = uri.find ("://");
|
||||
if (sep == std::string::npos)
|
||||
{
|
||||
/* No scheme means a simple file path; the path is a copy of the input. */
|
||||
m_path = uri;
|
||||
return;
|
||||
}
|
||||
|
||||
std::string scheme = uri.substr (0, sep);
|
||||
std::string rest = uri.substr (sep + 3);
|
||||
m_scheme = scheme;
|
||||
|
||||
if (scheme_is_file (scheme))
|
||||
{
|
||||
/* A true file uri on windows can start with file:///N:/, so we arrive
|
||||
* here with /N:/ (it could also be /N:\). Strip the leading slash in
|
||||
* that case before resolving. */
|
||||
const gchar *file_path = rest.c_str();
|
||||
if (!rest.empty() && rest.front() == '/' &&
|
||||
(rest.find (":/") != std::string::npos ||
|
||||
rest.find (":\\") != std::string::npos))
|
||||
file_path = rest.c_str() + 1;
|
||||
|
||||
gchar *resolved = gnc_resolve_file_path (file_path);
|
||||
m_path = std::string { resolved };
|
||||
g_free (resolved);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Network style uri: [user[:password]@]hostname[:port][/path].
|
||||
* Look for the '@' from the end, as the password may contain one too. */
|
||||
std::string hostpart;
|
||||
auto at = rest.rfind ('@');
|
||||
if (at != std::string::npos)
|
||||
{
|
||||
std::string userinfo = rest.substr (0, at);
|
||||
hostpart = rest.substr (at + 1);
|
||||
|
||||
/* Look for a password, searching from the start so a ':' in the
|
||||
* password doesn't confuse the split. */
|
||||
auto colon = userinfo.find (':');
|
||||
if (colon != std::string::npos)
|
||||
{
|
||||
m_username = userinfo.substr (0, colon);
|
||||
m_password = userinfo.substr (colon + 1);
|
||||
}
|
||||
else
|
||||
m_username = userinfo;
|
||||
}
|
||||
else
|
||||
hostpart = rest;
|
||||
|
||||
/* Split off the path part. */
|
||||
auto slash = hostpart.find ('/');
|
||||
if (slash != std::string::npos)
|
||||
{
|
||||
m_path = hostpart.substr (slash + 1);
|
||||
hostpart.erase (slash);
|
||||
}
|
||||
|
||||
/* Split off an optional port specifier. */
|
||||
auto port_colon = hostpart.find (':');
|
||||
if (port_colon != std::string::npos)
|
||||
{
|
||||
m_port = static_cast<int32_t> (
|
||||
g_ascii_strtoll (hostpart.c_str() + port_colon + 1, nullptr, 0));
|
||||
hostpart.erase (port_colon);
|
||||
}
|
||||
|
||||
m_hostname = hostpart;
|
||||
}
|
||||
|
||||
bool
|
||||
GncUri::scheme_is_file (const std::string& scheme) noexcept
|
||||
{
|
||||
return (!g_ascii_strcasecmp (scheme.c_str(), "file") ||
|
||||
!g_ascii_strcasecmp (scheme.c_str(), "xml") ||
|
||||
!g_ascii_strcasecmp (scheme.c_str(), "sqlite3"));
|
||||
}
|
||||
|
||||
bool
|
||||
GncUri::is_file_uri () const noexcept
|
||||
{
|
||||
return m_scheme && scheme_is_file (*m_scheme);
|
||||
}
|
||||
|
||||
bool
|
||||
GncUri::targets_local_fs () const noexcept
|
||||
{
|
||||
/* Targets the local fs when it has a path and either no scheme or a
|
||||
* file-type scheme (file, xml, sqlite). */
|
||||
return m_path && (!m_scheme || scheme_is_file (*m_scheme));
|
||||
}
|
||||
|
||||
std::optional<std::string>
|
||||
GncUri::try_str (bool allow_password) const
|
||||
{
|
||||
if (!m_path)
|
||||
return std::nullopt;
|
||||
|
||||
const std::string& path = *m_path;
|
||||
|
||||
if (!m_scheme || scheme_is_file (*m_scheme))
|
||||
{
|
||||
/* File based uri: ignore everything but the scheme and the path. The
|
||||
* path is resolved to an absolute name for a known (or absent) scheme;
|
||||
* for an unknown scheme it is used as is. */
|
||||
gchar *resolved = (m_scheme && !scheme_is_known (m_scheme->c_str()))
|
||||
? g_strdup (path.c_str())
|
||||
: gnc_resolve_file_path (path.c_str());
|
||||
std::string abs_path { resolved };
|
||||
g_free (resolved);
|
||||
|
||||
std::string scheme = m_scheme ? *m_scheme : std::string { "file" };
|
||||
|
||||
/* Arrive here with...
|
||||
*
|
||||
* /my/path/to/file with space.txt
|
||||
* becomes file:///my/path/to/file with space.txt
|
||||
*
|
||||
* c:\my\path\to\file with space.txt
|
||||
* becomes file:///c:\my\path\to\file with space.txt
|
||||
*
|
||||
* \\myserver\share\path\to\file with space.txt
|
||||
* becomes file://\\myserver\share\path\to\file with space.txt
|
||||
*/
|
||||
bool absolute = !abs_path.empty() &&
|
||||
(abs_path.front() == '/' || abs_path.front() == '\\');
|
||||
return absolute ? scheme + "://" + abs_path
|
||||
: scheme + ":///" + abs_path; // extra "/" for windows
|
||||
}
|
||||
|
||||
std::string userpass;
|
||||
if (m_username && !m_username->empty())
|
||||
{
|
||||
userpass = *m_username;
|
||||
if (allow_password && m_password && !m_password->empty())
|
||||
{
|
||||
userpass += ':';
|
||||
userpass += *m_password;
|
||||
}
|
||||
userpass += '@';
|
||||
}
|
||||
|
||||
std::string portstr;
|
||||
if (m_port != 0)
|
||||
portstr = ":" + std::to_string (m_port);
|
||||
|
||||
return *m_scheme + "://" + userpass + *m_hostname + portstr + "/" + path;
|
||||
}
|
||||
|
||||
std::string
|
||||
GncUri::str (bool allow_password) const
|
||||
{
|
||||
if (std::optional<std::string> result = try_str (allow_password))
|
||||
return std::move (*result);
|
||||
|
||||
/* try_str only fails when there is no path; a non-file scheme without a
|
||||
* hostname is already rejected by the component constructor. */
|
||||
throw std::invalid_argument ("GncUri::str: a path is required");
|
||||
}
|
||||
|
||||
/* Checks if the given scheme is used to refer to a file
|
||||
* (as opposed to a network service)
|
||||
* Note unknown schemes are always considered network schemes.
|
||||
*
|
||||
* *Compatibility note:*
|
||||
* This used to be the other way around before gnucash 3.4. Before
|
||||
* that unknown schemes were always considered local file system
|
||||
* uri schemes.
|
||||
*/
|
||||
gboolean
|
||||
gnc_uri_is_file_scheme (const gchar *scheme)
|
||||
{
|
||||
return scheme && GncUri::scheme_is_file (scheme);
|
||||
}
|
||||
|
||||
/* Checks if the given uri defines a file
|
||||
* (as opposed to a network service)
|
||||
*/
|
||||
gboolean
|
||||
gnc_uri_is_file_uri (const gchar *uri)
|
||||
{
|
||||
g_return_val_if_fail (uri != nullptr, FALSE);
|
||||
return GncUri { uri }.is_file_uri();
|
||||
}
|
||||
|
||||
/* Checks if the given uri is a valid uri
|
||||
*/
|
||||
gboolean
|
||||
gnc_uri_targets_local_fs (const gchar *uri)
|
||||
{
|
||||
g_return_val_if_fail (uri != nullptr, FALSE);
|
||||
return GncUri { uri }.targets_local_fs();
|
||||
}
|
||||
|
||||
/* Splits a uri into its separate components. Thin C veneer over the GncUri
|
||||
* class; each component is handed back as a freshly allocated string (or NULL
|
||||
* when absent) that the caller frees with g_free(). */
|
||||
void
|
||||
gnc_uri_get_components (const gchar *uri,
|
||||
gchar **scheme,
|
||||
gchar **hostname,
|
||||
gint32 *port,
|
||||
gchar **username,
|
||||
gchar **password,
|
||||
gchar **path)
|
||||
{
|
||||
*scheme = nullptr;
|
||||
*hostname = nullptr;
|
||||
*port = 0;
|
||||
*username = nullptr;
|
||||
*password = nullptr;
|
||||
*path = nullptr;
|
||||
|
||||
g_return_if_fail( uri != nullptr && strlen (uri) > 0);
|
||||
|
||||
GncUri parsed { uri };
|
||||
|
||||
*scheme = dup_or_null (parsed.scheme());
|
||||
*hostname = dup_or_null (parsed.hostname());
|
||||
*username = dup_or_null (parsed.username());
|
||||
*password = dup_or_null (parsed.password());
|
||||
*path = dup_or_null (parsed.path());
|
||||
*port = parsed.port();
|
||||
}
|
||||
|
||||
gchar *
|
||||
gnc_uri_get_scheme (const gchar *uri)
|
||||
{
|
||||
g_return_val_if_fail (uri != nullptr, nullptr);
|
||||
return dup_or_null (GncUri { uri }.scheme());
|
||||
}
|
||||
|
||||
gchar *
|
||||
gnc_uri_get_path (const gchar *uri)
|
||||
{
|
||||
g_return_val_if_fail (uri != nullptr, nullptr);
|
||||
return dup_or_null (GncUri { uri }.path());
|
||||
}
|
||||
|
||||
/* Generates a normalized uri from the separate components */
|
||||
gchar *
|
||||
gnc_uri_create_uri (const gchar *scheme,
|
||||
const gchar *hostname,
|
||||
gint32 port,
|
||||
const gchar *username,
|
||||
const gchar *password,
|
||||
const gchar *path)
|
||||
{
|
||||
g_return_val_if_fail( path != nullptr, nullptr );
|
||||
|
||||
/* For a non-file scheme a hostname is mandatory. (A missing scheme is
|
||||
* treated as a file scheme, which needs no hostname.) */
|
||||
if (scheme && !GncUri::scheme_is_file (scheme))
|
||||
g_return_val_if_fail( hostname != nullptr, nullptr );
|
||||
|
||||
GncUri uri { to_opt (scheme), to_opt (hostname), port,
|
||||
to_opt (username), to_opt (password), to_opt (path) };
|
||||
return g_strdup (uri.str().c_str());
|
||||
}
|
||||
|
||||
gchar *
|
||||
gnc_uri_normalize_uri (const gchar *uri, gboolean allow_password)
|
||||
{
|
||||
g_return_val_if_fail (uri != nullptr, nullptr);
|
||||
|
||||
GncUri parsed { uri };
|
||||
return gnc_uri_create_uri (
|
||||
parsed.scheme() ? parsed.scheme()->c_str() : nullptr,
|
||||
parsed.hostname() ? parsed.hostname()->c_str() : nullptr,
|
||||
parsed.port(),
|
||||
parsed.username() ? parsed.username()->c_str() : nullptr,
|
||||
(allow_password && parsed.password()) ? parsed.password()->c_str() : nullptr,
|
||||
parsed.path() ? parsed.path()->c_str() : nullptr );
|
||||
}
|
||||
|
||||
gchar *
|
||||
gnc_uri_add_extension ( const gchar *uri, const gchar *extension )
|
||||
{
|
||||
g_return_val_if_fail( uri != nullptr, nullptr );
|
||||
|
||||
/* Only add extension if the user provided the extension and the uri is
|
||||
* file based.
|
||||
*/
|
||||
if ( !extension || !gnc_uri_is_file_uri( uri ) )
|
||||
return g_strdup( uri );
|
||||
|
||||
/* Don't add extension if it's already there */
|
||||
if ( g_str_has_suffix( uri, extension ) )
|
||||
return g_strdup( uri );
|
||||
|
||||
/* Ok, all tests passed, let's add the extension */
|
||||
return g_strconcat( uri, extension, nullptr );
|
||||
}
|
||||
@ -0,0 +1,119 @@
|
||||
/********************************************************************\
|
||||
* gnc-uri.hpp -- C++ interface to parse and compose uris. *
|
||||
* *
|
||||
* Copyright (C) 2026 Brent McBride <mcbridebt@hotmail.com> *
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or *
|
||||
* modify it under the terms of the GNU General Public License as *
|
||||
* published by the Free Software Foundation; either version 2 of *
|
||||
* the License, or (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License*
|
||||
* along with this program; if not, contact: *
|
||||
* *
|
||||
* Free Software Foundation Voice: +1-617-542-5942 *
|
||||
* 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
|
||||
* Boston, MA 02110-1301, USA gnu@gnu.org *
|
||||
* *
|
||||
\********************************************************************/
|
||||
|
||||
#ifndef GNC_URI_HPP
|
||||
#define GNC_URI_HPP
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
/** @addtogroup Engine
|
||||
@{ */
|
||||
/** @file gnc-uri.hpp
|
||||
* @brief C++ interface to parse and compose GnuCash resource locators.
|
||||
* @author Copyright (C) 2026 Brent McBride <mcbridebt@hotmail.com>
|
||||
*
|
||||
* GnuCash refers to the books it stores by a uri, which may be a network
|
||||
* service (such as a database) or a local filesystem path. This is the C++
|
||||
* face of that utility; the C functions in gnc-uri-utils.h are thin veneers
|
||||
* over the class below and remain available for not-yet-migrated callers.
|
||||
*/
|
||||
|
||||
/** A parsed GnuCash resource locator.
|
||||
*
|
||||
* Construct one from a uri (or a bare local filesystem path) to inspect its
|
||||
* components, or from individual components to compose a normalized uri with
|
||||
* str(). Components that are absent from a uri are reported as std::nullopt,
|
||||
* preserving the historical distinction between a missing component and one
|
||||
* that is present but empty.
|
||||
*/
|
||||
class GncUri
|
||||
{
|
||||
public:
|
||||
/** Parse a uri, or a bare local filesystem path, into its components.
|
||||
* An empty string yields an empty GncUri (all components absent). */
|
||||
explicit GncUri (const std::string& uri);
|
||||
|
||||
/** Construct directly from individual components, typically to compose a
|
||||
* uri with str(). Absent components are represented by std::nullopt. */
|
||||
GncUri (std::optional<std::string> scheme,
|
||||
std::optional<std::string> hostname,
|
||||
int32_t port,
|
||||
std::optional<std::string> username,
|
||||
std::optional<std::string> password,
|
||||
std::optional<std::string> path);
|
||||
|
||||
const std::optional<std::string>& scheme() const noexcept { return m_scheme; }
|
||||
const std::optional<std::string>& hostname() const noexcept { return m_hostname; }
|
||||
const std::optional<std::string>& username() const noexcept { return m_username; }
|
||||
const std::optional<std::string>& password() const noexcept { return m_password; }
|
||||
const std::optional<std::string>& path() const noexcept { return m_path; }
|
||||
int32_t port() const noexcept { return m_port; }
|
||||
|
||||
/** True if this uri uses a file-type scheme (file, xml, sqlite3). A uri
|
||||
* without a scheme is not considered a file uri (matching the historical
|
||||
* gnc_uri_is_file_uri behaviour). */
|
||||
bool is_file_uri() const noexcept;
|
||||
|
||||
/** True if the uri refers to the local filesystem: it has a path and
|
||||
* either no scheme or a file-type scheme. */
|
||||
bool targets_local_fs() const noexcept;
|
||||
|
||||
/** Compose a normalized uri string from the components.
|
||||
*
|
||||
* @param allow_password When false, any password is omitted from the
|
||||
* result.
|
||||
* @return The composed uri. For a file-type (or absent) scheme the path
|
||||
* is resolved to an absolute name.
|
||||
* @throws std::invalid_argument when no path is present, or when a
|
||||
* non-file scheme is missing its hostname.
|
||||
*/
|
||||
std::string str (bool allow_password = true) const;
|
||||
|
||||
/** Like str(), but returns std::nullopt instead of throwing when the
|
||||
* components cannot form a valid uri (no path is present, or a non-file
|
||||
* scheme is missing its hostname). Intended for callers that historically
|
||||
* tolerated a NULL result from gnc_uri_normalize_uri / gnc_uri_create_uri.
|
||||
*
|
||||
* @param allow_password When false, any password is omitted from the
|
||||
* result.
|
||||
*/
|
||||
std::optional<std::string> try_str (bool allow_password = true) const;
|
||||
|
||||
/** True if @a scheme is a file-type scheme (file, xml, sqlite3). */
|
||||
static bool scheme_is_file (const std::string& scheme) noexcept;
|
||||
|
||||
private:
|
||||
std::optional<std::string> m_scheme;
|
||||
std::optional<std::string> m_hostname;
|
||||
std::optional<std::string> m_username;
|
||||
std::optional<std::string> m_password;
|
||||
std::optional<std::string> m_path;
|
||||
int32_t m_port = 0;
|
||||
};
|
||||
|
||||
/** @} */
|
||||
|
||||
#endif /* GNC_URI_HPP */
|
||||
@ -0,0 +1,170 @@
|
||||
/********************************************************************\
|
||||
* gtest-gnc-uri.cpp -- Unit tests for the GncUri C++ class. *
|
||||
* *
|
||||
* Copyright (C) 2026 Brent McBride <mcbridebt@hotmail.com> *
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or *
|
||||
* modify it under the terms of the GNU General Public License as *
|
||||
* published by the Free Software Foundation; either version 2 of *
|
||||
* the License, or (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License*
|
||||
* along with this program; if not, contact: *
|
||||
* *
|
||||
* Free Software Foundation Voice: +1-617-542-5942 *
|
||||
* 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
|
||||
* Boston, MA 02110-1301, USA gnu@gnu.org *
|
||||
* *
|
||||
\********************************************************************/
|
||||
|
||||
#include <config.h>
|
||||
#include <glib.h>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include "qof.h"
|
||||
#include "gnc-backend-prov.hpp"
|
||||
#include "gnc-uri-utils.h"
|
||||
#include "gnc-uri.hpp"
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wcpp"
|
||||
#include <gtest/gtest.h>
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
/* Parse a file uri into its components. */
|
||||
TEST(GncUri, ParseFileUri)
|
||||
{
|
||||
GncUri f { "file:///test/path/file.gnucash" };
|
||||
ASSERT_TRUE (f.scheme().has_value());
|
||||
EXPECT_EQ (*f.scheme(), "file");
|
||||
EXPECT_FALSE (f.hostname().has_value());
|
||||
ASSERT_TRUE (f.path().has_value());
|
||||
EXPECT_EQ (*f.path(), "/test/path/file.gnucash");
|
||||
EXPECT_TRUE (f.is_file_uri());
|
||||
EXPECT_TRUE (f.targets_local_fs());
|
||||
}
|
||||
|
||||
/* Parse a database uri with userinfo and a port. */
|
||||
TEST(GncUri, ParseDatabaseUri)
|
||||
{
|
||||
GncUri d { "postgres://dbuser:dbpass@www.gnucash.org:744/gnucash" };
|
||||
ASSERT_TRUE (d.scheme().has_value());
|
||||
EXPECT_EQ (*d.scheme(), "postgres");
|
||||
ASSERT_TRUE (d.hostname().has_value());
|
||||
EXPECT_EQ (*d.hostname(), "www.gnucash.org");
|
||||
ASSERT_TRUE (d.username().has_value());
|
||||
EXPECT_EQ (*d.username(), "dbuser");
|
||||
ASSERT_TRUE (d.password().has_value());
|
||||
EXPECT_EQ (*d.password(), "dbpass");
|
||||
EXPECT_EQ (d.port(), 744);
|
||||
EXPECT_FALSE (d.is_file_uri());
|
||||
EXPECT_FALSE (d.targets_local_fs());
|
||||
}
|
||||
|
||||
/* Compose a uri, with and without the password. */
|
||||
TEST(GncUri, ComposeWithAndWithoutPassword)
|
||||
{
|
||||
GncUri d { "postgres://dbuser:dbpass@www.gnucash.org:744/gnucash" };
|
||||
EXPECT_EQ (d.str (true), "postgres://dbuser:dbpass@www.gnucash.org:744/gnucash");
|
||||
EXPECT_EQ (d.str (false), "postgres://dbuser@www.gnucash.org:744/gnucash");
|
||||
EXPECT_EQ (d.try_str (false).value_or (""),
|
||||
"postgres://dbuser@www.gnucash.org:744/gnucash");
|
||||
}
|
||||
|
||||
/* A bare path has no scheme but still targets the local fs. */
|
||||
TEST(GncUri, BarePath)
|
||||
{
|
||||
GncUri p { "/test/path/file.gnucash" };
|
||||
EXPECT_FALSE (p.scheme().has_value());
|
||||
ASSERT_TRUE (p.path().has_value());
|
||||
EXPECT_EQ (*p.path(), "/test/path/file.gnucash");
|
||||
EXPECT_FALSE (p.is_file_uri()); /* no scheme -> not a file *uri* */
|
||||
EXPECT_TRUE (p.targets_local_fs());
|
||||
}
|
||||
|
||||
/* An empty uri yields an object with no components set. */
|
||||
TEST(GncUri, EmptyUri)
|
||||
{
|
||||
GncUri e { std::string {} };
|
||||
EXPECT_FALSE (e.scheme().has_value());
|
||||
EXPECT_FALSE (e.hostname().has_value());
|
||||
EXPECT_FALSE (e.path().has_value());
|
||||
EXPECT_EQ (e.port(), 0);
|
||||
EXPECT_FALSE (e.is_file_uri());
|
||||
}
|
||||
|
||||
/* A file uri carrying a Windows-style drive letter (file:///N:/...) has its
|
||||
* leading slash stripped before the path is resolved. The shape of the string
|
||||
* is what triggers this, so it exercises the branch on any platform. */
|
||||
TEST(GncUri, WindowsStyleDriveLetterPath)
|
||||
{
|
||||
GncUri w { "file:///N:/test/path/file.gnucash" };
|
||||
ASSERT_TRUE (w.scheme().has_value());
|
||||
EXPECT_EQ (*w.scheme(), "file");
|
||||
EXPECT_TRUE (w.path().has_value());
|
||||
EXPECT_TRUE (w.is_file_uri());
|
||||
}
|
||||
|
||||
/* Compose from individual components (mirrors gnc_uri_create_uri). With no
|
||||
* backend registered "xml" is an unknown scheme, so the relative path is used
|
||||
* as is. */
|
||||
TEST(GncUri, ComposeFromComponents)
|
||||
{
|
||||
GncUri c { std::string {"xml"}, std::nullopt, 0, std::nullopt, std::nullopt,
|
||||
std::string {"relative/path/file.gnucash"} };
|
||||
EXPECT_EQ (c.str(), "xml:///relative/path/file.gnucash");
|
||||
}
|
||||
|
||||
/* The component constructor rejects parts that can't form a valid uri: no path
|
||||
* at all, or a non-file scheme with a path but no hostname. */
|
||||
TEST(GncUri, ComponentCtorThrowsWhenIncomplete)
|
||||
{
|
||||
EXPECT_THROW ((GncUri { std::nullopt, std::nullopt, 0, std::nullopt,
|
||||
std::nullopt, std::nullopt }),
|
||||
std::invalid_argument);
|
||||
EXPECT_THROW ((GncUri { std::string {"postgres"}, std::nullopt, 0,
|
||||
std::nullopt, std::nullopt,
|
||||
std::string {"gnucash"} }),
|
||||
std::invalid_argument);
|
||||
}
|
||||
|
||||
/* The parsing ctor stays permissive, so a parsed uri can still lack a path (a
|
||||
* network scheme with a host but no path). Such an object can't be turned back
|
||||
* into a locator: try_str() returns nullopt and str() throws. */
|
||||
TEST(GncUri, ParsedUriWithoutPathCannotStringify)
|
||||
{
|
||||
GncUri incomplete { "postgres://www.gnucash.org" };
|
||||
EXPECT_FALSE (incomplete.try_str().has_value());
|
||||
EXPECT_THROW (incomplete.str(), std::invalid_argument);
|
||||
}
|
||||
|
||||
/* A minimal backend provider used only to populate the list of registered
|
||||
* access methods that the uri code consults. */
|
||||
struct UriTestProvider : public QofBackendProvider
|
||||
{
|
||||
UriTestProvider (const char* name, const char* method)
|
||||
: QofBackendProvider {name, method} {}
|
||||
QofBackend* create_backend (void) override { return nullptr; }
|
||||
bool type_check (const char*) override { return false; }
|
||||
};
|
||||
|
||||
/* Registering a provider whose access method matches a file-type scheme
|
||||
* ("xml") makes the known-scheme lookup find a match, so a uri composed for
|
||||
* that scheme has its path resolved instead of being used as is. An absolute
|
||||
* path resolves to itself, keeping the result predictable. */
|
||||
TEST(GncUri, KnownScheme)
|
||||
{
|
||||
qof_backend_register_provider (
|
||||
QofBackendProvider_ptr {new UriTestProvider {"Test Backend", "xml"}});
|
||||
|
||||
GncUri uri { std::string {"xml"}, std::nullopt, 0, std::nullopt,
|
||||
std::nullopt, std::string {"/test/path/file.gnucash"} };
|
||||
EXPECT_EQ (uri.str(), "xml:///test/path/file.gnucash");
|
||||
|
||||
qof_backend_unregister_all_providers ();
|
||||
}
|
||||
Loading…
Reference in new issue