Moving and renaming directory

Hey guys,

Beware: This might be/is a XY-Problem. More of that below.

I'm trying to move a folder from one place to another.
Let's say, I want move from /tmp/tmp.xxx/bar to /opt/comp/baz.

std::fs::rename does not work accross filesystem boundaries. So I searched for a crate that can do it for me.

I found fs_extra, which is somewhat outdated, but should work. But, it doesn't exactly do what I was hoping. Instead it moves the folder bar into baz, instead of renaming it.

Is there a way for me to do it?


XY-Explanation:

I have a tar archive which has always a folder (called bar) and I want to unpack it and rename it under its version (e.g 0.54.8). The tar crate doesn't let me remove the top level folder.
I don't want it to place under /opt/company and then rename it, because of race conditions. That's why I use the tempfile crate to create a temporary directory and then move it.

So I see two options: Either try to extract it directly into the correct directory and strip the top level folder or I need a way to move and rename a folder atomically.

Any suggestsions? :slight_smile:

1 Like

Using the tar crate, you can iterate over the entries of the archive and modify their paths to remove the top level folder before unpacking them.

What about std::process::Command::new("mv").args(&["src", "dst"]).spawn().wait()?

meh :confused:
Could work, but IMHO that would be a bad solution. You would depend on an external program and even on a certain OS. But thanks for the suggestion :slight_smile:

2 Likes

https://docs.rs/tar/0.4.29/tar/struct.Entry.html

I don't see a way to set the path of the entry. I can view it, but not modify. So that won't work sadly.

Quoting the docs:

A read-only view into an entry of an archive.

I mean that you can implement your own version of Archive::unpack by iterating over the entries yourself. Something like this:

use std::{path::{Component::Normal, Path, PathBuf}, io::{Read, Result}};
use tar::Archive;

pub fn unpack_sans_parent<R, P>(mut archive: Archive<R>, dst: P) -> Result<()>
where R: Read, P: AsRef<Path>
{
    for entry in archive.entries()? {
        let mut entry = entry?;
        let path: PathBuf = entry.path()?.components()
            .skip(1) // strip top-level directory
            .filter(|c| matches!(c, Normal(_))) // prevent traversal attacks
            .collect();
        entry.unpack(dst.as_ref().join(path))?;
    }
    Ok(())
}

path.join() is unsafe, as it allows path traversal.

Documentation of the tar crate warns against implementing custom unpack due to security pitfalls, so I'd be very careful about it. Maybe start by copying tar's own code and only customize it.

1 Like

In addition to path traversal, you also have to be careful about symbolic links (symlink -> .., and a symlink/oops file).

I believe the traditional Unix solution to this problem is to ensure that your temporary directory is on the same filesystem, typically by putting it in the same directory, but with a random .hexname. It's not a great solution, and works better for files than for directories, since files are easier to delete.

2 Likes

My code does include protection against join-based traversal attacks, adapted from the code in the tar crate, though it does not protect against traversal through symlinks. (Neither does the tar crate, fully.)

To protect against symlinks already existing on the filesystem, the caller can ensure that the destination directory is empty before calling the above code, or can adapt the std::fs::canonicalize approach from the tar crate.

To protect against traversal attacks using symlinks in untrusted tar files, do not use the tar crate at all, because it does not check for these. Update: This is incorrect; see below.

1 Like

Wait, that'sā€¦ bad, since Archive::unpack claims to never write outside of dst in its doc.

I can totally see how someone may end up thinking unpack handles all the security corner cases. This should either be fixed soon or clearified in the doc.

Apparently this was fixed a while ago but the issue was not updated:

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.