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
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "FileUtilsImpl.h"
8 
9 #include "DBSchema.h"
10 #include "mozilla/dom/InternalResponse.h"
11 #include "mozilla/dom/quota/FileStreams.h"
12 #include "mozilla/dom/quota/QuotaManager.h"
13 #include "mozilla/dom/quota/QuotaObject.h"
14 #include "mozilla/ScopeExit.h"
15 #include "mozilla/SnappyCompressOutputStream.h"
16 #include "mozilla/Unused.h"
17 #include "nsIObjectInputStream.h"
18 #include "nsIObjectOutputStream.h"
19 #include "nsIFile.h"
20 #include "nsIUUIDGenerator.h"
21 #include "nsNetCID.h"
22 #include "nsNetUtil.h"
23 #include "nsServiceManagerUtils.h"
24 #include "nsString.h"
25 #include "nsThreadUtils.h"
26 
27 namespace mozilla::dom::cache {
28 
29 using mozilla::dom::quota::Client;
30 using mozilla::dom::quota::CloneFileAndAppend;
31 using mozilla::dom::quota::FileInputStream;
32 using mozilla::dom::quota::FileOutputStream;
33 using mozilla::dom::quota::GetDirEntryKind;
34 using mozilla::dom::quota::nsIFileKind;
35 using mozilla::dom::quota::PERSISTENCE_TYPE_DEFAULT;
36 using mozilla::dom::quota::QuotaManager;
37 using mozilla::dom::quota::QuotaObject;
38 
39 namespace {
40 
41 // Const variable for generate padding size.
42 // XXX This will be tweaked to something more meaningful in Bug 1383656.
43 const int64_t kRoundUpNumber = 20480;
44 
45 enum BodyFileType { BODY_FILE_FINAL, BODY_FILE_TMP };
46 
47 Result<NotNull<nsCOMPtr<nsIFile>>, nsresult> BodyIdToFile(nsIFile& aBaseDir,
48                                                           const nsID& aId,
49                                                           BodyFileType aType);
50 
51 int64_t RoundUp(int64_t aX, int64_t aY);
52 
53 // The alogrithm for generating padding refers to the mitigation approach in
54 // https://github.com/whatwg/storage/issues/31.
55 // First, generate a random number between 0 and 100kB.
56 // Next, round up the sum of random number and response size to the nearest
57 // 20kB.
58 // Finally, the virtual padding size will be the result minus the response size.
59 int64_t BodyGeneratePadding(int64_t aBodyFileSize, uint32_t aPaddingInfo);
60 
61 nsresult DirectoryPaddingWrite(nsIFile& aBaseDir,
62                                DirPaddingFile aPaddingFileType,
63                                int64_t aPaddingSize);
64 
65 const auto kMorgueDirectory = u"morgue"_ns;
66 
IsFileNotFoundError(const nsresult aRv)67 bool IsFileNotFoundError(const nsresult aRv) {
68   return aRv == NS_ERROR_FILE_NOT_FOUND ||
69          aRv == NS_ERROR_FILE_TARGET_DOES_NOT_EXIST;
70 }
71 
BodyGetCacheDir(nsIFile & aBaseDir,const nsID & aId)72 Result<NotNull<nsCOMPtr<nsIFile>>, nsresult> BodyGetCacheDir(nsIFile& aBaseDir,
73                                                              const nsID& aId) {
74   QM_TRY_UNWRAP(auto cacheDir, CloneFileAndAppend(aBaseDir, kMorgueDirectory));
75 
76   // Some file systems have poor performance when there are too many files
77   // in a single directory.  Mitigate this issue by spreading the body
78   // files out into sub-directories.  We use the last byte of the ID for
79   // the name of the sub-directory.
80   QM_TRY(cacheDir->Append(IntToString(aId.m3[7])));
81 
82   // Callers call this function without checking if the directory already
83   // exists (idempotent usage). QM_OR_ELSE_WARN_IF is not used here since we
84   // just want to log NS_ERROR_FILE_ALREADY_EXISTS result and not spam the
85   // reports.
86   QM_TRY(QM_OR_ELSE_LOG_VERBOSE_IF(
87       // Expression.
88       ToResult(cacheDir->Create(nsIFile::DIRECTORY_TYPE, 0755)),
89       // Predicate.
90       IsSpecificError<NS_ERROR_FILE_ALREADY_EXISTS>,
91       // Fallback.
92       ErrToDefaultOk<>));
93 
94   return WrapNotNullUnchecked(std::move(cacheDir));
95 }
96 
97 }  // namespace
98 
BodyCreateDir(nsIFile & aBaseDir)99 nsresult BodyCreateDir(nsIFile& aBaseDir) {
100   QM_TRY_INSPECT(const auto& bodyDir,
101                  CloneFileAndAppend(aBaseDir, kMorgueDirectory));
102 
103   // Callers call this function without checking if the directory already
104   // exists (idempotent usage). QM_OR_ELSE_WARN_IF is not used here since we
105   // just want to log NS_ERROR_FILE_ALREADY_EXISTS result and not spam the
106   // reports.
107   QM_TRY(QM_OR_ELSE_LOG_VERBOSE_IF(
108       // Expression.
109       ToResult(bodyDir->Create(nsIFile::DIRECTORY_TYPE, 0755)),
110       // Predicate.
111       IsSpecificError<NS_ERROR_FILE_ALREADY_EXISTS>,
112       // Fallback.
113       ErrToDefaultOk<>));
114 
115   return NS_OK;
116 }
117 
BodyDeleteDir(const QuotaInfo & aQuotaInfo,nsIFile & aBaseDir)118 nsresult BodyDeleteDir(const QuotaInfo& aQuotaInfo, nsIFile& aBaseDir) {
119   QM_TRY_INSPECT(const auto& bodyDir,
120                  CloneFileAndAppend(aBaseDir, kMorgueDirectory));
121 
122   QM_TRY(RemoveNsIFileRecursively(aQuotaInfo, *bodyDir));
123 
124   return NS_OK;
125 }
126 
BodyStartWriteStream(const QuotaInfo & aQuotaInfo,nsIFile & aBaseDir,nsIInputStream & aSource,void * aClosure,nsAsyncCopyCallbackFun aCallback)127 Result<std::pair<nsID, nsCOMPtr<nsISupports>>, nsresult> BodyStartWriteStream(
128     const QuotaInfo& aQuotaInfo, nsIFile& aBaseDir, nsIInputStream& aSource,
129     void* aClosure, nsAsyncCopyCallbackFun aCallback) {
130   MOZ_DIAGNOSTIC_ASSERT(aClosure);
131   MOZ_DIAGNOSTIC_ASSERT(aCallback);
132 
133   QM_TRY_INSPECT(const auto& idGen, ToResultGet<nsCOMPtr<nsIUUIDGenerator>>(
134                                         MOZ_SELECT_OVERLOAD(do_GetService),
135                                         "@mozilla.org/uuid-generator;1"));
136 
137   nsID id;
138   QM_TRY(idGen->GenerateUUIDInPlace(&id));
139 
140   QM_TRY_INSPECT(const auto& finalFile,
141                  BodyIdToFile(aBaseDir, id, BODY_FILE_FINAL));
142 
143   {
144     QM_TRY_INSPECT(const bool& exists,
145                    MOZ_TO_RESULT_INVOKE(*finalFile, Exists));
146 
147     QM_TRY(OkIf(!exists), Err(NS_ERROR_FILE_ALREADY_EXISTS));
148   }
149 
150   QM_TRY_INSPECT(const auto& tmpFile,
151                  BodyIdToFile(aBaseDir, id, BODY_FILE_TMP));
152 
153   QM_TRY_INSPECT(const auto& fileStream,
154                  CreateFileOutputStream(PERSISTENCE_TYPE_DEFAULT, aQuotaInfo,
155                                         Client::DOMCACHE, tmpFile.get()));
156 
157   const auto compressed =
158       MakeRefPtr<SnappyCompressOutputStream>(fileStream.get());
159 
160   const nsCOMPtr<nsIEventTarget> target =
161       do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
162 
163   nsCOMPtr<nsISupports> copyContext;
164   QM_TRY(NS_AsyncCopy(&aSource, compressed, target,
165                       NS_ASYNCCOPY_VIA_WRITESEGMENTS, compressed->BlockSize(),
166                       aCallback, aClosure, true,
167                       true,  // close streams
168                       getter_AddRefs(copyContext)));
169 
170   return std::make_pair(id, std::move(copyContext));
171 }
172 
BodyCancelWrite(nsISupports & aCopyContext)173 void BodyCancelWrite(nsISupports& aCopyContext) {
174   QM_WARNONLY_TRY(NS_CancelAsyncCopy(&aCopyContext, NS_ERROR_ABORT));
175 
176   // TODO The partially written file must be cleaned up after the async copy
177   // makes its callback.
178 }
179 
BodyFinalizeWrite(nsIFile & aBaseDir,const nsID & aId)180 nsresult BodyFinalizeWrite(nsIFile& aBaseDir, const nsID& aId) {
181   QM_TRY_INSPECT(const auto& tmpFile,
182                  BodyIdToFile(aBaseDir, aId, BODY_FILE_TMP));
183 
184   QM_TRY_INSPECT(const auto& finalFile,
185                  BodyIdToFile(aBaseDir, aId, BODY_FILE_FINAL));
186 
187   nsAutoString finalFileName;
188   QM_TRY(finalFile->GetLeafName(finalFileName));
189 
190   // It's fine to not notify the QuotaManager that the path has been changed,
191   // because its path will be updated and its size will be recalculated when
192   // opening file next time.
193   QM_TRY(tmpFile->RenameTo(nullptr, finalFileName));
194 
195   return NS_OK;
196 }
197 
BodyOpen(const QuotaInfo & aQuotaInfo,nsIFile & aBaseDir,const nsID & aId)198 Result<NotNull<nsCOMPtr<nsIInputStream>>, nsresult> BodyOpen(
199     const QuotaInfo& aQuotaInfo, nsIFile& aBaseDir, const nsID& aId) {
200   QM_TRY_INSPECT(const auto& finalFile,
201                  BodyIdToFile(aBaseDir, aId, BODY_FILE_FINAL));
202 
203   QM_TRY_RETURN(CreateFileInputStream(PERSISTENCE_TYPE_DEFAULT, aQuotaInfo,
204                                       Client::DOMCACHE, finalFile.get())
205                     .map([](NotNull<RefPtr<FileInputStream>>&& stream) {
206                       return WrapNotNullUnchecked(
207                           nsCOMPtr<nsIInputStream>{stream.get()});
208                     }));
209 }
210 
BodyMaybeUpdatePaddingSize(const QuotaInfo & aQuotaInfo,nsIFile & aBaseDir,const nsID & aId,const uint32_t aPaddingInfo,int64_t * aPaddingSizeInOut)211 nsresult BodyMaybeUpdatePaddingSize(const QuotaInfo& aQuotaInfo,
212                                     nsIFile& aBaseDir, const nsID& aId,
213                                     const uint32_t aPaddingInfo,
214                                     int64_t* aPaddingSizeInOut) {
215   MOZ_DIAGNOSTIC_ASSERT(aPaddingSizeInOut);
216 
217   QM_TRY_INSPECT(const auto& bodyFile,
218                  BodyIdToFile(aBaseDir, aId, BODY_FILE_TMP));
219 
220   QuotaManager* quotaManager = QuotaManager::Get();
221   MOZ_DIAGNOSTIC_ASSERT(quotaManager);
222 
223   int64_t fileSize = 0;
224   RefPtr<QuotaObject> quotaObject = quotaManager->GetQuotaObject(
225       PERSISTENCE_TYPE_DEFAULT, aQuotaInfo, Client::DOMCACHE, bodyFile.get(),
226       -1, &fileSize);
227   MOZ_DIAGNOSTIC_ASSERT(quotaObject);
228   MOZ_DIAGNOSTIC_ASSERT(fileSize >= 0);
229   // XXXtt: bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1422815
230   if (!quotaObject) {
231     return NS_ERROR_UNEXPECTED;
232   }
233 
234   if (*aPaddingSizeInOut == InternalResponse::UNKNOWN_PADDING_SIZE) {
235     *aPaddingSizeInOut = BodyGeneratePadding(fileSize, aPaddingInfo);
236   }
237 
238   MOZ_DIAGNOSTIC_ASSERT(*aPaddingSizeInOut >= 0);
239 
240   if (!quotaObject->IncreaseSize(*aPaddingSizeInOut)) {
241     return NS_ERROR_FILE_NO_DEVICE_SPACE;
242   }
243 
244   return NS_OK;
245 }
246 
BodyDeleteFiles(const QuotaInfo & aQuotaInfo,nsIFile & aBaseDir,const nsTArray<nsID> & aIdList)247 nsresult BodyDeleteFiles(const QuotaInfo& aQuotaInfo, nsIFile& aBaseDir,
248                          const nsTArray<nsID>& aIdList) {
249   for (const auto id : aIdList) {
250     QM_TRY_INSPECT(const auto& bodyDir, BodyGetCacheDir(aBaseDir, id));
251 
252     const auto removeFileForId =
253         [&aQuotaInfo, &id](
254             nsIFile& bodyFile,
255             const nsACString& leafName) -> Result<bool, nsresult> {
256       nsID fileId;
257       QM_TRY(OkIf(fileId.Parse(leafName.BeginReading())), true,
258              ([&aQuotaInfo, &bodyFile](const auto) {
259                DebugOnly<nsresult> result =
260                    RemoveNsIFile(aQuotaInfo, bodyFile, /* aTrackQuota */ false);
261                MOZ_ASSERT(NS_SUCCEEDED(result));
262              }));
263 
264       if (id.Equals(fileId)) {
265         DebugOnly<nsresult> result = RemoveNsIFile(aQuotaInfo, bodyFile);
266         MOZ_ASSERT(NS_SUCCEEDED(result));
267         return true;
268       }
269 
270       return false;
271     };
272     QM_TRY(BodyTraverseFiles(aQuotaInfo, *bodyDir, removeFileForId,
273                              /* aCanRemoveFiles */ false,
274                              /* aTrackQuota */ true));
275   }
276 
277   return NS_OK;
278 }
279 
280 namespace {
281 
BodyIdToFile(nsIFile & aBaseDir,const nsID & aId,const BodyFileType aType)282 Result<NotNull<nsCOMPtr<nsIFile>>, nsresult> BodyIdToFile(
283     nsIFile& aBaseDir, const nsID& aId, const BodyFileType aType) {
284   QM_TRY_UNWRAP(auto bodyFile, BodyGetCacheDir(aBaseDir, aId));
285 
286   char idString[NSID_LENGTH];
287   aId.ToProvidedString(idString);
288 
289   NS_ConvertASCIItoUTF16 fileName(idString);
290 
291   if (aType == BODY_FILE_FINAL) {
292     fileName.AppendLiteral(".final");
293   } else {
294     fileName.AppendLiteral(".tmp");
295   }
296 
297   QM_TRY(bodyFile->Append(fileName));
298 
299   return bodyFile;
300 }
301 
RoundUp(const int64_t aX,const int64_t aY)302 int64_t RoundUp(const int64_t aX, const int64_t aY) {
303   MOZ_DIAGNOSTIC_ASSERT(aX >= 0);
304   MOZ_DIAGNOSTIC_ASSERT(aY > 0);
305 
306   MOZ_DIAGNOSTIC_ASSERT(INT64_MAX - ((aX - 1) / aY) * aY >= aY);
307   return aY + ((aX - 1) / aY) * aY;
308 }
309 
BodyGeneratePadding(const int64_t aBodyFileSize,const uint32_t aPaddingInfo)310 int64_t BodyGeneratePadding(const int64_t aBodyFileSize,
311                             const uint32_t aPaddingInfo) {
312   // Generate padding
313   int64_t randomSize = static_cast<int64_t>(aPaddingInfo);
314   MOZ_DIAGNOSTIC_ASSERT(INT64_MAX - aBodyFileSize >= randomSize);
315   randomSize += aBodyFileSize;
316 
317   return RoundUp(randomSize, kRoundUpNumber) - aBodyFileSize;
318 }
319 
DirectoryPaddingWrite(nsIFile & aBaseDir,DirPaddingFile aPaddingFileType,int64_t aPaddingSize)320 nsresult DirectoryPaddingWrite(nsIFile& aBaseDir,
321                                DirPaddingFile aPaddingFileType,
322                                int64_t aPaddingSize) {
323   MOZ_DIAGNOSTIC_ASSERT(aPaddingSize >= 0);
324 
325   QM_TRY_INSPECT(
326       const auto& file,
327       CloneFileAndAppend(aBaseDir, aPaddingFileType == DirPaddingFile::TMP_FILE
328                                        ? nsLiteralString(PADDING_TMP_FILE_NAME)
329                                        : nsLiteralString(PADDING_FILE_NAME)));
330 
331   QM_TRY_INSPECT(const auto& outputStream, NS_NewLocalFileOutputStream(file));
332 
333   nsCOMPtr<nsIObjectOutputStream> objectStream =
334       NS_NewObjectOutputStream(outputStream);
335 
336   QM_TRY(objectStream->Write64(aPaddingSize));
337 
338   return NS_OK;
339 }
340 
341 }  // namespace
342 
BodyDeleteOrphanedFiles(const QuotaInfo & aQuotaInfo,nsIFile & aBaseDir,const nsTArray<nsID> & aKnownBodyIdList)343 nsresult BodyDeleteOrphanedFiles(const QuotaInfo& aQuotaInfo, nsIFile& aBaseDir,
344                                  const nsTArray<nsID>& aKnownBodyIdList) {
345   // body files are stored in a directory structure like:
346   //
347   //  /morgue/01/{01fdddb2-884d-4c3d-95ba-0c8062f6c325}.final
348   //  /morgue/02/{02fdddb2-884d-4c3d-95ba-0c8062f6c325}.tmp
349 
350   QM_TRY_INSPECT(const auto& dir,
351                  CloneFileAndAppend(aBaseDir, kMorgueDirectory));
352 
353   // Iterate over all the intermediate morgue subdirs
354   QM_TRY(quota::CollectEachFile(
355       *dir,
356       [&aQuotaInfo, &aKnownBodyIdList](
357           const nsCOMPtr<nsIFile>& subdir) -> Result<Ok, nsresult> {
358         QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*subdir));
359 
360         switch (dirEntryKind) {
361           case nsIFileKind::ExistsAsDirectory: {
362             const auto removeOrphanedFiles =
363                 [&aQuotaInfo, &aKnownBodyIdList](
364                     nsIFile& bodyFile,
365                     const nsACString& leafName) -> Result<bool, nsresult> {
366               // Finally, parse the uuid out of the name.  If it fails to parse,
367               // then ignore the file.
368               auto cleanup = MakeScopeExit([&aQuotaInfo, &bodyFile] {
369                 DebugOnly<nsresult> result =
370                     RemoveNsIFile(aQuotaInfo, bodyFile);
371                 MOZ_ASSERT(NS_SUCCEEDED(result));
372               });
373 
374               nsID id;
375               QM_TRY(OkIf(id.Parse(leafName.BeginReading())), true);
376 
377               if (!aKnownBodyIdList.Contains(id)) {
378                 return true;
379               }
380 
381               cleanup.release();
382 
383               return false;
384             };
385 
386             // QM_OR_ELSE_WARN_IF is not used here since we just want to log
387             // NS_ERROR_FILE_FS_CORRUPTED result and not spam the reports (even
388             // a warning in the reports is not desired).
389             QM_TRY(QM_OR_ELSE_LOG_VERBOSE_IF(
390                 // Expression.
391                 ToResult(BodyTraverseFiles(aQuotaInfo, *subdir,
392                                            removeOrphanedFiles,
393                                            /* aCanRemoveFiles */ true,
394                                            /* aTrackQuota */ true)),
395                 // Predicate.
396                 IsSpecificError<NS_ERROR_FILE_FS_CORRUPTED>,
397                 // Fallback. We treat NS_ERROR_FILE_FS_CORRUPTED as if the
398                 // directory did not exist at all.
399                 ErrToDefaultOk<>));
400             break;
401           }
402 
403           case nsIFileKind::ExistsAsFile: {
404             // If a file got in here somehow, try to remove it and move on
405             DebugOnly<nsresult> result =
406                 RemoveNsIFile(aQuotaInfo, *subdir, /* aTrackQuota */ false);
407             MOZ_ASSERT(NS_SUCCEEDED(result));
408             break;
409           }
410 
411           case nsIFileKind::DoesNotExist:
412             // Ignore files that got removed externally while iterating.
413             break;
414         }
415 
416         return Ok{};
417       }));
418 
419   return NS_OK;
420 }
421 
422 namespace {
423 
GetMarkerFileHandle(const QuotaInfo & aQuotaInfo)424 Result<nsCOMPtr<nsIFile>, nsresult> GetMarkerFileHandle(
425     const QuotaInfo& aQuotaInfo) {
426   QM_TRY_UNWRAP(auto marker, CloneFileAndAppend(*aQuotaInfo.mDir, u"cache"_ns));
427 
428   QM_TRY(marker->Append(u"context_open.marker"_ns));
429 
430   return marker;
431 }
432 
433 }  // namespace
434 
CreateMarkerFile(const QuotaInfo & aQuotaInfo)435 nsresult CreateMarkerFile(const QuotaInfo& aQuotaInfo) {
436   QM_TRY_INSPECT(const auto& marker, GetMarkerFileHandle(aQuotaInfo));
437 
438   // Callers call this function without checking if the file already exists
439   // (idempotent usage). QM_OR_ELSE_WARN_IF is not used here since we just want
440   // to log NS_ERROR_FILE_ALREADY_EXISTS result and not spam the reports.
441   //
442   // TODO: In theory if this file exists, then Context::~Context should have
443   // cleaned it up, but obviously we can crash and not clean it up, which is
444   // the whole point of the marker file. In that case, we'll realize the marker
445   // file exists in SetupAction::RunSyncWithDBOnTarget and do some cleanup, but
446   // we won't delete the marker file, so if we see this marker file, it is part
447   // of our standard operating procedure to redundantly try and create the
448   // marker here. We currently treat this as idempotent usage, but we could
449   // make sure to delete the marker file when handling the existing marker
450   // file in SetupAction::RunSyncWithDBOnTarget and change
451   // QM_OR_ELSE_LOG_VERBOSE_IF to QM_OR_ELSE_WARN_IF in the end.
452   QM_TRY(QM_OR_ELSE_LOG_VERBOSE_IF(
453       // Expression.
454       ToResult(marker->Create(nsIFile::NORMAL_FILE_TYPE, 0644)),
455       // Predicate.
456       IsSpecificError<NS_ERROR_FILE_ALREADY_EXISTS>,
457       // Fallback.
458       ErrToDefaultOk<>));
459 
460   // Note, we don't need to fsync here.  We only care about actually
461   // writing the marker if later modifications to the Cache are
462   // actually flushed to the disk.  If the OS crashes before the marker
463   // is written then we are ensured no other changes to the Cache were
464   // flushed either.
465 
466   return NS_OK;
467 }
468 
DeleteMarkerFile(const QuotaInfo & aQuotaInfo)469 nsresult DeleteMarkerFile(const QuotaInfo& aQuotaInfo) {
470   QM_TRY_INSPECT(const auto& marker, GetMarkerFileHandle(aQuotaInfo));
471 
472   DebugOnly<nsresult> result =
473       RemoveNsIFile(aQuotaInfo, *marker, /* aTrackQuota */ false);
474   MOZ_ASSERT(NS_SUCCEEDED(result));
475 
476   // Again, no fsync is necessary.  If the OS crashes before the file
477   // removal is flushed, then the Cache will search for stale data on
478   // startup.  This will cause the next Cache access to be a bit slow, but
479   // it seems appropriate after an OS crash.
480 
481   return NS_OK;
482 }
483 
MarkerFileExists(const QuotaInfo & aQuotaInfo)484 bool MarkerFileExists(const QuotaInfo& aQuotaInfo) {
485   QM_TRY_INSPECT(const auto& marker, GetMarkerFileHandle(aQuotaInfo), false);
486 
487   QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE(marker, Exists), false);
488 }
489 
RemoveNsIFileRecursively(const QuotaInfo & aQuotaInfo,nsIFile & aFile,const bool aTrackQuota)490 nsresult RemoveNsIFileRecursively(const QuotaInfo& aQuotaInfo, nsIFile& aFile,
491                                   const bool aTrackQuota) {
492   QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(aFile));
493 
494   switch (dirEntryKind) {
495     case nsIFileKind::ExistsAsDirectory:
496       // Unfortunately, we need to traverse all the entries and delete files one
497       // by
498       // one to update their usages to the QuotaManager.
499       QM_TRY(quota::CollectEachFile(
500           aFile,
501           [&aQuotaInfo, &aTrackQuota](
502               const nsCOMPtr<nsIFile>& file) -> Result<Ok, nsresult> {
503             QM_TRY(RemoveNsIFileRecursively(aQuotaInfo, *file, aTrackQuota));
504 
505             return Ok{};
506           }));
507 
508       // In the end, remove the folder
509       QM_TRY(aFile.Remove(/* recursive */ false));
510 
511       break;
512 
513     case nsIFileKind::ExistsAsFile:
514       return RemoveNsIFile(aQuotaInfo, aFile, aTrackQuota);
515 
516     case nsIFileKind::DoesNotExist:
517       // Ignore files that got removed externally while iterating.
518       break;
519   }
520 
521   return NS_OK;
522 }
523 
RemoveNsIFile(const QuotaInfo & aQuotaInfo,nsIFile & aFile,const bool aTrackQuota)524 nsresult RemoveNsIFile(const QuotaInfo& aQuotaInfo, nsIFile& aFile,
525                        const bool aTrackQuota) {
526   int64_t fileSize = 0;
527   if (aTrackQuota) {
528     QM_TRY_INSPECT(
529         const auto& maybeFileSize,
530         QM_OR_ELSE_WARN_IF(
531             // Expression.
532             MOZ_TO_RESULT_INVOKE(aFile, GetFileSize).map(Some<int64_t>),
533             // Predicate.
534             IsFileNotFoundError,
535             // Fallback.
536             ErrToDefaultOk<Maybe<int64_t>>));
537 
538     if (!maybeFileSize) {
539       return NS_OK;
540     }
541 
542     fileSize = *maybeFileSize;
543   }
544 
545   QM_TRY(QM_OR_ELSE_WARN_IF(
546       // Expression.
547       ToResult(aFile.Remove(/* recursive */ false)),
548       // Predicate.
549       IsFileNotFoundError,
550       // Fallback.
551       ErrToDefaultOk<>));
552 
553   if (fileSize > 0) {
554     MOZ_ASSERT(aTrackQuota);
555     DecreaseUsageForQuotaInfo(aQuotaInfo, fileSize);
556   }
557 
558   return NS_OK;
559 }
560 
DecreaseUsageForQuotaInfo(const QuotaInfo & aQuotaInfo,const int64_t aUpdatingSize)561 void DecreaseUsageForQuotaInfo(const QuotaInfo& aQuotaInfo,
562                                const int64_t aUpdatingSize) {
563   MOZ_DIAGNOSTIC_ASSERT(aUpdatingSize > 0);
564 
565   QuotaManager* quotaManager = QuotaManager::Get();
566   MOZ_DIAGNOSTIC_ASSERT(quotaManager);
567 
568   quotaManager->DecreaseUsageForClient(
569       quota::ClientMetadata{aQuotaInfo, Client::DOMCACHE}, aUpdatingSize);
570 }
571 
DirectoryPaddingFileExists(nsIFile & aBaseDir,DirPaddingFile aPaddingFileType)572 bool DirectoryPaddingFileExists(nsIFile& aBaseDir,
573                                 DirPaddingFile aPaddingFileType) {
574   QM_TRY_INSPECT(
575       const auto& file,
576       CloneFileAndAppend(aBaseDir, aPaddingFileType == DirPaddingFile::TMP_FILE
577                                        ? nsLiteralString(PADDING_TMP_FILE_NAME)
578                                        : nsLiteralString(PADDING_FILE_NAME)),
579       false);
580 
581   QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE(file, Exists), false);
582 }
583 
DirectoryPaddingGet(nsIFile & aBaseDir)584 Result<int64_t, nsresult> DirectoryPaddingGet(nsIFile& aBaseDir) {
585   MOZ_DIAGNOSTIC_ASSERT(
586       !DirectoryPaddingFileExists(aBaseDir, DirPaddingFile::TMP_FILE));
587 
588   QM_TRY_INSPECT(
589       const auto& file,
590       CloneFileAndAppend(aBaseDir, nsLiteralString(PADDING_FILE_NAME)));
591 
592   QM_TRY_UNWRAP(auto stream, NS_NewLocalFileInputStream(file));
593 
594   QM_TRY_INSPECT(const auto& bufferedStream,
595                  NS_NewBufferedInputStream(stream.forget(), 512));
596 
597   const nsCOMPtr<nsIObjectInputStream> objectStream =
598       NS_NewObjectInputStream(bufferedStream);
599 
600   QM_TRY_RETURN(
601       MOZ_TO_RESULT_INVOKE(objectStream, Read64).map([](const uint64_t val) {
602         return int64_t(val);
603       }));
604 }
605 
DirectoryPaddingInit(nsIFile & aBaseDir)606 nsresult DirectoryPaddingInit(nsIFile& aBaseDir) {
607   QM_TRY(DirectoryPaddingWrite(aBaseDir, DirPaddingFile::FILE, 0));
608 
609   return NS_OK;
610 }
611 
UpdateDirectoryPaddingFile(nsIFile & aBaseDir,mozIStorageConnection & aConn,const int64_t aIncreaseSize,const int64_t aDecreaseSize,const bool aTemporaryFileExist)612 nsresult UpdateDirectoryPaddingFile(nsIFile& aBaseDir,
613                                     mozIStorageConnection& aConn,
614                                     const int64_t aIncreaseSize,
615                                     const int64_t aDecreaseSize,
616                                     const bool aTemporaryFileExist) {
617   MOZ_DIAGNOSTIC_ASSERT(aIncreaseSize >= 0);
618   MOZ_DIAGNOSTIC_ASSERT(aDecreaseSize >= 0);
619 
620   const auto directoryPaddingGetResult =
621       aTemporaryFileExist ? Maybe<int64_t>{} : [&aBaseDir] {
622         QM_TRY_RETURN(QM_OR_ELSE_WARN_IF(
623                           // Expression.
624                           DirectoryPaddingGet(aBaseDir).map(Some<int64_t>),
625                           // Predicate.
626                           IsFileNotFoundError,
627                           // Fallback.
628                           ErrToDefaultOk<Maybe<int64_t>>),
629                       Maybe<int64_t>{});
630       }();
631 
632   QM_TRY_INSPECT(
633       const int64_t& currentPaddingSize,
634       ([directoryPaddingGetResult, &aBaseDir, &aConn, aIncreaseSize,
635         aDecreaseSize]() -> Result<int64_t, nsresult> {
636         if (!directoryPaddingGetResult) {
637           // Fail to read padding size from the dir padding file, so try to
638           // restore.
639 
640           // Not delete the temporary padding file here, because we're going
641           // to overwrite it below anyway.
642           QM_TRY(DirectoryPaddingDeleteFile(aBaseDir, DirPaddingFile::FILE));
643 
644           // We don't need to add the aIncreaseSize or aDecreaseSize here,
645           // because it's already encompassed within the database.
646           QM_TRY_RETURN(db::FindOverallPaddingSize(aConn));
647         }
648 
649         int64_t currentPaddingSize = directoryPaddingGetResult.value();
650         bool shouldRevise = false;
651 
652         if (aIncreaseSize > 0) {
653           if (INT64_MAX - currentPaddingSize < aDecreaseSize) {
654             shouldRevise = true;
655           } else {
656             currentPaddingSize += aIncreaseSize;
657           }
658         }
659 
660         if (aDecreaseSize > 0) {
661           if (currentPaddingSize < aDecreaseSize) {
662             shouldRevise = true;
663           } else if (!shouldRevise) {
664             currentPaddingSize -= aDecreaseSize;
665           }
666         }
667 
668         if (shouldRevise) {
669           // If somehow runing into this condition, the tracking padding size is
670           // incorrect.
671           // Delete padding file to indicate the padding size is incorrect for
672           // avoiding error happening in the following lines.
673           QM_TRY(DirectoryPaddingDeleteFile(aBaseDir, DirPaddingFile::FILE));
674 
675           QM_TRY_UNWRAP(currentPaddingSize, db::FindOverallPaddingSize(aConn));
676 
677           // XXXtt: we should have an easy way to update (increase or
678           // recalulate) padding size in the QM. For now, only correct the
679           // padding size in padding file and make QM be able to get the correct
680           // size in the next QM initialization. We still want to catch this in
681           // the debug build.
682           MOZ_ASSERT(false, "The padding size is unsync with QM");
683         }
684 
685 #ifdef DEBUG
686         const int64_t lastPaddingSize = currentPaddingSize;
687         QM_TRY_UNWRAP(currentPaddingSize, db::FindOverallPaddingSize(aConn));
688 
689         MOZ_DIAGNOSTIC_ASSERT(currentPaddingSize == lastPaddingSize);
690 #endif  // DEBUG
691 
692         return currentPaddingSize;
693       }()));
694 
695   MOZ_DIAGNOSTIC_ASSERT(currentPaddingSize >= 0);
696 
697   QM_TRY(DirectoryPaddingWrite(aBaseDir, DirPaddingFile::TMP_FILE,
698                                currentPaddingSize));
699 
700   return NS_OK;
701 }
702 
DirectoryPaddingFinalizeWrite(nsIFile & aBaseDir)703 nsresult DirectoryPaddingFinalizeWrite(nsIFile& aBaseDir) {
704   MOZ_DIAGNOSTIC_ASSERT(
705       DirectoryPaddingFileExists(aBaseDir, DirPaddingFile::TMP_FILE));
706 
707   QM_TRY_INSPECT(
708       const auto& file,
709       CloneFileAndAppend(aBaseDir, nsLiteralString(PADDING_TMP_FILE_NAME)));
710 
711   QM_TRY(file->RenameTo(nullptr, nsLiteralString(PADDING_FILE_NAME)));
712 
713   return NS_OK;
714 }
715 
DirectoryPaddingRestore(nsIFile & aBaseDir,mozIStorageConnection & aConn,const bool aMustRestore)716 Result<int64_t, nsresult> DirectoryPaddingRestore(nsIFile& aBaseDir,
717                                                   mozIStorageConnection& aConn,
718                                                   const bool aMustRestore) {
719   // The content of padding file is untrusted, so remove it here.
720   QM_TRY(DirectoryPaddingDeleteFile(aBaseDir, DirPaddingFile::FILE));
721 
722   QM_TRY_INSPECT(const int64_t& paddingSize, db::FindOverallPaddingSize(aConn));
723   MOZ_DIAGNOSTIC_ASSERT(paddingSize >= 0);
724 
725   QM_TRY(DirectoryPaddingWrite(aBaseDir, DirPaddingFile::FILE, paddingSize),
726          (aMustRestore ? Err(tryTempError)
727                        : Result<int64_t, nsresult>{paddingSize}));
728 
729   QM_TRY(DirectoryPaddingDeleteFile(aBaseDir, DirPaddingFile::TMP_FILE));
730 
731   return paddingSize;
732 }
733 
DirectoryPaddingDeleteFile(nsIFile & aBaseDir,DirPaddingFile aPaddingFileType)734 nsresult DirectoryPaddingDeleteFile(nsIFile& aBaseDir,
735                                     DirPaddingFile aPaddingFileType) {
736   QM_TRY_INSPECT(
737       const auto& file,
738       CloneFileAndAppend(aBaseDir, aPaddingFileType == DirPaddingFile::TMP_FILE
739                                        ? nsLiteralString(PADDING_TMP_FILE_NAME)
740                                        : nsLiteralString(PADDING_FILE_NAME)));
741 
742   QM_TRY(QM_OR_ELSE_WARN_IF(
743       // Expression.
744       ToResult(file->Remove(/* recursive */ false)),
745       // Predicate.
746       IsFileNotFoundError,
747       // Fallback.
748       ErrToDefaultOk<>));
749 
750   return NS_OK;
751 }
752 }  // namespace mozilla::dom::cache
753