relative-path

github crates.io docs.rs build status

Portable relative UTF-8 paths for Rust.

This crate provides a module analogous to [std::path], with the following characteristics:

On top of this we support many operations that guarantee the same behavior across platforms.


Usage

Add relative-path to your Cargo.toml:

toml relative-path = "1.9.0"

Start using relative paths:

```rust use serde::{Serialize, Deserialize}; use relative_path::RelativePath;

[derive(Serialize, Deserialize)]

struct Manifest<'a> { #[serde(borrow)] source: &'a RelativePath, }

```


Serde Support

This library includes serde support that can be enabled with the serde feature.


Why is std::path a portability hazard?

Path representations differ across platforms.

If we use PathBuf, Storing paths in a manifest would allow our application to build and run on one platform but potentially not others.

Consider the following data model and corresponding toml for a manifest:

```rust use std::path::PathBuf;

use serde::{Serialize, Deserialize};

[derive(Serialize, Deserialize)]

struct Manifest { source: PathBuf, } ```

toml source = "C:\\Users\\udoprog\\repo\\data\\source"

This will run for you (assuming source exists). So you go ahead and check the manifest into git. The next day your Linux colleague calls you and wonders what they have ever done to wrong you?

So what went wrong? Well two things. You forgot to make the source relative, so anyone at the company which has a different username than you won't be able to use it. So you go ahead and fix that:

toml source = "data\\source"

But there is still one problem! A backslash (\) is only a legal path separator on Windows. Luckily you learn that forward slashes are supported both on Windows and Linux. So you opt for:

toml source = "data/source"

Things are working now. So all is well... Right? Sure, but we can do better.

This crate provides types that work with portable relative paths (hence the name). So by using [RelativePath] we can systematically help avoid portability issues like the one above. Avoiding issues at the source is preferably over spending 5 minutes of onboarding time on a theoretical problem, hoping that your new hires will remember what to do if they ever encounter it.

Using [RelativePathBuf] we can fix our data model like this:

```rust use relative_path::RelativePathBuf; use serde::{Serialize, Deserialize};

[derive(Serialize, Deserialize)]

pub struct Manifest { source: RelativePathBuf, } ```

And where it's used:

```rust use std::fs; use std::env::current_dir;

let manifest: Manifest = todo!();

let root = currentdir()?; let source = manifest.source.topath(&root); let content = fs::read(&source)?; ```


Overview

Conversion to a platform-specific [Path] happens through the [to_path] and [to_logical_path] functions. Where you are required to specify the path that prefixes the relative path. This can come from a function such as [std::env::current_dir].

```rust use std::env::current_dir; use std::path::Path;

use relative_path::RelativePath;

let root = current_dir()?;

// topath unconditionally concatenates a relative path with its base: let relativepath = RelativePath::new("../foo/./bar"); let fullpath = relativepath.topath(&root); asserteq!(full_path, root.join("..\foo\.\bar"));

// tologicalpath tries to apply the logical operations that the relative // path corresponds to: let relativepath = RelativePath::new("../foo/./bar"); let fullpath = relativepath.tological_path(&root);

// Replicate the operation performed by to_logical_path. let mut parent = root.clone(); parent.pop(); asserteq!(fullpath, parent.join("foo\bar")); ```

When two relative paths are compared to each other, their exact component makeup determines equality.

```rust use relative_path::RelativePath;

assert_ne!( RelativePath::new("foo/bar/../baz"), RelativePath::new("foo/baz") ); ```

Using platform-specific path separators to construct relative paths is not supported.

Path separators from other platforms are simply treated as part of a component:

```rust use relative_path::RelativePath;

assert_ne!( RelativePath::new("foo/bar"), RelativePath::new("foo\bar") );

asserteq!(1, RelativePath::new("foo\bar").components().count()); asserteq!(2, RelativePath::new("foo/bar").components().count()); ```

To see if two relative paths are equivalent you can use [normalize]:

```rust use relative_path::RelativePath;

assert_eq!( RelativePath::new("foo/bar/../baz").normalize(), RelativePath::new("foo/baz").normalize(), ); ```


Additional portability notes

While relative paths avoid the most egregious portability issue, that absolute paths will work equally unwell on all platforms. We cannot avoid all. This section tries to document additional portability hazards that we are aware of.

[RelativePath], similarly to [Path], makes no guarantees that its constituent components make up legal file names. While components are strictly separated by slashes, we can still store things in them which may not be used as legal paths on all platforms.

A relative path that accidentally contains a platform-specific components will largely result in a nonsensical paths being generated in the hope that they will fail fast during development and testing.

```rust use relative_path::{RelativePath, PathExt}; use std::path::Path;

if cfg!(windows) { asserteq!( Path::new("foo\c:\bar\baz"), RelativePath::new("c:\bar\baz").topath("foo") ); }

if cfg!(unix) { asserteq!( Path::new("foo/bar/baz"), RelativePath::new("/bar/baz").topath("foo") ); }

asserteq!( Path::new("foo").relativeto("bar")?, RelativePath::new("../foo"), ); ```