From 616df7bebacb948184c564c04cc2dfe4e019ff9f Mon Sep 17 00:00:00 2001 From: "Felix W. Dekker" Date: Tue, 24 Mar 2020 00:24:40 +0100 Subject: [PATCH] Implement separate user file Works towards #112. --- package.json | 2 +- src/main/index.html | 2 +- src/main/js/Commands.ts | 2 + src/main/js/FileSystem.ts | 9 +++ src/main/js/Shell.ts | 2 +- src/main/js/UserList.ts | 119 +++++++++++++++++++++++++++++--------- src/test/UserList.spec.ts | 85 +++++++++++++++++++++++++++ 7 files changed, 190 insertions(+), 31 deletions(-) create mode 100644 src/test/UserList.spec.ts diff --git a/package.json b/package.json index 6de72f7..076171f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fwdekker.com", - "version": "0.30.1", + "version": "0.31.0", "description": "The source code of [my personal website](https://fwdekker.com/).", "author": "Felix W. Dekker", "repository": { diff --git a/src/main/index.html b/src/main/index.html index ed5b06b..1255dfb 100644 --- a/src/main/index.html +++ b/src/main/index.html @@ -40,7 +40,7 @@ --> diff --git a/src/main/js/Commands.ts b/src/main/js/Commands.ts index d86c771..707c092 100644 --- a/src/main/js/Commands.ts +++ b/src/main/js/Commands.ts @@ -525,6 +525,8 @@ export const commandBinaries: { [key: string]: string } = { /dev Contains special files and device files that refer to physical devices. + /etc System configuration files and scripts. + /home Contains directories for users to store personal files in. /root The home directory of the root user.\`.trimMultiLines() diff --git a/src/main/js/FileSystem.ts b/src/main/js/FileSystem.ts index d3a7761..e0075e4 100644 --- a/src/main/js/FileSystem.ts +++ b/src/main/js/FileSystem.ts @@ -1,6 +1,7 @@ import {commandBinaries} from "./Commands"; import {emptyFunction, getFileExtension, IllegalArgumentError} from "./Shared"; import {Stream} from "./Stream"; +import {User} from "./UserList"; /** @@ -30,6 +31,14 @@ export class FileSystem { "dev": new Directory({ "null": new NullFile() }), + "etc": new Directory({ + "passwd": new File( + [ + new User("root", "root", "/root", ""), + new User("felix", "felix", undefined, "") + ].map(it => User.toString(it)).join("\n") + "\n" + ) + }), "home": new Directory({ "felix": new Directory({ "personal": new Directory({ diff --git a/src/main/js/Shell.ts b/src/main/js/Shell.ts index 91bb4e0..6906b99 100644 --- a/src/main/js/Shell.ts +++ b/src/main/js/Shell.ts @@ -50,8 +50,8 @@ export class Shell { */ constructor(inputHistory: InputHistory) { this.inputHistory = inputHistory; - this.userList = new UserList(); this.fileSystem = Persistence.getFileSystem(); + this.userList = new UserList(this.fileSystem); this.environment = Persistence.getEnvironment(this.userList); this.commands = new Commands(this.environment, this.userList, this.fileSystem); diff --git a/src/main/js/UserList.ts b/src/main/js/UserList.ts index a9af0ab..d9d08a9 100644 --- a/src/main/js/UserList.ts +++ b/src/main/js/UserList.ts @@ -1,44 +1,94 @@ /** * Manages a list of users. */ +import {File, FileSystem, Path} from "./FileSystem"; + + +/** + * Manages a file containing user data. + */ export class UserList { /** - * All users that exist in the system. + * The default contents of the user data file, inserted if the file is unexpectedly removed or invalidated. + * + * This is a function to prevent accidental modification of these data. */ - private _users: User[]; + private readonly GET_DEFAULT_USER = () => new User("root", "root", "/root", "The root user."); /** - * Constructs a new list of users. - * - * @param users the list of users that are available, or `undefined` if the default users should be available + * The file system in which the user data file is located. */ - constructor(users: User[] | undefined = undefined) { - if (users === undefined) - this._users = [ - new User("felix", "password", "/home/felix", "Why are you logged in on my account?"), - new User("root", "g9PjKu", "/root", "You're a hacker, Harry!") - ]; - else - this._users = users; + private readonly fileSystem: FileSystem; + /** + * The path to the file containing user data. + */ + private readonly userFilePath: Path; + + + /** + * Constructs a new user list manager. + * + * @param fileSystem the file system in which the user data file is located + * @param userFilePath the path to the file containing user data + */ + constructor(fileSystem: FileSystem, userFilePath: Path = new Path("/etc/passwd")) { + this.fileSystem = fileSystem; + this.userFilePath = userFilePath; + + this.userFile; // Initialize file } + /** + * Returns the user data file, creating it if it does not exist. + * + * @return the user data file, creating it if it does not exist + */ + private get userFile(): File { + let userFile = this.fileSystem.get(this.userFilePath); + if (userFile === undefined) { + userFile = new File(User.toString(this.GET_DEFAULT_USER()) + "\n"); + this.fileSystem.add(this.userFilePath, userFile, true); + } else if (!(userFile instanceof File)) { + userFile = new File(User.toString(this.GET_DEFAULT_USER()) + "\n"); + this.fileSystem.remove(this.userFilePath); + this.fileSystem.add(this.userFilePath, userFile, true); + } + return userFile as File; + } + /** * Returns a copy of the list of all users. */ get users(): User[] { - return this._users.slice(); + return this.userFile.open("read").read().split("\n").map(it => User.fromString(it)); } /** * Adds the given user to the user list. * + * If the user already exists, nothing happens. + * * @param user the user to add + * @return `true` if and only if the user was added */ - add(user: User) { - this._users.push(user); + add(user: User): boolean { + if (this.has(user.name)) + return false; + + this.userFile.open("append").writeLine(User.toString(user)); + return true; + } + + /** + * Returns the user with the given name, or `undefined` if there is no such user. + * + * @param name the name of the user to return + */ + get(name: string): User | undefined { + return this.users.find(it => it.name === name); } /** @@ -49,15 +99,6 @@ export class UserList { has(name: string): boolean { return this.get(name) !== undefined; } - - /** - * Returns the user with the given name, or `undefined` if there is no such user. - * - * @param name the name of the user to return - */ - get(name: string): User | undefined { - return this._users.find(it => it.name === name); - } } @@ -88,13 +129,35 @@ export class User { * * @param name the name of the user * @param password the password of the user - * @param home the path to the user's home directory + * @param home the path to the user's home directory, or `undefined` to use `/home/` * @param description the description of the user */ - constructor(name: string, password: string, home: string, description: string) { + constructor(name: string, password: string, home: string | undefined, description: string = "") { this.name = name; this.password = password; - this.home = home; + this.home = home ?? `/home/${name}`; this.description = description; } + + + /** + * Converts a string to a user object. + * + * @param string the string to convert to a user object + * @return the user object described by the given string + */ + static fromString(string: string): User { + const parts = string.split("|"); + return new User(parts[0], parts[1], parts[2], parts[3]); + } + + /** + * Converts a user object to a string. + * + * @param user the user to convert to a string + * @return the string describing the given user + */ + static toString(user: User): string { + return `${user.name}|${user.password}|${user.home}|${user.description}`; + } } diff --git a/src/test/UserList.spec.ts b/src/test/UserList.spec.ts new file mode 100644 index 0000000..aaf1e98 --- /dev/null +++ b/src/test/UserList.spec.ts @@ -0,0 +1,85 @@ +import "mocha"; +import {expect} from "chai"; + +import {Directory, File, FileSystem, Path} from "../main/js/FileSystem"; +import {User, UserList} from "../main/js/UserList"; + + +describe("user list", () => { + let fileSystem: FileSystem; + let userList: UserList; + let initialContents: string; + + + const readUserFile = () => (fileSystem.get(new Path("/etc/passwd")) as File).open("read").read(); + + + beforeEach(() => { + fileSystem = new FileSystem(new Directory()); + userList = new UserList(fileSystem); + initialContents = readUserFile(); + }); + + + describe("file management", () => { + it("populates the file with a default root account if the file disappeared", () => { + fileSystem.remove(new Path("/etc/passwd")); + + expect(userList.has("root")).to.be.true; + expect(readUserFile()).to.equal(initialContents); + }); + + it("populates the file with a default root account if the target is a directory", () => { + fileSystem.remove(new Path("/etc/passwd")); + fileSystem.add(new Path("/etc/passwd"), new Directory(), true); + + expect(userList.has("root")).to.be.true; + expect(readUserFile()).to.equal(initialContents); + }); + }); + + describe("add", () => { + it("adds the given user", () => { + const user = new User("user", "pwd", "/home", ""); + + userList.add(user); + + expect(readUserFile()).to.equal(initialContents + User.toString(user) + "\n"); + }); + + it("does not add duplicate users", () => { + const user = new User("user", "pwd", "/home", ""); + + userList.add(user); + userList.add(user); + + expect(readUserFile()).to.equal(initialContents + User.toString(user) + "\n"); + }); + }); + + describe("get", () => { + it("returns the indicated user if it exists", () => { + const user = new User("user", "pwd", "/home", ""); + + userList.add(user); + + expect(userList.get("user")).to.deep.equal(user); + }); + + it("returns undefined if the user does not exist", () => { + expect(userList.get("user")).to.be.undefined; + }); + }); + + describe("has", () => { + it("returns `true` if the user exists", () => { + userList.add(new User("user", "pwd", "/home", "")); + + expect(userList.has("user")).to.be.true; + }); + + it("returns `false` if the user does not exist", () => { + expect(userList.has("user")).to.be.false; + }); + }); +});