libzarr
Header-only C++17 Zarr v2/v3, WASM-compatible
Loading...
Searching...
No Matches
codecs.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2
3#ifndef LIBZARR_CODECS_HPP
4#define LIBZARR_CODECS_HPP
5
6#include <cstdint>
7#include <limits>
8#include <optional>
9#include <string>
10#include <vector>
11
12#include "libzarr/detail/common.hpp"
13#include "libzarr/metadata.hpp"
14#include "libzarr/types.hpp"
15
16#ifdef LIBZARR_HAS_ZLIB
18#endif
19#ifdef LIBZARR_HAS_BLOSC
21#endif
22#ifdef LIBZARR_HAS_ZSTD
24#endif
25
31
32namespace zarr {
33
38 public:
43 [[nodiscard]] static CodecPipeline resolve(const ArrayMeta& meta) {
45 p.chunk_shape_ = meta.chunk_shape;
46 p.itemsize_ = meta.dtype.itemsize;
47 const std::uint64_t count = meta.chunk_element_count();
48 if (meta.dtype.itemsize != 0 &&
49 count > std::numeric_limits<std::uint64_t>::max() / meta.dtype.itemsize) {
50 throw error("chunk byte size overflows uint64");
51 }
52 p.chunk_bytes_ = count * meta.dtype.itemsize;
53 // v3 core: byte order applies per complex component (two floats).
54 p.swap_width_ = is_complex(meta.dtype.kind) ? meta.dtype.itemsize / 2 : meta.dtype.itemsize;
55
56 // v3 core: array->array*, then exactly one array->bytes, then bytes->bytes*.
57 bool past_bytes = false;
58 bool have_bytes = false;
59 for (const CodecSpec& codec : meta.codecs) {
60 if (codec.name == "transpose") {
61 if (past_bytes) {
62 throw error("codec 'transpose' must precede the 'bytes' codec");
63 }
64 p.set_transpose(codec);
65 } else if (codec.name == "bytes") {
66 if (have_bytes) {
67 throw error("codec chain has more than one 'bytes' codec");
68 }
69 have_bytes = true;
70 past_bytes = true;
71 p.set_byte_order(codec);
72 } else if (codec.name == "sharding_indexed") {
73 // Sharding is not executed as a codec: metadata parsing lowers it
74 // into ArrayMeta::shard_levels and Array wraps the store instead.
75 throw error("codec 'sharding_indexed' must be lowered into shard levels, not resolved");
76 } else {
77 if (!past_bytes) {
78 throw error("codec '" + codec.name + "' must follow the 'bytes' codec");
79 }
80 p.add_byte_stage(codec, meta);
81 }
82 }
83 if (!have_bytes) {
84 throw error("codec chain is missing the 'bytes' (array->bytes) codec");
85 }
86 p.compute_expected_sizes();
87 return p;
88 }
89
91 [[nodiscard]] std::uint64_t decoded_chunk_bytes() const { return chunk_bytes_; }
92
96 [[nodiscard]] bool is_identity() const {
97 return !transpose_order_ && !byteswap_ && byte_stages_.empty();
98 }
99
104 [[nodiscard]] bool supports_partial_read() const {
105 return !transpose_order_ && byte_stages_.empty();
106 }
107
110 [[nodiscard]] Bytes encode(Bytes chunk) const {
111 if (chunk.size() != chunk_bytes_) {
112 throw error("encode: chunk buffer is " + std::to_string(chunk.size()) + " bytes, expected " +
113 std::to_string(chunk_bytes_));
114 }
115 if (transpose_order_) {
116 // Write support for transposed layouts (v2 order:"F") is deliberately
117 // absent: we emit canonical C-order arrays only.
118 throw error("writing to a transposed (order:'F') array is not supported");
119 }
120 if (byteswap_) {
121 detail::byteswap_inplace(chunk.data(), chunk.size() / swap_width_, swap_width_);
122 }
123 for (const ByteStage& stage : byte_stages_) {
124 chunk = encode_stage(stage, std::move(chunk));
125 }
126 return chunk;
127 }
128
131 [[nodiscard]] Bytes decode_range(Bytes raw) const {
132 assert(supports_partial_read());
133 if (byteswap_) {
134 detail::byteswap_inplace(raw.data(), raw.size() / swap_width_, swap_width_);
135 }
136 return raw;
137 }
138
141 [[nodiscard]] Bytes decode(Bytes stored) const {
142 for (std::size_t i = byte_stages_.size(); i-- > 0;) {
143 stored = decode_stage(byte_stages_[i], std::move(stored), decode_expected_[i]);
144 }
145 if (stored.size() != chunk_bytes_) {
146 throw error("decode: chunk is " + std::to_string(stored.size()) + " bytes, expected " +
147 std::to_string(chunk_bytes_));
148 }
149 if (byteswap_) {
150 detail::byteswap_inplace(stored.data(), stored.size() / swap_width_, swap_width_);
151 }
152 if (transpose_order_) {
153 Bytes out(stored.size());
154 detail::gather_strided(stored.data(), gather_strides_, out.data(), chunk_shape_, itemsize_);
155 return out;
156 }
157 return stored;
158 }
159
160 private:
161 struct ByteStage {
162 enum class Kind : std::uint8_t {
163 deflate, // gzip/zlib framing
164 crc32c,
165 blosc,
166 zstd,
167 shuffle, // v2 numcodecs shuffle filter
168 };
169 Kind kind = Kind::deflate;
170 // deflate
171 int level = 5;
172 bool gzip_framing = true;
173 // blosc (encode-side parameters; decode is self-describing)
174 std::string blosc_cname = "lz4";
175 int blosc_clevel = 5;
176 int blosc_shuffle = 1;
177 std::uint32_t blosc_typesize = 1;
178 std::uint64_t blosc_blocksize = 0;
179 // zstd
180 int zstd_level = 0;
181 bool zstd_checksum = false;
182 // shuffle
183 std::uint32_t shuffle_elementsize = 1;
184 };
185
186 [[nodiscard]] static Bytes encode_stage(const ByteStage& stage, Bytes data) {
187 switch (stage.kind) {
188 case ByteStage::Kind::deflate:
189#ifdef LIBZARR_HAS_ZLIB
190 return detail::deflate_bytes(data, stage.level, stage.gzip_framing, "encode");
191#else
192 throw error("codec requires zlib but LIBZARR_HAS_ZLIB is not defined");
193#endif
194 case ByteStage::Kind::crc32c: {
195 // v3 crc32c codec: little-endian CRC-32C of the payload, appended.
196 const std::uint32_t checksum = detail::crc32c(data.data(), data.size());
197 for (int i = 0; i < 4; ++i) {
198 data.push_back(static_cast<std::uint8_t>(checksum >> (8 * i)));
199 }
200 return data;
201 }
202 case ByteStage::Kind::blosc:
203#ifdef LIBZARR_HAS_BLOSC
204 {
205 detail::BloscParams params;
206 params.cname = stage.blosc_cname;
207 params.clevel = stage.blosc_clevel;
208 params.shuffle = stage.blosc_shuffle;
209 params.typesize = stage.blosc_typesize;
210 params.blocksize = detail::checked_size(stage.blosc_blocksize, "blosc blocksize");
211 return detail::blosc_compress_bytes(data, params, "encode");
212 }
213#else
214 throw error("codec requires blosc but LIBZARR_HAS_BLOSC is not defined");
215#endif
216 case ByteStage::Kind::zstd:
217#if !defined(LIBZARR_HAS_ZSTD)
218 throw error("codec requires zstd but LIBZARR_HAS_ZSTD is not defined");
219#elif defined(LIBZARR_ZSTD_DECODE_ONLY)
220 // Decode-only build: the compress side is not compiled in.
221 throw error("zstd encode is not compiled in (LIBZARR_ZSTD_DECODE_ONLY)");
222#else
223 return detail::zstd_compress_bytes(data, stage.zstd_level, stage.zstd_checksum, "encode");
224#endif
225 case ByteStage::Kind::shuffle:
226 return detail::shuffle_bytes(data, stage.shuffle_elementsize);
227 }
228 return data; // unreachable
229 }
230
231 [[nodiscard]] static Bytes decode_stage(const ByteStage& stage, Bytes data,
232 [[maybe_unused]] std::optional<std::uint64_t> expected) {
233 switch (stage.kind) {
234 case ByteStage::Kind::deflate:
235#ifdef LIBZARR_HAS_ZLIB
236 return detail::inflate_bytes(data, expected, "decode");
237#else
238 throw error("codec requires zlib but LIBZARR_HAS_ZLIB is not defined");
239#endif
240 case ByteStage::Kind::crc32c: {
241 if (data.size() < 4) {
242 throw error("decode: crc32c codec needs at least 4 bytes");
243 }
244 const std::size_t payload = data.size() - 4;
245 std::uint32_t stored_crc = 0;
246 for (int i = 3; i >= 0; --i) {
247 stored_crc = (stored_crc << 8U) | data[payload + static_cast<std::size_t>(i)];
248 }
249 if (detail::crc32c(data.data(), payload) != stored_crc) {
250 throw error("decode: crc32c checksum mismatch (corrupt chunk)");
251 }
252 data.resize(payload);
253 return data;
254 }
255 case ByteStage::Kind::blosc:
256#ifdef LIBZARR_HAS_BLOSC
257 return detail::blosc_decompress_bytes(data, expected, "decode");
258#else
259 throw error("codec requires blosc but LIBZARR_HAS_BLOSC is not defined");
260#endif
261 case ByteStage::Kind::zstd:
262#ifdef LIBZARR_HAS_ZSTD
263 return detail::zstd_decompress_bytes(data, expected, "decode");
264#else
265 throw error("codec requires zstd but LIBZARR_HAS_ZSTD is not defined");
266#endif
267 case ByteStage::Kind::shuffle:
268 return detail::unshuffle_bytes(data, stage.shuffle_elementsize);
269 }
270 return data; // unreachable
271 }
272
273 void set_transpose(const CodecSpec& codec) {
274 const auto it = codec.configuration.find("order");
275 if (it == codec.configuration.end() || !it->is_array()) {
276 throw error("codec 'transpose' requires an 'order' array");
277 }
278 const std::size_t rank = chunk_shape_.size();
279 std::vector<std::uint32_t> order;
280 std::vector<bool> seen(rank, false);
281 for (const json& v : *it) {
282 const std::uint64_t dim = detail::json_to_uint64(v, "codec 'transpose': 'order'");
283 if (dim >= rank) {
284 throw error("codec 'transpose': 'order' must be a permutation of 0.." +
285 std::to_string(rank == 0 ? 0 : rank - 1));
286 }
287 const auto d = static_cast<std::uint32_t>(dim);
288 if (seen[d]) {
289 throw error("codec 'transpose': repeated dimension " + std::to_string(d) + " in 'order'");
290 }
291 seen[d] = true;
292 order.push_back(d);
293 }
294 if (order.size() != rank) {
295 throw error("codec 'transpose': 'order' has " + std::to_string(order.size()) +
296 " entries for a rank-" + std::to_string(rank) + " array");
297 }
298 bool identity = true;
299 for (std::size_t i = 0; i < rank; ++i) {
300 identity = identity && order[i] == i;
301 }
302 if (identity) {
303 return; // no-op elision
304 }
305 transpose_order_ = order;
306 // Stored (encoded) dimension i holds source dimension order[i]; build the
307 // per-source-dimension byte strides used to gather back to C order.
308 std::vector<std::uint64_t> stored_shape(rank);
309 for (std::size_t i = 0; i < rank; ++i) {
310 stored_shape[i] = chunk_shape_[order[i]];
311 }
312 const std::vector<std::uint64_t> stored_strides =
313 detail::c_strides_bytes(stored_shape, itemsize_);
314 gather_strides_.assign(rank, 0);
315 for (std::size_t i = 0; i < rank; ++i) {
316 gather_strides_[order[i]] = stored_strides[i];
317 }
318 }
319
320 void set_byte_order(const CodecSpec& codec) {
321 std::string endian = "little";
322 const auto it = codec.configuration.find("endian");
323 if (it != codec.configuration.end()) {
324 if (!it->is_string()) {
325 throw error("codec 'bytes': 'endian' must be a string");
326 }
327 endian = it->get<std::string>();
328 }
329 if (endian != "little" && endian != "big") {
330 throw error("codec 'bytes': unknown endian '" + endian + "'");
331 }
332 const bool stored_little = endian == "little";
333 byteswap_ = swap_width_ > 1 && stored_little != detail::host_is_little_endian();
334 }
335
336 void add_byte_stage(const CodecSpec& codec, const ArrayMeta& meta) {
337 if (codec.name == "gzip" || codec.name == "zlib") {
338 add_deflate(codec);
339 } else if (codec.name == "crc32c") {
340 ByteStage stage;
341 stage.kind = ByteStage::Kind::crc32c;
342 byte_stages_.push_back(stage);
343 } else if (codec.name == "blosc") {
344 add_blosc(codec, meta);
345 } else if (codec.name == "zstd") {
346 add_zstd(codec);
347 } else if (codec.name == "shuffle") {
348 add_shuffle(codec, meta);
349 } else {
350 // sharding_indexed never reaches here: resolve()'s dispatch intercepts
351 // it (it is lowered into shard levels at parse time, not resolved).
352 throw error("unknown codec '" + codec.name + "'");
353 }
354 }
355
356 void add_deflate(const CodecSpec& codec) {
357 ByteStage stage;
358 stage.kind = ByteStage::Kind::deflate;
359 stage.gzip_framing = codec.name == "gzip";
360 const auto it = codec.configuration.find("level");
361 if (it != codec.configuration.end()) {
362 if (!it->is_number_integer() || it->get<std::int64_t>() < 0 || it->get<std::int64_t>() > 9) {
363 throw error("codec '" + codec.name + "': 'level' must be an integer in 0..9");
364 }
365 stage.level = it->get<int>();
366 }
367#ifndef LIBZARR_HAS_ZLIB
368 throw error("codec '" + codec.name +
369 "' is not built into this libzarr (compile with LIBZARR_HAS_ZLIB and link zlib)");
370#endif
371 byte_stages_.push_back(stage);
372 }
373
374 void add_zstd(const CodecSpec& codec) {
375 ByteStage stage;
376 stage.kind = ByteStage::Kind::zstd;
377 const json config = codec.configuration.is_object() ? codec.configuration : json::object();
378 const json level = config.value("level", json(std::int64_t{0}));
379 if (!level.is_number_integer()) {
380 throw error("codec 'zstd': 'level' must be an integer");
381 }
382 stage.zstd_level = level.get<int>();
383 const json checksum = config.value("checksum", json(false));
384 if (!checksum.is_boolean()) {
385 throw error("codec 'zstd': 'checksum' must be a boolean");
386 }
387 stage.zstd_checksum = checksum.get<bool>();
388#ifndef LIBZARR_HAS_ZSTD
389 throw error(
390 "codec 'zstd' is not built into this libzarr (compile with LIBZARR_HAS_ZSTD and link "
391 "zstd)");
392#endif
393 byte_stages_.push_back(stage);
394 }
395
396 void add_shuffle(const CodecSpec& codec, const ArrayMeta& meta) {
397 ByteStage stage;
398 stage.kind = ByteStage::Kind::shuffle;
399 const json config = codec.configuration.is_object() ? codec.configuration : json::object();
400 const std::int64_t elementsize = config.value("elementsize", std::int64_t{0});
401 if (elementsize < 0 || elementsize > 0xFFFF) {
402 throw error("filter 'shuffle': invalid elementsize " + std::to_string(elementsize));
403 }
404 // NCZarr writes elementsize 0 for "the dtype's item size"; the actual
405 // stored bytes are shuffled with the item size.
406 stage.shuffle_elementsize =
407 elementsize == 0 ? meta.dtype.itemsize : static_cast<std::uint32_t>(elementsize);
408 if (stage.shuffle_elementsize == 0) {
409 throw error("filter 'shuffle': element size cannot be zero");
410 }
411 byte_stages_.push_back(stage);
412 }
413
414 void add_blosc(const CodecSpec& codec, const ArrayMeta& meta) {
415 ByteStage stage;
416 stage.kind = ByteStage::Kind::blosc;
417 // A default-constructed CodecSpec has a *null* configuration, on which
418 // json::value() throws; normalize to an empty object.
419 const json config = codec.configuration.is_object() ? codec.configuration : json::object();
420 stage.blosc_cname = config.value("cname", "lz4");
421 const std::int64_t clevel = config.value("clevel", std::int64_t{5});
422 if (clevel < 0 || clevel > 9) {
423 throw error("codec 'blosc': 'clevel' must be in 0..9");
424 }
425 stage.blosc_clevel = static_cast<int>(clevel);
426 const json shuffle = config.value("shuffle", json("shuffle"));
427 if (shuffle == "noshuffle") {
428 stage.blosc_shuffle = 0;
429 } else if (shuffle == "shuffle") {
430 stage.blosc_shuffle = 1;
431 } else if (shuffle == "bitshuffle") {
432 stage.blosc_shuffle = 2;
433 } else if (shuffle.is_number_integer() && shuffle.get<std::int64_t>() >= -1 &&
434 shuffle.get<std::int64_t>() <= 2) {
435 // v2 numcodecs uses numeric shuffle; -1 = automatic (bitshuffle for
436 // 1-byte types, else byte shuffle) — read tolerance.
437 const auto n = shuffle.get<std::int64_t>();
438 if (n == -1) {
439 stage.blosc_shuffle = meta.dtype.itemsize == 1 ? 2 : 1;
440 } else {
441 stage.blosc_shuffle = static_cast<int>(n);
442 }
443 } else {
444 throw error("codec 'blosc': unknown shuffle " + shuffle.dump());
445 }
446 stage.blosc_typesize =
447 static_cast<std::uint32_t>(config.value("typesize", std::int64_t{meta.dtype.itemsize}));
448 stage.blosc_blocksize = static_cast<std::uint64_t>(config.value("blocksize", std::int64_t{0}));
449 bool known_cname = false;
450 for (const char* name : {"blosclz", "lz4", "lz4hc", "snappy", "zlib", "zstd"}) {
451 known_cname = known_cname || stage.blosc_cname == name;
452 }
453 if (!known_cname) {
454 throw error("codec 'blosc': unknown cname '" + stage.blosc_cname + "'");
455 }
456#ifndef LIBZARR_HAS_BLOSC
457 throw error(
458 "codec 'blosc' is not built into this libzarr (compile with LIBZARR_HAS_BLOSC and link "
459 "c-blosc)");
460#endif
461 byte_stages_.push_back(stage);
462 }
463
467 void compute_expected_sizes() {
468 decode_expected_.assign(byte_stages_.size(), std::nullopt);
469 std::optional<std::uint64_t> size = chunk_bytes_;
470 for (std::size_t i = 0; i < byte_stages_.size(); ++i) {
471 decode_expected_[i] = size; // decode of stage i must yield its encode input
472 if (!size) {
473 continue;
474 }
475 if (byte_stages_[i].kind == ByteStage::Kind::crc32c) {
476 size = *size + 4;
477 } else if (byte_stages_[i].kind == ByteStage::Kind::shuffle) {
478 // size-preserving
479 } else {
480 size = std::nullopt; // compressed size is unknowable
481 }
482 }
483 }
484
485 std::vector<std::uint64_t> chunk_shape_;
486 std::uint32_t itemsize_ = 1;
487 std::uint64_t chunk_bytes_ = 0;
488 std::uint32_t swap_width_ = 1;
489 bool byteswap_ = false;
490 std::optional<std::vector<std::uint32_t>> transpose_order_;
491 std::vector<std::uint64_t> gather_strides_;
492 std::vector<ByteStage> byte_stages_;
493 std::vector<std::optional<std::uint64_t>> decode_expected_;
494};
495
496} // namespace zarr
497
498#endif // LIBZARR_CODECS_HPP
Definition codecs.hpp:37
Bytes decode(Bytes stored) const
Definition codecs.hpp:141
bool is_identity() const
Definition codecs.hpp:96
bool supports_partial_read() const
Definition codecs.hpp:104
static CodecPipeline resolve(const ArrayMeta &meta)
Definition codecs.hpp:43
Bytes encode(Bytes chunk) const
Definition codecs.hpp:110
Bytes decode_range(Bytes raw) const
Definition codecs.hpp:131
std::uint64_t decoded_chunk_bytes() const
Size of a decoded full chunk in bytes.
Definition codecs.hpp:91
Definition types.hpp:36
nlohmann::json json
Definition metadata.hpp:30
CodecSpec shuffle(int elementsize=0)
shuffle: byte-transposition filter. elementsize 0 means the dtype size.
Definition metadata.hpp:69
Definition metadata.hpp:98
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
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
Definition metadata.hpp:36
std::uint32_t itemsize
Element size in bytes.
Definition types.hpp:126
DType kind
Element type kind.
Definition types.hpp:124
std::vector< std::uint8_t > Bytes
Owned byte buffer used throughout the value-based public API.
Definition types.hpp:42
constexpr bool is_complex(DType kind)
True for complex64/complex128.
Definition types.hpp:88