diff --git a/browser/js-kernel.js b/browser/js-kernel.js index e8f2b557..80c34336 100644 --- a/browser/js-kernel.js +++ b/browser/js-kernel.js @@ -981,6 +981,10 @@ var Module = (() => { const ECV_FIFO_READ = 10006; const ECV_FIFO_WRITE = 10007; + // emscripten-specific syscalls (not in Linux AArch64 table) + const ECV_CHMOD = 10008; + const ECV_FD_FDSTAT_GET = 10009; + SysFuncMap.set(ECV_CLONE, ___syscall_clone); SysFuncMap.set(ECV_WAIT4, ___syscall_wait4); SysFuncMap.set(ECV_EXECVE, ___syscall_execve); @@ -1032,6 +1036,16 @@ var Module = (() => { SysFuncMap.set(ECV_FIFO_READ, _fd_fifo_read); SysFuncMap.set(ECV_FIFO_WRITE, _fd_fifo_write); + // new filesystem syscalls + SysFuncMap.set(ECV_CHMOD, ___syscall_chmod); + SysFuncMap.set(ECV_FCHMOD, ___syscall_fchmod); + SysFuncMap.set(ECV_FCHMODAT, ___syscall_fchmodat); + SysFuncMap.set(ECV_FCHOWNAT, ___syscall_fchownat); + SysFuncMap.set(ECV_FDATASYNC, ___syscall_fdatasync); + SysFuncMap.set(ECV_RENAMEAT, ___syscall_renameat); + SysFuncMap.set(ECV_SYMLINKAT, ___syscall_symlinkat); + SysFuncMap.set(ECV_FD_FDSTAT_GET, _fd_fdstat_get); + class ExitStatus { name = "ExitStatus"; @@ -2968,19 +2982,31 @@ var Module = (() => { ["arch", "busybox"], ["ascii", "busybox"], ["basename", "busybox"], + ["chmod", "busybox"], + ["chown", "busybox"], ["clear", "busybox"], + ["cp", "busybox"], ["date", "busybox"], + ["dirname", "busybox"], + ["expr", "busybox"], + ["head", "busybox"], ["hostname", "busybox"], + ["ln", "busybox"], ["ls", "busybox"], ["mkdir", "busybox"], + ["mv", "busybox"], ["rm", "busybox"], ["rmdir", "busybox"], + ["seq", "busybox"], + ["sleep", "busybox"], + ["tail", "busybox"], ["tree", "busybox"], ["uname", "busybox"], ["vi", "busybox"], ["cat", "busybox"], ["touch", "busybox"], - ["ps", "busybox"] + ["ps", "busybox"], + ["wc", "busybox"] ])); } } @@ -4613,6 +4639,109 @@ var Module = (() => { return -e.errno } } + function ___syscall_chmod(path, mode) { + try { + path = SYSCALLS.getStr(path); + FS.chmod(path, mode); + return 0 + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno + } + } + + function ___syscall_fchmod(fd, mode) { + try { + FS.fchmod(fd, mode); + return 0 + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno + } + } + + function ___syscall_fchmodat(dirfd, path, mode, flags) { + try { + var nofollow = flags & 256; + path = SYSCALLS.getStr(path); + path = SYSCALLS.calculateAt(dirfd, path); + FS.chmod(path, mode, nofollow); + return 0 + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno + } + } + + function ___syscall_fchownat(dirfd, path, owner, group, flags) { + try { + path = SYSCALLS.getStr(path); + var nofollow = flags & 256; + flags = flags & ~256; + path = SYSCALLS.calculateAt(dirfd, path); + (nofollow ? FS.lchown : FS.chown)(path, owner, group); + return 0 + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno + } + } + + function ___syscall_fdatasync(fd) { + try { + var stream = SYSCALLS.getStreamFromFD(fd); + return 0 + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno + } + } + + function ___syscall_renameat(olddirfd, oldpath, newdirfd, newpath) { + try { + oldpath = SYSCALLS.getStr(oldpath); + newpath = SYSCALLS.getStr(newpath); + oldpath = SYSCALLS.calculateAt(olddirfd, oldpath); + newpath = SYSCALLS.calculateAt(newdirfd, newpath); + FS.rename(oldpath, newpath); + return 0 + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno + } + } + + function ___syscall_symlinkat(target, dirfd, linkpath) { + try { + target = SYSCALLS.getStr(target); + linkpath = SYSCALLS.getStr(linkpath); + linkpath = SYSCALLS.calculateAt(dirfd, linkpath); + FS.symlink(target, linkpath); + return 0 + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno + } + } + + function _fd_fdstat_get(fd, pbuf) { + try { + var rightsBase = 0; + var rightsInheriting = 0; + var flags = 0; + var stream = SYSCALLS.getStreamFromFD(fd); + var type = stream.tty ? 2 : FS.isDir(stream.mode) ? 3 : FS.isLink(stream.mode) ? 7 : 4; + (growMemViews(gWasmMemory), HEAP8)[pbuf] = type; + (growMemViews(gWasmMemory), HEAP16)[pbuf + 2 >> 1] = flags; + (growMemViews(gWasmMemory), HEAP64)[pbuf + 8 >> 3] = BigInt(rightsBase); + (growMemViews(gWasmMemory), HEAP64)[pbuf + 16 >> 3] = BigInt(rightsInheriting); + return 0 + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return e.errno + } + } + var runtimeKeepaliveCounter = 0; var ENV = {}; var getExecutableName = () => initProcessJsPath; diff --git a/browser/process.js b/browser/process.js index 787598b0..5359fc5d 100644 --- a/browser/process.js +++ b/browser/process.js @@ -217,7 +217,7 @@ var Module = (() => { } function initRuntime() { - wasmExports["ga"](); + wasmExports["Vc"](); } function preMain() { } @@ -326,7 +326,7 @@ var Module = (() => { function receiveInstance(instance) { wasmExports = instance.exports; - wasmTable = wasmExports["xc"]; + wasmTable = wasmExports["Fc"]; return wasmExports } @@ -801,6 +801,10 @@ var Module = (() => { const ECV_FIFO_READ = 10006; const ECV_FIFO_WRITE = 10007; + // emscripten-specific syscalls (not in Linux AArch64 table) + const ECV_CHMOD = 10008; + const ECV_FD_FDSTAT_GET = 10009; + // This function assumes that emscripten JS syscall is executed synchronously. function ecvProxySyscallJs(sysNum, ...callArgs) { @@ -1131,6 +1135,39 @@ var Module = (() => { function ___syscall_utimensat(dirfd, path, times, flags) { return ecvProxySyscallJs(ECV_UTIMENSAT, dirfd, path, times, flags); } + + function ___syscall_chmod(path, mode) { + return ecvProxySyscallJs(ECV_CHMOD, path, mode); + } + + function ___syscall_fchmod(fd, mode) { + return ecvProxySyscallJs(ECV_FCHMOD, fd, mode); + } + + function ___syscall_fchmodat2(dirfd, path, mode, flags) { + return ecvProxySyscallJs(ECV_FCHMODAT, dirfd, path, mode, flags); + } + + function ___syscall_fchownat(dirfd, path, owner, group, flags) { + return ecvProxySyscallJs(ECV_FCHOWNAT, dirfd, path, owner, group, flags); + } + + function ___syscall_fdatasync(fd) { + return ecvProxySyscallJs(ECV_FDATASYNC, fd); + } + + function ___syscall_renameat(olddirfd, oldpath, newdirfd, newpath) { + return ecvProxySyscallJs(ECV_RENAMEAT, olddirfd, oldpath, newdirfd, newpath); + } + + function ___syscall_symlinkat(target, dirfd, linkpath) { + return ecvProxySyscallJs(ECV_SYMLINKAT, target, dirfd, linkpath); + } + + function _fd_fdstat_get(fd, pbuf) { + return ecvProxySyscallJs(ECV_FD_FDSTAT_GET, fd, pbuf); + } + var __abort_js = () => abort(""); var runtimeKeepaliveCounter = 0; var __emscripten_runtime_keepalive_clear = () => { @@ -1530,68 +1567,76 @@ var Module = (() => { function assignWasmImports() { wasmImports = { e: ___ecv_syscall_ioctl, - ea: ___syscall_clone, - da: ___syscall_execve, + ma: ___syscall_clone, + la: ___syscall_execve, m: ___syscall_exit, - fa: ___syscall_getpgid, - y: ___syscall_pipe2, + na: ___syscall_getpgid, + F: ___syscall_pipe2, h: ___syscall_poll, i: ___syscall_pselect6, - p: ___syscall_sendfile, - ga: ___syscall_setpgid, - ca: ___syscall_wait4, - A: ___call_sighandler, + x: ___syscall_sendfile, + oa: ___syscall_setpgid, + ka: ___syscall_wait4, + C: ___call_sighandler, l: ___cxa_throw, - aa: ___syscall_chdir, - _: ___syscall_dup, - Z: ___syscall_dup3, - X: ___syscall_faccessat, + ia: ___syscall_chdir, + ba: ___syscall_chmod, + ga: ___syscall_dup, + fa: ___syscall_dup3, + da: ___syscall_faccessat, + ca: ___syscall_fchmod, + aa: ___syscall_fchmodat2, + _: ___syscall_fchownat, c: ___syscall_fcntl64, - U: ___syscall_fstat64, - P: ___syscall_ftruncate64, - O: ___syscall_getcwd, - N: ___syscall_getdents64, + Z: ___syscall_fdatasync, + W: ___syscall_fstat64, + R: ___syscall_ftruncate64, + Q: ___syscall_getcwd, + P: ___syscall_getdents64, j: ___syscall_ioctl, - R: ___syscall_lstat64, - I: ___syscall_mkdirat, - S: ___syscall_newfstatat, - H: ___syscall_openat, - z: ___syscall_readlinkat, - T: ___syscall_stat64, - x: ___syscall_statfs64, + T: ___syscall_lstat64, + K: ___syscall_mkdirat, + U: ___syscall_newfstatat, + $: ___syscall_openat, + B: ___syscall_readlinkat, + A: ___syscall_renameat, + V: ___syscall_stat64, + z: ___syscall_statfs64, + y: ___syscall_symlinkat, v: ___syscall_truncate64, u: ___syscall_unlinkat, t: ___syscall_utimensat, - ba: __abort_js, - L: __emscripten_init_main_thread_js, + ja: __abort_js, + N: __emscripten_init_main_thread_js, w: __emscripten_notify_mailbox_postmessage, - F: __emscripten_receive_on_main_thread_js, - C: __emscripten_runtime_keepalive_clear, + I: __emscripten_receive_on_main_thread_js, + E: __emscripten_runtime_keepalive_clear, n: __emscripten_thread_cleanup, - K: __emscripten_thread_mailbox_await, - W: __emscripten_thread_set_strongref, + M: __emscripten_thread_mailbox_await, + Y: __emscripten_thread_set_strongref, o: __tzset_js, - $: _clock_time_get, - ha: ecv_proxy_process_memory_copy_req, - E: _emscripten_check_blocking_allowed, - Y: _emscripten_date_now, - V: _emscripten_exit_with_live_runtime, + ha: _clock_time_get, + pa: ecv_proxy_process_memory_copy_req, + H: _emscripten_check_blocking_allowed, + ea: _emscripten_date_now, + X: _emscripten_exit_with_live_runtime, b: _emscripten_get_now, - s: _emscripten_resize_heap, - q: _environ_get, - r: _environ_sizes_get, - ia: execve_memory_copy_req, + r: _emscripten_resize_heap, + p: _environ_get, + q: _environ_sizes_get, + qa: execve_memory_copy_req, k: _exit, g: _fd_close, - G: _fd_pread, - D: _fd_pwrite, + s: _fd_fdstat_get, + J: _fd_pread, + G: _fd_pwrite, f: _fd_read, - J: _fd_seek, - Q: _fd_sync, + L: _fd_seek, + S: _fd_sync, d: _fd_write, a: wasmMemory, - B: _proc_exit, - M: _random_get + D: _proc_exit, + O: _random_get } } @@ -1600,10 +1645,10 @@ var Module = (() => { async function initWasmInstance() { // init Wasm module wasmExports = await instantiateWasm(); - _main = Module["_main"] = (a0, a1) => (_main = Module["_main"] = wasmExports["yc"])(a0, a1); - __emscripten_stack_restore = a0 => (__emscripten_stack_restore = wasmExports["Wc"])(a0); - __emscripten_stack_alloc = a0 => (__emscripten_stack_alloc = wasmExports["Xc"])(a0); - _emscripten_stack_get_current = () => (_emscripten_stack_get_current = wasmExports["Yc"])(); + _main = Module["_main"] = (a0, a1) => (_main = Module["_main"] = wasmExports["Gc"])(a0, a1); + __emscripten_stack_restore = a0 => (__emscripten_stack_restore = wasmExports["cd"])(a0); + __emscripten_stack_alloc = a0 => (__emscripten_stack_alloc = wasmExports["dd"])(a0); + _emscripten_stack_get_current = () => (_emscripten_stack_get_current = wasmExports["ed"])(); preInit(); moduleRtn = readyPromise; diff --git a/runtime/syscalls/SysTable.h b/runtime/syscalls/SysTable.h index 0fc7a6af..949a0152 100644 --- a/runtime/syscalls/SysTable.h +++ b/runtime/syscalls/SysTable.h @@ -86,6 +86,8 @@ // ioctls cmds (Linux) #define _LINUX_TCGETS 0x5401 #define _LINUX_TCSETS 0x5402 +#define _LINUX_TCSETSW 0x5403 +#define _LINUX_TCSETSF 0x5404 #define _LINUX_TIOCGWINSZ 0x5413 #define _LINUX_TIOCGPGRP 0x540f #define _LINUX_TIOSGPGRP 0x5410 diff --git a/runtime/syscalls/SyscallBrowser.cpp b/runtime/syscalls/SyscallBrowser.cpp index d879eca8..d0d3367d 100644 --- a/runtime/syscalls/SyscallBrowser.cpp +++ b/runtime/syscalls/SyscallBrowser.cpp @@ -341,9 +341,6 @@ constexpr uint32_t UNIMPLEMENTED_SYSCALLS[] = { ECV_LOOKUP_DCOOKIE, ECV_FLOCK, ECV_MKNODAT, - ECV_SYMLINKAT, - ECV_LINKAT, - ECV_RENAMEAT, ECV_RENAMEAT2, ECV_UMOUNT2, ECV_MOUNT, @@ -353,15 +350,10 @@ constexpr uint32_t UNIMPLEMENTED_SYSCALLS[] = { ECV_FALLOCATE, ECV_FCHDIR, ECV_CHROOT, - ECV_FCHMOD, - ECV_FCHMODAT, - ECV_FCHOWNAT, - ECV_FCHOWN, ECV_VHANGUP, ECV_QUOTACTL, ECV_QUOTACTL_FD, ECV_NEWFSTAT, - ECV_FDATASYNC, ECV_SYNC, ECV_SYNC_FILE_RANGE, ECV_SYNCFS, @@ -409,7 +401,6 @@ constexpr uint32_t UNIMPLEMENTED_SYSCALLS[] = { ECV_CLOCK_SETTIME, ECV_CLOCK_GETRES, ECV_CLOCK_ADJTIME, - ECV_NANOSLEEP, ECV_GETITIMER, ECV_SETITIMER, @@ -425,7 +416,6 @@ constexpr uint32_t UNIMPLEMENTED_SYSCALLS[] = { ECV_KEXEC_FILE_LOAD, ECV_INIT_MODULE, ECV_DELETE_MODULE, - ECV_PRLIMIT64, ECV_FINIT_MODULE, // System logging @@ -451,7 +441,6 @@ constexpr uint32_t UNIMPLEMENTED_SYSCALLS[] = { ECV_TKILL, ECV_SIGALTSTACK, ECV_RT_SIGSUSPEND, - ECV_RT_SIGPROCMASK, ECV_RT_SIGPENDING, ECV_RT_SIGTIMEDWAIT, ECV_RT_SIGQUEUEINFO, @@ -468,7 +457,6 @@ constexpr uint32_t UNIMPLEMENTED_SYSCALLS[] = { ECV_SETDOMAINNAME, ECV_GETRLIMIT, ECV_SETRLIMIT, - ECV_UMASK, ECV_GETCPU, ECV_SETTIMEOFDAY, ECV_ADJTIMEX, @@ -818,7 +806,9 @@ void RuntimeManager::SVCBrowserCall(uint8_t *arena_ptr) { } break; } - case _LINUX_TCSETS: { + case _LINUX_TCSETS: + case _LINUX_TCSETSW: + case _LINUX_TCSETSF: { struct termios t_wasm; auto t_host = *(_elfarm64_termios *) TranslateVMA(this, arena_ptr, arg); t_wasm.c_iflag = t_host.c_iflag; @@ -1499,6 +1489,104 @@ void RuntimeManager::SVCBrowserCall(uint8_t *arena_ptr) { X0_D = 0; break; } + case ECV_SYMLINKAT: /* int symlinkat(const char *target, int newdirfd, const char *linkpath) */ + { + int res = symlinkat((const char *) TranslateVMA(this, arena_ptr, X0_Q), X1_D, + (const char *) TranslateVMA(this, arena_ptr, X2_Q)); + X0_Q = SetSyscallRes(res); + break; + } + case ECV_LINKAT: /* int linkat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath, int flags) */ + { + int res = linkat(X0_D, (const char *) TranslateVMA(this, arena_ptr, X1_Q), X2_D, + (const char *) TranslateVMA(this, arena_ptr, X3_Q), X4_D); + X0_Q = SetSyscallRes(res); + break; + } + case ECV_RENAMEAT: /* int renameat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath) */ + { + int res = renameat(X0_D, (const char *) TranslateVMA(this, arena_ptr, X1_Q), X2_D, + (const char *) TranslateVMA(this, arena_ptr, X3_Q)); + X0_Q = SetSyscallRes(res); + break; + } + case ECV_FCHMOD: /* int fchmod(unsigned int fd, umode_t mode) */ + { + int res = fchmod(X0_D, X1_D); + X0_Q = SetSyscallRes(res); + break; + } + case ECV_FCHMODAT: /* int fchmodat(int dfd, const char *filename, umode_t mode) */ + { + int res = fchmodat(X0_D, (const char *) TranslateVMA(this, arena_ptr, X1_Q), X2_D, 0); + X0_Q = SetSyscallRes(res); + break; + } + case ECV_FCHOWNAT: /* int fchownat(int dfd, const char *filename, uid_t user, gid_t group, int flag) */ + { + int res = fchownat(X0_D, (const char *) TranslateVMA(this, arena_ptr, X1_Q), X2_D, X3_D, X4_D); + X0_Q = SetSyscallRes(res); + break; + } + case ECV_FDATASYNC: /* int fdatasync(unsigned int fd) */ + { + int res = fdatasync(X0_D); + X0_Q = SetSyscallRes(res); + break; + } + case ECV_NANOSLEEP: /* int nanosleep(const struct timespec *req, struct timespec *rem) */ + { + struct timespec _wasm_req, _wasm_rem; + auto _elf_req = (const struct _elfarm64_timespec *) TranslateVMA(this, arena_ptr, X0_Q); + _wasm_req.tv_sec = _elf_req->tv_sec; + _wasm_req.tv_nsec = _elf_req->tv_nsec; + int res = nanosleep(&_wasm_req, &_wasm_rem); + if (X1_Q != 0) { + struct _elfarm64_timespec _elf_rem; + _elf_rem.tv_sec = _wasm_rem.tv_sec; + _elf_rem.tv_nsec = _wasm_rem.tv_nsec; + memcpy(TranslateVMA(this, arena_ptr, X1_Q), &_elf_rem, sizeof(_elf_rem)); + } + X0_Q = SetSyscallRes(res); + break; + } + case ECV_UMASK: /* mode_t umask(mode_t mask) */ + { + mode_t res = umask(X0_D); + X0_Q = (uint64_t) res; + break; + } + case ECV_RT_SIGPROCMASK: /* int rt_sigprocmask(int how, const sigset_t *set, sigset_t *oldset, size_t sigsetsize) */ + { + const sigset_t *set_p = X1_Q == 0 ? nullptr : (const sigset_t *) TranslateVMA(this, arena_ptr, X1_Q); + sigset_t *oldset_p = X2_Q == 0 ? nullptr : (sigset_t *) TranslateVMA(this, arena_ptr, X2_Q); + int res = sigprocmask(X0_D, set_p, oldset_p); + X0_Q = SetSyscallRes(res); + break; + } + case ECV_PRLIMIT64: /* int prlimit64(pid_t pid, unsigned int resource, const struct rlimit64 *new_rlim, struct rlimit64 *old_rlim) */ + { + /* Linux rlimit64 for aarch64 uses 64-bit values */ + struct { + uint64_t rlim_cur; + uint64_t rlim_max; + } _linux_rlimit; + + if (X3_Q != 0) { + /* Get current limit */ + struct rlimit rl; + int res = getrlimit(X1_D, &rl); + if (res == 0) { + _linux_rlimit.rlim_cur = (uint64_t) rl.rlim_cur; + _linux_rlimit.rlim_max = (uint64_t) rl.rlim_max; + memcpy(TranslateVMA(this, arena_ptr, X3_Q), &_linux_rlimit, sizeof(_linux_rlimit)); + } + X0_Q = SetSyscallRes(res); + } else { + X0_Q = 0; + } + break; + } case ECV_STATX: /* statx (int dfd, const char *path, unsigned flags, unsigned mask, struct statx *buffer) */ { int dfd = X0_D; diff --git a/runtime/syscalls/SyscallWasi.cpp b/runtime/syscalls/SyscallWasi.cpp index 10e6c164..766c819f 100644 --- a/runtime/syscalls/SyscallWasi.cpp +++ b/runtime/syscalls/SyscallWasi.cpp @@ -127,7 +127,9 @@ void RuntimeManager::SVCWasiCall(uint8_t *arena_ptr) { unsigned long arg = X2_Q; switch (cmd) { case _LINUX_TCGETS: - case _LINUX_TCSETS: { + case _LINUX_TCSETS: + case _LINUX_TCSETSW: + case _LINUX_TCSETSF: { X0_D = ioctl(fd, cmd, TranslateVMA(this, arena_ptr, arg)); } break; default: X0_Q = -_LINUX_ENOTTY; break; diff --git a/tests/browser/playwright.config.js b/tests/browser/playwright.config.js index 9eeb6a31..20c9870e 100644 --- a/tests/browser/playwright.config.js +++ b/tests/browser/playwright.config.js @@ -40,7 +40,7 @@ module.exports = defineConfig({ }, { name: 'bash', - testMatch: 'bash.spec.js', + testMatch: ['bash.spec.js', 'syscall-probe.spec.js'], use: { browserName: 'chromium', baseURL: 'http://localhost:3001', diff --git a/tests/browser/syscall-probe.spec.js b/tests/browser/syscall-probe.spec.js new file mode 100644 index 00000000..1f5112c0 --- /dev/null +++ b/tests/browser/syscall-probe.spec.js @@ -0,0 +1,109 @@ +const { test, expect } = require('@playwright/test'); + +function readTerminalText() { + const xterm = window.__test_xterm; + if (!xterm) return ''; + const buf = xterm.buffer.active; + let text = ''; + for (let i = 0; i <= buf.cursorY + buf.baseY; i++) { + const line = buf.getLine(i); + if (line) { + text += line.translateToString(true) + '\n'; + } + } + return text; +} + +async function waitForTerminalContent(page, content, timeout = 30000) { + return page.waitForFunction( + ({ fn, expected }) => { + const text = new Function('return (' + fn + ')()')(); + if (text.includes(expected)) return text; + return false; + }, + { fn: readTerminalText.toString(), expected: content }, + { timeout } + ); +} + +async function typeCommand(page, command) { + await page.locator('.xterm-helper-textarea').focus(); + await page.keyboard.type(command); + await page.keyboard.press('Enter'); +} + +test.describe('Syscall probe - new syscalls', () => { + + test('touch + chmod (fchmodat)', async ({ page }) => { + await page.goto('/'); + await waitForTerminalContent(page, 'bash-static.wasm'); + + await typeCommand(page, 'touch /tmp/tfile && chmod 755 /tmp/tfile && echo CHMOD_OK'); + const out = await waitForTerminalContent(page, 'CHMOD_OK'); + expect(await out.jsonValue()).toContain('CHMOD_OK'); + }); + + test('ln -s (symlinkat)', async ({ page }) => { + await page.goto('/'); + await waitForTerminalContent(page, 'bash-static.wasm'); + + await typeCommand(page, 'touch /tmp/orig && ln -s /tmp/orig /tmp/slink && echo SYMLINK_OK'); + const out = await waitForTerminalContent(page, 'SYMLINK_OK'); + expect(await out.jsonValue()).toContain('SYMLINK_OK'); + }); + + test('mv (renameat)', async ({ page }) => { + await page.goto('/'); + await waitForTerminalContent(page, 'bash-static.wasm'); + + await typeCommand(page, 'touch /tmp/mvfile && mv /tmp/mvfile /tmp/mvdst && echo MV_OK'); + const out = await waitForTerminalContent(page, 'MV_OK'); + expect(await out.jsonValue()).toContain('MV_OK'); + }); + + test('umask', async ({ page }) => { + await page.goto('/'); + await waitForTerminalContent(page, 'bash-static.wasm'); + + await typeCommand(page, 'umask'); + const out = await waitForTerminalContent(page, '00'); + expect(await out.jsonValue()).toMatch(/\d{4}/); + }); + + test('head (basic busybox)', async ({ page }) => { + await page.goto('/'); + await waitForTerminalContent(page, 'bash-static.wasm'); + + await typeCommand(page, 'echo -e "line1\\nline2\\nline3" > /tmp/hf && head -n 2 /tmp/hf && echo HEADDONE'); + const out = await waitForTerminalContent(page, 'HEADDONE'); + const text = await out.jsonValue(); + expect(text).toContain('line1'); + }); + + test('mkdir + rmdir', async ({ page }) => { + await page.goto('/'); + await waitForTerminalContent(page, 'bash-static.wasm'); + + await typeCommand(page, 'mkdir /tmp/testd && rmdir /tmp/testd && echo RMDIR_OK'); + const out = await waitForTerminalContent(page, 'RMDIR_OK'); + expect(await out.jsonValue()).toContain('RMDIR_OK'); + }); + + test('basename + dirname', async ({ page }) => { + await page.goto('/'); + await waitForTerminalContent(page, 'bash-static.wasm'); + + await typeCommand(page, 'basename /usr/bin/bash'); + const out = await waitForTerminalContent(page, 'bash'); + expect(await out.jsonValue()).toContain('bash'); + }); + + test('expr', async ({ page }) => { + await page.goto('/'); + await waitForTerminalContent(page, 'bash-static.wasm'); + + await typeCommand(page, 'expr 2 + 3'); + const out = await waitForTerminalContent(page, '5'); + expect(await out.jsonValue()).toContain('5'); + }); +});