diff --git a/.travis.yml b/.travis.yml index 9fd1456..037ae36 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,5 +24,5 @@ notifications: on_success: never script: - - RUST_BACKTRACE=1 cargo test --verbose --all -- --nocapture - - RUST_BACKTRACE=1 cargo test --verbose --all --no-default-features -- --nocapture + - RUST_BACKTRACE=1 cargo test --verbose --all --no-fail-fast -- --nocapture + - RUST_BACKTRACE=1 cargo test --verbose --all --no-fail-fast --no-default-features -- --nocapture diff --git a/Cargo.toml b/Cargo.toml index c1a8c78..51b5f93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ license = "MIT OR Apache-2.0" name = "path_abs" readme = "README.md" repository = "https://github.com/vitiral/path_abs" -version = "0.3.17" +version = "0.4.0-beta1" [dependencies] std_prelude = "0.2.12" diff --git a/appveyor.yml b/appveyor.yml index 37e6831..29a6072 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -42,6 +42,7 @@ branches: # channels to enable unstable features when building for nightly. Or you could add additional # matrix entries to test different combinations of features. environment: + RUST_BACKTRACE: 1 matrix: ### MSVC Toolchains ### @@ -127,4 +128,4 @@ build: false #directly or perform other testing commands. Rust will automatically be placed in the PATH # environment variable. test_script: -- cargo test --verbose --all %cargoflags% -- --nocapture +- cargo test --verbose --all --no-fail-fast %cargoflags% -- --nocapture diff --git a/src/abs.rs b/src/abs.rs index bddbda5..4cc9670 100644 --- a/src/abs.rs +++ b/src/abs.rs @@ -13,7 +13,7 @@ use std_prelude::*; use super::{PathArc, PathDir, PathFile, Result}; #[derive(Clone, Eq, Hash, PartialEq, PartialOrd, Ord)] -/// An absolute ([canonicalized][1]) path that is guaranteed (when created) to exist. +/// An absolute (not _necessarily_ [canonicalized][1]) path that may or may not exist. /// /// [1]: https://doc.rust-lang.org/std/path/struct.Path.html?search=#method.canonicalize pub struct PathAbs(pub(crate) PathArc); @@ -31,7 +31,7 @@ impl PathAbs { /// ``` pub fn new>(path: P) -> Result { let arc = PathArc::new(path); - arc.canonicalize() + arc.absolute() } /// Resolve the `PathAbs` as a `PathFile`. Return an error if it is not a file. diff --git a/src/arc.rs b/src/arc.rs index c62de4e..6885cdf 100644 --- a/src/arc.rs +++ b/src/arc.rs @@ -9,13 +9,32 @@ use std::fmt; use std::fs; +use std::io; +use std::env; use std::ffi::OsStr; +use std::path::{Component, Components, Prefix}; use std_prelude::*; use super::{Error, Result}; use abs::PathAbs; use dir::{ListDir, PathDir}; +/// Same as [`std::env::current_dir`] except it uses a more descriptive error message +/// and returns `PathArc`. +/// +/// [`std::env::current_dir`]: https://doc.rust-lang.org/beta/std/env/fn.set_current_dir.html +pub fn current_dir(resolving: &PathArc) -> Result { + let cwd = env::current_dir().map_err(|e| { + Error::new( + e, + "getting current_dir while resolving absolute", + resolving.clone(), + ) + })?; + Ok(PathArc::from(cwd)) +} + + #[derive(Clone, Eq, Hash, PartialEq, PartialOrd, Ord)] /// A `PathBuf` that is atomically reference counted and reimplements the `PathBuf` /// methods to display the action and path when there is an error. @@ -101,8 +120,6 @@ impl PathArc { /// Returns the canonical form of the path with all intermediate components normalized and /// symbolic links resolved. /// - /// > This is identical to `PathAbs::new(path)`. - /// /// This function is identical to [std::path::Path::canonicalize][0] except: /// - It returns a `PathAbs` object /// - It has error messages which include the action and the path @@ -146,6 +163,69 @@ impl PathArc { pub fn as_path(&self) -> &Path { self.as_ref() } + + /// Convert the path to an absolute one, this is different from [`canonicalize`] in that it + /// _preserves_ symlinks and the destination may or may not exist. + /// + /// This function will: + /// - Use [`current_dir`] to resolve relative paths. + /// - Strip any `.` components (`/a/./c` -> `/a/c`) + /// - Resolve `..` _semantically_ (not using the file system). So, `a/b/c/../d => a/b/d` will + /// _always_ be true regardless of symlinks. If you want symlinks correctly resolved, use + /// `canonicalize()` instead. + /// + /// > On windows, this will sometimes call `canonicalize()` on the first component to guarantee + /// > it is the correct canonicalized prefix. For paths starting with root it also has to get + /// > the [`current_dir`] + /// + /// > On linux, the only syscall this will make is to get the [`current_dir`] for relative + /// > paths. + /// + /// [`canonicalize`]: struct.PathAbs.html#method.canonicalize + /// [`current_dir`]: fn.current_dir.html + pub fn absolute(&self) -> Result { + let mut components = self.components(); + let mut stack: Vec = Vec::new(); + + macro_rules! pop_stack { [] => {{ + if let None = stack.pop() { + return Err(Error::new( + io::Error::new(io::ErrorKind::NotFound, ".. consumed root"), + "resolving absolute", + self.clone(), + )); + } + }}} + + handle_prefix(self, &mut stack, &mut components, false)?; + + for component in components { + match component { + Component::CurDir => { /* ignore, probably impossible */ } + Component::Prefix(_) => unreachable!(), + Component::RootDir => { + if cfg!(unix) { + unreachable!("root is already handled on unix"); + } + // This is actually possible on windows because root is distinct + // from prefix (?) + stack.push(to_os(component)); + } + Component::ParentDir => pop_stack!(), + Component::Normal(_) => stack.push(to_os(component)), + } + } + + if stack.is_empty() { + return Err(Error::new( + io::Error::new(io::ErrorKind::NotFound, "resolving resulted in empty path"), + "resolving absolute", + self.clone(), + )); + } + + Ok(PathAbs(PathArc(Arc::new(PathBuf::from_iter(stack))))) + } } impl fmt::Debug for PathArc { @@ -230,3 +310,141 @@ impl Into for PathArc { } } } + +fn to_os(c: Component) -> OsString { + c.as_os_str().to_os_string() +} + +/// Handle the prefix in the components. +/// +/// Pretty much 100% of this logic is because windows is evil. You can't call `canonicalize` on `\` +/// since it depends on the current directory. You also can't call it when it would be a noop, i.e. +/// for `\\?\C:`. +fn handle_prefix( + resolving: &PathArc, + stack: &mut Vec, + components: &mut Components, + recursing: bool, +) -> Result<()> { + macro_rules! pop_stack { [] => {{ + if let None = stack.pop() { + return Err(Error::new( + io::Error::new(io::ErrorKind::NotFound, ".. consumed root"), + "resolving absolute", + resolving.clone(), + )); + } + }}} + loop { + // The whole reason we're here is because we haven't added anything to the stack yet. + assert_eq!(stack.len(), 0, "{:?}", stack); + + let component = match components.next() { + None => break, + Some(c) => c, + }; + + match component { + Component::CurDir => { + assert_eq!(recursing, false); + + // ignore + continue; + } + Component::Prefix(prefix) => { + assert!(!cfg!(unix), "Component::Prefix in unix"); + match prefix.kind() { + Prefix::Disk(_) | Prefix::UNC(_, _) => { + // Make the prefix a more "standard" form + let c = PathArc::new(component.as_os_str()).canonicalize()?; + stack.extend(c.components().map(to_os)); + } + _ => { + // Already in the "most standardized" form + // TODO: some more testing to make sure that canoninicalize() + // cannot be called on these forms would be good + stack.push(to_os(component)); + } + } + } + Component::RootDir => { + + if cfg!(windows) { + // we were called by something that got cwd... so it better not start with `\`. + assert!(!recursing); + + // https://stackoverflow.com/questions/151860 + // > In Windows [root is] relative to what drive your current working + // > directory is at the time. + // + // So, we need to push the "drive" first. + + let cwd = current_dir(resolving)?; + handle_prefix(resolving, stack, &mut cwd.components(), true)?; + { + // Double check that we aren't being dumb. `current_dir` + // should have always started with some kind of prefix. + + // TODO: not sure why, but this assertion actually can fail and + // does in the tests. + // assert_eq!(1, stack.len(), "{:?}", stack); + + let first = Path::new(&stack[0]).components().next().unwrap(); + if let Component::Prefix(prefix) = first { + if let Prefix::DeviceNS(_) = prefix.kind() { + } else if !prefix.kind().is_verbatim() { + panic!( + "First item kind is neither verbatim nor DeviceNs: {:?}", + stack + ) + } + } else { + panic!("First item is not a Prefix on windows: {:?}", stack) + } + } + } + // Always push the "root" component. + stack.push(to_os(component)); + } + Component::ParentDir | Component::Normal(_) => { + assert!(!recursing); + + // First item is either a ParentDir or Normal, in either + // case we need to get current_dir + let cwd = current_dir(resolving)?; + let mut cwd_components = cwd.components(); + handle_prefix(resolving, stack, &mut cwd_components, true)?; + stack.extend(cwd_components.map(to_os)); + + match component { + Component::ParentDir => pop_stack!(), + Component::Normal(_) => stack.push(to_os(component)), + _ => unreachable!(), + } + } + } + break; + } + Ok(()) +} + +#[test] +fn test_prefix_windows() { + fn f>(p: P) -> Result { + PathArc::new(p).absolute() + } + assert!(f(r"\\?\C:\blah\blah").is_ok()); + assert!(f(r"\blah\blah").is_ok()); + assert!(f(r"C:\blah\blah").is_ok()); + + // TODO: this is how to get the hostname, but getting the "share name" + // seems to be more difficult. + // let hostname = ::std::process::Command::new("hostname") + // .output() + // .expect("could not get hostname") + // .stdout; + // let hostname = ::std::str::from_utf8(&hostname).unwrap().trim(); + + // assert!(f(format!(r"\\{}\share", hostname)).is_ok()); + // assert!(f(format!(r"\\?\UNC\{}\share", hostname)).is_ok()); +} diff --git a/src/dir.rs b/src/dir.rs index 56242c8..4eb3d01 100644 --- a/src/dir.rs +++ b/src/dir.rs @@ -61,11 +61,6 @@ impl PathDir { /// /// If the path is actually a file returns `io::ErrorKind::InvalidInput`. /// - /// > This does not call [`Path::cannonicalize()`][1], instead trusting that the input is - /// > already a fully qualified path. - /// - /// [1]: https://doc.rust-lang.org/std/path/struct.Path.html?search=#method.canonicalize - /// /// # Examples /// ```rust /// # extern crate path_abs; @@ -172,10 +167,6 @@ impl PathDir { /// List the contents of the directory, returning an iterator of `PathType`s. /// - /// > **Warning**: because `PathAbs` is the canonicalized path, symlinks are always resolved. - /// > This means that if the directory contains a symlink you may get a path from a completely - /// > _different directory_. - /// /// # Examples /// ```rust /// # extern crate path_abs; @@ -287,21 +278,26 @@ impl PathDir { /// let dir = PathDir::create(example)?; /// let file = PathFile::create(dir.join("example.txt"))?; /// - /// dir.symlink(example_sym)?; - /// let dir_sym = PathDir::new(example_sym)?; + /// let dir_sym = dir.symlink(example_sym)?; + /// + /// // They have a different "absolute path" + /// assert_ne!(dir, dir_sym); + /// + /// // But they can be canonicalized to the same file. + /// let dir_can = dir_sym.canonicalize()?; + /// assert_eq!(dir, dir_can); /// - /// // They are canonicalized to the same file. - /// assert_eq!(dir, dir_sym); /// # Ok(()) } fn main() { try_main().unwrap() } /// ``` - pub fn symlink>(&self, dst: P) -> Result<()> { + pub fn symlink>(&self, dst: P) -> Result { symlink_dir(&self, &dst).map_err(|err| { Error::new( err, - &format!("linking to {} from", dst.as_ref().display()), + &format!("linking from {} to", dst.as_ref().display()), self.clone().into(), ) - }) + })?; + PathDir::new(dst) } /// Return a reference to a basic `std::path::Path` @@ -309,6 +305,16 @@ impl PathDir { self.as_ref() } + /// Returns the canonical form of the path with all intermediate components normalized and + /// symbolic links resolved. + /// + /// See [`PathAbs::canonicalize`] + /// + /// [`PathAbs::canonicalize`]: struct.PathAbs.html#method.canonicalize + pub fn canonicalize(&self) -> Result { + Ok(PathDir(self.0.canonicalize()?)) + } + /// Create a mock dir type. *For use in tests only*. /// /// See the docs for [`PathAbs::mock`](struct.PathAbs.html#method.mock) diff --git a/src/edit.rs b/src/edit.rs index 9d58d22..2e00d5d 100644 --- a/src/edit.rs +++ b/src/edit.rs @@ -52,7 +52,7 @@ impl FileEdit { Ok(FileEdit(FileOpen::open(path, options)?)) } - /// Shortcut to open the file if the path is already canonicalized. + /// Shortcut to open the file if the path is already absolute. pub(crate) fn open_path(path: PathFile, mut options: fs::OpenOptions) -> Result { options.write(true); options.read(true); diff --git a/src/file.rs b/src/file.rs index dc274f4..2abdd9d 100644 --- a/src/file.rs +++ b/src/file.rs @@ -41,11 +41,6 @@ impl PathFile { /// /// If the path is actually a dir returns `io::ErrorKind::InvalidInput`. /// - /// > This does not call [`Path::cannonicalize()`][1], instead trusting that the input is - /// > already a fully qualified path. - /// - /// [1]: https://doc.rust-lang.org/std/path/struct.Path.html?search=#method.canonicalize - /// /// # Examples /// ```rust /// # extern crate path_abs; @@ -375,21 +370,25 @@ impl PathFile { /// /// let contents = "This is some contents"; /// file.write_str(contents); - /// file.symlink(example_sym)?; - /// let file_sym = PathFile::new(example_sym)?; + /// let file_sym = file.symlink(example_sym)?; /// - /// // They are canonicalized to the same file. - /// assert_eq!(file, file_sym); + /// // They have a different "absolute path" + /// assert_ne!(file, file_sym); + /// + /// // But they can be canonicalized to the same file. + /// let file_can = file_sym.canonicalize()?; + /// assert_eq!(file, file_can); /// # Ok(()) } fn main() { try_main().unwrap() } /// ``` - pub fn symlink>(&self, dst: P) -> Result<()> { + pub fn symlink>(&self, dst: P) -> Result { symlink_file(&self, &dst).map_err(|err| { Error::new( err, - &format!("linking to {} from", dst.as_ref().display()), + &format!("linking from {} to", dst.as_ref().display()), self.clone().into(), ) - }) + })?; + PathFile::new(dst) } /// Remove (delete) the file from the filesystem, consuming self. @@ -424,6 +423,16 @@ impl PathFile { self.as_ref() } + /// Returns the canonical form of the path with all intermediate components normalized and + /// symbolic links resolved. + /// + /// See [`PathAbs::canonicalize`] + /// + /// [`PathAbs::canonicalize`]: struct.PathAbs.html#method.canonicalize + pub fn canonicalize(&self) -> Result { + Ok(PathFile(self.0.canonicalize()?)) + } + /// Create a mock file type. *For use in tests only*. /// /// See the docs for [`PathAbs::mock`](struct.PathAbs.html#method.mock) diff --git a/src/lib.rs b/src/lib.rs index 92a0263..9cc6f97 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,9 +12,10 @@ //! //! This includes: //! +//! - Cleaner _absolute_ paths (which is distinct from canonicalized paths). //! - Improved error messages, see the **Better Errors** section. -//! - Improved type safety. The types specify that a file _once_ existed and was _once_ a certain -//! type. Obviously a file/directory can be deleted/changed by another process. +//! - Improved type safety. The types specify that a file/dir _once_ existed and was _once_ a certain +//! type. Obviously a file/dir can be deleted/changed by another process. //! - More stringent mutability requirements. See the **Differing Method Signatures** section. //! - Cheap cloning: all path types are `Arc`, which a cheap operation compared to filesystem //! operations and allows more flexibility and ergonomics in the library for relatively low cost. @@ -22,8 +23,8 @@ //! ## Better Errors //! //! All errors include the **path** and **action** which caused the error, as well as the unaltered -//! `std::io::Error` message. Errors are `std::io::Error`, giving almost complete compatibility -//! with existing code. +//! `std::io::Error` message. Errors are convertable into `std::io::Error`, giving almost complete +//! compatibility with existing code. //! //! ### `set_len` (i.e. truncate a file): //! @@ -57,12 +58,12 @@ //! - [`PathArc`](struct.PathArc.html): a reference counted `PathBuf` with methods reimplemented //! with better error messages. Use this for a generic serializable path that may or may //! not exist. -//! - [`PathAbs`](struct.PathAbs.html): a reference counted absolute (canonicalized) path that is -//! guaranteed (on initialization) to exist. -//! - [`PathFile`](struct.PathFile.html): a `PathAbs` that is guaranteed to be a file, with -//! associated methods. -//! - [`PathDir`](struct.PathDir.html): a `PathAbs` that is guaranteed to be a directory, with -//! associated methods. +//! - [`PathAbs`](struct.PathAbs.html): a reference counted absolute (_not necessarily_ +//! canonicalized) path that is not necessarily guaranteed to exist. +//! - [`PathFile`](struct.PathFile.html): a `PathAbs` that is guaranteed (at instantiation) to +//! exist and be a file, with associated methods. +//! - [`PathDir`](struct.PathDir.html): a `PathAbs` that is guaranteed (at instantiation) to exist +//! and be a directory, with associated methods. //! - [`PathType`](struct.PathType.html): an enum containing either a PathFile or a PathDir. //! Returned by [`PathDir::list`][dir_list] //! @@ -71,7 +72,7 @@ //! that crate for more details on how the resulting encoding can be edited (by hand) even in the //! case of what *would be* ill-formed UTF-16. //! -//! [dir_list]: struct.PathDir.html#method.list) +//! [dir_list]: struct.PathDir.html#method.list //! //! //! ## Exported File Types @@ -117,7 +118,7 @@ //! use std::path::Path; //! use std::collections::HashSet; //! use path_abs::{ -//! PathAbs, // absolute path that exists +//! PathAbs, // absolute path //! PathDir, // absolute path to a directory //! PathFile, // absolute path to a file //! PathType, // enum of Dir or File @@ -239,7 +240,7 @@ mod write; mod read; pub use abs::PathAbs; -pub use arc::PathArc; +pub use arc::{PathArc, current_dir}; pub use dir::{ListDir, PathDir}; pub use file::PathFile; pub use ty::PathType; @@ -372,7 +373,6 @@ mod tests { } #[test] - /// Tests to make sure the error messages look like we expect. fn sanity_errors() { let tmp_dir = TempDir::new("example").expect("create temp dir"); diff --git a/src/open.rs b/src/open.rs index 7706284..ce94510 100644 --- a/src/open.rs +++ b/src/open.rs @@ -36,7 +36,7 @@ impl FileOpen { }) } - /// Shortcut to open the file if the path is already canonicalized. + /// Shortcut to open the file if the path is already absolute. /// /// Typically you should use `PathFile::open` instead (i.e. `file.open(options)` or /// `file.read()`). diff --git a/src/read.rs b/src/read.rs index 3f40e9e..51f1d46 100644 --- a/src/read.rs +++ b/src/read.rs @@ -51,7 +51,7 @@ impl FileRead { Ok(FileRead(FileOpen::open(path, options)?)) } - /// Shortcut to open the file if the path is already canonicalized. + /// Shortcut to open the file if the path is already absolute. pub(crate) fn read_path(path: PathFile) -> Result { let mut options = fs::OpenOptions::new(); options.read(true); diff --git a/src/ty.rs b/src/ty.rs index 4de79e5..90e6c4c 100644 --- a/src/ty.rs +++ b/src/ty.rs @@ -5,10 +5,9 @@ * http://opensource.org/licenses/MIT>, at your option. This file may not be * copied, modified, or distributed except according to those terms. */ -use std::io; use std_prelude::*; -use super::{Error, Result}; +use super::Result; use super::{PathAbs, PathArc, PathDir, PathFile}; #[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] @@ -20,8 +19,7 @@ use super::{PathAbs, PathArc, PathDir, PathFile}; /// - The items returned from `PathDir::list` /// - Serializing paths of different types. /// -/// > Note: symlinks are not supported because they are -/// > *impossible* for canonicalized paths. +/// Note that for symlinks, this returns the underlying file type. pub enum PathType { File(PathFile), Dir(PathDir), @@ -54,11 +52,7 @@ impl PathType { } else if ty.is_dir() { Ok(PathType::Dir(PathDir(abs))) } else { - Err(Error::new( - io::Error::new(io::ErrorKind::InvalidInput, "path is not a dir or a file"), - "resolving", - abs.into(), - )) + unreachable!("rust docs: The fs::metadata function follows symbolic links") } } diff --git a/src/write.rs b/src/write.rs index 39509f7..0ef5e92 100644 --- a/src/write.rs +++ b/src/write.rs @@ -49,7 +49,7 @@ impl FileWrite { Ok(FileWrite(FileOpen::open(path, options)?)) } - /// Shortcut to open the file if the path is already canonicalized. + /// Shortcut to open the file if the path is already absolute. pub(crate) fn open_path( path_file: PathFile, mut options: fs::OpenOptions, diff --git a/tests/test_absolute.rs b/tests/test_absolute.rs new file mode 100644 index 0000000..ee8f743 --- /dev/null +++ b/tests/test_absolute.rs @@ -0,0 +1,75 @@ +//! The absolute paths have some gotchas that need to be tested. +//! +//! - Using the current working directory +//! - `..` paths that consume the "root" + +extern crate path_abs; +extern crate tempdir; +#[macro_use] +extern crate pretty_assertions; + +use path_abs::*; +use std::path::Path; +use std::env; + +#[test] +fn test_absolute() { + let tmp = tempdir::TempDir::new("ex").unwrap(); + let tmp = tmp.path(); + let tmp_abs = PathArc::new(&tmp).canonicalize().unwrap(); + env::set_current_dir(&tmp_abs).unwrap(); + + // Create directory like: + // a/ + // + e/ -> b/c/d + // + b/ + // + c/ + // + d/ + + let a = PathDir::create(&tmp.join("a")).unwrap(); + let b = PathDir::create(&a.join("b")).unwrap(); + let c = PathDir::create(&b.join("c")).unwrap(); + let d = PathDir::create(&c.join("d")).unwrap(); + + // create symbolic link from a/e -> a/b/c/d + let e_sym = d.symlink(&a.join("e")).unwrap(); + let ty = e_sym.symlink_metadata().unwrap().file_type(); + assert!(ty.is_symlink(), "{}", e_sym.display()); + + assert_ne!(d, e_sym); + assert_eq!(d, e_sym.canonicalize().unwrap()); + + let a_cwd = Path::new("a"); + let b_cwd = a.join("b"); + let c_cwd = b.join("c"); + let d_cwd = c.join("d"); + let e_cwd = a.join("e"); + + assert_eq!(a, PathDir::new(&a_cwd).unwrap()); + assert_eq!(b, PathDir::new(&b_cwd).unwrap()); + assert_eq!(c, PathDir::new(&c_cwd).unwrap()); + assert_eq!(d, PathDir::new(&d_cwd).unwrap()); + assert_eq!(e_sym, PathDir::new(&e_cwd).unwrap()); + + assert_eq!(b, PathDir::new(c.join("..")).unwrap()); + assert_eq!(a, PathDir::new(c.join("..").join("..")).unwrap()); + // just create a PathType + let _ = PathType::new(&e_sym).unwrap(); + + let mut root_dots = tmp_abs.to_path_buf(); + let mut dots = tmp_abs.components().count() - 1; + if cfg!(windows) { + // windows has _two_ "roots", prefix _and_ "root". + dots -= 1; + } + for _ in 0..dots { + root_dots.push(".."); + } + let root = PathDir::new(root_dots).unwrap(); + if cfg!(windows) { + assert_eq!(PathDir::new("\\").unwrap(), root); + } else { + assert_eq!(PathDir::new("/").unwrap(), root); + } + assert!(PathDir::new(root.join("..")).is_err()); +}