chore: initial project skeleton
Acauã: Network Detection & Threat Hunting, heuristic analysis over a rolling 7-day window using deterministic math (time series, graph theory, information theory) instead of opaque ML models. Third product in the Ciberfauna ecosystem, alongside Guará and Sabiá. Follows the ecosystem's established conventions: OpenWolf project memory, bilingual PT-BR/EN docs and UI, Gitea Actions CI/CD on the same self-hosted runners, Conventional Commits, SemVer, logo and productName handling, MIT OR Apache-2.0 licensing. Architecture decision (made with the project owner after a dedicated consultation, not the original blueprint's suggested stack): Rust throughout, not Python/FastAPI/React. Most of the statistics (Bowley skew, MADM, Shannon entropy, PCR, Z-score, novel-edge detection, pivot chains) reduces to SQL over an embedded DuckDB/Parquet store; only the spectral analysis and graph centrality need host-language code, and both have workable Rust crates. Documented in full, including six corrections to the original blueprint's math (Rayleigh/Schuster instead of a degenerate Lomb-Scargle over bare timestamps, an explicit sentinel for Bowley skew's Q3=Q1 case, Jensen-Shannon instead of unbounded KL, a dedicated long-connection detector, JA4 vs. JA4+ licensing caution, deterministic per-alert evidence), in docs/architecture.md. Skeleton only: crates/acaua-core has the four detection pillars, fusion, storage, zeek log parsing and the updater as typed stubs that compile and return a clear "skeleton" status rather than real detection logic — that is separate, future work. crates/acaua-cli has serve/analyze/doctor/update. apps/gui is a minimal Tauri shell. Also already includes the one-click desktop self-update mechanism shared with Guará and Sabiá (AppImage/.deb/Windows installer), even with no release published yet, for consistency across the ecosystem. No Gitea repository exists for this project yet; this is a local-only commit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VAyr8d7HuAdUGB6aA5xmDD
This commit is contained in:
commit
1b974bb90d
95 files changed
+12016
No files matched your search
@@ -0,0 +1,594 @@
|
||||
use crate::error::{AcauaError, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::ffi::OsStr;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
const LATEST_RELEASE_URL: &str =
|
||||
"https://git.lucasrguerra.dev.br/api/v1/repos/ciberfauna/acaua/releases/latest";
|
||||
const RELEASES_PAGE_URL: &str = "https://git.lucasrguerra.dev.br/ciberfauna/acaua/releases";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Release {
|
||||
tag_name: String,
|
||||
assets: Vec<Asset>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Asset {
|
||||
name: String,
|
||||
browser_download_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct UpdateStatus {
|
||||
pub current_version: String,
|
||||
pub latest_version: Option<String>,
|
||||
pub update_available: bool,
|
||||
}
|
||||
|
||||
fn curl_text(url: &str) -> Result<String> {
|
||||
let output = Command::new("curl")
|
||||
.args(["-fsSL", "-H", "User-Agent: acaua-updater", url])
|
||||
.output()
|
||||
.map_err(|error| AcauaError::CommandFailed {
|
||||
cmd: format!("curl {url}"),
|
||||
detail: error.to_string(),
|
||||
})?;
|
||||
if !output.status.success() {
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: format!("curl {url}"),
|
||||
detail: String::from_utf8_lossy(&output.stderr).into_owned(),
|
||||
});
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
||||
}
|
||||
|
||||
fn curl_download(url: &str, destination: &Path) -> Result<()> {
|
||||
let status = Command::new("curl")
|
||||
.args(["-fsSL", "-H", "User-Agent: acaua-updater", "-o"])
|
||||
.arg(destination)
|
||||
.arg(url)
|
||||
.status()
|
||||
.map_err(|error| AcauaError::CommandFailed {
|
||||
cmd: format!("curl {url}"),
|
||||
detail: error.to_string(),
|
||||
})?;
|
||||
if !status.success() {
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: format!("curl {url}"),
|
||||
detail: "download failed".to_string(),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn asset_suffix_for_platform() -> String {
|
||||
let os = if cfg!(target_os = "linux") {
|
||||
"linux"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
"macos"
|
||||
} else {
|
||||
"windows"
|
||||
};
|
||||
let arch = if cfg!(target_arch = "x86_64") {
|
||||
"x86_64"
|
||||
} else {
|
||||
"aarch64"
|
||||
};
|
||||
let extension = if cfg!(windows) { "zip" } else { "tar.gz" };
|
||||
format!("-{os}-{arch}.{extension}")
|
||||
}
|
||||
|
||||
pub fn check(current_version: &str) -> Result<UpdateStatus> {
|
||||
let body = match curl_text(LATEST_RELEASE_URL) {
|
||||
Ok(body) => body,
|
||||
Err(_) => {
|
||||
return Ok(UpdateStatus {
|
||||
current_version: current_version.to_string(),
|
||||
latest_version: None,
|
||||
update_available: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
let release: Release = serde_json::from_str(&body)?;
|
||||
let latest = release.tag_name.trim_start_matches('v').to_string();
|
||||
let update_available = latest != current_version;
|
||||
Ok(UpdateStatus {
|
||||
current_version: current_version.to_string(),
|
||||
latest_version: Some(latest),
|
||||
update_available,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn apply(current_exe: &Path) -> Result<String> {
|
||||
let is_cli = current_exe
|
||||
.file_stem()
|
||||
.map(|stem| stem == "acaua")
|
||||
.unwrap_or(false);
|
||||
if !is_cli {
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: format!(
|
||||
"automatic updates only replace the acaua CLI; download the desktop installer from {RELEASES_PAGE_URL}"
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
let body = curl_text(LATEST_RELEASE_URL)?;
|
||||
let release: Release = serde_json::from_str(&body)?;
|
||||
let suffix = asset_suffix_for_platform();
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name.ends_with(&suffix))
|
||||
.ok_or_else(|| AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: format!(
|
||||
"no asset ending in `{suffix}` in release {}",
|
||||
release.tag_name
|
||||
),
|
||||
})?;
|
||||
let checksum_asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|candidate| candidate.name == format!("{}.sha256", asset.name));
|
||||
|
||||
let temp_dir = std::env::temp_dir().join("acaua-update");
|
||||
fs::create_dir_all(&temp_dir)?;
|
||||
let archive_path = temp_dir.join(&asset.name);
|
||||
curl_download(&asset.browser_download_url, &archive_path)?;
|
||||
|
||||
if let Some(checksum_asset) = checksum_asset {
|
||||
let expected = curl_text(&checksum_asset.browser_download_url)?;
|
||||
let expected_hash = expected
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.to_lowercase();
|
||||
let data = fs::read(&archive_path)?;
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&data);
|
||||
let actual_hash = format!("{:x}", hasher.finalize());
|
||||
if actual_hash != expected_hash {
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: "SHA-256 checksum mismatch; update discarded".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let extract_dir = temp_dir.join("extracted");
|
||||
let _ = fs::remove_dir_all(&extract_dir);
|
||||
fs::create_dir_all(&extract_dir)?;
|
||||
let status = Command::new("tar")
|
||||
.arg("xf")
|
||||
.arg(&archive_path)
|
||||
.arg("-C")
|
||||
.arg(&extract_dir)
|
||||
.status()
|
||||
.map_err(|error| AcauaError::CommandFailed {
|
||||
cmd: "tar xf".to_string(),
|
||||
detail: error.to_string(),
|
||||
})?;
|
||||
if !status.success() {
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "tar xf".to_string(),
|
||||
detail: "extraction failed".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let new_binary = find_binary(&extract_dir)?;
|
||||
let backup = current_exe.with_extension("bak");
|
||||
let _ = fs::remove_file(&backup);
|
||||
fs::rename(current_exe, &backup)?;
|
||||
fs::copy(&new_binary, current_exe)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut permissions = fs::metadata(current_exe)?.permissions();
|
||||
permissions.set_mode(0o755);
|
||||
fs::set_permissions(current_exe, permissions)?;
|
||||
}
|
||||
|
||||
Ok(release.tag_name)
|
||||
}
|
||||
|
||||
fn find_binary(directory: &Path) -> Result<PathBuf> {
|
||||
fn is_binary(name: &OsStr) -> bool {
|
||||
name == "acaua" || name == "acaua.exe"
|
||||
}
|
||||
|
||||
for entry in fs::read_dir(directory)? {
|
||||
let entry = entry?;
|
||||
if is_binary(&entry.file_name()) {
|
||||
return Ok(entry.path());
|
||||
}
|
||||
if entry.file_type().map(|kind| kind.is_dir()).unwrap_or(false) {
|
||||
for inner in fs::read_dir(entry.path())? {
|
||||
let inner = inner?;
|
||||
if is_binary(&inner.file_name()) {
|
||||
return Ok(inner.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: "binary not found in the downloaded package".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Desktop GUI installation type detected at runtime.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum DesktopInstall {
|
||||
/// Linux AppImage: real filesystem path of the .AppImage file (from APPIMAGE env var).
|
||||
AppImage(PathBuf),
|
||||
/// Linux Debian package installed via dpkg: `current_exe` is reliable here.
|
||||
Deb,
|
||||
/// Windows NSIS installer: downloaded setup executable run with `/S`
|
||||
/// (silent). Does not relaunch the app on its own — see the comment on
|
||||
/// the `WindowsInstaller` apply path.
|
||||
WindowsInstaller,
|
||||
}
|
||||
|
||||
/// Detects the desktop installation type given an optional `APPIMAGE` environment
|
||||
/// variable value and the current executable path.
|
||||
pub fn detect_desktop_install_from(
|
||||
appimage_env: Option<&std::ffi::OsStr>,
|
||||
current_exe: &Path,
|
||||
) -> Result<DesktopInstall> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let _ = (appimage_env, current_exe);
|
||||
Ok(DesktopInstall::WindowsInstaller)
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if let Some(val) = appimage_env {
|
||||
let path = PathBuf::from(val);
|
||||
if path.is_file() {
|
||||
return Ok(DesktopInstall::AppImage(path));
|
||||
}
|
||||
}
|
||||
if current_exe.is_file()
|
||||
&& current_exe
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|e| e.eq_ignore_ascii_case("appimage"))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Ok(DesktopInstall::AppImage(current_exe.to_path_buf()));
|
||||
}
|
||||
Ok(DesktopInstall::Deb)
|
||||
}
|
||||
#[cfg(not(any(windows, target_os = "linux")))]
|
||||
{
|
||||
let _ = (appimage_env, current_exe);
|
||||
Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: format!(
|
||||
"automatic self-update is not supported on this platform; download manually from {RELEASES_PAGE_URL}"
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Detects the desktop installation type using the live environment.
|
||||
pub fn detect_desktop_install() -> Result<DesktopInstall> {
|
||||
let exe = std::env::current_exe()?;
|
||||
detect_desktop_install_from(std::env::var_os("APPIMAGE").as_deref(), &exe)
|
||||
}
|
||||
|
||||
/// Returns the expected release asset suffix for a desktop installation and CPU architecture.
|
||||
pub fn gui_asset_suffix_for(install: &DesktopInstall, arch: &str) -> Result<&'static str> {
|
||||
match install {
|
||||
DesktopInstall::AppImage(_) => match arch {
|
||||
"x86_64" => Ok("_amd64.AppImage"),
|
||||
"aarch64" => Ok("_aarch64.AppImage"),
|
||||
other => Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: format!("unsupported architecture for AppImage: {other}"),
|
||||
}),
|
||||
},
|
||||
DesktopInstall::Deb => match arch {
|
||||
"x86_64" => Ok("_amd64.deb"),
|
||||
"aarch64" => Ok("_arm64.deb"),
|
||||
other => Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: format!("unsupported architecture for deb package: {other}"),
|
||||
}),
|
||||
},
|
||||
DesktopInstall::WindowsInstaller => match arch {
|
||||
"x86_64" => Ok("_x64-setup.exe"),
|
||||
other => Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: format!("unsupported architecture for Windows installer: {other}"),
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the expected release asset suffix for the current architecture.
|
||||
pub fn gui_asset_suffix(install: &DesktopInstall) -> Result<&'static str> {
|
||||
gui_asset_suffix_for(install, std::env::consts::ARCH)
|
||||
}
|
||||
|
||||
/// Downloads, verifies, and installs the latest desktop GUI release in place.
|
||||
pub fn apply_desktop(current_exe: &Path) -> Result<String> {
|
||||
apply_desktop_with_progress(current_exe, |_| {})
|
||||
}
|
||||
|
||||
/// Downloads, verifies, and installs the latest desktop GUI release in place,
|
||||
/// notifying the progress callback of phase transitions (`"downloading"`,
|
||||
/// `"verifying"`, `"installing"`, `"restarting"`).
|
||||
pub fn apply_desktop_with_progress<F>(current_exe: &Path, on_progress: F) -> Result<String>
|
||||
where
|
||||
F: Fn(&str),
|
||||
{
|
||||
let install =
|
||||
detect_desktop_install_from(std::env::var_os("APPIMAGE").as_deref(), current_exe)?;
|
||||
let body = curl_text(LATEST_RELEASE_URL)?;
|
||||
let release: Release = serde_json::from_str(&body)?;
|
||||
|
||||
let suffix = gui_asset_suffix(&install)?;
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name.ends_with(suffix))
|
||||
.ok_or_else(|| AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: format!(
|
||||
"no asset ending with `{suffix}` in release {}",
|
||||
release.tag_name
|
||||
),
|
||||
})?;
|
||||
let checksum_asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == format!("{}.sha256", asset.name));
|
||||
|
||||
let tmp_dir = std::env::temp_dir().join("acaua-desktop-update");
|
||||
fs::create_dir_all(&tmp_dir)?;
|
||||
let download_path = tmp_dir.join(&asset.name);
|
||||
|
||||
on_progress("downloading");
|
||||
curl_download(&asset.browser_download_url, &download_path)?;
|
||||
|
||||
if let Some(checksum_asset) = checksum_asset {
|
||||
on_progress("verifying");
|
||||
let expected = curl_text(&checksum_asset.browser_download_url)?;
|
||||
let expected_hash = expected
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.to_lowercase();
|
||||
let data = fs::read(&download_path)?;
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&data);
|
||||
let actual_hash: String = hasher
|
||||
.finalize()
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect();
|
||||
if actual_hash != expected_hash {
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: "SHA-256 checksum mismatch — update discarded".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
on_progress("installing");
|
||||
match install {
|
||||
DesktopInstall::AppImage(target_appimage) => {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let parent_dir = target_appimage.parent().unwrap_or_else(|| Path::new("."));
|
||||
let temp_name = format!(
|
||||
".{}.tmp-{}",
|
||||
target_appimage
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("appimage"),
|
||||
std::process::id()
|
||||
);
|
||||
let temp_file = parent_dir.join(temp_name);
|
||||
fs::copy(&download_path, &temp_file)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = fs::metadata(&temp_file)?.permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(&temp_file, perms)?;
|
||||
}
|
||||
fs::rename(&temp_file, &target_appimage)?;
|
||||
let _ = fs::remove_file(&download_path);
|
||||
|
||||
on_progress("restarting");
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
let err = Command::new(&target_appimage)
|
||||
.args(std::env::args().skip(1))
|
||||
.exec();
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: format!("failed to relaunch AppImage: {err}"),
|
||||
});
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
let _ = (&target_appimage, &download_path);
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: "AppImage updates are only supported on Linux".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
DesktopInstall::Deb => {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let status = Command::new("dpkg")
|
||||
.args(["-i"])
|
||||
.arg(&download_path)
|
||||
.status()
|
||||
.map_err(|e| AcauaError::CommandFailed {
|
||||
cmd: "dpkg -i".to_string(),
|
||||
detail: e.to_string(),
|
||||
})?;
|
||||
if !status.success() {
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "dpkg -i".to_string(),
|
||||
detail: format!("dpkg -i failed with exit code {:?}", status.code()),
|
||||
});
|
||||
}
|
||||
let _ = fs::remove_file(&download_path);
|
||||
|
||||
on_progress("restarting");
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
let err = Command::new(current_exe)
|
||||
.args(std::env::args().skip(1))
|
||||
.exec();
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: format!("failed to relaunch updated app: {err}"),
|
||||
});
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
let _ = (&download_path, current_exe);
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: "deb updates are only supported on Linux".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
DesktopInstall::WindowsInstaller => {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// `/S` is the one NSIS silent-install switch with a documented,
|
||||
// stable meaning. There is no standard NSIS or Tauri flag that
|
||||
// makes a silent install relaunch the app afterwards — that was
|
||||
// claimed in an earlier draft of this code and could not be
|
||||
// verified against NSIS's or Tauri's documentation; passing an
|
||||
// invented flag to a stock NSIS installer is silently ignored,
|
||||
// not harmful, but also not a relaunch mechanism. Spawning
|
||||
// detached and exiting immediately (instead of waiting on the
|
||||
// child) also avoids a self-deadlock: if the installer's script
|
||||
// waits for this process to close before it can overwrite the
|
||||
// running .exe, blocking here on the installer's exit status
|
||||
// would hang forever. The practical effect: the update installs
|
||||
// silently, but the app does not reopen itself — the user needs
|
||||
// to start it again. This needs real-world confirmation on
|
||||
// Windows before relying on it; if testing shows the bundled
|
||||
// NSIS template does support an explicit relaunch flag or step,
|
||||
// wire it in here instead of this comment.
|
||||
on_progress("restarting");
|
||||
Command::new(&download_path)
|
||||
.arg("/S")
|
||||
.spawn()
|
||||
.map_err(|e| AcauaError::CommandFailed {
|
||||
cmd: "installer".to_string(),
|
||||
detail: format!("failed to launch installer: {e}"),
|
||||
})?;
|
||||
std::process::exit(0);
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let _ = &download_path;
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: "Windows installer updates are only supported on Windows".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
Ok(release.tag_name)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn apply_refuses_to_overwrite_the_desktop_app() {
|
||||
let error = apply(Path::new("/opt/acaua/acaua-gui")).unwrap_err();
|
||||
assert!(error.to_string().contains("CLI"), "{error}");
|
||||
assert!(error.to_string().contains(RELEASES_PAGE_URL), "{error}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_desktop_install_variants() {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let temp_appimage =
|
||||
std::env::temp_dir().join(format!("test-acaua-{}.AppImage", std::process::id()));
|
||||
let _ = fs::write(&temp_appimage, b"dummy appimage");
|
||||
|
||||
let res = detect_desktop_install_from(
|
||||
Some(temp_appimage.as_os_str()),
|
||||
Path::new("/usr/bin/acaua-gui"),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(res, DesktopInstall::AppImage(temp_appimage.clone()));
|
||||
|
||||
let res_fallback = detect_desktop_install_from(None, &temp_appimage).unwrap();
|
||||
assert_eq!(
|
||||
res_fallback,
|
||||
DesktopInstall::AppImage(temp_appimage.clone())
|
||||
);
|
||||
|
||||
let _ = fs::remove_file(&temp_appimage);
|
||||
|
||||
let res_deb =
|
||||
detect_desktop_install_from(None, Path::new("/usr/bin/acaua-gui")).unwrap();
|
||||
assert_eq!(res_deb, DesktopInstall::Deb);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let res = detect_desktop_install_from(
|
||||
None,
|
||||
Path::new("C:/Program Files/Acaua/acaua-gui.exe"),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(res, DesktopInstall::WindowsInstaller);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gui_asset_suffix_matches_known_release_naming() {
|
||||
let appimage = DesktopInstall::AppImage(PathBuf::from("/tmp/Acaua.AppImage"));
|
||||
assert_eq!(
|
||||
gui_asset_suffix_for(&appimage, "x86_64").unwrap(),
|
||||
"_amd64.AppImage"
|
||||
);
|
||||
assert_eq!(
|
||||
gui_asset_suffix_for(&appimage, "aarch64").unwrap(),
|
||||
"_aarch64.AppImage"
|
||||
);
|
||||
|
||||
let deb = DesktopInstall::Deb;
|
||||
assert_eq!(gui_asset_suffix_for(&deb, "x86_64").unwrap(), "_amd64.deb");
|
||||
assert_eq!(gui_asset_suffix_for(&deb, "aarch64").unwrap(), "_arm64.deb");
|
||||
|
||||
let win = DesktopInstall::WindowsInstaller;
|
||||
assert_eq!(
|
||||
gui_asset_suffix_for(&win, "x86_64").unwrap(),
|
||||
"_x64-setup.exe"
|
||||
);
|
||||
|
||||
assert!(gui_asset_suffix_for(&appimage, "mips").is_err());
|
||||
assert!(gui_asset_suffix_for(&deb, "riscv64").is_err());
|
||||
assert!(gui_asset_suffix_for(&win, "arm").is_err());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user