From 02a4f59cddba7eddfdf7dc3f45785ca98e57be8b Mon Sep 17 00:00:00 2001
From: Pier Angelo Vendrame <pierov@torproject.org>
Date: Fri, 27 Jan 2023 17:33:29 +0100
Subject: [PATCH] Bug 9173: Change the default Firefox profile directory to be
 relative.

This commit makes Firefox look for the default profile directory in a
directory relative to the binary path.
The directory can be specified through the --with-relative-data-dir.
This is relative to the same directory as the firefox main binary for
Linux and Windows.

On macOS, we remove Contents/MacOS from it.
Or, in other words, the directory is relative to the application
bundle.

This behavior can be overriden at runtime, by placing a file called
system-install adjacent to the firefox main binary (also on macOS).
---
 browser/config/mozconfigs/base-browser |   2 +
 moz.configure                          |  19 ++++
 mozconfig-macos                        |   3 +
 toolkit/xre/nsAppRunner.cpp            |  16 ++--
 toolkit/xre/nsXREDirProvider.cpp       | 115 ++++++++++++++++++++++++
 toolkit/xre/nsXREDirProvider.h         |   8 ++
 xpcom/io/nsAppFileLocationProvider.cpp | 120 +++++++++++++++++++++++--
 7 files changed, 271 insertions(+), 12 deletions(-)

diff --git a/browser/config/mozconfigs/base-browser b/browser/config/mozconfigs/base-browser
index 18bf4f60b2658..14eb0dfb2b27a 100644
--- a/browser/config/mozconfigs/base-browser
+++ b/browser/config/mozconfigs/base-browser
@@ -45,3 +45,5 @@ ac_add_options MOZ_TELEMETRY_REPORTING=
 if test -z "$WASI_SYSROOT"; then
     ac_add_options --without-wasm-sandboxed-libraries
 fi
+
+ac_add_options --with-relative-data-dir=BaseBrowser/Data/Browser
diff --git a/moz.configure b/moz.configure
index 6a44e9dcd904d..815cddc2f2441 100755
--- a/moz.configure
+++ b/moz.configure
@@ -1017,6 +1017,25 @@ set_config("ZLIB_IN_MOZGLUE", zlib_in_mozglue)
 set_define("ZLIB_IN_MOZGLUE", zlib_in_mozglue)
 
 
+option(
+    "--with-relative-data-dir",
+    nargs=1,
+    help="Sets the data directories to be relative to the application directory"
+)
+
+
+@depends("--with-relative-data-dir", target)
+@imports("json")
+def relative_data_dir(value, target):
+    if value and target.os == "Android":
+        die("--with-relative-data-dir is not supported on Android")
+    if value:
+        return json.dumps(value[0])
+
+
+set_define("RELATIVE_DATA_DIR", relative_data_dir)
+
+
 option(
     "--with-base-browser-version",
     nargs=1,
diff --git a/mozconfig-macos b/mozconfig-macos
index 64f67c01ea150..074da9a4d7fe6 100644
--- a/mozconfig-macos
+++ b/mozconfig-macos
@@ -4,3 +4,6 @@ ac_add_options --enable-strip
 
 # See bug #41131
 ac_add_options --disable-update-agent
+
+# For base-browser we do not enable portable mode on macOS.
+ac_add_options --without-relative-data-dir
diff --git a/toolkit/xre/nsAppRunner.cpp b/toolkit/xre/nsAppRunner.cpp
index 79baf84160830..44ce6b8db2032 100644
--- a/toolkit/xre/nsAppRunner.cpp
+++ b/toolkit/xre/nsAppRunner.cpp
@@ -2777,6 +2777,8 @@ static nsresult ProfileMissingDialog(nsINativeAppSupport* aNative) {
 #endif    // MOZ_WIDGET_ANDROID
 }
 
+// If aUnlocker is NULL, it is also OK for the following arguments to be NULL:
+//   aProfileDir, aProfileLocalDir, aResult.
 static ReturnAbortOnError ProfileLockedDialog(nsIFile* aProfileDir,
                                               nsIFile* aProfileLocalDir,
                                               nsIProfileUnlocker* aUnlocker,
@@ -2784,17 +2786,21 @@ static ReturnAbortOnError ProfileLockedDialog(nsIFile* aProfileDir,
                                               nsIProfileLock** aResult) {
   nsresult rv;
 
-  bool exists;
-  aProfileDir->Exists(&exists);
-  if (!exists) {
-    return ProfileMissingDialog(aNative);
+  if (aProfileDir) {
+    bool exists;
+    aProfileDir->Exists(&exists);
+    if (!exists) {
+      return ProfileMissingDialog(aNative);
+    }
   }
 
   ScopedXPCOMStartup xpcom;
   rv = xpcom.Initialize();
   NS_ENSURE_SUCCESS(rv, rv);
 
-  mozilla::Telemetry::WriteFailedProfileLock(aProfileDir);
+#if defined(MOZ_TELEMETRY_REPORTING)
+  if (aProfileDir) mozilla::Telemetry::WriteFailedProfileLock(aProfileDir);
+#endif
 
   rv = xpcom.SetWindowCreator(aNative);
   NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE);
diff --git a/toolkit/xre/nsXREDirProvider.cpp b/toolkit/xre/nsXREDirProvider.cpp
index 5536f4423f4af..963e1aa8bdae8 100644
--- a/toolkit/xre/nsXREDirProvider.cpp
+++ b/toolkit/xre/nsXREDirProvider.cpp
@@ -111,6 +111,10 @@ nsIFile* gDataDirHome = nullptr;
 nsCOMPtr<nsIFile> gDataDirProfileLocal = nullptr;
 nsCOMPtr<nsIFile> gDataDirProfile = nullptr;
 
+#if defined(RELATIVE_DATA_DIR)
+mozilla::Maybe<nsCOMPtr<nsIFile>> gDataDirPortable;
+#endif
+
 // These are required to allow nsXREDirProvider to be usable in xpcshell tests.
 // where gAppData is null.
 #if defined(XP_MACOSX) || defined(XP_UNIX)
@@ -1324,10 +1328,96 @@ nsresult nsXREDirProvider::SetUserDataProfileDirectory(nsCOMPtr<nsIFile>& aFile,
   return NS_OK;
 }
 
+#if defined(RELATIVE_DATA_DIR)
+nsresult nsXREDirProvider::GetPortableDataDir(nsIFile** aFile,
+                                              bool& aIsPortable) {
+  if (gDataDirPortable) {
+    if (*gDataDirPortable) {
+      nsresult rv = (*gDataDirPortable)->Clone(aFile);
+      NS_ENSURE_SUCCESS(rv, rv);
+      aIsPortable = true;
+    } else {
+      aIsPortable = false;
+    }
+    return NS_OK;
+  }
+
+  nsCOMPtr<nsIFile> exeFile, exeDir;
+  bool persistent = false;
+  nsresult rv =
+      GetFile(XRE_EXECUTABLE_FILE, &persistent, getter_AddRefs(exeFile));
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = exeFile->Normalize();
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = exeFile->GetParent(getter_AddRefs(exeDir));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+#  if defined(XP_MACOSX)
+  nsAutoString exeDirPath;
+  rv = exeDir->GetPath(exeDirPath);
+  NS_ENSURE_SUCCESS(rv, rv);
+  // When the browser is installed in /Applications, we never run in portable
+  // mode.
+  if (exeDirPath.Find("/Applications/", true /* ignore case */) == 0) {
+    aIsPortable = false;
+    return NS_OK;
+  }
+#  endif
+
+  nsCOMPtr<nsIFile> systemInstallFile;
+  rv = exeDir->Clone(getter_AddRefs(systemInstallFile));
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = systemInstallFile->AppendNative("system-install"_ns);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  bool exists = false;
+  rv = systemInstallFile->Exists(&exists);
+  NS_ENSURE_SUCCESS(rv, rv);
+  if (exists) {
+    aIsPortable = false;
+    gDataDirPortable.emplace(nullptr);
+    return NS_OK;
+  }
+
+  nsCOMPtr<nsIFile> localDir = exeDir;
+#  if defined(XP_MACOSX)
+  rv = exeDir->GetParent(getter_AddRefs(localDir));
+  NS_ENSURE_SUCCESS(rv, rv);
+  exeDir = localDir;
+  rv = exeDir->GetParent(getter_AddRefs(localDir));
+  NS_ENSURE_SUCCESS(rv, rv);
+#  endif
+  rv = localDir->SetRelativePath(localDir.get(),
+                                 nsLiteralCString(RELATIVE_DATA_DIR));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+#  if defined(XP_MACOSX)
+  // On macOS we try to create the directory immediately to switch to
+  // system-install mode if needed (e.g., when running from the DMG).
+  rv = localDir->Exists(&exists);
+  NS_ENSURE_SUCCESS(rv, rv);
+  if (!exists) {
+    rv = localDir->Create(nsIFile::DIRECTORY_TYPE, 0700);
+    if (NS_FAILED(rv)) {
+      aIsPortable = false;
+      return NS_OK;
+    }
+  }
+#  endif
+
+  gDataDirPortable.emplace(localDir);
+  rv = (*gDataDirPortable)->Clone(aFile);
+  NS_ENSURE_SUCCESS(rv, rv);
+  aIsPortable = true;
+  return rv;
+}
+#endif
+
 nsresult nsXREDirProvider::GetUserDataDirectoryHome(nsIFile** aFile,
                                                     bool aLocal) {
   // Copied from nsAppFileLocationProvider (more or less)
   nsresult rv;
+  NS_ENSURE_ARG_POINTER(aFile);
   nsCOMPtr<nsIFile> localDir;
 
   if (aLocal && gDataDirHomeLocal) {
@@ -1337,6 +1427,24 @@ nsresult nsXREDirProvider::GetUserDataDirectoryHome(nsIFile** aFile,
     return gDataDirHome->Clone(aFile);
   }
 
+#if defined(RELATIVE_DATA_DIR)
+  RefPtr<nsXREDirProvider> singleton = GetSingleton();
+  if (!singleton) {
+    return NS_ERROR_OUT_OF_MEMORY;
+  }
+  bool isPortable = false;
+  rv = singleton->GetPortableDataDir(getter_AddRefs(localDir), isPortable);
+  NS_ENSURE_SUCCESS(rv, rv);
+  if (isPortable) {
+    if (aLocal) {
+      rv = localDir->AppendNative("Caches"_ns);
+      NS_ENSURE_SUCCESS(rv, rv);
+    }
+    NS_IF_ADDREF(*aFile = localDir);
+    return rv;
+  }
+#endif
+
 #if defined(XP_MACOSX)
   FSRef fsRef;
   OSType folderType;
@@ -1530,6 +1638,13 @@ nsresult nsXREDirProvider::AppendProfilePath(nsIFile* aFile, bool aLocal) {
     return NS_OK;
   }
 
+#if defined(RELATIVE_DATA_DIR)
+  if (gDataDirPortable && *gDataDirPortable) {
+    // Do nothing in portable mode
+    return NS_OK;
+  }
+#endif
+
   nsAutoCString profile;
   nsAutoCString appName;
   nsAutoCString vendor;
diff --git a/toolkit/xre/nsXREDirProvider.h b/toolkit/xre/nsXREDirProvider.h
index 40cd9c8eb06a4..4b3eb00173e97 100644
--- a/toolkit/xre/nsXREDirProvider.h
+++ b/toolkit/xre/nsXREDirProvider.h
@@ -155,6 +155,14 @@ class nsXREDirProvider final : public nsIDirectoryServiceProvider2,
  private:
   static nsresult SetUserDataProfileDirectory(nsCOMPtr<nsIFile>& aFile,
                                               bool aLocal);
+
+#if defined(RELATIVE_DATA_DIR)
+  /**
+   * Get the path to the portable data dir, if the application is running in
+   * portable mode.
+   */
+  nsresult GetPortableDataDir(nsIFile** aFile, bool& aIsPortable);
+#endif
 };
 
 #endif
diff --git a/xpcom/io/nsAppFileLocationProvider.cpp b/xpcom/io/nsAppFileLocationProvider.cpp
index ef974f99048f8..72d161c561481 100644
--- a/xpcom/io/nsAppFileLocationProvider.cpp
+++ b/xpcom/io/nsAppFileLocationProvider.cpp
@@ -229,13 +229,108 @@ nsresult nsAppFileLocationProvider::CloneMozBinDirectory(nsIFile** aLocalFile) {
   return NS_OK;
 }
 
+#ifdef RELATIVE_DATA_DIR
+static nsresult SetupPortableMode(nsIFile** aDirectory, bool aLocal,
+                                  bool& aIsPortable) {
+  // This is almost the same as nsXREDirProvider::GetPortableDataDir.
+  // However, it seems that this is never called, at least during simple usage
+  // of the browser.
+
+  nsresult rv = NS_ERROR_UNEXPECTED;
+  nsCOMPtr<nsIProperties> directoryService(
+      do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID, &rv));
+  NS_ENSURE_SUCCESS(rv, rv);
+  nsCOMPtr<nsIFile> exeFile, exeDir;
+  rv = directoryService->Get(XRE_EXECUTABLE_FILE, NS_GET_IID(nsIFile),
+                             getter_AddRefs(exeFile));
+  rv = exeFile->Normalize();
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = exeFile->GetParent(getter_AddRefs(exeDir));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+#  if defined(XP_MACOSX)
+  nsAutoString exeDirPath;
+  rv = exeDir->GetPath(exeDirPath);
+  NS_ENSURE_SUCCESS(rv, rv);
+  // When the browser is installed in /Applications, we never run in portable
+  // mode.
+  if (exeDirPath.Find("/Applications/", true /* ignore case */) == 0) {
+    aIsPortable = false;
+    return NS_OK;
+  }
+#  endif
+
+  nsCOMPtr<nsIFile> systemInstallFile;
+  rv = exeDir->Clone(getter_AddRefs(systemInstallFile));
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = systemInstallFile->AppendNative("system-install"_ns);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  bool exists = false;
+  rv = systemInstallFile->Exists(&exists);
+  NS_ENSURE_SUCCESS(rv, rv);
+  if (exists) {
+    aIsPortable = false;
+    return NS_OK;
+  }
+
+  nsCOMPtr<nsIFile> localDir = exeDir;
+#  if defined(XP_MACOSX)
+  rv = exeDir->GetParent(getter_AddRefs(localDir));
+  NS_ENSURE_SUCCESS(rv, rv);
+  exeDir = localDir;
+  rv = exeDir->GetParent(getter_AddRefs(localDir));
+  NS_ENSURE_SUCCESS(rv, rv);
+#  endif
+
+  rv = localDir->SetRelativePath(localDir.get(),
+                                 nsLiteralCString(RELATIVE_DATA_DIR));
+  NS_ENSURE_SUCCESS(rv, rv);
+  if (aLocal) {
+    rv = localDir->AppendNative("Caches"_ns);
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+
+  rv = localDir->Exists(&exists);
+  NS_ENSURE_SUCCESS(rv, rv);
+  if (!exists) {
+    rv = localDir->Create(nsIFile::DIRECTORY_TYPE, 0700);
+#  if defined(XP_MACOSX)
+    if (NS_FAILED(rv)) {
+      // On macOS, we forgive this failure to allow running from the DMG.
+      aIsPortable = false;
+      return NS_OK;
+    }
+#  else
+    NS_ENSURE_SUCCESS(rv, rv);
+#  endif
+  }
+
+  localDir.forget(aDirectory);
+  aIsPortable = true;
+  return rv;
+}
+#endif
+
 //----------------------------------------------------------------------------------------
 // GetProductDirectory - Gets the directory which contains the application data
 // folder
 //
-// UNIX   : ~/.mozilla/
-// WIN    : <Application Data folder on user's machine>\Mozilla
-// Mac    : :Documents:Mozilla:
+// If portable mode is enabled:
+//  - aLocal == false: $APP_ROOT/$RELATIVE_DATA_DIR
+//  - aLocal == true:  $APP_ROOT/$RELATIVE_DATA_DIR/Caches
+// where $APP_ROOT is:
+//  - the parent directory of the executable on Windows and Linux
+//  - the root of the app bundle on macOS
+//
+// Otherwise:
+//  - Windows:
+//    - aLocal == false: %APPDATA%/$MOZ_USER_DIR
+//    - aLocal == true: %LOCALAPPDATA%/$MOZ_USER_DIR
+//  - macOS:
+//    - aLocal == false: kDomainLibraryFolderType/$MOZ_USER_DIR
+//    - aLocal == true: kCachedDataFolderType/$MOZ_USER_DIR
+//  - Unix: ~/$MOZ_USER_DIR
 //----------------------------------------------------------------------------------------
 nsresult nsAppFileLocationProvider::GetProductDirectory(nsIFile** aLocalFile,
                                                         bool aLocal) {
@@ -243,10 +338,20 @@ nsresult nsAppFileLocationProvider::GetProductDirectory(nsIFile** aLocalFile,
     return NS_ERROR_INVALID_ARG;
   }
 
-  nsresult rv;
+  nsresult rv = NS_ERROR_UNEXPECTED;
   bool exists;
   nsCOMPtr<nsIFile> localDir;
 
+#if defined(RELATIVE_DATA_DIR)
+  bool isPortable = false;
+  rv = SetupPortableMode(aLocalFile, aLocal, isPortable);
+  // If portable mode is enabled, we absolutely want it (e.g., to be sure there
+  // will not be disk leaks), so a failure is to be propagated.
+  if (NS_FAILED(rv) || isPortable) {
+    return rv;
+  }
+#endif
+
 #if defined(MOZ_WIDGET_COCOA)
   FSRef fsRef;
   OSType folderType =
@@ -309,9 +414,10 @@ nsresult nsAppFileLocationProvider::GetProductDirectory(nsIFile** aLocalFile,
 // GetDefaultUserProfileRoot - Gets the directory which contains each user
 // profile dir
 //
-// UNIX   : ~/.mozilla/
-// WIN    : <Application Data folder on user's machine>\Mozilla\Profiles
-// Mac    : :Documents:Mozilla:Profiles:
+// - Windows and macOS: $PRODUCT_DIRECTORY/Profiles
+// - Unix: $PRODUCT_DIRECTORY
+// See also GetProductDirectory for instructions on how $PRODUCT_DIRECTORY is
+// generated.
 //----------------------------------------------------------------------------------------
 nsresult nsAppFileLocationProvider::GetDefaultUserProfileRoot(
     nsIFile** aLocalFile, bool aLocal) {
-- 
GitLab