diff --git a/src/main/js/Commands.ts b/src/main/js/Commands.ts
index e448094..6f483b2 100644
--- a/src/main/js/Commands.ts
+++ b/src/main/js/Commands.ts
@@ -267,6 +267,7 @@ export class Commands {
return node.contents;
})
+ .filter(it => it !== "")
.join("\n");
}
@@ -367,8 +368,8 @@ export class Commands {
return [path, `'${path}' is not a directory.`];
const dirList = [
- new Directory({}).nameString("./", path),
- new Directory({}).nameString("../", path.parent)
+ new Directory().nameString("./", path),
+ new Directory().nameString("../", path.parent)
];
const fileList: string[] = [];
@@ -417,6 +418,7 @@ export class Commands {
return error.message;
}
})
+ .filter(it => it !== "")
.join("\n");
}
@@ -479,11 +481,13 @@ export class Commands {
input.hasAnyOption(["f", "force"]),
input.hasAnyOption(["r", "R", "recursive"]),
input.hasOption("no-preserve-root")
- )
+ );
+ return "";
} catch (error) {
return error.message;
}
})
+ .filter(it => it !== "")
.join("\n");
}
@@ -498,6 +502,7 @@ export class Commands {
return error.message;
}
})
+ .filter(it => it !== "")
.join("\n");
}
@@ -524,6 +529,7 @@ export class Commands {
return error.message;
}
})
+ .filter(it => it !== "")
.join("\n");
}
diff --git a/src/main/js/FileSystem.ts b/src/main/js/FileSystem.ts
index 8172b9c..45fe27c 100644
--- a/src/main/js/FileSystem.ts
+++ b/src/main/js/FileSystem.ts
@@ -1,4 +1,4 @@
-import {emptyFunction, getFileExtension, IllegalStateError} from "./Shared";
+import {getFileExtension, IllegalArgumentError, IllegalStateError} from "./Shared";
/**
@@ -93,7 +93,7 @@ export class FileSystem {
if (this.has(destinationPath))
targetPath = destinationPath.getChild(sourcePath.fileName);
else
- targetPath = destinationPath.parent.getChild(destinationPath.fileName);
+ targetPath = destinationPath;
if (!this.has(targetPath.parent))
throw new Error(`The directory '${targetPath.parent}' does not exist.`);
@@ -102,7 +102,7 @@ export class FileSystem {
if (this.has(targetPath))
throw new Error(`The directory or file '${targetPath}' already exists.`);
- this.add(targetPath, source, false);
+ this.add(targetPath, source.copy(), false);
}
/**
@@ -116,9 +116,7 @@ export class FileSystem {
return this.root;
const parent = this.get(target.parent);
- if (!(parent instanceof Directory))
- return undefined;
- if (!parent.hasNode(target.fileName))
+ if (!(parent instanceof Directory) || !parent.hasNode(target.fileName))
return undefined;
return parent.getNode(target.fileName);
@@ -131,7 +129,14 @@ export class FileSystem {
* @return `true` if and only if there exists a node at the given path
*/
has(target: Path): boolean {
- return target.toString() === "/" || this.has(target.parent);
+ if (target.toString() === "/")
+ return true;
+
+ const parent = this.get(target.parent);
+ if (parent === undefined || !(parent instanceof Directory))
+ return false;
+
+ return parent.hasNode(target.fileName);
}
/**
@@ -172,12 +177,8 @@ export class FileSystem {
}
const parent = this.get(targetPath.parent);
- if (!(parent instanceof Directory)) {
- if (force)
- return;
- else
- throw new IllegalStateError(`'${targetPath.parent}' is not a directory, but its child exists.`);
- }
+ if (!(parent instanceof Directory))
+ throw new IllegalStateError(`'${targetPath.parent}' is not a directory, but its child exists.`);
if (target instanceof Directory) {
if (targetPath.toString() === "/" && !noPreserveRoot)
@@ -335,15 +336,6 @@ export abstract class Node {
*/
abstract nameString(name: string, path: Path): string;
- /**
- * Recursively visits all nodes contained within this node.
- *
- * @param fun the function to apply to each node, including this node
- * @param pre the function to apply to the current node before applying the first `fun`
- * @param post the function to apply to the current node after applying the last `fun`
- */
- abstract visit(fun: (node: Node) => void, pre: (node: Node) => void, post: (node: Node) => void): void;
-
/**
* Returns the JSON serialization of this node.
@@ -401,7 +393,7 @@ export class Directory extends Node {
constructor(nodes: { [name: string]: Node } = {}) {
super();
- this._nodes = Object.assign({}, nodes);
+ this._nodes = nodes;
}
@@ -459,6 +451,9 @@ export class Directory extends Node {
* @param node the node to add to this directory
*/
addNode(name: string, node: Node): void {
+ if (new Path(`/${name}`).toString() === "/" || name.indexOf("/") >= 0)
+ throw new IllegalArgumentError(`Cannot add node with name '${name}'.`);
+
this._nodes[name] = node;
}
@@ -497,17 +492,6 @@ export class Directory extends Node {
return `${name}`;
}
- visit(fun: (node: Node) => void,
- pre: (node: Node) => void = emptyFunction,
- post: (node: Node) => void = emptyFunction) {
- pre(this);
-
- fun(this);
- Object.keys(this._nodes).forEach(name => this._nodes[name].visit(fun, pre, post));
-
- post(this);
- }
-
/**
* Parses the given object into a directory.
@@ -568,14 +552,6 @@ export class File extends Node {
}
}
- visit(fun: (node: Node) => void,
- pre: (node: Node) => void = emptyFunction,
- post: (node: Node) => void = emptyFunction) {
- pre(this);
- fun(this);
- post(this);
- }
-
/**
* Parses the given object into a file.
diff --git a/src/main/js/Shared.ts b/src/main/js/Shared.ts
index 51e5b84..53e82f1 100644
--- a/src/main/js/Shared.ts
+++ b/src/main/js/Shared.ts
@@ -114,6 +114,22 @@ export function stripHtmlTags(string: string): string {
}
+/**
+ * Indicates that an argument is given to a function that should not have been given.
+ *
+ * The user should not be able to reach this state because user input should have been sanitized.
+ */
+export class IllegalArgumentError extends Error {
+ /**
+ * Constructs a new illegal argument error.
+ *
+ * @param message a message explaining why the error was thrown
+ */
+ constructor(message: string) {
+ super(message);
+ }
+}
+
/**
* Indicates that the program has ended up in a state that it should never end up in.
*
diff --git a/src/test/Directory.spec.ts b/src/test/Directory.spec.ts
new file mode 100644
index 0000000..3f2ec8f
--- /dev/null
+++ b/src/test/Directory.spec.ts
@@ -0,0 +1,143 @@
+import "mocha";
+import {expect} from "chai";
+
+import "../main/js/Extensions"
+import {Directory, File} from "../main/js/FileSystem";
+
+
+describe("directory", () => {
+ let directory: Directory;
+
+
+ beforeEach(() => {
+ directory = new Directory();
+ });
+
+
+ describe("constructor", () => {
+ it("has no nodes by default", () => {
+ expect(directory.nodeCount).to.equal(0);
+ });
+
+ it("has the given nodes", () => {
+ expect(new Directory({node: new File()}).hasNode("node")).to.be.true;
+ });
+ });
+
+ describe("getNode", () => {
+ it("throws an exception if he node does not exist", () => {
+ expect(() => directory.getNode("error")).to.throw;
+ });
+
+ it("does not contain itself", () => {
+ expect(() => directory.getNode(".")).to.throw;
+ });
+
+ it("returns the desired node", () => {
+ const file = new File();
+ directory.addNode("file", file);
+
+ expect(directory.getNode("file")).to.equal(file);
+ });
+ });
+
+ describe("hasNode", () => {
+ it("returns false if the node does not exist", () => {
+ expect(directory.hasNode("error")).to.be.false;
+ });
+
+ it("returns true if the node exists", () => {
+ directory.addNode("file", new File());
+
+ expect(directory.hasNode("file")).to.be.true;
+ });
+
+ it("returns true if the node is the reflexive node", () => {
+ expect(directory.hasNode(".")).to.be.true;
+ });
+
+ it("returns true if the node is the parent node", () => {
+ expect(directory.hasNode("..")).to.be.true;
+ });
+
+ it("returns true if the node refers to itself", () => {
+ expect(directory.hasNode("")).to.be.true;
+ });
+ });
+
+ describe("addNode", () => {
+ it("adds the given node", () => {
+ directory.addNode("file", new File());
+
+ expect(directory.hasNode("file")).to.be.true;
+ });
+
+ it("overwrites an existing node", () => {
+ directory.addNode("file", new File());
+ directory.addNode("file", new Directory());
+
+ expect(directory.getNode("file")).to.be.instanceOf(Directory);
+ });
+
+ it("refuses to add a node at the reflexive path", () => {
+ expect(() => directory.addNode(".", new File())).to.throw;
+ });
+
+ it("refuses to add a node at the parent path", () => {
+ expect(() => directory.addNode("..", new File())).to.throw;
+ });
+
+ it("refuses to add a node that refers to the directory", () => {
+ expect(() => directory.addNode("", new File())).to.throw;
+ });
+
+ it("refuses to add a node with a name containing a slash", () => {
+ expect(() => directory.addNode("a/b", new File())).to.throw;
+ });
+ });
+
+ describe("removeNode", () => {
+ it("removes the desired node", () => {
+ directory.addNode("file", new File());
+
+ directory.removeNode("file");
+
+ expect(directory.nodeCount).to.equal(0);
+ });
+
+ it("empties the directory if the name is empty", () => {
+ directory.addNode("file", new File());
+
+ directory.removeNode("");
+
+ expect(directory.nodeCount).to.equal(0);
+ });
+
+ it("empties the directory if the name is reflexive", () => {
+ directory.addNode("file", new File());
+
+ directory.removeNode(".");
+
+ expect(directory.nodeCount).to.equal(0);
+ });
+ });
+
+ describe("copy", () => {
+ it("returns a deep copy of the directory", () => {
+ directory = new Directory({
+ file: new File("contents"),
+ dir: new Directory()
+ });
+ const copy = directory.copy();
+
+ (directory.getNode("file")).contents = "changed";
+ expect((copy.getNode("file")).contents).to.equal("contents");
+
+ (directory.getNode("dir")).addNode("file2", new File());
+ expect((copy.getNode("dir")).nodeCount).to.equal(0);
+
+ directory.removeNode("file");
+ expect(copy.nodeCount).to.equal(2);
+ });
+ });
+});
diff --git a/src/test/FileSystem.spec.ts b/src/test/FileSystem.spec.ts
new file mode 100644
index 0000000..1d2d4ca
--- /dev/null
+++ b/src/test/FileSystem.spec.ts
@@ -0,0 +1,299 @@
+import "mocha";
+import {expect} from "chai";
+
+import "../main/js/Extensions"
+import {Directory, File, FileSystem, Path} from "../main/js/FileSystem";
+
+
+describe("input args", () => {
+ let fileSystem: FileSystem;
+
+
+ beforeEach(() => {
+ fileSystem = new FileSystem();
+ });
+
+
+ describe("constructor", () => {
+ it("uses the default root if no directory is given", () => {
+ expect(fileSystem.root).to.not.be.empty;
+ });
+
+ it("uses the given directory as root", () => {
+ const root = new Directory();
+ expect(new FileSystem(root).root).to.equal(root);
+ });
+ });
+
+ describe("add", () => {
+ it("adds the given node", () => {
+ fileSystem.add(new Path("/file"), new File(), false);
+
+ expect(fileSystem.has(new Path("/file"))).to.be.true;
+ });
+
+ it("adds intermediate directories if the option is given", () => {
+ fileSystem.add(new Path("/dir1"), new Directory(), false);
+ fileSystem.add(new Path("/dir1/file1"), new File(), false);
+
+ fileSystem.add(new Path("/dir1/dir2/file2"), new File(), true);
+
+ expect(fileSystem.get(new Path("/dir1/file1"))).to.be.instanceOf(File);
+ expect(fileSystem.get(new Path("/dir1/dir2"))).to.be.instanceOf(Directory);
+ expect(fileSystem.get(new Path("/dir1/dir2/file2"))).to.be.instanceOf(File);
+ });
+
+ it("fails if a node is added as root", () => {
+ expect(() => fileSystem.add(new Path("/"), new File(), false)).to.throw;
+ });
+
+ it("fails if an intermediate directory does not exist", () => {
+ expect(() => fileSystem.add(new Path("/dir1/dir2/file2"), new File(), false)).to.throw;
+ });
+
+ it("fails if an intermediate directory is not a directory", () => {
+ fileSystem.add(new Path("/file1"), new File(), false);
+
+ expect(() => fileSystem.add(new Path("/file1/file2"), new File(), false)).to.throw;
+ });
+
+ it("fails if a node already exists at the path", () => {
+ fileSystem.add(new Path("/file1"), new File(), false);
+
+ expect(() => fileSystem.add(new Path("/file1"), new File(), false)).to.throw;
+ });
+ });
+
+ describe("copy", () => {
+ it("throws an error if the source does not exist", () => {
+ expect(() => fileSystem.copy(new Path("/src"), new Path("/dst"), false)).to.throw;
+ });
+
+ it("throws an error if the source is a directory and the recursive options it not given", () => {
+ fileSystem.add(new Path("/src"), new Directory(), false);
+
+ expect(() => fileSystem.copy(new Path("/src"), new Path("/dst"), false)).to.throw;
+ });
+
+ describe("target is destination", () => {
+ it("throws an error if the target's parent does not exist", () => {
+ fileSystem.add(new Path("/src"), new File(), false);
+
+ expect(() => fileSystem.copy(new Path("/src"), new Path("/parent/dst"), false)).to.throw;
+ });
+
+ it("throws an error if the target's parent is not a directory", () => {
+ fileSystem.add(new Path("/src"), new File(), false);
+ fileSystem.add(new Path("/parent"), new Directory(), false);
+
+ expect(() => fileSystem.copy(new Path("/src"), new Path("/parent/dst"), false)).to.throw;
+ });
+
+ it("throws an error if the target already exists", () => {
+ fileSystem.add(new Path("/src"), new File(), false);
+ fileSystem.add(new Path("/parent"), new Directory(), false);
+ fileSystem.add(new Path("/parent/dst"), new File(), false);
+
+ expect(() => fileSystem.copy(new Path("/src"), new Path("/parent/dst"), false)).to.throw;
+ });
+
+ it("copies the source file to the target", () => {
+ fileSystem.add(new Path("/src"), new File(), false);
+
+ fileSystem.copy(new Path("/src"), new Path("/dst"), false);
+
+ expect(fileSystem.has(new Path("/dst"))).to.be.true;
+ });
+
+ it("copies the source directory to the target", () => {
+ fileSystem.add(new Path("/src"), new Directory(), false);
+ fileSystem.add(new Path("/src/file1"), new File(), false);
+ fileSystem.add(new Path("/src/file2"), new File(), false);
+
+ fileSystem.copy(new Path("/src"), new Path("/dst"), true);
+
+ expect(fileSystem.has(new Path("/dst"))).to.be.true;
+ expect(fileSystem.has(new Path("/dst/file1"))).to.be.true;
+ expect(fileSystem.has(new Path("/dst/file2"))).to.be.true;
+ });
+
+ it("makes a deep copy", () => {
+ const file = new File("old");
+ fileSystem.add(new Path("/src"), file, false);
+
+ fileSystem.copy(new Path("/src"), new Path("/dst"), false);
+ file.contents = "new";
+
+ expect((fileSystem.get(new Path("/dst"))).contents).to.equal("old");
+ });
+ });
+
+ describe("target is child of destination", () => {
+ it("throws an error if the target is not a directory", () => {
+ fileSystem.add(new Path("/src"), new File(), false);
+ fileSystem.add(new Path("/dst"), new File(), false);
+
+ expect(() => fileSystem.copy(new Path("/src"), new Path("/dst"), false)).to.throw;
+ });
+
+ it("throws an error if the target's child already exists", () => {
+ fileSystem.add(new Path("/src"), new File(), false);
+ fileSystem.add(new Path("/dst"), new Directory(), false);
+ fileSystem.add(new Path("/dst/src"), new File(), false);
+
+ expect(() => fileSystem.copy(new Path("/src"), new Path("/dst"), false)).to.throw;
+ });
+
+ it("copies the source file to the target", () => {
+ fileSystem.add(new Path("/src"), new File(), false);
+ fileSystem.add(new Path("/dst"), new Directory(), false);
+
+ fileSystem.copy(new Path("/src"), new Path("/dst"), false);
+
+ expect(fileSystem.has(new Path("/dst/src"))).to.be.true;
+ });
+
+ it("copies the source directory to the target", () => {
+ fileSystem.add(new Path("/src"), new Directory(), false);
+ fileSystem.add(new Path("/src/file1"), new File(), false);
+ fileSystem.add(new Path("/src/file2"), new File(), false);
+ fileSystem.add(new Path("/dst"), new Directory(), false);
+
+ fileSystem.copy(new Path("/src"), new Path("/dst"), true);
+
+ expect(fileSystem.has(new Path("/dst/src"))).to.be.true;
+ expect(fileSystem.has(new Path("/dst/src/file1"))).to.be.true;
+ expect(fileSystem.has(new Path("/dst/src/file2"))).to.be.true;
+ });
+
+ it("makes a deep copy", () => {
+ const file = new File("old");
+ fileSystem.add(new Path("/src"), file, false);
+ fileSystem.add(new Path("/dst"), new Directory(), false);
+
+ fileSystem.copy(new Path("/src"), new Path("/dst"), false);
+ file.contents = "new";
+
+ expect((fileSystem.get(new Path("/dst/src"))).contents).to.equal("old");
+ });
+ });
+ });
+
+ describe("get", () => {
+ it("returns the root node for the root path", () => {
+ expect(fileSystem.get(new Path("/"))).to.equal(fileSystem.root);
+ });
+
+ it("returns undefined if the parent is not a directory", () => {
+ fileSystem.add(new Path("/file1"), new File(), false);
+
+ expect(fileSystem.get(new Path("/file1/file2"))).to.be.undefined;
+ });
+
+ it("returns undefined if the parent does not contain the node", () => {
+ fileSystem.add(new Path("/dir"), new Directory(), false);
+
+ expect(fileSystem.get(new Path("/dir/file"))).to.be.undefined;
+ });
+
+ it("returns the node at the given path", () => {
+ const file = new File();
+ fileSystem.add(new Path("/dir"), new Directory(), false);
+ fileSystem.add(new Path("/dir/file"), file, false);
+
+ expect(fileSystem.get(new Path("/dir/file"))).to.equal(file);
+ });
+ });
+
+ describe("has", () => {
+ it("returns true for the root path", () => {
+ expect(fileSystem.has(new Path("/"))).to.be.true;
+ });
+
+ it("returns false if the node does not exist", () => {
+ expect(fileSystem.has(new Path("/error"))).to.be.false;
+ });
+
+ it("returns true if the node is at the root", () => {
+ fileSystem.add(new Path("/file"), new File(), false);
+
+ expect(fileSystem.has(new Path("/file"))).to.be.true;
+ });
+
+ it("returns false if an intermediate directory does not exist", () => {
+ fileSystem.add(new Path("/file"), new File(), false);
+
+ expect(fileSystem.has(new Path("/dir/file"))).to.be.false;
+ });
+ });
+
+ describe("move", () => {
+ it("moves a file", () => {
+ fileSystem.add(new Path("/src"), new File("old"), false);
+
+ fileSystem.move(new Path("/src"), new Path("/dst"));
+
+ expect(fileSystem.has(new Path("/src"))).to.be.false;
+ expect(fileSystem.has(new Path("/dst"))).to.be.true;
+ });
+
+ it("moves a directory", () => {
+ fileSystem.add(new Path("/src"), new Directory(), false);
+ fileSystem.add(new Path("/src/file1"), new File(), false);
+ fileSystem.add(new Path("/src/file2"), new File(), false);
+
+ fileSystem.move(new Path("/src"), new Path("/dst"));
+
+ expect(fileSystem.has(new Path("/src"))).to.be.false;
+ expect(fileSystem.has(new Path("/src/file1"))).to.be.false;
+ expect(fileSystem.has(new Path("/src/file2"))).to.be.false;
+ expect(fileSystem.has(new Path("/dst"))).to.be.true;
+ expect(fileSystem.has(new Path("/dst/file1"))).to.be.true;
+ expect(fileSystem.has(new Path("/dst/file2"))).to.be.true;
+ });
+ });
+
+ describe("remove", () => {
+ it("throws an error if the node does not exist", () => {
+ expect(() => fileSystem.remove(new Path("/error"), false, false, false)).to.throw;
+ });
+
+ it("does nothing if the node does not exist and the force option is given", () => {
+ expect(() => fileSystem.remove(new Path("/error"), true, false, false)).not.to.throw;
+ });
+
+ it("throws an error if a directory is removed without the recursive option", () => {
+ fileSystem.add(new Path("/dir"), new Directory(), false);
+
+ expect(() => fileSystem.remove(new Path("/dir"), false, false, false)).to.throw;
+ });
+
+ it("throws an error if the root is remove without the recursive or no-preserve-root option", () => {
+ expect(() => fileSystem.remove(new Path("/"), false, false, false)).to.throw;
+ expect(() => fileSystem.remove(new Path("/"), false, true, false)).to.throw;
+ expect(() => fileSystem.remove(new Path("/"), false, false, true)).to.throw;
+ });
+
+ it("removes a file", () => {
+ fileSystem.add(new Path("/file"), new File(), false);
+ fileSystem.remove(new Path("/file"), false, false, false);
+
+ expect(fileSystem.has(new Path("/file"))).to.be.false;
+ });
+
+ it("removes a directory", () => {
+ fileSystem.add(new Path("/dir"), new Directory(), false);
+ fileSystem.remove(new Path("/dir"), false, true, false);
+
+ expect(fileSystem.has(new Path("/dir"))).to.be.false;
+ });
+
+ it("removes the root", () => {
+ fileSystem.add(new Path("/dir"), new Directory(), false);
+ fileSystem.add(new Path("/file"), new File(), false);
+ fileSystem.remove(new Path("/"), false, true, true);
+
+ expect(fileSystem.has(new Path("/dir"))).to.be.false;
+ });
+ });
+});
diff --git a/src/test/InputParser.spec.ts b/src/test/InputParser.spec.ts
index f0fe659..5e5097f 100644
--- a/src/test/InputParser.spec.ts
+++ b/src/test/InputParser.spec.ts
@@ -203,7 +203,6 @@ describe("input args", () => {
});
it("should ignore the redirect target if inside quotation marks", () => {
- console.log(parser.parse("command '> file'").args);
expect(parser.parse("command '> file'").redirectTarget).to.have.members(["default"]);
});
diff --git a/src/test/Path.spec.ts b/src/test/Path.spec.ts
index 8a9b70d..03a4199 100644
--- a/src/test/Path.spec.ts
+++ b/src/test/Path.spec.ts
@@ -50,6 +50,20 @@ describe("paths", () => {
expect(new Path("/", "///", "/file").toString()).to.equal("/file");
});
});
+
+ describe("interpret", () => {
+ it("uses only the cwd if no paths are given", () => {
+ expect(Path.interpret("/cwd").toString()).to.equal("/cwd");
+ });
+
+ it("ignores the cwd if the first path is absolute", () => {
+ expect(Path.interpret("/cwd", "/dir1", "dir2").toString()).to.equal("/dir1/dir2");
+ });
+
+ it("uses all parts if the first path is relative", () => {
+ expect(Path.interpret("/cwd", "dir1", "/dir2").toString()).to.equal("/cwd/dir1/dir2");
+ });
+ });
});
describe("fileName", () => {