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 "mozilla/dom/cache/FileUtils.h"
8 
9 #include "mozilla/dom/quota/FileStreams.h"
10 #include "mozilla/SnappyCompressOutputStream.h"
11 #include "mozilla/Unused.h"
12 #include "nsIFile.h"
13 #include "nsIUUIDGenerator.h"
14 #include "nsNetCID.h"
15 #include "nsISimpleEnumerator.h"
16 #include "nsServiceManagerUtils.h"
17 #include "nsString.h"
18 #include "nsThreadUtils.h"
19 
20 namespace mozilla {
21 namespace dom {
22 namespace cache {
23 
24 using mozilla::dom::quota::FileInputStream;
25 using mozilla::dom::quota::FileOutputStream;
26 using mozilla::dom::quota::PERSISTENCE_TYPE_DEFAULT;
27 
28 namespace {
29 
30 enum BodyFileType
31 {
32   BODY_FILE_FINAL,
33   BODY_FILE_TMP
34 };
35 
36 nsresult
37 BodyIdToFile(nsIFile* aBaseDir, const nsID& aId, BodyFileType aType,
38              nsIFile** aBodyFileOut);
39 
40 } // namespace
41 
42 // static
43 nsresult
BodyCreateDir(nsIFile * aBaseDir)44 BodyCreateDir(nsIFile* aBaseDir)
45 {
46   MOZ_DIAGNOSTIC_ASSERT(aBaseDir);
47 
48   nsCOMPtr<nsIFile> aBodyDir;
49   nsresult rv = aBaseDir->Clone(getter_AddRefs(aBodyDir));
50   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
51 
52   rv = aBodyDir->Append(NS_LITERAL_STRING("morgue"));
53   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
54 
55   rv = aBodyDir->Create(nsIFile::DIRECTORY_TYPE, 0755);
56   if (rv == NS_ERROR_FILE_ALREADY_EXISTS) {
57     return NS_OK;
58   }
59   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
60 
61   return rv;
62 }
63 
64 // static
65 nsresult
BodyDeleteDir(nsIFile * aBaseDir)66 BodyDeleteDir(nsIFile* aBaseDir)
67 {
68   MOZ_DIAGNOSTIC_ASSERT(aBaseDir);
69 
70   nsCOMPtr<nsIFile> aBodyDir;
71   nsresult rv = aBaseDir->Clone(getter_AddRefs(aBodyDir));
72   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
73 
74   rv = aBodyDir->Append(NS_LITERAL_STRING("morgue"));
75   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
76 
77   rv = aBodyDir->Remove(/* recursive = */ true);
78   if (rv == NS_ERROR_FILE_NOT_FOUND ||
79       rv == NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) {
80     rv = NS_OK;
81   }
82   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
83 
84   return rv;
85 }
86 
87 // static
88 nsresult
BodyGetCacheDir(nsIFile * aBaseDir,const nsID & aId,nsIFile ** aCacheDirOut)89 BodyGetCacheDir(nsIFile* aBaseDir, const nsID& aId, nsIFile** aCacheDirOut)
90 {
91   MOZ_DIAGNOSTIC_ASSERT(aBaseDir);
92   MOZ_DIAGNOSTIC_ASSERT(aCacheDirOut);
93 
94   *aCacheDirOut = nullptr;
95 
96   nsresult rv = aBaseDir->Clone(aCacheDirOut);
97   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
98   MOZ_DIAGNOSTIC_ASSERT(*aCacheDirOut);
99 
100   rv = (*aCacheDirOut)->Append(NS_LITERAL_STRING("morgue"));
101   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
102 
103   // Some file systems have poor performance when there are too many files
104   // in a single directory.  Mitigate this issue by spreading the body
105   // files out into sub-directories.  We use the last byte of the ID for
106   // the name of the sub-directory.
107   nsAutoString subDirName;
108   subDirName.AppendInt(aId.m3[7]);
109   rv = (*aCacheDirOut)->Append(subDirName);
110   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
111 
112   rv = (*aCacheDirOut)->Create(nsIFile::DIRECTORY_TYPE, 0755);
113   if (rv == NS_ERROR_FILE_ALREADY_EXISTS) {
114     return NS_OK;
115   }
116   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
117 
118   return rv;
119 }
120 
121 // static
122 nsresult
BodyStartWriteStream(const QuotaInfo & aQuotaInfo,nsIFile * aBaseDir,nsIInputStream * aSource,void * aClosure,nsAsyncCopyCallbackFun aCallback,nsID * aIdOut,nsISupports ** aCopyContextOut)123 BodyStartWriteStream(const QuotaInfo& aQuotaInfo,
124                      nsIFile* aBaseDir, nsIInputStream* aSource,
125                      void* aClosure,
126                      nsAsyncCopyCallbackFun aCallback, nsID* aIdOut,
127                      nsISupports** aCopyContextOut)
128 {
129   MOZ_DIAGNOSTIC_ASSERT(aBaseDir);
130   MOZ_DIAGNOSTIC_ASSERT(aSource);
131   MOZ_DIAGNOSTIC_ASSERT(aClosure);
132   MOZ_DIAGNOSTIC_ASSERT(aCallback);
133   MOZ_DIAGNOSTIC_ASSERT(aIdOut);
134   MOZ_DIAGNOSTIC_ASSERT(aCopyContextOut);
135 
136   nsresult rv;
137   nsCOMPtr<nsIUUIDGenerator> idGen =
138     do_GetService("@mozilla.org/uuid-generator;1", &rv);
139   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
140 
141   rv = idGen->GenerateUUIDInPlace(aIdOut);
142   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
143 
144   nsCOMPtr<nsIFile> finalFile;
145   rv = BodyIdToFile(aBaseDir, *aIdOut, BODY_FILE_FINAL,
146                     getter_AddRefs(finalFile));
147   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
148 
149   bool exists;
150   rv = finalFile->Exists(&exists);
151   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
152   if (NS_WARN_IF(exists)) { return NS_ERROR_FILE_ALREADY_EXISTS; }
153 
154   nsCOMPtr<nsIFile> tmpFile;
155   rv = BodyIdToFile(aBaseDir, *aIdOut, BODY_FILE_TMP, getter_AddRefs(tmpFile));
156   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
157 
158   rv = tmpFile->Exists(&exists);
159   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
160   if (NS_WARN_IF(exists)) { return NS_ERROR_FILE_ALREADY_EXISTS; }
161 
162   nsCOMPtr<nsIOutputStream> fileStream =
163     FileOutputStream::Create(PERSISTENCE_TYPE_DEFAULT, aQuotaInfo.mGroup,
164                              aQuotaInfo.mOrigin, tmpFile);
165   if (NS_WARN_IF(!fileStream)) { return NS_ERROR_UNEXPECTED; }
166 
167   RefPtr<SnappyCompressOutputStream> compressed =
168     new SnappyCompressOutputStream(fileStream);
169 
170   nsCOMPtr<nsIEventTarget> target =
171     do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
172 
173   rv = NS_AsyncCopy(aSource, compressed, target, NS_ASYNCCOPY_VIA_WRITESEGMENTS,
174                     compressed->BlockSize(), aCallback, aClosure,
175                     true, true, // close streams
176                     aCopyContextOut);
177   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
178 
179   return rv;
180 }
181 
182 // static
183 void
BodyCancelWrite(nsIFile * aBaseDir,nsISupports * aCopyContext)184 BodyCancelWrite(nsIFile* aBaseDir, nsISupports* aCopyContext)
185 {
186   MOZ_DIAGNOSTIC_ASSERT(aBaseDir);
187   MOZ_DIAGNOSTIC_ASSERT(aCopyContext);
188 
189   nsresult rv = NS_CancelAsyncCopy(aCopyContext, NS_ERROR_ABORT);
190   Unused << NS_WARN_IF(NS_FAILED(rv));
191 
192   // The partially written file must be cleaned up after the async copy
193   // makes its callback.
194 }
195 
196 // static
197 nsresult
BodyFinalizeWrite(nsIFile * aBaseDir,const nsID & aId)198 BodyFinalizeWrite(nsIFile* aBaseDir, const nsID& aId)
199 {
200   MOZ_DIAGNOSTIC_ASSERT(aBaseDir);
201 
202   nsCOMPtr<nsIFile> tmpFile;
203   nsresult rv = BodyIdToFile(aBaseDir, aId, BODY_FILE_TMP, getter_AddRefs(tmpFile));
204   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
205 
206   nsCOMPtr<nsIFile> finalFile;
207   rv = BodyIdToFile(aBaseDir, aId, BODY_FILE_FINAL, getter_AddRefs(finalFile));
208   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
209 
210   nsAutoString finalFileName;
211   rv = finalFile->GetLeafName(finalFileName);
212   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
213 
214   rv = tmpFile->RenameTo(nullptr, finalFileName);
215   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
216 
217   return rv;
218 }
219 
220 // static
221 nsresult
BodyOpen(const QuotaInfo & aQuotaInfo,nsIFile * aBaseDir,const nsID & aId,nsIInputStream ** aStreamOut)222 BodyOpen(const QuotaInfo& aQuotaInfo, nsIFile* aBaseDir, const nsID& aId,
223          nsIInputStream** aStreamOut)
224 {
225   MOZ_DIAGNOSTIC_ASSERT(aBaseDir);
226   MOZ_DIAGNOSTIC_ASSERT(aStreamOut);
227 
228   nsCOMPtr<nsIFile> finalFile;
229   nsresult rv = BodyIdToFile(aBaseDir, aId, BODY_FILE_FINAL,
230                              getter_AddRefs(finalFile));
231   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
232 
233   bool exists;
234   rv = finalFile->Exists(&exists);
235   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
236   if (NS_WARN_IF(!exists)) { return NS_ERROR_FILE_NOT_FOUND; }
237 
238   nsCOMPtr<nsIInputStream> fileStream =
239     FileInputStream::Create(PERSISTENCE_TYPE_DEFAULT, aQuotaInfo.mGroup,
240                             aQuotaInfo.mOrigin, finalFile);
241   if (NS_WARN_IF(!fileStream)) { return NS_ERROR_UNEXPECTED; }
242 
243   fileStream.forget(aStreamOut);
244 
245   return rv;
246 }
247 
248 // static
249 nsresult
BodyDeleteFiles(nsIFile * aBaseDir,const nsTArray<nsID> & aIdList)250 BodyDeleteFiles(nsIFile* aBaseDir, const nsTArray<nsID>& aIdList)
251 {
252   nsresult rv = NS_OK;
253 
254   for (uint32_t i = 0; i < aIdList.Length(); ++i) {
255     nsCOMPtr<nsIFile> tmpFile;
256     rv = BodyIdToFile(aBaseDir, aIdList[i], BODY_FILE_TMP,
257                       getter_AddRefs(tmpFile));
258     if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
259 
260     rv = tmpFile->Remove(false /* recursive */);
261     if (rv == NS_ERROR_FILE_NOT_FOUND ||
262         rv == NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) {
263       rv = NS_OK;
264     }
265 
266     // Only treat file deletion as a hard failure in DEBUG builds.  Users
267     // can unfortunately hit this on windows if anti-virus is scanning files,
268     // etc.
269     MOZ_ASSERT(NS_SUCCEEDED(rv));
270 
271     nsCOMPtr<nsIFile> finalFile;
272     rv = BodyIdToFile(aBaseDir, aIdList[i], BODY_FILE_FINAL,
273                       getter_AddRefs(finalFile));
274     if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
275 
276     rv = finalFile->Remove(false /* recursive */);
277     if (rv == NS_ERROR_FILE_NOT_FOUND ||
278         rv == NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) {
279       rv = NS_OK;
280     }
281 
282     // Again, only treat removal as hard failure in debug build.
283     MOZ_ASSERT(NS_SUCCEEDED(rv));
284   }
285 
286   return NS_OK;
287 }
288 
289 namespace {
290 
291 nsresult
BodyIdToFile(nsIFile * aBaseDir,const nsID & aId,BodyFileType aType,nsIFile ** aBodyFileOut)292 BodyIdToFile(nsIFile* aBaseDir, const nsID& aId, BodyFileType aType,
293              nsIFile** aBodyFileOut)
294 {
295   MOZ_DIAGNOSTIC_ASSERT(aBaseDir);
296   MOZ_DIAGNOSTIC_ASSERT(aBodyFileOut);
297 
298   *aBodyFileOut = nullptr;
299 
300   nsresult rv = BodyGetCacheDir(aBaseDir, aId, aBodyFileOut);
301   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
302   MOZ_DIAGNOSTIC_ASSERT(*aBodyFileOut);
303 
304   char idString[NSID_LENGTH];
305   aId.ToProvidedString(idString);
306 
307   NS_ConvertASCIItoUTF16 fileName(idString);
308 
309   if (aType == BODY_FILE_FINAL) {
310     fileName.AppendLiteral(".final");
311   } else {
312     fileName.AppendLiteral(".tmp");
313   }
314 
315   rv = (*aBodyFileOut)->Append(fileName);
316   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
317 
318   return rv;
319 }
320 
321 } // namespace
322 
323 nsresult
BodyDeleteOrphanedFiles(nsIFile * aBaseDir,nsTArray<nsID> & aKnownBodyIdList)324 BodyDeleteOrphanedFiles(nsIFile* aBaseDir, nsTArray<nsID>& aKnownBodyIdList)
325 {
326   MOZ_DIAGNOSTIC_ASSERT(aBaseDir);
327 
328   // body files are stored in a directory structure like:
329   //
330   //  /morgue/01/{01fdddb2-884d-4c3d-95ba-0c8062f6c325}.final
331   //  /morgue/02/{02fdddb2-884d-4c3d-95ba-0c8062f6c325}.tmp
332 
333   nsCOMPtr<nsIFile> dir;
334   nsresult rv = aBaseDir->Clone(getter_AddRefs(dir));
335   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
336 
337   // Add the root morgue directory
338   rv = dir->Append(NS_LITERAL_STRING("morgue"));
339   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
340 
341   nsCOMPtr<nsISimpleEnumerator> entries;
342   rv = dir->GetDirectoryEntries(getter_AddRefs(entries));
343   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
344 
345   // Iterate over all the intermediate morgue subdirs
346   bool hasMore = false;
347   while (NS_SUCCEEDED(rv = entries->HasMoreElements(&hasMore)) && hasMore) {
348     nsCOMPtr<nsISupports> entry;
349     rv = entries->GetNext(getter_AddRefs(entry));
350     if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
351 
352     nsCOMPtr<nsIFile> subdir = do_QueryInterface(entry);
353 
354     bool isDir = false;
355     rv = subdir->IsDirectory(&isDir);
356     if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
357 
358     // If a file got in here somehow, try to remove it and move on
359     if (NS_WARN_IF(!isDir)) {
360       rv = subdir->Remove(false /* recursive */);
361       MOZ_ASSERT(NS_SUCCEEDED(rv));
362       continue;
363     }
364 
365     nsCOMPtr<nsISimpleEnumerator> subEntries;
366     rv = subdir->GetDirectoryEntries(getter_AddRefs(subEntries));
367     if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
368 
369     // Now iterate over all the files in the subdir
370     bool subHasMore = false;
371     while(NS_SUCCEEDED(rv = subEntries->HasMoreElements(&subHasMore)) &&
372           subHasMore) {
373       nsCOMPtr<nsISupports> subEntry;
374       rv = subEntries->GetNext(getter_AddRefs(subEntry));
375       if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
376 
377       nsCOMPtr<nsIFile> file = do_QueryInterface(subEntry);
378 
379       nsAutoCString leafName;
380       rv = file->GetNativeLeafName(leafName);
381       if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
382 
383       // Delete all tmp files regardless of known bodies.  These are
384       // all considered orphans.
385       if (StringEndsWith(leafName, NS_LITERAL_CSTRING(".tmp"))) {
386         // remove recursively in case its somehow a directory
387         rv = file->Remove(true /* recursive */);
388         MOZ_ASSERT(NS_SUCCEEDED(rv));
389         continue;
390       }
391 
392       nsCString suffix(NS_LITERAL_CSTRING(".final"));
393 
394       // Otherwise, it must be a .final file.  If its not, then just
395       // skip it.
396       if (NS_WARN_IF(!StringEndsWith(leafName, suffix) ||
397                      leafName.Length() != NSID_LENGTH - 1 + suffix.Length())) {
398         continue;
399       }
400 
401       // Finally, parse the uuid out of the name.  If its fails to parse,
402       // the ignore the file.
403       nsID id;
404       if (NS_WARN_IF(!id.Parse(leafName.BeginReading()))) {
405         continue;
406       }
407 
408       if (!aKnownBodyIdList.Contains(id)) {
409         // remove recursively in case its somehow a directory
410         rv = file->Remove(true /* recursive */);
411         MOZ_ASSERT(NS_SUCCEEDED(rv));
412       }
413     }
414   }
415 
416   return rv;
417 }
418 
419 namespace {
420 
421 nsresult
GetMarkerFileHandle(const QuotaInfo & aQuotaInfo,nsIFile ** aFileOut)422 GetMarkerFileHandle(const QuotaInfo& aQuotaInfo, nsIFile** aFileOut)
423 {
424   MOZ_DIAGNOSTIC_ASSERT(aFileOut);
425 
426   nsCOMPtr<nsIFile> marker;
427   nsresult rv = aQuotaInfo.mDir->Clone(getter_AddRefs(marker));
428   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
429 
430   rv = marker->Append(NS_LITERAL_STRING("cache"));
431   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
432 
433   rv = marker->Append(NS_LITERAL_STRING("context_open.marker"));
434   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
435 
436   marker.forget(aFileOut);
437 
438   return rv;
439 }
440 
441 } // namespace
442 
443 nsresult
CreateMarkerFile(const QuotaInfo & aQuotaInfo)444 CreateMarkerFile(const QuotaInfo& aQuotaInfo)
445 {
446   nsCOMPtr<nsIFile> marker;
447   nsresult rv = GetMarkerFileHandle(aQuotaInfo, getter_AddRefs(marker));
448   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
449 
450   rv = marker->Create(nsIFile::NORMAL_FILE_TYPE, 0644);
451   if (rv == NS_ERROR_FILE_ALREADY_EXISTS) {
452     rv = NS_OK;
453   }
454 
455   // Note, we don't need to fsync here.  We only care about actually
456   // writing the marker if later modifications to the Cache are
457   // actually flushed to the disk.  If the OS crashes before the marker
458   // is written then we are ensured no other changes to the Cache were
459   // flushed either.
460 
461   return rv;
462 }
463 
464 nsresult
DeleteMarkerFile(const QuotaInfo & aQuotaInfo)465 DeleteMarkerFile(const QuotaInfo& aQuotaInfo)
466 {
467   nsCOMPtr<nsIFile> marker;
468   nsresult rv = GetMarkerFileHandle(aQuotaInfo, getter_AddRefs(marker));
469   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
470 
471   rv = marker->Remove(/* recursive = */ false);
472   if (rv == NS_ERROR_FILE_NOT_FOUND ||
473       rv == NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) {
474     rv = NS_OK;
475   }
476 
477   // Again, no fsync is necessary.  If the OS crashes before the file
478   // removal is flushed, then the Cache will search for stale data on
479   // startup.  This will cause the next Cache access to be a bit slow, but
480   // it seems appropriate after an OS crash.
481 
482   return NS_OK;
483 }
484 
485 bool
MarkerFileExists(const QuotaInfo & aQuotaInfo)486 MarkerFileExists(const QuotaInfo& aQuotaInfo)
487 {
488   nsCOMPtr<nsIFile> marker;
489   nsresult rv = GetMarkerFileHandle(aQuotaInfo, getter_AddRefs(marker));
490   if (NS_WARN_IF(NS_FAILED(rv))) { return false; }
491 
492   bool exists = false;
493   rv = marker->Exists(&exists);
494   if (NS_WARN_IF(NS_FAILED(rv))) { return false; }
495 
496   return exists;
497 }
498 
499 } // namespace cache
500 } // namespace dom
501 } // namespace mozilla
502