diff options
Diffstat (limited to 'toolkit/profile/nsToolkitProfileService.cpp')
-rw-r--r-- | toolkit/profile/nsToolkitProfileService.cpp | 2233 |
1 files changed, 2233 insertions, 0 deletions
diff --git a/toolkit/profile/nsToolkitProfileService.cpp b/toolkit/profile/nsToolkitProfileService.cpp new file mode 100644 index 0000000000..be2892fed1 --- /dev/null +++ b/toolkit/profile/nsToolkitProfileService.cpp @@ -0,0 +1,2233 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/ArrayUtils.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/WidgetUtils.h" +#include "nsProfileLock.h" + +#include <stdio.h> +#include <stdlib.h> +#include <prprf.h> +#include <prtime.h> + +#ifdef XP_WIN +# include <windows.h> +# include <shlobj.h> +# include "mozilla/PolicyChecks.h" +#endif +#ifdef XP_UNIX +# include <unistd.h> +#endif + +#include "nsToolkitProfileService.h" +#include "CmdLineAndEnvUtils.h" +#include "nsIFile.h" + +#ifdef XP_MACOSX +# include <CoreFoundation/CoreFoundation.h> +# include "nsILocalFileMac.h" +#endif + +#ifdef MOZ_WIDGET_GTK +# include "mozilla/WidgetUtilsGtk.h" +#endif + +#include "nsAppDirectoryServiceDefs.h" +#include "nsDirectoryServiceDefs.h" +#include "nsNetCID.h" +#include "nsXULAppAPI.h" +#include "nsThreadUtils.h" + +#include "nsIRunnable.h" +#include "nsXREDirProvider.h" +#include "nsAppRunner.h" +#include "nsString.h" +#include "nsReadableUtils.h" +#include "nsNativeCharsetUtils.h" +#include "mozilla/Attributes.h" +#include "mozilla/Sprintf.h" +#include "nsPrintfCString.h" +#include "mozilla/UniquePtr.h" +#include "nsIToolkitShellService.h" +#include "mozilla/Telemetry.h" +#include "nsProxyRelease.h" +#include "prinrval.h" +#include "prthread.h" +#ifdef MOZ_BACKGROUNDTASKS +# include "mozilla/BackgroundTasks.h" +# include "SpecialSystemDirectory.h" +#endif + +using namespace mozilla; + +#define DEV_EDITION_NAME "dev-edition-default" +#define DEFAULT_NAME "default" +#define COMPAT_FILE u"compatibility.ini"_ns +#define PROFILE_DB_VERSION "2" +#define INSTALL_PREFIX "Install" +#define INSTALL_PREFIX_LENGTH 7 + +struct KeyValue { + KeyValue(const char* aKey, const char* aValue) : key(aKey), value(aValue) {} + + nsCString key; + nsCString value; +}; + +static bool GetStrings(const char* aString, const char* aValue, + void* aClosure) { + nsTArray<UniquePtr<KeyValue>>* array = + static_cast<nsTArray<UniquePtr<KeyValue>>*>(aClosure); + array->AppendElement(MakeUnique<KeyValue>(aString, aValue)); + + return true; +} + +/** + * Returns an array of the strings inside a section of an ini file. + */ +nsTArray<UniquePtr<KeyValue>> GetSectionStrings(nsINIParser* aParser, + const char* aSection) { + nsTArray<UniquePtr<KeyValue>> result; + aParser->GetStrings(aSection, &GetStrings, &result); + return result; +} + +void RemoveProfileRecursion(const nsCOMPtr<nsIFile>& aDirectoryOrFile, + bool aIsIgnoreRoot, bool aIsIgnoreLockfile, + nsTArray<nsCOMPtr<nsIFile>>& aOutUndeletedFiles) { + auto guardDeletion = MakeScopeExit( + [&] { aOutUndeletedFiles.AppendElement(aDirectoryOrFile); }); + + // We actually would not expect to see links in our profiles, but still. + bool isLink = false; + NS_ENSURE_SUCCESS_VOID(aDirectoryOrFile->IsSymlink(&isLink)); + + // Only check to see if we have a directory if it isn't a link. + bool isDir = false; + if (!isLink) { + NS_ENSURE_SUCCESS_VOID(aDirectoryOrFile->IsDirectory(&isDir)); + } + + if (isDir) { + nsCOMPtr<nsIDirectoryEnumerator> dirEnum; + NS_ENSURE_SUCCESS_VOID( + aDirectoryOrFile->GetDirectoryEntries(getter_AddRefs(dirEnum))); + + bool more = false; + while (NS_SUCCEEDED(dirEnum->HasMoreElements(&more)) && more) { + nsCOMPtr<nsISupports> item; + dirEnum->GetNext(getter_AddRefs(item)); + nsCOMPtr<nsIFile> file = do_QueryInterface(item); + if (file) { + // Do not delete the profile lock. + if (aIsIgnoreLockfile && nsProfileLock::IsMaybeLockFile(file)) continue; + // If some children's remove fails, we still continue the loop. + RemoveProfileRecursion(file, false, false, aOutUndeletedFiles); + } + } + } + // Do not delete the root directory (yet). + if (!aIsIgnoreRoot) { + NS_ENSURE_SUCCESS_VOID(aDirectoryOrFile->Remove(false)); + } + guardDeletion.release(); +} + +void RemoveProfileFiles(nsIToolkitProfile* aProfile, bool aInBackground) { + nsCOMPtr<nsIFile> rootDir; + aProfile->GetRootDir(getter_AddRefs(rootDir)); + nsCOMPtr<nsIFile> localDir; + aProfile->GetLocalDir(getter_AddRefs(localDir)); + + // XXX If we get here with an active quota manager, + // something went very wrong. We want to assert this. + + // Just lock the directories, don't mark the profile as locked or the lock + // will attempt to release its reference to the profile on the background + // thread which will assert. + nsCOMPtr<nsIProfileLock> lock; + NS_ENSURE_SUCCESS_VOID( + NS_LockProfilePath(rootDir, localDir, nullptr, getter_AddRefs(lock))); + + nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction( + "nsToolkitProfile::RemoveProfileFiles", + [rootDir, localDir, lock]() mutable { + // We try to remove every single file and directory and collect + // those whose removal failed. + nsTArray<nsCOMPtr<nsIFile>> undeletedFiles; + // The root dir might contain the temp dir, so remove the temp dir + // first. + bool equals; + nsresult rv = rootDir->Equals(localDir, &equals); + if (NS_SUCCEEDED(rv) && !equals) { + RemoveProfileRecursion(localDir, + /* aIsIgnoreRoot */ false, + /* aIsIgnoreLockfile */ false, undeletedFiles); + } + // Now remove the content of the profile dir (except lockfile) + RemoveProfileRecursion(rootDir, + /* aIsIgnoreRoot */ true, + /* aIsIgnoreLockfile */ true, undeletedFiles); + + // Retry loop if something was not deleted + if (undeletedFiles.Length() > 0) { + uint32_t retries = 1; + // XXX: Until bug 1716291 is fixed we just make one retry + while (undeletedFiles.Length() > 0 && retries <= 1) { + Unused << PR_Sleep(PR_MillisecondsToInterval(10 * retries)); + for (auto&& file : + std::exchange(undeletedFiles, nsTArray<nsCOMPtr<nsIFile>>{})) { + RemoveProfileRecursion(file, + /* aIsIgnoreRoot */ false, + /* aIsIgnoreLockfile */ true, + undeletedFiles); + } + retries++; + } + } + +#ifdef DEBUG + // XXX: Until bug 1716291 is fixed, we do not want to spam release + if (undeletedFiles.Length() > 0) { + NS_WARNING("Unable to remove all files from the profile directory:"); + // Log the file names of those we could not remove + for (auto&& file : undeletedFiles) { + nsAutoString leafName; + if (NS_SUCCEEDED(file->GetLeafName(leafName))) { + NS_WARNING(NS_LossyConvertUTF16toASCII(leafName).get()); + } + } + } +#endif + // XXX: Activate this assert once bug 1716291 is fixed + // MOZ_ASSERT(undeletedFiles.Length() == 0); + + // Now we can unlock the profile safely. + lock->Unlock(); + // nsIProfileLock is not threadsafe so release our reference to it on + // the main thread. + NS_ReleaseOnMainThread("nsToolkitProfile::RemoveProfileFiles::Unlock", + lock.forget()); + + if (undeletedFiles.Length() == 0) { + // We can safely remove the (empty) remaining profile directory + // and lockfile, no other files are here. + // As we do this only if we had no other blockers, this is as safe + // as deleting the lockfile explicitely after unlocking. + Unused << rootDir->Remove(true); + } + }); + + if (aInBackground) { + nsCOMPtr<nsIEventTarget> target = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + target->Dispatch(runnable, NS_DISPATCH_NORMAL); + } else { + runnable->Run(); + } +} + +nsToolkitProfile::nsToolkitProfile(const nsACString& aName, nsIFile* aRootDir, + nsIFile* aLocalDir, bool aFromDB) + : mName(aName), + mRootDir(aRootDir), + mLocalDir(aLocalDir), + mLock(nullptr), + mIndex(0), + mSection("Profile") { + NS_ASSERTION(aRootDir, "No file!"); + + RefPtr<nsToolkitProfile> prev = + nsToolkitProfileService::gService->mProfiles.getLast(); + if (prev) { + mIndex = prev->mIndex + 1; + } + mSection.AppendInt(mIndex); + + nsToolkitProfileService::gService->mProfiles.insertBack(this); + + // If this profile isn't in the database already add it. + if (!aFromDB) { + nsINIParser* db = &nsToolkitProfileService::gService->mProfileDB; + db->SetString(mSection.get(), "Name", mName.get()); + + bool isRelative = false; + nsCString descriptor; + nsToolkitProfileService::gService->GetProfileDescriptor(this, descriptor, + &isRelative); + + db->SetString(mSection.get(), "IsRelative", isRelative ? "1" : "0"); + db->SetString(mSection.get(), "Path", descriptor.get()); + } +} + +NS_IMPL_ISUPPORTS(nsToolkitProfile, nsIToolkitProfile) + +NS_IMETHODIMP +nsToolkitProfile::GetRootDir(nsIFile** aResult) { + NS_ADDREF(*aResult = mRootDir); + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfile::GetLocalDir(nsIFile** aResult) { + NS_ADDREF(*aResult = mLocalDir); + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfile::GetName(nsACString& aResult) { + aResult = mName; + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfile::SetName(const nsACString& aName) { + NS_ASSERTION(nsToolkitProfileService::gService, "Where did my service go?"); + + if (mName.Equals(aName)) { + return NS_OK; + } + + // Changing the name from the dev-edition default profile name makes this + // profile no longer the dev-edition default. + if (mName.EqualsLiteral(DEV_EDITION_NAME) && + nsToolkitProfileService::gService->mDevEditionDefault == this) { + nsToolkitProfileService::gService->mDevEditionDefault = nullptr; + } + + mName = aName; + + nsresult rv = nsToolkitProfileService::gService->mProfileDB.SetString( + mSection.get(), "Name", mName.get()); + NS_ENSURE_SUCCESS(rv, rv); + + // Setting the name to the dev-edition default profile name will cause this + // profile to become the dev-edition default. + if (aName.EqualsLiteral(DEV_EDITION_NAME) && + !nsToolkitProfileService::gService->mDevEditionDefault) { + nsToolkitProfileService::gService->mDevEditionDefault = this; + } + + return NS_OK; +} + +nsresult nsToolkitProfile::RemoveInternal(bool aRemoveFiles, + bool aInBackground) { + NS_ASSERTION(nsToolkitProfileService::gService, "Whoa, my service is gone."); + + if (mLock) return NS_ERROR_FILE_IS_LOCKED; + + if (!isInList()) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (aRemoveFiles) { + RemoveProfileFiles(this, aInBackground); + } + + nsINIParser* db = &nsToolkitProfileService::gService->mProfileDB; + db->DeleteSection(mSection.get()); + + // We make some assumptions that the profile's index in the database is based + // on its position in the linked list. Removing a profile means we have to fix + // the index of later profiles in the list. The easiest way to do that is just + // to move the last profile into the profile's position and just update its + // index. + RefPtr<nsToolkitProfile> last = + nsToolkitProfileService::gService->mProfiles.getLast(); + if (last != this) { + // Update the section in the db. + last->mIndex = mIndex; + db->RenameSection(last->mSection.get(), mSection.get()); + last->mSection = mSection; + + if (last != getNext()) { + last->remove(); + setNext(last); + } + } + + remove(); + + if (nsToolkitProfileService::gService->mNormalDefault == this) { + nsToolkitProfileService::gService->mNormalDefault = nullptr; + } + if (nsToolkitProfileService::gService->mDevEditionDefault == this) { + nsToolkitProfileService::gService->mDevEditionDefault = nullptr; + } + if (nsToolkitProfileService::gService->mDedicatedProfile == this) { + nsToolkitProfileService::gService->SetDefaultProfile(nullptr); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfile::Remove(bool removeFiles) { + return RemoveInternal(removeFiles, false /* in background */); +} + +NS_IMETHODIMP +nsToolkitProfile::RemoveInBackground(bool removeFiles) { + return RemoveInternal(removeFiles, true /* in background */); +} + +NS_IMETHODIMP +nsToolkitProfile::Lock(nsIProfileUnlocker** aUnlocker, + nsIProfileLock** aResult) { + if (mLock) { + NS_ADDREF(*aResult = mLock); + return NS_OK; + } + + RefPtr<nsToolkitProfileLock> lock = new nsToolkitProfileLock(); + + nsresult rv = lock->Init(this, aUnlocker); + if (NS_FAILED(rv)) return rv; + + NS_ADDREF(*aResult = lock); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(nsToolkitProfileLock, nsIProfileLock) + +nsresult nsToolkitProfileLock::Init(nsToolkitProfile* aProfile, + nsIProfileUnlocker** aUnlocker) { + nsresult rv; + rv = Init(aProfile->mRootDir, aProfile->mLocalDir, aUnlocker); + if (NS_SUCCEEDED(rv)) mProfile = aProfile; + + return rv; +} + +nsresult nsToolkitProfileLock::Init(nsIFile* aDirectory, + nsIFile* aLocalDirectory, + nsIProfileUnlocker** aUnlocker) { + nsresult rv; + + rv = mLock.Lock(aDirectory, aUnlocker); + + if (NS_SUCCEEDED(rv)) { + mDirectory = aDirectory; + mLocalDirectory = aLocalDirectory; + } + + return rv; +} + +NS_IMETHODIMP +nsToolkitProfileLock::GetDirectory(nsIFile** aResult) { + if (!mDirectory) { + NS_ERROR("Not initialized, or unlocked!"); + return NS_ERROR_NOT_INITIALIZED; + } + + NS_ADDREF(*aResult = mDirectory); + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfileLock::GetLocalDirectory(nsIFile** aResult) { + if (!mLocalDirectory) { + NS_ERROR("Not initialized, or unlocked!"); + return NS_ERROR_NOT_INITIALIZED; + } + + NS_ADDREF(*aResult = mLocalDirectory); + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfileLock::Unlock() { + if (!mDirectory) { + NS_ERROR("Unlocking a never-locked nsToolkitProfileLock!"); + return NS_ERROR_UNEXPECTED; + } + + // XXX If we get here with an active quota manager, + // something went very wrong. We want to assert this. + + mLock.Unlock(); + + if (mProfile) { + mProfile->mLock = nullptr; + mProfile = nullptr; + } + mDirectory = nullptr; + mLocalDirectory = nullptr; + + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfileLock::GetReplacedLockTime(PRTime* aResult) { + mLock.GetReplacedLockTime(aResult); + return NS_OK; +} + +nsToolkitProfileLock::~nsToolkitProfileLock() { + if (mDirectory) { + Unlock(); + } +} + +nsToolkitProfileService* nsToolkitProfileService::gService = nullptr; + +NS_IMPL_ISUPPORTS(nsToolkitProfileService, nsIToolkitProfileService) + +nsToolkitProfileService::nsToolkitProfileService() + : mStartupProfileSelected(false), + mStartWithLast(true), + mIsFirstRun(true), + mUseDevEditionProfile(false), +#ifdef MOZ_DEDICATED_PROFILES + mUseDedicatedProfile(!IsSnapEnvironment() && !UseLegacyProfiles()), +#else + mUseDedicatedProfile(false), +#endif + mStartupReason(u"unknown"_ns), + mMaybeLockProfile(false), + mUpdateChannel(MOZ_STRINGIFY(MOZ_UPDATE_CHANNEL)), + mProfileDBExists(false), + mProfileDBFileSize(0), + mProfileDBModifiedTime(0) { +#ifdef MOZ_DEV_EDITION + mUseDevEditionProfile = true; +#endif +} + +nsToolkitProfileService::~nsToolkitProfileService() { + gService = nullptr; + mProfiles.clear(); +} + +void nsToolkitProfileService::CompleteStartup() { + if (!mStartupProfileSelected) { + return; + } + + ScalarSet(mozilla::Telemetry::ScalarID::STARTUP_PROFILE_SELECTION_REASON, + mStartupReason); + + if (mMaybeLockProfile) { + nsCOMPtr<nsIToolkitShellService> shell = + do_GetService(NS_TOOLKITSHELLSERVICE_CONTRACTID); + if (!shell) { + return; + } + + bool isDefaultApp; + nsresult rv = shell->IsDefaultApplication(&isDefaultApp); + NS_ENSURE_SUCCESS_VOID(rv); + + if (isDefaultApp) { + mProfileDB.SetString(mInstallSection.get(), "Locked", "1"); + + // There is a very small chance that this could fail if something else + // overwrote the profiles database since we started up, probably less than + // a second ago. There isn't really a sane response here, all the other + // profile changes are already flushed so whether we fail to flush here or + // force quit the app makes no difference. + NS_ENSURE_SUCCESS_VOID(Flush()); + } + } +} + +// Tests whether the passed profile was last used by this install. +bool nsToolkitProfileService::IsProfileForCurrentInstall( + nsIToolkitProfile* aProfile) { + nsCOMPtr<nsIFile> profileDir; + nsresult rv = aProfile->GetRootDir(getter_AddRefs(profileDir)); + NS_ENSURE_SUCCESS(rv, false); + + nsCOMPtr<nsIFile> compatFile; + rv = profileDir->Clone(getter_AddRefs(compatFile)); + NS_ENSURE_SUCCESS(rv, false); + + rv = compatFile->Append(COMPAT_FILE); + NS_ENSURE_SUCCESS(rv, false); + + nsINIParser compatData; + rv = compatData.Init(compatFile); + NS_ENSURE_SUCCESS(rv, false); + + /** + * In xpcshell gDirServiceProvider doesn't have all the correct directories + * set so using NS_GetSpecialDirectory works better there. But in a normal + * app launch the component registry isn't initialized so + * NS_GetSpecialDirectory doesn't work. So we have to use two different + * paths to support testing. + */ + nsCOMPtr<nsIFile> currentGreDir; + rv = NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(currentGreDir)); + if (rv == NS_ERROR_NOT_INITIALIZED) { + currentGreDir = gDirServiceProvider->GetGREDir(); + MOZ_ASSERT(currentGreDir, "No GRE dir found."); + } else if (NS_FAILED(rv)) { + return false; + } + + nsCString lastGreDirStr; + rv = compatData.GetString("Compatibility", "LastPlatformDir", lastGreDirStr); + // If this string is missing then this profile is from an ancient version. + // We'll opt to use it in this case. + if (NS_FAILED(rv)) { + return true; + } + + nsCOMPtr<nsIFile> lastGreDir; + rv = NS_NewNativeLocalFile(""_ns, false, getter_AddRefs(lastGreDir)); + NS_ENSURE_SUCCESS(rv, false); + + rv = lastGreDir->SetPersistentDescriptor(lastGreDirStr); + NS_ENSURE_SUCCESS(rv, false); + +#ifdef XP_WIN +# if defined(MOZ_THUNDERBIRD) || defined(MOZ_SUITE) + mozilla::PathString lastGreDirPath, currentGreDirPath; + lastGreDirPath = lastGreDir->NativePath(); + currentGreDirPath = currentGreDir->NativePath(); + if (lastGreDirPath.Equals(currentGreDirPath, + nsCaseInsensitiveStringComparator)) { + return true; + } + + // Convert a 64-bit install path to what would have been the 32-bit install + // path to allow users to migrate their profiles from one to the other. + PWSTR pathX86 = nullptr; + HRESULT hres = + SHGetKnownFolderPath(FOLDERID_ProgramFilesX86, 0, nullptr, &pathX86); + if (SUCCEEDED(hres)) { + nsDependentString strPathX86(pathX86); + if (!StringBeginsWith(currentGreDirPath, strPathX86, + nsCaseInsensitiveStringComparator)) { + PWSTR path = nullptr; + hres = SHGetKnownFolderPath(FOLDERID_ProgramFiles, 0, nullptr, &path); + if (SUCCEEDED(hres)) { + if (StringBeginsWith(currentGreDirPath, nsDependentString(path), + nsCaseInsensitiveStringComparator)) { + currentGreDirPath.Replace(0, wcslen(path), strPathX86); + } + } + CoTaskMemFree(path); + } + } + CoTaskMemFree(pathX86); + + return lastGreDirPath.Equals(currentGreDirPath, + nsCaseInsensitiveStringComparator); +# endif +#endif + + bool equal; + rv = lastGreDir->Equals(currentGreDir, &equal); + NS_ENSURE_SUCCESS(rv, false); + + return equal; +} + +/** + * Used the first time an install with dedicated profile support runs. Decides + * whether to mark the passed profile as the default for this install. + * + * The goal is to reduce disruption but ideally end up with the OS default + * install using the old default profile. + * + * If the decision is to use the profile then it will be unassigned as the + * dedicated default for other installs. + * + * We won't attempt to use the profile if it was last used by a different + * install. + * + * If the profile is currently in use by an install that was either the OS + * default install or the profile has been explicitely chosen by some other + * means then we won't use it. + * + * aResult will be set to true if we chose to make the profile the new dedicated + * default. + */ +nsresult nsToolkitProfileService::MaybeMakeDefaultDedicatedProfile( + nsIToolkitProfile* aProfile, bool* aResult) { + nsresult rv; + *aResult = false; + + // If the profile was last used by a different install then we won't use it. + if (!IsProfileForCurrentInstall(aProfile)) { + return NS_OK; + } + + nsCString descriptor; + rv = GetProfileDescriptor(aProfile, descriptor, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + // Get a list of all the installs. + nsTArray<nsCString> installs = GetKnownInstalls(); + + // Cache the installs that use the profile. + nsTArray<nsCString> inUseInstalls; + + // See if the profile is already in use by an install that hasn't locked it. + for (uint32_t i = 0; i < installs.Length(); i++) { + const nsCString& install = installs[i]; + + nsCString path; + rv = mProfileDB.GetString(install.get(), "Default", path); + if (NS_FAILED(rv)) { + continue; + } + + // Is this install using the profile we care about? + if (!descriptor.Equals(path)) { + continue; + } + + // Is this profile locked to this other install? + nsCString isLocked; + rv = mProfileDB.GetString(install.get(), "Locked", isLocked); + if (NS_SUCCEEDED(rv) && isLocked.Equals("1")) { + return NS_OK; + } + + inUseInstalls.AppendElement(install); + } + + // At this point we've decided to take the profile. Strip it from other + // installs. + for (uint32_t i = 0; i < inUseInstalls.Length(); i++) { + // Removing the default setting entirely will make the install go through + // the first run process again at startup and create itself a new profile. + mProfileDB.DeleteString(inUseInstalls[i].get(), "Default"); + } + + // Set this as the default profile for this install. + SetDefaultProfile(aProfile); + + // SetDefaultProfile will have locked this profile to this install so no + // other installs will steal it, but this was auto-selected so we want to + // unlock it so that other installs can potentially take it. + mProfileDB.DeleteString(mInstallSection.get(), "Locked"); + + // Persist the changes. + rv = Flush(); + NS_ENSURE_SUCCESS(rv, rv); + + // Once XPCOM is available check if this is the default application and if so + // lock the profile again. + mMaybeLockProfile = true; + *aResult = true; + + return NS_OK; +} + +bool IsFileOutdated(nsIFile* aFile, bool aExists, PRTime aLastModified, + int64_t aLastSize) { + nsCOMPtr<nsIFile> file; + nsresult rv = aFile->Clone(getter_AddRefs(file)); + if (NS_FAILED(rv)) { + return false; + } + + bool exists; + rv = aFile->Exists(&exists); + if (NS_FAILED(rv) || exists != aExists) { + return true; + } + + if (!exists) { + return false; + } + + int64_t size; + rv = aFile->GetFileSize(&size); + if (NS_FAILED(rv) || size != aLastSize) { + return true; + } + + PRTime time; + rv = aFile->GetLastModifiedTime(&time); + if (NS_FAILED(rv) || time != aLastModified) { + return true; + } + + return false; +} + +nsresult UpdateFileStats(nsIFile* aFile, bool* aExists, PRTime* aLastModified, + int64_t* aLastSize) { + nsCOMPtr<nsIFile> file; + nsresult rv = aFile->Clone(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = file->Exists(aExists); + NS_ENSURE_SUCCESS(rv, rv); + + if (!(*aExists)) { + *aLastModified = 0; + *aLastSize = 0; + return NS_OK; + } + + rv = file->GetFileSize(aLastSize); + NS_ENSURE_SUCCESS(rv, rv); + + rv = file->GetLastModifiedTime(aLastModified); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfileService::GetIsListOutdated(bool* aResult) { + if (IsFileOutdated(mProfileDBFile, mProfileDBExists, mProfileDBModifiedTime, + mProfileDBFileSize)) { + *aResult = true; + return NS_OK; + } + + *aResult = false; + return NS_OK; +} + +struct ImportInstallsClosure { + nsINIParser* backupData; + nsINIParser* profileDB; +}; + +static bool ImportInstalls(const char* aSection, void* aClosure) { + ImportInstallsClosure* closure = + static_cast<ImportInstallsClosure*>(aClosure); + + nsTArray<UniquePtr<KeyValue>> strings = + GetSectionStrings(closure->backupData, aSection); + if (strings.IsEmpty()) { + return true; + } + + nsCString newSection(INSTALL_PREFIX); + newSection.Append(aSection); + nsCString buffer; + + for (uint32_t i = 0; i < strings.Length(); i++) { + closure->profileDB->SetString(newSection.get(), strings[i]->key.get(), + strings[i]->value.get()); + } + + return true; +} + +nsresult nsToolkitProfileService::Init() { + NS_ASSERTION(gDirServiceProvider, "No dirserviceprovider!"); + nsresult rv; + + rv = nsXREDirProvider::GetUserAppDataDirectory(getter_AddRefs(mAppData)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = nsXREDirProvider::GetUserLocalDataDirectory(getter_AddRefs(mTempData)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mAppData->Clone(getter_AddRefs(mProfileDBFile)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mProfileDBFile->AppendNative("profiles.ini"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mAppData->Clone(getter_AddRefs(mInstallDBFile)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mInstallDBFile->AppendNative("installs.ini"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString buffer; + + rv = UpdateFileStats(mProfileDBFile, &mProfileDBExists, + &mProfileDBModifiedTime, &mProfileDBFileSize); + if (NS_SUCCEEDED(rv) && mProfileDBExists) { + rv = mProfileDB.Init(mProfileDBFile); + // Init does not fail on parsing errors, only on OOM/really unexpected + // conditions. + if (NS_FAILED(rv)) { + return rv; + } + + rv = mProfileDB.GetString("General", "StartWithLastProfile", buffer); + if (NS_SUCCEEDED(rv)) { + mStartWithLast = !buffer.EqualsLiteral("0"); + } + + rv = mProfileDB.GetString("General", "Version", buffer); + if (NS_FAILED(rv)) { + // This is a profiles.ini written by an older version. We must restore + // any install data from the backup. + nsINIParser installDB; + + if (NS_SUCCEEDED(installDB.Init(mInstallDBFile))) { + // There is install data to import. + ImportInstallsClosure closure = {&installDB, &mProfileDB}; + installDB.GetSections(&ImportInstalls, &closure); + } + + rv = mProfileDB.SetString("General", "Version", PROFILE_DB_VERSION); + NS_ENSURE_SUCCESS(rv, rv); + } + } else { + rv = mProfileDB.SetString("General", "StartWithLastProfile", + mStartWithLast ? "1" : "0"); + NS_ENSURE_SUCCESS(rv, rv); + rv = mProfileDB.SetString("General", "Version", PROFILE_DB_VERSION); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCString installProfilePath; + + if (mUseDedicatedProfile) { + nsString installHash; + rv = gDirServiceProvider->GetInstallHash(installHash); + NS_ENSURE_SUCCESS(rv, rv); + CopyUTF16toUTF8(installHash, mInstallSection); + mInstallSection.Insert(INSTALL_PREFIX, 0); + + // Try to find the descriptor for the default profile for this install. + rv = mProfileDB.GetString(mInstallSection.get(), "Default", + installProfilePath); + + // Not having a value means this install doesn't appear in installs.ini so + // this is the first run for this install. + if (NS_FAILED(rv)) { + mIsFirstRun = true; + + // Gets the install section that would have been created if the install + // path has incorrect casing (see bug 1555319). We use this later during + // profile selection. + rv = gDirServiceProvider->GetLegacyInstallHash(installHash); + NS_ENSURE_SUCCESS(rv, rv); + CopyUTF16toUTF8(installHash, mLegacyInstallSection); + mLegacyInstallSection.Insert(INSTALL_PREFIX, 0); + } else { + mIsFirstRun = false; + } + } + + nsToolkitProfile* currentProfile = nullptr; + +#ifdef MOZ_DEV_EDITION + nsCOMPtr<nsIFile> ignoreDevEditionProfile; + rv = mAppData->Clone(getter_AddRefs(ignoreDevEditionProfile)); + if (NS_FAILED(rv)) { + return rv; + } + + rv = ignoreDevEditionProfile->AppendNative("ignore-dev-edition-profile"_ns); + if (NS_FAILED(rv)) { + return rv; + } + + bool shouldIgnoreSeparateProfile; + rv = ignoreDevEditionProfile->Exists(&shouldIgnoreSeparateProfile); + if (NS_FAILED(rv)) return rv; + + mUseDevEditionProfile = !shouldIgnoreSeparateProfile; +#endif + + nsCOMPtr<nsIToolkitProfile> autoSelectProfile; + + unsigned int nonDevEditionProfiles = 0; + unsigned int c = 0; + for (c = 0; true; ++c) { + nsAutoCString profileID("Profile"); + profileID.AppendInt(c); + + rv = mProfileDB.GetString(profileID.get(), "IsRelative", buffer); + if (NS_FAILED(rv)) break; + + bool isRelative = buffer.EqualsLiteral("1"); + + nsAutoCString filePath; + + rv = mProfileDB.GetString(profileID.get(), "Path", filePath); + if (NS_FAILED(rv)) { + NS_ERROR("Malformed profiles.ini: Path= not found"); + continue; + } + + nsAutoCString name; + + rv = mProfileDB.GetString(profileID.get(), "Name", name); + if (NS_FAILED(rv)) { + NS_ERROR("Malformed profiles.ini: Name= not found"); + continue; + } + + nsCOMPtr<nsIFile> rootDir; + rv = NS_NewNativeLocalFile(""_ns, true, getter_AddRefs(rootDir)); + NS_ENSURE_SUCCESS(rv, rv); + + if (isRelative) { + rv = rootDir->SetRelativeDescriptor(mAppData, filePath); + } else { + rv = rootDir->SetPersistentDescriptor(filePath); + } + if (NS_FAILED(rv)) continue; + + nsCOMPtr<nsIFile> localDir; + if (isRelative) { + rv = NS_NewNativeLocalFile(""_ns, true, getter_AddRefs(localDir)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = localDir->SetRelativeDescriptor(mTempData, filePath); + } else { + localDir = rootDir; + } + + currentProfile = new nsToolkitProfile(name, rootDir, localDir, true); + + // If a user has modified the ini file path it may make for a valid profile + // path but not match what we would have serialised and so may not match + // the path in the install section. Re-serialise it to get it in the + // expected form again. + bool nowRelative; + nsCString descriptor; + GetProfileDescriptor(currentProfile, descriptor, &nowRelative); + + if (isRelative != nowRelative || !descriptor.Equals(filePath)) { + mProfileDB.SetString(profileID.get(), "IsRelative", + nowRelative ? "1" : "0"); + mProfileDB.SetString(profileID.get(), "Path", descriptor.get()); + + // Should we flush now? It costs some startup time and we will fix it on + // the next startup anyway. If something else causes a flush then it will + // be fixed in the ini file then. + } + + rv = mProfileDB.GetString(profileID.get(), "Default", buffer); + if (NS_SUCCEEDED(rv) && buffer.EqualsLiteral("1")) { + mNormalDefault = currentProfile; + } + + // Is this the default profile for this install? + if (mUseDedicatedProfile && !mDedicatedProfile && + installProfilePath.Equals(descriptor)) { + // Found a profile for this install. + mDedicatedProfile = currentProfile; + } + + if (name.EqualsLiteral(DEV_EDITION_NAME)) { + mDevEditionDefault = currentProfile; + } else { + nonDevEditionProfiles++; + autoSelectProfile = currentProfile; + } + } + + // If there is only one non-dev-edition profile then mark it as the default. + if (!mNormalDefault && nonDevEditionProfiles == 1) { + SetNormalDefault(autoSelectProfile); + } + + if (!mUseDedicatedProfile) { + if (mUseDevEditionProfile) { + // When using the separate dev-edition profile not finding it means this + // is a first run. + mIsFirstRun = !mDevEditionDefault; + } else { + // If there are no normal profiles then this is a first run. + mIsFirstRun = nonDevEditionProfiles == 0; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfileService::SetStartWithLastProfile(bool aValue) { + if (mStartWithLast != aValue) { + // Note: the skeleton ui (see PreXULSkeletonUI.cpp) depends on this + // having this name and being under General. If that ever changes, + // the skeleton UI will just need to be updated. If it changes frequently, + // it's probably best we just mirror the value to the registry here. + nsresult rv = mProfileDB.SetString("General", "StartWithLastProfile", + aValue ? "1" : "0"); + NS_ENSURE_SUCCESS(rv, rv); + mStartWithLast = aValue; + } + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfileService::GetStartWithLastProfile(bool* aResult) { + *aResult = mStartWithLast; + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfileService::GetProfiles(nsISimpleEnumerator** aResult) { + *aResult = new ProfileEnumerator(mProfiles.getFirst()); + + NS_ADDREF(*aResult); + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfileService::ProfileEnumerator::HasMoreElements(bool* aResult) { + *aResult = mCurrent ? true : false; + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfileService::ProfileEnumerator::GetNext(nsISupports** aResult) { + if (!mCurrent) return NS_ERROR_FAILURE; + + NS_ADDREF(*aResult = mCurrent); + + mCurrent = mCurrent->getNext(); + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfileService::GetCurrentProfile(nsIToolkitProfile** aResult) { + NS_IF_ADDREF(*aResult = mCurrent); + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfileService::GetDefaultProfile(nsIToolkitProfile** aResult) { + if (mUseDedicatedProfile) { + NS_IF_ADDREF(*aResult = mDedicatedProfile); + return NS_OK; + } + + if (mUseDevEditionProfile) { + NS_IF_ADDREF(*aResult = mDevEditionDefault); + return NS_OK; + } + + NS_IF_ADDREF(*aResult = mNormalDefault); + return NS_OK; +} + +void nsToolkitProfileService::SetNormalDefault(nsIToolkitProfile* aProfile) { + if (mNormalDefault == aProfile) { + return; + } + + if (mNormalDefault) { + nsToolkitProfile* profile = + static_cast<nsToolkitProfile*>(mNormalDefault.get()); + mProfileDB.DeleteString(profile->mSection.get(), "Default"); + } + + mNormalDefault = aProfile; + + if (mNormalDefault) { + nsToolkitProfile* profile = + static_cast<nsToolkitProfile*>(mNormalDefault.get()); + mProfileDB.SetString(profile->mSection.get(), "Default", "1"); + } +} + +NS_IMETHODIMP +nsToolkitProfileService::SetDefaultProfile(nsIToolkitProfile* aProfile) { + if (mUseDedicatedProfile) { + if (mDedicatedProfile != aProfile) { + if (!aProfile) { + // Setting this to the empty string means no profile will be found on + // startup but we'll recognise that this install has been used + // previously. + mProfileDB.SetString(mInstallSection.get(), "Default", ""); + } else { + nsCString profilePath; + nsresult rv = GetProfileDescriptor(aProfile, profilePath, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + mProfileDB.SetString(mInstallSection.get(), "Default", + profilePath.get()); + } + mDedicatedProfile = aProfile; + + // Some kind of choice has happened here, lock this profile to this + // install. + mProfileDB.SetString(mInstallSection.get(), "Locked", "1"); + } + return NS_OK; + } + + if (mUseDevEditionProfile && aProfile != mDevEditionDefault) { + // The separate profile is hardcoded. + return NS_ERROR_FAILURE; + } + + SetNormalDefault(aProfile); + + return NS_OK; +} + +// Gets the profile root directory descriptor for storing in profiles.ini or +// installs.ini. +nsresult nsToolkitProfileService::GetProfileDescriptor( + nsIToolkitProfile* aProfile, nsACString& aDescriptor, bool* aIsRelative) { + nsCOMPtr<nsIFile> profileDir; + nsresult rv = aProfile->GetRootDir(getter_AddRefs(profileDir)); + NS_ENSURE_SUCCESS(rv, rv); + + // if the profile dir is relative to appdir... + bool isRelative; + rv = mAppData->Contains(profileDir, &isRelative); + + nsCString profilePath; + if (NS_SUCCEEDED(rv) && isRelative) { + // we use a relative descriptor + rv = profileDir->GetRelativeDescriptor(mAppData, profilePath); + } else { + // otherwise, a persistent descriptor + rv = profileDir->GetPersistentDescriptor(profilePath); + } + NS_ENSURE_SUCCESS(rv, rv); + + aDescriptor.Assign(profilePath); + if (aIsRelative) { + *aIsRelative = isRelative; + } + + return NS_OK; +} + +nsresult nsToolkitProfileService::CreateDefaultProfile( + nsIToolkitProfile** aResult) { + // Create a new default profile + nsAutoCString name; + if (mUseDevEditionProfile) { + name.AssignLiteral(DEV_EDITION_NAME); + } else if (mUseDedicatedProfile) { + name.AppendPrintf("default-%s", mUpdateChannel.get()); + } else { + name.AssignLiteral(DEFAULT_NAME); + } + + nsresult rv = CreateUniqueProfile(nullptr, name, aResult); + NS_ENSURE_SUCCESS(rv, rv); + + if (mUseDedicatedProfile) { + SetDefaultProfile(mCurrent); + } else if (mUseDevEditionProfile) { + mDevEditionDefault = mCurrent; + } else { + SetNormalDefault(mCurrent); + } + + return NS_OK; +} + +/** + * An implementation of SelectStartupProfile callable from JavaScript via XPCOM. + * See nsIToolkitProfileService.idl. + */ +NS_IMETHODIMP +nsToolkitProfileService::SelectStartupProfile( + const nsTArray<nsCString>& aArgv, bool aIsResetting, + const nsACString& aUpdateChannel, const nsACString& aLegacyInstallHash, + nsIFile** aRootDir, nsIFile** aLocalDir, nsIToolkitProfile** aProfile, + bool* aDidCreate) { + int argc = aArgv.Length(); + // Our command line handling expects argv to be null-terminated so construct + // an appropriate array. + auto argv = MakeUnique<char*[]>(argc + 1); + // Also, our command line handling removes things from the array without + // freeing them so keep track of what we've created separately. + auto allocated = MakeUnique<UniqueFreePtr<char>[]>(argc); + + for (int i = 0; i < argc; i++) { + allocated[i].reset(ToNewCString(aArgv[i])); + argv[i] = allocated[i].get(); + } + argv[argc] = nullptr; + + mUpdateChannel = aUpdateChannel; + if (!aLegacyInstallHash.IsEmpty()) { + mLegacyInstallSection.Assign(aLegacyInstallHash); + mLegacyInstallSection.Insert(INSTALL_PREFIX, 0); + } + + bool wasDefault; + nsresult rv = + SelectStartupProfile(&argc, argv.get(), aIsResetting, aRootDir, aLocalDir, + aProfile, aDidCreate, &wasDefault); + + // Since we were called outside of the normal startup path complete any + // startup tasks. + if (NS_SUCCEEDED(rv)) { + CompleteStartup(); + } + + return rv; +} + +static void SaltProfileName(nsACString& aName); + +/** + * Selects or creates a profile to use based on the profiles database, any + * environment variables and any command line arguments. Will not create + * a profile if aIsResetting is true. The profile is selected based on this + * order of preference: + * * Environment variables (set when restarting the application). + * * --profile command line argument. + * * --createprofile command line argument (this also causes the app to exit). + * * -p command line argument. + * * A new profile created if this is the first run of the application. + * * The default profile. + * aRootDir and aLocalDir are set to the data and local directories for the + * profile data. If a profile from the database was selected it will be + * returned in aProfile. + * aDidCreate will be set to true if a new profile was created. + * This function should be called once at startup and will fail if called again. + * aArgv should be an array of aArgc + 1 strings, the last element being null. + * Both aArgv and aArgc will be mutated. + */ +nsresult nsToolkitProfileService::SelectStartupProfile( + int* aArgc, char* aArgv[], bool aIsResetting, nsIFile** aRootDir, + nsIFile** aLocalDir, nsIToolkitProfile** aProfile, bool* aDidCreate, + bool* aWasDefaultSelection) { + if (mStartupProfileSelected) { + return NS_ERROR_ALREADY_INITIALIZED; + } + + mStartupProfileSelected = true; + *aDidCreate = false; + *aWasDefaultSelection = false; + + nsresult rv; + const char* arg; + + // Use the profile specified in the environment variables (generally from an + // app initiated restart). + nsCOMPtr<nsIFile> lf = GetFileFromEnv("XRE_PROFILE_PATH"); + if (lf) { + nsCOMPtr<nsIFile> localDir = GetFileFromEnv("XRE_PROFILE_LOCAL_PATH"); + if (!localDir) { + localDir = lf; + } + + // Clear out flags that we handled (or should have handled!) last startup. + const char* dummy; + CheckArg(*aArgc, aArgv, "p", &dummy); + CheckArg(*aArgc, aArgv, "profile", &dummy); + CheckArg(*aArgc, aArgv, "profilemanager"); + + nsCOMPtr<nsIToolkitProfile> profile; + GetProfileByDir(lf, localDir, getter_AddRefs(profile)); + + if (profile && mIsFirstRun && mUseDedicatedProfile) { + if (profile == + (mUseDevEditionProfile ? mDevEditionDefault : mNormalDefault)) { + // This is the first run of a dedicated profile build where the selected + // profile is the previous default so we should either make it the + // default profile for this install or push the user to a new profile. + + bool result; + rv = MaybeMakeDefaultDedicatedProfile(profile, &result); + NS_ENSURE_SUCCESS(rv, rv); + if (result) { + mStartupReason = u"restart-claimed-default"_ns; + + mCurrent = profile; + } else { + rv = CreateDefaultProfile(getter_AddRefs(mCurrent)); + if (NS_FAILED(rv)) { + *aProfile = nullptr; + return rv; + } + + rv = Flush(); + NS_ENSURE_SUCCESS(rv, rv); + + mStartupReason = u"restart-skipped-default"_ns; + *aDidCreate = true; + } + + NS_IF_ADDREF(*aProfile = mCurrent); + mCurrent->GetRootDir(aRootDir); + mCurrent->GetLocalDir(aLocalDir); + + return NS_OK; + } + } + + if (EnvHasValue("XRE_RESTARTED_BY_PROFILE_MANAGER")) { + mStartupReason = u"profile-manager"_ns; + } else if (aIsResetting) { + mStartupReason = u"profile-reset"_ns; + } else { + mStartupReason = u"restart"_ns; + } + + mCurrent = profile; + lf.forget(aRootDir); + localDir.forget(aLocalDir); + NS_IF_ADDREF(*aProfile = profile); + return NS_OK; + } + + // Check the -profile command line argument. It accepts a single argument that + // gives the path to use for the profile. + ArgResult ar = CheckArg(*aArgc, aArgv, "profile", &arg); + if (ar == ARG_BAD) { + PR_fprintf(PR_STDERR, "Error: argument --profile requires a path\n"); + return NS_ERROR_FAILURE; + } + if (ar) { + nsCOMPtr<nsIFile> lf; + rv = XRE_GetFileFromPath(arg, getter_AddRefs(lf)); + NS_ENSURE_SUCCESS(rv, rv); + + // Make sure that the profile path exists and it's a directory. + bool exists; + rv = lf->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + if (!exists) { + rv = lf->Create(nsIFile::DIRECTORY_TYPE, 0700); + NS_ENSURE_SUCCESS(rv, rv); + } else { + bool isDir; + rv = lf->IsDirectory(&isDir); + NS_ENSURE_SUCCESS(rv, rv); + if (!isDir) { + PR_fprintf( + PR_STDERR, + "Error: argument --profile requires a path to a directory\n"); + return NS_ERROR_FAILURE; + } + } + + mStartupReason = u"argument-profile"_ns; + + GetProfileByDir(lf, nullptr, getter_AddRefs(mCurrent)); + NS_ADDREF(*aRootDir = lf); + // If the root dir matched a profile then use its local dir, otherwise use + // the root dir as the local dir. + if (mCurrent) { + mCurrent->GetLocalDir(aLocalDir); + } else { + lf.forget(aLocalDir); + } + + NS_IF_ADDREF(*aProfile = mCurrent); + return NS_OK; + } + + // Check the -createprofile command line argument. It accepts a single + // argument that is either the name for the new profile or the name followed + // by the path to use. + ar = CheckArg(*aArgc, aArgv, "createprofile", &arg, CheckArgFlag::RemoveArg); + if (ar == ARG_BAD) { + PR_fprintf(PR_STDERR, + "Error: argument --createprofile requires a profile name\n"); + return NS_ERROR_FAILURE; + } + if (ar) { + const char* delim = strchr(arg, ' '); + nsCOMPtr<nsIToolkitProfile> profile; + if (delim) { + nsCOMPtr<nsIFile> lf; + rv = NS_NewNativeLocalFile(nsDependentCString(delim + 1), true, + getter_AddRefs(lf)); + if (NS_FAILED(rv)) { + PR_fprintf(PR_STDERR, "Error: profile path not valid.\n"); + return rv; + } + + // As with --profile, assume that the given path will be used for the + // main profile directory. + rv = CreateProfile(lf, nsDependentCSubstring(arg, delim), + getter_AddRefs(profile)); + } else { + rv = CreateProfile(nullptr, nsDependentCString(arg), + getter_AddRefs(profile)); + } + // Some pathological arguments can make it this far + if (NS_FAILED(rv) || NS_FAILED(Flush())) { + PR_fprintf(PR_STDERR, "Error creating profile.\n"); + } + return NS_ERROR_ABORT; + } + + // Check the -p command line argument. It either accepts a profile name and + // uses that named profile or without a name it opens the profile manager. + ar = CheckArg(*aArgc, aArgv, "p", &arg); + if (ar == ARG_BAD) { + return NS_ERROR_SHOW_PROFILE_MANAGER; + } + if (ar) { + rv = GetProfileByName(nsDependentCString(arg), getter_AddRefs(mCurrent)); + if (NS_SUCCEEDED(rv)) { + mStartupReason = u"argument-p"_ns; + + mCurrent->GetRootDir(aRootDir); + mCurrent->GetLocalDir(aLocalDir); + + NS_ADDREF(*aProfile = mCurrent); + return NS_OK; + } + + return NS_ERROR_SHOW_PROFILE_MANAGER; + } + + ar = CheckArg(*aArgc, aArgv, "profilemanager"); + if (ar == ARG_FOUND) { + return NS_ERROR_SHOW_PROFILE_MANAGER; + } + +#ifdef MOZ_BACKGROUNDTASKS + if (BackgroundTasks::IsBackgroundTaskMode()) { + // There are two cases: + // 1. ephemeral profile: create a new one in temporary directory. + // 2. non-ephemeral (persistent) profile: + // a. if no salted profile is known, create a new one in + // background task-specific directory. + // b. if salted profile is know, use salted path. + nsString installHash; + rv = gDirServiceProvider->GetInstallHash(installHash); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString profilePrefix(BackgroundTasks::GetProfilePrefix( + NS_LossyConvertUTF16toASCII(installHash))); + + nsCString taskName(BackgroundTasks::GetBackgroundTasks().ref()); + + nsCOMPtr<nsIFile> file; + + if (BackgroundTasks::IsEphemeralProfileTaskName(taskName)) { + // Background task mode does not enable legacy telemetry, so this is for + // completeness and testing only. + mStartupReason = u"backgroundtask-ephemeral"_ns; + + nsCOMPtr<nsIFile> rootDir; + rv = GetSpecialSystemDirectory(OS_TemporaryDirectory, + getter_AddRefs(rootDir)); + NS_ENSURE_SUCCESS(rv, rv); + + nsresult rv = BackgroundTasks::CreateEphemeralProfileDirectory( + rootDir, profilePrefix, getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + // In background task mode, NS_ERROR_UNEXPECTED is handled specially to + // exit with a non-zero exit code. + return NS_ERROR_UNEXPECTED; + } + *aDidCreate = true; + } else { + // Background task mode does not enable legacy telemetry, so this is for + // completeness and testing only. + mStartupReason = u"backgroundtask-not-ephemeral"_ns; + + // A non-ephemeral profile is required. + nsCOMPtr<nsIFile> rootDir; + nsresult rv = gDirServiceProvider->GetBackgroundTasksProfilesRootDir( + getter_AddRefs(rootDir)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString buffer; + rv = mProfileDB.GetString("BackgroundTasksProfiles", profilePrefix.get(), + buffer); + if (NS_SUCCEEDED(rv)) { + // We have a record of one! Use it. + rv = rootDir->Clone(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = file->AppendNative(buffer); + NS_ENSURE_SUCCESS(rv, rv); + } else { + nsCString saltedProfilePrefix = profilePrefix; + SaltProfileName(saltedProfilePrefix); + + nsresult rv = BackgroundTasks::CreateNonEphemeralProfileDirectory( + rootDir, saltedProfilePrefix, getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + // In background task mode, NS_ERROR_UNEXPECTED is handled specially + // to exit with a non-zero exit code. + return NS_ERROR_UNEXPECTED; + } + *aDidCreate = true; + + // Keep a record of the salted name. It's okay if this doesn't succeed: + // not great, but it's better for tasks (particularly, + // `backgroundupdate`) to run and not persist state correctly than to + // not run at all. + rv = + mProfileDB.SetString("BackgroundTasksProfiles", profilePrefix.get(), + saltedProfilePrefix.get()); + Unused << NS_WARN_IF(NS_FAILED(rv)); + + if (NS_SUCCEEDED(rv)) { + rv = Flush(); + Unused << NS_WARN_IF(NS_FAILED(rv)); + } + } + } + + nsCOMPtr<nsIFile> localDir = file; + file.forget(aRootDir); + localDir.forget(aLocalDir); + + // Background tasks never use profiles known to the profile service. + *aProfile = nullptr; + + return NS_OK; + } +#endif + + if (mIsFirstRun && mUseDedicatedProfile && + !mInstallSection.Equals(mLegacyInstallSection)) { + // The default profile could be assigned to a hash generated from an + // incorrectly cased version of the installation directory (see bug + // 1555319). Ideally we'd do all this while loading profiles.ini but we + // can't override the legacy section value before that for tests. + nsCString defaultDescriptor; + rv = mProfileDB.GetString(mLegacyInstallSection.get(), "Default", + defaultDescriptor); + + if (NS_SUCCEEDED(rv)) { + // There is a default here, need to see if it matches any profiles. + bool isRelative; + nsCString descriptor; + + for (RefPtr<nsToolkitProfile> profile : mProfiles) { + GetProfileDescriptor(profile, descriptor, &isRelative); + + if (descriptor.Equals(defaultDescriptor)) { + // Found the default profile. Copy the install section over to + // the correct location. We leave the old info in place for older + // versions of Firefox to use. + nsTArray<UniquePtr<KeyValue>> strings = + GetSectionStrings(&mProfileDB, mLegacyInstallSection.get()); + for (const auto& kv : strings) { + mProfileDB.SetString(mInstallSection.get(), kv->key.get(), + kv->value.get()); + } + + // Flush now. This causes a small blip in startup but it should be + // one time only whereas not flushing means we have to do this search + // on every startup. + Flush(); + + // Now start up with the found profile. + mDedicatedProfile = profile; + mIsFirstRun = false; + break; + } + } + } + } + + // If this is a first run then create a new profile. + if (mIsFirstRun) { + // If we're configured to always show the profile manager then don't create + // a new profile to use. + if (!mStartWithLast) { + return NS_ERROR_SHOW_PROFILE_MANAGER; + } + + bool skippedDefaultProfile = false; + + if (mUseDedicatedProfile) { + // This is the first run of a dedicated profile install. We have to decide + // whether to use the default profile used by non-dedicated-profile + // installs or to create a new profile. + + // Find what would have been the default profile for old installs. + nsCOMPtr<nsIToolkitProfile> profile = mNormalDefault; + if (mUseDevEditionProfile) { + profile = mDevEditionDefault; + } + + if (profile) { + nsCOMPtr<nsIFile> rootDir; + profile->GetRootDir(getter_AddRefs(rootDir)); + + nsCOMPtr<nsIFile> compat; + rootDir->Clone(getter_AddRefs(compat)); + compat->Append(COMPAT_FILE); + + bool exists; + rv = compat->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + // If the file is missing then either this is an empty profile (likely + // generated by bug 1518591) or it is from an ancient version. We'll opt + // to leave it for older versions in this case. + if (exists) { + bool result; + rv = MaybeMakeDefaultDedicatedProfile(profile, &result); + NS_ENSURE_SUCCESS(rv, rv); + if (result) { + mStartupReason = u"firstrun-claimed-default"_ns; + + mCurrent = profile; + rootDir.forget(aRootDir); + profile->GetLocalDir(aLocalDir); + profile.forget(aProfile); + return NS_OK; + } + + // We're going to create a new profile for this install even though + // another default exists. + skippedDefaultProfile = true; + } + } + } + + rv = CreateDefaultProfile(getter_AddRefs(mCurrent)); + if (NS_SUCCEEDED(rv)) { +#ifdef MOZ_CREATE_LEGACY_PROFILE + // If there is only one profile and it isn't meant to be the profile that + // older versions of Firefox use then we must create a default profile + // for older versions of Firefox to avoid the existing profile being + // auto-selected. + if ((mUseDedicatedProfile || mUseDevEditionProfile) && + mProfiles.getFirst() == mProfiles.getLast()) { + nsCOMPtr<nsIToolkitProfile> newProfile; + CreateProfile(nullptr, nsLiteralCString(DEFAULT_NAME), + getter_AddRefs(newProfile)); + SetNormalDefault(newProfile); + } +#endif + + rv = Flush(); + NS_ENSURE_SUCCESS(rv, rv); + + if (skippedDefaultProfile) { + mStartupReason = u"firstrun-skipped-default"_ns; + } else { + mStartupReason = u"firstrun-created-default"_ns; + } + + // Use the new profile. + mCurrent->GetRootDir(aRootDir); + mCurrent->GetLocalDir(aLocalDir); + NS_ADDREF(*aProfile = mCurrent); + + *aDidCreate = true; + return NS_OK; + } + } + + GetDefaultProfile(getter_AddRefs(mCurrent)); + + // None of the profiles was marked as default (generally only happens if the + // user modifies profiles.ini manually). Let the user choose. + if (!mCurrent) { + return NS_ERROR_SHOW_PROFILE_MANAGER; + } + + // Let the caller know that the profile was selected by default. + *aWasDefaultSelection = true; + mStartupReason = u"default"_ns; + + // Use the selected profile. + mCurrent->GetRootDir(aRootDir); + mCurrent->GetLocalDir(aLocalDir); + NS_ADDREF(*aProfile = mCurrent); + + return NS_OK; +} + +/** + * Creates a new profile for reset and mark it as the current profile. + */ +nsresult nsToolkitProfileService::CreateResetProfile( + nsIToolkitProfile** aNewProfile) { + nsAutoCString oldProfileName; + mCurrent->GetName(oldProfileName); + + nsCOMPtr<nsIToolkitProfile> newProfile; + // Make the new profile name the old profile (or "default-") + the time in + // seconds since epoch for uniqueness. + nsAutoCString newProfileName; + if (!oldProfileName.IsEmpty()) { + newProfileName.Assign(oldProfileName); + newProfileName.Append("-"); + } else { + newProfileName.AssignLiteral("default-"); + } + newProfileName.AppendPrintf("%" PRId64, PR_Now() / 1000); + nsresult rv = CreateProfile(nullptr, // choose a default dir for us + newProfileName, getter_AddRefs(newProfile)); + if (NS_FAILED(rv)) return rv; + + mCurrent = newProfile; + newProfile.forget(aNewProfile); + + // Don't flush the changes yet. That will happen once the migration + // successfully completes. + return NS_OK; +} + +/** + * This is responsible for deleting the old profile, copying its name to the + * current profile and if the old profile was default making the new profile + * default as well. + */ +nsresult nsToolkitProfileService::ApplyResetProfile( + nsIToolkitProfile* aOldProfile) { + // If the old profile would have been the default for old installs then mark + // the new profile as such. + if (mNormalDefault == aOldProfile) { + SetNormalDefault(mCurrent); + } + + if (mUseDedicatedProfile && mDedicatedProfile == aOldProfile) { + bool wasLocked = false; + nsCString val; + if (NS_SUCCEEDED( + mProfileDB.GetString(mInstallSection.get(), "Locked", val))) { + wasLocked = val.Equals("1"); + } + + SetDefaultProfile(mCurrent); + + // Make the locked state match if necessary. + if (!wasLocked) { + mProfileDB.DeleteString(mInstallSection.get(), "Locked"); + } + } + + nsCString name; + nsresult rv = aOldProfile->GetName(name); + NS_ENSURE_SUCCESS(rv, rv); + + // Don't remove the old profile's files until after we've successfully flushed + // the profile changes to disk. + rv = aOldProfile->Remove(false); + NS_ENSURE_SUCCESS(rv, rv); + + // Switching the name will make this the default for dev-edition if + // appropriate. + rv = mCurrent->SetName(name); + NS_ENSURE_SUCCESS(rv, rv); + + rv = Flush(); + NS_ENSURE_SUCCESS(rv, rv); + + // Now that the profile changes are flushed, try to remove the old profile's + // files. If we fail the worst that will happen is that an orphan directory is + // left. Let this run in the background while we start up. + RemoveProfileFiles(aOldProfile, true); + + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfileService::GetProfileByName(const nsACString& aName, + nsIToolkitProfile** aResult) { + for (RefPtr<nsToolkitProfile> profile : mProfiles) { + if (profile->mName.Equals(aName)) { + NS_ADDREF(*aResult = profile); + return NS_OK; + } + } + + return NS_ERROR_FAILURE; +} + +/** + * Finds a profile from the database that uses the given root and local + * directories. + */ +void nsToolkitProfileService::GetProfileByDir(nsIFile* aRootDir, + nsIFile* aLocalDir, + nsIToolkitProfile** aResult) { + for (RefPtr<nsToolkitProfile> profile : mProfiles) { + bool equal; + nsresult rv = profile->mRootDir->Equals(aRootDir, &equal); + if (NS_SUCCEEDED(rv) && equal) { + if (!aLocalDir) { + // If no local directory was given then we will just use the normal + // local directory for the profile. + profile.forget(aResult); + return; + } + + rv = profile->mLocalDir->Equals(aLocalDir, &equal); + if (NS_SUCCEEDED(rv) && equal) { + profile.forget(aResult); + return; + } + } + } +} + +nsresult NS_LockProfilePath(nsIFile* aPath, nsIFile* aTempPath, + nsIProfileUnlocker** aUnlocker, + nsIProfileLock** aResult) { + RefPtr<nsToolkitProfileLock> lock = new nsToolkitProfileLock(); + + nsresult rv = lock->Init(aPath, aTempPath, aUnlocker); + if (NS_FAILED(rv)) return rv; + + lock.forget(aResult); + return NS_OK; +} + +static void SaltProfileName(nsACString& aName) { + char salt[9]; + NS_MakeRandomString(salt, 8); + salt[8] = '.'; + + aName.Insert(salt, 0, 9); +} + +NS_IMETHODIMP +nsToolkitProfileService::CreateUniqueProfile(nsIFile* aRootDir, + const nsACString& aNamePrefix, + nsIToolkitProfile** aResult) { + nsCOMPtr<nsIToolkitProfile> profile; + nsresult rv = GetProfileByName(aNamePrefix, getter_AddRefs(profile)); + if (NS_FAILED(rv)) { + return CreateProfile(aRootDir, aNamePrefix, aResult); + } + + uint32_t suffix = 1; + while (true) { + nsPrintfCString name("%s-%d", PromiseFlatCString(aNamePrefix).get(), + suffix); + rv = GetProfileByName(name, getter_AddRefs(profile)); + if (NS_FAILED(rv)) { + return CreateProfile(aRootDir, name, aResult); + } + suffix++; + } +} + +NS_IMETHODIMP +nsToolkitProfileService::CreateProfile(nsIFile* aRootDir, + const nsACString& aName, + nsIToolkitProfile** aResult) { + nsresult rv = GetProfileByName(aName, aResult); + if (NS_SUCCEEDED(rv)) { + return rv; + } + + nsCOMPtr<nsIFile> rootDir(aRootDir); + + nsAutoCString dirName; + if (!rootDir) { + rv = gDirServiceProvider->GetUserProfilesRootDir(getter_AddRefs(rootDir)); + NS_ENSURE_SUCCESS(rv, rv); + + dirName = aName; + SaltProfileName(dirName); + + if (NS_IsNativeUTF8()) { + rootDir->AppendNative(dirName); + } else { + rootDir->Append(NS_ConvertUTF8toUTF16(dirName)); + } + } + + nsCOMPtr<nsIFile> localDir; + + bool isRelative; + rv = mAppData->Contains(rootDir, &isRelative); + if (NS_SUCCEEDED(rv) && isRelative) { + nsAutoCString path; + rv = rootDir->GetRelativeDescriptor(mAppData, path); + NS_ENSURE_SUCCESS(rv, rv); + + rv = NS_NewNativeLocalFile(""_ns, true, getter_AddRefs(localDir)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = localDir->SetRelativeDescriptor(mTempData, path); + } else { + localDir = rootDir; + } + + bool exists; + rv = rootDir->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (exists) { + rv = rootDir->IsDirectory(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (!exists) return NS_ERROR_FILE_NOT_DIRECTORY; + } else { + nsCOMPtr<nsIFile> profileDirParent; + nsAutoString profileDirName; + + rv = rootDir->GetParent(getter_AddRefs(profileDirParent)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = rootDir->GetLeafName(profileDirName); + NS_ENSURE_SUCCESS(rv, rv); + + // let's ensure that the profile directory exists. + rv = rootDir->Create(nsIFile::DIRECTORY_TYPE, 0700); + NS_ENSURE_SUCCESS(rv, rv); + rv = rootDir->SetPermissions(0700); +#ifndef ANDROID + // If the profile is on the sdcard, this will fail but its non-fatal + NS_ENSURE_SUCCESS(rv, rv); +#endif + } + + rv = localDir->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (!exists) { + rv = localDir->Create(nsIFile::DIRECTORY_TYPE, 0700); + NS_ENSURE_SUCCESS(rv, rv); + } + + // We created a new profile dir. Let's store a creation timestamp. + // Note that this code path does not apply if the profile dir was + // created prior to launching. + rv = CreateTimesInternal(rootDir); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIToolkitProfile> profile = + new nsToolkitProfile(aName, rootDir, localDir, false); + + if (aName.Equals(DEV_EDITION_NAME)) { + mDevEditionDefault = profile; + } + + profile.forget(aResult); + return NS_OK; +} + +/** + * Snaps (https://snapcraft.io/) use a different installation directory for + * every version of an application. Since dedicated profiles uses the + * installation directory to determine which profile to use this would lead + * snap users getting a new profile on every application update. + * + * However the only way to have multiple installation of a snap is to install + * a new snap instance. Different snap instances have different user data + * directories and so already will not share profiles, in fact one instance + * will not even be able to see the other instance's profiles since + * profiles.ini will be stored in different places. + * + * So we can just disable dedicated profile support in this case and revert + * back to the old method of just having a single default profile and still + * get essentially the same benefits as dedicated profiles provides. + */ +bool nsToolkitProfileService::IsSnapEnvironment() { +#ifdef MOZ_WIDGET_GTK + return widget::IsRunningUnderSnap(); +#else + return false; +#endif +} + +/** + * In some situations dedicated profile support does not work well. This + * includes a handful of linux distributions which always install different + * application versions to different locations, some application sandboxing + * systems as well as enterprise deployments. This environment variable provides + * a way to opt out of dedicated profiles for these cases. + * + * For Windows, we provide a policy to accomplish the same thing. + */ +bool nsToolkitProfileService::UseLegacyProfiles() { + bool legacyProfiles = !!PR_GetEnv("MOZ_LEGACY_PROFILES"); +#ifdef XP_WIN + legacyProfiles |= PolicyCheckBoolean(L"LegacyProfiles"); +#endif + return legacyProfiles; +} + +struct FindInstallsClosure { + nsINIParser* installData; + nsTArray<nsCString>* installs; +}; + +static bool FindInstalls(const char* aSection, void* aClosure) { + FindInstallsClosure* closure = static_cast<FindInstallsClosure*>(aClosure); + + // Check if the section starts with "Install" + if (strncmp(aSection, INSTALL_PREFIX, INSTALL_PREFIX_LENGTH) != 0) { + return true; + } + + nsCString install(aSection); + closure->installs->AppendElement(install); + + return true; +} + +nsTArray<nsCString> nsToolkitProfileService::GetKnownInstalls() { + nsTArray<nsCString> result; + FindInstallsClosure closure = {&mProfileDB, &result}; + + mProfileDB.GetSections(&FindInstalls, &closure); + + return result; +} + +nsresult nsToolkitProfileService::CreateTimesInternal(nsIFile* aProfileDir) { + nsresult rv = NS_ERROR_FAILURE; + nsCOMPtr<nsIFile> creationLog; + rv = aProfileDir->Clone(getter_AddRefs(creationLog)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = creationLog->AppendNative("times.json"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + bool exists = false; + creationLog->Exists(&exists); + if (exists) { + return NS_OK; + } + + rv = creationLog->Create(nsIFile::NORMAL_FILE_TYPE, 0700); + NS_ENSURE_SUCCESS(rv, rv); + + // We don't care about microsecond resolution. + int64_t msec = PR_Now() / PR_USEC_PER_MSEC; + + // Write it out. + PRFileDesc* writeFile; + rv = creationLog->OpenNSPRFileDesc(PR_WRONLY, 0700, &writeFile); + NS_ENSURE_SUCCESS(rv, rv); + + PR_fprintf(writeFile, "{\n\"created\": %lld,\n\"firstUse\": null\n}\n", msec); + PR_Close(writeFile); + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfileService::GetProfileCount(uint32_t* aResult) { + *aResult = 0; + for (nsToolkitProfile* profile : mProfiles) { + Unused << profile; + (*aResult)++; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfileService::Flush() { + if (GetIsListOutdated()) { + return NS_ERROR_DATABASE_CHANGED; + } + + nsresult rv; + + // If we aren't using dedicated profiles then nothing about the list of + // installs can have changed, so no need to update the backup. + if (mUseDedicatedProfile) { + // Export the installs to the backup. + nsTArray<nsCString> installs = GetKnownInstalls(); + + if (!installs.IsEmpty()) { + nsCString data; + nsCString buffer; + + for (uint32_t i = 0; i < installs.Length(); i++) { + nsTArray<UniquePtr<KeyValue>> strings = + GetSectionStrings(&mProfileDB, installs[i].get()); + if (strings.IsEmpty()) { + continue; + } + + // Strip "Install" from the start. + const nsDependentCSubstring& install = + Substring(installs[i], INSTALL_PREFIX_LENGTH); + data.AppendPrintf("[%s]\n", PromiseFlatCString(install).get()); + + for (uint32_t j = 0; j < strings.Length(); j++) { + data.AppendPrintf("%s=%s\n", strings[j]->key.get(), + strings[j]->value.get()); + } + + data.Append("\n"); + } + + FILE* writeFile; + rv = mInstallDBFile->OpenANSIFileDesc("w", &writeFile); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t length = data.Length(); + if (fwrite(data.get(), sizeof(char), length, writeFile) != length) { + fclose(writeFile); + return NS_ERROR_UNEXPECTED; + } + + fclose(writeFile); + } else { + rv = mInstallDBFile->Remove(false); + if (NS_FAILED(rv) && rv != NS_ERROR_FILE_NOT_FOUND) { + return rv; + } + } + } + + rv = mProfileDB.WriteToFile(mProfileDBFile); + NS_ENSURE_SUCCESS(rv, rv); + + rv = UpdateFileStats(mProfileDBFile, &mProfileDBExists, + &mProfileDBModifiedTime, &mProfileDBFileSize); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +already_AddRefed<nsToolkitProfileService> NS_GetToolkitProfileService() { + if (!nsToolkitProfileService::gService) { + nsToolkitProfileService::gService = new nsToolkitProfileService(); + nsresult rv = nsToolkitProfileService::gService->Init(); + if (NS_FAILED(rv)) { + NS_ERROR("nsToolkitProfileService::Init failed!"); + delete nsToolkitProfileService::gService; + return nullptr; + } + } + + return do_AddRef(nsToolkitProfileService::gService); +} + +nsresult XRE_GetFileFromPath(const char* aPath, nsIFile** aResult) { +#if defined(XP_MACOSX) + int32_t pathLen = strlen(aPath); + if (pathLen > MAXPATHLEN) return NS_ERROR_INVALID_ARG; + + CFURLRef fullPath = CFURLCreateFromFileSystemRepresentation( + nullptr, (const UInt8*)aPath, pathLen, true); + if (!fullPath) return NS_ERROR_FAILURE; + + nsCOMPtr<nsIFile> lf; + nsresult rv = NS_NewNativeLocalFile(""_ns, true, getter_AddRefs(lf)); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsILocalFileMac> lfMac = do_QueryInterface(lf, &rv); + if (NS_SUCCEEDED(rv)) { + rv = lfMac->InitWithCFURL(fullPath); + if (NS_SUCCEEDED(rv)) { + lf.forget(aResult); + } + } + } + CFRelease(fullPath); + return rv; + +#elif defined(XP_UNIX) + char fullPath[MAXPATHLEN]; + + if (!realpath(aPath, fullPath)) return NS_ERROR_FAILURE; + + return NS_NewNativeLocalFile(nsDependentCString(fullPath), true, aResult); +#elif defined(XP_WIN) + WCHAR fullPath[MAXPATHLEN]; + + if (!_wfullpath(fullPath, NS_ConvertUTF8toUTF16(aPath).get(), MAXPATHLEN)) + return NS_ERROR_FAILURE; + + return NS_NewLocalFile(nsDependentString(fullPath), true, aResult); + +#else +# error Platform-specific logic needed here. +#endif +} |