use crate::Result;
use serde_json::Value;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Range {
Whole,
At(u64, u64),
Suffix(u64),
}
enum Backend {
Local(PathBuf),
#[cfg(not(target_arch = "wasm32"))]
Remote {
base: RemoteBase,
fetcher: Box<crate::http::ObjectFetcher>,
},
}
#[cfg(not(target_arch = "wasm32"))]
#[derive(Debug, Clone)]
enum RemoteBase {
Url {
prefix: String,
query: String,
},
S3 {
bucket: String,
prefix: String,
},
}
const METADATA_FILES: [&str; 4] = [".zarray", ".zattrs", ".zgroup", "zarr.json"];
pub(crate) struct Store {
backend: Backend,
name: String,
consolidated: Option<HashMap<String, Value>>,
cache: Mutex<HashMap<String, Option<Value>>>,
}
#[derive(Clone)]
pub(crate) struct Node {
store: Arc<Store>,
key: String,
}
impl std::fmt::Debug for Node {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.display())
}
}
impl PartialEq for Node {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.store, &other.store) && self.key == other.key
}
}
fn join_key(base: &str, rel: &str) -> String {
let mut parts: Vec<&str> = if rel.starts_with('/') { Vec::new() } else { base.split('/').filter(|s| !s.is_empty()).collect() };
for seg in rel.split('/') {
match seg {
"" | "." => {}
".." => {
parts.pop();
}
s => parts.push(s),
}
}
parts.join("/")
}
fn local_node(p: &Path) -> bool {
crate::memfs::is_dir(p) && METADATA_FILES.iter().any(|f| crate::memfs::is_file(&p.join(f)))
}
impl Store {
fn new(backend: Backend, name: String) -> Self {
Self { backend, name, consolidated: None, cache: Mutex::new(HashMap::new()) }
}
pub(crate) fn get(&self, key: &str, range: Range) -> Result<Option<Vec<u8>>> {
match &self.backend {
Backend::Local(root) => {
use std::io::{Read, Seek, SeekFrom};
let p = if key.is_empty() { root.clone() } else { root.join(key) };
if crate::memfs::is_virtual(&p) {
let Ok(all) = crate::memfs::read(&p) else { return Ok(None) };
let len = all.len() as u64;
let (start, end) = match range {
Range::Whole => (0, len),
Range::At(offset, n) => (offset.min(len), offset.saturating_add(n).min(len)),
Range::Suffix(n) => (len.saturating_sub(n), len),
};
return Ok(Some(all[start as usize..end as usize].to_vec()));
}
let mut f = match std::fs::File::open(&p) {
Ok(f) if p.is_file() => f,
Ok(_) => return Ok(None),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(e.into()),
};
let mut out = Vec::new();
match range {
Range::Whole => {
f.read_to_end(&mut out)?;
}
Range::At(offset, len) => {
f.seek(SeekFrom::Start(offset))?;
f.take(len).read_to_end(&mut out)?;
}
Range::Suffix(n) => {
let size = f.metadata()?.len();
f.seek(SeekFrom::Start(size.saturating_sub(n)))?;
f.read_to_end(&mut out)?;
}
}
Ok(Some(out))
}
#[cfg(not(target_arch = "wasm32"))]
Backend::Remote { base, fetcher } => {
use crate::http::{ByteRange, Remote};
let remote = match base {
RemoteBase::Url { prefix, query } => {
Remote::Url(if key.is_empty() { format!("{prefix}{query}") } else { format!("{prefix}/{key}{query}") })
}
RemoteBase::S3 { bucket, prefix } => Remote::S3(crate::s3::S3Object {
bucket: bucket.clone(),
key: if prefix.is_empty() { key.to_string() } else { format!("{prefix}/{key}") },
}),
};
let range = match range {
Range::Whole => ByteRange::Whole,
Range::At(o, l) => ByteRange::At(o, l),
Range::Suffix(n) => ByteRange::Suffix(n),
};
fetcher.get(&remote, range)
}
}
}
fn is_local(&self) -> bool {
matches!(self.backend, Backend::Local(_))
}
fn json(&self, key: &str) -> Result<Option<Value>> {
if let Some(c) = &self.consolidated
&& let Some(v) = c.get(key)
{
return Ok(Some(v.clone()));
}
if self.consolidated.is_some() && !self.is_local() {
return Ok(None);
}
if let Some(v) = self.cache.lock().ok().and_then(|c| c.get(key).cloned()) {
return Ok(v);
}
let v = match self.get(key, Range::Whole)? {
Some(b) => Some(
serde_json::from_slice::<Value>(&b)
.map_err(|e| crate::Error::InvalidData(format!("Zarr: {key} of {} is not valid JSON: {e}", self.name)))?,
),
None => None,
};
if let Ok(mut c) = self.cache.lock() {
c.insert(key.to_string(), v.clone());
}
Ok(v)
}
fn probe(&self, key: &str) -> Result<Option<Value>> {
match self.json(key) {
Err(crate::Error::InvalidData(_)) => Ok(None),
r => r,
}
}
fn load_consolidated(&mut self) -> Result<()> {
let mut map = HashMap::new();
if let Some(m) = self.probe(".zmetadata")?
&& let Some(meta) = m["metadata"].as_object()
{
for (k, v) in meta {
map.insert(k.trim_start_matches('/').to_string(), v.clone());
}
} else if let Some(root) = self.probe("zarr.json")?
&& let Some(meta) = root["consolidated_metadata"]["metadata"].as_object()
&& !meta.is_empty()
{
let mut r = root.clone();
if let Some(o) = r.as_object_mut() {
o.remove("consolidated_metadata");
}
map.insert("zarr.json".to_string(), r);
for (k, v) in meta {
map.insert(join_key(k, "zarr.json"), v.clone());
}
}
if !map.is_empty() {
self.consolidated = Some(map);
}
Ok(())
}
}
impl Node {
pub(crate) fn local(path: &Path) -> Node {
let path = if crate::memfs::is_virtual(path) {
path.to_path_buf()
} else {
std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf())
};
let mut root = path.clone();
while let Some(parent) = root.parent()
&& local_node(parent)
{
root = parent.to_path_buf();
}
let key = path.strip_prefix(&root).map(|k| k.to_string_lossy().replace('\\', "/")).unwrap_or_default();
let mut store = Store::new(Backend::Local(root.clone()), root.display().to_string());
let _ = store.load_consolidated();
Node { store: Arc::new(store), key }
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn remote(loc: &crate::vfs::Location) -> Result<Node> {
use crate::vfs::Location;
let split = |path: &str| -> (String, String) {
let path = path.trim_end_matches('/');
let segs: Vec<&str> = path.split('/').collect();
match segs.iter().rposition(|s| s.to_ascii_lowercase().ends_with(".zarr")) {
Some(i) => (segs[..=i].join("/"), segs[i + 1..].join("/")),
None => (path.to_string(), String::new()),
}
};
let (base, key, name) = match loc {
Location::Url(u) => {
let (path, query) = u.split_once('?').map_or((u.as_str(), String::new()), |(p, q)| (p, format!("?{q}")));
let (prefix, key) = split(path);
(RemoteBase::Url { prefix: prefix.clone(), query }, key, prefix)
}
Location::S3 { bucket, key } => {
let (prefix, rest) = split(key);
let name = format!("s3://{bucket}/{prefix}");
(RemoteBase::S3 { bucket: bucket.clone(), prefix }, rest, name)
}
_ => return Err(crate::Error::InvalidArgument("not a remote location".into())),
};
let mut store = Store::new(Backend::Remote { base, fetcher: Box::new(crate::http::ObjectFetcher::new()) }, name);
store.load_consolidated()?;
let node = Node { store: Arc::new(store), key };
let mut found = node.store.consolidated.is_some();
for f in METADATA_FILES {
if found {
break;
}
found = node.store.probe(&join_key(&node.key, f))?.is_some();
}
if !found {
return Err(crate::Error::InvalidData(format!("Zarr: no Zarr metadata at {}", node.display())));
}
Ok(node)
}
pub(crate) fn join(&self, rel: &str) -> Node {
Node { store: self.store.clone(), key: join_key(&self.key, rel) }
}
pub(crate) fn parent(&self) -> Option<Node> {
if self.key.is_empty() {
return None;
}
Some(Node { store: self.store.clone(), key: self.key.rsplit_once('/').map_or("", |(p, _)| p).to_string() })
}
pub(crate) fn name(&self) -> String {
let k = if self.key.is_empty() { self.store.name.trim_end_matches('/') } else { &self.key };
k.rsplit(['/', '\\']).next().unwrap_or_default().to_string()
}
pub(crate) fn display(&self) -> String {
if self.key.is_empty() { self.store.name.clone() } else { format!("{}/{}", self.store.name.trim_end_matches('/'), self.key) }
}
pub(crate) fn json(&self, file: &str) -> Result<Option<Value>> {
self.store.json(&join_key(&self.key, file))
}
pub(crate) fn get(&self, rel: &str, range: Range) -> Result<Option<Vec<u8>>> {
self.store.get(&join_key(&self.key, rel), range)
}
pub(crate) fn children(&self) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
if let Some(c) = &self.store.consolidated {
let prefix = if self.key.is_empty() { String::new() } else { format!("{}/", self.key) };
for k in c.keys() {
if let Some(rest) = k.strip_prefix(&prefix)
&& let Some((child, _)) = rest.split_once('/')
&& !out.iter().any(|o| o == child)
{
out.push(child.to_string());
}
}
}
#[allow(irrefutable_let_patterns)]
if let Backend::Local(root) = &self.store.backend {
let dir = if self.key.is_empty() { root.clone() } else { root.join(&self.key) };
for (n, _) in crate::memfs::subdirs(&dir) {
if !out.contains(&n) {
out.push(n);
}
}
}
out.sort();
out
}
pub(crate) fn is_remote(&self) -> bool {
!self.store.is_local()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn joins_keys() {
assert_eq!(join_key("a/b", "c"), "a/b/c");
assert_eq!(join_key("a/b", "../c"), "a/c");
assert_eq!(join_key("a/b", "/c/./d"), "c/d");
assert_eq!(join_key("", "c/.zarray"), "c/.zarray");
assert_eq!(join_key("a", ".."), "");
}
}