From 7d603190df7a737c17844de9c7add0f94f32faf7 Mon Sep 17 00:00:00 2001 From: "Samani G. Gikandi" Date: Tue, 28 Jan 2020 09:39:18 -0800 Subject: [PATCH 1/5] Changes version to 0.6.0 and adds O(lg(n)) file retrieval **Summary** * Bumps version from `0.5.1-alpha.0` to `0.6.0-alpha.0` due to backwards incompatible API changes. * File retrieval can now be done in log time rather than linear time. * Nodes of the filesystem tree are now represented with a `DirEntry` sum-type over `File` and `Dir` to simplify the logic Implementing sub-linear retrieval while `File` and `Dir` types were separate added a lot of undue complexity to both the macro generation and runtime lookup code so I opted to introduce `DirEntry` across the board instead. I did my best to minimize the backwards incompatible changes wherever possible and to provide helper functionality to aid ergonomics and ease the migration. I am also happy to write up a Migration Guide if we feel that's prudent! **Additions** * Adds a `file_name` attribute to `include_dir::Dir` and `include_dir::File` * `std::convert::From` implementations for `DirEntry` to `Option`, `Option`, as well as the corresponding reference types to make conversion to the inner types easy. * `DirEntry::is_dir` and `DirEntry::is_file` allow one to check if a DirEntry is a particular variant. * Logarithmic file-retrieval is accomplished by sorting the filesystem entries at compile time and then performing a binary search on retrieval. **API Changes** * `Dir::files` now returns an iterator over files (`impl Iterator>`) instead of a slice of owned `Files` * `Dir::dirs` now returns an iterator over dirs (`impl Iterator>`) instead of a slice of owned `Dir` * `include_dir::globs::Globs` now takes and returns `&DirEntry` references to avoid copies. **Copying** There were a handful of places where we were performing copies where we now return references as copying large amounts of data could potentially be expensive. `DirEntry`, `File`, and `Diir` still _implement_ `Copy` so callers are free to make copies where it makes sense for their use case, however copying will not be done _within_ `include_dir`'s business logic. **Misc** * Updates code formatting as needed * Updates tests and documentation accordingly * Updates some macro generation error messages to be more descriptive --- README.md | 5 + include_dir/Cargo.toml | 4 +- include_dir/src/dir.rs | 86 ++++++---------- include_dir/src/direntry.rs | 135 ++++++++++++++++++++++++++ include_dir/src/file.rs | 25 +++-- include_dir/src/globs.rs | 51 +++------- include_dir/src/lib.rs | 17 ++-- include_dir/tests/integration_test.rs | 12 +-- include_dir_impl/Cargo.toml | 2 +- include_dir_impl/src/dir.rs | 69 +++++++++---- include_dir_impl/src/direntry.rs | 44 +++++++++ include_dir_impl/src/file.rs | 22 ++++- include_dir_impl/src/lib.rs | 19 ++-- 13 files changed, 339 insertions(+), 152 deletions(-) create mode 100644 include_dir/src/direntry.rs create mode 100644 include_dir_impl/src/direntry.rs 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/Cargo.toml b/include_dir/Cargo.toml index a3537819b1..82ace3ee4c 100644 --- a/include_dir/Cargo.toml +++ b/include_dir/Cargo.toml @@ -1,7 +1,7 @@ [package] authors = ["Michael Bryan "] name = "include_dir" -version = "0.5.1-alpha.0" +version = "0.6.0-alpha.0" description = "Embed the contents of a directory in your binary" license = "MIT" readme = "../README.md" @@ -25,7 +25,7 @@ repository = "Michael-F-Bryan/include_dir" [dependencies] glob = { version = "0.3", optional = true } proc-macro-hack = "0.5" -include_dir_impl = { path = "../include_dir_impl", version = "0.5.1-alpha.0" } +include_dir_impl = { path = "../include_dir_impl", version = "0.6.0-alpha.0" } [features] default = [ "search" ] diff --git a/include_dir/src/dir.rs b/include_dir/src/dir.rs index db7c64211b..a301e49eb5 100644 --- a/include_dir/src/dir.rs +++ b/include_dir/src/dir.rs @@ -1,5 +1,8 @@ use crate::file::File; use std::path::Path; +use std::ffi::OsStr; + +use crate::DirEntry; /// A directory entry. #[derive(Debug, Copy, Clone, PartialEq)] @@ -7,69 +10,42 @@ pub struct Dir<'a> { #[doc(hidden)] pub path: &'a str, #[doc(hidden)] - pub files: &'a [File<'a>], + pub file_name: Option<&'a str>, #[doc(hidden)] - pub dirs: &'a [Dir<'a>], + pub 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 +impl Dir<'_> { + /// The file name of the directory + /// + /// This will be none if the directory corresponds to the root directory included with [include_dir!()] + pub fn file_name(&self) -> Option<&'_ OsStr> { + self.file_name.map(OsStr::new) } - /// Get a list of the sub-directories inside this directory. - pub fn dirs(&self) -> &'a [Dir<'a>] { - self.dirs - } - - /// 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() + /// The directory's path relative to the directory included with [include_dir!()] + pub fn path(&self) -> &'_ Path { + Path::new(self.path) } - /// 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 + /// 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_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() + .filter_map(Into::into) + } + + /// Return an iterator over all sub-directories within the directory + pub fn dirs(&self) -> impl Iterator> { + self + .entries + .iter() + .filter_map(Into::into) } } diff --git a/include_dir/src/direntry.rs b/include_dir/src/direntry.rs new file mode 100644 index 0000000000..1b56b277ab --- /dev/null +++ b/include_dir/src/direntry.rs @@ -0,0 +1,135 @@ +use std::ffi::OsStr; +use std::path::Path; +use std::path; + +use crate::{File, Dir}; + +/// 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<'_> { + /// Returns the bare file name of the entry without any leading Path components + /// + /// This will be [`None`] if the `DirEntry` corresponds to the path included + /// via [`include_dir!()`] (i.e. the root path) + pub fn file_name(&self) -> Option<&'_ OsStr> { + match self { + DirEntry::Dir(dir) => { dir.file_name()} + DirEntry::File(file) => { file.file_name().into()} + } + } + + /// 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.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> From> for Option> { + fn from(entry: DirEntry<'a>) -> Self { + if let DirEntry::Dir(dir) = entry { + Some(dir) + } else { + None + } + } +} + +impl<'a> From<&'a DirEntry<'a>> for Option<&Dir<'a>> { + fn from(entry: &'a DirEntry<'a>) -> Self { + if let DirEntry::Dir(dir) = entry { + Some(dir) + } else { + None + } + } +} + +impl<'a> From<&'a DirEntry<'a>> for Option<&File<'a>> { + fn from(entry: &'a DirEntry<'a>) -> Self { + if let DirEntry::File(file) = entry { + Some(file) + } else { + None + } + } +} + +impl<'a> From> for Option> { + fn from(entry: DirEntry<'a>) -> Self { + if let DirEntry::File(file) = entry { + Some(file) + } else { + None + } + } +} diff --git a/include_dir/src/file.rs b/include_dir/src/file.rs index 7d57442841..380b9f2022 100644 --- a/include_dir/src/file.rs +++ b/include_dir/src/file.rs @@ -1,6 +1,7 @@ use std::fmt::{self, Debug, Formatter}; use std::path::Path; use std::str; +use std::ffi::OsStr; /// A file with its contents stored in a `&'static [u8]`. #[derive(Copy, Clone, PartialEq)] @@ -8,25 +9,31 @@ pub struct File<'a> { #[doc(hidden)] pub path: &'a str, #[doc(hidden)] + pub file_name: &'a str, + #[doc(hidden)] pub 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) - } - +impl File<'_> { /// 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) + } + + /// Returns the final component of the [`Path`] of the file + pub fn file_name(&self) -> &'_ OsStr { + OsStr::new(self.file_name) + } } 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/Cargo.toml b/include_dir_impl/Cargo.toml index 45d12e9b47..e08ab37e2c 100644 --- a/include_dir_impl/Cargo.toml +++ b/include_dir_impl/Cargo.toml @@ -1,7 +1,7 @@ [package] authors = ["Michael Bryan "] name = "include_dir_impl" -version = "0.5.1-alpha.0" +version = "0.6.0-alpha.0" description = "Implementation crate for include_dir" license = "MIT" readme = "../README.md" diff --git a/include_dir_impl/src/dir.rs b/include_dir_impl/src/dir.rs index 6d1f962856..9d3c73b31e 100644 --- a/include_dir_impl/src/dir.rs +++ b/include_dir_impl/src/dir.rs @@ -1,64 +1,93 @@ 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 files = Vec::new(); - let mut dirs = Vec::new(); + let mut entries = Vec::new(); - for entry in abs_path.read_dir().context("Couldn't read the directory")? { + let dir_iter = abs_path + .read_dir() + .context(format!("Could not read the directory '{}'", abs_path.display()))?; + + 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 file_name = { + let potential_file_name = self.root_rel_path + // n.b. - the root path *will not* have a file name per [PathBuf::file_name] + .file_name() + .map(|os_str| { + os_str + .to_str() + .unwrap_or_else(|| panic!( + "Path '{}' is not a valid UTF-8 and cannot be used.", self.root_rel_path.display() + )) + }); + // Seems we have to do this manually per https://github.com/dtolnay/quote/issues/129 + if let Some(file_name) = potential_file_name { + quote!(::std::option::Option::Some(#file_name)) + } else { + quote!(::std::option::Option::None) + } + }; + + let entries = &self.entries; let tok = quote! { $crate::Dir { path: #root_rel_path, - files: &[#( - #files - ),*], - dirs: &[#( - #dirs + file_name: #file_name, + entries: &[#( + #entries ),*], } }; 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..c92b1ace0a 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,11 +25,27 @@ 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 file_name = self.root_rel_path + .file_name() + .unwrap() + .to_str() + .unwrap_or_else(|| panic!( + "Path {} can not be included as it is not UTF-8", + self.root_rel_path.display() + )); + let tok = quote! { $crate::File { + file_name: #file_name, path: #root_rel_path, contents: include_bytes!(#abs_path), } 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 }) } From c21693750959c9feb400d3d76ab239f6505b1c1f Mon Sep 17 00:00:00 2001 From: "Samani G. Gikandi" Date: Tue, 25 Feb 2020 09:15:00 -0800 Subject: [PATCH 2/5] Reverts version from 0.6.0 to 0.5.1 --- include_dir/Cargo.toml | 4 ++-- include_dir_impl/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/include_dir/Cargo.toml b/include_dir/Cargo.toml index 82ace3ee4c..a3537819b1 100644 --- a/include_dir/Cargo.toml +++ b/include_dir/Cargo.toml @@ -1,7 +1,7 @@ [package] authors = ["Michael Bryan "] name = "include_dir" -version = "0.6.0-alpha.0" +version = "0.5.1-alpha.0" description = "Embed the contents of a directory in your binary" license = "MIT" readme = "../README.md" @@ -25,7 +25,7 @@ repository = "Michael-F-Bryan/include_dir" [dependencies] glob = { version = "0.3", optional = true } proc-macro-hack = "0.5" -include_dir_impl = { path = "../include_dir_impl", version = "0.6.0-alpha.0" } +include_dir_impl = { path = "../include_dir_impl", version = "0.5.1-alpha.0" } [features] default = [ "search" ] diff --git a/include_dir_impl/Cargo.toml b/include_dir_impl/Cargo.toml index e08ab37e2c..45d12e9b47 100644 --- a/include_dir_impl/Cargo.toml +++ b/include_dir_impl/Cargo.toml @@ -1,7 +1,7 @@ [package] authors = ["Michael Bryan "] name = "include_dir_impl" -version = "0.6.0-alpha.0" +version = "0.5.1-alpha.0" description = "Implementation crate for include_dir" license = "MIT" readme = "../README.md" From ac792404a9a6d2b5fc8e408f746a266a3d26fb8e Mon Sep 17 00:00:00 2001 From: "Samani G. Gikandi" Date: Tue, 25 Feb 2020 09:15:25 -0800 Subject: [PATCH 3/5] Adds constructors to `File` and `Dir` * All fields on `File` and `Dir` are now private. * Introduces `const fn new` constructors for these types. --- include_dir/src/dir.rs | 21 ++++++++++++++------- include_dir/src/direntry.rs | 4 ++-- include_dir/src/file.rs | 19 ++++++++++++------- include_dir_impl/src/dir.rs | 8 +------- include_dir_impl/src/file.rs | 6 +----- 5 files changed, 30 insertions(+), 28 deletions(-) diff --git a/include_dir/src/dir.rs b/include_dir/src/dir.rs index a301e49eb5..05c997bae5 100644 --- a/include_dir/src/dir.rs +++ b/include_dir/src/dir.rs @@ -7,15 +7,22 @@ use crate::DirEntry; /// A directory entry. #[derive(Debug, Copy, Clone, PartialEq)] pub struct Dir<'a> { - #[doc(hidden)] - pub path: &'a str, - #[doc(hidden)] - pub file_name: Option<&'a str>, - #[doc(hidden)] - pub entries: &'a [DirEntry<'a>] + path: &'a str, + file_name: Option<&'a str>, + entries: &'a [DirEntry<'a>] } -impl Dir<'_> { +impl<'a> Dir<'a> { + + /// FIXME: Docstring + pub const fn new(path: &'a str, file_name: Option<&'a str>, entries: &'a [DirEntry<'_>]) -> Self { + Self { + path, + file_name, + entries + } + } + /// The file name of the directory /// /// This will be none if the directory corresponds to the root directory included with [include_dir!()] diff --git a/include_dir/src/direntry.rs b/include_dir/src/direntry.rs index 1b56b277ab..9ab0b5ed74 100644 --- a/include_dir/src/direntry.rs +++ b/include_dir/src/direntry.rs @@ -43,10 +43,10 @@ impl DirEntry<'_> { }, // If there are more components and we are in a directory, keep searching if able (Some(child), DirEntry::Dir(current_dir)) => { - current_dir.entries + current_dir.entries() .binary_search_by_key(&child.into(), |entry| entry.file_name()) .ok() - .map(|index| ¤t_dir.entries[index]) + .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 diff --git a/include_dir/src/file.rs b/include_dir/src/file.rs index 380b9f2022..c0604c36b7 100644 --- a/include_dir/src/file.rs +++ b/include_dir/src/file.rs @@ -6,15 +6,20 @@ use std::ffi::OsStr; /// 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 file_name: &'a str, - #[doc(hidden)] - pub contents: &'a [u8], + path: &'a str, + file_name: &'a str, + contents: &'a [u8], } -impl File<'_> { +impl<'a> File<'a> { + /// FIXME: Docstring + pub const fn new(path: &'a str, file_name: &'a str, contents: &'a [u8]) -> Self { + Self { + path, + file_name, + contents, + } + } /// The file's raw contents. pub fn contents(&self) -> &'_ [u8] { self.contents diff --git a/include_dir_impl/src/dir.rs b/include_dir_impl/src/dir.rs index 9d3c73b31e..266b658935 100644 --- a/include_dir_impl/src/dir.rs +++ b/include_dir_impl/src/dir.rs @@ -83,13 +83,7 @@ impl ToTokens for Dir { let entries = &self.entries; let tok = quote! { - $crate::Dir { - path: #root_rel_path, - file_name: #file_name, - entries: &[#( - #entries - ),*], - } + $crate::Dir::new(#root_rel_path, #file_name, &[#(#entries),*]) }; tok.to_tokens(tokens); diff --git a/include_dir_impl/src/file.rs b/include_dir_impl/src/file.rs index c92b1ace0a..7ca783c397 100644 --- a/include_dir_impl/src/file.rs +++ b/include_dir_impl/src/file.rs @@ -44,11 +44,7 @@ impl ToTokens for File { )); let tok = quote! { - $crate::File { - file_name: #file_name, - path: #root_rel_path, - contents: include_bytes!(#abs_path), - } + $crate::File::new(#root_rel_path, #file_name, include_bytes!(#abs_path)) }; tok.to_tokens(tokens); From dff561ea6b7053b2de73841aa18bc6f315c9db6f Mon Sep 17 00:00:00 2001 From: "Samani G. Gikandi" Date: Tue, 25 Feb 2020 09:25:56 -0800 Subject: [PATCH 4/5] Removes `file_name` properties and accessors --- include_dir/src/dir.rs | 14 ++------------ include_dir/src/direntry.rs | 14 +------------- include_dir/src/file.rs | 12 ++---------- include_dir_impl/src/dir.rs | 21 +-------------------- include_dir_impl/src/file.rs | 11 +---------- 5 files changed, 7 insertions(+), 65 deletions(-) diff --git a/include_dir/src/dir.rs b/include_dir/src/dir.rs index 05c997bae5..813519322f 100644 --- a/include_dir/src/dir.rs +++ b/include_dir/src/dir.rs @@ -1,6 +1,5 @@ use crate::file::File; use std::path::Path; -use std::ffi::OsStr; use crate::DirEntry; @@ -8,28 +7,19 @@ use crate::DirEntry; #[derive(Debug, Copy, Clone, PartialEq)] pub struct Dir<'a> { path: &'a str, - file_name: Option<&'a str>, entries: &'a [DirEntry<'a>] } impl<'a> Dir<'a> { - /// FIXME: Docstring - pub const fn new(path: &'a str, file_name: Option<&'a str>, entries: &'a [DirEntry<'_>]) -> Self { + /// Create a new [`Dir`] + pub const fn new(path: &'a str, entries: &'a [DirEntry<'_>]) -> Self { Self { path, - file_name, entries } } - /// The file name of the directory - /// - /// This will be none if the directory corresponds to the root directory included with [include_dir!()] - pub fn file_name(&self) -> Option<&'_ OsStr> { - self.file_name.map(OsStr::new) - } - /// The directory's path relative to the directory included with [include_dir!()] pub fn path(&self) -> &'_ Path { Path::new(self.path) diff --git a/include_dir/src/direntry.rs b/include_dir/src/direntry.rs index 9ab0b5ed74..e62caf23cd 100644 --- a/include_dir/src/direntry.rs +++ b/include_dir/src/direntry.rs @@ -1,4 +1,3 @@ -use std::ffi::OsStr; use std::path::Path; use std::path; @@ -15,17 +14,6 @@ pub enum DirEntry<'a> { impl DirEntry<'_> { - /// Returns the bare file name of the entry without any leading Path components - /// - /// This will be [`None`] if the `DirEntry` corresponds to the path included - /// via [`include_dir!()`] (i.e. the root path) - pub fn file_name(&self) -> Option<&'_ OsStr> { - match self { - DirEntry::Dir(dir) => { dir.file_name()} - DirEntry::File(file) => { file.file_name().into()} - } - } - /// The [`Path`] that corresponds to the entry pub fn path(&self) -> &'_ Path { match self { @@ -44,7 +32,7 @@ impl DirEntry<'_> { // 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.file_name()) + .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)) diff --git a/include_dir/src/file.rs b/include_dir/src/file.rs index c0604c36b7..d5e19d3cdb 100644 --- a/include_dir/src/file.rs +++ b/include_dir/src/file.rs @@ -1,22 +1,19 @@ use std::fmt::{self, Debug, Formatter}; use std::path::Path; use std::str; -use std::ffi::OsStr; /// A file with its contents stored in a `&'static [u8]`. #[derive(Copy, Clone, PartialEq)] pub struct File<'a> { path: &'a str, - file_name: &'a str, contents: &'a [u8], } impl<'a> File<'a> { - /// FIXME: Docstring - pub const fn new(path: &'a str, file_name: &'a str, contents: &'a [u8]) -> Self { + /// Create a new [`File`] + pub const fn new(path: &'a str, contents: &'a [u8]) -> Self { Self { path, - file_name, contents, } } @@ -34,11 +31,6 @@ impl<'a> File<'a> { pub fn path(&self) -> &'_ Path { Path::new(self.path) } - - /// Returns the final component of the [`Path`] of the file - pub fn file_name(&self) -> &'_ OsStr { - OsStr::new(self.file_name) - } } impl<'a> Debug for File<'a> { diff --git a/include_dir_impl/src/dir.rs b/include_dir_impl/src/dir.rs index 266b658935..4ce4012a9d 100644 --- a/include_dir_impl/src/dir.rs +++ b/include_dir_impl/src/dir.rs @@ -61,29 +61,10 @@ impl ToTokens for Dir { 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 file_name = { - let potential_file_name = self.root_rel_path - // n.b. - the root path *will not* have a file name per [PathBuf::file_name] - .file_name() - .map(|os_str| { - os_str - .to_str() - .unwrap_or_else(|| panic!( - "Path '{}' is not a valid UTF-8 and cannot be used.", self.root_rel_path.display() - )) - }); - // Seems we have to do this manually per https://github.com/dtolnay/quote/issues/129 - if let Some(file_name) = potential_file_name { - quote!(::std::option::Option::Some(#file_name)) - } else { - quote!(::std::option::Option::None) - } - }; - let entries = &self.entries; let tok = quote! { - $crate::Dir::new(#root_rel_path, #file_name, &[#(#entries),*]) + $crate::Dir::new(#root_rel_path, &[#(#entries),*]) }; tok.to_tokens(tokens); diff --git a/include_dir_impl/src/file.rs b/include_dir_impl/src/file.rs index 7ca783c397..a14ef49dff 100644 --- a/include_dir_impl/src/file.rs +++ b/include_dir_impl/src/file.rs @@ -34,17 +34,8 @@ impl ToTokens for File { let abs_path = self.abs_path.display().to_string(); - let file_name = self.root_rel_path - .file_name() - .unwrap() - .to_str() - .unwrap_or_else(|| panic!( - "Path {} can not be included as it is not UTF-8", - self.root_rel_path.display() - )); - let tok = quote! { - $crate::File::new(#root_rel_path, #file_name, include_bytes!(#abs_path)) + $crate::File::new(#root_rel_path, include_bytes!(#abs_path)) }; tok.to_tokens(tokens); From a8bb89f1c2f8ddfd5343711a3c5c5e39723f6119 Mon Sep 17 00:00:00 2001 From: "Samani G. Gikandi" Date: Tue, 25 Feb 2020 09:57:37 -0800 Subject: [PATCH 5/5] Updates `DirEntry` conversions to use `TryFrom` --- include_dir/src/dir.rs | 11 ++++++---- include_dir/src/direntry.rs | 41 ++++++++++++++++++++++--------------- include_dir/src/file.rs | 6 +++--- 3 files changed, 35 insertions(+), 23 deletions(-) diff --git a/include_dir/src/dir.rs b/include_dir/src/dir.rs index 813519322f..f138c5f5b1 100644 --- a/include_dir/src/dir.rs +++ b/include_dir/src/dir.rs @@ -2,6 +2,7 @@ use crate::file::File; use std::path::Path; use crate::DirEntry; +use std::convert::TryInto; /// A directory entry. #[derive(Debug, Copy, Clone, PartialEq)] @@ -21,12 +22,12 @@ impl<'a> Dir<'a> { } /// The directory's path relative to the directory included with [include_dir!()] - pub fn path(&self) -> &'_ Path { + pub fn path(&self) -> &Path { Path::new(self.path) } /// Retrieve the entries within the directory - pub fn entries(&self) -> &'_ [DirEntry<'_>] { + pub fn entries(&self) -> &[DirEntry<'_>] { self.entries } @@ -35,7 +36,8 @@ impl<'a> Dir<'a> { self .entries .iter() - .filter_map(Into::into) + .map(TryInto::try_into) + .filter_map(Result::ok) } /// Return an iterator over all sub-directories within the directory @@ -43,6 +45,7 @@ impl<'a> Dir<'a> { self .entries .iter() - .filter_map(Into::into) + .map(TryInto::try_into) + .filter_map(Result::ok) } } diff --git a/include_dir/src/direntry.rs b/include_dir/src/direntry.rs index e62caf23cd..84056fa96a 100644 --- a/include_dir/src/direntry.rs +++ b/include_dir/src/direntry.rs @@ -2,6 +2,7 @@ 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)] @@ -82,42 +83,50 @@ impl DirEntry<'_> { } } -impl<'a> From> for Option> { - fn from(entry: DirEntry<'a>) -> Self { +impl<'a> TryFrom> for Dir<'a> { + type Error = (); + + fn try_from(entry: DirEntry<'a>) -> Result { if let DirEntry::Dir(dir) = entry { - Some(dir) + Ok(dir) } else { - None + Err(()) } } } -impl<'a> From<&'a DirEntry<'a>> for Option<&Dir<'a>> { - fn from(entry: &'a DirEntry<'a>) -> Self { +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 { - Some(dir) + Ok(dir) } else { - None + Err(()) } } } -impl<'a> From<&'a DirEntry<'a>> for Option<&File<'a>> { - fn from(entry: &'a DirEntry<'a>) -> Self { +impl<'a> TryFrom> for File<'a> { + type Error = (); + + fn try_from(entry: DirEntry<'a>) -> Result { if let DirEntry::File(file) = entry { - Some(file) + Ok(file) } else { - None + Err(()) } } } -impl<'a> From> for Option> { - fn from(entry: DirEntry<'a>) -> Self { +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 { - Some(file) + Ok(file) } else { - None + Err(()) } } } diff --git a/include_dir/src/file.rs b/include_dir/src/file.rs index d5e19d3cdb..94d0e6777f 100644 --- a/include_dir/src/file.rs +++ b/include_dir/src/file.rs @@ -18,17 +18,17 @@ impl<'a> File<'a> { } } /// The file's raw contents. - pub fn contents(&self) -> &'_ [u8] { + pub fn contents(&self) -> &[u8] { self.contents } /// The file's contents interpreted as a string. - pub fn contents_utf8(&self) -> Option<&'_ 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 { + pub fn path(&self) -> &Path { Path::new(self.path) } }