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 : * Permission is hereby granted, free of charge, to any person obtaining a
11 : * copy of this software and associated documentation files (the "Software"),
12 : * to deal in the Software without restriction, including without limitation
13 : * the rights to use, copy, modify, merge, publish, distribute, sublicense,
14 : * and/or sell copies of the Software, and to permit persons to whom the
15 : * Software is furnished to do so, subject to the following conditions:
16 : *
17 : * The above copyright notice and this permission notice shall be included
18 : * in all copies or substantial portions of the Software.
19 : *
20 : * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
21 : * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 : * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
23 : * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 : * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25 : * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
26 : * DEALINGS IN THE SOFTWARE.
27 : ****************************************************************************/
28 :
29 : #include "zarr.h"
30 :
31 : #include "cpl_minixml.h"
32 :
33 : #include <algorithm>
34 : #include <cassert>
35 : #include <limits>
36 : #include <map>
37 : #include <set>
38 :
39 : /************************************************************************/
40 : /* ZarrV2Group::Create() */
41 : /************************************************************************/
42 :
43 : std::shared_ptr<ZarrV2Group>
44 1015 : ZarrV2Group::Create(const std::shared_ptr<ZarrSharedResource> &poSharedResource,
45 : const std::string &osParentName, const std::string &osName)
46 : {
47 : auto poGroup = std::shared_ptr<ZarrV2Group>(
48 1015 : new ZarrV2Group(poSharedResource, osParentName, osName));
49 1015 : poGroup->SetSelf(poGroup);
50 1015 : return poGroup;
51 : }
52 :
53 : /************************************************************************/
54 : /* ZarrV2Group::~ZarrV2Group() */
55 : /************************************************************************/
56 :
57 2030 : ZarrV2Group::~ZarrV2Group()
58 : {
59 1015 : if (m_bValid && m_oAttrGroup.IsModified())
60 : {
61 54 : CPLJSONDocument oDoc;
62 27 : oDoc.SetRoot(m_oAttrGroup.Serialize());
63 : const std::string osAttrFilename =
64 27 : CPLFormFilename(m_osDirectoryName.c_str(), ".zattrs", nullptr);
65 27 : oDoc.Save(osAttrFilename);
66 27 : m_poSharedResource->SetZMetadataItem(osAttrFilename, oDoc.GetRoot());
67 : }
68 2030 : }
69 :
70 : /************************************************************************/
71 : /* ExploreDirectory() */
72 : /************************************************************************/
73 :
74 227 : void ZarrV2Group::ExploreDirectory() const
75 : {
76 227 : if (m_bDirectoryExplored || m_osDirectoryName.empty())
77 185 : return;
78 227 : m_bDirectoryExplored = true;
79 :
80 227 : const CPLStringList aosFiles(VSIReadDir(m_osDirectoryName.c_str()));
81 : // If the directory contains a .zarray, no need to recurse.
82 387 : for (int i = 0; i < aosFiles.size(); ++i)
83 : {
84 345 : if (strcmp(aosFiles[i], ".zarray") == 0)
85 185 : return;
86 : }
87 :
88 202 : for (int i = 0; i < aosFiles.size(); ++i)
89 : {
90 320 : if (aosFiles[i][0] != 0 && strcmp(aosFiles[i], ".") != 0 &&
91 145 : strcmp(aosFiles[i], "..") != 0 &&
92 130 : strcmp(aosFiles[i], ".zgroup") != 0 &&
93 402 : strcmp(aosFiles[i], ".zattrs") != 0 &&
94 : // Exclude filenames ending with '/'. This can happen on some
95 : // object storage like S3 where a "foo" file and a "foo/" directory
96 : // can coexist. The ending slash is only appended in that situation
97 : // where both a file and directory have the same name. So we can
98 : // safely ignore the one with an ending slash, as we will also
99 : // encounter its version without slash. Cf use case of
100 : // https://github.com/OSGeo/gdal/issues/8192
101 82 : aosFiles[i][strlen(aosFiles[i]) - 1] != '/')
102 : {
103 : const std::string osSubDir = CPLFormFilename(
104 164 : m_osDirectoryName.c_str(), aosFiles[i], nullptr);
105 : VSIStatBufL sStat;
106 : std::string osFilename =
107 164 : CPLFormFilename(osSubDir.c_str(), ".zarray", nullptr);
108 82 : if (VSIStatL(osFilename.c_str(), &sStat) == 0)
109 : {
110 45 : if (std::find(m_aosArrays.begin(), m_aosArrays.end(),
111 45 : aosFiles[i]) == m_aosArrays.end())
112 : {
113 35 : m_aosArrays.emplace_back(aosFiles[i]);
114 : }
115 : }
116 : else
117 : {
118 : osFilename =
119 37 : CPLFormFilename(osSubDir.c_str(), ".zgroup", nullptr);
120 37 : if (VSIStatL(osFilename.c_str(), &sStat) == 0)
121 31 : m_aosGroups.emplace_back(aosFiles[i]);
122 : }
123 : }
124 : }
125 : }
126 :
127 : /************************************************************************/
128 : /* OpenZarrArray() */
129 : /************************************************************************/
130 :
131 634 : std::shared_ptr<ZarrArray> ZarrV2Group::OpenZarrArray(const std::string &osName,
132 : CSLConstList) const
133 : {
134 634 : if (!CheckValidAndErrorOutIfNot())
135 0 : return nullptr;
136 :
137 634 : auto oIter = m_oMapMDArrays.find(osName);
138 634 : if (oIter != m_oMapMDArrays.end())
139 449 : return oIter->second;
140 :
141 185 : if (!m_bReadFromZMetadata && !m_osDirectoryName.empty())
142 : {
143 : const std::string osSubDir =
144 179 : CPLFormFilename(m_osDirectoryName.c_str(), osName.c_str(), nullptr);
145 : VSIStatBufL sStat;
146 : const std::string osZarrayFilename =
147 179 : CPLFormFilename(osSubDir.c_str(), ".zarray", nullptr);
148 179 : if (VSIStatL(osZarrayFilename.c_str(), &sStat) == 0)
149 : {
150 126 : CPLJSONDocument oDoc;
151 63 : if (!oDoc.Load(osZarrayFilename))
152 0 : return nullptr;
153 63 : const auto oRoot = oDoc.GetRoot();
154 : return LoadArray(osName, osZarrayFilename, oRoot, false,
155 126 : CPLJSONObject());
156 : }
157 : }
158 :
159 122 : return nullptr;
160 : }
161 :
162 : /************************************************************************/
163 : /* OpenZarrGroup() */
164 : /************************************************************************/
165 :
166 : std::shared_ptr<ZarrGroupBase>
167 529 : ZarrV2Group::OpenZarrGroup(const std::string &osName, CSLConstList) const
168 : {
169 529 : if (!CheckValidAndErrorOutIfNot())
170 0 : return nullptr;
171 :
172 529 : auto oIter = m_oMapGroups.find(osName);
173 529 : if (oIter != m_oMapGroups.end())
174 174 : return oIter->second;
175 :
176 355 : if (!m_bReadFromZMetadata && !m_osDirectoryName.empty())
177 : {
178 : const std::string osSubDir =
179 46 : CPLFormFilename(m_osDirectoryName.c_str(), osName.c_str(), nullptr);
180 : VSIStatBufL sStat;
181 : const std::string osZgroupFilename =
182 46 : CPLFormFilename(osSubDir.c_str(), ".zgroup", nullptr);
183 46 : if (VSIStatL(osZgroupFilename.c_str(), &sStat) == 0)
184 : {
185 84 : CPLJSONDocument oDoc;
186 42 : if (!oDoc.Load(osZgroupFilename))
187 0 : return nullptr;
188 :
189 : auto poSubGroup =
190 84 : ZarrV2Group::Create(m_poSharedResource, GetFullName(), osName);
191 42 : poSubGroup->m_poParent =
192 84 : std::dynamic_pointer_cast<ZarrGroupBase>(m_pSelf.lock());
193 42 : poSubGroup->SetUpdatable(m_bUpdatable);
194 42 : poSubGroup->SetDirectoryName(osSubDir);
195 42 : m_oMapGroups[osName] = poSubGroup;
196 :
197 : // Must be done after setting m_oMapGroups, to avoid infinite
198 : // recursion when opening NCZarr datasets with indexing variables
199 : // of dimensions
200 42 : poSubGroup->InitFromZGroup(oDoc.GetRoot());
201 :
202 42 : return poSubGroup;
203 : }
204 : }
205 :
206 313 : return nullptr;
207 : }
208 :
209 : /************************************************************************/
210 : /* ZarrV2Group::LoadAttributes() */
211 : /************************************************************************/
212 :
213 103 : void ZarrV2Group::LoadAttributes() const
214 : {
215 103 : if (m_bAttributesLoaded || m_osDirectoryName.empty())
216 86 : return;
217 41 : m_bAttributesLoaded = true;
218 :
219 41 : CPLJSONDocument oDoc;
220 : const std::string osZattrsFilename(
221 41 : CPLFormFilename(m_osDirectoryName.c_str(), ".zattrs", nullptr));
222 41 : CPLErrorStateBackuper oErrorStateBackuper(CPLQuietErrorHandler);
223 41 : if (!oDoc.Load(osZattrsFilename))
224 24 : return;
225 34 : auto oRoot = oDoc.GetRoot();
226 17 : m_oAttrGroup.Init(oRoot, m_bUpdatable);
227 : }
228 :
229 : /************************************************************************/
230 : /* ZarrV2Group::GetOrCreateSubGroup() */
231 : /************************************************************************/
232 :
233 : std::shared_ptr<ZarrV2Group>
234 115 : ZarrV2Group::GetOrCreateSubGroup(const std::string &osSubGroupFullname)
235 : {
236 : auto poSubGroup = std::dynamic_pointer_cast<ZarrV2Group>(
237 115 : OpenGroupFromFullname(osSubGroupFullname));
238 115 : if (poSubGroup)
239 : {
240 52 : return poSubGroup;
241 : }
242 :
243 63 : const auto nLastSlashPos = osSubGroupFullname.rfind('/');
244 : auto poBelongingGroup =
245 : (nLastSlashPos == 0)
246 63 : ? this
247 74 : : GetOrCreateSubGroup(osSubGroupFullname.substr(0, nLastSlashPos))
248 63 : .get();
249 :
250 : poSubGroup =
251 126 : ZarrV2Group::Create(m_poSharedResource, poBelongingGroup->GetFullName(),
252 189 : osSubGroupFullname.substr(nLastSlashPos + 1));
253 126 : poSubGroup->m_poParent = std::dynamic_pointer_cast<ZarrGroupBase>(
254 189 : poBelongingGroup->m_pSelf.lock());
255 126 : poSubGroup->SetDirectoryName(
256 : CPLFormFilename(poBelongingGroup->m_osDirectoryName.c_str(),
257 63 : poSubGroup->GetName().c_str(), nullptr));
258 63 : poSubGroup->m_bDirectoryExplored = true;
259 63 : poSubGroup->m_bAttributesLoaded = true;
260 63 : poSubGroup->m_bReadFromZMetadata = true;
261 63 : poSubGroup->SetUpdatable(m_bUpdatable);
262 :
263 63 : poBelongingGroup->m_oMapGroups[poSubGroup->GetName()] = poSubGroup;
264 63 : poBelongingGroup->m_aosGroups.emplace_back(poSubGroup->GetName());
265 63 : return poSubGroup;
266 : }
267 :
268 : /************************************************************************/
269 : /* ZarrV2Group::InitFromZMetadata() */
270 : /************************************************************************/
271 :
272 165 : void ZarrV2Group::InitFromZMetadata(const CPLJSONObject &obj)
273 : {
274 165 : m_bDirectoryExplored = true;
275 165 : m_bAttributesLoaded = true;
276 165 : m_bReadFromZMetadata = true;
277 :
278 330 : const auto metadata = obj["metadata"];
279 165 : if (metadata.GetType() != CPLJSONObject::Type::Object)
280 0 : return;
281 330 : const auto children = metadata.GetChildren();
282 330 : std::map<std::string, const CPLJSONObject *> oMapArrays;
283 :
284 : // First pass to create groups and collect arrays
285 915 : for (const auto &child : children)
286 : {
287 750 : const std::string osName(child.GetName());
288 750 : if (std::count(osName.begin(), osName.end(), '/') > 32)
289 : {
290 : // Avoid too deep recursion in GetOrCreateSubGroup()
291 0 : continue;
292 : }
293 750 : if (osName == ".zattrs")
294 : {
295 4 : m_oAttrGroup.Init(child, m_bUpdatable);
296 : }
297 1327 : else if (osName.size() > strlen("/.zgroup") &&
298 1327 : osName.substr(osName.size() - strlen("/.zgroup")) ==
299 : "/.zgroup")
300 : {
301 63 : GetOrCreateSubGroup(
302 126 : "/" + osName.substr(0, osName.size() - strlen("/.zgroup")));
303 : }
304 1201 : else if (osName.size() > strlen("/.zarray") &&
305 1201 : osName.substr(osName.size() - strlen("/.zarray")) ==
306 : "/.zarray")
307 : {
308 : auto osArrayFullname =
309 257 : osName.substr(0, osName.size() - strlen("/.zarray"));
310 257 : oMapArrays[osArrayFullname] = &child;
311 : }
312 : }
313 :
314 257 : const auto CreateArray = [this](const std::string &osArrayFullname,
315 : const CPLJSONObject &oArray,
316 41 : const CPLJSONObject &oAttributes)
317 : {
318 257 : const auto nLastSlashPos = osArrayFullname.rfind('/');
319 : auto poBelongingGroup =
320 : (nLastSlashPos == std::string::npos)
321 257 : ? this
322 298 : : GetOrCreateSubGroup("/" +
323 298 : osArrayFullname.substr(0, nLastSlashPos))
324 257 : .get();
325 : const auto osArrayName =
326 : nLastSlashPos == std::string::npos
327 : ? osArrayFullname
328 514 : : osArrayFullname.substr(nLastSlashPos + 1);
329 : const std::string osZarrayFilename = CPLFormFilename(
330 : CPLFormFilename(poBelongingGroup->m_osDirectoryName.c_str(),
331 : osArrayName.c_str(), nullptr),
332 257 : ".zarray", nullptr);
333 257 : poBelongingGroup->LoadArray(osArrayName, osZarrayFilename, oArray, true,
334 : oAttributes);
335 257 : };
336 :
337 : struct ArrayDesc
338 : {
339 : std::string osArrayFullname{};
340 : const CPLJSONObject *poArray = nullptr;
341 : const CPLJSONObject *poAttrs = nullptr;
342 : };
343 :
344 330 : std::vector<ArrayDesc> aoRegularArrays;
345 :
346 : // Second pass to read attributes and create arrays that are indexing
347 : // variable
348 915 : for (const auto &child : children)
349 : {
350 1500 : const std::string osName(child.GetName());
351 1331 : if (osName.size() > strlen("/.zattrs") &&
352 1331 : osName.substr(osName.size() - strlen("/.zattrs")) == "/.zattrs")
353 : {
354 : const auto osObjectFullnameNoLeadingSlash =
355 522 : osName.substr(0, osName.size() - strlen("/.zattrs"));
356 : auto poSubGroup = std::dynamic_pointer_cast<ZarrV2Group>(
357 783 : OpenGroupFromFullname('/' + osObjectFullnameNoLeadingSlash));
358 261 : if (poSubGroup)
359 : {
360 18 : poSubGroup->m_oAttrGroup.Init(child, m_bUpdatable);
361 : }
362 : else
363 : {
364 243 : auto oIter = oMapArrays.find(osObjectFullnameNoLeadingSlash);
365 243 : if (oIter != oMapArrays.end())
366 : {
367 : const auto nLastSlashPos =
368 242 : osObjectFullnameNoLeadingSlash.rfind('/');
369 : const auto osArrayName =
370 : (nLastSlashPos == std::string::npos)
371 : ? osObjectFullnameNoLeadingSlash
372 : : osObjectFullnameNoLeadingSlash.substr(
373 484 : nLastSlashPos + 1);
374 : const auto arrayDimensions =
375 726 : child["_ARRAY_DIMENSIONS"].ToArray();
376 953 : if (arrayDimensions.IsValid() &&
377 356 : arrayDimensions.Size() == 1 &&
378 356 : arrayDimensions[0].ToString() == osArrayName)
379 : {
380 88 : CreateArray(osObjectFullnameNoLeadingSlash,
381 88 : *(oIter->second), child);
382 88 : oMapArrays.erase(oIter);
383 : }
384 : else
385 : {
386 308 : ArrayDesc desc;
387 154 : desc.osArrayFullname = osObjectFullnameNoLeadingSlash;
388 154 : desc.poArray = oIter->second;
389 154 : desc.poAttrs = &child;
390 154 : aoRegularArrays.emplace_back(std::move(desc));
391 : }
392 : }
393 : }
394 : }
395 : }
396 :
397 : // Third pass to create non-indexing arrays with attributes
398 319 : for (const auto &desc : aoRegularArrays)
399 : {
400 154 : CreateArray(desc.osArrayFullname, *(desc.poArray), *(desc.poAttrs));
401 154 : oMapArrays.erase(desc.osArrayFullname);
402 : }
403 :
404 : // Fourth pass to create arrays without attributes
405 180 : for (const auto &kv : oMapArrays)
406 : {
407 15 : CreateArray(kv.first, *(kv.second), CPLJSONObject());
408 : }
409 : }
410 :
411 : /************************************************************************/
412 : /* ZarrV2Group::InitFromZGroup() */
413 : /************************************************************************/
414 :
415 105 : bool ZarrV2Group::InitFromZGroup(const CPLJSONObject &obj)
416 : {
417 : // Parse potential NCZarr (V2) extensions:
418 : // https://www.unidata.ucar.edu/software/netcdf/documentation/NUG/nczarr_head.html
419 315 : const auto nczarrGroup = obj["_NCZARR_GROUP"];
420 105 : if (nczarrGroup.GetType() == CPLJSONObject::Type::Object)
421 : {
422 24 : if (m_bUpdatable)
423 : {
424 4 : CPLError(CE_Failure, CPLE_NotSupported,
425 : "Update of NCZarr datasets is not supported");
426 4 : return false;
427 : }
428 20 : m_bDirectoryExplored = true;
429 :
430 : // If not opening from the root of the dataset, walk up to it
431 56 : if (!obj["_NCZARR_SUPERBLOCK"].IsValid() &&
432 36 : m_poParent.lock() == nullptr)
433 : {
434 : const std::string osParentGroupFilename(CPLFormFilename(
435 14 : CPLGetPath(m_osDirectoryName.c_str()), ".zgroup", nullptr));
436 : VSIStatBufL sStat;
437 7 : if (VSIStatL(osParentGroupFilename.c_str(), &sStat) == 0)
438 : {
439 12 : CPLJSONDocument oDoc;
440 6 : if (oDoc.Load(osParentGroupFilename))
441 : {
442 : auto poParent = ZarrV2Group::Create(
443 12 : m_poSharedResource, std::string(), std::string());
444 6 : poParent->m_bDirectoryExplored = true;
445 6 : poParent->SetDirectoryName(
446 : CPLGetPath(m_osDirectoryName.c_str()));
447 6 : poParent->InitFromZGroup(oDoc.GetRoot());
448 6 : m_poParentStrongRef = poParent;
449 6 : m_poParent = poParent;
450 :
451 : // Patch our name and fullname
452 6 : m_osName = CPLGetFilename(m_osDirectoryName.c_str());
453 : m_osFullName =
454 6 : poParent->GetFullName() == "/"
455 15 : ? m_osName
456 15 : : poParent->GetFullName() + "/" + m_osName;
457 : }
458 : }
459 : }
460 :
461 117 : const auto IsValidName = [](const std::string &s)
462 : {
463 351 : return !s.empty() && s != "." && s != ".." &&
464 351 : s.find("/") == std::string::npos &&
465 234 : s.find("\\") == std::string::npos;
466 : };
467 :
468 : // Create dimensions first, as they will be potentially patched
469 : // by the OpenMDArray() later
470 60 : const auto dims = nczarrGroup["dims"];
471 50 : for (const auto &jDim : dims.GetChildren())
472 : {
473 60 : const auto osName = jDim.GetName();
474 30 : const GUInt64 nSize = jDim.ToLong();
475 30 : if (!IsValidName(osName))
476 : {
477 0 : CPLError(CE_Failure, CPLE_AppDefined,
478 : "Invalid dimension name for %s", osName.c_str());
479 : }
480 30 : else if (nSize == 0)
481 : {
482 1 : CPLError(CE_Failure, CPLE_AppDefined,
483 : "Invalid dimension size for %s", osName.c_str());
484 : }
485 : else
486 : {
487 29 : CreateDimension(osName,
488 58 : std::string(), // type
489 29 : std::string(), // direction,
490 29 : nSize, nullptr);
491 : }
492 : }
493 :
494 60 : const auto vars = nczarrGroup["vars"].ToArray();
495 : // open first indexing variables
496 40 : std::set<std::string> oSetIndexingArrayNames;
497 57 : for (const auto &var : vars)
498 : {
499 111 : const auto osVarName = var.ToString();
500 37 : if (IsValidName(osVarName) &&
501 90 : m_oMapDimensions.find(osVarName) != m_oMapDimensions.end() &&
502 127 : m_oMapMDArrays.find(osVarName) == m_oMapMDArrays.end() &&
503 15 : oSetIndexingArrayNames.find(osVarName) ==
504 52 : oSetIndexingArrayNames.end())
505 : {
506 15 : oSetIndexingArrayNames.insert(osVarName);
507 15 : OpenMDArray(osVarName);
508 : }
509 : }
510 :
511 : // add regular arrays
512 40 : std::set<std::string> oSetRegularArrayNames;
513 57 : for (const auto &var : vars)
514 : {
515 111 : const auto osVarName = var.ToString();
516 37 : if (IsValidName(osVarName) &&
517 95 : m_oMapDimensions.find(osVarName) == m_oMapDimensions.end() &&
518 132 : m_oMapMDArrays.find(osVarName) == m_oMapMDArrays.end() &&
519 21 : oSetRegularArrayNames.find(osVarName) ==
520 58 : oSetRegularArrayNames.end())
521 : {
522 20 : oSetRegularArrayNames.insert(osVarName);
523 20 : m_aosArrays.emplace_back(osVarName);
524 : }
525 : }
526 :
527 : // Finally list groups
528 40 : std::set<std::string> oSetGroups;
529 60 : const auto groups = nczarrGroup["groups"].ToArray();
530 33 : for (const auto &group : groups)
531 : {
532 39 : const auto osGroupName = group.ToString();
533 26 : if (IsValidName(osGroupName) &&
534 26 : oSetGroups.find(osGroupName) == oSetGroups.end())
535 : {
536 12 : oSetGroups.insert(osGroupName);
537 12 : m_aosGroups.emplace_back(osGroupName);
538 : }
539 : }
540 : }
541 101 : return true;
542 : }
543 :
544 : /************************************************************************/
545 : /* ZarrV2Group::CreateOnDisk() */
546 : /************************************************************************/
547 :
548 233 : std::shared_ptr<ZarrV2Group> ZarrV2Group::CreateOnDisk(
549 : const std::shared_ptr<ZarrSharedResource> &poSharedResource,
550 : const std::string &osParentName, const std::string &osName,
551 : const std::string &osDirectoryName)
552 : {
553 233 : if (VSIMkdir(osDirectoryName.c_str(), 0755) != 0)
554 : {
555 : VSIStatBufL sStat;
556 5 : if (VSIStatL(osDirectoryName.c_str(), &sStat) == 0)
557 : {
558 2 : CPLError(CE_Failure, CPLE_FileIO, "Directory %s already exists.",
559 : osDirectoryName.c_str());
560 : }
561 : else
562 : {
563 3 : CPLError(CE_Failure, CPLE_FileIO, "Cannot create directory %s.",
564 : osDirectoryName.c_str());
565 : }
566 5 : return nullptr;
567 : }
568 :
569 : const std::string osZgroupFilename(
570 456 : CPLFormFilename(osDirectoryName.c_str(), ".zgroup", nullptr));
571 228 : VSILFILE *fp = VSIFOpenL(osZgroupFilename.c_str(), "wb");
572 228 : if (!fp)
573 : {
574 0 : CPLError(CE_Failure, CPLE_FileIO, "Cannot create file %s.",
575 : osZgroupFilename.c_str());
576 0 : return nullptr;
577 : }
578 228 : VSIFPrintfL(fp, "{\n \"zarr_format\": 2\n}\n");
579 228 : VSIFCloseL(fp);
580 :
581 456 : auto poGroup = ZarrV2Group::Create(poSharedResource, osParentName, osName);
582 228 : poGroup->SetDirectoryName(osDirectoryName);
583 228 : poGroup->SetUpdatable(true);
584 228 : poGroup->m_bDirectoryExplored = true;
585 :
586 456 : CPLJSONObject oObj;
587 228 : oObj.Add("zarr_format", 2);
588 228 : poSharedResource->SetZMetadataItem(osZgroupFilename, oObj);
589 :
590 228 : return poGroup;
591 : }
592 :
593 : /************************************************************************/
594 : /* ZarrV2Group::CreateGroup() */
595 : /************************************************************************/
596 :
597 : std::shared_ptr<GDALGroup>
598 75 : ZarrV2Group::CreateGroup(const std::string &osName,
599 : CSLConstList /* papszOptions */)
600 : {
601 75 : if (!CheckValidAndErrorOutIfNot())
602 0 : return nullptr;
603 :
604 75 : if (!m_bUpdatable)
605 : {
606 2 : CPLError(CE_Failure, CPLE_NotSupported,
607 : "Dataset not open in update mode");
608 2 : return nullptr;
609 : }
610 73 : if (!IsValidObjectName(osName))
611 : {
612 14 : CPLError(CE_Failure, CPLE_NotSupported, "Invalid group name");
613 14 : return nullptr;
614 : }
615 :
616 59 : GetGroupNames();
617 :
618 59 : if (std::find(m_aosGroups.begin(), m_aosGroups.end(), osName) !=
619 118 : m_aosGroups.end())
620 : {
621 2 : CPLError(CE_Failure, CPLE_AppDefined,
622 : "A group with same name already exists");
623 2 : return nullptr;
624 : }
625 :
626 : const std::string osDirectoryName =
627 114 : CPLFormFilename(m_osDirectoryName.c_str(), osName.c_str(), nullptr);
628 57 : auto poGroup = CreateOnDisk(m_poSharedResource, GetFullName(), osName,
629 114 : osDirectoryName);
630 57 : if (!poGroup)
631 2 : return nullptr;
632 55 : poGroup->m_poParent =
633 110 : std::dynamic_pointer_cast<ZarrGroupBase>(m_pSelf.lock());
634 55 : m_oMapGroups[osName] = poGroup;
635 55 : m_aosGroups.emplace_back(osName);
636 55 : return poGroup;
637 : }
638 :
639 : /************************************************************************/
640 : /* FillDTypeElts() */
641 : /************************************************************************/
642 :
643 313 : static CPLJSONObject FillDTypeElts(const GDALExtendedDataType &oDataType,
644 : size_t nGDALStartOffset,
645 : std::vector<DtypeElt> &aoDtypeElts,
646 : bool bUseUnicode)
647 : {
648 313 : CPLJSONObject dtype;
649 313 : const auto eClass = oDataType.GetClass();
650 : const size_t nNativeStartOffset =
651 313 : aoDtypeElts.empty()
652 313 : ? 0
653 4 : : aoDtypeElts.back().nativeOffset + aoDtypeElts.back().nativeSize;
654 626 : const std::string dummy("dummy");
655 :
656 313 : switch (eClass)
657 : {
658 4 : case GEDTC_STRING:
659 : {
660 4 : if (oDataType.GetMaxStringLength() == 0)
661 : {
662 0 : CPLError(CE_Failure, CPLE_NotSupported,
663 : "String arrays of unlimited size are not supported");
664 0 : dtype = CPLJSONObject();
665 0 : dtype.Deinit();
666 0 : return dtype;
667 : }
668 8 : DtypeElt elt;
669 4 : elt.nativeOffset = nNativeStartOffset;
670 4 : if (bUseUnicode)
671 : {
672 1 : elt.nativeType = DtypeElt::NativeType::STRING_UNICODE;
673 1 : elt.nativeSize = oDataType.GetMaxStringLength() * 4;
674 : #ifdef CPL_MSB
675 : elt.needByteSwapping = true;
676 : #endif
677 1 : dtype.Set(
678 : dummy,
679 : CPLSPrintf("<U%d", static_cast<int>(
680 1 : oDataType.GetMaxStringLength())));
681 : }
682 : else
683 : {
684 3 : elt.nativeType = DtypeElt::NativeType::STRING_ASCII;
685 3 : elt.nativeSize = oDataType.GetMaxStringLength();
686 3 : dtype.Set(
687 : dummy,
688 : CPLSPrintf("|S%d", static_cast<int>(
689 3 : oDataType.GetMaxStringLength())));
690 : }
691 4 : elt.gdalOffset = nGDALStartOffset;
692 4 : elt.gdalSize = sizeof(char *);
693 4 : aoDtypeElts.emplace_back(elt);
694 4 : break;
695 : }
696 :
697 305 : case GEDTC_NUMERIC:
698 : {
699 305 : const auto eDT = oDataType.GetNumericDataType();
700 305 : DtypeElt elt;
701 305 : bool bUnsupported = false;
702 305 : switch (eDT)
703 : {
704 112 : case GDT_Byte:
705 : {
706 112 : elt.nativeType = DtypeElt::NativeType::UNSIGNED_INT;
707 112 : dtype.Set(dummy, "|u1");
708 112 : break;
709 : }
710 3 : case GDT_Int8:
711 : {
712 3 : elt.nativeType = DtypeElt::NativeType::SIGNED_INT;
713 3 : dtype.Set(dummy, "|i1");
714 3 : break;
715 : }
716 8 : case GDT_UInt16:
717 : {
718 8 : elt.nativeType = DtypeElt::NativeType::UNSIGNED_INT;
719 8 : dtype.Set(dummy, "<u2");
720 8 : break;
721 : }
722 11 : case GDT_Int16:
723 : {
724 11 : elt.nativeType = DtypeElt::NativeType::SIGNED_INT;
725 11 : dtype.Set(dummy, "<i2");
726 11 : break;
727 : }
728 6 : case GDT_UInt32:
729 : {
730 6 : elt.nativeType = DtypeElt::NativeType::UNSIGNED_INT;
731 6 : dtype.Set(dummy, "<u4");
732 6 : break;
733 : }
734 7 : case GDT_Int32:
735 : {
736 7 : elt.nativeType = DtypeElt::NativeType::SIGNED_INT;
737 7 : dtype.Set(dummy, "<i4");
738 7 : break;
739 : }
740 4 : case GDT_UInt64:
741 : {
742 4 : elt.nativeType = DtypeElt::NativeType::UNSIGNED_INT;
743 4 : dtype.Set(dummy, "<u8");
744 4 : break;
745 : }
746 3 : case GDT_Int64:
747 : {
748 3 : elt.nativeType = DtypeElt::NativeType::SIGNED_INT;
749 3 : dtype.Set(dummy, "<i8");
750 3 : break;
751 : }
752 7 : case GDT_Float32:
753 : {
754 7 : elt.nativeType = DtypeElt::NativeType::IEEEFP;
755 7 : dtype.Set(dummy, "<f4");
756 7 : break;
757 : }
758 127 : case GDT_Float64:
759 : {
760 127 : elt.nativeType = DtypeElt::NativeType::IEEEFP;
761 127 : dtype.Set(dummy, "<f8");
762 127 : break;
763 : }
764 8 : case GDT_Unknown:
765 : case GDT_CInt16:
766 : case GDT_CInt32:
767 : {
768 8 : bUnsupported = true;
769 8 : break;
770 : }
771 4 : case GDT_CFloat32:
772 : {
773 4 : elt.nativeType = DtypeElt::NativeType::COMPLEX_IEEEFP;
774 4 : dtype.Set(dummy, "<c8");
775 4 : break;
776 : }
777 5 : case GDT_CFloat64:
778 : {
779 5 : elt.nativeType = DtypeElt::NativeType::COMPLEX_IEEEFP;
780 5 : dtype.Set(dummy, "<c16");
781 5 : break;
782 : }
783 0 : case GDT_TypeCount:
784 : {
785 : static_assert(GDT_TypeCount == GDT_Int8 + 1,
786 : "GDT_TypeCount == GDT_Int8 + 1");
787 0 : break;
788 : }
789 : }
790 305 : if (bUnsupported)
791 : {
792 8 : CPLError(CE_Failure, CPLE_NotSupported,
793 : "Unsupported data type: %s", GDALGetDataTypeName(eDT));
794 8 : dtype = CPLJSONObject();
795 8 : dtype.Deinit();
796 8 : return dtype;
797 : }
798 297 : elt.nativeOffset = nNativeStartOffset;
799 297 : elt.nativeSize = GDALGetDataTypeSizeBytes(eDT);
800 297 : elt.gdalOffset = nGDALStartOffset;
801 297 : elt.gdalSize = elt.nativeSize;
802 : #ifdef CPL_MSB
803 : elt.needByteSwapping = elt.nativeSize > 1;
804 : #endif
805 297 : aoDtypeElts.emplace_back(elt);
806 297 : break;
807 : }
808 :
809 4 : case GEDTC_COMPOUND:
810 : {
811 4 : const auto &comps = oDataType.GetComponents();
812 4 : CPLJSONArray array;
813 10 : for (const auto &comp : comps)
814 : {
815 6 : CPLJSONArray subArray;
816 6 : subArray.Add(comp->GetName());
817 : const auto subdtype = FillDTypeElts(
818 6 : comp->GetType(), nGDALStartOffset + comp->GetOffset(),
819 12 : aoDtypeElts, bUseUnicode);
820 6 : if (!subdtype.IsValid())
821 : {
822 0 : dtype = CPLJSONObject();
823 0 : dtype.Deinit();
824 0 : return dtype;
825 : }
826 6 : if (subdtype.GetType() == CPLJSONObject::Type::Object)
827 4 : subArray.Add(subdtype["dummy"]);
828 : else
829 2 : subArray.Add(subdtype);
830 6 : array.Add(subArray);
831 : }
832 4 : dtype = std::move(array);
833 4 : break;
834 : }
835 : }
836 305 : return dtype;
837 : }
838 :
839 : /************************************************************************/
840 : /* ZarrV2Group::CreateMDArray() */
841 : /************************************************************************/
842 :
843 321 : std::shared_ptr<GDALMDArray> ZarrV2Group::CreateMDArray(
844 : const std::string &osName,
845 : const std::vector<std::shared_ptr<GDALDimension>> &aoDimensions,
846 : const GDALExtendedDataType &oDataType, CSLConstList papszOptions)
847 : {
848 321 : if (!CheckValidAndErrorOutIfNot())
849 0 : return nullptr;
850 :
851 321 : if (!m_bUpdatable)
852 : {
853 0 : CPLError(CE_Failure, CPLE_NotSupported,
854 : "Dataset not open in update mode");
855 0 : return nullptr;
856 : }
857 321 : if (!IsValidObjectName(osName))
858 : {
859 14 : CPLError(CE_Failure, CPLE_NotSupported, "Invalid array name");
860 14 : return nullptr;
861 : }
862 :
863 614 : std::vector<DtypeElt> aoDtypeElts;
864 : const bool bUseUnicode =
865 307 : EQUAL(CSLFetchNameValueDef(papszOptions, "STRING_FORMAT", "ASCII"),
866 : "UNICODE");
867 614 : const auto dtype = FillDTypeElts(oDataType, 0, aoDtypeElts, bUseUnicode);
868 307 : if (!dtype.IsValid() || aoDtypeElts.empty())
869 8 : return nullptr;
870 :
871 299 : GetMDArrayNames();
872 :
873 299 : if (std::find(m_aosArrays.begin(), m_aosArrays.end(), osName) !=
874 598 : m_aosArrays.end())
875 : {
876 2 : CPLError(CE_Failure, CPLE_AppDefined,
877 : "An array with same name already exists");
878 2 : return nullptr;
879 : }
880 :
881 594 : CPLJSONObject oCompressor;
882 297 : oCompressor.Deinit();
883 : const char *pszCompressor =
884 297 : CSLFetchNameValueDef(papszOptions, "COMPRESS", "NONE");
885 297 : const CPLCompressor *psCompressor = nullptr;
886 297 : const CPLCompressor *psDecompressor = nullptr;
887 297 : if (!EQUAL(pszCompressor, "NONE"))
888 : {
889 17 : psCompressor = CPLGetCompressor(pszCompressor);
890 17 : psDecompressor = CPLGetCompressor(pszCompressor);
891 17 : if (psCompressor == nullptr || psDecompressor == nullptr)
892 : {
893 1 : CPLError(CE_Failure, CPLE_NotSupported,
894 : "Compressor/decompressor for %s not available",
895 : pszCompressor);
896 1 : return nullptr;
897 : }
898 : const char *pszOptions =
899 16 : CSLFetchNameValue(psCompressor->papszMetadata, "OPTIONS");
900 16 : if (pszOptions)
901 : {
902 32 : CPLXMLTreeCloser oTree(CPLParseXMLString(pszOptions));
903 : const auto psRoot =
904 16 : oTree.get() ? CPLGetXMLNode(oTree.get(), "=Options") : nullptr;
905 16 : if (psRoot)
906 : {
907 16 : for (const CPLXMLNode *psNode = psRoot->psChild;
908 37 : psNode != nullptr; psNode = psNode->psNext)
909 : {
910 21 : if (psNode->eType == CXT_Element &&
911 21 : strcmp(psNode->pszValue, "Option") == 0)
912 : {
913 : const char *pszName =
914 21 : CPLGetXMLValue(psNode, "name", nullptr);
915 : const char *pszType =
916 21 : CPLGetXMLValue(psNode, "type", nullptr);
917 21 : if (pszName && pszType)
918 : {
919 42 : const char *pszVal = CSLFetchNameValueDef(
920 : papszOptions,
921 42 : (std::string(pszCompressor) + '_' + pszName)
922 : .c_str(),
923 : CPLGetXMLValue(psNode, "default", nullptr));
924 21 : if (pszVal)
925 : {
926 21 : if (EQUAL(pszName, "SHUFFLE") &&
927 1 : EQUAL(pszVal, "BYTE"))
928 : {
929 1 : pszVal = "1";
930 1 : pszType = "integer";
931 : }
932 :
933 21 : if (!oCompressor.IsValid())
934 : {
935 16 : oCompressor = CPLJSONObject();
936 16 : oCompressor.Add(
937 : "id",
938 32 : CPLString(pszCompressor).tolower());
939 : }
940 :
941 : std::string osOptName(
942 42 : CPLString(pszName).tolower());
943 21 : if (STARTS_WITH(pszType, "int"))
944 19 : oCompressor.Add(osOptName, atoi(pszVal));
945 : else
946 2 : oCompressor.Add(osOptName, pszVal);
947 : }
948 : }
949 : }
950 : }
951 : }
952 : }
953 : }
954 :
955 592 : CPLJSONArray oFilters;
956 : const char *pszFilter =
957 296 : CSLFetchNameValueDef(papszOptions, "FILTER", "NONE");
958 296 : if (!EQUAL(pszFilter, "NONE"))
959 : {
960 1 : const auto psFilterCompressor = CPLGetCompressor(pszFilter);
961 1 : const auto psFilterDecompressor = CPLGetCompressor(pszFilter);
962 1 : if (psFilterCompressor == nullptr || psFilterDecompressor == nullptr)
963 : {
964 0 : CPLError(CE_Failure, CPLE_NotSupported,
965 : "Compressor/decompressor for filter %s not available",
966 : pszFilter);
967 0 : return nullptr;
968 : }
969 :
970 1 : CPLJSONObject oFilter;
971 1 : oFilter.Add("id", CPLString(pszFilter).tolower());
972 1 : oFilters.Add(oFilter);
973 :
974 : const char *pszOptions =
975 1 : CSLFetchNameValue(psFilterCompressor->papszMetadata, "OPTIONS");
976 1 : if (pszOptions)
977 : {
978 2 : CPLXMLTreeCloser oTree(CPLParseXMLString(pszOptions));
979 : const auto psRoot =
980 1 : oTree.get() ? CPLGetXMLNode(oTree.get(), "=Options") : nullptr;
981 1 : if (psRoot)
982 : {
983 1 : for (const CPLXMLNode *psNode = psRoot->psChild;
984 2 : psNode != nullptr; psNode = psNode->psNext)
985 : {
986 1 : if (psNode->eType == CXT_Element &&
987 1 : strcmp(psNode->pszValue, "Option") == 0)
988 : {
989 : const char *pszName =
990 1 : CPLGetXMLValue(psNode, "name", nullptr);
991 : const char *pszType =
992 1 : CPLGetXMLValue(psNode, "type", nullptr);
993 1 : if (pszName && pszType)
994 : {
995 2 : const char *pszVal = CSLFetchNameValueDef(
996 : papszOptions,
997 2 : (std::string(pszFilter) + '_' + pszName)
998 : .c_str(),
999 : CPLGetXMLValue(psNode, "default", nullptr));
1000 1 : if (pszVal)
1001 : {
1002 : std::string osOptName(
1003 0 : CPLString(pszName).tolower());
1004 0 : if (STARTS_WITH(pszType, "int"))
1005 0 : oFilter.Add(osOptName, atoi(pszVal));
1006 : else
1007 0 : oFilter.Add(osOptName, pszVal);
1008 : }
1009 : }
1010 : }
1011 : }
1012 : }
1013 : }
1014 :
1015 2 : if (EQUAL(pszFilter, "delta") &&
1016 1 : CSLFetchNameValue(papszOptions, "DELTA_DTYPE") == nullptr)
1017 : {
1018 1 : if (oDataType.GetClass() != GEDTC_NUMERIC)
1019 : {
1020 0 : CPLError(CE_Failure, CPLE_NotSupported,
1021 : "DELTA_DTYPE option must be specified");
1022 0 : return nullptr;
1023 : }
1024 1 : switch (oDataType.GetNumericDataType())
1025 : {
1026 0 : case GDT_Unknown:
1027 0 : break;
1028 0 : case GDT_Byte:
1029 0 : oFilter.Add("dtype", "u1");
1030 0 : break;
1031 0 : case GDT_Int8:
1032 0 : oFilter.Add("dtype", "i1");
1033 0 : break;
1034 1 : case GDT_UInt16:
1035 1 : oFilter.Add("dtype", "<u2");
1036 1 : break;
1037 0 : case GDT_Int16:
1038 0 : oFilter.Add("dtype", "<i2");
1039 0 : break;
1040 0 : case GDT_UInt32:
1041 0 : oFilter.Add("dtype", "<u4");
1042 0 : break;
1043 0 : case GDT_Int32:
1044 0 : oFilter.Add("dtype", "<i4");
1045 0 : break;
1046 0 : case GDT_UInt64:
1047 0 : oFilter.Add("dtype", "<u8");
1048 0 : break;
1049 0 : case GDT_Int64:
1050 0 : oFilter.Add("dtype", "<i8");
1051 0 : break;
1052 0 : case GDT_Float32:
1053 0 : oFilter.Add("dtype", "<f4");
1054 0 : break;
1055 0 : case GDT_Float64:
1056 0 : oFilter.Add("dtype", "<f8");
1057 0 : break;
1058 0 : case GDT_CInt16:
1059 0 : oFilter.Add("dtype", "<i2");
1060 0 : break;
1061 0 : case GDT_CInt32:
1062 0 : oFilter.Add("dtype", "<i4");
1063 0 : break;
1064 0 : case GDT_CFloat32:
1065 0 : oFilter.Add("dtype", "<f4");
1066 0 : break;
1067 0 : case GDT_CFloat64:
1068 0 : oFilter.Add("dtype", "<f8");
1069 0 : break;
1070 0 : case GDT_TypeCount:
1071 0 : break;
1072 : }
1073 : }
1074 : }
1075 :
1076 : const std::string osZarrayDirectory =
1077 592 : CPLFormFilename(m_osDirectoryName.c_str(), osName.c_str(), nullptr);
1078 296 : if (VSIMkdir(osZarrayDirectory.c_str(), 0755) != 0)
1079 : {
1080 : VSIStatBufL sStat;
1081 2 : if (VSIStatL(osZarrayDirectory.c_str(), &sStat) == 0)
1082 : {
1083 2 : CPLError(CE_Failure, CPLE_FileIO, "Directory %s already exists.",
1084 : osZarrayDirectory.c_str());
1085 : }
1086 : else
1087 : {
1088 0 : CPLError(CE_Failure, CPLE_FileIO, "Cannot create directory %s.",
1089 : osZarrayDirectory.c_str());
1090 : }
1091 2 : return nullptr;
1092 : }
1093 :
1094 588 : std::vector<GUInt64> anBlockSize;
1095 294 : if (!ZarrArray::FillBlockSize(aoDimensions, oDataType, anBlockSize,
1096 : papszOptions))
1097 3 : return nullptr;
1098 :
1099 291 : const bool bFortranOrder = EQUAL(
1100 : CSLFetchNameValueDef(papszOptions, "CHUNK_MEMORY_LAYOUT", "C"), "F");
1101 :
1102 : const char *pszDimSeparator =
1103 291 : CSLFetchNameValueDef(papszOptions, "DIM_SEPARATOR", ".");
1104 :
1105 291 : auto poArray = ZarrV2Array::Create(m_poSharedResource, GetFullName(),
1106 : osName, aoDimensions, oDataType,
1107 582 : aoDtypeElts, anBlockSize, bFortranOrder);
1108 :
1109 291 : if (!poArray)
1110 0 : return nullptr;
1111 : const std::string osZarrayFilename =
1112 582 : CPLFormFilename(osZarrayDirectory.c_str(), ".zarray", nullptr);
1113 291 : poArray->SetNew(true);
1114 291 : poArray->SetFilename(osZarrayFilename);
1115 291 : poArray->SetDimSeparator(pszDimSeparator);
1116 291 : poArray->SetDtype(dtype);
1117 291 : poArray->SetCompressorDecompressor(pszCompressor, psCompressor,
1118 : psDecompressor);
1119 291 : if (oCompressor.IsValid())
1120 16 : poArray->SetCompressorJson(oCompressor);
1121 291 : poArray->SetFilters(oFilters);
1122 291 : poArray->SetUpdatable(true);
1123 291 : poArray->SetDefinitionModified(true);
1124 291 : poArray->Flush();
1125 291 : RegisterArray(poArray);
1126 :
1127 291 : return poArray;
1128 : }
|