Added PHYSFS_getPrefDir().
authorRyan C. Gordon <icculus@icculus.org>
Wed, 21 Mar 2012 23:30:50 -0400
changeset 1242 1e6db80d2393
parent 1241 86dd9db48c76
child 1243 6bc95a0e7d9b
Added PHYSFS_getPrefDir().
docs/TODO.txt
extras/physfs-swig.i
src/physfs.c
src/physfs.h
src/physfs_internal.h
src/platform_beos.cpp
src/platform_macosx.c
src/platform_unix.c
src/platform_windows.c
test/test_physfs.c
test/test_physfs.pl
test/test_physfs.rb
--- a/docs/TODO.txt	Tue Mar 20 15:44:10 2012 -0400
+++ b/docs/TODO.txt	Wed Mar 21 23:30:50 2012 -0400
@@ -6,11 +6,6 @@
 
 From http://icculus.org/pipermail/physfs/2009-March/000698.html ...
 
-- Add an API to find the "pref path" ... this is the directory where an
-  app's configuration data is supposed to go...which is usually somewhere
-  under the user directory, but not always. As it is platform-dependent,
-  platform-version dependent and sometimes even user-dependent, this should be
-  handled by the library and not the app.
 - Archives formats provided by the implementation.
 - Write support for various archives. I haven't decided how to do this yet,
   but I'd like to.
@@ -50,7 +45,6 @@
 - Use __cdecl in physfs.h?
 - Look for FIXMEs (many marked with "!!!" in comments).
 - Find some way to relax or remove the security model for external tools.
-- OSX shouldn't use ~/.app for userdir.
 - fscanf and fprintf support in extras dir.
 - Why do we call it openArchive and dirClose?
 - Sanity check byte order at runtime.
--- a/extras/physfs-swig.i	Tue Mar 20 15:44:10 2012 -0400
+++ b/extras/physfs-swig.i	Wed Mar 21 23:30:50 2012 -0400
@@ -91,6 +91,7 @@
 %rename(unmount) PHYSFS_unmount;
 %rename(mountMemory) PHYSFS_mountMemory;
 %rename(mountHandle) PHYSFS_mountHandle;
+%rename(getPrefDir) PHYSFS_getPrefDir;
 #endif
 
 %include "../src/physfs.h"
--- a/src/physfs.c	Tue Mar 20 15:44:10 2012 -0400
+++ b/src/physfs.c	Wed Mar 21 23:30:50 2012 -0400
@@ -135,6 +135,7 @@
 static FileHandle *openReadList = NULL;
 static char *baseDir = NULL;
 static char *userDir = NULL;
+static char *prefDir = NULL;
 static int allowSymLinks = 0;
 
 /* mutexes ... */
@@ -1312,6 +1313,12 @@
         userDir = NULL;
     } /* if */
 
+    if (prefDir != NULL)
+    {
+        allocator.Free(prefDir);
+        prefDir = NULL;
+    } /* if */
+
     allowSymLinks = 0;
     initialized = 0;
 
@@ -1370,15 +1377,60 @@
 } /* PHYSFS_getCdRomDirsCallback */
 
 
+const char *PHYSFS_getPrefDir(const char *org, const char *app)
+{
+    const char dirsep = __PHYSFS_platformDirSeparator;
+    char *ptr = NULL;
+    PHYSFS_Stat statbuf;
+    int exists = 0;
+
+    BAIL_IF_MACRO(!initialized, PHYSFS_ERR_NOT_INITIALIZED, 0);
+    BAIL_IF_MACRO(!org, PHYSFS_ERR_INVALID_ARGUMENT, NULL);
+    BAIL_IF_MACRO(*org == '\0', PHYSFS_ERR_INVALID_ARGUMENT, NULL);
+    BAIL_IF_MACRO(!app, PHYSFS_ERR_INVALID_ARGUMENT, NULL);
+    BAIL_IF_MACRO(*app == '\0', PHYSFS_ERR_INVALID_ARGUMENT, NULL);
+
+    allocator.Free(prefDir);
+    prefDir = __PHYSFS_platformCalcPrefDir(org, app);
+    BAIL_IF_MACRO(!prefDir, ERRPASS, NULL);
+
+    #if !PHYSFS_PLATFORM_WINDOWS  /* Windows guarantees the dir exists here. */
+    if (__PHYSFS_platformStat(prefDir, &exists, &statbuf))
+        return prefDir;
+
+    for (ptr = strchr(prefDir, dirsep); ptr; ptr = strchr(ptr+1, dirsep))
+    {
+        *ptr = '\0';
+        __PHYSFS_platformMkDir(prefDir);
+        *ptr = dirsep;
+    } /* for */
+
+    if (!__PHYSFS_platformMkDir(prefDir))
+    {
+        allocator.Free(prefDir);
+        prefDir = NULL;
+    } /* if */
+    #endif
+
+    return prefDir;
+} /* PHYSFS_getPrefDir */
+
+
 const char *PHYSFS_getBaseDir(void)
 {
     return baseDir;   /* this is calculated in PHYSFS_init()... */
 } /* PHYSFS_getBaseDir */
 
 
+const char *__PHYSFS_getUserDir(void)  /* not deprecated internal version. */
+{
+    return userDir;   /* this is calculated in PHYSFS_init()... */
+} /* __PHYSFS_getUserDir */
+
+
 const char *PHYSFS_getUserDir(void)
 {
-    return userDir;   /* this is calculated in PHYSFS_init()... */
+    return __PHYSFS_getUserDir();
 } /* PHYSFS_getUserDir */
 
 
--- a/src/physfs.h	Tue Mar 20 15:44:10 2012 -0400
+++ b/src/physfs.h	Wed Mar 21 23:30:50 2012 -0400
@@ -111,7 +111,7 @@
  *  use the base dir for both searching and writing. There is a helper
  *  function (PHYSFS_setSaneConfig()) that puts together a basic configuration
  *  for you, based on a few parameters. Also see the comments on
- *  PHYSFS_getBaseDir(), and PHYSFS_getUserDir() for info on what those
+ *  PHYSFS_getBaseDir(), and PHYSFS_getPrefDir() for info on what those
  *  are and how they can help you determine an optimal search path.
  *
  * PhysicsFS 2.0 adds the concept of "mounting" archives to arbitrary points
@@ -765,7 +765,7 @@
  *
  *  \return READ ONLY string of base dir in platform-dependent notation.
  *
- * \sa PHYSFS_getUserDir
+ * \sa PHYSFS_getPrefDir
  */
 PHYSFS_DECL const char *PHYSFS_getBaseDir(void);
 
@@ -774,6 +774,8 @@
  * \fn const char *PHYSFS_getUserDir(void)
  * \brief Get the path where user's home directory resides.
  *
+ * \deprecated As of PhysicsFS 2.1, you probably want PHYSFS_getPrefDir().
+ *
  * Helper function.
  *
  * Get the "user dir". This is meant to be a suggestion of where a specific
@@ -783,14 +785,12 @@
  *  where "username" will either be the login name, or "default" if the
  *  platform doesn't support multiple users, either.
  *
- * You should probably use the user dir as the basis for your write dir, and
- *  also put it near the beginning of your search path.
- *
  *  \return READ ONLY string of user dir in platform-dependent notation.
  *
  * \sa PHYSFS_getBaseDir
+ * \sa PHYSFS_getPrefDir
  */
-PHYSFS_DECL const char *PHYSFS_getUserDir(void);
+PHYSFS_DECL const char *PHYSFS_getUserDir(void) PHYSFS_DEPRECATED;
 
 
 /**
@@ -3230,6 +3230,75 @@
  */
 PHYSFS_DECL void PHYSFS_setErrorCode(PHYSFS_ErrorCode code);
 
+
+/**
+ * \fn const char *PHYSFS_getPrefDir(const char *org, const char *app)
+ * \brief Get the user-and-app-specific path where files can be written.
+ *
+ * Helper function.
+ *
+ * Get the "pref dir". This is meant to be where users can write personal
+ *  files (preferences and save games, etc) that are specific to your
+ *  application. This directory is unique per user, per application.
+ *
+ * This function will decide the appropriate location in the native filesystem,
+ *  create the directory if necessary, and return a string in
+ *  platform-dependent notation, suitable for passing to PHYSFS_setWriteDir().
+ *
+ * On Windows, this might look like:
+ *  "C:\\Users\\bob\\AppData\\Roaming\\My Company\\My Program Name"
+ *
+ * On Linux, this might look like:
+ *  "/home/bob/.local/share/My Program Name"
+ *
+ * On Mac OS X, this might look like:
+ *  "/Users/bob/Library/Application Support/My Program Name"
+ *
+ * (etc.)
+ *
+ * You should probably use the pref dir for your write dir, and also put it
+ *  near the beginning of your search path. Older versions of PhysicsFS
+ *  offered only PHYSFS_getUserDir() and left you to figure out where the
+ *  files should go under that tree. This finds the correct location
+ *  for whatever platform, which not only changes between operating systems,
+ *  but also versions of the same operating system.
+ *
+ * You specify the name of your organization (if it's not a real organization,
+ *  your name or an Internet domain you own might do) and the name of your
+ *  application. These should be proper names.
+ *
+ * Both the (org) and (app) strings may become part of a directory name, so
+ *  please follow these rules:
+ *
+ *    - Try to use the same org string (including case-sensitivity) for
+ *      all your applications that use this function.
+ *    - Always use a unique app string for each one, and make sure it never
+ *      changes for an app once you've decided on it.
+ *    - Unicode characters are legal, as long as it's UTF-8 encoded, but...
+ *    - ...only use letters, numbers, and spaces. Avoid punctuation like
+ *      "Game Name 2: Bad Guy's Revenge!" ... "Game Name 2" is sufficient.
+ *
+ * The pointer returned by this function remains valid until you call this
+ *  function again, or call PHYSFS_deinit(). This is not necessarily a fast
+ *  call, though, so you should call this once at startup and copy the string
+ *  if you need it.
+ *
+ * You should assume the path returned by this function is the only safe
+ *  place to write files (and that PHYSFS_getUserDir() and PHYSFS_getBaseDir(),
+ *  while they might be writable, or even parents of the returned path, aren't
+ *  where you should be writing things).
+ *
+ *   \param org The name of your organization.
+ *   \param app The name of your application.
+ *  \return READ ONLY string of user dir in platform-dependent notation. NULL
+ *          if there's a problem (creating directory failed, etc).
+ *
+ * \sa PHYSFS_getBaseDir
+ * \sa PHYSFS_getUserDir
+ */
+PHYSFS_DECL const char *PHYSFS_getPrefDir(const char *org, const char *app);
+
+
 /* Everything above this line is part of the PhysicsFS 2.1 API. */
 
 
--- a/src/physfs_internal.h	Tue Mar 20 15:44:10 2012 -0400
+++ b/src/physfs_internal.h	Wed Mar 21 23:30:50 2012 -0400
@@ -633,13 +633,26 @@
  */
 char *__PHYSFS_platformGetUserName(void);
 
-/*
+/* !!! FIXME: should this be CalcUserDir, to match CalcBaseDir?
  * Get the platform-specific user dir.
  *  Caller will allocator.Free() the retval if it's not NULL. If it's NULL,
  *  the userdir will default to basedir/username.
  */
 char *__PHYSFS_platformGetUserDir(void);
 
+
+/* This is the cached version from PHYSFS_init(). This is a fast call. */
+const char *__PHYSFS_getUserDir(void);  /* not deprecated internal version. */
+
+/*
+ * Get the platform-specific pref dir.
+ *  Caller will allocator.Free() the retval if it's not NULL. If it's NULL,
+ *  it's a total failure. Caller will make missing directories if necessary;
+ *  this just reports the final path.
+ */
+char *__PHYSFS_platformCalcPrefDir(const char *org, const char *app);
+
+
 /*
  * Return a pointer that uniquely identifies the current thread.
  *  On a platform without threading, (0x1) will suffice. These numbers are
--- a/src/platform_beos.cpp	Tue Mar 20 15:44:10 2012 -0400
+++ b/src/platform_beos.cpp	Wed Mar 21 23:30:50 2012 -0400
@@ -183,6 +183,19 @@
 } /* __PHYSFS_platformCalcBaseDir */
 
 
+char *__PHYSFS_platformCalcPrefDir(const char *org, const char *app)
+{
+    /* !!! FIXME: there's a real API to determine this */
+    const char *userdir = __PHYSFS_getUserDir();
+    const char *append = "config/settings/";
+    const size_t len = strlen(userdir) + strlen(append) + strlen(app) + 1;
+    char *retval = allocator.Malloc(len);
+    BAIL_IF_MACRO(!retval, PHYSFS_ERR_OUT_OF_MEMORY, NULL);
+    snprintf(retval, len, "%s%s%s", userdir, append, app);
+    return retval;
+} /* __PHYSFS_platformCalcPrefDir */
+
+
 void *__PHYSFS_platformGetThreadID(void)
 {
     return (void *) find_thread(NULL);
--- a/src/platform_macosx.c	Tue Mar 20 15:44:10 2012 -0400
+++ b/src/platform_macosx.c	Wed Mar 21 23:30:50 2012 -0400
@@ -289,6 +289,19 @@
 } /* __PHYSFS_platformCalcBaseDir */
 
 
+char *__PHYSFS_platformCalcPrefDir(const char *org, const char *app)
+{
+    /* !!! FIXME: there's a real API to determine this */
+    const char *userdir = __PHYSFS_getUserDir();
+    const char *append = "Library/Application Support/";
+    const size_t len = strlen(userdir) + strlen(append) + strlen(app) + 1;
+    char *retval = allocator.Malloc(len);
+    BAIL_IF_MACRO(!retval, PHYSFS_ERR_OUT_OF_MEMORY, NULL);
+    snprintf(retval, len, "%s%s%s", userdir, append, app);
+    return retval;
+} /* __PHYSFS_platformCalcPrefDir */
+
+
 /* Platform allocator uses default CFAllocator at PHYSFS_init() time. */
 
 static CFAllocatorRef cfallocdef = NULL;
--- a/src/platform_unix.c	Tue Mar 20 15:44:10 2012 -0400
+++ b/src/platform_unix.c	Wed Mar 21 23:30:50 2012 -0400
@@ -293,6 +293,36 @@
 } /* __PHYSFS_platformCalcBaseDir */
 
 
+char *__PHYSFS_platformCalcPrefDir(const char *org, const char *app)
+{
+    /*
+     * We use XDG's base directory spec, even if you're not on Linux.
+     *  This isn't strictly correct, but the results are relatively sane
+     *  in any case.
+     *
+     * http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
+     */
+    const char *envr = getenv("XDG_DATA_HOME");
+    const char *append = "/";
+    char *retval = NULL;
+    size_t len = 0;
+
+    if (!envr)
+    {
+        /* You end up with "$HOME/.local/share/Game Name 2" */
+        envr = __PHYSFS_getUserDir();
+        BAIL_IF_MACRO(!envr, ERRPASS, NULL);  /* oh well. */
+        append = ".local/share/";
+    } /* if */
+
+    len = strlen(envr) + strlen(append) + strlen(app) + 1;
+    retval = (char *) allocator.Malloc(len);
+    BAIL_IF_MACRO(!retval, PHYSFS_ERR_OUT_OF_MEMORY, NULL);
+    snprintf(retval, len, "%s%s%s", envr, append, app);
+    return retval;
+} /* __PHYSFS_platformCalcPrefDir */
+
+
 int __PHYSFS_platformSetDefaultAllocator(PHYSFS_Allocator *a)
 {
     return 0;  /* just use malloc() and friends. */
--- a/src/platform_windows.c	Tue Mar 20 15:44:10 2012 -0400
+++ b/src/platform_windows.c	Wed Mar 21 23:30:50 2012 -0400
@@ -88,6 +88,7 @@
 } WinApiFile;
 
 
+/* !!! FIXME: we cache userDir in physfs.c during PHYSFS_init(), too. */
 static char *userDir = NULL;
 static HANDLE libUserEnv = NULL;
 static HANDLE detectCDThreadHandle = NULL;
@@ -441,6 +442,22 @@
 } /* __PHYSFS_platformCalcBaseDir */
 
 
+char *__PHYSFS_platformCalcPrefDir(const char *org, const char *app)
+{
+    // Vista and later has a new API for this, but SHGetFolderPath works there,
+    //  and apparently just wraps the new API. This is the new way to do it:
+    // SHGetKnownFolderPath(FOLDERID_RoamingAppData, KF_FLAG_CREATE,
+    //                      NULL, &wszPath);
+
+    WCHAR path[MAX_PATH];
+    if (!SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_APPDATA | CSIDL_FLAG_CREATE,
+                                   NULL, 0, path)))
+        BAIL_MACRO(PHYSFS_ERR_OS_ERROR, NULL);
+
+    return unicodeToUtf8Heap(path);
+} /* __PHYSFS_platformCalcPrefDir */
+
+
 char *__PHYSFS_platformGetUserName(void)
 {
     DWORD bufsize = 0;
--- a/test/test_physfs.c	Tue Mar 20 15:44:10 2012 -0400
+++ b/test/test_physfs.c	Wed Mar 21 23:30:50 2012 -0400
@@ -23,6 +23,9 @@
 
 #include <time.h>
 
+/* Define this, so the compiler doesn't complain about using old APIs. */
+#define PHYSFS_DEPRECATED
+
 #include "physfs.h"
 
 #define TEST_VERSION_MAJOR  2
@@ -386,6 +389,19 @@
 } /* cmd_getuserdir */
 
 
+static int cmd_getprefdir(char *args)
+{
+    char *org;
+    char *appName;
+    char *ptr = args;
+
+    org = ptr;
+    ptr = strchr(ptr, ' '); *ptr = '\0'; ptr++; appName = ptr;
+    printf("Pref dir is [%s].\n", PHYSFS_getPrefDir(org, appName));
+    return 1;
+} /* cmd_getprefdir */
+
+
 static int cmd_getwritedir(char *args)
 {
     printf("Write dir is [%s].\n", PHYSFS_getWriteDir());
@@ -1130,6 +1146,7 @@
     { "getsearchpath",  cmd_getsearchpath,  0, NULL                         },
     { "getbasedir",     cmd_getbasedir,     0, NULL                         },
     { "getuserdir",     cmd_getuserdir,     0, NULL                         },
+    { "getprefdir",     cmd_getprefdir,     2, "<org> <app>"                },
     { "getwritedir",    cmd_getwritedir,    0, NULL                         },
     { "setwritedir",    cmd_setwritedir,    1, "<newWriteDir>"              },
     { "permitsymlinks", cmd_permitsyms,     1, "<1or0>"                     },
--- a/test/test_physfs.pl	Tue Mar 20 15:44:10 2012 -0400
+++ b/test/test_physfs.pl	Wed Mar 21 23:30:50 2012 -0400
@@ -11,6 +11,7 @@
 
 print "user dir: ", physfs::getUserDir(), "\n";
 print "base dir: ", physfs::getBaseDir(), "\n";
+print "pref dir: ", physfs::getPrefDir("icculus.org", "test_physfs"), "\n";
 
 print "deinit...\n";
 physfs::deinit();
--- a/test/test_physfs.rb	Tue Mar 20 15:44:10 2012 -0400
+++ b/test/test_physfs.rb	Wed Mar 21 23:30:50 2012 -0400
@@ -9,6 +9,7 @@
 
 puts "user dir: " + Physfs.getUserDir()
 puts "base dir: " + Physfs.getBaseDir()
+puts "pref dir: " + Physfs.getPrefDir("icculus.org", "test_physfs")
 
 puts "deinit..."
 Physfs.deinit()