aria2_core/validation/
uri.rs1use crate::error::{Aria2Error, Result};
2
3const SUPPORTED_SCHEMES: &[&str] = &["http", "https", "ftp", "sftp", "file"];
4const DANGEROUS_SCHEMES: &[&str] = &["javascript", "data", "vbscript"];
5
6const URI_MAX_FILENAME_LEN: usize = 255;
7
8#[derive(Debug, Clone)]
9pub struct ValidatedUri {
10 pub original: String,
11 pub scheme: String,
12 pub is_magnet: bool,
13 pub is_torrent: bool,
14}
15
16pub fn validate(uri: &str) -> Result<ValidatedUri> {
17 let trimmed = uri.trim();
18 if trimmed.is_empty() {
19 return Err(Aria2Error::Fatal(crate::error::FatalError::Config(
20 "URI不能为空".into(),
21 )));
22 }
23
24 if trimmed.starts_with("magnet:?") || trimmed.starts_with("magnet?") {
25 return Ok(ValidatedUri {
26 original: trimmed.to_string(),
27 scheme: "magnet".to_string(),
28 is_magnet: true,
29 is_torrent: false,
30 });
31 }
32
33 let (scheme, rest) = match trimmed.split_once("://") {
34 Some(pair) => pair,
35 None => {
36 return Err(Aria2Error::Fatal(crate::error::FatalError::Config(
37 "URI缺少协议前缀".into(),
38 )));
39 }
40 };
41
42 let lower_scheme = scheme.to_lowercase();
43 for dangerous in DANGEROUS_SCHEMES {
44 if lower_scheme == *dangerous {
45 return Err(Aria2Error::Fatal(crate::error::FatalError::Config(
46 format!("不安全的协议: {}", scheme),
47 )));
48 }
49 }
50 if !SUPPORTED_SCHEMES.contains(&lower_scheme.as_str()) && lower_scheme != "magnet" {
51 return Err(Aria2Error::Fatal(crate::error::FatalError::Config(
52 format!("不支持的协议: {}", scheme),
53 )));
54 }
55 if rest.is_empty() {
56 return Err(Aria2Error::Fatal(crate::error::FatalError::Config(
57 "URI缺少路径".into(),
58 )));
59 }
60
61 Ok(ValidatedUri {
62 original: trimmed.to_string(),
63 scheme: lower_scheme.clone(),
64 is_magnet: false,
65 is_torrent: lower_scheme == "file" && rest.ends_with(".torrent"),
66 })
67}
68
69pub fn is_magnet_link(uri: &str) -> bool {
70 let t = uri.trim().to_lowercase();
71 t.starts_with("magnet:?") || t.starts_with("magnet?")
72}
73
74pub fn is_torrent_file(path: &str) -> bool {
75 path.trim().ends_with(".torrent")
76}
77
78pub fn sanitize_filename_from_uri(uri: &str) -> String {
79 let uri = uri.trim();
80 let path_part = uri
81 .rsplit('/')
82 .next()
83 .unwrap_or("")
84 .rsplit('\\')
85 .next()
86 .unwrap_or("");
87
88 let decoded = urlencoding_decode(path_part);
89 let cleaned = remove_traversal(&decoded);
90 truncate_filename(&cleaned)
91}
92
93fn urlencoding_decode(s: &str) -> String {
94 let mut result = String::with_capacity(s.len());
95 let mut chars = s.chars().peekable();
96 while let Some(c) = chars.next() {
97 if c == '%' {
98 let hex: String = chars.by_ref().take(2).collect();
99 if hex.len() == 2
100 && let Ok(byte) = u8::from_str_radix(&hex, 16)
101 {
102 result.push(byte as char);
103 continue;
104 }
105 result.push(c);
106 } else {
107 result.push(c);
108 }
109 }
110 result
111}
112
113fn remove_traversal(s: &str) -> String {
114 s.replace("../", "").replace("..\\", "").replace("./", "")
115}
116
117fn truncate_filename(s: &str) -> String {
118 if s.len() > URI_MAX_FILENAME_LEN {
119 s[..URI_MAX_FILENAME_LEN].to_string()
120 } else {
121 s.to_string()
122 }
123}