gnucash/libgnucash/core-utils/gnc-filepath-utils.cpp
Geert Janssens 1238b9d8cd Prevent gcc from searching config.h in the current directory
This will avoid a ninja-build from picking up a config.h generated by the autotools build
(in the root build directory). Picking up the wrong config.h may lead to all kinds of
subtle issues if the autotools run was done with different options than the cmake run.
2017-10-26 14:05:17 +02:00

831 lines
26 KiB
C++

/********************************************************************\
* gnc-filepath-utils.c -- file path resolutin utilitie *
* *
* 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 *
\********************************************************************/
/*
* @file gnc-filepath-utils.c
* @brief file path resolution utilities
* @author Copyright (c) 1998-2004 Linas Vepstas <linas@linas.org>
* @author Copyright (c) 2000 Dave Peticolas
*/
extern "C" {
#include <config.h>
#include <platform.h>
#if PLATFORM(WINDOWS)
#include <windows.h>
#include <Shlobj.h>
#endif
#include <glib.h>
#include <glib/gi18n.h>
#include <glib/gprintf.h>
#include <glib/gstdio.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#ifdef HAVE_UNISTD_H
# include <unistd.h>
#endif
#include <errno.h>
#include "gnc-path.h"
#include "gnc-filepath-utils.h"
#if defined (_MSC_VER) || defined (G_OS_WIN32)
#include <glib/gwin32.h>
#ifndef PATH_MAX
#define PATH_MAX MAXPATHLEN
#endif
#endif
#ifdef MAC_INTEGRATION
#include <Foundation/Foundation.h>
#endif
}
#include <boost/filesystem.hpp>
#if PLATFORM(WINDOWS)
#include <codecvt>
#include <locale>
#endif
namespace bfs = boost::filesystem;
namespace bst = boost::system;
/**
* Scrubs a filename by changing "strange" chars (e.g. those that are not
* valid in a win32 file name) to "_".
*
* @param filename File name - updated in place
*/
static void
scrub_filename(char* filename)
{
char* p;
#define STRANGE_CHARS "/:"
p = strpbrk(filename, STRANGE_CHARS);
while (p)
{
*p = '_';
p = strpbrk(filename, STRANGE_CHARS);
}
}
/** Check if the path exists and is a regular file.
*
* \param path -- freed if the path doesn't exist or isn't a regular file
*
* \return NULL or the path
*/
static gchar *
check_path_return_if_valid(gchar *path)
{
if (g_file_test(path, G_FILE_TEST_IS_REGULAR))
{
return path;
}
g_free (path);
return NULL;
}
/** @brief Create an absolute path when given a relative path;
* otherwise return the argument.
*
* @warning filefrag should be a simple path fragment. It shouldn't
* contain xml:// or http:// or <whatever>:// other protocol specifiers.
*
* If passed a string which g_path_is_absolute declares an absolute
* path, return the argument.
*
* Otherwise, assume that filefrag is a well-formed relative path and
* try to find a file with its path relative to
* \li the current working directory,
* \li the installed system-wide data directory (e.g., /usr/local/share/gnucash),
* \li the installed system configuration directory (e.g., /usr/local/etc/gnucash),
* \li or in the user's configuration directory (e.g., $HOME/.gnucash/data)
*
* The paths are searched for in that order. If a matching file is
* found, return the absolute path to it.
* If one isn't found, return a absolute path relative to the
* user's configuration directory and note in the trace file that it
* needs to be created.
*
* @param filefrag The file path to resolve
*
* @return An absolute file path.
*/
gchar *
gnc_resolve_file_path (const gchar * filefrag)
{
gchar *fullpath = NULL, *tmp_path = NULL;
/* seriously invalid */
if (!filefrag)
{
g_critical("filefrag is NULL");
return NULL;
}
/* ---------------------------------------------------- */
/* OK, now we try to find or build an absolute file path */
/* check for an absolute file path */
if (g_path_is_absolute(filefrag))
return g_strdup (filefrag);
/* Look in the current working directory */
tmp_path = g_get_current_dir();
fullpath = g_build_filename(tmp_path, filefrag, (gchar *)NULL);
g_free(tmp_path);
fullpath = check_path_return_if_valid(fullpath);
if (fullpath != NULL)
return fullpath;
/* Look in the data dir (e.g. $PREFIX/share/gnucash) */
tmp_path = gnc_path_get_pkgdatadir();
fullpath = g_build_filename(tmp_path, filefrag, (gchar *)NULL);
g_free(tmp_path);
fullpath = check_path_return_if_valid(fullpath);
if (fullpath != NULL)
return fullpath;
/* Look in the config dir (e.g. $PREFIX/share/gnucash/accounts) */
tmp_path = gnc_path_get_accountsdir();
fullpath = g_build_filename(tmp_path, filefrag, (gchar *)NULL);
g_free(tmp_path);
fullpath = check_path_return_if_valid(fullpath);
if (fullpath != NULL)
return fullpath;
/* Look in the users config dir (e.g. $HOME/.gnucash/data) */
fullpath = g_strdup(gnc_build_data_path(filefrag));
if (g_file_test(fullpath, G_FILE_TEST_IS_REGULAR))
return fullpath;
/* OK, it's not there. Note that it needs to be created and pass it
* back anyway */
g_warning("create new file %s", fullpath);
return fullpath;
}
/* Searches for a file fragment paths set via GNC_DOC_PATH environment
* variable. If this variable is not set, fall back to search in
* - a html directory in the local user's gnucash settings directory
* (typically $HOME/.gnucash/html)
* - the gnucash documentation directory
* (typically /usr/share/doc/gnucash)
* - the gnucash data directory
* (typically /usr/share/gnucash)
* It searches in this order.
*
* This is used by gnc_path_find_localized_file to search for
* localized versions of files if they exist.
*/
static gchar *
gnc_path_find_localized_html_file_internal (const gchar * file_name)
{
gchar *full_path = NULL;
int i;
const gchar *env_doc_path = g_getenv("GNC_DOC_PATH");
const gchar *default_dirs[] =
{
gnc_build_userdata_path ("html"),
gnc_path_get_pkgdocdir (),
gnc_path_get_pkgdatadir (),
NULL
};
gchar **dirs;
if (!file_name || *file_name == '\0')
return NULL;
/* Allow search path override via GNC_DOC_PATH environment variable */
if (env_doc_path)
dirs = g_strsplit (env_doc_path, G_SEARCHPATH_SEPARATOR_S, -1);
else
dirs = (gchar **)default_dirs;
for (i = 0; dirs[i]; i++)
{
full_path = g_build_filename (dirs[i], file_name, (gchar *)NULL);
g_debug ("Checking for existence of %s", full_path);
full_path = check_path_return_if_valid (full_path);
if (full_path != NULL)
return full_path;
}
return NULL;
}
/** @brief Find an absolute path to a localized version of a given
* relative path to a html or html related file.
* If no localized version exists, an absolute path to the file
* is searched for. If that file doesn't exist either, returns NULL.
*
* @warning file_name should be a simple path fragment. It shouldn't
* contain xml:// or http:// or <whatever>:// other protocol specifiers.
*
* If passed a string which g_path_is_absolute declares an absolute
* path, return the argument.
*
* Otherwise, assume that file_name is a well-formed relative path and
* try to find a file with its path relative to
* \li a localized subdirectory in the html directory
* of the user's configuration directory
* (e.g. $HOME/.gnucash/html/de_DE, $HOME/.gnucash/html/en,...)
* \li a localized subdirectory in the gnucash documentation directory
* (e.g. /usr/share/doc/gnucash/C,...)
* \li the html directory of the user's configuration directory
* (e.g. $HOME/.gnucash/html)
* \li the gnucash documentation directory
* (e.g. /usr/share/doc/gnucash/)
* \li last resort option: the gnucash data directory
* (e.g. /usr/share/gnucash/)
*
* The paths are searched for in that order. If a matching file is
* found, return the absolute path to it.
* If one isn't found, return NULL.
*
* @param file_name The file path to resolve
*
* @return An absolute file path or NULL if no file is found.
*/
gchar *
gnc_path_find_localized_html_file (const gchar *file_name)
{
gchar *loc_file_name = NULL;
gchar *full_path = NULL;
const gchar * const *lang;
if (!file_name || *file_name == '\0')
return NULL;
/* An absolute path is returned unmodified. */
if (g_path_is_absolute (file_name))
return g_strdup (file_name);
/* First try to find the file in any of the localized directories
* the user has set up on his system
*/
for (lang = g_get_language_names (); *lang; lang++)
{
loc_file_name = g_build_filename (*lang, file_name, (gchar *)NULL);
full_path = gnc_path_find_localized_html_file_internal (loc_file_name);
g_free (loc_file_name);
if (full_path != NULL)
return full_path;
}
/* If not found in a localized directory, try to find the file
* in any of the base directories
*/
return gnc_path_find_localized_html_file_internal (file_name);
}
/* ====================================================================== */
/** @brief Check that the supplied directory path exists, is a directory, and
* that the user has adequate permissions to use it.
*
* @param dirname The path to check
*/
static bool
gnc_validate_directory (const bfs::path &dirname, bool create)
{
if (dirname.empty())
return false;
if (!bfs::exists(dirname) && (!create))
throw (bfs::filesystem_error("", dirname,
bst::error_code(bst::errc::no_such_file_or_directory,
bst::generic_category())));
/* Optionally create directories if they don't exist yet
* Note this will do nothing if the directory and its
* parents already exist, but will fail if the path
* points to a file or a softlink. So it serves as a test
* for that as well.
*/
bfs::create_directories(dirname);
auto d = bfs::directory_entry (dirname);
auto perms = d.status().permissions();
/* On Windows only write permission will be checked.
* So strictly speaking we'd need two error messages here depending
* on the platform. For simplicity this detail is glossed over though. */
#if PLATFORM(WINDOWS)
auto check_perms = bfs::owner_read | bfs::owner_write;
#else
auto check_perms = bfs::owner_all;
#endif
if ((perms & check_perms) != check_perms)
throw (bfs::filesystem_error(
std::string(_("Insufficient permissions, at least write and access permissions required: "))
+ dirname.string(), dirname,
bst::error_code(bst::errc::permission_denied, bst::generic_category())));
return true;
}
static bool userdata_is_home = false;
static bool userdata_is_tmp = false;
static auto userdata_home = bfs::path();
static auto gnc_userdata_home = bfs::path();
/* Will attempt to copy all files and directories from src to dest
* Returns true if successful or false if not */
static bool
copy_recursive(const bfs::path& src, const bfs::path& dest)
{
if (!bfs::exists(src))
return false;
auto old_str = src.string();
auto old_len = old_str.size();
try
{
/* Note: the for(auto elem : iterator) construct fails
* on travis (g++ 4.8.x, boost 1.54) so I'm using
* a traditional for loop here */
for(auto direntry = bfs::recursive_directory_iterator(src);
direntry != bfs::recursive_directory_iterator(); ++direntry)
{
auto cur_str = direntry->path().string();
auto cur_len = cur_str.size();
auto rel_str = std::string(cur_str, old_len, cur_len - old_len);
auto relpath = bfs::path(rel_str).relative_path();
auto newpath = bfs::absolute (relpath, dest);
bfs::copy(direntry->path(), newpath);
}
}
catch(const bfs::filesystem_error& ex)
{
g_warning("An error occured while trying to migrate the user configation from\n%s to\n%s"
"(Error: %s)",
src.string().c_str(), gnc_userdata_home.string().c_str(),
ex.what());
return false;
}
return true;
}
#ifdef G_OS_WIN32
/* g_get_user_data_dir defaults to CSIDL_LOCAL_APPDATA, but
* the roaming profile makes more sense.
* So this function is a copy of glib's internal get_special_folder
* and minimally adjusted to fetch CSIDL_APPDATA
*/
static char *
win32_get_userdata_home (void)
{
wchar_t path[MAX_PATH+1];
HRESULT hr;
LPITEMIDLIST pidl = NULL;
BOOL b;
char *retval = NULL;
hr = SHGetSpecialFolderLocation (NULL, CSIDL_APPDATA, &pidl);
if (hr == S_OK)
{
b = SHGetPathFromIDListW (pidl, path);
if (b)
{
std::wstring_convert<std::codecvt_utf8<wchar_t>> utf8_conv;
retval = g_strdup(utf8_conv.to_bytes(path).c_str());
}
CoTaskMemFree (pidl);
}
return retval;
}
#elif defined MAC_INTEGRATION
static char*
quarz_get_userdata_home(void)
{
char *retval = NULL;
NSFileManager*fm = [NSFileManager defaultManager];
NSArray* appSupportDir = [fm URLsForDirectory:NSApplicationSupportDirectory
inDomains:NSUserDomainMask];
if ([appSupportDir count] > 0)
{
NSURL* dirUrl = [appSupportDir objectAtIndex:0];
NSString* dirPath = [dirUrl path];
retval = g_strdup([dirPath UTF8String]);
}
return retval;
}
#endif
static bfs::path
get_userdata_home(bool create)
{
auto try_home_dir = true;
gchar *data_dir = NULL;
#ifdef G_OS_WIN32
data_dir = win32_get_userdata_home ();
#elif defined MAC_INTEGRATION
data_dir = quarz_get_userdata_home ();
#endif
if (data_dir)
{
userdata_home = data_dir;
g_free(data_dir);
}
else
userdata_home = g_get_user_data_dir();
if (!userdata_home.empty())
{
try
{
gnc_validate_directory(userdata_home, create); // May throw
try_home_dir = false;
}
catch (const bfs::filesystem_error& ex)
{
auto path_string = userdata_home.string();
g_warning("%s is not a suitable base directory for the user data. "
"Trying home directory instead.\n(Error: %s)",
path_string.c_str(), ex.what());
}
}
if (try_home_dir)
{
userdata_home = g_get_home_dir();
try
{
/* Never attempt to create a home directory, hence the false below */
gnc_validate_directory(userdata_home, false); // May throw
userdata_is_home = true;
}
catch (const bfs::filesystem_error& ex)
{
g_warning("Cannot find suitable home directory. Using tmp directory instead.\n"
"(Error: %s)", ex.what());
userdata_home = g_get_tmp_dir();
userdata_is_tmp = true;
}
}
g_assert(!userdata_home.empty());
return userdata_home;
}
gboolean
gnc_filepath_init(gboolean create)
{
userdata_is_home = userdata_is_tmp = false;
auto gnc_userdata_home_exists = false;
auto gnc_userdata_home_from_env = false;
auto gnc_userdata_home_env = g_getenv("GNC_DATA_HOME");
if (gnc_userdata_home_env)
{
gnc_userdata_home = bfs::path(gnc_userdata_home_env);
try
{
gnc_userdata_home_exists = bfs::exists(gnc_userdata_home);
gnc_validate_directory(gnc_userdata_home, create); // May throw
gnc_userdata_home_from_env = true;
}
catch (const bfs::filesystem_error& ex)
{
auto path_string = userdata_home.string();
g_warning("%s (from environment variable 'GNC_DATA_HOME') is not a suitable directory for the user data. "
"Trying the default instead.\n(Error: %s)",
path_string.c_str(), ex.what());
}
}
if (!gnc_userdata_home_from_env)
{
auto userdata_home = get_userdata_home(create);
if (userdata_is_home)
{
/* If we get here that means the platform
* dependent gnc_userdata_home is not available for
* some reason. If legacy .gnucash directory still exists,
* use it as first fallback, but never create it (we want
* it to go away eventually).
* If missing, fall back to tmp_dir instead */
gnc_userdata_home = userdata_home / ".gnucash";
if (!bfs::exists(gnc_userdata_home))
{
userdata_home = g_get_tmp_dir();
userdata_is_home = false;
userdata_is_tmp = true;
}
}
/* The fall back to the tmp dir is to accomodate for very restricted
* distribution build environments. In some such cases
* there is no home directory available, which would cause the build
* to fail (as this code is actually run while compiling guile scripts).
* This is worked around by continuing with a userdata directory
* in the temporary directory which always exists. */
if (!userdata_is_home)
gnc_userdata_home = userdata_home / PACKAGE_NAME;
gnc_userdata_home_exists = bfs::exists(gnc_userdata_home);
/* This may throw and end the program!
* Note we always allow to create in the tmp_dir. This will
* skip migrating to that location in the next step but that's
* a good thing.
*/
try
{
gnc_validate_directory(gnc_userdata_home, (create || userdata_is_tmp));
}
catch (const bfs::filesystem_error& ex)
{
g_warning("User data directory doesn't exist, yet the calling code requested not to create it. Proceed with caution.\n"
"(Error: %s)", ex.what());
}
}
auto migrated = FALSE;
if (!userdata_is_home && !gnc_userdata_home_exists && create)
migrated = copy_recursive(bfs::path (g_get_home_dir()) / ".gnucash",
gnc_userdata_home);
/* Try to create the standard subdirectories for gnucash' user data */
try
{
gnc_validate_directory(gnc_userdata_home / "books", (create || userdata_is_tmp));
gnc_validate_directory(gnc_userdata_home / "checks", (create || userdata_is_tmp));
gnc_validate_directory(gnc_userdata_home / "translog", (create || userdata_is_tmp));
}
catch (const bfs::filesystem_error& ex)
{
g_warning("Default user data subdirectories don't exist, yet the calling code requested not to create them. Proceed with caution.\n"
"(Error: %s)", ex.what());
}
return migrated;
}
/** @fn const gchar * gnc_userdata_dir ()
* @brief Ensure that the user's configuration directory exists and is minimally populated.
*
* Note that the default path depends on the platform.
* - Windows: CSIDL_APPDATA/Gnucash
* - OS X: $HOME/Application Support/Gnucash
* - Linux: $XDG_DATA_HOME/Gnucash (or the default $HOME/.local/share/Gnucash)
*
* If any of these paths doesn't exist and can't be created the function will
* fall back to
* - $HOME/.gnucash
* - <TMP_DIR>/Gnucash
* (in that order)
*
* @return An absolute path to the configuration directory. This string is
* owned by the gnc_filepath_utils code and should not be freed by the user.
*/
/* Note Don't create missing directories automatically
* here and in the next function except if the
* target directory is the temporary directory. This
* should be done properly at a higher level (in the gui
* code most likely) very early in application startup.
* This call is just a fallback to prevent the code from
* crashing because no directories were configured. This
* weird construct is set up because compiling our guile
* scripts also triggers this code and that's not the
* right moment to start creating the necessary directories.
* FIXME A better approach would be to have the gnc_userdata_home
* verification/creation be part of the application code instead
* of libgnucash. If libgnucash needs access to this directory
* libgnucash will need some kind of initialization routine
* that the application can call to set (among others) the proper
* gnc_uderdata_home for libgnucash. The only other aspect to
* consider here is how to handle this in the bindings (if they
* need it).
*/
const gchar *
gnc_userdata_dir (void)
{
if (gnc_userdata_home.empty())
gnc_filepath_init(false);
return gnc_userdata_home.string().c_str();
}
static const bfs::path&
gnc_userdata_dir_as_path (void)
{
if (gnc_userdata_home.empty())
/* Don't create missing directories automatically except
* if the target directory is the temporary directory. This
* should be done properly at a higher level (in the gui
* code most likely) very early in application startup.
* This call is just a fallback to prevent the code from
* crashing because no directories were configured. */
gnc_filepath_init(false);
return gnc_userdata_home;
}
/** @fn gchar * gnc_build_userdata_path (const gchar *filename)
* @brief Make a path to filename in the user's configuration directory.
*
* @param filename The name of the file
*
* @return An absolute path. The returned string should be freed by the user
* using g_free().
*/
gchar *
gnc_build_userdata_path (const gchar *filename)
{
return g_strdup((gnc_userdata_dir_as_path() / filename).string().c_str());
}
static bfs::path
gnc_build_userdata_subdir_path (const gchar *subdir, const gchar *filename)
{
gchar* filename_dup = g_strdup(filename);
scrub_filename(filename_dup);
auto result = (gnc_userdata_dir_as_path() / subdir) / filename_dup;
g_free(filename_dup);
return result;
}
/** @fn gchar * gnc_build_book_path (const gchar *filename)
* @brief Make a path to filename in the book subdirectory of the user's configuration directory.
*
* @param filename The name of the file
*
* @return An absolute path. The returned string should be freed by the user
* using g_free().
*/
gchar *
gnc_build_book_path (const gchar *filename)
{
auto path = gnc_build_userdata_subdir_path("books", filename).string();
return g_strdup(path.c_str());
}
/** @fn gchar * gnc_build_translog_path (const gchar *filename)
* @brief Make a path to filename in the translog subdirectory of the user's configuration directory.
*
* @param filename The name of the file
*
* @return An absolute path. The returned string should be freed by the user
* using g_free().
*/
gchar *
gnc_build_translog_path (const gchar *filename)
{
auto path = gnc_build_userdata_subdir_path("translog", filename).string();
return g_strdup(path.c_str());
}
/** @fn gchar * gnc_build_data_path (const gchar *filename)
* @brief Make a path to filename in the data subdirectory of the user's configuration directory.
*
* @param filename The name of the file
*
* @return An absolute path. The returned string should be freed by the user
* using g_free().
*/
gchar *
gnc_build_data_path (const gchar *filename)
{
auto path = gnc_build_userdata_subdir_path("data", filename).string();
return g_strdup(path.c_str());
}
/** @fn gchar * gnc_build_report_path (const gchar *filename)
* @brief Make a path to filename in the report directory.
*
* @param filename The name of the file
*
* @return An absolute path. The returned string should be freed by the user
* using g_free().
*/
gchar *
gnc_build_report_path (const gchar *filename)
{
gchar *result = g_build_filename(gnc_path_get_reportdir(), filename, (gchar *)NULL);
return result;
}
/** @fn gchar * gnc_build_stdreports_path (const gchar *filename)
* @brief Make a path to filename in the standard reports directory.
*
* @param filename The name of the file
*
* @return An absolute path. The returned string should be freed by the user
* using g_free().
*/
gchar *
gnc_build_stdreports_path (const gchar *filename)
{
gchar *result = g_build_filename(gnc_path_get_stdreportsdir(), filename, (gchar *)NULL);
return result;
}
static gchar *
gnc_filepath_locate_file (const gchar *default_path, const gchar *name)
{
gchar *fullname;
g_return_val_if_fail (name != NULL, NULL);
if (g_path_is_absolute (name))
fullname = g_strdup (name);
else if (default_path)
fullname = g_build_filename (default_path, name, NULL);
else
fullname = gnc_resolve_file_path (name);
if (!g_file_test (fullname, G_FILE_TEST_IS_REGULAR))
{
g_warning ("Could not locate file %s", name);
g_free (fullname);
return NULL;
}
return fullname;
}
gchar *
gnc_filepath_locate_data_file (const gchar *name)
{
return gnc_filepath_locate_file (gnc_path_get_pkgdatadir(), name);
}
gchar *
gnc_filepath_locate_pixmap (const gchar *name)
{
gchar *default_path;
gchar *fullname;
gchar* pkgdatadir = gnc_path_get_pkgdatadir ();
default_path = g_build_filename (pkgdatadir, "pixmaps", NULL);
g_free(pkgdatadir);
fullname = gnc_filepath_locate_file (default_path, name);
g_free(default_path);
return fullname;
}
gchar *
gnc_filepath_locate_ui_file (const gchar *name)
{
gchar *default_path;
gchar *fullname;
gchar* pkgdatadir = gnc_path_get_pkgdatadir ();
default_path = g_build_filename (pkgdatadir, "ui", NULL);
g_free(pkgdatadir);
fullname = gnc_filepath_locate_file (default_path, name);
g_free(default_path);
return fullname;
}
gchar *
gnc_filepath_locate_doc_file (const gchar *name)
{
return gnc_filepath_locate_file (gnc_path_get_pkgdocdir(), name);
}
/* =============================== END OF FILE ========================== */