use windows_sys::Win32::Networking::WinHttp::{
WinHttpCloseHandle, WinHttpConnect, WinHttpOpen, WinHttpOpenRequest,
WinHttpQueryHeaders, WinHttpReadData, WinHttpReceiveResponse, WinHttpSendRequest,
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_FLAG_SECURE,
WINHTTP_QUERY_STATUS_CODE, WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_QUERY_LOCATION,
};
use windows_sys::Win32::Foundation::GetLastError;
use windows_sys::Win32::Storage::FileSystem::{
DeleteFileW, MoveFileExW, MOVEFILE_REPLACE_EXISTING,
};
use std::ptr;
use std::ffi::c_void;
use crate::utils::to_wstring;
use crate::w;
fn extract_json_string<'a>(json: &'a str, key_with_quotes: &str) -> Option<&'a str> {
let key_idx = json.find(key_with_quotes)?;
let after_key = &json[key_idx + key_with_quotes.len()..];
let colon_idx = after_key.find(':')?;
let start_quote = after_key[colon_idx..].find('"')? + colon_idx;
let after_start_quote = &after_key[start_quote + 1..];
let end_quote = after_start_quote.find('"')?;
Some(&after_start_quote[..end_quote])
}
fn find_object_range(json: &str, index: usize) -> Option<(usize, usize)> {
let mut balance = 0;
let start = json[..index].char_indices().rev().find_map(|(i, c)| {
match c {
'}' => { balance += 1; None },
'{' => if balance == 0 { Some(i) } else { balance -= 1; None },
_ => None,
}
})?;
let mut balance = 0;
let end_offset = json[index..].char_indices().find_map(|(i, c)| {
match c {
'{' => { balance += 1; None },
'}' => if balance == 0 { Some(i) } else { balance -= 1; None },
_ => None,
}
})?;
Some((start, index + end_offset + 1))
}
const WINHTTP_NO_PROXY_NAME: *const u16 = ptr::null();
const WINHTTP_NO_PROXY_BYPASS: *const u16 = ptr::null();
const WINHTTP_NO_REFERER: *const u16 = ptr::null();
const WINHTTP_DEFAULT_ACCEPT_TYPES: *const *const u16 = ptr::null();
const WINHTTP_NO_ADDITIONAL_HEADERS: *const u16 = ptr::null();
const GITHUB_API_HOST: &str = "api.github.com";
const REPO_OWNER: &str = "IRedDragonICY";
const REPO_NAME: &str = "compactrs";
struct WinHttpHandle(pub *mut c_void);
impl WinHttpHandle {
fn new(handle: *mut c_void) -> Option<Self> {
if handle.is_null() { None } else { Some(Self(handle)) }
}
}
impl Drop for WinHttpHandle {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe { WinHttpCloseHandle(self.0) };
}
}
}
#[derive(Debug, Clone)]
pub struct UpdateInfo {
pub version: String,
pub download_url: String,
}
struct WinHttpRequest {
_session: WinHttpHandle,
_connect: WinHttpHandle,
request: WinHttpHandle,
}
fn parse_url(url: &str) -> Result<(String, String), String> {
let content = url.strip_prefix("https://").ok_or_else(|| format!("Invalid URL scheme: {}", url))?;
let slash_idx = content.find('/');
let (host, path) = match slash_idx {
Some(idx) => (content[..idx].to_string(), content[idx..].to_string()),
None => (content.to_string(), "/".to_string()),
};
Ok((host, path))
}
fn perform_http_get(url: &str) -> Result<WinHttpRequest, String> {
const MAX_REDIRECTS: u32 = 5;
let mut current_url = url.to_string();
let session_raw = unsafe {
WinHttpOpen(
w!("compactrs/updater").as_ptr(),
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME,
WINHTTP_NO_PROXY_BYPASS,
0,
)
};
let session = WinHttpHandle::new(session_raw).ok_or_else(|| format!("WinHttpOpen failed: {}", unsafe { GetLastError() }))?;
for _ in 0..=MAX_REDIRECTS {
let (host, path) = parse_url(¤t_url)?;
let connect_raw = unsafe {
WinHttpConnect(session.0, to_wstring(&host).as_ptr(), 443, 0)
};
let connect = WinHttpHandle::new(connect_raw).ok_or_else(|| format!("WinHttpConnect failed: {}", unsafe { GetLastError() }))?;
let request_raw = unsafe {
WinHttpOpenRequest(
connect.0,
w!("GET").as_ptr(),
to_wstring(&path).as_ptr(),
ptr::null(),
WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES,
WINHTTP_FLAG_SECURE,
)
};
let request = WinHttpHandle::new(request_raw).ok_or_else(|| format!("WinHttpOpenRequest failed: {}", unsafe { GetLastError() }))?;
if unsafe { WinHttpSendRequest(request.0, WINHTTP_NO_ADDITIONAL_HEADERS, 0, ptr::null(), 0, 0, 0) } == 0 {
return Err(format!("WinHttpSendRequest failed: {}", unsafe { GetLastError() }));
}
if unsafe { WinHttpReceiveResponse(request.0, ptr::null_mut()) } == 0 {
return Err(format!("WinHttpReceiveResponse failed: {}", unsafe { GetLastError() }));
}
let mut status_code: u32 = 0;
let mut size = std::mem::size_of::<u32>() as u32;
unsafe {
WinHttpQueryHeaders(
request.0,
WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
ptr::null(),
&mut status_code as *mut _ as *mut c_void,
&mut size,
ptr::null_mut()
);
}
match status_code {
200 => {
return Ok(WinHttpRequest {
_session: session,
_connect: connect,
request,
});
},
301 | 302 | 307 | 308 => {
let mut size: u32 = 0;
unsafe {
WinHttpQueryHeaders(request.0, WINHTTP_QUERY_LOCATION, ptr::null(), ptr::null_mut(), &mut size, ptr::null_mut());
}
if size == 0 {
return Err(format!("Redirect {} missing Location header", status_code));
}
let mut buffer = vec![0u8; size as usize];
if unsafe { WinHttpQueryHeaders(request.0, WINHTTP_QUERY_LOCATION, ptr::null(), buffer.as_mut_ptr() as *mut c_void, &mut size, ptr::null_mut()) } == 0 {
return Err("Failed to read Location header".into());
}
let location_w: Vec<u16> = buffer
.chunks_exact(2)
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
.take((size / 2) as usize)
.collect();
let new_url = String::from_utf16_lossy(&location_w).trim_matches(char::from(0)).to_string();
current_url = new_url;
continue;
},
_ => return Err(format!("HTTP Request failed with status: {}", status_code)),
}
}
Err("Too many redirects".into())
}
fn read_data_stream<F>(request_handle: &WinHttpHandle, mut writer: F) -> Result<u64, String>
where F: FnMut(&[u8]) -> Result<(), String>
{
let mut total_bytes = 0;
let mut buffer = [0u8; 8192];
loop {
let mut read: u32 = 0;
if unsafe { WinHttpReadData(request_handle.0, buffer.as_mut_ptr() as *mut c_void, buffer.len() as u32, &mut read) } == 0 {
return Err(format!("WinHttpReadData failed: {}", unsafe { GetLastError() }));
}
if read == 0 { break; }
writer(&buffer[..read as usize])?;
total_bytes += read as u64;
}
Ok(total_bytes)
}
pub fn check_for_updates() -> Result<Option<UpdateInfo>, String> {
let url = format!("https://{}/repos/{}/{}/releases/latest", GITHUB_API_HOST, REPO_OWNER, REPO_NAME);
let req = perform_http_get(&url)?;
let mut body = Vec::new();
read_data_stream(&req.request, |chunk| {
body.extend_from_slice(chunk);
Ok(())
})?;
let json_str = String::from_utf8(body).map_err(|e| format!("Invalid UTF-8: {}", e))?;
let tag_name = extract_json_string(&json_str, "\"tag_name\"").ok_or("Missing tag_name")?;
let target = "\"compactrs.exe\"";
let download_url = json_str.match_indices(target)
.find_map(|(idx, _)| {
let (start, end) = find_object_range(&json_str, idx)?;
let chunk = &json_str[start..end];
extract_json_string(chunk, "\"browser_download_url\"")
})
.ok_or("No compactrs.exe asset found")?
.to_string();
let current_version = env!("APP_VERSION").trim_start_matches('v');
let remote_version = tag_name.trim_start_matches('v');
if remote_version != current_version {
Ok(Some(UpdateInfo { version: tag_name.to_string(), download_url }))
} else {
Ok(None)
}
}
pub fn download_and_start_update(url: &str) -> Result<(), String> {
let req = perform_http_get(url)?;
let temp_path = std::env::current_exe().map_err(|e| e.to_string())?.with_extension("tmp");
let mut file = std::fs::File::create(&temp_path).map_err(|e| e.to_string())?;
use std::io::Write;
let mut first_chunk = true;
let bytes_downloaded = read_data_stream(&req.request, |chunk| {
if first_chunk {
if chunk.len() < 2 || chunk[0] != 0x4D || chunk[1] != 0x5A {
return Err("Invalid executable (missing MZ header)".into());
}
first_chunk = false;
}
file.write_all(chunk).map_err(|e| e.to_string())
})?;
if bytes_downloaded == 0 {
let _ = std::fs::remove_file(&temp_path);
return Err("Empty download".into());
}
drop(file);
let current_exe = std::env::current_exe().map_err(|e| e.to_string())?;
let old_exe = current_exe.with_extension("old");
unsafe {
let _ = DeleteFileW(to_wstring(old_exe.to_str().unwrap()).as_ptr());
if MoveFileExW(to_wstring(current_exe.to_str().unwrap()).as_ptr(), to_wstring(old_exe.to_str().unwrap()).as_ptr(), MOVEFILE_REPLACE_EXISTING) == 0 {
return Err(format!("Failed to move current exe: {}", GetLastError()));
}
if MoveFileExW(to_wstring(temp_path.to_str().unwrap()).as_ptr(), to_wstring(current_exe.to_str().unwrap()).as_ptr(), MOVEFILE_REPLACE_EXISTING) == 0 {
let _ = MoveFileExW(to_wstring(old_exe.to_str().unwrap()).as_ptr(), to_wstring(current_exe.to_str().unwrap()).as_ptr(), MOVEFILE_REPLACE_EXISTING);
return Err(format!("Failed to replace exe: {}", GetLastError()));
}
}
Ok(())
}