use async_recursion::async_recursion;
use flate2::read::GzDecoder;
use itertools::Itertools;
use std::fs;
use std::fs::File;
use std::io;
use std::io::BufReader;
use std::io::Read;
use std::io::Seek;
use std::path::Path;
use std::path::PathBuf;
use thiserror::Error;
use crate::progress::Progress;
use crate::progress::ProgressBar;
#[derive(Error, Debug)]
pub enum UnpackError {
#[error("failed to unpack source: {0}")]
Io(#[from] io::Error),
#[error("failed to unpack zip source: {0}")]
Zip(#[from] zip::result::ZipError),
#[error("source returned HTML - it may have been moved or deleted")]
SourceMovedOrDeleted,
#[error("rockspec source has unsupported file type {0}")]
UnsupportedFileType(String),
#[error("could not determine mimetype of rockspec source")]
UnknownMimeType,
}
pub async fn unpack_src_rock<R: Read + Seek + Send>(
rock_src: R,
destination: PathBuf,
progress: &Progress<ProgressBar>,
) -> Result<PathBuf, UnpackError> {
progress.map(|p| {
p.set_message(format!(
"📦 Unpacking src.rock into {}",
destination.display()
))
});
unpack_src_rock_impl(rock_src, destination).await
}
async fn unpack_src_rock_impl<R: Read + Seek + Send>(
rock_src: R,
destination: PathBuf,
) -> Result<PathBuf, UnpackError> {
let mut zip = zip::ZipArchive::new(rock_src)?;
zip.extract(&destination)?;
Ok(destination)
}
#[async_recursion]
pub(crate) async fn unpack<R>(
mime_type: Option<&str>,
reader: R,
extract_nested_archive: bool,
file_name: String,
dest_dir: &Path,
progress: &Progress<ProgressBar>,
) -> Result<(), UnpackError>
where
R: Read + Seek + Send,
{
progress.map(|p| p.set_message(format!("📦 Unpacking {}", file_name)));
match mime_type {
Some("application/zip") => {
let mut archive = zip::ZipArchive::new(reader)?;
archive.extract(dest_dir)?;
}
Some("application/x-tar") => {
let mut archive = tar::Archive::new(reader);
archive.unpack(dest_dir)?;
}
Some("application/gzip") => {
let mut bufreader = BufReader::new(reader);
let extract_subdirectory =
extract_nested_archive && is_single_tar_directory(&mut bufreader)?;
bufreader.rewind()?;
let tar = GzDecoder::new(bufreader);
let mut archive = tar::Archive::new(tar);
if extract_subdirectory {
archive.entries()?.try_for_each(|entry| {
let mut entry = entry?;
let path: PathBuf = entry.path()?.components().skip(1).collect();
if path.components().count() > 0 {
let dest = dest_dir.join(path);
std::fs::create_dir_all(dest.parent().unwrap())?;
entry.unpack(dest)?;
}
Ok::<_, io::Error>(())
})?;
} else {
archive.entries()?.try_for_each(|entry| {
entry?.unpack_in(dest_dir)?;
Ok::<_, io::Error>(())
})?;
}
}
Some("text/html") => {
return Err(UnpackError::SourceMovedOrDeleted);
}
Some(other) => {
return Err(UnpackError::UnsupportedFileType(other.to_string()));
}
None => {
return Err(UnpackError::UnknownMimeType);
}
}
if extract_nested_archive {
if let Some((nested_archive_path, mime_type)) = get_single_archive_entry(dest_dir)? {
{
let mut file = File::open(&nested_archive_path)?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)?;
let file_name = nested_archive_path
.file_name()
.map(|os_str| os_str.to_string_lossy())
.unwrap_or(nested_archive_path.to_string_lossy())
.to_string();
unpack(
mime_type,
file,
extract_nested_archive, file_name,
dest_dir,
progress,
)
.await?;
fs::remove_file(nested_archive_path)?;
}
}
}
Ok(())
}
fn is_single_tar_directory<R: Read + Seek + Send>(reader: R) -> io::Result<bool> {
let tar = GzDecoder::new(reader);
let mut archive = tar::Archive::new(tar);
let entries: Vec<_> = archive
.entries()?
.filter_map(|entry| {
if entry.as_ref().ok()?.path().ok()?.file_name()? != "pax_global_header" {
Some(entry)
} else {
None
}
})
.try_collect()?;
if entries.is_empty() {
Ok(false)
} else {
let directory: PathBuf = entries[0].path()?.components().take(1).collect();
Ok(entries.into_iter().all(|entry| {
entry.path().is_ok_and(|path| {
path.to_str()
.unwrap()
.starts_with(directory.to_str().unwrap())
})
}))
}
}
fn get_single_archive_entry(dir: &Path) -> Result<Option<(PathBuf, Option<&str>)>, io::Error> {
let entries = std::fs::read_dir(dir)?
.filter_map(Result::ok)
.filter_map(|f| {
let f = f.path();
if f.extension()
.is_some_and(|ext| ext.to_string_lossy() != "rockspec")
{
Some(f)
} else {
None
}
})
.collect_vec();
if entries.len() != 1 {
return Ok(None);
}
let entry = entries.first().unwrap();
if !entry.is_file() {
return Ok(None);
}
if let mt @ Some(mime_type) =
infer::get_from_path(entry)?.map(|file_type| file_type.mime_type())
{
if matches!(
mime_type,
"application/zip" | "application/x-tar" | "application/gzip"
) {
return Ok(Some((entry.clone(), mt)));
}
}
Ok(None)
}
#[cfg(test)]
mod tests {
use crate::progress::MultiProgress;
use std::fs::File;
use tempdir::TempDir;
use super::*;
#[tokio::test]
pub async fn test_unpack_src_rock() {
let test_rock_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources")
.join("test")
.join("luatest-0.2-1.src.rock");
let file = File::open(&test_rock_path).unwrap();
let dest = TempDir::new("lux-test").unwrap();
unpack_src_rock(
file,
dest.into_path(),
&Progress::Progress(MultiProgress::new().new_bar()),
)
.await
.unwrap();
}
}