LCOV - code coverage report
Current view: top level - ogr - ogr_proj_p.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 129 150 86.0 %
Date: 2024-05-03 15:49:35 Functions: 25 29 86.2 %

          Line data    Source code
       1             : /******************************************************************************
       2             :  *
       3             :  * Project:  GDAL
       4             :  * Purpose:  PROJ-related functionality
       5             :  * Author:   Even Rouault <even dot rouault at spatialys dot com>
       6             :  *
       7             :  ******************************************************************************
       8             :  * Copyright (c) 2018, Even Rouault <even dot rouault at spatialys dot 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 "cpl_error.h"
      30             : #include "cpl_multiproc.h"
      31             : #include "cpl_string.h"
      32             : 
      33             : #include "ogr_proj_p.h"
      34             : #include "ogr_srs_api.h"
      35             : 
      36             : #include "proj.h"
      37             : 
      38             : #ifndef _WIN32
      39             : #include <sys/types.h>
      40             : #include <unistd.h>
      41             : #if defined(HAVE_PTHREAD_ATFORK)
      42             : #include <pthread.h>
      43             : #endif
      44             : #endif
      45             : 
      46             : #include <mutex>
      47             : #include <vector>
      48             : 
      49             : /*! @cond Doxygen_Suppress */
      50             : 
      51         128 : static void osr_proj_logger(void * /* user_data */, int level,
      52             :                             const char *message)
      53             : {
      54         128 :     if (level == PJ_LOG_ERROR)
      55             :     {
      56         128 :         CPLError(CE_Failure, CPLE_AppDefined, "PROJ: %s", message);
      57             :     }
      58           0 :     else if (level == PJ_LOG_DEBUG)
      59             :     {
      60           0 :         CPLDebug("PROJ", "%s", message);
      61             :     }
      62           0 :     else if (level == PJ_LOG_TRACE)
      63             :     {
      64           0 :         CPLDebug("PROJ_TRACE", "%s", message);
      65             :     }
      66         128 : }
      67             : 
      68             : static unsigned g_searchPathGenerationCounter = 0;
      69             : static unsigned g_auxDbPathsGenerationCounter = 0;
      70             : static std::mutex g_oSearchPathMutex;
      71             : static CPLStringList g_aosSearchpaths;
      72             : static CPLStringList g_aosAuxDbPaths;
      73             : #if PROJ_VERSION_MAJOR >= 7
      74             : static int g_projNetworkEnabled = -1;
      75             : static unsigned g_projNetworkEnabledGenerationCounter = 0;
      76             : #endif
      77             : 
      78             : #if !defined(_WIN32) && defined(HAVE_PTHREAD_ATFORK)
      79             : static bool g_bForkOccurred = false;
      80             : 
      81           0 : static void ForkOccurred(void)
      82             : {
      83           0 :     g_bForkOccurred = true;
      84           0 : }
      85             : #endif
      86             : 
      87             : struct OSRPJContextHolder
      88             : {
      89             :     unsigned searchPathGenerationCounter = 0;
      90             :     unsigned auxDbPathsGenerationCounter = 0;
      91             : #if PROJ_VERSION_MAJOR >= 7
      92             :     unsigned projNetworkEnabledGenerationCounter = 0;
      93             : #endif
      94             :     PJ_CONTEXT *context = nullptr;
      95             :     OSRProjTLSCache oCache;
      96             : #if !defined(_WIN32)
      97             : #if !defined(HAVE_PTHREAD_ATFORK)
      98             :     pid_t curpid = 0;
      99             : #endif
     100             : #endif
     101             : 
     102             : #if !defined(_WIN32)
     103        1168 :     OSRPJContextHolder()
     104        1168 :         : oCache(init())
     105             : #if !defined(HAVE_PTHREAD_ATFORK)
     106             :           ,
     107             :           curpid(getpid())
     108             : #endif
     109             :     {
     110             : #if HAVE_PTHREAD_ATFORK
     111             :         static std::once_flag flag;
     112        1168 :         std::call_once(
     113             :             flag,
     114        1086 :             []()
     115             :             {
     116        1086 :                 if (pthread_atfork(nullptr, nullptr, ForkOccurred) != 0)
     117             :                 {
     118           0 :                     CPLError(CE_Failure, CPLE_OutOfMemory,
     119             :                              "pthread_atfork() in ogr_proj_p failed");
     120             :                 }
     121        1086 :             });
     122             : #endif
     123        1168 :         init();
     124        1168 :     }
     125             : #else
     126             :     OSRPJContextHolder() : oCache(init())
     127             :     {
     128             :     }
     129             : #endif
     130             : 
     131             :     ~OSRPJContextHolder();
     132             : 
     133             :     PJ_CONTEXT *init();
     134             :     void deinit();
     135             : 
     136             :   private:
     137             :     OSRPJContextHolder(const OSRPJContextHolder &) = delete;
     138             :     OSRPJContextHolder &operator=(const OSRPJContextHolder &) = delete;
     139             : };
     140             : 
     141       55545 : static void OSRSetConfigOption(const char *pszKey, const char *pszValue,
     142             :                                bool bThreadLocal, void *)
     143             : {
     144       55545 :     if (!bThreadLocal && pszValue &&
     145        1926 :         (EQUAL(pszKey, "PROJ_LIB") || EQUAL(pszKey, "PROJ_DATA")))
     146             :     {
     147           2 :         const char *const apszSearchPaths[] = {pszValue, nullptr};
     148           2 :         OSRSetPROJSearchPaths(apszSearchPaths);
     149             :     }
     150       55545 : }
     151             : 
     152        1108 : static void OSRInstallSetConfigOptionCallback()
     153             : {
     154             :     static std::once_flag flag;
     155        1108 :     std::call_once(
     156             :         flag,
     157        1087 :         []() { CPLSubscribeToSetConfigOption(OSRSetConfigOption, nullptr); });
     158        1108 : }
     159             : 
     160     2894720 : PJ_CONTEXT *OSRPJContextHolder::init()
     161             : {
     162     2894720 :     if (!context)
     163             :     {
     164             :         static std::once_flag flag;
     165        1192 :         std::call_once(
     166             :             flag,
     167        1086 :             []()
     168             :             {
     169             :                 // Initialize g_aosSearchpaths from PROJ_DATA/PROJ_LIB configuration
     170             :                 // option.
     171        2172 :                 std::lock_guard<std::mutex> oLock(g_oSearchPathMutex);
     172        1086 :                 if (g_searchPathGenerationCounter == 0)
     173             :                 {
     174             :                     const char *pszProjData =
     175        1072 :                         CPLGetConfigOption("PROJ_DATA", nullptr);
     176        1072 :                     if (pszProjData == nullptr)
     177        1069 :                         pszProjData = CPLGetConfigOption("PROJ_LIB", nullptr);
     178        1072 :                     if (pszProjData)
     179             :                     {
     180           3 :                         const char *pszSep =
     181             : #ifdef _WIN32
     182             :                             ";"
     183             : #else
     184             :                             ":"
     185             : #endif
     186             :                             ;
     187             :                         g_aosSearchpaths =
     188           3 :                             CSLTokenizeString2(pszProjData, pszSep, 0);
     189           3 :                         g_searchPathGenerationCounter = 1;
     190             :                     }
     191             :                 }
     192             : 
     193        1086 :                 OSRInstallSetConfigOptionCallback();
     194        1086 :             });
     195             : 
     196        1192 :         context = proj_context_create();
     197        1192 :         proj_log_func(context, nullptr, osr_proj_logger);
     198             :     }
     199     2894710 :     return context;
     200             : }
     201             : 
     202        1159 : OSRPJContextHolder::~OSRPJContextHolder()
     203             : {
     204        1159 :     deinit();
     205        1159 : }
     206             : 
     207        2016 : void OSRPJContextHolder::deinit()
     208             : {
     209        2016 :     searchPathGenerationCounter = 0;
     210        2016 :     oCache.clear();
     211             : 
     212             :     // Destroy context in last
     213        2016 :     proj_context_destroy(context);
     214        2016 :     context = nullptr;
     215        2016 : }
     216             : 
     217             : #ifdef _WIN32
     218             : // Currently thread_local and C++ objects don't work well with DLL on Windows
     219             : static void FreeProjTLSContextHolder(void *pData)
     220             : {
     221             :     delete static_cast<OSRPJContextHolder *>(pData);
     222             : }
     223             : 
     224             : static OSRPJContextHolder &GetProjTLSContextHolder()
     225             : {
     226             :     static OSRPJContextHolder dummy;
     227             :     int bMemoryErrorOccurred = false;
     228             :     void *pData = CPLGetTLSEx(CTLS_PROJCONTEXTHOLDER, &bMemoryErrorOccurred);
     229             :     if (bMemoryErrorOccurred)
     230             :     {
     231             :         return dummy;
     232             :     }
     233             :     if (pData == nullptr)
     234             :     {
     235             :         auto pHolder = new OSRPJContextHolder();
     236             :         CPLSetTLSWithFreeFuncEx(CTLS_PROJCONTEXTHOLDER, pHolder,
     237             :                                 FreeProjTLSContextHolder,
     238             :                                 &bMemoryErrorOccurred);
     239             :         if (bMemoryErrorOccurred)
     240             :         {
     241             :             delete pHolder;
     242             :             return dummy;
     243             :         }
     244             :         return *pHolder;
     245             :     }
     246             :     return *static_cast<OSRPJContextHolder *>(pData);
     247             : }
     248             : #else
     249             : static thread_local OSRPJContextHolder g_tls_projContext;
     250             : 
     251     2950470 : static OSRPJContextHolder &GetProjTLSContextHolder()
     252             : {
     253     2950470 :     OSRPJContextHolder &l_projContext = g_tls_projContext;
     254             : 
     255             :     // Detect if we are now running in a child process created by fork()
     256             :     // In that situation we must make sure *not* to use the same underlying
     257             :     // file open descriptor to the sqlite3 database, since seeks&reads in one
     258             :     // of the parent or child will affect the other end.
     259             : #if defined(HAVE_PTHREAD_ATFORK)
     260     2950470 :     if (g_bForkOccurred)
     261             : #else
     262             :     const pid_t curpid = getpid();
     263             :     if (curpid != l_projContext.curpid)
     264             : #endif
     265             :     {
     266             : #if defined(HAVE_PTHREAD_ATFORK)
     267           0 :         g_bForkOccurred = false;
     268             : #else
     269             :         l_projContext.curpid = curpid;
     270             : #endif
     271           0 :         const auto osr_proj_logger_none = [](void *, int, const char *) {};
     272           0 :         proj_log_func(l_projContext.context, nullptr, osr_proj_logger_none);
     273           0 :         proj_context_set_autoclose_database(l_projContext.context, true);
     274             :         // dummy call to cause the database to be closed
     275           0 :         proj_context_get_database_path(l_projContext.context);
     276           0 :         proj_context_set_autoclose_database(l_projContext.context, false);
     277           0 :         proj_log_func(l_projContext.context, nullptr, osr_proj_logger);
     278             :     }
     279             : 
     280     2950470 :     return l_projContext;
     281             : }
     282             : #endif
     283             : 
     284     2892380 : PJ_CONTEXT *OSRGetProjTLSContext()
     285             : {
     286     2892380 :     auto &l_projContext = GetProjTLSContextHolder();
     287             :     // This .init() must be kept, even if OSRPJContextHolder constructor
     288             :     // calls it. The reason is that OSRCleanupTLSContext() calls deinit(),
     289             :     // so if reusing the object, we must re-init again.
     290     2892370 :     l_projContext.init();
     291             :     {
     292             :         // If OSRSetPROJSearchPaths() has been called since we created the
     293             :         // context, set the new search paths on the context.
     294     5784760 :         std::lock_guard<std::mutex> oLock(g_oSearchPathMutex);
     295     2892380 :         if (l_projContext.searchPathGenerationCounter !=
     296             :             g_searchPathGenerationCounter)
     297             :         {
     298          94 :             l_projContext.searchPathGenerationCounter =
     299             :                 g_searchPathGenerationCounter;
     300          94 :             proj_context_set_search_paths(l_projContext.context,
     301             :                                           g_aosSearchpaths.Count(),
     302          94 :                                           g_aosSearchpaths.List());
     303             :         }
     304     2892380 :         if (l_projContext.auxDbPathsGenerationCounter !=
     305             :             g_auxDbPathsGenerationCounter)
     306             :         {
     307           2 :             l_projContext.auxDbPathsGenerationCounter =
     308             :                 g_auxDbPathsGenerationCounter;
     309             :             std::string oMainPath(
     310           4 :                 proj_context_get_database_path(l_projContext.context));
     311           2 :             proj_context_set_database_path(l_projContext.context,
     312             :                                            oMainPath.c_str(),
     313           2 :                                            g_aosAuxDbPaths.List(), nullptr);
     314             :         }
     315             : #if PROJ_VERSION_MAJOR >= 7
     316             :         if (l_projContext.projNetworkEnabledGenerationCounter !=
     317             :             g_projNetworkEnabledGenerationCounter)
     318             :         {
     319             :             l_projContext.projNetworkEnabledGenerationCounter =
     320             :                 g_projNetworkEnabledGenerationCounter;
     321             :             proj_context_set_enable_network(l_projContext.context,
     322             :                                             g_projNetworkEnabled);
     323             :         }
     324             : #endif
     325             :     }
     326     2892380 :     return l_projContext.context;
     327             : }
     328             : 
     329             : /************************************************************************/
     330             : /*                         OSRGetProjTLSCache()                         */
     331             : /************************************************************************/
     332             : 
     333       57236 : OSRProjTLSCache *OSRGetProjTLSCache()
     334             : {
     335       57236 :     auto &l_projContext = GetProjTLSContextHolder();
     336       57236 :     return &l_projContext.oCache;
     337             : }
     338             : 
     339        2016 : void OSRProjTLSCache::clear()
     340             : {
     341        2016 :     m_oCacheEPSG.clear();
     342        2016 :     m_oCacheWKT.clear();
     343        2016 :     m_tlsContext = nullptr;
     344        2016 : }
     345             : 
     346       56395 : PJ_CONTEXT *OSRProjTLSCache::GetPJContext()
     347             : {
     348       56395 :     if (m_tlsContext == nullptr)
     349           5 :         m_tlsContext = OSRGetProjTLSContext();
     350       56395 :     return m_tlsContext;
     351             : }
     352             : 
     353       30777 : PJ *OSRProjTLSCache::GetPJForEPSGCode(int nCode, bool bUseNonDeprecated,
     354             :                                       bool bAddTOWGS84)
     355             : {
     356       30777 :     const EPSGCacheKey key(nCode, bUseNonDeprecated, bAddTOWGS84);
     357       30777 :     auto cached = m_oCacheEPSG.getPtr(key);
     358       30777 :     if (cached)
     359             :     {
     360       22974 :         return proj_clone(GetPJContext(), cached->get());
     361             :     }
     362        7803 :     return nullptr;
     363             : }
     364             : 
     365        7783 : void OSRProjTLSCache::CachePJForEPSGCode(int nCode, bool bUseNonDeprecated,
     366             :                                          bool bAddTOWGS84, PJ *pj)
     367             : {
     368        7783 :     const EPSGCacheKey key(nCode, bUseNonDeprecated, bAddTOWGS84);
     369        7783 :     m_oCacheEPSG.insert(key, UniquePtrPJ(proj_clone(GetPJContext(), pj)));
     370        7783 : }
     371             : 
     372       25921 : PJ *OSRProjTLSCache::GetPJForWKT(const std::string &wkt)
     373             : {
     374       25921 :     auto cached = m_oCacheWKT.getPtr(wkt);
     375       25921 :     if (cached)
     376             :     {
     377       24259 :         return proj_clone(GetPJContext(), cached->get());
     378             :     }
     379        1662 :     return nullptr;
     380             : }
     381             : 
     382        1379 : void OSRProjTLSCache::CachePJForWKT(const std::string &wkt, PJ *pj)
     383             : {
     384        1379 :     m_oCacheWKT.insert(wkt, UniquePtrPJ(proj_clone(GetPJContext(), pj)));
     385        1379 : }
     386             : 
     387             : /************************************************************************/
     388             : /*                         OSRCleanupTLSContext()                       */
     389             : /************************************************************************/
     390             : 
     391         857 : void OSRCleanupTLSContext()
     392             : {
     393         857 :     GetProjTLSContextHolder().deinit();
     394         857 : }
     395             : 
     396             : /*! @endcond */
     397             : 
     398             : /************************************************************************/
     399             : /*                        OSRSetPROJSearchPaths()                       */
     400             : /************************************************************************/
     401             : 
     402             : /** \brief Set the search path(s) for PROJ resource files.
     403             :  *
     404             :  * Note: starting with GDAL 3.7, CPLSetConfigOption("PROJ_DATA", ...) can
     405             :  * also been used for the same effect.
     406             :  *
     407             :  * @param papszPaths NULL terminated list of directory paths.
     408             :  * @since GDAL 3.0
     409             :  */
     410          22 : void OSRSetPROJSearchPaths(const char *const *papszPaths)
     411             : {
     412          44 :     std::lock_guard<std::mutex> oLock(g_oSearchPathMutex);
     413          22 :     g_searchPathGenerationCounter++;
     414          22 :     g_aosSearchpaths.Assign(CSLDuplicate(papszPaths), true);
     415          22 :     OSRInstallSetConfigOptionCallback();
     416          22 : }
     417             : 
     418             : /************************************************************************/
     419             : /*                        OSRGetPROJSearchPaths()                       */
     420             : /************************************************************************/
     421             : 
     422             : /** \brief Get the search path(s) for PROJ resource files.
     423             :  *
     424             :  * @return NULL terminated list of directory paths. To be freed with
     425             :  * CSLDestroy()
     426             :  * @since GDAL 3.0.3
     427             :  */
     428          24 : char **OSRGetPROJSearchPaths()
     429             : {
     430          48 :     std::lock_guard<std::mutex> oLock(g_oSearchPathMutex);
     431          24 :     if (g_searchPathGenerationCounter > 0 && !g_aosSearchpaths.empty())
     432             :     {
     433           8 :         return CSLDuplicate(g_aosSearchpaths.List());
     434             :     }
     435             : 
     436          16 :     const char *pszSep =
     437             : #ifdef _WIN32
     438             :         ";"
     439             : #else
     440             :         ":"
     441             : #endif
     442             :         ;
     443          16 :     return CSLTokenizeString2(proj_info().searchpath, pszSep, 0);
     444             : }
     445             : 
     446             : /************************************************************************/
     447             : /*                        OSRSetPROJAuxDbPaths()                        */
     448             : /************************************************************************/
     449             : 
     450             : /** \brief Set list of PROJ auxiliary database filenames.
     451             :  *
     452             :  * @param papszAux NULL-terminated list of auxiliary database filenames, or NULL
     453             :  * @since GDAL 3.3
     454             :  *
     455             :  * @see OSRGetPROJAuxDbPaths, proj_context_set_database_path
     456             :  */
     457           2 : void OSRSetPROJAuxDbPaths(const char *const *papszAux)
     458             : {
     459           4 :     std::lock_guard<std::mutex> oLock(g_oSearchPathMutex);
     460           2 :     g_auxDbPathsGenerationCounter++;
     461           2 :     g_aosAuxDbPaths.Assign(CSLDuplicate(papszAux), true);
     462           2 : }
     463             : 
     464             : /************************************************************************/
     465             : /*                        OSRGetPROJAuxDbPaths()                        */
     466             : /************************************************************************/
     467             : 
     468             : /** \brief Get PROJ auxiliary database filenames.
     469             :  *
     470             :  * @return NULL terminated list of PROJ auxiliary database filenames. To be
     471             :  * freed with CSLDestroy()
     472             :  * @since GDAL 3.3.0
     473             :  *
     474             :  * @see OSRSetPROJAuxDbPaths, proj_context_set_database_path
     475             :  */
     476           1 : char **OSRGetPROJAuxDbPaths(void)
     477             : {
     478           2 :     std::lock_guard<std::mutex> oLock(g_oSearchPathMutex);
     479             :     // Unfortunately, there is no getter for auxiliary database list at PROJ.
     480             :     // So, return our copy for now.
     481           2 :     return CSLDuplicate(g_aosAuxDbPaths.List());
     482             : }
     483             : 
     484             : /************************************************************************/
     485             : /*                       OSRSetPROJEnableNetwork()                      */
     486             : /************************************************************************/
     487             : 
     488             : /** \brief Enable or disable PROJ networking capabilities.
     489             :  *
     490             :  * @param enabled Set to TRUE to enable networking capabilities.
     491             :  * @since GDAL 3.4 and PROJ 7
     492             :  *
     493             :  * @see OSRGetPROJEnableNetwork, proj_context_set_enable_network
     494             :  */
     495           0 : void OSRSetPROJEnableNetwork(int enabled)
     496             : {
     497             : #if PROJ_VERSION_MAJOR >= 7
     498             :     std::lock_guard<std::mutex> oLock(g_oSearchPathMutex);
     499             :     if (g_projNetworkEnabled != enabled)
     500             :     {
     501             :         g_projNetworkEnabled = enabled;
     502             :         g_projNetworkEnabledGenerationCounter++;
     503             :     }
     504             : #else
     505           0 :     if (enabled)
     506             :     {
     507           0 :         CPLError(CE_Failure, CPLE_NotSupported,
     508             :                  "OSRSetPROJEnableNetwork() requires PROJ >= 7");
     509             :     }
     510             : #endif
     511           0 : }
     512             : 
     513             : /************************************************************************/
     514             : /*                        OSRGetPROJEnableNetwork()                     */
     515             : /************************************************************************/
     516             : 
     517             : /** \brief Get whether PROJ networking capabilities are enabled.
     518             :  *
     519             :  * @return TRUE if PROJ networking capabilities are enabled.
     520             :  * @since GDAL 3.4 and PROJ 7
     521             :  *
     522             :  * @see OSRSetPROJEnableNetwork, proj_context_is_network_enabled
     523             :  */
     524           0 : int OSRGetPROJEnableNetwork(void)
     525             : {
     526             : #if PROJ_VERSION_MAJOR >= 7
     527             :     std::lock_guard<std::mutex> oLock(g_oSearchPathMutex);
     528             :     if (g_projNetworkEnabled < 0)
     529             :     {
     530             :         g_oSearchPathMutex.unlock();
     531             :         const int ret = proj_context_is_network_enabled(OSRGetProjTLSContext());
     532             :         g_oSearchPathMutex.lock();
     533             :         g_projNetworkEnabled = ret;
     534             :     }
     535             :     return g_projNetworkEnabled;
     536             : #else
     537           0 :     return FALSE;
     538             : #endif
     539             : }
     540             : 
     541             : /************************************************************************/
     542             : /*                         OSRGetPROJVersion()                          */
     543             : /************************************************************************/
     544             : 
     545             : /** \brief Get the PROJ version
     546             :  *
     547             :  * @param pnMajor Pointer to major version number, or NULL
     548             :  * @param pnMinor Pointer to minor version number, or NULL
     549             :  * @param pnPatch Pointer to patch version number, or NULL
     550             :  * @since GDAL 3.0.1
     551             :  */
     552         183 : void OSRGetPROJVersion(int *pnMajor, int *pnMinor, int *pnPatch)
     553             : {
     554         183 :     auto info = proj_info();
     555         183 :     if (pnMajor)
     556          72 :         *pnMajor = info.major;
     557         183 :     if (pnMinor)
     558          63 :         *pnMinor = info.minor;
     559         183 :     if (pnPatch)
     560          48 :         *pnPatch = info.patch;
     561         183 : }

Generated by: LCOV version 1.14