lios 0.1.75

A gorgeous GTK4/VTE Linux terminal with live themes, glass backgrounds, session prompt profiles, Sixel images, and safe GPU controls.
use std::env;
use std::fs::{self, OpenOptions};
use std::io::{Read, Write};
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};

const DEFAULT_BACKGROUND_BYTES: &[u8] = include_bytes!("../assets/default-background.png");
const DEFAULT_BACKGROUND_FILE_NAME: &str = "default-background.png";
static ASSET_WRITE_ID: AtomicU64 = AtomicU64::new(0);

pub fn ensure_default_background() -> Result<PathBuf, String> {
    let path = default_background_path()?;
    install_default_background(&path)?;
    Ok(path)
}

fn install_default_background(path: &Path) -> Result<(), String> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).map_err(|error| {
            format!(
                "failed to create default background directory '{}': {error}",
                parent.display()
            )
        })?;
    }

    // Do not follow asset symlinks, block on a FIFO, or read an arbitrarily
    // large replacement. Open and inspect the same descriptor before reading.
    if let Ok(file) = OpenOptions::new()
        .read(true)
        .custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK)
        .open(path)
    {
        if file.metadata().is_ok_and(|metadata| {
            metadata.is_file() && metadata.len() == DEFAULT_BACKGROUND_BYTES.len() as u64
        }) {
            let mut contents = Vec::with_capacity(DEFAULT_BACKGROUND_BYTES.len());
            if file
                .take(DEFAULT_BACKGROUND_BYTES.len() as u64 + 1)
                .read_to_end(&mut contents)
                .is_ok()
                && contents == DEFAULT_BACKGROUND_BYTES
            {
                return Ok(());
            }
        }
    }

    // Atomic replacement cannot truncate the destination of a symlink or a
    // hard link, and a crash cannot leave a partially written installed image.
    let parent = path
        .parent()
        .ok_or("default background has no parent directory")?;
    let nonce = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_nanos();
    let id = ASSET_WRITE_ID.fetch_add(1, Ordering::Relaxed);
    let temporary = parent.join(format!(
        ".lios-background-{}-{nonce}-{id}.tmp",
        std::process::id()
    ));
    let mut file = OpenOptions::new()
        .write(true)
        .create_new(true)
        .mode(0o644)
        .open(&temporary)
        .map_err(|error| format!("failed to create default background: {error}"))?;
    let result = file
        .write_all(DEFAULT_BACKGROUND_BYTES)
        .and_then(|()| fs::rename(&temporary, path));
    if let Err(error) = result {
        let _ = fs::remove_file(&temporary);
        return Err(format!(
            "failed to install default background '{}': {error}",
            path.display()
        ));
    }
    Ok(())
}

fn default_background_path() -> Result<PathBuf, String> {
    Ok(data_home()?.join("lios").join(DEFAULT_BACKGROUND_FILE_NAME))
}

fn data_home() -> Result<PathBuf, String> {
    if let Ok(data_home) = env::var("XDG_DATA_HOME") {
        if !data_home.is_empty() {
            return Ok(PathBuf::from(data_home));
        }
    }

    let home = env::var("HOME")
        .map_err(|_| "unable to determine data directory; set HOME or XDG_DATA_HOME".to_string())?;
    if home.is_empty() {
        return Err("unable to determine data directory; HOME is empty".to_string());
    }

    Ok(PathBuf::from(home).join(".local/share"))
}

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

    #[test]
    fn background_replacement_is_bounded_and_does_not_follow_links() {
        let root = std::env::temp_dir().join(format!("lios-asset-test-{}", std::process::id()));
        fs::create_dir(&root).unwrap();
        let target = root.join("keep.txt");
        let image = root.join("image.png");
        fs::write(&target, "keep my data").unwrap();
        std::os::unix::fs::symlink(&target, &image).unwrap();
        install_default_background(&image).unwrap();
        assert_eq!(fs::read_to_string(&target).unwrap(), "keep my data");
        assert_eq!(fs::read(&image).unwrap(), DEFAULT_BACKGROUND_BYTES);
        let modified = fs::metadata(&image).unwrap().modified().unwrap();
        install_default_background(&image).unwrap();
        assert_eq!(fs::metadata(&image).unwrap().modified().unwrap(), modified);
        fs::remove_file(&image).unwrap();
        fs::hard_link(&target, &image).unwrap();
        install_default_background(&image).unwrap();
        assert_eq!(fs::read_to_string(&target).unwrap(), "keep my data");
        OpenOptions::new()
            .write(true)
            .open(&image)
            .unwrap()
            .set_len(1 << 34)
            .unwrap();
        install_default_background(&image).unwrap();
        assert_eq!(fs::read(&image).unwrap(), DEFAULT_BACKGROUND_BYTES);
        fs::remove_file(&image).unwrap();
        fs::create_dir(&image).unwrap();
        assert!(install_default_background(&image).is_err());
        assert_eq!(
            fs::read_dir(&root).unwrap().count(),
            2,
            "failed writes must clean up the temporary image"
        );
        fs::remove_dir_all(root).unwrap();
    }
}