use crate::net;
use crate::package::*;
pub(crate) fn path_source_uri(path: &Path) -> Result<String, PackageError> {
let url = Url::from_file_path(path)
.map_err(|_| format!("failed to convert {} to file:// URL", path.display()))?;
Ok(format!("path+{url}"))
}
pub(crate) fn path_from_source_uri(source: &str) -> Result<PathBuf, PackageError> {
let raw = source
.strip_prefix("path+")
.ok_or_else(|| format!("invalid path source: {source}"))?;
if crate::format::looks_like_windows_drive_path(raw) {
return Ok(PathBuf::from(raw));
}
if let Ok(url) = Url::parse(raw) {
return url
.to_file_path()
.map_err(|_| PackageError::Registry(format!("invalid file:// path source: {source}")));
}
Ok(PathBuf::from(raw))
}
pub(crate) fn archive_url_from_source_uri(source: &str) -> Result<&str, PackageError> {
source
.strip_prefix("archive+")
.ok_or_else(|| format!("invalid archive source: {source}").into())
}
pub(crate) fn archive_source_uri(raw: &str) -> Result<String, PackageError> {
Ok(format!("archive+{}", normalize_archive_url(raw)?))
}
pub(crate) fn registry_file_url_or_path(raw: &str) -> Result<Option<PathBuf>, PackageError> {
if crate::format::looks_like_windows_drive_path(raw) {
return Ok(Some(PathBuf::from(raw)));
}
if let Ok(url) = Url::parse(raw) {
if url.scheme() == "file" {
return url.to_file_path().map(Some).map_err(|_| {
PackageError::Registry(format!("invalid file:// registry URL: {raw}"))
});
}
return Ok(None);
}
Ok(Some(PathBuf::from(raw)))
}
pub(crate) fn normalize_archive_url(raw: &str) -> Result<String, PackageError> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err("archive URL cannot be empty".to_string().into());
}
if crate::format::looks_like_windows_drive_path(trimmed) {
return normalize_archive_path(trimmed);
}
if let Ok(url) = Url::parse(trimmed) {
return match url.scheme() {
"file" => {
let path = url.to_file_path().map_err(|_| {
PackageError::Registry(format!(
"invalid file:// package archive URL: {trimmed}"
))
})?;
if path.exists() {
let canonical = path.canonicalize().map_err(|error| {
format!("failed to canonicalize {}: {error}", path.display())
})?;
let url = Url::from_file_path(canonical)
.map_err(|_| format!("failed to convert {trimmed} to file:// URL"))?;
Ok(url.to_string())
} else {
Ok(url.to_string())
}
}
"http" | "https" => Ok(url.to_string()),
other => Err(format!("unsupported package archive URL scheme: {other}").into()),
};
}
normalize_archive_path(trimmed)
}
fn normalize_archive_path(raw: &str) -> Result<String, PackageError> {
let path = PathBuf::from(raw);
if !path.exists() {
return Err(format!("package archive not found: {}", path.display()).into());
}
let canonical = path
.canonicalize()
.map_err(|error| format!("failed to canonicalize {}: {error}", path.display()))?;
let url = Url::from_file_path(canonical)
.map_err(|_| format!("failed to convert {raw} to file:// URL"))?;
Ok(url.to_string())
}
fn package_registry_auth_token() -> Option<String> {
std::env::var(HARN_PACKAGE_REGISTRY_TOKEN_ENV)
.ok()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
}
fn apply_package_registry_auth(
request: reqwest::blocking::RequestBuilder,
) -> reqwest::blocking::RequestBuilder {
if let Some(token) = package_registry_auth_token() {
request.bearer_auth(token)
} else {
request
}
}
pub(crate) fn read_registry_source(source: &str) -> Result<String, PackageError> {
if let Some(path) = registry_file_url_or_path(source)? {
return fs::read_to_string(&path).map_err(|error| {
PackageError::Registry(format!(
"failed to read package registry {}: {error}",
path.display()
))
});
}
let url = Url::parse(source)
.map_err(|error| format!("invalid package registry URL {source:?}: {error}"))?;
match url.scheme() {
"http" | "https" => {}
other => return Err(format!("unsupported package registry URL scheme: {other}").into()),
}
let source_owned = source.to_string();
std::thread::scope(|scope| {
scope
.spawn(move || fetch_registry_blocking(url, &source_owned))
.join()
.map_err(|_| PackageError::Registry("registry fetch thread panicked".to_string()))?
})
}
fn fetch_registry_blocking(url: Url, source: &str) -> Result<String, PackageError> {
let display_source = net::diagnostic_text(source);
let client = net::blocking_http_client("cli.package.registry", Duration::from_secs(20))
.map_err(PackageError::Registry)?;
let response = apply_package_registry_auth(client.get(url))
.send()
.map_err(|error| {
format!(
"failed to fetch package registry {display_source}: {}",
net::reqwest_error(&error)
)
})?;
let status = response.status();
if !status.is_success() {
return Err(format!("GET {display_source} returned HTTP {status}").into());
}
response.text().map_err(|error| {
PackageError::Registry(format!(
"failed to read package registry response: {}",
net::reqwest_error(&error)
))
})
}
pub(crate) fn read_package_archive_bytes(source: &str) -> Result<Vec<u8>, PackageError> {
if let Some(path) = registry_file_url_or_path(source)? {
let metadata = fs::metadata(&path).map_err(|error| {
format!("failed to stat package archive {}: {error}", path.display())
})?;
if metadata.len() > PACKAGE_ARCHIVE_MAX_BYTES {
return Err(format!(
"package archive {} is {} bytes, above the {} byte limit",
path.display(),
metadata.len(),
PACKAGE_ARCHIVE_MAX_BYTES
)
.into());
}
return fs::read(&path).map_err(|error| {
PackageError::Registry(format!(
"failed to read package archive {}: {error}",
path.display()
))
});
}
let url = Url::parse(source)
.map_err(|error| format!("invalid package archive URL {source:?}: {error}"))?;
match url.scheme() {
"http" | "https" => {}
other => return Err(format!("unsupported package archive URL scheme: {other}").into()),
}
let source_owned = source.to_string();
std::thread::scope(|scope| {
scope
.spawn(move || fetch_package_archive_blocking(url, &source_owned))
.join()
.map_err(|_| {
PackageError::Registry("package archive fetch thread panicked".to_string())
})?
})
}
fn fetch_package_archive_blocking(url: Url, source: &str) -> Result<Vec<u8>, PackageError> {
let display_source = net::diagnostic_text(source);
let client = net::blocking_http_client("cli.package.archive", Duration::from_secs(30))
.map_err(PackageError::Registry)?;
let response = apply_package_registry_auth(client.get(url))
.send()
.map_err(|error| {
format!(
"failed to fetch package archive {display_source}: {}",
net::reqwest_error(&error)
)
})?;
let status = response.status();
if !status.is_success() {
return Err(format!("GET {display_source} returned HTTP {status}").into());
}
if response
.content_length()
.is_some_and(|length| length > PACKAGE_ARCHIVE_MAX_BYTES)
{
return Err(format!(
"package archive {display_source} is larger than the {PACKAGE_ARCHIVE_MAX_BYTES} byte limit"
)
.into());
}
let bytes = response.bytes().map_err(|error| {
format!(
"failed to read package archive response: {}",
net::reqwest_error(&error)
)
})?;
if bytes.len() as u64 > PACKAGE_ARCHIVE_MAX_BYTES {
return Err(format!(
"package archive {display_source} is larger than the {PACKAGE_ARCHIVE_MAX_BYTES} byte limit"
)
.into());
}
Ok(bytes.to_vec())
}