From 47b25c05a6be51b93c909d38a19440d1c04ba2f8 Mon Sep 17 00:00:00 2001
From: Ben Bridle <bridle.benjamin@gmail.com>
Date: Tue, 19 Dec 2023 16:23:29 +1300
Subject: Large collection of changes

---
 src/entry.rs            | 41 +++++++++++++----------
 src/error.rs            | 87 ++++++++++++++++++++++++++++++-------------------
 src/lib.rs              | 14 +++++---
 src/operations.rs       | 31 ++++++++----------
 src/operations/cp.rs    | 65 +++++++++++++++++-------------------
 src/operations/ls.rs    | 41 ++++++++++-------------
 src/operations/mkdir.rs | 21 +++++-------
 src/operations/rm.rs    | 30 +++++++----------
 8 files changed, 168 insertions(+), 162 deletions(-)

(limited to 'src')

diff --git a/src/entry.rs b/src/entry.rs
index 2679e42..6ea78bc 100644
--- a/src/entry.rs
+++ b/src/entry.rs
@@ -1,5 +1,5 @@
-use crate::EntryReadError;
 use std::path::{Path, PathBuf};
+use crate::*;
 
 #[derive(PartialEq)]
 pub enum EntryType {
@@ -10,23 +10,30 @@ pub enum EntryType {
 pub struct Entry {
     pub entry_type: EntryType,
     pub is_symlink: bool,
+    /// The final segment of the file path, including any file extensions.
     pub name: String,
     pub extension: String,
+    /// The canonical file path, with intermediate symbolic links resolved.
     pub path: PathBuf,
+    /// The file path as originally presented to the [Entry] constructor.
     pub original_path: PathBuf,
 }
+
 impl Entry {
-    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, EntryReadError> {
+    pub fn from_path(path: impl AsRef<Path>) -> ReadResult<Self> {
         let path = path.as_ref();
-        let metadata = path.metadata()?;
-        let canonical_path = std::fs::canonicalize(path)?;
-        let canonical_metadata = canonical_path.metadata()?;
+        macro_rules! raise {
+            ($err:expr) => { io_result_to_read_result($err, path)? }; }
+
+        let metadata = raise!(path.metadata());
+        let canonical_path = raise!(std::fs::canonicalize(path));
+        let canonical_metadata = raise!(canonical_path.metadata());
         let entry_type = if canonical_metadata.is_file() {
             EntryType::File
         } else if canonical_metadata.is_dir() {
             EntryType::Directory
         } else {
-            return Err(EntryReadError::NotFound);
+            unreachable!("Canonical metadata must describe either a file or a directory");
         };
 
         let name = match path.file_name() {
@@ -47,7 +54,11 @@ impl Entry {
         })
     }
 
-    /// Splits the filename on the last period, ignoring any period at the
+    pub fn parent(&self) -> Option<Entry> {
+        Entry::from_path(self.original_path.parent()?).ok()
+    }
+
+    /// Split the filename on the last period, ignoring any period at the
     /// start of the filename. If no extension is found, the extension is empty.
     pub fn split_name(&self) -> (String, String) {
         match self.name.rsplit_once(".") {
@@ -57,24 +68,18 @@ impl Entry {
     }
 
     pub fn is_file(&self) -> bool {
-        match self.entry_type {
-            EntryType::File => true,
-            _ => false,
-        }
+        self.entry_type == EntryType::File
     }
 
     pub fn is_directory(&self) -> bool {
-        match self.entry_type {
-            EntryType::Directory => true,
-            _ => false,
-        }
+        self.entry_type == EntryType::Directory
     }
 
-    pub fn read_as_bytes(&self) -> Result<Vec<u8>, EntryReadError> {
-        return Ok(std::fs::read(&self.path)?);
+    pub fn read_as_bytes(&self) -> ReadResult<Vec<u8>> {
+        Ok(io_result_to_read_result(std::fs::read(&self.path), &self.path)?)
     }
 
-    pub fn read_as_utf8_string(&self) -> Result<String, EntryReadError> {
+    pub fn read_as_utf8(&self) -> ReadResult<String> {
         return Ok(String::from_utf8_lossy(&self.read_as_bytes()?).to_string());
     }
 }
diff --git a/src/error.rs b/src/error.rs
index 0e8d99f..be0ebb7 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -1,48 +1,69 @@
-use std::io::Error as IoError;
-use std::io::ErrorKind;
+use std::io::{ErrorKind as IoErrorKind, Error as IoError};
+use crate::*;
 
-#[derive(Debug)]
-pub enum EntryReadError {
+#[derive(Debug, PartialEq)]
+pub enum EntryErrorKind {
     NotFound,
     PermissionDenied,
 }
-impl From<IoError> for EntryReadError {
-    fn from(io_error: IoError) -> Self {
-        match io_error.kind() {
-            ErrorKind::NotFound => Self::NotFound,
-            // An intermediate path component was a plain file, not a directory
-            ErrorKind::NotADirectory => Self::NotFound,
-            // A cyclic symbolic link chain was included in the provided path
-            ErrorKind::FilesystemLoop => Self::NotFound,
-            ErrorKind::PermissionDenied => Self::PermissionDenied,
-            err => panic!("Unexpected IoError encountered: {:?}", err),
-        }
-    }
+
+pub struct EntryReadError {
+    pub path: PathBuf,
+    pub error_kind: EntryErrorKind,
 }
 
-#[derive(Debug)]
-pub enum EntryWriteError {
-    NotFound,
-    PermissionDenied,
+pub struct EntryWriteError {
+    pub path: PathBuf,
+    pub error_kind: EntryErrorKind,
 }
+
 impl From<EntryReadError> for EntryWriteError {
     fn from(error: EntryReadError) -> Self {
-        match error {
-            EntryReadError::NotFound => EntryWriteError::NotFound,
-            EntryReadError::PermissionDenied => EntryWriteError::PermissionDenied,
+        EntryWriteError { path: error.path, error_kind: error.error_kind }
+    }
+}
+
+pub(crate) fn io_result_to_read_result<T>(io_result: Result<T, IoError>, path: &Path) -> ReadResult<T> {
+    match io_result {
+        Ok(t) => Ok(t),
+        Err(io_error) => {
+            match io_error_to_entry_error(io_error) {
+                Ok(error_kind) => Err( EntryReadError { path: path.to_path_buf(), error_kind }),
+                Err(err) => panic!("Unexpected IO error while attempting to read from {path:?}: {err:?}"),
+            }
         }
     }
 }
-impl From<IoError> for EntryWriteError {
-    fn from(io_error: IoError) -> Self {
-        match io_error.kind() {
-            ErrorKind::NotFound => Self::NotFound,
-            // An intermediate path component was a plain file, not a directory
-            ErrorKind::NotADirectory => Self::NotFound,
-            // A cyclic symbolic link chain was included in the provided path
-            ErrorKind::FilesystemLoop => Self::NotFound,
-            ErrorKind::PermissionDenied => Self::PermissionDenied,
-            err => panic!("Unexpected IoError encountered: {:?}", err),
+pub(crate) fn io_result_to_write_result<T>(io_result: Result<T, IoError>, path: &Path) -> WriteResult<T> {
+    match io_result {
+        Ok(t) => Ok(t),
+        Err(io_error) => {
+            match io_error_to_entry_error(io_error) {
+                Ok(error_kind) => Err( EntryWriteError { path: path.to_path_buf(), error_kind }),
+                Err(err) => panic!("Unexpected IO error while attempting to write to {path:?}: {err:?}"),
+            }
         }
     }
 }
+fn io_error_to_entry_error(io_error: IoError) -> Result<EntryErrorKind, IoErrorKind> {
+    match io_error.kind() {
+        IoErrorKind::NotFound => Ok(EntryErrorKind::NotFound),
+        // An intermediate path component was a plain file, not a directory
+        IoErrorKind::NotADirectory => Ok(EntryErrorKind::NotFound),
+        // A cyclic symbolic link chain was included in the provided path
+        IoErrorKind::FilesystemLoop => Ok(EntryErrorKind::NotFound),
+        IoErrorKind::PermissionDenied => Ok(EntryErrorKind::PermissionDenied),
+        err => Err(err),
+    }
+}
+
+impl std::fmt::Debug for EntryReadError {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
+        write!(f, "Error while attempting to read from file '{:?}': {:?}", self.path, self.error_kind)
+    }
+}
+impl std::fmt::Debug for EntryWriteError {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
+        write!(f, "Error while attempting to write to file '{:?}': {:?}", self.path, self.error_kind)
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index deea25c..38fb5ee 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,10 +1,16 @@
 #![feature(io_error_more)]
+#![feature(never_type)]
 
+mod entry;
 mod error;
-pub use error::*;
-
 mod operations;
+
+pub use entry::*;
+pub use error::*;
 pub use operations::*;
 
-mod entry;
-pub use entry::{Entry, EntryType};
+pub use std::path::{Path, PathBuf};
+
+pub type ReadResult<T> = Result<T, EntryReadError>;
+pub type WriteResult<T> = Result<T, EntryWriteError>;
+
diff --git a/src/operations.rs b/src/operations.rs
index 54ce8c7..dd0a132 100644
--- a/src/operations.rs
+++ b/src/operations.rs
@@ -1,30 +1,25 @@
 use crate::*;
-use std::path::Path;
-
-mod ls;
-pub use ls::*;
 
 mod cp;
-pub use cp::*;
-
+mod ls;
+mod mkdir;
 mod rm;
-pub use rm::*;
 
-mod mkdir;
+pub use cp::*;
+pub use ls::*;
 pub use mkdir::*;
+pub use rm::*;
 
-pub fn append_to_file<P>(_path: P, _content: &str) -> Result<(), EntryWriteError>
-where
-    P: AsRef<Path>,
-{
+/// Append bytes to the end of a file.
+#[must_use]
+pub fn append_to_file(_path: impl AsRef<Path>, _content: &str) -> WriteResult<()> {
     unimplemented!()
 }
 
-pub fn write_to_file<P>(path: P, content: &str) -> Result<(), EntryWriteError>
-where
-    P: AsRef<Path>,
-{
+/// Write a slice of bytes to a file, overwriting the existing file if it already exists.
+#[must_use]
+pub fn write_to_file(path: impl AsRef<Path>, content: impl AsRef<[u8]>) -> WriteResult<()> {
     make_parent_directory(&path)?;
-    std::fs::write(&path, content)?;
-    Ok(())
+    let write_result = std::fs::write(&path, content);
+    Ok(io_result_to_write_result(write_result, path.as_ref())?)
 }
diff --git a/src/operations/cp.rs b/src/operations/cp.rs
index be43826..27be3ce 100644
--- a/src/operations/cp.rs
+++ b/src/operations/cp.rs
@@ -1,62 +1,55 @@
-use crate::make_parent_directory;
-use crate::{get_entry, get_optional_entry, list_directory};
-use crate::{EntryType, EntryWriteError};
-use std::path::Path;
+use crate::*;
 
-pub fn copy<P, Q>(source_path: P, target_path: Q) -> Result<(), EntryWriteError>
-where
-    P: AsRef<Path>,
-    Q: AsRef<Path>,
-{
+#[must_use]
+pub fn copy(source_path: impl AsRef<Path>, destination_path: impl AsRef<Path>) -> WriteResult<()> {
     let source = get_entry(&source_path)?;
-    let target = get_optional_entry(&target_path)?;
-    let target_type = target.and_then(|e| Some(e.entry_type));
+    let destination = get_optional_entry(&destination_path)?;
+    let destination_type = destination.and_then(|e| Some(e.entry_type));
 
-    match (source.entry_type, target_type) {
+    match (source.entry_type, destination_type) {
         (EntryType::File, Some(EntryType::File)) => {
-            copy_file(source_path, target_path)?;
+            copy_file(source_path, destination_path)?;
         }
         (EntryType::File, Some(EntryType::Directory)) => {
-            let target_path = target_path.as_ref().join(source.name);
-            copy_file(source_path, target_path)?;
+            let destination_path = destination_path.as_ref().join(source.name);
+            copy_file(source_path, destination_path)?;
         }
         (EntryType::File, None) => {
-            make_parent_directory(&target_path)?;
-            copy_file(source_path, target_path)?;
+            make_parent_directory(&destination_path)?;
+            copy_file(&source_path, &destination_path)?;
         }
         (EntryType::Directory, Some(EntryType::File)) => {
-            std::fs::remove_file(&target_path)?;
-            copy_directory(&source_path, &target_path)?;
+            // The file is replaced by a copy of the source directory
+            remove_file(&destination_path)?;
+            copy_directory(&source_path, &destination_path)?;
         }
         (EntryType::Directory, Some(EntryType::Directory)) => {
-            let target_path = target_path.as_ref().join(source.name);
-            copy_directory(source_path, target_path)?;
+            // The destination is filled with the contents of the source directory
+            for entry in list_directory(source_path)? {
+                let destination_path = destination_path.as_ref().join(&entry.name);
+                copy_file(entry.path, destination_path)?;
+            }
         }
         (EntryType::Directory, None) => {
-            make_parent_directory(&target_path)?;
-            copy_directory(source_path, target_path)?;
+            // The destination is created as a copy of the source directory
+            copy_directory(&source_path, &destination_path)?;
         }
     }
     Ok(())
 }
 
-fn copy_file<P, Q>(source_path: P, target_path: Q) -> Result<(), EntryWriteError>
-where
-    P: AsRef<Path>,
-    Q: AsRef<Path>,
-{
-    std::fs::copy(source_path, target_path)?;
+#[must_use]
+fn copy_file(source_path: impl AsRef<Path>, destination_path: impl AsRef<Path>) -> WriteResult<()> {
+    let copy_result = std::fs::copy(&source_path, &destination_path);
+    io_result_to_write_result(copy_result, &destination_path.as_ref())?;
     Ok(())
 }
 
-fn copy_directory<P, Q>(source_path: P, target_path: Q) -> Result<(), EntryWriteError>
-where
-    P: AsRef<Path>,
-    Q: AsRef<Path>,
-{
+#[must_use]
+fn copy_directory(source_path: impl AsRef<Path>, destination_path: impl AsRef<Path>) -> WriteResult<()> {
     for entry in list_directory(&source_path)? {
-        let target_path = target_path.as_ref().join(entry.name);
-        copy(entry.path, &target_path)?;
+        let destination_path = destination_path.as_ref().join(entry.name);
+        copy(entry.path, &destination_path)?;
     }
     Ok(())
 }
diff --git a/src/operations/ls.rs b/src/operations/ls.rs
index 2d2da5d..9dd3258 100644
--- a/src/operations/ls.rs
+++ b/src/operations/ls.rs
@@ -1,31 +1,29 @@
-use crate::{Entry, EntryReadError, EntryType};
-use std::path::Path;
+use crate::*;
 
-pub fn get_entry<P>(path: P) -> Result<Entry, EntryReadError>
-where
-    P: AsRef<Path>,
-{
+/// Convert a file path into an [Entry].
+pub fn get_entry(path: impl AsRef<Path>) -> ReadResult<Entry> {
     Entry::from_path(path)
 }
 
-pub fn get_optional_entry<P>(path: P) -> Result<Option<Entry>, EntryReadError>
-where
-    P: AsRef<Path>,
-{
+/// Convert a file path that might not be valid into an [Entry]. This will return
+/// as [Option::None] instead of [EntryReadError::NotFound] if the file doesn't exist.
+pub fn get_optional_entry(path: impl AsRef<Path>) -> ReadResult<Option<Entry>> {
     match get_entry(path) {
         Ok(e) => Ok(Some(e)),
-        Err(EntryReadError::NotFound) => Ok(None),
+        Err(EntryReadError { error_kind: EntryErrorKind::NotFound, .. }) => Ok(None),
         Err(other) => Err(other),
     }
 }
 
-pub fn list_directory<P>(path: P) -> Result<Vec<Entry>, EntryReadError>
-where
-    P: AsRef<Path>,
-{
+/// Get an [Entry] for every file and subdirectory within a directory.
+pub fn list_directory(path: impl AsRef<Path>) -> ReadResult<Vec<Entry>>{
+    let path = path.as_ref();
+    macro_rules! raise {
+        ($err:expr) => {io_result_to_read_result($err, path)?}; }
+
     let mut entries = Vec::new();
-    for dir_entry in std::fs::read_dir(path)? {
-        let entry = match Entry::from_path(&dir_entry?.path()) {
+    for dir_entry in raise!(std::fs::read_dir(path)) {
+        let entry = match Entry::from_path(&raise!(dir_entry).path()) {
             Ok(v) => v,
             Err(_) => continue,
         };
@@ -34,12 +32,9 @@ where
     return Ok(entries);
 }
 
-/// Recursively descend into a directory and all sub-directories,
-/// returning an [`Entry`](struct.Entry.html) for each discovered file.
-pub fn traverse_directory<P>(path: P) -> Result<Vec<Entry>, EntryReadError>
-where
-    P: AsRef<Path>,
-{
+/// Recursively descend into a directory and all sub-directories, returning an
+/// [Entry] for each discovered file.
+pub fn traverse_directory(path: impl AsRef<Path>) -> ReadResult<Vec<Entry>> {
     let mut file_entries = Vec::new();
     for entry in list_directory(path)? {
         match entry.entry_type {
diff --git a/src/operations/mkdir.rs b/src/operations/mkdir.rs
index 011b8cf..9b2abd2 100644
--- a/src/operations/mkdir.rs
+++ b/src/operations/mkdir.rs
@@ -1,18 +1,15 @@
-use crate::EntryWriteError;
-use std::path::Path;
+use crate::*;
 
-pub fn make_directory<P>(path: P) -> Result<(), EntryWriteError>
-where
-    P: AsRef<Path>,
-{
-    std::fs::DirBuilder::new().recursive(true).create(path)?;
-    Ok(())
+/// Create a new directory and all parent directories.
+#[must_use]
+pub fn make_directory(path: impl AsRef<Path>) -> WriteResult<()> {
+    let make_result = std::fs::DirBuilder::new().recursive(true).create(&path);
+    Ok(io_result_to_write_result(make_result, &path.as_ref())?)
 }
 
-pub fn make_parent_directory<P>(path: P) -> Result<(), EntryWriteError>
-where
-    P: AsRef<Path>,
-{
+/// Create the parent directory of a path.
+#[must_use]
+pub fn make_parent_directory(path: impl AsRef<Path>) -> WriteResult<()> {
     match path.as_ref().parent() {
         Some(parent) => make_directory(parent),
         None => Ok(()),
diff --git a/src/operations/rm.rs b/src/operations/rm.rs
index 846a094..bf206a5 100644
--- a/src/operations/rm.rs
+++ b/src/operations/rm.rs
@@ -1,23 +1,17 @@
-use crate::EntryWriteError;
-use crate::{get_entry, EntryType};
-use std::path::Path;
+use crate::*;
 
-pub fn remove<P>(path: P) -> Result<(), EntryWriteError>
-where
-    P: AsRef<Path>,
-{
+#[must_use]
+pub fn remove(path: impl AsRef<Path>) -> WriteResult<()> {
     let entry = get_entry(&path)?;
-    match entry.entry_type {
-        EntryType::File => std::fs::remove_file(&path)?,
-        EntryType::Directory => std::fs::remove_dir_all(&path)?,
-    }
-    Ok(())
+    let remove_result = match entry.entry_type {
+        EntryType::File => std::fs::remove_file(&path),
+        EntryType::Directory => std::fs::remove_dir_all(&path),
+    };
+    Ok(io_result_to_write_result(remove_result, path.as_ref())?)
 }
 
-pub fn remove_file<P>(path: P) -> Result<(), EntryWriteError>
-where
-    P: AsRef<Path>,
-{
-    std::fs::remove_file(path)?;
-    Ok(())
+#[must_use]
+pub fn remove_file(path: impl AsRef<Path>) -> WriteResult<()> {
+    let remove_result = std::fs::remove_file(&path);
+    Ok(io_result_to_write_result(remove_result, &path.as_ref())?)
 }
-- 
cgit v1.2.3-70-g09d2