From e1fe000608711ce0e1a5f80ca78eff49334905f6 Mon Sep 17 00:00:00 2001 From: ChaosMakerMLG Date: Mon, 24 Nov 2025 03:49:51 +0100 Subject: [PATCH] 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 --- src/lib/assets/fs/fs.json | 2 +- src/lib/assets/fs/signature | 1 + src/lib/stores/bash/bash.ts | 32 ++++++++++ src/lib/stores/bash/commands/cd.ts | 54 ++++++++++++++++ src/lib/stores/bash/commands/ls.ts | 86 ++++++++++++++++++-------- src/lib/stores/bash/sort.ts | 23 +++---- src/lib/stores/bash/static.ts | 73 +++------------------- src/lib/stores/char.ts | 4 -- src/lib/stores/terminal/init.svelte.ts | 51 ++++++++++++--- src/lib/stores/terminal/terminal.ts | 3 +- 10 files changed, 215 insertions(+), 114 deletions(-) create mode 100644 src/lib/assets/fs/signature create mode 100644 src/lib/stores/bash/commands/cd.ts diff --git a/src/lib/assets/fs/fs.json b/src/lib/assets/fs/fs.json index 993f474..ddb21d7 100755 --- a/src/lib/assets/fs/fs.json +++ b/src/lib/assets/fs/fs.json @@ -620,7 +620,7 @@ "Mtime": "2025-09-13T18:32:08.743+02:00" }, { - "Name": "Desktop", + "Name": "Desktop Test", "Type": 16384, "ReadOnly": false, "Interactible": false, diff --git a/src/lib/assets/fs/signature b/src/lib/assets/fs/signature new file mode 100644 index 0000000..0e428e6 --- /dev/null +++ b/src/lib/assets/fs/signature @@ -0,0 +1 @@ +f6b90e56-2566-47b5-a7df-fb041128929a \ No newline at end of file diff --git a/src/lib/stores/bash/bash.ts b/src/lib/stores/bash/bash.ts index 2297f57..f81d4c5 100644 --- a/src/lib/stores/bash/bash.ts +++ b/src/lib/stores/bash/bash.ts @@ -161,4 +161,36 @@ export class Bash { 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}`); + } } diff --git a/src/lib/stores/bash/commands/cd.ts b/src/lib/stores/bash/commands/cd.ts new file mode 100644 index 0000000..5a77440 --- /dev/null +++ b/src/lib/stores/bash/commands/cd.ts @@ -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 +}; diff --git a/src/lib/stores/bash/commands/ls.ts b/src/lib/stores/bash/commands/ls.ts index f729b51..1ad2197 100644 --- a/src/lib/stores/bash/commands/ls.ts +++ b/src/lib/stores/bash/commands/ls.ts @@ -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_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'); 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 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 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')) { const current: LsEntry = { perms: formatPermission(node), children: formatChildren(node), - owners: formatOwners(node, flagInfo), + owners: formatOwners.call(this, node, flagInfo), size: formatSize.call(this, f_h, node, maxSizeWidth), modt: formatModtime(node), name: '.' @@ -105,7 +121,7 @@ function result_ls(this: Bash, data: any, args: CommandArgs): HTMLElement { ? { perms: formatPermission(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), modt: formatModtime(node.parent), name: '..' @@ -115,24 +131,11 @@ function result_ls(this: Bash, data: any, args: CommandArgs): HTMLElement { 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)); + if (flagInfo.has('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 @@ -170,12 +173,31 @@ function parsePerms(perms: NodePerms): string { 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 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')) { - 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}`; @@ -216,6 +238,19 @@ function formatModtime(node: TreeNode): string { ].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 flagSet = new Set(pFlags); @@ -245,7 +280,8 @@ export const ls: ICommand = { 'o', 'n', 'N', - 'L' + 'L', + 'm' ] as string[], help: 'PATH TO HELP.MD', root: false diff --git a/src/lib/stores/bash/sort.ts b/src/lib/stores/bash/sort.ts index 9693aeb..33dd745 100644 --- a/src/lib/stores/bash/sort.ts +++ b/src/lib/stores/bash/sort.ts @@ -1,23 +1,23 @@ import type { TreeNode } from './fs'; -export function asciiByteQSort(array: TreeNode[]) { - qSort(array, 0, array.length - 1); +export function asciiByteQSort(array: TreeNode[], reverse: boolean) { + 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; - let pivot: number = partition(array, start, end); - qSort(array, start, pivot - 1); - qSort(array, pivot + 1, end); + let pivot: number = partition(array, start, end, reverse); + qSort(array, start, pivot - 1, reverse); + 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 i: number = start - 1; 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++; let temp = part[i]; part[i] = part[j]; @@ -32,7 +32,7 @@ function partition(part: TreeNode[], start: number, end: number): number { 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); for (let i = 0; i < minLength; i++) { @@ -40,8 +40,9 @@ function compareStrings(a: string, b: string): number { const charCodeB = b.charCodeAt(i); 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; } diff --git a/src/lib/stores/bash/static.ts b/src/lib/stores/bash/static.ts index 3948c5c..e532973 100644 --- a/src/lib/stores/bash/static.ts +++ b/src/lib/stores/bash/static.ts @@ -1,7 +1,6 @@ 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 { cd } from './commands/cd'; export type ICommand = { method: (this: Bash, args: CommandArgs) => Result; @@ -63,6 +62,14 @@ export const PASSWD: User[] = [ 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: 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; }; -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 = { return: { method: cmd_return, @@ -133,12 +85,7 @@ export const COMMANDS = { help: 'PATH TO HELP.MD', root: false }, - cd: { - method: cmd_cd, - flags: [] as string[], - help: 'PATH TO HELP.MD', - root: false - }, + cd, ls } as const satisfies Record; diff --git a/src/lib/stores/char.ts b/src/lib/stores/char.ts index 801ea24..1b9eb51 100644 --- a/src/lib/stores/char.ts +++ b/src/lib/stores/char.ts @@ -10,12 +10,10 @@ export class Char { this.value = char as BrandedChar; } - // Public factory method static from(char: string): Char { return new Char(char); } - // Get the primitive value valueOf(): string { return this.value; } @@ -24,7 +22,6 @@ export class Char { return this.value; } - // Character operations charCode(): number { return this.value.charCodeAt(0); } @@ -69,7 +66,6 @@ export class Char { return this.value.toLowerCase() === other.value.toLowerCase(); } - // Static constructors static fromCharCode(charCode: number): Char { return Char.from(String.fromCharCode(charCode)); } diff --git a/src/lib/stores/terminal/init.svelte.ts b/src/lib/stores/terminal/init.svelte.ts index b85af85..58c9477 100644 --- a/src/lib/stores/terminal/init.svelte.ts +++ b/src/lib/stores/terminal/init.svelte.ts @@ -48,28 +48,63 @@ function jsonToTreeNode(data: any, parent?: TreeNode): TreeNode { return node; } +async function fetchFsJson(sig: string): Promise { + 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 { + 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 { const response = await fetch(path); if (!response.ok) throw new Error('Failed to fetch the file system json'); const data = await response.json(); - - const node: TreeNode = jsonToTreeNode(data); - return node; + return data; } export async function initTerminal(user: User, callbackInit: any): Promise { 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: { - user: user, - fs: fsJson + user, + fs } }; - let terminal = new Terminal(args); + const terminal = new Terminal(args); terminal.registerCallbacks(callbackInit); return terminal; diff --git a/src/lib/stores/terminal/terminal.ts b/src/lib/stores/terminal/terminal.ts index b59c835..2a8bd2a 100644 --- a/src/lib/stores/terminal/terminal.ts +++ b/src/lib/stores/terminal/terminal.ts @@ -99,8 +99,7 @@ export class Terminal { getCwd(): string { const fs: VirtualFS = this.bash.getFs(); - let temp: string = fs.formatPath(fs.pathArrayToString(this.bash.getCwd())); - return temp; + return fs.formatPath(fs.pathArrayToString(this.bash.getCwd())); } userLogin(username: string, passwd: string): ExitCode {