Compare commits

..

4 Commits

22 changed files with 471 additions and 313 deletions

View File

@@ -33,7 +33,7 @@
}, },
"Interactible": false, "Interactible": false,
"Func": null, "Func": null,
"Parent": null "Parent": 1
}, },
"2": { "2": {
"Inode": 2, "Inode": 2,

View File

@@ -1,68 +1,60 @@
import { COMMANDS, GROUP, PASSWD, type CommandArgs, type ICommand, type Result } from './static'; import { VirtualFS, type TreeNode } from './fs';
import { VirtualFS } from './fs';
import { Terminal, type PrintData } from '../terminal/terminal'; import { Terminal, type PrintData } from '../terminal/terminal';
import { Stack } from '../stack'; import { Stack } from '../stack';
import { PASSWD, type User } from './etc/userData';
export type Permission = { import { ExitCode } from './metadata';
r: boolean; import { COMMANDS, type CommandArgs, type CommandResultData, type ICommand } from './commandRegistry';
w: boolean; import { GROUP, type Group } from './etc/groupData';
x: boolean;
};
export type BashInitArgs = { export type BashInitArgs = {
stdio?: Terminal; io?: Terminal;
user: User; instanceId: number;
fs: any; user: {username: string, password: string};
fs?: VirtualFS;
}; };
export type TimeStamps = { export type Result = {
modified: Date; exitCode: ExitCode;
changed: Date; path: number; //the inode of the place that the command was executed in
accessed: Date; resultData?: CommandResultData;
};
// TODO: Finish this
// TODO: Change into a type instead of an enum for performance (low priority)
export enum ExitCode {
SUCCESS = 0,
ERROR = 1
}
export type User = {
username: string;
passwd: string; //HASHED PASSWORD //TODO: Make a formated type
readonly uid: number; // Normal user 1000+ System user 1-999 root - 0 //TODO: Make a formated type
readonly gid: number; // Primary group | 'Users' 1000 - Others - 1000+ root - 0 //TODO: Make a formated type
home: string; //TODO: Make a formated type
history: string[];
cwd?: number; //TODO: Make a formated type
pwd?: number; //TODO: Make a formated type
};
export type Group = {
groupname: string;
gid: number; // Primary group 'Users' 1000 - Others - 1000+ root - 0
members: number[]; //TODO: Make a formated type UID
}; };
export class Bash { export class Bash {
private readonly _instanceId: number;
private vfs: VirtualFS; private vfs: VirtualFS;
private _passwd: User[]; private _passwd: User[];
private _instances: Stack<User>; private _userInstances: Stack<User>;
private _group: Group[]; private _group: Group[];
private _terminal!: Terminal; private _terminal!: Terminal;
private user: User; private user: User;
private readonly _commands: Record<string, ICommand>; private readonly _commands: Record<string, ICommand>;
constructor(args: BashInitArgs) { constructor(args: BashInitArgs) {
this.user = args.user; this._instanceId = args.instanceId
this._commands = COMMANDS; this._commands = COMMANDS;
this._passwd = PASSWD; this._passwd = PASSWD;
this._group = GROUP; this._group = GROUP;
this._terminal = args.stdio!; this._userInstances = new Stack<User>();
this._instances = new Stack<User>(); this._terminal = args.io!;
this.vfs = new VirtualFS({ fs: args.fs, user: args.user }); const loginResult = this.userLogin(args.user.username, args.user.password);
if(loginResult == ExitCode.ERROR)
this._terminal.throwExeption(
`Failed to initialize bash instance - access denied for user ${args.user.username}`,
ExitCode.ERROR
)
this.user = this._userInstances.peek()!
this.vfs = this._terminal.fileSystem;
}
private _initNewUserSession(user: User): User {
if(!this._passwd.includes(user))
this._terminal.throwExeption(`user not found under the name ${user.username}`, ExitCode.ERROR)
this._userInstances.push(user);
return user
} }
private _appendNewResult(inode: number, output: any, cmd: string) { private _appendNewResult(inode: number, output: any, cmd: string) {
@@ -85,6 +77,10 @@ export class Bash {
} }
} }
clearTerminal(): void {
this._terminal.clearTerminal();
}
getCwd(): number { getCwd(): number {
return this.vfs.cwd; return this.vfs.cwd;
} }
@@ -113,7 +109,7 @@ export class Bash {
return this._group[1].members.includes(uid); return this._group[1].members.includes(uid);
} }
executeCommand(commandName: string, args: CommandArgs): void { async executeCommand(commandName: string, args: CommandArgs) {
let result: Result = { exitCode: ExitCode.ERROR, path: this.getCwd() }; let result: Result = { exitCode: ExitCode.ERROR, path: this.getCwd() };
const command = this._commands[commandName]; const command = this._commands[commandName];
if (!command) this.throwError(result); if (!command) this.throwError(result);
@@ -128,7 +124,7 @@ export class Bash {
let out: Result = command.method.call(this, args); let out: Result = command.method.call(this, args);
console.log(out); console.log(out);
this._appendNewResult(out.path, out.data?.data, this.user.history[0]); this._appendNewResult(out.path, out.resultData?.data, this.user.history[0]);
} }
throwError(result: Result): void { throwError(result: Result): void {
@@ -140,14 +136,25 @@ export class Bash {
} }
userLogout() { userLogout() {
this._instances.pop(); this._userInstances.pop();
if (this._instances.size() === 0) { if (this._userInstances.size() === 0) {
//TODO: Implement system logout //TODO: Implement system logout
} else { } else {
//this.changeUser(this._instances.peek()!); //this.changeUser(this._instances.peek()!);
} }
} }
userLogin(username: string, passwd: string): ExitCode {
const user: User | undefined =
this._passwd.find((user) => user.username === username);
if(user && user.passwd === passwd) {
this._initNewUserSession(user);
return ExitCode.SUCCESS;
}
return ExitCode.ERROR;
}
formatBytes(bytes: number, dPoint?: number, pow: 1024 | 1000 = 1024): string { formatBytes(bytes: number, dPoint?: number, pow: 1024 | 1000 = 1024): string {
if (!+bytes) return '0'; if (!+bytes) return '0';

View File

@@ -1,82 +1,30 @@
import { Bash, ExitCode, type Group, type User } from './bash'; import type { Bash, Result } from "./bash";
import { ls } from './commands/ls'; import { cd } from "./commands/cd";
import { cd } from './commands/cd'; import { clear } from "./commands/clear";
import { ls } from "./commands/ls";
export type ICommand = { export type ICommand = {
method: (this: Bash, args: CommandArgs) => Result; method: (this: Bash, args: CommandArgs) => Result;
flags: string[]; flags: string[];
help: string; help: string;
root: boolean; root: boolean;
}; };
export type CommandArgs = { export type CommandArgs = {
flags: string[]; flags: string[];
args: string[]; args: string[];
}; };
export type resultData = { export type CommandResultData = {
cmd: string; //the string that contains the shorthand for the command that was executed - used in a switch statement in parseResult cmd: string; //the string that contains the shorthand for the command that was executed - used in a switch statement in parseResult
data: any; //the data that the commmand may have returned like TreeNodes[] from ls data: any; //the data that the commmand may have returned like TreeNodes[] from ls
args?: CommandArgs; args?: CommandArgs;
}; };
export type Result = {
exitCode: ExitCode;
path: number; //the inode of the place that the command was executed in
data?: resultData;
};
export const GROUP: Group[] = [
{
groupname: 'sudo',
gid: 69,
members: [0, 1001]
},
{
groupname: 'users',
gid: 984,
members: [1001, 1002]
}
] as const;
export const PASSWD: User[] = [
{
username: 'root',
passwd: '123',
uid: 0,
gid: 0,
home: '/',
history: [] //TODO: Delete this and declare a new history array when logging the user in.
},
{
username: 'admin',
passwd: '456',
uid: 1000,
gid: 1000,
home: '/home/admin',
history: [] //TODO: Delete this and declare a new history array when logging the user in.
},
{
username: 'user',
passwd: '789',
uid: 1001,
gid: 1000,
home: '/home/user',
history: [] //TODO: Delete this and declare a new history array when logging the user in.
},
{
username: 'kamil',
passwd: '000',
uid: 1002,
gid: 1000,
home: '/home/kamil',
history: [] //TODO: Delete this and declare a new history array when logging the user in.
}
];
export const COMMANDS = { export const COMMANDS = {
cd, cd,
ls ls,
clear
} as const satisfies Record<string, ICommand>; } as const satisfies Record<string, ICommand>;
/* //export const commands { /* //export const commands {

View File

@@ -1,6 +1,7 @@
import { ExitCode, type Bash } from '../bash'; import type { Bash, Result } from '../bash';
import type { CommandArgs, ICommand } from '../commandRegistry';
import { Type, type TreeNode } from '../fs'; import { Type, type TreeNode } from '../fs';
import type { CommandArgs, ICommand, Result } from '../static'; import { ExitCode } from '../metadata';
export const cmd_cd = function (this: Bash, args: CommandArgs): Result { export const cmd_cd = function (this: Bash, args: CommandArgs): Result {
let result: Result = { exitCode: ExitCode.ERROR, path: this.getCwd() }; let result: Result = { exitCode: ExitCode.ERROR, path: this.getCwd() };

View File

@@ -0,0 +1,16 @@
import { ExitCode, type Bash } from "../bash";
import type { CommandArgs, ICommand, Result } from "../static";
export const cmd_clear = function(this: Bash, args: CommandArgs): Result {
let result: Result = { exitCode: ExitCode.ERROR, path: this.getCwd() };
this.clearTerminal();
result.exitCode = ExitCode.SUCCESS;
return result;
}
export const clear: ICommand = {
method: cmd_clear,
flags: [] as string[],
help: 'PATH TO HELP.md',
root: false
};

View File

@@ -1,7 +1,8 @@
import { Bash, ExitCode, type Permission, type TimeStamps } from '../bash'; import type { Bash, Result } from '../bash';
import { Type, type NodePerms, type TreeNode } from '../fs'; import type { CommandArgs, CommandResultData, ICommand } from '../commandRegistry';
import { Type, VirtualFS, type NodePerms, type TreeNode } from '../fs';
import { ExitCode, type Permission } from '../metadata';
import { Sort, SortNodeBy } from '../sort'; import { Sort, SortNodeBy } from '../sort';
import type { CommandArgs, ICommand, Result, resultData } from '../static';
type LsEntry = { type LsEntry = {
inode: number | null; inode: number | null;
@@ -38,8 +39,9 @@ const months: readonly string[] = [
]; ];
export const cmd_ls = function (this: Bash, args: CommandArgs): Result { export const cmd_ls = function (this: Bash, args: CommandArgs): Result {
const resultData: resultData = { cmd: 'ls', data: null, args: args }; const Fs = this.getFs();
const result: Result = { exitCode: ExitCode.ERROR, path: this.getCwd(), data: resultData }; const resultData: CommandResultData = { cmd: 'ls', data: null, args: args };
const result: Result = { exitCode: ExitCode.ERROR, path: this.getCwd(), resultData: resultData };
const nodes: TreeNode[] = []; const nodes: TreeNode[] = [];
//Check if any args contain the long flags with value and are valid flags inside the ls const //Check if any args contain the long flags with value and are valid flags inside the ls const
@@ -55,10 +57,10 @@ export const cmd_ls = function (this: Bash, args: CommandArgs): Result {
this.throwError(result); //No such flag/s this.throwError(result); //No such flag/s
} }
if (args.args.length === 0) nodes.push(this.getFs().getNodeByINode(this.getFs().cwd)); if (args.args.length === 0) nodes.push(Fs.getNodeByINode(Fs.cwd));
for (let i = 0; i < args.args.length; i++) { for (let i = 0; i < args.args.length; i++) {
const node = this.getFs().resolvePath(args.args[i]); const node = Fs.resolvePath(args.args[i]);
if (node === null) this.throwError(result); //no such path (i think this will never occur as backed methods have error cases implemented - which is wrong) if (node === null) this.throwError(result); //no such path (i think this will never occur as backed methods have error cases implemented - which is wrong)
nodes.push(node); nodes.push(node);
@@ -71,16 +73,19 @@ export const cmd_ls = function (this: Bash, args: CommandArgs): Result {
function result_ls(this: Bash, data: any, args: CommandArgs): HTMLElement { function result_ls(this: Bash, data: any, args: CommandArgs): HTMLElement {
const dummysonoerror: HTMLElement = document.createElement('div'); const dummysonoerror: HTMLElement = document.createElement('div');
const Fs: VirtualFS = this.getFs();
const flagInfo = checkFlags(args.flags); const flagInfo = checkFlags(args.flags);
const nodes: TreeNode[] = data; const nodes: TreeNode[] = data;
let nodeIndex: number = 0;
const f_a: boolean = flagInfo.has('a') || flagInfo.has('all'); const f_a: boolean = flagInfo.has('a') || flagInfo.has('all');
const f_A: boolean = flagInfo.has('A') || flagInfo.has('almost-all'); const f_A: boolean = flagInfo.has('A') || flagInfo.has('almost-all');
const f_G: boolean = flagInfo.has('G') || flagInfo.has('no-group'); const f_G: boolean = flagInfo.has('G') || flagInfo.has('no-group');
const f_h: boolean = flagInfo.has('h') || flagInfo.has('human-readable'); const f_h: boolean = flagInfo.has('h') || flagInfo.has('human-readable');
const f_r: boolean = flagInfo.has('r') || flagInfo.has('reverse'); const f_r: boolean = flagInfo.has('r') || flagInfo.has('reverse');
const f_R: boolean = flagInfo.has('R') || flagInfo.has('recursive');
const f_Q: boolean = flagInfo.has('Q') || flagInfo.has('quote-name'); const f_Q: boolean = flagInfo.has('Q') || flagInfo.has('quote-name');
const f_n: boolean = flagInfo.has('n') || flagInfo.has('numeric-uid-gid'); const f_n: boolean = flagInfo.has('n') || flagInfo.has('numeric-uid-gid');
const f_N: boolean = flagInfo.has('N') || flagInfo.has('literal'); const f_N: boolean = flagInfo.has('N') || flagInfo.has('literal');
@@ -102,14 +107,40 @@ function result_ls(this: Bash, data: any, args: CommandArgs): HTMLElement {
const w: HTMLElement = document.createElement('div'); const w: HTMLElement = document.createElement('div');
if(f_R) {
const treeWalkResult: TreeNode[] = [];
const treeWalkCallback = (node: TreeNode) => { treeWalkResult.push(node); }
for(const node of nodes) {
treeWalkResult.push(node);
Fs.recursiveTraversalPre(node, treeWalkCallback);
}
nodes.length = 0;
nodes.push(...treeWalkResult.filter((node) => node.type != Type.File));
}
for (const node of nodes) { for (const node of nodes) {
const elem: HTMLElement = document.createElement('div'); const elem: HTMLElement = document.createElement('div');
const children: TreeNode[] = node.children.map((child) => this.getFs().getNodeByINode(child)); const childrenMap: TreeNode[] = node.children.map((child) => Fs.getNodeByINode(child));
const rows: string[] = [];
const children: TreeNode[] = (f_a || f_A)
? childrenMap
: childrenMap.filter((child) => !child.name.startsWith('.'));
const timeArg = valuedArgs.find((flag) => flag.startsWith('time')); const timeArg = valuedArgs.find((flag) => flag.startsWith('time'));
const shouldNamesShift: boolean = children.some((child) => child.name.match(/\s/) !== null); const shouldNamesShift: boolean = children.some((child) => child.name.match(/\s/) !== null);
let timestamp: SortNodeBy.ATIME | SortNodeBy.CTIME | SortNodeBy.MTIME = SortNodeBy.MTIME; let timestamp: SortNodeBy.ATIME | SortNodeBy.CTIME | SortNodeBy.MTIME = SortNodeBy.MTIME;
if(f_a && !f_A) {
const current: TreeNode = node;
current.name = '.';
const parent: TreeNode = Fs.getNodeByINode(node.parent);
parent.name = '..';
children.unshift(current, parent);
}
if(timeArg) { if(timeArg) {
let value: string = timeArg.split('=')[1]; let value: string = timeArg.split('=')[1];
if (value && isValidNodeTimestamp(value)) timestamp = value; if (value && isValidNodeTimestamp(value)) timestamp = value;
@@ -131,15 +162,12 @@ function result_ls(this: Bash, data: any, args: CommandArgs): HTMLElement {
Sort.nodeArraySort.call(this, children, f_r, sortBy); Sort.nodeArraySort.call(this, children, f_r, sortBy);
} }
if (f_l || f_g || f_o) { if (f_l || f_g || f_o) {
const rows: string[] = [];
for (const node of nodes) {
const maxSizeWidth = Math.max( const maxSizeWidth = Math.max(
...children.map((child) => child.size.toString().length)); ...children.map((child) => child.size.toString().length));
for (const child of children) { for (const child of children) {
if (child.name.startsWith('.') && !(f_a || f_A)) continue;
const entry: LsEntry = { const entry: LsEntry = {
inode: null, inode: null,
perms: formatPermission(child), perms: formatPermission(child),
@@ -155,115 +183,92 @@ function result_ls(this: Bash, data: any, args: CommandArgs): HTMLElement {
rows.push(LsEntryUtils.toString(entry)); rows.push(LsEntryUtils.toString(entry));
} }
if (f_a && !f_A) {
const current: LsEntry = {
inode: null,
perms: formatPermission(node),
children: formatChildren(node),
owners: formatOwners.call(this, node, flagInfo),
size: formatSize.call(this, f_h, node, maxSizeWidth, f_si),
modt: formatModtime(node, timestamp),
name: shouldNamesShift ? ' .' : '.'
};
let parent: LsEntry = {
...current,
name: shouldNamesShift ? ' ..' : '..'
};
if (node.parent) {
const parentNode: TreeNode = this.getFs().getNodeByINode(node.parent);
parent = {
inode: null,
perms: formatPermission(parentNode),
children: formatChildren(parentNode),
owners: formatOwners.call(this, parentNode, flagInfo),
size: formatSize.call(this, f_h, parentNode, maxSizeWidth, f_si),
modt: formatModtime(parentNode, timestamp),
name: shouldNamesShift ? ' ..' : '..'
};
}
if (f_i) {
current.inode = node.inode;
parent.inode = node.parent ? node.parent : node.inode;
}
if (f_r) {
rows.push(LsEntryUtils.toString(parent), LsEntryUtils.toString(current));
} else {
rows.unshift(LsEntryUtils.toString(current), LsEntryUtils.toString(parent));
}
}
//TODO: Calculate the total size of contents in the node //TODO: Calculate the total size of contents in the node
rows.unshift('total ' + node.children.length.toString()); rows.unshift('total ' + node.children.length.toString());
if (nodes.length > 1) { if (nodes.length > 1) {
const nodePath: string = const nodePath: HTMLElement = document.createElement('p');
node.name === '/' ? '/:' : `${this.getFs().getPathByInode(node.inode).slice(1)}:`; nodePath.innerText = `${Fs.getPathByInode(node.inode)}:`;
rows.unshift(nodePath); w.appendChild(nodePath);
rows.push('\n'); if(nodeIndex +1 < nodes.length) elem.style.marginBottom = `${this.getTerminalFontSize() * 2}px`;
} }
for (let i = 0; i < rows.length; i++) { for (let i = 0; i < rows.length; i++) {
const p: HTMLElement = document.createElement('p'); const p: HTMLElement = document.createElement('p');
p.innerText = rows[i]; p.innerText = rows[i];
elem.appendChild(p); elem.appendChild(p);
} }
w.appendChild(elem); w.appendChild(elem);
} }
return w; else {
} const maxWidth: number = Math.ceil((this.getTerminalWidth() / this.getTerminalFontSize()) * 0.8);
const maxWidth: number = (this.getTerminalWidth() / this.getTerminalFontSize()) * 0.8;
const nameLengths: number[] = nodes.map(node => node.name.length);
let columns: string[][] = []; let columns: string[][] = [];
let colWidths: number[];
let lowBound: number = 1; let lowBound: number = 1;
let highBound: number = Math.floor(maxWidth / 3); let highBound: number = children.length;
let c: number = (lowBound + highBound) / 2;
nodeBinarySearch(nodes.length, nameLengths, lowBound, highBound, maxWidth); while(lowBound + 1 < highBound) {
const c = lowBound + (highBound - lowBound) / 2;
for(let i = 0; i < c -1; i++) { columns = [];
const colSize: number = i < (nodes.length % c) ? nodes.length : nodes.length -1; colWidths = [];
let fileIndex = 0;
for(let j = 0; j < colSize -1; j++){ for(let i = 0; i < c; i++) {
if(j >= nodes.length -1) break; if(fileIndex >= children.length) break;
columns[i] = [];
for(let j = 0; j < Math.ceil(children.length / c); j++) {
if(fileIndex >= children.length) break;
columns[i].push(children[fileIndex].name);
fileIndex++;
}
colWidths.push(Math.max(...columns[i].map((name) => name.length)));
}
columns[i].push(nodes[j].name); const calcWidth: number = colWidths.reduce((prev, curr) => prev + curr) + ((c-1) * 2);
}
if(calcWidth < maxWidth) lowBound = c + 1;
else if(calcWidth > maxWidth) highBound = c -1
} }
//w.appendChild();
const wrapper: HTMLElement = document.createElement('div');
wrapper.style.display = 'flex';
wrapper.style.columnGap = `${this.getTerminalFontSize() * 2}px`;
wrapper.style.marginBottom = `${this.getTerminalFontSize() * 2}px`;
let fileIndex = 0;
for(let i = 0; i < lowBound; i++) {
if(fileIndex >= children.length) break;
const col: HTMLElement = document.createElement('div');
for(let j = 0; j < Math.ceil(children.length / lowBound); j++) {
if(fileIndex >= children.length) break;
const entry: HTMLElement = document.createElement('p');
entry.innerText = formatName(children[fileIndex], flagInfo, shouldNamesShift);
col.appendChild(entry);
fileIndex++;
}
wrapper.appendChild(col);
}
if (nodes.length > 1) {
const nodePath: HTMLElement = document.createElement('p');
nodePath.innerText = `${Fs.getPathByInode(node.inode)}`;
w.appendChild(nodePath);
}
w.appendChild(wrapper);
}
nodeIndex++;
} }
return w; return w;
} }
function nodeBinarySearch(
n: number,
nameLengths: number[],
low: number,
high: number,
max: number) {
let c: number = (low + high) / 2;
if(low + 1 < high) {
const calcWidth: number =
nameLengths.reduce((result, value) => result + value) + (c-1) * 2
if (calcWidth <= max) {
low = c;
} else high = c;
nodeBinarySearch(n, nameLengths, low, high, max);
}
}
function isValidNodeSortMethod(value: string): value is SortNodeBy { function isValidNodeSortMethod(value: string): value is SortNodeBy {
return Object.values(SortNodeBy).includes(value as SortNodeBy); return Object.values(SortNodeBy).includes(value as SortNodeBy);
} }
@@ -355,11 +360,11 @@ function formatModtime(node: TreeNode, sortBy: SortNodeBy.ATIME | SortNodeBy.CTI
} }
function formatName(node: TreeNode, flag: any, shouldShift: boolean) { function formatName(node: TreeNode, flag: any, shouldShift: boolean) {
let name: string; let name: string = node.name;
const char: string = flag.has('Q') ? '"' : "'"; const char: string = flag.has('Q') ? '"' : "'";
if (/\s/.test(node.name)) { if (/\s/.test(node.name)) {
name = `${char}${node.name}${char}` name = `${char}${name}${char}`
} else { } else {
//Shift non quoted names 1 char right to align if any names in group have a quote //Shift non quoted names 1 char right to align if any names in group have a quote
name = `${shouldShift ? ' ' : ''}${node.name}`; name = `${shouldShift ? ' ' : ''}${node.name}`;
@@ -368,6 +373,25 @@ function formatName(node: TreeNode, flag: any, shouldShift: boolean) {
return flag.has('p') && node.type === Type.Directory ? `${name}/` : name; return flag.has('p') && node.type === Type.Directory ? `${name}/` : name;
} }
/* function formatOutputShort(node: TreeNode, flag: any) {
let output: string = node.name;
const char: string = flag.has('Q') ? '"' : "'";
if (/\s/.test(node.name)) {
if(flag.has('i')) {
}
else {
output = `${char}${node.name}${char}`;
}
if(flag.has('i')) {
output = `${node.inode} ${output}`
}
return flag.has('p') && node.type === Type.Directory ? ` ${output}/ ` : output;
} */
const checkFlags = (pFlags: string[]) => { const checkFlags = (pFlags: string[]) => {
const flagSet = new Set(pFlags); const flagSet = new Set(pFlags);
@@ -392,6 +416,7 @@ export const ls: ICommand = {
't', 't',
'S', 'S',
'r', 'r',
'R',
'Q', 'Q',
'p', 'p',
'o', 'o',
@@ -408,6 +433,7 @@ export const ls: ICommand = {
'no-group', 'no-group',
'human-readable', 'human-readable',
'reverse', 'reverse',
'recursive',
'quote-name', 'quote-name',
'indicator-style', 'indicator-style',
'literal', 'literal',

View File

@@ -1,13 +0,0 @@
import type { ICommand } from "../static"
import { cmd_ls } from "./ls"
type LsEntry
export class ls {
public const ls: ICommand = {
method: ls.cmd_ls
}
}

View File

@@ -0,0 +1,18 @@
export type Group = {
groupname: string;
gid: number; // Primary group 'Users' 1000 - Others - 1000+ root - 0
members: number[]; //TODO: Make a formated type UID
};
export const GROUP: Group[] = [
{
groupname: 'sudo',
gid: 69,
members: [0, 1001]
},
{
groupname: 'users',
gid: 984,
members: [1001, 1002]
}
] as const;

View File

@@ -0,0 +1,45 @@
export type User = {
username: string;
passwd: string; //HASHED PASSWORD //TODO: Make a formated type
readonly uid: number; // Normal user 1000+ System user 1-999 root - 0 //TODO: Make a formated type
readonly gid: number; // Primary group | 'Users' 1000 - Others - 1000+ root - 0 //TODO: Make a formated type
home: string; //TODO: Make a formated type
history: string[];
cwd?: number; //TODO: Make a formated type
pwd?: number; //TODO: Make a formated type
};
export const PASSWD: User[] = [
{
username: 'root',
passwd: '123',
uid: 0,
gid: 0,
home: '/',
history: []
},
{
username: 'admin',
passwd: '456',
uid: 1000,
gid: 1000,
home: '/home/admin',
history: []
},
{
username: 'user',
passwd: '789',
uid: 1001,
gid: 1000,
home: '/home/user',
history: []
},
{
username: 'kamil',
passwd: '000',
uid: 1002,
gid: 1000,
home: '/home/kamil',
history: []
}
];

View File

@@ -1,4 +1,5 @@
import type { Permission, TimeStamps, User } from './bash'; import type { Bash } from "./bash";
import type { Permission, TimeStamps } from "./metadata";
export enum Type { export enum Type {
Directory = 16384, Directory = 16384,
@@ -14,12 +15,12 @@ export type NodePerms = {
export type FsInitArgs = { export type FsInitArgs = {
fs: any; fs: any;
user: User; bash: Bash;
}; };
export type TreeNode = { export type TreeNode = {
inode: number; inode: number;
parent?: number; parent: number;
name: string; name: string;
type: Type; type: Type;
size: number; //Size in Bytes size: number; //Size in Bytes
@@ -37,6 +38,7 @@ export type TreeNode = {
export class VirtualFS { export class VirtualFS {
private FsTable: Map<number, TreeNode>; private FsTable: Map<number, TreeNode>;
private rootINode: number; private rootINode: number;
private _Bash: Bash;
home: number; home: number;
cwd: number; cwd: number;
@@ -45,9 +47,10 @@ export class VirtualFS {
constructor(args: FsInitArgs) { constructor(args: FsInitArgs) {
this.FsTable = args.fs; this.FsTable = args.fs;
this.rootINode = 1; this.rootINode = 1;
this.home = this._pathStringToINode(args.user.home); this._Bash = args.bash;
this.cwd = args.user.cwd ? args.user.cwd : this.home; this.home = this._pathStringToINode(args.bash.getUser().home);
this.pwd = args.user.pwd ? args.user.pwd : this.cwd; this.cwd = args.bash.getUser().cwd ? args.bash.getUser().cwd! : this.home;
this.pwd = args.bash.getUser().pwd ? args.bash.getUser().pwd! : this.cwd;
console.log(this.home); console.log(this.home);
console.log(this.cwd); console.log(this.cwd);
@@ -110,11 +113,11 @@ export class VirtualFS {
return typeof path === 'string' && path.startsWith('/'); return typeof path === 'string' && path.startsWith('/');
}; };
getPathByInode(inode: number): string { public getPathByInode(inode: number): string {
return this._iNodeToPathString(inode); return this._iNodeToPathString(inode);
} }
formatPath(path: string): string { public formatPath(path: string): string {
console.log(path, 'formatPath'); console.log(path, 'formatPath');
const prefix = this._iNodeToPathString(this.home); const prefix = this._iNodeToPathString(this.home);
@@ -123,7 +126,7 @@ export class VirtualFS {
} else return path; } else return path;
} }
resolvePath(path: string): TreeNode { public resolvePath(path: string): TreeNode {
if (path === '/') return this.getNodeByINode(this.rootINode); if (path === '/') return this.getNodeByINode(this.rootINode);
let parsedPath: string = path; let parsedPath: string = path;
@@ -144,22 +147,32 @@ export class VirtualFS {
return Node; return Node;
} }
getNodeByINode(inode: number): TreeNode { public getNodeByINode(inode: number): TreeNode {
const node: TreeNode | undefined = this.FsTable.get(inode); const node: TreeNode | undefined = this.FsTable.get(inode);
if (!node) throw new Error('Could not get the node, no such i node exists'); if (!node) throw new Error(`Could not get the node, no such inode exists - ${inode}`);
return node; return node;
} }
/* private _getPathToNode(node: TreeNode): string[] { public recursiveTraversalPre(node: TreeNode, callback: (param: TreeNode) => void, childIndex?: number[], depthIndex?: number): void {
const path: string[] = []; if(!depthIndex) depthIndex = 0;
let current = node; if(!childIndex) childIndex = [0];
path.push(node.name);
while (current.parent) { if(node.type != Type.File && node.children[childIndex[depthIndex]]) {
current = current.parent; node = this.getNodeByINode(node.children[childIndex[depthIndex]]);
path.unshift(current.name); depthIndex++;
if(!childIndex[depthIndex]) childIndex[depthIndex] = 0;
callback(node);
}
else {
node = this.getNodeByINode(node.parent);
childIndex[depthIndex] = 0;
depthIndex--;
childIndex[depthIndex]++;
} }
return path; if(depthIndex < 0) return;
} */
this.recursiveTraversalPre(node, callback, childIndex, depthIndex);
}
} }

View File

@@ -0,0 +1,17 @@
export type TimeStamps = {
modified: Date;
changed: Date;
accessed: Date;
};
// TODO: Finish this
export enum ExitCode {
SUCCESS = 0,
ERROR = 1
}
export type Permission = {
r: boolean;
w: boolean;
x: boolean;
};

View File

@@ -20,7 +20,7 @@ export class Sort {
reverse: boolean = false, reverse: boolean = false,
sortBy: SortNodeBy = SortNodeBy.NAME sortBy: SortNodeBy = SortNodeBy.NAME
): TreeNode[] { ): TreeNode[] {
if (nodes.length === 0) throw new Error('Tried to sort an empty node array!'); if (nodes.length === 0) {console.warn('Tried to sort an empty node array!'); return [];}
const parsedNodes: TreeNode[] = []; const parsedNodes: TreeNode[] = [];
if (typeof nodes[0] === 'number') { if (typeof nodes[0] === 'number') {

View File

@@ -4,10 +4,13 @@ import deFlag from '$lib/assets/deFlag.svg';
import frFlag from '$lib/assets/frFlag.svg'; import frFlag from '$lib/assets/frFlag.svg';
import jaFlag from '$lib/assets/jaFlag.svg'; import jaFlag from '$lib/assets/jaFlag.svg';
export const langs = { import { writable } from 'svelte/store';
pl: {},
en: {}, function getInitalLocale(): string {
de: {}, if (typeof navigator === 'undefined') return 'en-US';
ja: {},
fr: {} const sysPrefLocale = navigator.language
}; return sysPrefLocale
}
export const locale = writable(getInitalLocale());

View File

@@ -1,6 +1,7 @@
import type { User } from '../bash/bash'; import type { User } from '../bash/etc/userData';
import type { TreeNode } from '../bash/fs'; import type { TreeNode } from '../bash/fs';
import { Terminal, type TermInitArgs } from './terminal'; import { type ILocale } from './localeRegistry';
import { Terminal, type PageCallbacks, type TermInitArgs } from './terminal';
let initializing = $state(true); let initializing = $state(true);
@@ -8,7 +9,7 @@ export function isInitializing(): boolean {
return initializing; return initializing;
} }
function jsonToNodeTable(data: any, parent?: number): Map<number, TreeNode> { function jsonToNodeTable(data: any): Map<number, TreeNode> {
const FsTable: Map<number, TreeNode> = new Map<number, TreeNode>(); const FsTable: Map<number, TreeNode> = new Map<number, TreeNode>();
const entryList = Object.entries(data); const entryList = Object.entries(data);
@@ -99,16 +100,34 @@ async function fetchFileSystem(path: string): Promise<any> {
return data; return data;
} }
export async function initTerminal(user: User, callbackInit: any): Promise<Terminal> { async function fetchLocale(id: string): Promise<any> {
const response = await fetch(`/src/lib/assets/locales/terminal/${id}.json`);
if(!response.ok) throw new Error('Failed to fetch the chosen locale');
const data = await response.json();
return data;
}
export async function initTerminal(
user: {username: string, password: string},
callbackInit: PageCallbacks,
localeId: string
): Promise<Terminal> {
try { try {
const sig = await fetchFsSignature('/src/lib/assets/fs/signature'); const sig = await fetchFsSignature('/src/lib/assets/fs/signature');
const fsJson = await fetchFsJson(sig); const fsJson = await fetchFsJson(sig);
const fs: Map<number, TreeNode> = jsonToNodeTable(fsJson); const fs: Map<number, TreeNode> = jsonToNodeTable(fsJson);
const locale: ILocale = {
id: localeId,
keys: fetchLocale(localeId)
};
const args: TermInitArgs = { const args: TermInitArgs = {
fs,
locale,
bash: { bash: {
user, user,
fs instanceId: 0
} }
}; };

View File

@@ -0,0 +1,13 @@
export interface ILocale {
id: string;
keys?: {};
}
const en: ILocale = {
id: 'en_US',
keys: {}
}
export const LOCALES = {
en
} as const satisfies Record<string, ILocale>;

View File

@@ -1,7 +1,7 @@
import { mount, unmount } from 'svelte'; import { mount, unmount } from 'svelte';
import Output from '../../../modules/terminal/Output.svelte'; import Output from '../../../../modules/terminal/Output.svelte';
import { isInitializing } from './init.svelte'; import { isInitializing } from '../init.svelte';
import type { PrintData } from './terminal'; import type { PrintData } from '../terminal';
interface OutputProps { interface OutputProps {
path: string; path: string;
@@ -23,6 +23,8 @@ function appendOutput(container: HTMLElement, props: OutputProps): Output | unde
} }
export function print(e: HTMLElement, data: PrintData): void { export function print(e: HTMLElement, data: PrintData): void {
if(data.cmd == 'clear') return;
if (isInitializing()) { if (isInitializing()) {
console.error('Terminal is initializing! Skipping Print'); console.error('Terminal is initializing! Skipping Print');
return; return;
@@ -35,7 +37,7 @@ export function print(e: HTMLElement, data: PrintData): void {
} }
export function clear(): void { export function clear(): void {
for (const n of outputInstances) { for(const instance of outputInstances) {
unmount(n); unmount(instance, {});
} }
} }

View File

@@ -0,0 +1,4 @@
export interface KeyStroke {
keys: KeyboardEvent[];
}

View File

@@ -1,12 +1,16 @@
import { Bash, ExitCode, type BashInitArgs, type User } from '../bash/bash'; import { Bash, type BashInitArgs } from '../bash/bash';
import type { VirtualFS } from '../bash/fs'; import type { CommandArgs } from '../bash/commandRegistry';
import type { CommandArgs } from '../bash/static'; import type { User } from '../bash/etc/userData';
import { Char } from '../char'; import { VirtualFS, type TreeNode } from '../bash/fs';
import type { ExitCode } from '../bash/metadata';
import type { ILocale } from './localeRegistry';
export type TerminalMode = {}; export type TerminalMode = {};
export type TermInitArgs = { export type TermInitArgs = {
fs: Map<number, TreeNode>;
bash: BashInitArgs; bash: BashInitArgs;
locale: ILocale
}; };
export type ParsedInput = { export type ParsedInput = {
@@ -22,17 +26,34 @@ export type PrintData = {
export type PageCallbacks = { export type PageCallbacks = {
print: (data: PrintData) => void; print: (data: PrintData) => void;
clear: () => void;
getWidth: () => number; getWidth: () => number;
getFontSize: () => number; getFontSize: () => number;
}; };
export class Terminal { export class Terminal {
private bash: Bash; private _bashInstanceId: number;
private callbacks: Partial<PageCallbacks> = {}; private _bashInstances: Bash[];
private _callbacks: Partial<PageCallbacks> = {};
public fileSystem: VirtualFS;
public locale: ILocale;
constructor(args: TermInitArgs) { constructor(args: TermInitArgs) {
args.bash.stdio = this; args.bash.io = this;
this.bash = new Bash(args.bash); this._bashInstanceId = 0;
this._bashInstances = [];
this._initializeBashInstance(args.bash);
this.fileSystem = new VirtualFS({ fs: args.fs, bash: this._bashInstances[this._bashInstanceId] });
this.locale = args.locale;
}
private _initializeBashInstance(init: BashInitArgs) {
const instance: Bash = new Bash(init);
this._bashInstances.push(instance);
} }
private _parseInput(input: string): ParsedInput { private _parseInput(input: string): ParsedInput {
@@ -88,49 +109,50 @@ export class Terminal {
return result; return result;
} }
throwExeption(message: string, exitCode: ExitCode) {
console.error(message, exitCode);
}
executeCommand(input: string): void { executeCommand(input: string): void {
this.bash.updateHistory(input); this._bashInstances[this._bashInstanceId].updateHistory(input);
const parsed: ParsedInput = this._parseInput(input); const parsed: ParsedInput = this._parseInput(input);
console.log(parsed, 'executeCommand output'); console.log(parsed, 'executeCommand output');
this.bash.executeCommand(parsed.command, parsed.args); this._bashInstances[this._bashInstanceId].executeCommand(parsed.command, parsed.args);
} }
registerCallbacks(callbacks: PageCallbacks): void { registerCallbacks(callbacks: PageCallbacks): void {
this.callbacks = callbacks; this._callbacks = callbacks;
}
clearTerminal() {
this._callbacks.clear?.();
} }
getUser(): User { getUser(): User {
return this.bash.getUser(); return this._bashInstances[this._bashInstanceId].getUser();
} }
getTerminalWidth(): number { getTerminalWidth(): number {
const width = this.callbacks.getWidth?.(); const width = this._callbacks.getWidth?.();
if(!width) { throw new Error('somehow width is undefined still after all the checks'); } if(!width) { throw new Error('somehow width is undefined still after all the checks'); }
console.log(width);
return width; return width;
} }
getFontSize(): number { getFontSize(): number {
const size = this.callbacks.getFontSize?.(); const size = this._callbacks.getFontSize?.();
if(!size) { throw new Error('somehow font size is undefined still after all the checks'); } if(!size) { throw new Error('somehow font size is undefined still after all the checks'); }
console.log(size);
return size; return size;
} }
getCwd(): string { getCwd(): string {
const fs: VirtualFS = this.bash.getFs(); const fs: VirtualFS = this._bashInstances[this._bashInstanceId].getFs();
console.log(fs.getPathByInode(this.bash.getCwd())); console.log(fs.getPathByInode(this._bashInstances[this._bashInstanceId].getCwd()));
return fs.formatPath(fs.getPathByInode(this.bash.getCwd())); return fs.formatPath(fs.getPathByInode(this._bashInstances[this._bashInstanceId].getCwd()));
} }
//TODO: Later reimplement the backend helper methods
/* userLogin(username: string, passwd: string): ExitCode {
return this.bash.userLogin(username, passwd);
} */
PrintOutput(data: PrintData) { PrintOutput(data: PrintData) {
this.callbacks.print?.(data); this._callbacks.print?.(data);
} }
} }

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Languages, Settings2, SunMoon } from '@lucide/svelte'; import { Languages, Settings2, SunMoon } from '@lucide/svelte';
import { theme } from '$lib/stores/theme'; import { theme } from '$lib/stores/theme';
import { locale } from '$lib/stores/lang';
import plFlag from '$lib/assets/plFlag.svg'; import plFlag from '$lib/assets/plFlag.svg';
import enFlag from '$lib/assets/enFlag.svg'; import enFlag from '$lib/assets/enFlag.svg';
import deFlag from '$lib/assets/deFlag.svg'; import deFlag from '$lib/assets/deFlag.svg';
@@ -18,6 +19,10 @@
const langsMenu = window.document.getElementById('langs-menu'); const langsMenu = window.document.getElementById('langs-menu');
langsMenu?.classList.toggle('hide'); langsMenu?.classList.toggle('hide');
} }
function setLang(id: string) {
}
</script> </script>
<div <div
@@ -50,19 +55,21 @@
<label <label
class="border-primary-dark duration-100 hover:ml-1 hover:pl-1 has-checked:border-l-3 has-checked:hover:m-0 has-checked:hover:p-0 light:border-primary-light" class="border-primary-dark duration-100 hover:ml-1 hover:pl-1 has-checked:border-l-3 has-checked:hover:m-0 has-checked:hover:p-0 light:border-primary-light"
> >
<input name="langs" type="radio" class="lang hidden" id="pl" autocomplete="off" /> <input name="langs" type="radio" class="lang hidden" id="pl-PL" autocomplete="off" onclick={() => {console.log($locale)}}/>
<img class="flags mx-2" src={plFlag} alt="PL" height="26" width="26" /> <img class="flags mx-2" src={plFlag} alt="PL" height="26" width="26" />
</label> </label>
<label <label
class="border-primary-dark duration-100 hover:ml-1 hover:pl-1 has-checked:border-l-3 has-checked:hover:m-0 has-checked:hover:p-0 light:border-primary-light" class="border-primary-dark duration-100 hover:ml-1 hover:pl-1 has-checked:border-l-3 has-checked:hover:m-0 has-checked:hover:p-0 light:border-primary-light"
> >
<input name="langs" type="radio" class="lang hidden" id="en" autocomplete="off" /> <input name="langs" type="radio" class="lang hidden" id="en-US" autocomplete="off" onclick={(id) => {
console.log(id);
}}/>
<img class="flags mx-2" src={enFlag} alt="EN" height="26" width="26" /> <img class="flags mx-2" src={enFlag} alt="EN" height="26" width="26" />
</label> </label>
<label <label
class="border-primary-dark duration-100 hover:ml-1 hover:pl-1 has-checked:border-l-3 has-checked:hover:m-0 has-checked:hover:p-0 light:border-primary-light" class="border-primary-dark duration-100 hover:ml-1 hover:pl-1 has-checked:border-l-3 has-checked:hover:m-0 has-checked:hover:p-0 light:border-primary-light"
> >
<input name="langs" type="radio" class="lang hidden" id="en" autocomplete="off" /> <input name="langs" type="radio" class="lang hidden" id="fr-FR" autocomplete="off" />
<img class="flags mx-2" src={deFlag} alt="DE" height="26" width="26" /> <img class="flags mx-2" src={deFlag} alt="DE" height="26" width="26" />
</label> </label>
<label <label

View File

@@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import type { User } from '$lib/stores/bash/bash';
import { Terminal, type PrintData } from '$lib/stores/terminal/terminal'; import { Terminal, type PrintData } from '$lib/stores/terminal/terminal';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Input from './terminal/Input.svelte'; import Input from './terminal/Input.svelte';
import { initTerminal, isInitializing } from '$lib/stores/terminal/init.svelte'; import { initTerminal, isInitializing } from '$lib/stores/terminal/init.svelte';
import { clear, print } from '$lib/stores/terminal/stdio'; import { clear, print } from '$lib/stores/terminal/stdio/io';
import type { User } from '$lib/stores/bash/etc/userData';
const clearTerminal = (): void => clear(); const clearTerminal = (): void => clear();
@@ -22,7 +22,6 @@
} }
//gets an int from padding property value (which is a string) by cutting last 2 letters "px" and parsing to int //gets an int from padding property value (which is a string) by cutting last 2 letters "px" and parsing to int
const padding: number = parseInt(window.getComputedStyle(e, null).getPropertyValue('padding').slice(0, -2)); const padding: number = parseInt(window.getComputedStyle(e, null).getPropertyValue('padding').slice(0, -2));
console.log(padding);
return e.clientWidth - (padding * 2); return e.clientWidth - (padding * 2);
} }
@@ -31,12 +30,15 @@
if(!e) { if(!e) {
throw new Error('cant get font size of the terminal element. its null'); throw new Error('cant get font size of the terminal element. its null');
} }
const size: number = parseInt(window.getComputedStyle(e, null).getPropertyValue('font-size').slice(0, -2));
return size; const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
ctx.font = `${window.getComputedStyle(e, null).getPropertyValue('font-size')} 'JetBrains Mono', monospace;`;
return ctx.measureText('M').width;
} }
function handleInput(e: KeyboardEvent) { function inputHandler(event: KeyboardEvent) {
switch (e.key) { switch (event.key) {
case 'Enter': { case 'Enter': {
terminal.executeCommand(inputValue); terminal.executeCommand(inputValue);
updateTerminal(); updateTerminal();
@@ -55,6 +57,13 @@
//TODO: Make a traverse history function with up/down args //TODO: Make a traverse history function with up/down args
} }
} }
const elem = document.getElementById('cout');
if(!elem){
throw new Error('cant scroll to bottom, element is null');
}
elem.scrollTop = elem.scrollHeight;
} }
//Callback initializer //Callback initializer
@@ -64,6 +73,7 @@
if (!e) return; if (!e) return;
printOutput(e, data); printOutput(e, data);
}, },
clear: clearTerminal,
getWidth: getWidth, getWidth: getWidth,
getFontSize: getFontSize getFontSize: getFontSize
}; };
@@ -87,7 +97,7 @@
onMount(async () => { onMount(async () => {
try { try {
terminal = await initTerminal(testUser, callbackInit); terminal = await initTerminal({ username: 'root', password: '123'}, callbackInit, 'en_US');
updateTerminal(); updateTerminal();
} catch (error) { } catch (error) {
console.error('onMount trycatch failed', error); console.error('onMount trycatch failed', error);
@@ -95,8 +105,8 @@
}); });
</script> </script>
<label for="input" onkeydowncapture={(e) => handleInput(e)}> <label for="input" onkeydowncapture={(event) => inputHandler(event)} class="w-11/12">
<div id="terminal" class="terminal-window shadow-() size-full rounded-md shadow-bg"> <div id="terminal" class="terminal-window shadow-() h-full w-full rounded-md shadow-bg">
<div <div
class="terminal-bar flex h-9 w-full flex-row items-center rounded-t-md bg-bg-dark text-center font-terminal text-sm font-bold text-primary-dark light:bg-bg-dark-light light:text-primary-light" class="terminal-bar flex h-9 w-full flex-row items-center rounded-t-md bg-bg-dark text-center font-terminal text-sm font-bold text-primary-dark light:bg-bg-dark-light light:text-primary-light"
> >
@@ -105,7 +115,7 @@
<button class="size-2.5 cursor-pointer rounded-full p-0" title=""></button> <button class="size-2.5 cursor-pointer rounded-full p-0" title=""></button>
<button class="size-2.5 cursor-pointer rounded-full p-0" title=""></button> <button class="size-2.5 cursor-pointer rounded-full p-0" title=""></button>
</div> </div>
<div class=" flex"> <div class=" flex mr-2 grow">
<h5>{username}</h5> <h5>{username}</h5>
<!-- prettier-ignore --> <!-- prettier-ignore -->
<h5 class=" mr-2">@terminal: </h5> <h5 class=" mr-2">@terminal: </h5>
@@ -113,7 +123,7 @@
</div> </div>
</div> </div>
<div <div
class="inner-content scroll-hidden h-[860px] origin-top overflow-y-auto rounded-b-md bg-bg-light-dark p-4 text-text-dark shadow-subtle light:bg-bg-lighter-light light:text-text-light" class="inner-content scroll-hidden h-7/8 origin-top overflow-y-auto rounded-b-md bg-bg-light-dark p-4 text-text-dark shadow-subtle light:bg-bg-lighter-light light:text-text-light"
id="cout" id="cout"
> >
<div id="outputWrapper"></div> <div id="outputWrapper"></div>

View File

@@ -6,7 +6,7 @@
</script> </script>
<Settings></Settings> <Settings></Settings>
<div class="h-dvh w-full p-24"> <div class="h-dvh w-full p-24 flex justify-center">
<TerminalModule /> <TerminalModule />
</div> </div>