use std::collections::HashMap;
use crate::error::Error;
use crate::protocol::http::response::Response;
pub fn read_file(
url: &crate::url::Url,
range_start: Option<u64>,
range_end: Option<u64>,
) -> Result<Response, Error> {
let path = url.path();
let decoded_path = percent_decode(path);
#[cfg(windows)]
let decoded_path = strip_windows_leading_slash(&decoded_path);
let data = std::fs::read(&decoded_path)
.map_err(|e| Error::FileError(format!("{decoded_path}: {e}")))?;
#[allow(clippy::cast_possible_truncation)]
let data = match (range_start, range_end) {
(Some(start), Some(end)) => {
let s = start as usize;
let e = (end as usize).min(data.len().saturating_sub(1));
if s <= e && s < data.len() {
data[s..=e].to_vec()
} else {
return Err(Error::Transfer {
code: 36,
message: "Couldn't resume download".to_string(),
});
}
}
(Some(start), None) => {
let s = start as usize;
if s < data.len() {
data[s..].to_vec()
} else {
return Err(Error::Transfer {
code: 36,
message: "Couldn't resume download".to_string(),
});
}
}
(None, Some(last_n)) => {
let n = last_n as usize;
if n >= data.len() {
data
} else {
data[data.len() - n..].to_vec()
}
}
_ => data,
};
let mut headers = HashMap::new();
let _old = headers.insert("content-length".to_string(), data.len().to_string());
Ok(Response::new(200, headers, data, url.as_str().to_string()))
}
pub fn write_file(url: &crate::url::Url, data: &[u8]) -> Result<Response, Error> {
let path = url.path();
let decoded_path = percent_decode(path);
#[cfg(windows)]
let decoded_path = strip_windows_leading_slash(&decoded_path);
std::fs::write(&decoded_path, data).map_err(Error::Io)?;
let headers = HashMap::new();
Ok(Response::new(200, headers, Vec::new(), url.as_str().to_string()))
}
#[cfg(windows)]
fn strip_windows_leading_slash(path: &str) -> String {
let bytes = path.as_bytes();
if bytes.len() >= 3 && bytes[0] == b'/' && bytes[1].is_ascii_alphabetic() && bytes[2] == b':' {
path[1..].to_string()
} else {
path.to_string()
}
}
fn percent_decode(input: &str) -> String {
let mut result = Vec::with_capacity(input.len());
let bytes = input.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 2 < bytes.len() {
if let (Some(hi), Some(lo)) = (hex_val(bytes[i + 1]), hex_val(bytes[i + 2])) {
result.push(hi << 4 | lo);
i += 3;
continue;
}
}
result.push(bytes[i]);
i += 1;
}
String::from_utf8_lossy(&result).to_string()
}
const fn hex_val(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn percent_decode_no_encoding() {
assert_eq!(percent_decode("/path/to/file"), "/path/to/file");
}
#[test]
fn percent_decode_space() {
assert_eq!(percent_decode("/my%20file.txt"), "/my file.txt");
}
#[test]
fn percent_decode_multiple() {
assert_eq!(percent_decode("/a%20b%2Fc"), "/a b/c");
}
#[test]
fn percent_decode_incomplete() {
assert_eq!(percent_decode("/test%2"), "/test%2");
}
#[test]
fn read_file_nonexistent() {
let url = crate::url::Url::parse("file:///nonexistent/path").unwrap();
assert!(read_file(&url, None, None).is_err());
}
}