1#ifndef UZUKI2_PARSE_HPP
2#define UZUKI2_PARSE_HPP
11#include <unordered_set>
15#include "ritsuko/ritsuko.hpp"
16#include "sanisizer/sanisizer.hpp"
20#include "ExternalTracker.hpp"
44inline void validate_numeric_missing_placeholder(
const H5::Attribute& attr,
const H5::DataSet& data,
const Version& version) {
45 if (attr.getSpace().getSimpleExtentNdims() != 0) {
46 throw std::runtime_error(
"expected the '" + ritsuko::hdf5::get_name(attr) +
"' attribute to be a scalar");
48 if (version.lt(1, 2)) {
49 if (attr.getDataType().getClass() != data.getDataType().getClass()) {
50 throw std::runtime_error(
"expected the '" + ritsuko::hdf5::get_name(attr) +
"' attribute to have the same type class as its dataset");
53 if (attr.getDataType() != data.getDataType()) {
54 throw std::runtime_error(
"expected the '" + ritsuko::hdf5::get_name(attr) +
"' attribute to have the same type as its dataset");
59inline void validate_string_missing_placeholder(
const H5::Attribute& attr) {
60 if (attr.getSpace().getSimpleExtentNdims() != 0) {
61 throw std::runtime_error(
"expected the '" + ritsuko::hdf5::get_name(attr) +
"' attribute to be a scalar");
63 if (!ritsuko::hdf5::is_utf8_string(attr)) {
64 throw std::runtime_error(
"expected the '" + ritsuko::hdf5::get_name(attr) +
"' attribute to be a UTF-8 string");
68template<
class Host_,
class Function_>
69void parse_integer_like(
const H5::DataSet& handle, Host_* ptr,
bool is_scalar, Function_ check,
const Version& version)
try {
70 if (ritsuko::hdf5::exceeds_integer_limit(handle, 32,
true)) {
71 throw std::runtime_error(
"dataset cannot be represented by 32-bit signed integers");
74 bool has_missing =
false;
75 std::int32_t missing_value = -2147483648;
76 if (version.equals(1, 0)) {
79 const char* placeholder_name =
"missing-value-placeholder";
80 has_missing = handle.attrExists(placeholder_name);
82 auto attr = handle.openAttribute(placeholder_name);
83 validate_numeric_missing_placeholder(attr, handle, version);
84 attr.read(H5::PredType::NATIVE_INT32, &missing_value);
88 auto set = [&](hsize_t i, std::int32_t x) ->
void {
89 if (has_missing && x == missing_value) {
99 handle.read(&value, H5::PredType::NATIVE_INT32);
102 const hsize_t full_length = ptr->size();
103 ritsuko::hdf5::Stream1dNumericDataset<std::int32_t> stream(&handle, full_length);
104 auto buffer = sanisizer::create<std::vector<std::int32_t> >(stream.chunk_size());
106 const auto available = stream.load(buffer.data());
107 if (available == 0) {
110 for (I<
decltype(available)> i = 0; i < available; ++i) {
111 set(i + stream.start(), buffer[i]);
116}
catch (std::exception& e) {
117 throw std::runtime_error(
"failed to load integer dataset at '" + ritsuko::hdf5::get_name(handle) +
"'; " + std::string(e.what()));
120template<
class Host_,
class Function_>
121void parse_string_like(
const H5::DataSet& handle, Host_* ptr,
bool is_scalar, Function_ check)
try {
122 if (!ritsuko::hdf5::is_utf8_string(handle)) {
123 throw std::runtime_error(
"expected a datatype that can be represented by a UTF-8 string");
126 std::optional<std::string> missingness;
127 const char* placeholder_name =
"missing-value-placeholder";
128 if (handle.attrExists(placeholder_name)) {
129 auto attr = handle.openAttribute(placeholder_name);
130 validate_string_missing_placeholder(attr);
131 missingness = ritsuko::hdf5::read_scalar_string(attr);
134 auto set = [&](hsize_t i, std::string x) ->
void {
135 if (missingness.has_value() && x == *missingness) {
139 ptr->set(i, std::move(x));
144 auto x = ritsuko::hdf5::read_scalar_string(handle);
145 set(0, std::move(x));
147 const hsize_t full_length = ptr->size();
148 ritsuko::hdf5::Stream1dStringDataset stream(&handle, full_length);
149 auto buffer = sanisizer::create<std::vector<std::string> >(stream.chunk_size());
151 const auto available = stream.load(buffer.data());
152 if (available == 0) {
155 for (I<
decltype(available)> i = 0; i < available; ++i) {
156 set(i + stream.start(), std::move(buffer[i]));
161}
catch (std::exception& e) {
162 throw std::runtime_error(
"failed to load string dataset at '" + ritsuko::hdf5::get_name(handle) +
"'; " + std::string(e.what()));
165inline double r_missing_value() {
166 std::uint32_t tmp_value = 1;
167 auto tmp_ptr =
reinterpret_cast<unsigned char*
>(&tmp_value);
171 double missing_value = 0;
172 auto missing_ptr =
reinterpret_cast<unsigned char*
>(&missing_value);
175 if (tmp_ptr[0] == 1) {
176 missing_ptr +=
sizeof(double) - 1;
181 *(missing_ptr += step) = 0xf0;
182 *(missing_ptr += step) = 0x00;
183 *(missing_ptr += step) = 0x00;
184 *(missing_ptr += step) = 0x00;
185 *(missing_ptr += step) = 0x00;
186 *(missing_ptr += step) = 0x07;
187 *(missing_ptr += step) = 0xa2;
189 return missing_value;
192template<
class Host_,
class Function_>
193void parse_numbers(
const H5::DataSet& handle, Host_* ptr,
bool is_scalar, Function_ check,
const Version& version)
try {
194 if (version.lt(1, 3)) {
195 if (handle.getTypeClass() != H5T_FLOAT) {
196 throw std::runtime_error(
"expected a floating-point dataset");
199 if (ritsuko::hdf5::exceeds_float_limit(handle, 64)) {
200 throw std::runtime_error(
"dataset cannot be represented by 64-bit floats");
205 static_assert(std::numeric_limits<double>::is_iec559);
207 bool has_missing =
false;
208 double missing_value = 0;
209 if (version.equals(1, 0)) {
211 missing_value = r_missing_value();
213 const char* placeholder_name =
"missing-value-placeholder";
214 has_missing = handle.attrExists(placeholder_name);
216 auto attr = handle.openAttribute(placeholder_name);
217 validate_numeric_missing_placeholder(attr, handle, version);
218 attr.read(H5::PredType::NATIVE_DOUBLE, &missing_value);
222 bool should_compare_nan = version.lt(1, 3);
223 bool is_placeholder_nan = std::isnan(missing_value);
224 auto is_missing_value = [&](
double val) ->
bool {
225 if (should_compare_nan) {
226 auto xptr =
reinterpret_cast<const unsigned char*
>(&missing_value);
227 auto yptr =
reinterpret_cast<const unsigned char*
>(&val);
228 return std::memcmp(xptr, yptr,
sizeof(
double)) == 0;
229 }
else if (is_placeholder_nan) {
230 return std::isnan(val);
232 return val == missing_value;
236 auto set = [&](hsize_t i,
double x) ->
void {
237 if (has_missing && is_missing_value(x)) {
247 handle.read(&val, H5::PredType::NATIVE_DOUBLE);
250 const hsize_t full_length = ptr->size();
251 ritsuko::hdf5::Stream1dNumericDataset<double> stream(&handle, full_length);
252 std::vector<double> buffer(stream.chunk_size());
254 const auto available = stream.load(buffer.data());
255 if (available == 0) {
258 for (I<
decltype(available)> i = 0; i < available; ++i) {
259 set(i + stream.start(), buffer[i]);
264}
catch (std::exception& e) {
265 throw std::runtime_error(
"failed to load floating-point dataset at '" + ritsuko::hdf5::get_name(handle) +
"'; " + std::string(e.what()));
269void extract_names(
const H5::Group& handle, Host_* ptr)
try {
270 auto nhandle = handle.openDataSet(
"names");
271 if (!ritsuko::hdf5::is_utf8_string(nhandle)) {
272 throw std::runtime_error(
"expected 'names' to use a datatype that can be represented by a UTF-8 string");
275 const auto space = nhandle.getSpace();
276 if (space.getSimpleExtentNdims() != 1) {
277 throw std::runtime_error(
"expected 'names' to be a 1-dimensional dataset");
280 space.getSimpleExtentDims(&nlen);
281 if (!sanisizer::is_equal(nlen, ptr->size())) {
282 throw std::runtime_error(
"number of names should be equal to the object length");
285 ritsuko::hdf5::Stream1dStringDataset stream(&nhandle, nlen);
286 std::vector<std::string> buffer(stream.chunk_size());
288 const auto available = stream.load(buffer.data());
289 if (available == 0) {
292 for (I<
decltype(available)> i = 0; i < available; ++i) {
293 ptr->set_name(i + stream.start(), std::move(buffer[i]));
297}
catch (std::exception& e) {
298 throw std::runtime_error(
"failed to load names at '" + ritsuko::hdf5::get_name(handle) +
"'; " + std::string(e.what()));
301inline std::string read_uzuki_attr(
const H5::Group& handle,
const char* name) {
302 const auto attr = handle.openAttribute(name);
303 if (attr.getSpace().getSimpleExtentNdims() != 0) {
304 throw std::runtime_error(
"'" + std::string(name) +
"' should be a scalar attribute in '" + ritsuko::hdf5::get_name(handle) +
"'");
306 if (!ritsuko::hdf5::is_utf8_string(attr)) {
307 throw std::runtime_error(
"'" + std::string(name) +
"' should be stored as a UTF-8 string in '" + ritsuko::hdf5::get_name(handle) +
"'");
309 return ritsuko::hdf5::read_scalar_string(attr);
312template<
class Provisioner_,
class Externals_>
313std::shared_ptr<Base> parse_inner(
const H5::Group& handle, Externals_& ext,
const Version& version)
try {
314 auto object_type = read_uzuki_attr(handle,
"uzuki_object");
315 std::shared_ptr<Base> output;
317 if (object_type ==
"list") {
318 auto dhandle = handle.openGroup(
"data");
319 const auto len = dhandle.getNumObjs();
321 bool named = handle.exists(
"names");
322 auto lptr = Provisioner_::new_List(sanisizer::cast<std::size_t>(len), named);
325 for (I<
decltype(len)> i = 0; i < len; ++i) {
326 const auto istr = std::to_string(i);
328 auto lhandle = dhandle.openGroup(istr);
329 lptr->set(i, parse_inner<Provisioner_>(lhandle, ext, version));
330 }
catch (std::exception& e) {
331 throw std::runtime_error(
"failed to parse list element " + istr +
"; " + std::string(e.what()));
336 extract_names(handle, lptr);
339 }
else if (object_type ==
"vector") {
340 auto dhandle = handle.openDataSet(
"data");
341 const auto dspace = dhandle.getSpace();
342 const auto ndims = dspace.getSimpleExtentNdims();
345 bool is_scalar =
false;
348 }
else if (ndims == 1) {
349 dspace.getSimpleExtentDims(&len);
351 throw std::runtime_error(
"expected a scalar or 1-dimensional dataset in 'data'");
354 const bool named = handle.exists(
"names");
355 auto vector_type = read_uzuki_attr(handle,
"uzuki_type");
356 if (vector_type ==
"integer") {
357 auto iptr = Provisioner_::new_Integer(sanisizer::cast<std::size_t>(len), named, is_scalar);
363 [](std::int32_t) ->
void {},
367 }
else if (vector_type ==
"boolean") {
368 auto bptr = Provisioner_::new_Boolean(sanisizer::cast<std::size_t>(len), named, is_scalar);
374 [&](std::int32_t x) ->
void {
375 if (x != 0 && x != 1) {
376 throw std::runtime_error(
"boolean values should be 0 or 1");
382 }
else if (vector_type ==
"factor" || (version.equals(1, 0) && vector_type ==
"ordered")) {
383 auto levhandle = handle.openDataSet(
"levels");
384 if (!ritsuko::hdf5::is_utf8_string(levhandle)) {
385 throw std::runtime_error(
"expected a datatype that can be represented by a UTF-8 string for 'levels'");
389 auto lspace = levhandle.getSpace();
390 if (lspace.getSimpleExtentNdims() != 1) {
391 throw std::runtime_error(
"expected a 1-dimensional dataset for 'levels'");
393 lspace.getSimpleExtentDims(&levlen);
395 bool ordered =
false;
396 if (vector_type ==
"ordered") {
398 }
else if (handle.exists(
"ordered")) {
399 auto ohandle = handle.openDataSet(
"ordered");
400 if (ohandle.getSpace().getSimpleExtentNdims() != 0) {
401 throw std::runtime_error(
"expected 'ordered' to be a scalar dataset");
403 if (ritsuko::hdf5::exceeds_integer_limit(ohandle, 32,
true)) {
404 throw std::runtime_error(
"'ordered' value cannot be represented by a 32-bit integer");
406 std::int32_t tmp_ordered = 0;
407 ohandle.read(&tmp_ordered, H5::PredType::NATIVE_INT32);
408 ordered = tmp_ordered > 0;
411 auto fptr = Provisioner_::new_Factor(sanisizer::cast<std::size_t>(len), named, is_scalar, sanisizer::cast<std::size_t>(levlen), ordered);
417 [&](std::int32_t x) ->
void {
419 throw std::runtime_error(
"factor codes should be non-negative");
420 }
else if (sanisizer::is_greater_than_or_equal(x, levlen)) {
421 throw std::runtime_error(
"factor codes should be less than the number of levels");
427 std::unordered_set<std::string> present;
428 ritsuko::hdf5::Stream1dStringDataset stream(&levhandle, levlen);
429 std::vector<std::string> buffer(stream.chunk_size());
431 const auto available = stream.load(buffer.data());
432 if (available == 0) {
435 for (I<
decltype(available)> i = 0; i < available; ++i) {
437 if (present.find(x) != present.end()) {
438 throw std::runtime_error(
"levels should be unique");
440 fptr->set_level(i + stream.start(), x);
441 present.insert(std::move(x));
445 }
else if (vector_type ==
"vls" && !version.lt(1, 4)) {
446 constexpr auto precision = std::numeric_limits<std::uint64_t>::digits;
447 ritsuko::cvls::validate_pointer_datatype(dhandle, precision, precision);
448 auto hhandle = handle.openDataSet(
"heap");
449 ritsuko::cvls::validate_heap(hhandle);
451 const char* placeholder_name =
"missing-value-placeholder";
452 std::optional<std::string> missingness;
453 if (dhandle.attrExists(placeholder_name)) {
454 auto attr = dhandle.openAttribute(placeholder_name);
455 validate_string_missing_placeholder(attr);
456 missingness = ritsuko::hdf5::read_scalar_string(attr);
459 auto ptr = Provisioner_::new_String(sanisizer::cast<std::size_t>(len), named, is_scalar, StringVector::NONE);
462 auto set = [&](hsize_t i, std::string x) ->
void {
463 if (missingness.has_value() && x == *missingness) {
466 ptr->set(i, std::move(x));
471 ritsuko::cvls::Pointer<std::uint64_t, std::uint64_t> vlsptr;
472 dhandle.read(&vlsptr, ritsuko::cvls::define_pointer_datatype<std::uint64_t, std::uint64_t>());
475 hhandle.getSpace().getSimpleExtentDims(&hlen);
476 if (ritsuko::cvls::is_Pointer_out_of_range(vlsptr, hlen)) {
477 throw std::runtime_error(
"compressed VLS pointer in '" + ritsuko::hdf5::get_name(dhandle) +
"' is out of range of the heap");
480 H5::DataSpace dspace(1, &hlen);
481 const hsize_t len = vlsptr.length;
482 const hsize_t offset = vlsptr.offset;
483 dspace.selectHyperslab(H5S_SELECT_SET, &len, &offset);
484 H5::DataSpace mspace(1, &len);
486 std::vector<std::uint8_t> buffer(vlsptr.length);
487 hhandle.read(buffer.data(), H5::PredType::NATIVE_UINT8, mspace, dspace);
488 auto cptr =
reinterpret_cast<const char*
>(buffer.data());
489 set(0, std::string(cptr, cptr + ritsuko::hdf5::strnlen(cptr, vlsptr.length)));
492 ritsuko::cvls::Stream1dArray<std::uint64_t, std::uint64_t> stream(&dhandle, len, &hhandle);
493 std::vector<std::string> buffer(stream.chunk_size());
495 const auto available = stream.load(buffer.data());
496 if (available == 0) {
499 for (I<
decltype(available)> i = 0; i < available; ++i) {
500 set(i + stream.start(), std::move(buffer[i]));
505 }
else if (vector_type ==
"string" || (version.equals(1, 0) && (vector_type ==
"date" || vector_type ==
"date-time"))) {
507 if (version.equals(1, 0)) {
508 if (vector_type ==
"date") {
509 format = StringVector::DATE;
510 }
else if (vector_type ==
"date-time") {
511 format = StringVector::DATETIME;
514 }
else if (handle.exists(
"format")) {
515 auto fhandle = handle.openDataSet(
"format");
516 if (fhandle.getSpace().getSimpleExtentNdims() != 0) {
517 throw std::runtime_error(
"expected 'format' to be a scalar dataset");
519 if (!ritsuko::hdf5::is_utf8_string(fhandle)) {
520 throw std::runtime_error(
"expected 'format' to use a datatype that can be represented by a UTF-8 encoded string");
522 auto x = ritsuko::hdf5::read_scalar_string(fhandle);
524 format = StringVector::DATE;
525 }
else if (x ==
"date-time") {
526 format = StringVector::DATETIME;
528 throw std::runtime_error(
"unsupported format '" + x +
"'");
532 auto sptr = Provisioner_::new_String(sanisizer::cast<std::size_t>(len), named, is_scalar, format);
534 if (format == StringVector::NONE) {
539 [](
const std::string&) ->
void {}
542 }
else if (format == StringVector::DATE) {
547 [&](
const std::string& x) ->
void {
548 if (!ritsuko::is_date(x.c_str(), x.size())) {
549 throw std::runtime_error(
"dates should follow YYYY-MM-DD formatting");
554 }
else if (format == StringVector::DATETIME) {
559 [&](
const std::string& x) ->
void {
560 if (!ritsuko::is_rfc3339(x.c_str(), x.size())) {
561 throw std::runtime_error(
"date-times should follow the Internet Date/Time format");
567 }
else if (vector_type ==
"number") {
568 auto dptr = Provisioner_::new_Number(sanisizer::cast<std::size_t>(len), named, is_scalar);
574 [](
double) ->
void {},
579 throw std::runtime_error(
"unknown vector type '" + vector_type +
"'");
583 auto vptr =
static_cast<Vector*
>(output.get());
584 extract_names(handle, vptr);
587 }
else if (object_type ==
"nothing") {
588 output.reset(Provisioner_::new_Nothing());
590 }
else if (object_type ==
"external") {
591 auto ihandle = handle.openDataSet(
"index");
592 if (ritsuko::hdf5::exceeds_integer_limit(ihandle, 32,
true)) {
593 throw std::runtime_error(
"external index at 'index' cannot be represented by a 32-bit signed integer");
596 if (ihandle.getSpace().getSimpleExtentNdims() != 0) {
597 throw std::runtime_error(
"expected scalar dataset at 'index'");
601 ihandle.read(&idx, H5::PredType::NATIVE_INT32);
603 throw std::runtime_error(
"external index at 'index' should be non-negative");
604 }
else if (
static_cast<std::size_t
>(idx) >= ext.size()) {
605 throw std::runtime_error(
"external index at 'index' is out of range");
608 output.reset(Provisioner_::new_External(ext.get(idx)));
611 throw std::runtime_error(
"unknown uzuki2 object type '" + object_type +
"'");
615}
catch (std::exception& e) {
616 throw std::runtime_error(
"failed to load object at '" + ritsuko::hdf5::get_name(handle) +
"'; " + std::string(e.what()));
630 hsize_t buffer_size = 10000;
686template<
class Provisioner_,
class Externals_>
689 if (group.attrExists(
"uzuki_version")) {
690 auto ver_str = read_uzuki_attr(group,
"uzuki_version");
691 auto vraw = ritsuko::parse_version_string(ver_str.c_str(), ver_str.size(),
true);
692 version.major = vraw.major;
693 version.minor = vraw.minor;
696 ExternalTracker etrack(std::move(ext));
697 auto ptr = parse_inner<Provisioner_>(group, etrack, version);
700 throw std::runtime_error(
"top-level object should represent an R list");
704 return ParsedList(std::move(ptr), std::move(version));
723template<
class Provisioner_,
class Externals_>
725 H5::H5File fhandle(file, H5F_ACC_RDONLY);
737inline void validate(
const H5::Group& group,
int num_external,
const Options& options) {
750inline 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.
ParsedList parse(const H5::Group &group, Externals_ ext, const Options &options)
Definition parse_hdf5.hpp:687
void validate(const H5::Group &group, int num_external, const Options &options)
Definition parse_hdf5.hpp:737
Parse an R list from a HDF5 or JSON file.
Definition parse_json.hpp:31
Results of parsing a list from file.
Definition ParsedList.hpp:19
Options for HDF5 file parsing.
Definition parse_hdf5.hpp:626
bool strict_list
Definition parse_hdf5.hpp:638