LCOV - code coverage report
Current view: top level - frmts/eeda - eedacommon.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 160 225 71.1 %
Date: 2024-05-04 12:52:34 Functions: 6 8 75.0 %

          Line data    Source code
       1             : /******************************************************************************
       2             :  *
       3             :  * Project:  Earth Engine Data API Images driver
       4             :  * Purpose:  Earth Engine Data API Images driver
       5             :  * Author:   Even Rouault, even dot rouault at spatialys.com
       6             :  *
       7             :  ******************************************************************************
       8             :  * Copyright (c) 2017-2018, Planet Labs
       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_http.h"
      30             : #include "eeda.h"
      31             : #include "ogrgeojsonreader.h"
      32             : 
      33             : #include <stdlib.h>
      34             : #include <limits>
      35             : 
      36             : std::vector<EEDAIBandDesc>
      37          10 : BuildBandDescArray(json_object *poBands,
      38             :                    std::map<CPLString, CPLString> &oMapCodeToWKT)
      39             : {
      40          10 :     const auto nBandCount = json_object_array_length(poBands);
      41          10 :     std::vector<EEDAIBandDesc> aoBandDesc;
      42             : 
      43          34 :     for (auto i = decltype(nBandCount){0}; i < nBandCount; i++)
      44             :     {
      45          24 :         json_object *poBand = json_object_array_get_idx(poBands, i);
      46          48 :         if (poBand == nullptr ||
      47          24 :             json_object_get_type(poBand) != json_type_object)
      48           0 :             continue;
      49             : 
      50          24 :         json_object *poId = CPL_json_object_object_get(poBand, "id");
      51          24 :         const char *pszBandId = json_object_get_string(poId);
      52          24 :         if (pszBandId == nullptr)
      53           0 :             continue;
      54             : 
      55             :         json_object *poDataType =
      56          24 :             CPL_json_object_object_get(poBand, "dataType");
      57          48 :         if (poDataType == nullptr ||
      58          24 :             json_object_get_type(poDataType) != json_type_object)
      59             :         {
      60           0 :             continue;
      61             :         }
      62             : 
      63             :         json_object *poPrecision =
      64          24 :             CPL_json_object_object_get(poDataType, "precision");
      65          24 :         const char *pszPrecision = json_object_get_string(poPrecision);
      66          24 :         if (pszPrecision == nullptr)
      67           0 :             continue;
      68          24 :         GDALDataType eDT = GDT_Byte;
      69          24 :         if (EQUAL(pszPrecision, "INT"))
      70             :         {
      71             :             json_object *poRange =
      72          24 :                 CPL_json_object_object_get(poDataType, "range");
      73          24 :             if (poRange && json_object_get_type(poRange) == json_type_object)
      74             :             {
      75          24 :                 int nMin = 0;
      76          24 :                 int nMax = 0;
      77          24 :                 json_object *poMin = CPL_json_object_object_get(poRange, "min");
      78          24 :                 if (poMin)
      79             :                 {
      80           0 :                     nMin = json_object_get_int(poMin);
      81             :                 }
      82          24 :                 json_object *poMax = CPL_json_object_object_get(poRange, "max");
      83          24 :                 if (poMax)
      84             :                 {
      85          24 :                     nMax = json_object_get_int(poMax);
      86             :                 }
      87             : 
      88          24 :                 if (nMin == -128 && nMax == 127)
      89             :                 {
      90           0 :                     eDT = GDT_Int8;
      91             :                 }
      92          24 :                 else if (nMin < std::numeric_limits<GInt16>::min())
      93             :                 {
      94           0 :                     eDT = GDT_Int32;
      95             :                 }
      96          24 :                 else if (nMax > std::numeric_limits<GUInt16>::max())
      97             :                 {
      98           0 :                     eDT = GDT_UInt32;
      99             :                 }
     100          24 :                 else if (nMin < 0)
     101             :                 {
     102           0 :                     eDT = GDT_Int16;
     103             :                 }
     104          24 :                 else if (nMax > std::numeric_limits<GByte>::max())
     105             :                 {
     106          19 :                     eDT = GDT_UInt16;
     107             :                 }
     108             :             }
     109             :         }
     110           0 :         else if (EQUAL(pszPrecision, "FLOAT"))
     111             :         {
     112           0 :             eDT = GDT_Float32;
     113             :         }
     114           0 :         else if (EQUAL(pszPrecision, "DOUBLE"))
     115             :         {
     116           0 :             eDT = GDT_Float64;
     117             :         }
     118             :         else
     119             :         {
     120           0 :             CPLError(CE_Warning, CPLE_NotSupported,
     121             :                      "Unhandled dataType %s for band %s", pszPrecision,
     122             :                      pszBandId);
     123           0 :             continue;
     124             :         }
     125             : 
     126          24 :         json_object *poGrid = CPL_json_object_object_get(poBand, "grid");
     127          48 :         if (poGrid == nullptr ||
     128          24 :             json_object_get_type(poGrid) != json_type_object)
     129             :         {
     130           0 :             continue;
     131             :         }
     132             : 
     133          24 :         CPLString osWKT;
     134             :         // Cf https://developers.google.com/earth-engine/reference/rest/v1alpha/PixelGrid
     135          24 :         json_object *poCrs = CPL_json_object_object_get(poGrid, "crsCode");
     136          24 :         if (poCrs == nullptr)
     137           0 :             poCrs = CPL_json_object_object_get(poGrid, "crsWkt");
     138          24 :         if (poCrs ==
     139             :             nullptr)  // "wkt" must come from a preliminary version of the API
     140           0 :             poCrs = CPL_json_object_object_get(poGrid, "wkt");
     141          24 :         OGRSpatialReference oSRS;
     142          24 :         if (poCrs)
     143             :         {
     144          24 :             const char *pszStr = json_object_get_string(poCrs);
     145          24 :             if (pszStr == nullptr)
     146           0 :                 continue;
     147          24 :             if (STARTS_WITH(pszStr, "SR-ORG:"))
     148             :             {
     149             :                 // For EEDA:MCD12Q1 for example
     150             :                 pszStr =
     151           0 :                     CPLSPrintf("http://spatialreference.org/ref/sr-org/%s/",
     152             :                                pszStr + strlen("SR-ORG:"));
     153             :             }
     154             : 
     155             :             std::map<CPLString, CPLString>::const_iterator oIter =
     156          24 :                 oMapCodeToWKT.find(pszStr);
     157          24 :             if (oIter != oMapCodeToWKT.end())
     158             :             {
     159          15 :                 osWKT = oIter->second;
     160             :             }
     161           9 :             else if (oSRS.SetFromUserInput(pszStr) != OGRERR_NONE)
     162             :             {
     163           0 :                 CPLError(CE_Warning, CPLE_AppDefined, "Unrecognized crs: %s",
     164             :                          pszStr);
     165           0 :                 oMapCodeToWKT[pszStr] = "";
     166             :             }
     167             :             else
     168             :             {
     169           9 :                 char *pszWKT = nullptr;
     170           9 :                 oSRS.exportToWkt(&pszWKT);
     171           9 :                 if (pszWKT != nullptr)
     172           9 :                     osWKT = pszWKT;
     173           9 :                 CPLFree(pszWKT);
     174           9 :                 oMapCodeToWKT[pszStr] = osWKT;
     175             :             }
     176             :         }
     177             : 
     178             :         json_object *poAT =
     179          24 :             CPL_json_object_object_get(poGrid, "affineTransform");
     180          24 :         if (poAT == nullptr || json_object_get_type(poAT) != json_type_object)
     181             :         {
     182           0 :             continue;
     183             :         }
     184             :         std::vector<double> adfGeoTransform{
     185          48 :             json_object_get_double(
     186          24 :                 CPL_json_object_object_get(poAT, "translateX")),
     187          24 :             json_object_get_double(CPL_json_object_object_get(poAT, "scaleX")),
     188          24 :             json_object_get_double(CPL_json_object_object_get(poAT, "shearX")),
     189          48 :             json_object_get_double(
     190          24 :                 CPL_json_object_object_get(poAT, "translateY")),
     191          24 :             json_object_get_double(CPL_json_object_object_get(poAT, "shearY")),
     192          24 :             json_object_get_double(CPL_json_object_object_get(poAT, "scaleY")),
     193         144 :         };
     194             : 
     195             :         json_object *poDimensions =
     196          24 :             CPL_json_object_object_get(poGrid, "dimensions");
     197          48 :         if (poDimensions == nullptr ||
     198          24 :             json_object_get_type(poDimensions) != json_type_object)
     199             :         {
     200           0 :             continue;
     201             :         }
     202             :         json_object *poWidth =
     203          24 :             CPL_json_object_object_get(poDimensions, "width");
     204          24 :         int nWidth = json_object_get_int(poWidth);
     205             :         json_object *poHeight =
     206          24 :             CPL_json_object_object_get(poDimensions, "height");
     207          24 :         int nHeight = json_object_get_int(poHeight);
     208             : 
     209             : #if 0
     210             :         if( poWidth == nullptr && poHeight == nullptr && poX == nullptr && poY == nullptr &&
     211             :             dfResX == 1.0 && dfResY == 1.0 )
     212             :         {
     213             :             // e.g. EEDAI:LT5_L1T_8DAY_EVI/19840109
     214             :             const char* pszAuthorityName = oSRS.GetAuthorityName(nullptr);
     215             :             const char* pszAuthorityCode = oSRS.GetAuthorityCode(nullptr);
     216             :             if( pszAuthorityName && pszAuthorityCode &&
     217             :                 EQUAL(pszAuthorityName, "EPSG") &&
     218             :                 EQUAL(pszAuthorityCode, "4326") )
     219             :             {
     220             :                 dfX = -180;
     221             :                 dfY = 90;
     222             :                 nWidth = 1 << 30;
     223             :                 nHeight = 1 << 29;
     224             :                 dfResX = 360.0 / nWidth;
     225             :                 dfResY = -dfResX;
     226             :             }
     227             :         }
     228             : #endif
     229             : 
     230          24 :         if (nWidth <= 0 || nHeight <= 0)
     231             :         {
     232           0 :             CPLError(CE_Warning, CPLE_AppDefined,
     233             :                      "Invalid width/height for band %s", pszBandId);
     234           0 :             continue;
     235             :         }
     236             : 
     237          48 :         EEDAIBandDesc oDesc;
     238          24 :         oDesc.osName = pszBandId;
     239          24 :         oDesc.osWKT = std::move(osWKT);
     240          24 :         oDesc.eDT = eDT;
     241          24 :         oDesc.adfGeoTransform = std::move(adfGeoTransform);
     242          24 :         oDesc.nWidth = nWidth;
     243          24 :         oDesc.nHeight = nHeight;
     244          24 :         aoBandDesc.emplace_back(std::move(oDesc));
     245             :     }
     246          10 :     return aoBandDesc;
     247             : }
     248             : 
     249             : /************************************************************************/
     250             : /*                      GDALEEDABaseDataset()                           */
     251             : /************************************************************************/
     252             : 
     253          42 : GDALEEDABaseDataset::GDALEEDABaseDataset()
     254          42 :     : m_bMustCleanPersistent(false), m_nExpirationTime(0)
     255             : {
     256          42 : }
     257             : 
     258             : /************************************************************************/
     259             : /*                     ~GDALEEDABaseDataset()                           */
     260             : /************************************************************************/
     261             : 
     262          42 : GDALEEDABaseDataset::~GDALEEDABaseDataset()
     263             : {
     264          42 :     if (m_bMustCleanPersistent)
     265             :     {
     266          14 :         char **papszOptions = CSLSetNameValue(nullptr, "CLOSE_PERSISTENT",
     267             :                                               CPLSPrintf("EEDAI:%p", this));
     268          14 :         CPLHTTPDestroyResult(CPLHTTPFetch(m_osBaseURL, papszOptions));
     269          14 :         CSLDestroy(papszOptions);
     270             :     }
     271          42 : }
     272             : 
     273             : /************************************************************************/
     274             : /*                          ConvertPathToName()                        */
     275             : /************************************************************************/
     276             : 
     277          15 : CPLString GDALEEDABaseDataset::ConvertPathToName(const CPLString &path)
     278             : {
     279          15 :     size_t end = path.find('/');
     280          30 :     CPLString folder = path.substr(0, end);
     281             : 
     282          15 :     if (folder == "users")
     283             :     {
     284           2 :         return "projects/earthengine-legacy/assets/" + path;
     285             :     }
     286          14 :     else if (folder != "projects")
     287             :     {
     288          22 :         return "projects/earthengine-public/assets/" + path;
     289             :     }
     290             : 
     291             :     // Find the start and end positions of the third segment, if it exists.
     292           3 :     int segment = 1;
     293           3 :     size_t start = 0;
     294           8 :     while (end != std::string::npos && segment < 3)
     295             :     {
     296           5 :         segment++;
     297           5 :         start = end + 1;
     298           5 :         end = path.find('/', start);
     299             :     }
     300             : 
     301           3 :     end = (end == std::string::npos) ? path.size() : end;
     302             :     // segment is 3 if path has at least 3 segments.
     303           3 :     if (folder == "projects" && segment == 3)
     304             :     {
     305             :         // If the first segment is "projects" and the third segment is "assets",
     306             :         // path is a name, so return as-is.
     307           2 :         if (path.substr(start, end - start) == "assets")
     308             :         {
     309           1 :             return path;
     310             :         }
     311             :     }
     312           4 :     return "projects/earthengine-legacy/assets/" + path;
     313             : }
     314             : 
     315             : /************************************************************************/
     316             : /*                          GetBaseHTTPOptions()                        */
     317             : /************************************************************************/
     318             : 
     319          26 : char **GDALEEDABaseDataset::GetBaseHTTPOptions()
     320             : {
     321          26 :     m_bMustCleanPersistent = true;
     322             : 
     323          26 :     char **papszOptions = nullptr;
     324             :     papszOptions =
     325          26 :         CSLAddString(papszOptions, CPLSPrintf("PERSISTENT=EEDAI:%p", this));
     326             : 
     327             :     // Strategy to get the Bearer Authorization value:
     328             :     // - if it is specified in the EEDA_BEARER config option, use it
     329             :     // - otherwise if EEDA_BEARER_FILE is specified, read it and use its content
     330             :     // - otherwise if GOOGLE_APPLICATION_CREDENTIALS is specified, read the
     331             :     //   corresponding file to get the private key and client_email, to get a
     332             :     //   bearer using OAuth2ServiceAccount method
     333             :     // - otherwise if EEDA_PRIVATE_KEY and EEDA_CLIENT_EMAIL are set, use them
     334             :     //   to get a bearer using OAuth2ServiceAccount method
     335             :     // - otherwise if EEDA_PRIVATE_KEY_FILE and EEDA_CLIENT_EMAIL are set, use
     336             :     //   them to get a bearer
     337             : 
     338          52 :     CPLString osBearer(CPLGetConfigOption("EEDA_BEARER", m_osBearer));
     339          49 :     if (osBearer.empty() ||
     340          23 :         (!m_osBearer.empty() && time(nullptr) > m_nExpirationTime))
     341             :     {
     342           3 :         CPLString osBearerFile(CPLGetConfigOption("EEDA_BEARER_FILE", ""));
     343           3 :         if (!osBearerFile.empty())
     344             :         {
     345           0 :             VSILFILE *fp = VSIFOpenL(osBearerFile, "rb");
     346           0 :             if (fp == nullptr)
     347             :             {
     348           0 :                 CPLError(CE_Failure, CPLE_FileIO, "Cannot open %s",
     349             :                          osBearerFile.c_str());
     350             :             }
     351             :             else
     352             :             {
     353             :                 char abyBuffer[512];
     354           0 :                 size_t nRead = VSIFReadL(abyBuffer, 1, sizeof(abyBuffer), fp);
     355           0 :                 osBearer.assign(abyBuffer, nRead);
     356           0 :                 VSIFCloseL(fp);
     357             :             }
     358             :         }
     359             :         else
     360             :         {
     361           3 :             CPLString osPrivateKey(CPLGetConfigOption("EEDA_PRIVATE_KEY", ""));
     362             :             CPLString osClientEmail(
     363           3 :                 CPLGetConfigOption("EEDA_CLIENT_EMAIL", ""));
     364             : 
     365           3 :             if (osPrivateKey.empty())
     366             :             {
     367             :                 CPLString osPrivateKeyFile(
     368           4 :                     CPLGetConfigOption("EEDA_PRIVATE_KEY_FILE", ""));
     369           2 :                 if (!osPrivateKeyFile.empty())
     370             :                 {
     371           0 :                     VSILFILE *fp = VSIFOpenL(osPrivateKeyFile, "rb");
     372           0 :                     if (fp == nullptr)
     373             :                     {
     374           0 :                         CPLError(CE_Failure, CPLE_FileIO, "Cannot open %s",
     375             :                                  osPrivateKeyFile.c_str());
     376             :                     }
     377             :                     else
     378             :                     {
     379             :                         char *pabyBuffer =
     380           0 :                             static_cast<char *>(CPLMalloc(32768));
     381           0 :                         size_t nRead = VSIFReadL(pabyBuffer, 1, 32768, fp);
     382           0 :                         osPrivateKey.assign(pabyBuffer, nRead);
     383           0 :                         VSIFCloseL(fp);
     384           0 :                         CPLFree(pabyBuffer);
     385             :                     }
     386             :                 }
     387             :             }
     388             : 
     389             :             CPLString osServiceAccountJson(
     390           3 :                 CPLGetConfigOption("GOOGLE_APPLICATION_CREDENTIALS", ""));
     391           3 :             if (!osServiceAccountJson.empty())
     392             :             {
     393           1 :                 CPLJSONDocument oDoc;
     394           1 :                 if (!oDoc.Load(osServiceAccountJson))
     395             :                 {
     396           0 :                     CSLDestroy(papszOptions);
     397           0 :                     return nullptr;
     398             :                 }
     399             : 
     400           1 :                 osPrivateKey = oDoc.GetRoot().GetString("private_key");
     401           1 :                 osPrivateKey.replaceAll("\\n", "\n");
     402           1 :                 osClientEmail = oDoc.GetRoot().GetString("client_email");
     403             :             }
     404             : 
     405           3 :             char **papszMD = nullptr;
     406           3 :             if (!osPrivateKey.empty() && !osClientEmail.empty())
     407             :             {
     408           2 :                 CPLDebug("EEDA", "Requesting Bearer token");
     409           2 :                 osPrivateKey.replaceAll("\\n", "\n");
     410             :                 // CPLDebug("EEDA", "Private key: %s", osPrivateKey.c_str());
     411           2 :                 papszMD = GOA2GetAccessTokenFromServiceAccount(
     412             :                     osPrivateKey, osClientEmail,
     413             :                     "https://www.googleapis.com/auth/earthengine.readonly",
     414             :                     nullptr, nullptr);
     415           2 :                 if (papszMD == nullptr)
     416             :                 {
     417           0 :                     CSLDestroy(papszOptions);
     418           0 :                     return nullptr;
     419             :                 }
     420             :             }
     421             :             // Some Travis-CI workers are GCE machines, and for some tests, we
     422             :             // don't want this code path to be taken. And on AppVeyor/Window, we
     423             :             // would also attempt a network access
     424           2 :             else if (!CPLTestBool(CPLGetConfigOption("CPL_GCE_SKIP", "NO")) &&
     425           1 :                      CPLIsMachinePotentiallyGCEInstance())
     426             :             {
     427           1 :                 papszMD = GOA2GetAccessTokenFromCloudEngineVM(nullptr);
     428             :             }
     429             : 
     430           3 :             if (papszMD)
     431             :             {
     432           3 :                 osBearer = CSLFetchNameValueDef(papszMD, "access_token", "");
     433           3 :                 m_osBearer = osBearer;
     434           3 :                 m_nExpirationTime = CPLAtoGIntBig(
     435             :                     CSLFetchNameValueDef(papszMD, "expires_in", "0"));
     436           3 :                 if (m_nExpirationTime != 0)
     437           3 :                     m_nExpirationTime += time(nullptr) - 10;
     438           3 :                 CSLDestroy(papszMD);
     439             :             }
     440             :             else
     441             :             {
     442           0 :                 CPLError(CE_Failure, CPLE_AppDefined,
     443             :                          "Missing EEDA_BEARER, EEDA_BEARER_FILE or "
     444             :                          "GOOGLE_APPLICATION_CREDENTIALS or "
     445             :                          "EEDA_PRIVATE_KEY/EEDA_PRIVATE_KEY_FILE + "
     446             :                          "EEDA_CLIENT_EMAIL config option");
     447           0 :                 CSLDestroy(papszOptions);
     448           0 :                 return nullptr;
     449             :             }
     450             :         }
     451             :     }
     452          26 :     papszOptions = CSLAddString(
     453             :         papszOptions,
     454             :         CPLSPrintf("HEADERS=Authorization: Bearer %s", osBearer.c_str()));
     455             : 
     456          26 :     return papszOptions;
     457             : }
     458             : 
     459             : /* Add a small amount of random jitter to avoid cyclic server stampedes */
     460           0 : static double EEDABackoffFactor(double base)
     461             : {
     462             :     // coverity[dont_call]
     463           0 :     return base + rand() * 0.5 / RAND_MAX;
     464             : }
     465             : 
     466             : /************************************************************************/
     467             : /*                           EEDAHTTPFetch()                            */
     468             : /************************************************************************/
     469             : 
     470          26 : CPLHTTPResult *EEDAHTTPFetch(const char *pszURL, char **papszOptions)
     471             : {
     472             :     CPLHTTPResult *psResult;
     473          26 :     const int RETRY_COUNT = 4;
     474          26 :     double dfRetryDelay = 1.0;
     475          26 :     for (int i = 0; i <= RETRY_COUNT; i++)
     476             :     {
     477          26 :         psResult = CPLHTTPFetch(pszURL, papszOptions);
     478             : 
     479          26 :         if (psResult == nullptr)
     480           0 :             break;
     481          26 :         if (psResult->nDataLen != 0 && psResult->nStatus == 0 &&
     482          26 :             psResult->pszErrBuf == nullptr)
     483             :         {
     484             :             /* got a valid response */
     485          26 :             CPLErrorReset();
     486          26 :             break;
     487             :         }
     488             :         else
     489             :         {
     490           0 :             const char *pszErrorText =
     491           0 :                 psResult->pszErrBuf ? psResult->pszErrBuf : "(null)";
     492             : 
     493             :             /* Get HTTP status code */
     494           0 :             int nHTTPStatus = -1;
     495           0 :             if (psResult->pszErrBuf != nullptr &&
     496           0 :                 EQUALN(psResult->pszErrBuf,
     497             :                        "HTTP error code : ", strlen("HTTP error code : ")))
     498             :             {
     499             :                 nHTTPStatus =
     500           0 :                     atoi(psResult->pszErrBuf + strlen("HTTP error code : "));
     501           0 :                 if (psResult->pabyData)
     502           0 :                     pszErrorText =
     503             :                         reinterpret_cast<const char *>(psResult->pabyData);
     504             :             }
     505             : 
     506           0 :             if ((nHTTPStatus == 429 || nHTTPStatus == 500 ||
     507           0 :                  (nHTTPStatus >= 502 && nHTTPStatus <= 504)) &&
     508             :                 i < RETRY_COUNT)
     509             :             {
     510           0 :                 CPLError(CE_Warning, CPLE_FileIO,
     511             :                          "GET error when downloading %s, HTTP status=%d, "
     512             :                          "retrying in %.2fs : %s",
     513             :                          pszURL, nHTTPStatus, dfRetryDelay, pszErrorText);
     514           0 :                 CPLHTTPDestroyResult(psResult);
     515           0 :                 psResult = nullptr;
     516             : 
     517           0 :                 CPLSleep(dfRetryDelay);
     518           0 :                 dfRetryDelay *= EEDABackoffFactor(4);
     519             :             }
     520             :             else
     521             :             {
     522             :                 break;
     523             :             }
     524             :         }
     525             :     }
     526             : 
     527          26 :     return psResult;
     528             : }

Generated by: LCOV version 1.14