Merge Brent McBride's 'uri-utils-cpp' into future.

pull/2252/head
John Ralls 5 days ago
commit 287c802d8c

@ -70,7 +70,7 @@
#include "gnc-ui.h"
#include "gnc-ui-util.h"
#include <gnc-glib-utils.h>
#include "gnc-uri-utils.h"
#include "gnc-uri.hpp"
#include "gnc-version.h"
#include "gnc-warnings.h"
#include "gnc-window.h"
@ -1567,19 +1567,19 @@ gnc_main_window_generate_title (GncMainWindow *window)
filename = g_strdup(_("Unsaved Book"));
else
{
if (gnc_uri_targets_local_fs (uri))
GncUri parsed { uri };
if (parsed.targets_local_fs ())
{
/* The filename is a true file.
The Gnome HIG 2.0 recommends only the file name (no path) be used. (p15) */
gchar *path = gnc_uri_get_path ( uri );
filename = g_path_get_basename ( path );
g_free ( path );
filename = g_path_get_basename ( parsed.path ()->c_str () );
}
else
{
/* The filename is composed of database connection parameters.
For this we will show access_method://username@database[:port] */
filename = gnc_uri_normalize_uri (uri, FALSE);
auto normalized = parsed.try_str (false);
filename = normalized ? g_strdup (normalized->c_str ()) : nullptr;
}
}
@ -1733,11 +1733,12 @@ static gchar *generate_statusbar_lastmodified_message()
return nullptr;
else
{
if (gnc_uri_targets_local_fs (uri))
GncUri parsed { uri };
if (parsed.targets_local_fs ())
{
/* The filename is a true file. */
gchar *filepath = gnc_uri_get_path ( uri );
gchar *filename = g_path_get_basename ( filepath );
std::string filepath = parsed.path ().value_or ("");
gchar *filename = g_path_get_basename ( filepath.c_str () );
GFile *file = g_file_new_for_uri (uri);
GFileInfo *info = g_file_query_info (file,
G_FILE_ATTRIBUTE_TIME_MODIFIED,
@ -1748,7 +1749,7 @@ static gchar *generate_statusbar_lastmodified_message()
{
// Access the mtime information through stat(2)
struct stat statbuf;
int r = stat(filepath, &statbuf);
int r = stat(filepath.c_str(), &statbuf);
if (r == 0)
{
/* Translators: This is the date and time that is shown in
@ -1766,12 +1767,11 @@ static gchar *generate_statusbar_lastmodified_message()
}
else
{
g_warning("Unable to read mtime for file %s\n", filepath);
g_warning("Unable to read mtime for file %s\n", filepath.c_str());
// message is still nullptr
}
}
g_free(filename);
g_free(filepath);
g_object_unref (info);
g_object_unref (file);
}
@ -5480,7 +5480,12 @@ add_about_paths (GtkDialog *dialog)
for (const auto& ep : ep_vec)
{
gchar *env_name = g_strconcat (ep.env_name, ":", nullptr);
const gchar *uri = gnc_uri_create_uri ("file", nullptr, 0, nullptr, nullptr, ep.env_path);
GncUri file_uri { std::string {"file"}, std::nullopt, 0, std::nullopt,
std::nullopt,
ep.env_path ? std::optional<std::string> { ep.env_path }
: std::nullopt };
auto uri_str = file_uri.try_str ();
const gchar *uri = uri_str ? uri_str->c_str () : nullptr;
gchar *display_uri = gnc_doclink_get_unescaped_just_uri (uri);
gchar *url_tag = g_strdup_printf ("%s%d", "url_tag", row);

@ -40,7 +40,6 @@
#include "gnucash-register.h"
#include "gnc-prefs.h"
#include "gnc-ui-util.h"
#include "gnc-uri-utils.h"
#include "gnc-window.h"
#include "dialog-utils.h"
#include "dialog-doclink.h"

@ -37,7 +37,7 @@
#include <cstdint>
#include "gnc-ui.h"
#include "gnc-uri-utils.h"
#include "gnc-uri.hpp"
#include "gnc-ui-util.h"
#include "dialog-utils.h"
@ -738,8 +738,8 @@ CsvImpPriceAssist::check_for_valid_filename ()
return false;
}
auto filepath = gnc_uri_get_path (file_name);
auto starting_dir = g_path_get_dirname (filepath);
auto filepath = GncUri{file_name}.path().value_or ("");
auto starting_dir = g_path_get_dirname (filepath.c_str());
m_fc_file_name = file_name;
gnc_set_default_directory (GNC_PREFS_GROUP, starting_dir);
@ -747,7 +747,6 @@ CsvImpPriceAssist::check_for_valid_filename ()
DEBUG("file_name selected is %s", m_fc_file_name.c_str());
DEBUG("starting directory is %s", starting_dir);
g_free (filepath);
g_free (file_name);
g_free (starting_dir);

@ -40,7 +40,7 @@
#include "gnc-path.h"
#include "gnc-ui.h"
#include "gnc-uri-utils.h"
#include "gnc-uri.hpp"
#include "gnc-ui-util.h"
#include "dialog-utils.h"
@ -714,8 +714,8 @@ CsvImpTransAssist::check_for_valid_filename ()
return false;
}
auto filepath = gnc_uri_get_path (file_name);
auto starting_dir = g_path_get_dirname (filepath);
auto filepath = GncUri{file_name}.path().value_or ("");
auto starting_dir = g_path_get_dirname (filepath.c_str());
m_fc_file_name = file_name;
gnc_set_default_directory (GNC_PREFS_GROUP, starting_dir);
@ -723,7 +723,6 @@ CsvImpTransAssist::check_for_valid_filename ()
DEBUG("file_name selected is %s", m_fc_file_name.c_str());
DEBUG("starting directory is %s", starting_dir);
g_free (filepath);
g_free (file_name);
g_free (starting_dir);

@ -47,7 +47,7 @@
#include "SX-book.h"
#include "Recurrence.h"
#include <gnc-features.h>
#include "gnc-uri-utils.h"
#include "gnc-uri.hpp"
#include "gnc-filepath-utils.h"
#include <gnc-path.h>
#include "gnc-locale-utils.h"
@ -132,24 +132,13 @@ struct UriStrings
UriStrings::UriStrings(const std::string& uri)
{
gchar *scheme, *host, *username, *password, *dbname;
int portnum;
gnc_uri_get_components(uri.c_str(), &scheme, &host, &portnum, &username,
&password, &dbname);
m_protocol = std::string{scheme};
m_host = std::string{host};
if (dbname)
m_dbname = std::string{dbname};
if (username)
m_username = std::string{username};
if (password)
m_password = std::string{password};
m_portnum = portnum;
g_free(scheme);
g_free(host);
g_free(username);
g_free(password);
g_free(dbname);
GncUri parsed { uri };
m_protocol = parsed.scheme().value_or("");
m_host = parsed.hostname().value_or("");
m_dbname = parsed.path().value_or("");
m_username = parsed.username().value_or("");
m_password = parsed.password().value_or("");
m_portnum = parsed.port();
}
std::string
@ -366,9 +355,7 @@ GncDbiBackend<DbType::DBI_SQLITE>::session_begin(QofSession* session,
ENTER (" ");
/* Remove uri type if present */
auto path = gnc_uri_get_path (new_uri);
std::string filepath{path};
g_free(path);
std::string filepath = GncUri{new_uri}.path().value_or("");
GFileTest ftest = static_cast<decltype (ftest)> (
G_FILE_TEST_IS_REGULAR | G_FILE_TEST_EXISTS) ;
file_exists = g_file_test (filepath.c_str(), ftest);
@ -1033,14 +1020,12 @@ QofDbiBackendProvider<DbType::DBI_SQLITE>::type_check(const char *uri)
gchar buf[51]{};
G_GNUC_UNUSED size_t chars_read;
gint status;
gchar* filename;
// BAD if the path is null
g_return_val_if_fail (uri != nullptr, FALSE);
filename = gnc_uri_get_path (uri);
f = g_fopen (filename, "r");
g_free (filename);
std::string filename = GncUri{uri}.path().value_or("");
f = g_fopen (filename.c_str(), "r");
// OK if the file doesn't exist - new file
if (f == nullptr)

@ -36,7 +36,7 @@
#include <qof.h>
/* For cleaning up the database */
#include <dbi/dbi.h>
#include <gnc-uri-utils.h>
#include <gnc-uri.hpp>
/* For setup_business */
#include "Account.h"
#include <TransLog.h>
@ -88,9 +88,9 @@ static char*
normalize_path(char* path)
{
g_return_val_if_fail(path, nullptr);
auto rv = gnc_uri_normalize_uri (path, FALSE);
auto rv = GncUri { path }.try_str (false);
g_free (path);
return rv;
return rv ? g_strdup (rv->c_str ()) : nullptr;
}
@ -276,8 +276,16 @@ destroy_database (gchar* url)
dbi_result tables;
StrVec tblnames;
gnc_uri_get_components (url, &scheme, &host, &portnum,
&username, &password, &dbname);
if (url && *url)
{
GncUri parsed { url };
scheme = parsed.scheme () ? g_strdup (parsed.scheme ()->c_str ()) : nullptr;
host = parsed.hostname () ? g_strdup (parsed.hostname ()->c_str ()) : nullptr;
username = parsed.username () ? g_strdup (parsed.username ()->c_str ()) : nullptr;
password = parsed.password () ? g_strdup (parsed.password ()->c_str ()) : nullptr;
dbname = parsed.path () ? g_strdup (parsed.path ()->c_str ()) : nullptr;
portnum = parsed.port ();
}
if (g_strcmp0 (scheme, "postgres") == 0)
#if HAVE_LIBDBI_R
conn = dbi_conn_new_r (pgsql, dbi_instance);

@ -67,7 +67,7 @@
#include "qof.h"
#include "gnc-engine.h"
#include <gnc-uri-utils.h>
#include <gnc-uri.hpp>
#include "gnc-prefs.h"
#ifndef HAVE_STRPTIME
@ -117,7 +117,6 @@ QofXmlBackendProvider::type_check (const char *uri)
GStatBuf sbuf;
int rc;
FILE* t;
gchar* filename;
QofBookFileType xml_type;
gboolean result;
@ -126,8 +125,8 @@ QofXmlBackendProvider::type_check (const char *uri)
return FALSE;
}
filename = gnc_uri_get_path (uri);
t = g_fopen (filename, "r");
std::string filename = GncUri{uri}.path().value_or ("");
t = g_fopen (filename.c_str(), "r");
if (!t)
{
PINFO (" new file");
@ -135,7 +134,7 @@ QofXmlBackendProvider::type_check (const char *uri)
goto det_exit;
}
fclose (t);
rc = g_stat (filename, &sbuf);
rc = g_stat (filename.c_str(), &sbuf);
if (rc < 0)
{
result = FALSE;
@ -147,7 +146,7 @@ QofXmlBackendProvider::type_check (const char *uri)
result = TRUE;
goto det_exit;
}
xml_type = gnc_is_xml_data_file_v2 (filename, NULL);
xml_type = gnc_is_xml_data_file_v2 (filename.c_str(), NULL);
if ((xml_type == GNC_BOOK_XML2_FILE) ||
(xml_type == GNC_BOOK_XML1_FILE) ||
(xml_type == GNC_BOOK_POST_XML2_0_0_FILE))
@ -155,11 +154,10 @@ QofXmlBackendProvider::type_check (const char *uri)
result = TRUE;
goto det_exit;
}
PINFO (" %s is not a gnc XML file", filename);
PINFO (" %s is not a gnc XML file", filename.c_str());
result = FALSE;
det_exit:
g_free (filename);
return result;
}

@ -33,7 +33,8 @@
#endif
#include <gnc-engine.h> //for GNC_MOD_BACKEND
#include <gnc-uri-utils.h>
#include <gnc-filepath-utils.h>
#include <gnc-uri.hpp>
#include <TransLog.h>
#include <gnc-prefs.h>
@ -115,9 +116,7 @@ GncXmlBackend::session_begin(QofSession* session, const char* new_uri,
SessionOpenMode mode)
{
/* Make sure the directory is there */
auto path_str = gnc_uri_get_path (new_uri);
m_fullpath = path_str;
g_free (path_str);
m_fullpath = GncUri { new_uri }.path().value_or ("");
if (m_fullpath.empty())
{

@ -40,7 +40,7 @@
#include <TransLog.h>
#include <gnc-engine.h>
#include <gnc-prefs.h>
#include <gnc-uri-utils.h>
#include <gnc-uri.hpp>
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcpp"
@ -209,7 +209,7 @@ public:
TEST_P(LoadSaveFiles, test_file)
{
auto filename = GetParam();
auto base_url = gnc_uri_normalize_uri (filename.c_str (), FALSE);
auto base_url = GncUri { filename }.try_str (false);
/* Verify that we can write a compressed version of the original file that
* has the original content when uncompressed.
*/
@ -217,9 +217,9 @@ TEST_P(LoadSaveFiles, test_file)
/* Verify that we can read a compressed file and write an uncompressed file
* that has the original content.
*/
auto compressed_url = gnc_uri_normalize_uri (new_compressed_file.c_str (), FALSE);
auto compressed_url = GncUri { new_compressed_file }.try_str (false);
auto new_uncompressed_file = filename + "-test-uncompressed~";
auto uncompressed_url = gnc_uri_normalize_uri (new_uncompressed_file.c_str (), FALSE);
auto uncompressed_url = GncUri { new_uncompressed_file }.try_str (false);
const char *logdomain = "backend.xml";
GLogLevelFlags loglevel = static_cast<decltype (loglevel)>
(G_LOG_LEVEL_WARNING);
@ -230,7 +230,7 @@ TEST_P(LoadSaveFiles, test_file)
{
auto load_uncompressed_session = std::shared_ptr<QofSession>{qof_session_new (qof_book_new ()), qof_session_destroy};
QOF_SESSION_CHECKED_CALL(qof_session_begin, load_uncompressed_session, base_url, SESSION_READ_ONLY);
QOF_SESSION_CHECKED_CALL(qof_session_begin, load_uncompressed_session, base_url ? base_url->c_str () : nullptr, SESSION_READ_ONLY);
QOF_SESSION_CHECKED_CALL(qof_session_load, load_uncompressed_session, nullptr);
auto save_compressed_session = std::shared_ptr<QofSession>{qof_session_new (nullptr), qof_session_destroy};
@ -238,7 +238,7 @@ TEST_P(LoadSaveFiles, test_file)
g_unlink (new_compressed_file.c_str ());
g_unlink ((new_compressed_file + ".LCK").c_str ());
QOF_SESSION_CHECKED_CALL(qof_session_begin, save_compressed_session, compressed_url, SESSION_NEW_OVERWRITE);
QOF_SESSION_CHECKED_CALL(qof_session_begin, save_compressed_session, compressed_url ? compressed_url->c_str () : nullptr, SESSION_NEW_OVERWRITE);
qof_event_suspend ();
qof_session_swap_data (load_uncompressed_session.get (), save_compressed_session.get ());
@ -259,14 +259,14 @@ TEST_P(LoadSaveFiles, test_file)
{
auto load_compressed_session = std::shared_ptr<QofSession>{qof_session_new (qof_book_new ()), qof_session_destroy};
QOF_SESSION_CHECKED_CALL(qof_session_begin, load_compressed_session, compressed_url, SESSION_READ_ONLY);
QOF_SESSION_CHECKED_CALL(qof_session_begin, load_compressed_session, compressed_url ? compressed_url->c_str () : nullptr, SESSION_READ_ONLY);
QOF_SESSION_CHECKED_CALL(qof_session_load, load_compressed_session, nullptr);
auto save_uncompressed_session = std::shared_ptr<QofSession>{qof_session_new (nullptr), qof_session_destroy};
g_unlink (new_uncompressed_file.c_str ());
g_unlink ((new_uncompressed_file + ".LCK").c_str ());
QOF_SESSION_CHECKED_CALL(qof_session_begin, save_uncompressed_session, uncompressed_url, SESSION_NEW_OVERWRITE);
QOF_SESSION_CHECKED_CALL(qof_session_begin, save_uncompressed_session, uncompressed_url ? uncompressed_url->c_str () : nullptr, SESSION_NEW_OVERWRITE);
qof_event_suspend ();
qof_session_swap_data (load_compressed_session.get (), save_uncompressed_session.get ());
@ -281,10 +281,6 @@ TEST_P(LoadSaveFiles, test_file)
qof_session_end (save_uncompressed_session.get ());
}
g_free (base_url);
g_free (compressed_url);
g_free (uncompressed_url);
if (!compare_files (filename, new_uncompressed_file))
return;
}

@ -30,7 +30,7 @@
#include <gnc-prefs.h>
#include <Account.hpp>
#include <gnc-datetime.hpp>
#include <gnc-uri-utils.h>
#include <gnc-uri.hpp>
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcpp"
@ -63,10 +63,9 @@ static QofBook*
session_load (QofSession* session, const char* filename)
{
if (!session || !filename) return nullptr;
auto url = gnc_uri_normalize_uri (filename, FALSE);
auto url = GncUri { filename }.try_str (false);
qof_session_begin (session, url, SESSION_READ_ONLY);
g_free (url);
qof_session_begin (session, url ? url->c_str () : nullptr, SESSION_READ_ONLY);
if (qof_session_get_error(session) != 0)
{

@ -43,7 +43,7 @@
#include <TransLog.h>
#include <gnc-engine.h>
#include <gnc-prefs.h>
#include <gnc-uri-utils.h>
#include <gnc-uri.hpp>
#include <unittest-support.h>
#include <test-engine-stuff.h>
@ -92,15 +92,14 @@ test_load_file (const char* filename)
auto book = qof_book_new();
auto session = qof_session_new (book);
auto url = gnc_uri_normalize_uri (filename, FALSE);
auto url = GncUri { filename }.try_str (false);
remove_locks (filename);
ignore_lock = (g_strcmp0 (g_getenv ("SRCDIR"), ".") != 0);
/* gnc_prefs_set_file_save_compressed(FALSE); */
qof_session_begin (session, url,
qof_session_begin (session, url ? url->c_str () : nullptr,
ignore_lock ? SESSION_READ_ONLY : SESSION_NORMAL_OPEN);
g_free (url);
qof_session_load (session, NULL);

@ -29,6 +29,9 @@
#ifndef GNC_FILEPATH_UTILS_H
#define GNC_FILEPATH_UTILS_H
#define GNC_DATAFILE_EXT ".gnucash"
#define GNC_LOGFILE_EXT ".log" /* GnuCash transaction-log file extension */
#include <glib.h>
#ifdef __cplusplus

@ -74,6 +74,7 @@ set (engine_HEADERS
gnc-session.h
gnc-timezone.hpp
gnc-uri-utils.h
gnc-uri.hpp
gncAddress.h
gncAddressP.h
gncBillTerm.h
@ -174,7 +175,7 @@ set (engine_SOURCES
gnc-rational.cpp
gnc-session.c
gnc-timezone.cpp
gnc-uri-utils.c
gnc-uri.cpp
engine-helpers.c
guid.cpp
policy.cpp

@ -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 );
}

@ -58,27 +58,12 @@
#ifndef GNCURIUTILS_H_
#define GNCURIUTILS_H_
#define GNC_DATAFILE_EXT ".gnucash"
#define GNC_LOGFILE_EXT ".log"
#include "platform.h"
#ifdef __cplusplus
extern "C" {
#endif
/** Checks if the given uri is a valid uri
*
* A valid uri is defined by having at least a scheme and a path.
* If the uri is not referring to a file on the local file system
* a hostname should be set as well.
*
* @param uri The uri to check
*
* @return TRUE if the input is a valid uri, FALSE otherwise
*/
gboolean gnc_uri_is_uri (const gchar *uri);
/** Converts a uri in separate components.
*
* The function allocates memory for each of the components that it finds
@ -193,28 +178,6 @@ gchar *gnc_uri_create_uri (const gchar *scheme,
gchar *gnc_uri_normalize_uri (const gchar *uri, gboolean allow_password);
/** Checks if the given uri is a valid uri
*
* A valid uri is defined by having at least a scheme and a path.
* If the uri is not referring to a file on the local file system
* a hostname should be set as well.
*
* @param uri The uri to check
*
* @return TRUE if the input is a valid uri, FALSE otherwise
*/
gboolean gnc_uri_is_uri (const gchar *uri);
/** Checks if there is a backend that explicitly stated to handle the given scheme.
*
* @param scheme The scheme to check
*
* @return TRUE if at least one backend explicitly handles this scheme, otherwise FALSE
*/
gboolean gnc_uri_is_known_scheme (const gchar *scheme);
/** Checks if the given scheme is used to refer to a file
* (as opposed to a network service like a database or web url)
*

@ -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 */

@ -55,7 +55,7 @@ static QofLogModule log_module = QOF_MOD_SESSION;
#include "qof-backend.hpp"
#include "qofsession.hpp"
#include "gnc-backend-prov.hpp"
#include "gnc-uri-utils.h"
#include "gnc-uri.hpp"
#include <vector>
#include <boost/algorithm/string.hpp>
@ -275,7 +275,10 @@ QofSessionImpl::begin (const char* new_uri, SessionOpenMode mode) noexcept
char * scheme {g_uri_parse_scheme (new_uri)};
char * filename {nullptr};
if (g_strcmp0 (scheme, "file") == 0)
filename = gnc_uri_get_path(new_uri);
{
if (auto path = GncUri{new_uri}.path())
filename = g_strdup (path->c_str());
}
else if (!scheme)
filename = g_strdup (new_uri);

@ -130,6 +130,9 @@ set(test_qofsession_SOURCES
gnc_add_test(test-qofsession "${test_qofsession_SOURCES}"
gtest_engine_INCLUDES gtest_old_engine_LIBS)
gnc_add_test(test-gnc-uri gtest-gnc-uri.cpp
gtest_engine_INCLUDES gtest_old_engine_LIBS)
gnc_add_test(test-qofid test-qofid.cpp
gtest_engine_INCLUDES gtest_old_engine_LIBS)
@ -211,6 +214,7 @@ gnc_add_test(test-gnc-option "${test_gnc_option_SOURCES}"
set(test_engine_SOURCES_DIST
gtest-gnc-euro.cpp
gtest-gnc-uri.cpp
gtest-gnc-int128.cpp
gtest-gnc-rational.cpp
gtest-gnc-numeric.cpp
@ -231,7 +235,7 @@ set(test_engine_SOURCES_DIST
test-engine.c
test-gnc-date.c
test-gnc-guid.cpp
test-gnc-uri-utils.c
test-gnc-uri-utils.c
test-group-vs-book.cpp
test-guid.cpp
test-job.c

@ -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 ();
}

@ -28,6 +28,7 @@
#include <glib.h>
#include "qof.h"
#include <unittest-support.h>
#include "gnc-filepath-utils.h"
#include "gnc-uri-utils.h"
static const gchar *suitename = "/engine/uri-utils";
@ -272,6 +273,17 @@ test_gnc_uri_create_uri()
g_assert_cmpstr ( turi, ==, strs[i].created_uri );
g_free(turi);
}
/* A file-type scheme with a non-absolute path exercises the "scheme:///path"
* branch that inserts an extra slash. In the test environment no backends
* are registered, so an (unknown) file-type scheme leaves the path
* unresolved, making the result predictable. */
{
gchar *turi = gnc_uri_create_uri( "xml", NULL, 0, NULL, NULL,
"relative/path/file.gnucash" );
g_assert_cmpstr ( turi, ==, "xml:///relative/path/file.gnucash" );
g_free(turi);
}
}
/* TEST: gnc_uri_normalize_uri */
@ -319,6 +331,75 @@ test_gnc_uri_is_file_uri()
}
}
/* TEST: gnc_uri_targets_local_fs */
static void
test_gnc_uri_targets_local_fs()
{
struct test_local_fs_struct
{
const gchar *uri;
gboolean targets_local_fs;
} cases[] =
{
#ifndef G_OS_WIN32
{ "/test/path/file.gnucash", TRUE }, /* no scheme -> local */
{ "file:///test/path/file.gnucash", TRUE }, /* file scheme -> local */
{ "xml:///test/path/file.gnucash", TRUE },
{ "sqlite3:///test/path/file.gnucash", TRUE },
#else
{ "c:\\test\\path\\file.gnucash", TRUE }, /* no scheme -> local */
{ "file://c:\\test\\path\\file.gnucash", TRUE }, /* file scheme -> local */
{ "xml://c:\\test\\path\\file.gnucash", TRUE },
{ "sqlite3://c:\\test\\path\\file.gnucash", TRUE },
#endif
{ "mysql://www.gnucash.org/gnucash", FALSE }, /* db scheme -> not local */
{ "postgres://dbuser:dbpass@www.gnucash.org/gnucash", FALSE },
{ NULL, FALSE },
};
int i;
for (i = 0; cases[i].uri != NULL; i++)
g_assert_true ( gnc_uri_targets_local_fs (cases[i].uri) == cases[i].targets_local_fs );
}
/* TEST: gnc_uri_add_extension */
static void
test_gnc_uri_add_extension()
{
gchar *result;
/* A NULL uri returns NULL (and logs a critical from g_return_val_if_fail) */
if (g_test_undefined ())
{
g_test_expect_message ("gnc.engine", G_LOG_LEVEL_CRITICAL,
"*assertion 'uri != nullptr' failed*");
g_assert_null ( gnc_uri_add_extension (NULL, GNC_DATAFILE_EXT) );
g_test_assert_expected_messages ();
}
/* A non-file uri is never modified, only duplicated */
result = gnc_uri_add_extension ( "mysql://www.gnucash.org/gnucash", GNC_DATAFILE_EXT );
g_assert_cmpstr ( result, ==, "mysql://www.gnucash.org/gnucash" );
g_free (result);
#ifndef G_OS_WIN32
/* A file uri without the extension gets it appended */
result = gnc_uri_add_extension ( "file:///test/path/file", GNC_DATAFILE_EXT );
g_assert_cmpstr ( result, ==, "file:///test/path/file" GNC_DATAFILE_EXT );
g_free (result);
/* A file uri that already ends in the extension is left unchanged */
result = gnc_uri_add_extension ( "file:///test/path/file" GNC_DATAFILE_EXT, GNC_DATAFILE_EXT );
g_assert_cmpstr ( result, ==, "file:///test/path/file" GNC_DATAFILE_EXT );
g_free (result);
/* A NULL extension leaves the uri unchanged, only duplicated */
result = gnc_uri_add_extension ( "file:///test/path/file", NULL );
g_assert_cmpstr ( result, ==, "file:///test/path/file" );
g_free (result);
#endif
}
void
test_suite_gnc_uri_utils(void)
{
@ -329,5 +410,6 @@ test_suite_gnc_uri_utils(void)
GNC_TEST_ADD_FUNC(suitename, "gnc_uri_normalize_uri()", test_gnc_uri_normalize_uri);
GNC_TEST_ADD_FUNC(suitename, "gnc_uri_is_file_scheme()", test_gnc_uri_is_file_scheme);
GNC_TEST_ADD_FUNC(suitename, "gnc_uri_is_file_uri()", test_gnc_uri_is_file_uri);
GNC_TEST_ADD_FUNC(suitename, "gnc_uri_targets_local_fs()", test_gnc_uri_targets_local_fs);
GNC_TEST_ADD_FUNC(suitename, "gnc_uri_add_extension()", test_gnc_uri_add_extension);
}

@ -664,7 +664,7 @@ libgnucash/engine/gnc-rational.cpp
libgnucash/engine/gnc-session.c
libgnucash/engine/gncTaxTable.c
libgnucash/engine/gnc-timezone.cpp
libgnucash/engine/gnc-uri-utils.c
libgnucash/engine/gnc-uri.cpp
libgnucash/engine/gncVendor.c
libgnucash/engine/guid.cpp
libgnucash/engine/kvp-frame.cpp

Loading…
Cancel
Save