LCOV - code coverage report
Current view: top level - frmts/icechunk - icechunkmanifest.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 166 172 96.5 %
Date: 2026-07-24 18:27:47 Functions: 7 7 100.0 %

          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

Generated by: LCOV version 1.14