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/.
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"
24 #include "nsDirectoryServiceDefs.h"
25 #include "nsDirectoryServiceUtils.h"
27 #include "nsIGlobalObject.h"
28 #include "nsLocalFile.h"
29 #include "nsNetUtil.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
);
55 case NS_ERROR_FILE_UNRECOGNIZED_PATH
:
56 aErr
.ThrowOperationError(formattedMsg
);
59 case NS_ERROR_FILE_ACCESS_DENIED
:
60 aErr
.ThrowInvalidAccessError(formattedMsg
);
63 case NS_ERROR_FAILURE
:
65 aErr
.ThrowUnknownError(formattedMsg
);
70 static bool DoWindowsPathCheck() {
75 return xpc::IsInAutomation();
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");
90 return aFile
->InitWithPath(aPath
);
93 MOZ_RUNINIT StaticDataMutex
<Maybe
<PathUtils::DirectoryCache
>>
94 PathUtils::sDirCache
{"sDirCache"};
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
105 static nsresult
GetLeafNamePreservingRoot(nsIFile
* aFile
, nsString
& aResult
,
106 nsIFile
* aParent
= nullptr) {
109 nsCOMPtr
<nsIFile
> parent
= aParent
;
111 MOZ_TRY(aFile
->GetParent(getter_AddRefs(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
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:);
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
);
137 nsCOMPtr
<nsIFile
> path
= new nsLocalFile();
138 if (nsresult rv
= InitFileWithPath(path
, aPath
); NS_FAILED(rv
)) {
139 ThrowError(aErr
, rv
, ERROR_INITIALIZE_PATH
);
143 if (nsresult rv
= GetLeafNamePreservingRoot(path
, aResult
); NS_FAILED(rv
)) {
144 ThrowError(aErr
, rv
, "Could not get leaf name of path"_ns
);
149 void PathUtils::Parent(const GlobalObject
&, const nsAString
& aPath
,
150 const int32_t aDepth
, nsString
& aResult
,
152 if (aPath
.IsEmpty()) {
153 aErr
.ThrowNotAllowedError(ERROR_EMPTY_PATH
);
157 nsCOMPtr
<nsIFile
> path
= new nsLocalFile();
158 if (nsresult rv
= InitFileWithPath(path
, aPath
); NS_FAILED(rv
)) {
159 ThrowError(aErr
, rv
, ERROR_INITIALIZE_PATH
);
164 aErr
.ThrowNotSupportedError("A depth of at least 1 is required");
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
);
178 MOZ_ALWAYS_SUCCEEDS(parent
->GetPath(aResult
));
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
);
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
);
201 nsCOMPtr
<nsIFile
> path
= new nsLocalFile();
202 if (nsresult rv
= InitFileWithPath(path
, aComponents
[0]); NS_FAILED(rv
)) {
203 ThrowError(aErr
, rv
, ERROR_INITIALIZE_PATH
);
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
);
215 return path
.forget();
218 void PathUtils::JoinRelative(const GlobalObject
&, const nsAString
& aBasePath
,
219 const nsAString
& aRelativePath
, nsString
& aResult
,
221 if (aRelativePath
.IsEmpty()) {
226 nsCOMPtr
<nsIFile
> path
= new nsLocalFile();
227 if (nsresult rv
= InitFileWithPath(path
, aBasePath
); NS_FAILED(rv
)) {
228 ThrowError(aErr
, rv
, ERROR_INITIALIZE_PATH
);
232 if (nsresult rv
= path
->AppendRelativePath(aRelativePath
); NS_FAILED(rv
)) {
233 ThrowError(aErr
, rv
, ERROR_JOIN
);
237 MOZ_ALWAYS_SUCCEEDS(path
->GetPath(aResult
));
240 void PathUtils::ToExtendedWindowsPath(const GlobalObject
&,
241 const nsAString
& aPath
, nsString
& aResult
,
244 aErr
.ThrowNotAllowedError("Operation is windows specific"_ns
);
247 if (aPath
.IsEmpty()) {
248 aErr
.ThrowNotAllowedError(ERROR_EMPTY_PATH
);
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
);
266 ThrowError(aErr
, rv
, ERROR_INITIALIZE_PATH
);
270 MOZ_ALWAYS_SUCCEEDS(path
->GetPath(aResult
));
274 void PathUtils::Normalize(const GlobalObject
&, const nsAString
& aPath
,
275 nsString
& aResult
, ErrorResult
& aErr
) {
276 if (aPath
.IsEmpty()) {
277 aErr
.ThrowNotAllowedError(ERROR_EMPTY_PATH
);
281 nsCOMPtr
<nsIFile
> path
= new nsLocalFile();
282 if (nsresult rv
= InitFileWithPath(path
, aPath
); NS_FAILED(rv
)) {
283 ThrowError(aErr
, rv
, ERROR_INITIALIZE_PATH
);
287 if (nsresult rv
= path
->Normalize(); NS_FAILED(rv
)) {
288 ThrowError(aErr
, rv
, "Could not normalize path"_ns
);
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
);
302 nsCOMPtr
<nsIFile
> path
= new nsLocalFile();
303 if (nsresult rv
= InitFileWithPath(path
, aPath
); NS_FAILED(rv
)) {
304 ThrowError(aErr
, rv
, ERROR_INITIALIZE_PATH
);
309 auto* component
= aResult
.EmplaceBack(fallible
);
311 aErr
.Throw(NS_ERROR_OUT_OF_MEMORY
);
315 nsCOMPtr
<nsIFile
> parent
;
316 if (nsresult rv
= path
->GetParent(getter_AddRefs(parent
)); NS_FAILED(rv
)) {
317 ThrowError(aErr
, rv
, ERROR_GET_PARENT
);
321 // GetLeafPreservingRoot cannot fail if we pass it a parent path.
322 MOZ_ALWAYS_SUCCEEDS(GetLeafNamePreservingRoot(path
, *component
, parent
));
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
);
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
);
351 constexpr auto SEPARATOR
= u
'\\';
353 constexpr auto SEPARATOR
= u
'/';
356 constexpr auto PARENT
= u
".."_ns
;
357 constexpr auto CURRENT
= u
"."_ns
;
359 for (const nsAString
& pathComponent
:
360 nsCharSeparatedTokenizerTemplate
<NS_TokenizerIgnoreNothing
>{aPath
,
363 if (!aOptions
.mAllowEmpty
&& pathComponent
.IsEmpty()) {
364 aErr
.ThrowNotAllowedError(
365 "PathUtils.splitRelative: Empty directory components (\"\") not "
366 "allowed by options");
370 if (!aOptions
.mAllowParentDir
&& pathComponent
== PARENT
) {
371 aErr
.ThrowNotAllowedError(
372 "PathUtils.splitRelative: Parent directory components (\"..\") not "
373 "allowed by options");
377 if (!aOptions
.mAllowCurrentDir
&& pathComponent
== CURRENT
) {
378 aErr
.ThrowNotAllowedError(
379 "PathUtils.splitRelative: Current directory components (\".\") not "
380 "allowed by options");
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
);
395 nsCOMPtr
<nsIFile
> path
= new nsLocalFile();
396 if (nsresult rv
= InitFileWithPath(path
, aPath
); NS_FAILED(rv
)) {
397 ThrowError(aErr
, rv
, ERROR_INITIALIZE_PATH
);
401 if (nsresult rv
= net_GetURLSpecFromActualFile(path
, aResult
);
403 ThrowError(aErr
, rv
, "Could not retrieve URI spec"_ns
);
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
,
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
,
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
,
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
,
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
) {
491 PathUtils::DirectoryCache
& PathUtils::DirectoryCache::Ensure(
492 Maybe
<PathUtils::DirectoryCache
>& aCache
) {
493 if (aCache
.isNothing()) {
496 auto clearAtShutdown
= []() {
498 auto cache
= PathUtils::sDirCache
.Lock();
503 if (NS_IsMainThread()) {
506 NS_DispatchToMainThread(
507 NS_NewRunnableFunction(__func__
, std::move(clearAtShutdown
)));
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
]);
526 msg
.Append(errorName
);
528 aErr
.ThrowUnknownError(msg
);
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
);
544 if (RefPtr
<PopulateDirectoriesPromise
> p
=
545 PopulateDirectories(aRequestedDir
)) {
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
); });
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
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()) {
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
);
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__
);
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).
626 nsCOMPtr
<nsIFile
> path
;
628 MOZ_TRY(NS_GetSpecialDirectory(kDirectoryNames
[aRequestedDir
],
629 getter_AddRefs(path
)));
630 MOZ_TRY(path
->GetPath(mDirectories
[aRequestedDir
]));
635 } // namespace mozilla::dom