Bug 1857841 - pt 3. Add a new page kind named "fresh" r=glandium
[gecko.git] / dom / system / PathUtils.cpp
blob24d7b648db8cdb1e48ce44c9d7f4180927c69279
1 /* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
4 * You can obtain one at http://mozilla.org/MPL/2.0/.
5 */
7 #include "PathUtils.h"
9 #include "mozilla/ClearOnShutdown.h"
10 #include "mozilla/DataMutex.h"
11 #include "mozilla/ErrorNames.h"
12 #include "mozilla/ErrorResult.h"
13 #include "mozilla/Maybe.h"
14 #include "mozilla/MozPromise.h"
15 #include "mozilla/RefPtr.h"
16 #include "mozilla/ResultExtensions.h"
17 #include "mozilla/Span.h"
18 #include "mozilla/Try.h"
19 #include "mozilla/dom/DOMParser.h"
20 #include "mozilla/dom/PathUtilsBinding.h"
21 #include "mozilla/dom/Promise.h"
22 #include "nsAppDirectoryServiceDefs.h"
23 #include "nsCOMPtr.h"
24 #include "nsDirectoryServiceDefs.h"
25 #include "nsDirectoryServiceUtils.h"
26 #include "nsIFile.h"
27 #include "nsIGlobalObject.h"
28 #include "nsLocalFile.h"
29 #include "nsNetUtil.h"
30 #include "nsString.h"
31 #include "nsURLHelper.h"
32 #include "xpcpublic.h"
34 namespace mozilla::dom {
36 static constexpr auto ERROR_EMPTY_PATH =
37 "PathUtils does not support empty paths"_ns;
38 static constexpr auto ERROR_INITIALIZE_PATH = "Could not initialize path"_ns;
39 static constexpr auto ERROR_GET_PARENT = "Could not get parent path"_ns;
40 static constexpr auto ERROR_JOIN = "Could not append to path"_ns;
42 static constexpr auto COLON = ": "_ns;
44 static void ThrowError(ErrorResult& aErr, const nsresult aResult,
45 const nsCString& aMessage) {
46 nsAutoCStringN<32> errName;
47 GetErrorName(aResult, errName);
49 nsAutoCStringN<256> formattedMsg;
50 formattedMsg.Append(aMessage);
51 formattedMsg.Append(COLON);
52 formattedMsg.Append(errName);
54 switch (aResult) {
55 case NS_ERROR_FILE_UNRECOGNIZED_PATH:
56 aErr.ThrowOperationError(formattedMsg);
57 break;
59 case NS_ERROR_FILE_ACCESS_DENIED:
60 aErr.ThrowInvalidAccessError(formattedMsg);
61 break;
63 case NS_ERROR_FAILURE:
64 default:
65 aErr.ThrowUnknownError(formattedMsg);
66 break;
70 static bool DoWindowsPathCheck() {
71 #ifdef XP_WIN
72 # ifdef DEBUG
73 return true;
74 # else // DEBUG
75 return xpc::IsInAutomation();
76 # endif // DEBUG
77 #else // XP_WIN
78 return false;
79 #endif // XP_WIN
82 /* static */
83 nsresult PathUtils::InitFileWithPath(nsIFile* aFile, const nsAString& aPath) {
84 if (DoWindowsPathCheck()) {
85 MOZ_RELEASE_ASSERT(!aPath.Contains(u'/'),
86 "Windows paths cannot include forward slashes");
89 MOZ_ASSERT(aFile);
90 return aFile->InitWithPath(aPath);
93 StaticDataMutex<Maybe<PathUtils::DirectoryCache>> PathUtils::sDirCache{
94 "sDirCache"};
96 /**
97 * Return the leaf name, including leading path separators in the case of
98 * Windows UNC drive paths.
100 * @param aFile The file whose leaf name is to be returned.
101 * @param aResult The string to hold the resulting leaf name.
102 * @param aParent The pre-computed parent of |aFile|. If not provided, it will
103 * be computed.
105 static nsresult GetLeafNamePreservingRoot(nsIFile* aFile, nsString& aResult,
106 nsIFile* aParent = nullptr) {
107 MOZ_ASSERT(aFile);
109 nsCOMPtr<nsIFile> parent = aParent;
110 if (!parent) {
111 MOZ_TRY(aFile->GetParent(getter_AddRefs(parent)));
114 if (parent) {
115 return aFile->GetLeafName(aResult);
118 // We have reached the root path. On Windows, the leafname for a UNC path
119 // will not have the leading backslashes, so we need to use the entire path
120 // here:
122 // * for a UNIX root path (/) this will be /;
123 // * for a Windows drive path (e.g., C:), this will be the drive path (C:);
124 // and
125 // * for a Windows UNC server path (e.g., \\\\server), this will be the full
126 // server path (\\\\server).
127 return aFile->GetPath(aResult);
130 void PathUtils::Filename(const GlobalObject&, const nsAString& aPath,
131 nsString& aResult, ErrorResult& aErr) {
132 if (aPath.IsEmpty()) {
133 aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH);
134 return;
137 nsCOMPtr<nsIFile> path = new nsLocalFile();
138 if (nsresult rv = InitFileWithPath(path, aPath); NS_FAILED(rv)) {
139 ThrowError(aErr, rv, ERROR_INITIALIZE_PATH);
140 return;
143 if (nsresult rv = GetLeafNamePreservingRoot(path, aResult); NS_FAILED(rv)) {
144 ThrowError(aErr, rv, "Could not get leaf name of path"_ns);
145 return;
149 void PathUtils::Parent(const GlobalObject&, const nsAString& aPath,
150 const int32_t aDepth, nsString& aResult,
151 ErrorResult& aErr) {
152 if (aPath.IsEmpty()) {
153 aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH);
154 return;
157 nsCOMPtr<nsIFile> path = new nsLocalFile();
158 if (nsresult rv = InitFileWithPath(path, aPath); NS_FAILED(rv)) {
159 ThrowError(aErr, rv, ERROR_INITIALIZE_PATH);
160 return;
163 if (aDepth <= 0) {
164 aErr.ThrowNotSupportedError("A depth of at least 1 is required");
165 return;
168 nsCOMPtr<nsIFile> parent;
169 for (int32_t i = 0; path && i < aDepth; i++) {
170 if (nsresult rv = path->GetParent(getter_AddRefs(parent)); NS_FAILED(rv)) {
171 ThrowError(aErr, rv, ERROR_GET_PARENT);
172 return;
174 path = parent;
177 if (parent) {
178 MOZ_ALWAYS_SUCCEEDS(parent->GetPath(aResult));
179 } else {
180 aResult = VoidString();
184 void PathUtils::Join(const GlobalObject&, const Sequence<nsString>& aComponents,
185 nsString& aResult, ErrorResult& aErr) {
186 nsCOMPtr<nsIFile> path = Join(Span(aComponents), aErr);
187 if (aErr.Failed()) {
188 return;
191 MOZ_ALWAYS_SUCCEEDS(path->GetPath(aResult));
194 already_AddRefed<nsIFile> PathUtils::Join(
195 const Span<const nsString>& aComponents, ErrorResult& aErr) {
196 if (aComponents.IsEmpty() || aComponents[0].IsEmpty()) {
197 aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH);
198 return nullptr;
201 nsCOMPtr<nsIFile> path = new nsLocalFile();
202 if (nsresult rv = InitFileWithPath(path, aComponents[0]); NS_FAILED(rv)) {
203 ThrowError(aErr, rv, ERROR_INITIALIZE_PATH);
204 return nullptr;
207 const auto components = Span<const nsString>(aComponents).Subspan(1);
208 for (const auto& component : components) {
209 if (nsresult rv = path->Append(component); NS_FAILED(rv)) {
210 ThrowError(aErr, rv, ERROR_JOIN);
211 return nullptr;
215 return path.forget();
218 void PathUtils::JoinRelative(const GlobalObject&, const nsAString& aBasePath,
219 const nsAString& aRelativePath, nsString& aResult,
220 ErrorResult& aErr) {
221 if (aRelativePath.IsEmpty()) {
222 aResult = aBasePath;
223 return;
226 nsCOMPtr<nsIFile> path = new nsLocalFile();
227 if (nsresult rv = InitFileWithPath(path, aBasePath); NS_FAILED(rv)) {
228 ThrowError(aErr, rv, ERROR_INITIALIZE_PATH);
229 return;
232 if (nsresult rv = path->AppendRelativePath(aRelativePath); NS_FAILED(rv)) {
233 ThrowError(aErr, rv, ERROR_JOIN);
234 return;
237 MOZ_ALWAYS_SUCCEEDS(path->GetPath(aResult));
240 void PathUtils::ToExtendedWindowsPath(const GlobalObject&,
241 const nsAString& aPath, nsString& aResult,
242 ErrorResult& aErr) {
243 #ifndef XP_WIN
244 aErr.ThrowNotAllowedError("Operation is windows specific"_ns);
245 return;
246 #else
247 if (aPath.IsEmpty()) {
248 aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH);
249 return;
252 const nsAString& str1 = Substring(aPath, 1, 1);
253 const nsAString& str2 = Substring(aPath, 2, aPath.Length() - 2);
255 bool isUNC = aPath.Length() >= 2 &&
256 (aPath.First() == '\\' || aPath.First() == '/') &&
257 (str1.EqualsLiteral("\\") || str1.EqualsLiteral("/"));
259 constexpr auto pathPrefix = u"\\\\?\\"_ns;
260 const nsAString& uncPath = pathPrefix + u"UNC\\"_ns + str2;
261 const nsAString& normalPath = pathPrefix + aPath;
263 nsCOMPtr<nsIFile> path = new nsLocalFile();
264 if (nsresult rv = InitFileWithPath(path, isUNC ? uncPath : normalPath);
265 NS_FAILED(rv)) {
266 ThrowError(aErr, rv, ERROR_INITIALIZE_PATH);
267 return;
270 MOZ_ALWAYS_SUCCEEDS(path->GetPath(aResult));
271 #endif
274 void PathUtils::Normalize(const GlobalObject&, const nsAString& aPath,
275 nsString& aResult, ErrorResult& aErr) {
276 if (aPath.IsEmpty()) {
277 aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH);
278 return;
281 nsCOMPtr<nsIFile> path = new nsLocalFile();
282 if (nsresult rv = InitFileWithPath(path, aPath); NS_FAILED(rv)) {
283 ThrowError(aErr, rv, ERROR_INITIALIZE_PATH);
284 return;
287 if (nsresult rv = path->Normalize(); NS_FAILED(rv)) {
288 ThrowError(aErr, rv, "Could not normalize path"_ns);
289 return;
292 MOZ_ALWAYS_SUCCEEDS(path->GetPath(aResult));
295 void PathUtils::Split(const GlobalObject&, const nsAString& aPath,
296 nsTArray<nsString>& aResult, ErrorResult& aErr) {
297 if (aPath.IsEmpty()) {
298 aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH);
299 return;
302 nsCOMPtr<nsIFile> path = new nsLocalFile();
303 if (nsresult rv = InitFileWithPath(path, aPath); NS_FAILED(rv)) {
304 ThrowError(aErr, rv, ERROR_INITIALIZE_PATH);
305 return;
308 while (path) {
309 auto* component = aResult.EmplaceBack(fallible);
310 if (!component) {
311 aErr.Throw(NS_ERROR_OUT_OF_MEMORY);
312 return;
315 nsCOMPtr<nsIFile> parent;
316 if (nsresult rv = path->GetParent(getter_AddRefs(parent)); NS_FAILED(rv)) {
317 ThrowError(aErr, rv, ERROR_GET_PARENT);
318 return;
321 // GetLeafPreservingRoot cannot fail if we pass it a parent path.
322 MOZ_ALWAYS_SUCCEEDS(GetLeafNamePreservingRoot(path, *component, parent));
324 path = parent;
327 aResult.Reverse();
330 void PathUtils::SplitRelative(const GlobalObject& aGlobal,
331 const nsAString& aPath,
332 const SplitRelativeOptions& aOptions,
333 nsTArray<nsString>& aResult, ErrorResult& aErr) {
334 if (aPath.IsEmpty()) {
335 aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH);
336 return;
339 if (DoWindowsPathCheck()) {
340 MOZ_RELEASE_ASSERT(!aPath.Contains(u'/'),
341 "Windows paths cannot include forward slashes");
344 if (IsAbsolute(aGlobal, aPath)) {
345 aErr.ThrowNotAllowedError(
346 "PathUtils.splitRelative requires a relative path"_ns);
347 return;
350 #ifdef XP_WIN
351 constexpr auto SEPARATOR = u'\\';
352 #else
353 constexpr auto SEPARATOR = u'/';
354 #endif
356 constexpr auto PARENT = u".."_ns;
357 constexpr auto CURRENT = u"."_ns;
359 for (const nsAString& pathComponent :
360 nsCharSeparatedTokenizerTemplate<NS_TokenizerIgnoreNothing>{aPath,
361 SEPARATOR}
362 .ToRange()) {
363 if (!aOptions.mAllowEmpty && pathComponent.IsEmpty()) {
364 aErr.ThrowNotAllowedError(
365 "PathUtils.splitRelative: Empty directory components (\"\") not "
366 "allowed by options");
367 return;
370 if (!aOptions.mAllowParentDir && pathComponent == PARENT) {
371 aErr.ThrowNotAllowedError(
372 "PathUtils.splitRelative: Parent directory components (\"..\") not "
373 "allowed by options");
374 return;
377 if (!aOptions.mAllowCurrentDir && pathComponent == CURRENT) {
378 aErr.ThrowNotAllowedError(
379 "PathUtils.splitRelative: Current directory components (\".\") not "
380 "allowed by options");
381 return;
384 aResult.AppendElement(pathComponent);
388 void PathUtils::ToFileURI(const GlobalObject&, const nsAString& aPath,
389 nsCString& aResult, ErrorResult& aErr) {
390 if (aPath.IsEmpty()) {
391 aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH);
392 return;
395 nsCOMPtr<nsIFile> path = new nsLocalFile();
396 if (nsresult rv = InitFileWithPath(path, aPath); NS_FAILED(rv)) {
397 ThrowError(aErr, rv, ERROR_INITIALIZE_PATH);
398 return;
401 if (nsresult rv = net_GetURLSpecFromActualFile(path, aResult);
402 NS_FAILED(rv)) {
403 ThrowError(aErr, rv, "Could not retrieve URI spec"_ns);
404 return;
408 bool PathUtils::IsAbsolute(const GlobalObject&, const nsAString& aPath) {
409 nsCOMPtr<nsIFile> path = new nsLocalFile();
410 nsresult rv = InitFileWithPath(path, aPath);
411 return NS_SUCCEEDED(rv);
414 void PathUtils::GetProfileDirSync(const GlobalObject&, nsString& aResult,
415 ErrorResult& aErr) {
416 MOZ_ASSERT(NS_IsMainThread());
418 auto guard = sDirCache.Lock();
419 DirectoryCache::Ensure(guard.ref())
420 .GetDirectorySync(aResult, aErr, DirectoryCache::Directory::Profile);
422 void PathUtils::GetLocalProfileDirSync(const GlobalObject&, nsString& aResult,
423 ErrorResult& aErr) {
424 MOZ_ASSERT(NS_IsMainThread());
426 auto guard = sDirCache.Lock();
427 DirectoryCache::Ensure(guard.ref())
428 .GetDirectorySync(aResult, aErr, DirectoryCache::Directory::LocalProfile);
430 void PathUtils::GetTempDirSync(const GlobalObject&, nsString& aResult,
431 ErrorResult& aErr) {
432 MOZ_ASSERT(NS_IsMainThread());
434 auto guard = sDirCache.Lock();
435 DirectoryCache::Ensure(guard.ref())
436 .GetDirectorySync(aResult, aErr, DirectoryCache::Directory::Temp);
439 void PathUtils::GetXulLibraryPathSync(const GlobalObject&, nsString& aResult,
440 ErrorResult& aErr) {
441 MOZ_ASSERT(NS_IsMainThread());
443 auto guard = sDirCache.Lock();
444 DirectoryCache::Ensure(guard.ref())
445 .GetDirectorySync(aResult, aErr, DirectoryCache::Directory::XulLibrary);
448 already_AddRefed<Promise> PathUtils::GetProfileDirAsync(
449 const GlobalObject& aGlobal, ErrorResult& aErr) {
450 MOZ_ASSERT(!NS_IsMainThread());
452 auto guard = sDirCache.Lock();
453 return DirectoryCache::Ensure(guard.ref())
454 .GetDirectoryAsync(aGlobal, aErr, DirectoryCache::Directory::Profile);
457 already_AddRefed<Promise> PathUtils::GetLocalProfileDirAsync(
458 const GlobalObject& aGlobal, ErrorResult& aErr) {
459 MOZ_ASSERT(!NS_IsMainThread());
461 auto guard = sDirCache.Lock();
462 return DirectoryCache::Ensure(guard.ref())
463 .GetDirectoryAsync(aGlobal, aErr,
464 DirectoryCache::Directory::LocalProfile);
467 already_AddRefed<Promise> PathUtils::GetTempDirAsync(
468 const GlobalObject& aGlobal, ErrorResult& aErr) {
469 MOZ_ASSERT(!NS_IsMainThread());
471 auto guard = sDirCache.Lock();
472 return DirectoryCache::Ensure(guard.ref())
473 .GetDirectoryAsync(aGlobal, aErr, DirectoryCache::Directory::Temp);
476 already_AddRefed<Promise> PathUtils::GetXulLibraryPathAsync(
477 const GlobalObject& aGlobal, ErrorResult& aErr) {
478 MOZ_ASSERT(!NS_IsMainThread());
480 auto guard = sDirCache.Lock();
481 return DirectoryCache::Ensure(guard.ref())
482 .GetDirectoryAsync(aGlobal, aErr, DirectoryCache::Directory::XulLibrary);
485 PathUtils::DirectoryCache::DirectoryCache() {
486 for (auto& dir : mDirectories) {
487 dir.SetIsVoid(true);
491 PathUtils::DirectoryCache& PathUtils::DirectoryCache::Ensure(
492 Maybe<PathUtils::DirectoryCache>& aCache) {
493 if (aCache.isNothing()) {
494 aCache.emplace();
496 auto clearAtShutdown = []() {
497 RunOnShutdown([]() {
498 auto cache = PathUtils::sDirCache.Lock();
499 cache->reset();
503 if (NS_IsMainThread()) {
504 clearAtShutdown();
505 } else {
506 NS_DispatchToMainThread(
507 NS_NewRunnableFunction(__func__, std::move(clearAtShutdown)));
511 return aCache.ref();
514 void PathUtils::DirectoryCache::GetDirectorySync(
515 nsString& aResult, ErrorResult& aErr, const Directory aRequestedDir) {
516 MOZ_RELEASE_ASSERT(aRequestedDir < Directory::Count);
518 if (nsresult rv = PopulateDirectoriesImpl(aRequestedDir); NS_FAILED(rv)) {
519 nsAutoCStringN<32> errorName;
520 GetErrorName(rv, errorName);
522 nsAutoCStringN<256> msg;
523 msg.Append("Could not retrieve directory "_ns);
524 msg.Append(kDirectoryNames[aRequestedDir]);
525 msg.Append(COLON);
526 msg.Append(errorName);
528 aErr.ThrowUnknownError(msg);
529 return;
532 aResult = mDirectories[aRequestedDir];
535 already_AddRefed<Promise> PathUtils::DirectoryCache::GetDirectoryAsync(
536 const GlobalObject& aGlobal, ErrorResult& aErr,
537 const Directory aRequestedDir) {
538 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
539 RefPtr<Promise> promise = Promise::Create(global, aErr);
540 if (aErr.Failed()) {
541 return nullptr;
544 if (RefPtr<PopulateDirectoriesPromise> p =
545 PopulateDirectories(aRequestedDir)) {
546 p->Then(
547 GetCurrentSerialEventTarget(), __func__,
548 [promise, aRequestedDir](const Ok&) {
549 auto cache = PathUtils::sDirCache.Lock();
550 cache.ref()->ResolveWithDirectory(promise, aRequestedDir);
552 [promise](const nsresult& aRv) { promise->MaybeReject(aRv); });
553 } else {
554 ResolveWithDirectory(promise, aRequestedDir);
557 return promise.forget();
560 void PathUtils::DirectoryCache::ResolveWithDirectory(
561 Promise* aPromise, const Directory aRequestedDir) {
562 MOZ_RELEASE_ASSERT(aRequestedDir < Directory::Count);
563 MOZ_RELEASE_ASSERT(!mDirectories[aRequestedDir].IsVoid());
564 aPromise->MaybeResolve(mDirectories[aRequestedDir]);
567 already_AddRefed<PathUtils::DirectoryCache::PopulateDirectoriesPromise>
568 PathUtils::DirectoryCache::PopulateDirectories(
569 const PathUtils::DirectoryCache::Directory aRequestedDir) {
570 MOZ_RELEASE_ASSERT(aRequestedDir < Directory::Count);
572 // If we have already resolved the requested directory, we can return
573 // immediately.
574 // Otherwise, if we have already fired off a request to populate the entry,
575 // so we can return the corresponding promise immediately. caller will queue
576 // a Thenable onto that promise to resolve/reject the request.
577 if (!mDirectories[aRequestedDir].IsVoid()) {
578 return nullptr;
580 if (!mPromises[aRequestedDir].IsEmpty()) {
581 return mPromises[aRequestedDir].Ensure(__func__);
584 RefPtr<PopulateDirectoriesPromise> promise =
585 mPromises[aRequestedDir].Ensure(__func__);
587 if (NS_IsMainThread()) {
588 nsresult rv = PopulateDirectoriesImpl(aRequestedDir);
589 ResolvePopulateDirectoriesPromise(rv, aRequestedDir);
590 } else {
591 nsCOMPtr<nsIRunnable> runnable =
592 NS_NewRunnableFunction(__func__, [aRequestedDir]() {
593 auto cache = PathUtils::sDirCache.Lock();
594 nsresult rv = cache.ref()->PopulateDirectoriesImpl(aRequestedDir);
595 cache.ref()->ResolvePopulateDirectoriesPromise(rv, aRequestedDir);
597 NS_DispatchToMainThread(runnable.forget());
600 return promise.forget();
603 void PathUtils::DirectoryCache::ResolvePopulateDirectoriesPromise(
604 nsresult aRv, const PathUtils::DirectoryCache::Directory aRequestedDir) {
605 MOZ_RELEASE_ASSERT(aRequestedDir < Directory::Count);
607 if (NS_SUCCEEDED(aRv)) {
608 mPromises[aRequestedDir].Resolve(Ok{}, __func__);
609 } else {
610 mPromises[aRequestedDir].Reject(aRv, __func__);
614 nsresult PathUtils::DirectoryCache::PopulateDirectoriesImpl(
615 const PathUtils::DirectoryCache::Directory aRequestedDir) {
616 MOZ_RELEASE_ASSERT(NS_IsMainThread());
617 MOZ_RELEASE_ASSERT(aRequestedDir < Directory::Count);
619 if (!mDirectories[aRequestedDir].IsVoid()) {
620 // In between when this promise was dispatched to the main thread and now,
621 // the directory cache has had this entry populated (via the
622 // on-main-thread sync method).
623 return NS_OK;
626 nsCOMPtr<nsIFile> path;
628 MOZ_TRY(NS_GetSpecialDirectory(kDirectoryNames[aRequestedDir],
629 getter_AddRefs(path)));
630 MOZ_TRY(path->GetPath(mDirectories[aRequestedDir]));
632 return NS_OK;
635 } // namespace mozilla::dom