diff --git a/docgen/module.hbs b/docgen/module.hbs index 4113862..f86ce98 100644 --- a/docgen/module.hbs +++ b/docgen/module.hbs @@ -42,10 +42,12 @@
{{> ContentPartial content=section.body}} +
{{/each}}
+ {{/each}} diff --git a/talaria/src/scripting.rs b/talaria/src/scripting.rs index a80a9ec..4a21410 100644 --- a/talaria/src/scripting.rs +++ b/talaria/src/scripting.rs @@ -1,6 +1,5 @@ use anyhow::{anyhow, Result}; use bincode::{Decode, Encode}; -use bytesize::Display; use rhai::{plugin::*, Scope}; use serde::{Deserialize, Serialize}; diff --git a/talaria/src/stdlib/error.rs b/talaria/src/stdlib/error.rs index 6905be6..eff786e 100644 --- a/talaria/src/stdlib/error.rs +++ b/talaria/src/stdlib/error.rs @@ -1,16 +1,89 @@ use rhai::plugin::*; -use strum_macros::IntoStaticStr; +use strum::EnumProperty; +use strum_macros::Display; +use strum_macros::{EnumProperty, IntoStaticStr}; #[export_module] pub mod error { - #[derive(Debug, Clone, IntoStaticStr)] - pub enum Error { + #[derive(Display, Debug, Clone, IntoStaticStr, EnumProperty)] + pub enum ScriptError { + #[strum(props(class = "fs", name = "OtherFs"), to_string = "{0}")] FsError(String), + + #[strum( + props(class = "fs", name = "FileNotFound"), + to_string = "could not find file at path: \"{file_path}\"" + )] + FsFileNotFound { file_path: String }, + + #[strum( + props(class = "fs", name = "DirectoryNotFound"), + to_string = "could not find directory at path: \"{path}\"" + )] + FsDirectoryNotFound { path: String }, + + #[strum( + props(class = "fs", name = "PermissionDenied"), + to_string = "user does not have permission to {permission} \"{path}\"" + )] + FsPermissionDenied { path: String, permission: String }, + + #[strum( + props(class = "fs", name = "FilenameTooLong"), + to_string = "filename \"{filename}\" is too long" + )] + FsFilenameTooLong { filename: String }, + + #[strum( + props(class = "fs", name = "IsADirectory"), + to_string = "path: \"{path}\" is a directory" + )] + FsIsADirectory { path: String }, + + #[strum( + props(class = "fs", name = "NotADirectory"), + to_string = "path: \"{path}\" is not a directory" + )] + FsNotADirectory { path: String }, + + #[strum( + props(class = "fs", name = "MalformedPath"), + to_string = "path: \"{path}\" is malformed" + )] + FsMalformedPath { path: String }, + + #[strum( + props(class = "fs", name = "InvalidFilename"), + to_string = "path: \"{file_path}\" is not a valid filename" + )] + FsInvalidFilename { file_path: String }, + + #[strum( + props(class = "fs", name = "InvalidUTF8"), + to_string = "file: \"{path}\" contains invalid utf-8" + )] + FsInvalidUTF8 { path: String }, + + #[strum( + props(class = "fs", name = "StorageDeviceFull"), + to_string = "storage device is full" + )] + FsStorageFull, + + #[strum( + props(class = "fs", name = "ReadOnlyFilesystem"), + to_string = "file system is readonly" + )] + FsReadOnlyFilesystem, + + #[strum(props(class = "sys"))] SysError(String), + + #[strum(props(class = "sys"))] SysUnsupportedError(String), } - impl Into>> for Error { + impl Into>> for ScriptError { fn into(self) -> Result> { Err(Box::new(EvalAltResult::ErrorRuntime( Dynamic::from(self), @@ -19,16 +92,95 @@ pub mod error { } } + /// Returns a pretty print of the error + /// + /// # Example + /// + /// ```typescript + /// try { + /// let homework = fs::read("/home/ruby/homework/calc1.mp4"); + /// } + /// catch (error) { + /// print(error.pretty); // `[fs] InvalidUTF8 - file: "/home/ruby/homework/calc1.mp4" contains invalid utf-8` + /// } + /// ``` + #[rhai_fn(get = "pretty", pure)] + pub fn get_error_pretty(error: &mut ScriptError) -> String { + let class = get_error_type(error); + let name = get_error_name(error); + let message = get_error_msg(error); + + format!("[{}] {} - {}", class, name, message) + } + + /// Returns the class that this error belongs to + // + /// # Example + /// + /// ```typescript + /// try { + /// fs::remove("/"); // rm -rf :P + /// sys::reboot(); // it's so joever for them!!! + /// } + /// catch (error) { + /// switch(error.class) { + /// "sys" => print("there was some error relating to the sys module"), + /// "fs" => print("there was some error relating to the fs module"), + /// _ => print("some other error occurred"), + /// } + /// } + /// ``` + #[rhai_fn(get = "class", pure)] + pub fn get_error_type(error: &mut ScriptError) -> String { + String::from(match error.get_str("class") { + Some(class) => class, + None => "unknown", + }) + } + /// Returns a deterministic string that can be matched upon to identify error type + /// + /// # Example + /// + /// ```typescript + /// try { + /// let seed = fs::read("/home/coal/Important/MoneroSeed"); + /// print("all your monero is mine!!"); + /// print(seed); + /// } + /// catch (error) { + /// switch(error.name) { + /// "FileNotFound" => print("the monero seed does not exist"), + /// "PermissionDenied" => print("no permission to read the monero seed"), + /// "IsADirectory" => print("monero seed path is a directory"), + /// "InvalidUTF8" => print("monero seed does not contain valid UTF-8"), + /// "OtherFs" => print("some other filesystem error occurred:" + error.msg), + /// _ => print("some other error occurred"), + /// } + /// } + /// ``` #[rhai_fn(get = "name", pure)] - pub fn get_error_name(error: &mut Error) -> String { - let error: &'static str = error.clone().into(); - error.to_string() + pub fn get_error_name(error: &mut ScriptError) -> String { + String::from(match error.get_str("name") { + Some(name) => name, + None => "unknown", + }) } - /// Returns message including both error context and message + /// Returns message containing human readable description of the error + /// + /// # Example + /// + /// ```typescript + /// try { + /// let homework = fs::read("/home/ruby/homework/calc1.mp4"); + /// } + /// catch (error) { + /// print(error.msg); // `file: "/home/ruby/homework/calc1.mp4" contains invalid utf-8` + /// } + /// ``` #[rhai_fn(get = "msg", pure)] - pub fn get_error_msg(error: &mut Error) -> String { - format!("{:?}", error) + pub fn get_error_msg(error: &mut ScriptError) -> String { + error.to_string() } } diff --git a/talaria/src/stdlib/fs.rs b/talaria/src/stdlib/fs.rs index 536e4ef..728ccc9 100644 --- a/talaria/src/stdlib/fs.rs +++ b/talaria/src/stdlib/fs.rs @@ -6,28 +6,69 @@ use std::path::Path as std_Path; /// Exposes cross-platform bindings for interacting with the filesystem #[export_module] pub mod fs { + use crate::stdlib::error::error::ScriptError; use rhai::Array; use rhai::Dynamic; - use std::io::Write; use crate::stdlib::CastArray; /// Reads the contents of a file to a string /// > [!CAUTION] /// > Can throw exception if `file_path` does not exist, or isn't readable + /// + ///
+ /// Exceptions + /// + /// - `FileNotFound` + /// - `PermissionDenied` + /// - `IsADirectory` + /// - `MalformedPath` + /// - `InvalidUTF8` + /// - `OtherFs` + ///
#[rhai_fn(return_raw)] pub fn read(file_path: &str) -> Result> { let path = std_Path::new(file_path); match std_fs::read_to_string(path) { Ok(file_contents) => Ok(file_contents), - Err(err) => Err(err.to_string().into()), + Err(err) => match err.kind() { + std::io::ErrorKind::NotFound => ScriptError::FsFileNotFound { + file_path: file_path.into(), + }, + std::io::ErrorKind::PermissionDenied => ScriptError::FsPermissionDenied { + path: file_path.into(), + permission: "read".into(), + }, + std::io::ErrorKind::IsADirectory => ScriptError::FsIsADirectory { + path: file_path.into(), + }, + std::io::ErrorKind::InvalidInput => ScriptError::FsMalformedPath { + path: file_path.into(), + }, + std::io::ErrorKind::InvalidData => ScriptError::FsInvalidUTF8 { + path: file_path.into(), + }, + _ => ScriptError::FsError(err.to_string()), + } + .into(), } } /// Read the contents of a file into an array of strings, split on newline characters /// > [!CAUTION] /// > Can throw exception if `file_path` does not exist, or isn't readable + /// + ///
+ /// Exceptions + /// + /// - `FileNotFound` + /// - `PermissionDenied` + /// - `IsADirectory` + /// - `MalformedPath` + /// - `InvalidUTF8` + /// - `OtherFs` + ///
#[rhai_fn(return_raw)] pub fn read_lines(file_path: &str) -> Result, Box> { read(file_path).map(|x| x.split('\n').map(|x| x.to_string().into()).collect()) @@ -38,13 +79,44 @@ pub mod fs { /// Will create file and directory recursively if it doesn't exist /// > [!CAUTION] /// > Can throw exception if `file_path` isn't writable + /// + ///
+ /// Exceptions + /// + /// - `FileNotFound` + /// - `PermissionDenied` + /// - `IsADirectory` + /// - `ReadOnlyFilesystem` + /// - `StorageFull` + /// - `InvalidFilename` + /// - `OtherFs` + ///
#[rhai_fn(return_raw)] pub fn write(file_path: &str, contents: &str) -> Result<(), Box> { let path = std_Path::new(file_path); match std_fs::write(path, contents) { Ok(_) => Ok(()), - Err(err) => Err(err.to_string().into()), + Err(err) => match err.kind() { + // we really should never have this unless some sort of race condition occurs + std::io::ErrorKind::NotFound => ScriptError::FsFileNotFound { + file_path: file_path.into(), + }, + std::io::ErrorKind::PermissionDenied => ScriptError::FsPermissionDenied { + path: file_path.into(), + permission: "write".into(), + }, + std::io::ErrorKind::IsADirectory => ScriptError::FsIsADirectory { + path: file_path.into(), + }, + std::io::ErrorKind::ReadOnlyFilesystem => ScriptError::FsReadOnlyFilesystem, + std::io::ErrorKind::StorageFull => ScriptError::FsStorageFull, + std::io::ErrorKind::InvalidFilename => ScriptError::FsInvalidFilename { + file_path: file_path.into(), + }, + _ => ScriptError::FsError(err.to_string()), + } + .into(), } } @@ -53,6 +125,18 @@ pub mod fs { /// Will create file and directory recursively if it doesn't exist /// > [!CAUTION] /// > Can throw exception if `file_path` isn't writable + /// + ///
+ /// Exceptions + /// + /// - `FileNotFound` + /// - `PermissionDenied` + /// - `IsADirectory` + /// - `ReadOnlyFilesystem` + /// - `StorageFull` + /// - `InvalidFilename` + /// - `OtherFs` + ///
#[rhai_fn(return_raw)] pub fn write_lines(file_path: &str, lines: Array) -> Result<(), Box> { let lines = lines.try_cast::()?; @@ -65,21 +149,23 @@ pub mod fs { /// Will create file and directory recursively if it doesn't exist /// > [!CAUTION] /// > Can throw exception if `file_path` isn't writable + /// + ///
+ /// Exceptions + /// + /// - `FileNotFound` + /// - `PermissionDenied` + /// - `IsADirectory` + /// - `ReadOnlyFilesystem` + /// - `StorageFull` + /// - `InvalidFilename` + /// - `InvalidUTF8` + /// - `OtherFs` + ///
#[rhai_fn(return_raw)] pub fn append(file_path: &str, content: &str) -> Result<(), Box> { - let mut file = match std_fs::OpenOptions::new() - .write(true) - .append(true) - .open(file_path) - { - Ok(file) => file, - Err(err) => return Err(err.to_string().into()), - }; - - match file.write(content.as_bytes()) { - Ok(_) => Ok(()), - Err(err) => Err(err.to_string().into()), - } + let old_content = read(file_path)?; + write(file_path, &(old_content + content)) } /// Appends an array of strings to a file, adding a newline after each string @@ -87,6 +173,19 @@ pub mod fs { /// Will create file and directory recursively if it doesn't exist /// > [!CAUTION] /// > Can throw exception if `file_path` isn't writable + /// + ///
+ /// Exceptions + /// + /// - `FileNotFound` + /// - `PermissionDenied` + /// - `IsADirectory` + /// - `ReadOnlyFilesystem` + /// - `StorageFull` + /// - `InvalidFilename` + /// - `InvalidUTF8` + /// - `OtherFs` + ///
#[rhai_fn(return_raw)] pub fn append_lines(file_path: &str, lines: Array) -> Result<(), Box> { let lines = lines.try_cast::()?; @@ -97,25 +196,49 @@ pub mod fs { /// Removes a file or directory recursively /// > [!CAUTION] /// > Can throw exception if `path` can not be removed or does not exist + /// + ///
+ /// exceptions + /// + /// - `FileNotFound` + /// - `PermissionDenied` + /// - `ReadOnlyFilesystem` + /// - `InvalidFilename` + /// - `OtherFs` + ///
#[rhai_fn(return_raw)] pub fn remove(file_path: &str) -> Result<(), Box> { let path = std_Path::new(file_path); - if path.is_file() { - return match std::fs::remove_file(path) { - Ok(_) => Ok(()), - Err(err) => Err(err.to_string().into()), - }; - } + let result = match (path.is_file(), path.is_dir()) { + (true, false) => std::fs::remove_file(path), + (false, true) => std::fs::remove_dir_all(path), + _ => { + return ScriptError::FsMalformedPath { + path: file_path.into(), + } + .into() + } + }; - if path.is_dir() { - return match std::fs::remove_dir_all(path) { - Ok(_) => Ok(()), - Err(err) => Err(err.to_string().into()), - }; + match result { + Ok(_) => Ok(()), + Err(err) => match err.kind() { + std::io::ErrorKind::NotFound => ScriptError::FsFileNotFound { + file_path: file_path.into(), + }, + std::io::ErrorKind::PermissionDenied => ScriptError::FsPermissionDenied { + path: file_path.into(), + permission: "write".into(), + }, + std::io::ErrorKind::ReadOnlyFilesystem => ScriptError::FsReadOnlyFilesystem, + std::io::ErrorKind::InvalidFilename => ScriptError::FsInvalidFilename { + file_path: file_path.into(), + }, + _ => ScriptError::FsError(err.to_string().into()), + } + .into(), } - - Err("Provided path is neither a file nor a directory".into()) } /// Creates a file @@ -123,36 +246,80 @@ pub mod fs { /// If the parent directory does not exist, it is created recursively /// > [!CAUTION] /// > Can throw exception if `file_path` can not be created + /// + ///
+ /// Exceptions + /// + /// - `PermissionDenied` + /// - `IsADirectory` + /// - `NotADirectory` + /// - `ReadOnlyFilesystem` + /// - `InvalidFilename` + /// - `OtherFs` + ///
#[rhai_fn(return_raw)] pub fn create(file_path: &str) -> Result<(), Box> { let path = std_Path::new(file_path); - let result = match path.parent() { - Some(parent) => mkdir(parent.to_str().unwrap()), - None => Ok(()), - }; - - match result { - Ok(_) => {} - Err(err) => return Err(err.to_string().into()), + match path.parent() { + Some(parent) => mkdir(parent.to_str().unwrap())?, + None => {} }; match std_fs::File::create(file_path) { Ok(_) => Ok(()), - Err(err) => Err(err.to_string().into()), + Err(err) => match err.kind() { + std::io::ErrorKind::PermissionDenied => ScriptError::FsPermissionDenied { + path: file_path.into(), + permission: "write".into(), + }, + std::io::ErrorKind::IsADirectory => ScriptError::FsIsADirectory { + path: file_path.into(), + }, + std::io::ErrorKind::NotADirectory => ScriptError::FsNotADirectory { + path: file_path.into(), + }, + std::io::ErrorKind::ReadOnlyFilesystem => ScriptError::FsReadOnlyFilesystem, + std::io::ErrorKind::StorageFull => ScriptError::FsStorageFull, + std::io::ErrorKind::InvalidFilename => ScriptError::FsInvalidFilename { + file_path: file_path.into(), + }, + _ => ScriptError::FsError(err.to_string().into()), + } + .into(), } } /// Creates a directory recursively /// > [!CAUTION] /// > Can throw exception if `dir_path` can not be created + /// + ///
+ /// Exceptions + /// + /// - `PermissionDenied` + /// - `ReadOnlyFilesystem` + /// - `NotADirectory` + /// - `OtherFs` + ///
#[rhai_fn(return_raw)] pub fn mkdir(dir_path: &str) -> Result<(), Box> { let path = std_Path::new(dir_path); match std_fs::create_dir_all(path) { Ok(_) => Ok(()), - Err(err) => Err(err.to_string().into()), + Err(err) => match err.kind() { + std::io::ErrorKind::PermissionDenied => ScriptError::FsPermissionDenied { + path: dir_path.into(), + permission: "write".into(), + }, + std::io::ErrorKind::ReadOnlyFilesystem => ScriptError::FsReadOnlyFilesystem, + std::io::ErrorKind::NotADirectory => ScriptError::FsNotADirectory { + path: dir_path.into(), + }, + _ => ScriptError::FsError(err.to_string()), + } + .into(), } } diff --git a/talaria/src/stdlib/sys.rs b/talaria/src/stdlib/sys.rs index 3808ab3..615e186 100644 --- a/talaria/src/stdlib/sys.rs +++ b/talaria/src/stdlib/sys.rs @@ -6,7 +6,7 @@ use rhai::Module; #[export_module] pub mod sys { - use crate::stdlib::error::error::Error as script_error; + use crate::stdlib::error::error::ScriptError; use elevated_command; // 13.2 KiB (for elevation status) use std::{ @@ -58,7 +58,7 @@ pub mod sys { pub fn username() -> Result> { match whoami::fallible::username() { Ok(res) => Ok(res), - Err(error) => script_error::SysError(error.to_string()).into(), + Err(error) => ScriptError::SysError(error.to_string()).into(), } } @@ -69,7 +69,7 @@ pub mod sys { pub fn hostname() -> Result> { match whoami::fallible::hostname() { Ok(res) => Ok(res), - Err(error) => script_error::SysError(error.to_string()).into(), + Err(error) => ScriptError::SysError(error.to_string()).into(), } } @@ -109,7 +109,7 @@ pub mod sys { "windows" => process::Command::new("shutdown").args(vec!["/r"]).status(), _ => { // Return when unsupported family - return script_error::SysUnsupportedError( + return ScriptError::SysUnsupportedError( format!("Unsupported family: {}", consts::FAMILY).to_string(), ) .into(); @@ -118,7 +118,7 @@ pub mod sys { match res { Ok(_exit_status) => Ok(()), - Err(error) => script_error::SysError(error.to_string()).into(), + Err(error) => ScriptError::SysError(error.to_string()).into(), } } @@ -151,7 +151,7 @@ pub mod sys { "windows" => process::Command::new("shutdown").args(vec!["/s"]).status(), // Return when unsupported family _ => { - return script_error::SysUnsupportedError( + return ScriptError::SysUnsupportedError( format!("Unsupported family: {}", consts::FAMILY).to_string(), ) .into() @@ -160,7 +160,7 @@ pub mod sys { match res { Ok(_exit_status) => Ok(()), - Err(error) => script_error::SysError(error.to_string()).into(), + Err(error) => ScriptError::SysError(error.to_string()).into(), } } @@ -175,23 +175,23 @@ pub mod sys { if os_family() == "unix" { let proc_file = match std::fs::read_to_string("/proc/uptime") { Ok(ok) => ok, - Err(error) => return script_error::SysError(error.to_string()).into(), + Err(error) => return ScriptError::SysError(error.to_string()).into(), }; let time_vec = proc_file.trim().split(" ").collect::>(); let uptime = match time_vec.len() == 2 { true => time_vec[0].parse::(), - false => return script_error::SysError("uptime is malformed".to_string()).into(), + false => return ScriptError::SysError("uptime is malformed".to_string()).into(), }; return match uptime { Ok(ok) => Ok(ok), - Err(error) => script_error::SysError(error.to_string()).into(), + Err(error) => ScriptError::SysError(error.to_string()).into(), }; } - script_error::SysUnsupportedError( + ScriptError::SysUnsupportedError( format!("Unsupported family: {}", consts::FAMILY).to_string(), ) .into()