use crate::config::project::Compression;
use crate::error::FrostxError;
use std::collections::HashMap;
use std::io::Read as _;
use std::path::Path;
pub fn detect_compression(path: &Path) -> Result<Compression, FrostxError> {
let mut file = std::fs::File::open(path)?;
let mut magic = [0u8; 6];
let n = file.read(&mut magic)?;
if n >= 2 && magic[0] == 0x1f && magic[1] == 0x8b {
Ok(Compression::Gz)
} else if n >= 4 && magic[..4] == [0x28, 0xb5, 0x2f, 0xfd] {
Ok(Compression::Zstd)
} else if n >= 6 && magic[..6] == [0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00] {
Ok(Compression::Xz)
} else {
Err(FrostxError::Config(format!(
"{}: unrecognized archive format (expected a gzip, zstd, or xz tar archive)",
path.display()
)))
}
}
pub fn read_toml_entries(archive_path: &Path) -> Result<HashMap<String, String>, FrostxError> {
let compression = detect_compression(archive_path)?;
let file = std::fs::File::open(archive_path)?;
match compression {
Compression::Gz => {
let dec = flate2::read::GzDecoder::new(file);
collect_toml_entries(tar::Archive::new(dec))
}
Compression::Zstd => {
let dec = zstd::Decoder::new(file).map_err(FrostxError::Io)?;
collect_toml_entries(tar::Archive::new(dec))
}
Compression::Xz => {
let dec = xz2::read::XzDecoder::new(file);
collect_toml_entries(tar::Archive::new(dec))
}
}
}
fn collect_toml_entries<R: std::io::Read>(
mut archive: tar::Archive<R>,
) -> Result<HashMap<String, String>, FrostxError> {
let mut map = HashMap::new();
let entries = archive.entries().map_err(FrostxError::Io)?;
for entry in entries {
let Ok(mut entry) = entry else { continue };
let path = match entry.path() {
Ok(p) => p.into_owned(),
Err(_) => continue,
};
if path.extension().and_then(|e| e.to_str()) != Some("toml") {
continue;
}
let mut components = path.components();
components.next(); let rel: std::path::PathBuf = components.collect();
if rel.as_os_str().is_empty() {
continue;
}
let mut content = String::new();
if entry.read_to_string(&mut content).is_ok() {
map.insert(rel.to_string_lossy().into_owned(), content);
}
}
Ok(map)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
fn write_gz_archive(dir: &std::path::Path, files: &[(&str, &[u8])]) -> std::path::PathBuf {
let src = tempdir().unwrap();
for (rel, data) in files {
let dest = src.path().join(rel);
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(dest, data).unwrap();
}
let archive_path = dir.join("project.tar.gz");
let file = std::fs::File::create(&archive_path).unwrap();
let enc = flate2::write::GzEncoder::new(file, flate2::Compression::best());
let mut builder = tar::Builder::new(enc);
builder.follow_symlinks(false);
builder.append_dir_all("project", src.path()).unwrap();
let enc = builder.into_inner().unwrap();
enc.finish().unwrap();
archive_path
}
#[test]
fn detect_gz_by_magic() {
let tmp = tempdir().unwrap();
let archive = write_gz_archive(tmp.path(), &[("frostx.toml", b"id = \"test\"")]);
let c = detect_compression(&archive).unwrap();
assert!(matches!(c, Compression::Gz));
}
#[test]
fn detect_unknown_format_errors() {
let tmp = tempdir().unwrap();
let path = tmp.path().join("not_an_archive.tar.gz");
std::fs::write(&path, b"just plain text").unwrap();
assert!(detect_compression(&path).is_err());
}
#[test]
fn collect_toml_entries_gz() {
let tmp = tempdir().unwrap();
let archive = write_gz_archive(tmp.path(), &[("frostx.toml", b"id = \"abc\"")]);
let map = read_toml_entries(&archive).unwrap();
assert!(map.contains_key("frostx.toml"), "map = {map:?}");
assert!(map["frostx.toml"].contains("abc"));
}
#[test]
fn non_toml_entries_are_ignored() {
let tmp = tempdir().unwrap();
let archive = write_gz_archive(
tmp.path(),
&[
("frostx.toml", b"id = \"x\""),
("src/main.rs", b"fn main() {}"),
],
);
let map = read_toml_entries(&archive).unwrap();
assert_eq!(map.len(), 1, "only the .toml file should be in the map");
assert!(map.contains_key("frostx.toml"));
}
#[test]
fn relative_include_toml_in_archive() {
let main_toml = b"id = \"abc\"\ninclude = [\"./extra.toml\"]\n";
let extra_toml = b"[[rule]]\nafter = \"90d\"\nactions = []\n";
let tmp = tempdir().unwrap();
let archive = write_gz_archive(
tmp.path(),
&[("frostx.toml", main_toml), ("extra.toml", extra_toml)],
);
let map = read_toml_entries(&archive).unwrap();
assert!(map.contains_key("frostx.toml"), "map = {map:?}");
assert!(map.contains_key("extra.toml"), "map = {map:?}");
}
}