Coverage Report

Created: 2025-07-23 09:13

/src/gdal/ogr/ogrsf_frmts/geojson/ogrgeojsonutils.cpp
Line
Count
Source (jump to first uncovered line)
1
/******************************************************************************
2
 *
3
 * Project:  OpenGIS Simple Features Reference Implementation
4
 * Purpose:  Implementation of private utilities used within OGR GeoJSON Driver.
5
 * Author:   Mateusz Loskot, mateusz@loskot.net
6
 *
7
 ******************************************************************************
8
 * Copyright (c) 2007, Mateusz Loskot
9
 * Copyright (c) 2010-2013, Even Rouault <even dot rouault at spatialys.com>
10
 *
11
 * SPDX-License-Identifier: MIT
12
 ****************************************************************************/
13
14
#include "ogrgeojsonutils.h"
15
#include <assert.h>
16
#include "cpl_port.h"
17
#include "cpl_conv.h"
18
#include "cpl_json_streaming_parser.h"
19
#include "ogr_geometry.h"
20
#include <json.h>  // JSON-C
21
22
#include <algorithm>
23
#include <memory>
24
25
const char szESRIJSonFeaturesGeometryRings[] =
26
    "{\"features\":[{\"geometry\":{\"rings\":[";
27
28
// Cf https://github.com/OSGeo/gdal/issues/9996#issuecomment-2129845692
29
const char szESRIJSonFeaturesAttributes[] = "{\"features\":[{\"attributes\":{";
30
31
/************************************************************************/
32
/*                           SkipUTF8BOM()                              */
33
/************************************************************************/
34
35
static void SkipUTF8BOM(const char *&pszText)
36
903k
{
37
    /* Skip UTF-8 BOM (#5630) */
38
903k
    const GByte *pabyData = reinterpret_cast<const GByte *>(pszText);
39
903k
    if (pabyData[0] == 0xEF && pabyData[1] == 0xBB && pabyData[2] == 0xBF)
40
239
        pszText += 3;
41
903k
}
42
43
/************************************************************************/
44
/*                           IsJSONObject()                             */
45
/************************************************************************/
46
47
static bool IsJSONObject(const char *pszText)
48
881k
{
49
881k
    if (nullptr == pszText)
50
0
        return false;
51
52
881k
    SkipUTF8BOM(pszText);
53
54
    /* -------------------------------------------------------------------- */
55
    /*      This is a primitive test, but we need to perform it fast.       */
56
    /* -------------------------------------------------------------------- */
57
1.39M
    while (*pszText != '\0' && isspace(static_cast<unsigned char>(*pszText)))
58
513k
        pszText++;
59
60
881k
    const char *const apszPrefix[] = {"loadGeoJSON(", "jsonp("};
61
2.64M
    for (size_t iP = 0; iP < sizeof(apszPrefix) / sizeof(apszPrefix[0]); iP++)
62
1.76M
    {
63
1.76M
        if (strncmp(pszText, apszPrefix[iP], strlen(apszPrefix[iP])) == 0)
64
1.57k
        {
65
1.57k
            pszText += strlen(apszPrefix[iP]);
66
1.57k
            break;
67
1.57k
        }
68
1.76M
    }
69
70
881k
    if (*pszText != '{')
71
789k
        return false;
72
73
92.2k
    return true;
74
881k
}
75
76
/************************************************************************/
77
/*                           GetTopLevelType()                          */
78
/************************************************************************/
79
80
static std::string GetTopLevelType(const char *pszText)
81
41.3k
{
82
41.3k
    if (!strstr(pszText, "\"type\""))
83
19.5k
        return std::string();
84
85
21.7k
    SkipUTF8BOM(pszText);
86
87
21.7k
    struct MyParser : public CPLJSonStreamingParser
88
21.7k
    {
89
21.7k
        std::string m_osLevel{};
90
21.7k
        bool m_bInTopLevelType = false;
91
21.7k
        std::string m_osTopLevelTypeValue{};
92
93
21.7k
        void StartObjectMember(const char *pszKey, size_t nLength) override
94
100k
        {
95
100k
            m_bInTopLevelType = false;
96
100k
            if (nLength == strlen("type") && strcmp(pszKey, "type") == 0 &&
97
100k
                m_osLevel == "{")
98
14.4k
            {
99
14.4k
                m_bInTopLevelType = true;
100
14.4k
            }
101
100k
        }
102
103
21.7k
        void String(const char *pszValue, size_t nLength) override
104
42.2k
        {
105
42.2k
            if (m_bInTopLevelType)
106
13.5k
            {
107
13.5k
                m_osTopLevelTypeValue.assign(pszValue, nLength);
108
13.5k
                StopParsing();
109
13.5k
            }
110
42.2k
        }
111
112
21.7k
        void StartObject() override
113
42.8k
        {
114
42.8k
            m_osLevel += '{';
115
42.8k
            m_bInTopLevelType = false;
116
42.8k
        }
117
118
21.7k
        void EndObject() override
119
21.7k
        {
120
11.4k
            if (!m_osLevel.empty())
121
11.4k
                m_osLevel.pop_back();
122
11.4k
            m_bInTopLevelType = false;
123
11.4k
        }
124
125
21.7k
        void StartArray() override
126
37.7k
        {
127
37.7k
            m_osLevel += '[';
128
37.7k
            m_bInTopLevelType = false;
129
37.7k
        }
130
131
21.7k
        void EndArray() override
132
29.4k
        {
133
29.4k
            if (!m_osLevel.empty())
134
29.4k
                m_osLevel.pop_back();
135
29.4k
            m_bInTopLevelType = false;
136
29.4k
        }
137
21.7k
    };
138
139
21.7k
    MyParser oParser;
140
21.7k
    oParser.Parse(pszText, strlen(pszText), true);
141
21.7k
    return oParser.m_osTopLevelTypeValue;
142
41.3k
}
143
144
/************************************************************************/
145
/*                           GetCompactJSon()                           */
146
/************************************************************************/
147
148
static CPLString GetCompactJSon(const char *pszText, size_t nMaxSize)
149
79.3k
{
150
    /* Skip UTF-8 BOM (#5630) */
151
79.3k
    const GByte *pabyData = reinterpret_cast<const GByte *>(pszText);
152
79.3k
    if (pabyData[0] == 0xEF && pabyData[1] == 0xBB && pabyData[2] == 0xBF)
153
100
        pszText += 3;
154
155
79.3k
    CPLString osWithoutSpace;
156
79.3k
    bool bInString = false;
157
447M
    for (int i = 0; pszText[i] != '\0' && osWithoutSpace.size() < nMaxSize; i++)
158
447M
    {
159
447M
        if (bInString)
160
197M
        {
161
197M
            if (pszText[i] == '\\')
162
322k
            {
163
322k
                osWithoutSpace += pszText[i];
164
322k
                if (pszText[i + 1] == '\0')
165
221
                    break;
166
322k
                osWithoutSpace += pszText[i + 1];
167
322k
                i++;
168
322k
            }
169
196M
            else if (pszText[i] == '"')
170
3.41M
            {
171
3.41M
                bInString = false;
172
3.41M
                osWithoutSpace += '"';
173
3.41M
            }
174
193M
            else
175
193M
            {
176
193M
                osWithoutSpace += pszText[i];
177
193M
            }
178
197M
        }
179
250M
        else if (pszText[i] == '"')
180
3.45M
        {
181
3.45M
            bInString = true;
182
3.45M
            osWithoutSpace += '"';
183
3.45M
        }
184
246M
        else if (!isspace(static_cast<unsigned char>(pszText[i])))
185
209M
        {
186
209M
            osWithoutSpace += pszText[i];
187
209M
        }
188
447M
    }
189
79.3k
    return osWithoutSpace;
190
79.3k
}
191
192
/************************************************************************/
193
/*                          IsGeoJSONLikeObject()                       */
194
/************************************************************************/
195
196
static bool IsGeoJSONLikeObject(const char *pszText, bool &bMightBeSequence,
197
                                bool &bReadMoreBytes, GDALOpenInfo *poOpenInfo,
198
                                const char *pszExpectedDriverName)
199
450k
{
200
450k
    bMightBeSequence = false;
201
450k
    bReadMoreBytes = false;
202
203
450k
    if (!IsJSONObject(pszText))
204
416k
        return false;
205
206
33.6k
    const std::string osTopLevelType = GetTopLevelType(pszText);
207
33.6k
    if (osTopLevelType == "Topology")
208
68
        return false;
209
210
33.5k
    if (poOpenInfo->IsSingleAllowedDriver(pszExpectedDriverName) &&
211
33.5k
        GDALGetDriverByName(pszExpectedDriverName))
212
0
    {
213
0
        return true;
214
0
    }
215
216
33.5k
    if ((!poOpenInfo->papszAllowedDrivers ||
217
33.5k
         CSLFindString(poOpenInfo->papszAllowedDrivers, "JSONFG") >= 0) &&
218
33.5k
        GDALGetDriverByName("JSONFG") && JSONFGIsObject(pszText, poOpenInfo))
219
1.26k
    {
220
1.26k
        return false;
221
1.26k
    }
222
223
32.2k
    if (osTopLevelType == "FeatureCollection")
224
3.07k
    {
225
3.07k
        return true;
226
3.07k
    }
227
228
29.2k
    const std::string osWithoutSpace = GetCompactJSon(pszText, strlen(pszText));
229
29.2k
    if (osWithoutSpace.find("{\"features\":[") == 0 &&
230
29.2k
        osWithoutSpace.find(szESRIJSonFeaturesGeometryRings) != 0 &&
231
29.2k
        osWithoutSpace.find(szESRIJSonFeaturesAttributes) != 0)
232
7.22k
    {
233
7.22k
        return true;
234
7.22k
    }
235
236
    // See
237
    // https://raw.githubusercontent.com/icepack/icepack-data/master/meshes/larsen/larsen_inflow.geojson
238
    // "{"crs":...,"features":[..."
239
    // or
240
    // https://gist.githubusercontent.com/NiklasDallmann/27e339dd78d1942d524fbcd179f9fdcf/raw/527a8319d75a9e29446a32a19e4c902213b0d668/42XR9nLAh8Poh9Xmniqh3Bs9iisNm74mYMC56v3Wfyo=_isochrones_fails.geojson
241
    // "{"bbox":...,"features":[..."
242
21.9k
    if (osWithoutSpace.find(",\"features\":[") != std::string::npos)
243
3.04k
    {
244
3.04k
        return !ESRIJSONIsObject(pszText, poOpenInfo);
245
3.04k
    }
246
247
    // See https://github.com/OSGeo/gdal/issues/2720
248
18.9k
    if (osWithoutSpace.find("{\"coordinates\":[") == 0 ||
249
        // and https://github.com/OSGeo/gdal/issues/2787
250
18.9k
        osWithoutSpace.find("{\"geometry\":{\"coordinates\":[") == 0 ||
251
        // and https://github.com/qgis/QGIS/issues/61266
252
18.9k
        osWithoutSpace.find(
253
18.5k
            "{\"geometry\":{\"type\":\"Point\",\"coordinates\":[") == 0 ||
254
18.9k
        osWithoutSpace.find(
255
18.5k
            "{\"geometry\":{\"type\":\"LineString\",\"coordinates\":[") == 0 ||
256
18.9k
        osWithoutSpace.find(
257
18.4k
            "{\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[") == 0 ||
258
18.9k
        osWithoutSpace.find(
259
18.4k
            "{\"geometry\":{\"type\":\"MultiPoint\",\"coordinates\":[") == 0 ||
260
18.9k
        osWithoutSpace.find(
261
18.4k
            "{\"geometry\":{\"type\":\"MultiLineString\",\"coordinates\":[") ==
262
18.4k
            0 ||
263
18.9k
        osWithoutSpace.find(
264
18.4k
            "{\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[") ==
265
18.4k
            0 ||
266
18.9k
        osWithoutSpace.find("{\"geometry\":{\"type\":\"GeometryCollection\","
267
18.4k
                            "\"geometries\":[") == 0)
268
500
    {
269
500
        return true;
270
500
    }
271
272
18.4k
    if (osTopLevelType == "Feature" || osTopLevelType == "Point" ||
273
18.4k
        osTopLevelType == "LineString" || osTopLevelType == "Polygon" ||
274
18.4k
        osTopLevelType == "MultiPoint" || osTopLevelType == "MultiLineString" ||
275
18.4k
        osTopLevelType == "MultiPolygon" ||
276
18.4k
        osTopLevelType == "GeometryCollection")
277
7.94k
    {
278
7.94k
        bMightBeSequence = true;
279
7.94k
        return true;
280
7.94k
    }
281
282
    // See https://github.com/OSGeo/gdal/issues/3280
283
10.4k
    if (osWithoutSpace.find("{\"properties\":{") == 0)
284
3.89k
    {
285
3.89k
        bMightBeSequence = true;
286
3.89k
        bReadMoreBytes = true;
287
3.89k
        return false;
288
3.89k
    }
289
290
6.59k
    return false;
291
10.4k
}
292
293
static bool IsGeoJSONLikeObject(const char *pszText, GDALOpenInfo *poOpenInfo,
294
                                const char *pszExpectedDriverName)
295
4.07k
{
296
4.07k
    bool bMightBeSequence;
297
4.07k
    bool bReadMoreBytes;
298
4.07k
    return IsGeoJSONLikeObject(pszText, bMightBeSequence, bReadMoreBytes,
299
4.07k
                               poOpenInfo, pszExpectedDriverName);
300
4.07k
}
301
302
/************************************************************************/
303
/*                       ESRIJSONIsObject()                             */
304
/************************************************************************/
305
306
bool ESRIJSONIsObject(const char *pszText, GDALOpenInfo *poOpenInfo)
307
131k
{
308
131k
    if (!IsJSONObject(pszText))
309
120k
        return false;
310
311
11.2k
    if (poOpenInfo->IsSingleAllowedDriver("ESRIJSON") &&
312
11.2k
        GDALGetDriverByName("ESRIJSON"))
313
0
    {
314
0
        return true;
315
0
    }
316
317
11.2k
    if (  // ESRI Json geometry
318
11.2k
        (strstr(pszText, "\"geometryType\"") != nullptr &&
319
11.2k
         strstr(pszText, "\"esriGeometry") != nullptr)
320
321
        // ESRI Json "FeatureCollection"
322
11.2k
        || strstr(pszText, "\"fieldAliases\"") != nullptr
323
324
        // ESRI Json "FeatureCollection"
325
11.2k
        || (strstr(pszText, "\"fields\"") != nullptr &&
326
10.5k
            strstr(pszText, "\"esriFieldType") != nullptr))
327
759
    {
328
759
        return true;
329
759
    }
330
331
10.5k
    const std::string osWithoutSpace = GetCompactJSon(pszText, strlen(pszText));
332
10.5k
    if (osWithoutSpace.find(szESRIJSonFeaturesGeometryRings) == 0 ||
333
10.5k
        osWithoutSpace.find(szESRIJSonFeaturesAttributes) == 0 ||
334
10.5k
        osWithoutSpace.find("\"spatialReference\":{\"wkid\":") !=
335
10.5k
            std::string::npos)
336
357
    {
337
357
        return true;
338
357
    }
339
340
10.1k
    return false;
341
10.5k
}
342
343
/************************************************************************/
344
/*                       TopoJSONIsObject()                             */
345
/************************************************************************/
346
347
bool TopoJSONIsObject(const char *pszText, GDALOpenInfo *poOpenInfo)
348
128k
{
349
128k
    if (!IsJSONObject(pszText))
350
120k
        return false;
351
352
7.72k
    if (poOpenInfo->IsSingleAllowedDriver("TopoJSON") &&
353
7.72k
        GDALGetDriverByName("TopoJSON"))
354
0
    {
355
0
        return true;
356
0
    }
357
358
7.72k
    return GetTopLevelType(pszText) == "Topology";
359
7.72k
}
360
361
/************************************************************************/
362
/*                      IsLikelyNewlineSequenceGeoJSON()                */
363
/************************************************************************/
364
365
static GDALIdentifyEnum
366
IsLikelyNewlineSequenceGeoJSON(VSILFILE *fpL, const GByte *pabyHeader,
367
                               const char *pszFileContent)
368
7.41k
{
369
7.41k
    const size_t nBufferSize = 4096 * 10;
370
7.41k
    std::vector<GByte> abyBuffer;
371
7.41k
    abyBuffer.resize(nBufferSize + 1);
372
373
7.41k
    int nCurlLevel = 0;
374
7.41k
    bool bInString = false;
375
7.41k
    bool bLastIsEscape = false;
376
7.41k
    bool bFirstIter = true;
377
7.41k
    bool bEOLFound = false;
378
7.41k
    int nCountObject = 0;
379
12.5k
    while (true)
380
12.5k
    {
381
12.5k
        size_t nRead;
382
12.5k
        bool bEnd = false;
383
12.5k
        if (bFirstIter)
384
7.41k
        {
385
7.41k
            const char *pszText =
386
7.41k
                pszFileContent ? pszFileContent
387
7.41k
                               : reinterpret_cast<const char *>(pabyHeader);
388
7.41k
            assert(pszText);
389
7.41k
            nRead = std::min(strlen(pszText), nBufferSize);
390
7.41k
            memcpy(abyBuffer.data(), pszText, nRead);
391
7.41k
            bFirstIter = false;
392
7.41k
            if (fpL)
393
7.00k
            {
394
7.00k
                VSIFSeekL(fpL, nRead, SEEK_SET);
395
7.00k
            }
396
7.41k
        }
397
5.08k
        else
398
5.08k
        {
399
5.08k
            nRead = VSIFReadL(abyBuffer.data(), 1, nBufferSize, fpL);
400
5.08k
            bEnd = nRead < nBufferSize;
401
5.08k
        }
402
75.3M
        for (size_t i = 0; i < nRead; i++)
403
75.3M
        {
404
75.3M
            if (nCurlLevel == 0)
405
22.1k
            {
406
22.1k
                if (abyBuffer[i] == '{')
407
12.4k
                {
408
12.4k
                    nCountObject++;
409
12.4k
                    if (nCountObject == 2)
410
5.05k
                    {
411
5.05k
                        break;
412
5.05k
                    }
413
7.40k
                    nCurlLevel++;
414
7.40k
                }
415
9.67k
                else if (nCountObject == 1 && abyBuffer[i] == '\n')
416
5.45k
                {
417
5.45k
                    bEOLFound = true;
418
5.45k
                }
419
4.21k
                else if (!isspace(static_cast<unsigned char>(abyBuffer[i])))
420
707
                {
421
707
                    return GDAL_IDENTIFY_FALSE;
422
707
                }
423
22.1k
            }
424
75.3M
            else if (bInString)
425
35.6M
            {
426
35.6M
                if (bLastIsEscape)
427
52.6k
                {
428
52.6k
                    bLastIsEscape = false;
429
52.6k
                }
430
35.5M
                else if (abyBuffer[i] == '\\')
431
52.6k
                {
432
52.6k
                    bLastIsEscape = true;
433
52.6k
                }
434
35.5M
                else if (abyBuffer[i] == '"')
435
481k
                {
436
481k
                    bInString = false;
437
481k
                }
438
35.6M
            }
439
39.7M
            else if (abyBuffer[i] == '"')
440
482k
            {
441
482k
                bInString = true;
442
482k
            }
443
39.2M
            else if (abyBuffer[i] == '{')
444
238k
            {
445
238k
                nCurlLevel++;
446
238k
            }
447
39.0M
            else if (abyBuffer[i] == '}')
448
36.9k
            {
449
36.9k
                nCurlLevel--;
450
36.9k
            }
451
75.3M
        }
452
11.7k
        if (!fpL || bEnd || nCountObject == 2)
453
6.71k
            break;
454
11.7k
    }
455
6.71k
    if (bEOLFound && nCountObject == 2)
456
5.03k
        return GDAL_IDENTIFY_TRUE;
457
1.68k
    return GDAL_IDENTIFY_UNKNOWN;
458
6.71k
}
459
460
/************************************************************************/
461
/*                           GeoJSONFileIsObject()                      */
462
/************************************************************************/
463
464
static bool GeoJSONFileIsObject(GDALOpenInfo *poOpenInfo)
465
135k
{
466
    // By default read first 6000 bytes.
467
    // 6000 was chosen as enough bytes to
468
    // enable all current tests to pass.
469
470
135k
    if (poOpenInfo->fpL == nullptr || !poOpenInfo->TryToIngest(6000))
471
46.5k
    {
472
46.5k
        return false;
473
46.5k
    }
474
475
88.6k
    bool bMightBeSequence = false;
476
88.6k
    bool bReadMoreBytes = false;
477
88.6k
    if (!IsGeoJSONLikeObject(
478
88.6k
            reinterpret_cast<const char *>(poOpenInfo->pabyHeader),
479
88.6k
            bMightBeSequence, bReadMoreBytes, poOpenInfo, "GeoJSON"))
480
80.1k
    {
481
80.1k
        if (!(bReadMoreBytes && poOpenInfo->nHeaderBytes >= 6000 &&
482
80.1k
              poOpenInfo->TryToIngest(1000 * 1000) &&
483
80.1k
              !IsGeoJSONLikeObject(
484
922
                  reinterpret_cast<const char *>(poOpenInfo->pabyHeader),
485
922
                  bMightBeSequence, bReadMoreBytes, poOpenInfo, "GeoJSON")))
486
79.2k
        {
487
79.2k
            return false;
488
79.2k
        }
489
80.1k
    }
490
491
9.32k
    return !(bMightBeSequence && IsLikelyNewlineSequenceGeoJSON(
492
3.39k
                                     poOpenInfo->fpL, poOpenInfo->pabyHeader,
493
3.39k
                                     nullptr) == GDAL_IDENTIFY_TRUE);
494
88.6k
}
495
496
/************************************************************************/
497
/*                           GeoJSONIsObject()                          */
498
/************************************************************************/
499
500
bool GeoJSONIsObject(const char *pszText, GDALOpenInfo *poOpenInfo)
501
139k
{
502
139k
    bool bMightBeSequence = false;
503
139k
    bool bReadMoreBytes = false;
504
139k
    if (!IsGeoJSONLikeObject(pszText, bMightBeSequence, bReadMoreBytes,
505
139k
                             poOpenInfo, "GeoJSON"))
506
135k
    {
507
135k
        return false;
508
135k
    }
509
510
3.37k
    return !(bMightBeSequence &&
511
3.37k
             IsLikelyNewlineSequenceGeoJSON(nullptr, nullptr, pszText) ==
512
414
                 GDAL_IDENTIFY_TRUE);
513
139k
}
514
515
/************************************************************************/
516
/*                        GeoJSONSeqFileIsObject()                      */
517
/************************************************************************/
518
519
static bool GeoJSONSeqFileIsObject(GDALOpenInfo *poOpenInfo)
520
133k
{
521
    // By default read first 6000 bytes.
522
    // 6000 was chosen as enough bytes to
523
    // enable all current tests to pass.
524
525
133k
    if (poOpenInfo->fpL == nullptr || !poOpenInfo->TryToIngest(6000))
526
46.5k
    {
527
46.5k
        return false;
528
46.5k
    }
529
530
87.4k
    const char *pszText =
531
87.4k
        reinterpret_cast<const char *>(poOpenInfo->pabyHeader);
532
87.4k
    if (pszText[0] == '\x1e')
533
4.07k
        return IsGeoJSONLikeObject(pszText + 1, poOpenInfo, "GeoJSONSeq");
534
535
83.3k
    bool bMightBeSequence = false;
536
83.3k
    bool bReadMoreBytes = false;
537
83.3k
    if (!IsGeoJSONLikeObject(pszText, bMightBeSequence, bReadMoreBytes,
538
83.3k
                             poOpenInfo, "GeoJSONSeq"))
539
77.2k
    {
540
77.2k
        if (!(bReadMoreBytes && poOpenInfo->nHeaderBytes >= 6000 &&
541
77.2k
              poOpenInfo->TryToIngest(1000 * 1000) &&
542
77.2k
              IsGeoJSONLikeObject(
543
457
                  reinterpret_cast<const char *>(poOpenInfo->pabyHeader),
544
457
                  bMightBeSequence, bReadMoreBytes, poOpenInfo, "GeoJSONSeq")))
545
77.2k
        {
546
77.2k
            return false;
547
77.2k
        }
548
77.2k
    }
549
550
6.11k
    if (poOpenInfo->IsSingleAllowedDriver("GeoJSONSeq") &&
551
6.11k
        IsLikelyNewlineSequenceGeoJSON(poOpenInfo->fpL, poOpenInfo->pabyHeader,
552
0
                                       nullptr) != GDAL_IDENTIFY_FALSE &&
553
6.11k
        GDALGetDriverByName("GeoJSONSeq"))
554
0
    {
555
0
        return true;
556
0
    }
557
558
6.11k
    return bMightBeSequence && IsLikelyNewlineSequenceGeoJSON(
559
3.60k
                                   poOpenInfo->fpL, poOpenInfo->pabyHeader,
560
3.60k
                                   nullptr) == GDAL_IDENTIFY_TRUE;
561
6.11k
}
562
563
bool GeoJSONSeqIsObject(const char *pszText, GDALOpenInfo *poOpenInfo)
564
133k
{
565
133k
    if (pszText[0] == '\x1e')
566
1
        return IsGeoJSONLikeObject(pszText + 1, poOpenInfo, "GeoJSONSeq");
567
568
133k
    bool bMightBeSequence = false;
569
133k
    bool bReadMoreBytes = false;
570
133k
    if (!IsGeoJSONLikeObject(pszText, bMightBeSequence, bReadMoreBytes,
571
133k
                             poOpenInfo, "GeoJSONSeq"))
572
133k
    {
573
133k
        return false;
574
133k
    }
575
576
0
    if (poOpenInfo->IsSingleAllowedDriver("GeoJSONSeq") &&
577
0
        IsLikelyNewlineSequenceGeoJSON(nullptr, nullptr, pszText) !=
578
0
            GDAL_IDENTIFY_FALSE &&
579
0
        GDALGetDriverByName("GeoJSONSeq"))
580
0
    {
581
0
        return true;
582
0
    }
583
584
0
    return bMightBeSequence &&
585
0
           IsLikelyNewlineSequenceGeoJSON(nullptr, nullptr, pszText) ==
586
0
               GDAL_IDENTIFY_TRUE;
587
0
}
588
589
/************************************************************************/
590
/*                        JSONFGFileIsObject()                          */
591
/************************************************************************/
592
593
static bool JSONFGFileIsObject(GDALOpenInfo *poOpenInfo)
594
91.4k
{
595
    // 6000 somewhat arbitrary. Based on other JSON-like drivers
596
91.4k
    if (poOpenInfo->fpL == nullptr || !poOpenInfo->TryToIngest(6000))
597
45.7k
    {
598
45.7k
        return false;
599
45.7k
    }
600
601
45.6k
    const char *pszText =
602
45.6k
        reinterpret_cast<const char *>(poOpenInfo->pabyHeader);
603
45.6k
    return JSONFGIsObject(pszText, poOpenInfo);
604
91.4k
}
605
606
bool JSONFGIsObject(const char *pszText, GDALOpenInfo *poOpenInfo)
607
171k
{
608
171k
    if (!IsJSONObject(pszText))
609
131k
        return false;
610
611
39.6k
    if (poOpenInfo->IsSingleAllowedDriver("JSONFG") &&
612
39.6k
        GDALGetDriverByName("JSONFG"))
613
0
    {
614
0
        return true;
615
0
    }
616
617
39.6k
    const std::string osWithoutSpace = GetCompactJSon(pszText, strlen(pszText));
618
619
    // In theory, conformsTo should be required, but let be lax...
620
39.6k
    {
621
39.6k
        const auto nPos = osWithoutSpace.find("\"conformsTo\":[");
622
39.6k
        if (nPos != std::string::npos)
623
1.78k
        {
624
1.78k
            for (const char *pszVersion : {"0.1", "0.2", "0.3"})
625
5.23k
            {
626
5.23k
                if (osWithoutSpace.find(
627
5.23k
                        CPLSPrintf("\"[ogc-json-fg-1-%s:core]\"", pszVersion),
628
5.23k
                        nPos) != std::string::npos ||
629
5.23k
                    osWithoutSpace.find(
630
5.08k
                        CPLSPrintf(
631
5.08k
                            "\"http://www.opengis.net/spec/json-fg-1/%s\"",
632
5.08k
                            pszVersion),
633
5.08k
                        nPos) != std::string::npos)
634
184
                {
635
184
                    return true;
636
184
                }
637
5.23k
            }
638
1.78k
        }
639
39.6k
    }
640
641
39.4k
    if (osWithoutSpace.find("\"place\":{\"type\":") != std::string::npos ||
642
39.4k
        osWithoutSpace.find("\"place\":{\"coordinates\":") !=
643
39.0k
            std::string::npos ||
644
39.4k
        osWithoutSpace.find("\"time\":{\"date\":") != std::string::npos ||
645
39.4k
        osWithoutSpace.find("\"time\":{\"timestamp\":") != std::string::npos ||
646
39.4k
        osWithoutSpace.find("\"time\":{\"interval\":") != std::string::npos)
647
1.46k
    {
648
1.46k
        return true;
649
1.46k
    }
650
651
37.9k
    if (osWithoutSpace.find("\"coordRefSys\":") != std::string::npos ||
652
37.9k
        osWithoutSpace.find("\"featureType\":") != std::string::npos)
653
2.99k
    {
654
        // Check that coordRefSys and/or featureType are either at the
655
        // FeatureCollection or Feature level
656
2.99k
        struct MyParser : public CPLJSonStreamingParser
657
2.99k
        {
658
2.99k
            bool m_bFoundJSONFGFeatureType = false;
659
2.99k
            bool m_bFoundJSONFGCoordrefSys = false;
660
2.99k
            std::string m_osLevel{};
661
662
2.99k
            void StartObjectMember(const char *pszKey, size_t nLength) override
663
40.4k
            {
664
40.4k
                if (nLength == strlen("featureType") &&
665
40.4k
                    strcmp(pszKey, "featureType") == 0)
666
637
                {
667
637
                    m_bFoundJSONFGFeatureType =
668
637
                        (m_osLevel == "{" ||   // At FeatureCollection level
669
637
                         m_osLevel == "{[{");  // At Feature level
670
637
                    if (m_bFoundJSONFGFeatureType)
671
250
                        StopParsing();
672
637
                }
673
39.8k
                else if (nLength == strlen("coordRefSys") &&
674
39.8k
                         strcmp(pszKey, "coordRefSys") == 0)
675
1.74k
                {
676
1.74k
                    m_bFoundJSONFGCoordrefSys =
677
1.74k
                        (m_osLevel == "{" ||   // At FeatureCollection level
678
1.74k
                         m_osLevel == "{[{");  // At Feature level
679
1.74k
                    if (m_bFoundJSONFGCoordrefSys)
680
494
                        StopParsing();
681
1.74k
                }
682
40.4k
            }
683
684
2.99k
            void StartObject() override
685
18.0k
            {
686
18.0k
                m_osLevel += '{';
687
18.0k
            }
688
689
2.99k
            void EndObject() override
690
3.27k
            {
691
3.27k
                if (!m_osLevel.empty())
692
3.27k
                    m_osLevel.pop_back();
693
3.27k
            }
694
695
2.99k
            void StartArray() override
696
14.9k
            {
697
14.9k
                m_osLevel += '[';
698
14.9k
            }
699
700
2.99k
            void EndArray() override
701
8.13k
            {
702
8.13k
                if (!m_osLevel.empty())
703
8.13k
                    m_osLevel.pop_back();
704
8.13k
            }
705
2.99k
        };
706
707
2.99k
        MyParser oParser;
708
2.99k
        oParser.Parse(pszText, strlen(pszText), true);
709
2.99k
        if (oParser.m_bFoundJSONFGFeatureType ||
710
2.99k
            oParser.m_bFoundJSONFGCoordrefSys)
711
744
        {
712
744
            return true;
713
744
        }
714
2.99k
    }
715
716
37.2k
    return false;
717
37.9k
}
718
719
/************************************************************************/
720
/*                           IsLikelyESRIJSONURL()                      */
721
/************************************************************************/
722
723
static bool IsLikelyESRIJSONURL(const char *pszURL)
724
5.41k
{
725
    // URLs with f=json are strong candidates for ESRI JSON services
726
    // except if they have "/items?", in which case they are likely OAPIF
727
5.41k
    return (strstr(pszURL, "f=json") != nullptr ||
728
5.41k
            strstr(pszURL, "f=pjson") != nullptr ||
729
5.41k
            strstr(pszURL, "resultRecordCount=") != nullptr) &&
730
5.41k
           strstr(pszURL, "/items?") == nullptr;
731
5.41k
}
732
733
/************************************************************************/
734
/*                           GeoJSONGetSourceType()                     */
735
/************************************************************************/
736
737
GeoJSONSourceType GeoJSONGetSourceType(GDALOpenInfo *poOpenInfo)
738
136k
{
739
136k
    GeoJSONSourceType srcType = eGeoJSONSourceUnknown;
740
741
    // NOTE: Sometimes URL ends with .geojson token, for example
742
    //       http://example/path/2232.geojson
743
    //       It's important to test beginning of source first.
744
136k
    if (STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSON:http://") ||
745
136k
        STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSON:https://") ||
746
136k
        STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSON:ftp://"))
747
2
    {
748
2
        srcType = eGeoJSONSourceService;
749
2
    }
750
136k
    else if (STARTS_WITH_CI(poOpenInfo->pszFilename, "http://") ||
751
136k
             STARTS_WITH_CI(poOpenInfo->pszFilename, "https://") ||
752
136k
             STARTS_WITH_CI(poOpenInfo->pszFilename, "ftp://"))
753
1.16k
    {
754
1.16k
        if (poOpenInfo->IsSingleAllowedDriver("GeoJSON"))
755
0
        {
756
0
            return eGeoJSONSourceService;
757
0
        }
758
1.16k
        if ((strstr(poOpenInfo->pszFilename, "SERVICE=WFS") ||
759
1.16k
             strstr(poOpenInfo->pszFilename, "service=WFS") ||
760
1.16k
             strstr(poOpenInfo->pszFilename, "service=wfs")) &&
761
1.16k
            !strstr(poOpenInfo->pszFilename, "json"))
762
59
        {
763
59
            return eGeoJSONSourceUnknown;
764
59
        }
765
1.10k
        if (IsLikelyESRIJSONURL(poOpenInfo->pszFilename))
766
29
        {
767
29
            return eGeoJSONSourceUnknown;
768
29
        }
769
1.07k
        srcType = eGeoJSONSourceService;
770
1.07k
    }
771
135k
    else if (STARTS_WITH_CI(poOpenInfo->pszFilename, "GeoJSON:"))
772
1
    {
773
1
        VSIStatBufL sStat;
774
1
        if (VSIStatL(poOpenInfo->pszFilename + strlen("GeoJSON:"), &sStat) == 0)
775
0
        {
776
0
            return eGeoJSONSourceFile;
777
0
        }
778
1
        const char *pszText = poOpenInfo->pszFilename + strlen("GeoJSON:");
779
1
        if (GeoJSONIsObject(pszText, poOpenInfo))
780
0
            return eGeoJSONSourceText;
781
1
        return eGeoJSONSourceUnknown;
782
1
    }
783
135k
    else if (GeoJSONIsObject(poOpenInfo->pszFilename, poOpenInfo))
784
0
    {
785
0
        srcType = eGeoJSONSourceText;
786
0
    }
787
135k
    else if (GeoJSONFileIsObject(poOpenInfo))
788
7.64k
    {
789
7.64k
        srcType = eGeoJSONSourceFile;
790
7.64k
    }
791
792
136k
    return srcType;
793
136k
}
794
795
/************************************************************************/
796
/*                     ESRIJSONDriverGetSourceType()                    */
797
/************************************************************************/
798
799
GeoJSONSourceType ESRIJSONDriverGetSourceType(GDALOpenInfo *poOpenInfo)
800
129k
{
801
129k
    if (STARTS_WITH_CI(poOpenInfo->pszFilename, "ESRIJSON:http://") ||
802
129k
        STARTS_WITH_CI(poOpenInfo->pszFilename, "ESRIJSON:https://") ||
803
129k
        STARTS_WITH_CI(poOpenInfo->pszFilename, "ESRIJSON:ftp://"))
804
6
    {
805
6
        return eGeoJSONSourceService;
806
6
    }
807
129k
    else if (STARTS_WITH(poOpenInfo->pszFilename, "http://") ||
808
129k
             STARTS_WITH(poOpenInfo->pszFilename, "https://") ||
809
129k
             STARTS_WITH(poOpenInfo->pszFilename, "ftp://"))
810
653
    {
811
653
        if (poOpenInfo->IsSingleAllowedDriver("ESRIJSON"))
812
0
        {
813
0
            return eGeoJSONSourceService;
814
0
        }
815
653
        if (IsLikelyESRIJSONURL(poOpenInfo->pszFilename))
816
58
        {
817
58
            return eGeoJSONSourceService;
818
58
        }
819
595
        return eGeoJSONSourceUnknown;
820
653
    }
821
822
128k
    if (STARTS_WITH_CI(poOpenInfo->pszFilename, "ESRIJSON:"))
823
32
    {
824
32
        VSIStatBufL sStat;
825
32
        if (VSIStatL(poOpenInfo->pszFilename + strlen("ESRIJSON:"), &sStat) ==
826
32
            0)
827
0
        {
828
0
            return eGeoJSONSourceFile;
829
0
        }
830
32
        const char *pszText = poOpenInfo->pszFilename + strlen("ESRIJSON:");
831
32
        if (ESRIJSONIsObject(pszText, poOpenInfo))
832
0
            return eGeoJSONSourceText;
833
32
        return eGeoJSONSourceUnknown;
834
32
    }
835
836
128k
    if (poOpenInfo->fpL == nullptr)
837
46.5k
    {
838
46.5k
        const char *pszText = poOpenInfo->pszFilename;
839
46.5k
        if (ESRIJSONIsObject(pszText, poOpenInfo))
840
70
            return eGeoJSONSourceText;
841
46.5k
        return eGeoJSONSourceUnknown;
842
46.5k
    }
843
844
    // By default read first 6000 bytes.
845
    // 6000 was chosen as enough bytes to
846
    // enable all current tests to pass.
847
82.0k
    if (!poOpenInfo->TryToIngest(6000))
848
0
    {
849
0
        return eGeoJSONSourceUnknown;
850
0
    }
851
852
82.0k
    if (poOpenInfo->pabyHeader != nullptr &&
853
82.0k
        ESRIJSONIsObject(reinterpret_cast<const char *>(poOpenInfo->pabyHeader),
854
82.0k
                         poOpenInfo))
855
934
    {
856
934
        return eGeoJSONSourceFile;
857
934
    }
858
81.1k
    return eGeoJSONSourceUnknown;
859
82.0k
}
860
861
/************************************************************************/
862
/*                     TopoJSONDriverGetSourceType()                    */
863
/************************************************************************/
864
865
GeoJSONSourceType TopoJSONDriverGetSourceType(GDALOpenInfo *poOpenInfo)
866
129k
{
867
129k
    if (STARTS_WITH_CI(poOpenInfo->pszFilename, "TopoJSON:http://") ||
868
129k
        STARTS_WITH_CI(poOpenInfo->pszFilename, "TopoJSON:https://") ||
869
129k
        STARTS_WITH_CI(poOpenInfo->pszFilename, "TopoJSON:ftp://"))
870
16
    {
871
16
        return eGeoJSONSourceService;
872
16
    }
873
129k
    else if (STARTS_WITH(poOpenInfo->pszFilename, "http://") ||
874
129k
             STARTS_WITH(poOpenInfo->pszFilename, "https://") ||
875
129k
             STARTS_WITH(poOpenInfo->pszFilename, "ftp://"))
876
1.21k
    {
877
1.21k
        if (poOpenInfo->IsSingleAllowedDriver("TOPOJSON"))
878
0
        {
879
0
            return eGeoJSONSourceService;
880
0
        }
881
1.21k
        if (IsLikelyESRIJSONURL(poOpenInfo->pszFilename))
882
29
        {
883
29
            return eGeoJSONSourceUnknown;
884
29
        }
885
1.19k
        return eGeoJSONSourceService;
886
1.21k
    }
887
888
128k
    if (STARTS_WITH_CI(poOpenInfo->pszFilename, "TopoJSON:"))
889
90
    {
890
90
        VSIStatBufL sStat;
891
90
        if (VSIStatL(poOpenInfo->pszFilename + strlen("TopoJSON:"), &sStat) ==
892
90
            0)
893
0
        {
894
0
            return eGeoJSONSourceFile;
895
0
        }
896
90
        const char *pszText = poOpenInfo->pszFilename + strlen("TopoJSON:");
897
90
        if (TopoJSONIsObject(pszText, poOpenInfo))
898
0
            return eGeoJSONSourceText;
899
90
        return eGeoJSONSourceUnknown;
900
90
    }
901
902
128k
    if (poOpenInfo->fpL == nullptr)
903
46.4k
    {
904
46.4k
        const char *pszText = poOpenInfo->pszFilename;
905
46.4k
        if (TopoJSONIsObject(pszText, poOpenInfo))
906
0
            return eGeoJSONSourceText;
907
46.4k
        return eGeoJSONSourceUnknown;
908
46.4k
    }
909
910
    // By default read first 6000 bytes.
911
    // 6000 was chosen as enough bytes to
912
    // enable all current tests to pass.
913
81.5k
    if (!poOpenInfo->TryToIngest(6000))
914
0
    {
915
0
        return eGeoJSONSourceUnknown;
916
0
    }
917
918
81.5k
    if (poOpenInfo->pabyHeader != nullptr &&
919
81.5k
        TopoJSONIsObject(reinterpret_cast<const char *>(poOpenInfo->pabyHeader),
920
81.5k
                         poOpenInfo))
921
68
    {
922
68
        return eGeoJSONSourceFile;
923
68
    }
924
81.5k
    return eGeoJSONSourceUnknown;
925
81.5k
}
926
927
/************************************************************************/
928
/*                          GeoJSONSeqGetSourceType()                   */
929
/************************************************************************/
930
931
GeoJSONSourceType GeoJSONSeqGetSourceType(GDALOpenInfo *poOpenInfo)
932
135k
{
933
135k
    GeoJSONSourceType srcType = eGeoJSONSourceUnknown;
934
935
135k
    if (STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSONSeq:http://") ||
936
135k
        STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSONSeq:https://") ||
937
135k
        STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSONSeq:ftp://"))
938
0
    {
939
0
        srcType = eGeoJSONSourceService;
940
0
    }
941
135k
    else if (STARTS_WITH_CI(poOpenInfo->pszFilename, "http://") ||
942
135k
             STARTS_WITH_CI(poOpenInfo->pszFilename, "https://") ||
943
135k
             STARTS_WITH_CI(poOpenInfo->pszFilename, "ftp://"))
944
1.21k
    {
945
1.21k
        if (poOpenInfo->IsSingleAllowedDriver("GeoJSONSeq"))
946
0
        {
947
0
            return eGeoJSONSourceService;
948
0
        }
949
1.21k
        if (IsLikelyESRIJSONURL(poOpenInfo->pszFilename))
950
29
        {
951
29
            return eGeoJSONSourceUnknown;
952
29
        }
953
1.19k
        srcType = eGeoJSONSourceService;
954
1.19k
    }
955
134k
    else if (STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSONSeq:"))
956
158
    {
957
158
        VSIStatBufL sStat;
958
158
        if (VSIStatL(poOpenInfo->pszFilename + strlen("GEOJSONSeq:"), &sStat) ==
959
158
            0)
960
140
        {
961
140
            return eGeoJSONSourceFile;
962
140
        }
963
18
        const char *pszText = poOpenInfo->pszFilename + strlen("GEOJSONSeq:");
964
18
        if (GeoJSONSeqIsObject(pszText, poOpenInfo))
965
0
            return eGeoJSONSourceText;
966
18
        return eGeoJSONSourceUnknown;
967
18
    }
968
133k
    else if (GeoJSONSeqIsObject(poOpenInfo->pszFilename, poOpenInfo))
969
0
    {
970
0
        srcType = eGeoJSONSourceText;
971
0
    }
972
133k
    else if (GeoJSONSeqFileIsObject(poOpenInfo))
973
7.11k
    {
974
7.11k
        srcType = eGeoJSONSourceFile;
975
7.11k
    }
976
977
135k
    return srcType;
978
135k
}
979
980
/************************************************************************/
981
/*                      JSONFGDriverGetSourceType()                     */
982
/************************************************************************/
983
984
GeoJSONSourceType JSONFGDriverGetSourceType(GDALOpenInfo *poOpenInfo)
985
93.2k
{
986
93.2k
    GeoJSONSourceType srcType = eGeoJSONSourceUnknown;
987
988
93.2k
    if (STARTS_WITH_CI(poOpenInfo->pszFilename, "JSONFG:http://") ||
989
93.2k
        STARTS_WITH_CI(poOpenInfo->pszFilename, "JSONFG:https://") ||
990
93.2k
        STARTS_WITH_CI(poOpenInfo->pszFilename, "JSONFG:ftp://"))
991
108
    {
992
108
        srcType = eGeoJSONSourceService;
993
108
    }
994
93.1k
    else if (STARTS_WITH_CI(poOpenInfo->pszFilename, "http://") ||
995
93.1k
             STARTS_WITH_CI(poOpenInfo->pszFilename, "https://") ||
996
93.1k
             STARTS_WITH_CI(poOpenInfo->pszFilename, "ftp://"))
997
1.21k
    {
998
1.21k
        if (poOpenInfo->IsSingleAllowedDriver("JSONFG"))
999
0
        {
1000
0
            return eGeoJSONSourceService;
1001
0
        }
1002
1.21k
        if (IsLikelyESRIJSONURL(poOpenInfo->pszFilename))
1003
29
        {
1004
29
            return eGeoJSONSourceUnknown;
1005
29
        }
1006
1.19k
        srcType = eGeoJSONSourceService;
1007
1.19k
    }
1008
91.9k
    else if (STARTS_WITH_CI(poOpenInfo->pszFilename, "JSONFG:"))
1009
304
    {
1010
304
        VSIStatBufL sStat;
1011
304
        const size_t nJSONFGPrefixLen = strlen("JSONFG:");
1012
304
        if (VSIStatL(poOpenInfo->pszFilename + nJSONFGPrefixLen, &sStat) == 0)
1013
100
        {
1014
100
            return eGeoJSONSourceFile;
1015
100
        }
1016
204
        const char *pszText = poOpenInfo->pszFilename + nJSONFGPrefixLen;
1017
204
        if (JSONFGIsObject(pszText, poOpenInfo))
1018
20
            return eGeoJSONSourceText;
1019
184
        return eGeoJSONSourceUnknown;
1020
204
    }
1021
91.6k
    else if (JSONFGIsObject(poOpenInfo->pszFilename, poOpenInfo))
1022
256
    {
1023
256
        srcType = eGeoJSONSourceText;
1024
256
    }
1025
91.4k
    else if (JSONFGFileIsObject(poOpenInfo))
1026
850
    {
1027
850
        srcType = eGeoJSONSourceFile;
1028
850
    }
1029
1030
92.9k
    return srcType;
1031
93.2k
}
1032
1033
/************************************************************************/
1034
/*                        GeoJSONStringPropertyToFieldType()            */
1035
/************************************************************************/
1036
1037
OGRFieldType GeoJSONStringPropertyToFieldType(json_object *poObject,
1038
                                              int &nTZFlag)
1039
32.3k
{
1040
32.3k
    if (poObject == nullptr)
1041
1.04k
    {
1042
1.04k
        return OFTString;
1043
1.04k
    }
1044
31.3k
    const char *pszStr = json_object_get_string(poObject);
1045
1046
31.3k
    nTZFlag = 0;
1047
31.3k
    OGRField sWrkField;
1048
31.3k
    CPLPushErrorHandler(CPLQuietErrorHandler);
1049
31.3k
    const bool bSuccess = CPL_TO_BOOL(OGRParseDate(pszStr, &sWrkField, 0));
1050
31.3k
    CPLPopErrorHandler();
1051
31.3k
    CPLErrorReset();
1052
31.3k
    if (bSuccess)
1053
817
    {
1054
817
        const bool bHasDate =
1055
817
            strchr(pszStr, '/') != nullptr || strchr(pszStr, '-') != nullptr;
1056
817
        const bool bHasTime = strchr(pszStr, ':') != nullptr;
1057
817
        nTZFlag = sWrkField.Date.TZFlag;
1058
817
        if (bHasDate && bHasTime)
1059
817
            return OFTDateTime;
1060
0
        else if (bHasDate)
1061
0
            return OFTDate;
1062
0
        else
1063
0
            return OFTTime;
1064
        // TODO: What if both are false?
1065
817
    }
1066
30.4k
    return OFTString;
1067
31.3k
}
1068
1069
/************************************************************************/
1070
/*                   GeoJSONHTTPFetchWithContentTypeHeader()            */
1071
/************************************************************************/
1072
1073
CPLHTTPResult *GeoJSONHTTPFetchWithContentTypeHeader(const char *pszURL)
1074
1.76k
{
1075
1.76k
    std::string osHeaders;
1076
1.76k
    const char *pszGDAL_HTTP_HEADERS =
1077
1.76k
        CPLGetConfigOption("GDAL_HTTP_HEADERS", nullptr);
1078
1.76k
    bool bFoundAcceptHeader = false;
1079
1.76k
    if (pszGDAL_HTTP_HEADERS)
1080
0
    {
1081
0
        bool bHeadersDone = false;
1082
        // Compatibility hack for "HEADERS=Accept: text/plain, application/json"
1083
0
        if (strstr(pszGDAL_HTTP_HEADERS, "\r\n") == nullptr)
1084
0
        {
1085
0
            const char *pszComma = strchr(pszGDAL_HTTP_HEADERS, ',');
1086
0
            if (pszComma != nullptr && strchr(pszComma, ':') == nullptr)
1087
0
            {
1088
0
                osHeaders = pszGDAL_HTTP_HEADERS;
1089
0
                bFoundAcceptHeader =
1090
0
                    STARTS_WITH_CI(pszGDAL_HTTP_HEADERS, "Accept:");
1091
0
                bHeadersDone = true;
1092
0
            }
1093
0
        }
1094
0
        if (!bHeadersDone)
1095
0
        {
1096
            // We accept both raw headers with \r\n as a separator, or as
1097
            // a comma separated list of foo: bar values.
1098
0
            const CPLStringList aosTokens(
1099
0
                strstr(pszGDAL_HTTP_HEADERS, "\r\n")
1100
0
                    ? CSLTokenizeString2(pszGDAL_HTTP_HEADERS, "\r\n", 0)
1101
0
                    : CSLTokenizeString2(pszGDAL_HTTP_HEADERS, ",",
1102
0
                                         CSLT_HONOURSTRINGS));
1103
0
            for (int i = 0; i < aosTokens.size(); ++i)
1104
0
            {
1105
0
                if (!osHeaders.empty())
1106
0
                    osHeaders += "\r\n";
1107
0
                if (!bFoundAcceptHeader)
1108
0
                    bFoundAcceptHeader =
1109
0
                        STARTS_WITH_CI(aosTokens[i], "Accept:");
1110
0
                osHeaders += aosTokens[i];
1111
0
            }
1112
0
        }
1113
0
    }
1114
1.76k
    if (!bFoundAcceptHeader)
1115
1.76k
    {
1116
1.76k
        if (!osHeaders.empty())
1117
0
            osHeaders += "\r\n";
1118
1.76k
        osHeaders += "Accept: text/plain, application/json";
1119
1.76k
    }
1120
1121
1.76k
    CPLStringList aosOptions;
1122
1.76k
    aosOptions.SetNameValue("HEADERS", osHeaders.c_str());
1123
1.76k
    CPLHTTPResult *pResult = CPLHTTPFetch(pszURL, aosOptions.List());
1124
1125
1.76k
    if (nullptr == pResult || 0 == pResult->nDataLen ||
1126
1.76k
        0 != CPLGetLastErrorNo())
1127
1.76k
    {
1128
1.76k
        CPLHTTPDestroyResult(pResult);
1129
1.76k
        return nullptr;
1130
1.76k
    }
1131
1132
0
    if (0 != pResult->nStatus)
1133
0
    {
1134
0
        CPLError(CE_Failure, CPLE_AppDefined, "Curl reports error: %d: %s",
1135
0
                 pResult->nStatus, pResult->pszErrBuf);
1136
0
        CPLHTTPDestroyResult(pResult);
1137
0
        return nullptr;
1138
0
    }
1139
1140
0
    return pResult;
1141
0
}