From 12e2922fc5dd6f1ce836d88bbee33a3a4731e743 Mon Sep 17 00:00:00 2001 From: Brent McBride Date: Sun, 21 Jun 2026 21:19:38 -0700 Subject: [PATCH 1/2] Rename gnc-uri-utils.c to gnc-uri.cpp --- libgnucash/engine/{gnc-uri-utils.c => gnc-uri.cpp} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename libgnucash/engine/{gnc-uri-utils.c => gnc-uri.cpp} (100%) mode change 100755 => 100644 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 From 5ee139b5564944d7eb3f03fe5ae7e16211681f30 Mon Sep 17 00:00:00 2001 From: Brent McBride Date: Sun, 21 Jun 2026 21:19:50 -0700 Subject: [PATCH 2/2] Modernize gnc-uri utilities into a GncUri C++ class --- 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 +- .../dbi/test/test-backend-dbi-basic.cpp | 18 +- libgnucash/backend/xml/gnc-backend-xml.cpp | 14 +- libgnucash/backend/xml/gnc-xml-backend.cpp | 7 +- .../xml/test/gtest-load-save-files.cpp | 20 +- .../backend/xml/test/gtest-xml-contents.cpp | 7 +- .../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.h | 37 -- libgnucash/engine/gnc-uri.cpp | 577 +++++++++--------- 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 +- 21 files changed, 737 insertions(+), 427 deletions(-) mode change 100644 => 100755 libgnucash/engine/gnc-uri.cpp create mode 100644 libgnucash/engine/gnc-uri.hpp create mode 100644 libgnucash/engine/test/gtest-gnc-uri.cpp 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 -#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 { 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 #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 -#include "gnc-uri-utils.h" +#include "gnc-uri.hpp" #include "gnc-filepath-utils.h" #include #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::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 ( G_FILE_TEST_IS_REGULAR | G_FILE_TEST_EXISTS) ; file_exists = g_file_test (filepath.c_str(), ftest); @@ -1033,14 +1020,12 @@ QofDbiBackendProvider::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 /* For cleaning up the database */ #include -#include +#include /* For setup_business */ #include "Account.h" #include @@ -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 +#include #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 //for GNC_MOD_BACKEND -#include +#include +#include #include #include @@ -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 #include #include -#include +#include #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 (G_LOG_LEVEL_WARNING); @@ -230,7 +230,7 @@ TEST_P(LoadSaveFiles, test_file) { auto load_uncompressed_session = std::shared_ptr{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{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{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{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 #include #include -#include +#include #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 #include #include -#include +#include #include #include @@ -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 #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 + * Copyright (C) 2026 Brent McBride * * 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 +#include +#include +#include +#include +#include #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& 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 +to_opt (const gchar *s) +{ + return s ? std::optional { 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(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 scheme, + std::optional hostname, + int32_t port, + std::optional username, + std::optional password, + std::optional 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 ( + 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 +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 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 * + * * + * 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 +#include +#include + +/** @addtogroup Engine + @{ */ +/** @file gnc-uri.hpp + * @brief C++ interface to parse and compose GnuCash resource locators. + * @author Copyright (C) 2026 Brent McBride + * + * 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 scheme, + std::optional hostname, + int32_t port, + std::optional username, + std::optional password, + std::optional path); + + const std::optional& scheme() const noexcept { return m_scheme; } + const std::optional& hostname() const noexcept { return m_hostname; } + const std::optional& username() const noexcept { return m_username; } + const std::optional& password() const noexcept { return m_password; } + const std::optional& 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 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 m_scheme; + std::optional m_hostname; + std::optional m_username; + std::optional m_password; + std::optional 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 #include @@ -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 * + * * + * 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 +#include +#include +#include +#include +#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 +#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 #include "qof.h" #include +#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