GnuCash  5.6-150-g038405b370+
gnc-filepath-utils.cpp
1 /********************************************************************\
2  * gnc-filepath-utils.c -- file path resolution utility *
3  * *
4  * This program is free software; you can redistribute it and/or *
5  * modify it under the terms of the GNU General Public License as *
6  * published by the Free Software Foundation; either version 2 of *
7  * the License, or (at your option) any later version. *
8  * *
9  * This program is distributed in the hope that it will be useful, *
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of *
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
12  * GNU General Public License for more details. *
13  * *
14  * You should have received a copy of the GNU General Public License*
15  * along with this program; if not, contact: *
16  * *
17  * Free Software Foundation Voice: +1-617-542-5942 *
18  * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
19  * Boston, MA 02110-1301, USA gnu@gnu.org *
20 \********************************************************************/
21 
22 /*
23  * @file gnc-filepath-utils.c
24  * @brief file path resolution utilities
25  * @author Copyright (c) 1998-2004 Linas Vepstas <linas@linas.org>
26  * @author Copyright (c) 2000 Dave Peticolas
27  */
28 
29 #include <glib.h>
30 #include <glib/gi18n.h>
31 #include <glib/gprintf.h>
32 #include <glib/gstdio.h>
33 
34 #include <config.h>
35 
36 #include <platform.h>
37 #if PLATFORM(WINDOWS)
38 #include <windows.h>
39 #include <Shlobj.h>
40 #endif
41 
42 
43 #include <stdlib.h>
44 #include <stdio.h>
45 #include <string.h>
46 #include <sys/types.h>
47 #include <sys/stat.h>
48 #ifdef HAVE_UNISTD_H
49 # include <unistd.h>
50 #endif
51 #include <errno.h>
52 
53 #include "gnc-path.h"
54 #include "gnc-filepath-utils.h"
55 
56 #if defined (_MSC_VER) || defined (G_OS_WIN32)
57 #include <glib/gwin32.h>
58 #ifndef PATH_MAX
59 #define PATH_MAX MAXPATHLEN
60 #endif
61 #endif
62 #ifdef MAC_INTEGRATION
63 #include <Foundation/Foundation.h>
64 #endif
65 
66 #include "gnc-locale-utils.hpp"
67 #include <boost/filesystem.hpp>
68 #include <boost/locale.hpp>
69 #include <regex>
70 #include <iostream>
71 
72 /* Below cvt and bfs_locale should be used with boost::filesystem::path (bfs)
73  * objects created alter in this source file. The rationale is as follows:
74  * - a bfs object has an internal, locale and platform dependent
75  * representation of a file system path
76  * - glib's internal representation is always utf8
77  * - when creating a bfs object, we should pass a cvt to convert from
78  * utf8 to the object's internal representation
79  * - if we later want to use the bfs object's internal representation
80  * in a glib context we should imbue the locale below so that
81  * bfs will use it to convert back to utf8
82  * - if the bfs object's internal representation will never be used
83  * in a glib context, imbuing is not needed (although probably more
84  * future proof)
85  * - also note creating a bfs based on another one will inherit the
86  * locale from the source path. So in that case there's not need
87  * to imbue it again.
88  */
89 #if PLATFORM(WINDOWS)
90 #include <codecvt>
91 using codecvt = std::codecvt_utf8<wchar_t, 0x10FFFF, std::little_endian>;
92 using string = std::wstring;
93 #else
94 /* See https://stackoverflow.com/questions/41744559/is-this-a-bug-of-gcc */
95 template<class I, class E, class S>
96 struct codecvt_r : std::codecvt<I, E, S>
97 {
98  ~codecvt_r() {}
99 };
101 using string = std::string;
102 #endif
103 static codecvt cvt;
104 static std::locale bfs_locale(std::locale(), new codecvt);
105 
106 namespace bfs = boost::filesystem;
107 namespace bst = boost::system;
108 namespace bl = boost::locale;
109 
116 static gchar *
117 check_path_return_if_valid(gchar *path)
118 {
119  if (g_file_test(path, G_FILE_TEST_IS_REGULAR))
120  {
121  return path;
122  }
123  g_free (path);
124  return NULL;
125 }
126 
154 gchar *
155 gnc_resolve_file_path (const gchar * filefrag)
156 {
157  gchar *fullpath = NULL, *tmp_path = NULL;
158 
159  /* seriously invalid */
160  if (!filefrag)
161  {
162  g_critical("filefrag is NULL");
163  return NULL;
164  }
165 
166  /* ---------------------------------------------------- */
167  /* OK, now we try to find or build an absolute file path */
168 
169  /* check for an absolute file path */
170  if (g_path_is_absolute(filefrag))
171  return g_strdup (filefrag);
172 
173  /* Look in the current working directory */
174  tmp_path = g_get_current_dir();
175  fullpath = g_build_filename(tmp_path, filefrag, (gchar *)NULL);
176  g_free(tmp_path);
177  fullpath = check_path_return_if_valid(fullpath);
178  if (fullpath != NULL)
179  return fullpath;
180 
181  /* Look in the data dir (e.g. $PREFIX/share/gnucash) */
182  tmp_path = gnc_path_get_pkgdatadir();
183  fullpath = g_build_filename(tmp_path, filefrag, (gchar *)NULL);
184  g_free(tmp_path);
185  fullpath = check_path_return_if_valid(fullpath);
186  if (fullpath != NULL)
187  return fullpath;
188 
189  /* Look in the config dir (e.g. $PREFIX/share/gnucash/accounts) */
190  tmp_path = gnc_path_get_accountsdir();
191  fullpath = g_build_filename(tmp_path, filefrag, (gchar *)NULL);
192  g_free(tmp_path);
193  fullpath = check_path_return_if_valid(fullpath);
194  if (fullpath != NULL)
195  return fullpath;
196 
197  /* Look in the users config dir (e.g. $HOME/.gnucash/data) */
198  fullpath = g_strdup(gnc_build_data_path(filefrag));
199  if (g_file_test(fullpath, G_FILE_TEST_IS_REGULAR))
200  return fullpath;
201 
202  /* OK, it's not there. Note that it needs to be created and pass it
203  * back anyway */
204  g_warning("create new file %s", fullpath);
205  return fullpath;
206 
207 }
208 
209 gchar *gnc_file_path_relative_part (const gchar *prefix, const gchar *path)
210 {
211  std::string p{path};
212  if (p.find(prefix) == 0)
213  {
214  auto str = p.substr(strlen(prefix));
215  return g_strdup(str.c_str());
216  }
217  return g_strdup(path);
218 }
219 
220 /* Searches for a file fragment paths set via GNC_DOC_PATH environment
221  * variable. If this variable is not set, fall back to search in
222  * - a html directory in the local user's gnucash settings directory
223  * (typically $HOME/.gnucash/html)
224  * - the gnucash documentation directory
225  * (typically /usr/share/doc/gnucash)
226  * - the gnucash data directory
227  * (typically /usr/share/gnucash)
228  * It searches in this order.
229  *
230  * This is used by gnc_path_find_localized_file to search for
231  * localized versions of files if they exist.
232  */
233 static gchar *
234 gnc_path_find_localized_html_file_internal (const gchar * file_name)
235 {
236  gchar *full_path = NULL;
237  int i;
238  const gchar *env_doc_path = g_getenv("GNC_DOC_PATH");
239  const gchar *default_dirs[] =
240  {
241  gnc_build_userdata_path ("html"),
242  gnc_path_get_pkgdocdir (),
243  gnc_path_get_pkgdatadir (),
244  NULL
245  };
246  gchar **dirs;
247 
248  if (!file_name || *file_name == '\0')
249  return NULL;
250 
251  /* Allow search path override via GNC_DOC_PATH environment variable */
252  if (env_doc_path)
253  dirs = g_strsplit (env_doc_path, G_SEARCHPATH_SEPARATOR_S, -1);
254  else
255  dirs = (gchar **)default_dirs;
256 
257  for (i = 0; dirs[i]; i++)
258  {
259  full_path = g_build_filename (dirs[i], file_name, (gchar *)NULL);
260  g_debug ("Checking for existence of %s", full_path);
261  full_path = check_path_return_if_valid (full_path);
262  if (full_path != NULL)
263  return full_path;
264  }
265 
266  return NULL;
267 }
268 
303 gchar *
304 gnc_path_find_localized_html_file (const gchar *file_name)
305 {
306  gchar *loc_file_name = NULL;
307  gchar *full_path = NULL;
308  const gchar * const *lang;
309 
310  if (!file_name || *file_name == '\0')
311  return NULL;
312 
313  /* An absolute path is returned unmodified. */
314  if (g_path_is_absolute (file_name))
315  return g_strdup (file_name);
316 
317  /* First try to find the file in any of the localized directories
318  * the user has set up on his system
319  */
320  for (lang = g_get_language_names (); *lang; lang++)
321  {
322  loc_file_name = g_build_filename (*lang, file_name, (gchar *)NULL);
323  full_path = gnc_path_find_localized_html_file_internal (loc_file_name);
324  g_free (loc_file_name);
325  if (full_path != NULL)
326  return full_path;
327  }
328 
329  /* If not found in a localized directory, try to find the file
330  * in any of the base directories
331  */
332  return gnc_path_find_localized_html_file_internal (file_name);
333 
334 }
335 
336 /* ====================================================================== */
337 static auto gnc_userdata_home = bfs::path();
338 static auto gnc_userconfig_home = bfs::path();
339 static auto build_dir = bfs::path();
340 /*Provide static-stored strings for gnc_userdata_home and
341  * gnc_userconfig_home to ensure that the cstrings don't go out of
342  * scope when gnc_userdata_dir and gnc_userconfig_dir return them.
343  */
344 static std::string gnc_userdata_home_str;
345 static std::string gnc_userconfig_home_str;
346 
347 static bool dir_is_descendant (const bfs::path& path, const bfs::path& base)
348 {
349  auto test_path = path;
350  if (bfs::exists (path))
351  test_path = bfs::canonical (path);
352  auto test_base = base;
353  if (bfs::exists (base))
354  test_base = bfs::canonical (base);
355 
356  auto is_descendant = (test_path.string() == test_base.string());
357  while (!test_path.empty() && !is_descendant)
358  {
359  test_path = test_path.parent_path();
360  is_descendant = (test_path.string() == test_base.string());
361  }
362  return is_descendant;
363 }
364 
370 static bool
371 gnc_validate_directory (const bfs::path &dirname)
372 {
373  if (dirname.empty())
374  return false;
375 
376  auto create_dirs = true;
377  if (build_dir.empty() || !dir_is_descendant (dirname, build_dir))
378  {
379  /* Gnucash won't create a home directory
380  * if it doesn't exist yet. So if the directory to create
381  * is a descendant of the homedir, we can't create it either.
382  * This check is conditioned on do_homedir_check because
383  * we need to overrule it during build (when guile interferes)
384  * and testing.
385  */
386  bfs::path home_dir(g_get_home_dir(), cvt);
387  home_dir.imbue(bfs_locale);
388  auto homedir_exists = bfs::exists(home_dir);
389  auto is_descendant = dir_is_descendant (dirname, home_dir);
390  if (!homedir_exists && is_descendant)
391  create_dirs = false;
392  }
393 
394  /* Create directories if they don't exist yet and we can
395  *
396  * Note this will do nothing if the directory and its
397  * parents already exist, but will fail if the path
398  * points to a file or a softlink. So it serves as a test
399  * for that as well.
400  */
401  if (create_dirs)
402  bfs::create_directories(dirname);
403  else
404  throw (bfs::filesystem_error (
405  std::string (dirname.string() +
406  " is a descendant of a non-existing home directory. As " +
407  PACKAGE_NAME +
408  " will never create a home directory this path can't be used"),
409  dirname, bst::error_code(bst::errc::permission_denied, bst::generic_category())));
410 
411  auto d = bfs::directory_entry (dirname);
412  auto perms = d.status().permissions();
413 
414  /* On Windows only write permission will be checked.
415  * So strictly speaking we'd need two error messages here depending
416  * on the platform. For simplicity this detail is glossed over though. */
417 #if PLATFORM(WINDOWS)
418  auto check_perms = bfs::owner_read | bfs::owner_write;
419 #else
420  auto check_perms = bfs::owner_all;
421 #endif
422  if ((perms & check_perms) != check_perms)
423  throw (bfs::filesystem_error(
424  std::string("Insufficient permissions, at least write and access permissions required: ")
425  + dirname.string(), dirname,
426  bst::error_code(bst::errc::permission_denied, bst::generic_category())));
427 
428  return true;
429 }
430 
431 /* Will attempt to copy all files and directories from src to dest
432  * Returns true if successful or false if not */
433 static bool
434 copy_recursive(const bfs::path& src, const bfs::path& dest)
435 {
436  if (!bfs::exists(src))
437  return false;
438 
439  // Don't copy on self
440  if (src.compare(dest) == 0)
441  return false;
442 
443  auto old_str = src.string();
444  auto old_len = old_str.size();
445  try
446  {
447  /* Note: the for(auto elem : iterator) construct fails
448  * on travis (g++ 4.8.x, boost 1.54) so I'm using
449  * a traditional for loop here */
450  for(auto direntry = bfs::recursive_directory_iterator(src);
451  direntry != bfs::recursive_directory_iterator(); ++direntry)
452  {
453 #ifdef G_OS_WIN32
454  string cur_str = direntry->path().wstring();
455 #else
456  string cur_str = direntry->path().string();
457 #endif
458  auto cur_len = cur_str.size();
459  string rel_str(cur_str, old_len, cur_len - old_len);
460  bfs::path relpath(rel_str, cvt);
461  auto newpath = bfs::absolute (relpath.relative_path(), dest);
462  newpath.imbue(bfs_locale);
463  bfs::copy(direntry->path(), newpath);
464  }
465  }
466  catch(const bfs::filesystem_error& ex)
467  {
468  g_warning("An error occurred while trying to migrate the user configation from\n%s to\n%s"
469  "(Error: %s)",
470  src.string().c_str(), gnc_userdata_home_str.c_str(),
471  ex.what());
472  return false;
473  }
474 
475  return true;
476 }
477 
478 #ifdef G_OS_WIN32
479 /* g_get_user_data_dir defaults to CSIDL_LOCAL_APPDATA, but
480  * the roaming profile makes more sense.
481  * So this function is a copy of glib's internal get_special_folder
482  * and minimally adjusted to fetch CSIDL_APPDATA
483  */
484 static bfs::path
485 get_user_data_dir ()
486 {
487  wchar_t path[MAX_PATH+1];
488  HRESULT hr;
489  LPITEMIDLIST pidl = NULL;
490  BOOL b;
491 
492  hr = SHGetSpecialFolderLocation (NULL, CSIDL_APPDATA, &pidl);
493  if (hr == S_OK)
494  {
495  b = SHGetPathFromIDListW (pidl, path);
496  CoTaskMemFree (pidl);
497  }
498  bfs::path retval(path, cvt);
499  retval.imbue(bfs_locale);
500  return retval;
501 }
502 #elif defined MAC_INTEGRATION
503 static bfs::path
504 get_user_data_dir()
505 {
506  NSFileManager*fm = [NSFileManager defaultManager];
507  NSArray* appSupportDir = [fm URLsForDirectory:NSApplicationSupportDirectory
508  inDomains:NSUserDomainMask];
509  NSString *dirPath = nullptr;
510  if ([appSupportDir count] > 0)
511  {
512  NSURL* dirUrl = [appSupportDir objectAtIndex:0];
513  dirPath = [dirUrl path];
514  }
515  return [dirPath UTF8String];
516 }
517 #else
518 static bfs::path
519 get_user_data_dir()
520 {
521  return g_get_user_data_dir();
522 }
523 #endif
524 
525 /* Returns an absolute path to the user's data directory.
526  * Note the default path depends on the platform.
527  * - Windows: CSIDL_APPDATA
528  * - OS X: $HOME/Application Support
529  * - Linux: $XDG_DATA_HOME (or the default $HOME/.local/share)
530  */
531 static bfs::path
532 get_userdata_home(void)
533 {
534  auto try_tmp_dir = true;
535  auto userdata_home = get_user_data_dir();
536 
537  /* g_get_user_data_dir doesn't check whether the path exists nor attempts to
538  * create it. So while it may return an actual path we may not be able to use it.
539  * Let's check that now */
540  if (!userdata_home.empty())
541  {
542  try
543  {
544  gnc_validate_directory(userdata_home); // May throw
545  try_tmp_dir = false;
546  }
547  catch (const bfs::filesystem_error& ex)
548  {
549  auto path_string = userdata_home.string();
550  g_warning("%s is not a suitable base directory for the user data. "
551  "Trying temporary directory instead.\n(Error: %s)",
552  path_string.c_str(), ex.what());
553  }
554  }
555 
556  /* The path we got is not usable, so fall back to a path in TMP_DIR.
557  Hopefully we can always write there. */
558  if (try_tmp_dir)
559  {
560  bfs::path newpath(g_get_tmp_dir (), cvt);
561  userdata_home = newpath / g_get_user_name ();
562  userdata_home.imbue(bfs_locale);
563  }
564  g_assert(!userdata_home.empty());
565 
566  return userdata_home;
567 }
568 
569 /* Returns an absolute path to the user's config directory.
570  * Note the default path depends on the platform.
571  * - Windows: CSIDL_APPDATA
572  * - OS X: $HOME/Application Support
573  * - Linux: $XDG_CONFIG_HOME (or the default $HOME/.config)
574  */
575 static bfs::path
576 get_userconfig_home(void)
577 {
578  /* On Windows and Macs the data directory is used, for Linux
579  $HOME/.config is used */
580 #if defined (G_OS_WIN32) || defined (MAC_INTEGRATION)
581  return get_user_data_dir();
582 #else
583  return g_get_user_config_dir();
584 #endif
585 }
586 
587 static std::string migrate_gnc_datahome()
588 {
589  // Specify location of dictionaries
590  bfs::path old_dir(g_get_home_dir(), cvt);
591  old_dir /= ".gnucash";
592 
593  std::stringstream migration_msg;
594  migration_msg.imbue(gnc_get_boost_locale());
595 
596  /* Step 1: copy directory $HOME/.gnucash to $GNC_DATA_HOME */
597  auto full_copy = copy_recursive (old_dir, gnc_userdata_home);
598 
599  /* Step 2: move user editable config files from GNC_DATA_HOME to GNC_CONFIG_HOME
600  These files are:
601  - log.conf
602  - the most recent of "config-2.0.user", "config-1.8.user", "config-1.6.user",
603  "config.user"
604  Note: we'll also rename config.user to config-user.scm to make it more clear
605  this file is meant for custom scm code to load at run time */
606  auto failed = std::vector<std::string>{};
607  auto succeeded = std::vector<std::string>{};
608 
609  /* Move log.conf
610  * Note on OS X/Quarz and Windows GNC_DATA_HOME and GNC_CONFIG_HOME are the same, so this will do nothing */
611  auto oldlogpath = gnc_userdata_home / "log.conf";
612  auto newlogpath = gnc_userconfig_home / "log.conf";
613  try
614  {
615  if (bfs::exists (oldlogpath) && gnc_validate_directory (gnc_userconfig_home) &&
616  (oldlogpath != newlogpath))
617  {
618  bfs::rename (oldlogpath, newlogpath);
619  succeeded.emplace_back ("log.conf");
620  }
621  }
622  catch (const bfs::filesystem_error& ex)
623  {
624  failed.emplace_back ("log.conf");
625  }
626 
627  /* Move/rename the most relevant config*.user file. The relevance comes from
628  * the order in which these files were searched for at gnucash startup.
629  * Make note of other config*.user files found to inform the user they are now ignored */
630  auto user_config_files = std::vector<std::string>
631  {
632  "config-2.0.user", "config-1.8.user",
633  "config-1.6.user", "config.user"
634  };
635  auto conf_exist_vec = std::vector<std::string> {};
636  auto renamed_config = std::string();
637  for (auto conf_file : user_config_files)
638  {
639  auto oldconfpath = gnc_userdata_home / conf_file;
640  try
641  {
642  if (bfs::exists (oldconfpath) && gnc_validate_directory (gnc_userconfig_home))
643  {
644  // Only migrate the most relevant of the config*.user files
645  if (renamed_config.empty())
646  {
647  /* Translators: this string refers to a file name that gets renamed */
648  renamed_config = conf_file + " (" + _("Renamed to:") + " config-user.scm)";
649  auto newconfpath = gnc_userconfig_home / "config-user.scm";
650  bfs::rename (oldconfpath, newconfpath);
651  }
652  else
653  {
654  /* We want to report the obsolete file to the user */
655  conf_exist_vec.emplace_back (conf_file);
656  if (full_copy)
657  /* As we copied from .gnucash, just delete the obsolete file as well. It's still
658  * present in .gnucash if the user wants to recover it */
659  bfs::remove (oldconfpath);
660  }
661  }
662  }
663  catch (const bfs::filesystem_error& ex)
664  {
665  failed.emplace_back (conf_file);
666  }
667  }
668  if (!renamed_config.empty())
669  succeeded.emplace_back (renamed_config);
670 
671  /* Step 3: inform the user of additional changes */
672  if (full_copy || !succeeded.empty() || !conf_exist_vec.empty() || !failed.empty())
673  migration_msg << _("Notice") << std::endl << std::endl;
674 
675  if (full_copy)
676  {
677  migration_msg
678  << _("Your gnucash metadata has been migrated.") << std::endl << std::endl
679  /* Translators: this refers to a directory name. */
680  << _("Old location:") << " " << old_dir.string() << std::endl
681  /* Translators: this refers to a directory name. */
682  << _("New location:") << " " << gnc_userdata_home.string() << std::endl << std::endl
683  // Translators {1} will be replaced with the package name (typically Gnucash) at runtime
684  << bl::format (std::string{_("If you no longer intend to run {1} 2.6.x or older on this system you can safely remove the old directory.")})
685  % PACKAGE_NAME;
686  }
687 
688  if (full_copy &&
689  (!succeeded.empty() || !conf_exist_vec.empty() || !failed.empty()))
690  migration_msg << std::endl << std::endl
691  << _("In addition:");
692 
693  if (!succeeded.empty())
694  {
695  migration_msg << std::endl << std::endl;
696  if (full_copy)
697  migration_msg << bl::format (std::string{ngettext("The following file has been copied to {1} instead:",
698  "The following files have been copied to {1} instead:",
699  succeeded.size())}) % gnc_userconfig_home.string().c_str();
700  else
701  migration_msg << bl::format (std::string{_("The following file in {1} has been renamed:")})
702  % gnc_userconfig_home.string().c_str();
703 
704  migration_msg << std::endl;
705  for (const auto& success_file : succeeded)
706  migration_msg << "- " << success_file << std::endl;
707  }
708  if (!conf_exist_vec.empty())
709  {
710  migration_msg << "\n\n"
711  << ngettext("The following file has become obsolete and will be ignored:",
712  "The following files have become obsolete and will be ignored:",
713  conf_exist_vec.size())
714  << std::endl;
715  for (const auto& obs_file : conf_exist_vec)
716  migration_msg << "- " << obs_file << std::endl;
717  }
718  if (!failed.empty())
719  {
720  migration_msg << std::endl << std::endl
721  << bl::format (std::string{ngettext("The following file could not be moved to {1}:",
722  "The following files could not be moved to {1}:",
723  failed.size())}) % gnc_userconfig_home.string().c_str()
724  << std::endl;
725  for (const auto& failed_file : failed)
726  migration_msg << "- " << failed_file << std::endl;
727  }
728 
729  return migration_msg.str ();
730 }
731 
732 
733 
734 #if defined G_OS_WIN32 ||defined MAC_INTEGRATION
735 constexpr auto path_package = PACKAGE_NAME;
736 #else
737 constexpr auto path_package = PROJECT_NAME;
738 #endif
739 
740 // Initialize the user's config directory for gnucash
741 // creating it if it doesn't exist yet.
742 static void
743 gnc_file_path_init_config_home (void)
744 {
745  auto have_valid_userconfig_home = false;
746 
747  /* If this code is run while building/testing, use a fake GNC_CONFIG_HOME
748  * in the base of the build directory. This is to deal with all kinds of
749  * issues when the build environment is not a complete environment (like
750  * it could be missing a valid home directory). */
751  auto env_build_dir = g_getenv ("GNC_BUILDDIR");
752  bfs::path new_dir(env_build_dir ? env_build_dir : "", cvt);
753  new_dir.imbue(bfs_locale);
754  build_dir = std::move(new_dir);
755  auto running_uninstalled = (g_getenv ("GNC_UNINSTALLED") != NULL);
756  if (running_uninstalled && !build_dir.empty())
757  {
758  gnc_userconfig_home = build_dir / "gnc_config_home";
759  try
760  {
761  gnc_validate_directory (gnc_userconfig_home); // May throw
762  have_valid_userconfig_home = true;
763  }
764  catch (const bfs::filesystem_error& ex)
765  {
766  auto path_string = gnc_userconfig_home.string();
767  g_warning("%s (due to run during at build time) is not a suitable directory for user configuration files. "
768  "Trying another directory instead.\n(Error: %s)",
769  path_string.c_str(), ex.what());
770  }
771  }
772 
773  if (!have_valid_userconfig_home)
774  {
775  /* If environment variable GNC_CONFIG_HOME is set, try whether
776  * it points at a valid directory. */
777  auto gnc_userconfig_home_env = g_getenv ("GNC_CONFIG_HOME");
778  if (gnc_userconfig_home_env)
779  {
780  bfs::path newdir(gnc_userconfig_home_env, cvt);
781  newdir.imbue(bfs_locale);
782  gnc_userconfig_home = std::move(newdir);
783  try
784  {
785  gnc_validate_directory (gnc_userconfig_home); // May throw
786  have_valid_userconfig_home = true;
787  }
788  catch (const bfs::filesystem_error& ex)
789  {
790  auto path_string = gnc_userconfig_home.string();
791  g_warning("%s (from environment variable 'GNC_CONFIG_HOME') is not a suitable directory for user configuration files. "
792  "Trying the default instead.\n(Error: %s)",
793  path_string.c_str(), ex.what());
794  }
795  }
796  }
797 
798  if (!have_valid_userconfig_home)
799  {
800  /* Determine platform dependent default userconfig_home_path
801  * and check whether it's valid */
802  auto userconfig_home = get_userconfig_home();
803  gnc_userconfig_home = userconfig_home / path_package;
804  try
805  {
806  gnc_validate_directory (gnc_userconfig_home);
807  }
808  catch (const bfs::filesystem_error& ex)
809  {
810  g_warning ("User configuration directory doesn't exist, yet could not be created. Proceed with caution.\n"
811  "(Error: %s)", ex.what());
812  }
813  }
814  gnc_userconfig_home_str = gnc_userconfig_home.string();
815 }
816 
817 // Initialize the user's config directory for gnucash
818 // creating it if it didn't exist yet.
819 // The function will return true if the directory already
820 // existed or false if it had to be created
821 static bool
822 gnc_file_path_init_data_home (void)
823 {
824  // Initialize the user's data directory for gnucash
825  auto gnc_userdata_home_exists = false;
826  auto have_valid_userdata_home = false;
827 
828  /* If this code is run while building/testing, use a fake GNC_DATA_HOME
829  * in the base of the build directory. This is to deal with all kinds of
830  * issues when the build environment is not a complete environment (like
831  * it could be missing a valid home directory). */
832  auto env_build_dir = g_getenv ("GNC_BUILDDIR");
833  bfs::path new_dir(env_build_dir ? env_build_dir : "", cvt);
834  new_dir.imbue(bfs_locale);
835  build_dir = std::move(new_dir);
836  auto running_uninstalled = (g_getenv ("GNC_UNINSTALLED") != NULL);
837  if (running_uninstalled && !build_dir.empty())
838  {
839  gnc_userdata_home = build_dir / "gnc_data_home";
840  try
841  {
842  gnc_validate_directory (gnc_userdata_home); // May throw
843  have_valid_userdata_home = true;
844  gnc_userdata_home_exists = true; // To prevent possible migration further down
845  }
846  catch (const bfs::filesystem_error& ex)
847  {
848  auto path_string = gnc_userdata_home.string();
849  g_warning("%s (due to run during at build time) is not a suitable directory for user data. "
850  "Trying another directory instead.\n(Error: %s)",
851  path_string.c_str(), ex.what());
852  }
853  }
854 
855  if (!have_valid_userdata_home)
856  {
857  /* If environment variable GNC_DATA_HOME is set, try whether
858  * it points at a valid directory. */
859  auto gnc_userdata_home_env = g_getenv ("GNC_DATA_HOME");
860  if (gnc_userdata_home_env)
861  {
862  bfs::path newdir(gnc_userdata_home_env, cvt);
863  newdir.imbue(bfs_locale);
864  gnc_userdata_home = std::move(newdir);
865  try
866  {
867  gnc_userdata_home_exists = bfs::exists (gnc_userdata_home);
868  gnc_validate_directory (gnc_userdata_home); // May throw
869  have_valid_userdata_home = true;
870  }
871  catch (const bfs::filesystem_error& ex)
872  {
873  auto path_string = gnc_userdata_home.string();
874  g_warning("%s (from environment variable 'GNC_DATA_HOME') is not a suitable directory for user data. "
875  "Trying the default instead.\n(Error: %s)",
876  path_string.c_str(), ex.what());
877  }
878  }
879  }
880 
881  if (!have_valid_userdata_home)
882  {
883  /* Determine platform dependent default userdata_home_path
884  * and check whether it's valid */
885  auto userdata_home = get_userdata_home();
886  gnc_userdata_home = userdata_home / path_package;
887  try
888  {
889  gnc_userdata_home_exists = bfs::exists (gnc_userdata_home);
890  gnc_validate_directory (gnc_userdata_home);
891  }
892  catch (const bfs::filesystem_error& ex)
893  {
894  g_warning ("User data directory doesn't exist, yet could not be created. Proceed with caution.\n"
895  "(Error: %s)", ex.what());
896  }
897  }
898  gnc_userdata_home_str = gnc_userdata_home.string();
899  return gnc_userdata_home_exists;
900 }
901 
902 // Initialize the user's config and data directory for gnucash
903 // This function will also create these directories if they didn't
904 // exist yet.
905 // In addition it will trigger a migration if the user's data home
906 // didn't exist but the now obsolete GNC_DOT_DIR ($HOME/.gnucash)
907 // does.
908 // Finally it well ensure a number of default required directories
909 // will be created if they don't exist yet.
910 char *
912 {
913  gnc_userconfig_home = get_userconfig_home() / path_package;
914  gnc_userconfig_home_str = gnc_userconfig_home.string();
915 
916  gnc_file_path_init_config_home ();
917  auto gnc_userdata_home_exists = gnc_file_path_init_data_home ();
918 
919  /* Run migration code before creating the default directories
920  If migrating, these default directories are copied instead of created. */
921  auto migration_notice = std::string ();
922  if (!gnc_userdata_home_exists)
923  migration_notice = migrate_gnc_datahome();
924 
925  /* Try to create the standard subdirectories for gnucash' user data */
926  try
927  {
928  gnc_validate_directory (gnc_userdata_home / "books");
929  gnc_validate_directory (gnc_userdata_home / "checks");
930  gnc_validate_directory (gnc_userdata_home / "translog");
931  }
932  catch (const bfs::filesystem_error& ex)
933  {
934  g_warning ("Default user data subdirectories don't exist, yet could not be created. Proceed with caution.\n"
935  "(Error: %s)", ex.what());
936  }
937 
938  return migration_notice.empty() ? NULL : g_strdup (migration_notice.c_str());
939 }
940 
960 /* Note Don't create missing directories automatically
961  * here and in the next function except if the
962  * target directory is the temporary directory. This
963  * should be done properly at a higher level (in the gui
964  * code most likely) very early in application startup.
965  * This call is just a fallback to prevent the code from
966  * crashing because no directories were configured. This
967  * weird construct is set up because compiling our guile
968  * scripts also triggers this code and that's not the
969  * right moment to start creating the necessary directories.
970  * FIXME A better approach would be to have the gnc_userdata_home
971  * verification/creation be part of the application code instead
972  * of libgnucash. If libgnucash needs access to this directory
973  * libgnucash will need some kind of initialization routine
974  * that the application can call to set (among others) the proper
975  * gnc_uderdata_home for libgnucash. The only other aspect to
976  * consider here is how to handle this in the bindings (if they
977  * need it).
978  */
979 const gchar *
981 {
982  if (gnc_userdata_home.empty())
984  return g_strdup(gnc_userdata_home_str.c_str());
985 }
986 
1000 const gchar *
1002 {
1003  if (gnc_userdata_home.empty())
1005 
1006  return gnc_userconfig_home_str.c_str();
1007 }
1008 
1009 static const bfs::path&
1010 gnc_userdata_dir_as_path (void)
1011 {
1012  if (gnc_userdata_home.empty())
1013  /* Don't create missing directories automatically except
1014  * if the target directory is the temporary directory. This
1015  * should be done properly at a higher level (in the gui
1016  * code most likely) very early in application startup.
1017  * This call is just a fallback to prevent the code from
1018  * crashing because no directories were configured. */
1020 
1021  return gnc_userdata_home;
1022 }
1023 
1024 static const bfs::path&
1025 gnc_userconfig_dir_as_path (void)
1026 {
1027  if (gnc_userdata_home.empty())
1028  /* Don't create missing directories automatically except
1029  * if the target directory is the temporary directory. This
1030  * should be done properly at a higher level (in the gui
1031  * code most likely) very early in application startup.
1032  * This call is just a fallback to prevent the code from
1033  * crashing because no directories were configured. */
1035 
1036  return gnc_userconfig_home;
1037 }
1038 
1039 gchar *gnc_file_path_absolute (const gchar *prefix, const gchar *relative)
1040 {
1041  bfs::path path_relative (relative);
1042  path_relative.imbue (bfs_locale);
1043  bfs::path path_absolute;
1044  bfs::path path_head;
1045 
1046  if (prefix == nullptr)
1047  {
1048  const gchar *doc_dir = g_get_user_special_dir (G_USER_DIRECTORY_DOCUMENTS);
1049  if (doc_dir == nullptr)
1050  path_head = bfs::path (gnc_userdata_dir ()); // running as root maybe
1051  else
1052  path_head = bfs::path (doc_dir);
1053 
1054  path_head.imbue (bfs_locale);
1055  path_absolute = absolute (path_relative, path_head);
1056  }
1057  else
1058  {
1059  bfs::path path_head (prefix);
1060  path_head.imbue (bfs_locale);
1061  path_absolute = absolute (path_relative, path_head);
1062  }
1063  path_absolute.imbue (bfs_locale);
1064 
1065  return g_strdup (path_absolute.string().c_str());
1066 }
1067 
1077 gchar *
1078 gnc_build_userdata_path (const gchar *filename)
1079 {
1080  return g_strdup((gnc_userdata_dir_as_path() / filename).string().c_str());
1081 }
1082 
1092 gchar *
1093 gnc_build_userconfig_path (const gchar *filename)
1094 {
1095  return g_strdup((gnc_userconfig_dir_as_path() / filename).string().c_str());
1096 }
1097 
1098 /* Test whether c is a valid character for a win32 file name.
1099  * If so return false, otherwise return true.
1100  */
1101 static bool
1102 is_invalid_char (char c)
1103 {
1104  return (c == '/') || ( c == ':');
1105 }
1106 
1107 static bfs::path
1108 gnc_build_userdata_subdir_path (const gchar *subdir, const gchar *filename)
1109 {
1110  auto fn = std::string(filename);
1111 
1112  std::replace_if (fn.begin(), fn.end(), is_invalid_char, '_');
1113  auto result = (gnc_userdata_dir_as_path() / subdir) / fn;
1114  return result;
1115 }
1116 
1126 gchar *
1127 gnc_build_book_path (const gchar *filename)
1128 {
1129  auto path = gnc_build_userdata_subdir_path("books", filename).string();
1130  return g_strdup(path.c_str());
1131 }
1132 
1142 gchar *
1143 gnc_build_translog_path (const gchar *filename)
1144 {
1145  auto path = gnc_build_userdata_subdir_path("translog", filename).string();
1146  return g_strdup(path.c_str());
1147 }
1148 
1158 gchar *
1159 gnc_build_data_path (const gchar *filename)
1160 {
1161  auto path = gnc_build_userdata_subdir_path("data", filename).string();
1162  return g_strdup(path.c_str());
1163 }
1164 
1174 gchar *
1175 gnc_build_scm_path (const gchar *filename)
1176 {
1177  gchar *scmdir = gnc_path_get_scmdir ();
1178  gchar *result = g_build_filename (scmdir, filename, (gchar *)NULL);
1179  g_free (scmdir);
1180  return result;
1181 }
1182 
1192 gchar *
1193 gnc_build_report_path (const gchar *filename)
1194 {
1195  gchar *rptdir = gnc_path_get_reportdir ();
1196  gchar *result = g_build_filename (rptdir, filename, (gchar *)NULL);
1197  g_free (rptdir);
1198  return result;
1199 }
1200 
1210 gchar *
1211 gnc_build_reports_path (const gchar *dirname)
1212 {
1213  gchar *rptsdir = gnc_path_get_reportsdir ();
1214  gchar *result = g_build_filename (rptsdir, dirname, (gchar *)NULL);
1215  g_free (rptsdir);
1216  return result;
1217 }
1218 
1228 gchar *
1229 gnc_build_stdreports_path (const gchar *filename)
1230 {
1231  gchar *stdrptdir = gnc_path_get_stdreportsdir ();
1232  gchar *result = g_build_filename (stdrptdir, filename, (gchar *)NULL);
1233  g_free (stdrptdir);
1234  return result;
1235 }
1236 
1237 static gchar *
1238 gnc_filepath_locate_file (const gchar *default_path, const gchar *name)
1239 {
1240  gchar *fullname;
1241 
1242  g_return_val_if_fail (name != NULL, NULL);
1243 
1244  if (g_path_is_absolute (name))
1245  fullname = g_strdup (name);
1246  else if (default_path)
1247  fullname = g_build_filename (default_path, name, nullptr);
1248  else
1249  fullname = gnc_resolve_file_path (name);
1250 
1251  if (!g_file_test (fullname, G_FILE_TEST_IS_REGULAR))
1252  {
1253  g_warning ("Could not locate file %s", name);
1254  g_free (fullname);
1255  return NULL;
1256  }
1257 
1258  return fullname;
1259 }
1260 
1261 gchar *
1263 {
1264  gchar *pkgdatadir = gnc_path_get_pkgdatadir ();
1265  gchar *result = gnc_filepath_locate_file (pkgdatadir, name);
1266  g_free (pkgdatadir);
1267  return result;
1268 }
1269 
1270 gchar *
1271 gnc_filepath_locate_pixmap (const gchar *name)
1272 {
1273  gchar *default_path;
1274  gchar *fullname;
1275  gchar* pkgdatadir = gnc_path_get_pkgdatadir ();
1276 
1277  default_path = g_build_filename (pkgdatadir, "pixmaps", nullptr);
1278  g_free(pkgdatadir);
1279  fullname = gnc_filepath_locate_file (default_path, name);
1280  g_free(default_path);
1281 
1282  return fullname;
1283 }
1284 
1285 gchar *
1286 gnc_filepath_locate_ui_file (const gchar *name)
1287 {
1288  gchar *default_path;
1289  gchar *fullname;
1290  gchar* pkgdatadir = gnc_path_get_pkgdatadir ();
1291 
1292  default_path = g_build_filename (pkgdatadir, "ui", nullptr);
1293  g_free(pkgdatadir);
1294  fullname = gnc_filepath_locate_file (default_path, name);
1295  g_free(default_path);
1296 
1297  return fullname;
1298 }
1299 
1300 gchar *
1301 gnc_filepath_locate_doc_file (const gchar *name)
1302 {
1303  gchar *docdir = gnc_path_get_pkgdocdir ();
1304  gchar *result = gnc_filepath_locate_file (docdir, name);
1305  g_free (docdir);
1306  return result;
1307 }
1308 
1309 std::vector<EnvPaths>
1310 gnc_list_all_paths ()
1311 {
1312  if (gnc_userdata_home.empty())
1313  gnc_filepath_init ();
1314 
1315  return {
1316  { "GNC_USERDATA_DIR", gnc_userdata_home_str.c_str(), true},
1317  { "GNC_USERCONFIG_DIR", gnc_userconfig_home_str.c_str(), true },
1318  { "GNC_BIN", g_getenv ("GNC_BIN"), false },
1319  { "GNC_LIB", g_getenv ("GNC_LIB"), false },
1320  { "GNC_CONF", g_getenv ("GNC_CONF"), false },
1321  { "GNC_DATA", g_getenv ("GNC_DATA"), false },
1322  };
1323 }
1324 
1325 static const std::regex
1326 backup_regex (".*[.](?:xac|gnucash)[.][0-9]{14}[.](?:xac|gnucash)$");
1327 
1328 gboolean gnc_filename_is_backup (const char *filename)
1329 {
1330  return std::regex_match (filename, backup_regex);
1331 }
1332 
1333 static const std::regex
1334 datafile_regex (".*[.](?:xac|gnucash)$");
1335 
1336 gboolean gnc_filename_is_datafile (const char *filename)
1337 {
1338  return !gnc_filename_is_backup (filename) &&
1339  std::regex_match (filename, datafile_regex);
1340 }
1341 
1342 std::ofstream
1343 gnc_open_filestream(const char* path)
1344 {
1345  bfs::path bfs_path(path, cvt);
1346  bfs_path.imbue(bfs_locale);
1347  return std::ofstream(bfs_path.c_str());
1348 }
1349 /* =============================== END OF FILE ========================== */
gchar * gnc_filepath_locate_data_file(const gchar *name)
Given a file name, find the file in the directories associated with this application.
gchar * gnc_build_reports_path(const gchar *dirname)
Make a path to dirname in the reports directory.
gchar * gnc_build_book_path(const gchar *filename)
Make a path to filename in the book subdirectory of the user&#39;s configuration directory.
gchar * gnc_file_path_absolute(const gchar *prefix, const gchar *relative)
Given a prefix and a relative path, return the absolute path.
gchar * gnc_build_userdata_path(const gchar *filename)
Make a path to filename in the user&#39;s gnucash data directory.
gchar * gnc_build_data_path(const gchar *filename)
Make a path to filename in the data subdirectory of the user&#39;s configuration directory.
gchar * gnc_filepath_locate_ui_file(const gchar *name)
Given a ui file name, find the file in the ui directory associated with this application.
const gchar * gnc_userdata_dir(void)
Ensure that the user&#39;s configuration directory exists and is minimally populated. ...
gchar * gnc_resolve_file_path(const gchar *filefrag)
The gnc_resolve_file_path() routine is a utility that will accept a fragmentary filename as input...
gchar * gnc_build_stdreports_path(const gchar *filename)
Make a path to filename in the standard reports directory.
gchar * gnc_build_scm_path(const gchar *filename)
Make a path to filename in the scm directory.
gchar * gnc_filepath_locate_doc_file(const gchar *name)
Given a documentation file name, find the file in the doc directory associated with this application...
gchar * gnc_build_report_path(const gchar *filename)
Make a path to filename in the report directory.
char * gnc_filepath_init(void)
Initializes the gnucash user data directory.
gchar * gnc_file_path_relative_part(const gchar *prefix, const gchar *path)
Given a prefix and a path return the relative portion of the path.
gchar * gnc_filepath_locate_pixmap(const gchar *name)
Given a pixmap/pixbuf file name, find the file in the pixmap directory associated with this applicati...
gchar * gnc_build_translog_path(const gchar *filename)
Make a path to filename in the translog subdirectory of the user&#39;s configuration directory.
gchar * gnc_build_userconfig_path(const gchar *filename)
Make a path to filename in the user&#39;s configuration directory.
gchar * gnc_path_find_localized_html_file(const gchar *file_name)
Find an absolute path to a localized version of a given relative path to a html or html related file...
const gchar * gnc_userconfig_dir(void)
Return the user&#39;s config directory for gnucash.
File path resolution utility functions.