Skip to content

Commit

Permalink
Merge #11
Browse files Browse the repository at this point in the history
11: WIP #6: add absolute() method on PathArc, still need tests r=vitiral a=vitiral

This is a first attempt at implementing #6.
  • Loading branch information
bors[bot] committed Mar 23, 2018
2 parents 81c3eab + 946d726 commit f0df58c
Show file tree
Hide file tree
Showing 14 changed files with 366 additions and 63 deletions.
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 ###
Expand Down Expand Up @@ -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
4 changes: 2 additions & 2 deletions src/abs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -31,7 +31,7 @@ impl PathAbs {
/// ```
pub fn new<P: AsRef<Path>>(path: P) -> Result<PathAbs> {
let arc = PathArc::new(path);
arc.canonicalize()
arc.absolute()
}

/// Resolve the `PathAbs` as a `PathFile`. Return an error if it is not a file.
Expand Down
222 changes: 220 additions & 2 deletions src/arc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathArc> {
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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<PathAbs> {
let mut components = self.components();
let mut stack: Vec<OsString> = 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 {
Expand Down Expand Up @@ -230,3 +310,141 @@ impl Into<PathBuf> 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<OsString>,
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: AsRef<Path>>(p: P) -> Result<PathAbs> {
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());
}
38 changes: 22 additions & 16 deletions src/dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -287,28 +278,43 @@ 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<P: AsRef<Path>>(&self, dst: P) -> Result<()> {
pub fn symlink<P: AsRef<Path>>(&self, dst: P) -> Result<PathDir> {
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`
pub fn as_path(&self) -> &Path {
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<PathDir> {
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)
Expand Down
2 changes: 1 addition & 1 deletion src/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<FileEdit> {
options.write(true);
options.read(true);
Expand Down
Loading

0 comments on commit f0df58c

Please sign in to comment.