chore: initial sanitized public snapshot

This commit is contained in:
Aria2 Rust Pro Contributors
2026-07-18 16:01:12 +08:00
commit 7c6b6a3746
321 changed files with 76896 additions and 0 deletions
@@ -0,0 +1,356 @@
use std::collections::{BTreeMap, BTreeSet};
use aria2_rust_pro_compat::{global_option_specs, per_download_option_specs};
use aria2_rust_pro_core::{DownloadHandle, RequestGroup};
use base64::Engine;
use crate::{
model::RpcValue,
xmlrpc::{XmlRpcMember, XmlRpcValue},
};
/// Builds the unified option surface exposed by `getGlobalOption`.
pub(super) fn option_specs_for_global_view() -> Vec<&'static aria2_rust_pro_compat::OptionSpec> {
let mut specs = global_option_specs();
let mut seen = specs.iter().map(|spec| spec.name).collect::<BTreeSet<_>>();
for spec in per_download_option_specs() {
if seen.insert(spec.name) {
specs.push(spec);
}
}
specs
}
/// Lossily converts a `usize` into an `i64` for RPC payload rendering.
pub(super) fn i64_from_usize(value: usize) -> i64 {
i64::try_from(value).unwrap_or(i64::MAX)
}
/// Lossily converts a `usize` into a `u32` for engine-facing counters.
pub(super) fn u32_from_usize(value: usize) -> u32 {
u32::try_from(value).unwrap_or(u32::MAX)
}
/// Lossily converts a `usize` into a `u64` for RPC payload rendering.
pub(super) fn u64_from_usize(value: usize) -> u64 {
u64::try_from(value).unwrap_or(u64::MAX)
}
/// Converts a non-negative JSON-RPC integer into a platform `usize`.
pub(super) fn usize_from_i64(value: i64) -> Option<usize> {
usize::try_from(value).ok()
}
/// Lossily converts a `u64` into a `usize` for local indexing.
pub(super) fn usize_from_u64(value: u64) -> usize {
usize::try_from(value).unwrap_or(usize::MAX)
}
/// Returns the first per-download option key that aria2 forbids through `changeOption`.
pub(super) fn first_forbidden_change_option_key(map: &BTreeMap<String, RpcValue>) -> Option<&str> {
const FORBIDDEN: &[&str] = &[
"dry-run",
"metalink-base-uri",
"parameterized-uri",
"pause",
"piece-length",
"rpc-save-upload-metadata",
];
FORBIDDEN
.iter()
.find_map(|name| map.contains_key(*name).then_some(*name))
}
/// Returns the first global option key that aria2 forbids through `changeGlobalOption`.
pub(super) fn first_forbidden_change_global_option_key(
map: &BTreeMap<String, RpcValue>,
) -> Option<&str> {
const FORBIDDEN: &[&str] = &["checksum", "index-out", "out", "pause", "select-file"];
FORBIDDEN
.iter()
.find_map(|name| map.contains_key(*name).then_some(*name))
}
/// Parses a required RPC URI parameter into a normalized URI list.
pub(super) fn parse_uri_list_param(value: &RpcValue) -> Result<Vec<String>, String> {
match value {
RpcValue::String(uri) => Ok(vec![uri.clone()]),
RpcValue::Array(items) => {
let mut uris = Vec::with_capacity(items.len());
for item in items {
match item {
RpcValue::String(uri) => uris.push(uri.clone()),
_ => return Err("uri array must contain only strings".to_owned()),
}
}
if uris.is_empty() {
return Err("uri array must not be empty".to_owned());
}
Ok(uris)
}
_ => Err("uris must be an array of strings".to_owned()),
}
}
/// Parses a URI array parameter that may legally be empty.
pub(super) fn parse_uri_array_allow_empty(
value: &RpcValue,
label: &str,
) -> Result<Vec<String>, String> {
let RpcValue::Array(items) = value else {
return Err(format!("{label} must be an array of strings"));
};
let mut uris = Vec::with_capacity(items.len());
for item in items {
if let RpcValue::String(uri) = item {
uris.push(uri.clone());
}
}
Ok(uris)
}
/// Parses an optional webseed URI array for add-torrent style methods.
pub(super) fn parse_optional_uri_array(
value: &RpcValue,
method: &str,
) -> Result<Vec<String>, String> {
match value {
RpcValue::Array(items) => {
let mut uris = Vec::with_capacity(items.len());
for item in items {
match item {
RpcValue::String(uri) => uris.push(uri.clone()),
_ => {
return Err(format!("{method} webseed uris must contain only strings"));
}
}
}
Ok(uris)
}
_ => Err(format!("{method} webseed uris must be an array of strings")),
}
}
/// Parses an optional RPC options object into owned key-value entries.
pub(super) fn parse_optional_option_object(
value: Option<&RpcValue>,
method: &str,
) -> Result<Vec<(String, RpcValue)>, String> {
let Some(value) = value else {
return Ok(Vec::new());
};
let RpcValue::Object(options) = value else {
return Err(format!("{method} options must be a struct/object"));
};
Ok(options
.iter()
.map(|(key, value)| (key.clone(), value.clone()))
.collect())
}
/// Parses an optional queue position parameter.
pub(super) fn parse_optional_position(
value: Option<&RpcValue>,
method: &str,
) -> Result<Option<usize>, String> {
let Some(value) = value else {
return Ok(None);
};
match value {
RpcValue::Number(value) if *value >= 0 => Ok(usize_from_i64(*value)),
_ => Err(format!("{method} position must be a non-negative integer")),
}
}
/// Parses a required 1-based file index parameter.
pub(super) fn parse_required_file_index(value: &RpcValue) -> Option<usize> {
match value {
RpcValue::Number(value) if *value >= 1 => usize_from_i64(*value),
_ => None,
}
}
/// Returns whether a URI looks actionable for aria2-style add methods.
pub(super) fn is_rpc_uri_candidate(uri: &str) -> bool {
rpc_uri_has_ascii_prefix(uri, "magnet:?")
|| uri.contains("://")
|| rpc_uri_has_ascii_suffix(uri, ".torrent")
}
/// Returns whether a URI starts with an ASCII prefix, ignoring case.
pub(super) fn rpc_uri_has_ascii_prefix(uri: &str, prefix: &str) -> bool {
uri.get(..prefix.len())
.is_some_and(|head| head.eq_ignore_ascii_case(prefix))
}
/// Returns whether a URI ends with an ASCII suffix, ignoring case.
pub(super) fn rpc_uri_has_ascii_suffix(uri: &str, suffix: &str) -> bool {
uri.get(uri.len().saturating_sub(suffix.len())..)
.is_some_and(|tail| tail.eq_ignore_ascii_case(suffix))
}
/// Extracts a display file name from a URI when one is obvious.
pub(super) fn rpc_uri_file_name(uri: &str) -> Option<String> {
let trimmed = uri
.split(['?', '#'])
.next()
.unwrap_or(uri)
.trim_end_matches('/');
let candidate = trimmed.rsplit('/').next()?;
if candidate.is_empty() {
None
} else {
Some(candidate.to_owned())
}
}
/// Parses an optional status-field allowlist parameter.
pub(super) fn parse_optional_status_keys(
value: Option<&RpcValue>,
method: &str,
) -> Result<Option<BTreeSet<String>>, String> {
let Some(value) = value else {
return Ok(None);
};
let RpcValue::Array(items) = value else {
return Err(format!("{method} keys must be an array of strings"));
};
if items.is_empty() {
return Ok(None);
}
let mut keys = BTreeSet::new();
for item in items {
match item {
RpcValue::String(key) => {
keys.insert(key.clone());
}
_ => return Err(format!("{method} keys must contain only strings")),
}
}
Ok(Some(keys))
}
/// Filters a status payload down to the requested field set.
pub(super) fn filter_status_payload(
payload: RpcValue,
keys: Option<&BTreeSet<String>>,
) -> RpcValue {
let Some(keys) = keys else {
return payload;
};
match payload {
RpcValue::Object(fields) => RpcValue::Object(
fields
.into_iter()
.filter(|(key, _)| keys.contains(key))
.collect(),
),
other => other,
}
}
/// Applies RPC option values to a request group using aria2's stringly option model.
pub(super) fn apply_group_options(group: &mut RequestGroup, options: Vec<(String, RpcValue)>) {
for (key, value) in options {
match value {
RpcValue::String(value) => group.set_option(key, value),
RpcValue::Number(value) => group.set_option(key, value.to_string()),
RpcValue::Bool(value) => group.set_option(key, if value { "true" } else { "false" }),
RpcValue::Null => group.set_option(key, ""),
RpcValue::Array(_) | RpcValue::Object(_) => {}
}
}
}
/// Applies already-normalized string options to a request group directly.
pub(super) fn apply_group_string_options(group: &mut RequestGroup, options: Vec<(String, String)>) {
for (key, value) in options {
group.set_option(key, value);
}
}
/// Builds implied request-group options from a metalink plan entry.
pub(super) fn metalink_default_options(
entry: &aria2_rust_pro_protocol::metalink::MetalinkDownloadPlanEntry,
) -> Vec<(String, RpcValue)> {
let mut options = Vec::new();
if !entry.file_name.trim().is_empty() {
options.push(("out".to_owned(), RpcValue::String(entry.file_name.clone())));
}
if let Some(checksum) = &entry.checksum {
options.push((
"checksum".to_owned(),
RpcValue::String(format!("{}={}", checksum.algorithm, checksum.expected_hex)),
));
}
options
}
/// Decodes a base64 metalink payload when the caller did not send raw XML.
pub(super) fn decode_metalink_payload(value: &str) -> Option<String> {
let bytes = base64::engine::general_purpose::STANDARD
.decode(value.as_bytes())
.ok()?;
let text = String::from_utf8(bytes).ok()?;
text.contains("<metalink").then_some(text)
}
/// Slices a download-handle list using aria2's positive and negative offset rules.
pub(super) fn slice_handles_by_offset(
handles: Vec<DownloadHandle>,
offset: i64,
max: usize,
) -> Vec<DownloadHandle> {
if max == 0 || handles.is_empty() {
return Vec::new();
}
if offset >= 0 {
return handles
.into_iter()
.skip(usize_from_i64(offset).unwrap_or(usize::MAX))
.take(max)
.collect();
}
let reversed = handles.into_iter().rev().collect::<Vec<_>>();
let start = offset
.checked_neg()
.and_then(|value| value.checked_sub(1))
.and_then(usize_from_i64)
.unwrap_or_default();
reversed.into_iter().skip(start).take(max).collect()
}
/// Looks up a named XML-RPC struct member.
pub(super) fn xmlrpc_member_value<'a>(
members: &'a [XmlRpcMember],
name: &str,
) -> Option<&'a XmlRpcValue> {
members
.iter()
.find(|member| member.name == name)
.map(|member| &member.value)
}
/// Parses the completed byte count from a `Content-Range` header value.
pub(super) fn parse_content_range_completed_length(value: &str) -> Option<u64> {
let mut parts = value.split_whitespace();
let unit = parts.next()?;
if !unit.eq_ignore_ascii_case("bytes") {
return None;
}
let range = parts.next()?;
let (start, end) = range.split_once('-')?;
let start = start.parse::<u64>().ok()?;
let end = end.parse::<u64>().ok()?;
if end < start {
return None;
}
Some(end - start + 1)
}
/// Returns whether an HTTP status should preserve retry eligibility.
pub(super) fn is_retry_relevant_status(status: u16) -> bool {
matches!(status, 408 | 425 | 429 | 500 | 502 | 503 | 504)
}