sanitize pre-launch/wrapper/post-launch command

This commit is contained in:
Wyvest 2026-02-26 00:16:46 -05:00
parent 44742fa21f
commit 3e6f2361ee
5 changed files with 56 additions and 19 deletions

View file

@ -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<void>,
message: (event: MessagePayload) => Promise<void>,
process: (event: ProcessPayload) => Promise<void> },
'folders': { fromCluster: (folderName: string) => Promise<string>,
openCluster: (folderName: string) => Promise<null> },
'oneclient': { getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>>,
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<Partial<{ [key in number]: ClusterModel[] }>>,
getBundlesFor: (clusterId: number) => Promise<ModpackArchive[]>,
getVersions: () => Promise<OnlineClusterManifest>,
extractBundleOverrides: (bundlePath: string, clusterId: number) => Promise<null>,
@ -384,6 +379,11 @@ removeCape: (accessToken: string) => Promise<MojangFullPlayerProfile>,
convertUsernameUUID: (usernameUuid: string) => Promise<MowojangProfile>,
setDiscordRPCMessage: (message: string) => Promise<null>,
open: (input: string) => Promise<null> },
'events': { ingress: (event: IngressPayload) => Promise<void>,
message: (event: MessagePayload) => Promise<void>,
process: (event: ProcessPayload) => Promise<void> },
'folders': { fromCluster: (folderName: string) => Promise<string>,
openCluster: (folderName: string) => Promise<null> },
'debug': { openDevTools: () => Promise<void>,
isInDev: () => Promise<boolean>,
getArch: () => Promise<string>,

View file

@ -148,7 +148,7 @@ export function ProcessSettings(props: ProcessSettingsProps) {
<TextField
defaultValue={settings.hook_pre || ''}
onChange={(e) => {
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) {
<TextField
defaultValue={settings.hook_wrapper || ''}
onChange={(e) => {
settings.hook_wrapper = e.target.value || null;
settings.hook_wrapper = e.target.value;
}}
placeholder="gamescope"
/>
@ -176,7 +176,7 @@ export function ProcessSettings(props: ProcessSettingsProps) {
<TextField
defaultValue={settings.hook_post || ''}
onChange={(e) => {
settings.hook_post = e.target.value || null;
settings.hook_post = e.target.value;
}}
placeholder="echo 'Game exited'"
/>

View file

@ -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 {

View file

@ -318,6 +318,35 @@ pub struct ProfileUpdate {
pub hook_post: Option<String>,
}
#[inline]
fn normalize_optional_command(value: String) -> Option<String> {
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;

View file

@ -469,7 +469,7 @@ async fn flush_process_output_event(
}
async fn run_hook(hook: &str, cwd: PathBuf) -> LauncherResult<Option<i32>> {
let mut split = hook.split(' ');
let mut split = hook.split_whitespace();
let Some(command) = split.next() else {
return Ok(None);