From 28101de56231252ca0cfa6a9f107b75112c9acad Mon Sep 17 00:00:00 2001
From: Ben Bridle <bridle.benjamin@gmail.com>
Date: Wed, 31 Jan 2024 07:38:50 +1300
Subject: Implement new file device interface

This is a complete redesign of the file device. The most notable
addition is the ability to ascend and descend the file tree.
---
 src/devices.rs                           |  64 +++----
 src/devices/file.rs                      | 293 ++++++++++++++++++++++++-------
 src/devices/file/buffered_file.rs        | 127 ++++++++++++++
 src/devices/file/circular_path_buffer.rs |  66 +++++++
 src/devices/file/directory_entry.rs      |  30 ++++
 src/devices/file/directory_listing.rs    | 108 ++++++++++++
 src/devices/file/entry.rs                |  36 ++++
 src/devices/file/operations.rs           |  43 +++++
 src/main.rs                              |   2 +
 9 files changed, 674 insertions(+), 95 deletions(-)
 create mode 100644 src/devices/file/buffered_file.rs
 create mode 100644 src/devices/file/circular_path_buffer.rs
 create mode 100644 src/devices/file/directory_entry.rs
 create mode 100644 src/devices/file/directory_listing.rs
 create mode 100644 src/devices/file/entry.rs
 create mode 100644 src/devices/file/operations.rs

(limited to 'src')

diff --git a/src/devices.rs b/src/devices.rs
index 5b274c9..e1a00f7 100644
--- a/src/devices.rs
+++ b/src/devices.rs
@@ -182,22 +182,22 @@ impl DeviceBus for StandardDevices {
             // Stream
 
             // File
-            0xA0 => read_b!(self.file.file.is_some()),
-            0xA1 => read_b!(self.file.operation_state),
-            0xA2 => no_read!(),
-            0xA3 => no_read!(),
-            0xA4 => read_hh!(self.file.pointer),
-            0xA5 => read_hl!(self.file.pointer),
-            0xA6 => read_lh!(self.file.pointer),
-            0xA7 => read_ll!(self.file.pointer),
-            0xA8 => self.file.read(),
-            0xA9 => self.file.read(),
-            0xAA => no_read!(),
-            0xAB => no_read!(),
-            0xAC => read_hh!(self.file.file_size),
-            0xAD => read_hl!(self.file.file_size),
-            0xAE => read_lh!(self.file.file_size),
-            0xAF => read_ll!(self.file.file_size),
+            0xA0 => read_b!(self.file.entry.is_some()),
+            0xA1 => read_b!(self.file.move_success),
+            0xA2 => self.file.name_buffer.read_byte(),
+            0xA3 => read_b!(self.file.entry_type()),
+            0xA4 => self.file.read_byte(),
+            0xA5 => self.file.read_byte(),
+            0xA6 => self.file.read_child_name(),
+            0xA7 => read_b!(self.file.child_type()),
+            0xA8 => read_hh!(self.file.pointer()),
+            0xA9 => read_hl!(self.file.pointer()),
+            0xAA => read_lh!(self.file.pointer()),
+            0xAB => read_ll!(self.file.pointer()),
+            0xAC => read_hh!(self.file.length()),
+            0xAD => read_hl!(self.file.length()),
+            0xAE => read_lh!(self.file.length()),
+            0xAF => read_ll!(self.file.length()),
 
             _ => unimplemented!("Reading from device port 0x{port:02x}"),
         }
@@ -310,22 +310,22 @@ impl DeviceBus for StandardDevices {
             0x8E => no_write!(),
             0x8F => no_write!(),
             // File
-            0xA0 => self.file.push_name_byte(val),
-            0xA1 => no_write!(),
-            0xA2 => no_write!(),
-            0xA3 => no_write!(),
-            0xA4 => write_hh!(self.file.pointer),
-            0xA5 => write_hl!(self.file.pointer),
-            0xA6 => write_lh!(self.file.pointer),
-            0xA7 => write_ll!(self.file.pointer),
-            0xA8 => no_write!(),
-            0xA9 => no_write!(),
-            0xAA => no_write!(),
-            0xAB => no_write!(),
-            0xAC =>   write_hh!(self.file.file_size),
-            0xAD =>   write_hl!(self.file.file_size),
-            0xAE =>   write_lh!(self.file.file_size),
-            0xAF => { write_ll!(self.file.file_size); self.file.set_file_size() },
+            0xA0 => self.file.write_to_open_port(val),
+            0xA1 => self.file.write_to_move_port(val),
+            0xA2 => self.file.set_name_pointer(val),
+            0xA3 => self.file.ascend_to_parent(),
+            0xA4 => self.file.write_byte(val),
+            0xA5 => self.file.write_byte(val),
+            0xA6 => self.file.set_child_name_pointer(val),
+            0xA7 => self.file.descend_to_child(),
+            0xA8 =>   write_hh!(self.file.new_pointer),
+            0xA9 =>   write_hl!(self.file.new_pointer),
+            0xAA =>   write_lh!(self.file.new_pointer),
+            0xAB => { write_ll!(self.file.new_pointer); self.file.commit_pointer() },
+            0xAC =>   write_hh!(self.file.new_length),
+            0xAD =>   write_hl!(self.file.new_length),
+            0xAE =>   write_lh!(self.file.new_length),
+            0xAF => { write_ll!(self.file.new_length); self.file.commit_length() },
 
             // Bytestreams
             0x96 => self.stream.write_stdout(val),
diff --git a/src/devices/file.rs b/src/devices/file.rs
index f442dd8..163c10d 100644
--- a/src/devices/file.rs
+++ b/src/devices/file.rs
@@ -1,90 +1,257 @@
-use std::io::{ErrorKind, Read, Seek, SeekFrom};
-use std::fs::{File, OpenOptions};
+mod buffered_file;
+mod circular_path_buffer;
+mod directory_entry;
+mod directory_listing;
+mod entry;
+mod operations;
+
+pub use buffered_file::*;
+pub use circular_path_buffer::*;
+pub use directory_entry::*;
+pub use directory_listing::*;
+pub use entry::*;
+use operations::*;
+
+use std::fs::{OpenOptions, metadata};
+use std::os::unix::ffi::OsStrExt;
+use std::path::{Component, Path, PathBuf};
+
+fn is_blank_path(path: &Path) -> bool {
+    path == PathBuf::new()
+}
 
 pub struct FileDevice {
-    pub name: Vec<u8>,
-    pub rename: Vec<u8>,
+    /// The path to which the file device is confined. Files and directories
+    /// outside of this directory cannot be accessed.
+    pub base_path: PathBuf,
 
-    pub file: Option<File>,
-    pub operation_state: bool,
-    pub pointer: u32,
-    pub file_size: u32,
+    pub open_buffer: CircularPathBuffer,
+    pub move_buffer: CircularPathBuffer,
+    pub name_buffer: CircularPathBuffer,
+
+    pub entry: Option<(Entry, PathBuf)>,
+
+    pub move_success: bool,
+    pub new_pointer: u32,
+    pub new_length: u32,
 }
 
 impl FileDevice {
     pub fn new() -> Self {
         Self {
-            name: Vec::new(),
-            rename: Vec::new(),
-
-            file: None,
-            operation_state: false,
-            pointer: 0,
-            file_size: 0,
-        }
-    }
-
-    pub fn push_name_byte(&mut self, byte: u8) {
-        if byte != 0 { self.name.push(byte); return }
-        // If char was null, attempt to open the file and read file size.
-        let path: std::ffi::OsString = std::os::unix::ffi::OsStringExt::from_vec(
-                std::mem::take(&mut self.name));
-        let try_open = OpenOptions::new().read(true).write(true).open(path);
-        if let Ok(file) = try_open {
-            if let Ok(metadata) = file.metadata() {
-                // Success.
-                self.file = Some(file);
-                match u32::try_from(metadata.len()) {
-                    Ok(len) => self.file_size = len,
-                    Err(_)  => self.file_size = u32::MAX,
-                }
-                return;
+            base_path: PathBuf::from("/home/ben/Sandbox"),
+
+            open_buffer: CircularPathBuffer::new(),
+            move_buffer: CircularPathBuffer::new(),
+            name_buffer: CircularPathBuffer::new(),
+
+            entry: None,
+
+            move_success: false,
+            new_pointer: 0,
+            new_length: 0,
+        }
+    }
+
+    pub fn close_entry(&mut self) {
+        self.open_buffer.clear();
+        self.move_buffer.clear();
+        self.name_buffer.clear();
+        self.entry = None;
+        self.new_pointer = 0;
+        self.new_length = 0;
+    }
+
+    pub fn write_to_open_port(&mut self, byte: u8) {
+        if let Some(relative_path) = self.open_buffer.push_byte(byte) {
+            println!("Attempting to open path {relative_path:?}");
+            self.close_entry();
+            if !is_blank_path(&relative_path) {
+                if let Ok(path) = self.attach_base(&relative_path) {
+                    println!("Attempting to open absolute path {path:?}");
+                    let _ = self.open_entry(&path);
+                };
             }
         }
-        // Failure.
-        self.close();
     }
 
-    pub fn set_file_size(&mut self) {
-        if let Some(file) = &self.file {
-            if let Ok(()) = file.set_len(self.file_size.into()) {
-                return;
+    pub fn set_name_pointer(&mut self, byte: u8) {
+        self.name_buffer.set_pointer(byte);
+    }
+
+    /// Open the entry at the given path.
+    pub fn open_entry(&mut self, path: &Path) -> Result<(), ()> {
+        macro_rules! raise_on_err {
+            ($res:expr) => {match $res {Ok(v)=>v, Err(_)=>return Err(())} } }
+
+        if !path.starts_with(&self.base_path) { return Err(()); }
+        let metadata = raise_on_err!(metadata(&path));
+        if metadata.is_file() {
+            println!("Opening file as readable/writable");
+            if let Ok(file) = OpenOptions::new().read(true).write(true).open(&path) {
+                self.close_entry();
+                let file_entry = Entry::File(BufferedFile::new(file));
+                self.entry = Some((file_entry, path.to_owned()));
+                let relative = remove_base(&path, &self.base_path).unwrap();
+                self.name_buffer.populate(relative.as_os_str().as_bytes());
+                println!("Success, opened file {path:?}");
+                return Ok(());
             };
-        }
-        self.close();
+        } else if metadata.is_dir() {
+            println!("Opening directory");
+            if let Ok(listing) = DirectoryListing::from_path(&path, &self.base_path) {
+                let dir_entry = Entry::Directory(listing);
+                self.entry = Some((dir_entry, path.to_owned()));
+                let relative = remove_base(&path, &self.base_path).unwrap();
+                self.name_buffer.populate(relative.as_os_str().as_bytes());
+                println!("Success, opened directory {path:?}");
+                return Ok(());
+            };
+        };
+        return Err(());
     }
 
-    pub fn read(&mut self) -> u8 {
-        if let Some(file) = &mut self.file {
-            let mut buffer: [u8; 1] = [0; 1];
-            match file.read_exact(&mut buffer) {
-                Ok(()) => return buffer[0],
-                Err(err) => match err.kind() {
-                    ErrorKind::UnexpectedEof => return 0,
-                    _ => unimplemented!("File read error: ErrorKind::{:?}", err.kind()),
+    pub fn write_to_move_port(&mut self, byte: u8) {
+        if let Some(dest) = self.move_buffer.push_byte(byte) {
+            if let Some((_, source)) = &self.entry {
+                if is_blank_path(&dest) {
+                    self.move_success = delete_entry(&source);
+                } else if let Ok(destination) = self.attach_base(&dest) {
+                    println!("Attempting to move entry: {destination:?}");
+                    self.move_success = move_entry(&source, &destination);
+                }
+            } else {
+                if is_blank_path(&dest) {
+                    self.move_success = false;
+                } else if let Ok(destination) = self.attach_base(&dest) {
+                    println!("Attempting to create entry: {destination:?}");
+                    self.move_success = create_file(&destination);
                 }
             }
+            self.close_entry();
+        }
+    }
+
+    /// Attempt to open the parent directory of the current entry.
+    pub fn ascend_to_parent(&mut self) {
+        if let Some((_, path)) = &self.entry {
+            if let Some(parent_path) = path.parent() {
+                let path = parent_path.to_owned();
+                let _ = self.open_entry(&path);
+            }
         }
-        self.close();
-        return 0;
     }
 
-    pub fn seek(&mut self) {
-        if let Some(file) = &mut self.file {
-            let point = SeekFrom::Start(self.pointer.into());
-            if let Ok(pointer) = file.seek(point) {
-                if let Ok(pointer_u32) = pointer.try_into() {
-                    self.pointer = pointer_u32;
-                    return;
+    /// Attempt to open the currently-selected child.
+    pub fn descend_to_child(&mut self) {
+        if let Some((Entry::Directory(listing), _)) = &self.entry {
+            if let Some(child_path) = listing.child_path() {
+                if let Ok(child_path) = self.attach_base(&child_path) {
+                    let child_path = child_path.to_owned();
+                    let _ = self.open_entry(&child_path);
                 }
             };
         }
-        self.close();
     }
 
-    pub fn close(&mut self) {
-        self.file = None;
-        self.pointer = 0;
-        self.file_size = 0;
+    /// Return true if the currently-open entry is a directory.
+    pub fn entry_type(&self) -> bool {
+        match self.entry {
+            Some((Entry::Directory(_), _)) => true,
+            _ => false,
+        }
+    }
+
+    /// Return true if the currently-selected child is a directory.
+    pub fn read_child_name(&mut self) -> u8 {
+        if let Some((Entry::Directory(listing), _)) = &mut self.entry {
+            listing.child_name().read_byte()
+        } else {
+            0
+        }
+    }
+
+    pub fn set_child_name_pointer(&mut self, byte: u8) {
+        if let Some((Entry::Directory(listing), _)) = &mut self.entry {
+            listing.child_name().set_pointer(byte);
+        }
+    }
+
+    /// Return true if the currently-selected child is a directory.
+    pub fn child_type(&self) -> bool {
+        if let Some((Entry::Directory(listing), _)) = &self.entry {
+            match listing.child_type() {
+                Some(EntryType::Directory) => true,
+                Some(EntryType::File) => false,
+                None => false,
+            }
+        } else {
+            false
+        }
+    }
+
+    pub fn read_byte(&mut self) -> u8 {
+        match &mut self.entry {
+            Some((Entry::File(buffered_file), _)) => buffered_file.read_byte(),
+            _ => 0,
+        }
+    }
+
+    pub fn write_byte(&mut self, byte: u8) {
+        match &mut self.entry {
+            Some((Entry::File(buffered_file), _)) => buffered_file.write_byte(byte),
+            _ => (),
+        }
+    }
+
+    pub fn pointer(&mut self) -> u32 {
+        match &mut self.entry {
+            Some((Entry::File(buffered_file), _)) => buffered_file.pointer(),
+            Some((Entry::Directory(listing), _)) => listing.selected(),
+            _ => 0,
+        }
+    }
+
+    pub fn commit_pointer(&mut self) {
+        let pointer = std::mem::take(&mut self.new_pointer);
+        match &mut self.entry {
+            Some((Entry::File(buffered_file), _)) => buffered_file.set_pointer(pointer),
+            Some((Entry::Directory(listing), _)) => listing.set_selected(pointer),
+            _ => (),
+        }
+    }
+
+    pub fn length(&mut self) -> u32 {
+        match &mut self.entry {
+            Some((Entry::File(buffered_file), _)) => buffered_file.length(),
+            Some((Entry::Directory(listing), _)) => listing.length(),
+            _ => 0,
+        }
+    }
+
+    pub fn commit_length(&mut self) {
+        let length = std::mem::take(&mut self.new_length);
+        match &mut self.entry {
+            Some((Entry::File(buffered_file), _)) => buffered_file.set_length(length),
+            _ => (),
+        }
+    }
+
+    fn attach_base(&self, relative_path: &Path) -> Result<PathBuf, ()> {
+        let mut full_path = self.base_path.clone();
+        let mut has_root = false;
+        for component in relative_path.components() {
+            match component {
+                Component::Normal(s) => full_path.push(s),
+                Component::ParentDir => return Err(()),
+                Component::CurDir => continue,
+                Component::RootDir => has_root = true,
+                Component::Prefix(_) => continue,
+            }
+        };
+        match has_root {
+            true => Ok(full_path),
+            false => Err(())
+        }
     }
 }
diff --git a/src/devices/file/buffered_file.rs b/src/devices/file/buffered_file.rs
new file mode 100644
index 0000000..091b5d9
--- /dev/null
+++ b/src/devices/file/buffered_file.rs
@@ -0,0 +1,127 @@
+use std::fs::File;
+use std::io::{BufReader, BufWriter};
+use std::io::{Read, Write};
+use std::io::{ErrorKind, Seek, SeekFrom};
+
+enum AccessMode {
+    Read(BufReader<File>),
+    Write(BufWriter<File>),
+    None,
+}
+
+impl AccessMode {
+    pub fn unwrap(self) -> File {
+        match self {
+            Self::Read(reader) => reader.into_inner(),
+            Self::Write(writer) => writer.into_inner().unwrap(),
+            Self::None => unreachable!(),
+        }
+    }
+}
+
+impl Default for AccessMode {
+    fn default() -> Self {
+        Self::None
+    }
+}
+
+pub struct BufferedFile {
+    file: AccessMode,
+}
+
+impl BufferedFile {
+    pub fn new(file: File) -> Self {
+        Self {
+            file: AccessMode::Read(BufReader::new(file)),
+        }
+    }
+
+    pub fn read_byte(&mut self) -> u8 {
+        let mut buffer = [0u8; 1];
+
+        let read_result = match &mut self.file {
+            AccessMode::Read(reader) => reader.read_exact(&mut buffer),
+            AccessMode::Write(writer) => {
+                let address = writer.stream_position().unwrap();
+                let file = std::mem::take(&mut self.file).unwrap();
+                let mut reader = BufReader::new(file);
+                reader.seek(SeekFrom::Start(address)).unwrap();
+                let read_result = reader.read_exact(&mut buffer);
+                self.file = AccessMode::Read(reader);
+                read_result
+            }
+            AccessMode::None => unreachable!(),
+        };
+
+        match read_result {
+            Ok(_) => buffer[0],
+            Err(error) => match error.kind() {
+                ErrorKind::UnexpectedEof => 0,
+                _ => panic!("{error:?}"),
+            }
+        }
+    }
+
+    pub fn write_byte(&mut self, byte: u8) {
+        let mut buffer = [byte; 1];
+
+        let write_result = match &mut self.file {
+            AccessMode::Write(writer) => writer.write_all(&mut buffer),
+            AccessMode::Read(reader) => {
+                let address = reader.stream_position().unwrap();
+                let file = std::mem::take(&mut self.file).unwrap();
+                let mut writer = BufWriter::new(file);
+                writer.seek(SeekFrom::Start(address)).unwrap();
+                let write_result = writer.write_all(&mut buffer);
+                self.file = AccessMode::Write(writer);
+                write_result
+            }
+            AccessMode::None => unreachable!(),
+        };
+
+        write_result.unwrap();
+    }
+
+    pub fn pointer(&mut self) -> u32 {
+        let position = match &mut self.file {
+            AccessMode::Read(reader) => reader.stream_position(),
+            AccessMode::Write(writer) => writer.stream_position(),
+            AccessMode::None => unreachable!(),
+        };
+        u32::try_from(position.unwrap()).unwrap_or(u32::MAX)
+    }
+
+    pub fn set_pointer(&mut self, pointer: u32) {
+        let position = SeekFrom::Start(pointer as u64);
+        match &mut self.file {
+            AccessMode::Read(reader) => reader.seek(position).unwrap(),
+            AccessMode::Write(writer) => writer.seek(position).unwrap(),
+            AccessMode::None => unreachable!(),
+        };
+    }
+
+    pub fn length(&mut self) -> u32 {
+        let length = match &mut self.file {
+            AccessMode::Read(reader) => reader.stream_len(),
+            AccessMode::Write(writer) => writer.stream_len(),
+            AccessMode::None => unreachable!(),
+        };
+        u32::try_from(length.unwrap()).unwrap_or(u32::MAX)
+    }
+
+    pub fn set_length(&mut self, length: u32) {
+        match &mut self.file {
+            AccessMode::Read(_) => {
+                let file = std::mem::take(&mut self.file).unwrap();
+                file.set_len(length as u64).unwrap();
+                self.file = AccessMode::Read(BufReader::new(file));
+            }
+            AccessMode::Write(_) => {
+                let file = std::mem::take(&mut self.file).unwrap();
+                file.set_len(length as u64).unwrap();
+                self.file = AccessMode::Read(BufReader::new(file));
+            }
+            AccessMode::None => unreachable!(),
+        };
+    }
+}
diff --git a/src/devices/file/circular_path_buffer.rs b/src/devices/file/circular_path_buffer.rs
new file mode 100644
index 0000000..f828f73
--- /dev/null
+++ b/src/devices/file/circular_path_buffer.rs
@@ -0,0 +1,66 @@
+use std::ffi::OsString;
+use std::os::unix::ffi::OsStringExt;
+use std::path::PathBuf;
+
+pub struct CircularPathBuffer {
+    buffer: [u8; 256],
+    pointer: u8,
+}
+
+impl CircularPathBuffer {
+    pub fn new() -> Self {
+        Self { buffer: [0; 256] , pointer: 0 }
+    }
+
+    pub fn clear(&mut self) {
+        self.buffer.fill(0);
+        self.pointer = 0;
+    }
+
+    pub fn populate(&mut self, bytes: &[u8]) {
+        self.clear();
+        if bytes.len() > 255 {
+            unreachable!(
+                "Attempted to populate CircularPathBuffer with {} bytes: {:?}",
+                bytes.len(), String::from_utf8_lossy(bytes)
+            )
+        }
+        let self_slice = &mut self.buffer[..bytes.len()];
+        self_slice.copy_from_slice(&bytes);
+    }
+
+    pub fn set_pointer(&mut self, value: u8) {
+        self.pointer = 0;
+        if value != 0x00 {
+            for (i, c) in self.buffer.iter().enumerate() {
+                match c {
+                    b'/' => self.pointer = (i + 1) as u8,
+                    0x00 => break,
+                    _ => continue,
+                }
+            }
+        }
+    }
+
+    pub fn read_byte(&mut self) -> u8 {
+        let pointer = self.pointer as usize;
+        self.pointer = self.pointer.wrapping_add(1);
+        self.buffer[pointer]
+    }
+
+    // Returns an unsanitized relative path.
+    pub fn push_byte(&mut self, value: u8) -> Option<PathBuf> {
+        if value == 0x00 {
+            let pointer = self.pointer as usize;
+            let vec = self.buffer[..pointer].to_vec();
+            self.clear();
+            let os_string: OsString = OsStringExt::from_vec(vec);
+            Some(os_string.into())
+        } else {
+            let pointer = self.pointer as usize;
+            self.pointer = self.pointer.wrapping_add(1);
+            self.buffer[pointer] = value;
+            None
+        }
+    }
+}
diff --git a/src/devices/file/directory_entry.rs b/src/devices/file/directory_entry.rs
new file mode 100644
index 0000000..45de817
--- /dev/null
+++ b/src/devices/file/directory_entry.rs
@@ -0,0 +1,30 @@
+use crate::*;
+
+use std::cmp::Ordering;
+
+#[derive(PartialEq, Eq)]
+pub struct DirectoryChild {
+    pub byte_path: Vec<u8>,
+    pub entry_type: EntryType,
+}
+
+impl PartialOrd for DirectoryChild {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        let entry_type_ord = self.entry_type.cmp(&other.entry_type);
+        let final_order = match entry_type_ord {
+            Ordering::Equal => self.byte_path.cmp(&other.byte_path),
+            _ => entry_type_ord,
+        };
+        Some(final_order)
+    }
+}
+
+impl Ord for DirectoryChild {
+    fn cmp(&self, other: &Self) -> Ordering {
+        let entry_type_ord = self.entry_type.cmp(&other.entry_type);
+        match entry_type_ord {
+            Ordering::Equal => self.byte_path.cmp(&other.byte_path),
+            _ => entry_type_ord,
+        }
+    }
+}
diff --git a/src/devices/file/directory_listing.rs b/src/devices/file/directory_listing.rs
new file mode 100644
index 0000000..1f94a3a
--- /dev/null
+++ b/src/devices/file/directory_listing.rs
@@ -0,0 +1,108 @@
+use crate::*;
+
+use std::ffi::OsString;
+use std::os::unix::ffi::{OsStrExt, OsStringExt};
+use std::path::{Component, Path, PathBuf};
+
+
+pub struct DirectoryListing {
+    children: Vec<DirectoryChild>,
+    length: u32,
+    selected: u32,
+    name_buffer: CircularPathBuffer,
+}
+
+impl DirectoryListing {
+    pub fn from_path(path: &Path, base: &Path) -> Result<Self, ()> {
+        macro_rules! continue_on_err {
+            ($result:expr) => { match $result { Ok(v) => v, Err(_) => continue} };
+        }
+
+        let mut children = Vec::new();
+        if let Ok(iter_dir) = std::fs::read_dir(path) {
+            for (i, entry_result) in iter_dir.enumerate() {
+                if i == u16::MAX as usize {
+                    println!("Warning, this directory contains more than 65536 entries.")
+                };
+                let entry = continue_on_err!(entry_result);
+                let path = continue_on_err!(remove_base(&entry.path(), &base));
+                let byte_path = path.as_os_str().as_bytes();
+                if byte_path.len() > 255 {
+                    continue;
+                };
+                let metadata = continue_on_err!(std::fs::metadata(&entry.path()));
+                children.push( DirectoryChild {
+                    byte_path: Vec::from(byte_path),
+                    entry_type: if metadata.is_file() {
+                        EntryType::File
+                    } else if metadata.is_dir() {
+                        EntryType::Directory
+                    } else {
+                        unreachable!();
+                    },
+                } )
+            }
+            children.sort_unstable();
+            let length = u32::try_from(children.len()).unwrap_or(u32::MAX);
+            let selected = 0;
+            let name_buffer = CircularPathBuffer::new();
+            Ok(Self { children, length, selected, name_buffer } )
+        } else {
+            Err(())
+        }
+    }
+
+    pub fn get(&self, index: u32) -> Option<&DirectoryChild> {
+        self.children.get(index as usize)
+    }
+
+    pub fn length(&self) -> u32 {
+        self.length
+    }
+
+    pub fn selected(&self) -> u32 {
+        self.selected
+    }
+
+    pub fn set_selected(&mut self, index: u32) {
+        if let Some(info) = self.get(index) {
+            self.name_buffer.populate(&info.byte_path.clone());
+            self.selected = index;
+        } else {
+            self.name_buffer.clear();
+            self.selected = 0;
+        }
+    }
+
+    pub fn child_name(&mut self) -> &mut CircularPathBuffer {
+        &mut self.name_buffer
+    }
+
+    pub fn child_type(&self) -> Option<EntryType> {
+        self.get(self.selected).and_then(|i| Some(i.entry_type))
+    }
+
+    pub fn child_path(&self) -> Option<PathBuf> {
+        self.get(self.selected).and_then(|i| {
+            let os_string: OsString = OsStringExt::from_vec(i.byte_path.clone());
+            Some(os_string.into())
+        })
+    }
+}
+
+pub fn remove_base(absolute_path: &Path, base_path: &Path) -> Result<PathBuf, ()> {
+    if let Ok(relative) = absolute_path.strip_prefix(base_path) {
+        let mut baseless_path = PathBuf::from("/");
+        for component in relative.components() {
+            match component {
+                Component::Normal(s) => baseless_path.push(s),
+                Component::ParentDir => return Err(()),
+                Component::CurDir => continue,
+                Component::RootDir => continue,
+                Component::Prefix(_) => continue,
+            }
+        }
+        return Ok(baseless_path);
+    }
+    return Err(());
+}
diff --git a/src/devices/file/entry.rs b/src/devices/file/entry.rs
new file mode 100644
index 0000000..a91ae82
--- /dev/null
+++ b/src/devices/file/entry.rs
@@ -0,0 +1,36 @@
+use crate::*;
+
+use std::cmp::Ordering;
+
+pub enum Entry {
+    File(BufferedFile),
+    Directory(DirectoryListing),
+}
+
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub enum EntryType {
+    File,
+    Directory,
+}
+
+impl PartialOrd for EntryType {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        match (self, other) {
+            (EntryType::Directory, EntryType::Directory) => Some(Ordering::Equal  ),
+            (EntryType::Directory, EntryType::File     ) => Some(Ordering::Less   ),
+            (EntryType::File,      EntryType::Directory) => Some(Ordering::Greater),
+            (EntryType::File,      EntryType::File     ) => Some(Ordering::Equal  ),
+        }
+    }
+}
+
+impl Ord for EntryType {
+    fn cmp(&self, other: &Self) -> Ordering {
+        match (self, other) {
+            (EntryType::Directory, EntryType::Directory) => Ordering::Equal  ,
+            (EntryType::Directory, EntryType::File     ) => Ordering::Less   ,
+            (EntryType::File,      EntryType::Directory) => Ordering::Greater,
+            (EntryType::File,      EntryType::File     ) => Ordering::Equal  ,
+        }
+    }
+}
diff --git a/src/devices/file/operations.rs b/src/devices/file/operations.rs
new file mode 100644
index 0000000..f33509b
--- /dev/null
+++ b/src/devices/file/operations.rs
@@ -0,0 +1,43 @@
+use std::io::ErrorKind;
+use std::path::Path;
+
+pub fn entry_exists(source: &Path) -> bool {
+    std::fs::metadata(source).is_ok()
+}
+
+// Delete a file or folder, returning true if successful.
+pub fn delete_entry(source: &Path) -> bool {
+    match std::fs::remove_file(source) {
+        Ok(_) => true,
+        Err(e) => match e.kind() {
+            ErrorKind::NotFound => true,
+            ErrorKind::IsADirectory => match std::fs::remove_dir_all(source) {
+                Ok(_) => true,
+                Err(e) => match e.kind() {
+                    ErrorKind::NotFound => true,
+                    _ => false,
+                }
+            }
+            _ => false,
+        }
+    }
+}
+
+pub fn move_entry(source: &Path, destination: &Path) -> bool {
+    if !entry_exists(source) || entry_exists(destination) {
+        return false;
+    }
+    std::fs::rename(source, destination).is_ok()
+}
+
+pub fn create_file(destination: &Path) -> bool {
+    if entry_exists(destination) {
+        false
+    } else {
+        if let Some(parent_path) = destination.parent() {
+            let _ = std::fs::create_dir_all(parent_path);
+        }
+        std::fs::OpenOptions::new().write(true).create_new(true)
+            .open(destination).is_ok()
+    }
+}
diff --git a/src/main.rs b/src/main.rs
index 98bc20f..80dcb54 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,7 @@
 #![feature(bigint_helper_methods)]
+#![feature(io_error_more)]
 #![feature(split_array)]
+#![feature(seek_stream_len)]
 
 use std::io::Read;
 use std::process::exit;
-- 
cgit v1.2.3-70-g09d2