use std::fs::{self, File};
use std::io;
use std::path::{Path, PathBuf};
use crate::Error;
pub fn extract_hyperd(zip_path: &Path, dest_dir: &Path) -> Result<Vec<PathBuf>, Error> {
let file = File::open(zip_path).map_err(|source| Error::Io {
context: format!("opening zip {}", zip_path.display()),
source,
})?;
let mut archive = zip::ZipArchive::new(file).map_err(Error::Zip)?;
fs::create_dir_all(dest_dir).map_err(|source| Error::Io {
context: format!("creating {}", dest_dir.display()),
source,
})?;
let mut extracted = Vec::new();
let mut found_hyperd = false;
for i in 0..archive.len() {
let mut entry = archive.by_index(i).map_err(Error::Zip)?;
let Some(enclosed) = entry.enclosed_name() else {
continue;
};
let Some(rel) = strip_lib_hyper_prefix(&enclosed) else {
continue;
};
if rel.as_os_str().is_empty() {
continue;
}
let out_path = dest_dir.join(&rel);
if entry.is_dir() {
fs::create_dir_all(&out_path).map_err(|source| Error::Io {
context: format!("creating {}", out_path.display()),
source,
})?;
continue;
}
if let Some(parent) = out_path.parent() {
fs::create_dir_all(parent).map_err(|source| Error::Io {
context: format!("creating {}", parent.display()),
source,
})?;
}
let mut out = File::create(&out_path).map_err(|source| Error::Io {
context: format!("creating {}", out_path.display()),
source,
})?;
io::copy(&mut entry, &mut out).map_err(|source| Error::Io {
context: format!("writing {}", out_path.display()),
source,
})?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Some(mode) = entry.unix_mode() {
let _ = fs::set_permissions(&out_path, fs::Permissions::from_mode(mode));
}
}
if rel
.file_name()
.is_some_and(|n| n == "hyperd" || n == "hyperd.exe")
{
found_hyperd = true;
}
extracted.push(rel);
}
if !found_hyperd {
return Err(Error::HyperdNotInArchive);
}
Ok(extracted)
}
fn strip_lib_hyper_prefix(path: &Path) -> Option<PathBuf> {
let mut comps = path.components();
let first = comps.next()?;
let (a, b) = if first.as_os_str() == "lib" || first.as_os_str() == "bin" {
(first, comps.next()?)
} else {
(comps.next()?, comps.next()?)
};
if (a.as_os_str() == "lib" || a.as_os_str() == "bin") && b.as_os_str() == "hyper" {
Some(comps.as_path().to_path_buf())
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strip_prefix_matches_lib_hyper() {
assert_eq!(
strip_lib_hyper_prefix(Path::new("lib/hyper/hyperd")),
Some(PathBuf::from("hyperd"))
);
assert_eq!(
strip_lib_hyper_prefix(Path::new("lib/hyper/sub/file")),
Some(PathBuf::from("sub/file"))
);
assert_eq!(
strip_lib_hyper_prefix(Path::new("tableauhyperapi-cxx-x/lib/hyper/hyperd")),
Some(PathBuf::from("hyperd"))
);
assert_eq!(
strip_lib_hyper_prefix(Path::new("tableauhyperapi-cxx-x/lib/hyper/sub/a.so")),
Some(PathBuf::from("sub/a.so"))
);
assert_eq!(
strip_lib_hyper_prefix(Path::new("tableauhyperapi-cxx-x/bin/hyper/hyperd.exe")),
Some(PathBuf::from("hyperd.exe"))
);
assert_eq!(
strip_lib_hyper_prefix(Path::new("tableauhyperapi-cxx-x/bin/hyper/sub/extra.dll")),
Some(PathBuf::from("sub/extra.dll"))
);
assert_eq!(
strip_lib_hyper_prefix(Path::new("tableauhyperapi-cxx-x/include/foo.hpp")),
None
);
assert_eq!(
strip_lib_hyper_prefix(Path::new("tableauhyperapi-cxx-x/bin/tableauhyperapi.dll")),
None
);
assert_eq!(strip_lib_hyper_prefix(Path::new("other/file")), None);
}
#[test]
fn extract_fixture_zip() -> Result<(), Box<dyn std::error::Error>> {
use std::io::Write;
let tmp = tempfile::tempdir()?;
let zip_path = tmp.path().join("fixture.zip");
{
let file = File::create(&zip_path)?;
let mut zw = zip::ZipWriter::new(file);
let opts = zip::write::SimpleFileOptions::default();
zw.start_file("tableauhyperapi-cxx-fake/lib/hyper/hyperd", opts)?;
zw.write_all(b"fake hyperd")?;
zw.start_file("tableauhyperapi-cxx-fake/lib/hyper/sub/extra.txt", opts)?;
zw.write_all(b"extra")?;
zw.start_file("tableauhyperapi-cxx-fake/include/ignored.hpp", opts)?;
zw.write_all(b"nope")?;
zw.finish()?;
}
let out = tmp.path().join("out");
let files = extract_hyperd(&zip_path, &out)?;
assert!(files.iter().any(|p| p == Path::new("hyperd")));
assert!(files.iter().any(|p| p == Path::new("sub/extra.txt")));
assert!(out.join("hyperd").exists());
assert!(!out.join("ignored.hpp").exists());
Ok(())
}
}