Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add follow_symlinks option #10

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 52 additions & 2 deletions src/validation/filesystem.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use super::path::normalize_path;
use crate::validation::{Context, Reason};
use std::{
collections::HashMap,
Expand Down Expand Up @@ -135,6 +136,7 @@ pub struct Options {
root_directory: Option<PathBuf>,
default_file: OsString,
links_may_traverse_the_root_directory: bool,
follow_symlinks: bool,
// Note: the key is normalised to lowercase to make sure extensions are
// case insensitive
alternate_extensions: HashMap<String, Vec<OsString>>,
Expand Down Expand Up @@ -165,6 +167,7 @@ impl Options {
root_directory: None,
default_file: OsString::from(Options::DEFAULT_FILE),
links_may_traverse_the_root_directory: false,
follow_symlinks: true,
alternate_extensions: Options::default_alternate_extensions()
.into_iter()
.map(|(key, values)| {
Expand Down Expand Up @@ -254,6 +257,17 @@ impl Options {
}
}

/// Set [`Options::follow_symlinks()`].
pub fn set_follow_symlinks(
self,
value: bool,
) -> Self {
Options {
follow_symlinks: value,
..self
}
}

/// Set a function which will be executed after a link is resolved, allowing
/// you to apply custom business logic.
pub fn set_custom_validation<F>(self, custom_validation: F) -> Self
Expand Down Expand Up @@ -313,7 +327,12 @@ impl Options {
///
/// This will fail if the item doesn't exist.
fn canonicalize(&self, path: &Path) -> Result<PathBuf, Reason> {
let mut canonical = dunce::canonicalize(path)?;
let f = |p| match self.follow_symlinks {
true => dunce::canonicalize(p),
false => Ok(normalize_path(p)),
};

let mut canonical = f(path)?;

if canonical.is_dir() {
log::trace!(
Expand All @@ -324,7 +343,9 @@ impl Options {
canonical.push(&self.default_file);
// we need to canonicalize again because the default file may be a
// symlink, or not exist at all
canonical = dunce::canonicalize(canonical)?;
if self.follow_symlinks || !canonical.exists() {
canonical = dunce::canonicalize(canonical)?;
}
}

Ok(canonical)
Expand Down Expand Up @@ -406,6 +427,7 @@ impl Debug for Options {
root_directory,
default_file,
links_may_traverse_the_root_directory,
follow_symlinks,
alternate_extensions,
custom_validation: _,
} = self;
Expand All @@ -417,6 +439,7 @@ impl Debug for Options {
"links_may_traverse_the_root_directory",
links_may_traverse_the_root_directory,
)
.field( "follow_symlinks", follow_symlinks)
.field("alternate_extensions", alternate_extensions)
.finish()
}
Expand All @@ -428,6 +451,7 @@ impl PartialEq for Options {
root_directory,
default_file,
links_may_traverse_the_root_directory,
follow_symlinks,
alternate_extensions,
custom_validation: _,
} = self;
Expand All @@ -436,6 +460,7 @@ impl PartialEq for Options {
&& default_file == &other.default_file
&& links_may_traverse_the_root_directory
== &other.links_may_traverse_the_root_directory
&& follow_symlinks == &other.follow_symlinks
&& alternate_extensions == &other.alternate_extensions
}
}
Expand Down Expand Up @@ -607,6 +632,31 @@ mod tests {
assert_eq!(got, bar.join("index.html"));
}

#[test]
#[cfg(unix)]
fn a_symlink_from_root_tree_outside_is_not_resolved() {
use std::os::unix::fs;

init_logging();
let temp = tempfile::tempdir().unwrap();
let temp = dunce::canonicalize(temp.path()).unwrap();
let foo = temp.join("foo");
let bar = temp.join("bar");
touch(Options::DEFAULT_FILE, &[&temp, &foo]);
touch(Options::DEFAULT_FILE, &[&temp, &bar]);
fs::symlink("../bar/index.html",foo.join("link.html").as_path()).unwrap();
let options = Options::default()
.with_root_directory(&foo)
.unwrap()
.set_links_may_traverse_the_root_directory(false)
.set_follow_symlinks(false);
let link = Path::new("link.html");

let got = resolve_link(&foo, link, &options).unwrap();

assert_eq!(got, foo.join("link.html"));
}

#[test]
fn markdown_files_can_be_used_as_html() {
init_logging();
Expand Down
1 change: 1 addition & 0 deletions src/validation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod cache;
mod context;
mod filesystem;
mod web;
mod path;

pub use cache::{Cache, CacheEntry};
pub use context::{BasicContext, Context};
Expand Down
37 changes: 37 additions & 0 deletions src/validation/path.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@

use std::path::{Path, PathBuf, Component};

/// Normalize a path, removing things like `.` and `..`.
///
/// CAUTION: This does not resolve symlinks (unlike
/// [`std::fs::canonicalize`]). This may cause incorrect or surprising
/// behavior at times. This should be used carefully. Unfortunately,
/// [`std::fs::canonicalize`] can be hard to use correctly, since it can often
/// fail, or on Windows returns annoying device paths. This is a problem Cargo
/// needs to improve on.
pub fn normalize_path(path: &Path) -> PathBuf {
Copy link
Author

@Esgariot Esgariot Jul 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

taken from cargo crate, should probably include some copyright notice

cargo paths.rs

let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};

for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}