LCOV - code coverage report
Current view: top level - ogr/ogrsf_frmts/geojson - ogrgeojsonutils.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 438 493 88.8 %
Date: 2024-11-21 22:18:42 Functions: 33 33 100.0 %

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

Generated by: LCOV version 1.14