context: add ContextDriver::is_dir
list_files returns context-relative paths (rooted inside the chroot for an unshare context, on the remote for ssh): whether an entry is a directory can only be decided through the context, never with a host-side stat. Give every driver a proper is_dir rather than approximating it with exists, so callers can keep directories and files apart — the deb package-directory search lists directories only.
This commit is contained in:
@@ -70,6 +70,12 @@ pub trait ContextDriver {
|
|||||||
fn read_file(&self, path: &Path) -> io::Result<String>;
|
fn read_file(&self, path: &Path) -> io::Result<String>;
|
||||||
fn write_file(&self, path: &Path, content: &str) -> io::Result<()>;
|
fn write_file(&self, path: &Path, content: &str) -> io::Result<()>;
|
||||||
fn exists(&self, path: &Path) -> io::Result<bool>;
|
fn exists(&self, path: &Path) -> io::Result<bool>;
|
||||||
|
/// Check if a path is a directory inside the context
|
||||||
|
///
|
||||||
|
/// Distinct from [`ContextDriver::exists`] because paths returned by
|
||||||
|
/// [`ContextDriver::list_files`] are context-relative and can only be
|
||||||
|
/// classified through the context, never with a host-side stat.
|
||||||
|
fn is_dir(&self, path: &Path) -> io::Result<bool>;
|
||||||
|
|
||||||
/// Clean up any resources held by the driver (e.g. unmount overlay filesystems).
|
/// Clean up any resources held by the driver (e.g. unmount overlay filesystems).
|
||||||
/// Called before the chroot directory is removed.
|
/// Called before the chroot directory is removed.
|
||||||
@@ -313,6 +319,15 @@ impl Context {
|
|||||||
self.driver().as_ref().unwrap().exists(path)
|
self.driver().as_ref().unwrap().exists(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if a path is a directory inside context
|
||||||
|
///
|
||||||
|
/// Paths returned by [`Context::list_files`] are context-relative
|
||||||
|
/// (e.g. rooted inside the chroot for an unshare context): whether they
|
||||||
|
/// are directories can only be decided through the context.
|
||||||
|
pub fn is_dir(&self, path: &Path) -> io::Result<bool> {
|
||||||
|
self.driver().as_ref().unwrap().is_dir(path)
|
||||||
|
}
|
||||||
|
|
||||||
/// Clean up any resources held by the driver (e.g. unmount overlay filesystems).
|
/// Clean up any resources held by the driver (e.g. unmount overlay filesystems).
|
||||||
/// Called before the chroot directory is removed.
|
/// Called before the chroot directory is removed.
|
||||||
pub fn cleanup(&self) -> io::Result<()> {
|
pub fn cleanup(&self) -> io::Result<()> {
|
||||||
|
|||||||
@@ -155,6 +155,10 @@ impl ContextDriver for LocalDriver {
|
|||||||
fn exists(&self, path: &Path) -> io::Result<bool> {
|
fn exists(&self, path: &Path) -> io::Result<bool> {
|
||||||
Ok(path.exists())
|
Ok(path.exists())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_dir(&self, path: &Path) -> io::Result<bool> {
|
||||||
|
Ok(path.is_dir())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn copy_dir_recursive(src: &Path, dest: &Path) -> io::Result<()> {
|
fn copy_dir_recursive(src: &Path, dest: &Path) -> io::Result<()> {
|
||||||
|
|||||||
@@ -385,6 +385,40 @@ mod tests {
|
|||||||
assert!(!dest.join("src/.svn").exists());
|
assert!(!dest.join("src/.svn").exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The unshare driver maps context-relative paths onto the chroot root
|
||||||
|
/// on the host: `is_dir` must answer through that mapping (a host-side
|
||||||
|
/// stat of the unmapped path sees nothing), which is what lets the deb
|
||||||
|
/// package-directory search classify staged entries.
|
||||||
|
#[test]
|
||||||
|
fn test_unshare_is_dir_maps_through_the_chroot_root() {
|
||||||
|
let chroot = tempfile::tempdir().unwrap();
|
||||||
|
fs::create_dir_all(chroot.path().join("tmp/work/tree/debian")).unwrap();
|
||||||
|
fs::write(chroot.path().join("tmp/work/orig.tar.xz"), "tar").unwrap();
|
||||||
|
|
||||||
|
let base = Context::new(ContextConfig::Local).unwrap();
|
||||||
|
let ctx = Context::with_parent(
|
||||||
|
ContextConfig::Unshare {
|
||||||
|
path: chroot.path().to_string_lossy().to_string(),
|
||||||
|
parent: None,
|
||||||
|
},
|
||||||
|
Arc::new(base),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(ctx.is_dir(std::path::Path::new("/tmp/work/tree")).unwrap());
|
||||||
|
assert!(
|
||||||
|
ctx.exists(std::path::Path::new("/tmp/work/tree/debian"))
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!ctx.is_dir(std::path::Path::new("/tmp/work/orig.tar.xz"))
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!ctx.exists(std::path::Path::new("/tmp/work/missing"))
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// The overlay-mount path exposes the tree verbatim, so pruning happens
|
/// The overlay-mount path exposes the tree verbatim, so pruning happens
|
||||||
/// after the fact: nested VCS metadata must be removed recursively.
|
/// after the fact: nested VCS metadata must be removed recursively.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -296,6 +296,16 @@ impl ContextDriver for SchrootDriver {
|
|||||||
)?;
|
)?;
|
||||||
Ok(status.success())
|
Ok(status.success())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_dir(&self, path: &Path) -> io::Result<bool> {
|
||||||
|
let status = self.run(
|
||||||
|
"test",
|
||||||
|
&["-d".to_string(), path.to_string_lossy().to_string()],
|
||||||
|
&[],
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
Ok(status.success())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -306,6 +306,14 @@ impl ContextDriver for SshDriver {
|
|||||||
Err(_) => Ok(false),
|
Err(_) => Ok(false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_dir(&self, path: &Path) -> io::Result<bool> {
|
||||||
|
let sess = connect_ssh(&self.host, self.user.as_deref(), self.port)?;
|
||||||
|
let sftp = sess.sftp().map_err(io::Error::other)?;
|
||||||
|
// Same error tolerance as `exists`: an unreachable path is not a
|
||||||
|
// directory, and the caller decides what absence means.
|
||||||
|
Ok(sftp.stat(path).map(|stat| stat.is_dir()).unwrap_or(false))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SshDriver {
|
impl SshDriver {
|
||||||
|
|||||||
@@ -356,6 +356,11 @@ impl ContextDriver for UnshareDriver {
|
|||||||
let host_path = Path::new(&self.path).join(path.to_string_lossy().trim_start_matches('/'));
|
let host_path = Path::new(&self.path).join(path.to_string_lossy().trim_start_matches('/'));
|
||||||
self.parent().exists(&host_path)
|
self.parent().exists(&host_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_dir(&self, path: &Path) -> io::Result<bool> {
|
||||||
|
let host_path = Path::new(&self.path).join(path.to_string_lossy().trim_start_matches('/'));
|
||||||
|
Ok(host_path.is_dir())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UnshareDriver {
|
impl UnshareDriver {
|
||||||
|
|||||||
@@ -445,6 +445,10 @@ mod imp {
|
|||||||
self.inner.exists(path)
|
self.inner.exists(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_dir(&self, path: &Path) -> io::Result<bool> {
|
||||||
|
self.inner.is_dir(path)
|
||||||
|
}
|
||||||
|
|
||||||
fn cleanup(&self) -> io::Result<()> {
|
fn cleanup(&self) -> io::Result<()> {
|
||||||
self.inner.cleanup()
|
self.inner.cleanup()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user