use crate::error::LxAppError;
use ring::digest::{Context, SHA256};
use std::fs;
use std::io::{Cursor, Read};
use std::path::Path;
use tar::Archive;
use zstd::stream::read::Decoder as ZstdDecoder;
const ZSTD_MAGIC: [u8; 4] = [0x28, 0xB5, 0x2F, 0xFD];
pub fn extract_tar_zst(archive_path: &Path, destination: &Path) -> Result<(), LxAppError> {
let bytes = fs::read(archive_path).map_err(|e| {
LxAppError::IoError(format!(
"Failed to read archive {}: {}",
archive_path.display(),
e
))
})?;
if bytes.len() < ZSTD_MAGIC.len() {
return Err(LxAppError::IoError(format!(
"Failed to extract archive {}: empty or truncated ({} bytes)",
archive_path.display(),
bytes.len()
)));
}
if bytes[..ZSTD_MAGIC.len()] != ZSTD_MAGIC {
return Err(LxAppError::IoError(format!(
"Failed to extract archive {}: not zstd (magic {:02x}{:02x}{:02x}{:02x}, {} bytes)",
archive_path.display(),
bytes[0],
bytes[1],
bytes[2],
bytes[3],
bytes.len()
)));
}
let mut zstd_decoder = ZstdDecoder::new(Cursor::new(bytes)).map_err(|e| {
LxAppError::IoError(format!(
"Failed to create zstd decoder for {}: {}",
archive_path.display(),
e
))
})?;
zstd_decoder.window_log_max(31).map_err(|e| {
LxAppError::IoError(format!(
"Failed to configure zstd decoder for {}: {}",
archive_path.display(),
e
))
})?;
if destination.exists() {
fs::remove_dir_all(destination).map_err(|e| {
LxAppError::IoError(format!(
"Failed to clean destination {}: {}",
destination.display(),
e
))
})?;
}
fs::create_dir_all(destination)?;
let mut archive = Archive::new(zstd_decoder);
archive.set_preserve_permissions(false);
archive.set_unpack_xattrs(false);
archive.unpack(destination).map_err(|e| {
LxAppError::IoError(format!(
"Failed to extract archive {}: {}",
archive_path.display(),
e
))
})?;
Ok(())
}
pub fn verify_sha256(path: &Path, expected_hex: &str) -> Result<(), LxAppError> {
if expected_hex.is_empty() {
return Ok(());
}
let actual = sha256_hex(path)?;
if actual.eq_ignore_ascii_case(expected_hex) {
Ok(())
} else {
Err(LxAppError::IoError(format!(
"checksum mismatch: expected {}, got {}",
expected_hex, actual
)))
}
}
pub fn sha256_hex(path: &Path) -> Result<String, LxAppError> {
use std::fmt::Write;
let mut file = fs::File::open(path)?;
let mut ctx = Context::new(&SHA256);
let mut buf = vec![0u8; 256 * 1024];
loop {
let n = file.read(&mut buf)?;
if n == 0 {
break;
}
ctx.update(&buf[..n]);
}
let digest = ctx.finish();
let mut hex = String::with_capacity(digest.as_ref().len() * 2);
for b in digest.as_ref() {
let _ = write!(hex, "{:02x}", b);
}
Ok(hex)
}
#[cfg(test)]
mod tests {
use super::extract_tar_zst;
use std::fs;
fn write_tar_zst(src: &std::path::Path, dest: &std::path::Path) {
let file = fs::File::create(dest).unwrap();
let encoder = zstd::stream::write::Encoder::new(file, 0).unwrap();
let mut builder = tar::Builder::new(encoder);
builder.append_dir_all(".", src).unwrap();
builder.into_inner().unwrap().finish().unwrap();
}
#[test]
fn extracts_a_tar_zst_package() {
let root = std::env::temp_dir().join(format!("lx-archive-{}", std::process::id()));
let src = root.join("src");
let archive = root.join("pkg.lxapp");
let dest = root.join("out");
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(&src).unwrap();
fs::write(src.join("lxapp.json"), "{\"appId\":\"demo\"}\n").unwrap();
write_tar_zst(&src, &archive);
extract_tar_zst(&archive, &dest).unwrap();
let manifest = fs::read_to_string(dest.join("lxapp.json")).unwrap();
assert!(manifest.contains("demo"));
let _ = fs::remove_dir_all(&root);
}
#[test]
fn rejects_a_non_zstd_file() {
let path = std::env::temp_dir().join(format!("lx-archive-bad-{}", std::process::id()));
fs::write(&path, b"PK\x03\x04not-zstd").unwrap();
let dest = std::env::temp_dir().join(format!("lx-archive-bad-out-{}", std::process::id()));
let error = extract_tar_zst(&path, &dest).unwrap_err().to_string();
assert!(error.contains("not zstd"), "{error}");
let _ = fs::remove_file(&path);
let _ = fs::remove_dir_all(&dest);
}
#[test]
fn invalid_header_preserves_previous_install() {
let root = tempfile::tempdir().unwrap();
let archive = root.path().join("invalid.lxapp");
let destination = root.path().join("installed");
fs::create_dir_all(&destination).unwrap();
fs::write(destination.join("lxapp.json"), "previous manifest").unwrap();
fs::write(&archive, b"not a zstd archive").unwrap();
assert!(extract_tar_zst(&archive, &destination).is_err());
assert_eq!(
fs::read_to_string(destination.join("lxapp.json")).unwrap(),
"previous manifest"
);
}
}