#![cfg_attr(not(engine), allow(dead_code))]
use std::io::Read;
use std::path::Path;
use sha2::{Digest, Sha256};
pub const VERSION: &str = include_str!("../../engine/VERSION");
pub const ASSET: &str = if cfg!(windows) {
"ferrodock-engine-wsl-amd64.tar.zst"
} else if cfg!(target_os = "macos") {
"ferrodock-engine-arm64.tar.zst"
} else if cfg!(target_arch = "aarch64") {
"ferrodock-engine-linux-arm64.tar.zst"
} else {
"ferrodock-engine-linux-amd64.tar.zst"
};
fn base_url() -> String {
format!(
"https://github.com/manimovassagh/Ferrodock/releases/download/engine-v{}",
VERSION.trim()
)
}
pub fn parse_checksum(line: &str) -> Option<String> {
let digest = line.split_whitespace().next()?.to_lowercase();
(digest.len() == 64 && digest.bytes().all(|b| b.is_ascii_hexdigit())).then_some(digest)
}
pub fn fetch(mut progress: impl FnMut(u64, Option<u64>)) -> Result<Vec<u8>, String> {
let get = |url: &str| ureq::get(url).call().map_err(|e| format!("{url}: {e}"));
let checksum = get(&format!("{}/{ASSET}.sha256", base_url()))?
.body_mut()
.read_to_string()
.map_err(|e| e.to_string())?;
let expected = parse_checksum(&checksum).ok_or("the engine's checksum file is malformed")?;
let mut response = get(&format!("{}/{ASSET}", base_url()))?;
let total = response.body().content_length();
let mut reader = response.body_mut().as_reader();
let mut archive = Vec::with_capacity(total.unwrap_or(0) as usize);
let mut hasher = Sha256::new();
let mut buffer = vec![0u8; 256 * 1024];
loop {
let n = reader.read(&mut buffer).map_err(|e| e.to_string())?;
if n == 0 {
break;
}
hasher.update(&buffer[..n]);
archive.extend_from_slice(&buffer[..n]);
progress(archive.len() as u64, total);
}
let actual = format!("{:x}", hasher.finalize());
if actual != expected {
return Err("The engine download is damaged (checksum mismatch). Try again.".into());
}
Ok(archive)
}
#[cfg_attr(not(windows), allow(dead_code))]
pub fn decompress(archive: &[u8], path: &Path) -> Result<(), String> {
let mut decoder =
ruzstd::decoding::StreamingDecoder::new(archive).map_err(|e| e.to_string())?;
let mut file = std::fs::File::create(path).map_err(|e| format!("{}: {e}", path.display()))?;
std::io::copy(&mut decoder, &mut file)
.map(drop)
.map_err(|e| format!("Couldn't unpack the engine: {e}"))
}
#[cfg_attr(windows, allow(dead_code))]
pub fn unpack(archive: &[u8], dir: &Path, needs: &[&str]) -> Result<(), String> {
let staging = dir.with_extension("new");
let _ = std::fs::remove_dir_all(&staging);
std::fs::create_dir_all(&staging).map_err(|e| e.to_string())?;
let decoder = ruzstd::decoding::StreamingDecoder::new(archive).map_err(|e| e.to_string())?;
tar::Archive::new(decoder)
.unpack(&staging)
.map_err(|e| format!("Couldn't unpack the engine: {e}"))?;
for file in needs {
if !staging.join(file).is_file() {
return Err(format!("The engine image has no {file}"));
}
}
let _ = std::fs::remove_dir_all(dir);
std::fs::rename(&staging, dir).map_err(|e| e.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reads_sha256sum_lines() {
let digest = "a".repeat(64);
assert_eq!(
parse_checksum(&format!("{digest} ferrodock-engine-arm64.tar.zst\n")),
Some(digest)
);
assert_eq!(parse_checksum("nope file"), None);
assert_eq!(parse_checksum(""), None);
assert_eq!(VERSION.trim().split('.').count(), 3);
}
#[test]
fn unpacks_the_image() {
let mut tarball = tar::Builder::new(Vec::new());
for (name, body) in [
("kernel", &b"kernel image"[..]),
("rootfs.img", b"root disk"),
] {
let mut header = tar::Header::new_gnu();
header.set_size(body.len() as u64);
header.set_mode(0o644);
header.set_cksum();
tarball.append_data(&mut header, name, body).unwrap();
}
let archive = ruzstd::encoding::compress_to_vec(
tarball.into_inner().unwrap().as_slice(),
ruzstd::encoding::CompressionLevel::Fastest,
);
let dir = std::env::temp_dir().join(format!("ferrodock-engine-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("old"), "old").unwrap();
unpack(&archive, &dir, &["kernel", "rootfs.img"]).unwrap();
assert_eq!(std::fs::read(dir.join("kernel")).unwrap(), b"kernel image");
assert_eq!(std::fs::read(dir.join("rootfs.img")).unwrap(), b"root disk");
assert!(!dir.join("old").exists());
let tar = dir.with_extension("tar");
decompress(&archive, &tar).unwrap();
assert_eq!(
tar::Archive::new(std::fs::File::open(&tar).unwrap())
.entries()
.unwrap()
.count(),
2
);
std::fs::remove_file(&tar).unwrap();
std::fs::remove_dir_all(&dir).unwrap();
assert!(
unpack(
&ruzstd::encoding::compress_to_vec(
&b""[..],
ruzstd::encoding::CompressionLevel::Fastest
),
&dir,
&["kernel"]
)
.is_err()
);
}
}