libzarr
Header-only C++17 Zarr v2/v3, WASM-compatible
Loading...
Searching...
No Matches
zip.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2
3#ifndef LIBZARR_ZIP_HPP
4#define LIBZARR_ZIP_HPP
5
6#include <algorithm>
7#include <array>
8#include <cstdint>
9#include <map>
10#include <memory>
11#include <optional>
12#include <string>
13#include <string_view>
14#include <utility>
15#include <vector>
16
17#include "libzarr/detail/common.hpp"
18#include "libzarr/store.hpp"
19#include "libzarr/types.hpp"
20
28
29namespace zarr {
30
31namespace detail_zip {
32
33// APPNOTE 4.3.x structure signatures and fixed sizes.
34inline constexpr std::uint32_t kLocalSig = 0x04034b50;
35inline constexpr std::uint32_t kCentralSig = 0x02014b50;
36inline constexpr std::uint32_t kEocdSig = 0x06054b50;
37inline constexpr std::uint32_t kEocd64Sig = 0x06064b50;
38inline constexpr std::uint32_t kEocd64LocatorSig = 0x07064b50;
39inline constexpr std::size_t kEocdSize = 22;
40inline constexpr std::size_t kEocd64Size = 56;
41inline constexpr std::size_t kEocd64LocatorSize = 20;
42inline constexpr std::size_t kLocalHeaderSize = 30;
43inline constexpr std::size_t kCentralHeaderSize = 46;
44inline constexpr std::uint32_t kMax32 = 0xFFFFFFFFU;
45inline constexpr std::uint16_t kMax16 = 0xFFFFU;
46
47inline std::uint16_t rd16(const std::uint8_t* p) {
48 return static_cast<std::uint16_t>(p[0] | (static_cast<std::uint16_t>(p[1]) << 8U));
49}
50inline std::uint32_t rd32(const std::uint8_t* p) {
51 return static_cast<std::uint32_t>(p[0]) | (static_cast<std::uint32_t>(p[1]) << 8U) |
52 (static_cast<std::uint32_t>(p[2]) << 16U) | (static_cast<std::uint32_t>(p[3]) << 24U);
53}
54inline std::uint64_t rd64(const std::uint8_t* p) {
55 return static_cast<std::uint64_t>(rd32(p)) | (static_cast<std::uint64_t>(rd32(p + 4)) << 32U);
56}
57inline void wr16(Bytes& out, std::uint16_t v) {
58 out.push_back(static_cast<std::uint8_t>(v & 0xFFU));
59 out.push_back(static_cast<std::uint8_t>(v >> 8U));
60}
61inline void wr32(Bytes& out, std::uint32_t v) {
62 for (int i = 0; i < 4; ++i) {
63 out.push_back(static_cast<std::uint8_t>(v >> (8 * i)));
64 }
65}
66inline void wr64(Bytes& out, std::uint64_t v) {
67 for (int i = 0; i < 8; ++i) {
68 out.push_back(static_cast<std::uint8_t>(v >> (8 * i)));
69 }
70}
71
74inline std::uint32_t crc32(const std::uint8_t* data, std::size_t size) {
75 static const std::array<std::uint32_t, 256> table = [] {
76 std::array<std::uint32_t, 256> t{};
77 for (std::uint32_t i = 0; i < 256; ++i) {
78 std::uint32_t c = i;
79 for (int k = 0; k < 8; ++k) {
80 c = ((c & 1U) != 0) ? 0xEDB88320U ^ (c >> 1U) : c >> 1U;
81 }
82 t[i] = c;
83 }
84 return t;
85 }();
86 std::uint32_t c = 0xFFFFFFFFU;
87 for (std::size_t i = 0; i < size; ++i) {
88 c = table[(c ^ data[i]) & 0xFFU] ^ (c >> 8U);
89 }
90 return c ^ 0xFFFFFFFFU;
91}
92
93struct Entry {
94 std::uint64_t header_offset = 0;
95 std::uint64_t size = 0;
96 std::uint16_t method = 0;
99 std::optional<std::uint64_t> data_offset;
100};
101
102struct CentralDirectory {
103 std::uint64_t count = 0;
104 std::uint64_t size = 0;
105 std::uint64_t offset = 0;
106};
107
109struct PackEntry {
110 std::string name;
111 std::uint32_t crc = 0;
112 std::uint64_t size = 0;
113 std::uint64_t header_offset = 0;
114 bool wide = false;
115 bool far = false;
116};
117
118// APPNOTE 4.3.7. Determinism: zero timestamps (DOS epoch 1980-01-01), no
119// flags, no comments; version 4.5 only where ZIP64 structures are present.
120inline void append_local_header(Bytes& out, const PackEntry& e) {
121 wr32(out, kLocalSig);
122 wr16(out, e.wide ? 45 : 20);
123 wr16(out, 0); // flags
124 wr16(out, 0); // method: STORED
125 wr16(out, 0); // mod time
126 wr16(out, 0x0021); // mod date: 1980-01-01
127 wr32(out, e.crc);
128 const auto size32 = e.wide ? kMax32 : static_cast<std::uint32_t>(e.size);
129 wr32(out, size32); // compressed == uncompressed for STORED
130 wr32(out, size32);
131 wr16(out, static_cast<std::uint16_t>(e.name.size()));
132 wr16(out, e.wide ? 20 : 0); // extra length
133 out.insert(out.end(), e.name.begin(), e.name.end());
134 if (e.wide) {
135 wr16(out, 0x0001); // ZIP64 extra: usize + csize
136 wr16(out, 16);
137 wr64(out, e.size);
138 wr64(out, e.size);
139 }
140}
141
142// APPNOTE 4.3.12.
143inline void append_central_header(Bytes& cen, const PackEntry& e) {
144 const bool any64 = e.wide || e.far;
145 const auto size32 = e.wide ? kMax32 : static_cast<std::uint32_t>(e.size);
146 wr32(cen, kCentralSig);
147 wr16(cen, any64 ? 45 : 20); // version made by
148 wr16(cen, any64 ? 45 : 20); // version needed
149 wr16(cen, 0); // flags
150 wr16(cen, 0); // method
151 wr16(cen, 0); // time
152 wr16(cen, 0x0021); // date
153 wr32(cen, e.crc);
154 wr32(cen, size32);
155 wr32(cen, size32);
156 wr16(cen, static_cast<std::uint16_t>(e.name.size()));
157 wr16(cen, any64 ? static_cast<std::uint16_t>(4 + (e.wide ? 16 : 0) + (e.far ? 8 : 0)) : 0);
158 wr16(cen, 0); // comment length
159 wr16(cen, 0); // disk number
160 wr16(cen, 0); // internal attributes
161 wr32(cen, 0); // external attributes
162 wr32(cen, e.far ? kMax32 : static_cast<std::uint32_t>(e.header_offset));
163 cen.insert(cen.end(), e.name.begin(), e.name.end());
164 if (any64) {
165 wr16(cen, 0x0001);
166 wr16(cen, static_cast<std::uint16_t>((e.wide ? 16 : 0) + (e.far ? 8 : 0)));
167 if (e.wide) {
168 wr64(cen, e.size);
169 wr64(cen, e.size);
170 }
171 if (e.far) {
172 wr64(cen, e.header_offset);
173 }
174 }
175}
176
177// APPNOTE 4.3.14-16: EOCD64 + locator when any value overflows, then EOCD.
178inline void append_end_records(Bytes& out, std::uint64_t count, std::uint64_t cen_size,
179 std::uint64_t cen_offset, bool force_zip64) {
180 const bool eocd64 = force_zip64 || count >= kMax16 || cen_size >= kMax32 || cen_offset >= kMax32;
181 if (eocd64) {
182 const std::uint64_t eocd64_offset = out.size();
183 wr32(out, kEocd64Sig);
184 wr64(out, kEocd64Size - 12); // size of the remainder
185 wr16(out, 45); // version made by
186 wr16(out, 45); // version needed
187 wr32(out, 0); // this disk
188 wr32(out, 0); // central directory disk
189 wr64(out, count);
190 wr64(out, count);
191 wr64(out, cen_size);
192 wr64(out, cen_offset);
193 wr32(out, kEocd64LocatorSig);
194 wr32(out, 0); // disk with the EOCD64
195 wr64(out, eocd64_offset);
196 wr32(out, 1); // total disks
197 }
198 wr32(out, kEocdSig);
199 wr16(out, 0); // this disk
200 wr16(out, 0); // central directory disk
201 const auto count16 = eocd64 ? kMax16 : static_cast<std::uint16_t>(count);
202 wr16(out, count16);
203 wr16(out, count16);
204 wr32(out, eocd64 ? kMax32 : static_cast<std::uint32_t>(cen_size));
205 wr32(out, eocd64 ? kMax32 : static_cast<std::uint32_t>(cen_offset));
206 wr16(out, 0); // comment length
207}
208
209} // namespace detail_zip
210
215class ZipStore final : public Store {
216 public:
219 ZipStore(std::shared_ptr<Store> source, std::string archive_key)
220 : source_(std::move(source)), key_(std::move(archive_key)) {
221 if (!source_) {
222 throw error("ZipStore: null store");
223 }
224 parse_directory();
225 }
226
227 [[nodiscard]] std::optional<Bytes> read(std::string_view key) override {
228 return read_range(key, ByteRange::full());
229 }
230
231 [[nodiscard]] std::optional<Bytes> read_range(std::string_view key, ByteRange range) override {
232 const auto it = entries_.find(key);
233 if (it == entries_.end()) {
234 return std::nullopt;
235 }
236 detail_zip::Entry& entry = it->second;
237 if (entry.method != 0) {
238 // Scope guard: only STORED entries stay byte-range-readable.
239 throw error(context() + ": entry '" + std::string(key) + "' uses compression method " +
240 std::to_string(entry.method) + "; only STORED entries are supported");
241 }
242 std::uint64_t begin = 0;
243 std::uint64_t count = entry.size;
244 if (range.kind == ByteRange::Kind::slice) {
245 if (range.length > entry.size || range.offset > entry.size - range.length) {
246 throw error(context() + ": range out of bounds for entry '" + std::string(key) + "' (" +
247 std::to_string(entry.size) + " bytes)");
248 }
249 begin = range.offset;
250 count = range.length;
251 } else if (range.kind == ByteRange::Kind::suffix) {
252 if (range.length > entry.size) {
253 throw error(context() + ": suffix out of bounds for entry '" + std::string(key) + "' (" +
254 std::to_string(entry.size) + " bytes)");
255 }
256 begin = entry.size - range.length;
257 count = range.length;
258 }
259 return must_read(data_offset(key, entry) + begin, count);
260 }
261
262 [[nodiscard]] std::optional<std::uint64_t> size(std::string_view key) override {
263 const auto it = entries_.find(key);
264 if (it == entries_.end()) {
265 return std::nullopt;
266 }
267 return it->second.size;
268 }
269
270 [[nodiscard]] bool exists(std::string_view key) override {
271 return entries_.find(key) != entries_.end();
272 }
273
274 void write(std::string_view /*key*/, Bytes /*value*/) override {
275 throw error("ZipStore is read-only");
276 }
277 void erase(std::string_view /*key*/) override { throw error("ZipStore is read-only"); }
278
279 [[nodiscard]] std::vector<std::string> list_prefix(std::string_view prefix) override {
280 check_prefix(prefix);
281 std::vector<std::string> out;
282 for (auto it = entries_.lower_bound(prefix);
283 it != entries_.end() && detail::starts_with(it->first, prefix); ++it) {
284 out.push_back(it->first);
285 }
286 return out;
287 }
288
289 [[nodiscard]] DirListing list_dir(std::string_view prefix) override {
290 check_prefix(prefix);
291 DirListing out;
292 for (auto it = entries_.lower_bound(prefix);
293 it != entries_.end() && detail::starts_with(it->first, prefix); ++it) {
294 const auto rest = std::string_view(it->first).substr(prefix.size());
295 const auto slash = rest.find('/');
296 if (slash == std::string_view::npos) {
297 out.keys.emplace_back(rest);
298 } else {
299 const auto child = rest.substr(0, slash);
300 if (out.prefixes.empty() || out.prefixes.back() != child) {
301 out.prefixes.emplace_back(child);
302 }
303 }
304 }
305 return out;
306 }
307
309 [[nodiscard]] std::size_t entry_count() const { return entries_.size(); }
310
311 private:
312 [[nodiscard]] std::string context() const { return key_.empty() ? "zip archive" : key_; }
313
314 static void check_prefix(std::string_view prefix) {
315 if (!prefix.empty() && prefix.back() != '/') {
316 throw error("store prefix must be empty or end with '/', got '" + std::string(prefix) + "'");
317 }
318 }
319
320 [[nodiscard]] Bytes must_read(std::uint64_t offset, std::uint64_t length) {
321 auto bytes = source_->read_range(key_, ByteRange::slice(offset, length));
322 if (!bytes) {
323 throw error(context() + ": archive disappeared mid-read");
324 }
325 return *std::move(bytes);
326 }
327
328 void parse_directory() {
329 namespace z = detail_zip;
330 const auto archive_size = source_->size(key_);
331 if (!archive_size) {
332 throw error(context() + ": not found");
333 }
334 if (*archive_size < z::kEocdSize) {
335 throw error(context() + ": too small to be a zip archive");
336 }
337 // The EOCD sits within the final 22 + 65535 bytes (max comment); one
338 // suffix read also covers a possible ZIP64 locator just before it.
339 const std::uint64_t tail_len =
340 std::min<std::uint64_t>(*archive_size, z::kEocdSize + z::kMax16 + z::kEocd64LocatorSize);
341 auto tail_bytes = source_->read_range(key_, ByteRange::suffix(tail_len));
342 if (!tail_bytes) {
343 throw error(context() + ": archive disappeared mid-read");
344 }
345 const Bytes tail = *std::move(tail_bytes);
346 const std::uint64_t tail_start = *archive_size - tail_len;
347
348 const std::size_t eocd = find_eocd(tail);
349 const detail_zip::CentralDirectory dir = locate_directory(tail, tail_start, eocd);
350 const Bytes cen = must_read(dir.offset, dir.size);
351 std::size_t pos = 0;
352 for (std::uint64_t n = 0; n < dir.count; ++n) {
353 parse_entry(cen, pos);
354 }
355 }
356
358 [[nodiscard]] std::size_t find_eocd(const Bytes& tail) const {
359 namespace z = detail_zip;
360 for (std::size_t i = tail.size() - z::kEocdSize + 1; i-- > 0;) {
361 if (z::rd32(tail.data() + i) == z::kEocdSig &&
362 i + z::kEocdSize + z::rd16(tail.data() + i + 20) == tail.size()) {
363 return i;
364 }
365 }
366 throw error(context() + ": end-of-central-directory record not found (not a zip archive?)");
367 }
368
370 [[nodiscard]] detail_zip::CentralDirectory locate_directory(const Bytes& tail,
371 std::uint64_t tail_start,
372 std::size_t eocd) {
373 namespace z = detail_zip;
374 if (z::rd16(tail.data() + eocd + 4) != 0 || z::rd16(tail.data() + eocd + 6) != 0) {
375 throw error(context() + ": multi-disk archives are not supported");
376 }
377 z::CentralDirectory dir;
378 dir.count = z::rd16(tail.data() + eocd + 10);
379 dir.size = z::rd32(tail.data() + eocd + 12);
380 dir.offset = z::rd32(tail.data() + eocd + 16);
381 if (dir.count != z::kMax16 && dir.size != z::kMax32 && dir.offset != z::kMax32) {
382 return dir;
383 }
384 // ZIP64: the locator sits immediately before the EOCD.
385 if (eocd < z::kEocd64LocatorSize ||
386 z::rd32(tail.data() + eocd - z::kEocd64LocatorSize) != z::kEocd64LocatorSig) {
387 throw error(context() + ": ZIP64 locator not found");
388 }
389 const std::uint64_t eocd64_offset = z::rd64(tail.data() + eocd - z::kEocd64LocatorSize + 8);
390 Bytes eocd64;
391 if (eocd64_offset >= tail_start) {
392 const auto local = static_cast<std::size_t>(eocd64_offset - tail_start);
393 // APPNOTE 4.3.14: the ZIP64 EOCD must lie fully within the archive. A
394 // crafted locator can point past the tail we hold; assigning past end()
395 // is an out-of-bounds read (fuzz-found SEGV).
396 if (local > tail.size() || z::kEocd64Size > tail.size() - local) {
397 throw error(context() + ": ZIP64 end-of-central-directory record out of range");
398 }
399 eocd64.assign(tail.begin() + static_cast<std::ptrdiff_t>(local),
400 tail.begin() + static_cast<std::ptrdiff_t>(local + z::kEocd64Size));
401 } else {
402 eocd64 = must_read(eocd64_offset, z::kEocd64Size);
403 }
404 // must_read returns fewer bytes when the offset runs past EOF; the fixed
405 // field offsets below (rd32/rd64) require the whole record.
406 if (eocd64.size() < z::kEocd64Size) {
407 throw error(context() + ": ZIP64 end-of-central-directory record truncated");
408 }
409 if (z::rd32(eocd64.data()) != z::kEocd64Sig) {
410 throw error(context() + ": bad ZIP64 end-of-central-directory record");
411 }
412 dir.count = z::rd64(eocd64.data() + 32);
413 dir.size = z::rd64(eocd64.data() + 40);
414 dir.offset = z::rd64(eocd64.data() + 48);
415 return dir;
416 }
417
419 void parse_entry(const Bytes& cen, std::size_t& pos) {
420 namespace z = detail_zip;
421 if (pos + z::kCentralHeaderSize > cen.size() || z::rd32(cen.data() + pos) != z::kCentralSig) {
422 throw error(context() + ": corrupt central directory");
423 }
424 const std::uint8_t* h = cen.data() + pos;
425 z::Entry entry;
426 entry.method = z::rd16(h + 10);
427 std::uint64_t csize = z::rd32(h + 20);
428 entry.size = z::rd32(h + 24);
429 const std::size_t name_len = z::rd16(h + 28);
430 const std::size_t extra_len = z::rd16(h + 30);
431 const std::size_t comment_len = z::rd16(h + 32);
432 entry.header_offset = z::rd32(h + 42);
433 if (pos + z::kCentralHeaderSize + name_len + extra_len + comment_len > cen.size()) {
434 throw error(context() + ": corrupt central directory");
435 }
436 std::string name(reinterpret_cast<const char*>(h + z::kCentralHeaderSize), name_len);
437 apply_zip64_extra(h + z::kCentralHeaderSize + name_len, extra_len, entry, csize, name);
438
439 if (entry.method == 0 && csize != entry.size) {
440 throw error(context() + ": STORED entry '" + name + "' has mismatched sizes");
441 }
442 if (!name.empty() && name.back() != '/') { // skip directory placeholders
443 entries_.insert_or_assign(std::move(name), entry);
444 }
445 pos += z::kCentralHeaderSize + name_len + extra_len + comment_len;
446 }
447
451 void apply_zip64_extra(const std::uint8_t* extra, std::size_t extra_len, detail_zip::Entry& entry,
452 std::uint64_t& csize, const std::string& name) const {
453 namespace z = detail_zip;
454 std::size_t epos = 0;
455 while (epos + 4 <= extra_len) {
456 const std::uint16_t id = z::rd16(extra + epos);
457 const std::uint16_t len = z::rd16(extra + epos + 2);
458 if (epos + 4 + len > extra_len) {
459 throw error(context() + ": corrupt extra field in '" + name + "'");
460 }
461 if (id == 0x0001) {
462 const std::uint8_t* f = extra + epos + 4;
463 std::size_t fpos = 0;
464 const auto take64 = [&](std::uint64_t& value) {
465 if (fpos + 8 > len) {
466 throw error(context() + ": truncated ZIP64 extra field in '" + name + "'");
467 }
468 value = z::rd64(f + fpos);
469 fpos += 8;
470 };
471 if (entry.size == z::kMax32) {
472 take64(entry.size);
473 }
474 if (csize == z::kMax32) {
475 take64(csize);
476 }
477 if (entry.header_offset == z::kMax32) {
478 take64(entry.header_offset);
479 }
480 }
481 epos += std::size_t{4} + len;
482 }
483 }
484
488 std::uint64_t data_offset(std::string_view key, detail_zip::Entry& entry) {
489 namespace z = detail_zip;
490 if (!entry.data_offset) {
491 const Bytes lfh = must_read(entry.header_offset, z::kLocalHeaderSize);
492 if (z::rd32(lfh.data()) != z::kLocalSig) {
493 throw error(context() + ": corrupt local header for entry '" + std::string(key) + "'");
494 }
495 entry.data_offset = entry.header_offset + z::kLocalHeaderSize + z::rd16(lfh.data() + 26) +
496 z::rd16(lfh.data() + 28);
497 }
498 return *entry.data_offset;
499 }
500
501 std::shared_ptr<Store> source_;
502 std::string key_;
503 std::map<std::string, detail_zip::Entry, std::less<>> entries_;
504};
505
506namespace detail_zip {
507
511inline void zip_pack_impl(Store& source, Store& dest, const std::string& dest_key,
512 const std::string& prefix, bool force_zip64) {
513 namespace z = detail_zip;
514 Bytes out;
515 Bytes cen;
516 std::uint64_t count = 0;
517
518 for (const std::string& key : source.list_prefix(prefix)) {
519 const auto value = source.read(key);
520 if (!value) {
521 continue; // key vanished between list and read
522 }
523 z::PackEntry entry;
524 entry.name = key.substr(prefix.size());
525 if (entry.name.size() > z::kMax16) {
526 throw error("zip_pack: entry name too long: '" + entry.name + "'");
527 }
528 entry.crc = z::crc32(value->data(), value->size());
529 entry.size = value->size();
530 entry.header_offset = out.size();
531 entry.wide = force_zip64 || entry.size >= z::kMax32;
532 entry.far = force_zip64 || entry.header_offset >= z::kMax32;
533 z::append_local_header(out, entry);
534 out.insert(out.end(), value->begin(), value->end());
535 z::append_central_header(cen, entry);
536 ++count;
537 }
538
539 const std::uint64_t cen_offset = out.size();
540 out.insert(out.end(), cen.begin(), cen.end());
541 z::append_end_records(out, count, cen.size(), cen_offset, force_zip64);
542 dest.write(dest_key, std::move(out));
543}
544
545} // namespace detail_zip
546
550inline void zip_pack(Store& source, Store& dest, const std::string& dest_key,
551 const std::string& prefix = "") {
552 detail_zip::zip_pack_impl(source, dest, dest_key, prefix, /*force_zip64=*/false);
553}
554
555} // namespace zarr
556
557#endif // LIBZARR_ZIP_HPP
Definition store.hpp:83
Definition zip.hpp:215
void erase(std::string_view) override
Remove key; removing an absent key is a no-op.
Definition zip.hpp:277
std::optional< std::uint64_t > size(std::string_view key) override
Definition zip.hpp:262
DirListing list_dir(std::string_view prefix) override
Immediate children under prefix ("" or ending in '/').
Definition zip.hpp:289
std::optional< Bytes > read_range(std::string_view key, ByteRange range) override
Definition zip.hpp:231
bool exists(std::string_view key) override
True if key holds a value.
Definition zip.hpp:270
std::optional< Bytes > read(std::string_view key) override
Full value at key, or std::nullopt if the key is absent.
Definition zip.hpp:227
void write(std::string_view, Bytes) override
Create or replace the value at key.
Definition zip.hpp:274
std::size_t entry_count() const
Number of entries in the archive.
Definition zip.hpp:309
ZipStore(std::shared_ptr< Store > source, std::string archive_key)
Definition zip.hpp:219
std::vector< std::string > list_prefix(std::string_view prefix) override
All keys starting with prefix ("" or ending in '/'), sorted.
Definition zip.hpp:279
Definition types.hpp:36
Byte-range request for Store::read_range.
Definition store.hpp:28
@ suffix
the final length bytes
@ slice
length bytes starting at offset
std::uint64_t offset
Start of the range; used by Kind::slice only.
Definition store.hpp:39
static constexpr ByteRange full()
The whole value.
Definition store.hpp:44
std::uint64_t length
Number of bytes; used by Kind::slice and Kind::suffix.
Definition store.hpp:41
Kind kind
Which part of the value to read.
Definition store.hpp:37
Immediate children of a prefix, as returned by Store::list_dir.
Definition store.hpp:70
std::vector< std::string > keys
Child keys, relative to the queried prefix, sorted.
Definition store.hpp:72
std::vector< std::string > prefixes
Child prefixes ("directories"), relative, without trailing '/', sorted.
Definition store.hpp:74
std::vector< std::uint8_t > Bytes
Owned byte buffer used throughout the value-based public API.
Definition types.hpp:42
void zip_pack(Store &source, Store &dest, const std::string &dest_key, const std::string &prefix="")
Definition zip.hpp:550