/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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 "QuotaCommon.h" #include "mozIStorageConnection.h" #include "mozIStorageStatement.h" #include "mozilla/ErrorNames.h" #include "mozilla/Logging.h" #include "mozilla/Telemetry.h" #include "mozilla/TelemetryComms.h" #include "mozilla/TelemetryEventEnums.h" #include "mozilla/TextUtils.h" #include "nsIConsoleService.h" #include "nsIFile.h" #include "nsServiceManagerUtils.h" #include "nsStringFlags.h" #include "nsTStringRepr.h" #include "nsUnicharUtils.h" #include "nsXPCOM.h" #include "nsXULAppAPI.h" #ifdef XP_WIN # include "mozilla/Atomics.h" # include "mozilla/ipc/BackgroundParent.h" # include "mozilla/StaticPrefs_dom.h" # include "nsILocalFileWin.h" #endif namespace mozilla::dom::quota { using namespace mozilla::Telemetry; namespace { #ifdef DEBUG constexpr auto kDSStoreFileName = u".DS_Store"_ns; constexpr auto kDesktopFileName = u".desktop"_ns; constexpr auto kDesktopIniFileName = u"desktop.ini"_ns; constexpr auto kThumbsDbFileName = u"thumbs.db"_ns; #endif #ifdef XP_WIN Atomic gUseDOSDevicePathSyntax(-1); #endif LazyLogModule gLogger("QuotaManager"); void AnonymizeCString(nsACString& aCString, uint32_t aStart) { MOZ_ASSERT(!aCString.IsEmpty()); MOZ_ASSERT(aStart < aCString.Length()); char* iter = aCString.BeginWriting() + aStart; char* end = aCString.EndWriting(); while (iter != end) { char c = *iter; if (IsAsciiAlpha(c)) { *iter = 'a'; } else if (IsAsciiDigit(c)) { *iter = 'D'; } ++iter; } } } // namespace const char kQuotaGenericDelimiter = '|'; #ifdef NIGHTLY_BUILD const nsLiteralCString kQuotaInternalError = "internal"_ns; const nsLiteralCString kQuotaExternalError = "external"_ns; #endif LogModule* GetQuotaManagerLogger() { return gLogger; } void AnonymizeCString(nsACString& aCString) { if (aCString.IsEmpty()) { return; } AnonymizeCString(aCString, /* aStart */ 0); } void AnonymizeOriginString(nsACString& aOriginString) { if (aOriginString.IsEmpty()) { return; } int32_t start = aOriginString.FindChar(':'); if (start < 0) { start = 0; } AnonymizeCString(aOriginString, start); } #ifdef XP_WIN void CacheUseDOSDevicePathSyntaxPrefValue() { MOZ_ASSERT(XRE_IsParentProcess()); AssertIsOnBackgroundThread(); if (gUseDOSDevicePathSyntax == -1) { bool useDOSDevicePathSyntax = StaticPrefs::dom_quotaManager_useDOSDevicePathSyntax_DoNotUseDirectly(); gUseDOSDevicePathSyntax = useDOSDevicePathSyntax ? 1 : 0; } } #endif Result, nsresult> QM_NewLocalFile(const nsAString& aPath) { QM_TRY_UNWRAP(auto file, ToResultInvoke>(NS_NewLocalFile, aPath, /* aFollowLinks */ false), QM_PROPAGATE, [&aPath](const nsresult rv) { QM_WARNING("Failed to construct a file for path (%s)", NS_ConvertUTF16toUTF8(aPath).get()); }); #ifdef XP_WIN MOZ_ASSERT(gUseDOSDevicePathSyntax != -1); if (gUseDOSDevicePathSyntax) { QM_TRY_INSPECT(const auto& winFile, ToResultGet>( MOZ_SELECT_OVERLOAD(do_QueryInterface), file)); MOZ_ASSERT(winFile); winFile->SetUseDOSDevicePathSyntax(true); } #endif return file; } nsDependentCSubstring GetLeafName(const nsACString& aPath) { nsACString::const_iterator start, end; aPath.BeginReading(start); aPath.EndReading(end); bool found = RFindInReadable("/"_ns, start, end); if (found) { start = end; } aPath.EndReading(end); return nsDependentCSubstring(start.get(), end.get()); } Result, nsresult> CloneFileAndAppend( nsIFile& aDirectory, const nsAString& aPathElement) { QM_TRY_UNWRAP(auto resultFile, MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr, aDirectory, Clone)); QM_TRY(resultFile->Append(aPathElement)); return resultFile; } Result, nsresult> CreateStatement( mozIStorageConnection& aConnection, const nsACString& aStatementString) { QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr, aConnection, CreateStatement, aStatementString)); } template Result, nsresult> ExecuteSingleStep( nsCOMPtr&& aStatement) { QM_TRY_INSPECT(const bool& hasResult, MOZ_TO_RESULT_INVOKE(aStatement, ExecuteStep)); if constexpr (ResultHandling == SingleStepResult::AssertHasResult) { MOZ_ASSERT(hasResult); (void)hasResult; return WrapNotNullUnchecked(std::move(aStatement)); } else { return hasResult ? std::move(aStatement) : nullptr; } } template Result, nsresult> ExecuteSingleStep( nsCOMPtr&&); template Result, nsresult> ExecuteSingleStep( nsCOMPtr&&); template Result, nsresult> CreateAndExecuteSingleStepStatement(mozIStorageConnection& aConnection, const nsACString& aStatementString) { QM_TRY_UNWRAP(auto stmt, MOZ_TO_RESULT_INVOKE_TYPED( nsCOMPtr, aConnection, CreateStatement, aStatementString)); return ExecuteSingleStep(std::move(stmt)); } template Result, nsresult> CreateAndExecuteSingleStepStatement( mozIStorageConnection& aConnection, const nsACString& aStatementString); template Result, nsresult> CreateAndExecuteSingleStepStatement( mozIStorageConnection& aConnection, const nsACString& aStatementString); #ifdef QM_ENABLE_SCOPED_LOG_EXTRA_INFO MOZ_THREAD_LOCAL(const nsACString*) ScopedLogExtraInfo::sQueryValue; MOZ_THREAD_LOCAL(const nsACString*) ScopedLogExtraInfo::sContextValue; /* static */ auto ScopedLogExtraInfo::FindSlot(const char* aTag) { // XXX For now, don't use a real map but just allow the known tag values. if (aTag == kTagQuery) { return &sQueryValue; } if (aTag == kTagContext) { return &sContextValue; } MOZ_CRASH("Unknown tag!"); } ScopedLogExtraInfo::~ScopedLogExtraInfo() { if (mTag) { MOZ_ASSERT(&mCurrentValue == FindSlot(mTag)->get(), "Bad scoping of ScopedLogExtraInfo, must not be interleaved!"); FindSlot(mTag)->set(mPreviousValue); } } ScopedLogExtraInfo::ScopedLogExtraInfo(ScopedLogExtraInfo&& aOther) : mTag(aOther.mTag), mPreviousValue(aOther.mPreviousValue), mCurrentValue(std::move(aOther.mCurrentValue)) { aOther.mTag = nullptr; FindSlot(mTag)->set(&mCurrentValue); } /* static */ ScopedLogExtraInfo::ScopedLogExtraInfoMap ScopedLogExtraInfo::GetExtraInfoMap() { // This could be done in a cheaper way, but this is never called on a hot // path, so we anticipate using a real map inside here to make use simpler for // the caller(s). ScopedLogExtraInfoMap map; if (XRE_IsParentProcess()) { if (sQueryValue.get()) { map.emplace(kTagQuery, sQueryValue.get()); } if (sContextValue.get()) { map.emplace(kTagContext, sContextValue.get()); } } return map; } /* static */ void ScopedLogExtraInfo::Initialize() { MOZ_ALWAYS_TRUE(sQueryValue.init()); MOZ_ALWAYS_TRUE(sContextValue.init()); } void ScopedLogExtraInfo::AddInfo() { auto* slot = FindSlot(mTag); MOZ_ASSERT(slot); mPreviousValue = slot->get(); slot->set(&mCurrentValue); } #endif void LogError(const nsLiteralCString& aModule, const nsACString& aExpr, const nsACString& aSourceFile, int32_t aSourceLine, Maybe aRv) { nsAutoCString extraInfosString; const char* rvName = nullptr; if (aRv) { rvName = mozilla::GetStaticErrorName(*aRv); extraInfosString.AppendPrintf( "failed with " "result 0x%" PRIX32 "%s%s%s", static_cast(*aRv), rvName ? " (" : "", rvName ? rvName : "", rvName ? ")" : ""); } #ifdef QM_ENABLE_SCOPED_LOG_EXTRA_INFO const auto& extraInfos = ScopedLogExtraInfo::GetExtraInfoMap(); for (const auto& item : extraInfos) { extraInfosString.Append(", "_ns + nsDependentCString(item.first) + "="_ns + *item.second); } #endif #ifdef DEBUG NS_DebugBreak(NS_DEBUG_WARNING, nsAutoCString(aModule + " failure"_ns).get(), (extraInfosString.IsEmpty() ? nsPromiseFlatCString(aExpr) : static_cast( nsAutoCString(aExpr + extraInfosString))) .get(), nsPromiseFlatCString(aSourceFile).get(), aSourceLine); #endif #if defined(EARLY_BETA_OR_EARLIER) || defined(DEBUG) nsCOMPtr console = do_GetService(NS_CONSOLESERVICE_CONTRACTID); if (console) { NS_ConvertUTF8toUTF16 message(aModule + " failure: '"_ns + aExpr + "', file "_ns + GetLeafName(aSourceFile) + ", line "_ns + IntToCString(aSourceLine) + extraInfosString); // The concatenation above results in a message like: // QuotaManager failure: 'EXP', file XYZ, line N) console->LogStringMessage(message.get()); } # ifdef QM_ENABLE_SCOPED_LOG_EXTRA_INFO if (const auto contextIt = extraInfos.find(ScopedLogExtraInfo::kTagContext); contextIt != extraInfos.cend()) { // For now, we don't include aExpr in the telemetry event. It might help to // match locations across versions, but they might be large. auto extra = Some([&] { auto res = CopyableTArray{}; res.SetCapacity(5); res.AppendElement(EventExtraEntry{"module"_ns, aModule}); res.AppendElement(EventExtraEntry{"source_file"_ns, nsCString(GetLeafName(aSourceFile))}); res.AppendElement( EventExtraEntry{"source_line"_ns, IntToCString(aSourceLine)}); res.AppendElement(EventExtraEntry{ "context"_ns, nsPromiseFlatCString{*contextIt->second}}); if (rvName) { res.AppendElement(EventExtraEntry{"result"_ns, nsCString{rvName}}); } return res; }()); Telemetry::RecordEvent(Telemetry::EventID::DomQuotaTry_Error_Step, Nothing(), extra); } # endif #endif } #ifdef DEBUG Result WarnIfFileIsUnknown(nsIFile& aFile, const char* aSourceFile, const int32_t aSourceLine) { nsString leafName; nsresult rv = aFile.GetLeafName(leafName); if (NS_WARN_IF(NS_FAILED(rv))) { return Err(rv); } bool isDirectory; rv = aFile.IsDirectory(&isDirectory); if (NS_WARN_IF(NS_FAILED(rv))) { return Err(rv); } if (!isDirectory) { // Don't warn about OS metadata files. These files are only used in // different platforms, but the profile can be shared across different // operating systems, so we check it on all platforms. if (leafName.Equals(kDSStoreFileName) || leafName.Equals(kDesktopFileName) || leafName.Equals(kDesktopIniFileName, nsCaseInsensitiveStringComparator) || leafName.Equals(kThumbsDbFileName, nsCaseInsensitiveStringComparator)) { return false; } // Don't warn about files starting with ".". if (leafName.First() == char16_t('.')) { return false; } } NS_DebugBreak( NS_DEBUG_WARNING, nsPrintfCString("Something (%s) in the directory that doesn't belong!", NS_ConvertUTF16toUTF8(leafName).get()) .get(), nullptr, aSourceFile, aSourceLine); return true; } #endif } // namespace mozilla::dom::quota