#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use bzip2::read::BzDecoder;
use flate2::read::GzDecoder;
use snafu::ResultExt;
use xz2::read::XzDecoder;
use crate::{Result, error};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in crate::bin_resolver) enum ArchiveFormat {
Tar,
TarGz,
TarXz,
TarZst,
TarBz2,
Zip,
NakedBinary,
}
impl ArchiveFormat {
pub(in crate::bin_resolver) fn canonical_filename(&self) -> &'static str {
match self {
Self::Tar => "archive.tar",
Self::TarGz => "archive.tar.gz",
Self::TarXz => "archive.tar.xz",
Self::TarZst => "archive.tar.zst",
Self::TarBz2 => "archive.tar.bz2",
Self::Zip => "archive.zip",
Self::NakedBinary => {
if cfg!(windows) {
"archive.exe"
} else {
"archive"
}
}
}
}
pub(in crate::bin_resolver) fn all_formats() -> &'static [(ArchiveFormat, &'static str)] {
#[cfg(windows)]
{
&[
(Self::Tar, ".tar"),
(Self::TarGz, ".tar.gz"),
(Self::TarGz, ".tgz"),
(Self::TarXz, ".tar.xz"),
(Self::TarZst, ".tar.zst"),
(Self::TarBz2, ".tar.bz2"),
(Self::Zip, ".zip"),
(Self::NakedBinary, ".exe"),
]
}
#[cfg(not(windows))]
{
&[
(Self::Tar, ".tar"),
(Self::TarGz, ".tar.gz"),
(Self::TarGz, ".tgz"),
(Self::TarXz, ".tar.xz"),
(Self::TarZst, ".tar.zst"),
(Self::TarBz2, ".tar.bz2"),
(Self::Zip, ".zip"),
(Self::NakedBinary, ""),
]
}
}
}
pub(in crate::bin_resolver) fn extract_binary(
archive_path: &Path,
format: ArchiveFormat,
expected_binary_name: &str,
dest_dir: &Path,
) -> Result<PathBuf> {
match format {
ArchiveFormat::Tar => extract_tar(archive_path, expected_binary_name, dest_dir),
ArchiveFormat::TarGz => extract_tar_gz(archive_path, expected_binary_name, dest_dir),
ArchiveFormat::TarXz => extract_tar_xz(archive_path, expected_binary_name, dest_dir),
ArchiveFormat::TarZst => extract_tar_zst(archive_path, expected_binary_name, dest_dir),
ArchiveFormat::TarBz2 => extract_tar_bz2(archive_path, expected_binary_name, dest_dir),
ArchiveFormat::Zip => extract_zip(archive_path, expected_binary_name, dest_dir),
ArchiveFormat::NakedBinary => extract_naked_binary(archive_path, expected_binary_name, dest_dir),
}
}
fn extract_tar(archive_path: &Path, binary_name: &str, dest_dir: &Path) -> Result<PathBuf> {
let file = std::fs::File::open(archive_path).with_context(|_| error::IoSnafu {
path: archive_path.to_path_buf(),
})?;
extract_tar_archive(file, binary_name, dest_dir)
}
fn extract_tar_gz(archive_path: &Path, binary_name: &str, dest_dir: &Path) -> Result<PathBuf> {
let file = std::fs::File::open(archive_path).with_context(|_| error::IoSnafu {
path: archive_path.to_path_buf(),
})?;
let decoder = GzDecoder::new(file);
extract_tar_archive(decoder, binary_name, dest_dir)
}
fn extract_tar_xz(archive_path: &Path, binary_name: &str, dest_dir: &Path) -> Result<PathBuf> {
let file = std::fs::File::open(archive_path).with_context(|_| error::IoSnafu {
path: archive_path.to_path_buf(),
})?;
let decoder = XzDecoder::new(file);
extract_tar_archive(decoder, binary_name, dest_dir)
}
fn extract_tar_zst(archive_path: &Path, binary_name: &str, dest_dir: &Path) -> Result<PathBuf> {
let file = std::fs::File::open(archive_path).with_context(|_| error::IoSnafu {
path: archive_path.to_path_buf(),
})?;
let decoder =
zstd::stream::read::Decoder::new(file).map_err(|e| error::Error::ArchiveExtractionFailed {
source: Box::new(e) as Box<dyn std::error::Error + Send + Sync>,
})?;
extract_tar_archive(decoder, binary_name, dest_dir)
}
fn extract_tar_bz2(archive_path: &Path, binary_name: &str, dest_dir: &Path) -> Result<PathBuf> {
let file = std::fs::File::open(archive_path).with_context(|_| error::IoSnafu {
path: archive_path.to_path_buf(),
})?;
let decoder = BzDecoder::new(file);
extract_tar_archive(decoder, binary_name, dest_dir)
}
fn extract_tar_archive<R: std::io::Read>(reader: R, binary_name: &str, dest_dir: &Path) -> Result<PathBuf> {
let mut archive = tar::Archive::new(reader);
std::fs::create_dir_all(dest_dir).with_context(|_| error::IoSnafu {
path: dest_dir.to_path_buf(),
})?;
archive
.unpack(dest_dir)
.map_err(|e| error::Error::ArchiveExtractionFailed {
source: Box::new(e) as Box<dyn std::error::Error + Send + Sync>,
})?;
find_binary_in_dir(dest_dir, binary_name)
}
fn extract_zip(archive_path: &Path, binary_name: &str, dest_dir: &Path) -> Result<PathBuf> {
let file = std::fs::File::open(archive_path).with_context(|_| error::IoSnafu {
path: archive_path.to_path_buf(),
})?;
let mut archive = zip::ZipArchive::new(file).map_err(|e| error::Error::ArchiveExtractionFailed {
source: Box::new(e) as Box<dyn std::error::Error + Send + Sync>,
})?;
std::fs::create_dir_all(dest_dir).with_context(|_| error::IoSnafu {
path: dest_dir.to_path_buf(),
})?;
archive
.extract(dest_dir)
.map_err(|e| error::Error::ArchiveExtractionFailed {
source: Box::new(e) as Box<dyn std::error::Error + Send + Sync>,
})?;
find_binary_in_dir(dest_dir, binary_name)
}
fn extract_naked_binary(archive_path: &Path, binary_name: &str, dest_dir: &Path) -> Result<PathBuf> {
std::fs::create_dir_all(dest_dir).with_context(|_| error::IoSnafu {
path: dest_dir.to_path_buf(),
})?;
let dest_path = dest_dir.join(format!("{}{}", binary_name, std::env::consts::EXE_SUFFIX));
std::fs::copy(archive_path, &dest_path).with_context(|_| error::IoSnafu {
path: dest_path.clone(),
})?;
#[cfg(unix)]
{
let mut perms = std::fs::metadata(&dest_path)
.with_context(|_| error::IoSnafu {
path: dest_path.clone(),
})?
.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&dest_path, perms).with_context(|_| error::IoSnafu {
path: dest_path.clone(),
})?;
}
Ok(dest_path)
}
fn find_binary_in_dir(dir: &Path, binary_name: &str) -> Result<PathBuf> {
let exe_suffix = std::env::consts::EXE_SUFFIX;
let candidates = [
dir.join(format!("{}{}", binary_name, exe_suffix)),
dir.join(binary_name),
dir.join("bin").join(format!("{}{}", binary_name, exe_suffix)),
dir.join("bin").join(binary_name),
dir.join("target")
.join("release")
.join(format!("{}{}", binary_name, exe_suffix)),
dir.join("target").join("release").join(binary_name),
];
for candidate in &candidates {
if candidate.exists() && is_executable(candidate) {
return Ok(candidate.clone());
}
}
let err = std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("binary '{}' not found in extracted archive", binary_name),
);
Err(error::Error::ArchiveExtractionFailed {
source: Box::new(err) as Box<dyn std::error::Error + Send + Sync>,
})
}
#[cfg(unix)]
fn is_executable(path: &Path) -> bool {
std::fs::metadata(path)
.map(|m| m.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}
#[cfg(not(unix))]
fn is_executable(_path: &Path) -> bool {
true
}
#[cfg(test)]
mod tests {
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::{
fs,
io::{Cursor, Write},
};
use flate2::{Compression, write::GzEncoder};
use xz2::write::XzEncoder;
use zip::write::SimpleFileOptions;
use super::*;
use crate::error::Error;
impl ArchiveFormat {
fn suffix(&self) -> &'static str {
match self {
Self::Tar => ".tar",
Self::TarGz => ".tar.gz",
Self::TarXz => ".tar.xz",
Self::TarZst => ".tar.zst",
Self::TarBz2 => ".tar.bz2",
Self::Zip => ".zip",
Self::NakedBinary => {
if cfg!(windows) {
".exe"
} else {
""
}
}
}
}
}
#[derive(Debug, Clone, Copy)]
enum BinaryLocation {
Root,
BinDir,
TargetRelease,
}
impl BinaryLocation {
fn relative_path(&self, binary_name: &str) -> PathBuf {
match self {
Self::Root => PathBuf::from(binary_name),
Self::BinDir => PathBuf::from("bin").join(binary_name),
Self::TargetRelease => PathBuf::from("target").join("release").join(binary_name),
}
}
}
fn create_test_binary(temp_dir: &Path, binary_name: &str, location: BinaryLocation) -> PathBuf {
let binary_path = temp_dir.join(location.relative_path(binary_name));
fs::create_dir_all(binary_path.parent().unwrap()).unwrap();
let mut file = fs::File::create(&binary_path).unwrap();
file.write_all(b"#!/bin/sh\necho test").unwrap();
#[cfg(unix)]
{
let mut perms = fs::metadata(&binary_path).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&binary_path, perms).unwrap();
}
binary_path
}
fn create_test_tar_gz(binary_name: &str, location: BinaryLocation) -> Vec<u8> {
let temp_dir = tempfile::tempdir().unwrap();
create_test_binary(temp_dir.path(), binary_name, location);
let mut archive_data = Vec::new();
{
let encoder = GzEncoder::new(&mut archive_data, Compression::default());
let mut tar = tar::Builder::new(encoder);
tar.append_dir_all(".", temp_dir.path()).unwrap();
tar.finish().unwrap();
}
archive_data
}
fn create_test_tar_xz(binary_name: &str, location: BinaryLocation) -> Vec<u8> {
let temp_dir = tempfile::tempdir().unwrap();
create_test_binary(temp_dir.path(), binary_name, location);
let mut archive_data = Vec::new();
{
let encoder = XzEncoder::new(&mut archive_data, 6);
let mut tar = tar::Builder::new(encoder);
tar.append_dir_all(".", temp_dir.path()).unwrap();
tar.finish().unwrap();
}
archive_data
}
fn create_test_tar_zst(binary_name: &str, location: BinaryLocation) -> Vec<u8> {
let temp_dir = tempfile::tempdir().unwrap();
create_test_binary(temp_dir.path(), binary_name, location);
let mut archive_data = Vec::new();
{
let encoder = zstd::stream::write::Encoder::new(&mut archive_data, 3).unwrap();
let mut tar = tar::Builder::new(encoder);
tar.append_dir_all(".", temp_dir.path()).unwrap();
let encoder = tar.into_inner().unwrap();
encoder.finish().unwrap();
}
archive_data
}
fn create_test_zip(binary_name: &str, location: BinaryLocation) -> Vec<u8> {
let temp_dir = tempfile::tempdir().unwrap();
let binary_path = create_test_binary(temp_dir.path(), binary_name, location);
let relative_path = location.relative_path(binary_name);
let mut archive_data = Vec::new();
{
let cursor = Cursor::new(&mut archive_data);
let mut zip = zip::ZipWriter::new(cursor);
let options = SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated)
.unix_permissions(0o755);
zip.start_file(relative_path.to_string_lossy(), options).unwrap();
let content = fs::read(&binary_path).unwrap();
zip.write_all(&content).unwrap();
zip.finish().unwrap();
}
archive_data
}
#[test]
fn test_extract_tar_gz_from_root() {
let archive_data = create_test_tar_gz("testbin", BinaryLocation::Root);
let temp_archive = tempfile::NamedTempFile::new().unwrap();
fs::write(temp_archive.path(), &archive_data).unwrap();
let dest_dir = tempfile::tempdir().unwrap();
let result = extract_binary(
temp_archive.path(),
ArchiveFormat::TarGz,
"testbin",
dest_dir.path(),
);
let binary_path = result.unwrap();
assert!(binary_path.exists());
assert!(binary_path.to_string_lossy().contains("testbin"));
}
#[test]
fn test_extract_tar_gz_from_bin_dir() {
let archive_data = create_test_tar_gz("testbin", BinaryLocation::BinDir);
let temp_archive = tempfile::NamedTempFile::new().unwrap();
fs::write(temp_archive.path(), &archive_data).unwrap();
let dest_dir = tempfile::tempdir().unwrap();
let result = extract_binary(
temp_archive.path(),
ArchiveFormat::TarGz,
"testbin",
dest_dir.path(),
);
let binary_path = result.unwrap();
assert!(binary_path.exists());
assert!(binary_path.to_string_lossy().contains("bin"));
}
#[test]
fn test_extract_tar_gz_from_target_release() {
let archive_data = create_test_tar_gz("testbin", BinaryLocation::TargetRelease);
let temp_archive = tempfile::NamedTempFile::new().unwrap();
fs::write(temp_archive.path(), &archive_data).unwrap();
let dest_dir = tempfile::tempdir().unwrap();
let result = extract_binary(
temp_archive.path(),
ArchiveFormat::TarGz,
"testbin",
dest_dir.path(),
);
let binary_path = result.unwrap();
assert!(binary_path.exists());
assert!(
binary_path.starts_with(dest_dir.path()),
"Binary should be in dest_dir"
);
}
#[test]
fn test_extract_tar_xz() {
let archive_data = create_test_tar_xz("testbin", BinaryLocation::Root);
let temp_archive = tempfile::NamedTempFile::new().unwrap();
fs::write(temp_archive.path(), &archive_data).unwrap();
let dest_dir = tempfile::tempdir().unwrap();
let result = extract_binary(
temp_archive.path(),
ArchiveFormat::TarXz,
"testbin",
dest_dir.path(),
);
let binary_path = result.unwrap();
assert!(binary_path.exists());
}
#[test]
fn test_extract_tar_zst() {
let archive_data = create_test_tar_zst("testbin", BinaryLocation::Root);
let temp_archive = tempfile::NamedTempFile::new().unwrap();
fs::write(temp_archive.path(), &archive_data).unwrap();
let dest_dir = tempfile::tempdir().unwrap();
let result = extract_binary(
temp_archive.path(),
ArchiveFormat::TarZst,
"testbin",
dest_dir.path(),
);
let binary_path = result.unwrap();
assert!(binary_path.exists());
}
#[test]
fn test_extract_zip_from_root() {
let archive_data = create_test_zip("testbin", BinaryLocation::Root);
let temp_archive = tempfile::NamedTempFile::new().unwrap();
fs::write(temp_archive.path(), &archive_data).unwrap();
let dest_dir = tempfile::tempdir().unwrap();
let result = extract_binary(
temp_archive.path(),
ArchiveFormat::Zip,
"testbin",
dest_dir.path(),
);
let binary_path = result.unwrap();
assert!(binary_path.exists());
}
#[test]
fn test_extract_zip_from_bin_dir() {
let archive_data = create_test_zip("testbin", BinaryLocation::BinDir);
let temp_archive = tempfile::NamedTempFile::new().unwrap();
fs::write(temp_archive.path(), &archive_data).unwrap();
let dest_dir = tempfile::tempdir().unwrap();
let result = extract_binary(
temp_archive.path(),
ArchiveFormat::Zip,
"testbin",
dest_dir.path(),
);
let binary_path = result.unwrap();
assert!(binary_path.exists());
}
#[test]
fn test_extract_naked_binary() {
let temp_binary = tempfile::NamedTempFile::new().unwrap();
fs::write(temp_binary.path(), b"#!/bin/sh\necho test").unwrap();
let dest_dir = tempfile::tempdir().unwrap();
let result = extract_binary(
temp_binary.path(),
ArchiveFormat::NakedBinary,
"testbin",
dest_dir.path(),
);
let binary_path = result.unwrap();
assert!(binary_path.exists());
assert!(binary_path.to_string_lossy().contains("testbin"));
#[cfg(unix)]
{
let perms = fs::metadata(&binary_path).unwrap().permissions();
assert_eq!(perms.mode() & 0o777, 0o755, "Binary should have 755 permissions");
}
}
#[test]
fn test_binary_not_found_in_archive() {
let temp_src = tempfile::tempdir().unwrap();
let binary_path = temp_src.path().join("otherbinary");
fs::write(&binary_path, b"#!/bin/sh\necho test").unwrap();
#[cfg(unix)]
{
let mut perms = fs::metadata(&binary_path).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&binary_path, perms).unwrap();
}
let mut archive_data = Vec::new();
{
let encoder = GzEncoder::new(&mut archive_data, Compression::default());
let mut tar = tar::Builder::new(encoder);
tar.append_path_with_name(&binary_path, "otherbinary").unwrap();
tar.finish().unwrap();
}
let temp_dir = tempfile::tempdir().unwrap();
let archive_path = temp_dir.path().join("test.tar.gz");
fs::write(&archive_path, &archive_data).unwrap();
let dest_dir = tempfile::tempdir().unwrap();
let result = extract_binary(&archive_path, ArchiveFormat::TarGz, "testbin", dest_dir.path());
assert_matches::assert_matches!(result, Err(Error::ArchiveExtractionFailed { .. }));
}
#[test]
fn test_corrupt_tar_gz() {
let corrupt_data = b"This is not a valid gzip file";
let temp_dir = tempfile::tempdir().unwrap();
let archive_path = temp_dir.path().join("test.tar.gz");
fs::write(&archive_path, corrupt_data).unwrap();
let dest_dir = tempfile::tempdir().unwrap();
let result = extract_binary(&archive_path, ArchiveFormat::TarGz, "testbin", dest_dir.path());
assert_matches::assert_matches!(result, Err(Error::ArchiveExtractionFailed { .. }));
}
#[test]
fn test_corrupt_tar_xz() {
let corrupt_data = b"This is not a valid xz file";
let temp_dir = tempfile::tempdir().unwrap();
let archive_path = temp_dir.path().join("test.tar.xz");
fs::write(&archive_path, corrupt_data).unwrap();
let dest_dir = tempfile::tempdir().unwrap();
let result = extract_binary(&archive_path, ArchiveFormat::TarXz, "testbin", dest_dir.path());
assert_matches::assert_matches!(result, Err(Error::ArchiveExtractionFailed { .. }));
}
#[test]
fn test_corrupt_zip() {
let corrupt_data = b"This is not a valid zip file";
let temp_dir = tempfile::tempdir().unwrap();
let archive_path = temp_dir.path().join("test.zip");
fs::write(&archive_path, corrupt_data).unwrap();
let dest_dir = tempfile::tempdir().unwrap();
let result = extract_binary(&archive_path, ArchiveFormat::Zip, "testbin", dest_dir.path());
assert_matches::assert_matches!(result, Err(Error::ArchiveExtractionFailed { .. }));
}
#[test]
fn test_truncated_tar_gz() {
let full_archive = create_test_tar_gz("testbin", BinaryLocation::Root);
let truncated = &full_archive[..full_archive.len() / 2];
let temp_dir = tempfile::tempdir().unwrap();
let archive_path = temp_dir.path().join("test.tar.gz");
fs::write(&archive_path, truncated).unwrap();
let dest_dir = tempfile::tempdir().unwrap();
let result = extract_binary(&archive_path, ArchiveFormat::TarGz, "testbin", dest_dir.path());
assert_matches::assert_matches!(result, Err(Error::ArchiveExtractionFailed { .. }));
}
#[test]
fn test_empty_tar_gz() {
let mut archive_data = Vec::new();
{
let encoder = GzEncoder::new(&mut archive_data, Compression::default());
let mut tar = tar::Builder::new(encoder);
tar.finish().unwrap();
}
let temp_dir = tempfile::tempdir().unwrap();
let archive_path = temp_dir.path().join("test.tar.gz");
fs::write(&archive_path, &archive_data).unwrap();
let dest_dir = tempfile::tempdir().unwrap();
let result = extract_binary(&archive_path, ArchiveFormat::TarGz, "testbin", dest_dir.path());
assert_matches::assert_matches!(result, Err(Error::ArchiveExtractionFailed { .. }));
}
#[test]
#[cfg(unix)]
fn test_non_executable_file_in_archive() {
let temp_src = tempfile::tempdir().unwrap();
let binary_path = temp_src.path().join("testbin");
let mut file = fs::File::create(&binary_path).unwrap();
file.write_all(b"#!/bin/sh\necho test").unwrap();
drop(file);
let mut perms = fs::metadata(&binary_path).unwrap().permissions();
perms.set_mode(0o644);
fs::set_permissions(&binary_path, perms).unwrap();
let mut archive_data = Vec::new();
{
let encoder = GzEncoder::new(&mut archive_data, Compression::default());
let mut tar = tar::Builder::new(encoder);
tar.append_path_with_name(&binary_path, "testbin").unwrap();
tar.finish().unwrap();
}
let temp_dir = tempfile::tempdir().unwrap();
let archive_path = temp_dir.path().join("test.tar.gz");
fs::write(&archive_path, &archive_data).unwrap();
let dest_dir = tempfile::tempdir().unwrap();
let result = extract_binary(&archive_path, ArchiveFormat::TarGz, "testbin", dest_dir.path());
assert_matches::assert_matches!(result, Err(Error::ArchiveExtractionFailed { .. }));
}
#[test]
fn test_find_binary_with_exe_suffix() {
let temp_dir = tempfile::tempdir().unwrap();
let binary_name = "testbin";
let binary_path = temp_dir
.path()
.join(format!("{}{}", binary_name, std::env::consts::EXE_SUFFIX));
let mut file = fs::File::create(&binary_path).unwrap();
file.write_all(b"test").unwrap();
#[cfg(unix)]
{
let mut perms = fs::metadata(&binary_path).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&binary_path, perms).unwrap();
}
let result = find_binary_in_dir(temp_dir.path(), binary_name);
assert!(result.is_ok(), "Should find binary with EXE_SUFFIX");
assert_eq!(result.unwrap(), binary_path);
}
#[test]
#[cfg(unix)]
fn test_find_binary_without_exe_suffix() {
let temp_dir = tempfile::tempdir().unwrap();
let binary_name = "testbin";
let binary_path = temp_dir.path().join(binary_name);
let mut file = fs::File::create(&binary_path).unwrap();
file.write_all(b"test").unwrap();
let mut perms = fs::metadata(&binary_path).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&binary_path, perms).unwrap();
let result = find_binary_in_dir(temp_dir.path(), binary_name);
assert!(result.is_ok(), "Should find binary without .exe suffix on Unix");
assert_eq!(result.unwrap(), binary_path);
}
#[test]
fn test_find_binary_in_nested_locations() {
let temp_dir = tempfile::tempdir().unwrap();
let binary_name = "testbin";
let nested_path = temp_dir.path().join("target").join("release");
fs::create_dir_all(&nested_path).unwrap();
let binary_path = nested_path.join(binary_name);
let mut file = fs::File::create(&binary_path).unwrap();
file.write_all(b"test").unwrap();
#[cfg(unix)]
{
let mut perms = fs::metadata(&binary_path).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&binary_path, perms).unwrap();
}
let result = find_binary_in_dir(temp_dir.path(), binary_name);
assert!(result.is_ok(), "Should find binary in target/release");
assert_eq!(result.unwrap(), binary_path);
}
#[test]
fn archive_format_suffix_consistency() {
assert_eq!(ArchiveFormat::Tar.suffix(), ".tar");
assert_eq!(ArchiveFormat::TarGz.suffix(), ".tar.gz");
assert_eq!(ArchiveFormat::TarXz.suffix(), ".tar.xz");
assert_eq!(ArchiveFormat::TarZst.suffix(), ".tar.zst");
assert_eq!(ArchiveFormat::TarBz2.suffix(), ".tar.bz2");
assert_eq!(ArchiveFormat::Zip.suffix(), ".zip");
}
}