1use std::path::PathBuf;
2use std::str::FromStr;
3use std::sync::Arc;
4
5use zarrs::array::{Array, ArrayCreateError};
6use zarrs::filesystem::FilesystemStore;
7use zarrs::storage::ReadableListableStorage;
8
9use crate::error::{EcpError, Result, ResultExt};
10use crate::format::{FORMAT_VERSION, V1_ARRAYS};
11use crate::metric::Metric;
12
13fn open_optional(
16 store: &ReadableListableStorage,
17 path: &str,
18) -> Result<Option<Array<dyn zarrs::storage::ReadableListableStorageTraits>>> {
19 match Array::open(store.clone(), path) {
20 Ok(array) => Ok(Some(array)),
21 Err(ArrayCreateError::MissingMetadata) => Ok(None),
22 Err(other) => Err(EcpError::Store(format!("failed to open {path}: {other}"))),
23 }
24}
25
26fn open_info_field(
30 store: &ReadableListableStorage,
31 name: &str,
32) -> Result<Array<dyn zarrs::storage::ReadableListableStorageTraits>> {
33 open_optional(store, &format!("/info/{name}"))?
34 .ok_or_else(|| EcpError::Corrupt(format!("not a valid index: /info/{name} is missing")))
35}
36
37pub(super) fn read_format_version(store: &ReadableListableStorage) -> Result<Option<u32>> {
41 let Some(array) = open_optional(store, "/info/format_version")? else {
42 let mut missing = vec!["/info/format_version"];
43 for path in V1_ARRAYS {
44 if open_optional(store, path)?.is_none() {
45 missing.push(path);
46 }
47 }
48 if missing.len() > 1 {
49 return Err(EcpError::Corrupt(format!(
50 "not a valid index, missing: {}",
51 missing.join(", ")
52 )));
53 }
54 return Ok(None);
55 };
56
57 let version = array
58 .retrieve_array_subset::<Vec<u32>>(&array.subset_all())
59 .store_err("failed to retrieve info/format_version")?[0];
60 if version != FORMAT_VERSION {
61 return Err(EcpError::UnsupportedVersion(format!(
62 "index format version {version} is not supported, this build reads version {FORMAT_VERSION}"
63 )));
64 }
65 Ok(Some(version))
66}
67
68pub(super) fn read_info_fields(
71 store: &ReadableListableStorage,
72) -> Result<(u32, Metric, Option<bool>)> {
73 let levels_array = open_info_field(store, "levels")?;
74 let levels: u32 = levels_array
75 .retrieve_array_subset::<Vec<u32>>(&levels_array.subset_all())
76 .store_err("failed to retrieve info/levels")?[0];
77
78 let metric_array = open_info_field(store, "metric")?;
79 let metric_str = metric_array
80 .retrieve_array_subset::<Vec<String>>(&metric_array.subset_all())
81 .store_err("failed to retrieve info/metric")?
82 .remove(0);
83 let metric = Metric::from_str(&metric_str)
84 .map_err(|e| EcpError::Corrupt(format!("info/metric holds an unrecognized metric: {e}")))?;
85
86 let is_normalized = match open_optional(store, "/info/is_normalized")? {
87 Some(array) => Some(
88 array
89 .retrieve_array_subset::<Vec<bool>>(&array.subset_all())
90 .store_err("failed to retrieve info/is_normalized")?[0],
91 ),
92 None => None,
93 };
94
95 Ok((levels, metric, is_normalized))
96}
97
98pub(super) fn read_info_u32(store: &ReadableListableStorage, name: &str) -> Result<u32> {
100 let array = open_info_field(store, name)?;
101 Ok(array
102 .retrieve_array_subset::<Vec<u32>>(&array.subset_all())
103 .store_err_with(|| format!("failed to retrieve info/{name}"))?[0])
104}
105
106pub struct IndexInfo {
109 pub format_version: u32,
112 pub levels: u32,
114 pub metric: Metric,
115 pub is_normalized: Option<bool>,
118 pub total_items: u32,
120 pub next_item_id: u32,
123 pub total_representatives: u32,
126}
127
128impl IndexInfo {
129 pub fn load(index_path: PathBuf) -> Result<Self> {
131 if !index_path.exists() {
132 return Err(EcpError::NotFound(format!(
133 "index directory {} does not exist",
134 index_path.display()
135 )));
136 }
137 let store: ReadableListableStorage =
138 Arc::new(FilesystemStore::new(&index_path).store_err("failed to open store")?);
139 Self::load_from_store(store)
140 }
141
142 fn load_from_store(store: ReadableListableStorage) -> Result<Self> {
145 let format_version = read_format_version(&store)?.unwrap_or(FORMAT_VERSION);
146 let (levels, metric, is_normalized) = read_info_fields(&store)?;
147 let total_items = read_info_u32(&store, "total_items")?;
148 let next_item_id = read_info_u32(&store, "next_item_id")?;
149
150 let rep_ids_array = Array::open(store.clone(), "/rep_item_ids").map_err(|e| match e {
151 ArrayCreateError::MissingMetadata => {
152 EcpError::Corrupt("not a valid index: /rep_item_ids is missing".to_string())
153 }
154 other => EcpError::Store(format!("failed to open rep_item_ids: {other}")),
155 })?;
156 let total_representatives = rep_ids_array.shape()[0] as u32;
157
158 Ok(IndexInfo {
159 format_version,
160 levels,
161 metric,
162 is_normalized,
163 total_items,
164 next_item_id,
165 total_representatives,
166 })
167 }
168}
169
170#[cfg(test)]
171#[path = "utests/info.rs"]
172mod tests;