#include "io/hdf5.h" #include #include #include #include #include #include #include "foundation/fs_utf8.h" namespace joc::io { namespace { // Object header message ids of the HDF5 file format specification (version 3): // 0x02 points at the dense link heap, 0x06 is one link, 0x10 continues the // header in another block and 0x15 points at the dense attribute heap. constexpr std::uint8_t kMsgNil = 0x00; constexpr std::uint8_t kMsgDataspace = 0x01; constexpr std::uint8_t kMsgLinkInfo = 0x02; constexpr std::uint8_t kMsgDatatype = 0x03; constexpr std::uint8_t kMsgLink = 0x06; constexpr std::uint8_t kMsgLayout = 0x08; constexpr std::uint8_t kMsgAttribute = 0x0C; constexpr std::uint8_t kMsgContinuation = 0x10; constexpr std::uint8_t kMsgAttributeInfo = 0x15; constexpr std::uint64_t kUndefinedAddress = 0xFFFFFFFFFFFFFFFFull; constexpr std::uint64_t kMaxReadSize = 1ull << 32; // a dataset larger than this is refused constexpr int kMaxNesting = 32; Status fail(joc_error code, const std::string& message) { return Status::fail(code, stage::kRender, message); } std::uint64_t read_le(const std::uint8_t* data, std::size_t size) { std::uint64_t value = 0; for (std::size_t i = 0; i < size; ++i) { value |= static_cast(data[i]) << (8u * i); } return value; } std::uint64_t align_to_8(std::uint64_t value) { return (value + 7u) & ~std::uint64_t{7u}; } void swap_element_bytes(std::uint8_t* data, std::uint64_t size) { for (std::uint64_t i = 0; i < size / 2; ++i) { const std::uint8_t head = data[i]; data[i] = data[size - 1 - i]; data[size - 1 - i] = head; } } struct Message { std::uint8_t type = kMsgNil; std::uint64_t body = 0; // absolute file offset of the message body std::uint64_t size = 0; }; struct LinkEntry { std::string name; std::uint64_t address = kUndefinedAddress; }; struct AttributeEntry { std::string name; std::uint64_t body = 0; std::uint64_t size = 0; }; // Fractal heap header fields this reader needs; the free-space accounting is // deliberately not modelled because objects are enumerated from the blocks. struct FractalHeap { std::uint64_t id_length = 0; std::uint32_t max_object = 0; std::uint64_t object_count = 0; std::uint64_t huge_count = 0; std::uint64_t tiny_count = 0; std::uint16_t width = 0; std::uint64_t start_block = 0; std::uint64_t max_direct_block = 0; std::uint8_t offset_width = 0; std::uint16_t current_rows = 0; std::uint64_t root_block = kUndefinedAddress; }; struct Layout { bool compact = false; std::uint64_t address = kUndefinedAddress; std::uint64_t size = 0; std::uint64_t inline_offset = 0; }; enum class HeapObjectKind { Link, Attribute }; bool is_string_type(Hdf5Type type) { return type == Hdf5Type::String; } std::string trim_nul(std::string text) { while (!text.empty() && text.back() == '\0') { text.pop_back(); } return text; } // Attribute names are stored with their NUL terminator included in the name size. std::string attribute_name(const std::uint8_t* data, std::size_t size) { const std::size_t length = std::string(reinterpret_cast(data), size).find('\0'); return std::string(reinterpret_cast(data), length == std::string::npos ? size : length); } } // namespace std::uint64_t Hdf5DatasetInfo::element_count() const { std::uint64_t count = 1; for (const std::uint64_t dimension : shape) { count *= dimension; } return count; } struct Hdf5File::Impl { std::string file_path; mutable std::ifstream stream; bool opened = false; std::uint64_t file_size = 0; std::uint8_t offset_size = 8; std::uint8_t length_size = 8; std::uint64_t root_object = kUndefinedAddress; // Parsed metadata is cached by file address / path: the file itself is only // read again when a dataset's payload is requested. mutable std::map> headers; mutable std::map> link_cache; mutable std::map object_cache; Status read_at(std::uint64_t offset, std::uint64_t size, std::vector* out) const { if (out == nullptr) { return fail(JOC_ERR_INVALID_ARGUMENT, "HDF5 read without a destination buffer"); } out->clear(); if (size == 0) { return Status::success(); } if (offset > file_size || size > file_size - offset) { return fail(JOC_ERR_INPUT_FORMAT, "HDF5 structure runs past the end of " + file_path + " (offset " + std::to_string(offset) + ", size " + std::to_string(size) + ")"); } stream.clear(); stream.seekg(static_cast(offset), std::ios::beg); if (!stream.good()) { return fail(JOC_ERR_IO, "cannot seek in " + file_path); } out->resize(static_cast(size)); stream.read(reinterpret_cast(out->data()), static_cast(size)); if (stream.gcount() != static_cast(size)) { return fail(JOC_ERR_IO, "short read in " + file_path); } return Status::success(); } Status read_u64(std::uint64_t offset, std::uint8_t size, std::uint64_t* out) const { std::vector buffer; const Status status = read_at(offset, size, &buffer); if (!status.ok()) { return status; } *out = read_le(buffer.data(), buffer.size()); return Status::success(); } Status parse_messages(const std::uint8_t* data, std::size_t size, std::uint64_t base, bool creation_order, std::vector* out, int depth) const { std::size_t position = 0; while (position + 4u <= size) { const std::uint8_t type = data[position]; const std::uint64_t body_size = read_le(data + position + 1, 2); const std::size_t header_size = 4u + (creation_order ? 2u : 0u); if (position + header_size > size) { // A chunk may end with fewer bytes than a message header needs: // that leftover is padding, not a message. break; } const std::uint64_t body = position + header_size; if (body + body_size > size) { return fail(JOC_ERR_INPUT_FORMAT, "HDF5 object header message overruns its chunk in " + file_path); } if (type == kMsgNil) { if (body_size == 0) { break; } position = static_cast(body + body_size); continue; } if (type == kMsgContinuation) { if (depth >= kMaxNesting) { return fail(JOC_ERR_INPUT_FORMAT, "HDF5 object header continuation nesting in " + file_path); } if (body_size < offset_size + length_size) { return fail(JOC_ERR_INPUT_FORMAT, "malformed HDF5 continuation message in " + file_path); } const std::uint64_t next = read_le(data + body, offset_size); const std::uint64_t length = read_le(data + body + offset_size, length_size); std::vector block; const Status status = read_at(next, length, &block); if (!status.ok()) { return status; } std::size_t start = 0; std::size_t stop = block.size(); if (block.size() >= 8u && std::memcmp(block.data(), "OCHK", 4) == 0) { // A continuation block is "OCHK", the messages, then its own // checksum, which must not be walked as a message header. start = 4; stop = block.size() - 4; } const Status nested = parse_messages(block.data() + start, stop - start, next + start, creation_order, out, depth + 1); if (!nested.ok()) { return nested; } } else { Message message; message.type = type; message.body = base + body; message.size = body_size; out->push_back(message); } position = static_cast(body + body_size); } return Status::success(); } Status object_messages(std::uint64_t address, std::vector* out) const { const auto cached = headers.find(address); if (cached != headers.end()) { *out = cached->second; return Status::success(); } std::vector prefix; Status status = read_at(address, 8, &prefix); if (!status.ok()) { return status; } if (std::memcmp(prefix.data(), "OHDR", 4) != 0) { return fail(JOC_ERR_INPUT_FORMAT, "HDF5 object header signature missing in " + file_path); } if (prefix[4] != 2u) { return fail(JOC_ERR_NOT_SUPPORTED, "HDF5 object header version " + std::to_string(prefix[4]) + " is not supported"); } const std::uint8_t flags = prefix[5]; std::uint64_t position = address + 6; if ((flags & 0x20u) != 0u) { position += 16; // access/modification/change/birth times } if ((flags & 0x10u) != 0u) { position += 4; // attribute storage phase change values } const std::size_t width = std::size_t{1} << (flags & 0x03u); std::vector size_field; status = read_at(position, width, &size_field); if (!status.ok()) { return status; } const std::uint64_t chunk_size = read_le(size_field.data(), width); position += width; std::vector chunk; status = read_at(position, chunk_size, &chunk); if (!status.ok()) { return status; } std::vector messages; // A header that tracks attribute creation order stores a two byte // creation order in every message header. status = parse_messages(chunk.data(), chunk.size(), position, (flags & 0x04u) != 0u, &messages, 0); if (!status.ok()) { return status; } headers[address] = messages; *out = messages; return Status::success(); } Status parse_datatype(const std::uint8_t* data, std::size_t size, Hdf5TypeInfo* out) const { if (size < 8u) { return fail(JOC_ERR_INPUT_FORMAT, "truncated HDF5 datatype message in " + file_path); } const std::uint8_t version_class = data[0]; const std::uint8_t datatype_class = static_cast(version_class & 0x0Fu); const std::uint8_t bits = data[1]; const std::uint32_t element_size = static_cast(read_le(data + 4, 4)); Hdf5TypeInfo info; info.size = element_size; info.big_endian = (bits & 0x01u) != 0u; switch (datatype_class) { case 0: // fixed point info.is_signed = (bits & 0x08u) != 0u; switch (element_size) { case 1: info.type = info.is_signed ? Hdf5Type::Int8 : Hdf5Type::UInt8; break; case 2: info.type = info.is_signed ? Hdf5Type::Int16 : Hdf5Type::UInt16; break; case 4: info.type = info.is_signed ? Hdf5Type::Int32 : Hdf5Type::UInt32; break; case 8: info.type = info.is_signed ? Hdf5Type::Int64 : Hdf5Type::UInt64; break; default: return fail(JOC_ERR_NOT_SUPPORTED, "HDF5 integer of " + std::to_string(element_size) + " bytes is not supported"); } break; case 1: // floating point if (element_size == 4u) { info.type = Hdf5Type::Float32; } else if (element_size == 8u) { info.type = Hdf5Type::Float64; } else { return fail(JOC_ERR_NOT_SUPPORTED, "HDF5 float of " + std::to_string(element_size) + " bytes is not supported"); } break; case 3: // fixed length string info.type = Hdf5Type::String; break; default: return fail(JOC_ERR_NOT_SUPPORTED, "HDF5 datatype class " + std::to_string(datatype_class) + " (compound, variable length or reference) is not supported"); } *out = info; return Status::success(); } Status parse_dataspace(const std::uint8_t* data, std::size_t size, std::vector* out) const { if (size < 4u) { return fail(JOC_ERR_INPUT_FORMAT, "truncated HDF5 dataspace message in " + file_path); } const std::uint8_t version = data[0]; const std::uint8_t rank = data[1]; const std::uint8_t flags = data[2]; if (version != 1u && version != 2u) { return fail(JOC_ERR_NOT_SUPPORTED, "HDF5 dataspace version " + std::to_string(version) + " is not supported"); } // Version 1 aligns the dimension sizes on an eight byte boundary, so its // four byte preamble is padded; version 2 writes them immediately. const std::uint64_t start = (version == 1u) ? 8u : 4u; const std::uint64_t needed = start + static_cast(rank) * length_size; if (needed > size) { return fail(JOC_ERR_INPUT_FORMAT, "truncated HDF5 dataspace dimensions in " + file_path); } std::vector shape; shape.reserve(rank); for (std::uint8_t index = 0; index < rank; ++index) { shape.push_back(read_le(data + start + static_cast(index) * length_size, length_size)); } (void)flags; // maximum dimensions and permutation indexes are not needed *out = std::move(shape); return Status::success(); } Status parse_layout(const std::uint8_t* data, std::size_t size, Layout* out) const { if (size < 2u) { return fail(JOC_ERR_INPUT_FORMAT, "truncated HDF5 data layout message in " + file_path); } const std::uint8_t version = data[0]; const std::uint8_t storage_class = data[1]; if (version == 3u || version == 4u) { switch (storage_class) { case 0: { // compact: the data follows the two byte size field if (size < 4u) { return fail(JOC_ERR_INPUT_FORMAT, "truncated HDF5 compact layout in " + file_path); } out->compact = true; out->size = read_le(data + 2, 2); out->inline_offset = 4; return Status::success(); } case 1: { // contiguous: address then allocated size if (size < 2u + offset_size + length_size) { return fail(JOC_ERR_INPUT_FORMAT, "truncated HDF5 contiguous layout in " + file_path); } out->compact = false; out->address = read_le(data + 2, offset_size); out->size = read_le(data + 2 + offset_size, length_size); return Status::success(); } case 2: return fail(JOC_ERR_NOT_SUPPORTED, "chunked datasets are not supported"); default: return fail(JOC_ERR_NOT_SUPPORTED, "HDF5 data layout class " + std::to_string(storage_class) + " is not supported"); } } if (version == 1u || version == 2u) { // The older layouts only describe contiguous storage in a form this // reader can follow. if (storage_class != 1u || size < 2u + offset_size) { return fail(JOC_ERR_NOT_SUPPORTED, "HDF5 data layout version " + std::to_string(version) + " is not supported"); } out->compact = false; out->address = read_le(data + 2, offset_size); out->size = 0; // derived from the dataspace and datatype return Status::success(); } return fail(JOC_ERR_NOT_SUPPORTED, "HDF5 data layout version " + std::to_string(version) + " is not supported"); } Status read_fractal_heap(std::uint64_t address, FractalHeap* out) const { std::vector data; // The header ends with the root block address and the current row count, // 142 bytes in total for eight byte offsets and lengths. Status status = read_at(address, 144, &data); if (!status.ok()) { return status; } if (std::memcmp(data.data(), "FRHP", 4) != 0) { return fail(JOC_ERR_INPUT_FORMAT, "HDF5 fractal heap header missing in " + file_path); } std::size_t position = 4; const std::uint8_t version = data[position++]; if (version != 0u) { return fail(JOC_ERR_NOT_SUPPORTED, "HDF5 fractal heap version " + std::to_string(version) + " is not supported"); } out->id_length = read_le(data.data() + position, 2); position += 2; const std::uint64_t filter_length = read_le(data.data() + position, 2); position += 2; if (filter_length != 0u) { return fail(JOC_ERR_NOT_SUPPORTED, "filtered HDF5 fractal heaps are not supported"); } position += 1; // flags out->max_object = static_cast(read_le(data.data() + position, 4)); position += 4; position += static_cast(length_size); // next huge object id position += static_cast(offset_size); // huge object B-tree position += static_cast(length_size); // free space in managed blocks position += static_cast(offset_size); // free space B-tree position += static_cast(length_size); // managed space position += static_cast(length_size); // allocated managed space position += static_cast(length_size); // direct block iterator offset out->object_count = read_le(data.data() + position, length_size); position += static_cast(length_size); position += static_cast(length_size); // size of huge objects out->huge_count = read_le(data.data() + position, length_size); position += static_cast(length_size); position += static_cast(length_size); // size of tiny objects out->tiny_count = read_le(data.data() + position, length_size); position += static_cast(length_size); out->width = static_cast(read_le(data.data() + position, 2)); position += 2; out->start_block = read_le(data.data() + position, length_size); position += static_cast(length_size); out->max_direct_block = read_le(data.data() + position, length_size); position += static_cast(length_size); const std::uint64_t max_heap_bits = read_le(data.data() + position, 2); position += 2; position += 2; // starting number of rows in the root indirect block out->root_block = read_le(data.data() + position, offset_size); position += static_cast(offset_size); out->current_rows = static_cast(read_le(data.data() + position, 2)); out->offset_width = static_cast((max_heap_bits + 7u) / 8u); if (out->offset_width == 0u) { out->offset_width = 1; } if (out->id_length == 0u || out->start_block == 0u) { return fail(JOC_ERR_INPUT_FORMAT, "malformed HDF5 fractal heap header in " + file_path); } return Status::success(); } // Size of one heap object at `offset`, used to step from object to object. // Free space is zero filled, so the first byte that does not describe a // well formed message ends the enumeration. Status object_extent(HeapObjectKind kind, std::uint64_t offset, std::uint64_t limit, std::uint64_t* size, bool* valid) const { *valid = false; *size = 0; std::vector data; const std::uint64_t available = (limit > offset) ? (limit - offset) : 0; const std::uint64_t wanted = std::min(available, 8); if (wanted < 4u) { return Status::success(); } const Status status = read_at(offset, wanted, &data); if (!status.ok()) { return status; } if (kind == HeapObjectKind::Link) { if (data[0] != 1u) { return Status::success(); } const std::uint8_t flags = data[1]; std::uint64_t position = 2; if ((flags & 0x08u) != 0u) { position += 1; } if ((flags & 0x04u) != 0u) { position += 8; } if ((flags & 0x10u) != 0u) { position += 1; } const std::uint64_t name_size_field = 1ull << (flags & 0x03u); std::vector header; const Status head_status = read_at(offset, position + name_size_field, &header); if (!head_status.ok()) { return head_status; } const std::uint64_t name_length = read_le(header.data() + position, name_size_field); const std::uint64_t total = position + name_size_field + name_length + offset_size; if (name_length == 0u || name_length > 4096u || total > available) { return Status::success(); } *size = total; *valid = true; return Status::success(); } if (data[0] < 1u || data[0] > 3u) { return Status::success(); } std::vector header; const Status head_status = read_at(offset, std::min(available, 8), &header); if (!head_status.ok()) { return head_status; } if (header.size() < 8u) { return Status::success(); } const std::uint64_t name_size = read_le(header.data() + 2, 2); const std::uint64_t datatype_size = read_le(header.data() + 4, 2); const std::uint64_t dataspace_size = read_le(header.data() + 6, 2); // Version 1 pads the attribute name to eight bytes, versions 2 and 3 // write it (and, for version 3, a character set byte) unpadded. The // datatype and dataspace messages are always padded to eight bytes. const std::uint64_t name_offset = (data[0] == 3u) ? 9u : 8u; const std::uint64_t name_field = (data[0] == 1u) ? align_to_8(name_size) : name_size; const std::uint64_t datatype_offset = name_offset + name_field; const std::uint64_t dataspace_offset = datatype_offset + align_to_8(datatype_size); const std::uint64_t data_offset = dataspace_offset + align_to_8(dataspace_size); if (name_size == 0u || name_size > 4096u || data_offset > available || datatype_size < 8u || dataspace_size < 4u) { return Status::success(); } std::vector tail; const Status tail_status = read_at(offset + datatype_offset, datatype_size, &tail); if (!tail_status.ok()) { return tail_status; } const std::uint64_t element_size = read_le(tail.data() + 4, 4); std::vector space; const Status space_status = read_at(offset + dataspace_offset, dataspace_size, &space); if (!space_status.ok()) { return space_status; } std::vector shape; const Status shape_status = parse_dataspace(space.data(), space.size(), &shape); if (!shape_status.ok()) { return Status::success(); } std::uint64_t count = 1; for (const std::uint64_t dimension : shape) { if (dimension != 0u && count > kMaxReadSize / dimension) { return Status::success(); } count *= dimension; } if (count != 0u && element_size > kMaxReadSize / count) { return Status::success(); } const std::uint64_t total = data_offset + count * element_size; if (total > available) { return Status::success(); } *size = total; *valid = true; return Status::success(); } Status scan_direct_block(const FractalHeap& heap, std::uint64_t address, int row, HeapObjectKind kind, std::vector* out) const { std::vector prefix; Status status = read_at(address, 4 + 1 + offset_size, &prefix); if (!status.ok()) { return status; } if (std::memcmp(prefix.data(), "FHDB", 4) != 0) { return fail(JOC_ERR_INPUT_FORMAT, "HDF5 fractal heap direct block missing in " + file_path); } if (prefix[4] != 0u) { return fail(JOC_ERR_NOT_SUPPORTED, "unsupported HDF5 fractal heap block version"); } const std::uint64_t block_size = std::min(heap.start_block << row, heap.max_direct_block); if (block_size == 0u || address + block_size < address) { return fail(JOC_ERR_INPUT_FORMAT, "malformed HDF5 fractal heap block in " + file_path); } const std::uint64_t end = std::min(address + block_size, file_size); std::uint64_t position = address + 4 + 1 + offset_size + heap.offset_width + 4; while (position < end) { std::uint64_t object_size = 0; bool valid = false; status = object_extent(kind, position, end, &object_size, &valid); if (!status.ok()) { return status; } if (!valid || object_size == 0u) { break; } Message object; object.type = kMsgNil; // heap objects carry no message type object.body = position; object.size = object_size; out->push_back(object); position += object_size; } return Status::success(); } Status collect_blocks(const FractalHeap& heap, std::uint64_t block, int row, int depth, std::vector>* out) const { if (block == kUndefinedAddress || depth > kMaxNesting) { return Status::success(); } std::vector signature; Status status = read_at(block, 4, &signature); if (!status.ok()) { return status; } if (std::memcmp(signature.data(), "FHDB", 4) == 0) { out->emplace_back(block, row); return Status::success(); } if (std::memcmp(signature.data(), "FHIB", 4) != 0) { return fail(JOC_ERR_INPUT_FORMAT, "unknown HDF5 fractal heap block in " + file_path); } // Indirect block: version, heap header address, block offset, then one // address per child block, two per row more than the row above. const std::uint64_t header = 4 + 1 + offset_size + heap.offset_width; const int rows = (depth == 0) ? static_cast(heap.current_rows) : kMaxNesting - 1; std::uint64_t position = block + header; for (int current = 0; current <= rows; ++current) { const int children = 1 << std::min(current, 20); bool any = false; for (int index = 0; index < children; ++index) { std::uint64_t child = kUndefinedAddress; status = read_u64(position, offset_size, &child); if (!status.ok()) { return status; } position += offset_size; if (child != kUndefinedAddress) { any = true; status = collect_blocks(heap, child, current, depth + 1, out); if (!status.ok()) { return status; } } } if (!any) { break; } } return Status::success(); } Status heap_objects(std::uint64_t heap_address, HeapObjectKind kind, std::vector* out) const { FractalHeap heap; Status status = read_fractal_heap(heap_address, &heap); if (!status.ok()) { return status; } if (heap.huge_count != 0u) { return fail(JOC_ERR_NOT_SUPPORTED, "HDF5 heap object is stored as a huge object and is not supported"); } std::vector> blocks; status = collect_blocks(heap, heap.root_block, 0, 0, &blocks); if (!status.ok()) { return status; } for (const std::pair& block : blocks) { status = scan_direct_block(heap, block.first, block.second, kind, out); if (!status.ok()) { return status; } } return Status::success(); } Status parse_link(const std::uint8_t* data, std::size_t size, LinkEntry* out) const { if (size < 2u || data[0] != 1u) { return fail(JOC_ERR_INPUT_FORMAT, "malformed HDF5 link message in " + file_path); } const std::uint8_t flags = data[1]; std::uint64_t position = 2; std::uint8_t link_type = 0; if ((flags & 0x08u) != 0u) { if (position + 1u > size) { return fail(JOC_ERR_INPUT_FORMAT, "truncated HDF5 link message in " + file_path); } link_type = data[position]; position += 1; } if ((flags & 0x04u) != 0u) { position += 8; } if ((flags & 0x10u) != 0u) { position += 1; } const std::uint64_t name_size_field = 1ull << (flags & 0x03u); if (position + name_size_field > size) { return fail(JOC_ERR_INPUT_FORMAT, "truncated HDF5 link name in " + file_path); } const std::uint64_t name_length = read_le(data + position, name_size_field); position += name_size_field; if (position + name_length + offset_size > size) { return fail(JOC_ERR_INPUT_FORMAT, "truncated HDF5 link message in " + file_path); } const std::string name(reinterpret_cast(data + position), static_cast(name_length)); position += name_length; if (link_type != 0u) { return fail(JOC_ERR_NOT_SUPPORTED, std::string(link_type == 1u ? "soft" : "external") + " HDF5 link '" + name + "' is not supported"); } out->name = name; out->address = read_le(data + position, offset_size); return Status::success(); } Status links_of(const std::string& group_path, std::vector* out) const { const auto cached = link_cache.find(group_path); if (cached != link_cache.end()) { *out = cached->second; return Status::success(); } std::uint64_t address = kUndefinedAddress; Status status = object_address(group_path, &address); if (!status.ok()) { return status; } std::vector messages; status = object_messages(address, &messages); if (!status.ok()) { return status; } std::vector links; for (const Message& message : messages) { if (message.type == kMsgLink) { std::vector body; status = read_at(message.body, message.size, &body); if (!status.ok()) { return status; } LinkEntry entry; status = parse_link(body.data(), body.size(), &entry); if (!status.ok()) { return status; } links.push_back(std::move(entry)); } else if (message.type == kMsgLinkInfo) { std::vector body; status = read_at(message.body, message.size, &body); if (!status.ok()) { return status; } if (body.size() < 2u) { return fail(JOC_ERR_INPUT_FORMAT, "malformed HDF5 link info message in " + file_path); } const std::uint8_t flags = body[1]; std::uint64_t position = 2; if ((flags & 0x01u) != 0u) { position += static_cast(length_size); // maximum creation index } if (position + offset_size > body.size()) { return fail(JOC_ERR_INPUT_FORMAT, "malformed HDF5 link info message in " + file_path); } const std::uint64_t heap_address = read_le(body.data() + position, offset_size); if (heap_address == kUndefinedAddress) { // No dense storage: this group keeps its links compact. continue; } std::vector objects; status = heap_objects(heap_address, HeapObjectKind::Link, &objects); if (!status.ok()) { return status; } for (const Message& object : objects) { std::vector link_body; status = read_at(object.body, object.size, &link_body); if (!status.ok()) { return status; } LinkEntry entry; status = parse_link(link_body.data(), link_body.size(), &entry); if (!status.ok()) { return status; } links.push_back(std::move(entry)); } } } link_cache[group_path] = links; *out = links; return Status::success(); } Status object_address(const std::string& path, std::uint64_t* out) const { if (path.empty() || path == "/") { *out = root_object; return Status::success(); } const auto cached = object_cache.find(path); if (cached != object_cache.end()) { *out = cached->second; return Status::success(); } std::uint64_t current = root_object; std::string prefix; // A leading slash is the HDF5 spelling of "from the root group". std::size_t start = path[0] == '/' ? 1u : 0u; while (true) { const std::size_t slash = path.find('/', start); const std::string component = path.substr(start, slash == std::string::npos ? std::string::npos : slash - start); if (component.empty()) { return fail(JOC_ERR_INVALID_ARGUMENT, "empty link name in HDF5 path " + path); } std::vector entries; const Status status = links_of(prefix, &entries); if (!status.ok()) { return status; } const LinkEntry* found = nullptr; for (const LinkEntry& entry : entries) { if (entry.name == component) { found = &entry; break; } } if (found == nullptr) { return fail(JOC_ERR_INPUT_FORMAT, "HDF5 object not found: " + path); } current = found->address; if (slash == std::string::npos) { break; } prefix = prefix.empty() ? component : prefix + "/" + component; start = slash + 1; } object_cache[path] = current; *out = current; return Status::success(); } Status attribute_entries(const std::string& object_path, std::vector* out) const { std::uint64_t address = kUndefinedAddress; Status status = object_address(object_path, &address); if (!status.ok()) { return status; } std::vector messages; status = object_messages(address, &messages); if (!status.ok()) { return status; } std::vector entries; for (const Message& message : messages) { if (message.type == kMsgAttribute) { std::vector body; status = read_at(message.body, std::min(message.size, 8), &body); if (!status.ok()) { return status; } if (body.size() < 8u || body[0] < 1u || body[0] > 3u) { return fail(JOC_ERR_INPUT_FORMAT, "malformed HDF5 attribute message in " + file_path); } const std::uint64_t name_size = read_le(body.data() + 2, 2); const std::uint64_t name_offset = (body[0] == 3u) ? 9u : 8u; if (name_size == 0u || name_offset + name_size > message.size) { return fail(JOC_ERR_INPUT_FORMAT, "malformed HDF5 attribute name in " + file_path); } std::vector name; status = read_at(message.body + name_offset, name_size, &name); if (!status.ok()) { return status; } AttributeEntry entry; entry.name = attribute_name(name.data(), name.size()); entry.body = message.body; entry.size = message.size; entries.push_back(std::move(entry)); } else if (message.type == kMsgAttributeInfo) { std::vector body; status = read_at(message.body, message.size, &body); if (!status.ok()) { return status; } if (body.size() < 2u) { return fail(JOC_ERR_INPUT_FORMAT, "malformed HDF5 attribute info message in " + file_path); } const std::uint8_t flags = body[1]; std::uint64_t position = 2; if ((flags & 0x01u) != 0u) { position += 2; // maximum creation index } if (position + offset_size > body.size()) { return fail(JOC_ERR_INPUT_FORMAT, "malformed HDF5 attribute info message in " + file_path); } const std::uint64_t heap_address = read_le(body.data() + position, offset_size); if (heap_address == kUndefinedAddress) { // No dense storage: this object keeps its attributes compact. continue; } std::vector objects; status = heap_objects(heap_address, HeapObjectKind::Attribute, &objects); if (!status.ok()) { return status; } for (const Message& object : objects) { std::vector head; status = read_at(object.body, std::min(object.size, 8), &head); if (!status.ok()) { return status; } if (head.size() < 8u || head[0] < 1u || head[0] > 3u) { return fail(JOC_ERR_INPUT_FORMAT, "malformed HDF5 attribute in the dense storage of " + file_path); } const std::uint64_t name_size = read_le(head.data() + 2, 2); const std::uint64_t name_offset = (head[0] == 3u) ? 9u : 8u; if (name_size == 0u || name_offset + name_size > object.size) { return fail(JOC_ERR_INPUT_FORMAT, "malformed HDF5 attribute name in " + file_path); } std::vector name; status = read_at(object.body + name_offset, name_size, &name); if (!status.ok()) { return status; } AttributeEntry entry; entry.name = attribute_name(name.data(), name.size()); entry.body = object.body; entry.size = object.size; entries.push_back(std::move(entry)); } } } *out = std::move(entries); return Status::success(); } Status decode_attribute(std::uint64_t body_offset, std::uint64_t body_size, Hdf5Attribute* out) const { if (body_size > kMaxReadSize) { return fail(JOC_ERR_NOT_SUPPORTED, "HDF5 attribute is too large to read"); } std::vector body; Status status = read_at(body_offset, body_size, &body); if (!status.ok()) { return status; } if (body.size() < 8u || body[0] < 1u || body[0] > 3u) { return fail(JOC_ERR_INPUT_FORMAT, "malformed HDF5 attribute message in " + file_path); } const std::uint8_t version = body[0]; const std::uint64_t name_size = read_le(body.data() + 2, 2); const std::uint64_t datatype_size = read_le(body.data() + 4, 2); const std::uint64_t dataspace_size = read_le(body.data() + 6, 2); const std::uint64_t name_offset = (version == 3u) ? 9u : 8u; const std::uint64_t name_field = (version == 1u) ? align_to_8(name_size) : name_size; const std::uint64_t datatype_offset = name_offset + name_field; const std::uint64_t dataspace_offset = datatype_offset + align_to_8(datatype_size); const std::uint64_t data_offset = dataspace_offset + align_to_8(dataspace_size); if (name_size == 0u || datatype_size < 8u || dataspace_size < 4u || data_offset > body.size()) { return fail(JOC_ERR_INPUT_FORMAT, "malformed HDF5 attribute message in " + file_path); } Hdf5Attribute attribute; status = parse_datatype(body.data() + datatype_offset, static_cast(datatype_size), &attribute.type); if (!status.ok()) { return status; } status = parse_dataspace(body.data() + dataspace_offset, static_cast(dataspace_size), &attribute.shape); if (!status.ok()) { return status; } std::uint64_t count = 1; for (const std::uint64_t dimension : attribute.shape) { count *= dimension; } const std::uint64_t bytes = count * attribute.type.size; if (bytes > body.size() - data_offset) { return fail(JOC_ERR_INPUT_FORMAT, "truncated HDF5 attribute data in " + file_path); } attribute.raw.assign(body.begin() + static_cast(data_offset), body.begin() + static_cast(data_offset + bytes)); if (attribute.type.big_endian && !is_string_type(attribute.type.type)) { for (std::uint64_t index = 0; index < count; ++index) { swap_element_bytes(attribute.raw.data() + index * attribute.type.size, attribute.type.size); } attribute.type.big_endian = false; } if (is_string_type(attribute.type.type)) { attribute.text.assign(reinterpret_cast(attribute.raw.data()), attribute.raw.size()); attribute.text = trim_nul(std::move(attribute.text)); } *out = std::move(attribute); return Status::success(); } Status dataset_parts(const std::string& path, Hdf5DatasetInfo* info, Layout* layout) const { std::uint64_t address = kUndefinedAddress; Status status = object_address(path, &address); if (!status.ok()) { return status; } std::vector messages; status = object_messages(address, &messages); if (!status.ok()) { return status; } bool have_dataspace = false; bool have_datatype = false; bool have_layout = false; for (const Message& message : messages) { if (message.type == kMsgDataspace || message.type == kMsgDatatype || message.type == kMsgLayout) { std::vector body; status = read_at(message.body, message.size, &body); if (!status.ok()) { return status; } if (message.type == kMsgDataspace) { status = parse_dataspace(body.data(), body.size(), &info->shape); have_dataspace = status.ok(); } else if (message.type == kMsgDatatype) { status = parse_datatype(body.data(), body.size(), &info->type); have_datatype = status.ok(); } else { status = parse_layout(body.data(), body.size(), layout); if (status.ok() && layout->compact) { layout->inline_offset += message.body; } have_layout = status.ok(); } if (!status.ok()) { return status; } } } if (!have_dataspace || !have_datatype) { return fail(JOC_ERR_INPUT_FORMAT, "HDF5 object is not a dataset: " + path); } if (!have_layout) { return fail(JOC_ERR_INPUT_FORMAT, "HDF5 dataset has no data layout message: " + path); } return Status::success(); } // Reads exactly one dataset: the payload of every other object stays on disk. Status read_dataset(const std::string& path, Hdf5DatasetInfo* info, std::vector* raw) const { Layout layout; Status status = dataset_parts(path, info, &layout); if (!status.ok()) { return status; } const std::uint64_t count = info->element_count(); if (count > kMaxReadSize || info->type.size == 0u || (count != 0u && info->type.size > kMaxReadSize / count)) { return fail(JOC_ERR_NOT_SUPPORTED, "HDF5 dataset is too large to read: " + path); } const std::uint64_t bytes = count * info->type.size; std::vector data; if (bytes == 0u) { // An empty dataset has no payload to fetch, allocated or not. *raw = std::move(data); return Status::success(); } if (layout.compact) { if (layout.size < bytes) { return fail(JOC_ERR_INPUT_FORMAT, "truncated compact HDF5 dataset: " + path); } status = read_at(layout.inline_offset, bytes, &data); } else { if (layout.address == kUndefinedAddress) { return fail(JOC_ERR_INPUT_FORMAT, "HDF5 dataset storage is not allocated: " + path); } if (layout.size != 0u && layout.size < bytes) { return fail(JOC_ERR_INPUT_FORMAT, "HDF5 dataset storage is shorter than its shape: " + path); } status = read_at(layout.address, bytes, &data); } if (!status.ok()) { return status; } if (info->type.big_endian && !is_string_type(info->type.type)) { for (std::uint64_t index = 0; index < count; ++index) { swap_element_bytes(data.data() + index * info->type.size, info->type.size); } info->type.big_endian = false; } *raw = std::move(data); return Status::success(); } }; Hdf5File::Hdf5File() : impl_(std::make_unique()) {} Hdf5File::~Hdf5File() = default; Hdf5File::Hdf5File(Hdf5File&&) noexcept = default; Hdf5File& Hdf5File::operator=(Hdf5File&&) noexcept = default; Status Hdf5File::open(const std::string& path) { if (impl_ == nullptr) { return fail(JOC_ERR_STATE, "HDF5 reader is not constructed"); } if (!fs_utf8::exists(path)) { return fail(JOC_ERR_INPUT_NOT_FOUND, "HDF5 file not found: " + path); } std::error_code error; const std::uintmax_t size = fs_utf8::file_size(path, error); if (error) { return fail(JOC_ERR_IO, "cannot query the size of " + path); } impl_->stream = fs_utf8::open_input(path); if (!impl_->stream.good()) { return fail(JOC_ERR_IO, "cannot open " + path); } impl_->file_path = path; impl_->file_size = static_cast(size); impl_->opened = false; impl_->headers.clear(); impl_->link_cache.clear(); impl_->object_cache.clear(); std::vector signature; Status status = impl_->read_at(0, 8, &signature); if (!status.ok()) { return status; } static const std::uint8_t kMagic[8] = {0x89u, 'H', 'D', 'F', '\r', '\n', 0x1Au, '\n'}; if (std::memcmp(signature.data(), kMagic, 8) != 0) { return fail(JOC_ERR_INPUT_FORMAT, "not an HDF5 file: " + path); } std::vector superblock; status = impl_->read_at(0, 64, &superblock); if (!status.ok()) { return status; } const std::uint8_t version = superblock[8]; if (version > 1u) { return fail(JOC_ERR_NOT_SUPPORTED, "HDF5 superblock version " + std::to_string(version) + " is not supported"); } impl_->offset_size = superblock[13]; impl_->length_size = superblock[14]; if (impl_->offset_size == 0u || impl_->offset_size > 8u || impl_->length_size == 0u || impl_->length_size > 8u) { return fail(JOC_ERR_INPUT_FORMAT, "malformed HDF5 superblock in " + path); } const std::uint64_t root_entry = 56; status = impl_->read_u64(root_entry + impl_->offset_size, impl_->offset_size, &impl_->root_object); if (!status.ok()) { return status; } if (impl_->root_object == kUndefinedAddress) { return fail(JOC_ERR_INPUT_FORMAT, "HDF5 root group address is undefined in " + path); } std::vector root_prefix; status = impl_->read_at(impl_->root_object, 4, &root_prefix); if (!status.ok()) { return status; } if (std::memcmp(root_prefix.data(), "OHDR", 4) != 0) { return fail(JOC_ERR_NOT_SUPPORTED, "HDF5 root group does not use a version 2 object header in " + path); } impl_->opened = true; return Status::success(); } bool Hdf5File::is_open() const { return impl_ != nullptr && impl_->opened; } Status Hdf5File::links(const std::string& group_path, std::vector* names) const { if (names == nullptr) { return fail(JOC_ERR_INVALID_ARGUMENT, "null name list"); } if (!is_open()) { return fail(JOC_ERR_STATE, "HDF5 reader is not open"); } std::vector entries; const Status status = impl_->links_of(group_path, &entries); if (!status.ok()) { return status; } names->clear(); names->reserve(entries.size()); for (const LinkEntry& entry : entries) { names->push_back(entry.name); } return Status::success(); } bool Hdf5File::has_dataset(const std::string& path) const { if (!is_open()) { return false; } std::uint64_t address = kUndefinedAddress; if (!impl_->object_address(path, &address).ok()) { return false; } std::vector messages; if (!impl_->object_messages(address, &messages).ok()) { return false; } // A dataset is the object that carries both a dataspace and a datatype, // whatever its storage class turns out to be. bool dataspace = false; bool datatype = false; for (const Message& message : messages) { dataspace = dataspace || message.type == kMsgDataspace; datatype = datatype || message.type == kMsgDatatype; } return dataspace && datatype; } Status Hdf5File::dataset_info(const std::string& path, Hdf5DatasetInfo* out) const { if (out == nullptr) { return fail(JOC_ERR_INVALID_ARGUMENT, "null dataset info"); } if (!is_open()) { return fail(JOC_ERR_STATE, "HDF5 reader is not open"); } Layout layout; Hdf5DatasetInfo info; const Status status = impl_->dataset_parts(path, &info, &layout); if (!status.ok()) { return status; } *out = std::move(info); return Status::success(); } Status Hdf5File::read_dataset_raw(const std::string& path, std::vector* out) const { if (out == nullptr) { return fail(JOC_ERR_INVALID_ARGUMENT, "null dataset buffer"); } if (!is_open()) { return fail(JOC_ERR_STATE, "HDF5 reader is not open"); } Hdf5DatasetInfo info; return impl_->read_dataset(path, &info, out); } Status Hdf5File::read_dataset_double(const std::string& path, std::vector* out) const { if (out == nullptr) { return fail(JOC_ERR_INVALID_ARGUMENT, "null dataset buffer"); } if (!is_open()) { return fail(JOC_ERR_STATE, "HDF5 reader is not open"); } Hdf5DatasetInfo info; std::vector raw; const Status status = impl_->read_dataset(path, &info, &raw); if (!status.ok()) { return status; } const std::uint64_t count = info.element_count(); out->clear(); out->resize(static_cast(count)); for (std::uint64_t index = 0; index < count; ++index) { const std::uint8_t* element = raw.data() + index * info.type.size; double value = 0.0; switch (info.type.type) { case Hdf5Type::Float64: { std::memcpy(&value, element, sizeof(double)); break; } case Hdf5Type::Float32: { float narrow = 0.0f; std::memcpy(&narrow, element, sizeof(float)); value = static_cast(narrow); break; } case Hdf5Type::Int8: { value = static_cast(static_cast(element[0])); break; } case Hdf5Type::UInt8: { value = static_cast(element[0]); break; } case Hdf5Type::Int16: { std::int16_t narrow = 0; std::memcpy(&narrow, element, sizeof(narrow)); value = static_cast(narrow); break; } case Hdf5Type::UInt16: { std::uint16_t narrow = 0; std::memcpy(&narrow, element, sizeof(narrow)); value = static_cast(narrow); break; } case Hdf5Type::Int32: { std::int32_t narrow = 0; std::memcpy(&narrow, element, sizeof(narrow)); value = static_cast(narrow); break; } case Hdf5Type::UInt32: { std::uint32_t narrow = 0; std::memcpy(&narrow, element, sizeof(narrow)); value = static_cast(narrow); break; } case Hdf5Type::Int64: { std::int64_t narrow = 0; std::memcpy(&narrow, element, sizeof(narrow)); value = static_cast(narrow); break; } case Hdf5Type::UInt64: { std::uint64_t narrow = 0; std::memcpy(&narrow, element, sizeof(narrow)); value = static_cast(narrow); break; } default: return fail(JOC_ERR_NOT_SUPPORTED, "HDF5 dataset is not numeric and cannot be read as double: " + path); } (*out)[static_cast(index)] = value; } return Status::success(); } Status Hdf5File::attribute_names(const std::string& object_path, std::vector* names) const { if (names == nullptr) { return fail(JOC_ERR_INVALID_ARGUMENT, "null name list"); } if (!is_open()) { return fail(JOC_ERR_STATE, "HDF5 reader is not open"); } std::vector entries; const Status status = impl_->attribute_entries(object_path, &entries); if (!status.ok()) { return status; } names->clear(); names->reserve(entries.size()); for (const AttributeEntry& entry : entries) { names->push_back(entry.name); } return Status::success(); } Status Hdf5File::attribute(const std::string& object_path, const std::string& name, Hdf5Attribute* out) const { if (out == nullptr) { return fail(JOC_ERR_INVALID_ARGUMENT, "null attribute"); } if (!is_open()) { return fail(JOC_ERR_STATE, "HDF5 reader is not open"); } std::vector entries; Status status = impl_->attribute_entries(object_path, &entries); if (!status.ok()) { return status; } for (const AttributeEntry& entry : entries) { if (entry.name == name) { return impl_->decode_attribute(entry.body, entry.size, out); } } return fail(JOC_ERR_INPUT_FORMAT, "HDF5 attribute not found: " + name); } Status Hdf5File::attribute_text(const std::string& object_path, const std::string& name, std::string* out) const { if (out == nullptr) { return fail(JOC_ERR_INVALID_ARGUMENT, "null text"); } Hdf5Attribute attribute; Status status = this->attribute(object_path, name, &attribute); if (!status.ok()) { return status; } if (!is_string_type(attribute.type.type)) { return fail(JOC_ERR_INPUT_FORMAT, "HDF5 attribute is not a string: " + name); } *out = attribute.text; return Status::success(); } } // namespace joc::io