diff --git a/README.md b/README.md index 8db566aa6d8153b1aba81156fe9574d6f9dc5682..6c1aca46cba735bf8db84d538edae008a67f37b3 100644 --- a/README.md +++ b/README.md @@ -105,3 +105,14 @@ It can be tested out by navigating to a directory within a git repository, and r ```ion echo Current Branch: ${git::branch} ``` + +# Vim/NeoVim Syntax Highlighting Plugin + +We do have an [officially-supported syntax highlighting plugin](https://github.com/vmchale/ion-vim) for all the +vim/nvim users out there. + +```vimscript +Plugin 'vmchale/ion-vim' +``` + + diff --git a/src/shell/plugins/library_iter/mod.rs b/src/shell/plugins/library_iter/mod.rs index eb8366e2478a6d6c9176126912f5c9cf3905bf28..f09ab7d394d5c9cab5d0e3b199870bd87fc2d1a6 100644 --- a/src/shell/plugins/library_iter/mod.rs +++ b/src/shell/plugins/library_iter/mod.rs @@ -2,8 +2,7 @@ use libloading::Library; use std::fs::ReadDir; use types::Identifier; - -/// Grabs `Library` entries found within a given directory +/// Grabs all `Library` entries found within a given directory pub struct LibraryIterator { directory: ReadDir, } @@ -13,13 +12,17 @@ impl LibraryIterator { } impl Iterator for LibraryIterator { + // The `Identifier` is the name of the namespace for which values may be pulled. + // The `Library` is a handle to dynamic library loaded into memory. type Item = (Identifier, Library); fn next(&mut self) -> Option<(Identifier, Library)> { while let Some(entry) = self.directory.next() { let entry = if let Ok(entry) = entry { entry } else { continue }; let path = entry.path(); + // An entry is a library if it is a file with a 'so' extension. if path.is_file() && path.extension().map_or(false, |ext| ext == "so") { + // The identifier will be the file name of that file, without the extension. let identifier = match path.file_stem().unwrap().to_str() { Some(filename) => Identifier::from(filename), None => { @@ -28,6 +31,7 @@ impl Iterator for LibraryIterator { } }; + // This will attempt to load the library into memory. match Library::new(path.as_os_str()) { Ok(library) => return Some((identifier, library)), Err(why) => { diff --git a/src/shell/plugins/namespaces/mod.rs b/src/shell/plugins/namespaces/mod.rs index a5d9420ea86b441d23d3a66f3855a3c5b5e8e41c..90040d19d659b0f38328c92e9a95181953c83535 100644 --- a/src/shell/plugins/namespaces/mod.rs +++ b/src/shell/plugins/namespaces/mod.rs @@ -7,3 +7,57 @@ pub use self::redox::*; mod unix; #[cfg(all(unix, not(target_os = "redox")))] pub use self::unix::*; + +use std::ffi::CString; +use std::fmt::{self, Display, Formatter}; +use std::io; +use types::Identifier; + +#[repr(C)] +#[derive(Debug)] +/// The foregein structure returned when executing a namespace plugin function. +/// +/// This structure is a direct equivalent to `Option<CString>`. If the dynamic library from which +/// this result was returned was written in Rust, then the `data` pointer contained within was +/// created by calling `string.into_raw()` on a `CString`. In order to prevent a memory leak, this +/// structure should immediately be converted back into a `CString` by calling +/// `CString::from_raw()`. +struct NamespaceResult { + exists: bool, + data: *mut i8, +} + +impl NamespaceResult { + /// Converts the non-native structure into a proper, native Rust equivalent. + /// The `exists` field indicates whether the `data` field was initialized or not. + /// The `data` pointer is converted back into a native `CString` with `CString::from_raw()`. + fn into_option(self) -> Option<CString> { + if self.exists { Some(unsafe { CString::from_raw(self.data) }) } else { None } + } +} + +#[derive(Debug)] +/// A possible error that can be caused when attempting to obtain or execute a +/// function within a given namespace. +pub enum NamespaceError { + /// This occurs when a symbol could not be loaded from the library in question. It is an + /// error that infers that the problem is with the plugin, not Ion itself. + SymbolErr(io::Error), + /// Function names must be valid UTF-8. If they aren't something's wrong with the plugin. + UTF8Function, + /// The result from a plugin must be valid UTF-8. If it isn't, the plugin's bad. + UTF8Result, + /// This infers that the user called a function that doesn't exist in the library. Bad user, bad. + FunctionMissing(Identifier), +} + +impl Display for NamespaceError { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match *self { + NamespaceError::SymbolErr(ref error) => write!(f, "symbol error: {}", error), + NamespaceError::UTF8Function => write!(f, "function has invalid UTF-8 name"), + NamespaceError::UTF8Result => write!(f, "result is not valid UTF-8"), + NamespaceError::FunctionMissing(ref func) => write!(f, "{} doesn't exist in namespace", func), + } + } +} diff --git a/src/shell/plugins/namespaces/redox.rs b/src/shell/plugins/namespaces/redox.rs index aafb46ee55d33a38b2e1dcb97151100bf288c207..21336015591204605bfde03e6e3bb6909ee8a797 100644 --- a/src/shell/plugins/namespaces/redox.rs +++ b/src/shell/plugins/namespaces/redox.rs @@ -1,39 +1,5 @@ -use std::ffi::CString; -use std::fmt::{self, Display, Formatter}; -use std::io; use types::Identifier; - -#[derive(Debug)] -pub enum NamespaceError { - SymbolErr(io::Error), - UTF8Function, - UTF8Result, - FunctionMissing(Identifier), -} - -impl Display for NamespaceError { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match *self { - NamespaceError::SymbolErr(ref error) => write!(f, "symbol error: {}", error), - NamespaceError::UTF8Function => write!(f, "function has invalid UTF-8 name"), - NamespaceError::UTF8Result => write!(f, "result is not valid UTF-8"), - NamespaceError::FunctionMissing(func) => write!(f, "{} doesn't exist in namespace", func), - } - } -} - -#[repr(C)] -#[derive(Debug)] -struct NamespaceResult { - exists: bool, - data: *mut i8, -} - -impl NamespaceResult { - fn into_option(self) -> Option<CString> { - if self.exists { Some(unsafe { CString::from_raw(self.data) }) } else { None } - } -} +use super::NamespaceError; pub struct StringNamespace {} diff --git a/src/shell/plugins/namespaces/unix.rs b/src/shell/plugins/namespaces/unix.rs index 2ec1804daf7964e2b2f6d492ea16336ca702cb03..4d6ffec3323e56c2018fc5194bb80db054ee4d7a 100644 --- a/src/shell/plugins/namespaces/unix.rs +++ b/src/shell/plugins/namespaces/unix.rs @@ -1,52 +1,27 @@ use super::super::{LibraryIterator, config_dir}; +use super::{NamespaceError, NamespaceResult}; use fnv::FnvHashMap; use libloading::{Library, Symbol}; use libloading::os::unix::Symbol as RawSymbol; -use std::ffi::CString; -use std::fmt::{self, Display, Formatter}; use std::fs::read_dir; -use std::io; use std::slice; use std::str; use types::Identifier; -#[derive(Debug)] -pub enum NamespaceError { - SymbolErr(io::Error), - UTF8Function, - UTF8Result, - FunctionMissing(Identifier), -} - -impl Display for NamespaceError { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match *self { - NamespaceError::SymbolErr(ref error) => write!(f, "symbol error: {}", error), - NamespaceError::UTF8Function => write!(f, "function has invalid UTF-8 name"), - NamespaceError::UTF8Result => write!(f, "result is not valid UTF-8"), - NamespaceError::FunctionMissing(ref func) => write!(f, "{} doesn't exist in namespace", func), - } - } -} - -#[repr(C)] -#[derive(Debug)] -struct NamespaceResult { - exists: bool, - data: *mut i8, -} - -impl NamespaceResult { - fn into_option(self) -> Option<CString> { - if self.exists { Some(unsafe { CString::from_raw(self.data) }) } else { None } - } -} - +/// A dynamically-loaded string namespace from an external library. +/// +/// The purpose of the structure is to: A) hold a `Library` handle to the dynamically-loaded +/// plugin to ensure that the plugin remains loaded in memory, and it's contained symbols +/// remain validly-executable; and B) holds a map of functions that may be executed within +/// the namespace. pub struct StringNamespace { - /// Do not remove this field, as it ensures that the library remains loaded. + /// This field, although never used directly, is required to exist in order to ensure + /// that each element in the `symbols` field remains relevant. When Rust can support + /// self-referencing lifetimes, we won't need to hold raw symbols anymore. #[allow(dead_code)] library: Library, /// A hash map of symbols collected from the `Library` stored in the `library` field. + /// These are considered raw because they have their lifetimes erased. symbols: FnvHashMap<Identifier, RawSymbol<unsafe extern "C" fn() -> NamespaceResult>>, } @@ -55,10 +30,13 @@ impl StringNamespace { unsafe { let mut symbols = FnvHashMap::default(); { + // The `index` function contains a list of functions provided by the library. let index: Symbol<unsafe extern "C" fn() -> *const u8> = library.get(b"index\0").map_err(NamespaceError::SymbolErr)?; let symbol_list = index(); + // Yet we need to convert the raw stream of binary into a native slice if we + // want to properly reason about the contents of said aforementioned stream. let (mut start, mut counter) = (0, 0usize); let symbol_list: &[u8] = { let mut byte = *symbol_list.offset(0); @@ -68,25 +46,36 @@ impl StringNamespace { } slice::from_raw_parts(symbol_list, counter) }; - counter = 0; + + // Each function symbol is delimited by spaces, so this will slice our + // newly-created byte slice on each space, and then attempt to load and + // store that symbol for future use. for &byte in symbol_list { if byte == b' ' { if start == counter { start += 1; } else { + // Grab a slice and ensure that the name of the function is UTF-8. let slice = &symbol_list[start..counter]; let identifier = str::from_utf8(slice).map(Identifier::from).map_err(|_| { NamespaceError::UTF8Function })?; + + // To obtain the symbol, we need to create a new `\0`-ended byte array. + // TODO: There's no need to use a vector here. An array will do fine. let mut symbol = Vec::new(); symbol.reserve_exact(slice.len() + 1); symbol.extend_from_slice(slice); symbol.push(b'\0'); + + // Then attempt to load that symbol from the dynamic library. let symbol: Symbol<unsafe extern "C" fn() -> NamespaceResult> = library.get(symbol.as_slice()).map_err( NamespaceError::SymbolErr, )?; + + // And finally add the name of the function and it's function into the map. symbols.insert(identifier, symbol.into_raw()); start = counter + 1; } @@ -94,6 +83,8 @@ impl StringNamespace { counter += 1; } + // Identical to the logic in the loop above. Handles any unparsed stragglers that + // have been left over. if counter != start { let slice = &symbol_list[start..]; let identifier = str::from_utf8(slice).map(Identifier::from).map_err(|_| { @@ -110,10 +101,15 @@ impl StringNamespace { symbols.insert(identifier, symbol.into_raw()); } } + Ok(StringNamespace { library, symbols }) } } + /// Attempts to execute a function within a dynamically-loaded namespace. + /// + /// If the function exists, it is executed, and it's return value is then converted into a + /// proper Rusty type. pub fn execute(&self, function: Identifier) -> Result<Option<String>, NamespaceError> { let func = self.symbols.get(&function).ok_or( NamespaceError::FunctionMissing( @@ -134,6 +130,10 @@ impl StringNamespace { } } +/// Collects all dynamically-loaded namespaces and their associated symbols all at once. +/// +/// This function is meant to be called with `lazy_static` to ensure that there isn't a +/// cost to collecting all this information when the shell never uses it in the first place! pub fn collect() -> FnvHashMap<Identifier, StringNamespace> { let mut hashmap = FnvHashMap::default(); if let Some(mut path) = config_dir() { diff --git a/src/shell/variables.rs b/src/shell/variables.rs index 680885ca078f3558b28a61ef37ef73a23384d36c..688ff3da078beff09c2218e18d3fc560436bd839 100644 --- a/src/shell/variables.rs +++ b/src/shell/variables.rs @@ -145,10 +145,13 @@ impl Variables { pub fn get_var(&self, name: &str) -> Option<Value> { if let Some((name, variable)) = name.find("::").map(|pos| (&name[..pos], &name[pos + 2..])) { + // If the parsed name contains the '::' pattern, then a namespace was designated. Find it. match name { "env" => env::var(variable).map(Into::into).ok(), _ => { + // Attempt to obtain the given namespace from our lazily-generated map of namespaces. if let Some(namespace) = STRING_NAMESPACES.get(name.into()) { + // Attempt to execute the given function from that namespace, and map it's results. match namespace.execute(variable.into()) { Ok(value) => value.map(Into::into), Err(why) => { @@ -163,6 +166,7 @@ impl Variables { } } } else { + // Otherwise, it's just a simple variable name. self.variables.get(name).cloned().or_else(|| { env::var(name).map(Into::into).ok() })