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