Coverage Report

Created: 2026-09-26 08:22

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gdal/frmts/zarr/zarr_v3_group.cpp
Line
Count
Source
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 "zarr.h"
14
#include "zarr_v3_codec.h"
15
16
#include <algorithm>
17
#include <cassert>
18
#include <limits>
19
#include <map>
20
#include <set>
21
22
/************************************************************************/
23
/*                        ZarrV3Group::Create()                         */
24
/************************************************************************/
25
26
std::shared_ptr<ZarrV3Group>
27
ZarrV3Group::Create(const std::shared_ptr<ZarrSharedResource> &poSharedResource,
28
                    const std::string &osParentName, const std::string &osName,
29
                    const std::string &osRootDirectoryName)
30
10.1k
{
31
10.1k
    auto poGroup = std::shared_ptr<ZarrV3Group>(new ZarrV3Group(
32
10.1k
        poSharedResource, osParentName, osName, osRootDirectoryName));
33
10.1k
    poGroup->SetSelf(poGroup);
34
10.1k
    return poGroup;
35
10.1k
}
36
37
/************************************************************************/
38
/*                           OpenZarrArray()                            */
39
/************************************************************************/
40
41
std::shared_ptr<ZarrArray> ZarrV3Group::OpenZarrArray(const std::string &osName,
42
                                                      CSLConstList) const
43
74
{
44
74
    if (!CheckValidAndErrorOutIfNot())
45
0
        return nullptr;
46
47
74
    auto oIter = m_oMapMDArrays.find(osName);
48
74
    if (oIter != m_oMapMDArrays.end())
49
0
        return oIter->second;
50
51
74
    if (m_bReadFromConsolidatedMetadata)
52
0
        return nullptr;
53
54
74
    const std::string osSubDir =
55
74
        CPLFormFilenameSafe(m_osDirectoryName.c_str(), osName.c_str(), nullptr);
56
74
    const std::string osZarrayFilename =
57
74
        CPLFormFilenameSafe(osSubDir.c_str(), "zarr.json", nullptr);
58
59
74
    VSIStatBufL sStat;
60
74
    if (VSIStatL(osZarrayFilename.c_str(), &sStat) == 0)
61
0
    {
62
0
        CPLJSONDocument oDoc;
63
0
        if (!oDoc.Load(osZarrayFilename))
64
0
            return nullptr;
65
0
        const auto oRoot = oDoc.GetRoot();
66
0
        return LoadArray(osName, osZarrayFilename, oRoot);
67
0
    }
68
69
74
    return nullptr;
70
74
}
71
72
/************************************************************************/
73
/*                    ZarrV3Group::LoadAttributes()                     */
74
/************************************************************************/
75
76
void ZarrV3Group::LoadAttributes() const
77
0
{
78
0
    if (m_bAttributesLoaded)
79
0
        return;
80
0
    m_bAttributesLoaded = true;
81
82
0
    const std::string osFilename =
83
0
        CPLFormFilenameSafe(m_osDirectoryName.c_str(), "zarr.json", nullptr);
84
85
0
    VSIStatBufL sStat;
86
0
    if (VSIStatL(osFilename.c_str(), &sStat) == 0)
87
0
    {
88
0
        CPLJSONDocument oDoc;
89
0
        if (!oDoc.Load(osFilename))
90
0
            return;
91
0
        auto oRoot = oDoc.GetRoot();
92
0
        m_oAttrGroup.Init(oRoot["attributes"], m_bUpdatable);
93
0
    }
94
0
}
95
96
/************************************************************************/
97
/*                          ExploreDirectory()                          */
98
/************************************************************************/
99
100
void ZarrV3Group::ExploreDirectory() const
101
9.73k
{
102
9.73k
    if (m_bDirectoryExplored)
103
0
        return;
104
9.73k
    m_bDirectoryExplored = true;
105
106
9.73k
    auto psDir = VSIOpenDir(m_osDirectoryName.c_str(), 0, nullptr);
107
9.73k
    if (!psDir)
108
0
        return;
109
21.8k
    while (const VSIDIREntry *psEntry = VSIGetNextDirEntry(psDir))
110
12.1k
    {
111
12.1k
        if (VSI_ISDIR(psEntry->nMode))
112
9.80k
        {
113
9.80k
            std::string osName(psEntry->pszName);
114
9.80k
            while (!osName.empty() &&
115
9.80k
                   (osName.back() == '/' || osName.back() == '\\'))
116
0
                osName.pop_back();
117
9.80k
            if (osName.empty())
118
0
                continue;
119
9.80k
            const std::string osSubDir = CPLFormFilenameSafe(
120
9.80k
                m_osDirectoryName.c_str(), osName.c_str(), nullptr);
121
9.80k
            VSIStatBufL sStat;
122
9.80k
            const std::string osZarrJsonFilename =
123
9.80k
                CPLFormFilenameSafe(osSubDir.c_str(), "zarr.json", nullptr);
124
9.80k
            if (VSIStatL(osZarrJsonFilename.c_str(), &sStat) == 0)
125
85
            {
126
85
                CPLJSONDocument oDoc;
127
85
                if (oDoc.Load(osZarrJsonFilename.c_str()))
128
7
                {
129
7
                    const auto oRoot = oDoc.GetRoot();
130
7
                    if (oRoot.GetInteger("zarr_format") != 3)
131
7
                    {
132
7
                        CPLError(CE_Warning, CPLE_AppDefined,
133
7
                                 "Unhandled zarr_format value");
134
7
                        continue;
135
7
                    }
136
0
                    const std::string osNodeType = oRoot.GetString("node_type");
137
0
                    if (osNodeType == "array")
138
0
                    {
139
0
                        if (!cpl::contains(m_oSetArrayNames, osName))
140
0
                        {
141
0
                            m_oSetArrayNames.insert(osName);
142
0
                            m_aosArrays.emplace_back(std::move(osName));
143
0
                        }
144
0
                    }
145
0
                    else if (osNodeType == "group")
146
0
                    {
147
0
                        if (!cpl::contains(m_oSetGroupNames, osName))
148
0
                        {
149
0
                            m_oSetGroupNames.insert(osName);
150
0
                            m_aosGroups.emplace_back(std::move(osName));
151
0
                        }
152
0
                    }
153
0
                    else
154
0
                    {
155
0
                        CPLError(CE_Warning, CPLE_AppDefined,
156
0
                                 "Unhandled node_type value");
157
0
                        continue;
158
0
                    }
159
0
                }
160
85
            }
161
9.71k
            else
162
9.71k
            {
163
                // Implicit group (deprecated)
164
9.71k
                if (!cpl::contains(m_oSetGroupNames, osName))
165
9.71k
                {
166
9.71k
                    m_oSetGroupNames.insert(osName);
167
9.71k
                    m_aosGroups.emplace_back(std::move(osName));
168
9.71k
                }
169
9.71k
            }
170
9.80k
        }
171
12.1k
    }
172
9.73k
    VSICloseDir(psDir);
173
9.73k
}
174
175
/************************************************************************/
176
/*                      ZarrV3Group::ZarrV3Group()                      */
177
/************************************************************************/
178
179
ZarrV3Group::ZarrV3Group(
180
    const std::shared_ptr<ZarrSharedResource> &poSharedResource,
181
    const std::string &osParentName, const std::string &osName,
182
    const std::string &osDirectoryName)
183
10.1k
    : ZarrGroupBase(poSharedResource, osParentName, osName)
184
10.1k
{
185
10.1k
    m_osDirectoryName = osDirectoryName;
186
10.1k
}
187
188
/************************************************************************/
189
/*                     ZarrV3Group::~ZarrV3Group()                      */
190
/************************************************************************/
191
192
ZarrV3Group::~ZarrV3Group()
193
10.1k
{
194
10.1k
    ZarrV3Group::Close();
195
10.1k
}
196
197
/************************************************************************/
198
/*                    GenerateMultiscalesMetadata()                     */
199
/************************************************************************/
200
201
void ZarrV3Group::GenerateMultiscalesMetadata(const char *pszResampling)
202
0
{
203
0
    const auto aosGroupNames = GetGroupNames();
204
0
    if (aosGroupNames.empty())
205
0
    {
206
        // No child groups - remove stale multiscales metadata if present.
207
0
        if (!m_bAttributesLoaded)
208
0
            LoadAttributes();
209
0
        if (m_oAttrGroup.GetAttribute("multiscales"))
210
0
            m_oAttrGroup.DeleteAttribute("multiscales");
211
0
        auto poExistingConv = m_oAttrGroup.GetAttribute("zarr_conventions");
212
0
        if (poExistingConv)
213
0
        {
214
            // Preserve non-multiscales entries.
215
0
            const char *pszExisting = poExistingConv->ReadAsString();
216
0
            CPLJSONArray oFiltered;
217
0
            if (pszExisting)
218
0
            {
219
0
                CPLJSONDocument oDoc;
220
0
                if (oDoc.LoadMemory(pszExisting))
221
0
                {
222
0
                    for (const auto &oEntry : oDoc.GetRoot().ToArray())
223
0
                    {
224
0
                        if (oEntry.GetString("uuid") != ZARR_MULTISCALES_UUID)
225
0
                            oFiltered.Add(oEntry);
226
0
                    }
227
0
                }
228
0
            }
229
0
            m_oAttrGroup.DeleteAttribute("zarr_conventions");
230
0
            if (oFiltered.Size() > 0)
231
0
            {
232
0
                const auto oJsonDT =
233
0
                    GDALExtendedDataType::CreateString(0, GEDTST_JSON);
234
0
                auto poAttr = m_oAttrGroup.CreateAttribute("zarr_conventions",
235
0
                                                           {}, oJsonDT);
236
0
                if (poAttr)
237
0
                    poAttr->Write(
238
0
                        oFiltered.Format(CPLJSONObject::PrettyFormat::Plain)
239
0
                            .c_str());
240
0
            }
241
0
        }
242
0
        return;
243
0
    }
244
245
    // Collect {arrayName -> [(groupName, array)]} across child groups.
246
0
    struct LevelInfo
247
0
    {
248
0
        std::string osGroupName;  // empty for base (this group)
249
0
        std::shared_ptr<GDALMDArray> poArray;
250
0
    };
251
252
0
    std::map<std::string, std::vector<LevelInfo>> oMapArrayToLevels;
253
254
0
    for (const auto &osGroupName : aosGroupNames)
255
0
    {
256
0
        auto poChildGroup = OpenZarrGroup(osGroupName);
257
0
        if (!poChildGroup)
258
0
            continue;
259
0
        for (const auto &osArrayName : poChildGroup->GetMDArrayNames())
260
0
        {
261
0
            auto poArray = poChildGroup->OpenMDArray(osArrayName);
262
0
            if (poArray)
263
0
            {
264
0
                oMapArrayToLevels[osArrayName].push_back(
265
0
                    {osGroupName, std::move(poArray)});
266
0
            }
267
0
        }
268
0
    }
269
270
0
    if (oMapArrayToLevels.empty())
271
0
        return;
272
273
    // For each array found in child groups, check if the base (this group)
274
    // also has an array with the same name. If so, prepend it as the base
275
    // level with an empty group name (meaning "this group").
276
0
    for (auto &[osArrayName, aoLevels] : oMapArrayToLevels)
277
0
    {
278
0
        auto poBaseArray = OpenMDArray(osArrayName);
279
0
        if (poBaseArray)
280
0
        {
281
0
            aoLevels.insert(aoLevels.begin(),
282
0
                            LevelInfo{"", std::move(poBaseArray)});
283
0
        }
284
0
    }
285
286
    // Pick the first array name (alphabetical) with >= 2 levels
287
    // (base + at least one overview) and >= 2 dimensions (skip 1D
288
    // coordinate arrays).
289
    //
290
    // Expected hierarchy from BuildOverviews():
291
    //   /group/
292
    //     data        <- base array (e.g. 10980 x 10980)
293
    //     y, x        <- 1D coordinate arrays (skipped)
294
    //     ovr_2x/
295
    //       data      <- 2x overview  (5490 x 5490)
296
    //       y, x
297
    //     ovr_4x/
298
    //       data      <- 4x overview  (2745 x 2745)
299
    //       y, x
300
    //
301
    // Multiple >=2D arrays sharing the same name across levels is
302
    // possible but unusual; we use the first alphabetically.
303
0
    std::string osCanonicalArrayName;
304
0
    for (const auto &[osArrayName, aoLevels] : oMapArrayToLevels)
305
0
    {
306
0
        if (aoLevels.size() >= 2 &&
307
0
            aoLevels[0].poArray->GetDimensionCount() >= 2)
308
0
        {
309
0
            osCanonicalArrayName = osArrayName;
310
0
            break;
311
0
        }
312
0
    }
313
314
0
    if (osCanonicalArrayName.empty())
315
0
    {
316
0
        CPLDebug("ZARR", "GenerateMultiscalesMetadata: no array with "
317
0
                         ">=2 levels and >=2 dimensions found");
318
0
        return;
319
0
    }
320
321
0
    auto &aoLevels = oMapArrayToLevels[osCanonicalArrayName];
322
323
    // Sort by total element count, largest first (= full resolution).
324
0
    std::stable_sort(aoLevels.begin(), aoLevels.end(),
325
0
                     [](const LevelInfo &a, const LevelInfo &b)
326
0
                     {
327
0
                         const auto &dimsA = a.poArray->GetDimensions();
328
0
                         const auto &dimsB = b.poArray->GetDimensions();
329
0
                         GUInt64 sizeA = 1, sizeB = 1;
330
0
                         for (const auto &d : dimsA)
331
0
                             sizeA *= d->GetSize();
332
0
                         for (const auto &d : dimsB)
333
0
                             sizeB *= d->GetSize();
334
0
                         return sizeA > sizeB;
335
0
                     });
336
337
0
    const auto &poBaseArray = aoLevels[0].poArray;
338
0
    const size_t nBaseDimCount = poBaseArray->GetDimensionCount();
339
0
    const auto &oBaseType = poBaseArray->GetDataType();
340
341
    // Asset path for a level. Empty group name means the base array lives
342
    // in this group - use the array name directly (LoadOverviews resolves
343
    // single-component paths as array names in the parent group).
344
0
    const auto assetPath =
345
0
        [&osCanonicalArrayName](const std::string &osGroupName) -> std::string
346
0
    { return osGroupName.empty() ? osCanonicalArrayName : osGroupName; };
347
348
    // Base level: identity scale, no translation, no derived_from.
349
0
    CPLJSONArray oLayout;
350
0
    {
351
0
        CPLJSONObject oBaseItem;
352
0
        oBaseItem.Add("asset", assetPath(aoLevels[0].osGroupName));
353
354
0
        CPLJSONArray oScale;
355
0
        for (size_t iDim = 0; iDim < nBaseDimCount; ++iDim)
356
0
            oScale.Add(1.0);
357
0
        CPLJSONObject oTransform;
358
0
        oTransform.Add("scale", oScale);
359
0
        oBaseItem.Add("transform", oTransform);
360
361
0
        oLayout.Add(oBaseItem);
362
0
    }
363
364
    // Overview levels: sequential derived_from chain.
365
0
    for (size_t iLevel = 1; iLevel < aoLevels.size(); ++iLevel)
366
0
    {
367
0
        const auto &info = aoLevels[iLevel];
368
0
        const auto &poArray = info.poArray;
369
370
0
        if (poArray->GetDimensionCount() != nBaseDimCount ||
371
0
            poArray->GetDataType() != oBaseType)
372
0
        {
373
0
            CPLDebug("ZARR",
374
0
                     "GenerateMultiscalesMetadata: skipping level '%s' "
375
0
                     "(dim count or data type mismatch with base)",
376
0
                     info.osGroupName.c_str());
377
0
            continue;
378
0
        }
379
380
0
        const auto &apoDims = poArray->GetDimensions();
381
        // Previous valid level for sequential derived_from.
382
0
        const auto &oPrevDims = aoLevels[iLevel - 1].poArray->GetDimensions();
383
384
0
        CPLJSONObject oItem;
385
0
        oItem.Add("asset", assetPath(info.osGroupName));
386
0
        oItem.Add("derived_from", assetPath(aoLevels[iLevel - 1].osGroupName));
387
388
0
        CPLJSONArray oScale;
389
0
        CPLJSONArray oTranslation;
390
0
        for (size_t iDim = 0; iDim < nBaseDimCount; ++iDim)
391
0
        {
392
0
            const auto nOvSize = apoDims[iDim]->GetSize();
393
0
            const auto nPrevSize = oPrevDims[iDim]->GetSize();
394
0
            const double dfScale = nOvSize > 0
395
0
                                       ? static_cast<double>(nPrevSize) /
396
0
                                             static_cast<double>(nOvSize)
397
0
                                       : 0.0;
398
0
            oScale.Add(dfScale);
399
0
            oTranslation.Add(0.0);
400
0
        }
401
402
0
        CPLJSONObject oTransform;
403
0
        oTransform.Add("scale", oScale);
404
0
        oTransform.Add("translation", oTranslation);
405
0
        oItem.Add("transform", oTransform);
406
407
0
        if (pszResampling)
408
0
            oItem.Add("resampling_method", pszResampling);
409
410
0
        oLayout.Add(oItem);
411
0
    }
412
413
0
    if (oLayout.Size() < 2)
414
0
        return;
415
416
0
    CPLJSONObject oMultiscales;
417
0
    oMultiscales.Add("layout", oLayout);
418
419
    // Preserve existing zarr_conventions entries.
420
0
    if (!m_bAttributesLoaded)
421
0
        LoadAttributes();
422
423
0
    CPLJSONArray oZarrConventions;
424
0
    auto poExistingConv = GetAttribute("zarr_conventions");
425
0
    if (poExistingConv)
426
0
    {
427
0
        const char *pszExisting = poExistingConv->ReadAsString();
428
0
        if (pszExisting)
429
0
        {
430
0
            CPLJSONDocument oDoc;
431
0
            if (oDoc.LoadMemory(pszExisting))
432
0
            {
433
0
                for (const auto &oEntry : oDoc.GetRoot().ToArray())
434
0
                {
435
0
                    if (oEntry.GetString("uuid") != ZARR_MULTISCALES_UUID)
436
0
                        oZarrConventions.Add(oEntry);
437
0
                }
438
0
            }
439
0
        }
440
0
        DeleteAttribute("zarr_conventions");
441
0
    }
442
443
0
    {
444
0
        CPLJSONObject oConv;
445
0
        oConv.Set("uuid", ZARR_MULTISCALES_UUID);
446
0
        oConv.Set("schema_url",
447
0
                  "https://raw.githubusercontent.com/zarr-conventions/"
448
0
                  "multiscales/refs/tags/v1/schema.json");
449
0
        oConv.Set("spec_url", "https://github.com/zarr-conventions/"
450
0
                              "multiscales/blob/v1/README.md");
451
0
        oConv.Set("name", "multiscales");
452
0
        oConv.Set("description", "Multiscale layout of zarr datasets");
453
0
        oZarrConventions.Add(oConv);
454
0
    }
455
456
0
    if (GetAttribute("multiscales"))
457
0
        DeleteAttribute("multiscales");
458
459
0
    const auto oJsonDT = GDALExtendedDataType::CreateString(0, GEDTST_JSON);
460
0
    {
461
0
        auto poAttr = CreateAttribute("zarr_conventions", {}, oJsonDT);
462
0
        if (poAttr)
463
0
            poAttr->Write(
464
0
                oZarrConventions.Format(CPLJSONObject::PrettyFormat::Plain)
465
0
                    .c_str());
466
0
    }
467
0
    {
468
0
        auto poAttr = CreateAttribute("multiscales", {}, oJsonDT);
469
0
        if (poAttr)
470
0
            poAttr->Write(
471
0
                oMultiscales.Format(CPLJSONObject::PrettyFormat::Plain)
472
0
                    .c_str());
473
0
    }
474
0
}
475
476
/************************************************************************/
477
/*                               Close()                                */
478
/************************************************************************/
479
480
bool ZarrV3Group::Close()
481
160k
{
482
160k
    bool bRet = ZarrGroupBase::Close();
483
484
160k
    if (m_bValid && (m_oAttrGroup.IsModified() ||
485
160k
                     (m_bUpdatable && !m_bFileHasBeenWritten &&
486
0
                      m_poSharedResource->IsConsolidatedMetadataEnabled())))
487
0
    {
488
0
        LoadAttributes();
489
490
0
        CPLJSONDocument oDoc;
491
0
        auto oRoot = oDoc.GetRoot();
492
0
        oRoot.Add("zarr_format", 3);
493
0
        oRoot.Add("node_type", "group");
494
0
        oRoot.Add("attributes", m_oAttrGroup.Serialize());
495
0
        const std::string osZarrJsonFilename = CPLFormFilenameSafe(
496
0
            m_osDirectoryName.c_str(), "zarr.json", nullptr);
497
0
        if (!m_bFileHasBeenWritten)
498
0
        {
499
0
            oRoot.Add("consolidated_metadata",
500
0
                      m_poSharedResource->GetConsolidatedMetadataObj());
501
0
            bRet = oDoc.Save(osZarrJsonFilename) && bRet;
502
0
        }
503
0
        else
504
0
        {
505
0
            bRet = oDoc.Save(osZarrJsonFilename) && bRet;
506
0
            if (bRet)
507
0
                m_poSharedResource->SetZMetadataItem(osZarrJsonFilename, oRoot);
508
0
        }
509
0
        m_bFileHasBeenWritten = bRet;
510
0
    }
511
512
160k
    return bRet;
513
160k
}
514
515
/************************************************************************/
516
/*                  ZarrV3Group::GetOrCreateSubGroup()                  */
517
/************************************************************************/
518
519
std::shared_ptr<ZarrV3Group>
520
ZarrV3Group::GetOrCreateSubGroup(const std::string &osSubGroupFullname)
521
0
{
522
0
    auto poSubGroup = std::dynamic_pointer_cast<ZarrV3Group>(
523
0
        OpenGroupFromFullname(osSubGroupFullname));
524
0
    if (poSubGroup)
525
0
    {
526
0
        return poSubGroup;
527
0
    }
528
529
0
    const auto nLastSlashPos = osSubGroupFullname.rfind('/');
530
0
    auto poBelongingGroup =
531
0
        (nLastSlashPos == 0)
532
0
            ? this
533
0
            : GetOrCreateSubGroup(osSubGroupFullname.substr(0, nLastSlashPos))
534
0
                  .get();
535
536
0
    poSubGroup = ZarrV3Group::Create(
537
0
        m_poSharedResource, poBelongingGroup->GetFullName(),
538
0
        osSubGroupFullname.substr(nLastSlashPos + 1), m_osDirectoryName);
539
0
    poSubGroup->m_poParent = std::dynamic_pointer_cast<ZarrGroupBase>(
540
0
        poBelongingGroup->m_pSelf.lock());
541
0
    poSubGroup->SetDirectoryName(
542
0
        CPLFormFilenameSafe(poBelongingGroup->m_osDirectoryName.c_str(),
543
0
                            poSubGroup->GetName().c_str(), nullptr));
544
0
    poSubGroup->m_bDirectoryExplored = true;
545
0
    poSubGroup->m_bReadFromConsolidatedMetadata = true;
546
0
    poSubGroup->m_bFileHasBeenWritten = true;
547
0
    poSubGroup->SetUpdatable(m_bUpdatable);
548
549
0
    poBelongingGroup->m_oMapGroups[poSubGroup->GetName()] = poSubGroup;
550
0
    poBelongingGroup->m_oSetGroupNames.insert(poSubGroup->GetName());
551
0
    poBelongingGroup->m_aosGroups.emplace_back(poSubGroup->GetName());
552
0
    return poSubGroup;
553
0
}
554
555
/************************************************************************/
556
/*             ZarrV3Group::InitFromConsolidatedMetadata()              */
557
/************************************************************************/
558
559
void ZarrV3Group::InitFromConsolidatedMetadata(
560
    const CPLJSONObject &oConsolidatedMetadata,
561
    const CPLJSONObject &oRootAttributes)
562
0
{
563
0
    const auto metadata = oConsolidatedMetadata["metadata"];
564
0
    if (metadata.GetType() != CPLJSONObject::Type::Object)
565
0
    {
566
0
        CPLError(CE_Warning, CPLE_AppDefined,
567
0
                 "consolidated_metadata lacks 'metadata' object");
568
0
        return;
569
0
    }
570
0
    m_bDirectoryExplored = true;
571
0
    m_bAttributesLoaded = oRootAttributes.IsValid();
572
0
    m_bReadFromConsolidatedMetadata = true;
573
574
0
    if (oRootAttributes.IsValid())
575
0
    {
576
0
        m_oAttrGroup.Init(oRootAttributes, m_bUpdatable);
577
0
    }
578
579
0
    const auto children = metadata.GetChildren();
580
0
    std::map<std::string, const CPLJSONObject *> oMapArrays;
581
582
    // First pass to create groups and collect arrays
583
0
    for (const auto &child : children)
584
0
    {
585
0
        const std::string osName(child.GetName());
586
0
        if (std::count(osName.begin(), osName.end(), '/') > 32)
587
0
        {
588
            // Avoid too deep recursion in GetOrCreateSubGroup()
589
0
            continue;
590
0
        }
591
592
0
        const std::string osNodeType = child.GetString("node_type");
593
0
        if (osNodeType == "group")
594
0
        {
595
0
            auto poGroup = GetOrCreateSubGroup("/" + osName);
596
0
            poGroup->m_bAttributesLoaded = oRootAttributes.IsValid();
597
0
            auto oAttributes = child["attributes"];
598
0
            if (oAttributes.IsValid())
599
0
            {
600
0
                poGroup->m_oAttrGroup.Init(oAttributes, m_bUpdatable);
601
0
            }
602
0
        }
603
0
        else if (osNodeType == "array")
604
0
        {
605
0
            oMapArrays[osName] = &child;
606
0
        }
607
0
    }
608
609
0
    const auto CreateArray =
610
0
        [this](const std::string &osArrayFullname, const CPLJSONObject &oArray)
611
0
    {
612
0
        const auto nLastSlashPos = osArrayFullname.rfind('/');
613
0
        auto poBelongingGroup =
614
0
            (nLastSlashPos == std::string::npos)
615
0
                ? this
616
0
                : GetOrCreateSubGroup("/" +
617
0
                                      osArrayFullname.substr(0, nLastSlashPos))
618
0
                      .get();
619
0
        const auto osArrayName =
620
0
            nLastSlashPos == std::string::npos
621
0
                ? osArrayFullname
622
0
                : osArrayFullname.substr(nLastSlashPos + 1);
623
0
        const std::string osZarrayFilename = CPLFormFilenameSafe(
624
0
            CPLFormFilenameSafe(poBelongingGroup->m_osDirectoryName.c_str(),
625
0
                                osArrayName.c_str(), nullptr)
626
0
                .c_str(),
627
0
            "zarr.json", nullptr);
628
0
        poBelongingGroup->LoadArray(osArrayName, osZarrayFilename, oArray);
629
0
    };
630
631
0
    struct ArrayDesc
632
0
    {
633
0
        std::string osArrayFullname{};
634
0
        const CPLJSONObject *poArray = nullptr;
635
0
    };
636
637
0
    std::vector<ArrayDesc> aoRegularArrays;
638
639
    // Second pass to read attributes and create arrays that are indexing
640
    // variable
641
0
    for (const auto &child : children)
642
0
    {
643
0
        const std::string osName(child.GetName());
644
0
        const std::string osNodeType = child.GetString("node_type");
645
0
        if (osNodeType == "array")
646
0
        {
647
0
            auto oIter = oMapArrays.find(osName);
648
0
            if (oIter != oMapArrays.end())
649
0
            {
650
0
                const auto nLastSlashPos = osName.rfind('/');
651
0
                const std::string osArrayName =
652
0
                    (nLastSlashPos == std::string::npos)
653
0
                        ? osName
654
0
                        : osName.substr(nLastSlashPos + 1);
655
0
                const auto arrayDimensions = child["dimension_names"].ToArray();
656
0
                if (arrayDimensions.IsValid() && arrayDimensions.Size() == 1 &&
657
0
                    arrayDimensions[0].ToString() == osArrayName)
658
0
                {
659
0
                    CreateArray(osName, child);
660
0
                    oMapArrays.erase(oIter);
661
0
                }
662
0
                else
663
0
                {
664
0
                    ArrayDesc desc;
665
0
                    desc.osArrayFullname = std::move(osName);
666
0
                    desc.poArray = oIter->second;
667
0
                    aoRegularArrays.emplace_back(std::move(desc));
668
0
                }
669
0
            }
670
0
        }
671
0
    }
672
673
    // Third pass to create non-indexing arrays with attributes
674
0
    for (const auto &desc : aoRegularArrays)
675
0
    {
676
0
        CreateArray(desc.osArrayFullname, *(desc.poArray));
677
0
        oMapArrays.erase(desc.osArrayFullname);
678
0
    }
679
680
    // Fourth pass to create arrays without attributes
681
0
    for (const auto &kv : oMapArrays)
682
0
    {
683
0
        CreateArray(kv.first, *(kv.second));
684
0
    }
685
0
}
686
687
/************************************************************************/
688
/*                           OpenZarrGroup()                            */
689
/************************************************************************/
690
691
std::shared_ptr<ZarrGroupBase>
692
ZarrV3Group::OpenZarrGroup(const std::string &osName, CSLConstList) const
693
9.70k
{
694
9.70k
    if (!CheckValidAndErrorOutIfNot())
695
0
        return nullptr;
696
697
9.70k
    auto oIter = m_oMapGroups.find(osName);
698
9.70k
    if (oIter != m_oMapGroups.end())
699
0
        return oIter->second;
700
701
9.70k
    if (m_bReadFromConsolidatedMetadata)
702
0
        return nullptr;
703
704
9.70k
    const std::string osSubDir =
705
9.70k
        CPLFormFilenameSafe(m_osDirectoryName.c_str(), osName.c_str(), nullptr);
706
9.70k
    const std::string osSubDirZarrJsonFilename =
707
9.70k
        CPLFormFilenameSafe(osSubDir.c_str(), "zarr.json", nullptr);
708
709
9.70k
    VSIStatBufL sStat;
710
    // Explicit group
711
9.70k
    if (VSIStatL(osSubDirZarrJsonFilename.c_str(), &sStat) == 0)
712
0
    {
713
0
        CPLJSONDocument oDoc;
714
0
        if (oDoc.Load(osSubDirZarrJsonFilename.c_str()))
715
0
        {
716
0
            const auto oRoot = oDoc.GetRoot();
717
0
            if (oRoot.GetInteger("zarr_format") != 3)
718
0
            {
719
0
                CPLError(CE_Failure, CPLE_AppDefined,
720
0
                         "Unhandled zarr_format value");
721
0
                return nullptr;
722
0
            }
723
0
            const std::string osNodeType = oRoot.GetString("node_type");
724
0
            if (osNodeType != "group")
725
0
            {
726
0
                CPLError(CE_Failure, CPLE_AppDefined, "%s is a %s, not a group",
727
0
                         osName.c_str(), osNodeType.c_str());
728
0
                return nullptr;
729
0
            }
730
0
            auto poSubGroup = ZarrV3Group::Create(
731
0
                m_poSharedResource, GetFullName(), osName, osSubDir);
732
0
            poSubGroup->m_bFileHasBeenWritten = true;
733
0
            poSubGroup->m_poParent =
734
0
                std::dynamic_pointer_cast<ZarrGroupBase>(m_pSelf.lock());
735
0
            poSubGroup->SetUpdatable(m_bUpdatable);
736
0
            m_oMapGroups[osName] = poSubGroup;
737
0
            return poSubGroup;
738
0
        }
739
0
        return nullptr;
740
0
    }
741
742
    // Implicit group
743
9.70k
    if (VSIStatL(osSubDir.c_str(), &sStat) == 0 && VSI_ISDIR(sStat.st_mode))
744
9.70k
    {
745
        // Note: Python zarr v3.0.2 still generates implicit groups
746
        // See https://github.com/zarr-developers/zarr-python/issues/2794
747
9.70k
        CPLError(CE_Warning, CPLE_AppDefined,
748
9.70k
                 "Support for Zarr V3 implicit group is now deprecated, and "
749
9.70k
                 "may be removed in a future version");
750
9.70k
        auto poSubGroup = ZarrV3Group::Create(m_poSharedResource, GetFullName(),
751
9.70k
                                              osName, osSubDir);
752
9.70k
        poSubGroup->m_bFileHasBeenWritten = true;
753
9.70k
        poSubGroup->m_poParent =
754
9.70k
            std::dynamic_pointer_cast<ZarrGroupBase>(m_pSelf.lock());
755
9.70k
        poSubGroup->SetUpdatable(m_bUpdatable);
756
9.70k
        m_oMapGroups[osName] = poSubGroup;
757
9.70k
        return poSubGroup;
758
9.70k
    }
759
760
0
    return nullptr;
761
9.70k
}
762
763
/************************************************************************/
764
/*                     ZarrV3Group::CreateOnDisk()                      */
765
/************************************************************************/
766
767
std::shared_ptr<ZarrV3Group> ZarrV3Group::CreateOnDisk(
768
    const std::shared_ptr<ZarrSharedResource> &poSharedResource,
769
    const std::string &osParentFullName, const std::string &osName,
770
    const std::string &osDirectoryName)
771
74
{
772
74
    if (VSIMkdir(osDirectoryName.c_str(), 0755) != 0)
773
0
    {
774
0
        VSIStatBufL sStat;
775
0
        if (VSIStatL(osDirectoryName.c_str(), &sStat) == 0)
776
0
        {
777
0
            CPLError(CE_Failure, CPLE_FileIO, "Directory %s already exists.",
778
0
                     osDirectoryName.c_str());
779
0
        }
780
0
        else
781
0
        {
782
0
            CPLError(CE_Failure, CPLE_FileIO, "Cannot create directory %s.",
783
0
                     osDirectoryName.c_str());
784
0
        }
785
0
        return nullptr;
786
0
    }
787
788
74
    const std::string osZarrJsonFilename(
789
74
        CPLFormFilenameSafe(osDirectoryName.c_str(), "zarr.json", nullptr));
790
74
    VSILFILE *fp = nullptr;
791
74
    if (!(poSharedResource->IsConsolidatedMetadataEnabled() &&
792
74
          cpl::starts_with(osZarrJsonFilename, "/vsizip/") &&
793
0
          osParentFullName.empty() && osName == "/"))
794
74
    {
795
74
        fp = VSIFOpenL(osZarrJsonFilename.c_str(), "wb");
796
74
        if (!fp)
797
0
        {
798
0
            CPLError(CE_Failure, CPLE_FileIO, "Cannot create file %s.",
799
0
                     osZarrJsonFilename.c_str());
800
0
            return nullptr;
801
0
        }
802
74
        VSIFPrintfL(fp, "{\n"
803
74
                        "    \"zarr_format\": 3,\n"
804
74
                        "    \"node_type\": \"group\",\n"
805
74
                        "    \"attributes\": {}\n"
806
74
                        "}\n");
807
74
        VSIFCloseL(fp);
808
74
    }
809
810
74
    auto poGroup = ZarrV3Group::Create(poSharedResource, osParentFullName,
811
74
                                       osName, osDirectoryName);
812
74
    poGroup->SetUpdatable(true);
813
74
    poGroup->m_bDirectoryExplored = true;
814
74
    poGroup->m_bFileHasBeenWritten = fp != nullptr;
815
816
74
    CPLJSONObject oObj;
817
74
    oObj.Add("zarr_format", 3);
818
74
    oObj.Add("node_type", "group");
819
74
    oObj.Add("attributes", CPLJSONObject());
820
74
    poSharedResource->SetZMetadataItem(osZarrJsonFilename, oObj);
821
822
74
    return poGroup;
823
74
}
824
825
/************************************************************************/
826
/*                      ZarrV3Group::CreateGroup()                      */
827
/************************************************************************/
828
829
std::shared_ptr<GDALGroup>
830
ZarrV3Group::CreateGroup(const std::string &osName,
831
                         CSLConstList /* papszOptions */)
832
0
{
833
0
    if (!CheckValidAndErrorOutIfNot())
834
0
        return nullptr;
835
836
0
    if (!m_bUpdatable)
837
0
    {
838
0
        CPLError(CE_Failure, CPLE_NotSupported,
839
0
                 "Dataset not open in update mode");
840
0
        return nullptr;
841
0
    }
842
0
    if (!IsValidObjectName(osName))
843
0
    {
844
0
        CPLError(CE_Failure, CPLE_NotSupported, "Invalid group name");
845
0
        return nullptr;
846
0
    }
847
848
0
    GetGroupNames();
849
850
0
    if (cpl::contains(m_oSetGroupNames, osName))
851
0
    {
852
0
        CPLError(CE_Failure, CPLE_AppDefined,
853
0
                 "A group with same name (%s) already exists in group %s",
854
0
                 osName.c_str(), GetFullName().c_str());
855
0
        return nullptr;
856
0
    }
857
858
0
    const std::string osDirectoryName =
859
0
        CPLFormFilenameSafe(m_osDirectoryName.c_str(), osName.c_str(), nullptr);
860
0
    auto poGroup = CreateOnDisk(m_poSharedResource, GetFullName(), osName,
861
0
                                osDirectoryName);
862
0
    if (!poGroup)
863
0
        return nullptr;
864
0
    poGroup->m_poParent =
865
0
        std::dynamic_pointer_cast<ZarrGroupBase>(m_pSelf.lock());
866
0
    m_oMapGroups[osName] = poGroup;
867
0
    m_aosGroups.emplace_back(osName);
868
0
    return poGroup;
869
0
}
870
871
/************************************************************************/
872
/*                           FillDTypeElts()                            */
873
/************************************************************************/
874
875
static CPLJSONObject FillDTypeElts(const GDALExtendedDataType &oDataType,
876
                                   std::vector<DtypeElt> &aoDtypeElts)
877
148
{
878
148
    CPLJSONObject dtype;
879
148
    const std::string dummy("dummy");
880
881
148
    if (oDataType.GetClass() == GEDTC_STRING)
882
0
    {
883
0
        const int nMaxLen = std::max(
884
0
            2, atoi(CPLGetConfigOption("ZARR_VLEN_STRING_MAX_LENGTH", "256")));
885
0
        DtypeElt elt;
886
0
        elt.nativeType = DtypeElt::NativeType::STRING_ASCII;
887
0
        elt.nativeOffset = 0;
888
0
        elt.nativeSize = static_cast<size_t>(nMaxLen);
889
0
        elt.gdalOffset = 0;
890
0
        elt.gdalSize = oDataType.GetSize();
891
0
        aoDtypeElts.emplace_back(elt);
892
0
        dtype.Set(dummy, "string");
893
0
        return dtype;
894
0
    }
895
896
148
    const auto eDT = oDataType.GetNumericDataType();
897
148
    DtypeElt elt;
898
148
    bool bUnsupported = false;
899
148
    switch (eDT)
900
148
    {
901
44
        case GDT_UInt8:
902
44
        {
903
44
            elt.nativeType = DtypeElt::NativeType::UNSIGNED_INT;
904
44
            dtype.Set(dummy, "uint8");
905
44
            break;
906
0
        }
907
0
        case GDT_Int8:
908
0
        {
909
0
            elt.nativeType = DtypeElt::NativeType::SIGNED_INT;
910
0
            dtype.Set(dummy, "int8");
911
0
            break;
912
0
        }
913
9
        case GDT_UInt16:
914
9
        {
915
9
            elt.nativeType = DtypeElt::NativeType::UNSIGNED_INT;
916
9
            dtype.Set(dummy, "uint16");
917
9
            break;
918
0
        }
919
6
        case GDT_Int16:
920
6
        {
921
6
            elt.nativeType = DtypeElt::NativeType::SIGNED_INT;
922
6
            dtype.Set(dummy, "int16");
923
6
            break;
924
0
        }
925
0
        case GDT_UInt32:
926
0
        {
927
0
            elt.nativeType = DtypeElt::NativeType::UNSIGNED_INT;
928
0
            dtype.Set(dummy, "uint32");
929
0
            break;
930
0
        }
931
2
        case GDT_Int32:
932
2
        {
933
2
            elt.nativeType = DtypeElt::NativeType::SIGNED_INT;
934
2
            dtype.Set(dummy, "int32");
935
2
            break;
936
0
        }
937
0
        case GDT_UInt64:
938
0
        {
939
0
            elt.nativeType = DtypeElt::NativeType::UNSIGNED_INT;
940
0
            dtype.Set(dummy, "uint64");
941
0
            break;
942
0
        }
943
2
        case GDT_Int64:
944
2
        {
945
2
            elt.nativeType = DtypeElt::NativeType::SIGNED_INT;
946
2
            dtype.Set(dummy, "int64");
947
2
            break;
948
0
        }
949
1
        case GDT_Float16:
950
1
        {
951
1
            elt.nativeType = DtypeElt::NativeType::IEEEFP;
952
1
            dtype.Set(dummy, "float16");
953
1
            break;
954
0
        }
955
1
        case GDT_Float32:
956
1
        {
957
1
            elt.nativeType = DtypeElt::NativeType::IEEEFP;
958
1
            dtype.Set(dummy, "float32");
959
1
            break;
960
0
        }
961
83
        case GDT_Float64:
962
83
        {
963
83
            elt.nativeType = DtypeElt::NativeType::IEEEFP;
964
83
            dtype.Set(dummy, "float64");
965
83
            break;
966
0
        }
967
0
        case GDT_Unknown:
968
0
        case GDT_CInt16:
969
0
        case GDT_CInt32:
970
0
        {
971
0
            bUnsupported = true;
972
0
            break;
973
0
        }
974
0
        case GDT_CFloat16:
975
0
        {
976
0
            elt.nativeType = DtypeElt::NativeType::COMPLEX_IEEEFP;
977
0
            dtype.Set(dummy, "complex32");
978
0
            break;
979
0
        }
980
0
        case GDT_CFloat32:
981
0
        {
982
0
            elt.nativeType = DtypeElt::NativeType::COMPLEX_IEEEFP;
983
0
            dtype.Set(dummy, "complex64");
984
0
            break;
985
0
        }
986
0
        case GDT_CFloat64:
987
0
        {
988
0
            elt.nativeType = DtypeElt::NativeType::COMPLEX_IEEEFP;
989
0
            dtype.Set(dummy, "complex128");
990
0
            break;
991
0
        }
992
0
        case GDT_TypeCount:
993
0
        {
994
0
            static_assert(GDT_TypeCount == GDT_CFloat16 + 1,
995
0
                          "GDT_TypeCount == GDT_CFloat16 + 1");
996
0
            break;
997
0
        }
998
148
    }
999
148
    if (bUnsupported)
1000
0
    {
1001
0
        CPLError(CE_Failure, CPLE_NotSupported, "Unsupported data type: %s",
1002
0
                 GDALGetDataTypeName(eDT));
1003
0
        dtype = CPLJSONObject();
1004
0
        dtype.Deinit();
1005
0
        return dtype;
1006
0
    }
1007
148
    elt.nativeOffset = 0;
1008
148
    elt.nativeSize = GDALGetDataTypeSizeBytes(eDT);
1009
148
    elt.gdalOffset = 0;
1010
148
    elt.gdalSize = elt.nativeSize;
1011
#ifdef CPL_MSB
1012
    elt.needByteSwapping = elt.nativeSize > 1;
1013
#endif
1014
148
    aoDtypeElts.emplace_back(elt);
1015
1016
148
    return dtype;
1017
148
}
1018
1019
/************************************************************************/
1020
/*                     ZarrV3Group::CreateMDArray()                     */
1021
/************************************************************************/
1022
1023
std::shared_ptr<GDALMDArray> ZarrV3Group::CreateMDArray(
1024
    const std::string &osName,
1025
    const std::vector<std::shared_ptr<GDALDimension>> &aoDimensions,
1026
    const GDALExtendedDataType &oDataType, CSLConstList papszOptions)
1027
148
{
1028
148
    if (!CheckValidAndErrorOutIfNot())
1029
0
        return nullptr;
1030
1031
148
    if (!m_bUpdatable)
1032
0
    {
1033
0
        CPLError(CE_Failure, CPLE_NotSupported,
1034
0
                 "Dataset not open in update mode");
1035
0
        return nullptr;
1036
0
    }
1037
148
    if (!IsValidObjectName(osName))
1038
0
    {
1039
0
        CPLError(CE_Failure, CPLE_NotSupported, "Invalid array name");
1040
0
        return nullptr;
1041
0
    }
1042
1043
148
    if (oDataType.GetClass() != GEDTC_NUMERIC &&
1044
0
        oDataType.GetClass() != GEDTC_STRING)
1045
0
    {
1046
0
        CPLError(CE_Failure, CPLE_AppDefined,
1047
0
                 "Unsupported data type with Zarr V3");
1048
0
        return nullptr;
1049
0
    }
1050
1051
148
    if (!EQUAL(CSLFetchNameValueDef(papszOptions, "FILTER", "NONE"), "NONE"))
1052
0
    {
1053
0
        CPLError(CE_Failure, CPLE_AppDefined,
1054
0
                 "FILTER option not supported with Zarr V3");
1055
0
        return nullptr;
1056
0
    }
1057
1058
148
    std::vector<DtypeElt> aoDtypeElts;
1059
148
    const auto dtype = FillDTypeElts(oDataType, aoDtypeElts)["dummy"];
1060
148
    if (!dtype.IsValid() || aoDtypeElts.empty())
1061
0
        return nullptr;
1062
1063
148
    GetMDArrayNames();
1064
1065
148
    if (cpl::contains(m_oSetArrayNames, osName))
1066
0
    {
1067
0
        CPLError(CE_Failure, CPLE_AppDefined,
1068
0
                 "An array with same name (%s) already exists in group %s",
1069
0
                 osName.c_str(), GetFullName().c_str());
1070
0
        return nullptr;
1071
0
    }
1072
1073
148
    std::vector<GUInt64> anOuterBlockSize;
1074
148
    if (!ZarrArray::FillBlockSize(aoDimensions, oDataType, anOuterBlockSize,
1075
148
                                  papszOptions))
1076
0
        return nullptr;
1077
1078
148
    const char *pszDimSeparator =
1079
148
        CSLFetchNameValueDef(papszOptions, "DIM_SEPARATOR", "/");
1080
1081
148
    const std::string osArrayDirectory =
1082
148
        CPLFormFilenameSafe(m_osDirectoryName.c_str(), osName.c_str(), nullptr);
1083
148
    if (VSIMkdir(osArrayDirectory.c_str(), 0755) != 0)
1084
0
    {
1085
0
        VSIStatBufL sStat;
1086
0
        if (VSIStatL(osArrayDirectory.c_str(), &sStat) == 0)
1087
0
        {
1088
0
            CPLError(CE_Failure, CPLE_FileIO, "Directory %s already exists.",
1089
0
                     osArrayDirectory.c_str());
1090
0
        }
1091
0
        else
1092
0
        {
1093
0
            CPLError(CE_Failure, CPLE_FileIO, "Cannot create directory %s.",
1094
0
                     osArrayDirectory.c_str());
1095
0
        }
1096
0
        return nullptr;
1097
0
    }
1098
1099
148
    std::unique_ptr<ZarrV3CodecSequence> poCodecs;
1100
148
    CPLJSONArray oCodecs;
1101
1102
148
    const bool bIsString = (oDataType.GetClass() == GEDTC_STRING);
1103
1104
148
    const bool bFortranOrder = EQUAL(
1105
148
        CSLFetchNameValueDef(papszOptions, "CHUNK_MEMORY_LAYOUT", "C"), "F");
1106
148
    if (!bIsString && bFortranOrder && aoDimensions.size() > 1)
1107
0
    {
1108
0
        CPLJSONObject oCodec;
1109
0
        oCodec.Add("name", "transpose");
1110
0
        std::vector<int> anOrder;
1111
0
        const int nDims = static_cast<int>(aoDimensions.size());
1112
0
        for (int i = 0; i < nDims; ++i)
1113
0
        {
1114
0
            anOrder.push_back(nDims - 1 - i);
1115
0
        }
1116
0
        oCodec.Add("configuration",
1117
0
                   ZarrV3CodecTranspose::GetConfiguration(anOrder));
1118
0
        oCodecs.Add(oCodec);
1119
0
    }
1120
1121
    // Array-to-bytes codec: vlen-utf8 for strings, bytes for numeric
1122
148
    if (bIsString)
1123
0
    {
1124
0
        CPLJSONObject oCodec;
1125
0
        oCodec.Add("name", "vlen-utf8");
1126
0
        oCodecs.Add(oCodec);
1127
0
    }
1128
148
    else
1129
148
    {
1130
        // Not documented option, but 'bytes' codec is required
1131
148
        const char *pszEndian =
1132
148
            CSLFetchNameValueDef(papszOptions, "@ENDIAN", "little");
1133
148
        CPLJSONObject oCodec;
1134
148
        oCodec.Add("name", "bytes");
1135
148
        oCodec.Add("configuration", ZarrV3CodecBytes::GetConfiguration(
1136
148
                                        EQUAL(pszEndian, "little")));
1137
148
        oCodecs.Add(oCodec);
1138
148
    }
1139
1140
148
    const char *pszCompressor =
1141
148
        CSLFetchNameValueDef(papszOptions, "COMPRESS", "NONE");
1142
148
    if (EQUAL(pszCompressor, "GZIP"))
1143
0
    {
1144
0
        CPLJSONObject oCodec;
1145
0
        oCodec.Add("name", "gzip");
1146
0
        const char *pszLevel =
1147
0
            CSLFetchNameValueDef(papszOptions, "GZIP_LEVEL", "6");
1148
0
        oCodec.Add("configuration",
1149
0
                   ZarrV3CodecGZip::GetConfiguration(atoi(pszLevel)));
1150
0
        oCodecs.Add(oCodec);
1151
0
    }
1152
148
    else if (EQUAL(pszCompressor, "BLOSC"))
1153
0
    {
1154
0
        const auto psCompressor = CPLGetCompressor("blosc");
1155
0
        if (!psCompressor)
1156
0
            return nullptr;
1157
0
        const char *pszOptions =
1158
0
            CSLFetchNameValueDef(psCompressor->papszMetadata, "OPTIONS", "");
1159
0
        CPLXMLTreeCloser oTreeCompressor(CPLParseXMLString(pszOptions));
1160
0
        const auto psRoot =
1161
0
            oTreeCompressor.get()
1162
0
                ? CPLGetXMLNode(oTreeCompressor.get(), "=Options")
1163
0
                : nullptr;
1164
0
        if (!psRoot)
1165
0
            return nullptr;
1166
1167
0
        const char *cname = "zlib";
1168
0
        for (const CPLXMLNode *psNode = psRoot->psChild; psNode != nullptr;
1169
0
             psNode = psNode->psNext)
1170
0
        {
1171
0
            if (psNode->eType == CXT_Element)
1172
0
            {
1173
0
                const char *pszName = CPLGetXMLValue(psNode, "name", "");
1174
0
                if (EQUAL(pszName, "CNAME"))
1175
0
                {
1176
0
                    cname = CPLGetXMLValue(psNode, "default", cname);
1177
0
                }
1178
0
            }
1179
0
        }
1180
1181
0
        CPLJSONObject oCodec;
1182
0
        oCodec.Add("name", "blosc");
1183
0
        cname = CSLFetchNameValueDef(papszOptions, "BLOSC_CNAME", cname);
1184
0
        const int clevel =
1185
0
            atoi(CSLFetchNameValueDef(papszOptions, "BLOSC_CLEVEL", "5"));
1186
0
        const char *shuffle =
1187
0
            CSLFetchNameValueDef(papszOptions, "BLOSC_SHUFFLE", "BYTE");
1188
0
        shuffle = (EQUAL(shuffle, "0") || EQUAL(shuffle, "NONE")) ? "noshuffle"
1189
0
                  : (EQUAL(shuffle, "1") || EQUAL(shuffle, "BYTE")) ? "shuffle"
1190
0
                  : (EQUAL(shuffle, "2") || EQUAL(shuffle, "BIT"))
1191
0
                      ? "bitshuffle"
1192
0
                      : "invalid";
1193
0
        const int nDefaultTypeSize =
1194
0
            bIsString ? 1
1195
0
                      : GDALGetDataTypeSizeBytes(GDALGetNonComplexDataType(
1196
0
                            oDataType.GetNumericDataType()));
1197
0
        const int typesize =
1198
0
            atoi(CSLFetchNameValueDef(papszOptions, "BLOSC_TYPESIZE",
1199
0
                                      CPLSPrintf("%d", nDefaultTypeSize)));
1200
0
        const int blocksize =
1201
0
            atoi(CSLFetchNameValueDef(papszOptions, "BLOSC_BLOCKSIZE", "0"));
1202
0
        oCodec.Add("configuration",
1203
0
                   ZarrV3CodecBlosc::GetConfiguration(cname, clevel, shuffle,
1204
0
                                                      typesize, blocksize));
1205
0
        oCodecs.Add(oCodec);
1206
0
    }
1207
148
    else if (EQUAL(pszCompressor, "ZSTD"))
1208
0
    {
1209
0
        CPLJSONObject oCodec;
1210
0
        oCodec.Add("name", "zstd");
1211
0
        const char *pszLevel =
1212
0
            CSLFetchNameValueDef(papszOptions, "ZSTD_LEVEL", "13");
1213
0
        const bool bChecksum = CPLTestBool(
1214
0
            CSLFetchNameValueDef(papszOptions, "ZSTD_CHECKSUM", "FALSE"));
1215
0
        oCodec.Add("configuration", ZarrV3CodecZstd::GetConfiguration(
1216
0
                                        atoi(pszLevel), bChecksum));
1217
0
        oCodecs.Add(oCodec);
1218
0
    }
1219
148
    else if (!EQUAL(pszCompressor, "NONE"))
1220
0
    {
1221
0
        CPLError(CE_Failure, CPLE_AppDefined,
1222
0
                 "COMPRESS = %s not implemented with Zarr V3", pszCompressor);
1223
0
        return nullptr;
1224
0
    }
1225
1226
    // Sharding: wrap inner codecs into a sharding_indexed codec
1227
148
    const char *pszShardChunkShape =
1228
148
        CSLFetchNameValue(papszOptions, "SHARD_CHUNK_SHAPE");
1229
148
    if (pszShardChunkShape != nullptr)
1230
0
    {
1231
1232
0
        const CPLStringList aosChunkShape(
1233
0
            CSLTokenizeString2(pszShardChunkShape, ",", 0));
1234
0
        if (static_cast<size_t>(aosChunkShape.size()) != aoDimensions.size())
1235
0
        {
1236
0
            CPLError(CE_Failure, CPLE_AppDefined,
1237
0
                     "SHARD_CHUNK_SHAPE has %d values, expected %d",
1238
0
                     aosChunkShape.size(),
1239
0
                     static_cast<int>(aoDimensions.size()));
1240
0
            return nullptr;
1241
0
        }
1242
1243
0
        CPLJSONArray oChunkShapeArray;
1244
0
        for (int i = 0; i < aosChunkShape.size(); ++i)
1245
0
        {
1246
0
            const auto nInner = static_cast<GUInt64>(atoll(aosChunkShape[i]));
1247
0
            if (nInner == 0 || anOuterBlockSize[i] % nInner != 0)
1248
0
            {
1249
0
                CPLError(CE_Failure, CPLE_AppDefined,
1250
0
                         "SHARD_CHUNK_SHAPE[%d]=%s must divide "
1251
0
                         "BLOCKSIZE[%d]=" CPL_FRMT_GUIB " evenly",
1252
0
                         i, aosChunkShape[i], i, anOuterBlockSize[i]);
1253
0
                return nullptr;
1254
0
            }
1255
0
            oChunkShapeArray.Add(static_cast<uint64_t>(nInner));
1256
0
        }
1257
1258
        // Index codecs: always bytes(little) + crc32c
1259
0
        CPLJSONArray oIndexCodecs;
1260
0
        {
1261
0
            CPLJSONObject oBytesCodec;
1262
0
            oBytesCodec.Add("name", "bytes");
1263
0
            oBytesCodec.Add("configuration",
1264
0
                            ZarrV3CodecBytes::GetConfiguration(true));
1265
0
            oIndexCodecs.Add(oBytesCodec);
1266
0
        }
1267
0
        {
1268
0
            CPLJSONObject oCRC32CCodec;
1269
0
            oCRC32CCodec.Add("name", "crc32c");
1270
0
            oIndexCodecs.Add(oCRC32CCodec);
1271
0
        }
1272
1273
0
        CPLJSONObject oShardingConfig;
1274
0
        oShardingConfig.Add("chunk_shape", oChunkShapeArray);
1275
0
        oShardingConfig.Add("codecs", oCodecs);
1276
0
        oShardingConfig.Add("index_codecs", oIndexCodecs);
1277
0
        oShardingConfig.Add("index_location", "end");
1278
1279
0
        CPLJSONObject oShardingCodec;
1280
0
        oShardingCodec.Add("name", "sharding_indexed");
1281
0
        oShardingCodec.Add("configuration", oShardingConfig);
1282
1283
        // Replace top-level codecs with just the sharding codec
1284
0
        oCodecs = CPLJSONArray();
1285
0
        oCodecs.Add(oShardingCodec);
1286
0
    }
1287
1288
148
    std::vector<GUInt64> anInnerBlockSize = anOuterBlockSize;
1289
148
    if (oCodecs.Size() > 0)
1290
148
    {
1291
148
        std::vector<GByte> abyNoData;
1292
148
        poCodecs = ZarrV3Array::SetupCodecs(
1293
148
            GetFullName() + "/" + osName, oCodecs, anOuterBlockSize,
1294
148
            anInnerBlockSize, aoDtypeElts.back(), abyNoData);
1295
148
        if (!poCodecs)
1296
0
        {
1297
0
            return nullptr;
1298
0
        }
1299
148
    }
1300
1301
148
    auto poArray = ZarrV3Array::Create(m_poSharedResource, Self(), osName,
1302
148
                                       aoDimensions, oDataType, aoDtypeElts,
1303
148
                                       anOuterBlockSize, anInnerBlockSize);
1304
1305
148
    if (!poArray)
1306
0
        return nullptr;
1307
148
    poArray->SetNew(true);
1308
148
    const std::string osFilename =
1309
148
        CPLFormFilenameSafe(osArrayDirectory.c_str(), "zarr.json", nullptr);
1310
148
    poArray->SetFilename(osFilename);
1311
148
    poArray->SetDimSeparator(pszDimSeparator);
1312
148
    poArray->SetDtype(dtype);
1313
148
    const std::string osLastCodecName =
1314
148
        oCodecs.Size() > 0 ? oCodecs[oCodecs.Size() - 1].GetString("name")
1315
148
                           : std::string();
1316
148
    if (!osLastCodecName.empty() && osLastCodecName != "bytes" &&
1317
0
        osLastCodecName != "vlen-utf8")
1318
0
    {
1319
0
        poArray->SetStructuralInfo(
1320
0
            "COMPRESSOR", oCodecs[oCodecs.Size() - 1].ToString().c_str());
1321
0
    }
1322
148
    if (poCodecs)
1323
148
        poArray->SetCodecs(oCodecs, std::move(poCodecs));
1324
1325
148
    poArray->SetCreationOptions(papszOptions);
1326
148
    poArray->SetUpdatable(true);
1327
148
    poArray->SetDefinitionModified(true);
1328
148
    if (!cpl::starts_with(osFilename, "/vsi") && !poArray->Flush())
1329
0
        return nullptr;
1330
148
    RegisterArray(poArray);
1331
1332
148
    return poArray;
1333
148
}