Changelog:

- Added `getGroupByName() getGroupByGid() getUserByName getUserByUid()` methods to the `Bash` class.

- Fixed a bug inside the `localStorage` caching logic that saved only 32 characters out of 36 in the signature UUID.

- Added support for reverse order sorting in the quick sort implementation.

**Commands:**

- ls
	-l
	-a
	-A
	-U
	-g
	-G
	-h
	-f
	-n
	-N
	-r
	-Q
	-p
	-o
- cd
This commit is contained in:
2025-11-24 03:49:51 +01:00
committed by Kamil Olszewski
parent e853268e52
commit e1fe000608
10 changed files with 215 additions and 114 deletions

View File

@@ -620,7 +620,7 @@
"Mtime": "2025-09-13T18:32:08.743+02:00" "Mtime": "2025-09-13T18:32:08.743+02:00"
}, },
{ {
"Name": "Desktop", "Name": "Desktop Test",
"Type": 16384, "Type": 16384,
"ReadOnly": false, "ReadOnly": false,
"Interactible": false, "Interactible": false,

View File

@@ -0,0 +1 @@
f6b90e56-2566-47b5-a7df-fb041128929a

View File

@@ -161,4 +161,36 @@ export class Bash {
return `${(bytes / Math.pow(k, i)).toFixed(dp)}${units[i]}`; return `${(bytes / Math.pow(k, i)).toFixed(dp)}${units[i]}`;
} }
getGroupByName(name: string): Group {
const out: Group | undefined = this._group.find((group) => group.groupname === name);
console.log(out);
if (out) return out;
else throw new Error(`Cannot find a user group named ${name}`);
}
getUserByName(name: string): User {
const out: User | undefined = this._passwd.find((user) => user.username === name);
console.log(out);
if (out) return out;
else throw new Error(`Cannot find a user named ${name}`);
}
getGroupByGid(gid: number): Group {
const out: Group | undefined = this._group.find((group) => group.gid === gid);
console.log(out);
if (out) return out;
else throw new Error(`Cannot find a user group named ${name}`);
}
getUserByUid(uid: number): User {
const out: User | undefined = this._passwd.find((user) => user.uid === uid);
console.log(out);
if (out) return out;
else throw new Error(`Cannot find a user group named ${name}`);
}
} }

View File

@@ -0,0 +1,54 @@
import { ExitCode, type Bash } from '../bash';
import { Type, type TreeNode } from '../fs';
import type { CommandArgs, ICommand, Result } from '../static';
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.args.length > 1) return result; // Too many args
// if no args cd into home dir
if (args.args.length === 0) {
this.getFs().cwd = this.getFs().home;
result.exitCode = ExitCode.SUCCESS;
return result;
}
// if the arg is - cd make your current dir the prev dir and vice versa
if (args.args[0] === '-') {
[this.getFs().cwd, this.getFs().pwd] = [this.getFs().pwd, this.getFs().cwd];
result.exitCode = ExitCode.SUCCESS;
return result;
}
// Change the input STRING path from relative to absolute by replacing ~ with the home directory path
//TODO: Change that to a global function inside fs class to parse all possible path formats????? already exists, need to verify
let resolvedPath = path.startsWith('~')
? path.replace('~', this.getFs().pathArrayToString(this.getFs().home))
: path;
this.getFs().pwd = this.getFs().cwd;
targetNode = this.getFs()._getNodeByPathArray(this.getFs().resolvePath(resolvedPath)); // Conversion from STRING path to ARRAY
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
result.exitCode = ExitCode.SUCCESS;
console.log(this.getCwd());
return result;
};
export const cd: ICommand = {
method: cmd_cd,
flags: [] as string[],
help: 'PATH TO HELP.md',
root: false
};

View File

@@ -79,11 +79,12 @@ function result_ls(this: Bash, data: any, args: CommandArgs): HTMLElement {
const f_a: boolean = flagInfo.has('a') || flagInfo.has('f'); const f_a: boolean = flagInfo.has('a') || flagInfo.has('f');
const f_h: boolean = flagInfo.has('h'); const f_h: boolean = flagInfo.has('h');
if (flagInfo.has('l')) { if (flagInfo.has('l') || flagInfo.has('g') || flagInfo.has('o')) {
const w: HTMLElement = document.createElement('div'); const w: HTMLElement = document.createElement('div');
for (const node of nodes) { for (const node of nodes) {
if (!flagInfo.has('U') && !flagInfo.has('f')) asciiByteQSort(node.children); if (!flagInfo.has('U') && !flagInfo.has('f'))
asciiByteQSort(node.children, flagInfo.has('r'));
const elem: HTMLElement = document.createElement('div'); const elem: HTMLElement = document.createElement('div');
const rows: string[] = []; const rows: string[] = [];
@@ -92,11 +93,26 @@ function result_ls(this: Bash, data: any, args: CommandArgs): HTMLElement {
const sizes = node.children.map((child) => (child.type === Type.Directory ? '4096' : '1')); const sizes = node.children.map((child) => (child.type === Type.Directory ? '4096' : '1'));
const maxSizeWidth = Math.max(...sizes.map((size) => size.length)); const maxSizeWidth = Math.max(...sizes.map((size) => size.length));
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.call(this, child, flagInfo),
size: formatSize.call(this, f_h, child, maxSizeWidth),
modt: formatModtime(child),
name: formatName(child, flagInfo)
};
rows.push(LsEntryUtils.toString(cols));
}
if (f_a && !flagInfo.has('A')) { if (f_a && !flagInfo.has('A')) {
const current: LsEntry = { const current: LsEntry = {
perms: formatPermission(node), perms: formatPermission(node),
children: formatChildren(node), children: formatChildren(node),
owners: formatOwners(node, flagInfo), owners: formatOwners.call(this, node, flagInfo),
size: formatSize.call(this, f_h, node, maxSizeWidth), size: formatSize.call(this, f_h, node, maxSizeWidth),
modt: formatModtime(node), modt: formatModtime(node),
name: '.' name: '.'
@@ -105,7 +121,7 @@ function result_ls(this: Bash, data: any, args: CommandArgs): HTMLElement {
? { ? {
perms: formatPermission(node.parent), perms: formatPermission(node.parent),
children: formatChildren(node.parent), children: formatChildren(node.parent),
owners: formatOwners(node.parent, flagInfo), owners: formatOwners.call(this, node.parent, flagInfo),
size: formatSize.call(this, f_h, node.parent, maxSizeWidth), size: formatSize.call(this, f_h, node.parent, maxSizeWidth),
modt: formatModtime(node.parent), modt: formatModtime(node.parent),
name: '..' name: '..'
@@ -115,24 +131,11 @@ function result_ls(this: Bash, data: any, args: CommandArgs): HTMLElement {
name: '..' name: '..'
}; };
rows.push(LsEntryUtils.toString(current), LsEntryUtils.toString(parent)); if (flagInfo.has('r')) {
} rows.push(LsEntryUtils.toString(parent), LsEntryUtils.toString(current));
} else {
for (const child of node.children) { rows.unshift(LsEntryUtils.toString(current), LsEntryUtils.toString(parent));
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 //TODO: Calculate the total size of contents in the node
@@ -170,12 +173,31 @@ function parsePerms(perms: NodePerms): string {
return parts.join(''); return parts.join('');
} }
function formatOwners(node: TreeNode, flag: any): string { function formatOwners(this: Bash, node: TreeNode, flag: any): string {
const owner: string = node.owner; const owner: string = node.owner;
const group: string = node.group; const group: string = node.group;
if (flag.has('G') || flag.has('o')) {
if (flag.has('n')) {
const uid: number = this.getUserByName(owner).uid;
return `${uid}`;
}
return `${owner}`;
}
if (flag.has('g')) { if (flag.has('g')) {
return ''; if (flag.has('n')) {
const gid: number = this.getGroupByName(group).gid;
return `${gid}`;
}
return `${group}`;
}
if (flag.has('n')) {
const uid: number = this.getUserByName(owner).uid;
const gid: number = this.getGroupByName(group).gid;
return `${uid} ${gid}`;
} }
return `${owner} ${group}`; return `${owner} ${group}`;
@@ -216,6 +238,19 @@ function formatModtime(node: TreeNode): string {
].join(' '); ].join(' ');
} }
function formatName(node: TreeNode, flag: any) {
let name: string;
const char: string = flag.has('Q') ? '"' : "'";
if (flag.has('N')) {
name = node.name;
} else {
name = /\s/.test(node.name) ? `${char}${node.name}${char}` : `${node.name}`; //test if any spaces specifically '\s' (escape and 's' for space)
}
return flag.has('p') && node.type === Type.Directory ? `${name}/` : name;
}
const checkFlags = (pFlags: string[], dFlags: string[]) => { const checkFlags = (pFlags: string[], dFlags: string[]) => {
const flagSet = new Set(pFlags); const flagSet = new Set(pFlags);
@@ -245,7 +280,8 @@ export const ls: ICommand = {
'o', 'o',
'n', 'n',
'N', 'N',
'L' 'L',
'm'
] as string[], ] as string[],
help: 'PATH TO HELP.MD', help: 'PATH TO HELP.MD',
root: false root: false

View File

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

View File

@@ -1,7 +1,6 @@
import { Bash, ExitCode, type Group, type User } from './bash'; 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'; import { ls } from './commands/ls';
import { cd } from './commands/cd';
export type ICommand = { export type ICommand = {
method: (this: Bash, args: CommandArgs) => Result; method: (this: Bash, args: CommandArgs) => Result;
@@ -63,6 +62,14 @@ export const PASSWD: User[] = [
gid: 1000, gid: 1000,
home: '/home/user', home: '/home/user',
history: [] //TODO: Delete this and declare a new history array when logging the user in. history: [] //TODO: Delete this and declare a new history array when logging the user in.
},
{
username: 'kamil',
passwd: '000',
uid: 1003,
gid: 1000,
home: '/home/kamil',
history: [] //TODO: Delete this and declare a new history array when logging the user in.
} }
]; ];
@@ -71,61 +78,6 @@ export const cmd_return = function (this: Bash, args: CommandArgs): Result {
return result; return result;
}; };
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.args.length > 1) return result; // Too many args
// if no args cd into home dir
if (args.args.length === 0) {
this.getFs().cwd = this.getFs().home;
result.exitCode = ExitCode.SUCCESS;
return result;
}
// if the arg is - cd make your current dir the prev dir and vice versa
if (args.args[0] === '-') {
[this.getFs().cwd, this.getFs().pwd] = [this.getFs().pwd, this.getFs().cwd];
result.exitCode = ExitCode.SUCCESS;
return result;
}
// Change the input STRING path from relative to absolute by replacing ~ with the home directory path
//TODO: Change that to a global function inside fs class to parse all possible path formats????? already exists, need to verify
let resolvedPath = path.startsWith('~')
? path.replace('~', this.getFs().pathArrayToString(this.getFs().home))
: path;
this.getFs().pwd = this.getFs().cwd;
targetNode = this.getFs()._getNodeByPathArray(this.getFs().resolvePath(resolvedPath)); // Conversion from STRING path to ARRAY
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
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 = { export const COMMANDS = {
return: { return: {
method: cmd_return, method: cmd_return,
@@ -133,12 +85,7 @@ export const COMMANDS = {
help: 'PATH TO HELP.MD', help: 'PATH TO HELP.MD',
root: false root: false
}, },
cd: { cd,
method: cmd_cd,
flags: [] as string[],
help: 'PATH TO HELP.MD',
root: false
},
ls ls
} as const satisfies Record<string, ICommand>; } as const satisfies Record<string, ICommand>;

View File

@@ -10,12 +10,10 @@ export class Char {
this.value = char as BrandedChar; this.value = char as BrandedChar;
} }
// Public factory method
static from(char: string): Char { static from(char: string): Char {
return new Char(char); return new Char(char);
} }
// Get the primitive value
valueOf(): string { valueOf(): string {
return this.value; return this.value;
} }
@@ -24,7 +22,6 @@ export class Char {
return this.value; return this.value;
} }
// Character operations
charCode(): number { charCode(): number {
return this.value.charCodeAt(0); return this.value.charCodeAt(0);
} }
@@ -69,7 +66,6 @@ export class Char {
return this.value.toLowerCase() === other.value.toLowerCase(); return this.value.toLowerCase() === other.value.toLowerCase();
} }
// Static constructors
static fromCharCode(charCode: number): Char { static fromCharCode(charCode: number): Char {
return Char.from(String.fromCharCode(charCode)); return Char.from(String.fromCharCode(charCode));
} }

View File

@@ -48,28 +48,63 @@ function jsonToTreeNode(data: any, parent?: TreeNode): TreeNode {
return node; return node;
} }
async function fetchFsJson(sig: string): Promise<any> {
const signature: string | null = localStorage.getItem('signature');
if (signature !== sig || signature === null || localStorage.getItem('fs') === null) {
const fs: JSON = await fetchFileSystem('/src/lib/assets/fs/fs.json');
localStorage.setItem('signature', sig);
localStorage.setItem('fs', JSON.stringify(fs));
console.info('FS fetched from file, new sinature:', sig);
return fs;
} else {
const fs: string | null = localStorage.getItem('fs');
if (fs === null) throw new Error('FS in LocalStorage is null!');
console.info('FS fetched from localStorage with signature:', sig);
return JSON.parse(fs);
}
}
async function fetchFsSignature(path: string): Promise<any> {
try {
const response = await fetch(path);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.text();
return data.slice(0, 36); //32 characters of uuid + 4 dashes
} catch (error) {
console.error('Failed to fetch the file system signature', error);
}
}
async function fetchFileSystem(path: string): Promise<any> { async function fetchFileSystem(path: string): Promise<any> {
const response = await fetch(path); const response = await fetch(path);
if (!response.ok) throw new Error('Failed to fetch the file system json'); if (!response.ok) throw new Error('Failed to fetch the file system json');
const data = await response.json(); const data = await response.json();
return data;
const node: TreeNode = jsonToTreeNode(data);
return node;
} }
export async function initTerminal(user: User, callbackInit: any): Promise<Terminal> { export async function initTerminal(user: User, callbackInit: any): Promise<Terminal> {
try { try {
let fsJson = await fetchFileSystem('/src/lib/assets/fs/fs.json'); const sig = await fetchFsSignature('/src/lib/assets/fs/signature');
const fsJson = await fetchFsJson(sig);
const fs: TreeNode = jsonToTreeNode(fsJson);
let args: TermInitArgs = { const args: TermInitArgs = {
bash: { bash: {
user: user, user,
fs: fsJson fs
} }
}; };
let terminal = new Terminal(args); const terminal = new Terminal(args);
terminal.registerCallbacks(callbackInit); terminal.registerCallbacks(callbackInit);
return terminal; return terminal;

View File

@@ -99,8 +99,7 @@ export class Terminal {
getCwd(): string { getCwd(): string {
const fs: VirtualFS = this.bash.getFs(); const fs: VirtualFS = this.bash.getFs();
let temp: string = fs.formatPath(fs.pathArrayToString(this.bash.getCwd())); return fs.formatPath(fs.pathArrayToString(this.bash.getCwd()));
return temp;
} }
userLogin(username: string, passwd: string): ExitCode { userLogin(username: string, passwd: string): ExitCode {