uzuki2
Recovering R lists faithfully from HDF5 or JSON
Loading...
Searching...
No Matches
parse_json.hpp
Go to the documentation of this file.
1#ifndef UZUKI2_PARSE_JSON_HPP
2#define UZUKI2_PARSE_JSON_HPP
3
4#include <memory>
5#include <vector>
6#include <string>
7#include <stdexcept>
8#include <exception>
9#include <unordered_map>
10#include <unordered_set>
11#include <type_traits>
12#include <cstddef>
13
14#include "byteme/byteme.hpp"
15#include "millijson/millijson.hpp"
16#include "ritsuko/ritsuko.hpp"
18
19#include "interfaces.hpp"
20#include "Dummy.hpp"
21#include "ExternalTracker.hpp"
22#include "ParsedList.hpp"
23#include "utils.hpp"
24
30namespace uzuki2 {
31
40namespace json {
41
45inline const std::vector<std::shared_ptr<millijson::Base> >& extract_array(const std::unordered_map<std::string, std::shared_ptr<millijson::Base> >& properties, const std::string& name) {
46 auto it = properties.find(name);
47 if (it == properties.end()) {
48 throw std::runtime_error("no such property");
49 }
50
51 const auto& ptr = it->second;
52 if (ptr->type() != millijson::ARRAY) {
53 throw std::runtime_error("expected an array");
54 }
55
56 return static_cast<const millijson::Array*>(ptr.get())->value();
57}
58
59inline const millijson::Array* extract_array_or_null(const std::unordered_map<std::string, std::shared_ptr<millijson::Base> >& properties, const std::string& name) {
60 auto it = properties.find(name);
61 if (it == properties.end()) {
62 return NULL;
63 }
64
65 const auto ptr = it->second;
66 if (ptr->type() != millijson::ARRAY) {
67 throw std::runtime_error("expected an array");
68 }
69
70 return static_cast<const millijson::Array*>(ptr.get());
71}
72
73template<class Destination_>
74void fill_names(const millijson::Array* names_ptr, Destination_* dest) {
75 const auto& names = names_ptr->value();
76 if (names.size() != dest->size()) {
77 throw std::runtime_error("unexpected number of names");
78 }
79
80 const auto nnames = names.size();
81 for (I<decltype(nnames)> i = 0; i < nnames; ++i) {
82 if (names[i]->type() != millijson::STRING) {
83 throw std::runtime_error("expected a string at element " + ritsuko::itoa(i));
84 }
85 dest->set_name(i, static_cast<const millijson::String*>(names[i].get())->value());
86 }
87}
88
89template<class Function_>
90auto process_array_or_scalar_values(const std::unordered_map<std::string, std::shared_ptr<millijson::Base> >& properties, Function_ fun) {
91 const millijson::Array* names_ptr;
92 try {
93 names_ptr = extract_array_or_null(properties, "names");
94 } catch (...) {
95 std::throw_with_nested(std::runtime_error("failed to read 'names'"));
96 }
97
98 auto out_ptr = [&]{
99 try {
100 auto vIt = properties.find("values");
101 if (vIt == properties.end()) {
102 throw std::runtime_error("no such property");
103 }
104
105 const auto& values_ptr = vIt->second;
106 if (values_ptr->type() == millijson::ARRAY) {
107 return fun(static_cast<const millijson::Array*>(values_ptr.get())->value(), names_ptr != NULL, false);
108 } else {
109 std::vector<std::shared_ptr<millijson::Base> > temp { values_ptr };
110 return fun(temp, names_ptr != NULL, true);
111 }
112 } catch (...) {
113 std::throw_with_nested(std::runtime_error("failed to read 'values'"));
114 }
115 }();
116
117 if (names_ptr != NULL) {
118 try {
119 fill_names(names_ptr, out_ptr);
120 } catch (...) {
121 std::throw_with_nested(std::runtime_error("failed to parse 'names'"));
122 }
123 }
124 return out_ptr;
125}
126
127template<class Destination_, class Function_>
128void extract_integers(const std::vector<std::shared_ptr<millijson::Base> >& values, Destination_* dest, Function_ check, const Version& version) {
129 const auto n = values.size();
130 for (I<decltype(n)> i = 0; i < n; ++i) {
131 try {
132 if (values[i]->type() == millijson::NOTHING) {
133 dest->set_missing(i);
134 continue;
135 }
136
137 if (values[i]->type() != millijson::NUMBER) {
138 throw std::runtime_error("expected a number");
139 }
140
141 auto val = static_cast<const millijson::Number*>(values[i].get())->value();
142 if (val != std::floor(val)) {
143 throw std::runtime_error("expected an integer");
144 }
145
146 // 32-bit integers are always representable by doubles, as the latter have 53 bits of precision.
147 constexpr double upper = std::numeric_limits<std::int32_t>::max();
148 constexpr double lower = std::numeric_limits<std::int32_t>::min();
149 if (val < lower || val > upper) {
150 throw std::runtime_error("value cannot be represented by a 32-bit signed integer");
151 }
152
153 const std::int32_t ival = val;
154 if (version.equals(1, 0) && ival == -2147483648) {
155 dest->set_missing(i);
156 continue;
157 }
158
159 check(ival);
160 dest->set(i, ival);
161 } catch (...) {
162 std::throw_with_nested(std::runtime_error("failed to process element " + ritsuko::itoa(i)));
163 }
164 }
165}
166
167template<class Destination_, class Function_>
168void extract_strings(const std::vector<std::shared_ptr<millijson::Base> >& values, Destination_* dest, Function_ check) {
169 const auto n = values.size();
170 for (I<decltype(n)> i = 0; i < n; ++i) {
171 try {
172 if (values[i]->type() == millijson::NOTHING) {
173 dest->set_missing(i);
174 continue;
175 }
176
177 if (values[i]->type() != millijson::STRING) {
178 throw std::runtime_error("expected a string");
179 }
180
181 const auto& str = static_cast<const millijson::String*>(values[i].get())->value();
182 check(str);
183 dest->set(i, str);
184 } catch (...) {
185 std::throw_with_nested(std::runtime_error("failed to process element " + ritsuko::itoa(i)));
186 }
187 }
188}
189
190template<class Provisioner_, class Externals_>
191std::shared_ptr<Base> parse_object(const millijson::Base* contents, Externals_& ext, const Version& version) {
192 if (contents->type() != millijson::OBJECT) {
193 throw std::runtime_error("each R object should be represented by a JSON object");
194 }
195 const auto& map = static_cast<const millijson::Object*>(contents)->value();
196
197 const auto& type = [&]() -> const std::string& {
198 try {
199 auto tIt = map.find("type");
200 if (tIt == map.end()) {
201 throw std::runtime_error("no such property");
202 }
203
204 const auto& type_ptr = tIt->second;
205 if (type_ptr->type() != millijson::STRING) {
206 throw std::runtime_error("expected a string");
207 }
208 return static_cast<const millijson::String*>(type_ptr.get())->value();
209 } catch (...) {
210 std::throw_with_nested(std::runtime_error("failed to read 'type'"));
211 }
212 }();
213
214 std::shared_ptr<Base> output;
215 if (type == "nothing") {
216 output.reset(Provisioner_::new_Nothing());
217
218 } else if (type == "external") {
219 try {
220 auto iIt = map.find("index");
221 if (iIt == map.end()) {
222 throw std::runtime_error("no such property");
223 }
224 const auto& index_ptr = iIt->second;
225 if (index_ptr->type() != millijson::NUMBER) {
226 throw std::runtime_error("expected a number");
227 }
228
229 const auto flt_index = static_cast<const millijson::Number*>(index_ptr.get())->value();
230 if (flt_index != std::floor(flt_index)) {
231 throw std::runtime_error("expected an integer");
232 } else if (flt_index < 0) {
233 throw std::runtime_error("expected a non-negative integer");
234 }
235
236 std::int32_t index;
237 try {
238 index = sanisizer::from_float<std::int32_t>(flt_index);
239 } catch (...) {
240 throw std::runtime_error("value should fit in a 32-bit signed integer");
241 }
242 if (sanisizer::is_greater_than_or_equal(index, ext.size())) {
243 throw std::runtime_error("external index out of range");
244 }
245 output.reset(Provisioner_::new_External(ext.get(index)));
246 } catch (...) {
247 std::throw_with_nested(std::runtime_error("failed to read 'index'"));
248 }
249
250 } else if (type == "integer") {
251 process_array_or_scalar_values(
252 map,
253 [&](const auto& vals, bool named, bool scalar) -> auto {
254 auto ptr = Provisioner_::new_Integer(sanisizer::cast<std::size_t>(vals.size()), named, scalar);
255 output.reset(ptr);
256 extract_integers(vals, ptr, [](std::int32_t) -> void {}, version);
257 return ptr;
258 }
259 );
260
261 } else if (type == "factor" || (version.equals(1, 0) && type == "ordered")) {
262 bool ordered = false;
263 if (type == "ordered") {
264 ordered = true;
265 } else {
266 auto oIt = map.find("ordered");
267 if (oIt != map.end()) {
268 if (oIt->second->type() != millijson::BOOLEAN) {
269 throw std::runtime_error("expected a boolean at 'ordered'");
270 }
271 ordered = static_cast<const millijson::Boolean*>((oIt->second).get())->value();
272 }
273 }
274
275 bool level_failed = true;
276 try {
277 const std::string level_field = "levels"; // use a separate variable to avoid issues with overzealous GCC warnings.
278 const auto& lvals = extract_array(map, level_field);
279 const auto nlevels = lvals.size();
280
281 level_failed = false;
282 auto fptr = process_array_or_scalar_values(
283 map,
284 [&](const auto& vals, bool named, bool scalar) -> auto {
285 auto ptr = Provisioner_::new_Factor(sanisizer::cast<std::size_t>(vals.size()), named, scalar, sanisizer::cast<std::size_t>(nlevels), ordered);
286 output.reset(ptr);
287 extract_integers(
288 vals,
289 ptr,
290 [&](std::int32_t x) -> void {
291 if (x < 0) {
292 throw std::runtime_error("factor code should be non-negative");
293 } else if (sanisizer::is_greater_than_or_equal(x, nlevels)) {
294 throw std::runtime_error("factor code should be less than the number of levels");
295 }
296 },
297 version
298 );
299 return ptr;
300 }
301 );
302
303 level_failed = true;
304 std::unordered_set<std::string> existing;
305 for (I<decltype(nlevels)> l = 0; l < nlevels; ++l) {
306 try {
307 if (lvals[l]->type() != millijson::STRING) {
308 throw std::runtime_error("expected a string");
309 }
310
311 const auto& level = static_cast<const millijson::String*>(lvals[l].get())->value();
312 if (existing.find(level) != existing.end()) {
313 throw std::runtime_error("detected duplicate level '" + level + "'");
314 }
315 fptr->set_level(l, level);
316 existing.insert(level);
317 } catch (...) {
318 std::throw_with_nested(std::runtime_error("failed to read element " + ritsuko::itoa(l)));
319 }
320 }
321 } catch (...) {
322 if (level_failed) {
323 std::throw_with_nested(std::runtime_error("failed to read 'levels'"));
324 } else {
325 throw;
326 }
327 }
328
329 } else if (type == "boolean") {
330 process_array_or_scalar_values(
331 map,
332 [&](const auto& vals, bool named, bool scalar) -> auto {
333 const auto n = vals.size();
334 auto ptr = Provisioner_::new_Boolean(sanisizer::cast<std::size_t>(n), named, scalar);
335 output.reset(ptr);
336
337 for (I<decltype(n)> i = 0; i < n; ++i) {
338 try {
339 if (vals[i]->type() == millijson::NOTHING) {
340 ptr->set_missing(i);
341 continue;
342 }
343
344 if (vals[i]->type() != millijson::BOOLEAN) {
345 throw std::runtime_error("expected a boolean");
346 }
347 ptr->set(i, static_cast<const millijson::Boolean*>(vals[i].get())->value());
348 } catch (...) {
349 std::throw_with_nested(std::runtime_error("failed to read element " + ritsuko::itoa(i)));
350 }
351 }
352
353 return ptr;
354 }
355 );
356
357 } else if (type == "number") {
358 process_array_or_scalar_values(
359 map,
360 [&](const auto& vals, bool named, bool scalar) -> auto {
361 const auto n = vals.size();
362 auto ptr = Provisioner_::new_Number(sanisizer::cast<std::size_t>(n), named, scalar);
363 output.reset(ptr);
364
365 for (I<decltype(n)> i = 0; i < n; ++i) {
366 try {
367 if (vals[i]->type() == millijson::NOTHING) {
368 ptr->set_missing(i);
369 continue;
370 }
371
372 if (vals[i]->type() == millijson::NUMBER) {
373 ptr->set(i, static_cast<const millijson::Number*>(vals[i].get())->value());
374 } else if (vals[i]->type() == millijson::STRING) {
375 auto str = static_cast<const millijson::String*>(vals[i].get())->value();
376 if (str == "NaN") {
377 ptr->set(i, std::numeric_limits<double>::quiet_NaN());
378 } else if (str == "Inf") {
379 ptr->set(i, std::numeric_limits<double>::infinity());
380 } else if (str == "-Inf") {
381 ptr->set(i, -std::numeric_limits<double>::infinity());
382 } else {
383 throw std::runtime_error("unsupported string '" + str + "'");
384 }
385 } else {
386 throw std::runtime_error("expected a number");
387 }
388 } catch (...) {
389 std::throw_with_nested(std::runtime_error("failed to read element " + ritsuko::itoa(i)));
390 }
391 }
392
393 return ptr;
394 }
395 );
396
397 } else if (type == "string" || (version.equals(1, 0) && (type == "date" || type == "date-time"))) {
398 StringVector::Format format = StringVector::NONE;
399 if (version.equals(1, 0)) {
400 if (type == "date") {
401 format = StringVector::DATE;
402 } else if (type == "date-time") {
403 format = StringVector::DATETIME;
404 }
405 } else {
406 auto fIt = map.find("format");
407 if (fIt != map.end()) {
408 if (fIt->second->type() != millijson::STRING) {
409 throw std::runtime_error("expected a string at 'format'");
410 }
411 auto fptr = static_cast<const millijson::String*>(fIt->second.get());
412 if (fptr->value() == "date") {
413 format = StringVector::DATE;
414 } else if (fptr->value() == "date-time") {
415 format = StringVector::DATETIME;
416 } else {
417 throw std::runtime_error("unsupported format '" + fptr->value() + "' at 'format'");
418 }
419 }
420 }
421
422 process_array_or_scalar_values(
423 map,
424 [&](const auto& vals, bool named, bool scalar) -> auto {
425 auto ptr = Provisioner_::new_String(sanisizer::cast<std::size_t>(vals.size()), named, scalar, format);
426 output.reset(ptr);
427
428 if (format == StringVector::NONE) {
429 extract_strings(vals, ptr, [](const std::string&) -> void {});
430 } else if (format == StringVector::DATE) {
431 extract_strings(
432 vals,
433 ptr,
434 [&](const std::string& x) -> void {
435 if (!ritsuko::is_date(x.c_str(), x.size())) {
436 throw std::runtime_error("dates should follow YYYY-MM-DD formatting");
437 }
438 }
439 );
440 } else if (format == StringVector::DATETIME) {
441 extract_strings(
442 vals,
443 ptr,
444 [&](const std::string& x) -> void {
445 if (!ritsuko::is_rfc3339(x.c_str(), x.size())) {
446 throw std::runtime_error("date-times should follow the Internet Date/Time format");
447 }
448 }
449 );
450 }
451
452 return ptr;
453 }
454 );
455
456 } else if (type == "list") {
457 const millijson::Array* names_ptr;
458 try {
459 names_ptr = extract_array_or_null(map, "names");
460 } catch (...) {
461 std::throw_with_nested(std::runtime_error("failed to read 'names'"));
462 }
463
464 List* lptr;
465 try {
466 const std::string value_field = "values"; // use a separate variable to avoid issues with overzealous GCC warnings.
467 const auto& vals = extract_array(map, value_field);
468
469 const auto n = vals.size();
470 lptr = Provisioner_::new_List(sanisizer::cast<std::size_t>(n), names_ptr != NULL);
471 output.reset(lptr);
472
473 for (I<decltype(n)> i = 0; i < n; ++i) {
474 try {
475 lptr->set(i, parse_object<Provisioner_>(vals[i].get(), ext, version));
476 } catch (...) {
477 std::throw_with_nested(std::runtime_error("failed to read element " + ritsuko::itoa(i)));
478 }
479 }
480 } catch (...) {
481 std::throw_with_nested(std::runtime_error("failed to read 'values'"));
482 }
483
484 if (names_ptr != NULL) {
485 try {
486 fill_names(names_ptr, lptr);
487 } catch (...) {
488 std::throw_with_nested(std::runtime_error("failed to read 'names'"));
489 }
490 }
491
492 } else {
493 throw std::runtime_error("unknown object type '" + type + "' in 'type'");
494 }
495
496 return output;
497}
505struct Options {
510 bool parallel = false;
511
515 bool strict_list = true;
516
522};
523
544template<class Provisioner_, class Reader_, class Externals_>
545ParsedList parse(Reader_& reader, Externals_ ext, const Options& options) {
546 millijson::ParseOptions popt;
547 popt.buffer_size = options.buffer_size;
548 popt.parallel = options.parallel;
549 auto contents = millijson::parse(reader, popt);
550
551 Version version;
552 if (contents->type() == millijson::OBJECT) {
553 const auto& map = static_cast<const millijson::Object*>(contents.get())->value();
554 auto vIt = map.find("version");
555 if (vIt != map.end()) {
556 try {
557 if (vIt->second->type() != millijson::STRING) {
558 throw std::runtime_error("expected a string");
559 }
560 const auto& vstr = static_cast<const millijson::String*>(vIt->second.get())->value();
561 auto vraw = ritsuko::parse_version_string(vstr.c_str(), vstr.size(), /* skip_patch = */ true);
562 version.major = vraw.major;
563 version.minor = vraw.minor;
564 } catch (...) {
565 std::throw_with_nested(std::runtime_error("failed to read 'version'"));
566 }
567 }
568 }
569
570 ExternalTracker etrack(std::move(ext));
571 auto output = parse_object<Provisioner_>(contents.get(), etrack, version);
572
573 if (options.strict_list && output->type() != LIST) {
574 throw std::runtime_error("top-level object should represent an R list");
575 }
576 etrack.validate();
577
578 return ParsedList(std::move(output), std::move(version));
579}
580
599template<class Provisioner_, class Externals_>
600ParsedList parse_file(const std::string& file, Externals_ ext, const Options& options) {
601 std::unique_ptr<byteme::Reader> ptr;
602 if (byteme::is_gzip(file.c_str())) {
603 ptr.reset(new byteme::GzipFileReader(file.c_str(), {}));
604 } else {
605 ptr.reset(new byteme::RawFileReader(file.c_str(), {}));
606 }
607 return parse<Provisioner_>(*ptr, std::move(ext), options);
608}
609
629template<class Provisioner_, class Externals_>
630ParsedList parse_buffer(const unsigned char* buffer, std::size_t len, Externals_ ext, const Options& options) {
631 std::unique_ptr<byteme::Reader> ptr;
632 if (byteme::is_zlib_or_gzip(buffer, len)) {
633 ptr.reset(new byteme::ZlibBufferReader(buffer, len, {}));
634 } else {
635 ptr.reset(new byteme::RawBufferReader(buffer, len));
636 }
637 return parse<Provisioner_>(*ptr, std::move(ext), options);
638}
639
651inline void validate(byteme::Reader& reader, int num_external, const Options& options) {
652 parse<DummyProvisioner>(reader, DummyExternals(num_external), options);
653}
654
666inline void validate_file(const std::string& file, int num_external, const Options& options) {
667 parse_file<DummyProvisioner>(file, DummyExternals(num_external), options);
668}
669
682inline void validate_buffer(const unsigned char* buffer, std::size_t len, int num_external, const Options& options) {
683 parse_buffer<DummyProvisioner>(buffer, len, DummyExternals(num_external), options);
684}
685
686}
687
688}
689
690#endif
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
Interface for lists.
Definition interfaces.hpp:238
virtual void set(std::size_t i, std::shared_ptr< Base > v)=0
Format
Definition interfaces.hpp:159
Defines the interfaces to use in HDF5 parsing.
bool is_zlib_or_gzip(const unsigned char *buffer, std::size_t n)
bool is_gzip(const unsigned char *buffer, std::size_t n)
Integer_ from_float(Float_ x)
constexpr bool is_greater_than_or_equal(Left_ left, Right_ right)
constexpr Dest_ cap(Value_ x)
constexpr Dest_ cast(Value_ x)
ParsedList parse_buffer(const unsigned char *buffer, std::size_t len, Externals_ ext, const Options &options)
Definition parse_json.hpp:630
void validate_file(const std::string &file, int num_external, const Options &options)
Definition parse_json.hpp:666
ParsedList parse(Reader_ &reader, Externals_ ext, const Options &options)
Definition parse_json.hpp:545
void validate(byteme::Reader &reader, int num_external, const Options &options)
Definition parse_json.hpp:651
void validate_buffer(const unsigned char *buffer, std::size_t len, int num_external, const Options &options)
Definition parse_json.hpp:682
ParsedList parse_file(const std::string &file, Externals_ ext, const Options &options)
Definition parse_json.hpp:600
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 JSON file parsing.
Definition parse_json.hpp:505
std::size_t buffer_size
Definition parse_json.hpp:521
bool strict_list
Definition parse_json.hpp:515
bool parallel
Definition parse_json.hpp:510