tsafe-core 1.0.12

Core runtime engine for tsafe — encrypted credential storage, process injection contracts, audit log, RBAC
Documentation
//! Optional self-update check against a ProGet Universal Package feed.
//!
//! All functions are no-ops (return `None` / `Ok(())`) when the
//! `PROGET_BASE_URL` env var is unset or the request fails, so the caller
//! never needs to handle update-check failures as hard errors.

/// The version of this build. At runtime, prefers `TSAFE_CLI_VERSION` (set
/// by the CLI binary before launching the TUI) so the UI always shows the
/// installed binary version rather than the tsafe-core crate version.
pub const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");

/// Resolve the effective binary version for display: CLI version if injected,
/// otherwise the compiled-in tsafe-core version.
pub fn display_version() -> &'static str {
    // SAFETY: env var is set once at process start before any threads spawn.
    // We leak the String to get a 'static str for display convenience.
    std::env::var("TSAFE_CLI_VERSION")
        .ok()
        .map(|v| -> &'static str { Box::leak(v.into_boxed_str()) })
        .unwrap_or(PKG_VERSION)
}

/// Query ProGet for the latest Universal Package version (white-label; configure feed/package via env).
///
/// Returns `Some(version_string)` if a newer version is available, `None`
/// otherwise. Silently returns `None` when `PROGET_BASE_URL` is not set or
/// the request fails — callers should treat `None` as "no update info".
pub fn check_for_update() -> Option<String> {
    let base_url = std::env::var("PROGET_BASE_URL").ok()?;
    if !base_url.starts_with("https://") {
        return None;
    }
    let feed = std::env::var("PROGET_FEED").unwrap_or_else(|_| "tsafe".to_string());
    let pkg = std::env::var("PROGET_PACKAGE_NAME").unwrap_or_else(|_| "tsafe".to_string());
    let url = format!("{base_url}/upack/{feed}/versions?packageName={pkg}&count=1");

    let agent = ureq::AgentBuilder::new()
        .timeout_connect(std::time::Duration::from_secs(3))
        .timeout(std::time::Duration::from_secs(5))
        .build();

    let json: serde_json::Value = agent.get(&url).call().ok()?.into_json().ok()?;

    // ProGet returns a JSON array; the first element is the latest version.
    // The version field is lowercase in ProGet v5+ but may be "Version" on older.
    let entry = json.as_array()?.first()?;
    let latest = entry
        .get("version")
        .or_else(|| entry.get("Version"))
        .and_then(|v| v.as_str())?
        .to_string();

    if is_newer(&latest, PKG_VERSION) {
        Some(latest)
    } else {
        None
    }
}

/// Returns `true` if `candidate` is a strictly higher semver than `current`.
fn is_newer(candidate: &str, current: &str) -> bool {
    fn parse(v: &str) -> (u64, u64, u64) {
        let mut p = v.trim_start_matches('v').split('.');
        let major: u64 = p.next().and_then(|s| s.parse().ok()).unwrap_or(0);
        let minor: u64 = p.next().and_then(|s| s.parse().ok()).unwrap_or(0);
        let patch: u64 = p.next().and_then(|s| s.parse().ok()).unwrap_or(0);
        (major, minor, patch)
    }
    parse(candidate) > parse(current)
}

#[cfg(test)]
mod tests {
    use super::is_newer;

    #[test]
    fn newer_minor_detected() {
        assert!(is_newer("0.2.0", "0.1.0"));
    }

    #[test]
    fn newer_major_detected() {
        assert!(is_newer("1.0.0", "0.9.9"));
    }

    #[test]
    fn newer_patch_detected() {
        assert!(is_newer("0.1.1", "0.1.0"));
    }

    #[test]
    fn same_version_not_newer() {
        assert!(!is_newer("0.1.0", "0.1.0"));
    }

    #[test]
    fn older_version_not_newer() {
        assert!(!is_newer("0.0.9", "0.1.0"));
    }

    #[test]
    fn v_prefix_stripped() {
        assert!(is_newer("v0.2.0", "0.1.0"));
        assert!(!is_newer("v0.1.0", "0.1.0"));
    }
}