diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index 95ab754a..a14e9b16 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -58,6 +58,10 @@ pub enum Error { #[error("invalid datetime: {0}")] InvalidDatetime(String), + /// A file path could not be converted to a URL. + #[error("invalid file path: {0}")] + InvalidFilePath(String), + /// [std::io::Error] #[error(transparent)] Io(#[from] std::io::Error), diff --git a/crates/core/src/href.rs b/crates/core/src/href.rs index 6778e437..76d37708 100644 --- a/crates/core/src/href.rs +++ b/crates/core/src/href.rs @@ -1,6 +1,6 @@ //! Utilities and structures for working with hrefs. -use crate::Result; +use crate::{Error, Result}; use std::borrow::Cow; use url::Url; @@ -57,11 +57,21 @@ pub trait SelfHref { } } +/// Returns `true` if the href looks like a Windows absolute path (e.g. `C:\foo` or `D:/bar`). +pub fn is_windows_absolute_path(s: &str) -> bool { + let bytes = s.as_bytes(); + bytes.len() >= 3 + && bytes[0].is_ascii_alphabetic() + && bytes[1] == b':' + && (bytes[2] == b'\\' || bytes[2] == b'/') +} + /// Returns `true` if the href is absolute. /// -/// An href is absolute if it can be parsed to a url or starts with a `/`. +/// An href is absolute if it can be parsed to a url, starts with a `/`, or is +/// a Windows absolute path. pub fn is_absolute(href: &str) -> bool { - Url::parse(href).is_ok() || href.starts_with('/') + is_windows_absolute_path(href) || Url::parse(href).is_ok() || href.starts_with('/') } /// Makes an href absolute relative to a base. @@ -157,17 +167,15 @@ pub fn make_relative(href: &str, base: &str) -> String { /// /// Handles adding a `file://` prefix and making it absolute, if needed. pub fn make_url(href: &str) -> Result { - if let Ok(url) = Url::parse(href) { + if is_windows_absolute_path(href) || href.starts_with('/') { + Url::from_file_path(href).map_err(|_| Error::InvalidFilePath(href.to_string())) + } else if let Ok(url) = Url::parse(href) { Ok(url) } else { - let url = if href.starts_with("/") { - Url::parse(&format!("file://{href}")) - } else { - let current_dir = std::env::current_dir()?; - let url = make_url(&format!("{}/", current_dir.to_string_lossy()))?; - url.join(href) - }?; - Ok(url) + let current_dir = std::env::current_dir()?; + let url = Url::from_directory_path(¤t_dir) + .map_err(|_| Error::InvalidFilePath(current_dir.to_string_lossy().into_owned()))?; + Ok(url.join(href)?) } } diff --git a/crates/core/src/migrate.rs b/crates/core/src/migrate.rs index 7ee51c71..f11cf766 100644 --- a/crates/core/src/migrate.rs +++ b/crates/core/src/migrate.rs @@ -2,6 +2,7 @@ use crate::{Error, Result, Version}; use serde::{Serialize, de::DeserializeOwned}; use serde_json::{Map, Value}; use std::collections::HashMap; +use url::Url; /// Migrates a STAC object from one version to another. pub trait Migrate: Sized + Serialize + DeserializeOwned + std::fmt::Debug { @@ -193,16 +194,33 @@ fn migrate_bands(asset: &mut Map) -> Result<()> { fn migrate_links(object: &mut Map) { if let Some(links) = object.get_mut("links").and_then(|v| v.as_array_mut()) { for link in links { - if let Some(link) = link.as_object_mut() - && link - .get("rel") - .and_then(|v| v.as_str()) - .map(|s| s == "self") - .unwrap_or_default() - && let Some(href) = link.get("href").and_then(|v| v.as_str()) - && href.starts_with('/') - { - let _ = link.insert("href".to_string(), format!("file://{href}").into()); + let is_self_link = link + .as_object() + .and_then(|l| l.get("rel")) + .and_then(|v| v.as_str()) + .map(|s| s == "self") + .unwrap_or_default(); + if !is_self_link { + continue; + } + let href = link + .as_object() + .and_then(|l| l.get("href")) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + if let Some(href) = href { + let new_href = if href.starts_with('/') { + Some(format!("file://{href}")) + } else if crate::href::is_windows_absolute_path(&href) { + Url::from_file_path(&href).ok().map(|u| u.to_string()) + } else { + None + }; + if let Some(new_href) = new_href { + if let Some(link) = link.as_object_mut() { + let _ = link.insert("href".to_string(), new_href.into()); + } + } } } } diff --git a/crates/io/src/realized_href.rs b/crates/io/src/realized_href.rs index 8ddcb5d9..9afc947f 100644 --- a/crates/io/src/realized_href.rs +++ b/crates/io/src/realized_href.rs @@ -13,6 +13,9 @@ pub enum RealizedHref { impl From<&str> for RealizedHref { fn from(s: &str) -> RealizedHref { + if stac::href::is_windows_absolute_path(s) { + return RealizedHref::PathBuf(PathBuf::from(s)); + } if let Ok(url) = Url::parse(s) { if url.scheme() == "file" { url.to_file_path() diff --git a/crates/io/src/store.rs b/crates/io/src/store.rs index e0d77f15..26e15eeb 100644 --- a/crates/io/src/store.rs +++ b/crates/io/src/store.rs @@ -145,7 +145,9 @@ impl StacStore { } fn path(&self, href: &str) -> Result { - let result = if let Ok(url) = Url::parse(href) { + let result = if stac::href::is_windows_absolute_path(href) { + Path::parse(href) + } else if let Ok(url) = Url::parse(href) { // TODO check to see if the host and such match? or not? Path::from_url_path(url.path()) } else {