chore: initial sanitized public snapshot

This commit is contained in:
Aria2 Rust Pro Contributors
2026-07-18 15:24:15 +08:00
commit e489b29e01
321 changed files with 76890 additions and 0 deletions
@@ -0,0 +1,362 @@
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<F>(
&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))
}
}
}
}