use std::error::Error;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
static FETCH_LOCK: Mutex<()> = Mutex::new(());
pub const MACHINE_I386: u64 = 0x14C; pub const MACHINE_AMD64: u64 = 0x8664;
const WINVER_ALIASES: &[(&str, &str)] = &[
("win11", "26100.7920"), ("win11@24h2", "26100.7920"),
("win11@22h2", "22621.7079"),
("win11@21h2", "22000.3260"),
("win10", "19041.7291"), ("win10@22h2", "19041.7291"),
("win2019", "17763.8755"), ];
pub fn resolve_build(name: &str) -> String {
let n = name.trim().to_lowercase();
WINVER_ALIASES
.iter()
.find(|(alias, _)| *alias == n)
.map(|(_, build)| build.to_string())
.unwrap_or_else(|| name.trim().to_string())
}
pub fn known_aliases() -> Vec<(&'static str, &'static str)> {
WINVER_ALIASES.to_vec()
}
pub fn cache_folder(build: &str, is_x64: bool) -> PathBuf {
let arch_dir = if is_x64 { "x86_64" } else { "x86" };
PathBuf::from(format!("maps/winver/{}/{}/", build, arch_dir))
}
pub fn ensure_dll(
cache: &Path,
build: &str,
basename: &str,
machine_type: u64,
) -> Result<PathBuf, Box<dyn Error>> {
let dest = cache.join(basename);
if dest.is_file() {
return Ok(dest);
}
let _guard = FETCH_LOCK.lock().unwrap();
if dest.is_file() {
return Ok(dest);
}
fs::create_dir_all(cache)?;
let key = winbindex_key(basename, build, machine_type)?;
let url = format!(
"https://msdl.microsoft.com/download/symbols/{name}/{key}/{name}",
name = basename,
key = key
);
log::trace!("--winver: fetching {} (key {}) from symbol server", basename, key);
let bytes = http_get(&url)?;
if bytes.len() < 0x40 || &bytes[0..2] != b"MZ" {
return Err(format!(
"symbol server did not return a PE for {} (key {}, {} bytes)",
basename,
key,
bytes.len()
)
.into());
}
let tmp = cache.join(format!(".{}.part", basename));
fs::write(&tmp, &bytes)?;
fs::rename(&tmp, &dest)?;
log::trace!("--winver: cached {} ({} KB)", basename, bytes.len() / 1024);
Ok(dest)
}
fn winbindex_key(basename: &str, build: &str, machine_type: u64) -> Result<String, Box<dyn Error>> {
let index = load_winbindex_index(basename)?;
let obj = index
.as_object()
.ok_or("winbindex: unexpected JSON (not an object)")?;
let major = build.split('.').next().unwrap_or(build);
let major_needle = format!(".{}.", major);
let mut fallback: Option<(u64, u64, u64)> = None;
for entry in obj.values() {
let fi = match entry.get("fileInfo") {
Some(v) => v,
None => continue,
};
if fi.get("machineType").and_then(|v| v.as_u64()) != Some(machine_type) {
continue;
}
let version = fi.get("version").and_then(|v| v.as_str()).unwrap_or("");
let ts = match fi.get("timestamp").and_then(|v| v.as_u64()) {
Some(v) => v,
None => continue,
};
let vsize = match fi.get("virtualSize").and_then(|v| v.as_u64()) {
Some(v) => v,
None => continue,
};
if version.contains(build) {
return Ok(format!("{:08X}{:X}", ts as u32, vsize as u32)); }
if version.contains(&major_needle) {
let ubr = version
.split_whitespace()
.next()
.and_then(|v| v.rsplit('.').next())
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(0);
if fallback.map_or(true, |(u, _, _)| ubr > u) {
fallback = Some((ubr, ts, vsize));
}
}
}
if let Some((_, ts, vsize)) = fallback {
return Ok(format!("{:08X}{:X}", ts as u32, vsize as u32));
}
Err(format!(
"no {} variant for build {} (nor major {}) in winbindex",
basename, build, major
)
.into())
}
fn load_winbindex_index(basename: &str) -> Result<serde_json::Value, Box<dyn Error>> {
let idx_dir = PathBuf::from("maps/winver/.index");
let idx_path = idx_dir.join(format!("{}.json", basename));
if let Ok(text) = fs::read(&idx_path) {
if let Ok(v) = serde_json::from_slice::<serde_json::Value>(&text) {
return Ok(v);
}
}
let url = format!(
"https://winbindex.m417z.com/data/by_filename_compressed/{}.json.gz",
basename
);
let gz = http_get(&url)?;
let mut decoder = flate2::read::GzDecoder::new(&gz[..]);
let mut json = Vec::new();
decoder.read_to_end(&mut json)?;
fs::create_dir_all(&idx_dir)?;
let _ = fs::write(&idx_path, &json); Ok(serde_json::from_slice(&json)?)
}
const SEED_DATA_FILES: &[&str] = &["C_1252.NLS", "C_437.NLS", "locale.nls"];
const SEED_DLLS: &[&str] = &["ntdll.dll", "kernel32.dll", "kernelbase.dll"];
impl crate::emu::Emu {
pub(crate) fn ensure_maps_dll(&self, dll: &str) {
let filepath = self.cfg.get_maps_folder(dll);
if std::path::Path::new(&filepath).exists() {
return;
}
let build = self
.cfg
.winver
.clone()
.unwrap_or_else(|| resolve_build("win11"));
let machine = if self.cfg.arch.is_x64() {
MACHINE_AMD64
} else {
MACHINE_I386
};
let folder = self.cfg.maps_folder.clone();
if let Err(e) = ensure_dll(std::path::Path::new(&folder), &build, &dll.to_lowercase(), machine) {
log::warn!("winver: could not fetch {} (build {}): {}", dll, build, e);
}
}
pub fn set_maps_from_winver(&mut self, name: &str) -> Result<(), Box<dyn Error>> {
let build = resolve_build(name);
let is_x64 = self.cfg.arch.is_x64();
let machine = if is_x64 { MACHINE_AMD64 } else { MACHINE_I386 };
let cache = cache_folder(&build, is_x64);
fs::create_dir_all(&cache)?;
eprintln!(
"[mwemu] --winver {}: build {} ({}) → fetching system DLLs from the symbol server",
name,
build,
if is_x64 { "x64" } else { "x86" },
);
seed_data_files(&cache, is_x64);
for dll in SEED_DLLS {
match ensure_dll(&cache, &build, dll, machine) {
Ok(_) => {}
Err(e) => {
return Err(format!("--winver: failed to fetch {}: {}", dll, e).into());
}
}
}
self.cfg.winver = Some(build);
self.set_maps_folder(cache.to_str().unwrap());
Ok(())
}
}
fn seed_data_files(cache: &Path, is_x64: bool) {
let iso_caches: &[&str] = if is_x64 {
&[
"maps/iso/x86_64",
"../../maps/iso/x86_64",
"maps/iso/x86",
"../../maps/iso/x86",
]
} else {
&[
"maps/iso/x86",
"../../maps/iso/x86",
"maps/iso/x86_64",
"../../maps/iso/x86_64",
]
};
for f in SEED_DATA_FILES {
let dest = cache.join(f);
if dest.is_file() {
continue;
}
for src_dir in iso_caches {
let src = Path::new(src_dir).join(f);
if src.is_file() {
let _ = fs::copy(&src, &dest);
break;
}
}
}
}
fn http_get(url: &str) -> Result<Vec<u8>, Box<dyn Error>> {
let prev_level = log::max_level();
log::set_max_level(log::LevelFilter::Warn);
let result = (|| -> Result<Vec<u8>, Box<dyn Error>> {
let resp = ureq::get(url)
.timeout(std::time::Duration::from_secs(60))
.call()?;
if resp.status() != 200 {
return Err(format!("HTTP {} for {}", resp.status(), url).into());
}
let mut bytes = Vec::new();
resp.into_reader().read_to_end(&mut bytes)?;
Ok(bytes)
})();
log::set_max_level(prev_level);
result
}