use std::collections::{BTreeSet, HashMap};
use std::path::Path;
use std::sync::Arc;
use crate::attribute::AttributeMessage;
use crate::chunked_read::ChunkInfo;
use crate::chunked_write::{ChunkMeta, ChunkProvider};
use crate::convert::TryToUsize;
use crate::data_layout::DataLayout;
use crate::datatype::{Datatype, ReferenceType};
use crate::error::{Error, FormatError};
use crate::filter_pipeline::{
FILTER_DEFLATE, FILTER_FLETCHER32, FILTER_LZF, FILTER_SCALEOFFSET, FILTER_SHUFFLE,
FilterPipeline,
};
use crate::libver::LibVer;
use crate::reader::{Dataset, File, Group};
use crate::scaleoffset::{self, ScaleOffset};
use crate::shared_message::DatatypeLocation;
use crate::source::Source;
use crate::type_builders::{
AttrValue, DatasetBuilder, FinishedGroup, GroupBuilder, ObjectRefPatch, ObjectRefTarget,
VlStringElement,
};
use crate::vl_data::{
EmbeddedVlSlot, VlByteObject, VlenStringReadOptions, embedded_vlen_slots,
is_vlen_string_datatype,
};
use crate::writer::FileBuilder;
#[derive(Debug, Default, Clone)]
pub struct RepackOptions {
drop: Vec<String>,
libver_bounds: Option<(LibVer, LibVer)>,
}
impl RepackOptions {
pub fn new() -> Self {
Self::default()
}
pub fn drop_path(mut self, path: &str) -> Self {
self.drop.push(path.to_string());
self
}
pub fn drop_paths(&self) -> &[String] {
&self.drop
}
pub fn with_libver_bounds(mut self, low: LibVer, high: LibVer) -> Self {
self.libver_bounds = Some((low, high));
self
}
pub fn libver_bounds(&self) -> Option<(LibVer, LibVer)> {
self.libver_bounds
}
}
pub fn repack<P: AsRef<Path>, Q: AsRef<Path>>(
src: P,
dst: Q,
options: &RepackOptions,
) -> Result<(), Error> {
let file = Arc::new(File::open_streaming(src)?);
let drop: BTreeSet<String> = options.drop.iter().map(|p| normalize(p)).collect();
let mut matched: BTreeSet<String> = BTreeSet::new();
let mut builder = FileBuilder::new();
if let Some(info) = file.file_space_info() {
builder
.with_file_space_strategy(info.strategy, false, info.threshold)
.with_file_space_page_size(info.page_size);
}
let addr_map = build_object_address_map(&file)?;
let root = file.root();
populate(
&mut builder,
&root,
"",
&drop,
&mut matched,
&file,
&addr_map,
)?;
let (low, high) = match options.libver_bounds {
Some(explicit) => explicit,
None if builder.needs_latest_format() => (LibVer::Earliest, LibVer::WRITER_DEFAULT),
None => (
LibVer::Earliest,
file.libver_bound().max(LibVer::WRITER_OLDEST),
),
};
builder.with_libver_bounds(low, high);
if let Some(missing) = drop.iter().find(|d| !matched.contains(*d)) {
return Err(Error::RepackUnsupported(format!(
"drop path does not exist in the source: {missing}"
)));
}
builder.write(dst)?;
Ok(())
}
trait GroupSink: AttrSink {
fn sink_dataset(&mut self, name: &str) -> &mut DatasetBuilder;
fn sink_add_group(&mut self, group: FinishedGroup);
fn sink_commit_datatype(&mut self, name: &str, datatype: Datatype);
}
trait AttrSink {
fn sink_set_attr(&mut self, name: &str, value: AttrValue);
fn sink_set_attr_verbatim(&mut self, message: AttributeMessage);
fn sink_set_attr_var_len_verbatim(&mut self, message: AttributeMessage, strings: Vec<String>);
}
impl AttrSink for FileBuilder {
fn sink_set_attr(&mut self, name: &str, value: AttrValue) {
self.set_attr(name, value);
}
fn sink_set_attr_verbatim(&mut self, message: AttributeMessage) {
self.set_attr_verbatim(message);
}
fn sink_set_attr_var_len_verbatim(&mut self, message: AttributeMessage, strings: Vec<String>) {
self.set_attr_var_len_verbatim(message, strings);
}
}
impl AttrSink for GroupBuilder {
fn sink_set_attr(&mut self, name: &str, value: AttrValue) {
self.set_attr(name, value);
}
fn sink_set_attr_verbatim(&mut self, message: AttributeMessage) {
self.set_attr_verbatim(message);
}
fn sink_set_attr_var_len_verbatim(&mut self, message: AttributeMessage, strings: Vec<String>) {
self.set_attr_var_len_verbatim(message, strings);
}
}
impl AttrSink for DatasetBuilder {
fn sink_set_attr(&mut self, name: &str, value: AttrValue) {
self.set_attr(name, value);
}
fn sink_set_attr_verbatim(&mut self, message: AttributeMessage) {
self.set_attr_verbatim(message);
}
fn sink_set_attr_var_len_verbatim(&mut self, message: AttributeMessage, strings: Vec<String>) {
self.set_attr_var_len_verbatim(message, strings);
}
}
impl GroupSink for FileBuilder {
fn sink_dataset(&mut self, name: &str) -> &mut DatasetBuilder {
self.create_dataset(name)
}
fn sink_add_group(&mut self, group: FinishedGroup) {
self.add_group(group);
}
fn sink_commit_datatype(&mut self, name: &str, datatype: Datatype) {
self.commit_datatype(name, datatype);
}
}
impl GroupSink for GroupBuilder {
fn sink_dataset(&mut self, name: &str) -> &mut DatasetBuilder {
self.create_dataset(name)
}
fn sink_add_group(&mut self, group: FinishedGroup) {
self.add_group(group);
}
fn sink_commit_datatype(&mut self, name: &str, datatype: Datatype) {
self.commit_datatype(name, datatype);
}
}
fn populate<S: GroupSink>(
sink: &mut S,
src: &Group,
path: &str,
drop: &BTreeSet<String>,
matched: &mut BTreeSet<String>,
file: &Arc<File>,
addr_map: &HashMap<u64, String>,
) -> Result<(), Error> {
let owner = if path.is_empty() {
"root group".to_string()
} else {
format!("group {path}")
};
copy_attrs(
sink,
src.attr_messages()?,
|| src.attrs(),
&owner,
drop,
addr_map,
)?;
for name in src.named_datatypes()? {
let child_path = join(path, &name);
if drop.contains(&child_path) {
matched.insert(child_path);
continue;
}
let (datatype, _) = src.named_datatype_at(&name)?;
check_datatype(&datatype, &format!("committed datatype {child_path}"))?;
sink.sink_commit_datatype(&name, datatype);
}
let mut dataset_names = src.datasets()?;
dataset_names.sort();
for name in dataset_names {
let child_path = join(path, &name);
if drop.contains(&child_path) {
matched.insert(child_path);
continue;
}
let ds = src.dataset(&name)?;
emit_dataset(
sink.sink_dataset(&name),
&ds,
&child_path,
file,
drop,
addr_map,
)?;
}
let mut group_names = src.groups()?;
group_names.sort();
for name in group_names {
let child_path = join(path, &name);
if drop.contains(&child_path) {
matched.insert(child_path);
continue;
}
let child = src.group(&name)?;
let mut gb = GroupBuilder::new(&name);
populate(&mut gb, &child, &child_path, drop, matched, file, addr_map)?;
sink.sink_add_group(gb.finish());
}
Ok(())
}
fn emit_dataset(
db: &mut DatasetBuilder,
ds: &Dataset,
path: &str,
file: &Arc<File>,
drop: &BTreeSet<String>,
addr_map: &HashMap<u64, String>,
) -> Result<(), Error> {
if let Some(address) = ds.committed_datatype_address()? {
let type_path = committed_type_path(address, &format!("dataset {path}"), drop, addr_map)?;
db.with_committed_datatype(&type_path);
}
let datatype = ds.datatype()?;
let dataspace = ds.dataspace()?;
let layout = ds.data_layout()?;
let pipeline = ds.filter_pipeline_parsed();
check_datatype(&datatype, &format!("dataset {path}"))?;
check_layout(&layout, path)?;
let dims = dataspace.dimensions.clone();
let n_elements: u64 = dims.iter().product();
let vlen_slots = embedded_vlen_slots(&datatype).ok_or_else(|| {
Error::RepackUnsupported(format!(
"dataset {path}: datatype declares variable-length members its own element \
size cannot hold"
))
})?;
let reference_slots = embedded_reference_slots(&datatype).ok_or_else(|| {
Error::RepackUnsupported(format!(
"dataset {path}: datatype declares object references its own element size cannot hold"
))
})?;
let fill = ds.defined_fill_bytes()?;
if fill.is_some() && !(vlen_slots.is_empty() && reference_slots.is_empty()) {
return Err(Error::RepackUnsupported(format!(
"dataset {path}: a fill value on a variable-length or object-reference \
dataset cannot be repacked faithfully"
)));
}
if is_vlen_string_datatype(&datatype) {
emit_vlen_string_dataset(db, ds, path, &datatype, &dims, &layout, &pipeline)?;
copy_dataset_attrs(db, ds, path, drop, addr_map)?;
return Ok(());
}
if is_nonstring_vlen(&datatype) {
emit_vlen_sequence_dataset(db, ds, path, &datatype, &dims, &layout, &pipeline)?;
copy_dataset_attrs(db, ds, path, drop, addr_map)?;
return Ok(());
}
if is_object_reference(&datatype) {
emit_object_reference_dataset(db, ds, path, &dims, &layout, file, drop, addr_map)?;
copy_dataset_attrs(db, ds, path, drop, addr_map)?;
return Ok(());
}
if !vlen_slots.is_empty() || !reference_slots.is_empty() {
emit_embedded_address_dataset(
db,
ds,
path,
&datatype,
&dims,
&layout,
&pipeline,
&vlen_slots,
&reference_slots,
file,
drop,
addr_map,
)?;
copy_dataset_attrs(db, ds, path, drop, addr_map)?;
return Ok(());
}
db.fill = fill;
if let DataLayout::Chunked {
chunk_dimensions, ..
} = &layout
&& n_elements > 0
{
let rank = dims.len();
let chunk_dims: Vec<u64> = chunk_dimensions
.iter()
.take(rank)
.map(|&c| c as u64)
.collect();
if let Some(DenseChunkPlan { meta, grid_order }) =
try_plan_dense_chunks(ds, &dims, &chunk_dims)?
{
let maxshape = dataspace
.max_dimensions
.as_ref()
.filter(|ms| *ms != &dims)
.map(|ms| ms.as_slice());
let elem_size = datatype.type_size() as usize;
let provider = DatasetChunkProvider {
file: Arc::clone(file),
grid_order,
};
db.with_raw_chunks_lazy(
datatype,
&dims,
maxshape,
&chunk_dims,
elem_size,
ds.filter_pipeline_message_bytes(),
meta,
Box::new(provider),
);
copy_dataset_attrs(db, ds, path, drop, addr_map)?;
return Ok(());
}
}
check_pipeline(pipeline.as_ref(), path)?;
if n_elements == 0 {
db.with_dtype(datatype).with_shape(&dims);
} else {
let raw = ds.read_raw()?;
db.with_raw_data(datatype, raw, n_elements)
.with_shape(&dims);
}
carry_shape_and_pipeline(
db,
&dims,
dataspace.max_dimensions.as_deref(),
&layout,
&pipeline,
);
copy_dataset_attrs(db, ds, path, drop, addr_map)?;
Ok(())
}
fn copy_dataset_attrs(
db: &mut DatasetBuilder,
ds: &Dataset,
path: &str,
drop: &BTreeSet<String>,
addr_map: &HashMap<u64, String>,
) -> Result<(), Error> {
copy_attrs(
db,
ds.attr_messages()?,
|| ds.attrs(),
&format!("dataset {path}"),
drop,
addr_map,
)
}
fn carry_shape_and_pipeline(
db: &mut DatasetBuilder,
dims: &[u64],
max_dimensions: Option<&[u64]>,
layout: &DataLayout,
pipeline: &Option<FilterPipeline>,
) {
if let Some(maxshape) = max_dimensions
&& maxshape != dims
{
db.with_maxshape(maxshape);
}
if let DataLayout::Chunked {
chunk_dimensions, ..
} = layout
{
let rank = dims.len();
let logical: Vec<u64> = chunk_dimensions
.iter()
.take(rank)
.map(|&c| c as u64)
.collect();
db.with_chunks(&logical);
}
if let Some(p) = pipeline {
for f in &p.filters {
match f.filter_id {
FILTER_SHUFFLE => {
db.with_shuffle();
}
FILTER_FLETCHER32 => {
db.with_fletcher32();
}
FILTER_DEFLATE => {
db.with_deflate(f.client_data.first().copied().unwrap_or(6));
}
FILTER_LZF => {
db.with_lzf();
}
FILTER_SCALEOFFSET => {
if let Some(mode @ ScaleOffset::Integer(_)) =
scaleoffset::scale_offset_mode(&f.client_data)
{
db.with_scale_offset(mode);
} else {
unreachable!("check_pipeline rejected non-integer scale-offset");
}
}
_ => unreachable!("check_pipeline rejected unsupported filters"),
}
}
}
}
fn emit_vlen_string_dataset(
db: &mut DatasetBuilder,
ds: &Dataset,
path: &str,
datatype: &Datatype,
dims: &[u64],
layout: &DataLayout,
pipeline: &Option<FilterPipeline>,
) -> Result<(), Error> {
check_pipeline(pipeline.as_ref(), path)?;
let objects = ds.read_vlen_string_bytes(VlenStringReadOptions::default())?;
let elements: Vec<VlStringElement> = objects
.into_iter()
.map(|o| match o {
VlByteObject::Null => VlStringElement::Null,
VlByteObject::Bytes(bytes) => VlStringElement::Bytes(bytes),
})
.collect();
db.with_vlen_string_elements(datatype.clone(), &elements)
.map_err(Error::Format)?;
db.with_shape(dims);
carry_shape_and_pipeline(
db,
dims,
ds.dataspace()?.max_dimensions.as_deref(),
layout,
pipeline,
);
Ok(())
}
fn emit_vlen_sequence_dataset(
db: &mut DatasetBuilder,
ds: &Dataset,
path: &str,
datatype: &Datatype,
dims: &[u64],
layout: &DataLayout,
pipeline: &Option<FilterPipeline>,
) -> Result<(), Error> {
check_pipeline(pipeline.as_ref(), path)?;
let (objects, _element_size) = ds.read_vlen_sequence_bytes(VlenStringReadOptions::default())?;
let elements: Vec<VlStringElement> = objects
.into_iter()
.map(|o| match o {
VlByteObject::Null => VlStringElement::Null,
VlByteObject::Bytes(bytes) => VlStringElement::Bytes(bytes),
})
.collect();
db.with_vlen_sequence_elements(datatype.clone(), &elements)
.map_err(Error::Format)?;
db.with_shape(dims);
carry_shape_and_pipeline(
db,
dims,
ds.dataspace()?.max_dimensions.as_deref(),
layout,
pipeline,
);
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn emit_embedded_address_dataset(
db: &mut DatasetBuilder,
ds: &Dataset,
path: &str,
datatype: &Datatype,
dims: &[u64],
layout: &DataLayout,
pipeline: &Option<FilterPipeline>,
vlen_slots: &[EmbeddedVlSlot],
reference_slots: &[usize],
file: &Arc<File>,
drop: &BTreeSet<String>,
addr_map: &HashMap<u64, String>,
) -> Result<(), Error> {
check_pipeline(pipeline.as_ref(), path)?;
if !reference_slots.is_empty() {
check_embedded_reference_layout(ds, path, dims, layout, file)?;
}
let n_elements: u64 = dims.iter().product();
let (raw, vl_offsets, vl_elements) = if vlen_slots.is_empty() {
let raw = if n_elements == 0 {
Vec::new()
} else {
ds.read_raw()?
};
(raw, Vec::new(), Vec::new())
} else {
let data = ds.read_embedded_vlen_bytes(vlen_slots, VlenStringReadOptions::default())?;
let elements: Vec<VlStringElement> = data
.objects
.into_iter()
.map(|o| match o {
VlByteObject::Null => VlStringElement::Null,
VlByteObject::Bytes(bytes) => VlStringElement::Bytes(bytes),
})
.collect();
(data.raw, data.offsets, elements)
};
let reference_patches =
resolve_embedded_references(&raw, datatype, dims, path, drop, addr_map, reference_slots)?;
if vlen_slots.is_empty() {
db.with_embedded_object_references(datatype.clone(), raw, n_elements, reference_patches);
} else {
db.with_embedded_vlen_elements(
datatype.clone(),
raw,
n_elements,
&vl_offsets,
&vl_elements,
);
if !reference_patches.is_empty() {
db.reference_targets = Some(reference_patches);
}
}
db.with_shape(dims);
carry_shape_and_pipeline(
db,
dims,
ds.dataspace()?.max_dimensions.as_deref(),
layout,
pipeline,
);
Ok(())
}
fn resolve_embedded_references(
raw: &[u8],
datatype: &Datatype,
dims: &[u64],
path: &str,
drop: &BTreeSet<String>,
addr_map: &HashMap<u64, String>,
slots: &[usize],
) -> Result<Vec<ObjectRefPatch>, Error> {
if slots.is_empty() {
return Ok(Vec::new());
}
let stride = datatype.type_size() as usize;
let n_elements: usize = dims.iter().product::<u64>().to_usize()?;
let needed = n_elements
.checked_mul(stride)
.ok_or(FormatError::OffsetOverflow {
offset: n_elements as u64,
length: stride as u64,
})?;
if raw.len() < needed {
return Err(FormatError::UnexpectedEof {
expected: needed,
available: raw.len(),
}
.into());
}
let mut patches = Vec::with_capacity(n_elements * slots.len());
for e in 0..n_elements {
for &slot in slots {
let at = e * stride + slot;
let v = u64::from_le_bytes(
raw[at..at + 8]
.try_into()
.expect("slot offsets leave 8 bytes inside the element"),
);
patches.push(ObjectRefPatch {
byte_offset: at,
target: resolve_reference_address(v, path, drop, addr_map)?,
});
}
}
Ok(patches)
}
struct DatasetChunkProvider {
file: Arc<File>,
grid_order: Vec<ChunkInfo>,
}
impl ChunkProvider for DatasetChunkProvider {
fn chunk_bytes(&self, index: usize, out: &mut Vec<u8>) -> Result<(), FormatError> {
let info = &self.grid_order[index];
let source = self.file.source();
let len = info.chunk_size as usize;
let end = info
.address
.checked_add(len as u64)
.ok_or(FormatError::OffsetOverflow {
offset: info.address,
length: len as u64,
})?;
if end > source.len() {
return Err(FormatError::UnexpectedEof {
expected: end.to_usize().unwrap_or(usize::MAX),
available: source.len().to_usize().unwrap_or(usize::MAX),
});
}
let start = out.len();
out.resize(start + len, 0);
source.read_at(info.address, &mut out[start..])
}
}
struct DenseChunkPlan {
meta: Vec<ChunkMeta>,
grid_order: Vec<ChunkInfo>,
}
fn try_plan_dense_chunks(
ds: &Dataset,
dims: &[u64],
chunk_dims: &[u64],
) -> Result<Option<DenseChunkPlan>, Error> {
let Some(grid) = crate::chunked_read::plan_dense_grid(ds.raw_chunks()?, dims, chunk_dims)
else {
return Ok(None);
};
let grid_order = grid.grid_order;
let meta = grid_order
.iter()
.map(|info| ChunkMeta {
compressed_size: u64::from(info.chunk_size),
filter_mask: info.filter_mask,
})
.collect();
Ok(Some(DenseChunkPlan { meta, grid_order }))
}
fn attr_bytes_are_position_independent(dt: &Datatype) -> bool {
match dt {
Datatype::FixedPoint { .. }
| Datatype::FloatingPoint { .. }
| Datatype::Time { .. }
| Datatype::String { .. }
| Datatype::BitField { .. }
| Datatype::Opaque { .. } => true,
Datatype::Compound { members, .. } => members
.iter()
.all(|m| attr_bytes_are_position_independent(&m.datatype)),
Datatype::Enumeration { base_type, .. } | Datatype::Array { base_type, .. } => {
attr_bytes_are_position_independent(base_type)
}
Datatype::VariableLength { .. } | Datatype::Reference { .. } => false,
}
}
fn copy_attrs<S, F>(
sink: &mut S,
mut messages: Vec<AttributeMessage>,
decode: F,
owner: &str,
drop: &BTreeSet<String>,
addr_map: &HashMap<u64, String>,
) -> Result<(), Error>
where
S: AttrSink + ?Sized,
F: FnOnce() -> Result<std::collections::HashMap<String, AttrValue>, Error>,
{
messages.sort_by(|a, b| a.name.cmp(&b.name));
for message in &mut messages {
let DatatypeLocation::Committed(address) = message.datatype_location else {
continue;
};
let name = &message.name;
let type_path = committed_type_path(
address,
&format!("{owner} attribute {name:?}"),
drop,
addr_map,
)?;
message.datatype_location = DatatypeLocation::CommittedPath(type_path);
}
let any_needs_decoding = messages
.iter()
.any(|m| !attr_bytes_are_position_independent(&m.datatype));
let decoded = if any_needs_decoding {
decode()?
} else {
std::collections::HashMap::new()
};
for message in messages {
if attr_bytes_are_position_independent(&message.datatype) {
sink.sink_set_attr_verbatim(message);
} else if let Some(value) = decoded.get(&message.name) {
match (&message.datatype, value.as_strings()) {
(Datatype::VariableLength { .. }, Some(strings)) => {
let strings = strings.to_vec();
sink.sink_set_attr_var_len_verbatim(message, strings);
}
_ => sink.sink_set_attr(&message.name, value.clone()),
}
} else {
let name = &message.name;
return Err(Error::RepackUnsupported(format!(
"{owner}: attribute {name:?} has a datatype that cannot be repacked faithfully yet"
)));
}
}
Ok(())
}
fn check_datatype(dt: &Datatype, owner: &str) -> Result<(), Error> {
let bad = |what: &str| {
Err(Error::RepackUnsupported(format!(
"{owner}: {what} datatype cannot be repacked faithfully yet"
)))
};
match dt {
Datatype::FixedPoint { .. }
| Datatype::FloatingPoint { .. }
| Datatype::Time { .. }
| Datatype::String { .. }
| Datatype::BitField { .. }
| Datatype::Opaque { .. } => Ok(()),
Datatype::VariableLength { .. } if is_vlen_string_datatype(dt) => Ok(()),
Datatype::VariableLength { base_type, .. } => check_vlen_base_type(base_type, owner),
Datatype::Reference {
ref_type: ReferenceType::Object,
size: 8,
} => Ok(()),
Datatype::Reference {
ref_type: ReferenceType::Object,
..
} => bad("non-8-byte object reference"),
Datatype::Reference {
ref_type: ReferenceType::DatasetRegion,
..
} => bad("dataset-region reference"),
Datatype::Compound { members, .. } => {
for m in members {
check_datatype(&m.datatype, owner)?;
}
Ok(())
}
Datatype::Enumeration { base_type, .. } => check_datatype(base_type, owner),
Datatype::Array { base_type, .. } => check_datatype(base_type, owner),
}
}
fn is_nonstring_vlen(dt: &Datatype) -> bool {
matches!(dt, Datatype::VariableLength { .. }) && !is_vlen_string_datatype(dt)
}
fn check_vlen_base_type(dt: &Datatype, owner: &str) -> Result<(), Error> {
let bad = |what: &str| {
Err(Error::RepackUnsupported(format!(
"{owner}: variable-length sequence of {what} cannot be repacked faithfully yet"
)))
};
match dt {
Datatype::FixedPoint { .. }
| Datatype::FloatingPoint { .. }
| Datatype::Time { .. }
| Datatype::String { .. }
| Datatype::BitField { .. }
| Datatype::Opaque { .. } => Ok(()),
Datatype::Reference { .. } => bad("references"),
Datatype::VariableLength { .. } => bad("variable-length elements"),
Datatype::Compound { members, .. } => {
for m in members {
check_vlen_base_type(&m.datatype, owner)?;
}
Ok(())
}
Datatype::Enumeration { base_type, .. } => check_vlen_base_type(base_type, owner),
Datatype::Array { base_type, .. } => check_vlen_base_type(base_type, owner),
}
}
fn is_object_reference(dt: &Datatype) -> bool {
matches!(
dt,
Datatype::Reference {
ref_type: ReferenceType::Object,
..
}
)
}
fn is_dropped(path: &str, drop: &BTreeSet<String>) -> bool {
if drop.contains(path) {
return true;
}
let mut p = path;
while let Some(idx) = p.rfind('/') {
p = &p[..idx];
if drop.contains(p) {
return true;
}
}
false
}
fn build_object_address_map(file: &File) -> Result<HashMap<u64, String>, Error> {
let mut map = HashMap::new();
let root = file.root();
map.insert(root.header_address(), String::new());
collect_addresses(&root, "", &mut map)?;
Ok(map)
}
fn collect_addresses(
group: &Group,
prefix: &str,
map: &mut HashMap<u64, String>,
) -> Result<(), Error> {
for (name, ds) in group.iter_datasets()? {
map.insert(ds.header_address(), join(prefix, &name));
}
for name in group.named_datatypes()? {
let (_, address) = group.named_datatype_at(&name)?;
map.insert(address, join(prefix, &name));
}
for (name, child) in group.iter_groups()? {
let child_path = join(prefix, &name);
map.insert(child.header_address(), child_path.clone());
collect_addresses(&child, &child_path, map)?;
}
Ok(())
}
fn committed_type_path(
address: u64,
user: &str,
drop: &BTreeSet<String>,
addr_map: &HashMap<u64, String>,
) -> Result<String, Error> {
let path = addr_map.get(&address).ok_or_else(|| {
Error::RepackUnsupported(format!(
"{user}: names a committed datatype that is not reachable by a hard link in the \
source, so it has no place in the output"
))
})?;
if is_dropped(path, drop) {
return Err(Error::RepackUnsupported(format!(
"{user}: names the committed datatype {path:?}, which this repack drops"
)));
}
Ok(path.clone())
}
#[allow(clippy::too_many_arguments)]
fn emit_object_reference_dataset(
db: &mut DatasetBuilder,
ds: &Dataset,
path: &str,
dims: &[u64],
layout: &DataLayout,
file: &Arc<File>,
drop: &BTreeSet<String>,
addr_map: &HashMap<u64, String>,
) -> Result<(), Error> {
if matches!(layout, DataLayout::Chunked { .. }) {
return Err(Error::RepackUnsupported(format!(
"dataset {path}: chunked or filtered object-reference datasets cannot be repacked \
(their addresses live inside compressed chunks and would need rewriting in place)"
)));
}
if let Some(maxshape) = &ds.dataspace()?.max_dimensions
&& maxshape != dims
{
return Err(Error::RepackUnsupported(format!(
"dataset {path}: resizable object-reference datasets cannot be repacked"
)));
}
if file.base_address() != 0 {
return Err(Error::RepackUnsupported(format!(
"dataset {path}: object references in a file with a non-zero base address (userblock) \
cannot be repacked yet"
)));
}
let n_elements: usize = dims.iter().product::<u64>().to_usize()?;
let targets = if n_elements == 0 {
Vec::new()
} else {
let raw = ds.read_raw()?;
let needed = n_elements
.checked_mul(8)
.ok_or(FormatError::OffsetOverflow {
offset: n_elements as u64,
length: 8,
})?;
if raw.len() < needed {
return Err(FormatError::UnexpectedEof {
expected: needed,
available: raw.len(),
}
.into());
}
let mut targets = Vec::with_capacity(n_elements);
for chunk in raw[..needed].chunks_exact(8) {
let v = u64::from_le_bytes(chunk.try_into().expect("chunks_exact(8) yields 8 bytes"));
targets.push(resolve_reference_address(v, path, drop, addr_map)?);
}
targets
};
db.with_object_references(targets);
db.with_shape(dims);
Ok(())
}
fn check_embedded_reference_layout(
ds: &Dataset,
path: &str,
dims: &[u64],
layout: &DataLayout,
file: &Arc<File>,
) -> Result<(), Error> {
if matches!(layout, DataLayout::Chunked { .. }) {
return Err(Error::RepackUnsupported(format!(
"dataset {path}: chunked or filtered datasets with an object-reference member cannot \
be repacked (their addresses live inside compressed chunks and would need rewriting \
in place)"
)));
}
if let Some(maxshape) = &ds.dataspace()?.max_dimensions
&& maxshape != dims
{
return Err(Error::RepackUnsupported(format!(
"dataset {path}: resizable datasets with an object-reference member cannot be repacked"
)));
}
if file.base_address() != 0 {
return Err(Error::RepackUnsupported(format!(
"dataset {path}: object references in a file with a non-zero base address (userblock) \
cannot be repacked yet"
)));
}
Ok(())
}
fn resolve_reference_address(
address: u64,
path: &str,
drop: &BTreeSet<String>,
addr_map: &HashMap<u64, String>,
) -> Result<ObjectRefTarget, Error> {
if address == 0 || address == u64::MAX {
return Ok(ObjectRefTarget::Raw(address));
}
match addr_map.get(&address) {
Some(target_path) if is_dropped(target_path, drop) => {
Err(Error::RepackUnsupported(format!(
"dataset {path}: object reference to dropped object {target_path:?} cannot be repacked"
)))
}
Some(target_path) => Ok(ObjectRefTarget::Path(target_path.clone())),
None => Err(Error::RepackUnsupported(format!(
"dataset {path}: object reference to address {address:#x} resolves to no hard-linked \
object in the source (dangling, or a region target not supported yet)"
))),
}
}
fn embedded_reference_slots(datatype: &Datatype) -> Option<Vec<usize>> {
fn collect(datatype: &Datatype, base: usize, capacity: usize, out: &mut Vec<usize>) -> bool {
if out.len() > capacity {
return true;
}
match datatype {
Datatype::Reference {
ref_type: ReferenceType::Object,
size: 8,
} => {
out.push(base);
true
}
Datatype::Compound { members, .. } => {
for m in members {
let Some(at) = usize::try_from(m.byte_offset)
.ok()
.and_then(|off| base.checked_add(off))
else {
return false;
};
if !collect(&m.datatype, at, capacity, out) {
return false;
}
}
true
}
Datatype::Array {
base_type,
dimensions,
} => {
let mut probe = Vec::new();
if !collect(base_type, 0, capacity, &mut probe) {
return false;
}
if probe.is_empty() {
return true;
}
let count = dimensions
.iter()
.copied()
.fold(1u64, |a, b| a.saturating_mul(u64::from(b)));
if count > capacity as u64 {
return false;
}
let entries = usize::try_from(count).unwrap_or(usize::MAX);
let stride = base_type.type_size() as usize;
for i in 0..entries {
let Some(at) = i.checked_mul(stride).and_then(|off| base.checked_add(off))
else {
return false;
};
for &slot in &probe {
let Some(off) = at.checked_add(slot) else {
return false;
};
out.push(off);
if out.len() > capacity {
return true;
}
}
}
true
}
_ => true,
}
}
let element_size = datatype.type_size() as usize;
let capacity = element_size / 8;
let mut slots = Vec::new();
if !collect(datatype, 0, capacity, &mut slots) {
return None;
}
if slots.len() > capacity
|| slots
.iter()
.any(|&s| s.checked_add(8).is_none_or(|end| end > element_size))
{
return None;
}
Some(slots)
}
fn check_layout(layout: &DataLayout, path: &str) -> Result<(), Error> {
match layout {
DataLayout::Compact { .. } | DataLayout::Contiguous { .. } | DataLayout::Chunked { .. } => {
Ok(())
}
DataLayout::Virtual { .. } => Err(Error::RepackUnsupported(format!(
"dataset {path}: virtual data layout cannot be repacked"
))),
}
}
fn check_pipeline(pipeline: Option<&FilterPipeline>, path: &str) -> Result<(), Error> {
let Some(p) = pipeline else {
return Ok(());
};
let has = |id| p.filters.iter().any(|f| f.filter_id == id);
if has(FILTER_LZF) && has(FILTER_DEFLATE) {
return Err(Error::RepackUnsupported(format!(
"dataset {path}: an lzf + deflate pipeline cannot be re-encoded faithfully"
)));
}
for f in &p.filters {
match f.filter_id {
FILTER_DEFLATE | FILTER_SHUFFLE | FILTER_FLETCHER32 | FILTER_LZF => {}
FILTER_SCALEOFFSET => match scaleoffset::scale_offset_mode(&f.client_data) {
Some(ScaleOffset::Integer(_)) => {}
_ => {
return Err(Error::RepackUnsupported(format!(
"dataset {path}: only lossless integer scale-offset with an undefined fill value can be repacked faithfully"
)));
}
},
other => {
return Err(Error::RepackUnsupported(format!(
"dataset {path}: filter id {other} cannot be repacked yet"
)));
}
}
}
Ok(())
}
fn normalize(path: &str) -> String {
path.split('/')
.filter(|c| !c.is_empty())
.collect::<Vec<_>>()
.join("/")
}
fn join(parent: &str, name: &str) -> String {
if parent.is_empty() {
name.to_string()
} else {
format!("{parent}/{name}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lzf_plus_deflate_pipeline_is_refused() {
use crate::filter_pipeline::FilterDescription;
let pipeline = FilterPipeline {
version: 2,
filters: vec![
FilterDescription {
filter_id: FILTER_LZF,
name: Some("lzf".into()),
flags: 0,
client_data: vec![],
},
FilterDescription {
filter_id: FILTER_DEFLATE,
name: None,
flags: 0,
client_data: vec![6],
},
],
};
let err = check_pipeline(Some(&pipeline), "d").unwrap_err();
assert!(
matches!(&err, Error::RepackUnsupported(msg) if msg.contains("lzf + deflate")),
"unexpected error: {err:?}"
);
}
#[test]
fn repack_preserves_big_endian_time_dataset() {
use crate::datatype::{Datatype, DatatypeByteOrder};
use crate::reader::File;
use crate::writer::FileBuilder;
let dir = std::env::temp_dir();
let src = dir.join("hdf5_pure_repack_time_src.h5");
let dst = dir.join("hdf5_pure_repack_time_dst.h5");
let dt = Datatype::Time {
size: 4,
byte_order: DatatypeByteOrder::BigEndian,
bit_precision: 32,
};
let raw: Vec<u8> = vec![
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03,
];
{
let mut b = FileBuilder::new();
b.create_dataset("t")
.with_raw_data(dt.clone(), raw.clone(), 3)
.with_shape(&[3]);
b.write(&src).unwrap();
}
repack(&src, &dst, &RepackOptions::new()).unwrap();
let f = File::open(&dst).unwrap();
let ds = f.dataset("t").unwrap();
assert_eq!(
ds.datatype().unwrap(),
dt,
"time datatype incl. byte order must survive repack"
);
assert_eq!(
ds.read_raw().unwrap(),
raw,
"time element bytes must be preserved"
);
std::fs::remove_file(&src).ok();
std::fs::remove_file(&dst).ok();
}
#[test]
fn is_dropped_matches_self_and_ancestors() {
let drop: BTreeSet<String> = ["g/old", "lone"].iter().map(|s| s.to_string()).collect();
assert!(is_dropped("lone", &drop));
assert!(is_dropped("g/old", &drop));
assert!(is_dropped("g/old/child", &drop));
assert!(is_dropped("g/old/a/b", &drop));
assert!(!is_dropped("g", &drop));
assert!(!is_dropped("g/older", &drop));
assert!(!is_dropped("lonely", &drop));
assert!(!is_dropped("other/old", &drop));
}
}
#[cfg(test)]
mod attribute_fidelity_tests {
use super::*;
use crate::dataspace::{Dataspace, DataspaceType};
use crate::datatype::{
CharacterSet, CompoundMember, DatatypeByteOrder, ReferenceType, StringPadding,
};
use crate::{File, FileBuilder, RepackOptions};
use std::collections::BTreeMap;
fn i32_type() -> Datatype {
Datatype::FixedPoint {
size: 4,
byte_order: DatatypeByteOrder::LittleEndian,
signed: true,
bit_offset: 0,
bit_precision: 32,
}
}
fn messages_at(path: &str, file: &Path) -> BTreeMap<String, AttributeMessage> {
let f = File::open(file).unwrap();
let messages = if path.is_empty() {
f.root().attr_messages().unwrap()
} else if let Ok(ds) = f.dataset(path) {
ds.attr_messages().unwrap()
} else {
f.group(path).unwrap().attr_messages().unwrap()
};
messages.into_iter().map(|m| (m.name.clone(), m)).collect()
}
#[test]
fn a_position_independent_attribute_crosses_a_repack_unchanged() {
let dir = tempfile::tempdir().unwrap();
let (src, dst) = (dir.path().join("src.h5"), dir.path().join("dst.h5"));
let attrs = || {
[
("i32", AttrValue::I32(-7)),
("u32", AttrValue::U32(4_294_967_295)),
("ascii", AttrValue::AsciiString("m/s".into())),
("utf8", AttrValue::String("µm".into())),
("one_elem", AttrValue::I64Array(vec![9])),
("scalar", AttrValue::I64(9)),
("f64s", AttrValue::F64Array(vec![1.5, -2.5])),
]
};
let mut b = FileBuilder::new();
for (name, value) in attrs() {
b.set_attr(name, value);
}
let ds = b.create_dataset("data").with_f64_data(&[1.0, 2.0]);
for (name, value) in attrs() {
ds.set_attr(name, value);
}
let mut g = b.create_group("grp");
for (name, value) in attrs() {
g.set_attr(name, value);
}
g.create_dataset("inner").with_i32_data(&[1]);
b.add_group(g.finish());
b.write(&src).unwrap();
repack(&src, &dst, &RepackOptions::new()).unwrap();
for owner in ["", "data", "grp"] {
let before = messages_at(owner, &src);
let after = messages_at(owner, &dst);
assert_eq!(
before.keys().collect::<Vec<_>>(),
after.keys().collect::<Vec<_>>(),
"repack changed which attributes {owner:?} has"
);
for (name, source_message) in &before {
assert_eq!(
after.get(name),
Some(source_message),
"attribute {name:?} on {owner:?} was re-encoded rather than copied"
);
}
}
}
#[test]
fn an_encoding_this_crate_has_no_attr_value_for_still_crosses_a_repack() {
let dir = tempfile::tempdir().unwrap();
let (src, dst) = (dir.path().join("src.h5"), dir.path().join("dst.h5"));
let exotic = [
AttributeMessage {
name: "units".into(),
datatype: Datatype::String {
size: 16,
padding: StringPadding::NullTerminate,
charset: CharacterSet::Ascii,
},
dataspace: Dataspace {
space_type: DataspaceType::Scalar,
rank: 0,
dimensions: vec![],
max_dimensions: None,
},
raw_data: {
let mut v = b"m/s".to_vec();
v.resize(16, 0);
v
},
datatype_location: crate::shared_message::DatatypeLocation::Inline,
},
AttributeMessage {
name: "spaced".into(),
datatype: Datatype::String {
size: 8,
padding: StringPadding::SpacePad,
charset: CharacterSet::Utf8,
},
dataspace: Dataspace {
space_type: DataspaceType::Scalar,
rank: 0,
dimensions: vec![],
max_dimensions: None,
},
raw_data: b"ab ".to_vec(),
datatype_location: crate::shared_message::DatatypeLocation::Inline,
},
AttributeMessage {
name: "nothing".into(),
datatype: i32_type(),
dataspace: Dataspace {
space_type: DataspaceType::Null,
rank: 0,
dimensions: vec![],
max_dimensions: None,
},
raw_data: vec![],
datatype_location: crate::shared_message::DatatypeLocation::Inline,
},
AttributeMessage {
name: "grid".into(),
datatype: i32_type(),
dataspace: Dataspace {
space_type: DataspaceType::Simple,
rank: 2,
dimensions: vec![2, 3],
max_dimensions: None,
},
raw_data: (1i32..=6).flat_map(i32::to_le_bytes).collect(),
datatype_location: crate::shared_message::DatatypeLocation::Inline,
},
];
let mut b = FileBuilder::new();
for message in &exotic {
b.set_attr_verbatim(message.clone());
}
let ds = b.create_dataset("data").with_f64_data(&[1.0]);
for message in &exotic {
ds.set_attr_verbatim(message.clone());
}
b.write(&src).unwrap();
repack(&src, &dst, &RepackOptions::new()).unwrap();
for owner in ["", "data"] {
let before = messages_at(owner, &src);
let after = messages_at(owner, &dst);
for message in &exotic {
let name = &message.name;
assert_eq!(
before.get(name),
Some(message),
"the source file did not record {name:?} as written"
);
assert_eq!(
after.get(name),
Some(message),
"attribute {name:?} on {owner:?} changed across the repack"
);
}
}
c_library_reads_every_attribute(&dst, exotic.len());
}
#[cfg(all(not(target_pointer_width = "32"), target_endian = "little"))]
fn c_library_reads_every_attribute(file: &Path, expected: usize) {
let c = hdf5::File::open(file).expect("the C library must open the repacked file");
for names in [
c.attr_names().expect("root attribute names"),
c.dataset("data")
.expect("dataset")
.attr_names()
.expect("dataset attribute names"),
] {
assert_eq!(
names.len(),
expected,
"the C library found {names:?}, not all {expected} attributes"
);
for name in names {
c.attr(&name)
.unwrap_or_else(|e| panic!("the C library could not open {name:?}: {e}"));
}
}
}
#[cfg(not(all(not(target_pointer_width = "32"), target_endian = "little")))]
fn c_library_reads_every_attribute(_file: &Path, _expected: usize) {}
#[test]
fn an_attribute_addressing_the_heap_is_re_encoded_not_copied() {
let dir = tempfile::tempdir().unwrap();
let (src, dst) = (dir.path().join("src.h5"), dir.path().join("dst.h5"));
let fields: Vec<String> = vec!["x".into(), "y".into(), "velocity".into()];
let mut b = FileBuilder::new();
b.create_dataset("bulk").with_f64_data(&vec![0.0; 4096]);
b.set_attr("fields", AttrValue::VarLenAsciiArray(fields.clone()));
b.write(&src).unwrap();
repack(&src, &dst, &RepackOptions::new().drop_path("bulk")).unwrap();
let before = &messages_at("", &src)["fields"];
let after = &messages_at("", &dst)["fields"];
assert_eq!(
after.datatype, before.datatype,
"the attribute must stay variable-length"
);
assert_eq!(after.dataspace, before.dataspace);
assert_ne!(
after.raw_data, before.raw_data,
"a heap reference copied verbatim would point into the source file"
);
assert_eq!(
File::open(&dst).unwrap().root().attrs().unwrap()["fields"],
AttrValue::VarLenAsciiArray(fields),
"and it must still resolve to its own strings"
);
}
#[test]
fn a_nested_address_makes_the_whole_datatype_position_dependent() {
let vlen = Datatype::VariableLength {
is_string: true,
padding: None,
charset: Some(CharacterSet::Ascii),
base_type: Box::new(i32_type()),
};
let reference = Datatype::Reference {
size: 8,
ref_type: ReferenceType::Object,
};
let compound_of = |member: Datatype| Datatype::Compound {
size: 24,
members: vec![
CompoundMember {
name: "plain".into(),
byte_offset: 0,
datatype: i32_type(),
},
CompoundMember {
name: "nested".into(),
byte_offset: 8,
datatype: member,
},
],
};
let array_of = |base: Datatype| Datatype::Array {
base_type: Box::new(base),
dimensions: vec![2, 3],
};
for dependent in [
vlen.clone(),
reference.clone(),
compound_of(vlen.clone()),
compound_of(reference.clone()),
array_of(vlen.clone()),
array_of(reference.clone()),
array_of(compound_of(vlen)),
compound_of(array_of(reference)),
] {
assert!(
!attr_bytes_are_position_independent(&dependent),
"{dependent:?} holds an address and must not be copied verbatim"
);
}
for independent in [
i32_type(),
Datatype::String {
size: 8,
padding: crate::datatype::StringPadding::NullPad,
charset: CharacterSet::Utf8,
},
compound_of(i32_type()),
array_of(i32_type()),
Datatype::Enumeration {
size: 4,
base_type: Box::new(i32_type()),
members: vec![],
},
] {
assert!(
attr_bytes_are_position_independent(&independent),
"{independent:?} holds no address and can be copied verbatim"
);
}
}
}