use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use crate::array::{ArrayBuffer, ArrayData, ArrayLocation, ArrayMetadata, ByteOrder};
use crate::error::{Error, ExternalDataReason, Result};
pub trait ArrayBackend: Send + Sync {
fn name(&self) -> &'static str;
fn handles(&self, mime: Option<&str>, file: &Path) -> bool;
fn metadata(&self, file: &Path, path_in_file: &str) -> Result<ArrayMetadata>;
fn read(&self, file: &Path, path_in_file: &str, start: &[i64], count: &[i64]) -> Result<ArrayData>;
fn read_into(
&self,
file: &Path,
path_in_file: &str,
start: &[i64],
count: &[i64],
out: &mut ArrayBuffer<'_>,
) -> Result<ArrayMetadata>;
fn byte_order(&self, _file: &Path, _path_in_file: &str) -> Result<Option<ByteOrder>> {
Ok(None)
}
fn file_uuid(&self, _file: &Path) -> Result<Option<String>> {
Ok(None)
}
fn writes(&self) -> bool {
false
}
fn write(&self, file: &Path, path_in_file: &str, _data: &ArrayData) -> Result<()> {
Err(Error::ExternalData {
location: format!("{}:{}", file.display(), path_in_file),
reason: ExternalDataReason::Backend(format!(
"the `{}` backend reads but does not write",
self.name()
)),
})
}
fn write_many(&self, file: &Path, items: &[(&str, &ArrayData)]) -> Vec<Result<()>> {
items
.iter()
.map(|(path, data)| self.write(file, path, data))
.collect()
}
fn set_file_uuid(&self, file: &Path, _uuid: &str) -> Result<()> {
Err(Error::ExternalData {
location: file.display().to_string(),
reason: ExternalDataReason::Backend(format!(
"the `{}` backend cannot stamp a file uuid",
self.name()
)),
})
}
}
#[derive(Debug, Clone, Default)]
pub struct ExternalFiles {
root: Option<PathBuf>,
overrides: BTreeMap<String, PathBuf>,
offered: Vec<PathBuf>,
present: Vec<String>,
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum Origin {
Pinned,
Offered(usize),
Declared,
Companion,
Neighbour,
}
impl Origin {
pub fn is_a_departure(self) -> bool {
self != Origin::Declared
}
pub fn describe(self) -> &'static str {
match self {
Origin::Pinned => "a file pinned to that name by the caller",
Origin::Offered(_) => "a file the caller offered",
Origin::Declared => "the file the document names",
Origin::Companion => "the file beside the package with the package's name",
Origin::Neighbour => "another file in the package's folder",
}
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Candidate {
pub origin: Origin,
pub path: PathBuf,
}
impl ExternalFiles {
pub fn with_root(root: impl Into<PathBuf>) -> Self {
Self {
root: Some(root.into()),
overrides: BTreeMap::new(),
offered: Vec::new(),
present: Vec::new(),
}
}
pub fn set(&mut self, name: impl Into<String>, path: impl Into<PathBuf>) -> &mut Self {
self.overrides.insert(name.into(), path.into());
self
}
pub fn offer(&mut self, path: impl Into<PathBuf>) -> &mut Self {
self.offered.push(path.into());
self
}
pub fn offered(&self) -> &[PathBuf] {
&self.offered
}
pub fn declare_present(&mut self, name: impl Into<String>) -> &mut Self {
let name = name.into();
if !self.present.contains(&name) {
self.present.push(name);
}
self
}
pub fn present(&self) -> &[String] {
&self.present
}
pub fn is_virtual(&self) -> bool {
!self.present.is_empty()
}
pub fn candidates(&self, declared: &str, package: Option<&Path>) -> Vec<Candidate> {
let mut out: Vec<Candidate> = Vec::new();
let push = |origin: Origin, path: PathBuf, out: &mut Vec<Candidate>| {
if path.exists() && !out.iter().any(|c| c.path == path) {
out.push(Candidate { origin, path });
}
};
let declare = |origin: Origin, name: &str, out: &mut Vec<Candidate>| {
let path = PathBuf::from(name);
if !out.iter().any(|c| c.path == path) {
out.push(Candidate { origin, path });
}
};
let bare_declared = declared.rsplit(['/', '\\']).next().unwrap_or(declared);
for (rank, name) in self.present.iter().enumerate() {
let bare = name.rsplit(['/', '\\']).next().unwrap_or(name);
let origin = match name == declared || bare == bare_declared {
true => Origin::Pinned,
false => Origin::Offered(rank),
};
declare(origin, name, &mut out);
}
out.sort_by_key(|candidate| candidate.origin);
let bare = declared.rsplit(['/', '\\']).next().unwrap_or(declared);
for key in [declared, bare] {
if let Some(path) = self.overrides.get(key) {
push(Origin::Pinned, path.clone(), &mut out);
}
}
for (rank, path) in self.offered.iter().enumerate() {
push(Origin::Offered(rank), path.clone(), &mut out);
}
if !declared.is_empty() {
let written = Path::new(declared);
if written.is_absolute() {
push(Origin::Declared, written.to_path_buf(), &mut out);
}
if let Some(root) = &self.root {
for relative in [declared, bare] {
push(Origin::Declared, root.join(relative), &mut out);
}
}
}
if let Some(package) = package {
push(Origin::Companion, package.with_extension("h5"), &mut out);
}
if let Some(root) = &self.root
&& let Ok(entries) = std::fs::read_dir(root)
{
let mut neighbours: Vec<PathBuf> = entries
.flatten()
.map(|entry| entry.path())
.filter(|path| path.is_file())
.filter(|path| Some(path.as_path()) != package)
.filter(|path| !path.extension().is_some_and(|e| e.eq_ignore_ascii_case("epc")))
.collect();
neighbours.sort();
for path in neighbours {
push(Origin::Neighbour, path, &mut out);
}
}
out
}
pub fn root(&self) -> Option<&Path> {
self.root.as_deref()
}
pub fn set_root(&mut self, root: impl Into<PathBuf>) -> &mut Self {
self.root = Some(root.into());
self
}
pub fn resolve(&self, name: &str) -> Result<PathBuf> {
let missing = || Error::ExternalData {
location: name.to_string(),
reason: ExternalDataReason::FileMissing,
};
if name.is_empty() {
return Err(missing());
}
let bare = name.rsplit(['/', '\\']).next().unwrap_or(name);
for key in [name, bare] {
if let Some(path) = self.overrides.get(key) {
return Ok(path.clone());
}
}
let candidate = Path::new(name);
if candidate.is_absolute() && candidate.exists() {
return Ok(candidate.to_path_buf());
}
let root = self.root.as_deref().ok_or_else(missing)?;
for relative in [name, bare] {
let path = root.join(relative);
if path.exists() {
return Ok(path);
}
}
Err(missing())
}
pub fn destination_for(&self, name: &str) -> Result<PathBuf> {
let nowhere = || Error::ExternalData {
location: name.to_string(),
reason: ExternalDataReason::FileMissing,
};
if name.is_empty() {
return Err(nowhere());
}
let bare = name.rsplit(['/', '\\']).next().unwrap_or(name);
for key in [name, bare] {
if let Some(path) = self.overrides.get(key) {
return Ok(path.clone());
}
}
let candidate = Path::new(name);
if candidate.is_absolute() {
return Ok(candidate.to_path_buf());
}
self.root
.as_deref()
.map(|root| root.join(name))
.ok_or_else(nowhere)
}
}
#[derive(Default)]
pub struct Backends {
entries: Vec<Box<dyn ArrayBackend>>,
}
impl Backends {
pub fn none() -> Self {
Self::default()
}
pub fn compiled_in() -> Self {
#[allow(unused_mut)]
let mut backends = Self::default();
#[cfg(feature = "hdf5")]
backends.push(Box::new(crate::hdf5::Hdf5Backend));
backends
}
pub fn push(&mut self, backend: Box<dyn ArrayBackend>) -> &mut Self {
self.entries.insert(0, backend);
self
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn find(&self, location: &ArrayLocation, file: &Path) -> Result<&dyn ArrayBackend> {
self.entries
.iter()
.find(|backend| backend.handles(location.mime.as_deref(), file))
.map(|backend| backend.as_ref())
.ok_or_else(|| Error::ExternalData {
location: format!("{}:{}", file.display(), location.path_in_file),
reason: ExternalDataReason::FeatureNotEnabled {
feature: "hdf5",
mime: location.mime.clone(),
},
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_pinned_file_wins_over_the_package() {
let mut files = ExternalFiles::with_root("/nowhere");
files.set("model.h5", "/elsewhere/renamed.h5");
assert_eq!(
files.resolve("model.h5").unwrap(),
Path::new("/elsewhere/renamed.h5")
);
assert_eq!(
files.resolve("sub/model.h5").unwrap(),
Path::new("/elsewhere/renamed.h5")
);
}
#[test]
fn an_unknown_file_says_it_is_missing() {
let files = ExternalFiles::with_root("/nowhere");
assert!(matches!(
files.resolve("absent.h5"),
Err(Error::ExternalData {
reason: ExternalDataReason::FileMissing,
..
})
));
assert!(files.resolve("").is_err(), "an empty name resolves to nothing");
}
#[test]
fn without_a_backend_the_error_names_the_capability() {
let backends = Backends::none();
let location = ArrayLocation {
file: "model.h5".to_string(),
path_in_file: "/a/b".to_string(),
mime: Some("application/x-hdf5".to_string()),
start: Vec::new(),
count: Vec::new(),
proxy_uuid: None,
field_path: "Points".to_string(),
resource_uri: None,
};
match backends.find(&location, Path::new("model.h5")) {
Err(Error::ExternalData {
reason: ExternalDataReason::FeatureNotEnabled { feature, .. },
..
}) => assert_eq!(feature, "hdf5"),
Err(other) => panic!("wrong error: {other}"),
Ok(backend) => panic!("no backend was registered, yet `{}` answered", backend.name()),
}
}
}