1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "QuotaCommon.h"
9 #include "mozIStorageConnection.h"
10 #include "mozIStorageStatement.h"
11 #include "mozilla/ErrorNames.h"
12 #include "mozilla/Logging.h"
13 #include "mozilla/Telemetry.h"
14 #include "mozilla/TelemetryComms.h"
15 #include "mozilla/TelemetryEventEnums.h"
16 #include "mozilla/TextUtils.h"
17 #include "nsIConsoleService.h"
19 #include "nsServiceManagerUtils.h"
20 #include "nsStringFlags.h"
21 #include "nsTStringRepr.h"
22 #include "nsUnicharUtils.h"
24 #include "nsXULAppAPI.h"
27 # include "mozilla/Atomics.h"
28 # include "mozilla/ipc/BackgroundParent.h"
29 # include "mozilla/StaticPrefs_dom.h"
30 # include "nsILocalFileWin.h"
33 namespace mozilla::dom::quota
{
35 using namespace mozilla::Telemetry
;
40 constexpr auto kDSStoreFileName
= u
".DS_Store"_ns
;
41 constexpr auto kDesktopFileName
= u
".desktop"_ns
;
42 constexpr auto kDesktopIniFileName
= u
"desktop.ini"_ns
;
43 constexpr auto kThumbsDbFileName
= u
"thumbs.db"_ns
;
47 Atomic
<int32_t> gUseDOSDevicePathSyntax(-1);
50 LazyLogModule
gLogger("QuotaManager");
52 void AnonymizeCString(nsACString
& aCString
, uint32_t aStart
) {
53 MOZ_ASSERT(!aCString
.IsEmpty());
54 MOZ_ASSERT(aStart
< aCString
.Length());
56 char* iter
= aCString
.BeginWriting() + aStart
;
57 char* end
= aCString
.EndWriting();
62 if (IsAsciiAlpha(c
)) {
64 } else if (IsAsciiDigit(c
)) {
74 const char kQuotaGenericDelimiter
= '|';
77 const nsLiteralCString kQuotaInternalError
= "internal"_ns
;
78 const nsLiteralCString kQuotaExternalError
= "external"_ns
;
81 LogModule
* GetQuotaManagerLogger() { return gLogger
; }
83 void AnonymizeCString(nsACString
& aCString
) {
84 if (aCString
.IsEmpty()) {
87 AnonymizeCString(aCString
, /* aStart */ 0);
90 void AnonymizeOriginString(nsACString
& aOriginString
) {
91 if (aOriginString
.IsEmpty()) {
95 int32_t start
= aOriginString
.FindChar(':');
100 AnonymizeCString(aOriginString
, start
);
104 void CacheUseDOSDevicePathSyntaxPrefValue() {
105 MOZ_ASSERT(XRE_IsParentProcess());
106 AssertIsOnBackgroundThread();
108 if (gUseDOSDevicePathSyntax
== -1) {
109 bool useDOSDevicePathSyntax
=
110 StaticPrefs::dom_quotaManager_useDOSDevicePathSyntax_DoNotUseDirectly();
111 gUseDOSDevicePathSyntax
= useDOSDevicePathSyntax
? 1 : 0;
116 Result
<nsCOMPtr
<nsIFile
>, nsresult
> QM_NewLocalFile(const nsAString
& aPath
) {
117 QM_TRY_UNWRAP(auto file
,
118 ToResultInvoke
<nsCOMPtr
<nsIFile
>>(NS_NewLocalFile
, aPath
,
119 /* aFollowLinks */ false),
120 QM_PROPAGATE
, [&aPath
](const nsresult rv
) {
121 QM_WARNING("Failed to construct a file for path (%s)",
122 NS_ConvertUTF16toUTF8(aPath
).get());
126 MOZ_ASSERT(gUseDOSDevicePathSyntax
!= -1);
128 if (gUseDOSDevicePathSyntax
) {
129 QM_TRY_INSPECT(const auto& winFile
,
130 ToResultGet
<nsCOMPtr
<nsILocalFileWin
>>(
131 MOZ_SELECT_OVERLOAD(do_QueryInterface
), file
));
134 winFile
->SetUseDOSDevicePathSyntax(true);
141 nsDependentCSubstring
GetLeafName(const nsACString
& aPath
) {
142 nsACString::const_iterator start
, end
;
143 aPath
.BeginReading(start
);
144 aPath
.EndReading(end
);
146 bool found
= RFindInReadable("/"_ns
, start
, end
);
151 aPath
.EndReading(end
);
153 return nsDependentCSubstring(start
.get(), end
.get());
156 Result
<nsCOMPtr
<nsIFile
>, nsresult
> CloneFileAndAppend(
157 nsIFile
& aDirectory
, const nsAString
& aPathElement
) {
158 QM_TRY_UNWRAP(auto resultFile
, MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr
<nsIFile
>,
161 QM_TRY(resultFile
->Append(aPathElement
));
166 Result
<nsIFileKind
, nsresult
> GetDirEntryKind(nsIFile
& aFile
) {
167 QM_TRY_RETURN(QM_OR_ELSE_WARN(
168 MOZ_TO_RESULT_INVOKE(aFile
, IsDirectory
).map([](const bool isDirectory
) {
169 return isDirectory
? nsIFileKind::ExistsAsDirectory
170 : nsIFileKind::ExistsAsFile
;
172 ([](const nsresult rv
) -> Result
<nsIFileKind
, nsresult
> {
173 if (rv
== NS_ERROR_FILE_NOT_FOUND
||
174 rv
== NS_ERROR_FILE_TARGET_DOES_NOT_EXIST
176 // We treat ERROR_FILE_CORRUPT as if the file did not exist at
178 || (NS_ERROR_GET_MODULE(rv
) == NS_ERROR_MODULE_WIN32
&&
179 NS_ERROR_GET_CODE(rv
) == ERROR_FILE_CORRUPT
)
182 return nsIFileKind::DoesNotExist
;
189 Result
<nsCOMPtr
<mozIStorageStatement
>, nsresult
> CreateStatement(
190 mozIStorageConnection
& aConnection
, const nsACString
& aStatementString
) {
191 QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr
<mozIStorageStatement
>,
192 aConnection
, CreateStatement
,
196 template <SingleStepResult ResultHandling
>
197 Result
<SingleStepSuccessType
<ResultHandling
>, nsresult
> ExecuteSingleStep(
198 nsCOMPtr
<mozIStorageStatement
>&& aStatement
) {
199 QM_TRY_INSPECT(const bool& hasResult
,
200 MOZ_TO_RESULT_INVOKE(aStatement
, ExecuteStep
));
202 if constexpr (ResultHandling
== SingleStepResult::AssertHasResult
) {
203 MOZ_ASSERT(hasResult
);
206 return WrapNotNullUnchecked(std::move(aStatement
));
208 return hasResult
? std::move(aStatement
) : nullptr;
212 template Result
<SingleStepSuccessType
<SingleStepResult::AssertHasResult
>,
214 ExecuteSingleStep
<SingleStepResult::AssertHasResult
>(
215 nsCOMPtr
<mozIStorageStatement
>&&);
217 template Result
<SingleStepSuccessType
<SingleStepResult::ReturnNullIfNoResult
>,
219 ExecuteSingleStep
<SingleStepResult::ReturnNullIfNoResult
>(
220 nsCOMPtr
<mozIStorageStatement
>&&);
222 template <SingleStepResult ResultHandling
>
223 Result
<SingleStepSuccessType
<ResultHandling
>, nsresult
>
224 CreateAndExecuteSingleStepStatement(mozIStorageConnection
& aConnection
,
225 const nsACString
& aStatementString
) {
226 QM_TRY_UNWRAP(auto stmt
, MOZ_TO_RESULT_INVOKE_TYPED(
227 nsCOMPtr
<mozIStorageStatement
>, aConnection
,
228 CreateStatement
, aStatementString
));
230 return ExecuteSingleStep
<ResultHandling
>(std::move(stmt
));
233 template Result
<SingleStepSuccessType
<SingleStepResult::AssertHasResult
>,
235 CreateAndExecuteSingleStepStatement
<SingleStepResult::AssertHasResult
>(
236 mozIStorageConnection
& aConnection
, const nsACString
& aStatementString
);
238 template Result
<SingleStepSuccessType
<SingleStepResult::ReturnNullIfNoResult
>,
240 CreateAndExecuteSingleStepStatement
<SingleStepResult::ReturnNullIfNoResult
>(
241 mozIStorageConnection
& aConnection
, const nsACString
& aStatementString
);
243 #ifdef QM_ENABLE_SCOPED_LOG_EXTRA_INFO
244 MOZ_THREAD_LOCAL(const nsACString
*) ScopedLogExtraInfo::sQueryValue
;
245 MOZ_THREAD_LOCAL(const nsACString
*) ScopedLogExtraInfo::sContextValue
;
248 auto ScopedLogExtraInfo::FindSlot(const char* aTag
) {
249 // XXX For now, don't use a real map but just allow the known tag values.
251 if (aTag
== kTagQuery
) {
255 if (aTag
== kTagContext
) {
256 return &sContextValue
;
259 MOZ_CRASH("Unknown tag!");
262 ScopedLogExtraInfo::~ScopedLogExtraInfo() {
264 MOZ_ASSERT(&mCurrentValue
== FindSlot(mTag
)->get(),
265 "Bad scoping of ScopedLogExtraInfo, must not be interleaved!");
267 FindSlot(mTag
)->set(mPreviousValue
);
271 ScopedLogExtraInfo::ScopedLogExtraInfo(ScopedLogExtraInfo
&& aOther
)
273 mPreviousValue(aOther
.mPreviousValue
),
274 mCurrentValue(std::move(aOther
.mCurrentValue
)) {
275 aOther
.mTag
= nullptr;
276 FindSlot(mTag
)->set(&mCurrentValue
);
279 /* static */ ScopedLogExtraInfo::ScopedLogExtraInfoMap
280 ScopedLogExtraInfo::GetExtraInfoMap() {
281 // This could be done in a cheaper way, but this is never called on a hot
282 // path, so we anticipate using a real map inside here to make use simpler for
285 ScopedLogExtraInfoMap map
;
286 if (XRE_IsParentProcess()) {
287 if (sQueryValue
.get()) {
288 map
.emplace(kTagQuery
, sQueryValue
.get());
291 if (sContextValue
.get()) {
292 map
.emplace(kTagContext
, sContextValue
.get());
298 /* static */ void ScopedLogExtraInfo::Initialize() {
299 MOZ_ALWAYS_TRUE(sQueryValue
.init());
300 MOZ_ALWAYS_TRUE(sContextValue
.init());
303 void ScopedLogExtraInfo::AddInfo() {
304 auto* slot
= FindSlot(mTag
);
306 mPreviousValue
= slot
->get();
308 slot
->set(&mCurrentValue
);
314 nsDependentCSubstring
GetSourceTreeBase() {
315 static constexpr auto thisFileRelativeSourceFileName
=
316 "/dom/quota/QuotaCommon.cpp"_ns
;
318 static constexpr auto path
= nsLiteralCString(__FILE__
);
320 MOZ_ASSERT(StringEndsWith(path
, thisFileRelativeSourceFileName
));
321 return Substring(path
, 0,
322 path
.Length() - thisFileRelativeSourceFileName
.Length());
325 nsDependentCSubstring
MakeRelativeSourceFileName(
326 const nsACString
& aSourceFile
) {
327 static constexpr auto error
= "ERROR"_ns
;
329 static const auto sourceTreeBase
= GetSourceTreeBase();
331 if (MOZ_LIKELY(StringBeginsWith(aSourceFile
, sourceTreeBase
))) {
332 return Substring(aSourceFile
, sourceTreeBase
.Length() + 1);
335 nsCString::const_iterator begin
, end
;
336 if (RFindInReadable("/"_ns
, aSourceFile
.BeginReading(begin
),
337 aSourceFile
.EndReading(end
))) {
338 // Use the basename as a fallback, to avoid exposing any user parts of the
341 return Substring(begin
, aSourceFile
.EndReading(end
));
344 return nsDependentCSubstring
{static_cast<mozilla::Span
<const char>>(
345 static_cast<const nsCString
&>(error
))};
348 } // namespace detail
350 void LogError(const nsACString
& aExpr
, const Maybe
<nsresult
> aRv
,
351 const nsACString
& aSourceFile
, const int32_t aSourceLine
,
352 const Severity aSeverity
) {
353 #if defined(EARLY_BETA_OR_EARLIER) || defined(DEBUG)
354 nsAutoCString extraInfosString
;
356 nsAutoCString rvName
;
358 if (NS_ERROR_GET_MODULE(*aRv
) == NS_ERROR_MODULE_WIN32
) {
359 // XXX We could also try to get the Win32 error name here.
360 rvName
= nsPrintfCString("WIN32(0x%" PRIX16
")", NS_ERROR_GET_CODE(*aRv
));
362 rvName
= mozilla::GetStaticErrorName(*aRv
);
364 extraInfosString
.AppendPrintf(
366 "result 0x%" PRIX32
"%s%s%s",
367 static_cast<uint32_t>(*aRv
), !rvName
.IsEmpty() ? " (" : "",
368 !rvName
.IsEmpty() ? rvName
.get() : "", !rvName
.IsEmpty() ? ")" : "");
371 const auto relativeSourceFile
=
372 detail::MakeRelativeSourceFileName(aSourceFile
);
374 const auto severityString
= [&aSeverity
]() -> nsLiteralCString
{
376 case Severity::Error
:
378 case Severity::Warning
:
383 MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("Bad severity value!");
387 #ifdef QM_ENABLE_SCOPED_LOG_EXTRA_INFO
388 const auto& extraInfos
= ScopedLogExtraInfo::GetExtraInfoMap();
389 for (const auto& item
: extraInfos
) {
390 extraInfosString
.Append(", "_ns
+ nsDependentCString(item
.first
) + "="_ns
+
398 nsAutoCString("QM_TRY failure ("_ns
+ severityString
+ ")"_ns
).get(),
399 (extraInfosString
.IsEmpty() ? nsPromiseFlatCString(aExpr
)
400 : static_cast<const nsCString
&>(nsAutoCString(
401 aExpr
+ extraInfosString
)))
403 nsPromiseFlatCString(relativeSourceFile
).get(), aSourceLine
);
406 #if defined(EARLY_BETA_OR_EARLIER) || defined(DEBUG)
407 nsCOMPtr
<nsIConsoleService
> console
=
408 do_GetService(NS_CONSOLESERVICE_CONTRACTID
);
410 NS_ConvertUTF8toUTF16
message("QM_TRY failure ("_ns
+ severityString
+
411 ")"_ns
+ ": '"_ns
+ aExpr
+ "' at "_ns
+
412 relativeSourceFile
+ ":"_ns
+
413 IntToCString(aSourceLine
) + extraInfosString
);
415 // The concatenation above results in a message like:
416 // QM_TRY failure: 'EXPR' failed with result NS_ERROR_FAILURE at
417 // dom/quota/Foo.cpp:12345
419 console
->LogStringMessage(message
.get());
422 # ifdef QM_ENABLE_SCOPED_LOG_EXTRA_INFO
423 if (const auto contextIt
= extraInfos
.find(ScopedLogExtraInfo::kTagContext
);
424 contextIt
!= extraInfos
.cend()) {
425 // For now, we don't include aExpr in the telemetry event. It might help to
426 // match locations across versions, but they might be large.
427 auto extra
= Some([&] {
428 auto res
= CopyableTArray
<EventExtraEntry
>{};
430 // TODO We could still fill the module field, based on the source
431 // directory, but we probably don't need to.
432 // res.AppendElement(EventExtraEntry{"module"_ns, aModule});
434 EventExtraEntry
{"source_file"_ns
, nsCString(relativeSourceFile
)});
436 EventExtraEntry
{"source_line"_ns
, IntToCString(aSourceLine
)});
437 res
.AppendElement(EventExtraEntry
{
438 "context"_ns
, nsPromiseFlatCString
{*contextIt
->second
}});
439 res
.AppendElement(EventExtraEntry
{"severity"_ns
, severityString
});
441 if (!rvName
.IsEmpty()) {
442 res
.AppendElement(EventExtraEntry
{"result"_ns
, nsCString
{rvName
}});
445 // The sequence number is currently per-process, and we don't record the
446 // thread id. This is ok as long as we only record during storage
447 // initialization, and that happens (mostly) from a single thread. It's
448 // safe even if errors from multiple threads are interleaved, but the data
449 // will be hard to analyze then. In that case, we should record a pair of
450 // thread id and thread-local sequence number.
451 static Atomic
<int32_t> sSequenceNumber
{0};
454 EventExtraEntry
{"seq"_ns
, IntToCString(++sSequenceNumber
)});
459 Telemetry::RecordEvent(Telemetry::EventID::DomQuotaTry_Error_Step
,
467 Result
<bool, nsresult
> WarnIfFileIsUnknown(nsIFile
& aFile
,
468 const char* aSourceFile
,
469 const int32_t aSourceLine
) {
471 nsresult rv
= aFile
.GetLeafName(leafName
);
472 if (NS_WARN_IF(NS_FAILED(rv
))) {
477 rv
= aFile
.IsDirectory(&isDirectory
);
478 if (NS_WARN_IF(NS_FAILED(rv
))) {
483 // Don't warn about OS metadata files. These files are only used in
484 // different platforms, but the profile can be shared across different
485 // operating systems, so we check it on all platforms.
486 if (leafName
.Equals(kDSStoreFileName
) ||
487 leafName
.Equals(kDesktopFileName
) ||
488 leafName
.Equals(kDesktopIniFileName
,
489 nsCaseInsensitiveStringComparator
) ||
490 leafName
.Equals(kThumbsDbFileName
, nsCaseInsensitiveStringComparator
)) {
494 // Don't warn about files starting with ".".
495 if (leafName
.First() == char16_t('.')) {
502 nsPrintfCString("Something (%s) in the directory that doesn't belong!",
503 NS_ConvertUTF16toUTF8(leafName
).get())
505 nullptr, aSourceFile
, aSourceLine
);
511 } // namespace mozilla::dom::quota