Coverage Report

Created: 2026-05-16 08:20

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