From 564d10e1be466b4e794773d0a7637c8494caf8d3 Mon Sep 17 00:00:00 2001
From: Mark Poliakov <mark@alnyan.me>
Date: Tue, 23 Nov 2021 17:55:58 +0200
Subject: [PATCH] feature: simple ls(1p)

---
 Makefile                  |   1 +
 fs/macros/src/lib.rs      |  13 +++++
 fs/memfs/src/dir.rs       |  13 +++--
 fs/memfs/src/file.rs      |   5 +-
 fs/memfs/src/lib.rs       |   4 +-
 fs/memfs/src/tar.rs       |   7 ++-
 fs/vfs/src/file.rs        |  60 ++++++++++++++++++++++-
 fs/vfs/src/node.rs        | 100 ++++++++++++++++++++++++++++++--------
 kernel/src/syscall/arg.rs |  13 +++++
 kernel/src/syscall/mod.rs |  83 ++++++++++++++++---------------
 libsys/src/abi.rs         |   1 +
 libsys/src/calls.rs       |  18 ++++++-
 libsys/src/stat.rs        |  95 +++++++++++++++++++++++++++++++-----
 user/Cargo.toml           |   4 ++
 user/src/ls/main.rs       |  45 +++++++++++++++++
 15 files changed, 380 insertions(+), 82 deletions(-)
 create mode 100644 user/src/ls/main.rs

diff --git a/Makefile b/Makefile
index bbfd889..b73183a 100644
--- a/Makefile
+++ b/Makefile
@@ -96,6 +96,7 @@ initrd:
 	cp target/$(ARCH)-osdev5/$(PROFILE)/init $(O)/rootfs/init
 	cp target/$(ARCH)-osdev5/$(PROFILE)/shell $(O)/rootfs/bin
 	cp target/$(ARCH)-osdev5/$(PROFILE)/fuzzy $(O)/rootfs/bin
+	cp target/$(ARCH)-osdev5/$(PROFILE)/ls $(O)/rootfs/bin
 	cd $(O)/rootfs && tar cf ../initrd.img `find -type f -printf "%P\n"`
 ifeq ($(MACH),orangepi3)
 	$(MKIMAGE) \
diff --git a/fs/macros/src/lib.rs b/fs/macros/src/lib.rs
index dde1281..c3c4c84 100644
--- a/fs/macros/src/lib.rs
+++ b/fs/macros/src/lib.rs
@@ -94,6 +94,18 @@ fn impl_inode_fn<T: ToTokens>(name: &str, behavior: T) -> ImplItem {
                 #behavior
             }
         },
+        "readdir" => quote! {
+            fn readdir(
+                &mut self,
+                _node: VnodeRef,
+                _pos: usize,
+                _entries: &mut [libsys::stat::DirectoryEntry]
+            ) ->
+                Result<usize, libsys::error::Errno>
+            {
+                #behavior
+            }
+        },
         _ => panic!("TODO implement {:?}", name),
     })
 }
@@ -126,6 +138,7 @@ pub fn auto_inode(attr: TokenStream, input: TokenStream) -> TokenStream {
     missing.insert("size".to_string());
     missing.insert("ioctl".to_string());
     missing.insert("is_ready".to_string());
+    missing.insert("readdir".to_string());
 
     for item in &impl_item.items {
         match item {
diff --git a/fs/memfs/src/dir.rs b/fs/memfs/src/dir.rs
index 5766e1a..fc1caea 100644
--- a/fs/memfs/src/dir.rs
+++ b/fs/memfs/src/dir.rs
@@ -1,6 +1,9 @@
 use crate::{BlockAllocator, Bvec, FileInode};
 use alloc::boxed::Box;
-use libsys::{error::Errno, stat::Stat};
+use libsys::{
+    error::Errno,
+    stat::{DirectoryEntry, OpenFlags, Stat},
+};
 use vfs::{Vnode, VnodeImpl, VnodeKind, VnodeRef};
 
 pub struct DirInode<A: BlockAllocator + Copy + 'static> {
@@ -15,7 +18,7 @@ impl<A: BlockAllocator + Copy + 'static> VnodeImpl for DirInode<A> {
         name: &str,
         kind: VnodeKind,
     ) -> Result<VnodeRef, Errno> {
-        let vnode = Vnode::new(name, kind, Vnode::SEEKABLE);
+        let vnode = Vnode::new(name, kind, Vnode::SEEKABLE | Vnode::CACHE_READDIR);
         match kind {
             VnodeKind::Directory => vnode.set_data(Box::new(DirInode { alloc: self.alloc })),
             VnodeKind::Regular => vnode.set_data(Box::new(FileInode::new(Bvec::new(self.alloc)))),
@@ -32,7 +35,11 @@ impl<A: BlockAllocator + Copy + 'static> VnodeImpl for DirInode<A> {
         Ok(())
     }
 
-    fn stat(&mut self, _at: VnodeRef, _stat: &mut Stat) -> Result<(), Errno> {
+    fn stat(&mut self, node: VnodeRef, stat: &mut Stat) -> Result<(), Errno> {
+        let props = node.props();
+        stat.size = 0;
+        stat.blksize = 4096;
+        stat.mode = props.mode;
         Ok(())
     }
 }
diff --git a/fs/memfs/src/file.rs b/fs/memfs/src/file.rs
index 1fe8068..13d31b6 100644
--- a/fs/memfs/src/file.rs
+++ b/fs/memfs/src/file.rs
@@ -35,10 +35,11 @@ impl<'a, A: BlockAllocator + Copy + 'static> VnodeImpl for FileInode<'a, A> {
         Ok(self.data.size())
     }
 
-    fn stat(&mut self, _node: VnodeRef, stat: &mut Stat) -> Result<(), Errno> {
+    fn stat(&mut self, node: VnodeRef, stat: &mut Stat) -> Result<(), Errno> {
+        let props = node.props();
         stat.size = self.data.size() as u64;
         stat.blksize = 4096;
-        stat.mode = 0o755;
+        stat.mode = props.mode;
         Ok(())
     }
 }
diff --git a/fs/memfs/src/lib.rs b/fs/memfs/src/lib.rs
index 04be460..7e9e83c 100644
--- a/fs/memfs/src/lib.rs
+++ b/fs/memfs/src/lib.rs
@@ -69,7 +69,7 @@ impl<A: BlockAllocator + Copy + 'static> Ramfs<A> {
 
     fn create_node_initial(self: Rc<Self>, name: &str, tar: &Tar) -> VnodeRef {
         let kind = tar.node_kind();
-        let node = Vnode::new(name, kind, Vnode::SEEKABLE);
+        let node = Vnode::new(name, kind, Vnode::SEEKABLE | Vnode::CACHE_READDIR);
         node.props_mut().mode = tar.mode();
         node.set_fs(self.clone());
         match kind {
@@ -113,7 +113,7 @@ impl<A: BlockAllocator + Copy + 'static> Ramfs<A> {
     }
 
     unsafe fn load_tar(self: Rc<Self>, base: *const u8, size: usize) -> Result<VnodeRef, Errno> {
-        let root = Vnode::new("", VnodeKind::Directory, Vnode::SEEKABLE);
+        let root = Vnode::new("", VnodeKind::Directory, Vnode::SEEKABLE | Vnode::CACHE_READDIR);
         root.set_fs(self.clone());
         root.set_data(Box::new(DirInode::new(self.alloc)));
         root.props_mut().mode = FileMode::default_dir();
diff --git a/fs/memfs/src/tar.rs b/fs/memfs/src/tar.rs
index 2211f65..5326a2f 100644
--- a/fs/memfs/src/tar.rs
+++ b/fs/memfs/src/tar.rs
@@ -82,7 +82,12 @@ impl Tar {
     }
 
     pub fn mode(&self) -> FileMode {
-        FileMode::from_bits(from_octal(&self.mode) as u32).unwrap()
+        let t = match self.node_kind() {
+            VnodeKind::Regular => FileMode::S_IFREG,
+            VnodeKind::Directory => FileMode::S_IFDIR,
+            _ => todo!()
+        };
+        FileMode::from_bits(from_octal(&self.mode) as u32).unwrap() | t
     }
 
     pub fn data(&self) -> &[u8] {
diff --git a/fs/vfs/src/file.rs b/fs/vfs/src/file.rs
index cd63be4..4c267b6 100644
--- a/fs/vfs/src/file.rs
+++ b/fs/vfs/src/file.rs
@@ -1,9 +1,10 @@
-use crate::{VnodeKind, VnodeRef};
+use crate::{VnodeKind, VnodeRef, Vnode};
 use alloc::rc::Rc;
 use core::cell::RefCell;
 use core::cmp::min;
 use libsys::{
     error::Errno,
+    stat::DirectoryEntry,
     traits::{Read, Seek, SeekDir, Write},
 };
 
@@ -97,6 +98,9 @@ impl File {
     /// File has to be closed on execve() calls
     pub const CLOEXEC: u32 = 1 << 2;
 
+    pub const POS_CACHE_DOT: usize = usize::MAX - 1;
+    pub const POS_CACHE_DOT_DOT: usize = usize::MAX;
+
     /// Constructs a new file handle for a regular file
     pub fn normal(vnode: VnodeRef, pos: usize, flags: u32) -> FileRef {
         Rc::new(RefCell::new(Self {
@@ -125,6 +129,60 @@ impl File {
             _ => todo!(),
         }
     }
+
+    fn cache_readdir(inner: &mut NormalFile, entries: &mut [DirectoryEntry]) -> Result<usize, Errno> {
+        let mut count = entries.len();
+        let mut offset = 0usize;
+
+        if inner.pos == Self::POS_CACHE_DOT {
+            if count == 0 {
+                return Ok(offset);
+            }
+
+            entries[offset] = DirectoryEntry::from_str(".");
+            inner.pos = Self::POS_CACHE_DOT_DOT;
+
+            offset += 1;
+            count -= 1;
+        }
+
+        if inner.pos == Self::POS_CACHE_DOT_DOT {
+            if count == 0 {
+                return Ok(offset);
+            }
+
+            entries[offset] = DirectoryEntry::from_str("..");
+            inner.pos = 0;
+
+            offset += 1;
+            count -= 1;
+        }
+
+        if count == 0 {
+            return Ok(offset);
+        }
+
+        let count = inner.vnode.for_each_entry(inner.pos, count, |i, e| {
+            entries[offset + i] = DirectoryEntry::from_str(e.name());
+        });
+        inner.pos += count;
+        Ok(offset + count)
+    }
+
+    pub fn readdir(&mut self, entries: &mut [DirectoryEntry]) -> Result<usize, Errno> {
+        match &mut self.inner {
+            FileInner::Normal(inner) => {
+                assert_eq!(inner.vnode.kind(), VnodeKind::Directory);
+
+                if inner.vnode.flags() & Vnode::CACHE_READDIR != 0 {
+                    Self::cache_readdir(inner, entries)
+                } else {
+                    todo!();
+                }
+            },
+            _ => todo!(),
+        }
+    }
 }
 
 impl Drop for File {
diff --git a/fs/vfs/src/node.rs b/fs/vfs/src/node.rs
index 1e066b9..d21b058 100644
--- a/fs/vfs/src/node.rs
+++ b/fs/vfs/src/node.rs
@@ -1,11 +1,11 @@
-use crate::{Ioctx, File, FileRef, Filesystem};
+use crate::{File, FileRef, Filesystem, Ioctx};
 use alloc::{borrow::ToOwned, boxed::Box, rc::Rc, string::String, vec::Vec};
-use core::cell::{RefCell, RefMut};
+use core::cell::{RefCell, RefMut, Ref};
 use core::fmt;
 use libsys::{
     error::Errno,
     ioctl::IoctlCmd,
-    stat::{AccessMode, FileMode, OpenFlags, Stat},
+    stat::{AccessMode, DirectoryEntry, FileMode, OpenFlags, Stat},
 };
 
 /// Convenience type alias for [Rc<Vnode>]
@@ -74,6 +74,13 @@ pub trait VnodeImpl {
     /// Resizes the file storage if necessary.
     fn write(&mut self, node: VnodeRef, pos: usize, data: &[u8]) -> Result<usize, Errno>;
 
+    fn readdir(
+        &mut self,
+        node: VnodeRef,
+        pos: usize,
+        data: &mut [DirectoryEntry],
+    ) -> Result<usize, Errno>;
+
     /// Retrieves file status
     fn stat(&mut self, node: VnodeRef, stat: &mut Stat) -> Result<(), Errno>;
 
@@ -97,6 +104,8 @@ impl Vnode {
     /// be seeked to arbitrary offsets
     pub const SEEKABLE: u32 = 1 << 0;
 
+    pub const CACHE_READDIR: u32 = 1 << 1;
+
     /// Constructs a new [Vnode], wrapping it in [Rc]. The resulting node
     /// then needs to have [Vnode::set_data()] called on it to be usable.
     pub fn new(name: &str, kind: VnodeKind, flags: u32) -> VnodeRef {
@@ -127,6 +136,11 @@ impl Vnode {
         self.props.borrow_mut()
     }
 
+    /// Returns a borrowed reference to cached file properties
+    pub fn props(&self) -> Ref<VnodeProps> {
+        self.props.borrow()
+    }
+
     /// Sets an associated [VnodeImpl] for the [Vnode]
     pub fn set_data(&self, data: Box<dyn VnodeImpl>) {
         *self.data.borrow_mut() = Some(data);
@@ -163,6 +177,11 @@ impl Vnode {
         self.kind
     }
 
+    #[inline(always)]
+    pub const fn flags(&self) -> u32 {
+        self.flags
+    }
+
     // Tree operations
 
     /// Attaches `child` vnode to `self` in in-memory tree. NOTE: does not
@@ -235,6 +254,29 @@ impl Vnode {
             .cloned()
     }
 
+    pub(crate) fn for_each_entry<F: FnMut(usize, &VnodeRef)>(
+        &self,
+        offset: usize,
+        limit: usize,
+        mut f: F,
+    ) -> usize {
+        assert!(self.is_directory());
+        let mut count = 0;
+        for (index, item) in self
+            .tree
+            .borrow()
+            .children
+            .iter()
+            .skip(offset)
+            .take(limit)
+            .enumerate()
+        {
+            f(index, item);
+            count += 1;
+        }
+        count
+    }
+
     /// Looks up a child `name` in `self`. Will first try looking up a cached
     /// vnode and will load it from disk if it's missing.
     pub fn lookup_or_load(self: &VnodeRef, name: &str) -> Result<VnodeRef, Errno> {
@@ -308,35 +350,55 @@ impl Vnode {
 
     /// Opens a vnode for access
     pub fn open(self: &VnodeRef, flags: OpenFlags) -> Result<FileRef, Errno> {
-        if self.kind == VnodeKind::Directory {
-            return Err(Errno::IsADirectory);
+        let mut open_flags = 0;
+        if flags.contains(OpenFlags::O_DIRECTORY) {
+            if self.kind != VnodeKind::Directory {
+                return Err(Errno::NotADirectory);
+            }
+            if flags & OpenFlags::O_ACCESS != OpenFlags::O_RDONLY {
+                return Err(Errno::IsADirectory);
+            }
+
+            open_flags = File::READ;
+        } else {
+            if self.kind == VnodeKind::Directory {
+                return Err(Errno::IsADirectory);
+            }
+
+            match flags & OpenFlags::O_ACCESS {
+                OpenFlags::O_RDONLY => open_flags |= File::READ,
+                OpenFlags::O_WRONLY => open_flags |= File::WRITE,
+                OpenFlags::O_RDWR => open_flags |= File::READ | File::WRITE,
+                _ => unimplemented!(),
+            }
         }
 
-        let mut open_flags = 0;
-        match flags & OpenFlags::O_ACCESS {
-            OpenFlags::O_RDONLY => open_flags |= File::READ,
-            OpenFlags::O_WRONLY => open_flags |= File::WRITE,
-            OpenFlags::O_RDWR => open_flags |= File::READ | File::WRITE,
-            _ => unimplemented!(),
-        }
         if flags.contains(OpenFlags::O_CLOEXEC) {
             open_flags |= File::CLOEXEC;
         }
 
-        if let Some(ref mut data) = *self.data() {
-            let pos = data.open(self.clone(), flags)?;
-            Ok(File::normal(self.clone(), pos, open_flags))
+        if self.kind == VnodeKind::Directory && self.flags & Vnode::CACHE_READDIR != 0 {
+            Ok(File::normal(self.clone(), File::POS_CACHE_DOT, open_flags))
         } else {
-            Err(Errno::NotImplemented)
+            if let Some(ref mut data) = *self.data() {
+                let pos = data.open(self.clone(), flags)?;
+                Ok(File::normal(self.clone(), pos, open_flags))
+            } else {
+                Err(Errno::NotImplemented)
+            }
         }
     }
 
     /// Closes a vnode
     pub fn close(self: &VnodeRef) -> Result<(), Errno> {
-        if let Some(ref mut data) = *self.data() {
-            data.close(self.clone())
+        if self.kind == VnodeKind::Directory && self.flags & Vnode::CACHE_READDIR != 0 {
+            Ok(())
         } else {
-            Err(Errno::NotImplemented)
+            if let Some(ref mut data) = *self.data() {
+                data.close(self.clone())
+            } else {
+                Err(Errno::NotImplemented)
+            }
         }
     }
 
diff --git a/kernel/src/syscall/arg.rs b/kernel/src/syscall/arg.rs
index 937ec98..c64075d 100644
--- a/kernel/src/syscall/arg.rs
+++ b/kernel/src/syscall/arg.rs
@@ -62,6 +62,19 @@ pub fn struct_mut<'a, T>(base: usize) -> Result<&'a mut T, Errno> {
     Ok(unsafe { &mut *(bytes.as_mut_ptr() as *mut T) })
 }
 
+pub fn struct_buf_mut<'a, T>(base: usize, count: usize) -> Result<&'a mut [T], Errno> {
+    let layout = Layout::array::<T>(count).unwrap();
+    if base % layout.align() != 0 {
+        invalid_memory!(
+            "Structure pointer is misaligned: base={:#x}, expected {:?}",
+            base,
+            layout
+        );
+    }
+    let bytes = buf_mut(base, layout.size())?;
+    Ok(unsafe { core::slice::from_raw_parts_mut(bytes.as_mut_ptr() as *mut T, count) })
+}
+
 pub fn option_struct_ref<'a, T>(base: usize) -> Result<Option<&'a T>, Errno> {
     if base == 0 {
         Ok(None)
diff --git a/kernel/src/syscall/mod.rs b/kernel/src/syscall/mod.rs
index c3b8f45..1896f48 100644
--- a/kernel/src/syscall/mod.rs
+++ b/kernel/src/syscall/mod.rs
@@ -2,8 +2,8 @@
 
 use crate::arch::{machine, platform::exception::ExceptionFrame};
 use crate::debug::Level;
-use crate::proc::{self, elf, wait, Process, ProcessIo, Thread};
 use crate::dev::timer::TimestampSource;
+use crate::proc::{self, elf, wait, Process, ProcessIo, Thread};
 use core::mem::size_of;
 use core::ops::DerefMut;
 use core::time::Duration;
@@ -13,7 +13,9 @@ use libsys::{
     ioctl::IoctlCmd,
     proc::{ExitCode, Pid},
     signal::{Signal, SignalDestination},
-    stat::{FdSet, AccessMode, FileDescriptor, FileMode, OpenFlags, Stat, AT_EMPTY_PATH},
+    stat::{
+        AccessMode, DirectoryEntry, FdSet, FileDescriptor, FileMode, OpenFlags, Stat, AT_EMPTY_PATH,
+    },
     traits::{Read, Write},
 };
 use vfs::VnodeRef;
@@ -62,7 +64,7 @@ pub fn syscall(num: SystemCall, args: &[usize]) -> Result<usize, Errno> {
             let buf = arg::buf_mut(args[1], args[2])?;
 
             io.file(fd)?.borrow_mut().read(buf)
-        },
+        }
         SystemCall::Write => {
             let proc = Process::current();
             let fd = FileDescriptor::from(args[0] as u32);
@@ -70,7 +72,7 @@ pub fn syscall(num: SystemCall, args: &[usize]) -> Result<usize, Errno> {
             let buf = arg::buf_ref(args[1], args[2])?;
 
             io.file(fd)?.borrow_mut().write(buf)
-        },
+        }
         SystemCall::Open => {
             let at_fd = FileDescriptor::from_i32(args[0] as i32)?;
             let path = arg::str_ref(args[1], args[2])?;
@@ -88,7 +90,7 @@ pub fn syscall(num: SystemCall, args: &[usize]) -> Result<usize, Errno> {
 
             let file = io.ioctx().open(at, path, mode, opts)?;
             Ok(u32::from(io.place_file(file)?) as usize)
-        },
+        }
         SystemCall::Close => {
             let proc = Process::current();
             let mut io = proc.io.lock();
@@ -96,7 +98,7 @@ pub fn syscall(num: SystemCall, args: &[usize]) -> Result<usize, Errno> {
 
             io.close_file(fd)?;
             Ok(0)
-        },
+        }
         SystemCall::FileStatus => {
             let at_fd = FileDescriptor::from_i32(args[0] as i32)?;
             let filename = arg::str_ref(args[1], args[2])?;
@@ -107,7 +109,7 @@ pub fn syscall(num: SystemCall, args: &[usize]) -> Result<usize, Errno> {
             let mut io = proc.io.lock();
             find_at_node(&mut io, at_fd, filename, flags & AT_EMPTY_PATH != 0)?.stat(buf)?;
             Ok(0)
-        },
+        }
         SystemCall::Ioctl => {
             let fd = FileDescriptor::from(args[0] as u32);
             let cmd = IoctlCmd::try_from(args[1] as u32)?;
@@ -117,7 +119,7 @@ pub fn syscall(num: SystemCall, args: &[usize]) -> Result<usize, Errno> {
 
             let node = io.file(fd)?.borrow().node().ok_or(Errno::InvalidFile)?;
             node.ioctl(cmd, args[2], args[3])
-        },
+        }
         SystemCall::Select => {
             let rfds = arg::option_struct_mut::<FdSet>(args[0])?;
             let wfds = arg::option_struct_mut::<FdSet>(args[1])?;
@@ -128,7 +130,7 @@ pub fn syscall(num: SystemCall, args: &[usize]) -> Result<usize, Errno> {
             };
 
             wait::select(Thread::current(), rfds, wfds, timeout)
-        },
+        }
         SystemCall::Access => {
             let at_fd = FileDescriptor::from_i32(args[0] as i32)?;
             let path = arg::str_ref(args[1], args[2])?;
@@ -138,9 +140,18 @@ pub fn syscall(num: SystemCall, args: &[usize]) -> Result<usize, Errno> {
             let proc = Process::current();
             let mut io = proc.io.lock();
 
-            find_at_node(&mut io, at_fd, path, flags & AT_EMPTY_PATH != 0)?.check_access(io.ioctx(), mode)?;
+            find_at_node(&mut io, at_fd, path, flags & AT_EMPTY_PATH != 0)?
+                .check_access(io.ioctx(), mode)?;
             Ok(0)
-        },
+        }
+        SystemCall::ReadDirectory => {
+            let proc = Process::current();
+            let fd = FileDescriptor::from(args[0] as u32);
+            let mut io = proc.io.lock();
+            let buf = arg::struct_buf_mut::<DirectoryEntry>(args[1], args[2])?;
+
+            io.file(fd)?.borrow_mut().readdir(buf)
+        }
 
         // Process
         SystemCall::Clone => {
@@ -151,7 +162,7 @@ pub fn syscall(num: SystemCall, args: &[usize]) -> Result<usize, Errno> {
             Process::current()
                 .new_user_thread(entry, stack, arg)
                 .map(|e| e as usize)
-        },
+        }
         SystemCall::Exec => {
             let node = {
                 let proc = Process::current();
@@ -165,7 +176,7 @@ pub fn syscall(num: SystemCall, args: &[usize]) -> Result<usize, Errno> {
             let file = node.open(OpenFlags::O_RDONLY)?;
             Process::execve(move |space| elf::load_elf(space, file), 0).unwrap();
             panic!();
-        },
+        }
         SystemCall::Exit => {
             let status = ExitCode::from(args[0] as i32);
             let flags = args[1];
@@ -177,7 +188,7 @@ pub fn syscall(num: SystemCall, args: &[usize]) -> Result<usize, Errno> {
             }
 
             unreachable!();
-        },
+        }
         SystemCall::WaitPid => {
             // TODO special "pid" values
             let pid = unsafe { Pid::from_raw(args[0] as u32) };
@@ -190,17 +201,15 @@ pub fn syscall(num: SystemCall, args: &[usize]) -> Result<usize, Errno> {
                 }
                 e => e.map(|e| i32::from(e) as usize),
             }
-        },
+        }
         SystemCall::WaitTid => {
             let tid = args[0] as u32;
 
             match Thread::waittid(tid) {
-                Ok(_) => {
-                    Ok(0)
-                },
+                Ok(_) => Ok(0),
                 _ => todo!(),
             }
-        },
+        }
         SystemCall::GetPid => Ok(Process::current().id().value() as usize),
         SystemCall::GetTid => Ok(Thread::current().id() as usize),
         SystemCall::Sleep => {
@@ -214,15 +223,15 @@ pub fn syscall(num: SystemCall, args: &[usize]) -> Result<usize, Errno> {
                 }
             }
             res.map(|_| 0)
-        },
+        }
         SystemCall::SetSignalEntry => {
             Thread::current().set_signal_entry(args[0], args[1]);
             Ok(0)
-        },
+        }
         SystemCall::SignalReturn => {
             Thread::current().return_from_signal();
             unreachable!();
-        },
+        }
         SystemCall::SendSignal => {
             let target = SignalDestination::from(args[0] as isize);
             let signal = Signal::try_from(args[1] as u32)?;
@@ -235,11 +244,11 @@ pub fn syscall(num: SystemCall, args: &[usize]) -> Result<usize, Errno> {
                 _ => todo!(),
             };
             Ok(0)
-        },
+        }
         SystemCall::Yield => {
             proc::switch();
             Ok(0)
-        },
+        }
         SystemCall::GetSid => {
             // TODO handle kernel processes here?
             let pid = args[0] as u32;
@@ -250,13 +259,13 @@ pub fn syscall(num: SystemCall, args: &[usize]) -> Result<usize, Errno> {
                 let pid = unsafe { Pid::from_raw(pid) };
                 let proc = Process::get(pid).ok_or(Errno::DoesNotExist)?;
                 if proc.sid() != current.sid() {
-                    return Err(Errno::PermissionDenied)
+                    return Err(Errno::PermissionDenied);
                 }
                 proc
             };
 
             Ok(proc.sid().value() as usize)
-        },
+        }
         SystemCall::GetPgid => {
             // TODO handle kernel processes here?
             let pid = args[0] as u32;
@@ -269,10 +278,8 @@ pub fn syscall(num: SystemCall, args: &[usize]) -> Result<usize, Errno> {
             };
 
             Ok(proc.pgid().value() as usize)
-        },
-        SystemCall::GetPpid => {
-            Ok(Process::current().ppid().unwrap().value() as usize)
-        },
+        }
+        SystemCall::GetPpid => Ok(Process::current().ppid().unwrap().value() as usize),
         SystemCall::SetSid => {
             let proc = Process::current();
             let mut io = proc.io.lock();
@@ -282,17 +289,13 @@ pub fn syscall(num: SystemCall, args: &[usize]) -> Result<usize, Errno> {
             }
 
             todo!();
-        },
+        }
         SystemCall::SetPgid => {
             let pid = args[0] as u32;
             let pgid = args[1] as u32;
 
             let current = Process::current();
-            let proc = if pid == 0 {
-                current
-            } else {
-                todo!()
-            };
+            let proc = if pid == 0 { current } else { todo!() };
 
             if pgid == 0 {
                 proc.set_pgid(proc.id());
@@ -301,13 +304,13 @@ pub fn syscall(num: SystemCall, args: &[usize]) -> Result<usize, Errno> {
             }
 
             Ok(proc.pgid().value() as usize)
-        },
+        }
 
         // System
         SystemCall::GetCpuTime => {
             let time = machine::local_timer().timestamp()?;
             Ok(time.as_nanos() as usize)
-        },
+        }
 
         // Debugging
         SystemCall::DebugTrace => {
@@ -316,9 +319,9 @@ pub fn syscall(num: SystemCall, args: &[usize]) -> Result<usize, Errno> {
             print!(Level::Debug, "{}", buf);
             println!(Level::Debug, "");
             Ok(args[1])
-        },
+        }
 
         // Handled elsewhere
-        SystemCall::Fork => unreachable!()
+        SystemCall::Fork => unreachable!(),
     }
 }
diff --git a/libsys/src/abi.rs b/libsys/src/abi.rs
index fad27ff..7fb3eb4 100644
--- a/libsys/src/abi.rs
+++ b/libsys/src/abi.rs
@@ -12,6 +12,7 @@ pub enum SystemCall {
     Ioctl = 6,
     Select = 7,
     Access = 8,
+    ReadDirectory = 9,
     // Process manipulation
     Fork = 32,
     Clone = 33,
diff --git a/libsys/src/calls.rs b/libsys/src/calls.rs
index 98fdb53..aae3c28 100644
--- a/libsys/src/calls.rs
+++ b/libsys/src/calls.rs
@@ -4,7 +4,7 @@ use crate::{
     ioctl::IoctlCmd,
     proc::{ExitCode, Pid},
     signal::{Signal, SignalDestination},
-    stat::{AccessMode, FdSet, FileDescriptor, FileMode, OpenFlags, Stat},
+    stat::{AccessMode, DirectoryEntry, FdSet, FileDescriptor, FileMode, OpenFlags, Stat},
 };
 use core::time::Duration;
 
@@ -366,6 +366,20 @@ pub fn sys_getpgid(pid: Pid) -> Result<Pid, Errno> {
 
 #[inline(always)]
 pub fn sys_setpgid(pid: Pid, pgid: Pid) -> Result<Pid, Errno> {
-    Errno::from_syscall(unsafe { syscall!(SystemCall::SetPgid, argn!(pid.value()), argn!(pgid.value())) }).map(|e| unsafe { Pid::from_raw(e as u32) })
+    Errno::from_syscall(unsafe {
+        syscall!(SystemCall::SetPgid, argn!(pid.value()), argn!(pgid.value()))
+    })
+    .map(|e| unsafe { Pid::from_raw(e as u32) })
 }
 
+#[inline(always)]
+pub fn sys_readdir(fd: FileDescriptor, buf: &mut [DirectoryEntry]) -> Result<usize, Errno> {
+    Errno::from_syscall(unsafe {
+        syscall!(
+            SystemCall::ReadDirectory,
+            argn!(u32::from(fd)),
+            argp!(buf.as_mut_ptr()),
+            argn!(buf.len())
+        )
+    })
+}
diff --git a/libsys/src/stat.rs b/libsys/src/stat.rs
index a59be78..7ba0ecb 100644
--- a/libsys/src/stat.rs
+++ b/libsys/src/stat.rs
@@ -1,5 +1,5 @@
-use core::fmt;
 use crate::error::Errno;
+use core::fmt;
 
 const AT_FDCWD: i32 = -2;
 pub const AT_EMPTY_PATH: u32 = 1 << 16;
@@ -14,11 +14,16 @@ bitflags! {
         const O_CREAT =     1 << 4;
         const O_EXEC =      1 << 5;
         const O_CLOEXEC =   1 << 6;
+        const O_DIRECTORY = 1 << 7;
     }
 }
 
 bitflags! {
     pub struct FileMode: u32 {
+        const FILE_TYPE = 0xF << 12;
+        const S_IFREG = 0x8 << 12;
+        const S_IFDIR = 0x4 << 12;
+
         const USER_READ = 1 << 8;
         const USER_WRITE = 1 << 7;
         const USER_EXEC = 1 << 6;
@@ -42,26 +47,57 @@ bitflags! {
 
 #[derive(Clone, Default)]
 pub struct FdSet {
-    bits: [u64; 2]
+    bits: [u64; 2],
 }
 
 #[derive(Clone, Copy, Debug)]
 #[repr(transparent)]
 pub struct FileDescriptor(u32);
 
+#[derive(Clone, Copy)]
+pub struct DirectoryEntry {
+    name: [u8; 64],
+}
+
 struct FdSetIter<'a> {
     idx: u32,
-    set: &'a FdSet
+    set: &'a FdSet,
 }
 
 #[derive(Clone, Copy, Debug, Default)]
 #[repr(C)]
 pub struct Stat {
-    pub mode: u32,
+    pub mode: FileMode,
     pub size: u64,
     pub blksize: u32,
 }
 
+impl DirectoryEntry {
+    pub const fn empty() -> Self {
+        Self { name: [0; 64] }
+    }
+
+    pub fn from_str(i: &str) -> DirectoryEntry {
+        let mut res = DirectoryEntry { name: [0; 64] };
+        let bytes = i.as_bytes();
+        res.name[..bytes.len()].copy_from_slice(bytes);
+        res
+    }
+
+    pub fn as_str(&self) -> &str {
+        let zero = self.name.iter().position(|&c| c == 0).unwrap();
+        core::str::from_utf8(&self.name[..zero]).unwrap()
+    }
+}
+
+impl fmt::Debug for DirectoryEntry {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.debug_struct("DirectoryEntry")
+            .field("name", &self.as_str())
+            .finish()
+    }
+}
+
 impl FdSet {
     pub const fn empty() -> Self {
         Self { bits: [0; 2] }
@@ -93,10 +129,7 @@ impl FdSet {
     }
 
     pub fn iter(&self) -> impl Iterator<Item = FileDescriptor> + '_ {
-        FdSetIter {
-            idx: 0,
-            set: self
-        }
+        FdSetIter { idx: 0, set: self }
     }
 }
 
@@ -131,13 +164,51 @@ impl fmt::Debug for FdSet {
 
 impl FileMode {
     /// Returns default permission set for directories
-    pub const fn default_dir() -> Self {
-        unsafe { Self::from_bits_unchecked(0o755) }
+    pub fn default_dir() -> Self {
+        unsafe { Self::from_bits_unchecked(0o755) | Self::S_IFDIR }
     }
 
     /// Returns default permission set for regular files
-    pub const fn default_reg() -> Self {
-        unsafe { Self::from_bits_unchecked(0o644) }
+    pub fn default_reg() -> Self {
+        unsafe { Self::from_bits_unchecked(0o644) | Self::S_IFREG }
+    }
+}
+
+fn choose<T>(q: bool, a: T, b: T) -> T {
+    if q { a } else { b }
+}
+
+impl Default for FileMode {
+    fn default() -> Self {
+        unsafe { Self::from_bits_unchecked(0) }
+    }
+}
+
+impl fmt::Display for FileMode {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(
+            f,
+            "{}{}{}{}{}{}{}{}{}{}",
+            // File type
+            match *self & Self::FILE_TYPE {
+                Self::S_IFDIR => 'd',
+                Self::S_IFREG => '-',
+                _ => '?'
+            },
+            // User
+            choose(self.contains(Self::USER_READ), 'r', '-'),
+            choose(self.contains(Self::USER_WRITE), 'w', '-'),
+            choose(self.contains(Self::USER_EXEC), 'x', '-'),
+            // Group
+            choose(self.contains(Self::GROUP_READ), 'r', '-'),
+            choose(self.contains(Self::GROUP_WRITE), 'w', '-'),
+            choose(self.contains(Self::GROUP_EXEC), 'x', '-'),
+            // Other
+            choose(self.contains(Self::OTHER_READ), 'r', '-'),
+            choose(self.contains(Self::OTHER_WRITE), 'w', '-'),
+            choose(self.contains(Self::OTHER_EXEC), 'x', '-'),
+        );
+        Ok(())
     }
 }
 
diff --git a/user/Cargo.toml b/user/Cargo.toml
index 8cb69ab..91af3a7 100644
--- a/user/Cargo.toml
+++ b/user/Cargo.toml
@@ -17,6 +17,10 @@ path = "src/shell/main.rs"
 name = "fuzzy"
 path = "src/fuzzy/main.rs"
 
+[[bin]]
+name = "ls"
+path = "src/ls/main.rs"
+
 [dependencies]
 libusr = { path = "../libusr" }
 lazy_static = { version = "*", features = ["spin_no_std"] }
diff --git a/user/src/ls/main.rs b/user/src/ls/main.rs
new file mode 100644
index 0000000..91d8ebb
--- /dev/null
+++ b/user/src/ls/main.rs
@@ -0,0 +1,45 @@
+#![no_std]
+#![no_main]
+
+#[macro_use]
+extern crate libusr;
+#[macro_use]
+extern crate alloc;
+
+use libusr::sys::{sys_readdir, sys_openat, sys_close, sys_fstatat, stat::{FileMode, OpenFlags, DirectoryEntry, Stat}};
+use alloc::{string::String, borrow::ToOwned};
+
+#[no_mangle]
+fn main() -> i32 {
+    let mut buffer = [DirectoryEntry::empty(); 16];
+    let mut stat = Stat::default();
+    let mut data = vec![];
+
+    let fd = sys_openat(None, "/", FileMode::default_dir(), OpenFlags::O_DIRECTORY | OpenFlags::O_RDONLY).unwrap();
+
+    loop {
+        let count = sys_readdir(fd, &mut buffer).unwrap();
+        if count == 0 {
+            break;
+        }
+
+        buffer.iter().take(count).for_each(|e| data.push(e.as_str().to_owned()));
+    }
+
+    data.sort();
+
+    data.iter().for_each(|item| {
+        let stat = sys_fstatat(Some(fd), item, &mut stat, 0).map(|_| &stat);
+        if let Ok(stat) = stat {
+            print!("{} ", stat.mode);
+        } else {
+            print!("?????????? ");
+        }
+        println!("{}", item);
+    });
+
+    sys_close(fd).unwrap();
+
+
+    0
+}