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", () => {