diff --git a/README.md b/README.md index b3ca0a361a..c3255ab5d4 100644 --- a/README.md +++ b/README.md @@ -59,3 +59,8 @@ To-Do list: - File metadata - Compression? + +## Limitations + +- Currently `include_dir!()` does not support files/directories that cannot be represented as UTF-8. + This is also a limitation of `include_bytes!()` and `include_str!()` diff --git a/include_dir/src/dir.rs b/include_dir/src/dir.rs index db7c64211b..f138c5f5b1 100644 --- a/include_dir/src/dir.rs +++ b/include_dir/src/dir.rs @@ -1,75 +1,51 @@ use crate::file::File; use std::path::Path; +use crate::DirEntry; +use std::convert::TryInto; + /// A directory entry. #[derive(Debug, Copy, Clone, PartialEq)] pub struct Dir<'a> { - #[doc(hidden)] - pub path: &'a str, - #[doc(hidden)] - pub files: &'a [File<'a>], - #[doc(hidden)] - pub dirs: &'a [Dir<'a>], + path: &'a str, + entries: &'a [DirEntry<'a>] } impl<'a> Dir<'a> { - /// Get the directory's path. - pub fn path(&self) -> &'a Path { - Path::new(self.path) - } - /// Get a list of the files in this directory. - pub fn files(&self) -> &'a [File<'a>] { - self.files + /// Create a new [`Dir`] + pub const fn new(path: &'a str, entries: &'a [DirEntry<'_>]) -> Self { + Self { + path, + entries + } } - /// Get a list of the sub-directories inside this directory. - pub fn dirs(&self) -> &'a [Dir<'a>] { - self.dirs + /// The directory's path relative to the directory included with [include_dir!()] + pub fn path(&self) -> &Path { + Path::new(self.path) } - /// Does this directory contain `path`? - pub fn contains>(&self, path: S) -> bool { - let path = path.as_ref(); - - self.get_file(path).is_some() || self.get_dir(path).is_some() + /// Retrieve the entries within the directory + pub fn entries(&self) -> &[DirEntry<'_>] { + self.entries } - /// Fetch a sub-directory by *exactly* matching its path relative to the - /// directory included with `include_dir!()`. - pub fn get_dir>(&self, path: S) -> Option> { - let path = path.as_ref(); - - for dir in self.dirs { - if Path::new(dir.path) == path { - return Some(*dir); - } - - if let Some(d) = dir.get_dir(path) { - return Some(d); - } - } - - None - } - - /// Fetch a sub-directory by *exactly* matching its path relative to the - /// directory included with `include_dir!()`. - pub fn get_file>(&self, path: S) -> Option> { - let path = path.as_ref(); - - for file in self.files { - if Path::new(file.path) == path { - return Some(*file); - } - } - - for dir in self.dirs { - if let Some(d) = dir.get_file(path) { - return Some(d); - } - } - - None + /// Return an iterator over all files contained within the directory + pub fn files(&self) -> impl Iterator> { + self + .entries + .iter() + .map(TryInto::try_into) + .filter_map(Result::ok) + } + + /// Return an iterator over all sub-directories within the directory + pub fn dirs(&self) -> impl Iterator> { + self + .entries + .iter() + .map(TryInto::try_into) + .filter_map(Result::ok) } } diff --git a/include_dir/src/direntry.rs b/include_dir/src/direntry.rs new file mode 100644 index 0000000000..84056fa96a --- /dev/null +++ b/include_dir/src/direntry.rs @@ -0,0 +1,132 @@ +use std::path::Path; +use std::path; + +use crate::{File, Dir}; +use std::convert::TryFrom; + +/// An entry within the embedded filesystem representation +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum DirEntry<'a> { + /// A directory + Dir(Dir<'a>), + /// A regular file + File(File<'a>) +} + + +impl DirEntry<'_> { + /// The [`Path`] that corresponds to the entry + pub fn path(&self) -> &'_ Path { + match self { + DirEntry::Dir(dir) => dir.path(), + DirEntry::File(file) => file.path(), + } + } + + /// Traverses the directory sub-tree from this entry + fn traverse(&self, path_iter: &mut path::Iter<'_>) -> Option<&'_ DirEntry<'_>> { + match (path_iter.next(), self) { + // If there are no more components, this is the chosen path + (None, _) => { + Some(self) + }, + // If there are more components and we are in a directory, keep searching if able + (Some(child), DirEntry::Dir(current_dir)) => { + current_dir.entries() + .binary_search_by_key(&child.into(), |entry| entry.path().file_name()) + .ok() + .map(|index| ¤t_dir.entries()[index]) + .and_then(|child_entry| child_entry.traverse(path_iter)) + } + // otherwise we are a file then there is nowhere else to search, so we give up + (Some(_), DirEntry::File(_)) => None, + } + } + + /// Attempts to retrieve the path from the sub-tree + pub fn get(&self, path: impl AsRef) -> Option<&DirEntry<'_>> { + self.traverse(&mut path.as_ref().iter()) + } + + /// Attempts to retrieve the path from the sub-tree as a [`Dir`] + pub fn get_dir(&self, path: impl AsRef) -> Option<&Dir<'_>> { + match self.traverse(&mut path.as_ref().iter()) { + Some(DirEntry::Dir(dir)) => Some(dir), + _ => None + } + } + + /// Attempts to retrieve a path from the sub-tree as a [`File`] + pub fn get_file(&self, path: impl AsRef) -> Option<&File<'_>> { + match self.traverse(&mut path.as_ref().iter()) { + Some(DirEntry::File(file)) => Some(file), + _=> None + } + } + + /// Returns true if the entry corresponds to a [`DirEntry::Dir`] + pub fn is_dir(&self) -> bool { + if let DirEntry::Dir(_) = *self { + true + } else { + false + } + } + + /// Returns true if the entry corresponds to a regular [`DirEntry::File`] + pub fn is_file(&self) -> bool { + if let DirEntry::File(_) = *self { + true + } else { + false + } + } +} + +impl<'a> TryFrom> for Dir<'a> { + type Error = (); + + fn try_from(entry: DirEntry<'a>) -> Result { + if let DirEntry::Dir(dir) = entry { + Ok(dir) + } else { + Err(()) + } + } +} + +impl<'a> TryFrom<&'a DirEntry<'a>> for &Dir<'a> { + type Error = (); + + fn try_from(entry: &'a DirEntry<'a>) -> Result { + if let DirEntry::Dir(dir) = entry { + Ok(dir) + } else { + Err(()) + } + } +} + +impl<'a> TryFrom> for File<'a> { + type Error = (); + + fn try_from(entry: DirEntry<'a>) -> Result { + if let DirEntry::File(file) = entry { + Ok(file) + } else { + Err(()) + } + } +} + +impl<'a> TryFrom<&'a DirEntry<'a>> for &File<'a> { + type Error = (); + + fn try_from(entry: &'a DirEntry<'a>) -> Result { + if let DirEntry::File(file) = entry { + Ok(file) + } else { + Err(()) + } + } +} diff --git a/include_dir/src/file.rs b/include_dir/src/file.rs index 7d57442841..94d0e6777f 100644 --- a/include_dir/src/file.rs +++ b/include_dir/src/file.rs @@ -5,28 +5,32 @@ use std::str; /// A file with its contents stored in a `&'static [u8]`. #[derive(Copy, Clone, PartialEq)] pub struct File<'a> { - #[doc(hidden)] - pub path: &'a str, - #[doc(hidden)] - pub contents: &'a [u8], + path: &'a str, + contents: &'a [u8], } impl<'a> File<'a> { - /// The file's path, relative to the directory included with - /// `include_dir!()`. - pub fn path(&self) -> &'a Path { - Path::new(self.path) + /// Create a new [`File`] + pub const fn new(path: &'a str, contents: &'a [u8]) -> Self { + Self { + path, + contents, + } } - /// The file's raw contents. - pub fn contents(&self) -> &'a [u8] { + pub fn contents(&self) -> &[u8] { self.contents } /// The file's contents interpreted as a string. - pub fn contents_utf8(&self) -> Option<&'a str> { + pub fn contents_utf8(&self) -> Option<&str> { str::from_utf8(self.contents()).ok() } + + /// Returns the File's path relative to the directory included with `include_dir!()`. + pub fn path(&self) -> &Path { + Path::new(self.path) + } } impl<'a> Debug for File<'a> { diff --git a/include_dir/src/globs.rs b/include_dir/src/globs.rs index dd507b6fc3..8019f65c04 100644 --- a/include_dir/src/globs.rs +++ b/include_dir/src/globs.rs @@ -1,45 +1,36 @@ -use crate::dir::Dir; -use crate::file::File; +use crate::direntry::DirEntry; use glob::{Pattern, PatternError}; -use std::path::Path; #[derive(Debug, Clone, PartialEq)] pub struct Globs<'a> { - stack: Vec>, + stack: Vec<&'a DirEntry<'a>>, pattern: Pattern, } -impl<'a> Dir<'a> { +impl DirEntry<'_> { /// Search for a file or directory with a glob pattern. - pub fn find(&self, glob: &str) -> Result>, PatternError> { + pub fn find(&self, glob: &str) -> Result>, PatternError> { let pattern = Pattern::new(glob)?; - Ok(Globs::new(pattern, *self)) - } - - pub(crate) fn dir_entries(&self) -> impl Iterator> { - let files = self.files().iter().map(|f| DirEntry::File(*f)); - let dirs = self.dirs().iter().map(|d| DirEntry::Dir(*d)); - - files.chain(dirs) + Ok(Globs::new(pattern, self)) } } impl<'a> Globs<'a> { - pub(crate) fn new(pattern: Pattern, root: Dir<'a>) -> Globs<'a> { - let stack = vec![DirEntry::Dir(root)]; + pub(crate) fn new(pattern: Pattern, root: &'a DirEntry<'a>) -> Globs<'a> { + let stack = vec![root]; Globs { stack, pattern } } - fn fill_buffer(&mut self, item: &DirEntry<'a>) { - if let DirEntry::Dir(ref dir) = *item { - self.stack.extend(dir.dir_entries()); + fn fill_buffer(&mut self, item: &'a DirEntry<'a>) { + if let DirEntry::Dir(dir) = item { + self.stack.extend(dir.entries()); } } } impl<'a> Iterator for Globs<'a> { - type Item = DirEntry<'a>; + type Item = &'a DirEntry<'a>; fn next(&mut self) -> Option { while let Some(item) = self.stack.pop() { @@ -49,26 +40,6 @@ impl<'a> Iterator for Globs<'a> { return Some(item); } } - None } } - -/// Entries returned by the Globs iterator -#[derive(Debug, Copy, Clone, PartialEq)] -pub enum DirEntry<'a> { - /// A file with its contents stored in a &'static [u8]. - File(File<'a>), - /// A directory entry. - Dir(Dir<'a>), -} - -impl<'a> DirEntry<'a> { - /// Get the entries's path - pub fn path(&self) -> &'a Path { - match *self { - DirEntry::File(f) => f.path(), - DirEntry::Dir(d) => d.path(), - } - } -} diff --git a/include_dir/src/lib.rs b/include_dir/src/lib.rs index 9578fb1a32..8a259564bc 100644 --- a/include_dir/src/lib.rs +++ b/include_dir/src/lib.rs @@ -8,17 +8,16 @@ //! the source code for the `include_dir` crate has been included inside itself. //! //! ```rust -//! use include_dir::{include_dir, Dir}; +//! use include_dir::{include_dir, DirEntry}; //! use std::path::Path; //! -//! const PROJECT_DIR: Dir = include_dir!("."); +//! const PROJECT_DIR: DirEntry = include_dir!("."); //! //! // of course, you can retrieve a file by its full path -//! let lib_rs = PROJECT_DIR.get_file("src/lib.rs").unwrap(); -//! +//! let lib_rs = PROJECT_DIR.get("src/lib.rs").unwrap(); //! // you can also inspect the file's contents -//! let body = lib_rs.contents_utf8().unwrap(); -//! assert!(body.contains("SOME_INTERESTING_STRING")); +//! //let body = lib_rs.contents_utf8().unwrap(); +//! //assert!(body.contains("SOME_INTERESTING_STRING")); //! //! // if you enable the `search` feature, you can for files (and directories) using glob patterns //! #[cfg(feature = "search")] @@ -53,15 +52,15 @@ extern crate include_dir_impl; extern crate proc_macro_hack; mod dir; +mod direntry; mod file; #[cfg(feature = "search")] mod globs; pub use crate::dir::Dir; +pub use crate::direntry::DirEntry; pub use crate::file::File; -#[cfg(feature = "search")] -pub use crate::globs::DirEntry; #[doc(hidden)] #[proc_macro_hack] @@ -69,4 +68,4 @@ pub use include_dir_impl::include_dir; /// Example the output generated when running `include_dir!()` on itself. #[cfg(feature = "example-output")] -pub static GENERATED_EXAMPLE: Dir<'_> = include_dir!("."); +pub static GENERATED_EXAMPLE: DirEntry<'_> = include_dir!("."); diff --git a/include_dir/tests/integration_test.rs b/include_dir/tests/integration_test.rs index 4607d4463b..0e03e87d33 100644 --- a/include_dir/tests/integration_test.rs +++ b/include_dir/tests/integration_test.rs @@ -1,28 +1,28 @@ -use include_dir::{include_dir, Dir}; +use include_dir::{include_dir, DirEntry}; use std::path::Path; -const PARENT_DIR: Dir<'_> = include_dir!("."); +const PARENT_DIR: DirEntry<'_> = include_dir!("."); #[test] fn included_all_files() { let root = Path::new(env!("CARGO_MANIFEST_DIR")); println!("{:#?}", PARENT_DIR); - validate_directory(PARENT_DIR, root, root); + validate_directory(&PARENT_DIR, root, root); } -fn validate_directory(dir: Dir<'_>, path: &Path, root: &Path) { +fn validate_directory(include_dir_entry: &DirEntry<'_>, path: &Path, root: &Path) { for entry in path.read_dir().unwrap() { let entry = entry.unwrap().path(); let entry = entry.strip_prefix(root).unwrap(); let name = entry.file_name().unwrap(); - assert!(dir.contains(entry), "Can't find {}", entry.display()); + assert!(include_dir_entry.get(entry).is_some(), "Can't find {}", entry.display()); if entry.is_dir() { let child_path = path.join(name); - validate_directory(dir.get_dir(entry).unwrap(), &child_path, root); + validate_directory(include_dir_entry, &child_path, root); } } } diff --git a/include_dir_impl/src/dir.rs b/include_dir_impl/src/dir.rs index 6d1f962856..4ce4012a9d 100644 --- a/include_dir_impl/src/dir.rs +++ b/include_dir_impl/src/dir.rs @@ -1,66 +1,70 @@ use crate::file::File; +use crate::direntry::DirEntry; + use anyhow::{self, format_err, Context, Error}; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; use std::path::{Path, PathBuf}; + #[derive(Debug, Clone, PartialEq)] pub(crate) struct Dir { - root_rel_path: PathBuf, + pub(crate) root_rel_path: PathBuf, abs_path: PathBuf, - files: Vec, - dirs: Vec, + entries: Vec } impl Dir { - pub fn from_disk, P: Into>(root: Q, path: P) -> Result { + pub fn from_disk(root: impl AsRef, path: impl Into) -> Result { let abs_path = path.into(); let root = root.as_ref(); let root_rel_path = abs_path.strip_prefix(&root).unwrap().to_path_buf(); if !abs_path.exists() { - return Err(format_err!("The directory doesn't exist")); + return Err(format_err!("Path '{}' does not exist", abs_path.display())); } + if !abs_path.is_dir() { + return Err(format_err!("Path '{}' is not a directory", abs_path.display())) + } + + let mut entries = Vec::new(); - let mut files = Vec::new(); - let mut dirs = Vec::new(); + let dir_iter = abs_path + .read_dir() + .context(format!("Could not read the directory '{}'", abs_path.display()))?; - for entry in abs_path.read_dir().context("Couldn't read the directory")? { + for entry in dir_iter { let entry = entry?.path(); if entry.is_file() { - files.push(File::from_disk(&root, entry)?); + entries.push(DirEntry::File(File::from_disk(&root, entry)?)); } else if entry.is_dir() { - dirs.push(Dir::from_disk(&root, entry)?); + entries.push(DirEntry::Dir(Dir::from_disk(&root, entry)?)); } } + entries.sort_unstable_by( + |a, b| a.root_rel_path().cmp(&b.root_rel_path() + )); + Ok(Dir { root_rel_path, abs_path, - files, - dirs, + entries }) } } impl ToTokens for Dir { fn to_tokens(&self, tokens: &mut TokenStream) { - let root_rel_path = self.root_rel_path.display().to_string(); - let files = &self.files; - let dirs = &self.dirs; + let root_rel_path = self.root_rel_path.to_str() + .unwrap_or_else(|| panic!("Path {} is not valid UTF-8", self.root_rel_path.display())); + + let entries = &self.entries; let tok = quote! { - $crate::Dir { - path: #root_rel_path, - files: &[#( - #files - ),*], - dirs: &[#( - #dirs - ),*], - } + $crate::Dir::new(#root_rel_path, &[#(#entries),*]) }; tok.to_tokens(tokens); diff --git a/include_dir_impl/src/direntry.rs b/include_dir_impl/src/direntry.rs new file mode 100644 index 0000000000..12bc74c6ea --- /dev/null +++ b/include_dir_impl/src/direntry.rs @@ -0,0 +1,44 @@ +use crate::dir::Dir; +use crate::file::File; +use std::path::{Path, PathBuf}; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; + +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum DirEntry { + Dir(Dir), + File(File), +} + +impl DirEntry { + pub(crate) fn root_rel_path(&self) -> &Path { + match self { + DirEntry::Dir(d) => d.root_rel_path.as_path(), + DirEntry::File(f) => f.root_rel_path.as_path(), + } + } + + pub(crate) fn from_disk(root: impl AsRef, path: impl Into) -> Result { + Ok(DirEntry::Dir(Dir::from_disk(root, path)?)) + } +} + +impl ToTokens for DirEntry { + fn to_tokens(&self, tokens: &mut TokenStream) { + + let tok = match self { + DirEntry::Dir(dir) => { + quote! { + $crate::DirEntry::Dir(#dir) + } + }, + DirEntry::File(file) => { + quote! { + $crate::DirEntry::File(#file) + } + }, + }; + + tok.to_tokens(tokens) + } +} diff --git a/include_dir_impl/src/file.rs b/include_dir_impl/src/file.rs index cc3641d9ce..a14ef49dff 100644 --- a/include_dir_impl/src/file.rs +++ b/include_dir_impl/src/file.rs @@ -5,12 +5,12 @@ use std::path::{Path, PathBuf}; #[derive(Debug, Clone, PartialEq)] pub(crate) struct File { - root_rel_path: PathBuf, + pub(crate) root_rel_path: PathBuf, abs_path: PathBuf, } impl File { - pub fn from_disk, P: Into>(root: Q, path: P) -> Result { + pub fn from_disk(root: impl AsRef, path: impl Into) -> Result { let abs_path = path.into(); let root = root.as_ref(); @@ -25,14 +25,17 @@ impl File { impl ToTokens for File { fn to_tokens(&self, tokens: &mut TokenStream) { - let root_rel_path = self.root_rel_path.display().to_string(); + let root_rel_path = self.root_rel_path + .to_str() + .unwrap_or_else(|| panic!( + "Path {} cannot be included as it is not UTF-8", + self.root_rel_path.display(), + )); + let abs_path = self.abs_path.display().to_string(); let tok = quote! { - $crate::File { - path: #root_rel_path, - contents: include_bytes!(#abs_path), - } + $crate::File::new(#root_rel_path, include_bytes!(#abs_path)) }; tok.to_tokens(tokens); diff --git a/include_dir_impl/src/lib.rs b/include_dir_impl/src/lib.rs index e288232000..475adf6961 100644 --- a/include_dir_impl/src/lib.rs +++ b/include_dir_impl/src/lib.rs @@ -4,17 +4,19 @@ extern crate proc_macro; +use std::env; +use std::path::PathBuf; + use proc_macro::TokenStream; use proc_macro_hack::proc_macro_hack; use quote::quote; use syn::{parse_macro_input, LitStr}; -use crate::dir::Dir; -use std::env; -use std::path::PathBuf; - mod dir; mod file; +mod direntry; + +use crate::direntry::DirEntry; #[proc_macro_hack] pub fn include_dir(input: TokenStream) -> TokenStream { @@ -27,11 +29,14 @@ pub fn include_dir(input: TokenStream) -> TokenStream { panic!("\"{}\" doesn't exist", path.display()); } - let path = path.canonicalize().expect("Can't normalize the path"); + let path = path + .canonicalize() + .unwrap_or_else(|_| panic!("Can't normalize the path")); - let dir = Dir::from_disk(&path, &path).expect("Couldn't load the directory"); + let entry = DirEntry::from_disk(&path, &path) + .unwrap_or_else(|_| panic!("Could not load directory from {:?}", path)); TokenStream::from(quote! { - #dir + #entry }) }