#[must_use]
pub fn file_url(path: &std::path::Path) -> String {
let encoded = path
.to_string_lossy()
.replace('%', "%25")
.replace(' ', "%20");
format!("file://{encoded}")
}
#[must_use]
pub fn file_url_path(url: &str) -> Option<std::path::PathBuf> {
let rest = url
.strip_prefix("file://localhost")
.or_else(|| url.strip_prefix("file://"))?;
if !rest.starts_with('/') {
return None;
}
Some(std::path::PathBuf::from(
rest.replace("%20", " ").replace("%25", "%"),
))
}
#[cfg(test)]
mod file_url_tests {
use super::{file_url, file_url_path};
use std::path::{Path, PathBuf};
#[test]
fn a_path_with_a_space_round_trips() {
let path = Path::new("/tmp/polly space test/boxlite-runtime.tar.gz");
let url = file_url(path);
assert_eq!(
url, "file:///tmp/polly%20space%20test/boxlite-runtime.tar.gz",
"the emitted URL is what an operator pastes into BOXLITE_RUNTIME_URL"
);
assert!(
!url.contains(' '),
"curl rejects a file:// URL with a raw space (exit 3, \
\"URL rejected: Malformed input to a URL function\"): {url}"
);
assert_eq!(
file_url_path(&url).as_deref(),
Some(path),
"what build.rs verifies has to be the file the URL was built from"
);
}
#[test]
fn a_path_with_a_literal_percent_round_trips_too() {
for path in [
Path::new("/tmp/pct%20dir/boxlite-runtime.tar.gz"),
Path::new("/tmp/100% done/boxlite-runtime.tar.gz"),
Path::new("/tmp/%2520/boxlite-runtime.tar.gz"),
] {
let url = file_url(path);
assert_eq!(
file_url_path(&url).as_deref(),
Some(path),
"{url} did not decode back to the path it was built from"
);
}
assert_eq!(
file_url(Path::new("/tmp/pct%20dir/x.tar.gz")),
"file:///tmp/pct%2520dir/x.tar.gz"
);
}
#[test]
fn the_emitted_url_resolves_to_the_file_it_was_built_from() {
const BODY: &[u8] = b"not really an archive";
let dir = std::env::temp_dir().join(format!("rto-exec file url {}", std::process::id()));
std::fs::create_dir_all(&dir).expect("a temp directory with a space in its name");
let file = dir.join("boxlite-runtime.tar.gz");
std::fs::write(&file, BODY).expect("write the stand-in archive");
let url = file_url(&file);
assert!(
!url.contains(' ') && url.contains("%20"),
"the directory really does have a space in its name, so the URL really does have \
to carry an encoded one — that is the shape curl accepts: {url}"
);
let resolved = file_url_path(&url).expect("the emitter produces URLs the parser accepts");
assert_eq!(resolved, file);
assert_eq!(
std::fs::read(&resolved).expect("the resolved path is readable"),
BODY
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn only_absolute_file_urls_are_accepted() {
assert_eq!(
file_url_path("file://localhost/tmp/a%20b/x.tar.gz").as_deref(),
Some(Path::new("/tmp/a b/x.tar.gz"))
);
assert_eq!(
file_url_path("file:///tmp/x.tar.gz"),
Some(PathBuf::from("/tmp/x.tar.gz"))
);
assert_eq!(file_url_path("https://example.invalid/x.tar.gz"), None);
assert_eq!(file_url_path("/tmp/x.tar.gz"), None);
assert_eq!(file_url_path("file://relative/x.tar.gz"), None);
}
}