1#ifndef UZUKI2_PARSE_HPP
2#define UZUKI2_PARSE_HPP
11#include <unordered_set>
15#include "ritsuko/ritsuko.hpp"
20#include "ExternalTracker.hpp"
60inline void validate_numeric_missing_placeholder(
const H5::Attribute& attr,
const H5::DataSet& data,
const Version& version) {
61 if (attr.getSpace().getSimpleExtentNdims() != 0) {
62 throw std::runtime_error(
"expected the missing value placeholder to be a scalar attribute");
64 if (version.lt(1, 2)) {
65 if (attr.getDataType().getClass() != data.getDataType().getClass()) {
66 throw std::runtime_error(
"expected the missing value placeholder attribute to have the same type class as its dataset");
69 if (attr.getDataType() != data.getDataType()) {
70 throw std::runtime_error(
"expected the missing value placeholder attribute to have the same type as its dataset");
75inline void validate_string_missing_placeholder(
const H5::Attribute& attr) {
76 if (attr.getSpace().getSimpleExtentNdims() != 0) {
77 throw std::runtime_error(
"expected the missing value placeholder attribute to be a scalar");
79 if (!ritsuko::hdf5::is_utf8_string(attr)) {
80 throw std::runtime_error(
"expected the missing value placeholder attribute to be a UTF-8 string");
84template<
typename Type_,
class Stream_,
class Action_>
85void iterate_stream(Stream_& stream, Action_ action) {
88 auto available = stream.load(buffer.data());
92 for (I<
decltype(available)> i = 0; i < available; ++i) {
93 action(i + stream.start(), std::move(buffer[i]));
98template<
class Host_,
class Function_>
99void parse_integer_like(
const H5::DataSet& handle, Host_* ptr,
bool is_scalar, Function_ check,
const Version& version,
const Options& options) {
100 if (ritsuko::hdf5::exceeds_integer_limit(handle, 32,
true)) {
101 throw std::runtime_error(
"dataset cannot be represented by 32-bit signed integers");
104 bool has_missing =
false;
105 std::int32_t missing_value = -2147483648;
106 if (version.equals(1, 0)) {
109 const char* placeholder_name =
"missing-value-placeholder";
110 has_missing = handle.attrExists(placeholder_name);
113 auto attr = handle.openAttribute(placeholder_name);
114 validate_numeric_missing_placeholder(attr, handle, version);
115 attr.read(H5::PredType::NATIVE_INT32, &missing_value);
117 std::throw_with_nested(std::runtime_error(
"failed to read the '" + std::string(placeholder_name) +
"' attribute"));
122 auto set = [&](hsize_t i, std::int32_t x) ->
void {
123 if (has_missing && x == missing_value) {
133 handle.read(&value, H5::PredType::NATIVE_INT32);
136 ritsuko::hdf5::Stream1dNumericDataset<std::int32_t> stream(
138 static_cast<hsize_t
>(ptr->size()),
140 ritsuko::hdf5::Stream1dNumericDatasetOptions opt;
141 opt.contiguous_chunk_size = options.buffer_size;
145 iterate_stream<std::int32_t>(stream, set);
149template<
class Host_,
class Function_>
150void parse_string_like(
const H5::DataSet& handle, Host_* ptr,
bool is_scalar, Function_ check,
const Options& options) {
151 if (!ritsuko::hdf5::is_utf8_string(handle)) {
152 throw std::runtime_error(
"expected a datatype that can be represented by a UTF-8 string");
155 std::optional<std::string> missingness;
156 const char* placeholder_name =
"missing-value-placeholder";
157 if (handle.attrExists(placeholder_name)) {
159 auto attr = handle.openAttribute(placeholder_name);
160 validate_string_missing_placeholder(attr);
161 missingness = ritsuko::hdf5::read_scalar_string(attr);
163 std::throw_with_nested(std::runtime_error(
"failed to read the '" + std::string(placeholder_name) +
"' attribute"));
167 auto set = [&](hsize_t i, std::string x) ->
void {
168 if (missingness.has_value() && x == *missingness) {
172 ptr->set(i, std::move(x));
177 auto x = ritsuko::hdf5::read_scalar_string(handle);
178 set(0, std::move(x));
180 ritsuko::hdf5::Stream1dStringDataset stream(
182 static_cast<hsize_t
>(ptr->size()),
184 ritsuko::hdf5::Stream1dStringDatasetOptions opt;
185 opt.contiguous_chunk_size = options.buffer_size;
189 iterate_stream<std::string>(stream, set);
193inline double r_missing_value() {
194 std::uint32_t tmp_value = 1;
195 auto tmp_ptr =
reinterpret_cast<unsigned char*
>(&tmp_value);
199 double missing_value = 0;
200 auto missing_ptr =
reinterpret_cast<unsigned char*
>(&missing_value);
203 if (tmp_ptr[0] == 1) {
204 missing_ptr +=
sizeof(double) - 1;
209 *(missing_ptr += step) = 0xf0;
210 *(missing_ptr += step) = 0x00;
211 *(missing_ptr += step) = 0x00;
212 *(missing_ptr += step) = 0x00;
213 *(missing_ptr += step) = 0x00;
214 *(missing_ptr += step) = 0x07;
215 *(missing_ptr += step) = 0xa2;
217 return missing_value;
220template<
class Host_,
class Function_>
221void parse_numbers(
const H5::DataSet& handle, Host_* ptr,
bool is_scalar, Function_ check,
const Version& version,
const Options& options) {
222 if (version.lt(1, 3)) {
223 if (handle.getTypeClass() != H5T_FLOAT) {
224 throw std::runtime_error(
"expected a floating-point dataset");
227 if (ritsuko::hdf5::exceeds_float_limit(handle, 64)) {
228 throw std::runtime_error(
"dataset cannot be represented by 64-bit floats");
233 static_assert(std::numeric_limits<double>::is_iec559);
235 bool has_missing =
false;
236 double missing_value = 0;
237 if (version.equals(1, 0)) {
239 missing_value = r_missing_value();
241 const char* placeholder_name =
"missing-value-placeholder";
242 has_missing = handle.attrExists(placeholder_name);
245 auto attr = handle.openAttribute(placeholder_name);
246 validate_numeric_missing_placeholder(attr, handle, version);
247 attr.read(H5::PredType::NATIVE_DOUBLE, &missing_value);
249 std::throw_with_nested(std::runtime_error(
"failed to read the '" + std::string(placeholder_name) +
"' attribute"));
254 bool should_compare_nan = version.lt(1, 3);
255 bool is_placeholder_nan = std::isnan(missing_value);
256 auto is_missing_value = [&](
double val) ->
bool {
257 if (should_compare_nan) {
258 auto xptr =
reinterpret_cast<const unsigned char*
>(&missing_value);
259 auto yptr =
reinterpret_cast<const unsigned char*
>(&val);
260 return std::memcmp(xptr, yptr,
sizeof(
double)) == 0;
261 }
else if (is_placeholder_nan) {
262 return std::isnan(val);
264 return val == missing_value;
268 auto set = [&](hsize_t i,
double x) ->
void {
269 if (has_missing && is_missing_value(x)) {
279 handle.read(&val, H5::PredType::NATIVE_DOUBLE);
282 ritsuko::hdf5::Stream1dNumericDataset<double> stream(
284 static_cast<hsize_t
>(ptr->size()),
286 ritsuko::hdf5::Stream1dNumericDatasetOptions opt;
287 opt.contiguous_chunk_size = options.buffer_size;
291 iterate_stream<double>(stream, set);
296void parse_names(
const H5::DataSet& nhandle, Host_* ptr,
const Options& options) {
297 if (!ritsuko::hdf5::is_utf8_string(nhandle)) {
298 throw std::runtime_error(
"expected a datatype that can be represented by a UTF-8 string");
301 const auto space = nhandle.getSpace();
302 if (space.getSimpleExtentNdims() != 1) {
303 throw std::runtime_error(
"expected a 1-dimensional dataset");
306 space.getSimpleExtentDims(&nlen);
308 throw std::runtime_error(
"number of names should be equal to the object length");
311 ritsuko::hdf5::Stream1dStringDataset stream(
315 ritsuko::hdf5::Stream1dStringDatasetOptions opt;
316 opt.contiguous_chunk_size = options.buffer_size;
321 iterate_stream<std::string>(
323 [&](hsize_t pos, std::string val) ->
void {
324 ptr->set_name(pos, std::move(val));
329inline std::string safe_read_scalar_string_attribute(
const H5::Attribute& attr) {
330 if (attr.getSpace().getSimpleExtentNdims() != 0) {
331 throw std::runtime_error(
"expected attribute to be scalar");
333 if (!ritsuko::hdf5::is_utf8_string(attr)) {
334 throw std::runtime_error(
"expected attribute to be a UTF-8 string");
336 return ritsuko::hdf5::read_scalar_string(attr);
339template<
class Provisioner_,
class Externals_>
340std::shared_ptr<Base> parse_inner(
const H5::Group& handle, Externals_& ext,
const Version& version,
const Options& options) {
341 std::string object_type;
343 auto ahandle = handle.openAttribute(
"uzuki_object");
344 object_type = safe_read_scalar_string_attribute(ahandle);
346 std::throw_with_nested(std::runtime_error(
"failed to read the 'uzuki_object' attribute"));
349 std::shared_ptr<Base> output;
351 if (object_type ==
"list") {
352 const bool named = handle.exists(
"names");
355 auto dhandle = handle.openGroup(
"data");
356 const auto len = dhandle.getNumObjs();
360 for (I<
decltype(len)> i = 0; i < len; ++i) {
361 const auto istr = ritsuko::itoa(i);
363 auto lhandle = dhandle.openGroup(istr);
364 lptr->set(i, parse_inner<Provisioner_>(lhandle, ext, version, options));
366 std::throw_with_nested(std::runtime_error(
"failed to read element " + istr));
370 std::throw_with_nested(std::runtime_error(
"failed to read 'data'"));
374 auto lptr =
static_cast<List*
>(output.get());
376 auto nhandle = handle.openDataSet(
"names");
377 parse_names(nhandle, lptr, options);
379 std::throw_with_nested(std::runtime_error(
"failed to read 'names'"));
383 }
else if (object_type ==
"vector") {
384 std::string vector_type;
386 auto ahandle = handle.openAttribute(
"uzuki_type");
387 vector_type = safe_read_scalar_string_attribute(ahandle);
389 std::throw_with_nested(std::runtime_error(
"failed to read the 'uzuki_type' attribute"));
392 const bool named = handle.exists(
"names");
393 enum Failure { DATA, ORDERED, LEVELS, HEAP, FORMAT };
394 Failure who_failed = DATA;
397 auto dhandle = handle.openDataSet(
"data");
398 const auto dspace = dhandle.getSpace();
399 const auto ndims = dspace.getSimpleExtentNdims();
402 bool is_scalar =
false;
405 }
else if (ndims == 1) {
406 dspace.getSimpleExtentDims(&len);
408 throw std::runtime_error(
"expected a scalar or 1-dimensional dataset");
411 if (vector_type ==
"integer") {
418 [](std::int32_t) ->
void {},
423 }
else if (vector_type ==
"boolean") {
430 [&](std::int32_t x) ->
void {
431 if (x != 0 && x != 1) {
432 throw std::runtime_error(
"boolean values should be 0 or 1");
439 }
else if (vector_type ==
"factor" || (version.equals(1, 0) && vector_type ==
"ordered")) {
440 who_failed = ORDERED;
441 bool ordered =
false;
442 if (vector_type ==
"ordered") {
444 }
else if (handle.exists(
"ordered")) {
446 auto ohandle = handle.openDataSet(
"ordered");
447 if (ohandle.getSpace().getSimpleExtentNdims() != 0) {
448 throw std::runtime_error(
"expected 'ordered' to be a scalar dataset");
450 if (ritsuko::hdf5::exceeds_integer_limit(ohandle, 32,
true)) {
451 throw std::runtime_error(
"'ordered' value cannot be represented by a 32-bit integer");
453 std::int32_t tmp_ordered = 0;
454 ohandle.read(&tmp_ordered, H5::PredType::NATIVE_INT32);
455 ordered = tmp_ordered > 0;
457 std::throw_with_nested(std::runtime_error(
"failed to validate 'ordered'"));
462 auto levhandle = handle.openDataSet(
"levels");
463 if (!ritsuko::hdf5::is_utf8_string(levhandle)) {
464 throw std::runtime_error(
"expected a datatype that can be represented by a UTF-8 string for 'levels'");
466 auto lspace = levhandle.getSpace();
467 if (lspace.getSimpleExtentNdims() != 1) {
468 throw std::runtime_error(
"expected a 1-dimensional dataset for 'levels'");
471 lspace.getSimpleExtentDims(&levlen);
476 std::unordered_set<std::string> present;
477 ritsuko::hdf5::Stream1dStringDataset stream(
481 ritsuko::hdf5::Stream1dStringDatasetOptions opt;
482 opt.contiguous_chunk_size = options.buffer_size;
487 iterate_stream<std::string>(
489 [&](hsize_t pos, std::string val) ->
void {
490 if (present.find(val) != present.end()) {
491 throw std::runtime_error(
"detected duplicated factor level '" + val +
"'");
493 fptr->set_level(pos, val);
494 present.insert(std::move(val));
503 [&](std::int32_t x) ->
void {
505 throw std::runtime_error(
"factor codes should be non-negative");
507 throw std::runtime_error(
"factor codes should be less than the number of levels");
514 }
else if (vector_type ==
"vls" && !version.lt(1, 4)) {
515 constexpr auto precision = std::numeric_limits<std::uint64_t>::digits;
516 ritsuko::cvls::validate_pointer_datatype(dhandle, precision, precision);
519 auto hhandle = handle.openDataSet(
"heap");
520 auto hlen = ritsuko::cvls::validate_heap(hhandle);
523 const char* placeholder_name =
"missing-value-placeholder";
524 std::optional<std::string> missingness;
525 if (dhandle.attrExists(placeholder_name)) {
527 auto attr = dhandle.openAttribute(placeholder_name);
528 validate_string_missing_placeholder(attr);
529 missingness = ritsuko::hdf5::read_scalar_string(attr);
531 std::throw_with_nested(std::runtime_error(
"failed to validate the '" + std::string(placeholder_name) +
"'"));
538 auto set = [&](hsize_t i, std::string x) ->
void {
539 if (missingness.has_value() && x == *missingness) {
542 ptr->set(i, std::move(x));
547 ritsuko::cvls::Pointer<std::uint64_t, std::uint64_t> vlsptr;
548 dhandle.read(&vlsptr, ritsuko::cvls::define_pointer_datatype<std::uint64_t, std::uint64_t>());
549 if (ritsuko::cvls::is_Pointer_out_of_range(vlsptr, hlen)) {
550 throw std::runtime_error(
"compressed VLS pointer is out of range of the heap");
553 H5::DataSpace dspace(1, &hlen);
554 const hsize_t len = vlsptr.length;
555 const hsize_t offset = vlsptr.offset;
556 dspace.selectHyperslab(H5S_SELECT_SET, &len, &offset);
557 H5::DataSpace mspace(1, &len);
559 std::vector<std::uint8_t> buffer(vlsptr.length);
560 hhandle.read(buffer.data(), H5::PredType::NATIVE_UINT8, mspace, dspace);
561 auto cptr =
reinterpret_cast<const char*
>(buffer.data());
562 set(0, std::string(cptr, cptr + ritsuko::strnlen(cptr, vlsptr.length)));
565 ritsuko::cvls::Stream1dArray<std::uint64_t, std::uint64_t> stream(
571 ritsuko::cvls::Stream1dArrayOptions opt;
572 opt.contiguous_chunk_size = options.buffer_size;
576 iterate_stream<std::string>(stream, set);
579 }
else if (vector_type ==
"string" || (version.equals(1, 0) && (vector_type ==
"date" || vector_type ==
"date-time"))) {
581 if (version.equals(1, 0)) {
582 if (vector_type ==
"date") {
583 format = StringVector::DATE;
584 }
else if (vector_type ==
"date-time") {
585 format = StringVector::DATETIME;
588 }
else if (handle.exists(
"format")) {
590 auto fhandle = handle.openDataSet(
"format");
591 if (fhandle.getSpace().getSimpleExtentNdims() != 0) {
592 throw std::runtime_error(
"expected a scalar dataset");
594 if (!ritsuko::hdf5::is_utf8_string(fhandle)) {
595 throw std::runtime_error(
"expected a datatype that can be represented by a UTF-8 encoded string");
598 auto x = ritsuko::hdf5::read_scalar_string(fhandle);
600 format = StringVector::DATE;
601 }
else if (x ==
"date-time") {
602 format = StringVector::DATETIME;
604 throw std::runtime_error(
"unsupported format '" + x +
"'");
611 if (format == StringVector::NONE) {
616 [](
const std::string&) ->
void {},
620 }
else if (format == StringVector::DATE) {
625 [&](
const std::string& x) ->
void {
626 if (!ritsuko::is_date(x.c_str(), x.size())) {
627 throw std::runtime_error(
"dates should follow YYYY-MM-DD formatting");
633 }
else if (format == StringVector::DATETIME) {
638 [&](
const std::string& x) ->
void {
639 if (!ritsuko::is_rfc3339(x.c_str(), x.size())) {
640 throw std::runtime_error(
"date-times should follow the Internet Date/Time format");
647 }
else if (vector_type ==
"number") {
654 [](
double) ->
void {},
660 throw std::runtime_error(
"unknown vector type '" + vector_type +
"'");
664 switch (who_failed) {
665 case DATA: desc =
"data";
break;
666 case ORDERED: desc =
"ordered";
break;
667 case LEVELS: desc =
"levels";
break;
668 case HEAP: desc =
"heap";
break;
669 case FORMAT: desc =
"format";
break;
671 std::throw_with_nested(std::runtime_error(
"failed to read '" + desc +
"'"));
675 auto vptr =
static_cast<Vector*
>(output.get());
677 auto nhandle = handle.openDataSet(
"names");
678 parse_names(nhandle, vptr, options);
680 std::throw_with_nested(std::runtime_error(
"failed to read 'names'"));
684 }
else if (object_type ==
"nothing") {
685 output.reset(Provisioner_::new_Nothing());
687 }
else if (object_type ==
"external") {
689 auto ihandle = handle.openDataSet(
"index");
690 if (ritsuko::hdf5::exceeds_integer_limit(ihandle, 32,
true)) {
691 throw std::runtime_error(
"external index at 'index' cannot be represented by a 32-bit signed integer");
694 if (ihandle.getSpace().getSimpleExtentNdims() != 0) {
695 throw std::runtime_error(
"expected scalar dataset at 'index'");
699 ihandle.read(&idx, H5::PredType::NATIVE_INT32);
701 throw std::runtime_error(
"external index at 'index' should be non-negative");
703 throw std::runtime_error(
"external index at 'index' is out of range");
706 output.reset(Provisioner_::new_External(ext.get(idx)));
708 std::throw_with_nested(std::runtime_error(
"failed to read 'index'"));
712 throw std::runtime_error(
"unknown uzuki2 object type '" + object_type +
"'");
771template<
class Provisioner_,
class Externals_>
774 if (group.attrExists(
"uzuki_version")) {
776 auto vhandle = group.openAttribute(
"uzuki_version");
777 auto ver_str = safe_read_scalar_string_attribute(vhandle);
778 auto vraw = ritsuko::parse_version_string(ver_str.c_str(), ver_str.size(),
true);
779 version.major = vraw.major;
780 version.minor = vraw.minor;
782 std::throw_with_nested(std::runtime_error(
"failed to read the 'uzuki_version' attribute"));
786 ExternalTracker etrack(std::move(ext));
787 auto ptr = parse_inner<Provisioner_>(group, etrack, version, options);
790 throw std::runtime_error(
"top-level object should represent an R list");
794 return ParsedList(std::move(ptr), std::move(version));
816template<
class Provisioner_,
class Externals_>
819 H5::H5File fhandle(file, H5F_ACC_RDONLY);
822 std::throw_with_nested(std::runtime_error(
"failed to parse '" + name +
"' in '" + file +
"'"));
838inline void validate(
const H5::Group& group,
int num_external,
const Options& options) {
855inline void validate(
const std::string& file,
const std::string& name,
int num_external,
const Options& options) {
Dummy classes for parsing without storing the results.
Class to hold the parsed list.
Dummy class satisfying the Externals_ interface of hdf5::parse().
Definition Dummy.hpp:131
Format
Definition interfaces.hpp:159
Defines the interfaces to use in HDF5 parsing.
Container_ create(Value_ x, Args_ &&... args)
constexpr bool is_equal(Left_ left, Right_ right)
constexpr bool is_greater_than_or_equal(Left_ left, Right_ right)
constexpr Dest_ cap(Value_ x)
constexpr Dest_ cast(Value_ x)
ParsedList parse(const H5::Group &group, Externals_ ext, const Options &options)
Definition parse_hdf5.hpp:772
void validate(const H5::Group &group, int num_external, const Options &options)
Definition parse_hdf5.hpp:838
Parse an R list from a HDF5 or JSON file.
Definition parse_json.hpp:30
Results of parsing a list from file.
Definition ParsedList.hpp:19
Options for HDF5 file parsing.
Definition parse_hdf5.hpp:44
hsize_t buffer_size
Definition parse_hdf5.hpp:49
bool strict_list
Definition parse_hdf5.hpp:54