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::runtime::path::render_host_visible_path;
use crate::skill::dependencies::{DependencyArchiveType, DependencyExportSpec};
fn render_archive_path(path: &Path) -> String {
render_host_visible_path(path)
}
fn extracted_skill_manifest_is_file(skill_yaml: &Path) -> Result<bool, String> {
match fs::metadata(skill_yaml) {
Ok(metadata) if metadata.is_file() => Ok(true),
Ok(_) => Err(format!(
"Extracted skill manifest is not a file: {}",
render_archive_path(skill_yaml)
)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(error) => Err(format!(
"Failed to inspect extracted skill manifest {}: {}",
render_archive_path(skill_yaml),
error
)),
}
}
fn installed_export_target_is_file(target_path: &Path) -> Result<bool, String> {
match fs::metadata(target_path) {
Ok(metadata) if metadata.is_file() => Ok(true),
Ok(_) => Err(format!(
"Installed export target is not a file: {}",
render_archive_path(target_path)
)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(error) => Err(format!(
"Failed to inspect installed export target {}: {}",
render_archive_path(target_path),
error
)),
}
}
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 {}: {}",
render_archive_path(install_root),
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 {}: {}",
render_archive_path(temp_root),
error
)
})?;
let file = fs::File::open(archive_path).map_err(|error| {
format!(
"Failed to open {}: {}",
render_archive_path(archive_path),
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 '{}'",
render_archive_path(archive_path),
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 {}: {}",
render_archive_path(&target_path),
error
)
})?;
continue;
}
if let Some(parent) = target_path.parent() {
fs::create_dir_all(parent).map_err(|error| {
format!(
"Failed to create {}: {}",
render_archive_path(parent),
error
)
})?;
}
let mut output = fs::File::create(&target_path).map_err(|error| {
format!(
"Failed to create {}: {}",
render_archive_path(&target_path),
error
)
})?;
std::io::copy(&mut entry, &mut output).map_err(|error| {
format!(
"Failed to extract '{}' into {}: {}",
entry.name(),
render_archive_path(&target_path),
error
)
})?;
}
let skill_dir = temp_root.join(expected_skill_id);
let skill_yaml = skill_dir.join("skill.yaml");
if !extracted_skill_manifest_is_file(&skill_yaml)? {
return Err(format!(
"Skill package {} does not contain {}/skill.yaml",
render_archive_path(archive_path),
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 {}: {}",
render_archive_path(archive_path),
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,
render_archive_path(archive_path)
)
})?;
let mut entry = archive.by_name(&entry_name).map_err(|error| {
format!(
"Failed to read zip entry '{}' from {}: {}",
entry_name,
render_archive_path(archive_path),
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 {}: {}",
render_archive_path(parent),
error
)
})?;
}
let mut output = fs::File::create(&target_path).map_err(|error| {
format!(
"Failed to create {}: {}",
render_archive_path(&target_path),
error
)
})?;
std::io::copy(&mut entry, &mut output).map_err(|error| {
format!(
"Failed to extract '{}' into {}: {}",
export.archive_path,
render_archive_path(&target_path),
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 {}: {}",
render_archive_path(archive_path),
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 {}: {}",
render_archive_path(archive_path),
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))?
.into_owned();
let entry_path = normalize_tar_entry_match_path(&entry_path)?;
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 {}: {}",
render_archive_path(parent),
error
)
})?;
}
let mut output = fs::File::create(&target_path).map_err(|error| {
format!(
"Failed to create {}: {}",
render_archive_path(&target_path),
error
)
})?;
let mut buffer = Vec::new();
archive_entry.read_to_end(&mut buffer).map_err(|error| {
format!(
"Failed to extract '{}' from {}: {}",
export.archive_path,
render_archive_path(archive_path),
error
)
})?;
std::io::copy(&mut Cursor::new(buffer), &mut output).map_err(|error| {
format!(
"Failed to write {}: {}",
render_archive_path(&target_path),
error
)
})?;
extracted_entries.push((target_path, export.executable));
}
}
for export in exports {
let target_path = join_relative_target(install_root, &export.target_path);
if !installed_export_target_is_file(&target_path)? {
return Err(format!(
"tar.gz archive {} does not contain required export '{}'",
render_archive_path(archive_path),
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 normalize_tar_entry_match_path(entry_path: &Path) -> Result<String, String> {
let entry_text = entry_path
.to_str()
.ok_or_else(|| "tar.gz entry path must be valid UTF-8".to_string())?;
Ok(normalize_archive_entry_match_path(entry_text))
}
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 {}: {}",
render_archive_path(parent),
error
)
})?;
}
fs::copy(source, target).map_err(|error| {
format!(
"Failed to copy {} to {}: {}",
render_archive_path(source),
render_archive_path(target),
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 {}: {}", render_archive_path(_target), error))?
.permissions();
permissions.set_mode(0o755);
fs::set_permissions(_target, permissions).map_err(|error| {
format!(
"Failed to chmod {}: {}",
render_archive_path(_target),
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)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::runtime::path::render_host_visible_path;
#[test]
fn normalize_tar_entry_match_path_uses_utf8_forward_slash_text() {
let entry_path = Path::new(r"package\bin\demo.exe");
let normalized =
normalize_tar_entry_match_path(entry_path).expect("tar entry path should normalize");
assert_eq!(normalized, "package/bin/demo.exe");
}
#[test]
fn install_root_create_error_uses_host_visible_path() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_archive_install_root_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
std::fs::create_dir_all(&temp_root).expect("temp root should be created");
let install_root = temp_root.join("install-root-file");
std::fs::write(&install_root, b"not a directory")
.expect("install-root fixture file should be written");
let archive_path = temp_root.join("payload.bin");
let error = install_downloaded_payload(
&archive_path,
DependencyArchiveType::Raw,
&install_root,
&[],
)
.expect_err("file install root should fail");
let expected_prefix = format!(
"Failed to create {}:",
render_host_visible_path(&install_root)
);
assert!(
error.starts_with(&expected_prefix),
"unexpected error: {}",
error
);
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn extracted_skill_manifest_probe_errors_are_reported() {
let invalid_skill_yaml = PathBuf::from("invalid\0skill.yaml");
let error = extracted_skill_manifest_is_file(&invalid_skill_yaml)
.expect_err("invalid extracted skill manifest probe should fail");
assert!(
error.contains("Failed to inspect extracted skill manifest"),
"unexpected error: {}",
error
);
assert!(error.contains("invalid"), "unexpected error: {}", error);
}
#[test]
fn extracted_skill_manifest_rejects_directory_manifest() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_archive_directory_manifest_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let skill_yaml = temp_root.join("vulcan-codekit").join("skill.yaml");
std::fs::create_dir_all(&skill_yaml).expect("directory manifest should be created");
let error = extracted_skill_manifest_is_file(&skill_yaml)
.expect_err("directory extracted skill manifest should fail");
assert!(
error.contains("Extracted skill manifest is not a file"),
"unexpected error: {}",
error
);
assert!(
error.contains(&render_host_visible_path(&skill_yaml)),
"unexpected error: {}",
error
);
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn tar_gz_install_rejects_directory_export_target_when_export_missing() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_archive_directory_export_target_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
std::fs::create_dir_all(&temp_root).expect("temp root should be created");
let archive_path = temp_root.join("payload.tar.gz");
let archive_file =
std::fs::File::create(&archive_path).expect("tar.gz archive file should be created");
let encoder = flate2::write::GzEncoder::new(archive_file, flate2::Compression::default());
let mut builder = tar::Builder::new(encoder);
let mut header = tar::Header::new_gnu();
let body = b"unrelated payload";
header.set_size(body.len() as u64);
header.set_mode(0o644);
header.set_cksum();
builder
.append_data(&mut header, "other.txt", &body[..])
.expect("unrelated tar entry should be written");
let encoder = builder.into_inner().expect("tar builder should finish");
encoder.finish().expect("gzip encoder should finish");
let install_root = temp_root.join("install");
let export_target = install_root.join("bin").join("demo");
std::fs::create_dir_all(&export_target).expect("directory export target should be created");
let exports = vec![DependencyExportSpec {
archive_path: "bin/demo".to_string(),
target_path: "bin/demo".to_string(),
executable: false,
}];
let error = install_downloaded_payload(
&archive_path,
DependencyArchiveType::TarGz,
&install_root,
&exports,
)
.expect_err("directory export target should fail missing export validation");
assert!(
error.contains("Installed export target is not a file"),
"unexpected error: {}",
error
);
assert!(
error.contains(&render_host_visible_path(&export_target)),
"unexpected error: {}",
error
);
assert!(
export_target.is_dir(),
"directory export target should remain after rejection"
);
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn installed_export_target_probe_errors_are_reported() {
let invalid_target_path = PathBuf::from("invalid\0target");
let error = installed_export_target_is_file(&invalid_target_path)
.expect_err("invalid installed export target probe should fail");
assert!(
error.contains("Failed to inspect installed export target"),
"unexpected error: {}",
error
);
assert!(error.contains("invalid"), "unexpected error: {}", error);
}
}