oneseed 0.5.2

Deterministic cryptographic keys from a single seed
use std::fs;
use std::io::{Read, Write};
use std::path::Path;

const REPO: &str = "oeo/1seed";

pub fn update(check_only: bool) -> Result<(), Box<dyn std::error::Error>> {
    let current_version = env!("CARGO_PKG_VERSION");
    let binary_path = std::env::current_exe()?;

    if !is_writable(&binary_path) {
        return Err(format!(
            "Cannot write to {}. Try: sudo 1seed update",
            binary_path.display()
        )
        .into());
    }

    eprintln!("Checking for updates...");
    let latest = fetch_latest_version()?;
    let latest_version = latest.trim_start_matches('v');

    if latest_version == current_version {
        eprintln!("Already up to date (v{})", current_version);
        return Ok(());
    }

    eprintln!("Current: v{}", current_version);
    eprintln!("Latest:  v{}", latest_version);

    if check_only {
        eprintln!("\nRun '1seed update' to install the latest version");
        return Ok(());
    }

    eprint!("\nUpdate to v{}? [y/N] ", latest_version);
    std::io::stderr().flush()?;

    let mut response = String::new();
    std::io::stdin().read_line(&mut response)?;

    if response.trim().to_lowercase() != "y" {
        eprintln!("Cancelled");
        return Ok(());
    }

    let platform = detect_platform();
    eprintln!("Downloading v{} for {}...", latest_version, platform);

    let binary_data = download_and_extract(&latest, &platform)?;

    eprintln!("Installing...");
    let temp_path = binary_path.with_extension("new");
    fs::write(&temp_path, &binary_data)?;

    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        fs::set_permissions(&temp_path, fs::Permissions::from_mode(0o755))?;
    }

    fs::rename(&temp_path, &binary_path)?;

    eprintln!("✓ Updated to v{}", latest_version);
    Ok(())
}

fn fetch_latest_version() -> Result<String, Box<dyn std::error::Error>> {
    let url = format!("https://api.github.com/repos/{}/releases/latest", REPO);

    let response = ureq::get(&url).set("User-Agent", "1seed-updater").call()?;

    let json: serde_json::Value = response.into_json()?;

    Ok(json["tag_name"]
        .as_str()
        .ok_or("no tag_name in response")?
        .to_string())
}

fn detect_platform() -> String {
    let os = std::env::consts::OS;
    let arch = std::env::consts::ARCH;

    let os_name = match os {
        "linux" => "linux",
        "macos" => "darwin",
        "windows" => "windows",
        _ => os,
    };

    let arch_name = match arch {
        "x86_64" => "amd64",
        "aarch64" | "arm64" => "arm64",
        _ => arch,
    };

    format!("{}-{}", os_name, arch_name)
}

fn download_and_extract(
    version: &str,
    platform: &str,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
    let ext = if platform.contains("windows") {
        "zip"
    } else {
        "tar.gz"
    };

    let asset_name = format!("1seed-{}.{}", platform, ext);
    let url = format!(
        "https://github.com/{}/releases/download/{}/{}",
        REPO, version, asset_name
    );

    let response = ureq::get(&url).set("User-Agent", "1seed-updater").call()?;

    let mut archive_bytes = Vec::new();
    response.into_reader().read_to_end(&mut archive_bytes)?;

    if ext == "tar.gz" {
        extract_from_targz(&archive_bytes, platform)
    } else {
        Err("Windows zip extraction not yet implemented".into())
    }
}

fn extract_from_targz(
    archive_bytes: &[u8],
    platform: &str,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
    use flate2::read::GzDecoder;
    use tar::Archive;

    let decompressor = GzDecoder::new(archive_bytes);
    let mut archive = Archive::new(decompressor);

    let binary_name = format!("1seed-{}", platform);

    for entry in archive.entries()? {
        let mut entry = entry?;
        let path = entry.path()?;

        if path.file_name().and_then(|n| n.to_str()) == Some(&binary_name) {
            let mut binary_data = Vec::new();
            entry.read_to_end(&mut binary_data)?;
            return Ok(binary_data);
        }
    }

    Err(format!("Binary {} not found in archive", binary_name).into())
}

fn is_writable(path: &Path) -> bool {
    fs::OpenOptions::new().append(true).open(path).is_ok()
}