From 3e6f2361ee68f96d550e2c35a118dababb9bb18e Mon Sep 17 00:00:00 2001 From: Wyvest Date: Thu, 26 Feb 2026 00:16:46 -0500 Subject: [PATCH] sanitize pre-launch/wrapper/post-launch command --- apps/oneclient/frontend/src/bindings.gen.ts | 14 +++--- .../src/routes/app/settings/minecraft.tsx | 6 +-- packages/core/src/api/game/launch.rs | 10 ++++- packages/core/src/api/tauri/commands.rs | 43 ++++++++++++++++--- packages/core/src/store/processes.rs | 2 +- 5 files changed, 56 insertions(+), 19 deletions(-) diff --git a/apps/oneclient/frontend/src/bindings.gen.ts b/apps/oneclient/frontend/src/bindings.gen.ts index 607d6ecb..18714bb0 100644 --- a/apps/oneclient/frontend/src/bindings.gen.ts +++ b/apps/oneclient/frontend/src/bindings.gen.ts @@ -313,13 +313,8 @@ export type VersionType = */ "old_beta" -const ARGS_MAP = { 'core':'{"updateClusterById":["id","request"],"createSettingsProfile":["name"],"convertUsernameUUID":["username_uuid"],"refreshAccount":["uuid"],"killProcess":["pid"],"removeCluster":["id"],"writeSettings":["setting"],"getScreenshots":["id"],"getUsersFromAuthor":["provider","author"],"removePackage":["cluster_id","package_hash"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"changeSkin":["access_token","skin_url","skin_variant"],"getProcessLogTail":["id","max_lines"],"searchPackages":["provider","query"],"createCluster":["options"],"getPackageBody":["provider","body"],"getClusterById":["id"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"fetchMinecraftProfile":["uuid"],"fetchLoggedInProfile":["access_token"],"getProfileOrDefault":["name"],"isClusterRunning":["cluster_id"],"getDefaultUser":["fallback"],"refreshAccounts":[],"getRunningProcesses":[],"getLogs":["id"],"openMsaLogin":[],"getClusters":[],"getLoadersForVersion":["mc_version"],"launchCluster":["id","uuid","search_for_java"],"getGlobalProfile":[],"removeUser":["uuid"],"getGameVersions":[],"setDefaultUser":["uuid"],"updateClusterProfile":["name","profile"],"setClusterStage":["id","stage"],"getUser":["uuid"],"getUsers":[],"getPackage":["provider","slug"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"syncCluster":["cluster_id"],"togglePackage":["cluster_id","package_hash"],"changeCape":["access_token","cape_uuid"],"removeCape":["access_token"],"getWorlds":["id"],"getLogByName":["id","name"],"readSettings":[],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"getLinkedPackages":["cluster_id"],"getMultiplePackages":["provider","slugs"],"getRunningProcessesByClusterId":["cluster_id"],"installModpack":["modpack","cluster_id"],"setDiscordRPCMessage":["message"],"open":["input"]}', 'oneclient':'{"refreshArt":["path"],"cacheArt":["path"],"getVersions":[],"getBundlesFor":["cluster_id"],"updateBundlePackages":["cluster_id"],"checkForUpdate":[],"installUpdate":[],"downloadPackageFromBundle":["package","cluster_id","bundle_name","skip_compatibility"],"isBundleSyncing":[],"extractBundleOverrides":["bundle_path","cluster_id"],"getClustersGroupedByMajor":[]}', 'events':'{"message":["event"],"process":["event"],"ingress":["event"]}', 'debug':'{"getGitCommitHash":[],"isInDev":[],"getPackageVersion":[],"getArch":[],"getOsVersion":[],"getLocale":[],"getBuildTimestamp":[],"getType":[],"getFamily":[],"openDevTools":[],"getPlatform":[]}', 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}' } -export type Router = { 'events': { ingress: (event: IngressPayload) => Promise, -message: (event: MessagePayload) => Promise, -process: (event: ProcessPayload) => Promise }, -'folders': { fromCluster: (folderName: string) => Promise, -openCluster: (folderName: string) => Promise }, -'oneclient': { getClustersGroupedByMajor: () => Promise>, +const ARGS_MAP = { 'core':'{"createCluster":["options"],"refreshAccount":["uuid"],"changeSkin":["access_token","skin_url","skin_variant"],"removeUser":["uuid"],"getClusterById":["id"],"getProcessLogTail":["id","max_lines"],"launchCluster":["id","uuid","search_for_java"],"syncCluster":["cluster_id"],"getLogByName":["id","name"],"getGlobalProfile":[],"getUsers":[],"openMsaLogin":[],"getPackage":["provider","slug"],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"getDefaultUser":["fallback"],"getClusters":[],"getLogs":["id"],"getRunningProcesses":[],"refreshAccounts":[],"getPackageBody":["provider","body"],"changeCape":["access_token","cape_uuid"],"setClusterStage":["id","stage"],"readSettings":[],"getProfileOrDefault":["name"],"getLoadersForVersion":["mc_version"],"searchPackages":["provider","query"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"removeCluster":["id"],"getMultiplePackages":["provider","slugs"],"getUsersFromAuthor":["provider","author"],"getLinkedPackages":["cluster_id"],"removePackage":["cluster_id","package_hash"],"getScreenshots":["id"],"installModpack":["modpack","cluster_id"],"fetchMinecraftProfile":["uuid"],"convertUsernameUUID":["username_uuid"],"setDiscordRPCMessage":["message"],"isClusterRunning":["cluster_id"],"createSettingsProfile":["name"],"getRunningProcessesByClusterId":["cluster_id"],"setDefaultUser":["uuid"],"getGameVersions":[],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"updateClusterById":["id","request"],"togglePackage":["cluster_id","package_hash"],"killProcess":["pid"],"updateClusterProfile":["name","profile"],"getUser":["uuid"],"writeSettings":["setting"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"open":["input"],"removeCape":["access_token"],"fetchLoggedInProfile":["access_token"],"getWorlds":["id"]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'debug':'{"getArch":[],"getOsVersion":[],"getGitCommitHash":[],"openDevTools":[],"getType":[],"getPackageVersion":[],"getLocale":[],"getBuildTimestamp":[],"isInDev":[],"getPlatform":[],"getFamily":[]}', 'events':'{"message":["event"],"process":["event"],"ingress":["event"]}', 'oneclient':'{"updateBundlePackages":["cluster_id"],"isBundleSyncing":[],"getClustersGroupedByMajor":[],"downloadPackageFromBundle":["package","cluster_id","bundle_name","skip_compatibility"],"extractBundleOverrides":["bundle_path","cluster_id"],"getVersions":[],"refreshArt":["path"],"cacheArt":["path"],"installUpdate":[],"checkForUpdate":[],"getBundlesFor":["cluster_id"]}' } +export type Router = { 'oneclient': { getClustersGroupedByMajor: () => Promise>, getBundlesFor: (clusterId: number) => Promise, getVersions: () => Promise, extractBundleOverrides: (bundlePath: string, clusterId: number) => Promise, @@ -384,6 +379,11 @@ removeCape: (accessToken: string) => Promise, convertUsernameUUID: (usernameUuid: string) => Promise, setDiscordRPCMessage: (message: string) => Promise, open: (input: string) => Promise }, +'events': { ingress: (event: IngressPayload) => Promise, +message: (event: MessagePayload) => Promise, +process: (event: ProcessPayload) => Promise }, +'folders': { fromCluster: (folderName: string) => Promise, +openCluster: (folderName: string) => Promise }, 'debug': { openDevTools: () => Promise, isInDev: () => Promise, getArch: () => Promise, diff --git a/apps/onelauncher/frontend/src/routes/app/settings/minecraft.tsx b/apps/onelauncher/frontend/src/routes/app/settings/minecraft.tsx index f2b75003..41c37c88 100644 --- a/apps/onelauncher/frontend/src/routes/app/settings/minecraft.tsx +++ b/apps/onelauncher/frontend/src/routes/app/settings/minecraft.tsx @@ -148,7 +148,7 @@ export function ProcessSettings(props: ProcessSettingsProps) { { - settings.hook_pre = e.target.value || null; + settings.hook_pre = e.target.value; }} placeholder="echo 'Game started'" /> @@ -162,7 +162,7 @@ export function ProcessSettings(props: ProcessSettingsProps) { { - settings.hook_wrapper = e.target.value || null; + settings.hook_wrapper = e.target.value; }} placeholder="gamescope" /> @@ -176,7 +176,7 @@ export function ProcessSettings(props: ProcessSettingsProps) { { - settings.hook_post = e.target.value || null; + settings.hook_post = e.target.value; }} placeholder="echo 'Game exited'" /> diff --git a/packages/core/src/api/game/launch.rs b/packages/core/src/api/game/launch.rs index ff9caaba..c6e62253 100644 --- a/packages/core/src/api/game/launch.rs +++ b/packages/core/src/api/game/launch.rs @@ -88,8 +88,14 @@ pub async fn launch_minecraft( .join(&version_name) .join(format!("{version_name}.jar")); - let mut command = if let Some(wrapper) = &settings.hook_wrapper { - let mut cmd = Command::new(wrapper); + let mut command = if let Some(wrapper) = settings + .hook_wrapper + .as_deref() + .and_then(|hook| (!hook.trim().is_empty()).then_some(hook)) + { + let mut split = wrapper.split_whitespace(); + let mut cmd = Command::new(split.next().expect("hook wrapper should not be empty")); + cmd.args(split); cmd.arg(&java.absolute_path); cmd } else { diff --git a/packages/core/src/api/tauri/commands.rs b/packages/core/src/api/tauri/commands.rs index 21afff3d..8d9a0ee9 100644 --- a/packages/core/src/api/tauri/commands.rs +++ b/packages/core/src/api/tauri/commands.rs @@ -318,6 +318,35 @@ pub struct ProfileUpdate { pub hook_post: Option, } +#[inline] +fn normalize_optional_command(value: String) -> Option { + if value.trim().is_empty() { + None + } else { + Some(value) + } +} + +fn sanitize_settings_profile_commands(profile: &mut SettingsProfile) { + profile.launch_args = profile + .launch_args + .take() + .and_then(normalize_optional_command); + profile.launch_env = profile + .launch_env + .take() + .and_then(normalize_optional_command); + profile.hook_pre = profile.hook_pre.take().and_then(normalize_optional_command); + profile.hook_wrapper = profile + .hook_wrapper + .take() + .and_then(normalize_optional_command); + profile.hook_post = profile + .hook_post + .take() + .and_then(normalize_optional_command); +} + #[taurpc::ipc_type] pub struct TauriLauncherApiImpl; @@ -544,23 +573,23 @@ impl TauriLauncherApi for TauriLauncherApiImpl { } if let Some(launch_args) = profile.launch_args { - active_model.launch_args = Set(Some(launch_args)); + active_model.launch_args = Set(normalize_optional_command(launch_args)); } if let Some(launch_env) = profile.launch_env { - active_model.launch_env = Set(Some(launch_env)); + active_model.launch_env = Set(normalize_optional_command(launch_env)); } if let Some(hook_pre) = profile.hook_pre { - active_model.hook_pre = Set(Some(hook_pre)); + active_model.hook_pre = Set(normalize_optional_command(hook_pre)); } if let Some(hook_wrapper) = profile.hook_wrapper { - active_model.hook_wrapper = Set(Some(hook_wrapper)); + active_model.hook_wrapper = Set(normalize_optional_command(hook_wrapper)); } if let Some(hook_post) = profile.hook_post { - active_model.hook_post = Set(Some(hook_post)); + active_model.hook_post = Set(normalize_optional_command(hook_post)); } Ok(active_model) @@ -726,7 +755,9 @@ impl TauriLauncherApi for TauriLauncherApiImpl { Ok(settings.clone()) } - async fn write_settings(self, setting: Settings) -> LauncherResult<()> { + async fn write_settings(self, mut setting: Settings) -> LauncherResult<()> { + sanitize_settings_profile_commands(&mut setting.global_game_settings); + let state = State::get().await?; let mut settings = state.settings.write().await; diff --git a/packages/core/src/store/processes.rs b/packages/core/src/store/processes.rs index 2fe74b6b..d5e6502e 100644 --- a/packages/core/src/store/processes.rs +++ b/packages/core/src/store/processes.rs @@ -469,7 +469,7 @@ async fn flush_process_output_event( } async fn run_hook(hook: &str, cwd: PathBuf) -> LauncherResult> { - let mut split = hook.split(' '); + let mut split = hook.split_whitespace(); let Some(command) = split.next() else { return Ok(None);