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/Result.h"
17 #include "mozilla/ResultExtensions.h"
18 #include "mozilla/Span.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 "xpcpublic.h"
33 namespace mozilla::dom
{
35 static constexpr auto ERROR_EMPTY_PATH
=
36 "PathUtils does not support empty paths"_ns
;
37 static constexpr auto ERROR_INITIALIZE_PATH
= "Could not initialize path"_ns
;
38 static constexpr auto ERROR_GET_PARENT
= "Could not get parent path"_ns
;
39 static constexpr auto ERROR_JOIN
= "Could not append to path"_ns
;
41 static constexpr auto COLON
= ": "_ns
;
43 static void ThrowError(ErrorResult
& aErr
, const nsresult aResult
,
44 const nsCString
& aMessage
) {
45 nsAutoCStringN
<32> errName
;
46 GetErrorName(aResult
, errName
);
48 nsAutoCStringN
<256> formattedMsg
;
49 formattedMsg
.Append(aMessage
);
50 formattedMsg
.Append(COLON
);
51 formattedMsg
.Append(errName
);
54 case NS_ERROR_FILE_UNRECOGNIZED_PATH
:
55 aErr
.ThrowOperationError(formattedMsg
);
58 case NS_ERROR_FILE_ACCESS_DENIED
:
59 aErr
.ThrowInvalidAccessError(formattedMsg
);
62 case NS_ERROR_FAILURE
:
64 aErr
.ThrowUnknownError(formattedMsg
);
69 static bool DoWindowsPathCheck() {
74 return xpc::IsInAutomation();
82 nsresult
PathUtils::InitFileWithPath(nsIFile
* aFile
, const nsAString
& aPath
) {
83 if (DoWindowsPathCheck()) {
84 MOZ_RELEASE_ASSERT(!aPath
.Contains(u
'/'),
85 "Windows paths cannot include forward slashes");
89 return aFile
->InitWithPath(aPath
);
92 StaticDataMutex
<Maybe
<PathUtils::DirectoryCache
>> PathUtils::sDirCache
{
96 * Return the leaf name, including leading path separators in the case of
97 * Windows UNC drive paths.
99 * @param aFile The file whose leaf name is to be returned.
100 * @param aResult The string to hold the resulting leaf name.
101 * @param aParent The pre-computed parent of |aFile|. If not provided, it will
104 static nsresult
GetLeafNamePreservingRoot(nsIFile
* aFile
, nsString
& aResult
,
105 nsIFile
* aParent
= nullptr) {
108 nsCOMPtr
<nsIFile
> parent
= aParent
;
110 MOZ_TRY(aFile
->GetParent(getter_AddRefs(parent
)));
114 return aFile
->GetLeafName(aResult
);
117 // We have reached the root path. On Windows, the leafname for a UNC path
118 // will not have the leading backslashes, so we need to use the entire path
121 // * for a UNIX root path (/) this will be /;
122 // * for a Windows drive path (e.g., C:), this will be the drive path (C:);
124 // * for a Windows UNC server path (e.g., \\\\server), this will be the full
125 // server path (\\\\server).
126 return aFile
->GetPath(aResult
);
129 void PathUtils::Filename(const GlobalObject
&, const nsAString
& aPath
,
130 nsString
& aResult
, ErrorResult
& aErr
) {
131 if (aPath
.IsEmpty()) {
132 aErr
.ThrowNotAllowedError(ERROR_EMPTY_PATH
);
136 nsCOMPtr
<nsIFile
> path
= new nsLocalFile();
137 if (nsresult rv
= InitFileWithPath(path
, aPath
); NS_FAILED(rv
)) {
138 ThrowError(aErr
, rv
, ERROR_INITIALIZE_PATH
);
142 if (nsresult rv
= GetLeafNamePreservingRoot(path
, aResult
); NS_FAILED(rv
)) {
143 ThrowError(aErr
, rv
, "Could not get leaf name of path"_ns
);
148 void PathUtils::Parent(const GlobalObject
&, const nsAString
& aPath
,
149 const int32_t aDepth
, nsString
& aResult
,
151 if (aPath
.IsEmpty()) {
152 aErr
.ThrowNotAllowedError(ERROR_EMPTY_PATH
);
156 nsCOMPtr
<nsIFile
> path
= new nsLocalFile();
157 if (nsresult rv
= InitFileWithPath(path
, aPath
); NS_FAILED(rv
)) {
158 ThrowError(aErr
, rv
, ERROR_INITIALIZE_PATH
);
163 aErr
.ThrowNotSupportedError("A depth of at least 1 is required");
167 nsCOMPtr
<nsIFile
> parent
;
168 for (int32_t i
= 0; path
&& i
< aDepth
; i
++) {
169 if (nsresult rv
= path
->GetParent(getter_AddRefs(parent
)); NS_FAILED(rv
)) {
170 ThrowError(aErr
, rv
, ERROR_GET_PARENT
);
177 MOZ_ALWAYS_SUCCEEDS(parent
->GetPath(aResult
));
179 aResult
= VoidString();
183 void PathUtils::Join(const GlobalObject
&, const Sequence
<nsString
>& aComponents
,
184 nsString
& aResult
, ErrorResult
& aErr
) {
185 nsCOMPtr
<nsIFile
> path
= Join(Span(aComponents
), aErr
);
190 MOZ_ALWAYS_SUCCEEDS(path
->GetPath(aResult
));
193 already_AddRefed
<nsIFile
> PathUtils::Join(
194 const Span
<const nsString
>& aComponents
, ErrorResult
& aErr
) {
195 if (aComponents
.IsEmpty() || aComponents
[0].IsEmpty()) {
196 aErr
.ThrowNotAllowedError(ERROR_EMPTY_PATH
);
200 nsCOMPtr
<nsIFile
> path
= new nsLocalFile();
201 if (nsresult rv
= InitFileWithPath(path
, aComponents
[0]); NS_FAILED(rv
)) {
202 ThrowError(aErr
, rv
, ERROR_INITIALIZE_PATH
);
206 const auto components
= Span
<const nsString
>(aComponents
).Subspan(1);
207 for (const auto& component
: components
) {
208 if (nsresult rv
= path
->Append(component
); NS_FAILED(rv
)) {
209 ThrowError(aErr
, rv
, ERROR_JOIN
);
214 return path
.forget();
217 void PathUtils::JoinRelative(const GlobalObject
&, const nsAString
& aBasePath
,
218 const nsAString
& aRelativePath
, nsString
& aResult
,
220 if (aRelativePath
.IsEmpty()) {
225 nsCOMPtr
<nsIFile
> path
= new nsLocalFile();
226 if (nsresult rv
= InitFileWithPath(path
, aBasePath
); NS_FAILED(rv
)) {
227 ThrowError(aErr
, rv
, ERROR_INITIALIZE_PATH
);
231 if (nsresult rv
= path
->AppendRelativePath(aRelativePath
); NS_FAILED(rv
)) {
232 ThrowError(aErr
, rv
, ERROR_JOIN
);
236 MOZ_ALWAYS_SUCCEEDS(path
->GetPath(aResult
));
239 void PathUtils::ToExtendedWindowsPath(const GlobalObject
&,
240 const nsAString
& aPath
, nsString
& aResult
,
243 aErr
.ThrowNotAllowedError("Operation is windows specific"_ns
);
246 if (aPath
.IsEmpty()) {
247 aErr
.ThrowNotAllowedError(ERROR_EMPTY_PATH
);
251 const nsAString
& str1
= Substring(aPath
, 1, 1);
252 const nsAString
& str2
= Substring(aPath
, 2, aPath
.Length() - 2);
254 bool isUNC
= aPath
.Length() >= 2 &&
255 (aPath
.First() == '\\' || aPath
.First() == '/') &&
256 (str1
.EqualsLiteral("\\") || str1
.EqualsLiteral("/"));
258 constexpr auto pathPrefix
= u
"\\\\?\\"_ns
;
259 const nsAString
& uncPath
= pathPrefix
+ u
"UNC\\"_ns
+ str2
;
260 const nsAString
& normalPath
= pathPrefix
+ aPath
;
262 nsCOMPtr
<nsIFile
> path
= new nsLocalFile();
263 if (nsresult rv
= InitFileWithPath(path
, isUNC
? uncPath
: normalPath
);
265 ThrowError(aErr
, rv
, ERROR_INITIALIZE_PATH
);
269 MOZ_ALWAYS_SUCCEEDS(path
->GetPath(aResult
));
273 void PathUtils::Normalize(const GlobalObject
&, const nsAString
& aPath
,
274 nsString
& aResult
, ErrorResult
& aErr
) {
275 if (aPath
.IsEmpty()) {
276 aErr
.ThrowNotAllowedError(ERROR_EMPTY_PATH
);
280 nsCOMPtr
<nsIFile
> path
= new nsLocalFile();
281 if (nsresult rv
= InitFileWithPath(path
, aPath
); NS_FAILED(rv
)) {
282 ThrowError(aErr
, rv
, ERROR_INITIALIZE_PATH
);
286 if (nsresult rv
= path
->Normalize(); NS_FAILED(rv
)) {
287 ThrowError(aErr
, rv
, "Could not normalize path"_ns
);
291 MOZ_ALWAYS_SUCCEEDS(path
->GetPath(aResult
));
294 void PathUtils::Split(const GlobalObject
&, const nsAString
& aPath
,
295 nsTArray
<nsString
>& aResult
, ErrorResult
& aErr
) {
296 if (aPath
.IsEmpty()) {
297 aErr
.ThrowNotAllowedError(ERROR_EMPTY_PATH
);
301 nsCOMPtr
<nsIFile
> path
= new nsLocalFile();
302 if (nsresult rv
= InitFileWithPath(path
, aPath
); NS_FAILED(rv
)) {
303 ThrowError(aErr
, rv
, ERROR_INITIALIZE_PATH
);
308 auto* component
= aResult
.EmplaceBack(fallible
);
310 aErr
.Throw(NS_ERROR_OUT_OF_MEMORY
);
314 nsCOMPtr
<nsIFile
> parent
;
315 if (nsresult rv
= path
->GetParent(getter_AddRefs(parent
)); NS_FAILED(rv
)) {
316 ThrowError(aErr
, rv
, ERROR_GET_PARENT
);
320 // GetLeafPreservingRoot cannot fail if we pass it a parent path.
321 MOZ_ALWAYS_SUCCEEDS(GetLeafNamePreservingRoot(path
, *component
, parent
));
329 void PathUtils::SplitRelative(const GlobalObject
& aGlobal
,
330 const nsAString
& aPath
,
331 const SplitRelativeOptions
& aOptions
,
332 nsTArray
<nsString
>& aResult
, ErrorResult
& aErr
) {
333 if (aPath
.IsEmpty()) {
334 aErr
.ThrowNotAllowedError(ERROR_EMPTY_PATH
);
338 if (DoWindowsPathCheck()) {
339 MOZ_RELEASE_ASSERT(!aPath
.Contains(u
'/'),
340 "Windows paths cannot include forward slashes");
343 if (IsAbsolute(aGlobal
, aPath
)) {
344 aErr
.ThrowNotAllowedError(
345 "PathUtils.splitRelative requires a relative path"_ns
);
350 constexpr auto SEPARATOR
= u
'\\';
352 constexpr auto SEPARATOR
= u
'/';
355 constexpr auto PARENT
= u
".."_ns
;
356 constexpr auto CURRENT
= u
"."_ns
;
358 for (const nsAString
& pathComponent
:
359 nsCharSeparatedTokenizerTemplate
<NS_TokenizerIgnoreNothing
>{aPath
,
362 if (!aOptions
.mAllowEmpty
&& pathComponent
.IsEmpty()) {
363 aErr
.ThrowNotAllowedError(
364 "PathUtils.splitRelative: Empty directory components (\"\") not "
365 "allowed by options");
369 if (!aOptions
.mAllowParentDir
&& pathComponent
== PARENT
) {
370 aErr
.ThrowNotAllowedError(
371 "PathUtils.splitRelative: Parent directory components (\"..\") not "
372 "allowed by options");
376 if (!aOptions
.mAllowCurrentDir
&& pathComponent
== CURRENT
) {
377 aErr
.ThrowNotAllowedError(
378 "PathUtils.splitRelative: Current directory components (\".\") not "
379 "allowed by options");
383 aResult
.AppendElement(pathComponent
);
387 void PathUtils::ToFileURI(const GlobalObject
&, const nsAString
& aPath
,
388 nsCString
& aResult
, ErrorResult
& aErr
) {
389 if (aPath
.IsEmpty()) {
390 aErr
.ThrowNotAllowedError(ERROR_EMPTY_PATH
);
394 nsCOMPtr
<nsIFile
> path
= new nsLocalFile();
395 if (nsresult rv
= InitFileWithPath(path
, aPath
); NS_FAILED(rv
)) {
396 ThrowError(aErr
, rv
, ERROR_INITIALIZE_PATH
);
400 nsCOMPtr
<nsIURI
> uri
;
401 if (nsresult rv
= NS_NewFileURI(getter_AddRefs(uri
), path
); NS_FAILED(rv
)) {
402 ThrowError(aErr
, rv
, "Could not initialize File URI"_ns
);
406 if (nsresult rv
= uri
->GetSpec(aResult
); NS_FAILED(rv
)) {
407 ThrowError(aErr
, rv
, "Could not retrieve URI spec"_ns
);
412 bool PathUtils::IsAbsolute(const GlobalObject
&, const nsAString
& aPath
) {
413 nsCOMPtr
<nsIFile
> path
= new nsLocalFile();
414 nsresult rv
= InitFileWithPath(path
, aPath
);
415 return NS_SUCCEEDED(rv
);
418 void PathUtils::GetProfileDirSync(const GlobalObject
&, nsString
& aResult
,
420 MOZ_ASSERT(NS_IsMainThread());
422 auto guard
= sDirCache
.Lock();
423 DirectoryCache::Ensure(guard
.ref())
424 .GetDirectorySync(aResult
, aErr
, DirectoryCache::Directory::Profile
);
426 void PathUtils::GetLocalProfileDirSync(const GlobalObject
&, nsString
& aResult
,
428 MOZ_ASSERT(NS_IsMainThread());
430 auto guard
= sDirCache
.Lock();
431 DirectoryCache::Ensure(guard
.ref())
432 .GetDirectorySync(aResult
, aErr
, DirectoryCache::Directory::LocalProfile
);
434 void PathUtils::GetTempDirSync(const GlobalObject
&, nsString
& aResult
,
436 MOZ_ASSERT(NS_IsMainThread());
438 auto guard
= sDirCache
.Lock();
439 DirectoryCache::Ensure(guard
.ref())
440 .GetDirectorySync(aResult
, aErr
, DirectoryCache::Directory::Temp
);
443 void PathUtils::GetXulLibraryPathSync(const GlobalObject
&, nsString
& aResult
,
445 MOZ_ASSERT(NS_IsMainThread());
447 auto guard
= sDirCache
.Lock();
448 DirectoryCache::Ensure(guard
.ref())
449 .GetDirectorySync(aResult
, aErr
, DirectoryCache::Directory::XulLibrary
);
452 already_AddRefed
<Promise
> PathUtils::GetProfileDirAsync(
453 const GlobalObject
& aGlobal
, ErrorResult
& aErr
) {
454 MOZ_ASSERT(!NS_IsMainThread());
456 auto guard
= sDirCache
.Lock();
457 return DirectoryCache::Ensure(guard
.ref())
458 .GetDirectoryAsync(aGlobal
, aErr
, DirectoryCache::Directory::Profile
);
461 already_AddRefed
<Promise
> PathUtils::GetLocalProfileDirAsync(
462 const GlobalObject
& aGlobal
, ErrorResult
& aErr
) {
463 MOZ_ASSERT(!NS_IsMainThread());
465 auto guard
= sDirCache
.Lock();
466 return DirectoryCache::Ensure(guard
.ref())
467 .GetDirectoryAsync(aGlobal
, aErr
,
468 DirectoryCache::Directory::LocalProfile
);
471 already_AddRefed
<Promise
> PathUtils::GetTempDirAsync(
472 const GlobalObject
& aGlobal
, ErrorResult
& aErr
) {
473 MOZ_ASSERT(!NS_IsMainThread());
475 auto guard
= sDirCache
.Lock();
476 return DirectoryCache::Ensure(guard
.ref())
477 .GetDirectoryAsync(aGlobal
, aErr
, DirectoryCache::Directory::Temp
);
480 already_AddRefed
<Promise
> PathUtils::GetXulLibraryPathAsync(
481 const GlobalObject
& aGlobal
, ErrorResult
& aErr
) {
482 MOZ_ASSERT(!NS_IsMainThread());
484 auto guard
= sDirCache
.Lock();
485 return DirectoryCache::Ensure(guard
.ref())
486 .GetDirectoryAsync(aGlobal
, aErr
, DirectoryCache::Directory::XulLibrary
);
489 PathUtils::DirectoryCache::DirectoryCache() {
490 for (auto& dir
: mDirectories
) {
495 PathUtils::DirectoryCache
& PathUtils::DirectoryCache::Ensure(
496 Maybe
<PathUtils::DirectoryCache
>& aCache
) {
497 if (aCache
.isNothing()) {
500 auto clearAtShutdown
= []() {
502 auto cache
= PathUtils::sDirCache
.Lock();
507 if (NS_IsMainThread()) {
510 NS_DispatchToMainThread(
511 NS_NewRunnableFunction(__func__
, std::move(clearAtShutdown
)));
518 void PathUtils::DirectoryCache::GetDirectorySync(
519 nsString
& aResult
, ErrorResult
& aErr
, const Directory aRequestedDir
) {
520 MOZ_RELEASE_ASSERT(aRequestedDir
< Directory::Count
);
522 if (nsresult rv
= PopulateDirectoriesImpl(aRequestedDir
); NS_FAILED(rv
)) {
523 nsAutoCStringN
<32> errorName
;
524 GetErrorName(rv
, errorName
);
526 nsAutoCStringN
<256> msg
;
527 msg
.Append("Could not retrieve directory "_ns
);
528 msg
.Append(kDirectoryNames
[aRequestedDir
]);
530 msg
.Append(errorName
);
532 aErr
.ThrowUnknownError(msg
);
536 aResult
= mDirectories
[aRequestedDir
];
539 already_AddRefed
<Promise
> PathUtils::DirectoryCache::GetDirectoryAsync(
540 const GlobalObject
& aGlobal
, ErrorResult
& aErr
,
541 const Directory aRequestedDir
) {
542 nsCOMPtr
<nsIGlobalObject
> global
= do_QueryInterface(aGlobal
.GetAsSupports());
543 RefPtr
<Promise
> promise
= Promise::Create(global
, aErr
);
548 if (RefPtr
<PopulateDirectoriesPromise
> p
=
549 PopulateDirectories(aRequestedDir
)) {
551 GetCurrentSerialEventTarget(), __func__
,
552 [promise
, aRequestedDir
](const Ok
&) {
553 auto cache
= PathUtils::sDirCache
.Lock();
554 cache
.ref()->ResolveWithDirectory(promise
, aRequestedDir
);
556 [promise
](const nsresult
& aRv
) { promise
->MaybeReject(aRv
); });
558 ResolveWithDirectory(promise
, aRequestedDir
);
561 return promise
.forget();
564 void PathUtils::DirectoryCache::ResolveWithDirectory(
565 Promise
* aPromise
, const Directory aRequestedDir
) {
566 MOZ_RELEASE_ASSERT(aRequestedDir
< Directory::Count
);
567 MOZ_RELEASE_ASSERT(!mDirectories
[aRequestedDir
].IsVoid());
568 aPromise
->MaybeResolve(mDirectories
[aRequestedDir
]);
571 already_AddRefed
<PathUtils::DirectoryCache::PopulateDirectoriesPromise
>
572 PathUtils::DirectoryCache::PopulateDirectories(
573 const PathUtils::DirectoryCache::Directory aRequestedDir
) {
574 MOZ_RELEASE_ASSERT(aRequestedDir
< Directory::Count
);
576 // If we have already resolved the requested directory, we can return
578 // Otherwise, if we have already fired off a request to populate the entry,
579 // so we can return the corresponding promise immediately. caller will queue
580 // a Thenable onto that promise to resolve/reject the request.
581 if (!mDirectories
[aRequestedDir
].IsVoid()) {
584 if (!mPromises
[aRequestedDir
].IsEmpty()) {
585 return mPromises
[aRequestedDir
].Ensure(__func__
);
588 RefPtr
<PopulateDirectoriesPromise
> promise
=
589 mPromises
[aRequestedDir
].Ensure(__func__
);
591 if (NS_IsMainThread()) {
592 nsresult rv
= PopulateDirectoriesImpl(aRequestedDir
);
593 ResolvePopulateDirectoriesPromise(rv
, aRequestedDir
);
595 nsCOMPtr
<nsIRunnable
> runnable
=
596 NS_NewRunnableFunction(__func__
, [aRequestedDir
]() {
597 auto cache
= PathUtils::sDirCache
.Lock();
598 nsresult rv
= cache
.ref()->PopulateDirectoriesImpl(aRequestedDir
);
599 cache
.ref()->ResolvePopulateDirectoriesPromise(rv
, aRequestedDir
);
601 NS_DispatchToMainThread(runnable
.forget());
604 return promise
.forget();
607 void PathUtils::DirectoryCache::ResolvePopulateDirectoriesPromise(
608 nsresult aRv
, const PathUtils::DirectoryCache::Directory aRequestedDir
) {
609 MOZ_RELEASE_ASSERT(aRequestedDir
< Directory::Count
);
611 if (NS_SUCCEEDED(aRv
)) {
612 mPromises
[aRequestedDir
].Resolve(Ok
{}, __func__
);
614 mPromises
[aRequestedDir
].Reject(aRv
, __func__
);
618 nsresult
PathUtils::DirectoryCache::PopulateDirectoriesImpl(
619 const PathUtils::DirectoryCache::Directory aRequestedDir
) {
620 MOZ_RELEASE_ASSERT(NS_IsMainThread());
621 MOZ_RELEASE_ASSERT(aRequestedDir
< Directory::Count
);
623 if (!mDirectories
[aRequestedDir
].IsVoid()) {
624 // In between when this promise was dispatched to the main thread and now,
625 // the directory cache has had this entry populated (via the
626 // on-main-thread sync method).
630 nsCOMPtr
<nsIFile
> path
;
632 MOZ_TRY(NS_GetSpecialDirectory(kDirectoryNames
[aRequestedDir
],
633 getter_AddRefs(path
)));
634 MOZ_TRY(path
->GetPath(mDirectories
[aRequestedDir
]));
639 } // namespace mozilla::dom