libplasmoid-updater 0.2.0

Library for updating KDE Plasma 6 components from the KDE Store. Meant for use in topgrade.
Documentation
// SPDX-License-Identifier: GPL-3.0-or-later

use std::path::{Path, PathBuf};
use std::sync::OnceLock;

/// Returns the user's data directory, respecting XDG_DATA_HOME.
pub(crate) fn data_home() -> PathBuf {
    std::env::var("XDG_DATA_HOME")
        .map(PathBuf::from)
        .unwrap_or_else(|_| user_home().join(".local/share"))
}

/// Returns the user's cache directory, respecting XDG_CACHE_HOME.
pub(crate) fn cache_home() -> PathBuf {
    std::env::var("XDG_CACHE_HOME")
        .map(PathBuf::from)
        .unwrap_or_else(|_| user_home().join(".cache"))
}

/// Returns the XDG runtime directory, or a UID-namespaced /tmp fallback.
pub(crate) fn runtime_dir() -> PathBuf {
    std::env::var("XDG_RUNTIME_DIR")
        .map(PathBuf::from)
        .unwrap_or_else(|_| {
            PathBuf::from(format!(
                "/tmp/plasmoid-updater-{}",
                nix::unistd::Uid::effective()
            ))
        })
}

/// Returns the KNewStuff3 registry directory.
pub(crate) fn knewstuff_dir() -> PathBuf {
    data_home().join("knewstuff3")
}

/// Returns true if KDE Plasma is detected on this system.
///
/// Checks for the presence of the KNewStuff3 registry directory, which is
/// created when KDE Plasma runs for the first time. This works over SSH and
/// in headless environments where session environment variables are not set.
pub(crate) fn is_kde() -> bool {
    knewstuff_dir().exists()
}

static USER_HOME: OnceLock<PathBuf> = OnceLock::new();

/// Gets the user's home directory, even when running with sudo.
/// Cached after first call via `OnceLock`.
fn user_home() -> &'static Path {
    USER_HOME.get_or_init(resolve_user_home)
}

fn resolve_user_home() -> PathBuf {
    if let Ok(sudo_home) = std::env::var("SUDO_USER_HOME") {
        return PathBuf::from(sudo_home);
    }

    if let Ok(sudo_user) = std::env::var("SUDO_USER") {
        if let Ok(output) = std::process::Command::new("getent")
            .args(["passwd", &sudo_user])
            .output()
            && let Ok(line) = String::from_utf8(output.stdout)
            && let Some(home) = line.split(':').nth(5)
        {
            return PathBuf::from(home);
        }
        return PathBuf::from(format!("/home/{}", sudo_user));
    }

    std::env::var("HOME")
        .map(PathBuf::from)
        .unwrap_or_else(|_| {
            dirs::home_dir().unwrap_or_else(|| {
                log::warn!(
                    target: "paths",
                    "could not determine home directory; \
                     defaulting to /tmp/plasmoid-updater-fallback"
                );
                PathBuf::from("/tmp/plasmoid-updater-fallback")
            })
        })
}

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

    #[test]
    fn user_home_returns_consistent_path() {
        let first = user_home();
        let second = user_home();
        assert_eq!(first, second);
        assert!(std::ptr::eq(first, second), "should return same &'static ref");
    }

    #[test]
    fn data_home_is_under_user_home_or_xdg() {
        let dh = data_home();
        if std::env::var("XDG_DATA_HOME").is_ok() {
            // XDG override — just check it's a PathBuf
            assert!(!dh.as_os_str().is_empty());
        } else {
            assert!(dh.starts_with(user_home()));
        }
    }

    #[test]
    fn cache_home_is_under_user_home_or_xdg() {
        let ch = cache_home();
        if std::env::var("XDG_CACHE_HOME").is_ok() {
            assert!(!ch.as_os_str().is_empty());
        } else {
            assert!(ch.starts_with(user_home()));
        }
    }
}