forked from tools/josh
1
0
Fork 0

Implement separate user file

Works towards #112.
This commit is contained in:
Florine W. Dekker 2020-03-24 00:24:40 +01:00
parent 50fc0ffbd5
commit 616df7beba
Signed by: FWDekker
GPG Key ID: B1B567AF58D6EE0F
7 changed files with 190 additions and 31 deletions

View File

@ -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": {

View File

@ -40,7 +40,7 @@
--><span id="terminalInputPrefix"></span><!--
--><span id="terminalInputField" contenteditable="true" autocapitalize="none"
spellcheck="false"></span><!--
--></span><!--
--></span><!--
--><span id="terminalSuggestions"></span><!--
--></div>
</main>

View File

@ -525,6 +525,8 @@ export const commandBinaries: { [key: string]: string } = {
<u>/dev</u> Contains special files and device files that refer to physical devices.
<u>/etc</u> System configuration files and scripts.
<u>/home</u> Contains directories for users to store personal files in.
<u>/root</u> The home directory of the root user.\`.trimMultiLines()

View File

@ -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({

View File

@ -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);

View File

@ -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 <i>my</i> 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/<name>`
* @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}`;
}
}

85
src/test/UserList.spec.ts Normal file
View File

@ -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;
});
});
});