libzarr
Header-only C++17 Zarr v2/v3, WASM-compatible
Loading...
Searching...
No Matches
v3.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2
3#ifndef LIBZARR_V3_HPP
4#define LIBZARR_V3_HPP
5
6#include <array>
7#include <cmath>
8#include <cstdint>
9#include <cstring>
10#include <initializer_list>
11#include <optional>
12#include <string>
13#include <string_view>
14#include <vector>
15
16#include "libzarr/detail/common.hpp"
17#include "libzarr/metadata.hpp"
18#include "libzarr/types.hpp"
19#include "libzarr/v2.hpp"
20
25
26namespace zarr::v3 {
27
29inline constexpr const char* kMetaKey = "zarr.json";
30
32[[nodiscard]] inline std::string meta_key(const std::string& path) {
33 return path.empty() ? kMetaKey : path + "/" + kMetaKey;
34}
35
36namespace detail_v3 {
37
41inline void check_members(const json& j, std::initializer_list<std::string_view> known,
42 const std::string& ctx, bool lenient) {
43 if (lenient) {
44 return;
45 }
46 for (const auto& item : j.items()) {
47 bool recognized = false;
48 for (const std::string_view name : known) {
49 recognized = recognized || item.key() == name;
50 }
51 if (recognized) {
52 continue;
53 }
54 if (item.value().is_object() && !item.value().value("must_understand", true)) {
55 continue;
56 }
57 throw error(ctx + ": unknown metadata member '" + item.key() +
58 "' (v3 core requires rejecting unrecognized members; open in lenient mode to "
59 "ignore it)");
60 }
61}
62
63inline const json& require(const json& j, const char* name, const std::string& ctx) {
64 const auto it = j.find(name);
65 if (it == j.end()) {
66 throw error(ctx + ": missing required member '" + name + "'");
67 }
68 return *it;
69}
70
72inline std::uint32_t hex_digit(char c) {
73 if (c >= '0' && c <= '9') {
74 return static_cast<std::uint32_t>(c - '0');
75 }
76 if (c >= 'a' && c <= 'f') {
77 return static_cast<std::uint32_t>(c - 'a') + 10U;
78 }
79 if (c >= 'A' && c <= 'F') {
80 return static_cast<std::uint32_t>(c - 'A') + 10U;
81 }
82 return 99;
83}
84
87inline std::optional<Bytes> parse_bit_string(const std::string& s, std::uint32_t itemsize,
88 const std::string& ctx) {
89 const bool hex = detail::starts_with(s, "0x");
90 const bool bin = detail::starts_with(s, "0b");
91 if (!hex && !bin) {
92 return std::nullopt;
93 }
94 const std::string_view digits = std::string_view(s).substr(2);
95 const std::size_t per_byte = hex ? 2 : 8;
96 const std::uint32_t base = hex ? 16U : 2U;
97 if (digits.size() != itemsize * per_byte) {
98 throw error(ctx + ": bit-pattern fill_value '" + s + "' must have " +
99 std::to_string(itemsize * per_byte) + (hex ? " hex" : " binary") + " digits");
100 }
101 Bytes out(itemsize);
102 for (std::uint32_t b = 0; b < itemsize; ++b) {
103 std::uint32_t value = 0;
104 for (std::size_t d = 0; d < per_byte; ++d) {
105 const std::uint32_t digit = hex_digit(digits[b * per_byte + d]);
106 if (digit >= base) {
107 std::string msg = ctx;
108 msg += ": invalid digit in fill_value '";
109 msg += s;
110 msg += "'";
111 throw error(msg);
112 }
113 value = value * base + digit;
114 }
115 out[b] = static_cast<std::uint8_t>(value);
116 }
117 return out;
118}
119
122inline Bytes float_fill(const json& v, DType kind, std::uint32_t itemsize, const std::string& ctx) {
123 if (v.is_number()) {
124 return detail::fill_from_double(v.get<double>(), DataType{kind, itemsize}, ctx);
125 }
126 if (v.is_string()) {
127 const auto s = v.get<std::string>();
128 if (s == "NaN") {
129 return detail::quiet_nan_bytes(kind);
130 }
131 if (s == "Infinity") {
132 return detail::infinity_bytes(kind, false);
133 }
134 if (s == "-Infinity") {
135 return detail::infinity_bytes(kind, true);
136 }
137 if (auto bits = parse_bit_string(s, itemsize, ctx)) {
138 // v3 core: the string is the numeral of the bit pattern (big-endian
139 // digits); convert to native byte order.
140 if (detail::host_is_little_endian()) {
141 std::reverse(bits->begin(), bits->end());
142 }
143 return *std::move(bits);
144 }
145 throw error(ctx + ": cannot interpret float fill_value '" + s + "'");
146 }
147 throw error(ctx + ": cannot interpret float fill_value " + v.dump());
148}
149
150} // namespace detail_v3
151
153[[nodiscard]] inline DataType parse_data_type(const json& v, const std::string& ctx) {
154 if (!v.is_string()) {
155 throw error(ctx + ": extension data_type objects are not supported");
156 }
157 const auto s = v.get<std::string>();
158 struct NamedType {
159 std::string_view name;
160 DType kind;
161 };
162 static constexpr std::array<NamedType, 14> kNames{{{"bool", DType::boolean},
163 {"int8", DType::int8},
164 {"int16", DType::int16},
165 {"int32", DType::int32},
166 {"int64", DType::int64},
167 {"uint8", DType::uint8},
168 {"uint16", DType::uint16},
169 {"uint32", DType::uint32},
170 {"uint64", DType::uint64},
171 {"float16", DType::float16},
172 {"float32", DType::float32},
173 {"float64", DType::float64},
174 {"complex64", DType::complex64},
175 {"complex128", DType::complex128}}};
176 for (const NamedType& named : kNames) {
177 if (s == named.name) {
178 return DataType::of(named.kind);
179 }
180 }
181 if (s.size() > 1 && s[0] == 'r') {
182 std::uint64_t bits = 0;
183 for (std::size_t i = 1; i < s.size(); ++i) {
184 if (s[i] < '0' || s[i] > '9' || bits > 0xFFFFFF) {
185 bits = 0;
186 break;
187 }
188 bits = bits * 10 + static_cast<std::uint64_t>(s[i] - '0');
189 }
190 if (bits == 0 || bits % 8 != 0) {
191 throw error(ctx + ": raw data_type '" + s + "' must be r<bits> with bits a multiple of 8");
192 }
193 return DataType::raw_bytes(static_cast<std::uint32_t>(bits / 8));
194 }
195 throw error(ctx + ": unknown data_type '" + s + "'");
196}
197
198namespace detail_v3 {
199
202inline Bytes raw_fill(const json& v, DataType dt, const std::string& ctx) {
203 if (v.is_string()) {
204 if (auto bits = parse_bit_string(v.get<std::string>(), dt.itemsize, ctx)) {
205 return *std::move(bits);
206 }
207 throw error(ctx + ": raw fill_value string must be a 0x/0b bit pattern");
208 }
209 if (v.is_array() && v.size() == dt.itemsize) {
210 Bytes out(dt.itemsize);
211 for (std::uint32_t i = 0; i < dt.itemsize; ++i) {
212 const std::uint64_t byte = detail::json_to_uint64(v[i], ctx + ": fill_value");
213 if (byte > 0xFF) {
214 throw error(ctx + ": raw fill_value bytes must be 0..255");
215 }
216 out[i] = static_cast<std::uint8_t>(byte);
217 }
218 return out;
219 }
220 throw error(ctx + ": raw fill_value must be a bit-pattern string or an array of " +
221 std::to_string(dt.itemsize) + " byte values");
222}
223
224} // namespace detail_v3
225
227[[nodiscard]] inline std::optional<Bytes> parse_fill(const json& v, DataType dt,
228 const std::string& ctx, bool lenient) {
229 if (v.is_null()) {
230 // v3 requires a concrete fill_value; null appears from pre-final writers.
231 if (lenient) {
232 return std::nullopt;
233 }
234 throw error(ctx + ": v3 fill_value must not be null (open in lenient mode to read as zeros)");
235 }
236 switch (dt.kind) {
237 case DType::boolean:
238 if (!v.is_boolean()) {
239 throw error(ctx + ": bool fill_value must be true or false");
240 }
241 return detail::scalar_bytes<std::uint8_t>(v.get<bool>() ? 1 : 0);
242 case DType::float16:
243 case DType::float32:
244 case DType::float64:
245 return detail_v3::float_fill(v, dt.kind, dt.itemsize, ctx);
246 case DType::complex64:
247 case DType::complex128: {
248 // v3 core: complex fill_value is a [real, imaginary] two-element array.
249 if (!v.is_array() || v.size() != 2) {
250 throw error(ctx + ": complex fill_value must be a [re, im] array");
251 }
252 const DType component = dt.kind == DType::complex64 ? DType::float32 : DType::float64;
253 const std::uint32_t half = dt.itemsize / 2;
254 Bytes out = detail_v3::float_fill(v[0], component, half, ctx);
255 const Bytes imag = detail_v3::float_fill(v[1], component, half, ctx);
256 out.insert(out.end(), imag.begin(), imag.end());
257 return out;
258 }
259 case DType::raw:
260 return detail_v3::raw_fill(v, dt, ctx);
261 default:
262 if (v.is_number_unsigned()) {
263 return detail::fill_from_uint(v.get<std::uint64_t>(), dt, ctx);
264 }
265 if (v.is_number_integer()) {
266 return detail::fill_from_int(v.get<std::int64_t>(), dt, ctx);
267 }
268 if (v.is_number_float()) {
269 return detail::fill_from_double(v.get<double>(), dt, ctx);
270 }
271 throw error(ctx + ": integer fill_value expected, got " + v.dump());
272 }
273}
274
275namespace detail_v3 {
276
279inline CodecSpec parse_codec_entry(const json& c, std::size_t rank, const std::string& ctx) {
280 CodecSpec spec;
281 if (c.is_string()) {
282 // Pre-final v3 writers emitted bare codec-name strings (read tolerance).
283 spec.name = c.get<std::string>();
284 } else if (c.is_object() && c.contains("name") && c["name"].is_string()) {
285 spec.name = c["name"].get<std::string>();
286 spec.configuration = c.value("configuration", json::object());
287 if (!spec.configuration.is_object()) {
288 throw error(ctx + ": codec '" + spec.name + "' configuration must be an object");
289 }
290 } else {
291 throw error(ctx + ": each codec must be an object with a 'name'");
292 }
293 if (spec.name == "endian") {
294 // zarr-python 2.x's experimental v3 wrote "endian" for what the final
295 // spec names "bytes" (read tolerance).
296 spec.name = "bytes";
297 }
298 if (spec.name == "transpose" && spec.configuration.value("order", json()).is_string()) {
299 // 2022-draft transpose configs used "C"/"F" strings; the final spec
300 // requires an explicit permutation (read tolerance).
301 const auto order = spec.configuration["order"].get<std::string>();
302 if (order != "C" && order != "F") {
303 std::string msg = ctx;
304 msg += ": transpose order '";
305 msg += order;
306 msg += R"(' is not "C", "F" or an array)";
307 throw error(msg);
308 }
309 json perm = json::array();
310 for (std::size_t d = 0; d < rank; ++d) {
311 perm.push_back(order == "F" ? rank - 1 - d : d);
312 }
313 spec.configuration["order"] = perm;
314 }
315 return spec;
316}
317
319inline std::vector<CodecSpec> parse_codecs(const json& v, std::size_t rank,
320 const std::string& ctx) {
321 if (!v.is_array()) {
322 throw error(ctx + ": 'codecs' must be an array");
323 }
324 std::vector<CodecSpec> out;
325 out.reserve(v.size());
326 for (const json& c : v) {
327 out.push_back(parse_codec_entry(c, rank, ctx));
328 }
329 return out;
330}
331
333inline void parse_chunk_key_encoding(const json& cke, ArrayMeta& meta, const std::string& ctx) {
334 if (!cke.is_object() || !cke.contains("name")) {
335 throw error(ctx + ": chunk_key_encoding must be an object with a 'name'");
336 }
337 if (cke["name"] == "default") {
338 meta.key_encoding = ChunkKeyKind::v3_default;
339 meta.dimension_separator = '/';
340 } else if (cke["name"] == "v2") {
341 meta.key_encoding = ChunkKeyKind::v2;
342 meta.dimension_separator = '.';
343 } else {
344 throw error(ctx + ": unknown chunk_key_encoding '" + cke["name"].dump() + "'");
345 }
346 const json config = cke.value("configuration", json::object());
347 const json separator = config.value("separator", json());
348 if (!separator.is_null()) {
349 if (!separator.is_string() || (separator != "/" && separator != ".")) {
350 throw error(ctx + R"(: chunk_key_encoding separator must be "/" or ".")");
351 }
352 meta.dimension_separator = separator.get<std::string>()[0];
353 }
354}
355
357inline void parse_dimension_names(const json& j, ArrayMeta& meta, const std::string& ctx) {
358 const auto it = j.find("dimension_names");
359 if (it == j.end()) {
360 return;
361 }
362 if (!it->is_array() || it->size() != meta.shape.size()) {
363 throw error(ctx + ": 'dimension_names' must be an array of rank length");
364 }
365 for (const json& name : *it) {
366 if (!name.is_string() && !name.is_null()) {
367 throw error(ctx + ": dimension names must be strings or null");
368 }
369 }
370 meta.dimension_names = *it;
371}
372
376inline void lower_sharding(ArrayMeta& meta, const std::string& ctx) {
377 while (!meta.codecs.empty() && meta.codecs.front().name == "sharding_indexed") {
378 if (meta.codecs.size() != 1) {
379 // Ranges into a shard must map 1:1 onto stored bytes; codecs wrapped
380 // around the shard (outer transpose, whole-shard compression) break
381 // that, so they are rejected rather than silently degraded.
382 throw error(ctx +
383 ": sharding_indexed cannot be combined with other codecs at the same "
384 "level");
385 }
386 const json config = meta.codecs.front().configuration.is_object()
387 ? meta.codecs.front().configuration
388 : json::object();
389 const std::vector<std::uint64_t> inner_shape = detail::parse_extents(
390 require(config, "chunk_shape", ctx + ": sharding_indexed"), "chunk_shape", ctx);
391 if (inner_shape.size() != meta.chunk_shape.size()) {
392 throw error(ctx + ": sharding_indexed chunk_shape rank mismatch");
393 }
394 for (std::size_t d = 0; d < inner_shape.size(); ++d) {
395 // v3 sharding spec: the inner chunk shape must evenly divide the shard.
396 if (inner_shape[d] == 0 || meta.chunk_shape[d] % inner_shape[d] != 0) {
397 throw error(ctx + ": sharding_indexed chunk_shape must evenly divide the shard shape");
398 }
399 }
400
401 ShardLevel level;
402 level.shard_shape = meta.chunk_shape;
403 level.index_codecs =
404 parse_codecs(require(config, "index_codecs", ctx + ": sharding_indexed"), 1, ctx);
405 const json location = config.value("index_location", json("end"));
406 if (location != "end" && location != "start") {
407 throw error(ctx + R"(: index_location must be "end" or "start")");
408 }
409 level.index_at_end = location == "end";
410
411 meta.shard_levels.push_back(std::move(level));
412 meta.chunk_shape = inner_shape;
413 meta.codecs =
414 parse_codecs(require(config, "codecs", ctx + ": sharding_indexed"), meta.shape.size(), ctx);
415 }
416 for (const CodecSpec& codec : meta.codecs) {
417 if (codec.name == "sharding_indexed") {
418 throw error(ctx + ": sharding_indexed must be the sole codec of its level");
419 }
420 }
421}
422
423} // namespace detail_v3
424
425namespace detail_v3 {
426
427inline ArrayMeta parse_array_meta_impl(const json& j, const std::string& ctx, bool lenient) {
428 if (!j.is_object()) {
429 throw error(ctx + ": expected a JSON object");
430 }
431 if (detail::json_to_uint64(detail_v3::require(j, "zarr_format", ctx), ctx + ": zarr_format") !=
432 3) {
433 throw error(ctx + ": zarr_format must be 3");
434 }
435 if (detail_v3::require(j, "node_type", ctx) != "array") {
436 throw error(ctx + ": node_type must be 'array'");
437 }
438 detail_v3::check_members(
439 j,
440 {"zarr_format", "node_type", "shape", "data_type", "chunk_grid", "chunk_key_encoding",
441 "fill_value", "codecs", "attributes", "dimension_names", "storage_transformers"},
442 ctx, lenient);
443
444 ArrayMeta meta;
445 meta.format = ZarrFormat::v3;
446 meta.shape = detail::parse_extents(detail_v3::require(j, "shape", ctx), "shape", ctx);
447 meta.dtype = parse_data_type(detail_v3::require(j, "data_type", ctx), ctx);
448
449 const json& grid = detail_v3::require(j, "chunk_grid", ctx);
450 if (!grid.is_object() || grid.value("name", "") != std::string("regular")) {
451 throw error(ctx + ": only the 'regular' chunk_grid is supported");
452 }
453 // Bind `grid_ctx` as a named lvalue: passing `ctx + "..."` directly trips
454 // gcc's -Wdangling-reference on the reference initialization below.
455 const std::string grid_ctx = ctx + ": chunk_grid";
456 const json& grid_config = detail_v3::require(grid, "configuration", grid_ctx);
457 meta.chunk_shape = detail::parse_extents(detail_v3::require(grid_config, "chunk_shape", grid_ctx),
458 "chunk_shape", ctx);
459 if (meta.chunk_shape.size() != meta.shape.size()) {
460 throw error(ctx + ": chunk_shape rank " + std::to_string(meta.chunk_shape.size()) +
461 " != shape rank " + std::to_string(meta.shape.size()));
462 }
463 for (const std::uint64_t c : meta.chunk_shape) {
464 if (c == 0) {
465 throw error(ctx + ": chunk extents must be positive");
466 }
467 }
468
469 detail_v3::parse_chunk_key_encoding(detail_v3::require(j, "chunk_key_encoding", ctx), meta, ctx);
470
471 meta.fill = parse_fill(detail_v3::require(j, "fill_value", ctx), meta.dtype, ctx + ": fill_value",
472 lenient);
473 meta.codecs =
474 detail_v3::parse_codecs(detail_v3::require(j, "codecs", ctx), meta.shape.size(), ctx);
475 detail_v3::lower_sharding(meta, ctx);
476
477 meta.attributes = j.value("attributes", json::object());
478 if (!meta.attributes.is_object()) {
479 throw error(ctx + ": 'attributes' must be an object");
480 }
481 detail_v3::parse_dimension_names(j, meta, ctx);
482
483 const auto st_it = j.find("storage_transformers");
484 if (st_it != j.end() && !(st_it->is_array() && st_it->empty())) {
485 throw error(ctx + ": storage_transformers are not supported");
486 }
487 return meta;
488}
489
490} // namespace detail_v3
491
493[[nodiscard]] inline ArrayMeta parse_array_meta(const json& j, const std::string& ctx,
494 bool lenient = false) {
495 return detail::guard_json(ctx, [&] { return detail_v3::parse_array_meta_impl(j, ctx, lenient); });
496}
497
499struct GroupMeta {
501 json attributes = json::object();
505 std::optional<json> consolidated;
506};
507
509[[nodiscard]] inline GroupMeta parse_group_meta(const json& j, const std::string& ctx,
510 bool lenient = false) {
511 return detail::guard_json(ctx, [&]() -> GroupMeta {
512 if (!j.is_object()) {
513 throw error(ctx + ": expected a JSON object");
514 }
515 if (detail::json_to_uint64(detail_v3::require(j, "zarr_format", ctx), ctx + ": zarr_format") !=
516 3) {
517 throw error(ctx + ": zarr_format must be 3");
518 }
519 if (detail_v3::require(j, "node_type", ctx) != "group") {
520 throw error(ctx + ": node_type must be 'group'");
521 }
522 detail_v3::check_members(j, {"zarr_format", "node_type", "attributes", "consolidated_metadata"},
523 ctx, lenient);
524
525 GroupMeta meta;
526 meta.attributes = j.value("attributes", json::object());
527 const auto cons_it = j.find("consolidated_metadata");
528 if (cons_it != j.end() && cons_it->is_object() && cons_it->contains("metadata") &&
529 (*cons_it)["metadata"].is_object()) {
530 meta.consolidated = (*cons_it)["metadata"];
531 }
532 return meta;
533 });
534}
535
537[[nodiscard]] inline std::string chunk_key(const std::vector<std::uint64_t>& index,
538 char separator) {
539 std::string key = "c"; // rank 0: the key is exactly "c"
540 for (const std::uint64_t i : index) {
541 key += separator;
542 key += std::to_string(i);
543 }
544 return key;
545}
546
547// ---- emission (canonical, deterministic) ------------------------------------
548
550[[nodiscard]] inline std::string emit_data_type(DataType dt) {
551 switch (dt.kind) {
552 case DType::boolean:
553 return "bool";
554 case DType::int8:
555 return "int8";
556 case DType::int16:
557 return "int16";
558 case DType::int32:
559 return "int32";
560 case DType::int64:
561 return "int64";
562 case DType::uint8:
563 return "uint8";
564 case DType::uint16:
565 return "uint16";
566 case DType::uint32:
567 return "uint32";
568 case DType::uint64:
569 return "uint64";
570 case DType::float16:
571 return "float16";
572 case DType::float32:
573 return "float32";
574 case DType::float64:
575 return "float64";
576 case DType::complex64:
577 return "complex64";
578 case DType::complex128:
579 return "complex128";
580 case DType::raw:
581 return "r" + std::to_string(std::uint64_t{dt.itemsize} * 8);
582 }
583 throw error("v3 emission not implemented for this dtype");
584}
585
586namespace detail_v3 {
587
590inline std::string hex_bit_string(const Bytes& bytes, bool reverse_for_endianness) {
591 constexpr std::string_view kDigits = "0123456789abcdef";
592 std::string out = "0x";
593 for (std::size_t i = 0; i < bytes.size(); ++i) {
594 const std::size_t at =
595 reverse_for_endianness && detail::host_is_little_endian() ? bytes.size() - 1 - i : i;
596 out.push_back(kDigits[static_cast<std::size_t>(bytes[at]) >> 4U]);
597 out.push_back(kDigits[static_cast<std::size_t>(bytes[at]) & 0x0FU]);
598 }
599 return out;
600}
601
605inline json emit_float_fill(const std::uint8_t* data, DType kind, std::uint32_t width) {
606 double v = 0;
607 if (kind == DType::float16) {
608 std::uint16_t bits = 0;
609 std::memcpy(&bits, data, 2);
610 v = detail::half_bits_to_double(bits);
611 } else if (kind == DType::float32) {
612 float f = 0;
613 std::memcpy(&f, data, 4);
614 v = static_cast<double>(f);
615 } else {
616 std::memcpy(&v, data, 8);
617 }
618 if (std::isnan(v)) {
619 const Bytes bits(data, data + width);
620 if (bits == detail::quiet_nan_bytes(kind)) {
621 return "NaN";
622 }
623 return hex_bit_string(bits, /*reverse_for_endianness=*/true);
624 }
625 if (std::isinf(v)) {
626 return v > 0 ? "Infinity" : "-Infinity";
627 }
628 return v;
629}
630
631} // namespace detail_v3
632
636[[nodiscard]] inline json emit_fill(const std::optional<Bytes>& fill, DataType dt) {
637 const Bytes zeros(dt.itemsize, 0);
638 const Bytes& bytes = fill ? *fill : zeros;
639 switch (dt.kind) {
640 case DType::boolean:
641 return bytes[0] != 0;
642 case DType::float16:
643 case DType::float32:
644 case DType::float64:
645 return detail_v3::emit_float_fill(bytes.data(), dt.kind, dt.itemsize);
646 case DType::complex64:
647 case DType::complex128: {
648 const DType component = dt.kind == DType::complex64 ? DType::float32 : DType::float64;
649 const std::uint32_t half = dt.itemsize / 2;
650 return json::array({detail_v3::emit_float_fill(bytes.data(), component, half),
651 detail_v3::emit_float_fill(bytes.data() + half, component, half)});
652 }
653 case DType::raw:
654 return detail_v3::hex_bit_string(bytes, /*reverse_for_endianness=*/false);
655 default:
656 // Integers reuse the version-independent emission (plain JSON numbers).
657 return detail::fill_to_json(bytes, dt);
658 }
659}
660
661namespace detail_v3 {
662
663inline json emit_codec_list(const std::vector<CodecSpec>& codecs) {
664 json out = json::array();
665 for (const CodecSpec& codec : codecs) {
666 if (codec.name == "shuffle") {
667 throw error("the v2 shuffle filter cannot be represented in v3 metadata");
668 }
669 json c = {{"name", codec.name}};
670 if (codec.configuration.is_object() && !codec.configuration.empty()) {
671 c["configuration"] = codec.configuration;
672 }
673 out.push_back(std::move(c));
674 }
675 return out;
676}
677
678} // namespace detail_v3
679
683[[nodiscard]] inline json emit_array_meta(const ArrayMeta& meta) {
684 json j;
685 j["zarr_format"] = 3;
686 j["node_type"] = "array";
687 j["shape"] = meta.shape;
688 j["data_type"] = emit_data_type(meta.dtype);
689 const std::vector<std::uint64_t>& grid_shape =
690 meta.shard_levels.empty() ? meta.chunk_shape : meta.shard_levels.front().shard_shape;
691 j["chunk_grid"] = {{"name", "regular"}, {"configuration", {{"chunk_shape", grid_shape}}}};
692 j["chunk_key_encoding"] = {
693 {"name", meta.key_encoding == ChunkKeyKind::v3_default ? "default" : "v2"},
694 {"configuration", {{"separator", std::string(1, meta.dimension_separator)}}}};
695 j["fill_value"] = emit_fill(meta.fill, meta.dtype);
696
697 json codecs = detail_v3::emit_codec_list(meta.codecs);
698 for (std::size_t i = meta.shard_levels.size(); i-- > 0;) {
699 const ShardLevel& level = meta.shard_levels[i];
700 const std::vector<std::uint64_t>& inner_shape =
701 i + 1 < meta.shard_levels.size() ? meta.shard_levels[i + 1].shard_shape : meta.chunk_shape;
702 codecs = json::array({{{"name", "sharding_indexed"},
703 {"configuration",
704 {{"chunk_shape", inner_shape},
705 {"codecs", std::move(codecs)},
706 {"index_codecs", detail_v3::emit_codec_list(level.index_codecs)},
707 {"index_location", level.index_at_end ? "end" : "start"}}}}});
708 }
709 j["codecs"] = std::move(codecs);
710 if (meta.attributes.is_object() && !meta.attributes.empty()) {
711 j["attributes"] = meta.attributes;
712 }
713 if (meta.dimension_names.is_array()) {
714 j["dimension_names"] = meta.dimension_names;
715 }
716 return j;
717}
718
720[[nodiscard]] inline json emit_group_meta(const json& attributes) {
721 json j;
722 j["zarr_format"] = 3;
723 j["node_type"] = "group";
724 if (attributes.is_object() && !attributes.empty()) {
725 j["attributes"] = attributes;
726 }
727 return j;
728}
729
734inline void consolidate(Store& store) {
735 const auto root_bytes = store.read(kMetaKey);
736 if (!root_bytes) {
737 throw error("v3::consolidate: no zarr.json at the store root");
738 }
739 json root = detail::parse_json(*root_bytes, kMetaKey);
740 json metadata = json::object();
741 for (const std::string& key : store.list_prefix("")) {
742 if (key == kMetaKey || !detail::ends_with(key, std::string("/") + kMetaKey)) {
743 continue;
744 }
745 const std::string path = key.substr(0, key.size() - std::string(kMetaKey).size() - 1);
746 if (const auto bytes = store.read(key)) {
747 metadata[path] = detail::parse_json(*bytes, key);
748 }
749 }
750 root["consolidated_metadata"] = {
751 {"kind", "inline"}, {"must_understand", false}, {"metadata", std::move(metadata)}};
752 store.write(kMetaKey, canonical_json_bytes(root));
753}
754
755} // namespace zarr::v3
756
757#endif // LIBZARR_V3_HPP
Definition store.hpp:83
virtual void write(std::string_view key, Bytes value)=0
Create or replace the value at key.
virtual std::vector< std::string > list_prefix(std::string_view prefix)=0
All keys starting with prefix ("" or ending in '/'), sorted.
virtual std::optional< Bytes > read(std::string_view key)=0
Full value at key, or std::nullopt if the key is absent.
Definition types.hpp:36
nlohmann::json json
Definition metadata.hpp:30
Bytes canonical_json_bytes(const json &j)
Definition metadata.hpp:153
Definition metadata.hpp:98
json dimension_names
v3 dimension_names member, preserved verbatim (null when absent).
Definition metadata.hpp:115
std::vector< std::uint64_t > shape
Array shape; empty = 0-dimensional.
Definition metadata.hpp:102
char dimension_separator
Chunk-key separator ('.' or '/').
Definition metadata.hpp:113
DataType dtype
Element type.
Definition metadata.hpp:106
std::vector< std::uint64_t > chunk_shape
Chunk shape, same rank as shape; chunks may exceed the array extent.
Definition metadata.hpp:104
ChunkKeyKind key_encoding
Chunk-key scheme (see ChunkKeyKind).
Definition metadata.hpp:111
std::vector< ShardLevel > shard_levels
Sharding levels (empty = unsharded); see ShardLevel.
Definition metadata.hpp:121
json attributes
User attributes (v2 .zattrs / v3 attributes).
Definition metadata.hpp:123
std::vector< CodecSpec > codecs
Definition metadata.hpp:119
std::optional< Bytes > fill
Definition metadata.hpp:109
Definition metadata.hpp:36
json configuration
Codec-specific configuration.
Definition metadata.hpp:40
std::string name
Codec name ("transpose", "bytes", "gzip", "zlib", ...).
Definition metadata.hpp:38
A concrete element type: kind plus size (the size only varies for raw).
Definition types.hpp:122
std::uint32_t itemsize
Element size in bytes.
Definition types.hpp:126
static constexpr DataType raw_bytes(std::uint32_t size)
A raw byte-string type of size bytes (v2 |V<size>).
Definition types.hpp:139
DType kind
Element type kind.
Definition types.hpp:124
static constexpr DataType of(DType kind)
Definition types.hpp:131
Definition metadata.hpp:86
bool index_at_end
v3 sharding spec index_location: end (default) or start.
Definition metadata.hpp:92
std::vector< CodecSpec > index_codecs
Codecs of the shard index (fixed-size: bytes and optional crc32c).
Definition metadata.hpp:90
Result of parsing a v3 group zarr.json.
Definition v3.hpp:499
std::optional< json > consolidated
Definition v3.hpp:505
json attributes
User attributes.
Definition v3.hpp:501
DType
Definition types.hpp:52
std::vector< std::uint8_t > Bytes
Owned byte buffer used throughout the value-based public API.
Definition types.hpp:42
std::optional< Bytes > parse_fill(const json &v, DataType dt, const std::string &ctx)
Definition v2.hpp:279
ParsedDType parse_data_type(const std::string &text, const std::string &ctx)
Definition v2.hpp:126
json emit_fill(const std::optional< Bytes > &fill, DataType dt)
Definition v3.hpp:636
GroupMeta parse_group_meta(const json &j, const std::string &ctx, bool lenient=false)
Parses a v3 group zarr.json document.
Definition v3.hpp:509
constexpr const char * kMetaKey
v3 metadata document name.
Definition v3.hpp:29