1use crate::DownloadOptions;
12use crate::config::ProxyConfig;
13use crate::download::download as http_download;
14use crate::optimization::Optimizer;
15use std::error::Error;
16
17pub fn is_webdav_url(url: &str) -> bool {
19 let lower = url.to_lowercase();
20 lower.starts_with("webdav://") || lower.starts_with("webdavs://")
21}
22
23pub struct WebDavDownloader {
25 http_url: String,
26 output: String,
27 quiet: bool,
28 proxy: ProxyConfig,
29 optimizer: Optimizer,
30 username: Option<String>,
31 password: Option<String>,
32}
33
34impl WebDavDownloader {
35 pub fn new(
40 url: String,
41 output: String,
42 quiet: bool,
43 proxy: ProxyConfig,
44 optimizer: Optimizer,
45 ) -> Self {
46 let (http_url, username, password) = parse_webdav_url(&url);
47 Self { http_url, output, quiet, proxy, optimizer, username, password }
48 }
49
50 pub fn download(&self) -> Result<(), Box<dyn Error + Send + Sync>> {
51 let mut extra_headers: Vec<(String, String)> = Vec::new();
52
53 if let Some(user) = &self.username {
54 let pass = self.password.as_deref().unwrap_or("");
55 let creds = base64_encode(format!("{user}:{pass}").as_bytes());
56 extra_headers.push(("Authorization".to_string(), format!("Basic {creds}")));
57 }
58
59 let options = DownloadOptions {
60 quiet_mode: self.quiet,
61 output_path: Some(self.output.clone()),
62 verify_iso: false,
63 expected_sha256: None,
64 extra_headers,
65 };
66
67 http_download(
68 &self.http_url,
69 self.proxy.clone(),
70 self.optimizer.clone(),
71 options,
72 None,
73 )
74 }
75}
76
77fn parse_webdav_url(url: &str) -> (String, Option<String>, Option<String>) {
83 let (scheme, rest) = if url.to_lowercase().starts_with("webdavs://") {
84 ("https", &url["webdavs://".len()..])
85 } else {
86 ("http", &url["webdav://".len()..])
87 };
88
89 if let Some(at) = rest.find('@') {
91 let creds = &rest[..at];
92 let host_path = &rest[at + 1..];
93 let http_url = format!("{scheme}://{host_path}");
94 if let Some(colon) = creds.find(':') {
95 return (
96 http_url,
97 Some(creds[..colon].to_string()),
98 Some(creds[colon + 1..].to_string()),
99 );
100 }
101 return (http_url, Some(creds.to_string()), None);
102 }
103
104 (format!("{scheme}://{rest}"), None, None)
105}
106
107fn base64_encode(data: &[u8]) -> String {
109 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
110 let mut out = String::with_capacity((data.len() + 2) / 3 * 4);
111 for chunk in data.chunks(3) {
112 let b0 = chunk[0] as usize;
113 let b1 = if chunk.len() > 1 { chunk[1] as usize } else { 0 };
114 let b2 = if chunk.len() > 2 { chunk[2] as usize } else { 0 };
115 out.push(CHARS[b0 >> 2] as char);
116 out.push(CHARS[((b0 & 3) << 4) | (b1 >> 4)] as char);
117 out.push(if chunk.len() > 1 { CHARS[((b1 & 15) << 2) | (b2 >> 6)] as char } else { '=' });
118 out.push(if chunk.len() > 2 { CHARS[b2 & 63] as char } else { '=' });
119 }
120 out
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn parse_plain_webdav() {
129 let (url, user, pass) = parse_webdav_url("webdav://nas.local/files/report.pdf");
130 assert_eq!(url, "http://nas.local/files/report.pdf");
131 assert!(user.is_none() && pass.is_none());
132 }
133
134 #[test]
135 fn parse_webdavs_with_credentials() {
136 let (url, user, pass) = parse_webdav_url("webdavs://alice:secret@cloud.example.com/docs/file.zip");
137 assert_eq!(url, "https://cloud.example.com/docs/file.zip");
138 assert_eq!(user.as_deref(), Some("alice"));
139 assert_eq!(pass.as_deref(), Some("secret"));
140 }
141
142 #[test]
143 fn base64_roundtrip() {
144 assert_eq!(base64_encode(b"user:pass"), "dXNlcjpwYXNz");
145 assert_eq!(base64_encode(b"Man"), "TWFu");
146 assert_eq!(base64_encode(b""), "");
147 }
148}