libzarr
Header-only C++17 Zarr v2/v3, WASM-compatible
Loading...
Searching...
No Matches
metadata.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2
3#ifndef LIBZARR_METADATA_HPP
4#define LIBZARR_METADATA_HPP
5
6#include <cmath>
7#include <cstdint>
8#include <cstdlib>
9#include <cstring>
10#include <limits>
11#include <optional>
12#include <string>
13#include <vector>
14
15#include <nlohmann/json.hpp>
16
17#include "libzarr/detail/common.hpp"
18#include "libzarr/types.hpp"
19
24
25namespace zarr {
26
30using json = nlohmann::json;
31
36struct CodecSpec {
38 std::string name;
40 json configuration = json::object();
41};
42
46namespace codec {
47
49[[nodiscard]] inline CodecSpec gzip(int level = 5) { return {"gzip", {{"level", level}}}; }
50
52[[nodiscard]] inline CodecSpec zlib(int level = 5) { return {"zlib", {{"level", level}}}; }
53
55[[nodiscard]] inline CodecSpec blosc(const std::string& cname = "lz4", int clevel = 5,
56 const std::string& shuffle = "shuffle") {
57 return {"blosc", {{"cname", cname}, {"clevel", clevel}, {"shuffle", shuffle}}};
58}
59
61[[nodiscard]] inline CodecSpec zstd(int level = 0, bool checksum = false) {
62 return {"zstd", {{"level", level}, {"checksum", checksum}}};
63}
64
66[[nodiscard]] inline CodecSpec crc32c() { return {"crc32c", {}}; }
67
69[[nodiscard]] inline CodecSpec shuffle(int elementsize = 0) {
70 return {"shuffle", {{"elementsize", elementsize}}};
71}
72
73} // namespace codec
74
78enum class ChunkKeyKind : std::uint8_t {
79 v2,
80 v3_default,
81};
82
86struct ShardLevel {
88 std::vector<std::uint64_t> shard_shape;
90 std::vector<CodecSpec> index_codecs;
92 bool index_at_end = true;
93};
94
98struct ArrayMeta {
100 ZarrFormat format = ZarrFormat::v2;
102 std::vector<std::uint64_t> shape;
104 std::vector<std::uint64_t> chunk_shape;
109 std::optional<Bytes> fill;
111 ChunkKeyKind key_encoding = ChunkKeyKind::v2;
119 std::vector<CodecSpec> codecs;
121 std::vector<ShardLevel> shard_levels;
123 json attributes = json::object();
124
126 [[nodiscard]] std::uint64_t element_count() const {
127 return detail::checked_product(shape, "array shape");
128 }
130 [[nodiscard]] std::uint64_t chunk_element_count() const {
131 return detail::checked_product(chunk_shape, "chunk shape");
132 }
134 [[nodiscard]] std::vector<std::uint64_t> grid_shape() const {
135 std::vector<std::uint64_t> grid(shape.size());
136 for (std::size_t d = 0; d < shape.size(); ++d) {
137 grid[d] = detail::ceil_div(shape[d], chunk_shape[d]);
138 }
139 return grid;
140 }
141};
142
148 bool lenient = false;
149};
150
153[[nodiscard]] inline Bytes canonical_json_bytes(const json& j) {
154 const std::string text = j.dump(4);
155 return {text.begin(), text.end()};
156}
157
158namespace detail {
159
164template <typename Fn>
165auto guard_json(const std::string& ctx, const Fn& fn) -> decltype(fn()) {
166 try {
167 return fn();
168 } catch (const json::exception& e) {
169 throw error(ctx + ": malformed metadata (" + e.what() + ")");
170 }
171}
172
176inline json parse_json(const Bytes& bytes, const std::string& ctx) {
177 try {
178 return json::parse(bytes.begin(), bytes.end());
179 } catch (const json::exception& e) {
180 throw error(ctx + ": " + e.what());
181 }
182}
183
187inline std::uint64_t json_to_uint64(const json& v, const std::string& ctx) {
188 if (v.is_number_unsigned()) {
189 return v.get<std::uint64_t>();
190 }
191 if (v.is_number_integer()) {
192 const auto i = v.get<std::int64_t>();
193 if (i < 0) {
194 throw error(ctx + ": expected a non-negative integer, got " + std::to_string(i));
195 }
196 return static_cast<std::uint64_t>(i);
197 }
198 throw error(ctx + ": expected a non-negative integer, got " + v.dump());
199}
200
204inline std::int64_t lenient_int(const json& obj, const char* key, std::int64_t fallback,
205 const std::string& ctx) {
206 const auto it = obj.find(key);
207 if (it == obj.end()) {
208 return fallback;
209 }
210 if (it->is_number_integer() || it->is_number_unsigned()) {
211 return it->get<std::int64_t>();
212 }
213 if (it->is_string()) {
214 const auto s = it->get<std::string>();
215 if (!s.empty()) {
216 char* end = nullptr;
217 const long long v = std::strtoll(s.c_str(), &end, 10);
218 if (end == s.c_str() + s.size()) {
219 return static_cast<std::int64_t>(v);
220 }
221 }
222 }
223 throw error(ctx + ": '" + key + "' must be an integer, got " + it->dump());
224}
225
226template <typename T>
227Bytes scalar_bytes(T value) {
228 Bytes out(sizeof(T));
229 std::memcpy(out.data(), &value, sizeof(T));
230 return out;
231}
232
234inline std::vector<std::uint64_t> parse_extents(const json& v, const char* name,
235 const std::string& ctx) {
236 if (!v.is_array()) {
237 throw error(ctx + ": '" + name + "' must be an array");
238 }
239 std::vector<std::uint64_t> out;
240 out.reserve(v.size());
241 for (const json& e : v) {
242 out.push_back(json_to_uint64(e, ctx + ": " + name));
243 }
244 return out;
245}
246
250inline Bytes quiet_nan_bytes(DType kind) {
251 if (kind == DType::float16) {
252 return scalar_bytes<std::uint16_t>(0x7e00U);
253 }
254 if (kind == DType::float32) {
255 return scalar_bytes<std::uint32_t>(0x7fc00000U);
256 }
257 assert(kind == DType::float64);
258 return scalar_bytes<std::uint64_t>(0x7ff8000000000000ULL);
259}
260
262inline Bytes infinity_bytes(DType kind, bool negative) {
263 if (kind == DType::float16) {
264 return scalar_bytes<std::uint16_t>(negative ? 0xfc00U : 0x7c00U);
265 }
266 if (kind == DType::float32) {
267 const float inf = std::numeric_limits<float>::infinity();
268 return scalar_bytes(negative ? -inf : inf);
269 }
270 assert(kind == DType::float64);
271 const double inf = std::numeric_limits<double>::infinity();
272 return scalar_bytes(negative ? -inf : inf);
273}
274
275inline Bytes fill_from_double(double value, DataType dt, const std::string& ctx);
276
278inline Bytes fill_from_int(std::int64_t value, DataType dt, const std::string& ctx) {
279 const auto check = [&](std::int64_t lo, std::int64_t hi) {
280 if (value < lo || value > hi) {
281 throw error(ctx + ": fill_value " + std::to_string(value) + " out of range for dtype");
282 }
283 };
284 switch (dt.kind) {
285 case DType::boolean:
286 check(0, 1);
287 return scalar_bytes<std::uint8_t>(static_cast<std::uint8_t>(value));
288 case DType::int8:
289 check(std::numeric_limits<std::int8_t>::min(), std::numeric_limits<std::int8_t>::max());
290 return scalar_bytes<std::int8_t>(static_cast<std::int8_t>(value));
291 case DType::int16:
292 check(std::numeric_limits<std::int16_t>::min(), std::numeric_limits<std::int16_t>::max());
293 return scalar_bytes<std::int16_t>(static_cast<std::int16_t>(value));
294 case DType::int32:
295 check(std::numeric_limits<std::int32_t>::min(), std::numeric_limits<std::int32_t>::max());
296 return scalar_bytes<std::int32_t>(static_cast<std::int32_t>(value));
297 case DType::int64:
298 return scalar_bytes<std::int64_t>(value);
299 case DType::uint8:
300 case DType::uint16:
301 case DType::uint32:
302 case DType::uint64: {
303 if (value < 0) {
304 throw error(ctx + ": fill_value " + std::to_string(value) +
305 " is negative for unsigned dtype");
306 }
307 const auto u = static_cast<std::uint64_t>(value);
308 if (dt.kind == DType::uint8 && u > std::numeric_limits<std::uint8_t>::max()) {
309 check(0, 255);
310 }
311 if (dt.kind == DType::uint16 && u > std::numeric_limits<std::uint16_t>::max()) {
312 check(0, 65535);
313 }
314 if (dt.kind == DType::uint32 && u > std::numeric_limits<std::uint32_t>::max()) {
315 check(0, 4294967295LL);
316 }
317 if (dt.kind == DType::uint8) {
318 return scalar_bytes<std::uint8_t>(static_cast<std::uint8_t>(u));
319 }
320 if (dt.kind == DType::uint16) {
321 return scalar_bytes<std::uint16_t>(static_cast<std::uint16_t>(u));
322 }
323 if (dt.kind == DType::uint32) {
324 return scalar_bytes<std::uint32_t>(static_cast<std::uint32_t>(u));
325 }
326 return scalar_bytes<std::uint64_t>(u);
327 }
328 case DType::float32:
329 case DType::float64:
330 return fill_from_double(static_cast<double>(value), dt, ctx);
331 default:
332 throw error(ctx + ": numeric fill_value invalid for this dtype");
333 }
334}
335
338inline Bytes fill_from_uint(std::uint64_t value, DataType dt, const std::string& ctx) {
339 if (value <= static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max())) {
340 return fill_from_int(static_cast<std::int64_t>(value), dt, ctx);
341 }
342 if (dt.kind != DType::uint64) {
343 throw error(ctx + ": fill_value " + std::to_string(value) + " out of range for dtype");
344 }
345 return scalar_bytes<std::uint64_t>(value);
346}
347
348inline Bytes fill_from_double(double value, DataType dt, const std::string& ctx) {
349 switch (dt.kind) {
350 case DType::float16:
351 return scalar_bytes<std::uint16_t>(double_to_half_bits(value));
352 case DType::float32:
353 return scalar_bytes<float>(static_cast<float>(value));
354 case DType::float64:
355 return scalar_bytes<double>(value);
356 default:
357 // Tolerance: integral fills occasionally arrive as JSON floats
358 // (e.g. 1.0 for an int dtype); accept when exactly integral.
359 if (std::nearbyint(value) == value &&
360 value >= static_cast<double>(std::numeric_limits<std::int64_t>::min()) &&
361 value <= static_cast<double>(std::numeric_limits<std::int64_t>::max())) {
362 return fill_from_int(static_cast<std::int64_t>(value), dt, ctx);
363 }
364 throw error(ctx + ": non-integral fill_value for integer dtype");
365 }
366}
367
371inline json fill_to_json(const std::optional<Bytes>& fill, DataType dt) {
372 if (!fill) {
373 return nullptr;
374 }
375 const std::uint8_t* p = fill->data();
376 const auto load = [&](auto probe) {
377 decltype(probe) v;
378 std::memcpy(&v, p, sizeof(v));
379 return v;
380 };
381 switch (dt.kind) {
382 case DType::boolean:
383 return load(std::uint8_t{}) != 0;
384 case DType::int8:
385 return load(std::int8_t{});
386 case DType::int16:
387 return load(std::int16_t{});
388 case DType::int32:
389 return load(std::int32_t{});
390 case DType::int64:
391 return load(std::int64_t{});
392 case DType::uint8:
393 return load(std::uint8_t{});
394 case DType::uint16:
395 return load(std::uint16_t{});
396 case DType::uint32:
397 return load(std::uint32_t{});
398 case DType::uint64:
399 return load(std::uint64_t{});
400 case DType::float16:
401 case DType::float32:
402 case DType::float64: {
403 double v = 0;
404 if (dt.kind == DType::float16) {
405 v = half_bits_to_double(load(std::uint16_t{}));
406 } else if (dt.kind == DType::float32) {
407 v = static_cast<double>(load(float{}));
408 } else {
409 v = load(double{});
410 }
411 if (std::isnan(v)) {
412 return "NaN";
413 }
414 if (std::isinf(v)) {
415 return v > 0 ? "Infinity" : "-Infinity";
416 }
417 return v;
418 }
419 case DType::complex64:
420 case DType::complex128: {
421 const DType component = dt.kind == DType::complex64 ? DType::float32 : DType::float64;
422 const std::uint32_t half = dt.itemsize / 2;
423 return json::array({fill_to_json(Bytes(p, p + half), DataType::of(component)),
424 fill_to_json(Bytes(p + half, p + dt.itemsize), DataType::of(component))});
425 }
426 case DType::raw:
427 return base64_encode(p, dt.itemsize);
428 default:
429 throw error("fill_value emission not implemented for this dtype");
430 }
431}
432
433} // namespace detail
434
435} // namespace zarr
436
437#endif // LIBZARR_METADATA_HPP
nlohmann::json json
Definition metadata.hpp:30
Bytes canonical_json_bytes(const json &j)
Definition metadata.hpp:153
ChunkKeyKind
Definition metadata.hpp:78
CodecSpec zlib(int level=5)
zlib (RFC 1950) at level (0-9). Zarr v2 only.
Definition metadata.hpp:52
CodecSpec shuffle(int elementsize=0)
shuffle: byte-transposition filter. elementsize 0 means the dtype size.
Definition metadata.hpp:69
CodecSpec gzip(int level=5)
gzip (RFC 1952) at level (0-9).
Definition metadata.hpp:49
CodecSpec blosc(const std::string &cname="lz4", int clevel=5, const std::string &shuffle="shuffle")
blosc (v3-style named shuffle: "noshuffle", "shuffle" or "bitshuffle").
Definition metadata.hpp:55
CodecSpec zstd(int level=0, bool checksum=false)
zstd. Level 0 means zstd's default; checksum appends a frame checksum.
Definition metadata.hpp:61
CodecSpec crc32c()
crc32c (Castagnoli) trailing checksum. v3 only.
Definition metadata.hpp:66
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
std::uint64_t element_count() const
Number of elements in the whole array (1 for rank 0).
Definition metadata.hpp:126
char dimension_separator
Chunk-key separator ('.' or '/').
Definition metadata.hpp:113
ZarrFormat format
Format this array was read from / will be written as.
Definition metadata.hpp:100
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::uint64_t chunk_element_count() const
Number of elements in one (full) chunk (1 for rank 0).
Definition metadata.hpp:130
std::vector< std::uint64_t > grid_shape() const
Chunk-grid extent per dimension.
Definition metadata.hpp:134
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
static constexpr DataType of(DType kind)
Definition types.hpp:131
Options for opening arrays and groups.
Definition metadata.hpp:144
bool lenient
Definition metadata.hpp:148
Definition metadata.hpp:86
std::vector< std::uint64_t > shard_shape
Shard (outer chunk) shape at this level.
Definition metadata.hpp:88
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
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
ZarrFormat
Zarr storage format version.
Definition types.hpp:45