use object_store::{ObjectStoreScheme, path::Path};
use url::{Position, Url};
use super::data_file_error;
use crate::DeltaReaderError;
pub(super) fn resolve_data_file_path(
table_url: &Url,
file_path: &str,
) -> Result<Path, DeltaReaderError> {
let location = table_url
.join(file_path)
.map_err(|error| data_file_error("data_file_path_resolution_failed", error))?;
let mut key = location.path();
if !is_path_reference(file_path) {
let namespace = path_namespace(&location);
if storage_identity(table_url, path_namespace(table_url))
!= storage_identity(&location, namespace)
{
return Err(data_file_error(
"data_file_store_mismatch",
std::io::Error::other("data file URL does not identify the configured table store"),
));
}
if !namespace.is_empty() {
key = key
.strip_prefix('/')
.unwrap_or(key)
.split_once('/')
.map_or("", |(_, key)| key);
}
}
Path::from_url_path(key).map_err(|_| {
data_file_error(
"data_file_path_resolution_failed",
std::io::Error::other("invalid data file object key"),
)
})
}
fn is_path_reference(path: &str) -> bool {
if Url::parse(path).is_ok() {
return false;
}
let mut bytes = path
.trim_start_matches(|ch| ch <= ' ')
.bytes()
.filter(|byte| !matches!(byte, b'\t' | b'\r' | b'\n'));
!matches!(
(bytes.next(), bytes.next()),
(Some(b'/' | b'\\'), Some(b'/' | b'\\'))
)
}
#[derive(PartialEq, Eq)]
enum StorageIdentity<'a> {
Azure {
account: &'a str,
service: &'a str,
container: &'a str,
},
Url {
scheme: &'a str,
authority: &'a str,
namespace: &'a str,
},
}
fn storage_identity<'a>(url: &'a Url, namespace: &'a str) -> StorageIdentity<'a> {
let container = match url.scheme() {
"https" if url.username().is_empty() => Some(namespace),
"az" | "abfs" | "abfss" if !url.username().is_empty() => Some(url.username()),
_ => None,
};
if let Some(container) = container
&& url.password().is_none()
&& url.port().is_none_or(|port| port == 443)
&& let Some((account, suffix)) = url.host_str().and_then(|host| host.split_once('.'))
{
let service = match suffix {
"dfs.core.windows.net" | "blob.core.windows.net" => Some("core"),
"dfs.fabric.microsoft.com" | "blob.fabric.microsoft.com" => Some("fabric"),
_ => None,
};
if let Some(service) = service {
return StorageIdentity::Azure {
account,
service,
container,
};
}
}
StorageIdentity::Url {
scheme: storage_scheme(url),
authority: &url[Position::BeforeUsername..Position::AfterPort],
namespace,
}
}
fn storage_scheme(url: &Url) -> &str {
match url.scheme() {
"s3a" => "s3",
"az" | "adl" | "azure" | "abfs" | "abfss" if url.username().is_empty() => "az",
"az" | "abfs" | "abfss" => "abfs",
scheme => scheme,
}
}
fn path_namespace(url: &Url) -> &str {
let namespace_in_path = url.scheme() == "https"
&& match ObjectStoreScheme::parse(url) {
Ok((ObjectStoreScheme::MicrosoftAzure, _)) => true,
Ok((ObjectStoreScheme::AmazonS3, _)) => url.host_str().is_some_and(|host| {
host.strip_prefix("s3.")
.and_then(|host| host.strip_suffix(".amazonaws.com"))
.is_some_and(|region| !region.contains('.'))
|| host.ends_with(".r2.cloudflarestorage.com")
}),
_ => false,
};
if namespace_in_path {
let path = url.path().strip_prefix('/').unwrap_or(url.path());
path.split('/').next().unwrap_or("")
} else {
""
}
}