use std::fs;
use std::path::Path;
use crate::error::{Error, Result};
use crate::url::Url;
pub fn fetch(url: &Url) -> Result<Vec<u8>> {
if !url.host.is_empty() && !url.host.eq_ignore_ascii_case("localhost") {
return Err(Error::BadResponse(format!(
"file:// URLs with non-local host are not supported: {}",
url.host
)));
}
let path = Path::new(&url.path);
let meta = fs::metadata(path)?;
if meta.is_dir() {
return Err(Error::BadResponse(format!(
"path is a directory, not a file: {}",
url.path
)));
}
Ok(fs::read(path)?)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static COUNTER: AtomicU64 = AtomicU64::new(0);
fn unique_temp_path(label: &str) -> std::path::PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
std::env::temp_dir().join(format!("rsurl-file-{label}-{pid}-{nanos}-{n}"))
}
fn url_for(path: &str, host: &str) -> Url {
Url {
scheme: "file".into(),
userinfo: None,
host: host.into(),
port: 0,
path: path.into(),
}
}
#[test]
fn reads_existing_file() {
let p = unique_temp_path("read");
let payload = b"hello, file://!\n\x00binary\xffbytes";
{
let mut f = fs::File::create(&p).expect("create temp file");
f.write_all(payload).expect("write payload");
}
let url = url_for(p.to_str().expect("utf-8 temp path"), "");
let got = fetch(&url).expect("fetch should succeed");
assert_eq!(got, payload);
let url_local = url_for(p.to_str().unwrap(), "localhost");
let got_local = fetch(&url_local).expect("localhost should be accepted");
assert_eq!(got_local, payload);
let url_caps = url_for(p.to_str().unwrap(), "LocalHost");
let got_caps = fetch(&url_caps).expect("case-insensitive localhost");
assert_eq!(got_caps, payload);
let _ = fs::remove_file(&p);
}
#[test]
fn rejects_non_local_host() {
let url = url_for("/etc/hosts", "example.com");
match fetch(&url) {
Err(Error::BadResponse(msg)) => {
assert!(msg.contains("example.com"), "msg = {msg}");
}
other => panic!("expected BadResponse, got {other:?}"),
}
}
#[test]
fn missing_file_returns_io_error() {
let p = unique_temp_path("missing");
assert!(!p.exists(), "test setup: path should not exist");
let url = url_for(p.to_str().unwrap(), "");
match fetch(&url) {
Err(Error::Io(_)) => {}
other => panic!("expected Io error, got {other:?}"),
}
}
#[test]
fn directory_path_is_rejected() {
let dir = unique_temp_path("dir");
fs::create_dir(&dir).expect("create dir");
let url = url_for(dir.to_str().unwrap(), "");
let res = fetch(&url);
let _ = fs::remove_dir(&dir);
match res {
Err(Error::BadResponse(msg)) => {
assert!(msg.contains("directory"), "msg = {msg}");
}
other => panic!("expected BadResponse, got {other:?}"),
}
}
}