LCOV - code coverage report
Current view: top level - frmts/zarr - zarr_group.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 214 236 90.7 %
Date: 2024-05-13 13:33:37 Functions: 19 20 95.0 %

          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 <algorithm>
      32             : #include <cassert>
      33             : #include <limits>
      34             : #include <map>
      35             : #include <set>
      36             : 
      37             : /************************************************************************/
      38             : /*                          ~ZarrGroupBase()                            */
      39             : /************************************************************************/
      40             : 
      41        1417 : ZarrGroupBase::~ZarrGroupBase()
      42             : {
      43             :     // We need to explicitly flush arrays so that the _ARRAY_DIMENSIONS
      44             :     // is properly written. As it relies on checking if the dimensions of the
      45             :     // array have an indexing variable, then still need to be all alive.
      46        2549 :     for (auto &kv : m_oMapMDArrays)
      47             :     {
      48        1132 :         kv.second->Flush();
      49             :     }
      50        1417 : }
      51             : 
      52             : /************************************************************************/
      53             : /*                           GetMDArrayNames()                          */
      54             : /************************************************************************/
      55             : 
      56        1071 : std::vector<std::string> ZarrGroupBase::GetMDArrayNames(CSLConstList) const
      57             : {
      58        1071 :     if (!CheckValidAndErrorOutIfNot())
      59           0 :         return {};
      60             : 
      61        1071 :     if (!m_bDirectoryExplored)
      62         246 :         ExploreDirectory();
      63             : 
      64        1071 :     return m_aosArrays;
      65             : }
      66             : 
      67             : /************************************************************************/
      68             : /*                            RegisterArray()                           */
      69             : /************************************************************************/
      70             : 
      71        1137 : void ZarrGroupBase::RegisterArray(const std::shared_ptr<ZarrArray> &array) const
      72             : {
      73        1137 :     m_oMapMDArrays[array->GetName()] = array;
      74        1137 :     if (std::find(m_aosArrays.begin(), m_aosArrays.end(), array->GetName()) ==
      75        2274 :         m_aosArrays.end())
      76             :     {
      77        1071 :         m_aosArrays.emplace_back(array->GetName());
      78             :     }
      79        2274 :     array->RegisterGroup(
      80        2274 :         std::dynamic_pointer_cast<ZarrGroupBase>(m_pSelf.lock()));
      81        1137 : }
      82             : 
      83             : /************************************************************************/
      84             : /*                            GetGroupNames()                           */
      85             : /************************************************************************/
      86             : 
      87         277 : std::vector<std::string> ZarrGroupBase::GetGroupNames(CSLConstList) const
      88             : {
      89         277 :     if (!CheckValidAndErrorOutIfNot())
      90           0 :         return {};
      91             : 
      92         277 :     if (!m_bDirectoryExplored)
      93          36 :         ExploreDirectory();
      94             : 
      95         277 :     return m_aosGroups;
      96             : }
      97             : 
      98             : /************************************************************************/
      99             : /*                             DeleteGroup()                            */
     100             : /************************************************************************/
     101             : 
     102          18 : bool ZarrGroupBase::DeleteGroup(const std::string &osName,
     103             :                                 CSLConstList /*papszOptions*/)
     104             : {
     105          18 :     if (!CheckValidAndErrorOutIfNot())
     106           0 :         return false;
     107             : 
     108          18 :     if (!m_bUpdatable)
     109             :     {
     110           6 :         CPLError(CE_Failure, CPLE_NotSupported,
     111             :                  "Dataset not open in update mode");
     112           6 :         return false;
     113             :     }
     114             : 
     115          12 :     GetGroupNames();
     116             : 
     117          12 :     auto oIterNames = std::find(m_aosGroups.begin(), m_aosGroups.end(), osName);
     118          12 :     if (oIterNames == m_aosGroups.end())
     119             :     {
     120           6 :         CPLError(CE_Failure, CPLE_AppDefined,
     121             :                  "Group %s is not a sub-group of this group", osName.c_str());
     122           6 :         return false;
     123             :     }
     124             : 
     125             :     const std::string osSubDirName =
     126          12 :         CPLFormFilename(m_osDirectoryName.c_str(), osName.c_str(), nullptr);
     127           6 :     if (VSIRmdirRecursive(osSubDirName.c_str()) != 0)
     128             :     {
     129           0 :         CPLError(CE_Failure, CPLE_AppDefined, "Cannot delete %s",
     130             :                  osSubDirName.c_str());
     131           0 :         return false;
     132             :     }
     133             : 
     134           6 :     m_poSharedResource->DeleteZMetadataItemRecursive(osSubDirName);
     135             : 
     136           6 :     m_aosGroups.erase(oIterNames);
     137             : 
     138           6 :     auto oIter = m_oMapGroups.find(osName);
     139           6 :     if (oIter != m_oMapGroups.end())
     140             :     {
     141           4 :         oIter->second->Deleted();
     142           4 :         m_oMapGroups.erase(oIter);
     143             :     }
     144             : 
     145           6 :     return true;
     146             : }
     147             : 
     148             : /************************************************************************/
     149             : /*                       NotifyChildrenOfDeletion()                     */
     150             : /************************************************************************/
     151             : 
     152           6 : void ZarrGroupBase::NotifyChildrenOfDeletion()
     153             : {
     154           8 :     for (const auto &oIter : m_oMapGroups)
     155           2 :         oIter.second->ParentDeleted();
     156             : 
     157          10 :     for (const auto &oIter : m_oMapMDArrays)
     158           4 :         oIter.second->ParentDeleted();
     159             : 
     160           6 :     m_oAttrGroup.ParentDeleted();
     161             : 
     162          10 :     for (const auto &oIter : m_oMapDimensions)
     163           4 :         oIter.second->ParentDeleted();
     164           6 : }
     165             : 
     166             : /************************************************************************/
     167             : /*                  ZarrGroupBase::CreateAttribute()                    */
     168             : /************************************************************************/
     169             : 
     170          61 : std::shared_ptr<GDALAttribute> ZarrGroupBase::CreateAttribute(
     171             :     const std::string &osName, const std::vector<GUInt64> &anDimensions,
     172             :     const GDALExtendedDataType &oDataType, CSLConstList papszOptions)
     173             : {
     174          61 :     if (!CheckValidAndErrorOutIfNot())
     175           0 :         return nullptr;
     176             : 
     177          61 :     if (!m_bUpdatable)
     178             :     {
     179           3 :         CPLError(CE_Failure, CPLE_NotSupported,
     180             :                  "Dataset not open in update mode");
     181           3 :         return nullptr;
     182             :     }
     183          58 :     if (anDimensions.size() >= 2)
     184             :     {
     185           3 :         CPLError(CE_Failure, CPLE_NotSupported,
     186             :                  "Cannot create attributes of dimension >= 2");
     187           3 :         return nullptr;
     188             :     }
     189          55 :     LoadAttributes();
     190             :     return m_oAttrGroup.CreateAttribute(osName, anDimensions, oDataType,
     191          55 :                                         papszOptions);
     192             : }
     193             : 
     194             : /************************************************************************/
     195             : /*                  ZarrGroupBase::DeleteAttribute()                   */
     196             : /************************************************************************/
     197             : 
     198          18 : bool ZarrGroupBase::DeleteAttribute(const std::string &osName, CSLConstList)
     199             : {
     200          18 :     if (!CheckValidAndErrorOutIfNot())
     201           0 :         return false;
     202             : 
     203          18 :     if (!m_bUpdatable)
     204             :     {
     205           6 :         CPLError(CE_Failure, CPLE_NotSupported,
     206             :                  "Dataset not open in update mode");
     207           6 :         return false;
     208             :     }
     209             : 
     210          12 :     LoadAttributes();
     211          12 :     return m_oAttrGroup.DeleteAttribute(osName);
     212             : }
     213             : 
     214             : /************************************************************************/
     215             : /*                            GetDimensions()                           */
     216             : /************************************************************************/
     217             : 
     218             : std::vector<std::shared_ptr<GDALDimension>>
     219         547 : ZarrGroupBase::GetDimensions(CSLConstList) const
     220             : {
     221         547 :     if (!CheckValidAndErrorOutIfNot())
     222           0 :         return {};
     223             : 
     224         547 :     if (!m_bReadFromZMetadata && !m_bDimensionsInstantiated)
     225             :     {
     226         274 :         m_bDimensionsInstantiated = true;
     227             :         // We need to instantiate arrays to discover dimensions
     228         548 :         const auto aosArrays = GetMDArrayNames();
     229         308 :         for (const auto &osArray : aosArrays)
     230             :         {
     231          34 :             OpenMDArray(osArray);
     232             :         }
     233             :     }
     234             : 
     235        1094 :     std::vector<std::shared_ptr<GDALDimension>> oRes;
     236         962 :     for (const auto &oIter : m_oMapDimensions)
     237             :     {
     238         415 :         oRes.push_back(oIter.second);
     239             :     }
     240         547 :     return oRes;
     241             : }
     242             : 
     243             : /************************************************************************/
     244             : /*                            DeleteMDArray()                           */
     245             : /************************************************************************/
     246             : 
     247          18 : bool ZarrGroupBase::DeleteMDArray(const std::string &osName,
     248             :                                   CSLConstList /*papszOptions*/)
     249             : {
     250          18 :     if (!CheckValidAndErrorOutIfNot())
     251           0 :         return false;
     252             : 
     253          18 :     if (!m_bUpdatable)
     254             :     {
     255           6 :         CPLError(CE_Failure, CPLE_NotSupported,
     256             :                  "Dataset not open in update mode");
     257           6 :         return false;
     258             :     }
     259             : 
     260          12 :     GetMDArrayNames();
     261             : 
     262          12 :     auto oIterNames = std::find(m_aosArrays.begin(), m_aosArrays.end(), osName);
     263          12 :     if (oIterNames == m_aosArrays.end())
     264             :     {
     265           6 :         CPLError(CE_Failure, CPLE_AppDefined,
     266             :                  "Array %s is not an array of this group", osName.c_str());
     267           6 :         return false;
     268             :     }
     269             : 
     270             :     const std::string osSubDirName =
     271          12 :         CPLFormFilename(m_osDirectoryName.c_str(), osName.c_str(), nullptr);
     272           6 :     if (VSIRmdirRecursive(osSubDirName.c_str()) != 0)
     273             :     {
     274           0 :         CPLError(CE_Failure, CPLE_AppDefined, "Cannot delete %s",
     275             :                  osSubDirName.c_str());
     276           0 :         return false;
     277             :     }
     278             : 
     279           6 :     m_poSharedResource->DeleteZMetadataItemRecursive(osSubDirName);
     280             : 
     281           6 :     m_aosArrays.erase(oIterNames);
     282             : 
     283           6 :     auto oIter = m_oMapMDArrays.find(osName);
     284           6 :     if (oIter != m_oMapMDArrays.end())
     285             :     {
     286           4 :         oIter->second->Deleted();
     287           4 :         m_oMapMDArrays.erase(oIter);
     288             :     }
     289             : 
     290           6 :     return true;
     291             : }
     292             : 
     293             : /************************************************************************/
     294             : /*                             CreateDimension()                        */
     295             : /************************************************************************/
     296             : 
     297         475 : std::shared_ptr<GDALDimension> ZarrGroupBase::CreateDimension(
     298             :     const std::string &osName, const std::string &osType,
     299             :     const std::string &osDirection, GUInt64 nSize, CSLConstList)
     300             : {
     301         475 :     if (!CheckValidAndErrorOutIfNot())
     302           0 :         return nullptr;
     303             : 
     304         475 :     if (osName.empty())
     305             :     {
     306           0 :         CPLError(CE_Failure, CPLE_NotSupported,
     307             :                  "Empty dimension name not supported");
     308           0 :         return nullptr;
     309             :     }
     310         475 :     GetDimensions(nullptr);
     311             : 
     312         475 :     if (m_oMapDimensions.find(osName) != m_oMapDimensions.end())
     313             :     {
     314           0 :         CPLError(CE_Failure, CPLE_AppDefined,
     315             :                  "A dimension with same name already exists");
     316           0 :         return nullptr;
     317             :     }
     318             :     auto newDim(std::make_shared<ZarrDimension>(
     319         475 :         m_poSharedResource,
     320         950 :         std::dynamic_pointer_cast<ZarrGroupBase>(m_pSelf.lock()), GetFullName(),
     321         950 :         osName, osType, osDirection, nSize));
     322         475 :     newDim->SetXArrayDimension();
     323         475 :     m_oMapDimensions[osName] = newDim;
     324         475 :     return newDim;
     325             : }
     326             : 
     327             : /************************************************************************/
     328             : /*                          RenameDimension()                           */
     329             : /************************************************************************/
     330             : 
     331          12 : bool ZarrGroupBase::RenameDimension(const std::string &osOldName,
     332             :                                     const std::string &osNewName)
     333             : {
     334          12 :     if (m_oMapDimensions.find(osNewName) != m_oMapDimensions.end())
     335             :     {
     336           6 :         CPLError(CE_Failure, CPLE_AppDefined,
     337             :                  "A dimension with same name already exists");
     338           6 :         return false;
     339             :     }
     340           6 :     auto oIter = m_oMapDimensions.find(osOldName);
     341           6 :     if (oIter == m_oMapDimensions.end())
     342             :     {
     343           0 :         CPLAssert(false);
     344             :         return false;
     345             :     }
     346           6 :     auto poDim = std::move(oIter->second);
     347           6 :     m_oMapDimensions.erase(oIter);
     348           6 :     m_oMapDimensions[osNewName] = std::move(poDim);
     349           6 :     return true;
     350             : }
     351             : 
     352             : /************************************************************************/
     353             : /*                  ZarrGroupBase::UpdateDimensionSize()                */
     354             : /************************************************************************/
     355             : 
     356           7 : void ZarrGroupBase::UpdateDimensionSize(
     357             :     const std::shared_ptr<GDALDimension> &poUpdatedDim)
     358             : {
     359          14 :     const auto aosGroupNames = GetGroupNames();
     360           7 :     for (const auto &osGroupName : aosGroupNames)
     361             :     {
     362           0 :         auto poSubGroup = OpenZarrGroup(osGroupName);
     363           0 :         if (poSubGroup)
     364             :         {
     365           0 :             poSubGroup->UpdateDimensionSize(poUpdatedDim);
     366             :         }
     367             :     }
     368          14 :     const auto aosArrayNames = GetMDArrayNames();
     369          22 :     for (const auto &osArrayName : aosArrayNames)
     370             :     {
     371             :         // Disable checks that size of variables referenced by _ARRAY_DIMENSIONS
     372             :         // are consistent with array shapes, as we are in the middle of updating
     373             :         // things
     374          15 :         m_bDimSizeInUpdate = true;
     375          30 :         auto poArray = OpenZarrArray(osArrayName);
     376          15 :         m_bDimSizeInUpdate = false;
     377          15 :         if (poArray)
     378             :         {
     379          39 :             for (auto &poDim : poArray->GetDimensions())
     380             :             {
     381          24 :                 if (poDim->GetFullName() == poUpdatedDim->GetFullName())
     382             :                 {
     383             :                     auto poModifiableDim =
     384          30 :                         std::dynamic_pointer_cast<ZarrDimension>(poDim);
     385          15 :                     CPLAssert(poModifiableDim);
     386          15 :                     poModifiableDim->SetSize(poUpdatedDim->GetSize());
     387          15 :                     poArray->SetDefinitionModified(true);
     388             :                 }
     389             :             }
     390             :         }
     391             :     }
     392           7 : }
     393             : 
     394             : /************************************************************************/
     395             : /*                  ZarrGroupBase::NotifyArrayRenamed()                 */
     396             : /************************************************************************/
     397             : 
     398           6 : void ZarrGroupBase::NotifyArrayRenamed(const std::string &osOldName,
     399             :                                        const std::string &osNewName)
     400             : {
     401           7 :     for (auto &osName : m_aosArrays)
     402             :     {
     403           7 :         if (osName == osOldName)
     404             :         {
     405           6 :             osName = osNewName;
     406           6 :             break;
     407             :         }
     408             :     }
     409             : 
     410           6 :     auto oIter = m_oMapMDArrays.find(osOldName);
     411           6 :     if (oIter != m_oMapMDArrays.end())
     412             :     {
     413           6 :         auto poArray = std::move(oIter->second);
     414           6 :         m_oMapMDArrays.erase(oIter);
     415           6 :         m_oMapMDArrays[osNewName] = std::move(poArray);
     416             :     }
     417           6 : }
     418             : 
     419             : /************************************************************************/
     420             : /*                         IsValidObjectName()                          */
     421             : /************************************************************************/
     422             : 
     423             : /* static */
     424         647 : bool ZarrGroupBase::IsValidObjectName(const std::string &osName)
     425             : {
     426        1855 :     return !(osName.empty() || osName == "." || osName == ".." ||
     427        1208 :              osName.find('/') != std::string::npos ||
     428        1192 :              osName.find('\\') != std::string::npos ||
     429         592 :              osName.find(':') != std::string::npos ||
     430        1231 :              STARTS_WITH(osName.c_str(), ".z"));
     431             : }
     432             : 
     433             : /************************************************************************/
     434             : /*                 CheckArrayOrGroupWithSameNameDoesNotExist()          */
     435             : /************************************************************************/
     436             : 
     437          27 : bool ZarrGroupBase::CheckArrayOrGroupWithSameNameDoesNotExist(
     438             :     const std::string &osName) const
     439             : {
     440          54 :     const auto groupNames = GetGroupNames();
     441          27 :     if (std::find(groupNames.begin(), groupNames.end(), osName) !=
     442          54 :         groupNames.end())
     443             :     {
     444           9 :         CPLError(CE_Failure, CPLE_AppDefined,
     445             :                  "A group with same name already exists");
     446           9 :         return false;
     447             :     }
     448             : 
     449          36 :     const auto arrayNames = GetMDArrayNames();
     450          18 :     if (std::find(arrayNames.begin(), arrayNames.end(), osName) !=
     451          36 :         arrayNames.end())
     452             :     {
     453           6 :         CPLError(CE_Failure, CPLE_AppDefined,
     454             :                  "An array with same name already exists");
     455           6 :         return false;
     456             :     }
     457             : 
     458          12 :     return true;
     459             : }
     460             : 
     461             : /************************************************************************/
     462             : /*                              Rename()                                */
     463             : /************************************************************************/
     464             : 
     465          36 : bool ZarrGroupBase::Rename(const std::string &osNewName)
     466             : {
     467          36 :     if (!CheckValidAndErrorOutIfNot())
     468           3 :         return false;
     469             : 
     470          33 :     if (!m_bUpdatable)
     471             :     {
     472           6 :         CPLError(CE_Failure, CPLE_NotSupported,
     473             :                  "Dataset not open in update mode");
     474           6 :         return false;
     475             :     }
     476          27 :     if (!IsValidObjectName(osNewName))
     477             :     {
     478           6 :         CPLError(CE_Failure, CPLE_NotSupported, "Invalid group name");
     479           6 :         return false;
     480             :     }
     481          21 :     if (m_osName == "/")
     482             :     {
     483           6 :         CPLError(CE_Failure, CPLE_NotSupported, "Cannot rename root group");
     484           6 :         return false;
     485             :     }
     486             : 
     487          30 :     auto pParent = std::dynamic_pointer_cast<ZarrGroupBase>(m_poParent.lock());
     488          15 :     if (pParent)
     489             :     {
     490          15 :         if (!pParent->CheckArrayOrGroupWithSameNameDoesNotExist(osNewName))
     491           9 :             return false;
     492             :     }
     493             : 
     494          12 :     std::string osNewDirectoryName(m_osDirectoryName);
     495           6 :     osNewDirectoryName.resize(osNewDirectoryName.size() - m_osName.size());
     496           6 :     osNewDirectoryName += osNewName;
     497             : 
     498           6 :     if (VSIRename(m_osDirectoryName.c_str(), osNewDirectoryName.c_str()) != 0)
     499             :     {
     500           0 :         CPLError(CE_Failure, CPLE_AppDefined, "Renaming of %s to %s failed",
     501             :                  m_osDirectoryName.c_str(), osNewDirectoryName.c_str());
     502           0 :         return false;
     503             :     }
     504             : 
     505           6 :     if (pParent)
     506             :     {
     507           6 :         auto oIter = pParent->m_oMapGroups.find(m_osName);
     508           6 :         if (oIter != pParent->m_oMapGroups.end())
     509             :         {
     510           6 :             pParent->m_oMapGroups.erase(oIter);
     511           6 :             CPLAssert(m_pSelf.lock());
     512          12 :             pParent->m_oMapGroups[osNewName] =
     513          18 :                 std::dynamic_pointer_cast<ZarrGroupBase>(m_pSelf.lock());
     514             :         }
     515             : 
     516           6 :         for (auto &osName : pParent->m_aosGroups)
     517             :         {
     518           6 :             if (osName == m_osName)
     519             :             {
     520           6 :                 osName = osNewName;
     521           6 :                 break;
     522             :             }
     523             :         }
     524             :     }
     525             : 
     526           6 :     m_poSharedResource->RenameZMetadataRecursive(m_osDirectoryName,
     527             :                                                  osNewDirectoryName);
     528             : 
     529           6 :     m_osDirectoryName = std::move(osNewDirectoryName);
     530             : 
     531           6 :     BaseRename(osNewName);
     532             : 
     533           6 :     return true;
     534             : }
     535             : 
     536             : /************************************************************************/
     537             : /*                          ParentRenamed()                             */
     538             : /************************************************************************/
     539             : 
     540           4 : void ZarrGroupBase::ParentRenamed(const std::string &osNewParentFullName)
     541             : {
     542           8 :     auto pParent = std::dynamic_pointer_cast<ZarrGroupBase>(m_poParent.lock());
     543             :     // The parent necessarily exist, since it notified us
     544           4 :     CPLAssert(pParent);
     545             : 
     546           4 :     m_osDirectoryName = CPLFormFilename(pParent->m_osDirectoryName.c_str(),
     547           8 :                                         m_osName.c_str(), nullptr);
     548             : 
     549           4 :     GDALGroup::ParentRenamed(osNewParentFullName);
     550           4 : }
     551             : 
     552             : /************************************************************************/
     553             : /*                       NotifyChildrenOfRenaming()                     */
     554             : /************************************************************************/
     555             : 
     556          10 : void ZarrGroupBase::NotifyChildrenOfRenaming()
     557             : {
     558          14 :     for (const auto &oIter : m_oMapGroups)
     559           4 :         oIter.second->ParentRenamed(m_osFullName);
     560             : 
     561          19 :     for (const auto &oIter : m_oMapMDArrays)
     562           9 :         oIter.second->ParentRenamed(m_osFullName);
     563             : 
     564          10 :     m_oAttrGroup.ParentRenamed(m_osFullName);
     565             : 
     566          16 :     for (const auto &oIter : m_oMapDimensions)
     567           6 :         oIter.second->ParentRenamed(m_osFullName);
     568          10 : }

Generated by: LCOV version 1.14