Bug 1671035 - Add a WebIDL backed path manipulation utility r=Gijs,nika

PathUtils is a path manipulation component to IOUtils, which is based on
simplified file I/O. This work is part of the larger goal of removing
osfile.jsm et al., ospath.jsm et al., and the entire OS.* namespace, especially
from the startup path.

No equivalent was provided for OS.Path.fromFileURI because it is unused.

Differential Revision: https://phabricator.services.mozilla.com/D95105
This commit is contained in:
Barret Rennie 2020-11-05 18:08:38 +00:00
parent f3863e87b5
commit ac6be32a0a
8 changed files with 726 additions and 0 deletions

View File

@ -0,0 +1,73 @@
/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/**
* PathUtils is a set of utilities for operating on absolute paths.
*/
[ChromeOnly, Exposed=(Window, Worker)]
namespace PathUtils {
/**
* Return the last path component.
*
* @param path An absolute path.
*
* @returns The last path component.
*/
[Throws]
DOMString filename(DOMString path);
/**
* Return the parent directory name of the given path.
*
* @param path An absolute path.
*
* @return The parent directory.
*
* If the path provided is a root path (e.g., `C:` on Windows or `/`
* on *NIX), then null is returned.
*/
[Throws]
DOMString? parent(DOMString path);
/**
* Join the given components into a full path.
*
* @param components The path components. The first component must be an
* absolute path.
*/
[Throws]
DOMString join(DOMString... components);
/**
* Normalize a path by removing multiple separators and `..` and `.`
* directories.
*
* On UNIX platforms, the path must exist as symbolic links will be resolved.
*
* @param path The absolute path to normalize.
*
*/
[Throws]
DOMString normalize(DOMString path);
/**
* Split a path into its components.
*
* @param path An absolute path.
*/
[Throws]
sequence<DOMString> split(DOMString path);
/**
* Transform a file path into a file: URI
*
* @param path An absolute path.
*
* @return The file: URI as a string.
*/
[Throws]
UTF8String toFileURI(DOMString path);
};

View File

@ -65,6 +65,7 @@ WEBIDL_FILES = [
"MozStorageAsyncStatementParams.webidl",
"MozStorageStatementParams.webidl",
"MozStorageStatementRow.webidl",
"PathUtils.webidl",
"PrecompiledScript.webidl",
"PromiseDebugging.webidl",
"SessionStoreUtils.webidl",

242
dom/system/PathUtils.cpp Normal file
View File

@ -0,0 +1,242 @@
/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include "PathUtils.h"
#include "mozilla/ErrorNames.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/RefPtr.h"
#include "mozilla/Span.h"
#include "mozilla/dom/DOMParser.h"
#include "mozilla/dom/PathUtilsBinding.h"
#include "nsIFile.h"
#include "nsLocalFile.h"
#include "nsNetUtil.h"
#include "nsString.h"
namespace mozilla {
namespace dom {
static constexpr auto ERROR_EMPTY_PATH =
"PathUtils does not support empty paths"_ns;
static constexpr auto ERROR_INITIALIZE_PATH = "Could not initialize path"_ns;
static constexpr auto ERROR_GET_PARENT = "Could not get parent path"_ns;
static void ThrowError(ErrorResult& aErr, const nsresult aResult,
const nsCString& aMessage) {
nsAutoCStringN<32> errName;
GetErrorName(aResult, errName);
nsAutoCStringN<256> formattedMsg;
formattedMsg.Append(aMessage);
formattedMsg.Append(": "_ns);
formattedMsg.Append(errName);
switch (aResult) {
case NS_ERROR_FILE_UNRECOGNIZED_PATH:
aErr.ThrowOperationError(formattedMsg);
break;
case NS_ERROR_FILE_ACCESS_DENIED:
aErr.ThrowInvalidAccessError(formattedMsg);
break;
case NS_ERROR_FAILURE:
default:
aErr.ThrowUnknownError(formattedMsg);
break;
}
}
/**
* Return the leaf name, including leading path separators in the case of
* Windows UNC drive paths.
*
* @param aFile The file whose leaf name is to be returned.
* @param aResult The string to hold the resulting leaf name.
* @param aParent The pre-computed parent of |aFile|. If not provided, it will
* be computed.
*/
static nsresult GetLeafNamePreservingRoot(nsIFile* aFile, nsString& aResult,
nsIFile* aParent = nullptr) {
MOZ_ASSERT(aFile);
nsCOMPtr<nsIFile> parent = aParent;
if (!parent) {
MOZ_TRY(aFile->GetParent(getter_AddRefs(parent)));
}
if (parent) {
return aFile->GetLeafName(aResult);
}
// We have reached the root path. On Windows, the leafname for a UNC path
// will not have the leading backslashes, so we need to use the entire path
// here:
//
// * for a UNIX root path (/) this will be /;
// * for a Windows drive path (e.g., C:), this will be the drive path (C:);
// and
// * for a Windows UNC server path (e.g., \\\\server), this will be the full
// server path (\\\\server).
return aFile->GetPath(aResult);
}
void PathUtils::Filename(const GlobalObject&, const nsAString& aPath,
nsString& aResult, ErrorResult& aErr) {
if (aPath.IsEmpty()) {
aErr.ThrowNotAllowedError("PathUtils does not support empty paths");
return;
}
nsCOMPtr<nsIFile> path = new nsLocalFile();
if (nsresult rv = path->InitWithPath(aPath); NS_FAILED(rv)) {
ThrowError(aErr, rv, ERROR_INITIALIZE_PATH);
return;
}
if (nsresult rv = GetLeafNamePreservingRoot(path, aResult); NS_FAILED(rv)) {
ThrowError(aErr, rv, "Could not get leaf name of path"_ns);
return;
}
}
void PathUtils::Parent(const GlobalObject&, const nsAString& aPath,
nsString& aResult, ErrorResult& aErr) {
if (aPath.IsEmpty()) {
aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH);
return;
}
nsCOMPtr<nsIFile> path = new nsLocalFile();
if (nsresult rv = path->InitWithPath(aPath); NS_FAILED(rv)) {
ThrowError(aErr, rv, ERROR_INITIALIZE_PATH);
return;
}
nsCOMPtr<nsIFile> parent;
if (nsresult rv = path->GetParent(getter_AddRefs(parent)); NS_FAILED(rv)) {
ThrowError(aErr, rv, ERROR_GET_PARENT);
return;
}
if (parent) {
MOZ_ALWAYS_SUCCEEDS(parent->GetPath(aResult));
} else {
aResult = VoidString();
}
}
void PathUtils::Join(const GlobalObject&, const Sequence<nsString>& aComponents,
nsString& aResult, ErrorResult& aErr) {
if (aComponents.IsEmpty()) {
return;
}
if (aComponents[0].IsEmpty()) {
aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH);
return;
}
nsCOMPtr<nsIFile> path = new nsLocalFile();
if (nsresult rv = path->InitWithPath(aComponents[0]); NS_FAILED(rv)) {
ThrowError(aErr, rv, ERROR_INITIALIZE_PATH);
return;
}
const auto components = Span<const nsString>(aComponents).Subspan(1);
for (const auto& component : components) {
if (nsresult rv = path->Append(component); NS_FAILED(rv)) {
ThrowError(aErr, rv, "Could not append to path"_ns);
return;
}
}
MOZ_ALWAYS_SUCCEEDS(path->GetPath(aResult));
}
void PathUtils::Normalize(const GlobalObject&, const nsAString& aPath,
nsString& aResult, ErrorResult& aErr) {
if (aPath.IsEmpty()) {
aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH);
return;
}
nsCOMPtr<nsIFile> path = new nsLocalFile();
if (nsresult rv = path->InitWithPath(aPath); NS_FAILED(rv)) {
ThrowError(aErr, rv, ERROR_INITIALIZE_PATH);
return;
}
if (nsresult rv = path->Normalize(); NS_FAILED(rv)) {
ThrowError(aErr, rv, "Could not normalize path"_ns);
return;
}
MOZ_ALWAYS_SUCCEEDS(path->GetPath(aResult));
}
void PathUtils::Split(const GlobalObject&, const nsAString& aPath,
nsTArray<nsString>& aResult, ErrorResult& aErr) {
if (aPath.IsEmpty()) {
aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH);
return;
}
nsCOMPtr<nsIFile> path = new nsLocalFile();
if (nsresult rv = path->InitWithPath(aPath); NS_FAILED(rv)) {
ThrowError(aErr, rv, ERROR_INITIALIZE_PATH);
return;
}
while (path) {
auto* component = aResult.EmplaceBack(fallible);
if (!component) {
aErr.Throw(NS_ERROR_OUT_OF_MEMORY);
return;
}
nsCOMPtr<nsIFile> parent;
if (nsresult rv = path->GetParent(getter_AddRefs(parent)); NS_FAILED(rv)) {
ThrowError(aErr, rv, ERROR_GET_PARENT);
return;
}
// GetLeafPreservingRoot cannot fail if we pass it a parent path.
MOZ_ALWAYS_SUCCEEDS(GetLeafNamePreservingRoot(path, *component, parent));
path = parent;
}
aResult.Reverse();
}
void PathUtils::ToFileURI(const GlobalObject&, const nsAString& aPath,
nsCString& aResult, ErrorResult& aErr) {
if (aPath.IsEmpty()) {
aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH);
return;
}
nsCOMPtr<nsIFile> path = new nsLocalFile();
if (nsresult rv = path->InitWithPath(aPath); NS_FAILED(rv)) {
ThrowError(aErr, rv, ERROR_INITIALIZE_PATH);
return;
}
nsCOMPtr<nsIURI> uri;
if (nsresult rv = NS_NewFileURI(getter_AddRefs(uri), path); NS_FAILED(rv)) {
ThrowError(aErr, rv, "Could not initialize File URI"_ns);
return;
}
if (nsresult rv = uri->GetSpec(aResult); NS_FAILED(rv)) {
ThrowError(aErr, rv, "Could not retrieve URI spec"_ns);
return;
}
}
} // namespace dom
} // namespace mozilla

42
dom/system/PathUtils.h Normal file
View File

@ -0,0 +1,42 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_dom_PathUtils__
#define mozilla_dom_PathUtils__
#include "mozilla/ErrorResult.h"
#include "mozilla/dom/DOMParser.h"
#include "nsString.h"
#include "nsTArray.h"
namespace mozilla {
namespace dom {
class PathUtils final {
public:
static void Filename(const GlobalObject&, const nsAString& aPath,
nsString& aResult, ErrorResult& aErr);
static void Parent(const GlobalObject&, const nsAString& aPath,
nsString& aResult, ErrorResult& aErr);
static void Join(const GlobalObject&, const Sequence<nsString>& aComponents,
nsString& aResult, ErrorResult& aErr);
static void Normalize(const GlobalObject&, const nsAString& aPath,
nsString& aResult, ErrorResult& aErr);
static void Split(const GlobalObject&, const nsAString& aPath,
nsTArray<nsString>& aResult, ErrorResult& aErr);
static void ToFileURI(const GlobalObject&, const nsAString& aPath,
nsCString& aResult, ErrorResult& aErr);
};
} // namespace dom
} // namespace mozilla
#endif // namespace mozilla_dom_PathUtils__

View File

@ -75,6 +75,7 @@ EXPORTS.mozilla += [
EXPORTS.mozilla.dom += [
"IOUtils.h",
"PathUtils.h",
]
UNIFIED_SOURCES += [
@ -82,6 +83,7 @@ UNIFIED_SOURCES += [
"nsDeviceSensors.cpp",
"nsOSPermissionRequestBase.cpp",
"OSFileConstants.cpp",
"PathUtils.cpp",
]
EXTRA_JS_MODULES += [

View File

@ -3,3 +3,4 @@ support-files =
worker_constants.js
[test_constants.xhtml]
[test_pathutils.html]

View File

@ -0,0 +1,364 @@
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>PathUtils tests</title>
</head>
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script>
"use strict";
const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
const { Services } = ChromeUtils.import(
"resource://gre/modules/Services.jsm"
);
const UNRECOGNIZED_PATH = /Could not initialize path: NS_ERROR_FILE_UNRECOGNIZED_PATH/;
const EMPTY_PATH = /PathUtils does not support empty paths/;
add_task(function test_filename() {
Assert.throws(
() => PathUtils.filename(""),
EMPTY_PATH,
"PathUtils.filename() does not support empty paths"
);
Assert.throws(
() => PathUtils.filename("foo.txt"),
UNRECOGNIZED_PATH,
"PathUtils.filename() does not support relative paths"
);
if (Services.appinfo.OS === "WINNT") {
is(
PathUtils.filename("C:"),
"C:",
"PathUtils.filename() with a drive path"
);
is(
PathUtils.filename("C:\\"),
"C:",
"PathUtils.filename() with a drive path"
);
is(
PathUtils.filename("C:\\Windows"),
"Windows",
"PathUtils.filename() with a path with 2 components"
);
is(
PathUtils.filename("C:\\Windows\\"),
"Windows",
"PathUtils.filename() with a path with 2 components and a trailing slash"
);
is(
PathUtils.filename("C:\\Windows\\System32"),
"System32",
"PathUtils.filename() with a path with 3 components"
);
is(
PathUtils.filename("\\\\server"),
"\\\\server",
"PathUtils.filename() with a UNC server path"
);
is(
PathUtils.filename("C:\\file.dat"),
"file.dat",
"PathUtils.filename() with a file path"
);
} else {
is(
PathUtils.filename("/"),
"/",
"PathUtils.filename() with a root path"
);
is(
PathUtils.filename("/usr/"),
"usr",
"PathUtils.filename() with a non-root path"
);
is(
PathUtils.filename("/usr/lib/libfoo.so"),
"libfoo.so",
"PathUtils.filename() with a path with 3 components"
);
}
});
add_task(function test_parent() {
Assert.throws(
() => PathUtils.parent("."),
UNRECOGNIZED_PATH,
"PathUtils.parent() does not support relative paths"
);
Assert.throws(
() => PathUtils.parent(""),
EMPTY_PATH,
"PathUtils.parent() does not support empty paths"
);
if (Services.appinfo.OS === "WINNT") {
is(
PathUtils.parent("C:"),
null,
"PathUtils.parent() with a drive path"
);
is(
PathUtils.parent("\\\\server"),
null,
"PathUtils.parent() with a UNC server path"
);
is(
PathUtils.parent("\\\\server\\foo"),
"\\\\server",
"PathUtils.parent() with a UNC server path and child component"
);
} else {
is(
PathUtils.parent("/"),
null,
"PathUtils.parent() with a root path"
);
is(
PathUtils.parent("/var"),
"/",
"PathUtils.parent() with a 2 component path"
);
is(
PathUtils.parent("/var/run"),
"/var",
"PathUtils.parent() with a 3 component path"
);
}
});
add_task(function test_join() {
is(
PathUtils.join(),
"",
"PathUtils.join() with an empty sequence"
);
Assert.throws(
() => PathUtils.join(""),
EMPTY_PATH,
"PathUtils.join() does not support empty paths"
);
Assert.throws(
() => PathUtils.join("foo", "bar"),
UNRECOGNIZED_PATH,
"PathUtils.join() does not support relative paths"
);
Assert.throws(
() => PathUtils.join("."),
UNRECOGNIZED_PATH,
"PathUtils.join() does not support relative paths"
);
if (Services.appinfo.OS === "WINNT") {
is(
PathUtils.join("C:"),
"C:",
"PathUtils.join() with a single path"
);
is(
PathUtils.join("C:\\Windows", "System32"),
"C:\\Windows\\System32",
"PathUtils.join() with a 2 component path and an additional component"
);
is(
PathUtils.join("C:", "Users", "Example"),
"C:\\Users\\Example",
"PathUtils.join() with a root path and two additional components"
);
is(
PathUtils.join("\\\\server", "Files", "Example.dat"),
"\\\\server\\Files\\Example.dat",
"PathUtils.join() with a server path"
);
} else {
is(
PathUtils.join("/"),
"/",
"PathUtils.join() with a root path"
);
is(
PathUtils.join("/usr", "lib"),
"/usr/lib",
"PathUtils.join() with a 2 component path and an additional component"
);
is(
PathUtils.join("/", "home", "example"),
"/home/example",
"PathUtils.join() with a root path and two additional components"
);
}
});
add_task(async function test_normalize() {
Assert.throws(
() => PathUtils.normalize(""),
EMPTY_PATH,
"PathUtils.normalize() does not support empty paths"
);
Assert.throws(
() => PathUtils.normalize("."),
UNRECOGNIZED_PATH,
"PathUtils.normalize() does not support relative paths"
);
if (Services.appinfo.OS === "WINNT") {
is(
PathUtils.normalize("C:\\\\Windows\\\\..\\\\\\.\\Users\\..\\Windows"),
"C:\\Windows",
"PathUtils.normalize() with a non-normalized path"
);
} else {
// nsLocalFileUnix::Normalize() calls realpath, which resolves symlinks
// and requires the file to exist.
//
// On Darwin, the temp directory is located in `/private/var`, which is a
// symlink to `/var`, so we need to pre-normalize our temporary directory
// or expected paths won't match.
const tmpDir = PathUtils.join(
PathUtils.normalize(Services.dirsvc.get("TmpD", Ci.nsIFile).path),
"pathutils_test"
);
await IOUtils.makeDirectory(tmpDir, { ignoreExisting: true });
info(`created tmpDir ${tmpDir}`);
SimpleTest.registerCleanupFunction(async () => {
await IOUtils.remove(tmpDir, {
recursive: true,
});
});
await IOUtils.makeDirectory(PathUtils.join(tmpDir, "foo", "bar"), {
createAncestors: true,
});
is(
PathUtils.normalize("/"),
"/",
"PathUtils.normalize() with a normalized path"
);
is(
PathUtils.normalize(
PathUtils.join(
tmpDir,
"foo",
".",
"..",
"foo",
".",
"bar",
"..",
"bar"
)
),
PathUtils.join(tmpDir, "foo", "bar"),
"PathUtils.normalize() with a non-normalized path"
);
}
});
add_task(function test_split() {
Assert.throws(
() => PathUtils.split("foo"),
UNRECOGNIZED_PATH,
"PathUtils.split() does not support relative paths"
);
Assert.throws(
() => PathUtils.split(""),
EMPTY_PATH,
"PathUtils.split() does not support empty paths"
);
if (Services.appinfo.OS === "WINNT") {
Assert.deepEqual(
PathUtils.split("C:\\Users\\Example"),
["C:", "Users", "Example"],
"PathUtils.split() on an absolute path"
);
Assert.deepEqual(
PathUtils.split("C:\\Users\\Example\\"),
["C:", "Users", "Example"],
"PathUtils.split() on an absolute path with a trailing slash"
);
Assert.deepEqual(
PathUtils.split("\\\\server\\Files\\Example.dat"),
["\\\\server", "Files", "Example.dat"],
"PathUtils.split() with a server as the root"
);
} else {
Assert.deepEqual(
PathUtils.split("/home/foo"),
["/", "home", "foo"],
"PathUtils.split() on absolute path"
);
Assert.deepEqual(
PathUtils.split("/home/foo/"),
["/", "home", "foo"],
"PathUtils.split() on absolute path with trailing slash"
);
}
});
add_task(function test_toFileURI() {
Assert.throws(
() => PathUtils.toFileURI("."),
UNRECOGNIZED_PATH,
"PathUtils.toFileURI() does not support relative paths"
);
Assert.throws(
() => PathUtils.toFileURI(""),
EMPTY_PATH,
"Pathutils.toFileURI() does not support empty paths"
);
if (Services.appinfo.OS === "WINNT") {
is(
PathUtils.toFileURI("C:\\"),
"file:///C:/",
"PathUtils.toFileURI() with a root path"
);
is(
PathUtils.toFileURI("C:\\Windows\\"),
"file:///C:/Windows/",
"PathUtils.toFileURI() with a non-root directory path"
);
is(
PathUtils.toFileURI("C:\\Windows\\system32\\notepad.exe"),
"file:///C:/Windows/system32/notepad.exe",
"PathUtils.toFileURI() with a file path"
);
} else {
is(
PathUtils.toFileURI("/"),
"file:///",
"PathUtils.toFileURI() with a root path"
);
is(
PathUtils.toFileURI("/bin"),
"file:///bin/",
"PathUtils.toFileURI() with a non-root directory path"
);
is(
PathUtils.toFileURI("/bin/ls"),
"file:///bin/ls",
"PathUtils.toFileURI() with a file path"
);
}
});
</script>
<body>
</body>
</html>

View File

@ -402,6 +402,7 @@ module.exports = {
PannerNode: false,
ParentProcessMessageManager: false,
Path2D: false,
PathUtils: false,
PaymentAddress: false,
PaymentMethodChangeEvent: false,
PaymentRequest: false,