Bash and IO working, basic error setup. Changelog:

Commands:
    - ls (only with -l)
    - cd (basic, probably unfinished)
This commit is contained in:
2025-11-20 14:10:20 +01:00
committed by Kamil Olszewski
parent 4428cc7e8a
commit e853268e52
15 changed files with 775 additions and 441 deletions

View File

@@ -1,44 +1,43 @@
import { command } from '$app/server';
import { COMMANDS, GROUP, HELP_ARGS, PASSWD, type CommandArg, type ICommand } from './static';
import { COMMANDS, GROUP, PASSWD, type CommandArgs, type ICommand, type Result } from './static';
import { VirtualFS } from './fs';
import { Terminal, type PrintData } from '../terminal';
import { Terminal, type PrintData } from '../terminal/terminal';
import { Stack } from '../stack';
import path from 'path';
export interface Permission {
export type Permission = {
r: boolean;
w: boolean;
x: boolean;
}
};
export interface BashInitArgs {
export type BashInitArgs = {
stdio?: Terminal;
user: User;
fs: any;
}
};
// TODO: Finish this
// TODO: Change into a type instead of an enum for performance (low priority)
export enum ExitCode {
SUCCESS = 0,
ERROR = 1
}
export interface User {
export type User = {
username: string;
passwd: string; //HASHED PASSWORD
uid: number; // Normal user 1000+ System user 1-999 root - 0
gid: number; // Primary group | 'Users' 1000 - Others - 1000+ root - 0
home: 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?: string[];
pwd?: string[];
}
cwd?: string[]; //TODO: Make a formated type
pwd?: string[]; //TODO: Make a formated type
};
export interface Group {
export type Group = {
groupname: string;
gid: number; // Primary group 'Users' 1000 - Others - 1000+ root - 0
members: string[];
}
members: number[]; //TODO: Make a formated type UID
};
export class Bash {
private vfs: VirtualFS;
@@ -47,12 +46,10 @@ export class Bash {
private _group: Group[];
private _terminal!: Terminal;
private user: User;
private _helpArgs: CommandArg[];
private _commands: Record<string, ICommand>;
private readonly _commands: Record<string, ICommand>;
constructor(args: BashInitArgs) {
this.user = args.user;
this._helpArgs = HELP_ARGS;
this._commands = COMMANDS;
this._passwd = PASSWD;
this._group = GROUP;
@@ -60,8 +57,6 @@ export class Bash {
this._instances = new Stack<User>();
this.vfs = new VirtualFS({ fs: args.fs, user: args.user });
console.log(this._commands);
}
updateHistory(input: string): void {
@@ -90,6 +85,10 @@ export class Bash {
return this.vfs;
}
hasSudoPerms(uid: number): boolean {
return this._group[1].members.includes(uid);
}
changeUser(user: User) {
this.user = user;
this.vfs.home = this.vfs._splitPathString(user.home);
@@ -97,27 +96,28 @@ export class Bash {
this.vfs.pwd = user.pwd ? user.pwd : this.vfs._splitPathString(user.home);
}
executeCommand(commandName: string, ...args: string[]): void {
executeCommand(commandName: string, args: CommandArgs): void {
let result: Result = { exitCode: ExitCode.ERROR };
const command = this._commands[commandName];
if (!command) this.throwError(ExitCode.ERROR);
if (!command) this.throwError(result);
if (command.root) {
if (this._group[1].members.includes(this.user.username)) {
let out: ExitCode = command.method.call(this, ...args);
this.throwError(out);
if (this.hasSudoPerms(this.user.uid)) {
let out: Result = command.method.call(this, args);
this.appendNewResult(this.getPwd(), out, this.user.history[0]);
}
this.throwError(ExitCode.ERROR);
this.throwError(result);
}
let out: ExitCode = command.method.call(this, ...args);
this.throwError(out);
let out: Result = command.method.call(this, args);
this.appendNewResult(this.getPwd(), out.data?.data, this.user.history[0]);
}
throwError(code: ExitCode, data?: any): void {
//TODO: Make data some interface format or smh.
switch (code) {
default:
this.appendNewResult(this.vfs.pwd, 'Success!');
break;
throwError(result: Result): void {
switch (result.exitCode) {
default: {
throw new Error(`Error, dont know where, just look for it;`);
}
}
}
@@ -141,12 +141,24 @@ export class Bash {
}
}
appendNewResult(path: string[], output: any) {
private appendNewResult(path: string[], output: any, cmd: string) {
const data: PrintData = {
path: this.vfs.formatPath(this.vfs.pathArrayToString(path)),
output: output
output: output,
cmd: cmd
};
console.log('NEW RESULT - ', data);
this._terminal.PrintOutput(data);
}
formatBytes(bytes: number, dPoint?: number): string {
if (!+bytes) return '0';
const k: number = 1024;
const dp: number = dPoint ? (dPoint < 0 ? 0 : dPoint) : 1;
const units: string[] = ['', 'K', 'M', 'G', 'T', 'P'];
const i: number = Math.floor(Math.log(bytes) / Math.log(k));
return `${(bytes / Math.pow(k, i)).toFixed(dp)}${units[i]}`;
}
}

View File

@@ -0,0 +1,252 @@
import { Bash, ExitCode, type Permission } from '../bash';
import { Type, type NodePerms, type TreeNode } from '../fs';
import { asciiByteQSort } from '../sort';
import type { CommandArgs, ICommand, Result, resultData } from '../static';
type LsEntry = {
perms: string;
children: string;
owners: string;
size: string;
modt: string;
name: string;
};
const LsEntryUtils = {
toString(entry: LsEntry): string {
return Object.entries(entry)
.filter(([_, value]) => value !== '')
.map(([_, value]) => value)
.join(' ');
}
};
const months: readonly string[] = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
];
export const cmd_ls = function (this: Bash, args: CommandArgs): Result {
const resultData: resultData = { cmd: 'ls', data: null, args: args };
const result: Result = { exitCode: ExitCode.ERROR, data: resultData };
const nodes: TreeNode[] = [];
const paths: string[][] = [];
//Check if args contain any nonexistent flags, if so add it to an array and check its length. if 0 no bad flags
const invalidItems = args.flags.filter((flag) => !ls.flags.includes(flag));
console.log(invalidItems);
if (invalidItems.length > 0) {
this.throwError(result); //No such flag
}
if (args.args.length === 0) {
const node = this.getFs()._getNodeByPathArray(this.getFs().cwd);
if (node === null) this.throwError(result); //no such path
nodes.push(node!);
}
for (let i = 0; i < args.args.length; i++) {
if (args.args.length !== 0) paths.push(this.getFs().resolvePath(args.args[i]));
const node = this.getFs()._getNodeByPathArray(paths[i]);
if (node === null) this.throwError(result); //no such path
nodes.push(node!);
}
result.exitCode = ExitCode.SUCCESS;
resultData.data = result_ls.call(this, nodes, args);
return result;
};
function result_ls(this: Bash, data: any, args: CommandArgs): HTMLElement {
const dummysonoerror: HTMLElement = document.createElement('div');
const flagInfo = checkFlags(args.flags, ls.flags);
const nodes: TreeNode[] = data;
const f_a: boolean = flagInfo.has('a') || flagInfo.has('f');
const f_h: boolean = flagInfo.has('h');
if (flagInfo.has('l')) {
const w: HTMLElement = document.createElement('div');
for (const node of nodes) {
if (!flagInfo.has('U') && !flagInfo.has('f')) asciiByteQSort(node.children);
const elem: HTMLElement = document.createElement('div');
const rows: string[] = [];
//TODO: Actually calculate sizes here instead of defining numbers for types of nodes
const sizes = node.children.map((child) => (child.type === Type.Directory ? '4096' : '1'));
const maxSizeWidth = Math.max(...sizes.map((size) => size.length));
if (f_a && !flagInfo.has('A')) {
const current: LsEntry = {
perms: formatPermission(node),
children: formatChildren(node),
owners: formatOwners(node, flagInfo),
size: formatSize.call(this, f_h, node, maxSizeWidth),
modt: formatModtime(node),
name: '.'
};
const parent: LsEntry = node.parent
? {
perms: formatPermission(node.parent),
children: formatChildren(node.parent),
owners: formatOwners(node.parent, flagInfo),
size: formatSize.call(this, f_h, node.parent, maxSizeWidth),
modt: formatModtime(node.parent),
name: '..'
}
: {
...current,
name: '..'
};
rows.push(LsEntryUtils.toString(current), LsEntryUtils.toString(parent));
}
for (const child of node.children) {
if (child.name.startsWith('.') && !(f_a || flagInfo.has('A'))) continue;
const cols: LsEntry = {
perms: formatPermission(child),
children: formatChildren(child),
owners: formatOwners(child, flagInfo),
size: formatSize.call(this, f_h, child, maxSizeWidth),
modt: formatModtime(child),
name: /\s/.test(child.name) ? `'${child.name}'` : `${child.name}`
};
if (flagInfo.has('g')) cols.owners = '';
rows.push(LsEntryUtils.toString(cols));
}
//TODO: Calculate the total size of contents in the node
rows.unshift('total ' + node.children.length.toString());
if (nodes.length > 1) {
const nodePath: string =
node.name === '/' ? '/:' : `${this.getFs()._getPathToNode(node).join('/').slice(1)}:`;
rows.unshift(nodePath);
rows.push('\n');
}
for (let i = 0; i < rows.length; i++) {
const p: HTMLElement = document.createElement('p');
p.innerText = rows[i];
elem.appendChild(p);
}
w.appendChild(elem);
}
return w;
}
return dummysonoerror; //TEMP SO NO ERROR CUZ RETURNS HTMLElement EVERY TIME, DELETE LATER
}
function parsePerms(perms: NodePerms): string {
const parts: string[] = [];
//for each key (key representing key name and p representing the key contents) of entries in perms as types keyof NodePerms and Permission
for (const [key, p] of Object.entries(perms) as [keyof NodePerms, Permission][]) {
const perms: string = `${p.r ? 'r' : '-'}${p.w ? 'w' : '-'}${p.x ? 'x' : '-'}`;
parts.push(perms);
}
return parts.join('');
}
function formatOwners(node: TreeNode, flag: any): string {
const owner: string = node.owner;
const group: string = node.group;
if (flag.has('g')) {
return '';
}
return `${owner} ${group}`;
}
function formatPermission(node: TreeNode): string {
return `${node.type === Type.Directory ? 'd' : '-'}${parsePerms(node.permission)}`;
}
function formatChildren(node: TreeNode): string {
const c = node.children.length.toString();
return c.length > 1 ? c : ` ${c}`;
}
function formatSize(this: Bash, human: boolean, node: TreeNode, max: number): string {
const byteSize: number = node.type === Type.Directory ? 4096 : 1; //TEMP, later calculate the size.
let size: string;
if (human) {
size = this.formatBytes(byteSize);
} else size = byteSize.toString();
return size.padStart(max, ' ');
}
function formatModtime(node: TreeNode): string {
const now = new Date();
const hours: string = node.modtime.getHours().toString().padStart(2, '0');
const minutes: string = node.modtime.getMinutes().toString().padStart(2, '0');
const time: string =
now.getFullYear() === node.modtime.getFullYear()
? `${hours}:${minutes}`
: node.modtime.getFullYear().toString();
return [
months[node.modtime.getMonth()],
node.modtime.getDate().toString().padStart(2, ' '),
`${time}`
].join(' ');
}
const checkFlags = (pFlags: string[], dFlags: string[]) => {
const flagSet = new Set(pFlags);
return { has: (flag: string) => flagSet.has(flag) };
};
export const ls: ICommand = {
method: cmd_ls,
flags: [
'l',
'a',
'A',
'c',
'U',
'g',
'G',
'h',
'f',
'x',
'X',
'u',
't',
'S',
'r',
'Q',
'p',
'o',
'n',
'N',
'L'
] as string[],
help: 'PATH TO HELP.MD',
root: false
};

View File

@@ -1,3 +1,4 @@
import type { readonly } from 'svelte/store';
import type { Permission, User } from './bash';
export enum Type {
@@ -5,18 +6,18 @@ export enum Type {
File = 32768
}
type NodePerms = {
export type NodePerms = {
user: Permission;
group: Permission;
other: Permission;
};
export interface FsInitArgs {
export type FsInitArgs = {
fs: any;
user: User;
}
};
export interface TreeNode {
export type TreeNode = {
name: string;
type: Type;
readonly: boolean;
@@ -29,7 +30,8 @@ export interface TreeNode {
owner: string;
group: string;
modtime: Date;
}
parent?: TreeNode;
};
export class VirtualFS {
private root: TreeNode; // TODO make this the correct type
@@ -73,7 +75,6 @@ export class VirtualFS {
}
formatPath(path: string): string {
console.log('FORMAT PATH ', path);
const prefix = this.pathArrayToString(this.home);
if (path.startsWith(prefix)) {
return path.replace(prefix, '~');
@@ -91,9 +92,7 @@ export class VirtualFS {
}
const start = this._isAbsolutePath(path) ? [] : this.cwd.slice();
console.log('START', start);
const parts = this._splitPathString(path);
console.log('PARTS', parts);
for (let i = 0; i < parts.length; i++) {
const seg = parts[i];
@@ -111,7 +110,7 @@ export class VirtualFS {
return start;
}
_getNodeByPathArray(path: string[]): TreeNode {
_getNodeByPathArray(path: string[]): TreeNode | null {
if (path.length === 1 && path[0] === '/') return this.root;
let node: TreeNode = this.root;
@@ -122,9 +121,24 @@ export class VirtualFS {
if (node.type === Type.File) return node;
const newNode = node.children.find((child) => child.name === seg);
console.log(newNode);
if (newNode !== undefined) node = newNode;
else return null;
}
return node;
}
_getPathToNode(node: TreeNode): string[] {
const path: string[] = [];
let current = node;
path.push(node.name);
while (current.parent) {
current = current.parent;
path.unshift(current.name);
}
return path;
}
}

View File

@@ -0,0 +1,47 @@
import type { TreeNode } from './fs';
export function asciiByteQSort(array: TreeNode[]) {
qSort(array, 0, array.length - 1);
}
function qSort(array: TreeNode[], start: number, end: number) {
if (end <= start) return;
let pivot: number = partition(array, start, end);
qSort(array, start, pivot - 1);
qSort(array, pivot + 1, end);
}
function partition(part: TreeNode[], start: number, end: number): number {
let pivot: TreeNode = part[end];
let i: number = start - 1;
for (let j = start; j <= end; j++) {
if (compareStrings(part[j].name, pivot.name) < 0) {
i++;
let temp = part[i];
part[i] = part[j];
part[j] = temp;
}
}
i++;
let temp = part[i];
part[i] = part[end];
part[end] = temp;
return i;
}
function compareStrings(a: string, b: string): number {
const minLength = Math.min(a.length, b.length);
for (let i = 0; i < minLength; i++) {
const charCodeA = a.charCodeAt(i);
const charCodeB = b.charCodeAt(i);
if (charCodeA !== charCodeB) {
return charCodeA - charCodeB;
}
}
return a.length - b.length;
}

View File

@@ -1,27 +1,43 @@
import { Bash, ExitCode, type Group, type User } from './bash';
import { Type, type TreeNode } from './fs';
import type { Char } from '../char';
import { ls } from './commands/ls';
export type CommandArg = `-${string}`;
export interface ICommand {
method: (this: Bash, ...args: any[]) => ExitCode;
args: CommandArg[] | string[] | null;
export type ICommand = {
method: (this: Bash, args: CommandArgs) => Result;
flags: string[];
help: string;
root: boolean;
}
};
export type CommandArgs = {
flags: string[];
args: string[];
};
export type resultData = {
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
args?: CommandArgs;
};
export type Result = {
exitCode: ExitCode;
data?: resultData;
};
export const GROUP: Group[] = [
{
groupname: 'sudo',
gid: 69,
members: ['root', 'admin']
members: [0, 1001]
},
{
groupname: 'users',
gid: 1000,
members: ['admin', 'user']
members: [1001, 1002]
}
];
] as const;
export const PASSWD: User[] = [
{
@@ -50,30 +66,32 @@ export const PASSWD: User[] = [
}
];
export const HELP_ARGS: CommandArg[] = ['-h', '--help'];
export const cmd_return = function (this: Bash, ...args: string[]): ExitCode {
return 0;
export const cmd_return = function (this: Bash, args: CommandArgs): Result {
let result: Result = { exitCode: ExitCode.ERROR };
return result;
};
export const cmd_cd = function (this: Bash, ...args: string[]): ExitCode {
const path = args[0];
let targetNode: TreeNode;
export const cmd_cd = function (this: Bash, args: CommandArgs): Result {
let result: Result = { exitCode: ExitCode.ERROR };
const path = args.args[0];
let targetNode: TreeNode | null;
if (args.length > 1) return ExitCode.ERROR; // Too many args
if (args.args.length > 1) return result; // Too many args
// if no args cd into home dir
if (args.length === 0) {
if (args.args.length === 0) {
this.getFs().cwd = this.getFs().home;
return ExitCode.SUCCESS;
result.exitCode = ExitCode.SUCCESS;
return result;
}
// if the arg is - cd make your current dir the prev dir and vice versa
if (args[0] === '-') {
if (args.args[0] === '-') {
[this.getFs().cwd, this.getFs().pwd] = [this.getFs().pwd, this.getFs().cwd];
return ExitCode.SUCCESS;
result.exitCode = ExitCode.SUCCESS;
return result;
}
// Change the input STRING path from relative to absolute by replacing ~ with the home directory path
@@ -87,28 +105,42 @@ export const cmd_cd = function (this: Bash, ...args: string[]): ExitCode {
this.getFs().pwd = this.getFs().cwd;
targetNode = this.getFs()._getNodeByPathArray(this.getFs().resolvePath(resolvedPath)); // Conversion from STRING path to ARRAY
if (!targetNode) return ExitCode.ERROR;
if (targetNode.type !== Type.Directory) return ExitCode.ERROR;
if (targetNode === null) return result;
if (targetNode.type !== Type.Directory) return result;
//if () return ExitCode.ERROR; // Check for read permissions on node and user
this.getFs().cwd = this.getFs().resolvePath(resolvedPath); // CD was successfull, change current dir to the verified target dir
return ExitCode.SUCCESS;
result.exitCode = ExitCode.SUCCESS;
return result;
};
/* const compareArrays = (A: string[], B: string[]): { value: string; isInB: boolean }[] => {
const result = A.map((item) => ({ value: item, isInB: B.includes(item) }));
// Validate all B items are in A
const invalidItems = B.filter((item) => !A.includes(item));
if (invalidItems.length > 0) {
throw new Error(`Items '${invalidItems.join("', '")}' from B not found in A`);
}
return result;
}; */
export const COMMANDS = {
return: {
method: cmd_return,
args: [] as CommandArg[],
flags: [] as string[],
help: 'PATH TO HELP.MD',
root: false
},
cd: {
method: cmd_cd,
args: [] as string[],
flags: [] as string[],
help: 'PATH TO HELP.MD',
root: false
}
} satisfies Record<string, ICommand>;
},
ls
} as const satisfies Record<string, ICommand>;
/* //export const commands {
return: {