use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use rust_hdf5::format::messages::MSG_OBJ_HEADER_CONTINUATION;
use rust_hdf5::format::object_header::{ObjectHeader, OCHK_SIGNATURE};
use rust_hdf5::format::superblock::SuperblockV2V3;
use rust_hdf5::{DatatypeMessage, H5File};
fn unique_tmp(label: &str) -> PathBuf {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!(
"rust_hdf5_ochk_write_{}_{}_{}",
label,
std::process::id(),
n
));
std::fs::create_dir_all(&dir).unwrap();
dir.join(format!("{label}.h5"))
}
fn cleanup(path: &PathBuf) {
let _ = std::fs::remove_file(path);
if let Some(dir) = path.parent() {
let _ = std::fs::remove_dir_all(dir);
}
}
fn note(i: u8) -> (DatatypeMessage, Vec<u8>) {
let mut text = vec![b'x'; 256];
text[0] = b'0' + i;
text[255] = 0;
(
DatatypeMessage::FixedString {
size: 256,
padding: 0,
charset: 0,
},
text,
)
}
fn write_notes(path: &PathBuf, count: u8) {
let file = H5File::create(path).unwrap();
file.new_dataset::<i32>()
.shape([8usize])
.create("data")
.unwrap()
.write_raw(&(0..8i32).collect::<Vec<_>>())
.unwrap();
for i in 0..count {
let (dt, text) = note(i);
file.set_attr_typed(&format!("note{i}"), dt, text).unwrap();
}
file.close().unwrap();
}
fn root_chunks(path: &PathBuf) -> (ObjectHeader, Vec<(u64, u64)>) {
let bytes = std::fs::read(path).unwrap();
let superblock = SuperblockV2V3::decode(&bytes).unwrap();
let at = (superblock.base_address + superblock.root_group_object_header_address) as usize;
let (header, _) = ObjectHeader::decode(&bytes[at..]).unwrap();
let chunks = header
.messages
.iter()
.filter(|m| m.msg_type == MSG_OBJ_HEADER_CONTINUATION)
.map(|m| {
(
u64::from_le_bytes(m.data[..8].try_into().unwrap()),
u64::from_le_bytes(m.data[8..16].try_into().unwrap()),
)
})
.collect();
(header, chunks)
}
fn check_notes(path: &PathBuf, count: u8) {
let file = H5File::open(path).unwrap();
let mut names = file.attr_names().unwrap();
names.sort();
assert_eq!(
names,
(0..count).map(|i| format!("note{i}")).collect::<Vec<_>>()
);
for i in 0..count {
let text = file.attr_string(&format!("note{i}")).unwrap();
let expected = String::from_utf8(note(i).1[..255].to_vec()).unwrap();
assert_eq!(text, expected, "note{i}");
}
let data: Vec<i32> = file.dataset("data").unwrap().read_raw().unwrap();
assert_eq!(data, (0..8).collect::<Vec<i32>>());
}
#[test]
fn a_large_root_attribute_set_spills_into_a_continuation_chunk() {
let path = unique_tmp("notes");
write_notes(&path, 6);
let (header, chunks) = root_chunks(&path);
assert_eq!(chunks.len(), 1, "one continuation chunk");
let (addr, len) = chunks[0];
let bytes = std::fs::read(&path).unwrap();
assert_eq!(&bytes[addr as usize..addr as usize + 4], &OCHK_SIGNATURE);
assert!(
!header
.messages
.iter()
.any(|m| m.msg_type == rust_hdf5::format::messages::MSG_ATTRIBUTE),
"chunk 0 still holds an attribute message"
);
assert!(len > 6 * 256, "the continuation holds the attributes");
check_notes(&path, 6);
cleanup(&path);
}
#[test]
fn a_continuation_chunk_follows_chunk_zero_in_the_same_block() {
let path = unique_tmp("contiguous");
write_notes(&path, 6);
let bytes = std::fs::read(&path).unwrap();
let superblock = SuperblockV2V3::decode(&bytes).unwrap();
let root = superblock.base_address + superblock.root_group_object_header_address;
let (_, chunks) = root_chunks(&path);
let (addr, _) = chunks[0];
let (_, chunk0_len) = ObjectHeader::decode(&bytes[root as usize..]).unwrap();
assert_eq!(addr, root + chunk0_len as u64);
cleanup(&path);
}
#[test]
fn a_header_that_fits_its_estimate_stays_one_chunk() {
let path = unique_tmp("fits");
write_notes(&path, 0);
let (header, chunks) = root_chunks(&path);
assert!(chunks.is_empty(), "{:?}", chunks);
assert!(header.messages.iter().any(|m| m.msg_type == 0x06));
cleanup(&path);
}
#[test]
fn a_reopened_file_keeps_the_attributes_its_continuation_chunk_held() {
let path = unique_tmp("reopen");
write_notes(&path, 6);
{
let file = H5File::open_rw(&path).unwrap();
let (dt, text) = note(6);
file.set_attr_typed("note6", dt, text).unwrap();
file.close().unwrap();
}
let (_, chunks) = root_chunks(&path);
assert_eq!(chunks.len(), 1);
check_notes(&path, 7);
cleanup(&path);
}