gnucash future: Multiple changes pushed
John Ralls
jralls at code.gnucash.org
Tue Jun 23 19:04:20 EDT 2026
Updated via https://github.com/Gnucash/gnucash/commit/287c802d (commit)
via https://github.com/Gnucash/gnucash/commit/590d76cc (commit)
via https://github.com/Gnucash/gnucash/commit/5ee139b5 (commit)
via https://github.com/Gnucash/gnucash/commit/12e2922f (commit)
from https://github.com/Gnucash/gnucash/commit/2b93466c (commit)
commit 287c802d8c22b5dfe3dbcb0d0c6ed74ae0d8f0d4
Merge: 590d76ccc4 5ee139b556
Author: John Ralls <jralls at ceridwen.us>
Date: Tue Jun 23 16:02:03 2026 -0700
Merge Brent McBride's 'uri-utils-cpp' into future.
commit 590d76ccc4a4ce370266d809fe246d03d83c44b6
Author: John Ralls <jralls at ceridwen.us>
Date: Tue Jun 23 15:35:45 2026 -0700
Clang: -Wno-character-conversion was introduced in clang 21.
Gcc doesn't have it at all.
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 67867ba2c1..76385829a8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -625,8 +625,12 @@ set(CMAKE_C_EXTENSIONS ON)
# -Wno-deprecated-volatile, -Wno-volatile: Guile heavily uses
# -volatile. Guile is C but we include its headers in C++ swig files.
+# -Wno-character-conversions: Googletest casts char8_t* to char32_t
if (CMAKE_CXX_COMPILER_ID MATCHES "AppleClang|Clang")
set(volatile_warning "-Wno-deprecated-volatile")
+ if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "21")
+ set (character_conversions "-Wno-character-conversion")
+ endif()
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
set(volatile_warning "-Wno-volatile")
else()
@@ -634,13 +638,12 @@ else()
endif()
if (UNIX)
set( CMAKE_C_FLAGS "-Werror -Wall -Wmissing-prototypes -Wmissing-declarations ${CMAKE_C_FLAGS}")
- # -Wno-character-conversions: Googletest casts char8_t* to char32_t
- set( CMAKE_CXX_FLAGS "-Werror -Wall -Wmissing-declarations -Wno-character-conversion ${volatile_warning} ${CMAKE_CXX_FLAGS}")
+ set( CMAKE_CXX_FLAGS "-Werror -Wall -Wmissing-declarations ${character_conversions} ${volatile_warning} ${CMAKE_CXX_FLAGS}")
set( CMAKE_C_FLAGS_RELEASE "-O3 -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2 ${CMAKE_C_FLAGS}")
endif()
if (MINGW)
set( CMAKE_C_FLAGS "-Werror -Wall -Wmissing-prototypes -Wmissing-declarations ${CMAKE_C_FLAGS}")
- set( CMAKE_CXX_FLAGS "-Werror -Wall -Wmissing-declarations -Wno-character-conversion ${volatile_warning} -DWINVER=0x0A00 ${CMAKE_CXX_FLAGS}") # Workaround for bug in gtest on mingw, see https://github.com/google/googletest/issues/893 and https://github.com/google/googletest/issues/920
+ set( CMAKE_CXX_FLAGS "-Werror -Wall -Wmissing-declarations ${character_conversions} ${volatile_warning} -DWINVER=0x0A00 ${CMAKE_CXX_FLAGS}") # Workaround for bug in gtest on mingw, see https://github.com/google/googletest/issues/893 and https://github.com/google/googletest/issues/920
endif()
if (APPLE)
commit 5ee139b5564944d7eb3f03fe5ae7e16211681f30
Author: Brent McBride <mcbridebt at hotmail.com>
Date: Sun Jun 21 21:19:50 2026 -0700
Modernize gnc-uri utilities into a GncUri C++ class
diff --git a/gnucash/gnome-utils/gnc-main-window.cpp b/gnucash/gnome-utils/gnc-main-window.cpp
index 84d87a4505..41eeed391f 100644
--- a/gnucash/gnome-utils/gnc-main-window.cpp
+++ b/gnucash/gnome-utils/gnc-main-window.cpp
@@ -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);
diff --git a/gnucash/gnome/gnc-plugin-page-invoice.cpp b/gnucash/gnome/gnc-plugin-page-invoice.cpp
index 17c6676e2e..df3b7d045d 100644
--- a/gnucash/gnome/gnc-plugin-page-invoice.cpp
+++ b/gnucash/gnome/gnc-plugin-page-invoice.cpp
@@ -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"
diff --git a/gnucash/import-export/csv-imp/assistant-csv-price-import.cpp b/gnucash/import-export/csv-imp/assistant-csv-price-import.cpp
index 3543b51747..c4ae82a765 100644
--- a/gnucash/import-export/csv-imp/assistant-csv-price-import.cpp
+++ b/gnucash/import-export/csv-imp/assistant-csv-price-import.cpp
@@ -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);
diff --git a/gnucash/import-export/csv-imp/assistant-csv-trans-import.cpp b/gnucash/import-export/csv-imp/assistant-csv-trans-import.cpp
index c62ed927ae..9b652a5867 100644
--- a/gnucash/import-export/csv-imp/assistant-csv-trans-import.cpp
+++ b/gnucash/import-export/csv-imp/assistant-csv-trans-import.cpp
@@ -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);
diff --git a/libgnucash/backend/dbi/gnc-backend-dbi.cpp b/libgnucash/backend/dbi/gnc-backend-dbi.cpp
index 08c0baa805..0944aedc96 100644
--- a/libgnucash/backend/dbi/gnc-backend-dbi.cpp
+++ b/libgnucash/backend/dbi/gnc-backend-dbi.cpp
@@ -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)
diff --git a/libgnucash/backend/dbi/test/test-backend-dbi-basic.cpp b/libgnucash/backend/dbi/test/test-backend-dbi-basic.cpp
index 8ba7ce591e..2a1d0c9129 100644
--- a/libgnucash/backend/dbi/test/test-backend-dbi-basic.cpp
+++ b/libgnucash/backend/dbi/test/test-backend-dbi-basic.cpp
@@ -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);
diff --git a/libgnucash/backend/xml/gnc-backend-xml.cpp b/libgnucash/backend/xml/gnc-backend-xml.cpp
index 8a941b28b1..1a0e7f0138 100644
--- a/libgnucash/backend/xml/gnc-backend-xml.cpp
+++ b/libgnucash/backend/xml/gnc-backend-xml.cpp
@@ -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;
}
diff --git a/libgnucash/backend/xml/gnc-xml-backend.cpp b/libgnucash/backend/xml/gnc-xml-backend.cpp
index 6f48e574f1..f8beafd13b 100644
--- a/libgnucash/backend/xml/gnc-xml-backend.cpp
+++ b/libgnucash/backend/xml/gnc-xml-backend.cpp
@@ -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())
{
diff --git a/libgnucash/backend/xml/test/gtest-load-save-files.cpp b/libgnucash/backend/xml/test/gtest-load-save-files.cpp
index b8ee039650..f5c76427e0 100644
--- a/libgnucash/backend/xml/test/gtest-load-save-files.cpp
+++ b/libgnucash/backend/xml/test/gtest-load-save-files.cpp
@@ -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;
}
diff --git a/libgnucash/backend/xml/test/gtest-xml-contents.cpp b/libgnucash/backend/xml/test/gtest-xml-contents.cpp
index f01ba3e2e7..7c716c0c80 100644
--- a/libgnucash/backend/xml/test/gtest-xml-contents.cpp
+++ b/libgnucash/backend/xml/test/gtest-xml-contents.cpp
@@ -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)
{
diff --git a/libgnucash/backend/xml/test/test-load-xml2.cpp b/libgnucash/backend/xml/test/test-load-xml2.cpp
index 4ed7384a7c..5cc111f3c3 100644
--- a/libgnucash/backend/xml/test/test-load-xml2.cpp
+++ b/libgnucash/backend/xml/test/test-load-xml2.cpp
@@ -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);
diff --git a/libgnucash/core-utils/gnc-filepath-utils.h b/libgnucash/core-utils/gnc-filepath-utils.h
index 38c5ca3afe..0d9747633d 100644
--- a/libgnucash/core-utils/gnc-filepath-utils.h
+++ b/libgnucash/core-utils/gnc-filepath-utils.h
@@ -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
diff --git a/libgnucash/engine/CMakeLists.txt b/libgnucash/engine/CMakeLists.txt
index 4797652a9d..71f384fe0a 100644
--- a/libgnucash/engine/CMakeLists.txt
+++ b/libgnucash/engine/CMakeLists.txt
@@ -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
diff --git a/libgnucash/engine/gnc-uri-utils.h b/libgnucash/engine/gnc-uri-utils.h
index 68e4aca2b7..e743094376 100644
--- a/libgnucash/engine/gnc-uri-utils.h
+++ b/libgnucash/engine/gnc-uri-utils.h
@@ -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)
*
diff --git a/libgnucash/engine/gnc-uri.cpp b/libgnucash/engine/gnc-uri.cpp
old mode 100644
new mode 100755
index fd962dc2bb..3f30857a83
--- a/libgnucash/engine/gnc-uri.cpp
+++ b/libgnucash/engine/gnc-uri.cpp
@@ -1,8 +1,9 @@
/*
- * gnc-uri-utils.c -- utility functions to convert uri in separate
+ * gnc-uri.cpp -- utility functions to convert uri in separate
* components and back.
*
* Copyright (C) 2010 Geert Janssens <janssens.geert at telenet.be>
+ * Copyright (C) 2026 Brent McBride <mcbridebt at 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
@@ -23,53 +24,47 @@
*/
#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"
-/* Checks if the given uri is a valid uri
- */
-gboolean gnc_uri_is_uri (const gchar *uri)
+/* 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;
+}
- 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;
+/* 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;
}
-/* 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)
+/* True when the scheme is one of the registered backend access methods. */
+static bool
+scheme_is_known (const gchar *scheme)
{
- gboolean is_known_scheme = FALSE;
- GList *node;
+ bool is_known_scheme = false;
GList *known_scheme_list = qof_backend_get_registered_access_method_list();
- for ( node = known_scheme_list; node != NULL; node = node->next )
+ for ( GList *node = known_scheme_list; node != nullptr; node = node->next )
{
- gchar *known_scheme = node->data;
+ gchar *known_scheme = static_cast<gchar*>(node->data);
if ( !g_ascii_strcasecmp (scheme, known_scheme) )
{
- is_known_scheme = TRUE;
+ is_known_scheme = true;
break;
}
}
@@ -78,252 +73,158 @@ gboolean gnc_uri_is_known_scheme (const gchar *scheme)
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.
+/* ---------------------------------------------------------------------------
+ * GncUri - C++ interface
*
- * *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)
+ * 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)
{
- return (scheme &&
- (!g_ascii_strcasecmp (scheme, "file") ||
- !g_ascii_strcasecmp (scheme, "xml") ||
- !g_ascii_strcasecmp (scheme, "sqlite3")));
+ /* 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");
}
-/* Checks if the given uri defines a file
- * (as opposed to a network service)
- */
-gboolean gnc_uri_is_file_uri (const gchar *uri)
+GncUri::GncUri (const std::string& 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);
+ if (uri.empty())
+ return;
- splituri = g_strsplit ( uri, "://", 2 );
- if ( splituri[1] == NULL )
+ auto sep = uri.find ("://");
+ if (sep == std::string::npos)
{
- /* No scheme means simple file path.
- Set path to copy of the input. */
- *path = g_strdup ( uri );
- g_strfreev ( splituri );
+ /* No scheme means a simple file path; the path is a copy of the input. */
+ m_path = uri;
return;
}
- /* At least a scheme was found, set it here */
- *scheme = g_strdup ( splituri[0] );
+ std::string scheme = uri.substr (0, sep);
+ std::string rest = uri.substr (sep + 3);
+ m_scheme = scheme;
- if ( gnc_uri_is_file_scheme ( *scheme ) )
+ if (scheme_is_file (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 );
+ /* 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;
}
- /* 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 )
+ /* 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)
{
- /* 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 )
+ 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)
{
- /* There is password in the url */
- delimiter[0] = '\0';
- *password = g_strdup ( (const gchar*)(delimiter + 1) );
+ m_username = userinfo.substr (0, colon);
+ m_password = userinfo.substr (colon + 1);
}
- *username = g_strdup ( (const gchar*)tmpusername );
+ else
+ m_username = userinfo;
}
else
- {
- /* No username and password were given */
- tmphostname = url;
- }
+ hostpart = rest;
- /* Find the path part */
- delimiter = g_strstr_len ( tmphostname, -1, "/" );
- if ( delimiter != NULL )
+ /* Split off the path part. */
+ auto slash = hostpart.find ('/');
+ if (slash != std::string::npos)
{
- 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) );
+ m_path = hostpart.substr (slash + 1);
+ hostpart.erase (slash);
}
- /* Check for a port specifier */
- delimiter = g_strstr_len ( tmphostname, -1, ":" );
- if ( delimiter != NULL )
+ /* Split off an optional port specifier. */
+ auto port_colon = hostpart.find (':');
+ if (port_colon != std::string::npos)
{
- delimiter[0] = '\0';
- *port = g_ascii_strtoll ( delimiter + 1, NULL, 0 );
+ m_port = static_cast<int32_t> (
+ g_ascii_strtoll (hostpart.c_str() + port_colon + 1, nullptr, 0));
+ hostpart.erase (port_colon);
}
- *hostname = g_strdup ( (const gchar*)tmphostname );
-
- g_free ( url );
-
- return;
+ 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"));
}
-gchar *gnc_uri_get_scheme (const gchar *uri)
+bool
+GncUri::is_file_uri () const noexcept
{
- 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;
+ return m_scheme && scheme_is_file (*m_scheme);
}
-gchar *gnc_uri_get_path (const gchar *uri)
+bool
+GncUri::targets_local_fs () const noexcept
{
- 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;
+ /* 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));
}
-/* 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)
+std::optional<std::string>
+GncUri::try_str (bool allow_password) const
{
- gchar *userpass = NULL, *portstr = NULL, *uri = NULL;
+ if (!m_path)
+ return std::nullopt;
- g_return_val_if_fail( path != 0, NULL );
+ const std::string& path = *m_path;
- if (!scheme || gnc_uri_is_file_scheme (scheme))
+ if (!m_scheme || scheme_is_file (*m_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 );
+ /* 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);
- if (!scheme)
- uri_scheme = g_strdup ("file");
- else
- uri_scheme = g_strdup (scheme);
+ std::string scheme = m_scheme ? *m_scheme : std::string { "file" };
/* Arrive here with...
*
@@ -335,84 +236,162 @@ gchar *gnc_uri_create_uri (const gchar *scheme,
*
* \\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
*/
+ bool absolute = !abs_path.empty() &&
+ (abs_path.front() == '/' || abs_path.front() == '\\');
+ return absolute ? scheme + "://" + abs_path
+ : scheme + ":///" + abs_path; // extra "/" for windows
+ }
- 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 );
+ 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 += '@';
+ }
- g_free (uri_scheme);
- g_free (abs_path);
+ std::string portstr;
+ if (m_port != 0)
+ portstr = ":" + std::to_string (m_port);
- return uri;
- }
+ return *m_scheme + "://" + userpass + *m_hostname + portstr + "/" + path;
+}
- /* 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 );
+std::string
+GncUri::str (bool allow_password) const
+{
+ if (std::optional<std::string> result = try_str (allow_password))
+ return std::move (*result);
- 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 ( "" );
+ /* 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");
+}
- if ( port != 0 )
- portstr = g_strdup_printf ( ":%d", port );
- else
- portstr = g_strdup ( "" );
+/* 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();
+}
- // 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 );
+/* 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_free ( userpass );
- g_free ( portstr );
+ g_return_if_fail( uri != nullptr && strlen (uri) > 0);
- return uri;
+ 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_normalize_uri (const gchar *uri, gboolean allow_password)
+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;
- 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_return_val_if_fail (uri != nullptr, nullptr);
+ return dup_or_null (GncUri { uri }.scheme());
+}
- g_free (scheme);
- g_free (hostname);
- g_free (username);
- g_free (password);
- g_free (path);
+gchar *
+gnc_uri_get_path (const gchar *uri)
+{
+ g_return_val_if_fail (uri != nullptr, nullptr);
+ return dup_or_null (GncUri { uri }.path());
+}
- return newuri;
+/* 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 )
+gchar *
+gnc_uri_add_extension ( const gchar *uri, const gchar *extension )
{
- g_return_val_if_fail( uri != 0, NULL );
+ g_return_val_if_fail( uri != nullptr, nullptr );
/* Only add extension if the user provided the extension and the uri is
* file based.
@@ -425,5 +404,5 @@ gchar *gnc_uri_add_extension ( const gchar *uri, const gchar *extension )
return g_strdup( uri );
/* Ok, all tests passed, let's add the extension */
- return g_strconcat( uri, extension, NULL );
+ return g_strconcat( uri, extension, nullptr );
}
diff --git a/libgnucash/engine/gnc-uri.hpp b/libgnucash/engine/gnc-uri.hpp
new file mode 100644
index 0000000000..4f3a066a50
--- /dev/null
+++ b/libgnucash/engine/gnc-uri.hpp
@@ -0,0 +1,119 @@
+/********************************************************************\
+ * gnc-uri.hpp -- C++ interface to parse and compose uris. *
+ * *
+ * Copyright (C) 2026 Brent McBride <mcbridebt at 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 at 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 at 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 */
diff --git a/libgnucash/engine/qofsession.cpp b/libgnucash/engine/qofsession.cpp
index 81392db879..d2808dafaf 100644
--- a/libgnucash/engine/qofsession.cpp
+++ b/libgnucash/engine/qofsession.cpp
@@ -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);
diff --git a/libgnucash/engine/test/CMakeLists.txt b/libgnucash/engine/test/CMakeLists.txt
index 6d5b073694..aeab39a551 100644
--- a/libgnucash/engine/test/CMakeLists.txt
+++ b/libgnucash/engine/test/CMakeLists.txt
@@ -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
diff --git a/libgnucash/engine/test/gtest-gnc-uri.cpp b/libgnucash/engine/test/gtest-gnc-uri.cpp
new file mode 100644
index 0000000000..b356c03c35
--- /dev/null
+++ b/libgnucash/engine/test/gtest-gnc-uri.cpp
@@ -0,0 +1,170 @@
+/********************************************************************\
+ * gtest-gnc-uri.cpp -- Unit tests for the GncUri C++ class. *
+ * *
+ * Copyright (C) 2026 Brent McBride <mcbridebt at 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 at 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 ();
+}
diff --git a/libgnucash/engine/test/test-gnc-uri-utils.c b/libgnucash/engine/test/test-gnc-uri-utils.c
index 171fbdf28f..4d827b076d 100644
--- a/libgnucash/engine/test/test-gnc-uri-utils.c
+++ b/libgnucash/engine/test/test-gnc-uri-utils.c
@@ -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);
}
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 6aa4fe2df2..df15450bbe 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -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
commit 12e2922fc5dd6f1ce836d88bbee33a3a4731e743
Author: Brent McBride <mcbridebt at hotmail.com>
Date: Sun Jun 21 21:19:38 2026 -0700
Rename gnc-uri-utils.c to gnc-uri.cpp
diff --git a/libgnucash/engine/gnc-uri-utils.c b/libgnucash/engine/gnc-uri.cpp
old mode 100755
new mode 100644
similarity index 100%
rename from libgnucash/engine/gnc-uri-utils.c
rename to libgnucash/engine/gnc-uri.cpp
Summary of changes:
CMakeLists.txt | 9 +-
gnucash/gnome-utils/gnc-main-window.cpp | 31 +-
gnucash/gnome/gnc-plugin-page-invoice.cpp | 1 -
.../csv-imp/assistant-csv-price-import.cpp | 7 +-
.../csv-imp/assistant-csv-trans-import.cpp | 7 +-
libgnucash/backend/dbi/gnc-backend-dbi.cpp | 37 +-
.../backend/dbi/test/test-backend-dbi-basic.cpp | 18 +-
libgnucash/backend/xml/gnc-backend-xml.cpp | 14 +-
libgnucash/backend/xml/gnc-xml-backend.cpp | 7 +-
.../backend/xml/test/gtest-load-save-files.cpp | 20 +-
libgnucash/backend/xml/test/gtest-xml-contents.cpp | 7 +-
libgnucash/backend/xml/test/test-load-xml2.cpp | 7 +-
libgnucash/core-utils/gnc-filepath-utils.h | 3 +
libgnucash/engine/CMakeLists.txt | 3 +-
libgnucash/engine/gnc-uri-utils.c | 429 ---------------------
libgnucash/engine/gnc-uri-utils.h | 37 --
libgnucash/engine/gnc-uri.cpp | 408 ++++++++++++++++++++
libgnucash/engine/gnc-uri.hpp | 119 ++++++
libgnucash/engine/qofsession.cpp | 7 +-
libgnucash/engine/test/CMakeLists.txt | 6 +-
libgnucash/engine/test/gtest-gnc-uri.cpp | 170 ++++++++
libgnucash/engine/test/test-gnc-uri-utils.c | 84 +++-
po/POTFILES.in | 2 +-
23 files changed, 873 insertions(+), 560 deletions(-)
delete mode 100755 libgnucash/engine/gnc-uri-utils.c
create mode 100755 libgnucash/engine/gnc-uri.cpp
create mode 100644 libgnucash/engine/gnc-uri.hpp
create mode 100644 libgnucash/engine/test/gtest-gnc-uri.cpp
More information about the gnucash-changes
mailing list