use std::fs; use std::path::Path; use sysinfo::Pid; pub fn check_folder_completeness>(folder_path: P, required_files: &[&str]) -> f32 { let mut found = 0; for file in required_files { let file_path = folder_path.as_ref().join(file); if file_path.exists() { found += 1; } } if required_files.is_empty() { 100.0 } else { (found as f32 / required_files.len() as f32) * 100.0 } } pub fn get_osu_user_config>( osu_folder_path: P, ) -> Option> { if !osu_folder_path.as_ref().exists() { return None; } let current_user = std::env::var("USERNAME").unwrap_or_else(|_| "Admin".to_string()); let osu_config_path = osu_folder_path .as_ref() .join(format!("osu!.{}.cfg", current_user)); if !osu_config_path.exists() { return None; } let mut config_map = std::collections::HashMap::new(); if let Ok(contents) = std::fs::read_to_string(osu_config_path) { for line in contents.lines() { if let Some((key, value)) = line.split_once(" = ") { config_map.insert(key.trim().to_string(), value.trim().to_string()); } } } return Some(config_map); } pub fn set_osu_user_config_vals( osu_folder_path: &str, key_values: &[(&str, Option<&str>)], ) -> Result { let current_user = std::env::var("USERNAME").unwrap_or_else(|_| "Admin".to_string()); let osu_config_path = Path::new(osu_folder_path).join(format!("osu!.{}.cfg", current_user)); if !osu_config_path.exists() { return Err("osu! user config file does not exist".to_string()); } let mut lines = fs::read_to_string(&osu_config_path) .map_err(|e| e.to_string())? .lines() .map(|line| line.to_string()) .collect::>(); let mut keys_to_set: std::collections::HashMap<&str, &str> = std::collections::HashMap::new(); let mut keys_to_add: std::collections::HashSet<&str> = std::collections::HashSet::new(); for (key, value_opt) in key_values.iter() { if let Some(value) = value_opt { keys_to_set.insert(*key, *value); keys_to_add.insert(*key); } } let mut updates = Vec::new(); for (i, line) in lines.iter().enumerate() { if let Some((existing_key, _)) = line.split_once(" = ") { let trimmed_key = existing_key.trim(); if let Some(new_value) = keys_to_set.get(trimmed_key) { updates.push((i, trimmed_key.to_string(), new_value.to_string())); } } } for (i, trimmed_key, new_value) in updates { lines[i] = format!("{} = {}", trimmed_key, new_value); keys_to_add.remove(trimmed_key.as_str()); } for key in keys_to_add { if let Some(value) = keys_to_set.get(key) { lines.push(format!("{} = {}", key, value)); } } let new_content = lines.join("\n") + "\n"; fs::write(&osu_config_path, new_content).map_err(|e| e.to_string())?; Ok(true) } pub fn set_osu_config_vals( osu_folder_path: &str, key_values: &[(&str, Option<&str>)], ) -> Result { let osu_config_path = Path::new(osu_folder_path).join("osu!.cfg"); if !osu_config_path.exists() { return Err("osu!.cfg file does not exist".to_string()); } let mut lines = fs::read_to_string(&osu_config_path) .map_err(|e| e.to_string())? .lines() .map(|line| line.to_string()) .collect::>(); let mut keys_to_set: std::collections::HashMap<&str, &str> = std::collections::HashMap::new(); let mut keys_to_add: std::collections::HashSet<&str> = std::collections::HashSet::new(); for (key, value_opt) in key_values.iter() { if let Some(value) = value_opt { keys_to_set.insert(*key, *value); keys_to_add.insert(*key); } } let mut updates = Vec::new(); for (i, line) in lines.iter().enumerate() { if let Some((existing_key, _)) = line.split_once(" = ") { let trimmed_key = existing_key.trim(); if let Some(new_value) = keys_to_set.get(trimmed_key) { updates.push((i, trimmed_key.to_string(), new_value.to_string())); } } } for (i, trimmed_key, new_value) in updates { lines[i] = format!("{} = {}", trimmed_key, new_value); keys_to_add.remove(trimmed_key.as_str()); } for key in keys_to_add { if let Some(value) = keys_to_set.get(key) { lines.push(format!("{} = {}", key, value)); } } let new_content = lines.join("\n") + "\n"; fs::write(&osu_config_path, new_content).map_err(|e| e.to_string())?; Ok(true) } pub fn get_osu_config>( osu_folder_path: P, ) -> Option> { if !osu_folder_path.as_ref().exists() { return None; } let osu_config_path = osu_folder_path.as_ref().join("osu!.cfg"); if !osu_config_path.exists() { return None; } let mut config_map = std::collections::HashMap::new(); if let Ok(contents) = std::fs::read_to_string(osu_config_path) { for line in contents.lines() { if let Some((key, value)) = line.split_once(" = ") { config_map.insert(key.trim().to_string(), value.trim().to_string()); } } } return Some(config_map); } #[cfg(not(windows))] pub fn is_osuwinello_available() -> bool { use std::process::Command; Command::new("osu-wine") .arg("--info") .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .status() .is_ok() } #[cfg(windows)] pub fn is_osuwinello_available() -> bool { false } #[cfg(not(windows))] pub fn is_wmctrl_available() -> bool { use std::process::Command; Command::new("wmctrl") .arg("-V") .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .status() .is_ok() } #[cfg(windows)] pub fn is_wmctrl_available() -> bool { false } #[cfg(not(windows))] pub fn get_window_title_by_pid(target_pid: Pid) -> String { use std::process::Command; let find_title = || -> Option { let output = Command::new("wmctrl").arg("-lp").output().ok()?; if !output.status.success() { return None; } let output_str = String::from_utf8(output.stdout).ok()?; for line in output_str.lines() { let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() < 4 { continue; } if let Ok(pid) = parts[2].parse::() { if pid == target_pid.as_u32() { let title = parts[4..].join(" "); return Some(title); } } } None }; find_title().unwrap_or_default() } #[cfg(windows)] pub fn get_window_title_by_pid(pid: Pid) -> String { use std::ffi::OsString; use std::os::windows::ffi::OsStringExt; use std::sync::{Arc, Mutex}; use winapi::shared::windef::HWND; use winapi::um::winuser::{ EnumWindows, GetWindowTextW, GetWindowThreadProcessId, IsWindowVisible, }; extern "system" fn enum_windows_proc( hwnd: HWND, lparam: winapi::shared::minwindef::LPARAM, ) -> i32 { unsafe { let data = &mut *(lparam as *mut (u32, Arc>>)); let target_pid = data.0; let result = &data.1; let mut window_pid = 0u32; GetWindowThreadProcessId(hwnd, &mut window_pid); if window_pid == target_pid && IsWindowVisible(hwnd) != 0 { let mut title = vec![0u16; 512]; let length = GetWindowTextW(hwnd, title.as_mut_ptr(), title.len() as i32); if length > 0 { let title = OsString::from_wide(&title[..length as usize]); let title_str = title.to_string_lossy().into_owned(); if !title_str.is_empty() { *result.lock().unwrap() = Some(title_str); return 0; } } } 1 } } let result = Arc::new(Mutex::new(None)); let mut data = (pid.as_u32(), Arc::clone(&result)); unsafe { EnumWindows( Some(enum_windows_proc), &mut data as *mut _ as winapi::shared::minwindef::LPARAM, ); } result.lock().unwrap().clone().unwrap_or_default() } pub async fn is_net8_installed() -> bool { use std::process::Command; let output_result = Command::new("dotnet").arg("--list-runtimes").output(); match output_result { Ok(output) => { if !output.status.success() { eprintln!( "Error: `dotnet --list-runtimes` failed with status: {}", output.status ); eprintln!("stderr: {}", String::from_utf8_lossy(&output.stderr)); return false; } let stdout_str = String::from_utf8_lossy(&output.stdout); stdout_str .lines() .any(|line| line.starts_with("Microsoft.WindowsDesktop.App 8.")) } Err(_) => false, } } #[cfg(not(windows))] pub fn encrypt_password(password: &str, _entropy: &str) -> Result { Ok(password.to_string()) } #[cfg(windows)] pub fn encrypt_password(password: &str, entropy: &str) -> Result { use base64::{Engine as _, engine::general_purpose}; use std::ffi::OsStr; use std::os::windows::ffi::OsStrExt; use std::ptr; use std::slice; use winapi::shared::minwindef::{BYTE, DWORD, LPVOID}; use winapi::shared::ntdef::LPCWSTR; use winapi::um::dpapi::{CRYPTPROTECT_UI_FORBIDDEN, CryptProtectData}; use winapi::um::winbase::LocalFree; use winapi::um::wincrypt::DATA_BLOB; let description = "Encrypted"; let password_bytes = password.as_bytes(); let mut input_blob = DATA_BLOB { cbData: password_bytes.len() as DWORD, pbData: password_bytes.as_ptr() as *mut BYTE, }; let entropy_bytes = entropy.as_bytes(); let mut entropy_blob = DATA_BLOB { cbData: entropy_bytes.len() as DWORD, pbData: entropy_bytes.as_ptr() as *mut BYTE, }; let mut output_blob = DATA_BLOB { cbData: 0, pbData: ptr::null_mut(), }; let wide_description: Vec = OsStrExt::encode_wide(OsStr::new(description)) .chain(Some(0)) .collect(); let p_description: LPCWSTR = wide_description.as_ptr(); let result = unsafe { CryptProtectData( &mut input_blob, p_description, &mut entropy_blob, ptr::null_mut(), ptr::null_mut(), CRYPTPROTECT_UI_FORBIDDEN, &mut output_blob, ) }; if result == 0 { return Err("CryptProtectData failed".to_string()); } let encrypted_data = unsafe { slice::from_raw_parts(output_blob.pbData, output_blob.cbData as usize).to_vec() }; unsafe { LocalFree(output_blob.pbData as LPVOID); } let base64_string = general_purpose::STANDARD.encode(&encrypted_data); Ok(base64_string) }