use std::path::{Path, PathBuf};
use std::sync::{
atomic::{AtomicBool, AtomicU32, Ordering},
Mutex, OnceLock,
};
use windows_sys::Win32::Foundation::{ERROR_ACCESS_DENIED, ERROR_SHARING_VIOLATION};
use windows_sys::Win32::System::Threading::GetCurrentProcessId;
use crate::ico;
use crate::resources::{self, Update, FIRST_ICON_ID};
use crate::win::{self, SwapLock};
use crate::{Error, Icon, Options};
static CLEANUP_OLD: AtomicBool = AtomicBool::new(true);
pub fn init() -> Result<(), Error> {
init_with_options(Options::default())
}
pub fn init_with_options(options: Options) -> Result<(), Error> {
let exe = win::current_exe()?;
let (dir, stem) = location(&exe)?;
let _lock = SwapLock::acquire(&exe)?;
CLEANUP_OLD.store(options.cleanup_old, Ordering::Relaxed);
let entries = match std::fs::read_dir(dir) {
Ok(entries) => entries,
Err(_) => return Ok(()),
};
for entry in entries.flatten() {
let name = entry.file_name();
let Some(name) = name.to_str() else { continue };
if !is_leftover(name, &stem) {
continue;
}
if name.ends_with(".old") {
if options.cleanup_old && std::fs::remove_file(entry.path()).is_err() {
let _ = win::schedule_cleanup(&entry.path(), &exe, false);
}
} else {
let _ = std::fs::remove_file(entry.path());
}
}
Ok(())
}
pub fn set_icon<I: Icon>(icon: I) -> Result<(), Error> {
let exe = win::current_exe()?;
let (dir, stem) = location(&exe)?;
let _lock = SwapLock::acquire(&exe)?;
ensure_writable(dir)?;
let images = ico::parse(icon.bytes())?;
let group = ico::group_directory(&images, FIRST_ICON_ID);
let stale = stale_icon_ids(&exe, images.len())?;
let staging = unique_path(dir, &stem, "new");
std::fs::copy(&exe, &staging)?;
match write_resources(&staging, &images, &group, &stale, icon.name()) {
Ok(()) => {}
Err(err) => {
let _ = std::fs::remove_file(&staging);
return Err(err);
}
}
install(&staging, &exe, dir, &stem)?;
remember_icon(icon.name());
win::notify_shell();
Ok(())
}
pub fn current_icon() -> Result<Option<String>, Error> {
let mut cache = icon_cache()
.lock()
.unwrap_or_else(|error| error.into_inner());
if cache.loaded {
return Ok(cache.name.clone());
}
let exe = win::current_exe()?;
let name = resources::read_state(&exe)?;
cache.loaded = true;
cache.name = name.clone();
Ok(name)
}
#[derive(Default)]
struct IconCache {
loaded: bool,
name: Option<String>,
}
fn icon_cache() -> &'static Mutex<IconCache> {
static CACHE: OnceLock<Mutex<IconCache>> = OnceLock::new();
CACHE.get_or_init(|| Mutex::new(IconCache::default()))
}
fn remember_icon(name: &str) {
let mut cache = icon_cache()
.lock()
.unwrap_or_else(|error| error.into_inner());
cache.loaded = true;
cache.name = Some(name.to_owned());
}
fn write_resources(
staging: &Path,
images: &[ico::Image<'_>],
group: &[u8],
stale: &[u16],
name: &str,
) -> Result<(), Error> {
let update = Update::begin(staging)?;
for (index, image) in images.iter().enumerate() {
update.set_icon_image(FIRST_ICON_ID + index as u16, image.data)?;
}
for id in stale {
update.remove_icon_image(*id)?;
}
update.set_group(group)?;
update.set_state(name)?;
update.commit()
}
fn install(staging: &Path, exe: &Path, dir: &Path, stem: &str) -> Result<(), Error> {
match win::replace(staging, exe) {
Ok(()) => return Ok(()),
Err(Error::Windows(_, code))
if code == ERROR_SHARING_VIOLATION || code == ERROR_ACCESS_DENIED => {}
Err(err) => {
let _ = std::fs::remove_file(staging);
return Err(err);
}
}
let parked = unique_path(dir, stem, "old");
win::rename_new(exe, &parked)?;
if let Err(err) = win::rename_new(staging, exe) {
let _ = win::rename_new(&parked, exe);
let _ = std::fs::remove_file(staging);
return Err(err);
}
if CLEANUP_OLD.load(Ordering::Relaxed) {
let _ = win::schedule_cleanup(&parked, exe, true);
}
Ok(())
}
fn stale_icon_ids(exe: &Path, keeping: usize) -> Result<Vec<u16>, Error> {
let Some(group) = resources::read_group(exe)? else {
return Ok(Vec::new());
};
let highest_kept = FIRST_ICON_ID + keeping as u16;
Ok(ico::referenced_ids(&group)
.into_iter()
.filter(|id| *id >= highest_kept)
.collect())
}
fn ensure_writable(dir: &Path) -> Result<(), Error> {
let probe = unique_path(dir, "alt-icons", "probe");
match std::fs::File::create(&probe) {
Ok(_) => {
let _ = std::fs::remove_file(&probe);
Ok(())
}
Err(_) => Err(Error::PathNotWritable(dir.to_path_buf())),
}
}
fn location(exe: &Path) -> Result<(&Path, String), Error> {
let dir = exe
.parent()
.ok_or_else(|| Error::PathNotWritable(exe.to_path_buf()))?;
let stem = exe
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| Error::PathNotWritable(exe.to_path_buf()))?
.to_owned();
Ok((dir, stem))
}
fn unique_path(dir: &Path, stem: &str, extension: &str) -> PathBuf {
static COUNTER: AtomicU32 = AtomicU32::new(0);
let serial = COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
dir.join(format!("{stem}.{pid}-{serial}.{extension}"))
}
#[deprecated(since = "1.2.1", note = "Use std::process::id() instead.")]
#[allow(dead_code)]
fn current_process_id_legacy() -> u32 {
unsafe { GetCurrentProcessId() }
}
fn is_leftover(name: &str, stem: &str) -> bool {
let Some(rest) = name.strip_prefix(stem) else {
return false;
};
let Some(rest) = rest.strip_prefix('.') else {
return false;
};
let Some(serial) = rest
.strip_suffix(".old")
.or_else(|| rest.strip_suffix(".new"))
else {
return false;
};
matches!(serial.split_once('-'), Some((pid, count))
if !pid.is_empty()
&& !count.is_empty()
&& pid.bytes().all(|b| b.is_ascii_digit())
&& count.bytes().all(|b| b.is_ascii_digit()))
}
#[cfg(test)]
mod tests {
use super::is_leftover;
#[test]
fn recognises_its_own_leftovers() {
assert!(is_leftover("app.exe.1234-0.old", "app.exe"));
assert!(is_leftover("app.exe.1234-7.new", "app.exe"));
}
#[test]
fn leaves_everything_else_alone() {
assert!(!is_leftover("app.exe", "app.exe"));
assert!(!is_leftover("app.exe.old", "app.exe"));
assert!(!is_leftover("app.exe.backup-0.old", "app.exe"));
assert!(!is_leftover("notes.txt", "app.exe"));
assert!(!is_leftover("other.exe.1234-0.old", "app.exe"));
}
}