LCOV - code coverage report
Current view: top level - frmts/zarr - zarr_v3_array.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 975 1111 87.8 %
Date: 2026-02-11 08:43:47 Functions: 35 37 94.6 %

          Line data    Source code
       1             : /******************************************************************************
       2             :  *
       3             :  * Project:  GDAL
       4             :  * Purpose:  Zarr driver
       5             :  * Author:   Even Rouault <even dot rouault at spatialys.com>
       6             :  *
       7             :  ******************************************************************************
       8             :  * Copyright (c) 2021, Even Rouault <even dot rouault at spatialys.com>
       9             :  *
      10             :  * SPDX-License-Identifier: MIT
      11             :  ****************************************************************************/
      12             : 
      13             : #include "cpl_enumerate.h"
      14             : #include "cpl_float.h"
      15             : #include "cpl_vsi_virtual.h"
      16             : #include "gdal_thread_pool.h"
      17             : #include "zarr.h"
      18             : #include "zarr_v3_codec.h"
      19             : 
      20             : #include <algorithm>
      21             : #include <cassert>
      22             : #include <cinttypes>
      23             : #include <cmath>
      24             : #include <cstdlib>
      25             : #include <limits>
      26             : #include <map>
      27             : #include <set>
      28             : 
      29             : /************************************************************************/
      30             : /*                      ZarrV3Array::ZarrV3Array()                      */
      31             : /************************************************************************/
      32             : 
      33        1176 : ZarrV3Array::ZarrV3Array(
      34             :     const std::shared_ptr<ZarrSharedResource> &poSharedResource,
      35             :     const std::shared_ptr<ZarrGroupBase> &poParent, const std::string &osName,
      36             :     const std::vector<std::shared_ptr<GDALDimension>> &aoDims,
      37             :     const GDALExtendedDataType &oType, const std::vector<DtypeElt> &aoDtypeElts,
      38             :     const std::vector<GUInt64> &anOuterBlockSize,
      39        1176 :     const std::vector<GUInt64> &anInnerBlockSize)
      40        1176 :     : GDALAbstractMDArray(poParent->GetFullName(), osName),
      41             :       ZarrArray(poSharedResource, poParent, osName, aoDims, oType, aoDtypeElts,
      42        1176 :                 anOuterBlockSize, anInnerBlockSize)
      43             : {
      44        1176 : }
      45             : 
      46             : /************************************************************************/
      47             : /*                        ZarrV3Array::Create()                         */
      48             : /************************************************************************/
      49             : 
      50        1176 : std::shared_ptr<ZarrV3Array> ZarrV3Array::Create(
      51             :     const std::shared_ptr<ZarrSharedResource> &poSharedResource,
      52             :     const std::shared_ptr<ZarrGroupBase> &poParent, const std::string &osName,
      53             :     const std::vector<std::shared_ptr<GDALDimension>> &aoDims,
      54             :     const GDALExtendedDataType &oType, const std::vector<DtypeElt> &aoDtypeElts,
      55             :     const std::vector<GUInt64> &anOuterBlockSize,
      56             :     const std::vector<GUInt64> &anInnerBlockSize)
      57             : {
      58             :     auto arr = std::shared_ptr<ZarrV3Array>(
      59             :         new ZarrV3Array(poSharedResource, poParent, osName, aoDims, oType,
      60        2352 :                         aoDtypeElts, anOuterBlockSize, anInnerBlockSize));
      61        1176 :     if (arr->m_nTotalInnerChunkCount == 0)
      62           1 :         return nullptr;
      63        1175 :     arr->SetSelf(arr);
      64             : 
      65        1175 :     return arr;
      66             : }
      67             : 
      68             : /************************************************************************/
      69             : /*                            ~ZarrV3Array()                            */
      70             : /************************************************************************/
      71             : 
      72        2352 : ZarrV3Array::~ZarrV3Array()
      73             : {
      74        1176 :     ZarrV3Array::Flush();
      75        2352 : }
      76             : 
      77             : /************************************************************************/
      78             : /*                               Flush()                                */
      79             : /************************************************************************/
      80             : 
      81        5217 : bool ZarrV3Array::Flush()
      82             : {
      83        5217 :     if (!m_bValid)
      84           4 :         return true;
      85             : 
      86        5213 :     bool ret = ZarrV3Array::FlushDirtyBlock();
      87             : 
      88        5213 :     if (!m_aoDims.empty())
      89             :     {
      90        9036 :         for (const auto &poDim : m_aoDims)
      91             :         {
      92             :             const auto poZarrDim =
      93        6752 :                 dynamic_cast<const ZarrDimension *>(poDim.get());
      94        6752 :             if (poZarrDim && poZarrDim->IsXArrayDimension())
      95             :             {
      96        3995 :                 if (poZarrDim->IsModified())
      97           8 :                     m_bDefinitionModified = true;
      98             :             }
      99             :             else
     100             :             {
     101        2757 :                 break;
     102             :             }
     103             :         }
     104             :     }
     105             : 
     106        5213 :     CPLJSONObject oAttrs;
     107       10384 :     if (m_oAttrGroup.IsModified() || m_bUnitModified || m_bOffsetModified ||
     108       10384 :         m_bScaleModified || m_bSRSModified)
     109             :     {
     110          42 :         m_bNew = false;
     111             : 
     112          42 :         oAttrs = SerializeSpecialAttributes();
     113             : 
     114          42 :         m_bDefinitionModified = true;
     115             :     }
     116             : 
     117        5213 :     if (m_bDefinitionModified)
     118             :     {
     119         163 :         if (!Serialize(oAttrs))
     120           1 :             ret = false;
     121         163 :         m_bDefinitionModified = false;
     122             :     }
     123             : 
     124        5213 :     return ret;
     125             : }
     126             : 
     127             : /************************************************************************/
     128             : /*                       ZarrV3Array::Serialize()                       */
     129             : /************************************************************************/
     130             : 
     131         163 : bool ZarrV3Array::Serialize(const CPLJSONObject &oAttrs)
     132             : {
     133         326 :     CPLJSONDocument oDoc;
     134         326 :     CPLJSONObject oRoot = oDoc.GetRoot();
     135             : 
     136         163 :     oRoot.Add("zarr_format", 3);
     137         163 :     oRoot.Add("node_type", "array");
     138             : 
     139         163 :     CPLJSONArray oShape;
     140         417 :     for (const auto &poDim : m_aoDims)
     141             :     {
     142         254 :         oShape.Add(static_cast<GInt64>(poDim->GetSize()));
     143             :     }
     144         163 :     oRoot.Add("shape", oShape);
     145             : 
     146         163 :     oRoot.Add("data_type", m_dtype.ToString());
     147             : 
     148             :     {
     149         326 :         CPLJSONObject oChunkGrid;
     150         163 :         oRoot.Add("chunk_grid", oChunkGrid);
     151         163 :         oChunkGrid.Add("name", "regular");
     152         326 :         CPLJSONObject oConfiguration;
     153         163 :         oChunkGrid.Add("configuration", oConfiguration);
     154         163 :         CPLJSONArray oChunks;
     155         417 :         for (const auto nBlockSize : m_anOuterBlockSize)
     156             :         {
     157         254 :             oChunks.Add(static_cast<GInt64>(nBlockSize));
     158             :         }
     159         163 :         oConfiguration.Add("chunk_shape", oChunks);
     160             :     }
     161             : 
     162             :     {
     163         326 :         CPLJSONObject oChunkKeyEncoding;
     164         163 :         oRoot.Add("chunk_key_encoding", oChunkKeyEncoding);
     165         163 :         oChunkKeyEncoding.Add("name", m_bV2ChunkKeyEncoding ? "v2" : "default");
     166         163 :         CPLJSONObject oConfiguration;
     167         163 :         oChunkKeyEncoding.Add("configuration", oConfiguration);
     168         163 :         oConfiguration.Add("separator", m_osDimSeparator);
     169             :     }
     170             : 
     171         163 :     if (m_pabyNoData == nullptr)
     172             :     {
     173         277 :         if (m_oType.GetNumericDataType() == GDT_Float16 ||
     174         277 :             m_oType.GetNumericDataType() == GDT_Float32 ||
     175         131 :             m_oType.GetNumericDataType() == GDT_Float64)
     176             :         {
     177          19 :             oRoot.Add("fill_value", "NaN");
     178             :         }
     179             :         else
     180             :         {
     181         120 :             oRoot.AddNull("fill_value");
     182             :         }
     183             :     }
     184             :     else
     185             :     {
     186          48 :         if (m_oType.GetNumericDataType() == GDT_CFloat16 ||
     187          48 :             m_oType.GetNumericDataType() == GDT_CFloat32 ||
     188          16 :             m_oType.GetNumericDataType() == GDT_CFloat64)
     189             :         {
     190             :             double adfNoDataValue[2];
     191          16 :             GDALCopyWords(m_pabyNoData, m_oType.GetNumericDataType(), 0,
     192             :                           adfNoDataValue, GDT_CFloat64, 0, 1);
     193          16 :             CPLJSONArray oArray;
     194          48 :             for (int i = 0; i < 2; ++i)
     195             :             {
     196          32 :                 if (std::isnan(adfNoDataValue[i]))
     197           6 :                     oArray.Add("NaN");
     198          26 :                 else if (adfNoDataValue[i] ==
     199          26 :                          std::numeric_limits<double>::infinity())
     200           4 :                     oArray.Add("Infinity");
     201          44 :                 else if (adfNoDataValue[i] ==
     202          22 :                          -std::numeric_limits<double>::infinity())
     203           4 :                     oArray.Add("-Infinity");
     204             :                 else
     205          18 :                     oArray.Add(adfNoDataValue[i]);
     206             :             }
     207          16 :             oRoot.Add("fill_value", oArray);
     208             :         }
     209             :         else
     210             :         {
     211           8 :             SerializeNumericNoData(oRoot);
     212             :         }
     213             :     }
     214             : 
     215         163 :     if (m_poCodecs)
     216             :     {
     217         147 :         oRoot.Add("codecs", m_poCodecs->GetJSon());
     218             :     }
     219             : 
     220         163 :     oRoot.Add("attributes", oAttrs);
     221             : 
     222             :     // Set dimension_names
     223         163 :     if (!m_aoDims.empty())
     224             :     {
     225         288 :         CPLJSONArray oDimensions;
     226         382 :         for (const auto &poDim : m_aoDims)
     227             :         {
     228             :             const auto poZarrDim =
     229         254 :                 dynamic_cast<const ZarrDimension *>(poDim.get());
     230         254 :             if (poZarrDim && poZarrDim->IsXArrayDimension())
     231             :             {
     232         238 :                 oDimensions.Add(poDim->GetName());
     233             :             }
     234             :             else
     235             :             {
     236          16 :                 oDimensions = CPLJSONArray();
     237          16 :                 break;
     238             :             }
     239             :         }
     240         144 :         if (oDimensions.Size() > 0)
     241             :         {
     242         128 :             oRoot.Add("dimension_names", oDimensions);
     243             :         }
     244             :     }
     245             : 
     246             :     // TODO: codecs
     247             : 
     248         163 :     const bool bRet = oDoc.Save(m_osFilename);
     249         163 :     if (bRet)
     250         162 :         m_poSharedResource->SetZMetadataItem(m_osFilename, oRoot);
     251         326 :     return bRet;
     252             : }
     253             : 
     254             : /************************************************************************/
     255             : /*                   ZarrV3Array::NeedDecodedBuffer()                   */
     256             : /************************************************************************/
     257             : 
     258       15835 : bool ZarrV3Array::NeedDecodedBuffer() const
     259             : {
     260       31670 :     for (const auto &elt : m_aoDtypeElts)
     261             :     {
     262       15835 :         if (elt.needByteSwapping || elt.gdalTypeIsApproxOfNative)
     263             :         {
     264           0 :             return true;
     265             :         }
     266             :     }
     267       15835 :     return false;
     268             : }
     269             : 
     270             : /************************************************************************/
     271             : /*                ZarrV3Array::AllocateWorkingBuffers()                 */
     272             : /************************************************************************/
     273             : 
     274         823 : bool ZarrV3Array::AllocateWorkingBuffers() const
     275             : {
     276         823 :     if (m_bAllocateWorkingBuffersDone)
     277          10 :         return m_bWorkingBuffersOK;
     278             : 
     279         813 :     m_bAllocateWorkingBuffersDone = true;
     280             : 
     281         813 :     size_t nSizeNeeded = m_nInnerBlockSizeBytes;
     282         813 :     if (NeedDecodedBuffer())
     283             :     {
     284           0 :         size_t nDecodedBufferSize = m_oType.GetSize();
     285           0 :         for (const auto &nBlockSize : m_anInnerBlockSize)
     286             :         {
     287           0 :             if (nDecodedBufferSize > std::numeric_limits<size_t>::max() /
     288           0 :                                          static_cast<size_t>(nBlockSize))
     289             :             {
     290           0 :                 CPLError(CE_Failure, CPLE_AppDefined, "Too large chunk size");
     291           0 :                 return false;
     292             :             }
     293           0 :             nDecodedBufferSize *= static_cast<size_t>(nBlockSize);
     294             :         }
     295           0 :         if (nSizeNeeded >
     296           0 :             std::numeric_limits<size_t>::max() - nDecodedBufferSize)
     297             :         {
     298           0 :             CPLError(CE_Failure, CPLE_AppDefined, "Too large chunk size");
     299           0 :             return false;
     300             :         }
     301           0 :         nSizeNeeded += nDecodedBufferSize;
     302             :     }
     303             : 
     304             :     // Reserve a buffer for tile content
     305         813 :     if (nSizeNeeded > 1024 * 1024 * 1024 &&
     306           0 :         !CPLTestBool(CPLGetConfigOption("ZARR_ALLOW_BIG_TILE_SIZE", "NO")))
     307             :     {
     308           0 :         CPLError(CE_Failure, CPLE_AppDefined,
     309             :                  "Zarr tile allocation would require " CPL_FRMT_GUIB " bytes. "
     310             :                  "By default the driver limits to 1 GB. To allow that memory "
     311             :                  "allocation, set the ZARR_ALLOW_BIG_TILE_SIZE configuration "
     312             :                  "option to YES.",
     313             :                  static_cast<GUIntBig>(nSizeNeeded));
     314           0 :         return false;
     315             :     }
     316             : 
     317         813 :     m_bWorkingBuffersOK =
     318         813 :         AllocateWorkingBuffers(m_abyRawBlockData, m_abyDecodedBlockData);
     319         813 :     return m_bWorkingBuffersOK;
     320             : }
     321             : 
     322       11510 : bool ZarrV3Array::AllocateWorkingBuffers(
     323             :     ZarrByteVectorQuickResize &abyRawBlockData,
     324             :     ZarrByteVectorQuickResize &abyDecodedBlockData) const
     325             : {
     326             :     // This method should NOT modify any ZarrArray member, as it is going to
     327             :     // be called concurrently from several threads.
     328             : 
     329             :     // Set those #define to avoid accidental use of some global variables
     330             : #define m_abyRawBlockData cannot_use_here
     331             : #define m_abyDecodedBlockData cannot_use_here
     332             : 
     333       11510 :     const size_t nSizeNeeded = m_nInnerBlockSizeBytes;
     334             :     try
     335             :     {
     336       11510 :         abyRawBlockData.resize(nSizeNeeded);
     337             :     }
     338           0 :     catch (const std::bad_alloc &e)
     339             :     {
     340           0 :         CPLError(CE_Failure, CPLE_OutOfMemory, "%s", e.what());
     341           0 :         return false;
     342             :     }
     343             : 
     344       11510 :     if (NeedDecodedBuffer())
     345             :     {
     346           0 :         size_t nDecodedBufferSize = m_oType.GetSize();
     347           0 :         for (const auto &nBlockSize : m_anInnerBlockSize)
     348             :         {
     349           0 :             nDecodedBufferSize *= static_cast<size_t>(nBlockSize);
     350             :         }
     351             :         try
     352             :         {
     353           0 :             abyDecodedBlockData.resize(nDecodedBufferSize);
     354             :         }
     355           0 :         catch (const std::bad_alloc &e)
     356             :         {
     357           0 :             CPLError(CE_Failure, CPLE_OutOfMemory, "%s", e.what());
     358           0 :             return false;
     359             :         }
     360             :     }
     361             : 
     362       11510 :     return true;
     363             : #undef m_abyRawBlockData
     364             : #undef m_abyDecodedBlockData
     365             : }
     366             : 
     367             : /************************************************************************/
     368             : /*                     ZarrV3Array::LoadBlockData()                     */
     369             : /************************************************************************/
     370             : 
     371        1687 : bool ZarrV3Array::LoadBlockData(const uint64_t *blockIndices,
     372             :                                 bool &bMissingBlockOut) const
     373             : {
     374        1687 :     return LoadBlockData(blockIndices,
     375             :                          false,  // use mutex
     376        1687 :                          m_poCodecs.get(), m_abyRawBlockData,
     377        3374 :                          m_abyDecodedBlockData, bMissingBlockOut);
     378             : }
     379             : 
     380       12384 : bool ZarrV3Array::LoadBlockData(const uint64_t *blockIndices, bool bUseMutex,
     381             :                                 ZarrV3CodecSequence *poCodecs,
     382             :                                 ZarrByteVectorQuickResize &abyRawBlockData,
     383             :                                 ZarrByteVectorQuickResize &abyDecodedBlockData,
     384             :                                 bool &bMissingBlockOut) const
     385             : {
     386             :     // This method should NOT modify any ZarrArray member, as it is going to
     387             :     // be called concurrently from several threads.
     388             : 
     389             :     // Set those #define to avoid accidental use of some global variables
     390             : #define m_abyRawBlockData cannot_use_here
     391             : #define m_abyDecodedBlockData cannot_use_here
     392             : #define m_poCodecs cannot_use_here
     393             : 
     394       12384 :     bMissingBlockOut = false;
     395             : 
     396       24768 :     std::string osFilename;
     397       12384 :     if (poCodecs && poCodecs->SupportsPartialDecoding())
     398             :     {
     399        1138 :         std::vector<uint64_t> outerChunkIndices;
     400        3414 :         for (size_t i = 0; i < GetDimensionCount(); ++i)
     401             :         {
     402             :             // Note: m_anOuterBlockSize[i]/m_anInnerBlockSize[i] is an integer
     403        6828 :             outerChunkIndices.push_back(blockIndices[i] *
     404        4552 :                                         m_anInnerBlockSize[i] /
     405        2276 :                                         m_anOuterBlockSize[i]);
     406             :         }
     407             : 
     408        1138 :         osFilename = BuildChunkFilename(outerChunkIndices.data());
     409             :     }
     410             :     else
     411             :     {
     412       11246 :         osFilename = BuildChunkFilename(blockIndices);
     413             :     }
     414             : 
     415             :     // For network file systems, get the streaming version of the filename,
     416             :     // as we don't need arbitrary seeking in the file
     417             :     // ... unless we do partial decoding, in which case range requests within
     418             :     // a shard are much more efficient
     419       12384 :     if (!(poCodecs && poCodecs->SupportsPartialDecoding()))
     420             :     {
     421       11246 :         osFilename = VSIFileManager::GetHandler(osFilename.c_str())
     422       11246 :                          ->GetStreamingFilename(osFilename);
     423             :     }
     424             : 
     425             :     // First if we have a tile presence cache, check tile presence from it
     426             :     bool bEarlyRet;
     427       12384 :     if (bUseMutex)
     428             :     {
     429       10697 :         std::lock_guard<std::mutex> oLock(m_oMutex);
     430       10697 :         bEarlyRet = IsBlockMissingFromCacheInfo(osFilename, blockIndices);
     431             :     }
     432             :     else
     433             :     {
     434        1687 :         bEarlyRet = IsBlockMissingFromCacheInfo(osFilename, blockIndices);
     435             :     }
     436       12384 :     if (bEarlyRet)
     437             :     {
     438          13 :         bMissingBlockOut = true;
     439          13 :         return true;
     440             :     }
     441       12371 :     VSIVirtualHandleUniquePtr fp;
     442             :     // This is the number of files returned in a S3 directory listing operation
     443       12371 :     constexpr uint64_t MAX_TILES_ALLOWED_FOR_DIRECTORY_LISTING = 1000;
     444       12371 :     const char *const apszOpenOptions[] = {"IGNORE_FILENAME_RESTRICTIONS=YES",
     445             :                                            nullptr};
     446       12371 :     const auto nErrorBefore = CPLGetErrorCounter();
     447       24733 :     if ((m_osDimSeparator == "/" && !m_anOuterBlockSize.empty() &&
     448       37113 :          m_anOuterBlockSize.back() > MAX_TILES_ALLOWED_FOR_DIRECTORY_LISTING) ||
     449       12371 :         (m_osDimSeparator != "/" &&
     450           9 :          m_nTotalInnerChunkCount > MAX_TILES_ALLOWED_FOR_DIRECTORY_LISTING))
     451             :     {
     452             :         // Avoid issuing ReadDir() when a lot of files are expected
     453             :         CPLConfigOptionSetter optionSetter("GDAL_DISABLE_READDIR_ON_OPEN",
     454           0 :                                            "YES", true);
     455           0 :         fp.reset(VSIFOpenEx2L(osFilename.c_str(), "rb", 0, apszOpenOptions));
     456             :     }
     457             :     else
     458             :     {
     459       12371 :         fp.reset(VSIFOpenEx2L(osFilename.c_str(), "rb", 0, apszOpenOptions));
     460             :     }
     461       12371 :     if (fp == nullptr)
     462             :     {
     463         306 :         if (nErrorBefore != CPLGetErrorCounter())
     464             :         {
     465           0 :             return false;
     466             :         }
     467             :         else
     468             :         {
     469             :             // Missing files are OK and indicate nodata_value
     470         306 :             CPLDebugOnly(ZARR_DEBUG_KEY, "Block %s missing (=nodata)",
     471             :                          osFilename.c_str());
     472         306 :             bMissingBlockOut = true;
     473         306 :             return true;
     474             :         }
     475             :     }
     476             : 
     477       12065 :     bMissingBlockOut = false;
     478             : 
     479       12065 :     if (poCodecs && poCodecs->SupportsPartialDecoding())
     480             :     {
     481        1132 :         std::vector<size_t> anStartIdx;
     482        1132 :         std::vector<size_t> anCount;
     483        3396 :         for (size_t i = 0; i < GetDimensionCount(); ++i)
     484             :         {
     485        2264 :             anStartIdx.push_back(
     486        4528 :                 static_cast<size_t>((blockIndices[i] * m_anInnerBlockSize[i]) %
     487        2264 :                                     m_anOuterBlockSize[i]));
     488        2264 :             anCount.push_back(static_cast<size_t>(m_anInnerBlockSize[i]));
     489             :         }
     490        1132 :         if (!poCodecs->DecodePartial(fp.get(), abyRawBlockData, anStartIdx,
     491             :                                      anCount))
     492         436 :             return false;
     493             :     }
     494             :     else
     495             :     {
     496       10933 :         CPLAssert(abyRawBlockData.capacity() >= m_nInnerBlockSizeBytes);
     497             :         // should not fail
     498       10933 :         abyRawBlockData.resize(m_nInnerBlockSizeBytes);
     499             : 
     500       10933 :         bool bRet = true;
     501       10933 :         size_t nRawDataSize = abyRawBlockData.size();
     502       10933 :         if (poCodecs == nullptr)
     503             :         {
     504           6 :             nRawDataSize = fp->Read(&abyRawBlockData[0], 1, nRawDataSize);
     505             :         }
     506             :         else
     507             :         {
     508       10927 :             fp->Seek(0, SEEK_END);
     509       10927 :             const auto nSize = fp->Tell();
     510       10927 :             fp->Seek(0, SEEK_SET);
     511       10927 :             if (nSize >
     512       10927 :                 static_cast<vsi_l_offset>(std::numeric_limits<int>::max()))
     513             :             {
     514           0 :                 CPLError(CE_Failure, CPLE_AppDefined, "Too large tile %s",
     515             :                          osFilename.c_str());
     516           0 :                 bRet = false;
     517             :             }
     518             :             else
     519             :             {
     520             :                 try
     521             :                 {
     522       10927 :                     abyRawBlockData.resize(static_cast<size_t>(nSize));
     523             :                 }
     524           0 :                 catch (const std::exception &)
     525             :                 {
     526           0 :                     CPLError(CE_Failure, CPLE_OutOfMemory,
     527             :                              "Cannot allocate memory for tile %s",
     528             :                              osFilename.c_str());
     529           0 :                     bRet = false;
     530             :                 }
     531             : 
     532       32780 :                 if (bRet &&
     533       21853 :                     (abyRawBlockData.empty() ||
     534       10926 :                      fp->Read(&abyRawBlockData[0], 1, abyRawBlockData.size()) !=
     535       10926 :                          abyRawBlockData.size()))
     536             :                 {
     537           1 :                     CPLError(CE_Failure, CPLE_AppDefined,
     538             :                              "Could not read tile %s correctly",
     539             :                              osFilename.c_str());
     540           1 :                     bRet = false;
     541             :                 }
     542             :                 else
     543             :                 {
     544       10926 :                     if (!poCodecs->Decode(abyRawBlockData))
     545             :                     {
     546         131 :                         CPLError(CE_Failure, CPLE_AppDefined,
     547             :                                  "Decompression of tile %s failed",
     548             :                                  osFilename.c_str());
     549         131 :                         bRet = false;
     550             :                     }
     551             :                 }
     552             :             }
     553             :         }
     554       10933 :         if (!bRet)
     555         132 :             return false;
     556             : 
     557       10801 :         if (nRawDataSize != abyRawBlockData.size())
     558             :         {
     559           0 :             CPLError(CE_Failure, CPLE_AppDefined,
     560             :                      "Decompressed tile %s has not expected size. "
     561             :                      "Got %u instead of %u",
     562             :                      osFilename.c_str(),
     563           0 :                      static_cast<unsigned>(abyRawBlockData.size()),
     564             :                      static_cast<unsigned>(nRawDataSize));
     565           0 :             return false;
     566             :         }
     567             :     }
     568             : 
     569       11497 :     if (!abyDecodedBlockData.empty())
     570             :     {
     571             :         const size_t nSourceSize =
     572           0 :             m_aoDtypeElts.back().nativeOffset + m_aoDtypeElts.back().nativeSize;
     573           0 :         const auto nDTSize = m_oType.GetSize();
     574           0 :         const size_t nValues = abyDecodedBlockData.size() / nDTSize;
     575           0 :         CPLAssert(nValues == m_nInnerBlockSizeBytes / nSourceSize);
     576           0 :         const GByte *pSrc = abyRawBlockData.data();
     577           0 :         GByte *pDst = &abyDecodedBlockData[0];
     578           0 :         for (size_t i = 0; i < nValues;
     579           0 :              i++, pSrc += nSourceSize, pDst += nDTSize)
     580             :         {
     581           0 :             DecodeSourceElt(m_aoDtypeElts, pSrc, pDst);
     582             :         }
     583             :     }
     584             : 
     585       11497 :     return true;
     586             : 
     587             : #undef m_abyRawBlockData
     588             : #undef m_abyDecodedBlockData
     589             : #undef m_poCodecs
     590             : }
     591             : 
     592             : /************************************************************************/
     593             : /*                         ZarrV3Array::IRead()                         */
     594             : /************************************************************************/
     595             : 
     596         743 : bool ZarrV3Array::IRead(const GUInt64 *arrayStartIdx, const size_t *count,
     597             :                         const GInt64 *arrayStep, const GPtrDiff_t *bufferStride,
     598             :                         const GDALExtendedDataType &bufferDataType,
     599             :                         void *pDstBuffer) const
     600             : {
     601             :     // For sharded arrays, pre-populate the block cache via ReadMultiRange()
     602             :     // so that the base-class block-by-block loop hits memory, not HTTP.
     603         743 :     if (m_poCodecs && m_poCodecs->SupportsPartialDecoding())
     604             :     {
     605         501 :         PreloadShardedBlocks(arrayStartIdx, count);
     606             :     }
     607         743 :     return ZarrArray::IRead(arrayStartIdx, count, arrayStep, bufferStride,
     608         743 :                             bufferDataType, pDstBuffer);
     609             : }
     610             : 
     611             : /************************************************************************/
     612             : /*                 ZarrV3Array::PreloadShardedBlocks()                  */
     613             : /************************************************************************/
     614             : 
     615         501 : void ZarrV3Array::PreloadShardedBlocks(const GUInt64 *arrayStartIdx,
     616             :                                        const size_t *count) const
     617             : {
     618         501 :     const size_t nDims = m_aoDims.size();
     619         501 :     if (nDims == 0)
     620           2 :         return;
     621             : 
     622             :     // Calculate needed block index range
     623        1002 :     std::vector<uint64_t> anBlockMin(nDims), anBlockMax(nDims);
     624         501 :     size_t nTotalBlocks = 1;
     625        1503 :     for (size_t i = 0; i < nDims; ++i)
     626             :     {
     627        1002 :         anBlockMin[i] = arrayStartIdx[i] / m_anInnerBlockSize[i];
     628        2004 :         anBlockMax[i] =
     629        1002 :             (arrayStartIdx[i] + count[i] - 1) / m_anInnerBlockSize[i];
     630        1002 :         nTotalBlocks *= static_cast<size_t>(anBlockMax[i] - anBlockMin[i] + 1);
     631             :     }
     632             : 
     633         501 :     if (nTotalBlocks <= 1)
     634           2 :         return;  // single block — no batching benefit
     635             : 
     636         499 :     CPLDebugOnly("ZARR", "PreloadShardedBlocks: %" PRIu64 " blocks to batch",
     637             :                  static_cast<uint64_t>(nTotalBlocks));
     638             : 
     639             :     // Enumerate all needed blocks, grouped by shard filename
     640             :     struct BlockInfo
     641             :     {
     642             :         std::vector<uint64_t> anBlockIndices{};
     643             :         std::vector<size_t> anStartIdx{};
     644             :         std::vector<size_t> anCount{};
     645             :     };
     646             : 
     647         998 :     std::map<std::string, std::vector<BlockInfo>> oShardToBlocks;
     648             : 
     649             :     // Iterate over all needed block indices
     650         998 :     std::vector<uint64_t> anCur(nDims);
     651         499 :     size_t dimIdx = 0;
     652       15380 : lbl_next:
     653       15380 :     if (dimIdx == nDims)
     654             :     {
     655             :         // Skip blocks already in cache
     656       24792 :         const std::vector<uint64_t> cacheKey(anCur.begin(), anCur.end());
     657       12396 :         if (m_oChunkCache.find(cacheKey) == m_oChunkCache.end())
     658             :         {
     659             :             // Compute shard filename and inner chunk start/count
     660       24686 :             std::vector<uint64_t> outerIdx(nDims);
     661       24686 :             BlockInfo info;
     662       12343 :             info.anBlockIndices = anCur;
     663       12343 :             info.anStartIdx.resize(nDims);
     664       12343 :             info.anCount.resize(nDims);
     665       37029 :             for (size_t i = 0; i < nDims; ++i)
     666             :             {
     667       49372 :                 outerIdx[i] =
     668       24686 :                     anCur[i] * m_anInnerBlockSize[i] / m_anOuterBlockSize[i];
     669       49372 :                 info.anStartIdx[i] = static_cast<size_t>(
     670       24686 :                     (anCur[i] * m_anInnerBlockSize[i]) % m_anOuterBlockSize[i]);
     671       24686 :                 info.anCount[i] = static_cast<size_t>(m_anInnerBlockSize[i]);
     672             :             }
     673             : 
     674       24686 :             std::string osFilename = BuildChunkFilename(outerIdx.data());
     675       12343 :             oShardToBlocks[osFilename].push_back(std::move(info));
     676             :         }
     677             :     }
     678             :     else
     679             :     {
     680        2984 :         anCur[dimIdx] = anBlockMin[dimIdx];
     681             :         while (true)
     682             :         {
     683       14881 :             dimIdx++;
     684       14881 :             goto lbl_next;
     685       14881 :         lbl_return:
     686       14881 :             dimIdx--;
     687       14881 :             if (anCur[dimIdx] == anBlockMax[dimIdx])
     688        2984 :                 break;
     689       11897 :             ++anCur[dimIdx];
     690             :         }
     691             :     }
     692       15380 :     if (dimIdx > 0)
     693       14881 :         goto lbl_return;
     694             : 
     695             :     // For each shard with >1 uncached block, batch-read
     696         499 :     const char *const apszOpenOptions[] = {"IGNORE_FILENAME_RESTRICTIONS=YES",
     697             :                                            nullptr};
     698             : 
     699        4944 :     for (auto &[osFilename, aBlocks] : oShardToBlocks)
     700             :     {
     701        4445 :         if (aBlocks.size() <= 1)
     702         933 :             continue;
     703             : 
     704             :         VSIVirtualHandleUniquePtr fp(
     705        3950 :             VSIFOpenEx2L(osFilename.c_str(), "rb", 0, apszOpenOptions));
     706        3950 :         if (!fp)
     707           2 :             continue;
     708             : 
     709             :         // Build request list
     710             :         std::vector<std::pair<std::vector<size_t>, std::vector<size_t>>>
     711        3948 :             anRequests;
     712        3948 :         anRequests.reserve(aBlocks.size());
     713       15790 :         for (const auto &info : aBlocks)
     714             :         {
     715       11842 :             anRequests.push_back({info.anStartIdx, info.anCount});
     716             :         }
     717             : 
     718        3948 :         std::vector<ZarrByteVectorQuickResize> aResults;
     719        3948 :         if (!m_poCodecs->BatchDecodePartial(fp.get(), anRequests, aResults))
     720         436 :             continue;
     721             : 
     722             :         // Store results in block cache
     723        3512 :         const bool bNeedDecode = NeedDecodedBuffer();
     724       13610 :         for (size_t i = 0; i < aBlocks.size(); ++i)
     725             :         {
     726       10098 :             if (aResults[i].empty())
     727             :             {
     728           0 :                 CachedBlock cachedBlock;
     729           0 :                 m_oChunkCache[aBlocks[i].anBlockIndices] =
     730           0 :                     std::move(cachedBlock);
     731           0 :                 continue;
     732             :             }
     733             : 
     734       20196 :             CachedBlock cachedBlock;
     735       10098 :             if (bNeedDecode)
     736             :             {
     737           0 :                 const size_t nSourceSize = m_aoDtypeElts.back().nativeOffset +
     738           0 :                                            m_aoDtypeElts.back().nativeSize;
     739           0 :                 const auto nGDALDTSize = m_oType.GetSize();
     740           0 :                 const size_t nValues = aResults[i].size() / nSourceSize;
     741           0 :                 ZarrByteVectorQuickResize abyDecoded;
     742           0 :                 abyDecoded.resize(nValues * nGDALDTSize);
     743           0 :                 const GByte *pSrc = aResults[i].data();
     744           0 :                 GByte *pDst = abyDecoded.data();
     745           0 :                 for (size_t v = 0; v < nValues;
     746           0 :                      v++, pSrc += nSourceSize, pDst += nGDALDTSize)
     747             :                 {
     748           0 :                     DecodeSourceElt(m_aoDtypeElts, pSrc, pDst);
     749             :                 }
     750           0 :                 std::swap(cachedBlock.abyDecoded, abyDecoded);
     751             :             }
     752             :             else
     753             :             {
     754       10098 :                 std::swap(cachedBlock.abyDecoded, aResults[i]);
     755             :             }
     756       10098 :             m_oChunkCache[aBlocks[i].anBlockIndices] = std::move(cachedBlock);
     757             :         }
     758             :     }
     759             : }
     760             : 
     761             : /************************************************************************/
     762             : /*                      ZarrV3Array::IAdviseRead()                      */
     763             : /************************************************************************/
     764             : 
     765           7 : bool ZarrV3Array::IAdviseRead(const GUInt64 *arrayStartIdx, const size_t *count,
     766             :                               CSLConstList papszOptions) const
     767             : {
     768          14 :     std::vector<uint64_t> anIndicesCur;
     769           7 :     int nThreadsMax = 0;
     770          14 :     std::vector<uint64_t> anReqBlocksIndices;
     771           7 :     size_t nReqBlocks = 0;
     772           7 :     if (!IAdviseReadCommon(arrayStartIdx, count, papszOptions, anIndicesCur,
     773             :                            nThreadsMax, anReqBlocksIndices, nReqBlocks))
     774             :     {
     775           2 :         return false;
     776             :     }
     777           5 :     if (nThreadsMax <= 1)
     778             :     {
     779           0 :         return true;
     780             :     }
     781             : 
     782             :     const int nThreads = static_cast<int>(
     783           5 :         std::min(static_cast<size_t>(nThreadsMax), nReqBlocks));
     784             : 
     785           5 :     CPLWorkerThreadPool *wtp = GDALGetGlobalThreadPool(nThreadsMax);
     786           5 :     if (wtp == nullptr)
     787           0 :         return false;
     788             : 
     789             :     struct JobStruct
     790             :     {
     791             :         JobStruct() = default;
     792             : 
     793             :         JobStruct(const JobStruct &) = delete;
     794             :         JobStruct &operator=(const JobStruct &) = delete;
     795             : 
     796             :         JobStruct(JobStruct &&) = default;
     797             :         JobStruct &operator=(JobStruct &&) = default;
     798             : 
     799             :         const ZarrV3Array *poArray = nullptr;
     800             :         bool *pbGlobalStatus = nullptr;
     801             :         int *pnRemainingThreads = nullptr;
     802             :         const std::vector<uint64_t> *panReqBlocksIndices = nullptr;
     803             :         size_t nFirstIdx = 0;
     804             :         size_t nLastIdxNotIncluded = 0;
     805             :     };
     806             : 
     807           5 :     std::vector<JobStruct> asJobStructs;
     808             : 
     809           5 :     bool bGlobalStatus = true;
     810           5 :     int nRemainingThreads = nThreads;
     811             :     // Check for very highly overflow in below loop
     812           5 :     assert(static_cast<size_t>(nThreads) <
     813             :            std::numeric_limits<size_t>::max() / nReqBlocks);
     814             : 
     815             :     // Setup jobs
     816          25 :     for (int i = 0; i < nThreads; i++)
     817             :     {
     818          20 :         JobStruct jobStruct;
     819          20 :         jobStruct.poArray = this;
     820          20 :         jobStruct.pbGlobalStatus = &bGlobalStatus;
     821          20 :         jobStruct.pnRemainingThreads = &nRemainingThreads;
     822          20 :         jobStruct.panReqBlocksIndices = &anReqBlocksIndices;
     823          20 :         jobStruct.nFirstIdx = static_cast<size_t>(i * nReqBlocks / nThreads);
     824          20 :         jobStruct.nLastIdxNotIncluded = std::min(
     825          20 :             static_cast<size_t>((i + 1) * nReqBlocks / nThreads), nReqBlocks);
     826          20 :         asJobStructs.emplace_back(std::move(jobStruct));
     827             :     }
     828             : 
     829          20 :     const auto JobFunc = [](void *pThreadData)
     830             :     {
     831          20 :         const JobStruct *jobStruct =
     832             :             static_cast<const JobStruct *>(pThreadData);
     833             : 
     834          20 :         const auto poArray = jobStruct->poArray;
     835          20 :         const size_t l_nDims = poArray->GetDimensionCount();
     836          20 :         ZarrByteVectorQuickResize abyRawBlockData;
     837          20 :         ZarrByteVectorQuickResize abyDecodedBlockData;
     838           0 :         std::unique_ptr<ZarrV3CodecSequence> poCodecs;
     839          20 :         if (poArray->m_poCodecs)
     840             :         {
     841          20 :             std::lock_guard<std::mutex> oLock(poArray->m_oMutex);
     842          20 :             poCodecs = poArray->m_poCodecs->Clone();
     843             :         }
     844             : 
     845       10717 :         for (size_t iReq = jobStruct->nFirstIdx;
     846       10717 :              iReq < jobStruct->nLastIdxNotIncluded; ++iReq)
     847             :         {
     848             :             // Check if we must early exit
     849             :             {
     850       10697 :                 std::lock_guard<std::mutex> oLock(poArray->m_oMutex);
     851       10697 :                 if (!(*jobStruct->pbGlobalStatus))
     852           0 :                     return;
     853             :             }
     854             : 
     855             :             const uint64_t *blockIndices =
     856       10697 :                 jobStruct->panReqBlocksIndices->data() + iReq * l_nDims;
     857             : 
     858       10697 :             if (!poArray->AllocateWorkingBuffers(abyRawBlockData,
     859             :                                                  abyDecodedBlockData))
     860             :             {
     861           0 :                 std::lock_guard<std::mutex> oLock(poArray->m_oMutex);
     862           0 :                 *jobStruct->pbGlobalStatus = false;
     863           0 :                 break;
     864             :             }
     865             : 
     866       10697 :             bool bIsEmpty = false;
     867       10697 :             bool success = poArray->LoadBlockData(
     868             :                 blockIndices,
     869             :                 true,  // use mutex
     870             :                 poCodecs.get(), abyRawBlockData, abyDecodedBlockData, bIsEmpty);
     871             : 
     872       10697 :             std::lock_guard<std::mutex> oLock(poArray->m_oMutex);
     873       10697 :             if (!success)
     874             :             {
     875           0 :                 *jobStruct->pbGlobalStatus = false;
     876           0 :                 break;
     877             :             }
     878             : 
     879       21394 :             CachedBlock cachedBlock;
     880       10697 :             if (!bIsEmpty)
     881             :             {
     882       10695 :                 if (!abyDecodedBlockData.empty())
     883           0 :                     std::swap(cachedBlock.abyDecoded, abyDecodedBlockData);
     884             :                 else
     885       10695 :                     std::swap(cachedBlock.abyDecoded, abyRawBlockData);
     886             :             }
     887             :             const std::vector<uint64_t> cacheKey{blockIndices,
     888       21394 :                                                  blockIndices + l_nDims};
     889       10697 :             poArray->m_oChunkCache[cacheKey] = std::move(cachedBlock);
     890             :         }
     891             : 
     892          20 :         std::lock_guard<std::mutex> oLock(poArray->m_oMutex);
     893          20 :         (*jobStruct->pnRemainingThreads)--;
     894             :     };
     895             : 
     896             :     // Start jobs
     897          25 :     for (int i = 0; i < nThreads; i++)
     898             :     {
     899          20 :         if (!wtp->SubmitJob(JobFunc, &asJobStructs[i]))
     900             :         {
     901           0 :             std::lock_guard<std::mutex> oLock(m_oMutex);
     902           0 :             bGlobalStatus = false;
     903           0 :             nRemainingThreads = i;
     904           0 :             break;
     905             :         }
     906             :     }
     907             : 
     908             :     // Wait for all jobs to be finished
     909             :     while (true)
     910             :     {
     911             :         {
     912          21 :             std::lock_guard<std::mutex> oLock(m_oMutex);
     913          21 :             if (nRemainingThreads == 0)
     914           5 :                 break;
     915             :         }
     916          16 :         wtp->WaitEvent();
     917          16 :     }
     918             : 
     919           5 :     return bGlobalStatus;
     920             : }
     921             : 
     922             : /************************************************************************/
     923             : /*                    ZarrV3Array::FlushDirtyBlock()                    */
     924             : /************************************************************************/
     925             : 
     926       17356 : bool ZarrV3Array::FlushDirtyBlock() const
     927             : {
     928       17356 :     if (!m_bDirtyBlock)
     929        6603 :         return true;
     930       10753 :     m_bDirtyBlock = false;
     931             : 
     932       21506 :     std::string osFilename = BuildChunkFilename(m_anCachedBlockIndices.data());
     933             : 
     934             :     const size_t nSourceSize =
     935       10753 :         m_aoDtypeElts.back().nativeOffset + m_aoDtypeElts.back().nativeSize;
     936       10753 :     const auto &abyBlock = m_abyDecodedBlockData.empty()
     937             :                                ? m_abyRawBlockData
     938       10753 :                                : m_abyDecodedBlockData;
     939             : 
     940       10753 :     if (IsEmptyBlock(abyBlock))
     941             :     {
     942           3 :         m_bCachedBlockEmpty = true;
     943             : 
     944             :         VSIStatBufL sStat;
     945           3 :         if (VSIStatL(osFilename.c_str(), &sStat) == 0)
     946             :         {
     947           0 :             CPLDebugOnly(ZARR_DEBUG_KEY,
     948             :                          "Deleting tile %s that has now empty content",
     949             :                          osFilename.c_str());
     950           0 :             return VSIUnlink(osFilename.c_str()) == 0;
     951             :         }
     952           3 :         return true;
     953             :     }
     954             : 
     955       10750 :     if (!m_abyDecodedBlockData.empty())
     956             :     {
     957           0 :         const size_t nDTSize = m_oType.GetSize();
     958           0 :         const size_t nValues = m_abyDecodedBlockData.size() / nDTSize;
     959           0 :         GByte *pDst = &m_abyRawBlockData[0];
     960           0 :         const GByte *pSrc = m_abyDecodedBlockData.data();
     961           0 :         for (size_t i = 0; i < nValues;
     962           0 :              i++, pDst += nSourceSize, pSrc += nDTSize)
     963             :         {
     964           0 :             EncodeElt(m_aoDtypeElts, pSrc, pDst);
     965             :         }
     966             :     }
     967             : 
     968       10750 :     const size_t nSizeBefore = m_abyRawBlockData.size();
     969       10750 :     if (m_poCodecs)
     970             :     {
     971       10750 :         if (!m_poCodecs->Encode(m_abyRawBlockData))
     972             :         {
     973           0 :             m_abyRawBlockData.resize(nSizeBefore);
     974           0 :             return false;
     975             :         }
     976             :     }
     977             : 
     978       10750 :     if (m_osDimSeparator == "/")
     979             :     {
     980       10750 :         std::string osDir = CPLGetDirnameSafe(osFilename.c_str());
     981             :         VSIStatBufL sStat;
     982       10750 :         if (VSIStatL(osDir.c_str(), &sStat) != 0)
     983             :         {
     984         212 :             if (VSIMkdirRecursive(osDir.c_str(), 0755) != 0)
     985             :             {
     986           0 :                 CPLError(CE_Failure, CPLE_AppDefined,
     987             :                          "Cannot create directory %s", osDir.c_str());
     988           0 :                 m_abyRawBlockData.resize(nSizeBefore);
     989           0 :                 return false;
     990             :             }
     991             :         }
     992             :     }
     993             : 
     994       10750 :     VSILFILE *fp = VSIFOpenL(osFilename.c_str(), "wb");
     995       10750 :     if (fp == nullptr)
     996             :     {
     997           0 :         CPLError(CE_Failure, CPLE_AppDefined, "Cannot create tile %s",
     998             :                  osFilename.c_str());
     999           0 :         m_abyRawBlockData.resize(nSizeBefore);
    1000           0 :         return false;
    1001             :     }
    1002             : 
    1003       10750 :     bool bRet = true;
    1004       10750 :     const size_t nRawDataSize = m_abyRawBlockData.size();
    1005       10750 :     if (VSIFWriteL(m_abyRawBlockData.data(), 1, nRawDataSize, fp) !=
    1006             :         nRawDataSize)
    1007             :     {
    1008           0 :         CPLError(CE_Failure, CPLE_AppDefined,
    1009             :                  "Could not write tile %s correctly", osFilename.c_str());
    1010           0 :         bRet = false;
    1011             :     }
    1012       10750 :     VSIFCloseL(fp);
    1013             : 
    1014       10750 :     m_abyRawBlockData.resize(nSizeBefore);
    1015             : 
    1016       10750 :     return bRet;
    1017             : }
    1018             : 
    1019             : /************************************************************************/
    1020             : /*                        ZarrV3Array::IWrite()                         */
    1021             : /************************************************************************/
    1022             : 
    1023          80 : bool ZarrV3Array::IWrite(const GUInt64 *arrayStartIdx, const size_t *count,
    1024             :                          const GInt64 *arrayStep,
    1025             :                          const GPtrDiff_t *bufferStride,
    1026             :                          const GDALExtendedDataType &bufferDataType,
    1027             :                          const void *pSrcBuffer)
    1028             : {
    1029          80 :     if (m_poCodecs && m_poCodecs->SupportsPartialDecoding())
    1030             :     {
    1031           0 :         CPLError(CE_Failure, CPLE_NotSupported,
    1032             :                  "Writing to sharded dataset is not supported");
    1033           0 :         return false;
    1034             :     }
    1035          80 :     return ZarrArray::IWrite(arrayStartIdx, count, arrayStep, bufferStride,
    1036          80 :                              bufferDataType, pSrcBuffer);
    1037             : }
    1038             : 
    1039             : /************************************************************************/
    1040             : /*                         BuildChunkFilename()                         */
    1041             : /************************************************************************/
    1042             : 
    1043       35486 : std::string ZarrV3Array::BuildChunkFilename(const uint64_t *blockIndices) const
    1044             : {
    1045       35486 :     if (m_aoDims.empty())
    1046             :     {
    1047             :         return CPLFormFilenameSafe(
    1048           0 :             CPLGetDirnameSafe(m_osFilename.c_str()).c_str(),
    1049           0 :             m_bV2ChunkKeyEncoding ? "0" : "c", nullptr);
    1050             :     }
    1051             :     else
    1052             :     {
    1053       70972 :         std::string osFilename(CPLGetDirnameSafe(m_osFilename.c_str()));
    1054       35486 :         osFilename += '/';
    1055       35486 :         if (!m_bV2ChunkKeyEncoding)
    1056             :         {
    1057       35477 :             osFilename += 'c';
    1058             :         }
    1059      106465 :         for (size_t i = 0; i < m_aoDims.size(); ++i)
    1060             :         {
    1061       70979 :             if (i > 0 || !m_bV2ChunkKeyEncoding)
    1062       70970 :                 osFilename += m_osDimSeparator;
    1063       70979 :             osFilename += std::to_string(blockIndices[i]);
    1064             :         }
    1065       35486 :         return osFilename;
    1066             :     }
    1067             : }
    1068             : 
    1069             : /************************************************************************/
    1070             : /*                          GetDataDirectory()                          */
    1071             : /************************************************************************/
    1072             : 
    1073           3 : std::string ZarrV3Array::GetDataDirectory() const
    1074             : {
    1075           3 :     return std::string(CPLGetDirnameSafe(m_osFilename.c_str()));
    1076             : }
    1077             : 
    1078             : /************************************************************************/
    1079             : /*                    GetChunkIndicesFromFilename()                     */
    1080             : /************************************************************************/
    1081             : 
    1082             : CPLStringList
    1083          15 : ZarrV3Array::GetChunkIndicesFromFilename(const char *pszFilename) const
    1084             : {
    1085          15 :     if (!m_bV2ChunkKeyEncoding)
    1086             :     {
    1087          15 :         if (pszFilename[0] != 'c')
    1088           4 :             return CPLStringList();
    1089          11 :         if (m_osDimSeparator == "/")
    1090             :         {
    1091          11 :             if (pszFilename[1] != '/' && pszFilename[1] != '\\')
    1092           0 :                 return CPLStringList();
    1093             :         }
    1094           0 :         else if (pszFilename[1] != m_osDimSeparator[0])
    1095             :         {
    1096           0 :             return CPLStringList();
    1097             :         }
    1098             :     }
    1099             :     return CPLStringList(
    1100          11 :         CSLTokenizeString2(pszFilename + (m_bV2ChunkKeyEncoding ? 0 : 2),
    1101          22 :                            m_osDimSeparator.c_str(), 0));
    1102             : }
    1103             : 
    1104             : /************************************************************************/
    1105             : /*                            ParseDtypeV3()                            */
    1106             : /************************************************************************/
    1107             : 
    1108        1083 : static GDALExtendedDataType ParseDtypeV3(const CPLJSONObject &obj,
    1109             :                                          std::vector<DtypeElt> &elts)
    1110             : {
    1111             :     do
    1112             :     {
    1113        1083 :         if (obj.GetType() == CPLJSONObject::Type::String)
    1114             :         {
    1115        2166 :             const auto str = obj.ToString();
    1116        1083 :             DtypeElt elt;
    1117        1083 :             GDALDataType eDT = GDT_Unknown;
    1118             : 
    1119        1083 :             if (str == "bool")  // boolean
    1120             :             {
    1121           0 :                 elt.nativeType = DtypeElt::NativeType::BOOLEAN;
    1122           0 :                 eDT = GDT_UInt8;
    1123             :             }
    1124        1083 :             else if (str == "int8")
    1125             :             {
    1126           6 :                 elt.nativeType = DtypeElt::NativeType::SIGNED_INT;
    1127           6 :                 eDT = GDT_Int8;
    1128             :             }
    1129        1077 :             else if (str == "uint8")
    1130             :             {
    1131         104 :                 elt.nativeType = DtypeElt::NativeType::UNSIGNED_INT;
    1132         104 :                 eDT = GDT_UInt8;
    1133             :             }
    1134         973 :             else if (str == "int16")
    1135             :             {
    1136          11 :                 elt.nativeType = DtypeElt::NativeType::SIGNED_INT;
    1137          11 :                 eDT = GDT_Int16;
    1138             :             }
    1139         962 :             else if (str == "uint16")
    1140             :             {
    1141           9 :                 elt.nativeType = DtypeElt::NativeType::UNSIGNED_INT;
    1142           9 :                 eDT = GDT_UInt16;
    1143             :             }
    1144         953 :             else if (str == "int32")
    1145             :             {
    1146           7 :                 elt.nativeType = DtypeElt::NativeType::SIGNED_INT;
    1147           7 :                 eDT = GDT_Int32;
    1148             :             }
    1149         946 :             else if (str == "uint32")
    1150             :             {
    1151           7 :                 elt.nativeType = DtypeElt::NativeType::UNSIGNED_INT;
    1152           7 :                 eDT = GDT_UInt32;
    1153             :             }
    1154         939 :             else if (str == "int64")
    1155             :             {
    1156         146 :                 elt.nativeType = DtypeElt::NativeType::SIGNED_INT;
    1157         146 :                 eDT = GDT_Int64;
    1158             :             }
    1159         793 :             else if (str == "uint64")
    1160             :             {
    1161           6 :                 elt.nativeType = DtypeElt::NativeType::UNSIGNED_INT;
    1162           6 :                 eDT = GDT_UInt64;
    1163             :             }
    1164         787 :             else if (str == "float16")
    1165             :             {
    1166           3 :                 elt.nativeType = DtypeElt::NativeType::IEEEFP;
    1167           3 :                 eDT = GDT_Float16;
    1168             :             }
    1169         784 :             else if (str == "float32")
    1170             :             {
    1171         734 :                 elt.nativeType = DtypeElt::NativeType::IEEEFP;
    1172         734 :                 eDT = GDT_Float32;
    1173             :             }
    1174          50 :             else if (str == "float64")
    1175             :             {
    1176          19 :                 elt.nativeType = DtypeElt::NativeType::IEEEFP;
    1177          19 :                 eDT = GDT_Float64;
    1178             :             }
    1179          31 :             else if (str == "complex64")
    1180             :             {
    1181          15 :                 elt.nativeType = DtypeElt::NativeType::COMPLEX_IEEEFP;
    1182          15 :                 eDT = GDT_CFloat32;
    1183             :             }
    1184          16 :             else if (str == "complex128")
    1185             :             {
    1186          15 :                 elt.nativeType = DtypeElt::NativeType::COMPLEX_IEEEFP;
    1187          15 :                 eDT = GDT_CFloat64;
    1188             :             }
    1189             :             else
    1190           1 :                 break;
    1191             : 
    1192        1082 :             elt.gdalType = GDALExtendedDataType::Create(eDT);
    1193        1082 :             elt.gdalSize = elt.gdalType.GetSize();
    1194        1082 :             if (!elt.gdalTypeIsApproxOfNative)
    1195        1082 :                 elt.nativeSize = elt.gdalSize;
    1196             : 
    1197        1082 :             if (elt.nativeSize > 1)
    1198             :             {
    1199         972 :                 elt.needByteSwapping = (CPL_IS_LSB == 0);
    1200             :             }
    1201             : 
    1202        1082 :             elts.emplace_back(elt);
    1203        1082 :             return GDALExtendedDataType::Create(eDT);
    1204             :         }
    1205             :     } while (false);
    1206           1 :     CPLError(CE_Failure, CPLE_AppDefined,
    1207             :              "Invalid or unsupported format for data_type: %s",
    1208           2 :              obj.ToString().c_str());
    1209           1 :     return GDALExtendedDataType::Create(GDT_Unknown);
    1210             : }
    1211             : 
    1212             : /************************************************************************/
    1213             : /*                     ParseNoDataStringAsDouble()                      */
    1214             : /************************************************************************/
    1215             : 
    1216         255 : static double ParseNoDataStringAsDouble(const std::string &osVal, bool &bOK)
    1217             : {
    1218         255 :     double dfNoDataValue = std::numeric_limits<double>::quiet_NaN();
    1219         255 :     if (osVal == "NaN")
    1220             :     {
    1221             :         // initialized above
    1222             :     }
    1223          15 :     else if (osVal == "Infinity" || osVal == "+Infinity")
    1224             :     {
    1225           5 :         dfNoDataValue = std::numeric_limits<double>::infinity();
    1226             :     }
    1227          10 :     else if (osVal == "-Infinity")
    1228             :     {
    1229           5 :         dfNoDataValue = -std::numeric_limits<double>::infinity();
    1230             :     }
    1231             :     else
    1232             :     {
    1233           5 :         bOK = false;
    1234             :     }
    1235         255 :     return dfNoDataValue;
    1236             : }
    1237             : 
    1238             : /************************************************************************/
    1239             : /*                        ParseNoDataComponent()                        */
    1240             : /************************************************************************/
    1241             : 
    1242             : template <typename T, typename Tint>
    1243          40 : static T ParseNoDataComponent(const CPLJSONObject &oObj, bool &bOK)
    1244             : {
    1245          40 :     if (oObj.GetType() == CPLJSONObject::Type::Integer ||
    1246          62 :         oObj.GetType() == CPLJSONObject::Type::Long ||
    1247          22 :         oObj.GetType() == CPLJSONObject::Type::Double)
    1248             :     {
    1249          22 :         return static_cast<T>(oObj.ToDouble());
    1250             :     }
    1251          18 :     else if (oObj.GetType() == CPLJSONObject::Type::String)
    1252             :     {
    1253          54 :         const auto osVal = oObj.ToString();
    1254          18 :         if (STARTS_WITH(osVal.c_str(), "0x"))
    1255             :         {
    1256           2 :             if (osVal.size() > 2 + 2 * sizeof(T))
    1257             :             {
    1258           0 :                 bOK = false;
    1259           0 :                 return 0;
    1260             :             }
    1261           2 :             Tint nVal = static_cast<Tint>(
    1262           2 :                 std::strtoull(osVal.c_str() + 2, nullptr, 16));
    1263             :             T fVal;
    1264             :             static_assert(sizeof(nVal) == sizeof(fVal),
    1265             :                           "sizeof(nVal) == sizeof(dfVal)");
    1266           2 :             memcpy(&fVal, &nVal, sizeof(nVal));
    1267           2 :             return fVal;
    1268             :         }
    1269             :         else
    1270             :         {
    1271          16 :             return static_cast<T>(ParseNoDataStringAsDouble(osVal, bOK));
    1272             :         }
    1273             :     }
    1274             :     else
    1275             :     {
    1276           0 :         bOK = false;
    1277           0 :         return 0;
    1278             :     }
    1279             : }
    1280             : 
    1281             : /************************************************************************/
    1282             : /*                       ZarrV3Group::LoadArray()                       */
    1283             : /************************************************************************/
    1284             : 
    1285             : std::shared_ptr<ZarrArray>
    1286        1096 : ZarrV3Group::LoadArray(const std::string &osArrayName,
    1287             :                        const std::string &osZarrayFilename,
    1288             :                        const CPLJSONObject &oRoot) const
    1289             : {
    1290             :     // Add osZarrayFilename to m_poSharedResource during the scope
    1291             :     // of this function call.
    1292        1096 :     ZarrSharedResource::SetFilenameAdder filenameAdder(m_poSharedResource,
    1293        2192 :                                                        osZarrayFilename);
    1294        1096 :     if (!filenameAdder.ok())
    1295           0 :         return nullptr;
    1296             : 
    1297             :     // Warn about unknown members (the spec suggests to error out, but let be
    1298             :     // a bit more lenient)
    1299       12035 :     for (const auto &oNode : oRoot.GetChildren())
    1300             :     {
    1301       21878 :         const auto osName = oNode.GetName();
    1302       29529 :         if (osName != "zarr_format" && osName != "node_type" &&
    1303       22956 :             osName != "shape" && osName != "chunk_grid" &&
    1304       16386 :             osName != "data_type" && osName != "chunk_key_encoding" &&
    1305        7638 :             osName != "fill_value" &&
    1306             :             // Below are optional
    1307        8090 :             osName != "dimension_names" && osName != "codecs" &&
    1308       22674 :             osName != "storage_transformers" && osName != "attributes")
    1309             :         {
    1310           4 :             CPLError(CE_Warning, CPLE_AppDefined,
    1311             :                      "%s array definition contains a unknown member (%s). "
    1312             :                      "Interpretation of the array might be wrong.",
    1313             :                      osZarrayFilename.c_str(), osName.c_str());
    1314             :         }
    1315             :     }
    1316             : 
    1317        3288 :     const auto oStorageTransformers = oRoot["storage_transformers"].ToArray();
    1318        1096 :     if (oStorageTransformers.Size() > 0)
    1319             :     {
    1320           1 :         CPLError(CE_Failure, CPLE_AppDefined,
    1321             :                  "storage_transformers are not supported.");
    1322           1 :         return nullptr;
    1323             :     }
    1324             : 
    1325        3285 :     const auto oShape = oRoot["shape"].ToArray();
    1326        1095 :     if (!oShape.IsValid())
    1327             :     {
    1328           2 :         CPLError(CE_Failure, CPLE_AppDefined, "shape missing or not an array");
    1329           2 :         return nullptr;
    1330             :     }
    1331             : 
    1332             :     // Parse chunk_grid
    1333        3279 :     const auto oChunkGrid = oRoot["chunk_grid"];
    1334        1093 :     if (oChunkGrid.GetType() != CPLJSONObject::Type::Object)
    1335             :     {
    1336           1 :         CPLError(CE_Failure, CPLE_AppDefined,
    1337             :                  "chunk_grid missing or not an object");
    1338           1 :         return nullptr;
    1339             :     }
    1340             : 
    1341        3276 :     const auto oChunkGridName = oChunkGrid["name"];
    1342        1092 :     if (oChunkGridName.ToString() != "regular")
    1343             :     {
    1344           1 :         CPLError(CE_Failure, CPLE_AppDefined,
    1345             :                  "Only chunk_grid.name = regular supported");
    1346           1 :         return nullptr;
    1347             :     }
    1348             : 
    1349        3273 :     const auto oChunks = oChunkGrid["configuration"]["chunk_shape"].ToArray();
    1350        1091 :     if (!oChunks.IsValid())
    1351             :     {
    1352           1 :         CPLError(
    1353             :             CE_Failure, CPLE_AppDefined,
    1354             :             "chunk_grid.configuration.chunk_shape missing or not an array");
    1355           1 :         return nullptr;
    1356             :     }
    1357             : 
    1358        1090 :     if (oShape.Size() != oChunks.Size())
    1359             :     {
    1360           1 :         CPLError(CE_Failure, CPLE_AppDefined,
    1361             :                  "shape and chunks arrays are of different size");
    1362           1 :         return nullptr;
    1363             :     }
    1364             : 
    1365             :     // Parse chunk_key_encoding
    1366        3267 :     const auto oChunkKeyEncoding = oRoot["chunk_key_encoding"];
    1367        1089 :     if (oChunkKeyEncoding.GetType() != CPLJSONObject::Type::Object)
    1368             :     {
    1369           1 :         CPLError(CE_Failure, CPLE_AppDefined,
    1370             :                  "chunk_key_encoding missing or not an object");
    1371           1 :         return nullptr;
    1372             :     }
    1373             : 
    1374        2176 :     std::string osDimSeparator;
    1375        1088 :     bool bV2ChunkKeyEncoding = false;
    1376        3264 :     const auto oChunkKeyEncodingName = oChunkKeyEncoding["name"];
    1377        1088 :     if (oChunkKeyEncodingName.ToString() == "default")
    1378             :     {
    1379        1075 :         osDimSeparator = "/";
    1380             :     }
    1381          13 :     else if (oChunkKeyEncodingName.ToString() == "v2")
    1382             :     {
    1383          12 :         osDimSeparator = ".";
    1384          12 :         bV2ChunkKeyEncoding = true;
    1385             :     }
    1386             :     else
    1387             :     {
    1388           1 :         CPLError(CE_Failure, CPLE_AppDefined,
    1389             :                  "Unsupported chunk_key_encoding.name");
    1390           1 :         return nullptr;
    1391             :     }
    1392             : 
    1393             :     {
    1394        2174 :         auto oConfiguration = oChunkKeyEncoding["configuration"];
    1395        1087 :         if (oConfiguration.GetType() == CPLJSONObject::Type::Object)
    1396             :         {
    1397        2064 :             auto oSeparator = oConfiguration["separator"];
    1398        1032 :             if (oSeparator.IsValid())
    1399             :             {
    1400        1032 :                 osDimSeparator = oSeparator.ToString();
    1401        1032 :                 if (osDimSeparator != "/" && osDimSeparator != ".")
    1402             :                 {
    1403           1 :                     CPLError(CE_Failure, CPLE_AppDefined,
    1404             :                              "Separator can only be '/' or '.'");
    1405           1 :                     return nullptr;
    1406             :                 }
    1407             :             }
    1408             :         }
    1409             :     }
    1410             : 
    1411        3258 :     CPLJSONObject oAttributes = oRoot["attributes"];
    1412             : 
    1413             :     // Deep-clone of oAttributes
    1414        1086 :     if (oAttributes.IsValid())
    1415             :     {
    1416        1018 :         oAttributes = oAttributes.Clone();
    1417             :     }
    1418             : 
    1419        2172 :     std::vector<std::shared_ptr<GDALDimension>> aoDims;
    1420        3078 :     for (int i = 0; i < oShape.Size(); ++i)
    1421             :     {
    1422        1992 :         const auto nSize = static_cast<GUInt64>(oShape[i].ToLong());
    1423        1992 :         if (nSize == 0)
    1424             :         {
    1425           0 :             CPLError(CE_Failure, CPLE_AppDefined, "Invalid content for shape");
    1426           0 :             return nullptr;
    1427             :         }
    1428        1992 :         aoDims.emplace_back(std::make_shared<ZarrDimension>(
    1429        1992 :             m_poSharedResource,
    1430        3984 :             std::dynamic_pointer_cast<ZarrGroupBase>(m_pSelf.lock()),
    1431        3984 :             std::string(), CPLSPrintf("dim%d", i), std::string(), std::string(),
    1432        1992 :             nSize));
    1433             :     }
    1434             : 
    1435             :     // Deal with dimension_names
    1436        3258 :     const auto dimensionNames = oRoot["dimension_names"];
    1437             : 
    1438         599 :     const auto FindDimension = [this, &aoDims, &osArrayName, &oAttributes](
    1439             :                                    const std::string &osDimName,
    1440        6241 :                                    std::shared_ptr<GDALDimension> &poDim, int i)
    1441             :     {
    1442         599 :         auto oIter = m_oMapDimensions.find(osDimName);
    1443         599 :         if (oIter != m_oMapDimensions.end())
    1444             :         {
    1445         296 :             if (m_bDimSizeInUpdate ||
    1446         148 :                 oIter->second->GetSize() == poDim->GetSize())
    1447             :             {
    1448         148 :                 poDim = oIter->second;
    1449         148 :                 return true;
    1450             :             }
    1451             :             else
    1452             :             {
    1453           0 :                 CPLError(CE_Warning, CPLE_AppDefined,
    1454             :                          "Size of _ARRAY_DIMENSIONS[%d] different "
    1455             :                          "from the one of shape",
    1456             :                          i);
    1457           0 :                 return false;
    1458             :             }
    1459             :         }
    1460             : 
    1461             :         // Try to load the indexing variable.
    1462             :         // Not in m_oMapMDArrays,
    1463             :         // then stat() the indexing variable.
    1464         826 :         else if (osArrayName != osDimName &&
    1465         826 :                  m_oMapMDArrays.find(osDimName) == m_oMapMDArrays.end())
    1466             :         {
    1467         750 :             std::string osDirName = m_osDirectoryName;
    1468             :             while (true)
    1469             :             {
    1470             :                 const std::string osArrayFilenameDim = CPLFormFilenameSafe(
    1471        1623 :                     CPLFormFilenameSafe(osDirName.c_str(), osDimName.c_str(),
    1472             :                                         nullptr)
    1473             :                         .c_str(),
    1474        1623 :                     "zarr.json", nullptr);
    1475             :                 VSIStatBufL sStat;
    1476        1623 :                 if (VSIStatL(osArrayFilenameDim.c_str(), &sStat) == 0)
    1477             :                 {
    1478           4 :                     CPLJSONDocument oDoc;
    1479           2 :                     if (oDoc.Load(osArrayFilenameDim))
    1480             :                     {
    1481           2 :                         LoadArray(osDimName, osArrayFilenameDim,
    1482           4 :                                   oDoc.GetRoot());
    1483             :                     }
    1484             :                 }
    1485             :                 else
    1486             :                 {
    1487             :                     // Recurse to upper level for datasets such as
    1488             :                     // /vsis3/hrrrzarr/sfc/20210809/20210809_00z_anl.zarr/0.1_sigma_level/HAIL_max_fcst/0.1_sigma_level/HAIL_max_fcst
    1489             :                     std::string osDirNameNew =
    1490        1621 :                         CPLGetPathSafe(osDirName.c_str());
    1491        1621 :                     if (!osDirNameNew.empty() && osDirNameNew != osDirName)
    1492             :                     {
    1493        1248 :                         osDirName = std::move(osDirNameNew);
    1494        1248 :                         continue;
    1495             :                     }
    1496             :                 }
    1497         375 :                 break;
    1498        1248 :             }
    1499             :         }
    1500             : 
    1501         451 :         oIter = m_oMapDimensions.find(osDimName);
    1502             :         // cppcheck-suppress knownConditionTrueFalse
    1503         453 :         if (oIter != m_oMapDimensions.end() &&
    1504           2 :             oIter->second->GetSize() == poDim->GetSize())
    1505             :         {
    1506           2 :             poDim = oIter->second;
    1507           2 :             return true;
    1508             :         }
    1509             : 
    1510         898 :         std::string osType;
    1511         898 :         std::string osDirection;
    1512         449 :         if (aoDims.size() == 1 && osArrayName == osDimName)
    1513             :         {
    1514          76 :             ZarrArray::GetDimensionTypeDirection(oAttributes, osType,
    1515             :                                                  osDirection);
    1516             :         }
    1517             : 
    1518             :         auto poDimLocal = std::make_shared<ZarrDimension>(
    1519         449 :             m_poSharedResource,
    1520         898 :             std::dynamic_pointer_cast<ZarrGroupBase>(m_pSelf.lock()),
    1521         898 :             GetFullName(), osDimName, osType, osDirection, poDim->GetSize());
    1522         449 :         poDimLocal->SetXArrayDimension();
    1523         449 :         m_oMapDimensions[osDimName] = poDimLocal;
    1524         449 :         poDim = poDimLocal;
    1525         449 :         return true;
    1526        1086 :     };
    1527             : 
    1528        1086 :     if (dimensionNames.GetType() == CPLJSONObject::Type::Array)
    1529             :     {
    1530         343 :         const auto arrayDims = dimensionNames.ToArray();
    1531         343 :         if (arrayDims.Size() == oShape.Size())
    1532             :         {
    1533         941 :             for (int i = 0; i < oShape.Size(); ++i)
    1534             :             {
    1535         599 :                 if (arrayDims[i].GetType() == CPLJSONObject::Type::String)
    1536             :                 {
    1537        1797 :                     const auto osDimName = arrayDims[i].ToString();
    1538         599 :                     FindDimension(osDimName, aoDims[i], i);
    1539             :                 }
    1540             :             }
    1541             :         }
    1542             :         else
    1543             :         {
    1544           1 :             CPLError(
    1545             :                 CE_Failure, CPLE_AppDefined,
    1546             :                 "Size of dimension_names[] different from the one of shape");
    1547           1 :             return nullptr;
    1548             :         }
    1549             :     }
    1550         743 :     else if (dimensionNames.IsValid())
    1551             :     {
    1552           1 :         CPLError(CE_Failure, CPLE_AppDefined,
    1553             :                  "dimension_names should be an array");
    1554           1 :         return nullptr;
    1555             :     }
    1556             : 
    1557        3252 :     auto oDtype = oRoot["data_type"];
    1558        1084 :     if (!oDtype.IsValid())
    1559             :     {
    1560           1 :         CPLError(CE_Failure, CPLE_NotSupported, "data_type missing");
    1561           1 :         return nullptr;
    1562             :     }
    1563        1083 :     if (oDtype["fallback"].IsValid())
    1564           1 :         oDtype = oDtype["fallback"];
    1565        2166 :     std::vector<DtypeElt> aoDtypeElts;
    1566        2166 :     const auto oType = ParseDtypeV3(oDtype, aoDtypeElts);
    1567        2166 :     if (oType.GetClass() == GEDTC_NUMERIC &&
    1568        1083 :         oType.GetNumericDataType() == GDT_Unknown)
    1569           1 :         return nullptr;
    1570             : 
    1571        2164 :     std::vector<GUInt64> anOuterBlockSize;
    1572        1082 :     if (!ZarrArray::ParseChunkSize(oChunks, oType, anOuterBlockSize))
    1573           1 :         return nullptr;
    1574             : 
    1575        2162 :     std::vector<GByte> abyNoData;
    1576             : 
    1577        3243 :     auto oFillValue = oRoot["fill_value"];
    1578        1081 :     auto eFillValueType = oFillValue.GetType();
    1579             : 
    1580        1081 :     if (!oFillValue.IsValid())
    1581             :     {
    1582           0 :         CPLError(CE_Warning, CPLE_AppDefined, "Missing fill_value is invalid");
    1583             :     }
    1584        1081 :     else if (eFillValueType == CPLJSONObject::Type::Null)
    1585             :     {
    1586         113 :         CPLError(CE_Warning, CPLE_AppDefined, "fill_value = null is invalid");
    1587             :     }
    1588         968 :     else if (GDALDataTypeIsComplex(oType.GetNumericDataType()) &&
    1589             :              eFillValueType != CPLJSONObject::Type::Array)
    1590             :     {
    1591           4 :         CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
    1592           4 :         return nullptr;
    1593             :     }
    1594         964 :     else if (eFillValueType == CPLJSONObject::Type::String)
    1595             :     {
    1596         490 :         const auto osFillValue = oFillValue.ToString();
    1597         245 :         if (STARTS_WITH(osFillValue.c_str(), "0x"))
    1598             :         {
    1599           3 :             if (osFillValue.size() > 2 + 2 * oType.GetSize())
    1600             :             {
    1601           0 :                 CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
    1602           1 :                 return nullptr;
    1603             :             }
    1604             :             uint64_t nVal = static_cast<uint64_t>(
    1605           3 :                 std::strtoull(osFillValue.c_str() + 2, nullptr, 16));
    1606           3 :             if (oType.GetSize() == 4)
    1607             :             {
    1608           1 :                 abyNoData.resize(oType.GetSize());
    1609           1 :                 uint32_t nTmp = static_cast<uint32_t>(nVal);
    1610           1 :                 memcpy(&abyNoData[0], &nTmp, sizeof(nTmp));
    1611             :             }
    1612           2 :             else if (oType.GetSize() == 8)
    1613             :             {
    1614           1 :                 abyNoData.resize(oType.GetSize());
    1615           1 :                 memcpy(&abyNoData[0], &nVal, sizeof(nVal));
    1616             :             }
    1617             :             else
    1618             :             {
    1619           1 :                 CPLError(CE_Failure, CPLE_AppDefined,
    1620             :                          "Hexadecimal representation of fill_value no "
    1621             :                          "supported for this data type");
    1622           1 :                 return nullptr;
    1623             :             }
    1624             :         }
    1625         242 :         else if (STARTS_WITH(osFillValue.c_str(), "0b"))
    1626             :         {
    1627           3 :             if (osFillValue.size() > 2 + 8 * oType.GetSize())
    1628             :             {
    1629           0 :                 CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
    1630           1 :                 return nullptr;
    1631             :             }
    1632             :             uint64_t nVal = static_cast<uint64_t>(
    1633           3 :                 std::strtoull(osFillValue.c_str() + 2, nullptr, 2));
    1634           3 :             if (oType.GetSize() == 4)
    1635             :             {
    1636           1 :                 abyNoData.resize(oType.GetSize());
    1637           1 :                 uint32_t nTmp = static_cast<uint32_t>(nVal);
    1638           1 :                 memcpy(&abyNoData[0], &nTmp, sizeof(nTmp));
    1639             :             }
    1640           2 :             else if (oType.GetSize() == 8)
    1641             :             {
    1642           1 :                 abyNoData.resize(oType.GetSize());
    1643           1 :                 memcpy(&abyNoData[0], &nVal, sizeof(nVal));
    1644             :             }
    1645             :             else
    1646             :             {
    1647           1 :                 CPLError(CE_Failure, CPLE_AppDefined,
    1648             :                          "Binary representation of fill_value no supported for "
    1649             :                          "this data type");
    1650           1 :                 return nullptr;
    1651             :             }
    1652             :         }
    1653             :         else
    1654             :         {
    1655         239 :             bool bOK = true;
    1656         239 :             double dfNoDataValue = ParseNoDataStringAsDouble(osFillValue, bOK);
    1657         239 :             if (!bOK)
    1658             :             {
    1659           1 :                 CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
    1660           2 :                 return nullptr;
    1661             :             }
    1662         238 :             else if (oType.GetNumericDataType() == GDT_Float16)
    1663             :             {
    1664             :                 const GFloat16 hfNoDataValue =
    1665           1 :                     static_cast<GFloat16>(dfNoDataValue);
    1666           1 :                 abyNoData.resize(sizeof(hfNoDataValue));
    1667           1 :                 memcpy(&abyNoData[0], &hfNoDataValue, sizeof(hfNoDataValue));
    1668             :             }
    1669         237 :             else if (oType.GetNumericDataType() == GDT_Float32)
    1670             :             {
    1671         220 :                 const float fNoDataValue = static_cast<float>(dfNoDataValue);
    1672         220 :                 abyNoData.resize(sizeof(fNoDataValue));
    1673         220 :                 memcpy(&abyNoData[0], &fNoDataValue, sizeof(fNoDataValue));
    1674             :             }
    1675          17 :             else if (oType.GetNumericDataType() == GDT_Float64)
    1676             :             {
    1677          16 :                 abyNoData.resize(sizeof(dfNoDataValue));
    1678          16 :                 memcpy(&abyNoData[0], &dfNoDataValue, sizeof(dfNoDataValue));
    1679             :             }
    1680             :             else
    1681             :             {
    1682           1 :                 CPLError(CE_Failure, CPLE_AppDefined,
    1683             :                          "Invalid fill_value for this data type");
    1684           1 :                 return nullptr;
    1685             :             }
    1686             :         }
    1687             :     }
    1688         719 :     else if (eFillValueType == CPLJSONObject::Type::Boolean ||
    1689         521 :              eFillValueType == CPLJSONObject::Type::Integer ||
    1690         521 :              eFillValueType == CPLJSONObject::Type::Long ||
    1691             :              eFillValueType == CPLJSONObject::Type::Double)
    1692             :     {
    1693         695 :         const double dfNoDataValue = oFillValue.ToDouble();
    1694         695 :         if (oType.GetNumericDataType() == GDT_Int64)
    1695             :         {
    1696             :             const int64_t nNoDataValue =
    1697         140 :                 static_cast<int64_t>(oFillValue.ToLong());
    1698         140 :             abyNoData.resize(oType.GetSize());
    1699         140 :             GDALCopyWords(&nNoDataValue, GDT_Int64, 0, &abyNoData[0],
    1700             :                           oType.GetNumericDataType(), 0, 1);
    1701             :         }
    1702         555 :         else if (oType.GetNumericDataType() == GDT_UInt64 &&
    1703             :                  /* we can't really deal with nodata value between */
    1704             :                  /* int64::max and uint64::max due to json-c limitations */
    1705           0 :                  dfNoDataValue >= 0)
    1706             :         {
    1707             :             const int64_t nNoDataValue =
    1708           0 :                 static_cast<int64_t>(oFillValue.ToLong());
    1709           0 :             abyNoData.resize(oType.GetSize());
    1710           0 :             GDALCopyWords(&nNoDataValue, GDT_Int64, 0, &abyNoData[0],
    1711             :                           oType.GetNumericDataType(), 0, 1);
    1712             :         }
    1713             :         else
    1714             :         {
    1715         555 :             abyNoData.resize(oType.GetSize());
    1716         555 :             GDALCopyWords(&dfNoDataValue, GDT_Float64, 0, &abyNoData[0],
    1717             :                           oType.GetNumericDataType(), 0, 1);
    1718         695 :         }
    1719             :     }
    1720          24 :     else if (eFillValueType == CPLJSONObject::Type::Array)
    1721             :     {
    1722          24 :         const auto oFillValueArray = oFillValue.ToArray();
    1723          44 :         if (oFillValueArray.Size() == 2 &&
    1724          20 :             GDALDataTypeIsComplex(oType.GetNumericDataType()))
    1725             :         {
    1726          20 :             if (oType.GetNumericDataType() == GDT_CFloat64)
    1727             :             {
    1728          10 :                 bool bOK = true;
    1729             :                 const double adfNoDataValue[2] = {
    1730          10 :                     ParseNoDataComponent<double, uint64_t>(oFillValueArray[0],
    1731             :                                                            bOK),
    1732          10 :                     ParseNoDataComponent<double, uint64_t>(oFillValueArray[1],
    1733             :                                                            bOK),
    1734          20 :                 };
    1735          10 :                 if (!bOK)
    1736             :                 {
    1737           2 :                     CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
    1738           2 :                     return nullptr;
    1739             :                 }
    1740           8 :                 abyNoData.resize(oType.GetSize());
    1741           8 :                 CPLAssert(sizeof(adfNoDataValue) == oType.GetSize());
    1742           8 :                 memcpy(abyNoData.data(), adfNoDataValue,
    1743             :                        sizeof(adfNoDataValue));
    1744             :             }
    1745             :             else
    1746             :             {
    1747          10 :                 CPLAssert(oType.GetNumericDataType() == GDT_CFloat32);
    1748          10 :                 bool bOK = true;
    1749             :                 const float afNoDataValue[2] = {
    1750          10 :                     ParseNoDataComponent<float, uint32_t>(oFillValueArray[0],
    1751             :                                                           bOK),
    1752          10 :                     ParseNoDataComponent<float, uint32_t>(oFillValueArray[1],
    1753             :                                                           bOK),
    1754          20 :                 };
    1755          10 :                 if (!bOK)
    1756             :                 {
    1757           2 :                     CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
    1758           2 :                     return nullptr;
    1759             :                 }
    1760           8 :                 abyNoData.resize(oType.GetSize());
    1761           8 :                 CPLAssert(sizeof(afNoDataValue) == oType.GetSize());
    1762           8 :                 memcpy(abyNoData.data(), afNoDataValue, sizeof(afNoDataValue));
    1763             :             }
    1764             :         }
    1765             :         else
    1766             :         {
    1767           4 :             CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
    1768           4 :             return nullptr;
    1769             :         }
    1770             :     }
    1771             :     else
    1772             :     {
    1773           0 :         CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
    1774           0 :         return nullptr;
    1775             :     }
    1776             : 
    1777        3195 :     const auto oCodecs = oRoot["codecs"].ToArray();
    1778        1065 :     std::unique_ptr<ZarrV3CodecSequence> poCodecs;
    1779        2130 :     std::vector<GUInt64> anInnerBlockSize = anOuterBlockSize;
    1780        1065 :     if (oCodecs.Size() > 0)
    1781             :     {
    1782        2070 :         poCodecs = ZarrV3Array::SetupCodecs(oCodecs, anOuterBlockSize,
    1783             :                                             anInnerBlockSize,
    1784        2070 :                                             aoDtypeElts.back(), abyNoData);
    1785        1035 :         if (!poCodecs)
    1786             :         {
    1787          18 :             return nullptr;
    1788             :         }
    1789             :     }
    1790             : 
    1791        1047 :     auto poArray = ZarrV3Array::Create(m_poSharedResource, Self(), osArrayName,
    1792             :                                        aoDims, oType, aoDtypeElts,
    1793        2094 :                                        anOuterBlockSize, anInnerBlockSize);
    1794        1047 :     if (!poArray)
    1795           1 :         return nullptr;
    1796        1046 :     poArray->SetUpdatable(m_bUpdatable);  // must be set before SetAttributes()
    1797        1046 :     poArray->SetFilename(osZarrayFilename);
    1798        1046 :     poArray->SetIsV2ChunkKeyEncoding(bV2ChunkKeyEncoding);
    1799        1046 :     poArray->SetDimSeparator(osDimSeparator);
    1800        1046 :     if (!abyNoData.empty())
    1801             :     {
    1802         933 :         poArray->RegisterNoDataValue(abyNoData.data());
    1803             :     }
    1804        1046 :     poArray->SetAttributes(Self(), oAttributes);
    1805        1046 :     poArray->SetDtype(oDtype);
    1806        2063 :     if (oCodecs.Size() > 0 &&
    1807        2063 :         oCodecs[oCodecs.Size() - 1].GetString("name") != "bytes")
    1808             :     {
    1809        1332 :         poArray->SetStructuralInfo(
    1810        1332 :             "COMPRESSOR", oCodecs[oCodecs.Size() - 1].ToString().c_str());
    1811             :     }
    1812        1046 :     if (poCodecs)
    1813        1017 :         poArray->SetCodecs(oCodecs, std::move(poCodecs));
    1814        1046 :     RegisterArray(poArray);
    1815             : 
    1816             :     // If this is an indexing variable, attach it to the dimension.
    1817        1046 :     if (aoDims.size() == 1 && aoDims[0]->GetName() == poArray->GetName())
    1818             :     {
    1819          76 :         auto oIter = m_oMapDimensions.find(poArray->GetName());
    1820          76 :         if (oIter != m_oMapDimensions.end())
    1821             :         {
    1822          76 :             oIter->second->SetIndexingVariable(poArray);
    1823             :         }
    1824             :     }
    1825             : 
    1826        1046 :     if (CPLTestBool(m_poSharedResource->GetOpenOptions().FetchNameValueDef(
    1827             :             "CACHE_TILE_PRESENCE", "NO")))
    1828             :     {
    1829           3 :         poArray->BlockCachePresence();
    1830             :     }
    1831             : 
    1832        1046 :     return poArray;
    1833             : }
    1834             : 
    1835             : /************************************************************************/
    1836             : /*                  ZarrV3Array::GetRawBlockInfoInfo()                  */
    1837             : /************************************************************************/
    1838             : 
    1839           6 : CPLStringList ZarrV3Array::GetRawBlockInfoInfo() const
    1840             : {
    1841           6 :     CPLStringList aosInfo(m_aosStructuralInfo);
    1842           6 :     if (m_oType.GetSize() > 1)
    1843             :     {
    1844             :         // By default, assume that the ENDIANNESS is the native one.
    1845             :         // Otherwise there will be a ZarrV3CodecBytes instance.
    1846             :         if constexpr (CPL_IS_LSB)
    1847           5 :             aosInfo.SetNameValue("ENDIANNESS", "LITTLE");
    1848             :         else
    1849             :             aosInfo.SetNameValue("ENDIANNESS", "BIG");
    1850             :     }
    1851             : 
    1852           6 :     if (m_poCodecs)
    1853             :     {
    1854           6 :         bool bHasOtherCodec = false;
    1855          11 :         for (const auto &poCodec : m_poCodecs->GetCodecs())
    1856             :         {
    1857           6 :             if (poCodec->GetName() == ZarrV3CodecBytes::NAME &&
    1858           1 :                 m_oType.GetSize() > 1)
    1859             :             {
    1860             :                 auto poBytesCodec =
    1861           1 :                     dynamic_cast<const ZarrV3CodecBytes *>(poCodec.get());
    1862           1 :                 if (poBytesCodec)
    1863             :                 {
    1864           1 :                     if (poBytesCodec->IsLittle())
    1865           0 :                         aosInfo.SetNameValue("ENDIANNESS", "LITTLE");
    1866             :                     else
    1867           1 :                         aosInfo.SetNameValue("ENDIANNESS", "BIG");
    1868             :                 }
    1869             :             }
    1870           5 :             else if (poCodec->GetName() == ZarrV3CodecTranspose::NAME &&
    1871           1 :                      m_aoDims.size() > 1)
    1872             :             {
    1873             :                 auto poTransposeCodec =
    1874           1 :                     dynamic_cast<const ZarrV3CodecTranspose *>(poCodec.get());
    1875           1 :                 if (poTransposeCodec && !poTransposeCodec->IsNoOp())
    1876             :                 {
    1877           1 :                     const auto &anOrder = poTransposeCodec->GetOrder();
    1878           1 :                     const int nDims = static_cast<int>(anOrder.size());
    1879           2 :                     std::string osOrder("[");
    1880           3 :                     for (int i = 0; i < nDims; ++i)
    1881             :                     {
    1882           2 :                         if (i > 0)
    1883           1 :                             osOrder += ',';
    1884           2 :                         osOrder += std::to_string(anOrder[i]);
    1885             :                     }
    1886           1 :                     osOrder += ']';
    1887           1 :                     aosInfo.SetNameValue("TRANSPOSE_ORDER", osOrder.c_str());
    1888             :                 }
    1889             :             }
    1890           5 :             else if (poCodec->GetName() != ZarrV3CodecGZip::NAME &&
    1891           5 :                      poCodec->GetName() != ZarrV3CodecBlosc::NAME &&
    1892           2 :                      poCodec->GetName() != ZarrV3CodecZstd::NAME)
    1893             :             {
    1894           2 :                 bHasOtherCodec = true;
    1895             :             }
    1896             :         }
    1897           6 :         if (bHasOtherCodec)
    1898             :         {
    1899           2 :             aosInfo.SetNameValue("CODECS", m_oJSONCodecs.ToString().c_str());
    1900             :         }
    1901             : 
    1902           6 :         if (m_poCodecs->SupportsPartialDecoding())
    1903             :         {
    1904           2 :             aosInfo.SetNameValue("CHUNK_TYPE", "INNER");
    1905             :         }
    1906             :     }
    1907             : 
    1908           6 :     return aosInfo;
    1909             : }
    1910             : 
    1911             : /************************************************************************/
    1912             : /*                      ZarrV3Array::SetupCodecs()                      */
    1913             : /************************************************************************/
    1914             : 
    1915        1164 : /* static */ std::unique_ptr<ZarrV3CodecSequence> ZarrV3Array::SetupCodecs(
    1916             :     const CPLJSONArray &oCodecs, const std::vector<GUInt64> &anOuterBlockSize,
    1917             :     std::vector<GUInt64> &anInnerBlockSize, DtypeElt &zarrDataType,
    1918             :     const std::vector<GByte> &abyNoData)
    1919             : 
    1920             : {
    1921             :     // Byte swapping will be done by the codec chain
    1922        1164 :     zarrDataType.needByteSwapping = false;
    1923             : 
    1924        2328 :     ZarrArrayMetadata oInputArrayMetadata;
    1925        1164 :     if (!abyNoData.empty() && zarrDataType.gdalTypeIsApproxOfNative)
    1926             :     {
    1927             :         // This cannot happen today with the data types we support, but
    1928             :         // might in the future. In which case we'll have to translate the
    1929             :         // nodata value from its GDAL representation to the native one
    1930             :         // (since that's what zarr_v3_codec_sharding::FillWithNoData()
    1931             :         // expects
    1932           0 :         CPLError(CE_Warning, CPLE_AppDefined,
    1933             :                  "Zarr driver issue: gdalTypeIsApproxOfNative is not taken "
    1934             :                  "into account by codecs. Nodata will be assumed to be zero by "
    1935             :                  "sharding codec");
    1936             :     }
    1937             :     else
    1938             :     {
    1939        1164 :         oInputArrayMetadata.abyNoData = abyNoData;
    1940             :     }
    1941        3313 :     for (auto &nSize : anOuterBlockSize)
    1942             :     {
    1943        2149 :         oInputArrayMetadata.anBlockSizes.push_back(static_cast<size_t>(nSize));
    1944             :     }
    1945        1164 :     oInputArrayMetadata.oElt = zarrDataType;
    1946        2328 :     auto poCodecs = std::make_unique<ZarrV3CodecSequence>(oInputArrayMetadata);
    1947        2328 :     ZarrArrayMetadata oOutputArrayMetadata;
    1948        1164 :     if (!poCodecs->InitFromJson(oCodecs, oOutputArrayMetadata))
    1949             :     {
    1950          18 :         return nullptr;
    1951             :     }
    1952        2292 :     std::vector<size_t> anOuterBlockSizeSizet;
    1953        3259 :     for (auto nVal : oOutputArrayMetadata.anBlockSizes)
    1954             :     {
    1955        2113 :         anOuterBlockSizeSizet.push_back(static_cast<size_t>(nVal));
    1956             :     }
    1957        1146 :     anInnerBlockSize.clear();
    1958        3259 :     for (size_t nVal : poCodecs->GetInnerMostBlockSize(anOuterBlockSizeSizet))
    1959             :     {
    1960        2113 :         anInnerBlockSize.push_back(nVal);
    1961             :     }
    1962        1146 :     return poCodecs;
    1963             : }
    1964             : 
    1965             : /************************************************************************/
    1966             : /*                       ZarrV3Array::SetCodecs()                       */
    1967             : /************************************************************************/
    1968             : 
    1969        1146 : void ZarrV3Array::SetCodecs(const CPLJSONArray &oJSONCodecs,
    1970             :                             std::unique_ptr<ZarrV3CodecSequence> &&poCodecs)
    1971             : {
    1972        1146 :     m_oJSONCodecs = oJSONCodecs;
    1973        1146 :     m_poCodecs = std::move(poCodecs);
    1974        1146 : }
    1975             : 
    1976             : /************************************************************************/
    1977             : /*                     ZarrV3Array::LoadOverviews()                     */
    1978             : /************************************************************************/
    1979             : 
    1980          78 : void ZarrV3Array::LoadOverviews() const
    1981             : {
    1982          78 :     if (m_bOverviewsLoaded)
    1983          36 :         return;
    1984          44 :     m_bOverviewsLoaded = true;
    1985             : 
    1986             :     // Cf https://github.com/zarr-conventions/multiscales
    1987             :     // and https://github.com/zarr-conventions/spatial
    1988             : 
    1989          44 :     const auto poRG = GetRootGroup();
    1990          44 :     if (!poRG)
    1991             :     {
    1992           1 :         CPLError(CE_Warning, CPLE_AppDefined,
    1993             :                  "LoadOverviews(): cannot access root group");
    1994           1 :         return;
    1995             :     }
    1996             : 
    1997          43 :     auto poGroup = GetParentGroup();
    1998          43 :     if (!poGroup)
    1999             :     {
    2000           0 :         CPLDebugOnly(ZARR_DEBUG_KEY,
    2001             :                      "LoadOverviews(): cannot access parent group");
    2002           0 :         return;
    2003             :     }
    2004             : 
    2005             :     // Look for "zarr_conventions" and "multiscales" attributes in our
    2006             :     // immediate parent, or in our grandparent if not found.
    2007          86 :     auto poAttrZarrConventions = poGroup->GetAttribute("zarr_conventions");
    2008          86 :     auto poAttrMultiscales = poGroup->GetAttribute("multiscales");
    2009          43 :     if (!poAttrZarrConventions || !poAttrMultiscales)
    2010             :     {
    2011          43 :         poGroup = poGroup->GetParentGroup();
    2012          43 :         if (poGroup)
    2013             :         {
    2014          43 :             poAttrZarrConventions = poGroup->GetAttribute("zarr_conventions");
    2015          43 :             poAttrMultiscales = poGroup->GetAttribute("multiscales");
    2016             :         }
    2017          43 :         if (!poAttrZarrConventions || !poAttrMultiscales)
    2018             :         {
    2019           0 :             return;
    2020             :         }
    2021             :     }
    2022             : 
    2023          43 :     const char *pszZarrConventions = poAttrZarrConventions->ReadAsString();
    2024          43 :     const char *pszMultiscales = poAttrMultiscales->ReadAsString();
    2025          43 :     if (!pszZarrConventions || !pszMultiscales)
    2026           0 :         return;
    2027             : 
    2028          43 :     CPLJSONDocument oDoc;
    2029          43 :     if (!oDoc.LoadMemory(pszZarrConventions))
    2030           0 :         return;
    2031          43 :     const auto oZarrConventions = oDoc.GetRoot();
    2032             : 
    2033          43 :     if (!oDoc.LoadMemory(pszMultiscales))
    2034           0 :         return;
    2035          43 :     const auto oMultiscales = oDoc.GetRoot();
    2036             : 
    2037          43 :     if (!oZarrConventions.IsValid() ||
    2038          43 :         oZarrConventions.GetType() != CPLJSONObject::Type::Array ||
    2039         129 :         !oMultiscales.IsValid() ||
    2040          43 :         oMultiscales.GetType() != CPLJSONObject::Type::Object)
    2041             :     {
    2042           0 :         return;
    2043             :     }
    2044             : 
    2045          43 :     const auto oZarrConventionsArray = oZarrConventions.ToArray();
    2046          43 :     const auto hasMultiscalesUUIDLambda = [](const CPLJSONObject &obj)
    2047             :     {
    2048          43 :         constexpr const char *MULTISCALES_UUID =
    2049             :             "d35379db-88df-4056-af3a-620245f8e347";
    2050          43 :         return obj.GetString("uuid") == MULTISCALES_UUID;
    2051             :     };
    2052             :     const bool bFoundMultiScalesUUID =
    2053          86 :         std::find_if(oZarrConventionsArray.begin(), oZarrConventionsArray.end(),
    2054         129 :                      hasMultiscalesUUIDLambda) != oZarrConventionsArray.end();
    2055          43 :     if (!bFoundMultiScalesUUID)
    2056           0 :         return;
    2057             : 
    2058          57 :     const auto hasSpatialUUIDLambda = [](const CPLJSONObject &obj)
    2059             :     {
    2060          57 :         constexpr const char *SPATIAL_UUID =
    2061             :             "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4";
    2062          57 :         return obj.GetString("uuid") == SPATIAL_UUID;
    2063             :     };
    2064             :     const bool bFoundSpatialUUID =
    2065          86 :         std::find_if(oZarrConventionsArray.begin(), oZarrConventionsArray.end(),
    2066         129 :                      hasSpatialUUIDLambda) != oZarrConventionsArray.end();
    2067             : 
    2068          86 :     const auto oLayout = oMultiscales["layout"];
    2069          43 :     if (!oLayout.IsValid() && oLayout.GetType() != CPLJSONObject::Type::Array)
    2070             :     {
    2071           1 :         CPLError(CE_Warning, CPLE_AppDefined,
    2072             :                  "layout not found in multiscales");
    2073           1 :         return;
    2074             :     }
    2075             : 
    2076             :     // is pixel-is-area ?
    2077         126 :     auto poSpatialRegistration = poGroup->GetAttribute("spatial:registration");
    2078             :     const char *pszSpatialRegistration =
    2079          42 :         poSpatialRegistration ? poSpatialRegistration->ReadAsString() : nullptr;
    2080          42 :     const bool bHasExplicitPixelSpatialRegistration =
    2081          56 :         bFoundSpatialUUID && pszSpatialRegistration &&
    2082          14 :         strcmp(pszSpatialRegistration, "pixel") == 0;
    2083             : 
    2084          84 :     std::vector<std::string> aosSpatialDimensions;
    2085          84 :     std::set<std::string> oSetSpatialDimensionNames;
    2086         126 :     auto poSpatialDimensions = poGroup->GetAttribute("spatial:dimensions");
    2087          42 :     if (bFoundSpatialUUID && poSpatialDimensions)
    2088             :     {
    2089          14 :         aosSpatialDimensions = poSpatialDimensions->ReadAsStringArray();
    2090          42 :         for (const auto &osDimName : aosSpatialDimensions)
    2091             :         {
    2092          28 :             oSetSpatialDimensionNames.insert(osDimName);
    2093             :         }
    2094             :     }
    2095             : 
    2096             :     // Multiscales convention: asset/derived_from paths are relative to
    2097             :     // the group holding the convention metadata, not the store root.
    2098          42 :     const std::string osGroupPrefix = poGroup->GetFullName().back() == '/'
    2099          40 :                                           ? poGroup->GetFullName()
    2100         124 :                                           : poGroup->GetFullName() + '/';
    2101             :     const auto resolveAssetPath =
    2102         170 :         [&osGroupPrefix](const std::string &osRelative) -> std::string
    2103         170 :     { return osGroupPrefix + osRelative; };
    2104             : 
    2105         164 :     for (const auto &oLayoutItem : oLayout.ToArray())
    2106             :     {
    2107         244 :         const std::string osAsset = oLayoutItem.GetString("asset");
    2108         122 :         if (osAsset.empty())
    2109             :         {
    2110           1 :             CPLError(CE_Warning, CPLE_AppDefined,
    2111             :                      "multiscales.layout[].asset not found");
    2112           1 :             continue;
    2113             :         }
    2114             : 
    2115             :         // Resolve "asset" to a MDArray
    2116           0 :         std::shared_ptr<GDALGroup> poAssetGroup;
    2117             :         {
    2118         121 :             CPLErrorStateBackuper oBackuper(CPLQuietErrorHandler);
    2119             :             poAssetGroup =
    2120         121 :                 poRG->OpenGroupFromFullname(resolveAssetPath(osAsset));
    2121             :         }
    2122           0 :         std::shared_ptr<GDALMDArray> poAssetArray;
    2123         121 :         if (poAssetGroup)
    2124             :         {
    2125          99 :             poAssetArray = poAssetGroup->OpenMDArray(GetName());
    2126             :         }
    2127          22 :         else if (osAsset.find('/') == std::string::npos)
    2128             :         {
    2129           1 :             poAssetArray = poGroup->OpenMDArray(osAsset);
    2130           1 :             if (!poAssetArray)
    2131             :             {
    2132           1 :                 CPLError(CE_Warning, CPLE_AppDefined,
    2133             :                          "multiscales.layout[].asset=%s ignored, because it is "
    2134             :                          "not a valid group or array name",
    2135             :                          osAsset.c_str());
    2136           1 :                 continue;
    2137             :             }
    2138             :         }
    2139             :         else
    2140             :         {
    2141             :             poAssetArray =
    2142          21 :                 poRG->OpenMDArrayFromFullname(resolveAssetPath(osAsset));
    2143          21 :             if (poAssetArray && poAssetArray->GetName() != GetName())
    2144             :             {
    2145           6 :                 continue;
    2146             :             }
    2147             :         }
    2148         114 :         if (!poAssetArray)
    2149             :         {
    2150          10 :             continue;
    2151             :         }
    2152         104 :         if (poAssetArray->GetDimensionCount() != GetDimensionCount())
    2153             :         {
    2154           3 :             CPLError(
    2155             :                 CE_Warning, CPLE_AppDefined,
    2156             :                 "multiscales.layout[].asset=%s (%s) ignored, because it  has "
    2157             :                 "not the same dimension count as %s (%s)",
    2158           1 :                 osAsset.c_str(), poAssetArray->GetFullName().c_str(),
    2159           2 :                 GetName().c_str(), GetFullName().c_str());
    2160           1 :             continue;
    2161             :         }
    2162         103 :         if (poAssetArray->GetDataType() != GetDataType())
    2163             :         {
    2164           3 :             CPLError(
    2165             :                 CE_Warning, CPLE_AppDefined,
    2166             :                 "multiscales.layout[].asset=%s (%s) ignored, because it has "
    2167             :                 "not the same data type as %s (%s)",
    2168           1 :                 osAsset.c_str(), poAssetArray->GetFullName().c_str(),
    2169           2 :                 GetName().c_str(), GetFullName().c_str());
    2170           1 :             continue;
    2171             :         }
    2172             : 
    2173         102 :         bool bAssetIsDownsampledOfThis = false;
    2174         199 :         for (size_t iDim = 0; iDim < GetDimensionCount(); ++iDim)
    2175             :         {
    2176         296 :             if (poAssetArray->GetDimensions()[iDim]->GetSize() <
    2177         148 :                 GetDimensions()[iDim]->GetSize())
    2178             :             {
    2179          51 :                 bAssetIsDownsampledOfThis = true;
    2180          51 :                 break;
    2181             :             }
    2182             :         }
    2183         102 :         if (!bAssetIsDownsampledOfThis)
    2184             :         {
    2185             :             // not an error
    2186          51 :             continue;
    2187             :         }
    2188             : 
    2189             :         // Inspect dimensions of the asset
    2190          51 :         std::map<std::string, size_t> oMapAssetDimNameToIdx;
    2191          51 :         const auto &apoAssetDims = poAssetArray->GetDimensions();
    2192          51 :         size_t nCountSpatialDimsFoundInAsset = 0;
    2193         150 :         for (const auto &[idx, poDim] : cpl::enumerate(apoAssetDims))
    2194             :         {
    2195          99 :             oMapAssetDimNameToIdx[poDim->GetName()] = idx;
    2196          99 :             if (cpl::contains(oSetSpatialDimensionNames, poDim->GetName()))
    2197          43 :                 ++nCountSpatialDimsFoundInAsset;
    2198             :         }
    2199             :         const bool bAssetHasAllSpatialDims =
    2200          51 :             (nCountSpatialDimsFoundInAsset == aosSpatialDimensions.size());
    2201             : 
    2202             :         // Consistency checks on "derived_from" and "transform"
    2203         102 :         const auto oDerivedFrom = oLayoutItem["derived_from"];
    2204         102 :         const auto oTransform = oLayoutItem["transform"];
    2205          75 :         if (oDerivedFrom.IsValid() && oTransform.IsValid() &&
    2206          99 :             oDerivedFrom.GetType() == CPLJSONObject::Type::String &&
    2207          24 :             oTransform.GetType() == CPLJSONObject::Type::Object)
    2208             :         {
    2209          48 :             const std::string osDerivedFrom = oDerivedFrom.ToString();
    2210             :             // Resolve "derived_from" to a MDArray
    2211           0 :             std::shared_ptr<GDALGroup> poDerivedFromGroup;
    2212             :             {
    2213          24 :                 CPLErrorStateBackuper oBackuper(CPLQuietErrorHandler);
    2214          72 :                 poDerivedFromGroup = poRG->OpenGroupFromFullname(
    2215          72 :                     resolveAssetPath(osDerivedFrom));
    2216             :             }
    2217           0 :             std::shared_ptr<GDALMDArray> poDerivedFromArray;
    2218          24 :             if (poDerivedFromGroup)
    2219             :             {
    2220          19 :                 poDerivedFromArray = poDerivedFromGroup->OpenMDArray(GetName());
    2221             :             }
    2222           5 :             else if (osDerivedFrom.find('/') == std::string::npos)
    2223             :             {
    2224           1 :                 poDerivedFromArray = poGroup->OpenMDArray(osDerivedFrom);
    2225           1 :                 if (!poDerivedFromArray)
    2226             :                 {
    2227           1 :                     CPLError(CE_Warning, CPLE_AppDefined,
    2228             :                              "multiscales.layout[].asset=%s refers to "
    2229             :                              "derived_from=%s which does not exist",
    2230             :                              osAsset.c_str(), osDerivedFrom.c_str());
    2231           1 :                     poDerivedFromArray.reset();
    2232             :                 }
    2233             :             }
    2234             :             else
    2235             :             {
    2236          12 :                 poDerivedFromArray = poRG->OpenMDArrayFromFullname(
    2237          12 :                     resolveAssetPath(osDerivedFrom));
    2238             :             }
    2239          24 :             if (poDerivedFromArray && bAssetHasAllSpatialDims)
    2240             :             {
    2241          22 :                 if (poDerivedFromArray->GetDimensionCount() !=
    2242          22 :                     GetDimensionCount())
    2243             :                 {
    2244           1 :                     CPLError(CE_Warning, CPLE_AppDefined,
    2245             :                              "multiscales.layout[].asset=%s refers to "
    2246             :                              "derived_from=%s that does not have the expected "
    2247             :                              "number of dimensions. Ignoring that asset",
    2248             :                              osAsset.c_str(), osDerivedFrom.c_str());
    2249           3 :                     continue;
    2250             :                 }
    2251             : 
    2252          42 :                 const auto oScale = oTransform["scale"];
    2253          21 :                 if (oScale.GetType() == CPLJSONObject::Type::Array &&
    2254             :                     bHasExplicitPixelSpatialRegistration)
    2255             :                 {
    2256          10 :                     const auto oScaleArray = oScale.ToArray();
    2257          10 :                     if (oScaleArray.size() != GetDimensionCount())
    2258             :                     {
    2259             : 
    2260           1 :                         CPLError(CE_Warning, CPLE_AppDefined,
    2261             :                                  "multiscales.layout[].asset=%s has a "
    2262             :                                  "transform.scale array with an unexpected "
    2263             :                                  "number of values. Ignoring the asset",
    2264             :                                  osAsset.c_str());
    2265           1 :                         continue;
    2266             :                     }
    2267             : 
    2268          27 :                     for (size_t iDim = 0; iDim < GetDimensionCount(); ++iDim)
    2269             :                     {
    2270          18 :                         const double dfScale = oScaleArray[iDim].ToDouble();
    2271             :                         const double dfExpectedScale =
    2272          18 :                             static_cast<double>(
    2273          18 :                                 poDerivedFromArray->GetDimensions()[iDim]
    2274          18 :                                     ->GetSize()) /
    2275          18 :                             static_cast<double>(
    2276          18 :                                 poAssetArray->GetDimensions()[iDim]->GetSize());
    2277          18 :                         constexpr double EPSILON = 1e-3;
    2278          18 :                         if (std::fabs(dfScale - dfExpectedScale) >
    2279          18 :                             EPSILON * dfExpectedScale)
    2280             :                         {
    2281           2 :                             CPLError(CE_Warning, CPLE_AppDefined,
    2282             :                                      "multiscales.layout[].asset=%s has a "
    2283             :                                      "transform.scale[%d]=%f value whereas %f "
    2284             :                                      "was expected. "
    2285             :                                      "Assuming that later value as the scale.",
    2286             :                                      osAsset.c_str(), static_cast<int>(iDim),
    2287             :                                      dfScale, dfExpectedScale);
    2288             :                         }
    2289             :                     }
    2290             :                 }
    2291             : 
    2292          40 :                 const auto oTranslation = oTransform["translation"];
    2293          20 :                 if (oTranslation.GetType() == CPLJSONObject::Type::Array &&
    2294             :                     bHasExplicitPixelSpatialRegistration)
    2295             :                 {
    2296           9 :                     const auto oTranslationArray = oTranslation.ToArray();
    2297           9 :                     if (oTranslationArray.size() != GetDimensionCount())
    2298             :                     {
    2299           1 :                         CPLError(CE_Warning, CPLE_AppDefined,
    2300             :                                  "multiscales.layout[].asset=%s has a "
    2301             :                                  "transform.translation array with an "
    2302             :                                  "unexpected number of values. "
    2303             :                                  "Ignoring the asset",
    2304             :                                  osAsset.c_str());
    2305           1 :                         continue;
    2306             :                     }
    2307             : 
    2308          24 :                     for (size_t iDim = 0; iDim < GetDimensionCount(); ++iDim)
    2309             :                     {
    2310             :                         const double dfOffset =
    2311          16 :                             oTranslationArray[iDim].ToDouble();
    2312          16 :                         if (dfOffset != 0)
    2313             :                         {
    2314           1 :                             CPLError(CE_Warning, CPLE_AppDefined,
    2315             :                                      "multiscales.layout[].asset=%s has a "
    2316             :                                      "transform.translation[%d]=%f value. "
    2317             :                                      "Ignoring that offset.",
    2318             :                                      osAsset.c_str(), static_cast<int>(iDim),
    2319             :                                      dfOffset);
    2320             :                         }
    2321             :                     }
    2322             :                 }
    2323             :             }
    2324             :         }
    2325             : 
    2326          48 :         if (bFoundSpatialUUID && bAssetHasAllSpatialDims)
    2327             :         {
    2328          38 :             const auto oSpatialShape = oLayoutItem["spatial:shape"];
    2329          19 :             if (oSpatialShape.IsValid())
    2330             :             {
    2331          19 :                 if (oSpatialShape.GetType() != CPLJSONObject::Type::Array)
    2332             :                 {
    2333           1 :                     CPLError(
    2334             :                         CE_Warning, CPLE_AppDefined,
    2335             :                         "multiscales.layout[].asset=%s ignored, because its "
    2336             :                         "spatial:shape property is not an array",
    2337             :                         osAsset.c_str());
    2338           3 :                     continue;
    2339             :                 }
    2340          18 :                 const auto oSpatialShapeArray = oSpatialShape.ToArray();
    2341          18 :                 if (oSpatialShapeArray.size() != aosSpatialDimensions.size())
    2342             :                 {
    2343           1 :                     CPLError(
    2344             :                         CE_Warning, CPLE_AppDefined,
    2345             :                         "multiscales.layout[].asset=%s ignored, because its "
    2346             :                         "spatial:shape property has not the expected number "
    2347             :                         "of values",
    2348             :                         osAsset.c_str());
    2349           1 :                     continue;
    2350             :                 }
    2351             : 
    2352          17 :                 bool bSkip = false;
    2353          68 :                 for (const auto &[idx, oShapeVal] :
    2354          85 :                      cpl::enumerate(oSpatialShapeArray))
    2355             :                 {
    2356             :                     const auto oIter =
    2357          34 :                         oMapAssetDimNameToIdx.find(aosSpatialDimensions[idx]);
    2358          34 :                     if (oIter != oMapAssetDimNameToIdx.end())
    2359             :                     {
    2360          68 :                         const auto poDim = apoAssetDims[oIter->second];
    2361          34 :                         if (poDim->GetSize() !=
    2362          34 :                             static_cast<uint64_t>(oShapeVal.ToLong()))
    2363             :                         {
    2364           1 :                             bSkip = true;
    2365           1 :                             CPLError(CE_Warning, CPLE_AppDefined,
    2366             :                                      "multiscales.layout[].asset=%s ignored, "
    2367             :                                      "because its "
    2368             :                                      "spatial:shape[%d] value is %" PRIu64
    2369             :                                      " whereas %" PRIu64 " was expected.",
    2370           1 :                                      osAsset.c_str(), static_cast<int>(idx),
    2371           1 :                                      static_cast<uint64_t>(oShapeVal.ToLong()),
    2372           1 :                                      static_cast<uint64_t>(poDim->GetSize()));
    2373             :                         }
    2374             :                     }
    2375             :                 }
    2376          17 :                 if (bSkip)
    2377           1 :                     continue;
    2378             :             }
    2379             :         }
    2380             : 
    2381          45 :         m_apoOverviews.push_back(std::move(poAssetArray));
    2382             :     }
    2383             : }
    2384             : 
    2385             : /************************************************************************/
    2386             : /*                   ZarrV3Array::GetOverviewCount()                    */
    2387             : /************************************************************************/
    2388             : 
    2389          78 : int ZarrV3Array::GetOverviewCount() const
    2390             : {
    2391          78 :     LoadOverviews();
    2392          78 :     return static_cast<int>(m_apoOverviews.size());
    2393             : }
    2394             : 
    2395             : /************************************************************************/
    2396             : /*                      ZarrV3Array::GetOverview()                      */
    2397             : /************************************************************************/
    2398             : 
    2399          43 : std::shared_ptr<GDALMDArray> ZarrV3Array::GetOverview(int idx) const
    2400             : {
    2401          43 :     if (idx < 0 || idx >= GetOverviewCount())
    2402          12 :         return nullptr;
    2403          31 :     return m_apoOverviews[idx];
    2404             : }

Generated by: LCOV version 1.14