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 "icechunkrepo.h"
14 : #include "icechunkmanifest.h"
15 : #include "icechunksnapshot.h"
16 : #include "icechunkutils.h"
17 : #include "icechunkdrivercore.h"
18 :
19 : #include "cpl_json.h"
20 : #include "cpl_mem_cache.h"
21 :
22 : #include <cinttypes>
23 : #include <limits>
24 : #include <mutex>
25 :
26 : /* ------------------------------------------------------------------------- */
27 :
28 : #if defined(__GNUC__)
29 : #pragma GCC diagnostic push
30 : #pragma GCC diagnostic ignored "-Weffc++"
31 : #endif
32 :
33 : #if defined(__clang__)
34 : #pragma clang diagnostic push
35 : #pragma clang diagnostic ignored "-Wweak-vtables"
36 : #endif
37 :
38 : #include "generated/repo_generated.h"
39 :
40 : #if defined(__clang__)
41 : #pragma clang diagnostic pop
42 : #endif
43 :
44 : #if defined(__GNUC__)
45 : #pragma GCC diagnostic pop
46 : #endif
47 :
48 : /* ------------------------------------------------------------------------- */
49 :
50 : using namespace flatbuffers;
51 : using namespace generated;
52 :
53 : namespace gdal::icechunk
54 : {
55 : IcechunkFile::IcechunkFile() = default;
56 :
57 : IcechunkFile::~IcechunkFile() = default;
58 :
59 : IcechunkRepo::IcechunkRepo() = default;
60 :
61 : IcechunkRepo::~IcechunkRepo() = default;
62 :
63 : /************************************************************************/
64 : /* IcechunkRepo::OpenV1() */
65 : /************************************************************************/
66 :
67 : /* static */
68 12 : std::unique_ptr<IcechunkRepo> IcechunkRepo::OpenV1(const char *pszRootPath)
69 : {
70 12 : auto repo = std::make_unique<IcechunkRepo>();
71 12 : repo->m_osRootPath = pszRootPath;
72 :
73 : const std::string osRefsDir =
74 24 : CPLFormFilenameSafe(pszRootPath, "refs", nullptr);
75 24 : const CPLStringList aosRefs(VSIReadDir(osRefsDir.c_str()));
76 58 : for (const char *pszRef : aosRefs)
77 : {
78 46 : if (strcmp(pszRef, ".") != 0 && strcmp(pszRef, "..") != 0)
79 : {
80 : const std::string osRefDir =
81 44 : CPLFormFilenameSafe(osRefsDir.c_str(), pszRef, nullptr);
82 22 : if (STARTS_WITH(pszRef, "branch."))
83 : {
84 24 : CPLJSONDocument oDoc;
85 12 : if (oDoc.Load(CPLFormFilenameSafe(osRefDir.c_str(), "ref.json",
86 : nullptr)))
87 : {
88 : const auto osSnapshotId =
89 24 : oDoc.GetRoot().GetString("snapshot");
90 12 : repo->m_oMapBranchNameToSnapshotId[pszRef +
91 24 : strlen("branch.")] =
92 12 : osSnapshotId;
93 : }
94 : }
95 10 : else if (STARTS_WITH(pszRef, "tag."))
96 : {
97 20 : CPLJSONDocument oDoc;
98 10 : if (oDoc.Load(CPLFormFilenameSafe(osRefDir.c_str(), "ref.json",
99 : nullptr)))
100 : {
101 : const auto osSnapshotId =
102 20 : oDoc.GetRoot().GetString("snapshot");
103 20 : repo->m_oMapTagNameToSnapshotId[pszRef + strlen("tag.")] =
104 10 : osSnapshotId;
105 : }
106 : }
107 : }
108 : }
109 :
110 24 : return repo;
111 : }
112 :
113 : /************************************************************************/
114 : /* ProcessConfig() */
115 : /************************************************************************/
116 :
117 1057 : static void ProcessConfig(const CPLJSONObject &oConfig)
118 : {
119 3171 : const auto oVCC = oConfig["virtual_chunk_containers"];
120 1057 : if (oVCC.GetType() == CPLJSONObject::Type::Object)
121 : {
122 2102 : for (const auto &oVCCChild : oVCC.GetChildren())
123 : {
124 3153 : const auto osURLPrefix = oVCCChild.GetString("url_prefix");
125 3153 : const auto oStore = oVCCChild["store"];
126 2102 : if (cpl::starts_with(osURLPrefix, "s3://") &&
127 1051 : oStore.GetType() == CPLJSONObject::Type::Object)
128 : {
129 3153 : const auto oS3 = oStore["s3"];
130 1051 : if (oS3.GetType() == CPLJSONObject::Type::Object)
131 : {
132 2102 : std::string osPath("/vsis3/");
133 1051 : osPath += osURLPrefix.substr(strlen("s3://"));
134 :
135 3153 : const std::string osRegion = oS3.GetString("region");
136 1051 : if (!osRegion.empty())
137 : {
138 1051 : CPLDebug("Icechunk", "Set AWS_DEFAULT_REGION=%s for %s",
139 : osRegion.c_str(), osPath.c_str());
140 1051 : VSISetPathSpecificOption(osPath.c_str(),
141 : "AWS_DEFAULT_REGION",
142 : osRegion.c_str());
143 : }
144 :
145 1051 : const bool bAnonymous = oS3.GetBool("anonymous");
146 1051 : if (bAnonymous)
147 : {
148 1051 : CPLDebug("Icechunk",
149 : "Set AWS_NO_SIGN_REQUEST=YES for %s",
150 : osPath.c_str());
151 1051 : VSISetPathSpecificOption(osPath.c_str(),
152 : "AWS_NO_SIGN_REQUEST", "YES");
153 : }
154 :
155 1051 : const bool bRequesterPays = oS3.GetBool("requester_pays");
156 1051 : if (bRequesterPays &&
157 0 : !CPLTestBool(VSIGetPathSpecificOption(
158 : osPath.c_str(), "AWS_REQUEST_PAYER", "NO")))
159 : {
160 0 : CPLError(CE_Warning, CPLE_AppDefined,
161 : "AWS_REQUEST_PAYER=YES must be set to "
162 : "access %s",
163 : osPath.c_str());
164 : }
165 : }
166 : }
167 0 : else if ((cpl::starts_with(osURLPrefix, "gs://") ||
168 0 : cpl::starts_with(osURLPrefix, "gcs://")) &&
169 0 : oStore.GetType() == CPLJSONObject::Type::Object)
170 : {
171 0 : const auto oGCS = oStore["gcs"];
172 0 : if (oGCS.GetType() == CPLJSONObject::Type::Object)
173 : {
174 0 : std::string osPath("/vsigs/");
175 0 : osPath += osURLPrefix.substr(osURLPrefix.find("://") + 3);
176 :
177 0 : const auto bAnonymous = oGCS.GetBool("anonymous");
178 0 : if (bAnonymous)
179 : {
180 0 : CPLDebug("Icechunk",
181 : "Set GS_NO_SIGN_REQUEST=YES for %s",
182 : osPath.c_str());
183 0 : VSISetPathSpecificOption(osPath.c_str(),
184 : "GS_NO_SIGN_REQUEST", "YES");
185 : }
186 : }
187 : }
188 : }
189 : }
190 1057 : }
191 :
192 : /************************************************************************/
193 : /* IcechunkRepo::Open() */
194 : /************************************************************************/
195 :
196 : /** Open a "repo" file */
197 : /* static */
198 2214 : std::unique_ptr<IcechunkRepo> IcechunkRepo::Open(const char *pszFilename,
199 : VSIVirtualHandle *fp)
200 : {
201 2214 : CPLDebugOnly("Icechunk", "Opening repo %s", pszFilename);
202 4428 : std::string osFilename(pszFilename);
203 2214 : VSIVirtualHandleUniquePtr tmpFp;
204 2214 : if (!fp)
205 : {
206 2186 : if (strcmp(CPLGetFilename(pszFilename), "repo") != 0)
207 : {
208 : osFilename =
209 2186 : CPLFormFilenameSafe(osFilename.c_str(), "repo", nullptr);
210 2186 : tmpFp = VSIFilesystemHandler::OpenStatic(osFilename.c_str(), "rb");
211 : // For network file systems, try to read one byte
212 2186 : char chDummy = 1;
213 2186 : if (tmpFp && tmpFp->Read(&chDummy, 1) != 1)
214 : {
215 0 : tmpFp.reset();
216 : }
217 :
218 2186 : if (tmpFp)
219 : {
220 2171 : CPL_IGNORE_RET_VAL(tmpFp->Seek(0, SEEK_SET));
221 2171 : pszFilename = osFilename.c_str();
222 : }
223 : else
224 : {
225 : VSIStatBufL sStat;
226 30 : if (VSIStatL(CPLFormFilenameSafe(pszFilename, "refs", nullptr)
227 : .c_str(),
228 15 : &sStat) == 0)
229 : {
230 : // Icechunk v1
231 12 : return OpenV1(pszFilename);
232 : }
233 : }
234 : }
235 : else
236 : {
237 0 : tmpFp = VSIFilesystemHandler::OpenStatic(pszFilename, "rb");
238 : }
239 :
240 2174 : fp = tmpFp.get();
241 2174 : if (!fp)
242 : {
243 3 : CPLError(CE_Failure, CPLE_FileIO, "Cannot open %s", pszFilename);
244 3 : return nullptr;
245 : }
246 : }
247 :
248 2199 : int nFileVersion = 0;
249 2199 : auto [buffer, size] =
250 4398 : DecompressFile(pszFilename, fp, FILE_TYPE_REPO_INFO, &nFileVersion);
251 2199 : if (!buffer)
252 2 : return nullptr;
253 :
254 : {
255 2197 : Verifier verifier(buffer.get(), size);
256 2197 : if (!VerifyRepoBuffer(verifier))
257 : {
258 3 : CPLError(CE_Failure, CPLE_AppDefined, "%s: invalid Repo Flatbuffer",
259 : pszFilename);
260 3 : return nullptr;
261 : }
262 : }
263 :
264 4388 : auto repo = std::make_unique<IcechunkRepo>();
265 2194 : repo->m_osRootPath = CPLGetPathSafe(pszFilename);
266 :
267 2194 : const Repo *repoPtr = GetRepo(buffer.get());
268 :
269 2194 : const int nSpecVersion = repoPtr->spec_version();
270 2194 : CPLDebugOnly("Icechunk", "Repo spec_version = %d", nSpecVersion);
271 2194 : if (nSpecVersion != 1 && nSpecVersion != 2)
272 : {
273 1 : CPLError(CE_Failure, CPLE_AppDefined, "%s: invalid spec_version %d",
274 : pszFilename, nSpecVersion);
275 1 : return nullptr;
276 : }
277 2193 : if (nFileVersion != nSpecVersion)
278 : {
279 1 : CPLError(CE_Failure, CPLE_AppDefined,
280 : "%s: file version=%d != spec_version=%d", pszFilename,
281 : nFileVersion, nSpecVersion);
282 1 : return nullptr;
283 : }
284 :
285 2192 : const auto status = repoPtr->status();
286 2192 : if (status)
287 : {
288 2192 : const auto availability = status->availability();
289 2192 : if (availability == RepoAvailability::Offline)
290 : {
291 1 : const auto reason = status->limited_availability_reason();
292 1 : CPLError(CE_Failure, CPLE_AppDefined,
293 : "%s: repository is offline: %s", pszFilename,
294 0 : reason ? reason->c_str() : "unknown reason");
295 1 : return nullptr;
296 : }
297 : }
298 :
299 : {
300 2191 : const auto metadata = repoPtr->metadata();
301 2191 : if (metadata && nSpecVersion == 2)
302 : {
303 2123 : for (const auto &md : *metadata)
304 : {
305 0 : if (const auto value = md->value())
306 : {
307 0 : if (!flexbuffers::VerifyBuffer(value->data(),
308 0 : value->size()))
309 : {
310 0 : CPLError(CE_Failure, CPLE_AppDefined,
311 : "%s: flexbuffers::VerifyBuffer() failed",
312 : pszFilename);
313 0 : return nullptr;
314 : }
315 : if constexpr (IS_DEBUG_BUILD)
316 : {
317 0 : std::string val;
318 0 : flexbuffers::GetRoot(value->data(), value->size())
319 0 : .ToString(true, true, val);
320 0 : CPLDebugOnly("Icechunk", "metadata %s=%s",
321 : md->name() ? md->name()->c_str()
322 : : "(null)",
323 : val.c_str());
324 : }
325 : }
326 : }
327 : }
328 : }
329 :
330 2191 : if (const auto config = repoPtr->config())
331 : {
332 1057 : if (!flexbuffers::VerifyBuffer(config->data(), config->size()))
333 : {
334 0 : CPLError(CE_Failure, CPLE_AppDefined,
335 : "%s: flexbuffers::VerifyBuffer() failed", pszFilename);
336 0 : return nullptr;
337 : }
338 1057 : std::string val;
339 1057 : flexbuffers::GetRoot(config->data(), config->size())
340 1057 : .ToString(true, true, val);
341 1057 : CPLDebugOnly("Icechunk", "config %s", val.c_str());
342 1057 : CPLJSONDocument oDoc;
343 1057 : if (!oDoc.LoadMemory(val))
344 : {
345 0 : CPLError(CE_Failure, CPLE_AppDefined,
346 : "%s: invalid configuration: %s", pszFilename, val.c_str());
347 0 : return nullptr;
348 : }
349 1057 : ProcessConfig(oDoc.GetRoot());
350 : }
351 :
352 2191 : const auto snapshots = repoPtr->snapshots();
353 2191 : CPLAssertAlways(snapshots); // guaranteed by VerifyRepoBuffer()
354 8324 : for (uint32_t snapshotIdx = 0; snapshotIdx < snapshots->size();
355 : ++snapshotIdx)
356 : {
357 6134 : const auto *snapshot = (*snapshots)[snapshotIdx];
358 6134 : const auto *id = snapshot->id();
359 6134 : CPLAssertNotNull(id); // guaranteed by VerifyRepoBuffer()
360 6134 : CPLAssertNotNull(id->bytes()); // guaranteed by VerifyRepoBuffer()
361 :
362 6134 : const auto *message = snapshot->message();
363 6134 : CPLAssertNotNull(message); // guaranteed by VerifyRepoBuffer()
364 :
365 6134 : CPLDebugOnly("Icechunk",
366 : "snapshot '%s', parent_offset %d, message '%s'",
367 : CrockfordBase32Encode(*(id->bytes())).c_str(),
368 : snapshot->parent_offset(), message->c_str());
369 :
370 6134 : const auto metadata = snapshot->metadata();
371 6134 : if (metadata && nSpecVersion == 2)
372 : {
373 10019 : for (const auto &md : *metadata)
374 : {
375 3951 : if (const auto value = md->value())
376 : {
377 3951 : if (!flexbuffers::VerifyBuffer(value->data(),
378 3951 : value->size()))
379 : {
380 1 : CPLError(CE_Failure, CPLE_AppDefined,
381 : "%s: flexbuffers::VerifyBuffer() failed",
382 : pszFilename);
383 1 : return nullptr;
384 : }
385 : if constexpr (IS_DEBUG_BUILD)
386 : {
387 7900 : std::string val;
388 3950 : flexbuffers::GetRoot(value->data(), value->size())
389 3950 : .ToString(true, true, val);
390 3950 : CPLDebugOnly("Icechunk", " metadata %s=%s",
391 : md->name() ? md->name()->c_str()
392 : : "(null)",
393 : val.c_str());
394 : }
395 : }
396 : }
397 : }
398 : }
399 :
400 : // Parse tags
401 2190 : const auto tags = repoPtr->tags();
402 2190 : CPLAssertAlways(tags); // guaranteed by VerifyRepoBuffer()
403 2191 : for (const auto &ref : *tags)
404 : {
405 3 : const auto *refName = ref->name();
406 3 : CPLAssertNotNull(refName); // guaranteed by VerifyRepoBuffer()
407 3 : CPLDebugOnly("Icechunk", "tag '%s', snapshot_index %u",
408 : refName->c_str(), ref->snapshot_index());
409 :
410 3 : if (ref->snapshot_index() >= snapshots->size())
411 : {
412 1 : CPLError(CE_Failure, CPLE_AppDefined,
413 : "%s: tag '%s', invalid snapshot_index %u", pszFilename,
414 : refName->c_str(), ref->snapshot_index());
415 2 : return nullptr;
416 : }
417 :
418 2 : const auto *snapshot = (*snapshots)[ref->snapshot_index()];
419 2 : const auto *id = snapshot->id();
420 2 : CPLAssertNotNull(id); // guaranteed by VerifyRepoBuffer()
421 2 : if (!repo->m_oMapTagNameToSnapshotId
422 2 : .insert({GetString(refName),
423 4 : CrockfordBase32Encode(*(id->bytes()))})
424 2 : .second)
425 : {
426 1 : CPLError(CE_Failure, CPLE_AppDefined, "%s: more than one tag '%s'",
427 : pszFilename, refName->c_str());
428 1 : return nullptr;
429 : }
430 : }
431 :
432 : // Parse branches
433 2188 : const auto branches = repoPtr->branches();
434 2188 : CPLAssertAlways(branches); // guaranteed by VerifyRepoBuffer()
435 4374 : for (const auto &ref : *branches)
436 : {
437 2188 : const auto *refName = ref->name();
438 2188 : CPLAssertNotNull(refName); // guaranteed by VerifyRepoBuffer()
439 2188 : CPLDebugOnly("Icechunk", "branch '%s', snapshot_index %u",
440 : refName->c_str(), ref->snapshot_index());
441 :
442 2188 : if (ref->snapshot_index() >= snapshots->size())
443 : {
444 1 : CPLError(CE_Failure, CPLE_AppDefined,
445 : "%s: branch '%s', invalid snapshot_index %u", pszFilename,
446 : refName->c_str(), ref->snapshot_index());
447 2 : return nullptr;
448 : }
449 :
450 2187 : const auto *snapshot = (*snapshots)[ref->snapshot_index()];
451 2187 : const auto *id = snapshot->id();
452 2187 : CPLAssertNotNull(id); // guaranteed by VerifyRepoBuffer()
453 2187 : if (!repo->m_oMapBranchNameToSnapshotId
454 2187 : .insert({GetString(refName),
455 4374 : CrockfordBase32Encode(*(id->bytes()))})
456 2187 : .second)
457 : {
458 1 : CPLError(CE_Failure, CPLE_AppDefined,
459 : "%s: more than one branch '%s'", pszFilename,
460 : refName->c_str());
461 1 : return nullptr;
462 : }
463 : }
464 :
465 2186 : return repo;
466 : }
467 :
468 : /************************************************************************/
469 : /* IcechunkRepo::OpenSnapshotOnBranch() */
470 : /************************************************************************/
471 :
472 : /** Open the snapshot corresponding to the passed branch name. */
473 : std::unique_ptr<IcechunkSnapshot>
474 2192 : IcechunkRepo::OpenSnapshotOnBranch(const std::string &name,
475 : bool emitErrorIfUnknownBranch) const
476 : {
477 2192 : const auto oIter = m_oMapBranchNameToSnapshotId.find(name);
478 2192 : if (oIter == m_oMapBranchNameToSnapshotId.end())
479 : {
480 1 : if (emitErrorIfUnknownBranch)
481 0 : CPLError(CE_Failure, CPLE_AppDefined, "No branch '%s'",
482 : name.c_str());
483 1 : return nullptr;
484 : }
485 2191 : const auto &snapshotId = oIter->second;
486 :
487 : std::string osSnapshotFilename = CPLFormFilenameSafe(
488 2191 : CPLFormFilenameSafe(m_osRootPath.c_str(), "snapshots", nullptr).c_str(),
489 6573 : snapshotId.c_str(), nullptr);
490 2191 : return IcechunkSnapshot::Open(osSnapshotFilename.c_str());
491 : }
492 :
493 : /************************************************************************/
494 : /* IcechunkRepo::OpenSnapshotOnTag() */
495 : /************************************************************************/
496 :
497 : /** Open the snapshot corresponding to the passed tag name. */
498 : std::unique_ptr<IcechunkSnapshot>
499 3 : IcechunkRepo::OpenSnapshotOnTag(const std::string &name,
500 : bool emitErrorIfUnknownTag) const
501 : {
502 3 : const auto oIter = m_oMapTagNameToSnapshotId.find(name);
503 3 : if (oIter == m_oMapTagNameToSnapshotId.end())
504 : {
505 1 : if (emitErrorIfUnknownTag)
506 0 : CPLError(CE_Failure, CPLE_AppDefined, "No tag '%s'", name.c_str());
507 1 : return nullptr;
508 : }
509 2 : const auto &snapshotId = oIter->second;
510 :
511 : std::string osSnapshotFilename = CPLFormFilenameSafe(
512 2 : CPLFormFilenameSafe(m_osRootPath.c_str(), "snapshots", nullptr).c_str(),
513 6 : snapshotId.c_str(), nullptr);
514 2 : return IcechunkSnapshot::Open(osSnapshotFilename.c_str());
515 : }
516 :
517 : /************************************************************************/
518 : /* GetRepoCache() */
519 : /************************************************************************/
520 :
521 : using CacheType =
522 : lru11::Cache<std::string, std::shared_ptr<IcechunkManifest>, std::mutex>;
523 :
524 9107 : static CacheType &GetRepoCache()
525 : {
526 : static lru11::Cache<std::string, std::shared_ptr<IcechunkManifest>,
527 : std::mutex>
528 9107 : goCache;
529 9107 : return goCache;
530 : }
531 :
532 : /************************************************************************/
533 : /* IcechunkRepo::OpenManifest() */
534 : /************************************************************************/
535 :
536 : /** Open the manifest corresponding to the passed manifest id. */
537 : std::shared_ptr<IcechunkManifest>
538 8101 : IcechunkRepo::OpenManifest(const std::string &manifestId,
539 : uint64_t nExpectedFileSize,
540 : uint32_t nExpectedChunkRefs) const
541 : {
542 8101 : CacheType &goCache = GetRepoCache();
543 8101 : std::shared_ptr<IcechunkManifest> manifest;
544 : const std::string cacheKey =
545 16202 : std::string(m_osRootPath).append("|").append(manifestId);
546 8101 : if (goCache.tryGet(cacheKey, manifest))
547 6004 : return manifest;
548 :
549 : std::string osFilename = CPLFormFilenameSafe(
550 2097 : CPLFormFilenameSafe(m_osRootPath.c_str(), "manifests", nullptr).c_str(),
551 6291 : manifestId.c_str(), nullptr);
552 2097 : manifest = IcechunkManifest::Open(osFilename.c_str());
553 2097 : if (manifest)
554 : {
555 : VSIStatBufL sStat;
556 4166 : if (VSIStatL(manifest->GetFilename().c_str(), &sStat) == 0 &&
557 2083 : static_cast<uint64_t>(sStat.st_size) != nExpectedFileSize)
558 : {
559 0 : CPLError(CE_Failure, CPLE_AppDefined,
560 : "Actual file size of manifest %s = %" PRIu64
561 : " does not match expected one = %" PRIu64,
562 0 : osFilename.c_str(), static_cast<uint64_t>(sStat.st_size),
563 : nExpectedFileSize);
564 0 : manifest.reset();
565 : }
566 2083 : else if (manifest->GetChunkRefsCount() != nExpectedChunkRefs)
567 : {
568 1 : CPLError(CE_Failure, CPLE_AppDefined,
569 : "Actual count of chunk references in manifest %s = %u "
570 : "does not match expected one = %u",
571 : osFilename.c_str(), manifest->GetChunkRefsCount(),
572 : nExpectedChunkRefs);
573 1 : manifest.reset();
574 : }
575 : else
576 : {
577 2082 : goCache.insert(cacheKey, manifest);
578 : }
579 : }
580 2097 : return manifest;
581 : }
582 :
583 : /************************************************************************/
584 : /* IcechunkRepo::ClearCaches() */
585 : /************************************************************************/
586 :
587 1006 : /* static */ void IcechunkRepo::ClearCaches()
588 : {
589 1006 : CacheType &goCache = GetRepoCache();
590 1006 : goCache.clear();
591 1006 : }
592 :
593 : } // namespace gdal::icechunk
|