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 2097 : IcechunkManifest::Open(const char *pszFilename)
83 : {
84 2097 : CPLDebugOnly("Icechunk", "Opening manifest %s", pszFilename);
85 4194 : auto fp = VSIFilesystemHandler::OpenStatic(pszFilename, "rb");
86 2097 : if (!fp)
87 : {
88 1 : CPLError(CE_Failure, CPLE_FileIO, "Cannot open %s", pszFilename);
89 1 : return nullptr;
90 : }
91 :
92 2096 : int nVersion = 0;
93 2096 : auto [buffer, size] =
94 4192 : DecompressFile(pszFilename, fp.get(), FILE_TYPE_MANIFEST, &nVersion);
95 2096 : if (!buffer)
96 1 : return nullptr;
97 :
98 : {
99 2095 : Verifier verifier(buffer.get(), size);
100 2095 : if (!VerifyManifestBuffer(verifier))
101 : {
102 1 : CPLError(CE_Failure, CPLE_AppDefined,
103 : "%s: invalid Manifest Flatbuffer", pszFilename);
104 1 : return nullptr;
105 : }
106 : }
107 :
108 2094 : const auto *fbsManifest = GetManifest(buffer.get());
109 2094 : const auto *id = fbsManifest->id();
110 2094 : CPLAssertNotNull(id); // guaranteed by VerifyManifestBuffer()
111 2094 : const auto idBytes = id->bytes();
112 2094 : CPLAssertNotNull(idBytes); // guaranteed by VerifyManifestBuffer()
113 4188 : const std::string idBase32 = CrockfordBase32Encode(*idBytes);
114 2094 : if (idBase32 != CPLGetFilename(pszFilename))
115 : {
116 1 : CPLError(CE_Failure, CPLE_AppDefined, "%s: id=%s != expected %s",
117 : pszFilename, idBase32.c_str(), CPLGetFilename(pszFilename));
118 1 : return nullptr;
119 : }
120 :
121 4186 : auto manifest = std::make_unique<IcechunkManifest>();
122 2093 : manifest->m_osFilename = pszFilename;
123 :
124 2093 : constexpr int COMPRESSION_ALG_NONE = 0;
125 2093 : constexpr int COMPRESSION_ALG_ZSTD_DICT = 1;
126 2093 : const int nCompressionAlg = nVersion == 1
127 2093 : ? COMPRESSION_ALG_NONE
128 2091 : : fbsManifest->compression_algorithm();
129 2093 : if (nCompressionAlg != COMPRESSION_ALG_NONE &&
130 : nCompressionAlg != COMPRESSION_ALG_ZSTD_DICT)
131 : {
132 1 : CPLError(CE_Failure, CPLE_AppDefined,
133 : "%s: invalid compression_algorithm = %d", pszFilename,
134 : nCompressionAlg);
135 1 : return nullptr;
136 : }
137 :
138 : struct ZSTDContextFreer
139 : {
140 13 : void operator()(ZSTD_DCtx *ctx)
141 : {
142 13 : ZSTD_freeDCtx(ctx);
143 13 : }
144 : };
145 :
146 2092 : std::unique_ptr<ZSTD_DCtx, ZSTDContextFreer> dctx;
147 2092 : if (nCompressionAlg == COMPRESSION_ALG_ZSTD_DICT)
148 : {
149 13 : dctx.reset(ZSTD_createDCtx());
150 13 : if (!dctx)
151 0 : return nullptr;
152 13 : if (const auto location_dictionary = fbsManifest->location_dictionary())
153 : {
154 : #if (ZSTD_VERSION_MAJOR > 1) || \
155 : (ZSTD_VERSION_MAJOR == 1 && ZSTD_VERSION_MINOR >= 4)
156 2 : CPLDebugOnly("Icechunk", "%s: ZSTD dictionary of size %u",
157 : pszFilename,
158 : static_cast<uint32_t>(location_dictionary->size()));
159 2 : if (ZSTD_isError(ZSTD_DCtx_loadDictionary(
160 2 : dctx.get(), location_dictionary->data(),
161 4 : location_dictionary->size())))
162 : {
163 1 : CPLError(CE_Failure, CPLE_AppDefined,
164 : "%s: ZSTD_DCtx_loadDictionary() failed", pszFilename);
165 1 : return nullptr;
166 : }
167 : #else
168 : #error "ZSTD_DCtx_loadDictionary() requires libzstd >= 1.4"
169 : #endif
170 : }
171 : }
172 :
173 : // 1024 should be sufficiently large for any practical purpose
174 4182 : std::vector<char> achTempDecompressedLocation(1024);
175 :
176 2091 : const auto *fbsArrays = fbsManifest->arrays();
177 2091 : CPLAssertAlways(fbsArrays); // guaranteed by VerifyManifestBuffer()
178 2091 : manifest->m_arrayManifests.reserve(fbsArrays->size());
179 :
180 4175 : for (const auto *arrayManifestFbs : *fbsArrays)
181 : {
182 2092 : const auto *fbsNodeId = arrayManifestFbs->node_id();
183 2092 : CPLAssertNotNull(fbsNodeId); // guaranteed by VerifyManifestBuffer()
184 2092 : const auto fbsNodeIdBytes = fbsNodeId->bytes();
185 2092 : CPLAssertAlways(
186 : fbsNodeIdBytes); // guaranteed by VerifyManifestBuffer()
187 :
188 2092 : ArrayManifest arrayManifest;
189 2092 : ObjectId8 &nodeId = arrayManifest.nodeId;
190 : static_assert(sizeof(*fbsNodeIdBytes) == sizeof(nodeId));
191 2092 : memcpy(nodeId.data(), fbsNodeIdBytes->data(), sizeof(nodeId));
192 :
193 : // We rely on that order, required by the spec, in GetChunkRef()
194 2093 : if (!manifest->m_arrayManifests.empty() &&
195 1 : nodeId <= manifest->m_arrayManifests.back().nodeId)
196 : {
197 1 : CPLError(
198 : CE_Failure, CPLE_AppDefined,
199 : "%s: arrayManifests array not sorted by increasing node id",
200 : pszFilename);
201 1 : return nullptr;
202 : }
203 :
204 2096 : const auto GetNodeIdStr = [fbsNodeIdBytes]()
205 2096 : { return CrockfordBase32Encode(*fbsNodeIdBytes); };
206 :
207 2091 : CPLDebugOnly("Icechunk", "%s: manifest nodeId %s", pszFilename,
208 : GetNodeIdStr().c_str());
209 2091 : const auto *refs = arrayManifestFbs->refs();
210 2091 : CPLAssertAlways(refs); // guaranteed by VerifyManifestBuffer()
211 2091 : arrayManifest.chunkRefs.reserve(refs->size());
212 :
213 2091 : manifest->m_chunkRefsCount += refs->size();
214 :
215 14730 : for (const auto *ref : *refs)
216 : {
217 12646 : const auto *index = ref->index();
218 12646 : CPLAssertAlways(index); // guaranteed by VerifyManifestBuffer()
219 :
220 12646 : ChunkRef chunkRef;
221 :
222 12646 : chunkRef.idx = ChunkIdx(index->begin(), index->end());
223 12646 : if (!arrayManifest.chunkRefs.empty())
224 : {
225 10556 : const auto &prevChunkRef = arrayManifest.chunkRefs.back();
226 :
227 : // Not formally needed by the spec, but cannot hurt
228 10556 : if (chunkRef.idx.size() != prevChunkRef.idx.size())
229 : {
230 1 : CPLError(CE_Failure, CPLE_AppDefined,
231 : "%s: chunkRefs array for node %s: chunk index do "
232 : "not have the same dimension",
233 2 : pszFilename, GetNodeIdStr().c_str());
234 1 : return nullptr;
235 : }
236 :
237 : // We rely on that order, required by the spec, in GetChunkRef()
238 10555 : if (chunkRef.idx <= prevChunkRef.idx)
239 : {
240 1 : CPLError(CE_Failure, CPLE_AppDefined,
241 : "%s: chunkRefs array for node %s: not sorted by "
242 : "increasing chunk index",
243 2 : pszFilename, GetNodeIdStr().c_str());
244 1 : return nullptr;
245 : }
246 : }
247 :
248 12644 : chunkRef.offset = ref->offset();
249 12644 : chunkRef.length = ref->length();
250 25288 : if (chunkRef.offset >
251 12644 : std::numeric_limits<uint64_t>::max() - chunkRef.length)
252 : {
253 1 : CPLError(CE_Failure, CPLE_AppDefined,
254 : "%s: chunkRef: invalid offset/size", pszFilename);
255 1 : return nullptr;
256 : }
257 :
258 : #ifdef DEBUG
259 12643 : chunkRef.checksumLastModified = ref->checksum_last_modified();
260 :
261 12643 : if (const auto checksumEtag = ref->checksum_etag())
262 : {
263 0 : chunkRef.checksumEtag = GetString(checksumEtag);
264 : }
265 : #endif
266 :
267 12643 : int nAlternativeCount = 0;
268 12643 : if (const auto inlineContent = ref->inline_())
269 : {
270 2100 : ++nAlternativeCount;
271 2100 : chunkRef.inlineContent.insert(chunkRef.inlineContent.end(),
272 2100 : inlineContent->begin(),
273 4200 : inlineContent->end());
274 :
275 2100 : if (chunkRef.offset != 0 || chunkRef.length != 0)
276 : {
277 1 : CPLError(CE_Failure, CPLE_AppDefined,
278 : "%s: chunkRef: offset/size != 0 found with inline "
279 : "content",
280 : pszFilename);
281 1 : return nullptr;
282 : }
283 : }
284 :
285 12642 : if (const auto chunk_id = ref->chunk_id())
286 : {
287 258 : ++nAlternativeCount;
288 258 : CPLAssertAlways(
289 : chunk_id->bytes()); // guaranteed by VerifyManifestBuffer()
290 258 : chunkRef.chunkId = CrockfordBase32Encode(*(chunk_id->bytes()));
291 : }
292 :
293 12642 : if (const auto location = ref->location())
294 : {
295 10273 : ++nAlternativeCount;
296 10273 : chunkRef.location = GetString(location);
297 : }
298 :
299 12642 : if (const auto compressed_location = ref->compressed_location())
300 : {
301 12 : ++nAlternativeCount;
302 12 : if (nCompressionAlg == COMPRESSION_ALG_NONE)
303 : {
304 : // Code path likely not possible when using Icechunk writer
305 : const char *pchLocation = reinterpret_cast<const char *>(
306 1 : compressed_location->data());
307 : chunkRef.location.insert(
308 0 : chunkRef.location.end(), pchLocation,
309 1 : pchLocation + compressed_location->size());
310 : }
311 : else
312 : {
313 11 : CPLAssert(nCompressionAlg == COMPRESSION_ALG_ZSTD_DICT);
314 :
315 11 : const size_t nStatus = ZSTD_decompressDCtx(
316 11 : dctx.get(), achTempDecompressedLocation.data(),
317 : achTempDecompressedLocation.size(),
318 11 : compressed_location->data(),
319 11 : compressed_location->size());
320 11 : if (ZSTD_isError(nStatus))
321 : {
322 1 : CPLError(CE_Failure, CPLE_AppDefined,
323 : "%s: chunkRef node_id %s: "
324 : "ZSTD_decompressDCtx() failed",
325 2 : pszFilename, GetNodeIdStr().c_str());
326 1 : return nullptr;
327 : }
328 10 : chunkRef.location.assign(achTempDecompressedLocation.data(),
329 10 : nStatus);
330 : }
331 : }
332 :
333 12641 : if (nAlternativeCount == 0)
334 : {
335 1 : CPLError(CE_Failure, CPLE_AppDefined,
336 : "%s: chunkRef node_id %s: not inline, chunk or "
337 : "virtual location",
338 2 : pszFilename, GetNodeIdStr().c_str());
339 1 : return nullptr;
340 : }
341 12640 : else if (nAlternativeCount > 1)
342 : {
343 2 : CPLError(CE_Failure, CPLE_AppDefined,
344 : "%s: chunkRef node_id %s: more than one method among "
345 : "inline, chunk or virtual location found. "
346 : "inlineContent.size() = %" PRIu64 ", offset = %" PRIu64
347 : ", length = %" PRIu64 ", chunkId=%s, location=%s",
348 2 : pszFilename, GetNodeIdStr().c_str(),
349 1 : static_cast<uint64_t>(chunkRef.inlineContent.size()),
350 : chunkRef.offset, chunkRef.length,
351 : chunkRef.chunkId.c_str(), chunkRef.location.c_str());
352 1 : return nullptr;
353 : }
354 :
355 12639 : arrayManifest.chunkRefs.push_back(std::move(chunkRef));
356 : }
357 :
358 2084 : manifest->m_arrayManifests.push_back(std::move(arrayManifest));
359 : }
360 :
361 2083 : return manifest;
362 : }
363 :
364 : #if defined(__GNUC__)
365 : #pragma GCC diagnostic pop
366 : #endif
367 :
368 : /************************************************************************/
369 : /* IcechunkManifest::GetChunkFilename() */
370 : /************************************************************************/
371 :
372 4 : std::string IcechunkManifest::GetChunkFilename(const std::string &chunkId) const
373 : {
374 : return CPLFormFilenameSafe(
375 8 : CPLFormFilenameSafe(
376 8 : CPLGetDirnameSafe(CPLGetDirnameSafe(m_osFilename.c_str()).c_str())
377 : .c_str(),
378 : "chunks", nullptr)
379 : .c_str(),
380 12 : chunkId.c_str(), nullptr);
381 : }
382 :
383 : /************************************************************************/
384 : /* IcechunkManifest::GetChunkRef() */
385 : /************************************************************************/
386 :
387 : const IcechunkManifest::ChunkRef *
388 8086 : IcechunkManifest::GetChunkRef(const ObjectId8 &nodeId,
389 : const ChunkIdx &idx) const
390 : {
391 : #if __cplusplus >= 202002L
392 : const auto iterArrayManifests = std::ranges::lower_bound(
393 : m_arrayManifests, nodeId, {}, &ArrayManifest::nodeId);
394 : #else
395 16172 : ArrayManifest arrayManifestLookup;
396 8086 : arrayManifestLookup.nodeId = nodeId;
397 : const auto iterArrayManifests = std::lower_bound(
398 : m_arrayManifests.begin(), m_arrayManifests.end(), arrayManifestLookup,
399 8086 : [](const ArrayManifest &a, const ArrayManifest &b)
400 16172 : { return a.nodeId < b.nodeId; });
401 : #endif
402 16172 : if (iterArrayManifests == m_arrayManifests.end() ||
403 8086 : iterArrayManifests->nodeId != nodeId)
404 : {
405 0 : return nullptr;
406 : }
407 8086 : const auto &arrayManifest = *iterArrayManifests;
408 :
409 8104 : if (idx.empty() && arrayManifest.chunkRefs.size() == 1 &&
410 8104 : arrayManifest.chunkRefs[0].idx.size() == 1 &&
411 0 : arrayManifest.chunkRefs[0].idx[0] == 0)
412 : {
413 : // Special case for scalar arrays such as "crs" written by Icechunk v0
414 0 : return &(arrayManifest.chunkRefs[0]);
415 : }
416 :
417 : #if __cplusplus >= 202002L
418 : const auto iterChunkRefs = std::ranges::lower_bound(
419 : arrayManifest.chunkRefs, idx, {}, &ChunkRef::idx);
420 : #else
421 16172 : ChunkRef chunkRefLookup;
422 8086 : chunkRefLookup.idx = idx;
423 : const auto iterChunkRefs = std::lower_bound(
424 : arrayManifest.chunkRefs.begin(), arrayManifest.chunkRefs.end(),
425 : chunkRefLookup,
426 32293 : [](const ChunkRef &a, const ChunkRef &b) { return a.idx < b.idx; });
427 : #endif
428 16172 : if (iterChunkRefs == arrayManifest.chunkRefs.end() ||
429 8086 : iterChunkRefs->idx != idx)
430 : {
431 6 : if (!arrayManifest.chunkRefs.empty() &&
432 3 : idx.size() != arrayManifest.chunkRefs.front().idx.size())
433 : {
434 1 : CPLError(CE_Failure, CPLE_AppDefined,
435 : "GetChunkRef(%s): querying with index of dimension %u "
436 : "whereas chunk refs have dimension %u",
437 2 : CrockfordBase32Encode(nodeId).c_str(),
438 1 : static_cast<unsigned>(idx.size()),
439 : static_cast<unsigned>(
440 1 : arrayManifest.chunkRefs.front().idx.size()));
441 : }
442 :
443 3 : return nullptr;
444 : }
445 :
446 8083 : return &(*iterChunkRefs);
447 : }
448 :
449 : } // namespace gdal::icechunk
|