/src/gdal/frmts/icechunk/icechunksnapshot.cpp
Line | Count | Source |
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 "icechunksnapshot.h" |
14 | | #include "icechunkutils.h" |
15 | | #include "icechunkdrivercore.h" |
16 | | |
17 | | #include "cpl_vsi_virtual.h" |
18 | | |
19 | | #include <algorithm> |
20 | | #include <cinttypes> |
21 | | #include <limits> |
22 | | #if __cplusplus >= 202002L |
23 | | #include <ranges> |
24 | | #endif |
25 | | |
26 | | /* ------------------------------------------------------------------------- */ |
27 | | |
28 | | #if defined(__GNUC__) |
29 | | #pragma GCC diagnostic push |
30 | | #pragma GCC diagnostic ignored "-Weffc++" |
31 | | #pragma GCC diagnostic ignored "-Wnull-dereference" |
32 | | #endif |
33 | | |
34 | | #if defined(__clang__) |
35 | | #pragma clang diagnostic push |
36 | | #pragma clang diagnostic ignored "-Wweak-vtables" |
37 | | #endif |
38 | | |
39 | | #include "flatbuffers/flexbuffers.h" |
40 | | #include "generated/snapshot_generated.h" |
41 | | |
42 | | #if defined(__clang__) |
43 | | #pragma clang diagnostic pop |
44 | | #endif |
45 | | |
46 | | #if defined(__GNUC__) |
47 | | #pragma GCC diagnostic pop |
48 | | #endif |
49 | | |
50 | | /* ------------------------------------------------------------------------- */ |
51 | | |
52 | | using namespace flatbuffers; |
53 | | using namespace generated; |
54 | | |
55 | | namespace gdal::icechunk |
56 | | { |
57 | 0 | IcechunkSnapshot::IcechunkSnapshot() = default; |
58 | | |
59 | 0 | IcechunkSnapshot::~IcechunkSnapshot() = default; |
60 | | |
61 | | #if defined(__GNUC__) |
62 | | #pragma GCC diagnostic push |
63 | | #pragma GCC diagnostic ignored "-Wnull-dereference" |
64 | | #endif |
65 | | |
66 | | /************************************************************************/ |
67 | | /* IcechunkSnapshot::Open() */ |
68 | | /************************************************************************/ |
69 | | |
70 | | std::unique_ptr<IcechunkSnapshot> |
71 | | IcechunkSnapshot::Open(const char *pszFilename) |
72 | 0 | { |
73 | 0 | CPLDebugOnly("Icechunk", "Opening snapshot %s", pszFilename); |
74 | 0 | auto fp = VSIFilesystemHandler::OpenStatic(pszFilename, "rb"); |
75 | 0 | if (!fp) |
76 | 0 | { |
77 | 0 | CPLError(CE_Failure, CPLE_FileIO, "Cannot open %s", pszFilename); |
78 | 0 | return nullptr; |
79 | 0 | } |
80 | | |
81 | 0 | int nVersion = 0; |
82 | 0 | auto [buffer, size] = |
83 | 0 | DecompressFile(pszFilename, fp.get(), FILE_TYPE_SNAPSHOT, &nVersion); |
84 | 0 | if (!buffer) |
85 | 0 | return nullptr; |
86 | | |
87 | 0 | { |
88 | 0 | Verifier verifier(buffer.get(), size); |
89 | 0 | if (!VerifySnapshotBuffer(verifier)) |
90 | 0 | { |
91 | 0 | CPLError(CE_Failure, CPLE_AppDefined, |
92 | 0 | "%s: invalid Snapshot Flatbuffer", pszFilename); |
93 | 0 | return nullptr; |
94 | 0 | } |
95 | 0 | } |
96 | | |
97 | 0 | auto snapshot = std::make_unique<IcechunkSnapshot>(); |
98 | 0 | snapshot->m_osFilename = pszFilename; |
99 | |
|
100 | 0 | const auto *fbsSnapshot = GetSnapshot(buffer.get()); |
101 | 0 | const auto *id = fbsSnapshot->id(); |
102 | 0 | CPLAssertNotNull(id); // guaranteed by VerifySnapshotBuffer() |
103 | 0 | const auto idBytes = id->bytes(); |
104 | 0 | CPLAssertNotNull(idBytes); // guaranteed by VerifySnapshotBuffer() |
105 | 0 | const std::string snapshotIdBase32 = CrockfordBase32Encode(*idBytes); |
106 | 0 | if (snapshotIdBase32 != CPLGetFilename(pszFilename)) |
107 | 0 | { |
108 | 0 | CPLError(CE_Failure, CPLE_AppDefined, "%s: id=%s != expected %s", |
109 | 0 | pszFilename, snapshotIdBase32.c_str(), |
110 | 0 | CPLGetFilename(pszFilename)); |
111 | 0 | return nullptr; |
112 | 0 | } |
113 | | |
114 | 0 | const auto *message = fbsSnapshot->message(); |
115 | 0 | CPLAssertNotNull(message); // guaranteed by VerifySnapshotBuffer() |
116 | 0 | snapshot->m_osCommitMessage = GetString(message); |
117 | 0 | CPLDebugOnly("Icechunk", "snapshot %s: commit message: '%s'", pszFilename, |
118 | 0 | snapshot->m_osCommitMessage.c_str()); |
119 | |
|
120 | 0 | snapshot->m_flushTimestamp = fbsSnapshot->flushed_at(); |
121 | |
|
122 | 0 | { |
123 | 0 | const auto *metadata = fbsSnapshot->metadata(); |
124 | 0 | if (metadata && nVersion == 2) |
125 | 0 | { |
126 | 0 | for (const auto &md : *metadata) |
127 | 0 | { |
128 | 0 | if (const auto value = md->value()) |
129 | 0 | { |
130 | 0 | if (!flexbuffers::VerifyBuffer(value->data(), |
131 | 0 | value->size())) |
132 | 0 | { |
133 | 0 | CPLError(CE_Failure, CPLE_AppDefined, |
134 | 0 | "%s: flexbuffers::VerifyBuffer() failed", |
135 | 0 | pszFilename); |
136 | 0 | return nullptr; |
137 | 0 | } |
138 | | if constexpr (IS_DEBUG_BUILD) |
139 | | { |
140 | | std::string val; |
141 | | flexbuffers::GetRoot(value->data(), value->size()) |
142 | | .ToString(true, true, val); |
143 | | CPLDebugOnly("Icechunk", "snapshot %s: metadata %s=%s", |
144 | | snapshotIdBase32.c_str(), |
145 | | md->name() ? md->name()->c_str() |
146 | | : "(null)", |
147 | | val.c_str()); |
148 | | } |
149 | 0 | } |
150 | 0 | } |
151 | 0 | } |
152 | 0 | } |
153 | | |
154 | | /* --------------------------------------------------------------------*/ |
155 | | /* Parse nodes[] array */ |
156 | | /* --------------------------------------------------------------------*/ |
157 | 0 | const auto *nodes = fbsSnapshot->nodes(); |
158 | 0 | CPLAssertAlways(nodes); // guaranteed by VerifySnapshotBuffer() |
159 | 0 | snapshot->m_nodes.reserve(nodes->size()); |
160 | 0 | bool needSort = false; |
161 | 0 | for (const auto *nodePtr : *nodes) |
162 | 0 | { |
163 | 0 | const auto *fbsNodeId = nodePtr->id(); |
164 | 0 | CPLAssertNotNull(fbsNodeId); // guaranteed by VerifySnapshotBuffer() |
165 | 0 | const auto fbsNodeIdBytes = fbsNodeId->bytes(); |
166 | 0 | CPLAssertAlways( |
167 | 0 | fbsNodeIdBytes); // guaranteed by VerifySnapshotBuffer() |
168 | | |
169 | 0 | ObjectId8 nodeId; |
170 | 0 | static_assert(sizeof(*fbsNodeIdBytes) == sizeof(nodeId)); |
171 | 0 | memcpy(nodeId.data(), fbsNodeIdBytes->data(), sizeof(nodeId)); |
172 | |
|
173 | 0 | const auto *pathPtr = nodePtr->path(); |
174 | 0 | CPLAssertNotNull(pathPtr); // guaranteed by VerifySnapshotBuffer() |
175 | |
|
176 | 0 | const std::string path = GetString(pathPtr); |
177 | 0 | if (path.empty() || path[0] != '/' || |
178 | 0 | (path.size() > 1 && path.back() == '/')) |
179 | 0 | { |
180 | 0 | CPLError(CE_Failure, CPLE_AppDefined, "%s: invalid node path '%s'", |
181 | 0 | pszFilename, path.c_str()); |
182 | 0 | return nullptr; |
183 | 0 | } |
184 | | |
185 | | // Additional checks to avoid later issues. Might not be strictly |
186 | | // necessary, but if removing them, VSIIcechunkFileSystem should be |
187 | | // reviewed against risks of path traversal. |
188 | 0 | if (path.find('\\') != std::string::npos || |
189 | 0 | path.find("/./") != std::string::npos || |
190 | 0 | path.find("/../") != std::string::npos || |
191 | 0 | cpl::ends_with(path, "/..")) |
192 | 0 | { |
193 | 0 | CPLError(CE_Failure, CPLE_AppDefined, |
194 | 0 | "%s: path traversal pattern in node path '%s'", |
195 | 0 | pszFilename, path.c_str()); |
196 | 0 | return nullptr; |
197 | 0 | } |
198 | | |
199 | 0 | needSort = needSort || (!snapshot->m_nodes.empty() && |
200 | 0 | snapshot->m_nodes.back().path > path); |
201 | |
|
202 | 0 | Node node; |
203 | 0 | node.id = nodeId; |
204 | 0 | node.path = path; |
205 | |
|
206 | 0 | const auto *userData = nodePtr->user_data(); |
207 | 0 | CPLAssertAlways(userData); // guaranteed by VerifySnapshotBuffer() |
208 | 0 | node.content.assign(reinterpret_cast<const char *>(userData->data()), |
209 | 0 | userData->size()); |
210 | |
|
211 | 0 | CPLDebugOnly("Icechunk", "snapshot %s, node %s, path %s, user_data %s", |
212 | 0 | snapshotIdBase32.c_str(), |
213 | 0 | CrockfordBase32Encode(*(fbsNodeId->bytes())).c_str(), |
214 | 0 | path.c_str(), node.content.c_str()); |
215 | |
|
216 | 0 | if (const auto arrayData = nodePtr->node_data_as_Array()) |
217 | 0 | { |
218 | 0 | node.isArray = true; |
219 | |
|
220 | 0 | uint64_t totalNumChunks = 1; |
221 | |
|
222 | 0 | if (nVersion == 2) |
223 | 0 | { |
224 | 0 | const auto *shape = arrayData->shape_v2(); |
225 | 0 | if (!shape) |
226 | 0 | { |
227 | 0 | CPLError(CE_Failure, CPLE_AppDefined, |
228 | 0 | "%s: missing shape_v2 in ArrayData", pszFilename); |
229 | 0 | return nullptr; |
230 | 0 | } |
231 | 0 | for (const auto *dimShape : *shape) |
232 | 0 | { |
233 | 0 | const uint32_t numChunks = dimShape->num_chunks(); |
234 | 0 | if (numChunks == 0) |
235 | 0 | { |
236 | 0 | CPLError(CE_Failure, CPLE_AppDefined, |
237 | 0 | "%s: numChunks == 0", pszFilename); |
238 | 0 | return nullptr; |
239 | 0 | } |
240 | 0 | node.numChunks.push_back(numChunks); |
241 | 0 | if (numChunks > |
242 | 0 | std::numeric_limits<uint64_t>::max() / totalNumChunks) |
243 | 0 | { |
244 | 0 | CPLError(CE_Failure, CPLE_AppDefined, |
245 | 0 | "%s: too many chunks", pszFilename); |
246 | 0 | return nullptr; |
247 | 0 | } |
248 | 0 | totalNumChunks *= numChunks; |
249 | 0 | } |
250 | 0 | } |
251 | 0 | else |
252 | 0 | { |
253 | 0 | const auto *shape = arrayData->shape(); |
254 | 0 | CPLAssertAlways(shape); // guaranteed by VerifySnapshotBuffer() |
255 | 0 | for (const auto *dimShape : *shape) |
256 | 0 | { |
257 | 0 | const uint64_t array_length = dimShape->array_length(); |
258 | 0 | const uint64_t chunk_length = dimShape->chunk_length(); |
259 | 0 | if (!array_length || !chunk_length) |
260 | 0 | { |
261 | 0 | CPLError(CE_Failure, CPLE_AppDefined, |
262 | 0 | "%s: invalid shape in ArrayData", pszFilename); |
263 | 0 | return nullptr; |
264 | 0 | } |
265 | 0 | const uint64_t numChunks64 = |
266 | 0 | cpl::div_round_up(array_length, chunk_length); |
267 | 0 | if (numChunks64 > std::numeric_limits<uint32_t>::max()) |
268 | 0 | { |
269 | 0 | CPLError( |
270 | 0 | CE_Failure, CPLE_AppDefined, |
271 | 0 | "%s: invalid shape in ArrayData: too many chunks", |
272 | 0 | pszFilename); |
273 | 0 | return nullptr; |
274 | 0 | } |
275 | 0 | const uint32_t numChunks = |
276 | 0 | static_cast<uint32_t>(numChunks64); |
277 | 0 | node.numChunks.push_back(numChunks); |
278 | 0 | if (numChunks > |
279 | 0 | std::numeric_limits<uint64_t>::max() / totalNumChunks) |
280 | 0 | { |
281 | 0 | CPLError(CE_Failure, CPLE_AppDefined, |
282 | 0 | "%s: too many chunks", pszFilename); |
283 | 0 | return nullptr; |
284 | 0 | } |
285 | 0 | totalNumChunks *= numChunks; |
286 | 0 | } |
287 | 0 | } |
288 | | |
289 | 0 | const auto *manifests = arrayData->manifests(); |
290 | 0 | CPLAssertAlways(manifests); // guaranteed by VerifySnapshotBuffer() |
291 | | |
292 | 0 | uint64_t totalNumChunksFromManifests = 0; |
293 | 0 | for (const auto *manifestPtr : *manifests) |
294 | 0 | { |
295 | 0 | const auto manifestId = manifestPtr->object_id(); |
296 | 0 | CPLAssertNotNull( |
297 | 0 | manifestId); // guaranteed by VerifySnapshotBuffer() |
298 | 0 | const auto manifestIdBytes = manifestId->bytes(); |
299 | 0 | CPLAssertAlways( |
300 | 0 | manifestIdBytes); // guaranteed by VerifySnapshotBuffer() |
301 | | |
302 | 0 | ManifestRef manifestRef; |
303 | 0 | static_assert(sizeof(*manifestIdBytes) == |
304 | 0 | sizeof(manifestRef.manifestId)); |
305 | 0 | memcpy(manifestRef.manifestId.data(), manifestIdBytes->data(), |
306 | 0 | sizeof(manifestRef.manifestId)); |
307 | |
|
308 | 0 | const auto extents = manifestPtr->extents(); |
309 | 0 | CPLAssertAlways( |
310 | 0 | extents); // guaranteed by VerifySnapshotBuffer() |
311 | | |
312 | 0 | CPLDebugOnly("Icechunk", "snapshot %s, manifest ref %s:", |
313 | 0 | snapshotIdBase32.c_str(), |
314 | 0 | CrockfordBase32Encode(*manifestIdBytes).c_str()); |
315 | |
|
316 | 0 | uint64_t chunkCountFromManifest = 1; |
317 | 0 | if (node.numChunks.empty() && extents->size() == 1 && |
318 | 0 | node.numChunks.empty()) |
319 | 0 | { |
320 | | // Special case for scalar arrays such as "crs" written by Icechunk v0 |
321 | 0 | const auto *extent = (*extents)[0]; |
322 | 0 | if (extent->from() != 0 || extent->to() != 1) |
323 | 0 | { |
324 | 0 | CPLError(CE_Failure, CPLE_AppDefined, |
325 | 0 | "%s: array %s: invalid manifest extent " |
326 | 0 | "[%u, %u[ for dim %u", |
327 | 0 | pszFilename, path.c_str(), extent->from(), |
328 | 0 | extent->to(), 0); |
329 | 0 | return nullptr; |
330 | 0 | } |
331 | | |
332 | 0 | manifestRef.extents.emplace_back(extent->from(), |
333 | 0 | extent->to()); |
334 | 0 | CPLDebugOnly("Icechunk", |
335 | 0 | "snapshot %s, from %" PRIu32 " to %" PRIu32, |
336 | 0 | snapshotIdBase32.c_str(), |
337 | 0 | manifestRef.extents.back().from, |
338 | 0 | manifestRef.extents.back().to); |
339 | 0 | } |
340 | 0 | else |
341 | 0 | { |
342 | 0 | if (node.numChunks.size() != extents->size()) |
343 | 0 | { |
344 | 0 | CPLError( |
345 | 0 | CE_Failure, CPLE_AppDefined, |
346 | 0 | "%s: array %s: manifest extents has not expected " |
347 | 0 | "dimension count. Got %d from manifest extents, " |
348 | 0 | "expected %d from node shape", |
349 | 0 | pszFilename, path.c_str(), |
350 | 0 | static_cast<int>(extents->size()), |
351 | 0 | static_cast<int>(node.numChunks.size())); |
352 | 0 | return nullptr; |
353 | 0 | } |
354 | 0 | for (unsigned iDim = 0; iDim < extents->size(); ++iDim) |
355 | 0 | { |
356 | 0 | const auto *extent = (*extents)[iDim]; |
357 | 0 | if (extent->from() >= extent->to() || |
358 | 0 | extent->from() >= node.numChunks[iDim] || |
359 | 0 | extent->to() > node.numChunks[iDim]) |
360 | 0 | { |
361 | 0 | CPLError(CE_Failure, CPLE_AppDefined, |
362 | 0 | "%s: array %s: invalid manifest extent " |
363 | 0 | "[%u, %u[ for dim %u", |
364 | 0 | pszFilename, path.c_str(), extent->from(), |
365 | 0 | extent->to(), iDim); |
366 | 0 | return nullptr; |
367 | 0 | } |
368 | | |
369 | | // Overflow cannot happen given the validation of extent |
370 | | // w.r.t node.numChunks and the fact that |
371 | | // times(node.numChunks) has been checked to fit on uint64_t |
372 | 0 | chunkCountFromManifest *= extent->to() - extent->from(); |
373 | |
|
374 | 0 | manifestRef.extents.emplace_back(extent->from(), |
375 | 0 | extent->to()); |
376 | 0 | CPLDebugOnly("Icechunk", |
377 | 0 | "snapshot %s, from %" PRIu32 |
378 | 0 | " to %" PRIu32, |
379 | 0 | snapshotIdBase32.c_str(), |
380 | 0 | manifestRef.extents.back().from, |
381 | 0 | manifestRef.extents.back().to); |
382 | 0 | } |
383 | 0 | } |
384 | | |
385 | 0 | node.manifestRefs.push_back(std::move(manifestRef)); |
386 | |
|
387 | 0 | if (totalNumChunksFromManifests > |
388 | 0 | std::numeric_limits<uint64_t>::max() - |
389 | 0 | chunkCountFromManifest) |
390 | 0 | { |
391 | 0 | totalNumChunksFromManifests = |
392 | 0 | std::numeric_limits<uint64_t>::max(); |
393 | 0 | break; |
394 | 0 | } |
395 | 0 | totalNumChunksFromManifests += chunkCountFromManifest; |
396 | 0 | } |
397 | | |
398 | | // Quick partial consistency check |
399 | 0 | if (totalNumChunksFromManifests > totalNumChunks) |
400 | 0 | { |
401 | 0 | CPLError(CE_Failure, CPLE_AppDefined, |
402 | 0 | "%s: array %s: chunks referenced by manifest extents " |
403 | 0 | "= %" PRIu64 " > chunks in the array = %" PRIu64, |
404 | 0 | pszFilename, path.c_str(), totalNumChunksFromManifests, |
405 | 0 | totalNumChunks); |
406 | 0 | return nullptr; |
407 | 0 | } |
408 | | |
409 | | if constexpr (IS_DEBUG_BUILD) |
410 | | { |
411 | | // Check that all chunks of this array are referenced at most |
412 | | // once |
413 | | ChunkIdx anChunkIdx(node.numChunks.size()); |
414 | | std::string chunkStr; |
415 | | for (uint64_t iChunk = 0; iChunk < totalNumChunks; ++iChunk) |
416 | | { |
417 | | chunkStr = '['; |
418 | | for (size_t iDim = 0; iDim < node.numChunks.size(); ++iDim) |
419 | | { |
420 | | if (iDim > 0) |
421 | | chunkStr += ", "; |
422 | | chunkStr += std::to_string(anChunkIdx[iDim]); |
423 | | } |
424 | | chunkStr += ']'; |
425 | | |
426 | | size_t counter = node.countManifestIdForChunk(anChunkIdx); |
427 | | if (counter > 1) |
428 | | { |
429 | | CPLError(CE_Failure, CPLE_AppDefined, |
430 | | "%s: array %s: found more than one manifest " |
431 | | "ref for chunk %s", |
432 | | pszFilename, path.c_str(), chunkStr.c_str()); |
433 | | return nullptr; |
434 | | } |
435 | | |
436 | | if (iChunk + 1 < totalNumChunks) |
437 | | { |
438 | | // Increment anChunkIdx |
439 | | for (size_t iDim = node.numChunks.size(); iDim > 0; |
440 | | /* */) |
441 | | { |
442 | | --iDim; |
443 | | ++anChunkIdx[iDim]; |
444 | | if (anChunkIdx[iDim] < node.numChunks[iDim]) |
445 | | { |
446 | | break; |
447 | | } |
448 | | anChunkIdx[iDim] = 0; |
449 | | } |
450 | | } |
451 | | } |
452 | | } |
453 | 0 | } |
454 | | |
455 | 0 | snapshot->m_nodes.push_back(std::move(node)); |
456 | 0 | } |
457 | | |
458 | 0 | if (needSort) |
459 | 0 | { |
460 | | // Nominally not needed but there has been a confusion in the spec |
461 | | // regarding sort order, so normalize things |
462 | | // Cf https://github.com/earth-mover/icechunk/issues/2183 |
463 | 0 | std::sort(snapshot->m_nodes.begin(), snapshot->m_nodes.end(), |
464 | 0 | [](const Node &a, const Node &b) { return a.path < b.path; }); |
465 | 0 | } |
466 | | |
467 | | /* --------------------------------------------------------------------*/ |
468 | | /* Parse manifest_files_v2 / manifest_files[] array */ |
469 | | /* --------------------------------------------------------------------*/ |
470 | 0 | if (nVersion == 2) |
471 | 0 | { |
472 | 0 | const auto manifests = fbsSnapshot->manifest_files_v2(); |
473 | 0 | if (manifests) |
474 | 0 | { |
475 | 0 | snapshot->m_manifestInfos.reserve(manifests->size()); |
476 | 0 | for (const auto *manifestPtr : *manifests) |
477 | 0 | { |
478 | 0 | const auto manifestId = manifestPtr->id(); |
479 | 0 | if (!manifestId || !manifestId->bytes()) |
480 | 0 | { |
481 | 0 | CPLError(CE_Failure, CPLE_AppDefined, |
482 | 0 | "%s: missing manifest id", pszFilename); |
483 | 0 | return nullptr; |
484 | 0 | } |
485 | | |
486 | 0 | ManifestInfo info; |
487 | 0 | info.sizeBytes = manifestPtr->size_bytes(); |
488 | 0 | info.numChunkRefs = manifestPtr->num_chunk_refs(); |
489 | 0 | static_assert(sizeof(*(manifestId->bytes())) == |
490 | 0 | sizeof(info.id)); |
491 | 0 | memcpy(info.id.data(), manifestId->bytes()->data(), |
492 | 0 | sizeof(info.id)); |
493 | 0 | info.strId = CrockfordBase32Encode(info.id); |
494 | |
|
495 | 0 | CPLDebugOnly("Icechunk", |
496 | 0 | "snapshot %s, manifest %s, size_bytes %" PRIu64 |
497 | 0 | ", num_chunk_refs %u", |
498 | 0 | snapshotIdBase32.c_str(), info.strId.c_str(), |
499 | 0 | info.sizeBytes, info.numChunkRefs); |
500 | |
|
501 | 0 | if (!snapshot->m_manifestInfos.empty() && |
502 | 0 | info.id <= snapshot->m_manifestInfos.back().id) |
503 | 0 | { |
504 | 0 | CPLError( |
505 | 0 | CE_Failure, CPLE_AppDefined, |
506 | 0 | "%s: ManifestInfo array not sorted by increasing id", |
507 | 0 | pszFilename); |
508 | 0 | return nullptr; |
509 | 0 | } |
510 | | |
511 | 0 | snapshot->m_manifestInfos.push_back(std::move(info)); |
512 | 0 | } |
513 | 0 | } |
514 | 0 | } |
515 | 0 | else |
516 | 0 | { |
517 | 0 | const auto manifests = fbsSnapshot->manifest_files(); |
518 | 0 | CPLAssertAlways(manifests); // guaranteed by VerifySnapshotBuffer() |
519 | | |
520 | 0 | snapshot->m_manifestInfos.reserve(manifests->size()); |
521 | 0 | for (const auto *manifestPtr : *manifests) |
522 | 0 | { |
523 | 0 | const auto manifestId = manifestPtr->id(); |
524 | |
|
525 | 0 | ManifestInfo info; |
526 | 0 | info.sizeBytes = manifestPtr->size_bytes(); |
527 | 0 | info.numChunkRefs = manifestPtr->num_chunk_refs(); |
528 | 0 | static_assert(sizeof(*(manifestId.bytes())) == sizeof(info.id)); |
529 | 0 | memcpy(info.id.data(), manifestId.bytes()->data(), sizeof(info.id)); |
530 | 0 | info.strId = CrockfordBase32Encode(info.id); |
531 | |
|
532 | 0 | CPLDebugOnly("Icechunk", |
533 | 0 | "snapshot %s, manifest %s, size_bytes %" PRIu64 |
534 | 0 | ", num_chunk_refs %u", |
535 | 0 | snapshotIdBase32.c_str(), info.strId.c_str(), |
536 | 0 | info.sizeBytes, info.numChunkRefs); |
537 | |
|
538 | 0 | if (!snapshot->m_manifestInfos.empty() && |
539 | 0 | info.id <= snapshot->m_manifestInfos.back().id) |
540 | 0 | { |
541 | 0 | CPLError(CE_Failure, CPLE_AppDefined, |
542 | 0 | "%s: ManifestInfo array not sorted by increasing id", |
543 | 0 | pszFilename); |
544 | 0 | return nullptr; |
545 | 0 | } |
546 | | |
547 | 0 | snapshot->m_manifestInfos.push_back(std::move(info)); |
548 | 0 | } |
549 | 0 | } |
550 | | |
551 | 0 | return snapshot; |
552 | 0 | } |
553 | | |
554 | | #if defined(__GNUC__) |
555 | | #pragma GCC diagnostic pop |
556 | | #endif |
557 | | |
558 | | /************************************************************************/ |
559 | | /* IcechunkSnapshot::GetManifestInfoFromId() */ |
560 | | /************************************************************************/ |
561 | | |
562 | | const IcechunkSnapshot::ManifestInfo * |
563 | | IcechunkSnapshot::GetManifestInfoFromId(const ObjectId12 &id) const |
564 | 0 | { |
565 | | #if __cplusplus >= 202002L |
566 | | const auto iter = |
567 | | std::ranges::lower_bound(m_manifestInfos, id, {}, &ManifestInfo::id); |
568 | | #else |
569 | 0 | ManifestInfo lookup; |
570 | 0 | lookup.id = id; |
571 | 0 | const auto iter = |
572 | 0 | std::lower_bound(m_manifestInfos.begin(), m_manifestInfos.end(), lookup, |
573 | 0 | [](const ManifestInfo &a, const ManifestInfo &b) |
574 | 0 | { return a.id < b.id; }); |
575 | 0 | #endif |
576 | 0 | if (iter != m_manifestInfos.end() && iter->id == id) |
577 | 0 | return &(*iter); |
578 | 0 | return nullptr; |
579 | 0 | } |
580 | | |
581 | | /************************************************************************/ |
582 | | /* IcechunkSnapshot::GetNodeFromPath() */ |
583 | | /************************************************************************/ |
584 | | |
585 | | const IcechunkSnapshot::Node * |
586 | | IcechunkSnapshot::GetNodeFromPath(const std::string &path) const |
587 | 0 | { |
588 | | #if __cplusplus >= 202002L |
589 | | const auto iter = std::ranges::lower_bound(m_nodes, path, {}, &Node::path); |
590 | | #else |
591 | 0 | Node lookup; |
592 | 0 | lookup.path = path; |
593 | 0 | const auto iter = std::lower_bound(m_nodes.begin(), m_nodes.end(), lookup, |
594 | 0 | [](const Node &a, const Node &b) |
595 | 0 | { return a.path < b.path; }); |
596 | 0 | #endif |
597 | 0 | if (iter != m_nodes.end() && iter->path == path) |
598 | 0 | return &(*iter); |
599 | 0 | return nullptr; |
600 | 0 | } |
601 | | |
602 | | /************************************************************************/ |
603 | | /* DoRefExtentsMatchChunkIdx() */ |
604 | | /************************************************************************/ |
605 | | |
606 | | inline bool DoRefExtentsMatchChunkIdx( |
607 | | const std::vector<IcechunkSnapshot::ChunkIndexRange> &extents, |
608 | | const ChunkIdx &anChunkIdx) |
609 | 0 | { |
610 | 0 | CPLAssert(anChunkIdx.size() == extents.size()); |
611 | 0 | for (size_t iDim = 0; iDim < extents.size(); ++iDim) |
612 | 0 | { |
613 | 0 | if (anChunkIdx[iDim] < extents[iDim].from || |
614 | 0 | anChunkIdx[iDim] >= extents[iDim].to) |
615 | 0 | { |
616 | 0 | return false; |
617 | 0 | } |
618 | 0 | } |
619 | 0 | return true; |
620 | 0 | } |
621 | | |
622 | | /************************************************************************/ |
623 | | /* IcechunkSnapshot::findManifestIdForChunk() */ |
624 | | /************************************************************************/ |
625 | | |
626 | | const ObjectId12 * |
627 | | IcechunkSnapshot::Node::findManifestIdForChunk(const ChunkIdx &anChunkIdx) const |
628 | 0 | { |
629 | 0 | CPLAssert(anChunkIdx.size() == numChunks.size()); |
630 | | |
631 | | // Special case for scalar arrays such as "crs" written by Icechunk v0 |
632 | 0 | if (anChunkIdx.empty() && manifestRefs.size() == 1 && |
633 | 0 | manifestRefs[0].extents.size() == 1 && |
634 | 0 | manifestRefs[0].extents[0].from == 0 && |
635 | 0 | manifestRefs[0].extents[0].to == 1) |
636 | 0 | { |
637 | 0 | return &(manifestRefs[0].manifestId); |
638 | 0 | } |
639 | | |
640 | | // Heuristics to find more quickly the chunk, assuming the passed chunk |
641 | | // index is contained in the last ChunkRef. |
642 | 0 | thread_local const Node *lastNode = nullptr; |
643 | 0 | thread_local size_t lastRefsIdx = 0; |
644 | 0 | if (lastNode == this && lastRefsIdx < manifestRefs.size()) |
645 | 0 | { |
646 | 0 | const auto &ref = manifestRefs[lastRefsIdx]; |
647 | 0 | const bool match = DoRefExtentsMatchChunkIdx(ref.extents, anChunkIdx); |
648 | 0 | CPLDebugOnly("Icechunk", "findManifestIdForChunk() guess: %s", |
649 | 0 | match ? "success" : "missed"); |
650 | 0 | if (match) |
651 | 0 | { |
652 | 0 | return &(ref.manifestId); |
653 | 0 | } |
654 | 0 | } |
655 | | |
656 | | // Note: if there are many manifestRefs in a node, this linear search could |
657 | | // become a bottleneck. Consider RTree / KDTree maybe. |
658 | 0 | for (const auto &ref : manifestRefs) |
659 | 0 | { |
660 | 0 | if (DoRefExtentsMatchChunkIdx(ref.extents, anChunkIdx)) |
661 | 0 | { |
662 | 0 | lastNode = this; |
663 | 0 | lastRefsIdx = &ref - manifestRefs.data(); |
664 | 0 | return &(ref.manifestId); |
665 | 0 | } |
666 | 0 | } |
667 | | |
668 | 0 | return nullptr; |
669 | 0 | } |
670 | | |
671 | | } // namespace gdal::icechunk |