1use std::collections::HashMap;
10
11use hdf5_reader::group::Group;
12
13use crate::error::{Error, Result};
14use crate::types::{NcDimension, NcVariable};
15
16use super::attributes;
17use super::types::hdf5_to_nc_type;
18
19fn leaf_name(name: &str) -> &str {
20 name.rsplit('/').next().unwrap_or(name)
21}
22
23pub fn extract_variables(
31 group: &Group,
32 dimensions: &[NcDimension],
33 dim_addr_map: &HashMap<u64, NcDimension>,
34 metadata_mode: crate::NcMetadataMode,
35) -> Result<Vec<NcVariable>> {
36 let datasets = group.datasets()?;
37 extract_variables_from_datasets(&datasets, group, dimensions, dim_addr_map, metadata_mode)
38}
39
40pub fn extract_variables_from_datasets(
41 datasets: &[hdf5_reader::Dataset],
42 group: &Group,
43 dimensions: &[NcDimension],
44 dim_addr_map: &HashMap<u64, NcDimension>,
45 metadata_mode: crate::NcMetadataMode,
46) -> Result<Vec<NcVariable>> {
47 let mut variables = Vec::new();
48
49 for ds in datasets {
50 if let Some(variable) =
51 extract_variable(ds, group, dimensions, dim_addr_map, metadata_mode)?
52 {
53 variables.push(variable);
54 }
55 }
56
57 Ok(variables)
58}
59
60pub fn extract_variable(
61 ds: &hdf5_reader::Dataset,
62 group: &Group,
63 dimensions: &[NcDimension],
64 dim_addr_map: &HashMap<u64, NcDimension>,
65 metadata_mode: crate::NcMetadataMode,
66) -> Result<Option<NcVariable>> {
67 let strict = metadata_mode == crate::NcMetadataMode::Strict;
68
69 let is_dim_scale = match ds.attribute("CLASS") {
70 Ok(attr) => match attr.read_string() {
71 Ok(value) => value == "DIMENSION_SCALE",
72 Err(err) if strict => {
73 return Err(Error::InvalidData(format!(
74 "dataset '{}' has unreadable CLASS attribute: {err}",
75 ds.name()
76 )))
77 }
78 Err(_) => false,
79 },
80 Err(_) => false,
81 };
82
83 if is_dim_scale && is_dimension_only_scale(ds, strict)? {
84 return Ok(None);
85 }
86
87 let nc_type = match hdf5_to_nc_type(ds.dtype()) {
88 Ok(t) => t,
89 Err(err) if strict => {
90 return Err(Error::InvalidData(format!(
91 "dataset '{}' uses unsupported NetCDF-4 type: {err}",
92 ds.name()
93 )))
94 }
95 Err(_) => return Ok(None),
96 };
97
98 let var_dims = if is_dim_scale {
99 resolve_coordinate_variable_dimensions(ds, dim_addr_map, metadata_mode)?
100 } else {
101 resolve_variable_dimensions(ds, group, dimensions, dim_addr_map, metadata_mode)?
102 };
103 let is_unlimited = var_dims.iter().any(|d| d.is_unlimited);
104 let shape = ds.shape();
105 let elem_size = u64::try_from(nc_type.size()?)
106 .map_err(|_| Error::InvalidData("NetCDF-4 type size exceeds u64 capacity".to_string()))?;
107 let (data_size, record_size) = compute_storage_sizes(shape, elem_size, is_unlimited)?;
108 let var_attrs = attributes::extract_variable_attributes(ds, metadata_mode)?;
109
110 Ok(Some(NcVariable {
111 name: leaf_name(ds.name()).to_string(),
112 dimensions: var_dims,
113 dtype: nc_type,
114 attributes: var_attrs,
115 data_offset: ds.address(),
116 _data_size: data_size,
117 is_record_var: is_unlimited,
118 record_size,
119 }))
120}
121
122fn is_dimension_only_scale(ds: &hdf5_reader::Dataset, strict: bool) -> Result<bool> {
123 match ds.attribute("NAME") {
124 Ok(attr) => match attr.read_string() {
125 Ok(value) => Ok(super::dimensions::is_dimension_without_variable_name(
126 &value,
127 )),
128 Err(err) if strict => Err(Error::InvalidData(format!(
129 "dimension scale '{}' has unreadable NAME attribute: {err}",
130 ds.name()
131 ))),
132 Err(_) => Ok(false),
133 },
134 Err(_) => Ok(false),
135 }
136}
137
138fn resolve_coordinate_variable_dimensions(
139 ds: &hdf5_reader::Dataset,
140 dim_addr_map: &HashMap<u64, NcDimension>,
141 metadata_mode: crate::NcMetadataMode,
142) -> Result<Vec<NcDimension>> {
143 if let Some(dim) = dim_addr_map.get(&ds.address()) {
144 Ok(vec![dim.clone()])
145 } else if metadata_mode == crate::NcMetadataMode::Lossy {
146 Ok(vec![NcDimension {
147 name: leaf_name(ds.name()).to_string(),
148 size: ds.shape().first().copied().unwrap_or(0),
149 is_unlimited: ds
150 .max_dims()
151 .is_some_and(|md| !md.is_empty() && md[0] == u64::MAX),
152 }])
153 } else {
154 Err(Error::InvalidData(format!(
155 "coordinate variable '{}' is not registered as a dimension scale",
156 ds.name()
157 )))
158 }
159}
160
161fn resolve_variable_dimensions(
168 ds: &hdf5_reader::Dataset,
169 group: &Group,
170 dimensions: &[NcDimension],
171 dim_addr_map: &HashMap<u64, NcDimension>,
172 metadata_mode: crate::NcMetadataMode,
173) -> Result<Vec<NcDimension>> {
174 resolve_variable_dimensions_with_mode(ds, group, dimensions, dim_addr_map, metadata_mode)
175}
176
177fn resolve_variable_dimensions_with_mode(
178 ds: &hdf5_reader::Dataset,
179 group: &Group,
180 dimensions: &[NcDimension],
181 dim_addr_map: &HashMap<u64, NcDimension>,
182 metadata_mode: crate::NcMetadataMode,
183) -> Result<Vec<NcDimension>> {
184 match resolve_variable_dimensions_from_dimlist(ds, group, dim_addr_map) {
185 Ok(dims) => Ok(dims),
186 Err(_err) if metadata_mode == crate::NcMetadataMode::Lossy => {
187 Ok(resolve_variable_dimensions_by_size(ds, dimensions))
188 }
189 Err(err) => Err(err),
190 }
191}
192
193fn resolve_variable_dimensions_from_dimlist(
195 ds: &hdf5_reader::Dataset,
196 group: &Group,
197 dim_addr_map: &HashMap<u64, NcDimension>,
198) -> Result<Vec<NcDimension>> {
199 let dim_addrs = resolve_dimension_scale_addresses(ds, group)?;
200 let mut var_dims = Vec::with_capacity(dim_addrs.len());
201 for dim_addr in dim_addrs {
202 if let Some(dim) = dim_addr_map.get(&dim_addr) {
203 var_dims.push(dim.clone());
204 } else {
205 return Err(Error::InvalidData(format!(
206 "dataset '{}' references unknown dimension scale address {dim_addr:#x}",
207 ds.name()
208 )));
209 }
210 }
211
212 if let Some(max_dims) = ds.max_dims() {
214 for (i, md) in max_dims.iter().enumerate() {
215 if *md == u64::MAX && i < var_dims.len() {
216 var_dims[i].is_unlimited = true;
217 }
218 }
219 }
220
221 Ok(var_dims)
222}
223
224pub(crate) fn resolve_dimension_scale_addresses(
225 ds: &hdf5_reader::Dataset,
226 group: &Group,
227) -> Result<Vec<u64>> {
228 let ndim = ds.ndim();
230 if ndim == 0 {
231 return Ok(Vec::new());
232 }
233
234 let attr = ds.attribute("DIMENSION_LIST").map_err(|_| {
235 Error::InvalidData(format!(
236 "dataset '{}' is missing required DIMENSION_LIST metadata",
237 ds.name()
238 ))
239 })?;
240 let raw_data = &attr.raw_data;
241 let offset_size = group.offset_size();
242
243 if raw_data.is_empty() {
244 return Err(Error::InvalidData(format!(
245 "dataset '{}' has empty DIMENSION_LIST metadata",
246 ds.name()
247 )));
248 }
249
250 let entry_size = 4 + usize::from(offset_size) + 4;
251 if raw_data.len() < ndim * entry_size {
252 return Err(Error::InvalidData(format!(
253 "dataset '{}' has truncated DIMENSION_LIST metadata",
254 ds.name()
255 )));
256 }
257
258 let mut dim_addrs = Vec::with_capacity(ndim);
259 let mut cursor = hdf5_reader::io::Cursor::new(raw_data);
260
261 for _ in 0..ndim {
262 let seq_len = cursor.read_u32_le().map_err(|err| {
263 Error::InvalidData(format!(
264 "dataset '{}' has invalid DIMENSION_LIST entry count: {err}",
265 ds.name()
266 ))
267 })? as usize;
268 let heap_addr = cursor.read_offset(offset_size).map_err(|err| {
269 Error::InvalidData(format!(
270 "dataset '{}' has invalid DIMENSION_LIST heap address: {err}",
271 ds.name()
272 ))
273 })?;
274 let heap_idx = cursor.read_u32_le().map_err(|err| {
275 Error::InvalidData(format!(
276 "dataset '{}' has invalid DIMENSION_LIST heap index: {err}",
277 ds.name()
278 ))
279 })? as u16;
280
281 if seq_len == 0 || hdf5_reader::io::Cursor::is_undefined_offset(heap_addr, offset_size) {
282 return Err(Error::InvalidData(format!(
283 "dataset '{}' has an unresolved DIMENSION_LIST reference",
284 ds.name()
285 )));
286 }
287
288 let collection = hdf5_reader::global_heap::GlobalHeapCollection::parse_at_storage(
289 group.storage(),
290 heap_addr,
291 offset_size,
292 group.length_size(),
293 )
294 .map_err(|err| {
295 Error::InvalidData(format!(
296 "dataset '{}' has unreadable DIMENSION_LIST heap object: {err}",
297 ds.name()
298 ))
299 })?;
300
301 let heap_obj = collection.get_object(heap_idx).ok_or_else(|| {
302 Error::InvalidData(format!(
303 "dataset '{}' references missing DIMENSION_LIST heap object {}",
304 ds.name(),
305 heap_idx
306 ))
307 })?;
308
309 let refs = hdf5_reader::reference::read_object_references(&heap_obj.data, offset_size)
310 .map_err(|err| {
311 Error::InvalidData(format!(
312 "dataset '{}' has invalid DIMENSION_LIST references: {err}",
313 ds.name()
314 ))
315 })?;
316
317 if refs.is_empty() {
318 return Err(Error::InvalidData(format!(
319 "dataset '{}' has empty DIMENSION_LIST references",
320 ds.name()
321 )));
322 }
323
324 dim_addrs.push(refs[0]);
325 }
326
327 Ok(dim_addrs)
328}
329
330fn compute_storage_sizes(shape: &[u64], elem_size: u64, is_unlimited: bool) -> Result<(u64, u64)> {
331 let total_elements =
332 crate::types::checked_shape_elements(shape, "NetCDF-4 variable element count")?;
333 let data_size = crate::types::checked_mul_u64(
334 total_elements,
335 elem_size,
336 "NetCDF-4 variable size in bytes",
337 )?;
338
339 let record_elements = if is_unlimited && shape.len() > 1 {
340 crate::types::checked_shape_elements(&shape[1..], "NetCDF-4 record element count")?
341 } else {
342 1
343 };
344 let record_size =
345 crate::types::checked_mul_u64(record_elements, elem_size, "NetCDF-4 record size in bytes")?;
346
347 Ok((data_size, record_size))
348}
349
350fn resolve_variable_dimensions_by_size(
353 ds: &hdf5_reader::Dataset,
354 dimensions: &[NcDimension],
355) -> Vec<NcDimension> {
356 let shape = ds.shape();
357
358 let mut var_dims = Vec::with_capacity(shape.len());
361 let mut used = vec![false; dimensions.len()];
362
363 for &dim_size in shape {
364 let mut matched = false;
365 for (i, dim) in dimensions.iter().enumerate() {
366 if !used[i] && dim.size == dim_size {
367 var_dims.push(dim.clone());
368 used[i] = true;
369 matched = true;
370 break;
371 }
372 }
373 if !matched {
374 var_dims.push(NcDimension {
376 name: format!("dim_{}", dim_size),
377 size: dim_size,
378 is_unlimited: false,
379 });
380 }
381 }
382
383 if let Some(max_dims) = ds.max_dims() {
385 for (i, md) in max_dims.iter().enumerate() {
386 if *md == u64::MAX && i < var_dims.len() {
387 var_dims[i].is_unlimited = true;
388 }
389 }
390 }
391
392 var_dims
393}
394
395#[cfg(test)]
396mod tests {
397 use super::compute_storage_sizes;
398
399 #[test]
400 fn compute_storage_sizes_detects_overflow() {
401 let err = compute_storage_sizes(&[u64::MAX, 2], 8, false).unwrap_err();
402 assert!(matches!(err, crate::Error::InvalidData(_)));
403 }
404
405 #[test]
406 fn compute_storage_sizes_record_dims() {
407 let (data_size, record_size) = compute_storage_sizes(&[10, 3, 4], 4, true).unwrap();
408 assert_eq!(data_size, 480);
409 assert_eq!(record_size, 48);
410 }
411}