Files
uv/crates/uv-extract/src/sync.rs
T
konsti 4f4492dd53 Add hint for venv in source distribution error (#15202)
Venvs should not be in source distributions, and on Unix, we now reject
them for having a link outside the source directory. This PR adds a hint
for that since users were confused (#15096).

In the process, we're differentiating IO errors for format error for
uncompression generally.

Fixes #15096
2025-08-18 22:07:57 +00:00

116 lines
4.4 KiB
Rust

use std::path::{Path, PathBuf};
use std::sync::{LazyLock, Mutex};
use crate::Error;
use crate::vendor::{CloneableSeekableReader, HasLength};
use rayon::prelude::*;
use rustc_hash::FxHashSet;
use tracing::warn;
use uv_configuration::RAYON_INITIALIZE;
use zip::ZipArchive;
/// Unzip a `.zip` archive into the target directory.
pub fn unzip<R: Send + std::io::Read + std::io::Seek + HasLength>(
reader: R,
target: &Path,
) -> Result<(), Error> {
// Unzip in parallel.
let reader = std::io::BufReader::new(reader);
let archive = ZipArchive::new(CloneableSeekableReader::new(reader))?;
let directories = Mutex::new(FxHashSet::default());
// Initialize the threadpool with the user settings.
LazyLock::force(&RAYON_INITIALIZE);
(0..archive.len())
.into_par_iter()
.map(|file_number| {
let mut archive = archive.clone();
let mut file = archive.by_index(file_number)?;
// Determine the path of the file within the wheel.
let Some(enclosed_name) = file.enclosed_name() else {
warn!("Skipping unsafe file name: {}", file.name());
return Ok(());
};
// Create necessary parent directories.
let path = target.join(enclosed_name);
if file.is_dir() {
let mut directories = directories.lock().unwrap();
if directories.insert(path.clone()) {
fs_err::create_dir_all(path).map_err(Error::Io)?;
}
return Ok(());
}
if let Some(parent) = path.parent() {
let mut directories = directories.lock().unwrap();
if directories.insert(parent.to_path_buf()) {
fs_err::create_dir_all(parent).map_err(Error::Io)?;
}
}
// Copy the file contents.
let outfile = fs_err::File::create(&path).map_err(Error::Io)?;
let size = file.size();
if size > 0 {
let mut writer = if let Ok(size) = usize::try_from(size) {
std::io::BufWriter::with_capacity(std::cmp::min(size, 1024 * 1024), outfile)
} else {
std::io::BufWriter::new(outfile)
};
std::io::copy(&mut file, &mut writer).map_err(Error::io_or_compression)?;
}
// See `uv_extract::stream::unzip`. For simplicity, this is identical with the code there except for being
// sync.
#[cfg(unix)]
{
use std::fs::Permissions;
use std::os::unix::fs::PermissionsExt;
if let Some(mode) = file.unix_mode() {
// https://github.com/pypa/pip/blob/3898741e29b7279e7bffe044ecfbe20f6a438b1e/src/pip/_internal/utils/unpacking.py#L88-L100
let has_any_executable_bit = mode & 0o111;
if has_any_executable_bit != 0 {
let permissions = fs_err::metadata(&path).map_err(Error::Io)?.permissions();
if permissions.mode() & 0o111 != 0o111 {
fs_err::set_permissions(
&path,
Permissions::from_mode(permissions.mode() | 0o111),
)
.map_err(Error::Io)?;
}
}
}
}
Ok(())
})
.collect::<Result<_, Error>>()
}
/// Extract the top-level directory from an unpacked archive.
///
/// The specification says:
/// > A .tar.gz source distribution (sdist) contains a single top-level directory called
/// > `{name}-{version}` (e.g. foo-1.0), containing the source files of the package.
///
/// This function returns the path to that top-level directory.
pub fn strip_component(source: impl AsRef<Path>) -> Result<PathBuf, Error> {
// TODO(konstin): Verify the name of the directory.
let top_level = fs_err::read_dir(source.as_ref())
.map_err(Error::Io)?
.collect::<std::io::Result<Vec<fs_err::DirEntry>>>()
.map_err(Error::Io)?;
match top_level.as_slice() {
[root] => Ok(root.path()),
[] => Err(Error::EmptyArchive),
_ => Err(Error::NonSingularArchive(
top_level
.into_iter()
.map(|entry| entry.file_name())
.collect(),
)),
}
}