LCOV - code coverage report
Current view: top level - frmts/zarr - zarr_v2_array.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 921 998 92.3 %
Date: 2025-09-10 17:48:50 Functions: 29 31 93.5 %

          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_float.h"
      14             : #include "cpl_vsi_virtual.h"
      15             : #include "gdal_thread_pool.h"
      16             : #include "zarr.h"
      17             : 
      18             : #include "netcdf_cf_constants.h"  // for CF_UNITS, etc
      19             : 
      20             : #include <algorithm>
      21             : #include <cassert>
      22             : #include <cstdlib>
      23             : #include <limits>
      24             : #include <map>
      25             : #include <set>
      26             : 
      27             : /************************************************************************/
      28             : /*                       ZarrV2Array::ZarrV2Array()                     */
      29             : /************************************************************************/
      30             : 
      31         963 : ZarrV2Array::ZarrV2Array(
      32             :     const std::shared_ptr<ZarrSharedResource> &poSharedResource,
      33             :     const std::string &osParentName, const std::string &osName,
      34             :     const std::vector<std::shared_ptr<GDALDimension>> &aoDims,
      35             :     const GDALExtendedDataType &oType, const std::vector<DtypeElt> &aoDtypeElts,
      36         963 :     const std::vector<GUInt64> &anBlockSize, bool bFortranOrder)
      37             :     : GDALAbstractMDArray(osParentName, osName),
      38             :       ZarrArray(poSharedResource, osParentName, osName, aoDims, oType,
      39             :                 aoDtypeElts, anBlockSize),
      40         963 :       m_bFortranOrder(bFortranOrder)
      41             : {
      42         963 :     m_oCompressorJSon.Deinit();
      43         963 : }
      44             : 
      45             : /************************************************************************/
      46             : /*                         ZarrV2Array::Create()                        */
      47             : /************************************************************************/
      48             : 
      49             : std::shared_ptr<ZarrV2Array>
      50         963 : ZarrV2Array::Create(const std::shared_ptr<ZarrSharedResource> &poSharedResource,
      51             :                     const std::string &osParentName, const std::string &osName,
      52             :                     const std::vector<std::shared_ptr<GDALDimension>> &aoDims,
      53             :                     const GDALExtendedDataType &oType,
      54             :                     const std::vector<DtypeElt> &aoDtypeElts,
      55             :                     const std::vector<GUInt64> &anBlockSize, bool bFortranOrder)
      56             : {
      57             :     auto arr = std::shared_ptr<ZarrV2Array>(
      58             :         new ZarrV2Array(poSharedResource, osParentName, osName, aoDims, oType,
      59        1926 :                         aoDtypeElts, anBlockSize, bFortranOrder));
      60         963 :     if (arr->m_nTotalTileCount == 0)
      61           1 :         return nullptr;
      62         962 :     arr->SetSelf(arr);
      63             : 
      64         962 :     return arr;
      65             : }
      66             : 
      67             : /************************************************************************/
      68             : /*                             ~ZarrV2Array()                           */
      69             : /************************************************************************/
      70             : 
      71        1926 : ZarrV2Array::~ZarrV2Array()
      72             : {
      73         963 :     ZarrV2Array::Flush();
      74        1926 : }
      75             : 
      76             : /************************************************************************/
      77             : /*                                Flush()                               */
      78             : /************************************************************************/
      79             : 
      80        4207 : bool ZarrV2Array::Flush()
      81             : {
      82        4207 :     if (!m_bValid)
      83          12 :         return true;
      84             : 
      85        4195 :     bool ret = ZarrV2Array::FlushDirtyTile();
      86             : 
      87        4195 :     if (m_bDefinitionModified)
      88             :     {
      89         322 :         if (!Serialize())
      90           3 :             ret = false;
      91         322 :         m_bDefinitionModified = false;
      92             :     }
      93             : 
      94        4195 :     CPLJSONArray j_ARRAY_DIMENSIONS;
      95        4195 :     bool bDimensionsModified = false;
      96        4195 :     if (!m_aoDims.empty())
      97             :     {
      98        8047 :         for (const auto &poDim : m_aoDims)
      99             :         {
     100             :             const auto poZarrDim =
     101        5262 :                 dynamic_cast<const ZarrDimension *>(poDim.get());
     102        5262 :             if (poZarrDim && poZarrDim->IsXArrayDimension())
     103             :             {
     104        4189 :                 if (poZarrDim->IsModified())
     105          16 :                     bDimensionsModified = true;
     106        4189 :                 j_ARRAY_DIMENSIONS.Add(poDim->GetName());
     107             :             }
     108             :             else
     109             :             {
     110        1073 :                 j_ARRAY_DIMENSIONS = CPLJSONArray();
     111        1073 :                 break;
     112             :             }
     113             :         }
     114             :     }
     115             : 
     116        8331 :     if (m_oAttrGroup.IsModified() || bDimensionsModified ||
     117        4120 :         (m_bNew && j_ARRAY_DIMENSIONS.Size() != 0) || m_bUnitModified ||
     118        8331 :         m_bOffsetModified || m_bScaleModified || m_bSRSModified)
     119             :     {
     120         348 :         m_bNew = false;
     121             : 
     122         696 :         auto oAttrs = SerializeSpecialAttributes();
     123             : 
     124         348 :         if (j_ARRAY_DIMENSIONS.Size() != 0)
     125             :         {
     126         332 :             oAttrs.Delete("_ARRAY_DIMENSIONS");
     127         332 :             oAttrs.Add("_ARRAY_DIMENSIONS", j_ARRAY_DIMENSIONS);
     128             :         }
     129             : 
     130         696 :         CPLJSONDocument oDoc;
     131         348 :         oDoc.SetRoot(oAttrs);
     132             :         const std::string osAttrFilename =
     133         348 :             CPLFormFilenameSafe(CPLGetDirnameSafe(m_osFilename.c_str()).c_str(),
     134         696 :                                 ".zattrs", nullptr);
     135         348 :         if (!oDoc.Save(osAttrFilename))
     136           7 :             ret = false;
     137         348 :         m_poSharedResource->SetZMetadataItem(osAttrFilename, oAttrs);
     138             :     }
     139             : 
     140        4195 :     return ret;
     141             : }
     142             : 
     143             : /************************************************************************/
     144             : /*           StripUselessItemsFromCompressorConfiguration()             */
     145             : /************************************************************************/
     146             : 
     147          28 : static void StripUselessItemsFromCompressorConfiguration(CPLJSONObject &o)
     148             : {
     149          28 :     if (o.GetType() == CPLJSONObject::Type::Object)
     150             :     {
     151          18 :         o.Delete("num_threads");  // Blosc
     152          18 :         o.Delete("typesize");     // Blosc
     153          18 :         o.Delete("header");       // LZ4
     154             :     }
     155          28 : }
     156             : 
     157             : /************************************************************************/
     158             : /*                    ZarrV2Array::Serialize()                          */
     159             : /************************************************************************/
     160             : 
     161         322 : bool ZarrV2Array::Serialize()
     162             : {
     163         644 :     CPLJSONDocument oDoc;
     164         644 :     CPLJSONObject oRoot = oDoc.GetRoot();
     165             : 
     166         644 :     CPLJSONArray oChunks;
     167         782 :     for (const auto nBlockSize : m_anBlockSize)
     168             :     {
     169         460 :         oChunks.Add(static_cast<GInt64>(nBlockSize));
     170             :     }
     171         322 :     oRoot.Add("chunks", oChunks);
     172             : 
     173         322 :     if (m_oCompressorJSon.IsValid())
     174             :     {
     175          28 :         oRoot.Add("compressor", m_oCompressorJSon);
     176          84 :         CPLJSONObject compressor = oRoot["compressor"];
     177          28 :         StripUselessItemsFromCompressorConfiguration(compressor);
     178             :     }
     179             :     else
     180             :     {
     181         294 :         oRoot.AddNull("compressor");
     182             :     }
     183             : 
     184         322 :     if (m_dtype.GetType() == CPLJSONObject::Type::Object)
     185         310 :         oRoot.Add("dtype", m_dtype["dummy"]);
     186             :     else
     187          12 :         oRoot.Add("dtype", m_dtype);
     188             : 
     189         322 :     if (m_pabyNoData == nullptr)
     190             :     {
     191         312 :         oRoot.AddNull("fill_value");
     192             :     }
     193             :     else
     194             :     {
     195          10 :         switch (m_oType.GetClass())
     196             :         {
     197           8 :             case GEDTC_NUMERIC:
     198             :             {
     199           8 :                 SerializeNumericNoData(oRoot);
     200           8 :                 break;
     201             :             }
     202             : 
     203           1 :             case GEDTC_STRING:
     204             :             {
     205             :                 char *pszStr;
     206           1 :                 char **ppszStr = reinterpret_cast<char **>(m_pabyNoData);
     207           1 :                 memcpy(&pszStr, ppszStr, sizeof(pszStr));
     208           1 :                 if (pszStr)
     209             :                 {
     210             :                     const size_t nNativeSize =
     211           1 :                         m_aoDtypeElts.back().nativeOffset +
     212           1 :                         m_aoDtypeElts.back().nativeSize;
     213           3 :                     char *base64 = CPLBase64Encode(
     214           1 :                         static_cast<int>(std::min(nNativeSize, strlen(pszStr))),
     215             :                         reinterpret_cast<const GByte *>(pszStr));
     216           1 :                     oRoot.Add("fill_value", base64);
     217           1 :                     CPLFree(base64);
     218             :                 }
     219             :                 else
     220             :                 {
     221           0 :                     oRoot.AddNull("fill_value");
     222             :                 }
     223           1 :                 break;
     224             :             }
     225             : 
     226           1 :             case GEDTC_COMPOUND:
     227             :             {
     228           1 :                 const size_t nNativeSize = m_aoDtypeElts.back().nativeOffset +
     229           1 :                                            m_aoDtypeElts.back().nativeSize;
     230           2 :                 std::vector<GByte> nativeNoData(nNativeSize);
     231           1 :                 EncodeElt(m_aoDtypeElts, m_pabyNoData, &nativeNoData[0]);
     232           1 :                 char *base64 = CPLBase64Encode(static_cast<int>(nNativeSize),
     233           1 :                                                nativeNoData.data());
     234           1 :                 oRoot.Add("fill_value", base64);
     235           1 :                 CPLFree(base64);
     236             :             }
     237             :         }
     238             :     }
     239             : 
     240         322 :     if (m_oFiltersArray.Size() == 0)
     241         321 :         oRoot.AddNull("filters");
     242             :     else
     243           1 :         oRoot.Add("filters", m_oFiltersArray);
     244             : 
     245         322 :     oRoot.Add("order", m_bFortranOrder ? "F" : "C");
     246             : 
     247         322 :     CPLJSONArray oShape;
     248         782 :     for (const auto &poDim : m_aoDims)
     249             :     {
     250         460 :         oShape.Add(static_cast<GInt64>(poDim->GetSize()));
     251             :     }
     252         322 :     oRoot.Add("shape", oShape);
     253             : 
     254         322 :     oRoot.Add("zarr_format", 2);
     255             : 
     256         322 :     if (m_osDimSeparator != ".")
     257             :     {
     258          10 :         oRoot.Add("dimension_separator", m_osDimSeparator);
     259             :     }
     260             : 
     261         322 :     bool ret = oDoc.Save(m_osFilename);
     262             : 
     263         322 :     m_poSharedResource->SetZMetadataItem(m_osFilename, oRoot);
     264             : 
     265         644 :     return ret;
     266             : }
     267             : 
     268             : /************************************************************************/
     269             : /*                  ZarrV2Array::NeedDecodedBuffer()                    */
     270             : /************************************************************************/
     271             : 
     272       11698 : bool ZarrV2Array::NeedDecodedBuffer() const
     273             : {
     274             :     const size_t nSourceSize =
     275       11698 :         m_aoDtypeElts.back().nativeOffset + m_aoDtypeElts.back().nativeSize;
     276       11687 :     if (m_oType.GetClass() == GEDTC_COMPOUND &&
     277           6 :         nSourceSize != m_oType.GetSize())
     278             :     {
     279           4 :         return true;
     280             :     }
     281       11683 :     else if (m_oType.GetClass() != GEDTC_STRING)
     282             :     {
     283       23232 :         for (const auto &elt : m_aoDtypeElts)
     284             :         {
     285       11656 :             if (elt.needByteSwapping || elt.gdalTypeIsApproxOfNative ||
     286       11575 :                 elt.nativeType == DtypeElt::NativeType::STRING_ASCII ||
     287       11577 :                 elt.nativeType == DtypeElt::NativeType::STRING_UNICODE)
     288             :             {
     289          82 :                 return true;
     290             :             }
     291             :         }
     292             :     }
     293       11592 :     return false;
     294             : }
     295             : 
     296             : /************************************************************************/
     297             : /*               ZarrV2Array::AllocateWorkingBuffers()                  */
     298             : /************************************************************************/
     299             : 
     300        1951 : bool ZarrV2Array::AllocateWorkingBuffers() const
     301             : {
     302        1951 :     if (m_bAllocateWorkingBuffersDone)
     303        1435 :         return m_bWorkingBuffersOK;
     304             : 
     305         516 :     m_bAllocateWorkingBuffersDone = true;
     306             : 
     307         516 :     size_t nSizeNeeded = m_nTileSize;
     308         516 :     if (m_bFortranOrder || m_oFiltersArray.Size() != 0)
     309             :     {
     310          46 :         if (nSizeNeeded > std::numeric_limits<size_t>::max() / 2)
     311             :         {
     312           1 :             CPLError(CE_Failure, CPLE_AppDefined, "Too large chunk size");
     313           1 :             return false;
     314             :         }
     315          45 :         nSizeNeeded *= 2;
     316             :     }
     317         515 :     if (NeedDecodedBuffer())
     318             :     {
     319          43 :         size_t nDecodedBufferSize = m_oType.GetSize();
     320         126 :         for (const auto &nBlockSize : m_anBlockSize)
     321             :         {
     322          83 :             if (nDecodedBufferSize > std::numeric_limits<size_t>::max() /
     323          83 :                                          static_cast<size_t>(nBlockSize))
     324             :             {
     325           0 :                 CPLError(CE_Failure, CPLE_AppDefined, "Too large chunk size");
     326           0 :                 return false;
     327             :             }
     328          83 :             nDecodedBufferSize *= static_cast<size_t>(nBlockSize);
     329             :         }
     330          43 :         if (nSizeNeeded >
     331          43 :             std::numeric_limits<size_t>::max() - nDecodedBufferSize)
     332             :         {
     333           0 :             CPLError(CE_Failure, CPLE_AppDefined, "Too large chunk size");
     334           0 :             return false;
     335             :         }
     336          43 :         nSizeNeeded += nDecodedBufferSize;
     337             :     }
     338             : 
     339             :     // Reserve a buffer for tile content
     340         517 :     if (nSizeNeeded > 1024 * 1024 * 1024 &&
     341           2 :         !CPLTestBool(CPLGetConfigOption("ZARR_ALLOW_BIG_TILE_SIZE", "NO")))
     342             :     {
     343           2 :         CPLError(CE_Failure, CPLE_AppDefined,
     344             :                  "Zarr tile allocation would require " CPL_FRMT_GUIB " bytes. "
     345             :                  "By default the driver limits to 1 GB. To allow that memory "
     346             :                  "allocation, set the ZARR_ALLOW_BIG_TILE_SIZE configuration "
     347             :                  "option to YES.",
     348             :                  static_cast<GUIntBig>(nSizeNeeded));
     349           2 :         return false;
     350             :     }
     351             : 
     352        1026 :     m_bWorkingBuffersOK = AllocateWorkingBuffers(
     353         513 :         m_abyRawTileData, m_abyTmpRawTileData, m_abyDecodedTileData);
     354         513 :     return m_bWorkingBuffersOK;
     355             : }
     356             : 
     357       11184 : bool ZarrV2Array::AllocateWorkingBuffers(
     358             :     ZarrByteVectorQuickResize &abyRawTileData,
     359             :     ZarrByteVectorQuickResize &abyTmpRawTileData,
     360             :     ZarrByteVectorQuickResize &abyDecodedTileData) const
     361             : {
     362             :     // This method should NOT modify any ZarrArray member, as it is going to
     363             :     // be called concurrently from several threads.
     364             : 
     365             :     // Set those #define to avoid accidental use of some global variables
     366             : #define m_abyTmpRawTileData cannot_use_here
     367             : #define m_abyRawTileData cannot_use_here
     368             : #define m_abyDecodedTileData cannot_use_here
     369             : 
     370             :     try
     371             :     {
     372       11184 :         abyRawTileData.resize(m_nTileSize);
     373       11171 :         if (m_bFortranOrder || m_oFiltersArray.Size() != 0)
     374          45 :             abyTmpRawTileData.resize(m_nTileSize);
     375             :     }
     376           0 :     catch (const std::bad_alloc &e)
     377             :     {
     378           0 :         CPLError(CE_Failure, CPLE_OutOfMemory, "%s", e.what());
     379           0 :         return false;
     380             :     }
     381             : 
     382       11167 :     if (NeedDecodedBuffer())
     383             :     {
     384          43 :         size_t nDecodedBufferSize = m_oType.GetSize();
     385         126 :         for (const auto &nBlockSize : m_anBlockSize)
     386             :         {
     387          83 :             nDecodedBufferSize *= static_cast<size_t>(nBlockSize);
     388             :         }
     389             :         try
     390             :         {
     391          43 :             abyDecodedTileData.resize(nDecodedBufferSize);
     392             :         }
     393           0 :         catch (const std::bad_alloc &e)
     394             :         {
     395           0 :             CPLError(CE_Failure, CPLE_OutOfMemory, "%s", e.what());
     396           0 :             return false;
     397             :         }
     398             :     }
     399             : 
     400       11172 :     return true;
     401             : #undef m_abyTmpRawTileData
     402             : #undef m_abyRawTileData
     403             : #undef m_abyDecodedTileData
     404             : }
     405             : 
     406             : /************************************************************************/
     407             : /*                      ZarrV2Array::LoadTileData()                     */
     408             : /************************************************************************/
     409             : 
     410        4882 : bool ZarrV2Array::LoadTileData(const uint64_t *tileIndices,
     411             :                                bool &bMissingTileOut) const
     412             : {
     413        9764 :     return LoadTileData(tileIndices,
     414             :                         false,  // use mutex
     415        4882 :                         m_psDecompressor, m_abyRawTileData, m_abyTmpRawTileData,
     416        4882 :                         m_abyDecodedTileData, bMissingTileOut);
     417             : }
     418             : 
     419       15542 : bool ZarrV2Array::LoadTileData(const uint64_t *tileIndices, bool bUseMutex,
     420             :                                const CPLCompressor *psDecompressor,
     421             :                                ZarrByteVectorQuickResize &abyRawTileData,
     422             :                                ZarrByteVectorQuickResize &abyTmpRawTileData,
     423             :                                ZarrByteVectorQuickResize &abyDecodedTileData,
     424             :                                bool &bMissingTileOut) const
     425             : {
     426             :     // This method should NOT modify any ZarrArray member, as it is going to
     427             :     // be called concurrently from several threads.
     428             : 
     429             :     // Set those #define to avoid accidental use of some global variables
     430             : #define m_abyTmpRawTileData cannot_use_here
     431             : #define m_abyRawTileData cannot_use_here
     432             : #define m_abyDecodedTileData cannot_use_here
     433             : #define m_psDecompressor cannot_use_here
     434             : 
     435       15542 :     bMissingTileOut = false;
     436             : 
     437       30218 :     std::string osFilename = BuildTileFilename(tileIndices);
     438             : 
     439             :     // For network file systems, get the streaming version of the filename,
     440             :     // as we don't need arbitrary seeking in the file
     441       15545 :     osFilename = VSIFileManager::GetHandler(osFilename.c_str())
     442       15552 :                      ->GetStreamingFilename(osFilename);
     443             : 
     444             :     const auto CheckTilePresence =
     445       31296 :         [this, &osFilename, &tileIndices, &bMissingTileOut]()
     446             :     {
     447       15554 :         CPL_IGNORE_RET_VAL(osFilename);
     448       31108 :         auto poTilePresenceArray = OpenTilePresenceCache(false);
     449       15554 :         if (poTilePresenceArray)
     450             :         {
     451          18 :             std::vector<GUInt64> anTileIdx(m_aoDims.size());
     452          18 :             const std::vector<size_t> anCount(m_aoDims.size(), 1);
     453          18 :             const std::vector<GInt64> anArrayStep(m_aoDims.size(), 0);
     454          18 :             const std::vector<GPtrDiff_t> anBufferStride(m_aoDims.size(), 0);
     455          18 :             const auto eByteDT = GDALExtendedDataType::Create(GDT_Byte);
     456          54 :             for (size_t i = 0; i < m_aoDims.size(); ++i)
     457             :             {
     458          36 :                 anTileIdx[i] = static_cast<GUInt64>(tileIndices[i]);
     459             :             }
     460          18 :             GByte byValue = 0;
     461          36 :             if (poTilePresenceArray->Read(
     462          18 :                     anTileIdx.data(), anCount.data(), anArrayStep.data(),
     463          36 :                     anBufferStride.data(), eByteDT, &byValue) &&
     464          18 :                 byValue == 0)
     465             :             {
     466          13 :                 CPLDebugOnly(ZARR_DEBUG_KEY, "Tile %s missing (=nodata)",
     467             :                              osFilename.c_str());
     468          13 :                 bMissingTileOut = true;
     469          13 :                 return true;
     470             :             }
     471             :         }
     472       15541 :         return false;
     473       15536 :     };
     474             : 
     475             :     // First if we have a tile presence cache, check tile presence from it
     476             :     bool bEarlyRet;
     477       15536 :     if (bUseMutex)
     478             :     {
     479       10654 :         std::lock_guard<std::mutex> oLock(m_oMutex);
     480       10672 :         bEarlyRet = CheckTilePresence();
     481             :     }
     482             :     else
     483             :     {
     484        4882 :         bEarlyRet = CheckTilePresence();
     485             :     }
     486       15554 :     if (bEarlyRet)
     487          13 :         return true;
     488             : 
     489       15541 :     VSILFILE *fp = nullptr;
     490             :     // This is the number of files returned in a S3 directory listing operation
     491       15541 :     constexpr uint64_t MAX_TILES_ALLOWED_FOR_DIRECTORY_LISTING = 1000;
     492       15541 :     const char *const apszOpenOptions[] = {"IGNORE_FILENAME_RESTRICTIONS=YES",
     493             :                                            nullptr};
     494       15541 :     const auto nErrorBefore = CPLGetErrorCounter();
     495       15559 :     if ((m_osDimSeparator == "/" && !m_anBlockSize.empty() &&
     496       31097 :          m_anBlockSize.back() > MAX_TILES_ALLOWED_FOR_DIRECTORY_LISTING) ||
     497       15540 :         (m_osDimSeparator != "/" &&
     498       15516 :          m_nTotalTileCount > MAX_TILES_ALLOWED_FOR_DIRECTORY_LISTING))
     499             :     {
     500             :         // Avoid issuing ReadDir() when a lot of files are expected
     501             :         CPLConfigOptionSetter optionSetter("GDAL_DISABLE_READDIR_ON_OPEN",
     502       10977 :                                            "YES", true);
     503       10905 :         fp = VSIFOpenEx2L(osFilename.c_str(), "rb", 0, apszOpenOptions);
     504             :     }
     505             :     else
     506             :     {
     507        4560 :         fp = VSIFOpenEx2L(osFilename.c_str(), "rb", 0, apszOpenOptions);
     508             :     }
     509       15467 :     if (fp == nullptr)
     510             :     {
     511        2714 :         if (nErrorBefore != CPLGetErrorCounter())
     512             :         {
     513           3 :             return false;
     514             :         }
     515             :         else
     516             :         {
     517             :             // Missing files are OK and indicate nodata_value
     518        2711 :             CPLDebugOnly(ZARR_DEBUG_KEY, "Tile %s missing (=nodata)",
     519             :                          osFilename.c_str());
     520        2711 :             bMissingTileOut = true;
     521        2711 :             return true;
     522             :         }
     523             :     }
     524             : 
     525       12753 :     bMissingTileOut = false;
     526       12753 :     bool bRet = true;
     527       12753 :     size_t nRawDataSize = abyRawTileData.size();
     528       12710 :     if (psDecompressor == nullptr)
     529             :     {
     530        7241 :         nRawDataSize = VSIFReadL(&abyRawTileData[0], 1, nRawDataSize, fp);
     531             :     }
     532             :     else
     533             :     {
     534        5469 :         VSIFSeekL(fp, 0, SEEK_END);
     535        5468 :         const auto nSize = VSIFTellL(fp);
     536        5259 :         VSIFSeekL(fp, 0, SEEK_SET);
     537        5241 :         if (nSize > static_cast<vsi_l_offset>(std::numeric_limits<int>::max()))
     538             :         {
     539           0 :             CPLError(CE_Failure, CPLE_AppDefined, "Too large tile %s",
     540             :                      osFilename.c_str());
     541           0 :             bRet = false;
     542             :         }
     543             :         else
     544             :         {
     545       10658 :             ZarrByteVectorQuickResize abyCompressedData;
     546             :             try
     547             :             {
     548        5228 :                 abyCompressedData.resize(static_cast<size_t>(nSize));
     549             :             }
     550           0 :             catch (const std::exception &)
     551             :             {
     552           0 :                 CPLError(CE_Failure, CPLE_OutOfMemory,
     553             :                          "Cannot allocate memory for tile %s",
     554             :                          osFilename.c_str());
     555           0 :                 bRet = false;
     556             :             }
     557             : 
     558       15593 :             if (bRet &&
     559       10448 :                 (abyCompressedData.empty() ||
     560        5148 :                  VSIFReadL(&abyCompressedData[0], 1, abyCompressedData.size(),
     561        5230 :                            fp) != abyCompressedData.size()))
     562             :             {
     563           0 :                 CPLError(CE_Failure, CPLE_AppDefined,
     564             :                          "Could not read tile %s correctly",
     565             :                          osFilename.c_str());
     566           0 :                 bRet = false;
     567             :             }
     568             :             else
     569             :             {
     570        5100 :                 void *out_buffer = &abyRawTileData[0];
     571        5261 :                 if (!psDecompressor->pfnFunc(
     572        5239 :                         abyCompressedData.data(), abyCompressedData.size(),
     573             :                         &out_buffer, &nRawDataSize, nullptr,
     574        5191 :                         psDecompressor->user_data))
     575             :                 {
     576           3 :                     CPLError(CE_Failure, CPLE_AppDefined,
     577             :                              "Decompression of tile %s failed",
     578             :                              osFilename.c_str());
     579           3 :                     bRet = false;
     580             :                 }
     581             :             }
     582             :         }
     583             :     }
     584       12551 :     VSIFCloseL(fp);
     585       12572 :     if (!bRet)
     586           3 :         return false;
     587             : 
     588       13111 :     for (int i = m_oFiltersArray.Size(); i > 0;)
     589             :     {
     590          12 :         --i;
     591          12 :         const auto &oFilter = m_oFiltersArray[i];
     592          24 :         const auto osFilterId = oFilter["id"].ToString();
     593             :         const auto psFilterDecompressor =
     594          22 :             EQUAL(osFilterId.c_str(), "shuffle") ? ZarrGetShuffleDecompressor()
     595          10 :             : EQUAL(osFilterId.c_str(), "quantize")
     596          18 :                 ? ZarrGetQuantizeDecompressor()
     597           8 :             : EQUAL(osFilterId.c_str(), "fixedscaleoffset")
     598           8 :                 ? ZarrGetFixedScaleOffsetDecompressor()
     599           4 :                 : CPLGetDecompressor(osFilterId.c_str());
     600          12 :         CPLAssert(psFilterDecompressor);
     601             : 
     602          12 :         CPLStringList aosOptions;
     603          52 :         for (const auto &obj : oFilter.GetChildren())
     604             :         {
     605          80 :             aosOptions.SetNameValue(obj.GetName().c_str(),
     606         120 :                                     obj.ToString().c_str());
     607             :         }
     608          12 :         void *out_buffer = &abyTmpRawTileData[0];
     609          12 :         size_t nOutSize = abyTmpRawTileData.size();
     610          12 :         if (!psFilterDecompressor->pfnFunc(
     611          12 :                 abyRawTileData.data(), nRawDataSize, &out_buffer, &nOutSize,
     612          12 :                 aosOptions.List(), psFilterDecompressor->user_data))
     613             :         {
     614           0 :             CPLError(CE_Failure, CPLE_AppDefined,
     615             :                      "Filter %s for tile %s failed", osFilterId.c_str(),
     616             :                      osFilename.c_str());
     617           0 :             return false;
     618             :         }
     619             : 
     620          12 :         nRawDataSize = nOutSize;
     621          12 :         std::swap(abyRawTileData, abyTmpRawTileData);
     622             :     }
     623       12722 :     if (nRawDataSize != abyRawTileData.size())
     624             :     {
     625          35 :         CPLError(CE_Failure, CPLE_AppDefined,
     626             :                  "Decompressed tile %s has not expected size after filters",
     627             :                  osFilename.c_str());
     628           0 :         return false;
     629             :     }
     630             : 
     631       12472 :     if (m_bFortranOrder && !m_aoDims.empty())
     632             :     {
     633          46 :         BlockTranspose(abyRawTileData, abyTmpRawTileData, true);
     634          46 :         std::swap(abyRawTileData, abyTmpRawTileData);
     635             :     }
     636             : 
     637       12472 :     if (!abyDecodedTileData.empty())
     638             :     {
     639             :         const size_t nSourceSize =
     640         326 :             m_aoDtypeElts.back().nativeOffset + m_aoDtypeElts.back().nativeSize;
     641         326 :         const auto nDTSize = m_oType.GetSize();
     642         326 :         const size_t nValues = abyDecodedTileData.size() / nDTSize;
     643         326 :         const GByte *pSrc = abyRawTileData.data();
     644         326 :         GByte *pDst = &abyDecodedTileData[0];
     645        2217 :         for (size_t i = 0; i < nValues;
     646        1931 :              i++, pSrc += nSourceSize, pDst += nDTSize)
     647             :         {
     648        1931 :             DecodeSourceElt(m_aoDtypeElts, pSrc, pDst);
     649             :         }
     650             :     }
     651             : 
     652       12476 :     return true;
     653             : 
     654             : #undef m_abyTmpRawTileData
     655             : #undef m_abyRawTileData
     656             : #undef m_abyDecodedTileData
     657             : #undef m_psDecompressor
     658             : }
     659             : 
     660             : /************************************************************************/
     661             : /*                      ZarrV2Array::IAdviseRead()                      */
     662             : /************************************************************************/
     663             : 
     664           6 : bool ZarrV2Array::IAdviseRead(const GUInt64 *arrayStartIdx, const size_t *count,
     665             :                               CSLConstList papszOptions) const
     666             : {
     667          12 :     std::vector<uint64_t> anIndicesCur;
     668           6 :     int nThreadsMax = 0;
     669          12 :     std::vector<uint64_t> anReqTilesIndices;
     670           6 :     size_t nReqTiles = 0;
     671           6 :     if (!IAdviseReadCommon(arrayStartIdx, count, papszOptions, anIndicesCur,
     672             :                            nThreadsMax, anReqTilesIndices, nReqTiles))
     673             :     {
     674           2 :         return false;
     675             :     }
     676           4 :     if (nThreadsMax <= 1)
     677             :     {
     678           0 :         return true;
     679             :     }
     680             : 
     681             :     const int nThreads =
     682           4 :         static_cast<int>(std::min(static_cast<size_t>(nThreadsMax), nReqTiles));
     683             : 
     684           4 :     CPLWorkerThreadPool *wtp = GDALGetGlobalThreadPool(nThreadsMax);
     685           4 :     if (wtp == nullptr)
     686           0 :         return false;
     687             : 
     688             :     struct JobStruct
     689             :     {
     690             :         JobStruct() = default;
     691             : 
     692             :         JobStruct(const JobStruct &) = delete;
     693             :         JobStruct &operator=(const JobStruct &) = delete;
     694             : 
     695             :         JobStruct(JobStruct &&) = default;
     696             :         JobStruct &operator=(JobStruct &&) = default;
     697             : 
     698             :         const ZarrV2Array *poArray = nullptr;
     699             :         bool *pbGlobalStatus = nullptr;
     700             :         int *pnRemainingThreads = nullptr;
     701             :         const std::vector<uint64_t> *panReqTilesIndices = nullptr;
     702             :         size_t nFirstIdx = 0;
     703             :         size_t nLastIdxNotIncluded = 0;
     704             :     };
     705             : 
     706           4 :     std::vector<JobStruct> asJobStructs;
     707             : 
     708           4 :     bool bGlobalStatus = true;
     709           4 :     int nRemainingThreads = nThreads;
     710             :     // Check for very highly overflow in below loop
     711           4 :     assert(static_cast<size_t>(nThreads) <
     712             :            std::numeric_limits<size_t>::max() / nReqTiles);
     713             : 
     714             :     // Setup jobs
     715          20 :     for (int i = 0; i < nThreads; i++)
     716             :     {
     717          16 :         JobStruct jobStruct;
     718          16 :         jobStruct.poArray = this;
     719          16 :         jobStruct.pbGlobalStatus = &bGlobalStatus;
     720          16 :         jobStruct.pnRemainingThreads = &nRemainingThreads;
     721          16 :         jobStruct.panReqTilesIndices = &anReqTilesIndices;
     722          16 :         jobStruct.nFirstIdx = static_cast<size_t>(i * nReqTiles / nThreads);
     723          16 :         jobStruct.nLastIdxNotIncluded = std::min(
     724          16 :             static_cast<size_t>((i + 1) * nReqTiles / nThreads), nReqTiles);
     725          16 :         asJobStructs.emplace_back(std::move(jobStruct));
     726             :     }
     727             : 
     728          16 :     const auto JobFunc = [](void *pThreadData)
     729             :     {
     730          16 :         const JobStruct *jobStruct =
     731             :             static_cast<const JobStruct *>(pThreadData);
     732             : 
     733          16 :         const auto poArray = jobStruct->poArray;
     734          16 :         const auto &aoDims = poArray->GetDimensions();
     735          16 :         const size_t l_nDims = poArray->GetDimensionCount();
     736          16 :         ZarrByteVectorQuickResize abyRawTileData;
     737          16 :         ZarrByteVectorQuickResize abyDecodedTileData;
     738          16 :         ZarrByteVectorQuickResize abyTmpRawTileData;
     739             :         const CPLCompressor *psDecompressor =
     740          16 :             CPLGetDecompressor(poArray->m_osDecompressorId.c_str());
     741             : 
     742       10688 :         for (size_t iReq = jobStruct->nFirstIdx;
     743       10688 :              iReq < jobStruct->nLastIdxNotIncluded; ++iReq)
     744             :         {
     745             :             // Check if we must early exit
     746             :             {
     747       10672 :                 std::lock_guard<std::mutex> oLock(poArray->m_oMutex);
     748       10672 :                 if (!(*jobStruct->pbGlobalStatus))
     749           0 :                     return;
     750             :             }
     751             : 
     752             :             const uint64_t *tileIndices =
     753       10672 :                 jobStruct->panReqTilesIndices->data() + iReq * l_nDims;
     754             : 
     755       10671 :             uint64_t nTileIdx = 0;
     756       32010 :             for (size_t j = 0; j < l_nDims; ++j)
     757             :             {
     758       21342 :                 if (j > 0)
     759       10671 :                     nTileIdx *= aoDims[j - 1]->GetSize();
     760       21339 :                 nTileIdx += tileIndices[j];
     761             :             }
     762             : 
     763       10668 :             if (!poArray->AllocateWorkingBuffers(
     764             :                     abyRawTileData, abyTmpRawTileData, abyDecodedTileData))
     765             :             {
     766           0 :                 std::lock_guard<std::mutex> oLock(poArray->m_oMutex);
     767           0 :                 *jobStruct->pbGlobalStatus = false;
     768           0 :                 break;
     769             :             }
     770             : 
     771       10665 :             bool bIsEmpty = false;
     772       10665 :             bool success = poArray->LoadTileData(tileIndices,
     773             :                                                  true,  // use mutex
     774             :                                                  psDecompressor, abyRawTileData,
     775             :                                                  abyTmpRawTileData,
     776             :                                                  abyDecodedTileData, bIsEmpty);
     777             : 
     778       10559 :             std::lock_guard<std::mutex> oLock(poArray->m_oMutex);
     779       10672 :             if (!success)
     780             :             {
     781           0 :                 *jobStruct->pbGlobalStatus = false;
     782           0 :                 break;
     783             :             }
     784             : 
     785       21344 :             CachedTile cachedTile;
     786       10672 :             if (!bIsEmpty)
     787             :             {
     788       10670 :                 if (!abyDecodedTileData.empty())
     789           0 :                     std::swap(cachedTile.abyDecoded, abyDecodedTileData);
     790             :                 else
     791       10670 :                     std::swap(cachedTile.abyDecoded, abyRawTileData);
     792             :             }
     793       10672 :             poArray->m_oMapTileIndexToCachedTile[nTileIdx] =
     794       21344 :                 std::move(cachedTile);
     795             :         }
     796             : 
     797          16 :         std::lock_guard<std::mutex> oLock(poArray->m_oMutex);
     798          16 :         (*jobStruct->pnRemainingThreads)--;
     799             :     };
     800             : 
     801             :     // Start jobs
     802          20 :     for (int i = 0; i < nThreads; i++)
     803             :     {
     804          16 :         if (!wtp->SubmitJob(JobFunc, &asJobStructs[i]))
     805             :         {
     806           0 :             std::lock_guard<std::mutex> oLock(m_oMutex);
     807           0 :             bGlobalStatus = false;
     808           0 :             nRemainingThreads = i;
     809           0 :             break;
     810             :         }
     811             :     }
     812             : 
     813             :     // Wait for all jobs to be finished
     814             :     while (true)
     815             :     {
     816             :         {
     817          15 :             std::lock_guard<std::mutex> oLock(m_oMutex);
     818          15 :             if (nRemainingThreads == 0)
     819           4 :                 break;
     820             :         }
     821          11 :         wtp->WaitEvent();
     822          11 :     }
     823             : 
     824           4 :     return bGlobalStatus;
     825             : }
     826             : 
     827             : /************************************************************************/
     828             : /*                    ZarrV2Array::FlushDirtyTile()                     */
     829             : /************************************************************************/
     830             : 
     831       19916 : bool ZarrV2Array::FlushDirtyTile() const
     832             : {
     833       19916 :     if (!m_bDirtyTile)
     834        7846 :         return true;
     835       12070 :     m_bDirtyTile = false;
     836             : 
     837       24140 :     std::string osFilename = BuildTileFilename(m_anCachedTiledIndices.data());
     838             : 
     839             :     const size_t nSourceSize =
     840       12070 :         m_aoDtypeElts.back().nativeOffset + m_aoDtypeElts.back().nativeSize;
     841             :     const auto &abyTile =
     842       12070 :         m_abyDecodedTileData.empty() ? m_abyRawTileData : m_abyDecodedTileData;
     843             : 
     844       12070 :     if (IsEmptyTile(abyTile))
     845             :     {
     846         690 :         m_bCachedTiledEmpty = true;
     847             : 
     848             :         VSIStatBufL sStat;
     849         690 :         if (VSIStatL(osFilename.c_str(), &sStat) == 0)
     850             :         {
     851         108 :             CPLDebugOnly(ZARR_DEBUG_KEY,
     852             :                          "Deleting tile %s that has now empty content",
     853             :                          osFilename.c_str());
     854         108 :             return VSIUnlink(osFilename.c_str()) == 0;
     855             :         }
     856         582 :         return true;
     857             :     }
     858             : 
     859       11380 :     if (!m_abyDecodedTileData.empty())
     860             :     {
     861          20 :         const size_t nDTSize = m_oType.GetSize();
     862          20 :         const size_t nValues = m_abyDecodedTileData.size() / nDTSize;
     863          20 :         GByte *pDst = &m_abyRawTileData[0];
     864          20 :         const GByte *pSrc = m_abyDecodedTileData.data();
     865         140 :         for (size_t i = 0; i < nValues;
     866         120 :              i++, pDst += nSourceSize, pSrc += nDTSize)
     867             :         {
     868         120 :             EncodeElt(m_aoDtypeElts, pSrc, pDst);
     869             :         }
     870             :     }
     871             : 
     872       11380 :     if (m_bFortranOrder && !m_aoDims.empty())
     873             :     {
     874          20 :         BlockTranspose(m_abyRawTileData, m_abyTmpRawTileData, false);
     875          20 :         std::swap(m_abyRawTileData, m_abyTmpRawTileData);
     876             :     }
     877             : 
     878       11380 :     size_t nRawDataSize = m_abyRawTileData.size();
     879       11383 :     for (const auto &oFilter : m_oFiltersArray)
     880             :     {
     881           8 :         const auto osFilterId = oFilter["id"].ToString();
     882           4 :         if (osFilterId == "quantize" || osFilterId == "fixedscaleoffset")
     883             :         {
     884           1 :             CPLError(CE_Failure, CPLE_NotSupported,
     885             :                      "%s filter not supported for writing", osFilterId.c_str());
     886           1 :             return false;
     887             :         }
     888             :         const auto psFilterCompressor =
     889           3 :             EQUAL(osFilterId.c_str(), "shuffle")
     890           3 :                 ? ZarrGetShuffleCompressor()
     891           2 :                 : CPLGetCompressor(osFilterId.c_str());
     892           3 :         CPLAssert(psFilterCompressor);
     893             : 
     894           3 :         CPLStringList aosOptions;
     895           9 :         for (const auto &obj : oFilter.GetChildren())
     896             :         {
     897          12 :             aosOptions.SetNameValue(obj.GetName().c_str(),
     898          18 :                                     obj.ToString().c_str());
     899             :         }
     900           3 :         void *out_buffer = &m_abyTmpRawTileData[0];
     901           3 :         size_t nOutSize = m_abyTmpRawTileData.size();
     902           3 :         if (!psFilterCompressor->pfnFunc(
     903           3 :                 m_abyRawTileData.data(), nRawDataSize, &out_buffer, &nOutSize,
     904           3 :                 aosOptions.List(), psFilterCompressor->user_data))
     905             :         {
     906           0 :             CPLError(CE_Failure, CPLE_AppDefined,
     907             :                      "Filter %s for tile %s failed", osFilterId.c_str(),
     908             :                      osFilename.c_str());
     909           0 :             return false;
     910             :         }
     911             : 
     912           3 :         nRawDataSize = nOutSize;
     913           3 :         std::swap(m_abyRawTileData, m_abyTmpRawTileData);
     914             :     }
     915             : 
     916       11379 :     if (m_osDimSeparator == "/")
     917             :     {
     918          20 :         std::string osDir = CPLGetDirnameSafe(osFilename.c_str());
     919             :         VSIStatBufL sStat;
     920          20 :         if (VSIStatL(osDir.c_str(), &sStat) != 0)
     921             :         {
     922          20 :             if (VSIMkdirRecursive(osDir.c_str(), 0755) != 0)
     923             :             {
     924           0 :                 CPLError(CE_Failure, CPLE_AppDefined,
     925             :                          "Cannot create directory %s", osDir.c_str());
     926           0 :                 return false;
     927             :             }
     928             :         }
     929             :     }
     930             : 
     931       11379 :     if (m_psCompressor == nullptr && m_psDecompressor != nullptr)
     932             :     {
     933             :         // Case of imagecodecs_tiff
     934             : 
     935           1 :         CPLError(CE_Failure, CPLE_NotSupported,
     936             :                  "Only decompression supported for '%s' compression method",
     937             :                  m_osDecompressorId.c_str());
     938           1 :         return false;
     939             :     }
     940             : 
     941       11378 :     VSILFILE *fp = VSIFOpenL(osFilename.c_str(), "wb");
     942       11378 :     if (fp == nullptr)
     943             :     {
     944           0 :         CPLError(CE_Failure, CPLE_AppDefined, "Cannot create tile %s",
     945             :                  osFilename.c_str());
     946           0 :         return false;
     947             :     }
     948             : 
     949       11378 :     bool bRet = true;
     950       11378 :     if (m_psCompressor == nullptr)
     951             :     {
     952        5928 :         if (VSIFWriteL(m_abyRawTileData.data(), 1, nRawDataSize, fp) !=
     953             :             nRawDataSize)
     954             :         {
     955           2 :             CPLError(CE_Failure, CPLE_AppDefined,
     956             :                      "Could not write tile %s correctly", osFilename.c_str());
     957           2 :             bRet = false;
     958             :         }
     959             :     }
     960             :     else
     961             :     {
     962       10900 :         std::vector<GByte> abyCompressedData;
     963             :         try
     964             :         {
     965        5450 :             constexpr size_t MIN_BUF_SIZE = 64;  // somewhat arbitrary
     966        5450 :             abyCompressedData.resize(static_cast<size_t>(
     967        5450 :                 MIN_BUF_SIZE + nRawDataSize + nRawDataSize / 3));
     968             :         }
     969           0 :         catch (const std::exception &)
     970             :         {
     971           0 :             CPLError(CE_Failure, CPLE_OutOfMemory,
     972             :                      "Cannot allocate memory for tile %s", osFilename.c_str());
     973           0 :             bRet = false;
     974             :         }
     975             : 
     976        5450 :         if (bRet)
     977             :         {
     978        5450 :             void *out_buffer = &abyCompressedData[0];
     979        5450 :             size_t out_size = abyCompressedData.size();
     980       10900 :             CPLStringList aosOptions;
     981        5450 :             const auto &compressorConfig = m_oCompressorJSon;
     982       16350 :             for (const auto &obj : compressorConfig.GetChildren())
     983             :             {
     984       21800 :                 aosOptions.SetNameValue(obj.GetName().c_str(),
     985       32700 :                                         obj.ToString().c_str());
     986             :             }
     987        5450 :             if (EQUAL(m_psCompressor->pszId, "blosc") &&
     988           0 :                 m_oType.GetClass() == GEDTC_NUMERIC)
     989             :             {
     990             :                 aosOptions.SetNameValue(
     991             :                     "TYPESIZE",
     992             :                     CPLSPrintf("%d", GDALGetDataTypeSizeBytes(
     993             :                                          GDALGetNonComplexDataType(
     994           0 :                                              m_oType.GetNumericDataType()))));
     995             :             }
     996             : 
     997        5450 :             if (!m_psCompressor->pfnFunc(
     998        5450 :                     m_abyRawTileData.data(), nRawDataSize, &out_buffer,
     999        5450 :                     &out_size, aosOptions.List(), m_psCompressor->user_data))
    1000             :             {
    1001           0 :                 CPLError(CE_Failure, CPLE_AppDefined,
    1002             :                          "Compression of tile %s failed", osFilename.c_str());
    1003           0 :                 bRet = false;
    1004             :             }
    1005        5450 :             abyCompressedData.resize(out_size);
    1006             :         }
    1007             : 
    1008       10900 :         if (bRet &&
    1009        5450 :             VSIFWriteL(abyCompressedData.data(), 1, abyCompressedData.size(),
    1010        5450 :                        fp) != abyCompressedData.size())
    1011             :         {
    1012           0 :             CPLError(CE_Failure, CPLE_AppDefined,
    1013             :                      "Could not write tile %s correctly", osFilename.c_str());
    1014           0 :             bRet = false;
    1015             :         }
    1016             :     }
    1017       11378 :     VSIFCloseL(fp);
    1018             : 
    1019       11378 :     return bRet;
    1020             : }
    1021             : 
    1022             : /************************************************************************/
    1023             : /*                          BuildTileFilename()                         */
    1024             : /************************************************************************/
    1025             : 
    1026       27618 : std::string ZarrV2Array::BuildTileFilename(const uint64_t *tileIndices) const
    1027             : {
    1028       27618 :     std::string osFilename;
    1029       27620 :     if (m_aoDims.empty())
    1030             :     {
    1031           4 :         osFilename = "0";
    1032             :     }
    1033             :     else
    1034             :     {
    1035       83337 :         for (size_t i = 0; i < m_aoDims.size(); ++i)
    1036             :         {
    1037       55705 :             if (!osFilename.empty())
    1038       28101 :                 osFilename += m_osDimSeparator;
    1039       55746 :             osFilename += std::to_string(tileIndices[i]);
    1040             :         }
    1041             :     }
    1042             : 
    1043       55162 :     return CPLFormFilenameSafe(CPLGetDirnameSafe(m_osFilename.c_str()).c_str(),
    1044       82756 :                                osFilename.c_str(), nullptr);
    1045             : }
    1046             : 
    1047             : /************************************************************************/
    1048             : /*                          GetDataDirectory()                          */
    1049             : /************************************************************************/
    1050             : 
    1051           2 : std::string ZarrV2Array::GetDataDirectory() const
    1052             : {
    1053           2 :     return CPLGetDirnameSafe(m_osFilename.c_str());
    1054             : }
    1055             : 
    1056             : /************************************************************************/
    1057             : /*                        GetTileIndicesFromFilename()                  */
    1058             : /************************************************************************/
    1059             : 
    1060             : CPLStringList
    1061           5 : ZarrV2Array::GetTileIndicesFromFilename(const char *pszFilename) const
    1062             : {
    1063             :     return CPLStringList(
    1064           5 :         CSLTokenizeString2(pszFilename, m_osDimSeparator.c_str(), 0));
    1065             : }
    1066             : 
    1067             : /************************************************************************/
    1068             : /*                             ParseDtype()                             */
    1069             : /************************************************************************/
    1070             : 
    1071          26 : static size_t GetAlignment(const CPLJSONObject &obj)
    1072             : {
    1073          26 :     if (obj.GetType() == CPLJSONObject::Type::String)
    1074             :     {
    1075          69 :         const auto str = obj.ToString();
    1076          23 :         if (str.size() < 3)
    1077           0 :             return 1;
    1078          23 :         const char chType = str[1];
    1079          23 :         const int nBytes = atoi(str.c_str() + 2);
    1080          23 :         if (chType == 'S')
    1081           2 :             return sizeof(char *);
    1082          21 :         if (chType == 'c' && nBytes == 8)
    1083           0 :             return sizeof(float);
    1084          21 :         if (chType == 'c' && nBytes == 16)
    1085           0 :             return sizeof(double);
    1086          21 :         return nBytes;
    1087             :     }
    1088           3 :     else if (obj.GetType() == CPLJSONObject::Type::Array)
    1089             :     {
    1090           6 :         const auto oArray = obj.ToArray();
    1091           3 :         size_t nAlignment = 1;
    1092           9 :         for (const auto &oElt : oArray)
    1093             :         {
    1094           6 :             const auto oEltArray = oElt.ToArray();
    1095          12 :             if (!oEltArray.IsValid() || oEltArray.Size() != 2 ||
    1096          12 :                 oEltArray[0].GetType() != CPLJSONObject::Type::String)
    1097             :             {
    1098           0 :                 return 1;
    1099             :             }
    1100           6 :             nAlignment = std::max(nAlignment, GetAlignment(oEltArray[1]));
    1101           6 :             if (nAlignment == sizeof(void *))
    1102           0 :                 break;
    1103             :         }
    1104           3 :         return nAlignment;
    1105             :     }
    1106           0 :     return 1;
    1107             : }
    1108             : 
    1109         695 : static GDALExtendedDataType ParseDtype(const CPLJSONObject &obj,
    1110             :                                        std::vector<DtypeElt> &elts)
    1111             : {
    1112          29 :     const auto AlignOffsetOn = [](size_t offset, size_t alignment)
    1113          29 :     { return offset + (alignment - (offset % alignment)) % alignment; };
    1114             : 
    1115             :     do
    1116             :     {
    1117         695 :         if (obj.GetType() == CPLJSONObject::Type::String)
    1118             :         {
    1119        1368 :             const auto str = obj.ToString();
    1120         684 :             char chEndianness = 0;
    1121             :             char chType;
    1122             :             int nBytes;
    1123         684 :             DtypeElt elt;
    1124         684 :             if (str.size() < 3)
    1125           3 :                 break;
    1126         681 :             chEndianness = str[0];
    1127         681 :             chType = str[1];
    1128         681 :             nBytes = atoi(str.c_str() + 2);
    1129         681 :             if (nBytes <= 0 || nBytes >= 1000)
    1130             :                 break;
    1131             : 
    1132         679 :             elt.needByteSwapping = false;
    1133         679 :             if ((nBytes > 1 && chType != 'S') || chType == 'U')
    1134             :             {
    1135         373 :                 if (chEndianness == '<')
    1136         330 :                     elt.needByteSwapping = (CPL_IS_LSB == 0);
    1137          43 :                 else if (chEndianness == '>')
    1138          43 :                     elt.needByteSwapping = (CPL_IS_LSB != 0);
    1139             :             }
    1140             : 
    1141             :             GDALDataType eDT;
    1142         679 :             if (!elts.empty())
    1143             :             {
    1144          11 :                 elt.nativeOffset =
    1145          11 :                     elts.back().nativeOffset + elts.back().nativeSize;
    1146             :             }
    1147         679 :             elt.nativeSize = nBytes;
    1148         679 :             if (chType == 'b' && nBytes == 1)  // boolean
    1149             :             {
    1150          66 :                 elt.nativeType = DtypeElt::NativeType::BOOLEAN;
    1151          66 :                 eDT = GDT_Byte;
    1152             :             }
    1153         613 :             else if (chType == 'u' && nBytes == 1)
    1154             :             {
    1155         219 :                 elt.nativeType = DtypeElt::NativeType::UNSIGNED_INT;
    1156         219 :                 eDT = GDT_Byte;
    1157             :             }
    1158         394 :             else if (chType == 'i' && nBytes == 1)
    1159             :             {
    1160          12 :                 elt.nativeType = DtypeElt::NativeType::SIGNED_INT;
    1161          12 :                 eDT = GDT_Int8;
    1162             :             }
    1163         382 :             else if (chType == 'i' && nBytes == 2)
    1164             :             {
    1165          20 :                 elt.nativeType = DtypeElt::NativeType::SIGNED_INT;
    1166          20 :                 eDT = GDT_Int16;
    1167             :             }
    1168         362 :             else if (chType == 'i' && nBytes == 4)
    1169             :             {
    1170          26 :                 elt.nativeType = DtypeElt::NativeType::SIGNED_INT;
    1171          26 :                 eDT = GDT_Int32;
    1172             :             }
    1173         336 :             else if (chType == 'i' && nBytes == 8)
    1174             :             {
    1175          15 :                 elt.nativeType = DtypeElt::NativeType::SIGNED_INT;
    1176          15 :                 eDT = GDT_Int64;
    1177             :             }
    1178         321 :             else if (chType == 'u' && nBytes == 2)
    1179             :             {
    1180          29 :                 elt.nativeType = DtypeElt::NativeType::UNSIGNED_INT;
    1181          29 :                 eDT = GDT_UInt16;
    1182             :             }
    1183         292 :             else if (chType == 'u' && nBytes == 4)
    1184             :             {
    1185          18 :                 elt.nativeType = DtypeElt::NativeType::UNSIGNED_INT;
    1186          18 :                 eDT = GDT_UInt32;
    1187             :             }
    1188         274 :             else if (chType == 'u' && nBytes == 8)
    1189             :             {
    1190          14 :                 elt.nativeType = DtypeElt::NativeType::UNSIGNED_INT;
    1191          14 :                 eDT = GDT_UInt64;
    1192             :             }
    1193         260 :             else if (chType == 'f' && nBytes == 2)
    1194             :             {
    1195             :                 // elt.nativeType = DtypeElt::NativeType::IEEEFP;
    1196             :                 // elt.gdalTypeIsApproxOfNative = true;
    1197             :                 // eDT = GDT_Float32;
    1198           3 :                 elt.nativeType = DtypeElt::NativeType::IEEEFP;
    1199           3 :                 eDT = GDT_Float16;
    1200             :             }
    1201         257 :             else if (chType == 'f' && nBytes == 4)
    1202             :             {
    1203          44 :                 elt.nativeType = DtypeElt::NativeType::IEEEFP;
    1204          44 :                 eDT = GDT_Float32;
    1205             :             }
    1206         213 :             else if (chType == 'f' && nBytes == 8)
    1207             :             {
    1208         175 :                 elt.nativeType = DtypeElt::NativeType::IEEEFP;
    1209         175 :                 eDT = GDT_Float64;
    1210             :             }
    1211          38 :             else if (chType == 'c' && nBytes == 8)
    1212             :             {
    1213          11 :                 elt.nativeType = DtypeElt::NativeType::COMPLEX_IEEEFP;
    1214          11 :                 eDT = GDT_CFloat32;
    1215             :             }
    1216          27 :             else if (chType == 'c' && nBytes == 16)
    1217             :             {
    1218          12 :                 elt.nativeType = DtypeElt::NativeType::COMPLEX_IEEEFP;
    1219          12 :                 eDT = GDT_CFloat64;
    1220             :             }
    1221          15 :             else if (chType == 'S')
    1222             :             {
    1223           9 :                 elt.nativeType = DtypeElt::NativeType::STRING_ASCII;
    1224           9 :                 elt.gdalType = GDALExtendedDataType::CreateString(nBytes);
    1225           9 :                 elt.gdalSize = elt.gdalType.GetSize();
    1226           9 :                 elts.emplace_back(elt);
    1227           9 :                 return GDALExtendedDataType::CreateString(nBytes);
    1228             :             }
    1229           6 :             else if (chType == 'U')
    1230             :             {
    1231           5 :                 elt.nativeType = DtypeElt::NativeType::STRING_UNICODE;
    1232             :                 // the dtype declaration is number of UCS4 characters. Store it
    1233             :                 // as bytes
    1234           5 :                 elt.nativeSize *= 4;
    1235             :                 // We can really map UCS4 size to UTF-8
    1236           5 :                 elt.gdalType = GDALExtendedDataType::CreateString();
    1237           5 :                 elt.gdalSize = elt.gdalType.GetSize();
    1238           5 :                 elts.emplace_back(elt);
    1239           5 :                 return GDALExtendedDataType::CreateString();
    1240             :             }
    1241             :             else
    1242           1 :                 break;
    1243         664 :             elt.gdalType = GDALExtendedDataType::Create(eDT);
    1244         664 :             elt.gdalSize = elt.gdalType.GetSize();
    1245         664 :             elts.emplace_back(elt);
    1246         664 :             return GDALExtendedDataType::Create(eDT);
    1247             :         }
    1248          11 :         else if (obj.GetType() == CPLJSONObject::Type::Array)
    1249             :         {
    1250           9 :             bool error = false;
    1251           9 :             const auto oArray = obj.ToArray();
    1252           9 :             std::vector<std::unique_ptr<GDALEDTComponent>> comps;
    1253           9 :             size_t offset = 0;
    1254           9 :             size_t alignmentMax = 1;
    1255          29 :             for (const auto &oElt : oArray)
    1256             :             {
    1257          20 :                 const auto oEltArray = oElt.ToArray();
    1258          40 :                 if (!oEltArray.IsValid() || oEltArray.Size() != 2 ||
    1259          40 :                     oEltArray[0].GetType() != CPLJSONObject::Type::String)
    1260             :                 {
    1261           0 :                     error = true;
    1262           0 :                     break;
    1263             :                 }
    1264          20 :                 GDALExtendedDataType subDT = ParseDtype(oEltArray[1], elts);
    1265          35 :                 if (subDT.GetClass() == GEDTC_NUMERIC &&
    1266          15 :                     subDT.GetNumericDataType() == GDT_Unknown)
    1267             :                 {
    1268           0 :                     error = true;
    1269           0 :                     break;
    1270             :                 }
    1271             : 
    1272          40 :                 const std::string osName = oEltArray[0].ToString();
    1273             :                 // Add padding for alignment
    1274          20 :                 const size_t alignmentSub = GetAlignment(oEltArray[1]);
    1275          20 :                 assert(alignmentSub);
    1276          20 :                 alignmentMax = std::max(alignmentMax, alignmentSub);
    1277          20 :                 offset = AlignOffsetOn(offset, alignmentSub);
    1278          40 :                 comps.emplace_back(std::unique_ptr<GDALEDTComponent>(
    1279          40 :                     new GDALEDTComponent(osName, offset, subDT)));
    1280          20 :                 offset += subDT.GetSize();
    1281             :             }
    1282           9 :             if (error)
    1283           0 :                 break;
    1284           9 :             size_t nTotalSize = offset;
    1285           9 :             nTotalSize = AlignOffsetOn(nTotalSize, alignmentMax);
    1286          18 :             return GDALExtendedDataType::Create(obj.ToString(), nTotalSize,
    1287          18 :                                                 std::move(comps));
    1288             :         }
    1289             :     } while (false);
    1290           8 :     CPLError(CE_Failure, CPLE_AppDefined,
    1291             :              "Invalid or unsupported format for dtype: %s",
    1292          16 :              obj.ToString().c_str());
    1293           8 :     return GDALExtendedDataType::Create(GDT_Unknown);
    1294             : }
    1295             : 
    1296         687 : static void SetGDALOffset(const GDALExtendedDataType &dt,
    1297             :                           const size_t nBaseOffset, std::vector<DtypeElt> &elts,
    1298             :                           size_t &iCurElt)
    1299             : {
    1300         687 :     if (dt.GetClass() == GEDTC_COMPOUND)
    1301             :     {
    1302           9 :         const auto &comps = dt.GetComponents();
    1303          29 :         for (const auto &comp : comps)
    1304             :         {
    1305          20 :             const size_t nBaseOffsetSub = nBaseOffset + comp->GetOffset();
    1306          20 :             SetGDALOffset(comp->GetType(), nBaseOffsetSub, elts, iCurElt);
    1307             :         }
    1308             :     }
    1309             :     else
    1310             :     {
    1311         678 :         elts[iCurElt].gdalOffset = nBaseOffset;
    1312         678 :         iCurElt++;
    1313             :     }
    1314         687 : }
    1315             : 
    1316             : /************************************************************************/
    1317             : /*                     ZarrV2Group::LoadArray()                         */
    1318             : /************************************************************************/
    1319             : 
    1320             : std::shared_ptr<ZarrArray>
    1321         693 : ZarrV2Group::LoadArray(const std::string &osArrayName,
    1322             :                        const std::string &osZarrayFilename,
    1323             :                        const CPLJSONObject &oRoot, bool bLoadedFromZMetadata,
    1324             :                        const CPLJSONObject &oAttributesIn) const
    1325             : {
    1326             :     // Add osZarrayFilename to m_poSharedResource during the scope
    1327             :     // of this function call.
    1328         693 :     ZarrSharedResource::SetFilenameAdder filenameAdder(m_poSharedResource,
    1329        1386 :                                                        osZarrayFilename);
    1330         693 :     if (!filenameAdder.ok())
    1331           2 :         return nullptr;
    1332             : 
    1333        2073 :     const auto osFormat = oRoot["zarr_format"].ToString();
    1334         691 :     if (osFormat != "2")
    1335             :     {
    1336           3 :         CPLError(CE_Failure, CPLE_NotSupported,
    1337             :                  "Invalid value for zarr_format: %s", osFormat.c_str());
    1338           3 :         return nullptr;
    1339             :     }
    1340             : 
    1341         688 :     bool bFortranOrder = false;
    1342         688 :     const char *orderKey = "order";
    1343        2064 :     const auto osOrder = oRoot[orderKey].ToString();
    1344         688 :     if (osOrder == "C")
    1345             :     {
    1346             :         // ok
    1347             :     }
    1348          33 :     else if (osOrder == "F")
    1349             :     {
    1350          30 :         bFortranOrder = true;
    1351             :     }
    1352             :     else
    1353             :     {
    1354           3 :         CPLError(CE_Failure, CPLE_NotSupported, "Invalid value for %s",
    1355             :                  orderKey);
    1356           3 :         return nullptr;
    1357             :     }
    1358             : 
    1359        2055 :     const auto oShape = oRoot["shape"].ToArray();
    1360         685 :     if (!oShape.IsValid())
    1361             :     {
    1362           3 :         CPLError(CE_Failure, CPLE_AppDefined, "shape missing or not an array");
    1363           3 :         return nullptr;
    1364             :     }
    1365             : 
    1366         682 :     const char *chunksKey = "chunks";
    1367        2046 :     const auto oChunks = oRoot[chunksKey].ToArray();
    1368         682 :     if (!oChunks.IsValid())
    1369             :     {
    1370           3 :         CPLError(CE_Failure, CPLE_AppDefined, "%s missing or not an array",
    1371             :                  chunksKey);
    1372           3 :         return nullptr;
    1373             :     }
    1374             : 
    1375         679 :     if (oShape.Size() != oChunks.Size())
    1376             :     {
    1377           2 :         CPLError(CE_Failure, CPLE_AppDefined,
    1378             :                  "shape and chunks arrays are of different size");
    1379           2 :         return nullptr;
    1380             :     }
    1381             : 
    1382        1354 :     CPLJSONObject oAttributes(oAttributesIn);
    1383         677 :     if (!bLoadedFromZMetadata)
    1384             :     {
    1385         782 :         CPLJSONDocument oDoc;
    1386             :         const std::string osZattrsFilename(CPLFormFilenameSafe(
    1387         391 :             CPLGetDirnameSafe(osZarrayFilename.c_str()).c_str(), ".zattrs",
    1388         782 :             nullptr));
    1389         782 :         CPLErrorStateBackuper oErrorStateBackuper(CPLQuietErrorHandler);
    1390         391 :         if (oDoc.Load(osZattrsFilename))
    1391             :         {
    1392         148 :             oAttributes = oDoc.GetRoot();
    1393             :         }
    1394             :     }
    1395             : 
    1396             :     // Deep-clone of oAttributes
    1397             :     {
    1398         677 :         CPLJSONDocument oTmpDoc;
    1399         677 :         oTmpDoc.SetRoot(oAttributes);
    1400         677 :         CPL_IGNORE_RET_VAL(oTmpDoc.LoadMemory(oTmpDoc.SaveAsString()));
    1401         677 :         oAttributes = oTmpDoc.GetRoot();
    1402             :     }
    1403             : 
    1404        1354 :     std::vector<std::shared_ptr<GDALDimension>> aoDims;
    1405        1728 :     for (int i = 0; i < oShape.Size(); ++i)
    1406             :     {
    1407        1052 :         const auto nSize = static_cast<GUInt64>(oShape[i].ToLong());
    1408        1052 :         if (nSize == 0)
    1409             :         {
    1410           1 :             CPLError(CE_Failure, CPLE_AppDefined, "Invalid content for shape");
    1411           1 :             return nullptr;
    1412             :         }
    1413        1051 :         aoDims.emplace_back(std::make_shared<ZarrDimension>(
    1414        1051 :             m_poSharedResource,
    1415        2102 :             std::dynamic_pointer_cast<ZarrGroupBase>(m_pSelf.lock()),
    1416        2102 :             std::string(), CPLSPrintf("dim%d", i), std::string(), std::string(),
    1417        1051 :             nSize));
    1418             :     }
    1419             : 
    1420             :     // XArray extension
    1421        2028 :     const auto arrayDimensionsObj = oAttributes["_ARRAY_DIMENSIONS"];
    1422             : 
    1423             :     const auto FindDimension =
    1424         515 :         [this, &aoDims, bLoadedFromZMetadata, &osArrayName,
    1425             :          &oAttributes](const std::string &osDimName,
    1426        5872 :                        std::shared_ptr<GDALDimension> &poDim, int i)
    1427             :     {
    1428         515 :         auto oIter = m_oMapDimensions.find(osDimName);
    1429         515 :         if (oIter != m_oMapDimensions.end())
    1430             :         {
    1431         250 :             if (m_bDimSizeInUpdate ||
    1432         124 :                 oIter->second->GetSize() == poDim->GetSize())
    1433             :             {
    1434         126 :                 poDim = oIter->second;
    1435         126 :                 return true;
    1436             :             }
    1437             :             else
    1438             :             {
    1439           0 :                 CPLError(CE_Warning, CPLE_AppDefined,
    1440             :                          "Size of _ARRAY_DIMENSIONS[%d] different "
    1441             :                          "from the one of shape",
    1442             :                          i);
    1443           0 :                 return false;
    1444             :             }
    1445             :         }
    1446             : 
    1447             :         // Try to load the indexing variable.
    1448             : 
    1449             :         // If loading from zmetadata, we should have normally
    1450             :         // already loaded the dimension variables, unless they
    1451             :         // are in a upper level.
    1452         571 :         if (bLoadedFromZMetadata && osArrayName != osDimName &&
    1453         571 :             m_oMapMDArrays.find(osDimName) == m_oMapMDArrays.end())
    1454             :         {
    1455         182 :             auto poParent = m_poParent.lock();
    1456         198 :             while (poParent != nullptr)
    1457             :             {
    1458          22 :                 oIter = poParent->m_oMapDimensions.find(osDimName);
    1459          28 :                 if (oIter != poParent->m_oMapDimensions.end() &&
    1460           6 :                     oIter->second->GetSize() == poDim->GetSize())
    1461             :                 {
    1462           6 :                     poDim = oIter->second;
    1463           6 :                     return true;
    1464             :                 }
    1465          16 :                 poParent = poParent->m_poParent.lock();
    1466             :             }
    1467             :         }
    1468             : 
    1469             :         // Not loading from zmetadata, and not in m_oMapMDArrays,
    1470             :         // then stat() the indexing variable.
    1471         282 :         else if (!bLoadedFromZMetadata && osArrayName != osDimName &&
    1472         282 :                  m_oMapMDArrays.find(osDimName) == m_oMapMDArrays.end())
    1473             :         {
    1474          75 :             std::string osDirName = m_osDirectoryName;
    1475             :             while (true)
    1476             :             {
    1477         163 :                 if (CPLHasPathTraversal(osDimName.c_str()))
    1478             :                 {
    1479           0 :                     CPLError(CE_Failure, CPLE_AppDefined,
    1480             :                              "Path traversal detected in %s",
    1481             :                              osDimName.c_str());
    1482           0 :                     return false;
    1483             :                 }
    1484             :                 const std::string osArrayFilenameDim = CPLFormFilenameSafe(
    1485         163 :                     CPLFormFilenameSafe(osDirName.c_str(), osDimName.c_str(),
    1486             :                                         nullptr)
    1487             :                         .c_str(),
    1488         163 :                     ".zarray", nullptr);
    1489             :                 VSIStatBufL sStat;
    1490         163 :                 if (VSIStatL(osArrayFilenameDim.c_str(), &sStat) == 0)
    1491             :                 {
    1492         108 :                     CPLJSONDocument oDoc;
    1493          54 :                     if (oDoc.Load(osArrayFilenameDim))
    1494             :                     {
    1495          54 :                         LoadArray(osDimName, osArrayFilenameDim, oDoc.GetRoot(),
    1496         108 :                                   false, CPLJSONObject());
    1497             :                     }
    1498             :                 }
    1499             :                 else
    1500             :                 {
    1501             :                     // Recurse to upper level for datasets such as
    1502             :                     // /vsis3/hrrrzarr/sfc/20210809/20210809_00z_anl.zarr/0.1_sigma_level/HAIL_max_fcst/0.1_sigma_level/HAIL_max_fcst
    1503             :                     std::string osDirNameNew =
    1504         109 :                         CPLGetPathSafe(osDirName.c_str());
    1505         109 :                     if (!osDirNameNew.empty() && osDirNameNew != osDirName)
    1506             :                     {
    1507          88 :                         osDirName = std::move(osDirNameNew);
    1508          88 :                         continue;
    1509             :                     }
    1510             :                 }
    1511          75 :                 break;
    1512          88 :             }
    1513             :         }
    1514             : 
    1515         383 :         oIter = m_oMapDimensions.find(osDimName);
    1516         403 :         if (oIter != m_oMapDimensions.end() &&
    1517          20 :             oIter->second->GetSize() == poDim->GetSize())
    1518             :         {
    1519          20 :             poDim = oIter->second;
    1520          20 :             return true;
    1521             :         }
    1522             : 
    1523         726 :         std::string osType;
    1524         726 :         std::string osDirection;
    1525         363 :         if (aoDims.size() == 1 && osArrayName == osDimName)
    1526             :         {
    1527         132 :             ZarrArray::GetDimensionTypeDirection(oAttributes, osType,
    1528             :                                                  osDirection);
    1529             :         }
    1530             : 
    1531             :         auto poDimLocal = std::make_shared<ZarrDimension>(
    1532         363 :             m_poSharedResource,
    1533         726 :             std::dynamic_pointer_cast<ZarrGroupBase>(m_pSelf.lock()),
    1534         726 :             GetFullName(), osDimName, osType, osDirection, poDim->GetSize());
    1535         363 :         poDimLocal->SetXArrayDimension();
    1536         363 :         m_oMapDimensions[osDimName] = poDimLocal;
    1537         363 :         poDim = poDimLocal;
    1538         363 :         return true;
    1539         676 :     };
    1540             : 
    1541         676 :     if (arrayDimensionsObj.GetType() == CPLJSONObject::Type::Array)
    1542             :     {
    1543         690 :         const auto arrayDims = arrayDimensionsObj.ToArray();
    1544         345 :         if (arrayDims.Size() == oShape.Size())
    1545             :         {
    1546         345 :             bool ok = true;
    1547         860 :             for (int i = 0; i < oShape.Size(); ++i)
    1548             :             {
    1549         515 :                 if (arrayDims[i].GetType() == CPLJSONObject::Type::String)
    1550             :                 {
    1551        1030 :                     const auto osDimName = arrayDims[i].ToString();
    1552         515 :                     ok &= FindDimension(osDimName, aoDims[i], i);
    1553             :                 }
    1554             :             }
    1555         345 :             if (ok)
    1556             :             {
    1557         345 :                 oAttributes.Delete("_ARRAY_DIMENSIONS");
    1558             :             }
    1559             :         }
    1560             :         else
    1561             :         {
    1562           0 :             CPLError(
    1563             :                 CE_Warning, CPLE_AppDefined,
    1564             :                 "Size of _ARRAY_DIMENSIONS different from the one of shape");
    1565             :         }
    1566             :     }
    1567             : 
    1568             :     // _NCZARR_ARRAY extension
    1569        2028 :     const auto nczarrArrayDimrefs = oRoot["_NCZARR_ARRAY"]["dimrefs"].ToArray();
    1570         676 :     if (nczarrArrayDimrefs.IsValid())
    1571             :     {
    1572          42 :         const auto arrayDims = nczarrArrayDimrefs.ToArray();
    1573          21 :         if (arrayDims.Size() == oShape.Size())
    1574             :         {
    1575             :             auto poRG =
    1576          42 :                 std::dynamic_pointer_cast<ZarrGroupBase>(m_pSelf.lock());
    1577          21 :             CPLAssert(poRG != nullptr);
    1578             :             while (true)
    1579             :             {
    1580          48 :                 auto poNewRG = poRG->m_poParent.lock();
    1581          48 :                 if (poNewRG == nullptr)
    1582          21 :                     break;
    1583          27 :                 poRG = std::move(poNewRG);
    1584          27 :             }
    1585             : 
    1586          49 :             for (int i = 0; i < oShape.Size(); ++i)
    1587             :             {
    1588          28 :                 if (arrayDims[i].GetType() == CPLJSONObject::Type::String)
    1589             :                 {
    1590          84 :                     const auto osDimFullpath = arrayDims[i].ToString();
    1591             :                     const std::string osArrayFullname =
    1592          56 :                         (GetFullName() != "/" ? GetFullName() : std::string()) +
    1593          56 :                         '/' + osArrayName;
    1594          48 :                     if (aoDims.size() == 1 &&
    1595          20 :                         (osDimFullpath == osArrayFullname ||
    1596          34 :                          osDimFullpath == "/" + osArrayFullname))
    1597             :                     {
    1598             :                         // If this is an indexing variable, then fetch the
    1599             :                         // dimension type and direction, and patch the dimension
    1600          28 :                         std::string osType;
    1601          28 :                         std::string osDirection;
    1602          14 :                         ZarrArray::GetDimensionTypeDirection(
    1603             :                             oAttributes, osType, osDirection);
    1604             : 
    1605             :                         auto poDimLocal = std::make_shared<ZarrDimension>(
    1606          14 :                             m_poSharedResource,
    1607          28 :                             std::dynamic_pointer_cast<ZarrGroupBase>(
    1608          14 :                                 m_pSelf.lock()),
    1609          14 :                             GetFullName(), osArrayName, osType, osDirection,
    1610          28 :                             aoDims[i]->GetSize());
    1611          14 :                         aoDims[i] = poDimLocal;
    1612             : 
    1613          14 :                         m_oMapDimensions[osArrayName] = std::move(poDimLocal);
    1614             :                     }
    1615          14 :                     else if (auto poDim =
    1616          28 :                                  poRG->OpenDimensionFromFullname(osDimFullpath))
    1617             :                     {
    1618          13 :                         if (poDim->GetSize() != aoDims[i]->GetSize())
    1619             :                         {
    1620           1 :                             CPLError(CE_Failure, CPLE_AppDefined,
    1621             :                                      "Inconsistency in size between NCZarr "
    1622             :                                      "dimension %s and regular dimension",
    1623             :                                      osDimFullpath.c_str());
    1624             :                         }
    1625             :                         else
    1626             :                         {
    1627          12 :                             aoDims[i] = std::move(poDim);
    1628             :                         }
    1629             :                     }
    1630             :                     else
    1631             :                     {
    1632           1 :                         CPLError(CE_Failure, CPLE_AppDefined,
    1633             :                                  "Cannot find NCZarr dimension %s",
    1634             :                                  osDimFullpath.c_str());
    1635             :                     }
    1636             :                 }
    1637             :             }
    1638             :         }
    1639             :         else
    1640             :         {
    1641           0 :             CPLError(CE_Warning, CPLE_AppDefined,
    1642             :                      "Size of _NCZARR_ARRAY.dimrefs different from the one of "
    1643             :                      "shape");
    1644             :         }
    1645             :     }
    1646             : 
    1647         676 :     constexpr const char *dtypeKey = "dtype";
    1648        2028 :     auto oDtype = oRoot[dtypeKey];
    1649         676 :     if (!oDtype.IsValid())
    1650             :     {
    1651           1 :         CPLError(CE_Failure, CPLE_NotSupported, "%s missing", dtypeKey);
    1652           1 :         return nullptr;
    1653             :     }
    1654        1350 :     std::vector<DtypeElt> aoDtypeElts;
    1655        1350 :     const auto oType = ParseDtype(oDtype, aoDtypeElts);
    1656        1332 :     if (oType.GetClass() == GEDTC_NUMERIC &&
    1657         657 :         oType.GetNumericDataType() == GDT_Unknown)
    1658           8 :         return nullptr;
    1659         667 :     size_t iCurElt = 0;
    1660         667 :     SetGDALOffset(oType, 0, aoDtypeElts, iCurElt);
    1661             : 
    1662        1334 :     std::vector<GUInt64> anBlockSize;
    1663         667 :     if (!ZarrArray::ParseChunkSize(oChunks, oType, anBlockSize))
    1664           2 :         return nullptr;
    1665             : 
    1666        1995 :     std::string osDimSeparator = oRoot["dimension_separator"].ToString();
    1667         665 :     if (osDimSeparator.empty())
    1668         655 :         osDimSeparator = ".";
    1669             : 
    1670        1330 :     std::vector<GByte> abyNoData;
    1671             : 
    1672             :     struct NoDataFreer
    1673             :     {
    1674             :         std::vector<GByte> &m_abyNodata;
    1675             :         const GDALExtendedDataType &m_oType;
    1676             : 
    1677         665 :         NoDataFreer(std::vector<GByte> &abyNoDataIn,
    1678             :                     const GDALExtendedDataType &oTypeIn)
    1679         665 :             : m_abyNodata(abyNoDataIn), m_oType(oTypeIn)
    1680             :         {
    1681         665 :         }
    1682             : 
    1683         665 :         ~NoDataFreer()
    1684         665 :         {
    1685         665 :             if (!m_abyNodata.empty())
    1686         112 :                 m_oType.FreeDynamicMemory(&m_abyNodata[0]);
    1687         665 :         }
    1688             :     };
    1689             : 
    1690        1330 :     NoDataFreer NoDataFreer(abyNoData, oType);
    1691             : 
    1692        1995 :     auto oFillValue = oRoot["fill_value"];
    1693         665 :     auto eFillValueType = oFillValue.GetType();
    1694             : 
    1695             :     // Normally arrays are not supported, but that's what NCZarr 4.8.0 outputs
    1696         666 :     if (eFillValueType == CPLJSONObject::Type::Array &&
    1697         666 :         oFillValue.ToArray().Size() == 1)
    1698             :     {
    1699           0 :         oFillValue = oFillValue.ToArray()[0];
    1700           0 :         eFillValueType = oFillValue.GetType();
    1701             :     }
    1702             : 
    1703         665 :     if (!oFillValue.IsValid())
    1704             :     {
    1705             :         // fill_value is normally required but some implementations
    1706             :         // are lacking it: https://github.com/Unidata/netcdf-c/issues/2059
    1707           1 :         CPLError(CE_Warning, CPLE_AppDefined, "fill_value missing");
    1708             :     }
    1709         664 :     else if (eFillValueType == CPLJSONObject::Type::Null)
    1710             :     {
    1711             :         // Nothing to do
    1712             :     }
    1713         120 :     else if (eFillValueType == CPLJSONObject::Type::String)
    1714             :     {
    1715          98 :         const auto osFillValue = oFillValue.ToString();
    1716          86 :         if (oType.GetClass() == GEDTC_NUMERIC &&
    1717          37 :             CPLGetValueType(osFillValue.c_str()) != CPL_VALUE_STRING)
    1718             :         {
    1719           8 :             abyNoData.resize(oType.GetSize());
    1720             :             // Be tolerant with numeric values serialized as strings.
    1721           8 :             if (oType.GetNumericDataType() == GDT_Int64)
    1722             :             {
    1723             :                 const int64_t nVal = static_cast<int64_t>(
    1724           2 :                     std::strtoll(osFillValue.c_str(), nullptr, 10));
    1725           2 :                 GDALCopyWords(&nVal, GDT_Int64, 0, &abyNoData[0],
    1726             :                               oType.GetNumericDataType(), 0, 1);
    1727             :             }
    1728           6 :             else if (oType.GetNumericDataType() == GDT_UInt64)
    1729             :             {
    1730             :                 const uint64_t nVal = static_cast<uint64_t>(
    1731           2 :                     std::strtoull(osFillValue.c_str(), nullptr, 10));
    1732           2 :                 GDALCopyWords(&nVal, GDT_UInt64, 0, &abyNoData[0],
    1733             :                               oType.GetNumericDataType(), 0, 1);
    1734             :             }
    1735             :             else
    1736             :             {
    1737           4 :                 const double dfNoDataValue = CPLAtof(osFillValue.c_str());
    1738           4 :                 GDALCopyWords(&dfNoDataValue, GDT_Float64, 0, &abyNoData[0],
    1739             :                               oType.GetNumericDataType(), 0, 1);
    1740             :             }
    1741             :         }
    1742          41 :         else if (oType.GetClass() == GEDTC_NUMERIC)
    1743             :         {
    1744             :             double dfNoDataValue;
    1745          29 :             if (osFillValue == "NaN")
    1746             :             {
    1747          10 :                 dfNoDataValue = std::numeric_limits<double>::quiet_NaN();
    1748             :             }
    1749          19 :             else if (osFillValue == "Infinity")
    1750             :             {
    1751           9 :                 dfNoDataValue = std::numeric_limits<double>::infinity();
    1752             :             }
    1753          10 :             else if (osFillValue == "-Infinity")
    1754             :             {
    1755           9 :                 dfNoDataValue = -std::numeric_limits<double>::infinity();
    1756             :             }
    1757             :             else
    1758             :             {
    1759           1 :                 CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
    1760           2 :                 return nullptr;
    1761             :             }
    1762          28 :             if (oType.GetNumericDataType() == GDT_Float16)
    1763             :             {
    1764             :                 const GFloat16 hfNoDataValue =
    1765           0 :                     static_cast<GFloat16>(dfNoDataValue);
    1766           0 :                 abyNoData.resize(sizeof(hfNoDataValue));
    1767           0 :                 memcpy(&abyNoData[0], &hfNoDataValue, sizeof(hfNoDataValue));
    1768             :             }
    1769          28 :             if (oType.GetNumericDataType() == GDT_Float32)
    1770             :             {
    1771          12 :                 const float fNoDataValue = static_cast<float>(dfNoDataValue);
    1772          12 :                 abyNoData.resize(sizeof(fNoDataValue));
    1773          12 :                 memcpy(&abyNoData[0], &fNoDataValue, sizeof(fNoDataValue));
    1774             :             }
    1775          16 :             else if (oType.GetNumericDataType() == GDT_Float64)
    1776             :             {
    1777          15 :                 abyNoData.resize(sizeof(dfNoDataValue));
    1778          15 :                 memcpy(&abyNoData[0], &dfNoDataValue, sizeof(dfNoDataValue));
    1779             :             }
    1780             :             else
    1781             :             {
    1782           1 :                 CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
    1783           1 :                 return nullptr;
    1784             :             }
    1785             :         }
    1786          12 :         else if (oType.GetClass() == GEDTC_STRING)
    1787             :         {
    1788             :             // zarr.open('unicode_be.zarr', mode = 'w', shape=(1,), dtype =
    1789             :             // '>U1', compressor = None) oddly generates "fill_value": "0"
    1790           7 :             if (osFillValue != "0")
    1791             :             {
    1792           3 :                 std::vector<GByte> abyNativeFillValue(osFillValue.size() + 1);
    1793           3 :                 memcpy(&abyNativeFillValue[0], osFillValue.data(),
    1794             :                        osFillValue.size());
    1795           3 :                 int nBytes = CPLBase64DecodeInPlace(&abyNativeFillValue[0]);
    1796           3 :                 abyNativeFillValue.resize(nBytes + 1);
    1797           3 :                 abyNativeFillValue[nBytes] = 0;
    1798           3 :                 abyNoData.resize(oType.GetSize());
    1799           3 :                 char *pDstStr = CPLStrdup(
    1800           3 :                     reinterpret_cast<const char *>(&abyNativeFillValue[0]));
    1801           3 :                 char **pDstPtr = reinterpret_cast<char **>(&abyNoData[0]);
    1802           3 :                 memcpy(pDstPtr, &pDstStr, sizeof(pDstStr));
    1803             :             }
    1804             :         }
    1805             :         else
    1806             :         {
    1807           5 :             std::vector<GByte> abyNativeFillValue(osFillValue.size() + 1);
    1808           5 :             memcpy(&abyNativeFillValue[0], osFillValue.data(),
    1809             :                    osFillValue.size());
    1810           5 :             int nBytes = CPLBase64DecodeInPlace(&abyNativeFillValue[0]);
    1811           5 :             abyNativeFillValue.resize(nBytes);
    1812           5 :             if (abyNativeFillValue.size() !=
    1813           5 :                 aoDtypeElts.back().nativeOffset + aoDtypeElts.back().nativeSize)
    1814             :             {
    1815           0 :                 CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
    1816           0 :                 return nullptr;
    1817             :             }
    1818           5 :             abyNoData.resize(oType.GetSize());
    1819           5 :             ZarrArray::DecodeSourceElt(aoDtypeElts, abyNativeFillValue.data(),
    1820           5 :                                        &abyNoData[0]);
    1821             :         }
    1822             :     }
    1823          71 :     else if (eFillValueType == CPLJSONObject::Type::Boolean ||
    1824          20 :              eFillValueType == CPLJSONObject::Type::Integer ||
    1825          12 :              eFillValueType == CPLJSONObject::Type::Long ||
    1826             :              eFillValueType == CPLJSONObject::Type::Double)
    1827             :     {
    1828          70 :         if (oType.GetClass() == GEDTC_NUMERIC)
    1829             :         {
    1830          69 :             const double dfNoDataValue = oFillValue.ToDouble();
    1831          69 :             if (oType.GetNumericDataType() == GDT_Int64)
    1832             :             {
    1833             :                 const int64_t nNoDataValue =
    1834           2 :                     static_cast<int64_t>(oFillValue.ToLong());
    1835           2 :                 abyNoData.resize(oType.GetSize());
    1836           2 :                 GDALCopyWords(&nNoDataValue, GDT_Int64, 0, &abyNoData[0],
    1837             :                               oType.GetNumericDataType(), 0, 1);
    1838             :             }
    1839          70 :             else if (oType.GetNumericDataType() == GDT_UInt64 &&
    1840             :                      /* we can't really deal with nodata value between */
    1841             :                      /* int64::max and uint64::max due to json-c limitations */
    1842           3 :                      dfNoDataValue >= 0)
    1843             :             {
    1844             :                 const int64_t nNoDataValue =
    1845           3 :                     static_cast<int64_t>(oFillValue.ToLong());
    1846           3 :                 abyNoData.resize(oType.GetSize());
    1847           3 :                 GDALCopyWords(&nNoDataValue, GDT_Int64, 0, &abyNoData[0],
    1848             :                               oType.GetNumericDataType(), 0, 1);
    1849             :             }
    1850             :             else
    1851             :             {
    1852          64 :                 abyNoData.resize(oType.GetSize());
    1853          64 :                 GDALCopyWords(&dfNoDataValue, GDT_Float64, 0, &abyNoData[0],
    1854             :                               oType.GetNumericDataType(), 0, 1);
    1855             :             }
    1856             :         }
    1857             :         else
    1858             :         {
    1859           1 :             CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
    1860           1 :             return nullptr;
    1861          69 :         }
    1862             :     }
    1863             :     else
    1864             :     {
    1865           1 :         CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
    1866           1 :         return nullptr;
    1867             :     }
    1868             : 
    1869         661 :     const CPLCompressor *psCompressor = nullptr;
    1870         661 :     const CPLCompressor *psDecompressor = nullptr;
    1871        1983 :     const auto oCompressor = oRoot["compressor"];
    1872        1322 :     std::string osDecompressorId("NONE");
    1873             : 
    1874         661 :     if (!oCompressor.IsValid())
    1875             :     {
    1876           1 :         CPLError(CE_Failure, CPLE_AppDefined, "compressor missing");
    1877           1 :         return nullptr;
    1878             :     }
    1879         660 :     if (oCompressor.GetType() == CPLJSONObject::Type::Null)
    1880             :     {
    1881             :         // nothing to do
    1882             :     }
    1883          37 :     else if (oCompressor.GetType() == CPLJSONObject::Type::Object)
    1884             :     {
    1885          36 :         osDecompressorId = oCompressor["id"].ToString();
    1886          36 :         if (osDecompressorId.empty())
    1887             :         {
    1888           1 :             CPLError(CE_Failure, CPLE_AppDefined, "Missing compressor id");
    1889           1 :             return nullptr;
    1890             :         }
    1891          35 :         if (osDecompressorId == "imagecodecs_tiff")
    1892             :         {
    1893           5 :             psDecompressor = ZarrGetTIFFDecompressor();
    1894             :         }
    1895             :         else
    1896             :         {
    1897          30 :             psCompressor = CPLGetCompressor(osDecompressorId.c_str());
    1898          30 :             psDecompressor = CPLGetDecompressor(osDecompressorId.c_str());
    1899          30 :             if (psCompressor == nullptr || psDecompressor == nullptr)
    1900             :             {
    1901           1 :                 CPLError(CE_Failure, CPLE_AppDefined,
    1902             :                          "Decompressor %s not handled",
    1903             :                          osDecompressorId.c_str());
    1904           1 :                 return nullptr;
    1905             :             }
    1906             :         }
    1907             :     }
    1908             :     else
    1909             :     {
    1910           1 :         CPLError(CE_Failure, CPLE_AppDefined, "Invalid compressor");
    1911           1 :         return nullptr;
    1912             :     }
    1913             : 
    1914        1314 :     CPLJSONArray oFiltersArray;
    1915        1971 :     const auto oFilters = oRoot["filters"];
    1916         657 :     if (!oFilters.IsValid())
    1917             :     {
    1918           1 :         CPLError(CE_Failure, CPLE_AppDefined, "filters missing");
    1919           1 :         return nullptr;
    1920             :     }
    1921         656 :     if (oFilters.GetType() == CPLJSONObject::Type::Null)
    1922             :     {
    1923             :     }
    1924          43 :     else if (oFilters.GetType() == CPLJSONObject::Type::Array)
    1925             :     {
    1926          41 :         oFiltersArray = oFilters.ToArray();
    1927          54 :         for (const auto &oFilter : oFiltersArray)
    1928             :         {
    1929          28 :             const auto osFilterId = oFilter["id"].ToString();
    1930          14 :             if (osFilterId.empty())
    1931             :             {
    1932           1 :                 CPLError(CE_Failure, CPLE_AppDefined, "Missing filter id");
    1933           1 :                 return nullptr;
    1934             :             }
    1935          13 :             if (!EQUAL(osFilterId.c_str(), "shuffle") &&
    1936          21 :                 !EQUAL(osFilterId.c_str(), "quantize") &&
    1937           8 :                 !EQUAL(osFilterId.c_str(), "fixedscaleoffset"))
    1938             :             {
    1939             :                 const auto psFilterCompressor =
    1940           4 :                     CPLGetCompressor(osFilterId.c_str());
    1941             :                 const auto psFilterDecompressor =
    1942           4 :                     CPLGetDecompressor(osFilterId.c_str());
    1943           4 :                 if (psFilterCompressor == nullptr ||
    1944             :                     psFilterDecompressor == nullptr)
    1945             :                 {
    1946           0 :                     CPLError(CE_Failure, CPLE_AppDefined,
    1947             :                              "Filter %s not handled", osFilterId.c_str());
    1948           0 :                     return nullptr;
    1949             :                 }
    1950             :             }
    1951             :         }
    1952             :     }
    1953             :     else
    1954             :     {
    1955           2 :         CPLError(CE_Failure, CPLE_AppDefined, "Invalid filters");
    1956           2 :         return nullptr;
    1957             :     }
    1958             : 
    1959         653 :     auto poArray = ZarrV2Array::Create(m_poSharedResource, GetFullName(),
    1960             :                                        osArrayName, aoDims, oType, aoDtypeElts,
    1961        1306 :                                        anBlockSize, bFortranOrder);
    1962         653 :     if (!poArray)
    1963           1 :         return nullptr;
    1964         652 :     poArray->SetCompressorJson(oCompressor);
    1965         652 :     poArray->SetUpdatable(m_bUpdatable);  // must be set before SetAttributes()
    1966         652 :     poArray->SetFilename(osZarrayFilename);
    1967         652 :     poArray->SetDimSeparator(osDimSeparator);
    1968         652 :     poArray->SetCompressorDecompressor(osDecompressorId, psCompressor,
    1969             :                                        psDecompressor);
    1970         652 :     poArray->SetFilters(oFiltersArray);
    1971         652 :     if (!abyNoData.empty())
    1972             :     {
    1973         112 :         poArray->RegisterNoDataValue(abyNoData.data());
    1974             :     }
    1975             : 
    1976        1956 :     const auto gridMapping = oAttributes["grid_mapping"];
    1977         652 :     if (gridMapping.GetType() == CPLJSONObject::Type::String)
    1978             :     {
    1979           2 :         const std::string gridMappingName = gridMapping.ToString();
    1980           1 :         if (m_oMapMDArrays.find(gridMappingName) == m_oMapMDArrays.end())
    1981             :         {
    1982           1 :             if (CPLHasPathTraversal(gridMappingName.c_str()))
    1983             :             {
    1984           0 :                 CPLError(CE_Failure, CPLE_AppDefined,
    1985             :                          "Path traversal detected in %s",
    1986             :                          gridMappingName.c_str());
    1987           0 :                 return nullptr;
    1988             :             }
    1989             :             const std::string osArrayFilenameDim = CPLFormFilenameSafe(
    1990           1 :                 CPLFormFilenameSafe(m_osDirectoryName.c_str(),
    1991             :                                     gridMappingName.c_str(), nullptr)
    1992             :                     .c_str(),
    1993           2 :                 ".zarray", nullptr);
    1994             :             VSIStatBufL sStat;
    1995           1 :             if (VSIStatL(osArrayFilenameDim.c_str(), &sStat) == 0)
    1996             :             {
    1997           2 :                 CPLJSONDocument oDoc;
    1998           1 :                 if (oDoc.Load(osArrayFilenameDim))
    1999             :                 {
    2000           1 :                     LoadArray(gridMappingName, osArrayFilenameDim,
    2001           2 :                               oDoc.GetRoot(), false, CPLJSONObject());
    2002             :                 }
    2003             :             }
    2004             :         }
    2005             :     }
    2006             : 
    2007         652 :     poArray->ParseSpecialAttributes(m_pSelf.lock(), oAttributes);
    2008         652 :     poArray->SetAttributes(oAttributes);
    2009         652 :     poArray->SetDtype(oDtype);
    2010         652 :     RegisterArray(poArray);
    2011             : 
    2012             :     // If this is an indexing variable, attach it to the dimension.
    2013         652 :     if (aoDims.size() == 1 && aoDims[0]->GetName() == poArray->GetName())
    2014             :     {
    2015         146 :         auto oIter = m_oMapDimensions.find(poArray->GetName());
    2016         146 :         if (oIter != m_oMapDimensions.end())
    2017             :         {
    2018         146 :             oIter->second->SetIndexingVariable(poArray);
    2019             :         }
    2020             :     }
    2021             : 
    2022         652 :     if (CPLTestBool(m_poSharedResource->GetOpenOptions().FetchNameValueDef(
    2023             :             "CACHE_TILE_PRESENCE", "NO")))
    2024             :     {
    2025           2 :         poArray->CacheTilePresence();
    2026             :     }
    2027             : 
    2028         652 :     return poArray;
    2029             : }
    2030             : 
    2031             : /************************************************************************/
    2032             : /*                    ZarrV2Group::SetCompressorJson()                  */
    2033             : /************************************************************************/
    2034             : 
    2035         669 : void ZarrV2Array::SetCompressorJson(const CPLJSONObject &oCompressor)
    2036             : {
    2037         669 :     m_oCompressorJSon = oCompressor;
    2038         669 :     if (oCompressor.GetType() != CPLJSONObject::Type::Null)
    2039             :         m_aosStructuralInfo.SetNameValue("COMPRESSOR",
    2040          51 :                                          oCompressor.ToString().c_str());
    2041         669 : }
    2042             : 
    2043             : /************************************************************************/
    2044             : /*                     ZarrV2Group::SetFilters()                        */
    2045             : /************************************************************************/
    2046             : 
    2047         962 : void ZarrV2Array::SetFilters(const CPLJSONArray &oFiltersArray)
    2048             : {
    2049         962 :     m_oFiltersArray = oFiltersArray;
    2050         962 :     if (oFiltersArray.Size() > 0)
    2051             :         m_aosStructuralInfo.SetNameValue("FILTERS",
    2052          14 :                                          oFiltersArray.ToString().c_str());
    2053         962 : }

Generated by: LCOV version 1.14