use std::env;
use std::ffi::c_void;
use std::fs;
use std::io;
use std::iter;
use std::mem::{size_of, transmute};
use std::path::{Path, PathBuf};
use std::ptr;
use std::slice;
use windows_sys::core::BOOL;
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Storage::FileSystem::{
GetFileVersionInfoSizeW, GetFileVersionInfoW, VerQueryValueW,
};
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleW, GetProcAddress};
use windows_sys::Win32::System::Threading::GetCurrentProcess;
use super::exports::{wide_path, ProcAddress};
use crate::error::BackendError;
pub(super) const CONPTY_DLL: &str = "conpty.dll";
pub(super) const OPEN_CONSOLE_EXE: &str = "OpenConsole.exe";
const PRODUCT_VERSION_KEY: &str = "ProductVersion";
pub(super) const UNKNOWN_VERSION: &str = "unknown";
const IMAGE_FILE_MACHINE_I386: u16 = 0x014c;
const IMAGE_FILE_MACHINE_AMD64: u16 = 0x8664;
const IMAGE_FILE_MACHINE_ARM64: u16 = 0xAA64;
type IsWow64Process2Fn = unsafe extern "system" fn(HANDLE, *mut u16, *mut u16) -> BOOL;
pub(super) struct ValidatedBundle {
pub(super) dir: PathBuf,
pub(super) dll: PathBuf,
}
pub(super) fn validate(dir: &Path, verify_pair: bool) -> Result<ValidatedBundle, BackendError> {
let absolute = absolute_dir(dir)
.map_err(|source| BackendError::dll_not_found(dir.to_path_buf(), source))?;
let dll = absolute.join(CONPTY_DLL);
let metadata = match fs::metadata(&dll) {
Ok(metadata) => metadata,
Err(source) => {
return Err(BackendError::dll_not_found(absolute, source));
},
};
if !metadata.is_file() {
return Err(BackendError::dll_not_found(
absolute,
io::Error::new(
io::ErrorKind::InvalidInput,
"conpty.dll is not a regular file",
),
));
}
let host = find_console_host(&absolute)
.ok_or_else(|| BackendError::open_console_missing(dll.clone()))?;
if verify_pair {
check_version_pair(&dll, &host)?;
}
Ok(ValidatedBundle { dir: absolute, dll })
}
pub(super) fn exe_dir() -> Option<PathBuf> {
env::current_exe().ok()?.parent().map(Path::to_path_buf)
}
#[cfg(feature = "tracing")]
pub(super) fn log_rejected(dir: &Path, err: &BackendError) {
tracing::warn!(
dir = %dir.display(),
error = %err,
"ignoring the bundled conpty.dll; falling back to the system ConPTY"
);
}
#[cfg(not(feature = "tracing"))]
pub(super) const fn log_rejected(_dir: &Path, _err: &BackendError) {}
pub(super) fn absolute_dir(dir: &Path) -> io::Result<PathBuf> {
let dir = if dir.is_absolute() {
dir.to_path_buf()
} else {
env::current_dir()?.join(dir)
};
if !dir.is_absolute() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"a drive-relative path resolves against that drive's current \
directory, which this loader refuses to depend on",
));
}
Ok(dir)
}
pub(super) fn find_console_host(dir: &Path) -> Option<PathBuf> {
let adjacent = dir.join(OPEN_CONSOLE_EXE);
if adjacent.is_file() {
return Some(adjacent);
}
let candidate = dir.join(native_arch_subdir()?).join(OPEN_CONSOLE_EXE);
candidate.is_file().then_some(candidate)
}
pub(super) fn native_arch_subdir() -> Option<&'static str> {
machine_arch_subdir(native_machine())
}
pub(super) const fn machine_arch_subdir(machine: u16) -> Option<&'static str> {
match machine {
IMAGE_FILE_MACHINE_AMD64 => Some("x64"),
IMAGE_FILE_MACHINE_ARM64 => Some("arm64"),
IMAGE_FILE_MACHINE_I386 => Some("x86"),
_ => None,
}
}
fn native_machine() -> u16 {
#[cfg(target_arch = "x86_64")]
const COMPILED_FOR: u16 = IMAGE_FILE_MACHINE_AMD64;
#[cfg(target_arch = "aarch64")]
const COMPILED_FOR: u16 = IMAGE_FILE_MACHINE_ARM64;
#[cfg(target_arch = "x86")]
const COMPILED_FOR: u16 = IMAGE_FILE_MACHINE_I386;
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "x86")))]
const COMPILED_FOR: u16 = 0;
let module_name: Vec<u16> = "kernel32.dll".encode_utf16().chain(iter::once(0)).collect();
let module = unsafe { GetModuleHandleW(module_name.as_ptr()) };
if module.is_null() {
return COMPILED_FOR;
}
let Some(address) = (unsafe { GetProcAddress(module, b"IsWow64Process2\0".as_ptr()) }) else {
return COMPILED_FOR;
};
let is_wow64_process2 = unsafe { transmute::<ProcAddress, IsWow64Process2Fn>(address) };
let mut process_machine = 0;
let mut native = 0;
let current_process = unsafe { GetCurrentProcess() };
let ok = unsafe { is_wow64_process2(current_process, &mut process_machine, &mut native) };
selected_native_machine(ok, native, COMPILED_FOR)
}
pub(super) const fn selected_native_machine(ok: BOOL, native: u16, compiled_for: u16) -> u16 {
if ok == 0 {
compiled_for
} else {
native
}
}
fn check_version_pair(dll: &Path, host: &Path) -> Result<(), BackendError> {
let dll_version = read_product_version(dll);
let exe_version = read_product_version(host);
if versions_are_compatible(dll_version.as_deref(), exe_version.as_deref()) {
return Ok(());
}
Err(BackendError::version_mismatch(
dll.to_path_buf(),
dll_version.unwrap_or_else(|| UNKNOWN_VERSION.to_owned()),
exe_version.unwrap_or_else(|| UNKNOWN_VERSION.to_owned()),
))
}
pub(super) fn versions_are_compatible(dll: Option<&str>, host: Option<&str>) -> bool {
match (dll.and_then(parse_version), host.and_then(parse_version)) {
(Some(dll), Some(host)) => dll == host,
_ => false,
}
}
pub(super) fn parse_version(text: &str) -> Option<[u64; 4]> {
let text = text.trim_matches(|c: char| c == '\0' || c.is_whitespace());
let mut parts = [0; 4];
let mut seen = 0;
for field in text.split('.') {
if seen == parts.len() {
break;
}
let Ok(value) = field.trim().parse::<u64>() else {
break;
};
parts[seen] = value;
seen += 1;
}
(seen > 0).then_some(parts)
}
pub(super) fn read_product_version(path: &Path) -> Option<String> {
let path = wide_path(path).ok()?;
let mut ignored_handle = 0;
let size = unsafe { GetFileVersionInfoSizeW(path.as_ptr(), &mut ignored_handle) };
if size == 0 {
return None;
}
let mut block: Vec<u32> = vec![0; (size as usize).div_ceil(size_of::<u32>())];
let read = unsafe { GetFileVersionInfoW(path.as_ptr(), 0, size, block.as_mut_ptr().cast()) };
if read == 0 {
return None;
}
let (value, len) = unsafe { query_version_value(&block, "\\VarFileInfo\\Translation") }?;
let count = translation_count(len);
let translations = unsafe { slice::from_raw_parts(value.cast::<[u16; 2]>(), count) };
for &[language, code_page] in translations {
let sub_block =
format!("\\StringFileInfo\\{language:04x}{code_page:04x}\\{PRODUCT_VERSION_KEY}");
let Some((value, len)) = (unsafe { query_version_value(&block, &sub_block) }) else {
continue;
};
let text = unsafe { slice::from_raw_parts(value.cast::<u16>(), len as usize) };
let text = String::from_utf16_lossy(text);
let text = trim_resource_string(&text);
if !text.is_empty() {
return Some(text.to_owned());
}
}
None
}
pub(super) const fn translation_count(byte_len: u32) -> usize {
byte_len as usize / size_of::<[u16; 2]>()
}
pub(super) fn trim_resource_string(text: &str) -> &str {
text.trim_matches(|c: char| c == '\0' || c.is_whitespace())
}
unsafe fn query_version_value(block: &[u32], sub_block: &str) -> Option<(*const c_void, u32)> {
let sub_block: Vec<u16> = sub_block.encode_utf16().chain(iter::once(0)).collect();
let mut value: *mut c_void = ptr::null_mut();
let mut len = 0;
let found = unsafe {
VerQueryValueW(
block.as_ptr().cast(),
sub_block.as_ptr(),
&mut value,
&mut len,
)
};
if found == 0 || value.is_null() || len == 0 {
return None;
}
Some((value.cast_const(), len))
}