Coverage Report

Created: 2025-06-13 06:29

/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
0
{
37
    /* Skip UTF-8 BOM (#5630) */
38
0
    const GByte *pabyData = reinterpret_cast<const GByte *>(pszText);
39
0
    if (pabyData[0] == 0xEF && pabyData[1] == 0xBB && pabyData[2] == 0xBF)
40
0
        pszText += 3;
41
0
}
42
43
/************************************************************************/
44
/*                           IsJSONObject()                             */
45
/************************************************************************/
46
47
static bool IsJSONObject(const char *pszText)
48
0
{
49
0
    if (nullptr == pszText)
50
0
        return false;
51
52
0
    SkipUTF8BOM(pszText);
53
54
    /* -------------------------------------------------------------------- */
55
    /*      This is a primitive test, but we need to perform it fast.       */
56
    /* -------------------------------------------------------------------- */
57
0
    while (*pszText != '\0' && isspace(static_cast<unsigned char>(*pszText)))
58
0
        pszText++;
59
60
0
    const char *const apszPrefix[] = {"loadGeoJSON(", "jsonp("};
61
0
    for (size_t iP = 0; iP < sizeof(apszPrefix) / sizeof(apszPrefix[0]); iP++)
62
0
    {
63
0
        if (strncmp(pszText, apszPrefix[iP], strlen(apszPrefix[iP])) == 0)
64
0
        {
65
0
            pszText += strlen(apszPrefix[iP]);
66
0
            break;
67
0
        }
68
0
    }
69
70
0
    if (*pszText != '{')
71
0
        return false;
72
73
0
    return true;
74
0
}
75
76
/************************************************************************/
77
/*                           GetTopLevelType()                          */
78
/************************************************************************/
79
80
static std::string GetTopLevelType(const char *pszText)
81
0
{
82
0
    if (!strstr(pszText, "\"type\""))
83
0
        return std::string();
84
85
0
    SkipUTF8BOM(pszText);
86
87
0
    struct MyParser : public CPLJSonStreamingParser
88
0
    {
89
0
        std::string m_osLevel{};
90
0
        bool m_bInTopLevelType = false;
91
0
        std::string m_osTopLevelTypeValue{};
92
93
0
        void StartObjectMember(const char *pszKey, size_t nLength) override
94
0
        {
95
0
            m_bInTopLevelType = false;
96
0
            if (nLength == strlen("type") && strcmp(pszKey, "type") == 0 &&
97
0
                m_osLevel == "{")
98
0
            {
99
0
                m_bInTopLevelType = true;
100
0
            }
101
0
        }
102
103
0
        void String(const char *pszValue, size_t nLength) override
104
0
        {
105
0
            if (m_bInTopLevelType)
106
0
            {
107
0
                m_osTopLevelTypeValue.assign(pszValue, nLength);
108
0
                StopParsing();
109
0
            }
110
0
        }
111
112
0
        void StartObject() override
113
0
        {
114
0
            m_osLevel += '{';
115
0
            m_bInTopLevelType = false;
116
0
        }
117
118
0
        void EndObject() override
119
0
        {
120
0
            if (!m_osLevel.empty())
121
0
                m_osLevel.pop_back();
122
0
            m_bInTopLevelType = false;
123
0
        }
124
125
0
        void StartArray() override
126
0
        {
127
0
            m_osLevel += '[';
128
0
            m_bInTopLevelType = false;
129
0
        }
130
131
0
        void EndArray() override
132
0
        {
133
0
            if (!m_osLevel.empty())
134
0
                m_osLevel.pop_back();
135
0
            m_bInTopLevelType = false;
136
0
        }
137
0
    };
138
139
0
    MyParser oParser;
140
0
    oParser.Parse(pszText, strlen(pszText), true);
141
0
    return oParser.m_osTopLevelTypeValue;
142
0
}
143
144
/************************************************************************/
145
/*                           GetCompactJSon()                           */
146
/************************************************************************/
147
148
static CPLString GetCompactJSon(const char *pszText, size_t nMaxSize)
149
0
{
150
    /* Skip UTF-8 BOM (#5630) */
151
0
    const GByte *pabyData = reinterpret_cast<const GByte *>(pszText);
152
0
    if (pabyData[0] == 0xEF && pabyData[1] == 0xBB && pabyData[2] == 0xBF)
153
0
        pszText += 3;
154
155
0
    CPLString osWithoutSpace;
156
0
    bool bInString = false;
157
0
    for (int i = 0; pszText[i] != '\0' && osWithoutSpace.size() < nMaxSize; i++)
158
0
    {
159
0
        if (bInString)
160
0
        {
161
0
            if (pszText[i] == '\\')
162
0
            {
163
0
                osWithoutSpace += pszText[i];
164
0
                if (pszText[i + 1] == '\0')
165
0
                    break;
166
0
                osWithoutSpace += pszText[i + 1];
167
0
                i++;
168
0
            }
169
0
            else if (pszText[i] == '"')
170
0
            {
171
0
                bInString = false;
172
0
                osWithoutSpace += '"';
173
0
            }
174
0
            else
175
0
            {
176
0
                osWithoutSpace += pszText[i];
177
0
            }
178
0
        }
179
0
        else if (pszText[i] == '"')
180
0
        {
181
0
            bInString = true;
182
0
            osWithoutSpace += '"';
183
0
        }
184
0
        else if (!isspace(static_cast<unsigned char>(pszText[i])))
185
0
        {
186
0
            osWithoutSpace += pszText[i];
187
0
        }
188
0
    }
189
0
    return osWithoutSpace;
190
0
}
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
0
{
200
0
    bMightBeSequence = false;
201
0
    bReadMoreBytes = false;
202
203
0
    if (!IsJSONObject(pszText))
204
0
        return false;
205
206
0
    const std::string osTopLevelType = GetTopLevelType(pszText);
207
0
    if (osTopLevelType == "Topology")
208
0
        return false;
209
210
0
    if (poOpenInfo->IsSingleAllowedDriver(pszExpectedDriverName) &&
211
0
        GDALGetDriverByName(pszExpectedDriverName))
212
0
    {
213
0
        return true;
214
0
    }
215
216
0
    if ((!poOpenInfo->papszAllowedDrivers ||
217
0
         CSLFindString(poOpenInfo->papszAllowedDrivers, "JSONFG") >= 0) &&
218
0
        GDALGetDriverByName("JSONFG") && JSONFGIsObject(pszText, poOpenInfo))
219
0
    {
220
0
        return false;
221
0
    }
222
223
0
    if (osTopLevelType == "FeatureCollection")
224
0
    {
225
0
        return true;
226
0
    }
227
228
0
    const std::string osWithoutSpace = GetCompactJSon(pszText, strlen(pszText));
229
0
    if (osWithoutSpace.find("{\"features\":[") == 0 &&
230
0
        osWithoutSpace.find(szESRIJSonFeaturesGeometryRings) != 0 &&
231
0
        osWithoutSpace.find(szESRIJSonFeaturesAttributes) != 0)
232
0
    {
233
0
        return true;
234
0
    }
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
0
    if (osWithoutSpace.find(",\"features\":[") != std::string::npos)
243
0
    {
244
0
        return !ESRIJSONIsObject(pszText, poOpenInfo);
245
0
    }
246
247
    // See https://github.com/OSGeo/gdal/issues/2720
248
0
    if (osWithoutSpace.find("{\"coordinates\":[") == 0 ||
249
        // and https://github.com/OSGeo/gdal/issues/2787
250
0
        osWithoutSpace.find("{\"geometry\":{\"coordinates\":[") == 0 ||
251
        // and https://github.com/qgis/QGIS/issues/61266
252
0
        osWithoutSpace.find(
253
0
            "{\"geometry\":{\"type\":\"Point\",\"coordinates\":[") == 0 ||
254
0
        osWithoutSpace.find(
255
0
            "{\"geometry\":{\"type\":\"LineString\",\"coordinates\":[") == 0 ||
256
0
        osWithoutSpace.find(
257
0
            "{\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[") == 0 ||
258
0
        osWithoutSpace.find(
259
0
            "{\"geometry\":{\"type\":\"MultiPoint\",\"coordinates\":[") == 0 ||
260
0
        osWithoutSpace.find(
261
0
            "{\"geometry\":{\"type\":\"MultiLineString\",\"coordinates\":[") ==
262
0
            0 ||
263
0
        osWithoutSpace.find(
264
0
            "{\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[") ==
265
0
            0 ||
266
0
        osWithoutSpace.find("{\"geometry\":{\"type\":\"GeometryCollection\","
267
0
                            "\"geometries\":[") == 0)
268
0
    {
269
0
        return true;
270
0
    }
271
272
0
    if (osTopLevelType == "Feature" || osTopLevelType == "Point" ||
273
0
        osTopLevelType == "LineString" || osTopLevelType == "Polygon" ||
274
0
        osTopLevelType == "MultiPoint" || osTopLevelType == "MultiLineString" ||
275
0
        osTopLevelType == "MultiPolygon" ||
276
0
        osTopLevelType == "GeometryCollection")
277
0
    {
278
0
        bMightBeSequence = true;
279
0
        return true;
280
0
    }
281
282
    // See https://github.com/OSGeo/gdal/issues/3280
283
0
    if (osWithoutSpace.find("{\"properties\":{") == 0)
284
0
    {
285
0
        bMightBeSequence = true;
286
0
        bReadMoreBytes = true;
287
0
        return false;
288
0
    }
289
290
0
    return false;
291
0
}
292
293
static bool IsGeoJSONLikeObject(const char *pszText, GDALOpenInfo *poOpenInfo,
294
                                const char *pszExpectedDriverName)
295
0
{
296
0
    bool bMightBeSequence;
297
0
    bool bReadMoreBytes;
298
0
    return IsGeoJSONLikeObject(pszText, bMightBeSequence, bReadMoreBytes,
299
0
                               poOpenInfo, pszExpectedDriverName);
300
0
}
301
302
/************************************************************************/
303
/*                       ESRIJSONIsObject()                             */
304
/************************************************************************/
305
306
bool ESRIJSONIsObject(const char *pszText, GDALOpenInfo *poOpenInfo)
307
0
{
308
0
    if (!IsJSONObject(pszText))
309
0
        return false;
310
311
0
    if (poOpenInfo->IsSingleAllowedDriver("ESRIJSON") &&
312
0
        GDALGetDriverByName("ESRIJSON"))
313
0
    {
314
0
        return true;
315
0
    }
316
317
0
    if (  // ESRI Json geometry
318
0
        (strstr(pszText, "\"geometryType\"") != nullptr &&
319
0
         strstr(pszText, "\"esriGeometry") != nullptr)
320
321
        // ESRI Json "FeatureCollection"
322
0
        || strstr(pszText, "\"fieldAliases\"") != nullptr
323
324
        // ESRI Json "FeatureCollection"
325
0
        || (strstr(pszText, "\"fields\"") != nullptr &&
326
0
            strstr(pszText, "\"esriFieldType") != nullptr))
327
0
    {
328
0
        return true;
329
0
    }
330
331
0
    const std::string osWithoutSpace = GetCompactJSon(pszText, strlen(pszText));
332
0
    if (osWithoutSpace.find(szESRIJSonFeaturesGeometryRings) == 0 ||
333
0
        osWithoutSpace.find(szESRIJSonFeaturesAttributes) == 0 ||
334
0
        osWithoutSpace.find("\"spatialReference\":{\"wkid\":") !=
335
0
            std::string::npos)
336
0
    {
337
0
        return true;
338
0
    }
339
340
0
    return false;
341
0
}
342
343
/************************************************************************/
344
/*                       TopoJSONIsObject()                             */
345
/************************************************************************/
346
347
bool TopoJSONIsObject(const char *pszText, GDALOpenInfo *poOpenInfo)
348
0
{
349
0
    if (!IsJSONObject(pszText))
350
0
        return false;
351
352
0
    if (poOpenInfo->IsSingleAllowedDriver("TopoJSON") &&
353
0
        GDALGetDriverByName("TopoJSON"))
354
0
    {
355
0
        return true;
356
0
    }
357
358
0
    return GetTopLevelType(pszText) == "Topology";
359
0
}
360
361
/************************************************************************/
362
/*                      IsLikelyNewlineSequenceGeoJSON()                */
363
/************************************************************************/
364
365
static GDALIdentifyEnum
366
IsLikelyNewlineSequenceGeoJSON(VSILFILE *fpL, const GByte *pabyHeader,
367
                               const char *pszFileContent)
368
0
{
369
0
    const size_t nBufferSize = 4096 * 10;
370
0
    std::vector<GByte> abyBuffer;
371
0
    abyBuffer.resize(nBufferSize + 1);
372
373
0
    int nCurlLevel = 0;
374
0
    bool bInString = false;
375
0
    bool bLastIsEscape = false;
376
0
    bool bFirstIter = true;
377
0
    bool bEOLFound = false;
378
0
    int nCountObject = 0;
379
0
    while (true)
380
0
    {
381
0
        size_t nRead;
382
0
        bool bEnd = false;
383
0
        if (bFirstIter)
384
0
        {
385
0
            const char *pszText =
386
0
                pszFileContent ? pszFileContent
387
0
                               : reinterpret_cast<const char *>(pabyHeader);
388
0
            assert(pszText);
389
0
            nRead = std::min(strlen(pszText), nBufferSize);
390
0
            memcpy(abyBuffer.data(), pszText, nRead);
391
0
            bFirstIter = false;
392
0
            if (fpL)
393
0
            {
394
0
                VSIFSeekL(fpL, nRead, SEEK_SET);
395
0
            }
396
0
        }
397
0
        else
398
0
        {
399
0
            nRead = VSIFReadL(abyBuffer.data(), 1, nBufferSize, fpL);
400
0
            bEnd = nRead < nBufferSize;
401
0
        }
402
0
        for (size_t i = 0; i < nRead; i++)
403
0
        {
404
0
            if (nCurlLevel == 0)
405
0
            {
406
0
                if (abyBuffer[i] == '{')
407
0
                {
408
0
                    nCountObject++;
409
0
                    if (nCountObject == 2)
410
0
                    {
411
0
                        break;
412
0
                    }
413
0
                    nCurlLevel++;
414
0
                }
415
0
                else if (nCountObject == 1 && abyBuffer[i] == '\n')
416
0
                {
417
0
                    bEOLFound = true;
418
0
                }
419
0
                else if (!isspace(static_cast<unsigned char>(abyBuffer[i])))
420
0
                {
421
0
                    return GDAL_IDENTIFY_FALSE;
422
0
                }
423
0
            }
424
0
            else if (bInString)
425
0
            {
426
0
                if (bLastIsEscape)
427
0
                {
428
0
                    bLastIsEscape = false;
429
0
                }
430
0
                else if (abyBuffer[i] == '\\')
431
0
                {
432
0
                    bLastIsEscape = true;
433
0
                }
434
0
                else if (abyBuffer[i] == '"')
435
0
                {
436
0
                    bInString = false;
437
0
                }
438
0
            }
439
0
            else if (abyBuffer[i] == '"')
440
0
            {
441
0
                bInString = true;
442
0
            }
443
0
            else if (abyBuffer[i] == '{')
444
0
            {
445
0
                nCurlLevel++;
446
0
            }
447
0
            else if (abyBuffer[i] == '}')
448
0
            {
449
0
                nCurlLevel--;
450
0
            }
451
0
        }
452
0
        if (!fpL || bEnd || nCountObject == 2)
453
0
            break;
454
0
    }
455
0
    if (bEOLFound && nCountObject == 2)
456
0
        return GDAL_IDENTIFY_TRUE;
457
0
    return GDAL_IDENTIFY_UNKNOWN;
458
0
}
459
460
/************************************************************************/
461
/*                           GeoJSONFileIsObject()                      */
462
/************************************************************************/
463
464
static bool GeoJSONFileIsObject(GDALOpenInfo *poOpenInfo)
465
0
{
466
    // By default read first 6000 bytes.
467
    // 6000 was chosen as enough bytes to
468
    // enable all current tests to pass.
469
470
0
    if (poOpenInfo->fpL == nullptr || !poOpenInfo->TryToIngest(6000))
471
0
    {
472
0
        return false;
473
0
    }
474
475
0
    bool bMightBeSequence = false;
476
0
    bool bReadMoreBytes = false;
477
0
    if (!IsGeoJSONLikeObject(
478
0
            reinterpret_cast<const char *>(poOpenInfo->pabyHeader),
479
0
            bMightBeSequence, bReadMoreBytes, poOpenInfo, "GeoJSON"))
480
0
    {
481
0
        if (!(bReadMoreBytes && poOpenInfo->nHeaderBytes >= 6000 &&
482
0
              poOpenInfo->TryToIngest(1000 * 1000) &&
483
0
              !IsGeoJSONLikeObject(
484
0
                  reinterpret_cast<const char *>(poOpenInfo->pabyHeader),
485
0
                  bMightBeSequence, bReadMoreBytes, poOpenInfo, "GeoJSON")))
486
0
        {
487
0
            return false;
488
0
        }
489
0
    }
490
491
0
    return !(bMightBeSequence && IsLikelyNewlineSequenceGeoJSON(
492
0
                                     poOpenInfo->fpL, poOpenInfo->pabyHeader,
493
0
                                     nullptr) == GDAL_IDENTIFY_TRUE);
494
0
}
495
496
/************************************************************************/
497
/*                           GeoJSONIsObject()                          */
498
/************************************************************************/
499
500
bool GeoJSONIsObject(const char *pszText, GDALOpenInfo *poOpenInfo)
501
0
{
502
0
    bool bMightBeSequence = false;
503
0
    bool bReadMoreBytes = false;
504
0
    if (!IsGeoJSONLikeObject(pszText, bMightBeSequence, bReadMoreBytes,
505
0
                             poOpenInfo, "GeoJSON"))
506
0
    {
507
0
        return false;
508
0
    }
509
510
0
    return !(bMightBeSequence &&
511
0
             IsLikelyNewlineSequenceGeoJSON(nullptr, nullptr, pszText) ==
512
0
                 GDAL_IDENTIFY_TRUE);
513
0
}
514
515
/************************************************************************/
516
/*                        GeoJSONSeqFileIsObject()                      */
517
/************************************************************************/
518
519
static bool GeoJSONSeqFileIsObject(GDALOpenInfo *poOpenInfo)
520
0
{
521
    // By default read first 6000 bytes.
522
    // 6000 was chosen as enough bytes to
523
    // enable all current tests to pass.
524
525
0
    if (poOpenInfo->fpL == nullptr || !poOpenInfo->TryToIngest(6000))
526
0
    {
527
0
        return false;
528
0
    }
529
530
0
    const char *pszText =
531
0
        reinterpret_cast<const char *>(poOpenInfo->pabyHeader);
532
0
    if (pszText[0] == '\x1e')
533
0
        return IsGeoJSONLikeObject(pszText + 1, poOpenInfo, "GeoJSONSeq");
534
535
0
    bool bMightBeSequence = false;
536
0
    bool bReadMoreBytes = false;
537
0
    if (!IsGeoJSONLikeObject(pszText, bMightBeSequence, bReadMoreBytes,
538
0
                             poOpenInfo, "GeoJSONSeq"))
539
0
    {
540
0
        if (!(bReadMoreBytes && poOpenInfo->nHeaderBytes >= 6000 &&
541
0
              poOpenInfo->TryToIngest(1000 * 1000) &&
542
0
              IsGeoJSONLikeObject(
543
0
                  reinterpret_cast<const char *>(poOpenInfo->pabyHeader),
544
0
                  bMightBeSequence, bReadMoreBytes, poOpenInfo, "GeoJSONSeq")))
545
0
        {
546
0
            return false;
547
0
        }
548
0
    }
549
550
0
    if (poOpenInfo->IsSingleAllowedDriver("GeoJSONSeq") &&
551
0
        IsLikelyNewlineSequenceGeoJSON(poOpenInfo->fpL, poOpenInfo->pabyHeader,
552
0
                                       nullptr) != GDAL_IDENTIFY_FALSE &&
553
0
        GDALGetDriverByName("GeoJSONSeq"))
554
0
    {
555
0
        return true;
556
0
    }
557
558
0
    return bMightBeSequence && IsLikelyNewlineSequenceGeoJSON(
559
0
                                   poOpenInfo->fpL, poOpenInfo->pabyHeader,
560
0
                                   nullptr) == GDAL_IDENTIFY_TRUE;
561
0
}
562
563
bool GeoJSONSeqIsObject(const char *pszText, GDALOpenInfo *poOpenInfo)
564
0
{
565
0
    if (pszText[0] == '\x1e')
566
0
        return IsGeoJSONLikeObject(pszText + 1, poOpenInfo, "GeoJSONSeq");
567
568
0
    bool bMightBeSequence = false;
569
0
    bool bReadMoreBytes = false;
570
0
    if (!IsGeoJSONLikeObject(pszText, bMightBeSequence, bReadMoreBytes,
571
0
                             poOpenInfo, "GeoJSONSeq"))
572
0
    {
573
0
        return false;
574
0
    }
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
0
{
595
    // 6000 somewhat arbitrary. Based on other JSON-like drivers
596
0
    if (poOpenInfo->fpL == nullptr || !poOpenInfo->TryToIngest(6000))
597
0
    {
598
0
        return false;
599
0
    }
600
601
0
    const char *pszText =
602
0
        reinterpret_cast<const char *>(poOpenInfo->pabyHeader);
603
0
    return JSONFGIsObject(pszText, poOpenInfo);
604
0
}
605
606
bool JSONFGIsObject(const char *pszText, GDALOpenInfo *poOpenInfo)
607
0
{
608
0
    if (!IsJSONObject(pszText))
609
0
        return false;
610
611
0
    if (poOpenInfo->IsSingleAllowedDriver("JSONFG") &&
612
0
        GDALGetDriverByName("JSONFG"))
613
0
    {
614
0
        return true;
615
0
    }
616
617
0
    const std::string osWithoutSpace = GetCompactJSon(pszText, strlen(pszText));
618
619
    // In theory, conformsTo should be required, but let be lax...
620
0
    {
621
0
        const auto nPos = osWithoutSpace.find("\"conformsTo\":[");
622
0
        if (nPos != std::string::npos)
623
0
        {
624
0
            for (const char *pszVersion : {"0.1", "0.2", "0.3"})
625
0
            {
626
0
                if (osWithoutSpace.find(
627
0
                        CPLSPrintf("\"[ogc-json-fg-1-%s:core]\"", pszVersion),
628
0
                        nPos) != std::string::npos ||
629
0
                    osWithoutSpace.find(
630
0
                        CPLSPrintf(
631
0
                            "\"http://www.opengis.net/spec/json-fg-1/%s\"",
632
0
                            pszVersion),
633
0
                        nPos) != std::string::npos)
634
0
                {
635
0
                    return true;
636
0
                }
637
0
            }
638
0
        }
639
0
    }
640
641
0
    if (osWithoutSpace.find("\"place\":{\"type\":") != std::string::npos ||
642
0
        osWithoutSpace.find("\"place\":{\"coordinates\":") !=
643
0
            std::string::npos ||
644
0
        osWithoutSpace.find("\"time\":{\"date\":") != std::string::npos ||
645
0
        osWithoutSpace.find("\"time\":{\"timestamp\":") != std::string::npos ||
646
0
        osWithoutSpace.find("\"time\":{\"interval\":") != std::string::npos)
647
0
    {
648
0
        return true;
649
0
    }
650
651
0
    if (osWithoutSpace.find("\"coordRefSys\":") != std::string::npos ||
652
0
        osWithoutSpace.find("\"featureType\":") != std::string::npos)
653
0
    {
654
        // Check that coordRefSys and/or featureType are either at the
655
        // FeatureCollection or Feature level
656
0
        struct MyParser : public CPLJSonStreamingParser
657
0
        {
658
0
            bool m_bFoundJSONFGFeatureType = false;
659
0
            bool m_bFoundJSONFGCoordrefSys = false;
660
0
            std::string m_osLevel{};
661
662
0
            void StartObjectMember(const char *pszKey, size_t nLength) override
663
0
            {
664
0
                if (nLength == strlen("featureType") &&
665
0
                    strcmp(pszKey, "featureType") == 0)
666
0
                {
667
0
                    m_bFoundJSONFGFeatureType =
668
0
                        (m_osLevel == "{" ||   // At FeatureCollection level
669
0
                         m_osLevel == "{[{");  // At Feature level
670
0
                    if (m_bFoundJSONFGFeatureType)
671
0
                        StopParsing();
672
0
                }
673
0
                else if (nLength == strlen("coordRefSys") &&
674
0
                         strcmp(pszKey, "coordRefSys") == 0)
675
0
                {
676
0
                    m_bFoundJSONFGCoordrefSys =
677
0
                        (m_osLevel == "{" ||   // At FeatureCollection level
678
0
                         m_osLevel == "{[{");  // At Feature level
679
0
                    if (m_bFoundJSONFGCoordrefSys)
680
0
                        StopParsing();
681
0
                }
682
0
            }
683
684
0
            void StartObject() override
685
0
            {
686
0
                m_osLevel += '{';
687
0
            }
688
689
0
            void EndObject() override
690
0
            {
691
0
                if (!m_osLevel.empty())
692
0
                    m_osLevel.pop_back();
693
0
            }
694
695
0
            void StartArray() override
696
0
            {
697
0
                m_osLevel += '[';
698
0
            }
699
700
0
            void EndArray() override
701
0
            {
702
0
                if (!m_osLevel.empty())
703
0
                    m_osLevel.pop_back();
704
0
            }
705
0
        };
706
707
0
        MyParser oParser;
708
0
        oParser.Parse(pszText, strlen(pszText), true);
709
0
        if (oParser.m_bFoundJSONFGFeatureType ||
710
0
            oParser.m_bFoundJSONFGCoordrefSys)
711
0
        {
712
0
            return true;
713
0
        }
714
0
    }
715
716
0
    return false;
717
0
}
718
719
/************************************************************************/
720
/*                           IsLikelyESRIJSONURL()                      */
721
/************************************************************************/
722
723
static bool IsLikelyESRIJSONURL(const char *pszURL)
724
0
{
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
0
    return (strstr(pszURL, "f=json") != nullptr ||
728
0
            strstr(pszURL, "f=pjson") != nullptr ||
729
0
            strstr(pszURL, "resultRecordCount=") != nullptr) &&
730
0
           strstr(pszURL, "/items?") == nullptr;
731
0
}
732
733
/************************************************************************/
734
/*                           GeoJSONGetSourceType()                     */
735
/************************************************************************/
736
737
GeoJSONSourceType GeoJSONGetSourceType(GDALOpenInfo *poOpenInfo)
738
0
{
739
0
    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
0
    if (STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSON:http://") ||
745
0
        STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSON:https://") ||
746
0
        STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSON:ftp://"))
747
0
    {
748
0
        srcType = eGeoJSONSourceService;
749
0
    }
750
0
    else if (STARTS_WITH_CI(poOpenInfo->pszFilename, "http://") ||
751
0
             STARTS_WITH_CI(poOpenInfo->pszFilename, "https://") ||
752
0
             STARTS_WITH_CI(poOpenInfo->pszFilename, "ftp://"))
753
0
    {
754
0
        if (poOpenInfo->IsSingleAllowedDriver("GeoJSON"))
755
0
        {
756
0
            return eGeoJSONSourceService;
757
0
        }
758
0
        if ((strstr(poOpenInfo->pszFilename, "SERVICE=WFS") ||
759
0
             strstr(poOpenInfo->pszFilename, "service=WFS") ||
760
0
             strstr(poOpenInfo->pszFilename, "service=wfs")) &&
761
0
            !strstr(poOpenInfo->pszFilename, "json"))
762
0
        {
763
0
            return eGeoJSONSourceUnknown;
764
0
        }
765
0
        if (IsLikelyESRIJSONURL(poOpenInfo->pszFilename))
766
0
        {
767
0
            return eGeoJSONSourceUnknown;
768
0
        }
769
0
        srcType = eGeoJSONSourceService;
770
0
    }
771
0
    else if (STARTS_WITH_CI(poOpenInfo->pszFilename, "GeoJSON:"))
772
0
    {
773
0
        VSIStatBufL sStat;
774
0
        if (VSIStatL(poOpenInfo->pszFilename + strlen("GeoJSON:"), &sStat) == 0)
775
0
        {
776
0
            return eGeoJSONSourceFile;
777
0
        }
778
0
        const char *pszText = poOpenInfo->pszFilename + strlen("GeoJSON:");
779
0
        if (GeoJSONIsObject(pszText, poOpenInfo))
780
0
            return eGeoJSONSourceText;
781
0
        return eGeoJSONSourceUnknown;
782
0
    }
783
0
    else if (GeoJSONIsObject(poOpenInfo->pszFilename, poOpenInfo))
784
0
    {
785
0
        srcType = eGeoJSONSourceText;
786
0
    }
787
0
    else if (GeoJSONFileIsObject(poOpenInfo))
788
0
    {
789
0
        srcType = eGeoJSONSourceFile;
790
0
    }
791
792
0
    return srcType;
793
0
}
794
795
/************************************************************************/
796
/*                     ESRIJSONDriverGetSourceType()                    */
797
/************************************************************************/
798
799
GeoJSONSourceType ESRIJSONDriverGetSourceType(GDALOpenInfo *poOpenInfo)
800
0
{
801
0
    if (STARTS_WITH_CI(poOpenInfo->pszFilename, "ESRIJSON:http://") ||
802
0
        STARTS_WITH_CI(poOpenInfo->pszFilename, "ESRIJSON:https://") ||
803
0
        STARTS_WITH_CI(poOpenInfo->pszFilename, "ESRIJSON:ftp://"))
804
0
    {
805
0
        return eGeoJSONSourceService;
806
0
    }
807
0
    else if (STARTS_WITH(poOpenInfo->pszFilename, "http://") ||
808
0
             STARTS_WITH(poOpenInfo->pszFilename, "https://") ||
809
0
             STARTS_WITH(poOpenInfo->pszFilename, "ftp://"))
810
0
    {
811
0
        if (poOpenInfo->IsSingleAllowedDriver("ESRIJSON"))
812
0
        {
813
0
            return eGeoJSONSourceService;
814
0
        }
815
0
        if (IsLikelyESRIJSONURL(poOpenInfo->pszFilename))
816
0
        {
817
0
            return eGeoJSONSourceService;
818
0
        }
819
0
        return eGeoJSONSourceUnknown;
820
0
    }
821
822
0
    if (STARTS_WITH_CI(poOpenInfo->pszFilename, "ESRIJSON:"))
823
0
    {
824
0
        VSIStatBufL sStat;
825
0
        if (VSIStatL(poOpenInfo->pszFilename + strlen("ESRIJSON:"), &sStat) ==
826
0
            0)
827
0
        {
828
0
            return eGeoJSONSourceFile;
829
0
        }
830
0
        const char *pszText = poOpenInfo->pszFilename + strlen("ESRIJSON:");
831
0
        if (ESRIJSONIsObject(pszText, poOpenInfo))
832
0
            return eGeoJSONSourceText;
833
0
        return eGeoJSONSourceUnknown;
834
0
    }
835
836
0
    if (poOpenInfo->fpL == nullptr)
837
0
    {
838
0
        const char *pszText = poOpenInfo->pszFilename;
839
0
        if (ESRIJSONIsObject(pszText, poOpenInfo))
840
0
            return eGeoJSONSourceText;
841
0
        return eGeoJSONSourceUnknown;
842
0
    }
843
844
    // By default read first 6000 bytes.
845
    // 6000 was chosen as enough bytes to
846
    // enable all current tests to pass.
847
0
    if (!poOpenInfo->TryToIngest(6000))
848
0
    {
849
0
        return eGeoJSONSourceUnknown;
850
0
    }
851
852
0
    if (poOpenInfo->pabyHeader != nullptr &&
853
0
        ESRIJSONIsObject(reinterpret_cast<const char *>(poOpenInfo->pabyHeader),
854
0
                         poOpenInfo))
855
0
    {
856
0
        return eGeoJSONSourceFile;
857
0
    }
858
0
    return eGeoJSONSourceUnknown;
859
0
}
860
861
/************************************************************************/
862
/*                     TopoJSONDriverGetSourceType()                    */
863
/************************************************************************/
864
865
GeoJSONSourceType TopoJSONDriverGetSourceType(GDALOpenInfo *poOpenInfo)
866
0
{
867
0
    if (STARTS_WITH_CI(poOpenInfo->pszFilename, "TopoJSON:http://") ||
868
0
        STARTS_WITH_CI(poOpenInfo->pszFilename, "TopoJSON:https://") ||
869
0
        STARTS_WITH_CI(poOpenInfo->pszFilename, "TopoJSON:ftp://"))
870
0
    {
871
0
        return eGeoJSONSourceService;
872
0
    }
873
0
    else if (STARTS_WITH(poOpenInfo->pszFilename, "http://") ||
874
0
             STARTS_WITH(poOpenInfo->pszFilename, "https://") ||
875
0
             STARTS_WITH(poOpenInfo->pszFilename, "ftp://"))
876
0
    {
877
0
        if (poOpenInfo->IsSingleAllowedDriver("TOPOJSON"))
878
0
        {
879
0
            return eGeoJSONSourceService;
880
0
        }
881
0
        if (IsLikelyESRIJSONURL(poOpenInfo->pszFilename))
882
0
        {
883
0
            return eGeoJSONSourceUnknown;
884
0
        }
885
0
        return eGeoJSONSourceService;
886
0
    }
887
888
0
    if (STARTS_WITH_CI(poOpenInfo->pszFilename, "TopoJSON:"))
889
0
    {
890
0
        VSIStatBufL sStat;
891
0
        if (VSIStatL(poOpenInfo->pszFilename + strlen("TopoJSON:"), &sStat) ==
892
0
            0)
893
0
        {
894
0
            return eGeoJSONSourceFile;
895
0
        }
896
0
        const char *pszText = poOpenInfo->pszFilename + strlen("TopoJSON:");
897
0
        if (TopoJSONIsObject(pszText, poOpenInfo))
898
0
            return eGeoJSONSourceText;
899
0
        return eGeoJSONSourceUnknown;
900
0
    }
901
902
0
    if (poOpenInfo->fpL == nullptr)
903
0
    {
904
0
        const char *pszText = poOpenInfo->pszFilename;
905
0
        if (TopoJSONIsObject(pszText, poOpenInfo))
906
0
            return eGeoJSONSourceText;
907
0
        return eGeoJSONSourceUnknown;
908
0
    }
909
910
    // By default read first 6000 bytes.
911
    // 6000 was chosen as enough bytes to
912
    // enable all current tests to pass.
913
0
    if (!poOpenInfo->TryToIngest(6000))
914
0
    {
915
0
        return eGeoJSONSourceUnknown;
916
0
    }
917
918
0
    if (poOpenInfo->pabyHeader != nullptr &&
919
0
        TopoJSONIsObject(reinterpret_cast<const char *>(poOpenInfo->pabyHeader),
920
0
                         poOpenInfo))
921
0
    {
922
0
        return eGeoJSONSourceFile;
923
0
    }
924
0
    return eGeoJSONSourceUnknown;
925
0
}
926
927
/************************************************************************/
928
/*                          GeoJSONSeqGetSourceType()                   */
929
/************************************************************************/
930
931
GeoJSONSourceType GeoJSONSeqGetSourceType(GDALOpenInfo *poOpenInfo)
932
0
{
933
0
    GeoJSONSourceType srcType = eGeoJSONSourceUnknown;
934
935
0
    if (STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSONSeq:http://") ||
936
0
        STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSONSeq:https://") ||
937
0
        STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSONSeq:ftp://"))
938
0
    {
939
0
        srcType = eGeoJSONSourceService;
940
0
    }
941
0
    else if (STARTS_WITH_CI(poOpenInfo->pszFilename, "http://") ||
942
0
             STARTS_WITH_CI(poOpenInfo->pszFilename, "https://") ||
943
0
             STARTS_WITH_CI(poOpenInfo->pszFilename, "ftp://"))
944
0
    {
945
0
        if (poOpenInfo->IsSingleAllowedDriver("GeoJSONSeq"))
946
0
        {
947
0
            return eGeoJSONSourceService;
948
0
        }
949
0
        if (IsLikelyESRIJSONURL(poOpenInfo->pszFilename))
950
0
        {
951
0
            return eGeoJSONSourceUnknown;
952
0
        }
953
0
        srcType = eGeoJSONSourceService;
954
0
    }
955
0
    else if (STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSONSeq:"))
956
0
    {
957
0
        VSIStatBufL sStat;
958
0
        if (VSIStatL(poOpenInfo->pszFilename + strlen("GEOJSONSeq:"), &sStat) ==
959
0
            0)
960
0
        {
961
0
            return eGeoJSONSourceFile;
962
0
        }
963
0
        const char *pszText = poOpenInfo->pszFilename + strlen("GEOJSONSeq:");
964
0
        if (GeoJSONSeqIsObject(pszText, poOpenInfo))
965
0
            return eGeoJSONSourceText;
966
0
        return eGeoJSONSourceUnknown;
967
0
    }
968
0
    else if (GeoJSONSeqIsObject(poOpenInfo->pszFilename, poOpenInfo))
969
0
    {
970
0
        srcType = eGeoJSONSourceText;
971
0
    }
972
0
    else if (GeoJSONSeqFileIsObject(poOpenInfo))
973
0
    {
974
0
        srcType = eGeoJSONSourceFile;
975
0
    }
976
977
0
    return srcType;
978
0
}
979
980
/************************************************************************/
981
/*                      JSONFGDriverGetSourceType()                     */
982
/************************************************************************/
983
984
GeoJSONSourceType JSONFGDriverGetSourceType(GDALOpenInfo *poOpenInfo)
985
0
{
986
0
    GeoJSONSourceType srcType = eGeoJSONSourceUnknown;
987
988
0
    if (STARTS_WITH_CI(poOpenInfo->pszFilename, "JSONFG:http://") ||
989
0
        STARTS_WITH_CI(poOpenInfo->pszFilename, "JSONFG:https://") ||
990
0
        STARTS_WITH_CI(poOpenInfo->pszFilename, "JSONFG:ftp://"))
991
0
    {
992
0
        srcType = eGeoJSONSourceService;
993
0
    }
994
0
    else if (STARTS_WITH_CI(poOpenInfo->pszFilename, "http://") ||
995
0
             STARTS_WITH_CI(poOpenInfo->pszFilename, "https://") ||
996
0
             STARTS_WITH_CI(poOpenInfo->pszFilename, "ftp://"))
997
0
    {
998
0
        if (poOpenInfo->IsSingleAllowedDriver("JSONFG"))
999
0
        {
1000
0
            return eGeoJSONSourceService;
1001
0
        }
1002
0
        if (IsLikelyESRIJSONURL(poOpenInfo->pszFilename))
1003
0
        {
1004
0
            return eGeoJSONSourceUnknown;
1005
0
        }
1006
0
        srcType = eGeoJSONSourceService;
1007
0
    }
1008
0
    else if (STARTS_WITH_CI(poOpenInfo->pszFilename, "JSONFG:"))
1009
0
    {
1010
0
        VSIStatBufL sStat;
1011
0
        const size_t nJSONFGPrefixLen = strlen("JSONFG:");
1012
0
        if (VSIStatL(poOpenInfo->pszFilename + nJSONFGPrefixLen, &sStat) == 0)
1013
0
        {
1014
0
            return eGeoJSONSourceFile;
1015
0
        }
1016
0
        const char *pszText = poOpenInfo->pszFilename + nJSONFGPrefixLen;
1017
0
        if (JSONFGIsObject(pszText, poOpenInfo))
1018
0
            return eGeoJSONSourceText;
1019
0
        return eGeoJSONSourceUnknown;
1020
0
    }
1021
0
    else if (JSONFGIsObject(poOpenInfo->pszFilename, poOpenInfo))
1022
0
    {
1023
0
        srcType = eGeoJSONSourceText;
1024
0
    }
1025
0
    else if (JSONFGFileIsObject(poOpenInfo))
1026
0
    {
1027
0
        srcType = eGeoJSONSourceFile;
1028
0
    }
1029
1030
0
    return srcType;
1031
0
}
1032
1033
/************************************************************************/
1034
/*                        GeoJSONStringPropertyToFieldType()            */
1035
/************************************************************************/
1036
1037
OGRFieldType GeoJSONStringPropertyToFieldType(json_object *poObject,
1038
                                              int &nTZFlag)
1039
0
{
1040
0
    if (poObject == nullptr)
1041
0
    {
1042
0
        return OFTString;
1043
0
    }
1044
0
    const char *pszStr = json_object_get_string(poObject);
1045
1046
0
    nTZFlag = 0;
1047
0
    OGRField sWrkField;
1048
0
    CPLPushErrorHandler(CPLQuietErrorHandler);
1049
0
    const bool bSuccess = CPL_TO_BOOL(OGRParseDate(pszStr, &sWrkField, 0));
1050
0
    CPLPopErrorHandler();
1051
0
    CPLErrorReset();
1052
0
    if (bSuccess)
1053
0
    {
1054
0
        const bool bHasDate =
1055
0
            strchr(pszStr, '/') != nullptr || strchr(pszStr, '-') != nullptr;
1056
0
        const bool bHasTime = strchr(pszStr, ':') != nullptr;
1057
0
        nTZFlag = sWrkField.Date.TZFlag;
1058
0
        if (bHasDate && bHasTime)
1059
0
            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
0
    }
1066
0
    return OFTString;
1067
0
}
1068
1069
/************************************************************************/
1070
/*                   GeoJSONHTTPFetchWithContentTypeHeader()            */
1071
/************************************************************************/
1072
1073
CPLHTTPResult *GeoJSONHTTPFetchWithContentTypeHeader(const char *pszURL)
1074
0
{
1075
0
    std::string osHeaders;
1076
0
    const char *pszGDAL_HTTP_HEADERS =
1077
0
        CPLGetConfigOption("GDAL_HTTP_HEADERS", nullptr);
1078
0
    bool bFoundAcceptHeader = false;
1079
0
    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
0
    if (!bFoundAcceptHeader)
1115
0
    {
1116
0
        if (!osHeaders.empty())
1117
0
            osHeaders += "\r\n";
1118
0
        osHeaders += "Accept: text/plain, application/json";
1119
0
    }
1120
1121
0
    CPLStringList aosOptions;
1122
0
    aosOptions.SetNameValue("HEADERS", osHeaders.c_str());
1123
0
    CPLHTTPResult *pResult = CPLHTTPFetch(pszURL, aosOptions.List());
1124
1125
0
    if (nullptr == pResult || 0 == pResult->nDataLen ||
1126
0
        0 != CPLGetLastErrorNo())
1127
0
    {
1128
0
        CPLHTTPDestroyResult(pResult);
1129
0
        return nullptr;
1130
0
    }
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
}