use std::fs;
use std::io::{Cursor, Read};
use std::path::{Path, PathBuf};
use flate2::read::GzDecoder;
use tar::Archive;
use zip::ZipArchive;
use crate::skill::dependencies::{DependencyArchiveType, DependencyExportSpec};
pub fn install_downloaded_payload(
archive_path: &Path,
archive_type: DependencyArchiveType,
install_root: &Path,
exports: &[DependencyExportSpec],
) -> Result<(), String> {
fs::create_dir_all(install_root)
.map_err(|error| format!("Failed to create {}: {}", install_root.display(), error))?;
match archive_type {
DependencyArchiveType::Raw => install_from_raw_file(archive_path, install_root, exports),
DependencyArchiveType::Zip => install_from_zip_archive(archive_path, install_root, exports),
DependencyArchiveType::TarGz => {
install_from_tar_gz_archive(archive_path, install_root, exports)
}
}
}
pub fn extract_skill_package_zip(
archive_path: &Path,
temp_root: &Path,
expected_skill_id: &str,
) -> Result<PathBuf, String> {
fs::create_dir_all(temp_root)
.map_err(|error| format!("Failed to create {}: {}", temp_root.display(), error))?;
let file = fs::File::open(archive_path)
.map_err(|error| format!("Failed to open {}: {}", archive_path.display(), error))?;
let mut archive =
ZipArchive::new(file).map_err(|error| format!("Failed to open zip archive: {}", error))?;
for index in 0..archive.len() {
let mut entry = archive
.by_index(index)
.map_err(|error| format!("Failed to read zip entry #{}: {}", index, error))?;
let entry_path = normalize_zip_entry_path(entry.name())?;
if entry_path.components().next().is_none() {
continue;
}
let top_level = entry_path
.components()
.next()
.and_then(|component| component.as_os_str().to_str())
.ok_or_else(|| {
format!(
"Failed to read the top-level directory of zip entry '{}'",
entry.name()
)
})?;
if top_level != expected_skill_id {
return Err(format!(
"Skill package {} must contain only the top-level directory '{}', but found '{}'",
archive_path.display(),
expected_skill_id,
top_level
));
}
let target_path = temp_root.join(&entry_path);
if entry.is_dir() {
fs::create_dir_all(&target_path).map_err(|error| {
format!("Failed to create {}: {}", target_path.display(), error)
})?;
continue;
}
if let Some(parent) = target_path.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("Failed to create {}: {}", parent.display(), error))?;
}
let mut output = fs::File::create(&target_path)
.map_err(|error| format!("Failed to create {}: {}", target_path.display(), error))?;
std::io::copy(&mut entry, &mut output).map_err(|error| {
format!(
"Failed to extract '{}' into {}: {}",
entry.name(),
target_path.display(),
error
)
})?;
}
let skill_dir = temp_root.join(expected_skill_id);
let skill_yaml = skill_dir.join("skill.yaml");
if !skill_yaml.exists() {
return Err(format!(
"Skill package {} does not contain {}/skill.yaml",
archive_path.display(),
expected_skill_id
));
}
Ok(skill_dir)
}
fn install_from_raw_file(
archive_path: &Path,
install_root: &Path,
exports: &[DependencyExportSpec],
) -> Result<(), String> {
if exports.len() != 1 {
return Err("raw dependency payload must declare exactly one export".to_string());
}
let export = &exports[0];
let target_path = join_relative_target(install_root, &export.target_path);
copy_file_with_parent_dir(archive_path, &target_path)?;
mark_executable_if_needed(&target_path, export.executable)?;
Ok(())
}
fn install_from_zip_archive(
archive_path: &Path,
install_root: &Path,
exports: &[DependencyExportSpec],
) -> Result<(), String> {
let file = fs::File::open(archive_path)
.map_err(|error| format!("Failed to open {}: {}", archive_path.display(), error))?;
let mut archive =
ZipArchive::new(file).map_err(|error| format!("Failed to open zip archive: {}", error))?;
for export in exports {
let entry_name = resolve_zip_export_entry_name(&mut archive, &export.archive_path)
.ok_or_else(|| {
format!(
"Failed to read zip entry '{}' from {}: specified file not found in archive",
export.archive_path,
archive_path.display()
)
})?;
let mut entry = archive.by_name(&entry_name).map_err(|error| {
format!(
"Failed to read zip entry '{}' from {}: {}",
entry_name,
archive_path.display(),
error
)
})?;
let target_path = join_relative_target(install_root, &export.target_path);
if let Some(parent) = target_path.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("Failed to create {}: {}", parent.display(), error))?;
}
let mut output = fs::File::create(&target_path)
.map_err(|error| format!("Failed to create {}: {}", target_path.display(), error))?;
std::io::copy(&mut entry, &mut output).map_err(|error| {
format!(
"Failed to extract '{}' into {}: {}",
export.archive_path,
target_path.display(),
error
)
})?;
mark_executable_if_needed(&target_path, export.executable)?;
}
Ok(())
}
fn install_from_tar_gz_archive(
archive_path: &Path,
install_root: &Path,
exports: &[DependencyExportSpec],
) -> Result<(), String> {
let bytes = fs::read(archive_path)
.map_err(|error| format!("Failed to read {}: {}", archive_path.display(), error))?;
let decoder = GzDecoder::new(Cursor::new(bytes));
let mut archive = Archive::new(decoder);
let mut extracted_entries: Vec<(PathBuf, bool)> = Vec::new();
for archive_entry in archive.entries().map_err(|error| {
format!(
"Failed to enumerate tar.gz entries from {}: {}",
archive_path.display(),
error
)
})? {
let mut archive_entry =
archive_entry.map_err(|error| format!("Failed to read tar entry: {}", error))?;
let entry_path = archive_entry
.path()
.map_err(|error| format!("Failed to read tar entry path: {}", error))?
.to_string_lossy()
.replace('\\', "/");
if let Some(export) = exports
.iter()
.find(|export| archive_entry_matches_export(&entry_path, &export.archive_path))
{
let target_path = join_relative_target(install_root, &export.target_path);
if let Some(parent) = target_path.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("Failed to create {}: {}", parent.display(), error))?;
}
let mut output = fs::File::create(&target_path).map_err(|error| {
format!("Failed to create {}: {}", target_path.display(), error)
})?;
let mut buffer = Vec::new();
archive_entry.read_to_end(&mut buffer).map_err(|error| {
format!(
"Failed to extract '{}' from {}: {}",
export.archive_path,
archive_path.display(),
error
)
})?;
std::io::copy(&mut Cursor::new(buffer), &mut output)
.map_err(|error| format!("Failed to write {}: {}", target_path.display(), error))?;
extracted_entries.push((target_path, export.executable));
}
}
for export in exports {
let target_path = join_relative_target(install_root, &export.target_path);
if !target_path.exists() {
return Err(format!(
"tar.gz archive {} does not contain required export '{}'",
archive_path.display(),
export.archive_path
));
}
}
for (target_path, executable) in extracted_entries {
mark_executable_if_needed(&target_path, executable)?;
}
Ok(())
}
fn resolve_zip_export_entry_name<R: Read + std::io::Seek>(
archive: &mut ZipArchive<R>,
expected_archive_path: &str,
) -> Option<String> {
let expected = normalize_archive_entry_match_path(expected_archive_path);
if archive
.file_names()
.any(|name| normalize_archive_entry_match_path(name) == expected)
{
return Some(expected);
}
archive.file_names().find_map(|name| {
let normalized_name = normalize_archive_entry_match_path(name);
if strip_one_leading_archive_component(&normalized_name).as_deref()
== Some(expected.as_str())
{
Some(normalized_name)
} else {
None
}
})
}
fn archive_entry_matches_export(entry_path: &str, export_archive_path: &str) -> bool {
let normalized_entry = normalize_archive_entry_match_path(entry_path);
let normalized_export = normalize_archive_entry_match_path(export_archive_path);
normalized_entry == normalized_export
|| strip_one_leading_archive_component(&normalized_entry).as_deref()
== Some(normalized_export.as_str())
}
fn normalize_archive_entry_match_path(raw: &str) -> String {
raw.replace('\\', "/").trim_matches('/').to_string()
}
fn strip_one_leading_archive_component(normalized_path: &str) -> Option<String> {
let mut components = normalized_path
.split('/')
.filter(|component| !component.is_empty());
components.next()?;
let remainder = components.collect::<Vec<_>>();
if remainder.is_empty() {
None
} else {
Some(remainder.join("/"))
}
}
fn join_relative_target(root: &Path, relative_target: &str) -> PathBuf {
let normalized = relative_target.replace('/', std::path::MAIN_SEPARATOR_STR);
root.join(normalized)
}
fn copy_file_with_parent_dir(source: &Path, target: &Path) -> Result<(), String> {
if let Some(parent) = target.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("Failed to create {}: {}", parent.display(), error))?;
}
fs::copy(source, target).map_err(|error| {
format!(
"Failed to copy {} to {}: {}",
source.display(),
target.display(),
error
)
})?;
Ok(())
}
fn mark_executable_if_needed(_target: &Path, executable: bool) -> Result<(), String> {
if !executable {
return Ok(());
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut permissions = fs::metadata(_target)
.map_err(|error| format!("Failed to stat {}: {}", _target.display(), error))?
.permissions();
permissions.set_mode(0o755);
fs::set_permissions(_target, permissions)
.map_err(|error| format!("Failed to chmod {}: {}", _target.display(), error))?;
}
Ok(())
}
fn normalize_zip_entry_path(entry_name: &str) -> Result<PathBuf, String> {
let normalized = entry_name.replace('\\', "/");
let mut path = PathBuf::new();
for component in Path::new(&normalized).components() {
match component {
std::path::Component::Normal(value) => path.push(value),
std::path::Component::CurDir => {}
std::path::Component::ParentDir => {
return Err(format!(
"Zip entry '{}' must not contain parent-directory traversal",
entry_name
));
}
std::path::Component::RootDir | std::path::Component::Prefix(_) => {
return Err(format!(
"Zip entry '{}' must not use an absolute path",
entry_name
));
}
}
}
Ok(path)
}