aboutsummaryrefslogtreecommitdiff
path: root/modules/core
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-10-27 12:32:37 +0100
committerAgando <roduccomm@gmail.com>2026-10-27 12:32:37 +0100
commit9a61780447066919964b66cdbaa35b9165629419 (patch)
treea96a40371314705317e8cc8473c8d32690ff1b39 /modules/core
parent97279c0eb918701ac915187f5deb9f0adf9236bd (diff)
fix(newos4): never overwrite a directory node; ls honours a path after its flags
MAINT-010 and MAINT-016, the two high items the review left open. Both were re-probed before the change and the new test is red on HEAD (5 passed, 7 FAILED). - virtual-fs.js rename()/copy(): they assigned dst.dir.content[dst.name] with no destination type check, so `mv file /docs` replaced the /docs node and every child vanished while the command printed "Moved: …". Refuse when the destination is a real directory node (the virtual /sys entry is exempt). rename() also erased the file on `mv a a`, because assigning and then deleting the same key removes it; that path is now an explicit no-op. - module-fs.js mv/cp: a refusal used to surface as "No such file or directory", which sent the operator looking for a missing source. The destination is now inspected first and the message says "is a directory — refusing to overwrite it". - module-fs.js ls: the path was only read from argv[0], so `ls -l /work` listed the cwd. The first non-flag argument is now the target; the path-first form is unchanged. Tests: modules/test/test-fs-mv-cp-guard.js (12 asserts) — RED on HEAD (A1-A3, B1, B2, C2, E1), GREEN now; maintain --fix reports "node tests 7/7 pass". Browser on v4.6.104: "mv: /vtest: is a directory — refusing to overwrite it", child and source intact, "ls -l /vtest" lists /vtest (keep.txt), not the cwd.
Diffstat (limited to 'modules/core')
-rw-r--r--modules/core/core-bundle.js29
-rw-r--r--modules/core/module-fs.js21
-rw-r--r--modules/core/module-loader.js2
-rw-r--r--modules/core/virtual-fs.js8
4 files changed, 51 insertions, 9 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index 84d194d..d40988e 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -688,6 +688,11 @@ class VirtualFileSystem {
if (!dst || !dst.name) return false;
const node = src.dir.content[src.name];
if (!node) return false;
+ // mv a a is a no-op, not a delete (assign-then-delete on the same key used to erase it)
+ if (src.dir === dst.dir && src.name === dst.name) return true;
+ const existing = dst.dir.content[dst.name];
+ // Never clobber a directory node: its children would vanish with it (MAINT-010).
+ if (existing && existing.type === 'dir' && !existing._virtual) return false;
dst.dir.content[dst.name] = node;
delete src.dir.content[src.name];
this.saveToStorage();
@@ -703,6 +708,9 @@ class VirtualFileSystem {
if (!dst || !dst.name) return false;
const node = src.dir.content[src.name];
if (!node) return false;
+ const existing = dst.dir.content[dst.name];
+ // Never clobber a directory node: its children would vanish with it (MAINT-010).
+ if (existing && existing.type === 'dir' && !existing._virtual) return false;
dst.dir.content[dst.name] = JSON.parse(JSON.stringify(node));
this.saveToStorage();
this._emitChange(dstPath);
@@ -6482,8 +6490,11 @@ class FileSystemModule {
registerCommands() {
const self = this;
this.commands['ls'] = (args) => {
- const target = args && args.length > 0 && !args[0].startsWith('-') ? args[0] : undefined;
- const flags = (args || []).filter(a => a.startsWith('-'));
+ const argv = args || [];
+ // v4.6.104: the path may follow the flags — `ls -l /work` used to list the cwd
+ // because only argv[0] was considered (MAINT-016).
+ const flags = argv.filter(a => a.startsWith('-'));
+ const target = argv.find(a => !a.startsWith('-'));
const showLong = flags.some(f => f.includes('l'));
const showHidden = flags.some(f => f.includes('a'));
const files = this.terminal.fs.ls(target);
@@ -6618,14 +6629,24 @@ class FileSystemModule {
};
this.commands['mv'] = (args) => {
if(!args[0] || !args[1]) return this.terminal.print('Usage: mv <source> <dest>', 'warning');
- if (this.terminal.fs.mv(args[0], args[1]))
+ const fs = this.terminal.fs;
+ const d = fs._resolvePath(args[1]);
+ const dn = d && d.name ? d.dir.content[d.name] : null;
+ if (dn && dn.type === 'dir' && !dn._virtual)
+ return this.terminal.print('mv: ' + args[1] + ': is a directory — refusing to overwrite it', 'error');
+ if (fs.mv(args[0], args[1]))
this.terminal.print('Moved: ' + args[0] + ' to ' + args[1], 'success');
else
this.terminal.print('mv: ' + args[0] + ': No such file or directory', 'error');
};
this.commands['cp'] = (args) => {
if(!args[0] || !args[1]) return this.terminal.print('Usage: cp <source> <dest>', 'warning');
- if (this.terminal.fs.cp(args[0], args[1]))
+ const fs = this.terminal.fs;
+ const d = fs._resolvePath(args[1]);
+ const dn = d && d.name ? d.dir.content[d.name] : null;
+ if (dn && dn.type === 'dir' && !dn._virtual)
+ return this.terminal.print('cp: ' + args[1] + ': is a directory — refusing to overwrite it', 'error');
+ if (fs.cp(args[0], args[1]))
this.terminal.print('Copied: ' + args[0] + ' to ' + args[1], 'success');
else
this.terminal.print('cp: ' + args[0] + ': No such file or directory', 'error');
diff --git a/modules/core/module-fs.js b/modules/core/module-fs.js
index f2b215a..583cfda 100644
--- a/modules/core/module-fs.js
+++ b/modules/core/module-fs.js
@@ -44,8 +44,11 @@ class FileSystemModule {
registerCommands() {
const self = this;
this.commands['ls'] = (args) => {
- const target = args && args.length > 0 && !args[0].startsWith('-') ? args[0] : undefined;
- const flags = (args || []).filter(a => a.startsWith('-'));
+ const argv = args || [];
+ // v4.6.104: the path may follow the flags — `ls -l /work` used to list the cwd
+ // because only argv[0] was considered (MAINT-016).
+ const flags = argv.filter(a => a.startsWith('-'));
+ const target = argv.find(a => !a.startsWith('-'));
const showLong = flags.some(f => f.includes('l'));
const showHidden = flags.some(f => f.includes('a'));
const files = this.terminal.fs.ls(target);
@@ -180,14 +183,24 @@ class FileSystemModule {
};
this.commands['mv'] = (args) => {
if(!args[0] || !args[1]) return this.terminal.print('Usage: mv <source> <dest>', 'warning');
- if (this.terminal.fs.mv(args[0], args[1]))
+ const fs = this.terminal.fs;
+ const d = fs._resolvePath(args[1]);
+ const dn = d && d.name ? d.dir.content[d.name] : null;
+ if (dn && dn.type === 'dir' && !dn._virtual)
+ return this.terminal.print('mv: ' + args[1] + ': is a directory — refusing to overwrite it', 'error');
+ if (fs.mv(args[0], args[1]))
this.terminal.print('Moved: ' + args[0] + ' to ' + args[1], 'success');
else
this.terminal.print('mv: ' + args[0] + ': No such file or directory', 'error');
};
this.commands['cp'] = (args) => {
if(!args[0] || !args[1]) return this.terminal.print('Usage: cp <source> <dest>', 'warning');
- if (this.terminal.fs.cp(args[0], args[1]))
+ const fs = this.terminal.fs;
+ const d = fs._resolvePath(args[1]);
+ const dn = d && d.name ? d.dir.content[d.name] : null;
+ if (dn && dn.type === 'dir' && !dn._virtual)
+ return this.terminal.print('cp: ' + args[1] + ': is a directory — refusing to overwrite it', 'error');
+ if (fs.cp(args[0], args[1]))
this.terminal.print('Copied: ' + args[0] + ' to ' + args[1], 'success');
else
this.terminal.print('cp: ' + args[0] + ': No such file or directory', 'error');
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index 18137ec..0d61710 100644
--- a/modules/core/module-loader.js
+++ b/modules/core/module-loader.js
@@ -4,7 +4,7 @@
// Core module bundle loaded synchronously in main.html.
// This loader only handles version cache-busting, splash, terminal init, and optional ext modules.
- const LATEST_VER = 'v4.6.103';
+ const LATEST_VER = 'v4.6.104';
const CACHE = '?v=' + LATEST_VER;
const stored = localStorage.getItem('webx11_cache_ver');
if (stored !== LATEST_VER) {
diff --git a/modules/core/virtual-fs.js b/modules/core/virtual-fs.js
index 3ed1b40..3a3e119 100644
--- a/modules/core/virtual-fs.js
+++ b/modules/core/virtual-fs.js
@@ -686,6 +686,11 @@ class VirtualFileSystem {
if (!dst || !dst.name) return false;
const node = src.dir.content[src.name];
if (!node) return false;
+ // mv a a is a no-op, not a delete (assign-then-delete on the same key used to erase it)
+ if (src.dir === dst.dir && src.name === dst.name) return true;
+ const existing = dst.dir.content[dst.name];
+ // Never clobber a directory node: its children would vanish with it (MAINT-010).
+ if (existing && existing.type === 'dir' && !existing._virtual) return false;
dst.dir.content[dst.name] = node;
delete src.dir.content[src.name];
this.saveToStorage();
@@ -701,6 +706,9 @@ class VirtualFileSystem {
if (!dst || !dst.name) return false;
const node = src.dir.content[src.name];
if (!node) return false;
+ const existing = dst.dir.content[dst.name];
+ // Never clobber a directory node: its children would vanish with it (MAINT-010).
+ if (existing && existing.type === 'dir' && !existing._virtual) return false;
dst.dir.content[dst.name] = JSON.parse(JSON.stringify(node));
this.saveToStorage();
this._emitChange(dstPath);