//! Fill Value messages (0x0005, and the old 0x0004) and applying them on read. //! //! HDF5 allocates storage lazily: a chunk nobody wrote to does not exist in the //! file, and a contiguous dataset nobody wrote to has no data address at all. //! Reading such a region must yield the dataset's *fill value* (zeros unless //! the creator chose otherwise). The readers in [`crate::chunked_read`] leave //! those regions zeroed; [`apply_to_unallocated_chunks`] then overwrites exactly //! the chunk-grid cells that are absent from the chunk index — so it can never //! mistake a stored zero for a hole — and is skipped entirely in the common //! case of a zero fill value. #[cfg(not(feature = "std"))] use alloc::{format, vec, vec::Vec}; use crate::chunked_read::{alloc_output, checked_byte_len, list_chunks}; use crate::data_layout::DataLayout; use crate::dataspace::Dataspace; use crate::error::FormatError; use crate::message_type::MessageType; use crate::object_header::HeaderMessage; /// Largest fill value accepted. A fill value is one element of the dataset's /// datatype; this only bounds the allocation driven by the message's size field. const MAX_FILL_VALUE_SIZE: usize = 1 << 20; /// Parse a Fill Value message, returning the user-defined fill value bytes, or /// `None` when the dataset uses the default (all zeros) or has the fill value /// explicitly undefined. pub fn parse_fill_value(msg: &HeaderMessage) -> Result>, FormatError> { let data = msg.data.as_slice(); let value_at = |pos: usize| -> Result>, FormatError> { let size_bytes = data.get(pos..pos + 4).ok_or(FormatError::UnexpectedEof { expected: pos + 4, available: data.len(), })?; let size = u32::from_le_bytes([size_bytes[0], size_bytes[1], size_bytes[2], size_bytes[3]]) as usize; if size == 0 { return Ok(None); } if size > MAX_FILL_VALUE_SIZE { return Err(FormatError::Overflow(format!( "fill value of {size} bytes exceeds the {MAX_FILL_VALUE_SIZE}-byte limit" ))); } let start = pos + 4; let value = data.get(start..start.saturating_add(size)) .ok_or(FormatError::UnexpectedEof { expected: start.saturating_add(size), available: data.len(), })?; Ok(Some(value.to_vec())) }; match msg.msg_type { // Old fill value message: size(4), value. MessageType::FillValueOld => value_at(0), MessageType::FillValue => { let version = *data.first().ok_or(FormatError::UnexpectedEof { expected: 1, available: 0, })?; match version { // version, alloc time, write time, defined, [size, value] 1 | 2 => { let defined = *data.get(3).ok_or(FormatError::UnexpectedEof { expected: 4, available: data.len(), })?; if version == 2 && defined == 0 { Ok(None) } else if data.len() < 8 && version == 1 { // v1 always carries a size, but tolerate its absence. Ok(None) } else { value_at(4) } } // version, flags (bit 4 = undefined, bit 5 = defined), [size, value] 3 => { let flags = *data.get(1).ok_or(FormatError::UnexpectedEof { expected: 2, available: data.len(), })?; if flags & 0x10 != 0 || flags & 0x20 == 0 { Ok(None) } else { value_at(2) } } v => Err(FormatError::UnsupportedVersion(v)), } } _ => Ok(None), } } /// The fill value that applies to a dataset given its header messages. The new /// message wins over the old one when both are present. pub fn dataset_fill_value(messages: &[HeaderMessage]) -> Result>, FormatError> { for wanted in [MessageType::FillValue, MessageType::FillValueOld] { if let Some(msg) = messages.iter().find(|m| m.msg_type == wanted) { if crate::shared_message::is_shared(msg.flags) { // A shared fill value is legal but vanishingly rare; treat it // as the default rather than misparsing the reference. return Ok(None); } if let Some(value) = parse_fill_value(msg)? { return Ok(Some(value)); } } } Ok(None) } /// `true` when a fill value is absent or all zeros, i.e. identical to what the /// readers already produce for unallocated storage. pub fn is_default(fill: Option<&[u8]>) -> bool { fill.is_none_or(|f| f.iter().all(|&b| b == 0)) } /// A whole dataset's worth of fill value: what reading a dataset with no /// allocated storage at all must return. pub fn filled_dataset( dataspace: &Dataspace, elem_size: usize, fill: Option<&[u8]>, ) -> Result, FormatError> { let total = checked_byte_len(dataspace.checked_num_elements()?, elem_size)?; let mut out = alloc_output(total)?; if let Some(fill) = fill.filter(|f| f.len() == elem_size && !is_default(Some(f))) { for element in out.chunks_exact_mut(elem_size) { element.copy_from_slice(fill); } } Ok(out) } /// Whether the layout has any storage in the file at all. A dataset that was /// created but never written to has none. pub fn has_storage(layout: &DataLayout) -> bool { !matches!( layout, DataLayout::Contiguous { address: None, .. } | DataLayout::Chunked { btree_address: None, .. } ) } /// Run a full-dataset `read`, giving unallocated storage its fill value: a /// dataset with no storage at all reads as entirely fill value (instead of /// failing), and a chunked dataset has the fill value written into every /// chunk the file never allocated. #[allow(clippy::too_many_arguments)] pub fn read_full_with_fill>( messages: &[HeaderMessage], file_data: &[u8], layout: &DataLayout, dataspace: &Dataspace, elem_size: usize, offset_size: u8, length_size: u8, read: impl FnOnce() -> Result, E>, ) -> Result, E> { // A dataset with external raw data also has no data address in this // file. It is NOT unallocated — its values live elsewhere — so it must // never be answered with the fill value. if messages .iter() .any(|m| m.msg_type == MessageType::ExternalDataFiles) { return Err(FormatError::ExternalDataFilesUnsupported.into()); } let fill = dataset_fill_value(messages)?; if !has_storage(layout) { return Ok(filled_dataset(dataspace, elem_size, fill.as_deref())?); } let mut output = read()?; apply_to_unallocated_chunks( &mut output, file_data, layout, dataspace, elem_size, fill.as_deref(), offset_size, length_size, )?; Ok(output) } /// Overwrite, in a fully read chunked dataset `output`, every region whose /// chunk was never allocated with `fill`. No-op for non-chunked layouts, a /// default fill value, or a fill value whose size doesn't match the element. #[allow(clippy::too_many_arguments)] pub fn apply_to_unallocated_chunks( output: &mut [u8], file_data: &[u8], layout: &DataLayout, dataspace: &Dataspace, elem_size: usize, fill: Option<&[u8]>, offset_size: u8, length_size: u8, ) -> Result<(), FormatError> { let Some(fill) = fill.filter(|f| f.len() == elem_size && !is_default(Some(f))) else { return Ok(()); }; if !matches!(layout, DataLayout::Chunked { .. }) || elem_size == 0 { return Ok(()); } let (chunks, chunk_dims) = list_chunks( file_data, layout, dataspace, elem_size, offset_size, length_size, )?; let rank = chunk_dims.len(); let ds_dims: Vec = dataspace.dimensions.iter().map(|&d| d as usize).collect(); if rank == 0 || ds_dims.len() != rank || chunk_dims.contains(&0) { return Ok(()); } // Row-major strides over the dataset and over the chunk grid. let mut ds_strides = vec![1usize; rank]; for i in (0..rank - 1).rev() { ds_strides[i] = ds_strides[i + 1].saturating_mul(ds_dims[i + 1]); } let grid: Vec = ds_dims .iter() .zip(&chunk_dims) .map(|(&d, &c)| d.div_ceil(c)) .collect(); let cells = grid .iter() .try_fold(1usize, |acc, &g| acc.checked_mul(g)) .ok_or_else(|| FormatError::Overflow("chunk grid size overflows".into()))?; if cells == 0 { return Ok(()); } let mut allocated = vec![false; cells]; for chunk in &chunks { // Undefined address: the index has a slot for the chunk but no storage. if chunk.address == u64::MAX || chunk.offsets.len() < rank { continue; } let mut cell = 0usize; let mut in_range = true; for d in 0..rank { let coord = chunk.offsets[d] as usize / chunk_dims[d]; if coord >= grid[d] { in_range = false; break; } cell = cell * grid[d] + coord; } if in_range { allocated[cell] = true; } } let mut coord = vec![0usize; rank]; for (cell, is_allocated) in allocated.iter().enumerate() { if *is_allocated { continue; } // Decode the cell index into grid coordinates. let mut rem = cell; for d in (0..rank).rev() { coord[d] = rem % grid[d]; rem /= grid[d]; } fill_cell( output, &coord, &chunk_dims, &ds_dims, &ds_strides, elem_size, fill, ); } Ok(()) } /// Fill the part of chunk-grid cell `coord` that lies inside the dataset. fn fill_cell( output: &mut [u8], coord: &[usize], chunk_dims: &[usize], ds_dims: &[usize], ds_strides: &[usize], elem_size: usize, fill: &[u8], ) { let rank = coord.len(); let start: Vec = (0..rank).map(|d| coord[d] * chunk_dims[d]).collect(); let end: Vec = (0..rank) .map(|d| (start[d] + chunk_dims[d]).min(ds_dims[d])) .collect(); if (0..rank).any(|d| start[d] >= end[d]) { return; } // Walk every row (all dims but the last) and fill the run along the last. let run = end[rank - 1] - start[rank - 1]; let mut idx = start.clone(); loop { let first: usize = (0..rank).map(|d| idx[d] * ds_strides[d]).sum(); let from = first * elem_size; let to = from + run * elem_size; if let Some(region) = output.get_mut(from..to) { for element in region.chunks_exact_mut(elem_size) { element.copy_from_slice(fill); } } // Advance the odometer over dims 0..rank-1. let mut d = rank - 1; loop { if d == 0 { return; } d -= 1; idx[d] += 1; if idx[d] < end[d] { break; } idx[d] = start[d]; } } } #[cfg(test)] mod tests { use super::*; fn msg(msg_type: MessageType, data: &[u8]) -> HeaderMessage { HeaderMessage { msg_type, size: data.len(), flags: 0, creation_order: None, data: data.to_vec(), } } #[test] fn parses_v3_defined_undefined_and_default() { // Real message for h5py `fillvalue=-1` on an i4 dataset (HDF5 2.0). let defined = msg( MessageType::FillValue, &[3, 0x2b, 4, 0, 0, 0, 0xff, 0xff, 0xff, 0xff], ); assert_eq!(parse_fill_value(&defined).unwrap(), Some(vec![0xff; 4])); let default = msg(MessageType::FillValue, &[3, 0x0a]); assert_eq!(parse_fill_value(&default).unwrap(), None); let undefined = msg(MessageType::FillValue, &[3, 0x19]); assert_eq!(parse_fill_value(&undefined).unwrap(), None); } #[test] fn parses_v2_and_old_messages() { let v2 = msg(MessageType::FillValue, &[2, 2, 2, 1, 2, 0, 0, 0, 7, 0]); assert_eq!(parse_fill_value(&v2).unwrap(), Some(vec![7, 0])); let v2_undefined = msg(MessageType::FillValue, &[2, 2, 2, 0]); assert_eq!(parse_fill_value(&v2_undefined).unwrap(), None); let old = msg(MessageType::FillValueOld, &[2, 0, 0, 0, 9, 9]); assert_eq!(parse_fill_value(&old).unwrap(), Some(vec![9, 9])); } #[test] fn truncated_or_oversized_fill_is_an_error() { let short = msg(MessageType::FillValue, &[3, 0x29, 4, 0, 0, 0, 0xff]); assert!(parse_fill_value(&short).is_err()); let huge = msg(MessageType::FillValue, &[3, 0x29, 0xff, 0xff, 0xff, 0x7f]); assert!(matches!( parse_fill_value(&huge), Err(FormatError::Overflow(_)) )); } #[test] fn fill_cell_clips_edge_chunks_in_2d() { // 3x5 dataset, 2x2 chunks; fill grid cell (1, 2): rows 2..3, cols 4..5. let mut out = vec![0u8; 15]; fill_cell(&mut out, &[1, 2], &[2, 2], &[3, 5], &[5, 1], 1, &[9]); let mut expected = vec![0u8; 15]; expected[2 * 5 + 4] = 9; assert_eq!(out, expected); // Interior cell (0, 1): rows 0..2, cols 2..4. let mut out = vec![0u8; 15]; fill_cell(&mut out, &[0, 1], &[2, 2], &[3, 5], &[5, 1], 1, &[7]); let filled: Vec = out .iter() .enumerate() .filter(|(_, b)| **b == 7) .map(|(i, _)| i) .collect(); assert_eq!(filled, [2, 3, 7, 8]); } }