use super::compat_support::apply_bt_select_file_option; use super::{ CoreError, DownloadEngine, DownloadId, DownloadStatus, InProcessRpcDispatcher, JsonRpcRequest, JsonRpcResponse, QueuePositionMode, RpcError, RpcValue, SaveSessionTarget, first_forbidden_change_global_option_key, first_forbidden_change_option_key, i64_from_usize, is_rpc_uri_candidate, parse_optional_position, parse_required_file_index, parse_uri_array_allow_empty, state_transition_rpc_error, }; impl InProcessRpcDispatcher { /// Handles `aria2.changeGlobalOption` after rejecting unsupported dynamic keys. pub(super) fn handle_change_global_option( &mut self, request: JsonRpcRequest, ) -> JsonRpcResponse { let Some(RpcValue::Object(map)) = request.params.first() else { return JsonRpcResponse::error( request.id, RpcError::invalid_params("aria2.changeGlobalOption needs option object"), ); }; if let Some(option) = first_forbidden_change_global_option_key(map) { return JsonRpcResponse::error( request.id, RpcError::invalid_params(&format!( "aria2.changeGlobalOption does not allow dynamic updates for option: {option}" )), ); } let patch = self.rpc_object_to_patch(map.clone()); self.engine.apply_options(patch); JsonRpcResponse::success(request.id, RpcValue::String("OK".to_owned())) } /// Handles `aria2.changeOption` by applying validated per-download option patches. pub(super) fn handle_change_option(&mut self, request: JsonRpcRequest) -> JsonRpcResponse { let gid = match self.parse_gid_from_first_param(&request, "aria2.changeOption") { Ok(gid) => gid, Err(error) => return JsonRpcResponse::error(request.id, error), }; let Some(RpcValue::Object(map)) = request.params.get(1) else { return JsonRpcResponse::error( request.id, RpcError::invalid_params("aria2.changeOption needs option object"), ); }; if let Some(option) = first_forbidden_change_option_key(map) { return JsonRpcResponse::error( request.id, RpcError::invalid_params(&format!( "aria2.changeOption does not allow dynamic updates for option: {option}" )), ); } let patch = self.rpc_object_to_patch(map.clone()); let select_file_option = map .get("select-file") .map(|value| self.option_value_text(&self.rpc_value_to_option_value(value.clone()))); match self.engine.handle_mut(gid) { Some(group) => { if let Some(select_file_value) = select_file_option && let Err(message) = apply_bt_select_file_option(group, &select_file_value) { return JsonRpcResponse::error( request.id, RpcError::invalid_params(&format!("invalid select-file option: {message}")), ); } group.options_mut().merge(patch); JsonRpcResponse::success(request.id, RpcValue::String("OK".to_owned())) } None => JsonRpcResponse::error( request.id, RpcError::unsupported(&format!("Cannot change option for GID#{gid}")), ), } } /// Handles `aria2.changePosition` by moving waiting downloads within the queue. pub(super) fn handle_change_position(&mut self, request: JsonRpcRequest) -> JsonRpcResponse { let gid = match self.parse_gid_from_first_param(&request, "aria2.changePosition") { Ok(gid) => gid, Err(error) => return JsonRpcResponse::error(request.id, error), }; let Some(position) = request.params.get(1).and_then(|value| match value { RpcValue::Number(value) => Some(*value), _ => None, }) else { return JsonRpcResponse::error( request.id, RpcError::invalid_params("aria2.changePosition needs position"), ); }; let Some(mode_text) = request.params.get(2).and_then(|value| match value { RpcValue::String(value) => Some(value.as_str()), _ => None, }) else { return JsonRpcResponse::error( request.id, RpcError::invalid_params("aria2.changePosition needs mode"), ); }; let mode = match mode_text { "POS_SET" => QueuePositionMode::Set, "POS_CUR" => QueuePositionMode::Cur, "POS_END" => QueuePositionMode::End, _ => { return JsonRpcResponse::error( request.id, RpcError::invalid_params("Illegal argument."), ); } }; match self.engine.change_position(gid, position, mode) { Ok(dest) => { JsonRpcResponse::success(request.id, RpcValue::Number(i64_from_usize(dest))) } Err(_) => JsonRpcResponse::error( request.id, RpcError::unsupported(&format!("GID#{gid} not found in the waiting queue.")), ), } } /// Handles `aria2.changeUri` by removing and inserting source URIs for a download. pub(super) fn handle_change_uri(&mut self, request: JsonRpcRequest) -> JsonRpcResponse { let gid = match self.parse_gid_from_first_param(&request, "aria2.changeUri") { Ok(gid) => gid, Err(error) => return JsonRpcResponse::error(request.id, error), }; let Some(file_index) = request.params.get(1).and_then(parse_required_file_index) else { return JsonRpcResponse::error( request.id, RpcError::invalid_params("aria2.changeUri needs fileIndex"), ); }; let Some(del_uris_value) = request.params.get(2) else { return JsonRpcResponse::error( request.id, RpcError::invalid_params("aria2.changeUri needs delUris"), ); }; let del_uris = match parse_uri_array_allow_empty(del_uris_value, "aria2.changeUri delUris") { Ok(uris) => uris, Err(error) => { return JsonRpcResponse::error(request.id, RpcError::invalid_params(&error)); } }; let Some(add_uris_value) = request.params.get(3) else { return JsonRpcResponse::error( request.id, RpcError::invalid_params("aria2.changeUri needs addUris"), ); }; let add_uris = match parse_uri_array_allow_empty(add_uris_value, "aria2.changeUri addUris") { Ok(uris) => uris, Err(error) => { return JsonRpcResponse::error(request.id, RpcError::invalid_params(&error)); } }; let position = match parse_optional_position(request.params.get(4), "aria2.changeUri") { Ok(position) => position, Err(error) => { return JsonRpcResponse::error(request.id, RpcError::invalid_params(&error)); } }; if file_index != 1 { return JsonRpcResponse::error( request.id, RpcError::unsupported("fileIndex is out of range"), ); } let Some(group) = self.engine.handle_mut(gid) else { return JsonRpcResponse::error( request.id, RpcError::unsupported(&format!("Cannot remove URIs from GID#{gid}")), ); }; let mut deleted = 0_i64; for uri in del_uris { if group.context_mut().remove_first_matching_uri(&uri) { deleted += 1; } } let mut inserted = 0_i64; if let Some(mut position) = position { for uri in add_uris { if !is_rpc_uri_candidate(&uri) { continue; } group.context_mut().insert_uri(position, uri); position += 1; inserted += 1; } } else { for uri in add_uris { if !is_rpc_uri_candidate(&uri) { continue; } group.context_mut().append_uri(uri); inserted += 1; } } JsonRpcResponse::success( request.id, RpcValue::Array(vec![RpcValue::Number(deleted), RpcValue::Number(inserted)]), ) } /// Handles `aria2.purgeDownloadResult` by removing all stopped download results. pub(super) fn handle_purge_download_result( &mut self, request: JsonRpcRequest, ) -> JsonRpcResponse { self.engine.purge_download_results(); JsonRpcResponse::success(request.id, RpcValue::String("OK".to_owned())) } /// Handles `aria2.removeDownloadResult` for a single stopped download result. pub(super) fn handle_remove_download_result( &mut self, request: JsonRpcRequest, ) -> JsonRpcResponse { let Some(gid_text) = request.params.first().and_then(|value| match value { RpcValue::String(gid) => Some(gid.clone()), _ => None, }) else { return JsonRpcResponse::error( request.id, RpcError::invalid_params("aria2.removeDownloadResult needs gid"), ); }; let Some(gid) = DownloadId::parse_hex(&gid_text) else { return JsonRpcResponse::error( request.id, RpcError::unsupported(&format!("Invalid GID {gid_text}")), ); }; match self.engine.remove_download_result(gid) { Ok(()) => JsonRpcResponse::success(request.id, RpcValue::String("OK".to_owned())), Err(_) => JsonRpcResponse::error( request.id, RpcError { code: crate::model::RpcErrorCode::ApplicationError, kind: crate::model::RpcErrorKind::Internal, message: format!("Could not remove download result of GID#{gid_text}"), }, ), } } /// Handles `aria2.saveSession` by writing the runtime session target when configured. pub(super) fn handle_save_session(&mut self, request: JsonRpcRequest) -> JsonRpcResponse { let target = self .engine .session() .session_file() .cloned() .map(SaveSessionTarget::Path) .unwrap_or(SaveSessionTarget::Memory); match self.engine.save_session(target) { Ok(()) => JsonRpcResponse::success(request.id, RpcValue::String("OK".to_owned())), Err(error) => { JsonRpcResponse::error(request.id, RpcError::unsupported(&error.to_string())) } } } /// Handles graceful shutdown requests with the aria2 success sentinel. pub(super) fn handle_shutdown(&mut self, request: JsonRpcRequest) -> JsonRpcResponse { match self.engine.shutdown() { Ok(()) => JsonRpcResponse::success(request.id, RpcValue::String("OK".to_owned())), Err(error) => { JsonRpcResponse::error(request.id, RpcError::unsupported(&error.to_string())) } } } /// Handles forced shutdown requests with the aria2 success sentinel. pub(super) fn handle_force_shutdown(&mut self, request: JsonRpcRequest) -> JsonRpcResponse { match self.engine.force_shutdown() { Ok(()) => JsonRpcResponse::success(request.id, RpcValue::String("OK".to_owned())), Err(error) => { JsonRpcResponse::error(request.id, RpcError::unsupported(&error.to_string())) } } } /// Handles pause-all variants by pausing eligible active or waiting downloads. pub(super) fn handle_pause_all(&mut self, request: JsonRpcRequest) -> JsonRpcResponse { let gids: Vec<_> = self .engine .registry() .handles() .filter(|handle| { self.engine .registry() .get(handle.gid()) .is_some_and(|group| { matches!( group.status(), DownloadStatus::Active | DownloadStatus::Waiting ) }) }) .map(|handle| handle.gid()) .collect(); for gid in gids { let _ = self.engine.pause(gid); } JsonRpcResponse::success(request.id, RpcValue::String("OK".to_owned())) } /// Handles `aria2.unpauseAll` by resuming eligible paused downloads. pub(super) fn handle_unpause_all(&mut self, request: JsonRpcRequest) -> JsonRpcResponse { let gids: Vec<_> = self .engine .registry() .handles() .filter(|handle| { self.engine .registry() .get(handle.gid()) .is_some_and(|group| group.status() == &DownloadStatus::Paused) }) .map(|handle| handle.gid()) .collect(); for gid in gids { let _ = self.engine.resume(gid); } JsonRpcResponse::success(request.id, RpcValue::String("OK".to_owned())) } /// Handles single-download pause, resume, and remove state transitions. pub(super) fn handle_state_transition( &mut self, request: JsonRpcRequest, method: &'static str, mut apply: F, ) -> JsonRpcResponse where F: FnMut(&mut DownloadEngine, DownloadId) -> Result<(), CoreError>, { let Some(RpcValue::String(gid)) = request.params.first() else { return JsonRpcResponse::error(request.id, RpcError::invalid_params(method)); }; let Some(gid) = DownloadId::parse_hex(gid) else { return JsonRpcResponse::error( request.id, RpcError::unsupported(&format!("Invalid GID {gid}")), ); }; match apply(&mut self.engine, gid) { Ok(()) => JsonRpcResponse::success(request.id, RpcValue::String(gid.to_string())), Err(error) => { JsonRpcResponse::error(request.id, state_transition_rpc_error(method, gid, &error)) } } } }