use std::path::{Path, PathBuf};
use std::process::Command;
use crate::util::fs_ctx;
use crate::{Res, tmp_manifests};
const MANIFEST_XML: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity type="win32" name="truce.standalone" version="1.0.0.0"/>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">True/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2,PerMonitor</dpiAwareness>
</windowsSettings>
</application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>
"#;
pub(crate) fn embed_dpi_manifest(exe: &Path) -> Res {
let manifest_path = tmp_manifests().join("truce-standalone.manifest");
fs_ctx::write(&manifest_path, MANIFEST_XML)?;
if let Some(mt) = locate_mt_exe() {
let resource_arg = format!("-outputresource:{};#1", exe.display());
let result = Command::new(&mt)
.args([
"-nologo",
"-manifest",
&manifest_path.display().to_string(),
&resource_arg,
])
.status();
match result {
Ok(s) if s.success() => return Ok(()),
Ok(s) => eprintln!(
" warning: mt.exe exited with {s} embedding manifest into {} \
— falling back to sidecar",
exe.display()
),
Err(e) => {
eprintln!(" warning: failed to invoke mt.exe ({e}) — falling back to sidecar");
}
}
} else {
eprintln!(
" note: mt.exe not found (install the Windows 10/11 SDK to embed \
the DPI manifest); writing sidecar instead."
);
}
fs_ctx::copy(&manifest_path, sidecar_path(exe))?;
Ok(())
}
fn sidecar_path(exe: &Path) -> PathBuf {
let mut s = exe.as_os_str().to_owned();
s.push(".manifest");
PathBuf::from(s)
}
fn locate_mt_exe() -> Option<PathBuf> {
if let Ok(p) = which("mt.exe") {
return Some(p);
}
let sdk_bin = PathBuf::from(r"C:\Program Files (x86)\Windows Kits\10\bin");
std::fs::read_dir(&sdk_bin)
.ok()?
.flatten()
.map(|e| e.path().join(r"x64\mt.exe"))
.filter(|p| p.exists())
.max()
}
fn which(name: &str) -> std::io::Result<PathBuf> {
let path = std::env::var_os("PATH")
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "PATH not set"))?;
for dir in std::env::split_paths(&path) {
let candidate = dir.join(name);
if candidate.is_file() {
return Ok(candidate);
}
}
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("{name} not found"),
))
}
fn make_int_resource(id: u16) -> *const u16 {
std::ptr::without_provenance(id as usize)
}
fn u16_or(value: usize, ctx: &str) -> std::result::Result<u16, crate::BoxErr> {
u16::try_from(value)
.map_err(|_| -> crate::BoxErr { format!("{ctx}: value {value} exceeds u16 range").into() })
}
fn u32_or(value: usize, ctx: &str) -> std::result::Result<u32, crate::BoxErr> {
u32::try_from(value)
.map_err(|_| -> crate::BoxErr { format!("{ctx}: value {value} exceeds u32 range").into() })
}
fn build_group_icon_blob(entries: &[IcoEntry]) -> std::result::Result<Vec<u8>, crate::BoxErr> {
let count = u16_or(entries.len(), "RT_GROUP_ICON entry count")?;
let mut grp = Vec::with_capacity(6 + entries.len() * 14);
grp.extend_from_slice(&[0, 0]); grp.extend_from_slice(&1u16.to_le_bytes()); grp.extend_from_slice(&count.to_le_bytes());
for (i, e) in entries.iter().enumerate() {
grp.push(e.width);
grp.push(e.height);
grp.push(e.color_count);
grp.push(0); grp.extend_from_slice(&e.planes.to_le_bytes());
grp.extend_from_slice(&e.bit_count.to_le_bytes());
grp.extend_from_slice(&e.size_bytes.to_le_bytes());
let res_id = u16_or(i + 1, "RT_ICON resource id")?;
grp.extend_from_slice(&res_id.to_le_bytes());
}
Ok(grp)
}
#[allow(clippy::too_many_lines)]
pub(crate) fn embed_icon(exe: &Path, ico: &Path) -> Res {
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::System::LibraryLoader::{
BeginUpdateResourceW, EndUpdateResourceW, UpdateResourceW,
};
const RT_ICON: u16 = 3;
const RT_GROUP_ICON: u16 = 14;
const LANG_NEUTRAL: u16 = 0;
let ico_bytes = std::fs::read(ico)
.map_err(|e| -> crate::BoxErr { format!("read {}: {e}", ico.display()).into() })?;
let entries = parse_ico_directory(&ico_bytes, ico)?;
let grp = build_group_icon_blob(&entries)?;
let grp_len = u32_or(grp.len(), "RT_GROUP_ICON payload size")?;
let exe_w: Vec<u16> = exe
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
unsafe {
let h = BeginUpdateResourceW(exe_w.as_ptr(), 0);
if h.is_null() {
return Err(format!(
"BeginUpdateResource failed for {} (error {})",
exe.display(),
std::io::Error::last_os_error()
)
.into());
}
let group_ok = UpdateResourceW(
h,
make_int_resource(RT_GROUP_ICON),
make_int_resource(1),
LANG_NEUTRAL,
grp.as_ptr().cast(),
grp_len,
);
if group_ok == 0 {
EndUpdateResourceW(h, 1);
return Err(format!(
"UpdateResource(RT_GROUP_ICON) failed for {}: {}",
exe.display(),
std::io::Error::last_os_error()
)
.into());
}
for (i, e) in entries.iter().enumerate() {
let img = &ico_bytes[e.offset..e.offset + e.size_bytes as usize];
let img_len = u32_or(img.len(), "RT_ICON payload size")?;
let res_id = u16_or(i + 1, "RT_ICON resource id")?;
let ok = UpdateResourceW(
h,
make_int_resource(RT_ICON),
make_int_resource(res_id),
LANG_NEUTRAL,
img.as_ptr().cast(),
img_len,
);
if ok == 0 {
EndUpdateResourceW(h, 1);
return Err(format!(
"UpdateResource(RT_ICON #{}) failed for {}: {}",
i + 1,
exe.display(),
std::io::Error::last_os_error()
)
.into());
}
}
let commit_ok = EndUpdateResourceW(h, 0);
if commit_ok == 0 {
return Err(format!(
"EndUpdateResource (commit) failed for {}: {}",
exe.display(),
std::io::Error::last_os_error()
)
.into());
}
}
Ok(())
}
#[derive(Debug)]
struct IcoEntry {
width: u8,
height: u8,
color_count: u8,
planes: u16,
bit_count: u16,
size_bytes: u32,
offset: usize,
}
fn parse_ico_directory(
bytes: &[u8],
path: &Path,
) -> std::result::Result<Vec<IcoEntry>, crate::BoxErr> {
if bytes.len() < 6 {
return Err(format!("{}: truncated .ico (no header)", path.display()).into());
}
let reserved = u16::from_le_bytes([bytes[0], bytes[1]]);
let kind = u16::from_le_bytes([bytes[2], bytes[3]]);
let count = u16::from_le_bytes([bytes[4], bytes[5]]) as usize;
if reserved != 0 || kind != 1 {
return Err(format!(
"{}: not an icon file (reserved={reserved}, type={kind})",
path.display()
)
.into());
}
let mut out = Vec::with_capacity(count);
for i in 0..count {
let off = 6 + i * 16;
if off + 16 > bytes.len() {
return Err(format!("{}: truncated ICONDIRENTRY {}", path.display(), i).into());
}
let size_bytes = u32::from_le_bytes([
bytes[off + 8],
bytes[off + 9],
bytes[off + 10],
bytes[off + 11],
]);
let image_off = u32::from_le_bytes([
bytes[off + 12],
bytes[off + 13],
bytes[off + 14],
bytes[off + 15],
]) as usize;
if image_off + size_bytes as usize > bytes.len() {
return Err(format!(
"{}: ICONDIRENTRY {} points past end of file",
path.display(),
i
)
.into());
}
out.push(IcoEntry {
width: bytes[off],
height: bytes[off + 1],
color_count: bytes[off + 2],
planes: u16::from_le_bytes([bytes[off + 4], bytes[off + 5]]),
bit_count: u16::from_le_bytes([bytes[off + 6], bytes[off + 7]]),
size_bytes,
offset: image_off,
});
}
Ok(out)
}