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