From ed02fe3b975b2276ea1909a72cabd6689ddc8567 Mon Sep 17 00:00:00 2001 From: Ryan Breen Date: Tue, 13 Jan 2026 10:58:00 -0500 Subject: [PATCH] feat(shell): implement getcwd/chdir syscalls for shell navigation Add per-process current working directory (cwd) tracking with full POSIX-compliant getcwd and chdir syscalls: Kernel changes: - Add cwd field to Process struct (initialized to "/") - Implement sys_getcwd (syscall 79) with EINVAL/ERANGE error handling - Implement sys_chdir (syscall 80) with ENOENT/ENOTDIR validation - Add relative path resolution to sys_open using cwd - Fork inherits cwd from parent process - Add get_current_cwd() and normalize_path() helpers Shell enhancements: - Add cd and pwd built-in commands to init_shell - Update ls_cmd to default to "." (cwd) instead of "/" (root) Test coverage: - New cwd_test.rs with 7 tests covering all requirements - 8 boot stages for CWD validation - Tests verify: initial cwd, chdir, ENOENT, ENOTDIR, relative paths, fork inheritance, getcwd error handling, return value pointer All 210 boot stages pass. Co-Authored-By: Claude Opus 4.5 --- docs/planning/SHELL_AND_APPS_ROADMAP.md | 508 ++++++++++++++++++++++++ kernel/build.rs | 1 + kernel/src/main.rs | 4 + kernel/src/process/fork.rs | 7 +- kernel/src/process/manager.rs | 18 +- kernel/src/process/process.rs | 5 + kernel/src/syscall/dispatcher.rs | 2 + kernel/src/syscall/errno.rs | 3 + kernel/src/syscall/fs.rs | 310 ++++++++++++++- kernel/src/syscall/handler.rs | 2 + kernel/src/syscall/mod.rs | 4 + kernel/src/test_exec.rs | 27 ++ libs/libbreenix/src/process.rs | 44 ++ libs/libbreenix/src/syscall.rs | 2 + userspace/tests/Cargo.toml | 4 + userspace/tests/build.sh | 1 + userspace/tests/cwd_test.rs | 306 ++++++++++++++ userspace/tests/init_shell.rs | 67 +++- userspace/tests/ls_cmd.rs | 6 +- xtask/src/main.rs | 49 +++ 20 files changed, 1353 insertions(+), 17 deletions(-) create mode 100644 docs/planning/SHELL_AND_APPS_ROADMAP.md create mode 100644 userspace/tests/cwd_test.rs diff --git a/docs/planning/SHELL_AND_APPS_ROADMAP.md b/docs/planning/SHELL_AND_APPS_ROADMAP.md new file mode 100644 index 00000000..22875fe2 --- /dev/null +++ b/docs/planning/SHELL_AND_APPS_ROADMAP.md @@ -0,0 +1,508 @@ +# Shell Enhancement & Third-Party Application Roadmap + +## Vision + +Transform Breenix from a kernel with embedded test programs into a proper operating system where users can: +- Log into an interactive shell +- Navigate the filesystem (`cd /bin`, `cd /home`) +- Run programs installed on disk (not hardcoded) +- Eventually: build and run third-party applications (GNU coreutils, etc.) + +--- + +## Current State + +### What We Have (init_shell.rs) + +| Feature | Status | Notes | +|---------|--------|-------| +| Command parsing | ✅ | Pipelines, background jobs, redirection | +| Job control | ✅ | fg, bg, jobs, Ctrl+C handling | +| Signal handling | ✅ | SIGINT, SIGCHLD | +| Built-in commands | ✅ | help, exit, clear, echo, jobs, fg, bg | +| External programs | ⚠️ | Hardcoded PROGRAM_REGISTRY (17 programs) | +| Working directory | ❌ | No `cd`, no `pwd`, no cwd tracking | +| PATH resolution | ❌ | No PATH, no filesystem search | +| Paging/scrolling | ❌ | Output overflows QEMU window | + +### Current Program Execution Model + +``` +User types: "hello" + ↓ +Shell looks up in PROGRAM_REGISTRY (static array) + ↓ +Finds binary_name = "hello_world\0" + ↓ +Calls exec_program() with hardcoded binary + ↓ +Kernel loads EMBEDDED program (not from disk) +``` + +**Problem**: Programs are compiled into the kernel, not loaded from filesystem. + +--- + +## Phase 1: Basic Navigation (cd, pwd, cwd) + +**Goal**: User can navigate the filesystem + +### 1.1 Working Directory Tracking + +Add per-process current working directory: + +```rust +// In kernel process structure +pub struct Process { + // ... existing fields + pub cwd: PathBuf, // Current working directory +} +``` + +Syscalls needed: +- `sys_getcwd(buf, size)` - Get current working directory +- `sys_chdir(path)` - Change current working directory + +### 1.2 Shell Built-ins + +Add to init_shell.rs: + +```rust +fn builtin_cd(args: &[&str]) -> i32 { + let path = args.get(0).unwrap_or(&"/"); + libbreenix::process::chdir(path) +} + +fn builtin_pwd() -> i32 { + let mut buf = [0u8; 256]; + let len = libbreenix::process::getcwd(&mut buf); + io::print(core::str::from_utf8(&buf[..len]).unwrap()); + 0 +} +``` + +### 1.3 Relative Path Resolution + +Update file syscalls to respect cwd: +- `sys_open("foo.txt")` → opens `{cwd}/foo.txt` +- `sys_stat("../bar")` → resolves relative to cwd + +### Deliverables +- [ ] `sys_getcwd` and `sys_chdir` syscalls +- [ ] Per-process cwd in kernel +- [ ] `cd` and `pwd` shell built-ins +- [ ] Relative path resolution in VFS + +--- + +## Phase 2: Filesystem-Based Program Loading + +**Goal**: Load and execute ELF binaries from disk + +### 2.1 Current exec() Flow + +``` +exec("hello_world") → Looks up in EMBEDDED_BINARIES → Loads from memory +``` + +### 2.2 Target exec() Flow + +``` +exec("/bin/hello") → Opens file from Ext2 → Reads ELF → Maps into memory → Jumps to entry +``` + +### 2.3 Implementation Steps + +1. **Modify ELF loader** to accept file descriptor instead of memory slice: + ```rust + pub fn load_elf_from_file(fd: i32) -> Result + ``` + +2. **Update sys_execve** to: + - Open file at path + - Verify it's an ELF executable + - Load into new address space + - Close file, transfer control + +3. **Add execute permission check** (if we have permissions): + ```rust + if !file.mode.contains(S_IXUSR) { + return Err(EACCES); + } + ``` + +### Deliverables +- [ ] File-based ELF loader +- [ ] Updated sys_execve to load from path +- [ ] Execute permission checking +- [ ] Error handling for missing/invalid executables + +--- + +## Phase 3: PATH Resolution + +**Goal**: `ls` finds and runs `/bin/ls` + +### 3.1 Environment Variables + +Add environment variable support: + +```rust +// Per-process environment +pub struct Process { + pub env: BTreeMap, // PATH=/bin:/usr/bin +} +``` + +Syscalls: +- `sys_getenv(name, buf, size)` - Get environment variable +- `sys_setenv(name, value)` - Set environment variable + +### 3.2 PATH Search Algorithm + +In shell (not kernel): + +```rust +fn find_executable(name: &str) -> Option { + // If absolute path, use directly + if name.starts_with('/') { + return Some(PathBuf::from(name)); + } + + // Search PATH directories + let path = getenv("PATH").unwrap_or("/bin:/usr/bin"); + for dir in path.split(':') { + let candidate = format!("{}/{}", dir, name); + if file_exists(&candidate) && is_executable(&candidate) { + return Some(PathBuf::from(candidate)); + } + } + None +} +``` + +### 3.3 Remove PROGRAM_REGISTRY + +Replace hardcoded registry with PATH-based lookup: + +```rust +// Before +fn execute_command(cmd: &str) { + if let Some(entry) = PROGRAM_REGISTRY.iter().find(|e| e.name == cmd) { + exec_program(entry.binary_name); + } +} + +// After +fn execute_command(cmd: &str) { + if let Some(path) = find_executable(cmd) { + exec(&path); + } else { + println!("{}: command not found", cmd); + } +} +``` + +### Deliverables +- [ ] Environment variable syscalls +- [ ] PATH search in shell +- [ ] Remove PROGRAM_REGISTRY +- [ ] Default PATH set at login + +--- + +## Phase 4: Filesystem Layout + +**Goal**: Standard Unix directory structure + +### 4.1 Directory Structure + +``` +/ +├── bin/ # Essential user binaries +│ ├── ls +│ ├── cat +│ ├── echo +│ └── sh +├── sbin/ # System binaries +├── usr/ +│ ├── bin/ # Non-essential user binaries +│ └── lib/ # Libraries +├── home/ +│ └── user/ # User home directory +├── tmp/ # Temporary files +├── var/ # Variable data +│ └── log/ +└── etc/ # Configuration + └── passwd +``` + +### 4.2 Build System Integration + +Modify xtask to: +1. Create filesystem image with proper layout +2. Install compiled programs to /bin +3. Copy test programs to /home/user + +```rust +// In xtask build +fn create_filesystem_image() { + create_dir("/bin"); + create_dir("/home/user"); + + // Install coreutils + copy("target/x86_64-breenix/release/ls", "/bin/ls"); + copy("target/x86_64-breenix/release/cat", "/bin/cat"); + // ... +} +``` + +### 4.3 Initial ramdisk or Ext2 Image + +Options: +1. **Initramfs** - Embedded in kernel, unpacked to tmpfs at boot +2. **Ext2 image** - Separate disk image, already mostly working + +Current: We have Ext2 driver and disk image support. Need to populate the image. + +### Deliverables +- [ ] Standard directory layout in disk image +- [ ] xtask creates populated filesystem +- [ ] Programs installed to /bin at build time +- [ ] /home/user for user files + +--- + +## Phase 5: Output Paging + +**Goal**: Long output doesn't overflow screen + +### 5.1 Simple Pager (less/more) + +Implement a basic pager: + +```rust +// /bin/more +fn main() { + let lines_per_page = 24; + let mut line_count = 0; + + for line in stdin.lines() { + print!("{}", line); + line_count += 1; + + if line_count >= lines_per_page { + print!("--More--"); + wait_for_key(); + line_count = 0; + } + } +} +``` + +### 5.2 Pipe Integration + +User can pipe long output: +``` +help | more +ls -la /bin | more +``` + +### Deliverables +- [ ] `more` command implementation +- [ ] Pipe to pager works +- [ ] Optional: `less` with scrollback + +--- + +## Phase 6: Coreutils + +**Goal**: Basic Unix utilities + +### Essential Commands + +| Command | Complexity | Notes | +|---------|------------|-------| +| ls | Medium | Directory listing, requires stat | +| cat | Simple | Already have basic version | +| echo | Simple | Already implemented | +| mkdir | Simple | ✅ Already implemented | +| rmdir | Simple | ✅ Already implemented | +| rm | Simple | Unlink files | +| cp | Medium | Copy files | +| mv | Medium | Rename/move | +| head/tail | Simple | First/last N lines | +| wc | Simple | Word/line/char count | +| grep | Medium | Pattern matching | +| chmod | Simple | Change permissions | + +### Implementation Order + +1. **Already done**: mkdir, rmdir, echo +2. **Next**: ls (with -l option), cat, rm, cp +3. **Then**: head, tail, wc +4. **Later**: grep, chmod, more advanced tools + +--- + +## Phase 7: Libc Foundation + +**Goal**: C runtime for third-party applications + +### 7.1 Options + +| Option | Effort | Compatibility | +|--------|--------|---------------| +| Custom libc | High | Limited, but tailored | +| musl port | Medium | Good POSIX compliance | +| newlib port | Medium | Designed for embedded/OS | + +**Recommendation**: Start with musl-libc port + +### 7.2 Musl Port Steps + +1. **Syscall layer**: Map musl syscalls to Breenix syscalls +2. **Thread support**: Requires pthread syscalls (clone, futex) +3. **Signal handling**: Full POSIX signals +4. **Stdio**: Already have basic file ops +5. **Memory**: malloc via brk/sbrk (already have) + +### 7.3 Required Kernel Features + +| Feature | Status | Priority | +|---------|--------|----------| +| fork/exec | ✅ | - | +| wait/waitpid | ✅ | - | +| brk/sbrk | ✅ | - | +| open/read/write/close | ✅ | - | +| stat/fstat | ✅ | - | +| mmap | ⚠️ Partial | High | +| signals | ⚠️ Basic | High | +| clone (threads) | ❌ | Medium | +| futex | ❌ | Medium | +| pipe | ✅ | - | +| dup/dup2 | ✅ | - | +| socket | ✅ | - | + +### Deliverables +- [ ] Complete mmap implementation +- [ ] Full signal support (sigaction, sigprocmask) +- [ ] clone() for threading +- [ ] futex for synchronization +- [ ] musl syscall wrapper layer + +--- + +## Phase 8: Cross-Compiler Toolchain + +**Goal**: Build programs for Breenix from host + +### 8.1 GCC/Clang Cross-Compiler + +Create x86_64-breenix target: + +```bash +# Configure GCC for Breenix +./configure --target=x86_64-breenix --with-sysroot=/path/to/breenix-sysroot +``` + +### 8.2 Sysroot Structure + +``` +breenix-sysroot/ +├── usr/ +│ ├── include/ # C headers (from musl) +│ └── lib/ # libc.a, crt0.o +└── lib/ + └── libc.so # If dynamic linking +``` + +### 8.3 Build System + +```bash +# Cross-compile hello.c for Breenix +x86_64-breenix-gcc -o hello hello.c + +# Install to Breenix filesystem +cp hello /path/to/breenix-disk/bin/ +``` + +--- + +## Phase 9: Third-Party Applications + +**Goal**: Run GNU software + +### 9.1 Porting Workflow + +1. Download source (e.g., GNU coreutils) +2. Configure with cross-compiler +3. Fix any Breenix-specific issues +4. Build and install to sysroot + +### 9.2 Initial Targets + +| Application | Complexity | Notes | +|-------------|------------|-------| +| busybox | Medium | Many utils in one binary | +| GNU coreutils | Medium | Standard Unix tools | +| vim/vi | High | Requires termcap/ncurses | +| bash | High | Complex, but worth it | +| gcc | Very High | Self-hosting goal | + +### 9.3 Self-Hosting Goal + +Ultimate goal: Build Breenix on Breenix +- Requires: GCC, binutils, make, shell, coreutils +- This is a major milestone for any OS project + +--- + +## Timeline & Dependencies + +``` +Phase 1: Navigation ──────┐ + ↓ +Phase 2: Disk Loading ────┼───→ Phase 3: PATH + ↓ +Phase 4: FS Layout ───────┘ + ↓ +Phase 5: Paging ──────────→ Phase 6: Coreutils + ↓ + Phase 7: Libc ───→ Phase 8: Toolchain + ↓ + Phase 9: Third-Party Apps +``` + +**Critical Path**: Phases 1-4 are foundational. Phase 7 (libc) is the gate to third-party apps. + +--- + +## Immediate Next Steps + +1. **Implement `sys_getcwd` and `sys_chdir`** - Kernel syscalls for cwd +2. **Add `cd` and `pwd` to shell** - User-visible navigation +3. **Modify ELF loader** - Accept file path instead of embedded binary +4. **Create filesystem layout** - /bin, /home in disk image +5. **Install programs to /bin** - xtask builds and installs + +--- + +## Success Metrics + +### Milestone 1: Navigation +- [ ] User can `cd /bin` and `pwd` shows `/bin` +- [ ] `ls` shows directory contents + +### Milestone 2: Disk Programs +- [ ] Programs loaded from /bin, not embedded +- [ ] PROGRAM_REGISTRY eliminated + +### Milestone 3: Self-Sufficient Shell +- [ ] Boot → login → navigate → run programs from disk +- [ ] No hardcoded program list + +### Milestone 4: Third-Party Ready +- [ ] C program compiles with cross-compiler +- [ ] C program runs on Breenix +- [ ] printf("Hello World") works diff --git a/kernel/build.rs b/kernel/build.rs index 7e88bf2e..b0690556 100644 --- a/kernel/build.rs +++ b/kernel/build.rs @@ -110,6 +110,7 @@ fn main() { println!("cargo:rerun-if-changed={}/http_test.rs", userspace_tests); println!("cargo:rerun-if-changed={}/pipeline_test.rs", userspace_tests); println!("cargo:rerun-if-changed={}/sigchld_job_test.rs", userspace_tests); + println!("cargo:rerun-if-changed={}/cwd_test.rs", userspace_tests); println!("cargo:rerun-if-changed={}/lib.rs", libbreenix_dir.to_str().unwrap()); } } else { diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 4e345bef..b0220033 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -834,6 +834,10 @@ fn kernel_main_continue() -> ! { log::info!("=== FS TEST: devfs device files ==="); test_exec::test_devfs(); + // Test current working directory syscalls (getcwd, chdir) + log::info!("=== FS TEST: cwd syscalls (getcwd, chdir) ==="); + test_exec::test_cwd(); + // Test Rust std library support log::info!("=== STD TEST: Rust std library support ==="); test_exec::test_hello_std_real(); diff --git a/kernel/src/process/fork.rs b/kernel/src/process/fork.rs index 4150f672..517ec47a 100644 --- a/kernel/src/process/fork.rs +++ b/kernel/src/process/fork.rs @@ -437,8 +437,7 @@ pub fn copy_stack_contents( /// /// - **umask**: Not yet tracked per-process (uses global default). TODO when implemented. /// -/// - **Current working directory**: Not yet tracked per-process (uses global cwd). -/// TODO when per-process cwd is implemented. +/// - **Current working directory**: Inherited from parent in fork_internal(). /// /// Note: Memory (pages, heap bounds) and stack are copied separately by copy_user_pages() /// and copy_stack_contents() before this function is called. @@ -501,8 +500,8 @@ pub fn copy_process_state( // 5. Copy umask (when per-process umask is implemented) // TODO: child_process.umask = parent_process.umask; - // 6. Copy current working directory (when per-process cwd is implemented) - // TODO: child_process.cwd = parent_process.cwd.clone(); + // 6. Current working directory: inherited from parent in fork_internal() + // (before copy_process_state is called) log::debug!( "copy_process_state: completed state copy for child {}", diff --git a/kernel/src/process/manager.rs b/kernel/src/process/manager.rs index 10624d53..6d4d4b7f 100644 --- a/kernel/src/process/manager.rs +++ b/kernel/src/process/manager.rs @@ -562,7 +562,7 @@ impl ProcessManager { ) -> Result { // Get the parent process info we need (including page table for memory copying) #[cfg_attr(not(feature = "testing"), allow(unused_variables))] - let (parent_name, parent_entry_point, parent_pgid, parent_sid, parent_thread_info, parent_heap_start, parent_heap_end) = { + let (parent_name, parent_entry_point, parent_pgid, parent_sid, parent_cwd, parent_thread_info, parent_heap_start, parent_heap_end) = { let parent = self .processes .get(&parent_pid) @@ -579,6 +579,7 @@ impl ProcessManager { parent.entry_point, parent.pgid, parent.sid, + parent.cwd.clone(), _parent_thread.clone(), parent.heap_start, parent.heap_end, @@ -601,9 +602,10 @@ impl ProcessManager { // Create the child process with the same entry point let mut child_process = Process::new(child_pid, child_name.clone(), parent_entry_point); child_process.parent = Some(parent_pid); - // POSIX: Child inherits parent's process group and session + // POSIX: Child inherits parent's process group, session, and working directory child_process.pgid = parent_pgid; child_process.sid = parent_sid; + child_process.cwd = parent_cwd.clone(); // COPY-ON-WRITE FORK: Share pages between parent and child #[cfg(feature = "testing")] @@ -672,7 +674,7 @@ impl ProcessManager { ) -> Result { // Get the parent process info we need (including page table for memory copying) #[cfg_attr(not(feature = "testing"), allow(unused_variables))] - let (parent_name, parent_entry_point, parent_pgid, parent_sid, parent_thread_info, parent_heap_start, parent_heap_end) = { + let (parent_name, parent_entry_point, parent_pgid, parent_sid, parent_cwd, parent_thread_info, parent_heap_start, parent_heap_end) = { let parent = self .processes .get(&parent_pid) @@ -689,6 +691,7 @@ impl ProcessManager { parent.entry_point, parent.pgid, parent.sid, + parent.cwd.clone(), _parent_thread.clone(), parent.heap_start, parent.heap_end, @@ -711,9 +714,10 @@ impl ProcessManager { // Create the child process with the same entry point let mut child_process = Process::new(child_pid, child_name.clone(), parent_entry_point); child_process.parent = Some(parent_pid); - // POSIX: Child inherits parent's process group and session + // POSIX: Child inherits parent's process group, session, and working directory child_process.pgid = parent_pgid; child_process.sid = parent_sid; + child_process.cwd = parent_cwd.clone(); // COPY-ON-WRITE FORK: Share pages between parent and child // Pages are marked read-only and only copied when written to @@ -1096,16 +1100,18 @@ impl ProcessManager { // Create child process name let child_name = format!("{}_child_{}", parent.name, child_pid.as_u64()); - // Capture parent's pgid and sid before borrowing page_table + // Capture parent's pgid, sid, and cwd before borrowing page_table let parent_pgid = parent.pgid; let parent_sid = parent.sid; + let parent_cwd = parent.cwd.clone(); // Create the child process with the same entry point let mut child_process = Process::new(child_pid, child_name.clone(), parent.entry_point); child_process.parent = Some(parent_pid); - // POSIX: Child inherits parent's process group and session + // POSIX: Child inherits parent's process group, session, and working directory child_process.pgid = parent_pgid; child_process.sid = parent_sid; + child_process.cwd = parent_cwd; // Extract parent heap bounds before we drop the parent borrow let parent_heap_start = parent.heap_start; diff --git a/kernel/src/process/process.rs b/kernel/src/process/process.rs index 748d3899..59469f6a 100644 --- a/kernel/src/process/process.rs +++ b/kernel/src/process/process.rs @@ -54,6 +54,9 @@ pub struct Process { /// a controlling terminal. Initially set to pid on process creation. pub sid: ProcessId, + /// Current working directory (absolute path) + pub cwd: String, + /// Process name (for debugging) pub name: String, @@ -130,6 +133,8 @@ impl Process { pgid: id, // By default, a process's sid equals its pid (process is its own session leader) sid: id, + // Default working directory is root + cwd: String::from("/"), name, state: ProcessState::Creating, entry_point, diff --git a/kernel/src/syscall/dispatcher.rs b/kernel/src/syscall/dispatcher.rs index 461e78b7..4d64ad85 100644 --- a/kernel/src/syscall/dispatcher.rs +++ b/kernel/src/syscall/dispatcher.rs @@ -74,6 +74,8 @@ pub fn dispatch_syscall( SyscallNumber::GetSid => super::session::sys_getsid(arg1 as i32), // Filesystem syscalls SyscallNumber::Access => super::fs::sys_access(arg1, arg2 as u32), + SyscallNumber::Getcwd => super::fs::sys_getcwd(arg1, arg2), + SyscallNumber::Chdir => super::fs::sys_chdir(arg1), SyscallNumber::Open => super::fs::sys_open(arg1, arg2 as u32, arg3 as u32), SyscallNumber::Lseek => super::fs::sys_lseek(arg1 as i32, arg2 as i64, arg3 as i32), SyscallNumber::Fstat => super::fs::sys_fstat(arg1 as i32, arg2), diff --git a/kernel/src/syscall/errno.rs b/kernel/src/syscall/errno.rs index b71406bd..1c7e84c6 100644 --- a/kernel/src/syscall/errno.rs +++ b/kernel/src/syscall/errno.rs @@ -58,6 +58,9 @@ pub const ENOSPC: i32 = 28; /// Broken pipe pub const EPIPE: i32 = 32; +/// Result too large / buffer too small +pub const ERANGE: i32 = 34; + /// Function not implemented (used by syscall dispatcher) #[allow(dead_code)] pub const ENOSYS: i32 = 38; diff --git a/kernel/src/syscall/fs.rs b/kernel/src/syscall/fs.rs index e0277556..fc6dc78e 100644 --- a/kernel/src/syscall/fs.rs +++ b/kernel/src/syscall/fs.rs @@ -153,12 +153,28 @@ pub fn sys_open(pathname: u64, flags: u32, mode: u32) -> SyscallResult { use spin::Mutex; // Copy path from userspace - let path = match copy_cstr_from_user(pathname) { + let raw_path = match copy_cstr_from_user(pathname) { Ok(p) => p, Err(errno) => return SyscallResult::Err(errno), }; - log::debug!("sys_open: path={:?}, flags={:#x}, mode={:#o}", path, flags, mode); + log::debug!("sys_open: raw_path={:?}, flags={:#x}, mode={:#o}", raw_path, flags, mode); + + // Resolve relative paths using current working directory + let path = if raw_path.starts_with('/') { + raw_path + } else { + // Get current process's cwd + let cwd = get_current_cwd().unwrap_or_else(|| alloc::string::String::from("/")); + let absolute = if cwd.ends_with('/') { + alloc::format!("{}{}", cwd, raw_path) + } else { + alloc::format!("{}/{}", cwd, raw_path) + }; + normalize_path(&absolute) + }; + + log::debug!("sys_open: resolved path={:?}", path); // Check for /dev directory itself if path == "/dev" || path == "/dev/" { @@ -1814,3 +1830,293 @@ fn handle_devfs_getdents64( log::debug!("handle_devfs_getdents64: wrote {} bytes, new_position={}", bytes_written, new_position); SyscallResult::Ok(bytes_written as u64) } + +/// sys_getcwd - Get current working directory +/// +/// Returns the absolute pathname of the current working directory. +/// +/// # Arguments +/// * `buf` - Buffer to store the path (userspace pointer) +/// * `size` - Size of the buffer +/// +/// # Returns +/// Pointer to buf on success (as u64), negative errno on failure +/// +/// # Errors +/// * EFAULT - Invalid buffer pointer +/// * ERANGE - Buffer too small +/// * ENOENT - cwd has been unlinked (not implemented yet) +pub fn sys_getcwd(buf: u64, size: u64) -> SyscallResult { + use super::errno::{EFAULT, EINVAL, ERANGE}; + + log::debug!("sys_getcwd: buf={:#x}, size={}", buf, size); + + // Validate buffer pointer + if buf == 0 { + return SyscallResult::Err(EFAULT as u64); + } + + // Size must be at least 1 for the null terminator + if size == 0 { + return SyscallResult::Err(EINVAL as u64); + } + + // Get current process + let thread_id = match crate::task::scheduler::current_thread_id() { + Some(id) => id, + None => { + log::error!("sys_getcwd: No current thread"); + return SyscallResult::Err(3); // ESRCH + } + }; + + let manager_guard = crate::process::manager(); + let process = match &*manager_guard { + Some(manager) => match manager.find_process_by_thread(thread_id) { + Some((_, p)) => p, + None => { + log::error!("sys_getcwd: Process not found for thread {}", thread_id); + return SyscallResult::Err(3); // ESRCH + } + }, + None => { + log::error!("sys_getcwd: Process manager not initialized"); + return SyscallResult::Err(3); // ESRCH + } + }; + + // Get the cwd from the process + let cwd = &process.cwd; + let cwd_bytes = cwd.as_bytes(); + let required_size = cwd_bytes.len() + 1; // +1 for null terminator + + // Check if buffer is large enough + if required_size > size as usize { + log::debug!("sys_getcwd: buffer too small ({} < {})", size, required_size); + return SyscallResult::Err(ERANGE as u64); + } + + // Copy to user buffer with null terminator + let user_buf = buf as *mut u8; + unsafe { + core::ptr::copy_nonoverlapping(cwd_bytes.as_ptr(), user_buf, cwd_bytes.len()); + *user_buf.add(cwd_bytes.len()) = 0; // Null terminator + } + + log::debug!("sys_getcwd: returning {:?}", cwd); + SyscallResult::Ok(buf) +} + +/// sys_chdir - Change current working directory +/// +/// Changes the current working directory to the specified path. +/// +/// # Arguments +/// * `pathname` - Path to the new working directory (userspace pointer) +/// +/// # Returns +/// 0 on success, negative errno on failure +/// +/// # Errors +/// * ENOENT - Directory does not exist +/// * ENOTDIR - Path is not a directory +/// * EACCES - Permission denied +/// * EIO - I/O error +pub fn sys_chdir(pathname: u64) -> SyscallResult { + use super::errno::{EACCES, EIO, ENOENT, ENOTDIR}; + use super::userptr::copy_cstr_from_user; + use crate::fs::ext2::{self, FileType as Ext2FileType}; + use alloc::string::String; + + // Copy path from userspace + let path = match copy_cstr_from_user(pathname) { + Ok(p) => p, + Err(errno) => return SyscallResult::Err(errno), + }; + + log::debug!("sys_chdir: path={:?}", path); + + // Handle empty path + if path.is_empty() { + return SyscallResult::Err(ENOENT as u64); + } + + // Get current process cwd for resolving relative paths + let thread_id = match crate::task::scheduler::current_thread_id() { + Some(id) => id, + None => { + log::error!("sys_chdir: No current thread"); + return SyscallResult::Err(3); // ESRCH + } + }; + + // First, get the current cwd for relative path resolution + let current_cwd = { + let manager_guard = crate::process::manager(); + match &*manager_guard { + Some(manager) => match manager.find_process_by_thread(thread_id) { + Some((_, p)) => p.cwd.clone(), + None => return SyscallResult::Err(3), // ESRCH + }, + None => return SyscallResult::Err(3), // ESRCH + } + }; + + // Normalize the path (handle relative paths) + let absolute_path = if path.starts_with('/') { + path.clone() + } else { + // Combine current cwd with relative path + if current_cwd.ends_with('/') { + alloc::format!("{}{}", current_cwd, path) + } else { + alloc::format!("{}/{}", current_cwd, path) + } + }; + + // Normalize the path (resolve . and ..) + let normalized = normalize_path(&absolute_path); + + // Handle /dev directory and its contents specially + if normalized == "/dev" { + // /dev is always accessible as a directory + let mut manager_guard = crate::process::manager(); + if let Some(manager) = &mut *manager_guard { + if let Some((_, process)) = manager.find_process_by_thread_mut(thread_id) { + process.cwd = String::from("/dev"); + log::info!("sys_chdir: changed cwd to /dev"); + return SyscallResult::Ok(0); + } + } + return SyscallResult::Err(3); // ESRCH + } + + // Handle paths under /dev - these are device files, not directories + if normalized.starts_with("/dev/") { + let device_name = &normalized[5..]; // Strip "/dev/" prefix + if crate::fs::devfs::lookup(device_name).is_some() { + // Device exists but is a file, not a directory + log::debug!("sys_chdir: /dev/{} is a device file, not a directory", device_name); + return SyscallResult::Err(ENOTDIR as u64); + } else { + // Device doesn't exist + log::debug!("sys_chdir: /dev/{} not found", device_name); + return SyscallResult::Err(ENOENT as u64); + } + } + + // Get the root filesystem + let fs_guard = ext2::root_fs(); + let fs = match fs_guard.as_ref() { + Some(fs) => fs, + None => { + log::error!("sys_chdir: ext2 root filesystem not mounted"); + return SyscallResult::Err(EIO as u64); + } + }; + + // Resolve the path to an inode number + let inode_num = match fs.resolve_path(&normalized) { + Ok(ino) => ino, + Err(e) => { + log::debug!("sys_chdir: path resolution failed: {}", e); + let errno = if e.contains("not found") { + ENOENT + } else if e.contains("Not a directory") { + ENOTDIR + } else if e.contains("permission") { + EACCES + } else { + EIO + }; + return SyscallResult::Err(errno as u64); + } + }; + + // Read the inode to verify it's a directory + let inode = match fs.read_inode(inode_num) { + Ok(ino) => ino, + Err(_) => { + log::error!("sys_chdir: failed to read inode {}", inode_num); + return SyscallResult::Err(EIO as u64); + } + }; + + // Check if it's a directory + if !matches!(inode.file_type(), Ext2FileType::Directory) { + log::debug!("sys_chdir: {} is not a directory", normalized); + return SyscallResult::Err(ENOTDIR as u64); + } + + // Drop filesystem lock before getting process lock + drop(fs_guard); + + // Update the process's cwd + let mut manager_guard = crate::process::manager(); + let process = match &mut *manager_guard { + Some(manager) => match manager.find_process_by_thread_mut(thread_id) { + Some((_, p)) => p, + None => { + log::error!("sys_chdir: Process not found for thread {}", thread_id); + return SyscallResult::Err(3); // ESRCH + } + }, + None => { + log::error!("sys_chdir: Process manager not initialized"); + return SyscallResult::Err(3); // ESRCH + } + }; + + process.cwd = normalized.clone(); + log::info!("sys_chdir: changed cwd to {}", normalized); + SyscallResult::Ok(0) +} + +/// Normalize a path by resolving . and .. components +/// +/// Examples: +/// - "/foo/bar/../baz" -> "/foo/baz" +/// - "/foo/./bar" -> "/foo/bar" +/// - "/../foo" -> "/foo" (can't go above root) +fn normalize_path(path: &str) -> alloc::string::String { + use alloc::string::String; + use alloc::vec::Vec; + + let mut components: Vec<&str> = Vec::new(); + + for component in path.split('/') { + match component { + "" | "." => continue, // Skip empty and current directory + ".." => { + // Go up one level (but not above root) + components.pop(); + } + _ => components.push(component), + } + } + + if components.is_empty() { + String::from("/") + } else { + let mut result = String::new(); + for component in components { + result.push('/'); + result.push_str(component); + } + result + } +} + +/// Get the current working directory for the current process +/// +/// Returns None if the current thread or process cannot be determined. +fn get_current_cwd() -> Option { + let thread_id = crate::task::scheduler::current_thread_id()?; + let manager_guard = crate::process::manager(); + match &*manager_guard { + Some(manager) => manager + .find_process_by_thread(thread_id) + .map(|(_, p)| p.cwd.clone()), + None => None, + } +} diff --git a/kernel/src/syscall/handler.rs b/kernel/src/syscall/handler.rs index 5c515f19..e9d65049 100644 --- a/kernel/src/syscall/handler.rs +++ b/kernel/src/syscall/handler.rs @@ -276,6 +276,8 @@ pub extern "C" fn rust_syscall_handler(frame: &mut SyscallFrame) { Some(SyscallNumber::GetSid) => super::session::sys_getsid(args.0 as i32), // Filesystem syscalls Some(SyscallNumber::Access) => super::fs::sys_access(args.0, args.1 as u32), + Some(SyscallNumber::Getcwd) => super::fs::sys_getcwd(args.0, args.1), + Some(SyscallNumber::Chdir) => super::fs::sys_chdir(args.0), Some(SyscallNumber::Open) => super::fs::sys_open(args.0, args.1 as u32, args.2 as u32), Some(SyscallNumber::Lseek) => super::fs::sys_lseek(args.0 as i32, args.1 as i64, args.2 as i32), Some(SyscallNumber::Fstat) => super::fs::sys_fstat(args.0 as i32, args.1), diff --git a/kernel/src/syscall/mod.rs b/kernel/src/syscall/mod.rs index 5af0b5ff..cb471bb0 100644 --- a/kernel/src/syscall/mod.rs +++ b/kernel/src/syscall/mod.rs @@ -68,6 +68,8 @@ pub enum SyscallNumber { Pipe2 = 293, // Linux syscall number for pipe2 // Filesystem syscalls Access = 21, // Linux syscall number for access + Getcwd = 79, // Linux syscall number for getcwd + Chdir = 80, // Linux syscall number for chdir Rename = 82, // Linux syscall number for rename Mkdir = 83, // Linux syscall number for mkdir Rmdir = 84, // Linux syscall number for rmdir @@ -131,6 +133,8 @@ impl SyscallNumber { 293 => Some(Self::Pipe2), // Filesystem syscalls 21 => Some(Self::Access), + 79 => Some(Self::Getcwd), + 80 => Some(Self::Chdir), 82 => Some(Self::Rename), 83 => Some(Self::Mkdir), 84 => Some(Self::Rmdir), diff --git a/kernel/src/test_exec.rs b/kernel/src/test_exec.rs index e4d136e4..b811e9b5 100644 --- a/kernel/src/test_exec.rs +++ b/kernel/src/test_exec.rs @@ -2006,6 +2006,33 @@ pub fn test_devfs() { } } +/// Test current working directory syscalls (getcwd, chdir) +pub fn test_cwd() { + log::info!("Testing cwd syscalls (getcwd, chdir)"); + + #[cfg(feature = "testing")] + let cwd_test_elf_buf = crate::userspace_test::get_test_binary("cwd_test"); + #[cfg(feature = "testing")] + let cwd_test_elf: &[u8] = &cwd_test_elf_buf; + #[cfg(not(feature = "testing"))] + let cwd_test_elf = &create_hello_world_elf(); + + match crate::process::creation::create_user_process( + String::from("cwd_test"), + cwd_test_elf, + ) { + Ok(pid) => { + log::info!("Created cwd_test process with PID {:?}", pid); + log::info!("CWD test: process scheduled for execution."); + log::info!(" -> Userspace will emit CWD_TEST_PASSED marker if successful"); + } + Err(e) => { + log::error!("Failed to create cwd_test process: {}", e); + log::error!("CWD test cannot run without valid userspace process"); + } + } +} + /// Test Rust std library support via hello_std_real pub fn test_hello_std_real() { log::info!("Testing Rust std library support (hello_std_real)"); diff --git a/libs/libbreenix/src/process.rs b/libs/libbreenix/src/process.rs index 2d39e74f..08b444a6 100644 --- a/libs/libbreenix/src/process.rs +++ b/libs/libbreenix/src/process.rs @@ -262,3 +262,47 @@ pub fn setsid() -> i32 { pub fn getsid(pid: i32) -> i32 { unsafe { raw::syscall1(nr::GETSID, pid as u64) as i32 } } + +/// Get the current working directory. +/// +/// Writes the absolute pathname of the current working directory +/// to the provided buffer. +/// +/// # Arguments +/// * `buf` - Buffer to store the path +/// * `size` - Size of the buffer +/// +/// # Returns +/// * On success: pointer to buf (as usize) +/// * On error: 0 and sets errno +/// +/// # Errors +/// * EFAULT - Invalid buffer pointer +/// * ERANGE - Buffer too small +#[inline] +pub fn getcwd(buf: &mut [u8]) -> i64 { + unsafe { + raw::syscall2(nr::GETCWD, buf.as_mut_ptr() as u64, buf.len() as u64) as i64 + } +} + +/// Change the current working directory. +/// +/// Changes the current working directory to the specified path. +/// +/// # Arguments +/// * `path` - Path to the new working directory (must be null-terminated) +/// +/// # Returns +/// * On success: 0 +/// * On error: negative errno +/// +/// # Errors +/// * ENOENT - Directory does not exist +/// * ENOTDIR - Path is not a directory +/// * EACCES - Permission denied +#[inline] +pub fn chdir(path: &[u8]) -> i32 { + debug_assert!(path.last() == Some(&0), "chdir path must be null-terminated"); + unsafe { raw::syscall1(nr::CHDIR, path.as_ptr() as u64) as i32 } +} diff --git a/libs/libbreenix/src/syscall.rs b/libs/libbreenix/src/syscall.rs index f80af75b..f64d261b 100644 --- a/libs/libbreenix/src/syscall.rs +++ b/libs/libbreenix/src/syscall.rs @@ -28,6 +28,8 @@ pub mod nr { pub const IOCTL: u64 = 16; // Linux x86_64 ioctl pub const ACCESS: u64 = 21; // Linux x86_64 access pub const PIPE: u64 = 22; // Linux x86_64 pipe + pub const GETCWD: u64 = 79; // Linux x86_64 getcwd + pub const CHDIR: u64 = 80; // Linux x86_64 chdir pub const SELECT: u64 = 23; // Linux x86_64 select pub const PIPE2: u64 = 293; // Linux x86_64 pipe2 pub const DUP: u64 = 32; // Linux x86_64 dup diff --git a/userspace/tests/Cargo.toml b/userspace/tests/Cargo.toml index 2c51d100..4e31832b 100644 --- a/userspace/tests/Cargo.toml +++ b/userspace/tests/Cargo.toml @@ -234,6 +234,10 @@ path = "access_test.rs" name = "devfs_test" path = "devfs_test.rs" +[[bin]] +name = "cwd_test" +path = "cwd_test.rs" + # Coreutils [[bin]] name = "cat" diff --git a/userspace/tests/build.sh b/userspace/tests/build.sh index b747081e..e4dd4c50 100755 --- a/userspace/tests/build.sh +++ b/userspace/tests/build.sh @@ -89,6 +89,7 @@ BINARIES=( "fs_link_test" "access_test" "devfs_test" + "cwd_test" "fork_memory_test" "fork_state_test" "cow_signal_test" diff --git a/userspace/tests/cwd_test.rs b/userspace/tests/cwd_test.rs new file mode 100644 index 00000000..d0f535cd --- /dev/null +++ b/userspace/tests/cwd_test.rs @@ -0,0 +1,306 @@ +//! Current Working Directory Test +//! +//! Tests the getcwd and chdir syscalls to verify: +//! 1. Initial cwd is "/" (root) +//! 2. chdir to existing directory works +//! 3. chdir to non-existent directory returns ENOENT +//! 4. chdir to a file (not directory) returns ENOTDIR +//! 5. Relative path support (cd .. works) +//! 6. Fork inherits cwd from parent +//! 7. getcwd error handling (EINVAL, ERANGE) +//! 8. getcwd returns buffer pointer on success +//! +//! Test markers: +//! - CWD_INITIAL_OK: Initial cwd is "/" +//! - CWD_CHDIR_OK: chdir to valid directory works +//! - CWD_ENOENT_OK: chdir to non-existent path returns ENOENT +//! - CWD_ENOTDIR_OK: chdir to file returns ENOTDIR +//! - CWD_RELATIVE_OK: Relative path navigation works +//! - CWD_FORK_OK: Fork inherits cwd from parent +//! - CWD_ERRORS_OK: getcwd error handling works +//! - CWD_TEST_PASSED: All tests passed + +#![no_std] +#![no_main] + +use core::panic::PanicInfo; +use libbreenix::io::{print, println}; +use libbreenix::process::{chdir, exit, fork, getcwd, waitpid}; + +/// Print a number +fn print_num(n: i32) { + if n < 0 { + print("-"); + print_num(-n); + return; + } + if n >= 10 { + print_num(n / 10); + } + let digit = b'0' + (n % 10) as u8; + let buf = [digit]; + libbreenix::io::write(1, &buf); +} + +/// Print a u64 in hex +fn print_hex(n: u64) { + print("0x"); + for i in (0..16).rev() { + let nibble = ((n >> (i * 4)) & 0xF) as u8; + let c = if nibble < 10 { b'0' + nibble } else { b'a' + nibble - 10 }; + libbreenix::io::write(1, &[c]); + } +} + +/// Get current working directory as a string slice +/// Also verifies that getcwd returns the buffer pointer (POSIX requirement) +fn get_cwd_str_verified(buf: &mut [u8]) -> Option<&str> { + let buf_ptr = buf.as_mut_ptr() as u64; + let result = getcwd(buf); + + // POSIX: getcwd returns pointer to buf on success + if result <= 0 { + return None; + } + + // Verify return value is the buffer pointer + if result as u64 != buf_ptr { + print(" WARNING: getcwd returned "); + print_hex(result as u64); + print(" but buf is at "); + print_hex(buf_ptr); + println(""); + // Still try to extract the string + } + + let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len()); + core::str::from_utf8(&buf[..len]).ok() +} + +#[no_mangle] +pub extern "C" fn _start() -> ! { + println("=== CWD Syscall Test ==="); + println(""); + + let mut buf = [0u8; 256]; + let mut all_passed = true; + + // Test 1: Initial cwd should be "/" + print("Test 1: Initial cwd... "); + match get_cwd_str_verified(&mut buf) { + Some(cwd) if cwd == "/" => { + println("PASS (/)"); + println("CWD_INITIAL_OK"); + } + Some(cwd) => { + print("FAIL (expected /, got "); + print(cwd); + println(")"); + all_passed = false; + } + None => { + println("FAIL (getcwd failed)"); + all_passed = false; + } + } + + // Test 2: chdir to existing directory + print("Test 2: chdir to /dev... "); + let dev_path = b"/dev\0"; + let result = chdir(dev_path); + if result == 0 { + // Verify cwd changed + match get_cwd_str_verified(&mut buf) { + Some(cwd) if cwd == "/dev" => { + println("PASS"); + println("CWD_CHDIR_OK"); + } + Some(cwd) => { + print("FAIL (cwd is "); + print(cwd); + println(", expected /dev)"); + all_passed = false; + } + None => { + println("FAIL (getcwd failed after chdir)"); + all_passed = false; + } + } + } else { + print("FAIL (chdir returned "); + print_num(result); + println(")"); + all_passed = false; + } + + // Test 3: chdir to non-existent directory should fail with ENOENT + print("Test 3: chdir to /nonexistent... "); + let nonexistent_path = b"/nonexistent\0"; + let result = chdir(nonexistent_path); + if result == -2 { + // ENOENT = 2 + println("PASS (ENOENT)"); + println("CWD_ENOENT_OK"); + } else if result == 0 { + println("FAIL (should not succeed)"); + all_passed = false; + } else { + print("FAIL (expected -2/ENOENT, got "); + print_num(result); + println(")"); + all_passed = false; + } + + // Test 4: chdir to a device file (not directory) should fail with ENOTDIR + // Use /dev/null which is guaranteed to exist and is NOT a directory + let _ = chdir(b"/\0"); + print("Test 4: chdir to /dev/null (file)... "); + let file_path = b"/dev/null\0"; + let result = chdir(file_path); + if result == -20 { + // ENOTDIR = 20 + println("PASS (ENOTDIR)"); + println("CWD_ENOTDIR_OK"); + } else if result == 0 { + println("FAIL (chdir to file should not succeed)"); + all_passed = false; + } else { + // Any other error is a FAIL - we must get ENOTDIR specifically + print("FAIL (expected -20/ENOTDIR, got "); + print_num(result); + println(")"); + all_passed = false; + } + + // Test 5: Relative path navigation + // Start from root, cd to /dev, then cd .. should go back to / + print("Test 5: Relative path (cd ..)... "); + let _ = chdir(b"/dev\0"); + let result = chdir(b"..\0"); + if result == 0 { + match get_cwd_str_verified(&mut buf) { + Some(cwd) if cwd == "/" => { + println("PASS"); + println("CWD_RELATIVE_OK"); + } + Some(cwd) => { + print("FAIL (cwd is "); + print(cwd); + println(", expected /)"); + all_passed = false; + } + None => { + println("FAIL (getcwd failed)"); + all_passed = false; + } + } + } else { + print("FAIL (chdir .. returned "); + print_num(result); + println(")"); + all_passed = false; + } + + // Test 6: Fork inherits cwd from parent + print("Test 6: Fork cwd inheritance... "); + // First change to /dev so child can verify it inherited non-root cwd + let _ = chdir(b"/dev\0"); + let pid = fork(); + if pid < 0 { + print("FAIL (fork failed: "); + print_num(pid as i32); + println(")"); + all_passed = false; + } else if pid == 0 { + // Child process - verify cwd is /dev (inherited from parent) + let mut child_buf = [0u8; 256]; + match get_cwd_str_verified(&mut child_buf) { + Some(cwd) if cwd == "/dev" => { + println("child: cwd=/dev (inherited)"); + exit(0); // Success + } + Some(cwd) => { + print("child: FAIL cwd="); + print(cwd); + println(" (expected /dev)"); + exit(1); // Failure + } + None => { + println("child: FAIL getcwd failed"); + exit(1); + } + } + } else { + // Parent - wait for child and check exit status + let mut status: i32 = 0; + let wait_result = waitpid(pid as i32, &mut status as *mut i32, 0); + if wait_result == pid { + // Check if child exited normally with status 0 + let exited = (status & 0x7f) == 0; + let exit_code = (status >> 8) & 0xff; + if exited && exit_code == 0 { + println("PASS"); + println("CWD_FORK_OK"); + } else { + print("FAIL (child exit code "); + print_num(exit_code); + println(")"); + all_passed = false; + } + } else { + println("FAIL (waitpid failed)"); + all_passed = false; + } + } + // Return to root for remaining tests + let _ = chdir(b"/\0"); + + // Test 7: getcwd error handling + print("Test 7: getcwd error handling... "); + let mut test7_passed = true; + + // Test 7a: size=0 should return EINVAL (-22) + let mut tiny_buf = [0u8; 0]; + let result = getcwd(&mut tiny_buf); + if result != -22 { + print("7a: size=0 expected -22/EINVAL, got "); + print_num(result as i32); + print("; "); + test7_passed = false; + } + + // Test 7b: buffer too small should return ERANGE (-34) + // cwd is "/" which needs 2 bytes (/ + null), so 1-byte buffer should fail + let mut small_buf = [0u8; 1]; + let result = getcwd(&mut small_buf); + if result != -34 { + print("7b: small buf expected -34/ERANGE, got "); + print_num(result as i32); + print("; "); + test7_passed = false; + } + + if test7_passed { + println("PASS"); + println("CWD_ERRORS_OK"); + } else { + println("FAIL"); + all_passed = false; + } + + println(""); + if all_passed { + println("=== All CWD tests PASSED ==="); + println("CWD_TEST_PASSED"); + exit(0); + } else { + println("=== Some CWD tests FAILED ==="); + exit(1); + } +} + +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + println("PANIC in cwd_test!"); + exit(255); +} diff --git a/userspace/tests/init_shell.rs b/userspace/tests/init_shell.rs index e03e1731..8cc1bcd1 100644 --- a/userspace/tests/init_shell.rs +++ b/userspace/tests/init_shell.rs @@ -22,8 +22,8 @@ use core::sync::atomic::{AtomicBool, Ordering}; use libbreenix::fs::{open, O_DIRECTORY, O_RDONLY, O_WRONLY}; use libbreenix::io::{close, dup2, pipe, print, println, read, write}; use libbreenix::process::{ - exec, execv, fork, getpgrp, setpgid, waitpid, wexitstatus, wifexited, wifsignaled, wifstopped, - yield_now, WNOHANG, WUNTRACED, + chdir, exec, execv, fork, getcwd, getpgrp, setpgid, waitpid, wexitstatus, wifexited, + wifsignaled, wifstopped, yield_now, WNOHANG, WUNTRACED, }; use libbreenix::signal::{kill, sigaction, Sigaction, SIGCHLD, SIGCONT, SIGINT}; use libbreenix::termios::{ @@ -1204,6 +1204,16 @@ fn print_num(mut n: u64) { } } +/// Print a signed number (i32) +fn print_i32(n: i32) { + if n < 0 { + print("-"); + print_num((-n) as u64); + } else { + print_num(n as u64); + } +} + /// Read a line from stdin, handling backspace and yielding on EAGAIN /// Returns None if interrupted by SIGINT (Ctrl+C) fn read_line() -> Option<&'static str> { @@ -1267,6 +1277,8 @@ fn cmd_help() { println("Built-in commands:"); println(" help - Show this help message"); println(" echo - Echo text back to the terminal"); + println(" cd - Change current directory (cd /path)"); + println(" pwd - Print current working directory"); println(" ps - List processes (placeholder)"); println(" jobs - List background and stopped jobs"); println(" bg - Resume stopped job in background (bg %N)"); @@ -1346,6 +1358,55 @@ fn cmd_uptime() { println(""); } +/// Handle the "cd" command - change current directory +fn cmd_cd(args: &str) { + // If no argument, go to root + let path = if args.is_empty() { "/" } else { args.trim() }; + + // Build null-terminated path + let mut path_buf = [0u8; 256]; + let path_bytes = path.as_bytes(); + if path_bytes.len() >= path_buf.len() { + println("cd: path too long"); + return; + } + path_buf[..path_bytes.len()].copy_from_slice(path_bytes); + path_buf[path_bytes.len()] = 0; // null terminator + + let result = chdir(&path_buf[..=path_bytes.len()]); + if result < 0 { + print("cd: "); + print(path); + match -result { + 2 => println(": No such file or directory"), + 20 => println(": Not a directory"), + 13 => println(": Permission denied"), + _ => { + print(": error "); + print_i32(-result); + println(""); + } + } + } +} + +/// Handle the "pwd" command - print working directory +fn cmd_pwd() { + let mut buf = [0u8; 256]; + let result = getcwd(&mut buf); + if result > 0 { + // Find the null terminator and print the path + let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len()); + if let Ok(path) = core::str::from_utf8(&buf[..len]) { + println(path); + } else { + println("pwd: invalid path encoding"); + } + } else { + println("pwd: cannot get current directory"); + } +} + /// Handle the "clear" command fn cmd_clear() { // Use ANSI escape sequences to clear screen and move cursor to home @@ -1681,6 +1742,8 @@ fn handle_command(line: &str) { match cmd { "help" => cmd_help(), "echo" => cmd_echo(args), + "cd" => cmd_cd(args), + "pwd" => cmd_pwd(), "ps" => cmd_ps(), "jobs" => list_jobs(), "bg" => builtin_bg(args), diff --git a/userspace/tests/ls_cmd.rs b/userspace/tests/ls_cmd.rs index 66013b3e..0aef1ff4 100644 --- a/userspace/tests/ls_cmd.rs +++ b/userspace/tests/ls_cmd.rs @@ -99,11 +99,11 @@ extern "C" fn rust_main(stack_ptr: *const u64) -> ! { // Get command-line arguments from the original stack pointer let args = unsafe { argv::get_args_from_stack(stack_ptr) }; - // Default to root directory if no arguments + // Default to current directory if no arguments let path: &[u8] = if args.argc >= 2 { - args.argv(1).unwrap_or(b"/") + args.argv(1).unwrap_or(b".") } else { - b"/" + b"." }; match ls_directory(path) { diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 1fb867dd..452719bc 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1218,6 +1218,55 @@ fn get_boot_stages() -> Vec { failure_meaning: "devfs test failed - /dev/null, /dev/zero, or /dev/console broken", check_hint: "Check kernel/src/fs/devfs/mod.rs and syscall routing in sys_open for /dev/* paths", }, + // Current working directory syscalls test + BootStage { + name: "CWD initial OK", + marker: "CWD_INITIAL_OK", + failure_meaning: "Initial cwd is not / - process cwd not initialized correctly", + check_hint: "Check kernel/src/process/process.rs cwd initialization and kernel/src/syscall/fs.rs sys_getcwd", + }, + BootStage { + name: "CWD chdir OK", + marker: "CWD_CHDIR_OK", + failure_meaning: "chdir to valid directory failed - cwd update not working", + check_hint: "Check kernel/src/syscall/fs.rs sys_chdir path resolution and process cwd update", + }, + BootStage { + name: "CWD ENOENT OK", + marker: "CWD_ENOENT_OK", + failure_meaning: "chdir to nonexistent path did not return ENOENT", + check_hint: "Check kernel/src/syscall/fs.rs sys_chdir error handling for missing paths", + }, + BootStage { + name: "CWD ENOTDIR OK", + marker: "CWD_ENOTDIR_OK", + failure_meaning: "chdir to file did not return ENOTDIR", + check_hint: "Check kernel/src/syscall/fs.rs sys_chdir directory type validation", + }, + BootStage { + name: "CWD relative OK", + marker: "CWD_RELATIVE_OK", + failure_meaning: "Relative path navigation (cd ..) failed", + check_hint: "Check kernel/src/syscall/fs.rs normalize_path function for .. handling", + }, + BootStage { + name: "CWD fork inheritance OK", + marker: "CWD_FORK_OK", + failure_meaning: "Fork did not inherit cwd from parent", + check_hint: "Check kernel/src/process/manager.rs fork_internal clones parent cwd to child", + }, + BootStage { + name: "CWD getcwd errors OK", + marker: "CWD_ERRORS_OK", + failure_meaning: "getcwd error handling (EINVAL/ERANGE) failed", + check_hint: "Check kernel/src/syscall/fs.rs sys_getcwd validates size and returns correct error codes", + }, + BootStage { + name: "CWD test passed", + marker: "CWD_TEST_PASSED", + failure_meaning: "One or more cwd tests failed", + check_hint: "Check userspace/tests/cwd_test.rs output for specific failure", + }, // Rust std library test - validates real Rust std works in userspace BootStage { name: "Rust std println! works",