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()
)
})?;
}
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(());
}
}
}
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();
}
}