diff --git a/crates/snapbox/src/dir/fixture.rs b/crates/snapbox/src/dir/fixture.rs
new file mode 100644
index 00000000..9b6b135e
--- /dev/null
+++ b/crates/snapbox/src/dir/fixture.rs
@@ -0,0 +1,114 @@
+/// Collection of files
+pub trait DirFixture: std::fmt::Debug {
+ /// Initialize a test fixture directory `root`
+ fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error>;
+}
+
+#[cfg(feature = "dir")] // for documentation purposes only
+impl DirFixture for std::path::Path {
+ fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
+ super::copy_template(self, root)
+ }
+}
+
+#[cfg(feature = "dir")] // for documentation purposes only
+impl DirFixture for &'_ std::path::Path {
+ fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
+ std::path::Path::new(self).write_to_path(root)
+ }
+}
+
+#[cfg(feature = "dir")] // for documentation purposes only
+impl DirFixture for &'_ std::path::PathBuf {
+ fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
+ std::path::Path::new(self).write_to_path(root)
+ }
+}
+
+#[cfg(feature = "dir")] // for documentation purposes only
+impl DirFixture for std::path::PathBuf {
+ fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
+ std::path::Path::new(self).write_to_path(root)
+ }
+}
+
+#[cfg(feature = "dir")] // for documentation purposes only
+impl DirFixture for str {
+ fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
+ std::path::Path::new(self).write_to_path(root)
+ }
+}
+
+#[cfg(feature = "dir")] // for documentation purposes only
+impl DirFixture for &'_ str {
+ fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
+ std::path::Path::new(self).write_to_path(root)
+ }
+}
+
+#[cfg(feature = "dir")] // for documentation purposes only
+impl DirFixture for &'_ String {
+ fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
+ std::path::Path::new(self).write_to_path(root)
+ }
+}
+
+#[cfg(feature = "dir")] // for documentation purposes only
+impl DirFixture for String {
+ fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
+ std::path::Path::new(self).write_to_path(root)
+ }
+}
+
+impl
DirFixture for &[(P, S)]
+where
+ P: AsRef,
+ P: std::fmt::Debug,
+ S: AsRef<[u8]>,
+ S: std::fmt::Debug,
+{
+ fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
+ let root = super::ops::canonicalize(root)
+ .map_err(|e| format!("Failed to canonicalize {}: {}", root.display(), e))?;
+
+ for (path, content) in self.iter() {
+ let rel_path = path.as_ref();
+ let path = root.join(rel_path);
+ let path = super::ops::normalize_path(&path);
+ if !path.starts_with(&root) {
+ return Err(crate::assert::Error::new(format!(
+ "Fixture {} is for outside of the target root",
+ rel_path.display(),
+ )));
+ }
+
+ let content = content.as_ref();
+
+ if let Some(dir) = path.parent() {
+ std::fs::create_dir_all(dir).map_err(|e| {
+ format!(
+ "Failed to create fixture directory {}: {}",
+ dir.display(),
+ e
+ )
+ })?;
+ }
+ std::fs::write(&path, content)
+ .map_err(|e| format!("Failed to write fixture {}: {}", path.display(), e))?;
+ }
+ Ok(())
+ }
+}
+
+impl DirFixture for [(P, S); N]
+where
+ P: AsRef,
+ P: std::fmt::Debug,
+ S: AsRef<[u8]>,
+ S: std::fmt::Debug,
+{
+ fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
+ let s: &[(P, S)] = self;
+ s.write_to_path(root)
+ }
+}
diff --git a/crates/snapbox/src/dir/mod.rs b/crates/snapbox/src/dir/mod.rs
index 56c84870..18ea24e7 100644
--- a/crates/snapbox/src/dir/mod.rs
+++ b/crates/snapbox/src/dir/mod.rs
@@ -1,6 +1,7 @@
//! Initialize working directories and assert on how they've changed
mod diff;
+mod fixture;
mod ops;
mod root;
#[cfg(test)]
@@ -8,6 +9,7 @@ mod tests;
pub use diff::FileType;
pub use diff::PathDiff;
+pub use fixture::DirFixture;
#[cfg(feature = "dir")]
pub use ops::copy_template;
pub use ops::resolve_dir;
diff --git a/crates/snapbox/src/dir/ops.rs b/crates/snapbox/src/dir/ops.rs
index 036a4524..d37081f6 100644
--- a/crates/snapbox/src/dir/ops.rs
+++ b/crates/snapbox/src/dir/ops.rs
@@ -172,6 +172,50 @@ pub fn strip_trailing_slash(path: &std::path::Path) -> &std::path::Path {
path.components().as_path()
}
+/// 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(crate) fn normalize_path(path: &std::path::Path) -> std::path::PathBuf {
+ use std::path::Component;
+
+ let mut components = path.components().peekable();
+ let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
+ components.next();
+ std::path::PathBuf::from(c.as_os_str())
+ } else {
+ std::path::PathBuf::new()
+ };
+
+ for component in components {
+ match component {
+ Component::Prefix(..) => unreachable!(),
+ Component::RootDir => {
+ ret.push(Component::RootDir);
+ }
+ Component::CurDir => {}
+ Component::ParentDir => {
+ if ret.ends_with(Component::ParentDir) {
+ ret.push(Component::ParentDir);
+ } else {
+ let popped = ret.pop();
+ if !popped && !ret.has_root() {
+ ret.push(Component::ParentDir);
+ }
+ }
+ }
+ Component::Normal(c) => {
+ ret.push(c);
+ }
+ }
+ }
+ ret
+}
+
pub(crate) fn display_relpath(path: impl AsRef) -> String {
let path = path.as_ref();
let relpath = if let Ok(cwd) = std::env::current_dir() {
diff --git a/crates/snapbox/src/dir/root.rs b/crates/snapbox/src/dir/root.rs
index 1981a700..d381a48c 100644
--- a/crates/snapbox/src/dir/root.rs
+++ b/crates/snapbox/src/dir/root.rs
@@ -43,21 +43,17 @@ impl DirRoot {
}
#[cfg(feature = "dir")]
- pub fn with_template(
- self,
- template_root: &std::path::Path,
- ) -> Result {
+ pub fn with_template(self, template: &F) -> Result
+ where
+ F: crate::dir::DirFixture + ?Sized,
+ {
match &self.0 {
DirRootInner::None | DirRootInner::Immutable(_) => {
return Err("Sandboxing is disabled".into());
}
DirRootInner::MutablePath(path) | DirRootInner::MutableTemp { path, .. } => {
- crate::debug!(
- "Initializing {} from {}",
- path.display(),
- template_root.display()
- );
- super::copy_template(template_root, path)?;
+ crate::debug!("Initializing {} from {:?}", path.display(), template);
+ template.write_to_path(path)?;
}
}