Line data Source code
1 : /******************************************************************************
2 : *
3 : * Project: GDAL
4 : * Purpose: Icechunk driver
5 : * Author: Even Rouault <even dot rouault at spatialys.com>
6 : *
7 : ******************************************************************************
8 : * Copyright (c) 2026, Even Rouault <even dot rouault at spatialys.com>
9 : *
10 : * SPDX-License-Identifier: MIT
11 : ****************************************************************************/
12 :
13 : #include "icechunkmanifest.h"
14 : #include "icechunkutils.h"
15 : #include "icechunkdrivercore.h"
16 :
17 : /* ------------------------------------------------------------------------- */
18 :
19 : #if defined(__clang__)
20 : #pragma clang diagnostic push
21 : #pragma clang diagnostic ignored "-Wdocumentation"
22 : #pragma clang diagnostic ignored "-Wdocumentation-unknown-command"
23 : #endif
24 : #include <zstd.h>
25 : #if defined(__clang__)
26 : #pragma clang diagnostic pop
27 : #endif
28 :
29 : /* ------------------------------------------------------------------------- */
30 :
31 : #include <algorithm>
32 : #include <cinttypes>
33 : #include <limits>
34 : #if __cplusplus >= 202002L
35 : #include <ranges>
36 : #endif
37 :
38 : /* ------------------------------------------------------------------------- */
39 :
40 : #if defined(__GNUC__)
41 : #pragma GCC diagnostic push
42 : #pragma GCC diagnostic ignored "-Weffc++"
43 : #pragma GCC diagnostic ignored "-Wnull-dereference"
44 : #endif
45 :
46 : #if defined(__clang__)
47 : #pragma clang diagnostic push
48 : #pragma clang diagnostic ignored "-Wweak-vtables"
49 : #endif
50 :
51 : #include "generated/manifest_generated.h"
52 :
53 : #if defined(__clang__)
54 : #pragma clang diagnostic pop
55 : #endif
56 :
57 : #if defined(__GNUC__)
58 : #pragma GCC diagnostic pop
59 : #endif
60 :
61 : /* ------------------------------------------------------------------------- */
62 :
63 : using namespace flatbuffers;
64 : using namespace generated;
65 :
66 : namespace gdal::icechunk
67 : {
68 : IcechunkManifest::IcechunkManifest() = default;
69 :
70 : IcechunkManifest::~IcechunkManifest() = default;
71 :
72 : #if defined(__GNUC__)
73 : #pragma GCC diagnostic push
74 : #pragma GCC diagnostic ignored "-Wnull-dereference"
75 : #endif
76 :
77 : /************************************************************************/
78 : /* IcechunkManifest::Open() */
79 : /************************************************************************/
80 :
81 : std::unique_ptr<IcechunkManifest>
82 3616 : IcechunkManifest::Open(const char *pszFilename)
83 : {
84 3616 : CPLDebugOnly("Icechunk", "Opening manifest %s", pszFilename);
85 7232 : auto fp = VSIFilesystemHandler::OpenStatic(pszFilename, "rb");
86 3616 : if (!fp)
87 : {
88 1 : CPLError(CE_Failure, CPLE_FileIO, "Cannot open %s", pszFilename);
89 1 : return nullptr;
90 : }
91 :
92 3615 : int nVersion = 0;
93 3615 : auto [buffer, size] =
94 7230 : DecompressFile(pszFilename, fp.get(), FILE_TYPE_MANIFEST, &nVersion);
95 3615 : if (!buffer)
96 1 : return nullptr;
97 :
98 : {
99 : // By default max_tables is 1 million, which can be insufficient for
100 : // some datasets. See https://github.com/OSGeo/gdal/issues/14830
101 : const uoffset_t max_tables = static_cast<uoffset_t>(
102 3614 : std::min<uint64_t>(size, std::numeric_limits<uoffset_t>::max()));
103 3614 : Verifier verifier(buffer.get(), size, /* max_depth = */ 64, max_tables);
104 3614 : if (!VerifyManifestBuffer(verifier))
105 : {
106 1 : CPLError(CE_Failure, CPLE_AppDefined,
107 : "%s: invalid Manifest Flatbuffer", pszFilename);
108 1 : return nullptr;
109 : }
110 : }
111 :
112 3613 : const auto *fbsManifest = GetManifest(buffer.get());
113 3613 : const auto *id = fbsManifest->id();
114 3613 : CPLAssertNotNull(id); // guaranteed by VerifyManifestBuffer()
115 3613 : const auto idBytes = id->bytes();
116 3613 : CPLAssertNotNull(idBytes); // guaranteed by VerifyManifestBuffer()
117 7226 : const std::string idBase32 = CrockfordBase32Encode(*idBytes);
118 3613 : if (idBase32 != CPLGetFilename(pszFilename))
119 : {
120 1 : CPLError(CE_Failure, CPLE_AppDefined, "%s: id=%s != expected %s",
121 : pszFilename, idBase32.c_str(), CPLGetFilename(pszFilename));
122 1 : return nullptr;
123 : }
124 :
125 7224 : auto manifest = std::make_unique<IcechunkManifest>();
126 3612 : manifest->m_osFilename = pszFilename;
127 :
128 3612 : constexpr int COMPRESSION_ALG_NONE = 0;
129 3612 : constexpr int COMPRESSION_ALG_ZSTD_DICT = 1;
130 3612 : const int nCompressionAlg = nVersion == 1
131 3612 : ? COMPRESSION_ALG_NONE
132 3609 : : fbsManifest->compression_algorithm();
133 3612 : if (nCompressionAlg != COMPRESSION_ALG_NONE &&
134 : nCompressionAlg != COMPRESSION_ALG_ZSTD_DICT)
135 : {
136 1 : CPLError(CE_Failure, CPLE_AppDefined,
137 : "%s: invalid compression_algorithm = %d", pszFilename,
138 : nCompressionAlg);
139 1 : return nullptr;
140 : }
141 :
142 : struct ZSTDContextFreer
143 : {
144 14 : void operator()(ZSTD_DCtx *ctx)
145 : {
146 14 : ZSTD_freeDCtx(ctx);
147 14 : }
148 : };
149 :
150 3611 : std::unique_ptr<ZSTD_DCtx, ZSTDContextFreer> dctx;
151 3611 : if (nCompressionAlg == COMPRESSION_ALG_ZSTD_DICT)
152 : {
153 14 : dctx.reset(ZSTD_createDCtx());
154 14 : if (!dctx)
155 0 : return nullptr;
156 14 : if (const auto location_dictionary = fbsManifest->location_dictionary())
157 : {
158 : #if (ZSTD_VERSION_MAJOR > 1) || \
159 : (ZSTD_VERSION_MAJOR == 1 && ZSTD_VERSION_MINOR >= 4)
160 2 : CPLDebugOnly("Icechunk", "%s: ZSTD dictionary of size %u",
161 : pszFilename,
162 : static_cast<uint32_t>(location_dictionary->size()));
163 2 : if (ZSTD_isError(ZSTD_DCtx_loadDictionary(
164 2 : dctx.get(), location_dictionary->data(),
165 4 : location_dictionary->size())))
166 : {
167 1 : CPLError(CE_Failure, CPLE_AppDefined,
168 : "%s: ZSTD_DCtx_loadDictionary() failed", pszFilename);
169 1 : return nullptr;
170 : }
171 : #else
172 : #error "ZSTD_DCtx_loadDictionary() requires libzstd >= 1.4"
173 : #endif
174 : }
175 : }
176 :
177 : // 1024 should be sufficiently large for any practical purpose
178 7220 : std::vector<char> achTempDecompressedLocation(1024);
179 :
180 3610 : const auto *fbsArrays = fbsManifest->arrays();
181 3610 : CPLAssertAlways(fbsArrays); // guaranteed by VerifyManifestBuffer()
182 3610 : manifest->m_arrayManifests.reserve(fbsArrays->size());
183 :
184 7213 : for (const auto *arrayManifestFbs : *fbsArrays)
185 : {
186 3611 : const auto *fbsNodeId = arrayManifestFbs->node_id();
187 3611 : CPLAssertNotNull(fbsNodeId); // guaranteed by VerifyManifestBuffer()
188 3611 : const auto fbsNodeIdBytes = fbsNodeId->bytes();
189 3611 : CPLAssertAlways(
190 : fbsNodeIdBytes); // guaranteed by VerifyManifestBuffer()
191 :
192 3611 : ArrayManifest arrayManifest;
193 3611 : ObjectId8 &nodeId = arrayManifest.nodeId;
194 : static_assert(sizeof(*fbsNodeIdBytes) == sizeof(nodeId));
195 3611 : memcpy(nodeId.data(), fbsNodeIdBytes->data(), sizeof(nodeId));
196 :
197 : // We rely on that order, required by the spec, in GetChunkRef()
198 3612 : if (!manifest->m_arrayManifests.empty() &&
199 1 : nodeId <= manifest->m_arrayManifests.back().nodeId)
200 : {
201 1 : CPLError(
202 : CE_Failure, CPLE_AppDefined,
203 : "%s: arrayManifests array not sorted by increasing node id",
204 : pszFilename);
205 1 : return nullptr;
206 : }
207 :
208 3615 : const auto GetNodeIdStr = [fbsNodeIdBytes]()
209 3615 : { return CrockfordBase32Encode(*fbsNodeIdBytes); };
210 :
211 3610 : CPLDebugOnly("Icechunk", "%s: manifest nodeId %s", pszFilename,
212 : GetNodeIdStr().c_str());
213 3610 : const auto *refs = arrayManifestFbs->refs();
214 3610 : CPLAssertAlways(refs); // guaranteed by VerifyManifestBuffer()
215 3610 : arrayManifest.chunkRefs.reserve(refs->size());
216 :
217 3610 : manifest->m_chunkRefsCount += refs->size();
218 :
219 1475430 : for (const auto *ref : *refs)
220 : {
221 1471820 : const auto *index = ref->index();
222 1471820 : CPLAssertAlways(index); // guaranteed by VerifyManifestBuffer()
223 :
224 1471820 : ChunkRef chunkRef;
225 :
226 1471820 : chunkRef.idx = ChunkIdx(index->begin(), index->end());
227 1471820 : if (!arrayManifest.chunkRefs.empty())
228 : {
229 1468220 : const auto &prevChunkRef = arrayManifest.chunkRefs.back();
230 :
231 : // Not formally needed by the spec, but cannot hurt
232 1468220 : if (chunkRef.idx.size() != prevChunkRef.idx.size())
233 : {
234 1 : CPLError(CE_Failure, CPLE_AppDefined,
235 : "%s: chunkRefs array for node %s: chunk index do "
236 : "not have the same dimension",
237 2 : pszFilename, GetNodeIdStr().c_str());
238 1 : return nullptr;
239 : }
240 :
241 : // We rely on that order, required by the spec, in GetChunkRef()
242 1468220 : if (chunkRef.idx <= prevChunkRef.idx)
243 : {
244 1 : CPLError(CE_Failure, CPLE_AppDefined,
245 : "%s: chunkRefs array for node %s: not sorted by "
246 : "increasing chunk index",
247 2 : pszFilename, GetNodeIdStr().c_str());
248 1 : return nullptr;
249 : }
250 : }
251 :
252 1471820 : chunkRef.offset = ref->offset();
253 1471820 : chunkRef.length = ref->length();
254 2943650 : if (chunkRef.offset >
255 1471820 : std::numeric_limits<uint64_t>::max() - chunkRef.length)
256 : {
257 1 : CPLError(CE_Failure, CPLE_AppDefined,
258 : "%s: chunkRef: invalid offset/size", pszFilename);
259 1 : return nullptr;
260 : }
261 :
262 1471820 : chunkRef.checksumLastModified = ref->checksum_last_modified();
263 :
264 1471820 : if (const auto checksumEtag = ref->checksum_etag())
265 : {
266 0 : chunkRef.checksumEtag = GetString(checksumEtag);
267 : }
268 :
269 1471820 : int nAlternativeCount = 0;
270 1471820 : if (const auto inlineContent = ref->inline_())
271 : {
272 271355 : ++nAlternativeCount;
273 271355 : chunkRef.inlineContent.insert(chunkRef.inlineContent.end(),
274 271355 : inlineContent->begin(),
275 542710 : inlineContent->end());
276 :
277 271355 : if (chunkRef.offset != 0 || chunkRef.length != 0)
278 : {
279 1 : CPLError(CE_Failure, CPLE_AppDefined,
280 : "%s: chunkRef: offset/size != 0 found with inline "
281 : "content",
282 : pszFilename);
283 1 : return nullptr;
284 : }
285 : }
286 :
287 1471820 : if (const auto chunk_id = ref->chunk_id())
288 : {
289 1183090 : ++nAlternativeCount;
290 1183090 : CPLAssertAlways(
291 : chunk_id->bytes()); // guaranteed by VerifyManifestBuffer()
292 1183090 : chunkRef.chunkId = CrockfordBase32Encode(*(chunk_id->bytes()));
293 : }
294 :
295 1471820 : if (const auto location = ref->location())
296 : {
297 17364 : ++nAlternativeCount;
298 17364 : chunkRef.location = GetString(location);
299 : }
300 :
301 1471820 : if (const auto compressed_location = ref->compressed_location())
302 : {
303 12 : ++nAlternativeCount;
304 12 : if (nCompressionAlg == COMPRESSION_ALG_NONE)
305 : {
306 : // Code path likely not possible when using Icechunk writer
307 : const char *pchLocation = reinterpret_cast<const char *>(
308 1 : compressed_location->data());
309 : chunkRef.location.insert(
310 0 : chunkRef.location.end(), pchLocation,
311 1 : pchLocation + compressed_location->size());
312 : }
313 : else
314 : {
315 11 : CPLAssert(nCompressionAlg == COMPRESSION_ALG_ZSTD_DICT);
316 :
317 11 : const size_t nStatus = ZSTD_decompressDCtx(
318 11 : dctx.get(), achTempDecompressedLocation.data(),
319 : achTempDecompressedLocation.size(),
320 11 : compressed_location->data(),
321 11 : compressed_location->size());
322 11 : if (ZSTD_isError(nStatus))
323 : {
324 1 : CPLError(CE_Failure, CPLE_AppDefined,
325 : "%s: chunkRef node_id %s: "
326 : "ZSTD_decompressDCtx() failed",
327 2 : pszFilename, GetNodeIdStr().c_str());
328 1 : return nullptr;
329 : }
330 10 : chunkRef.location.assign(achTempDecompressedLocation.data(),
331 10 : nStatus);
332 : }
333 : }
334 :
335 1471820 : if (nAlternativeCount == 0)
336 : {
337 1 : CPLError(CE_Failure, CPLE_AppDefined,
338 : "%s: chunkRef node_id %s: not inline, chunk or "
339 : "virtual location",
340 2 : pszFilename, GetNodeIdStr().c_str());
341 1 : return nullptr;
342 : }
343 1471820 : else if (nAlternativeCount > 1)
344 : {
345 2 : CPLError(CE_Failure, CPLE_AppDefined,
346 : "%s: chunkRef node_id %s: more than one method among "
347 : "inline, chunk or virtual location found. "
348 : "inlineContent.size() = %" PRIu64 ", offset = %" PRIu64
349 : ", length = %" PRIu64 ", chunkId=%s, location=%s",
350 2 : pszFilename, GetNodeIdStr().c_str(),
351 1 : static_cast<uint64_t>(chunkRef.inlineContent.size()),
352 : chunkRef.offset, chunkRef.length,
353 : chunkRef.chunkId.c_str(), chunkRef.location.c_str());
354 1 : return nullptr;
355 : }
356 :
357 1471820 : arrayManifest.chunkRefs.push_back(std::move(chunkRef));
358 : }
359 :
360 3603 : manifest->m_arrayManifests.push_back(std::move(arrayManifest));
361 : }
362 :
363 3602 : return manifest;
364 : }
365 :
366 : #if defined(__GNUC__)
367 : #pragma GCC diagnostic pop
368 : #endif
369 :
370 : /************************************************************************/
371 : /* IcechunkManifest::GetChunkFilename() */
372 : /************************************************************************/
373 :
374 7 : std::string IcechunkManifest::GetChunkFilename(const std::string &chunkId) const
375 : {
376 : return CPLFormFilenameSafe(
377 14 : CPLFormFilenameSafe(
378 14 : CPLGetDirnameSafe(CPLGetDirnameSafe(m_osFilename.c_str()).c_str())
379 : .c_str(),
380 : "chunks", nullptr)
381 : .c_str(),
382 21 : chunkId.c_str(), nullptr);
383 : }
384 :
385 : /************************************************************************/
386 : /* IcechunkManifest::GetChunkRef() */
387 : /************************************************************************/
388 :
389 : const IcechunkManifest::ChunkRef *
390 8092 : IcechunkManifest::GetChunkRef(const ObjectId8 &nodeId,
391 : const ChunkIdx &idx) const
392 : {
393 : #if __cplusplus >= 202002L
394 : const auto iterArrayManifests = std::ranges::lower_bound(
395 : m_arrayManifests, nodeId, {}, &ArrayManifest::nodeId);
396 : #else
397 16184 : ArrayManifest arrayManifestLookup;
398 8092 : arrayManifestLookup.nodeId = nodeId;
399 : const auto iterArrayManifests = std::lower_bound(
400 : m_arrayManifests.begin(), m_arrayManifests.end(), arrayManifestLookup,
401 8092 : [](const ArrayManifest &a, const ArrayManifest &b)
402 16184 : { return a.nodeId < b.nodeId; });
403 : #endif
404 16184 : if (iterArrayManifests == m_arrayManifests.end() ||
405 8092 : iterArrayManifests->nodeId != nodeId)
406 : {
407 0 : return nullptr;
408 : }
409 8092 : const auto &arrayManifest = *iterArrayManifests;
410 :
411 8112 : if (idx.empty() && arrayManifest.chunkRefs.size() == 1 &&
412 8112 : arrayManifest.chunkRefs[0].idx.size() == 1 &&
413 0 : arrayManifest.chunkRefs[0].idx[0] == 0)
414 : {
415 : // Special case for scalar arrays such as "crs" written by Icechunk v0
416 0 : return &(arrayManifest.chunkRefs[0]);
417 : }
418 :
419 : #if __cplusplus >= 202002L
420 : const auto iterChunkRefs = std::ranges::lower_bound(
421 : arrayManifest.chunkRefs, idx, {}, &ChunkRef::idx);
422 : #else
423 16184 : ChunkRef chunkRefLookup;
424 8092 : chunkRefLookup.idx = idx;
425 : const auto iterChunkRefs = std::lower_bound(
426 : arrayManifest.chunkRefs.begin(), arrayManifest.chunkRefs.end(),
427 : chunkRefLookup,
428 32324 : [](const ChunkRef &a, const ChunkRef &b) { return a.idx < b.idx; });
429 : #endif
430 16184 : if (iterChunkRefs == arrayManifest.chunkRefs.end() ||
431 8092 : iterChunkRefs->idx != idx)
432 : {
433 6 : if (!arrayManifest.chunkRefs.empty() &&
434 3 : idx.size() != arrayManifest.chunkRefs.front().idx.size())
435 : {
436 1 : CPLError(CE_Failure, CPLE_AppDefined,
437 : "GetChunkRef(%s): querying with index of dimension %u "
438 : "whereas chunk refs have dimension %u",
439 2 : CrockfordBase32Encode(nodeId).c_str(),
440 1 : static_cast<unsigned>(idx.size()),
441 : static_cast<unsigned>(
442 1 : arrayManifest.chunkRefs.front().idx.size()));
443 : }
444 :
445 3 : return nullptr;
446 : }
447 :
448 8089 : return &(*iterChunkRefs);
449 : }
450 :
451 : } // namespace gdal::icechunk
|