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 : constexpr const char szESRIJSonFeaturesGeometryRings[] =
26 : "\"features\":[{\"geometry\":{\"rings\":[";
27 :
28 : // Cf https://github.com/OSGeo/gdal/issues/9996#issuecomment-2129845692
29 : constexpr const char szESRIJSonFeaturesAttributes[] =
30 : "\"features\":[{\"attributes\":{";
31 :
32 : /************************************************************************/
33 : /* SkipUTF8BOM() */
34 : /************************************************************************/
35 :
36 290997 : static void SkipUTF8BOM(const char *&pszText)
37 : {
38 : /* Skip UTF-8 BOM (#5630) */
39 290997 : const GByte *pabyData = reinterpret_cast<const GByte *>(pszText);
40 290997 : if (pabyData[0] == 0xEF && pabyData[1] == 0xBB && pabyData[2] == 0xBF)
41 9 : pszText += 3;
42 290997 : }
43 :
44 : /************************************************************************/
45 : /* IsJSONObject() */
46 : /************************************************************************/
47 :
48 288724 : static bool IsJSONObject(const char *pszText)
49 : {
50 288724 : if (nullptr == pszText)
51 0 : return false;
52 :
53 288724 : SkipUTF8BOM(pszText);
54 :
55 : /* -------------------------------------------------------------------- */
56 : /* This is a primitive test, but we need to perform it fast. */
57 : /* -------------------------------------------------------------------- */
58 290672 : while (*pszText != '\0' && isspace(static_cast<unsigned char>(*pszText)))
59 1948 : pszText++;
60 :
61 288724 : const char *const apszPrefix[] = {"loadGeoJSON(", "jsonp("};
62 866142 : for (size_t iP = 0; iP < sizeof(apszPrefix) / sizeof(apszPrefix[0]); iP++)
63 : {
64 577433 : if (strncmp(pszText, apszPrefix[iP], strlen(apszPrefix[iP])) == 0)
65 : {
66 15 : pszText += strlen(apszPrefix[iP]);
67 15 : break;
68 : }
69 : }
70 :
71 288724 : if (*pszText != '{')
72 283499 : return false;
73 :
74 5225 : return true;
75 : }
76 :
77 : /************************************************************************/
78 : /* GetTopLevelType() */
79 : /************************************************************************/
80 :
81 2382 : static std::string GetTopLevelType(const char *pszText)
82 : {
83 2382 : if (!strstr(pszText, "\"type\""))
84 109 : return std::string();
85 :
86 2273 : SkipUTF8BOM(pszText);
87 :
88 : struct MyParser : public CPLJSonStreamingParser
89 : {
90 : std::string m_osLevel{};
91 : bool m_bInTopLevelType = false;
92 : std::string m_osTopLevelTypeValue{};
93 :
94 3017 : void StartObjectMember(std::string_view sKey) override
95 : {
96 3017 : m_bInTopLevelType = false;
97 3017 : if (sKey == "type" && m_osLevel == "{")
98 : {
99 2230 : m_bInTopLevelType = true;
100 : }
101 3017 : }
102 :
103 2846 : void String(std::string_view sValue) override
104 : {
105 2846 : if (m_bInTopLevelType)
106 : {
107 2230 : m_osTopLevelTypeValue = sValue;
108 2230 : StopParsing();
109 : }
110 2846 : }
111 :
112 2452 : void StartObject() override
113 : {
114 2452 : m_osLevel += '{';
115 2452 : m_bInTopLevelType = false;
116 2452 : }
117 :
118 178 : void EndObject() override
119 : {
120 178 : if (!m_osLevel.empty())
121 178 : m_osLevel.pop_back();
122 178 : m_bInTopLevelType = false;
123 178 : }
124 :
125 195 : void StartArray() override
126 : {
127 195 : m_osLevel += '[';
128 195 : m_bInTopLevelType = false;
129 195 : }
130 :
131 145 : void EndArray() override
132 : {
133 145 : if (!m_osLevel.empty())
134 145 : m_osLevel.pop_back();
135 145 : m_bInTopLevelType = false;
136 145 : }
137 : };
138 :
139 4546 : MyParser oParser;
140 2273 : oParser.Parse(std::string_view(pszText), true);
141 2273 : return oParser.m_osTopLevelTypeValue;
142 : }
143 :
144 : /************************************************************************/
145 : /* GetCompactJSon() */
146 : /************************************************************************/
147 :
148 3353 : static CPLString GetCompactJSon(const char *pszText, size_t nMaxSize)
149 : {
150 : /* Skip UTF-8 BOM (#5630) */
151 3353 : const GByte *pabyData = reinterpret_cast<const GByte *>(pszText);
152 3353 : if (pabyData[0] == 0xEF && pabyData[1] == 0xBB && pabyData[2] == 0xBF)
153 6 : pszText += 3;
154 :
155 3353 : CPLString osWithoutSpace;
156 3353 : bool bInString = false;
157 3168140 : for (int i = 0; pszText[i] != '\0' && osWithoutSpace.size() < nMaxSize; i++)
158 : {
159 3164780 : if (bInString)
160 : {
161 1002880 : if (pszText[i] == '\\')
162 : {
163 1996 : osWithoutSpace += pszText[i];
164 1996 : if (pszText[i + 1] == '\0')
165 0 : break;
166 1996 : osWithoutSpace += pszText[i + 1];
167 1996 : i++;
168 : }
169 1000890 : else if (pszText[i] == '"')
170 : {
171 70053 : bInString = false;
172 70053 : osWithoutSpace += '"';
173 : }
174 : else
175 : {
176 930834 : osWithoutSpace += pszText[i];
177 : }
178 : }
179 2161900 : else if (pszText[i] == '"')
180 : {
181 70070 : bInString = true;
182 70070 : osWithoutSpace += '"';
183 : }
184 2091830 : else if (!isspace(static_cast<unsigned char>(pszText[i])))
185 : {
186 999942 : osWithoutSpace += pszText[i];
187 : }
188 : }
189 3353 : return osWithoutSpace;
190 : }
191 :
192 : /************************************************************************/
193 : /* IsGeoJSONLikeObject() */
194 : /************************************************************************/
195 :
196 122679 : static bool IsGeoJSONLikeObject(const char *pszText, bool &bMightBeSequence,
197 : bool &bReadMoreBytes, GDALOpenInfo *poOpenInfo,
198 : const char *pszExpectedDriverName)
199 : {
200 122679 : bMightBeSequence = false;
201 122679 : bReadMoreBytes = false;
202 :
203 122679 : if (!IsJSONObject(pszText))
204 120532 : return false;
205 :
206 4294 : const std::string osTopLevelType = GetTopLevelType(pszText);
207 2147 : if (osTopLevelType == "Topology")
208 8 : return false;
209 :
210 2178 : if (poOpenInfo->IsSingleAllowedDriver(pszExpectedDriverName) &&
211 39 : GDALGetDriverByName(pszExpectedDriverName))
212 : {
213 39 : return true;
214 : }
215 :
216 4202 : if ((!poOpenInfo->papszAllowedDrivers ||
217 2 : CSLFindString(poOpenInfo->papszAllowedDrivers, "JSONFG") >= 0) &&
218 2102 : GDALGetDriverByName("JSONFG") && JSONFGIsObject(pszText, poOpenInfo))
219 : {
220 406 : return false;
221 : }
222 :
223 1694 : if (osTopLevelType == "FeatureCollection")
224 : {
225 1116 : return true;
226 : }
227 :
228 1156 : const std::string osWithoutSpace = GetCompactJSon(pszText, strlen(pszText));
229 583 : if (osWithoutSpace.find("{\"features\":[") == 0 &&
230 5 : osWithoutSpace.find(szESRIJSonFeaturesGeometryRings) ==
231 583 : std::string::npos &&
232 3 : osWithoutSpace.find(szESRIJSonFeaturesAttributes) == std::string::npos)
233 : {
234 3 : return true;
235 : }
236 :
237 : // See
238 : // https://raw.githubusercontent.com/icepack/icepack-data/master/meshes/larsen/larsen_inflow.geojson
239 : // "{"crs":...,"features":[..."
240 : // or
241 : // https://gist.githubusercontent.com/NiklasDallmann/27e339dd78d1942d524fbcd179f9fdcf/raw/527a8319d75a9e29446a32a19e4c902213b0d668/42XR9nLAh8Poh9Xmniqh3Bs9iisNm74mYMC56v3Wfyo=_isochrones_fails.geojson
242 : // "{"bbox":...,"features":[..."
243 575 : if (osWithoutSpace.find(",\"features\":[") != std::string::npos)
244 : {
245 41 : return !ESRIJSONIsObject(pszText, poOpenInfo);
246 : }
247 :
248 : // See https://github.com/OSGeo/gdal/issues/2720
249 1065 : if (osWithoutSpace.find("{\"coordinates\":[") == 0 ||
250 : // and https://github.com/OSGeo/gdal/issues/2787
251 1059 : osWithoutSpace.find("{\"geometry\":{\"coordinates\":[") == 0 ||
252 : // and https://github.com/qgis/QGIS/issues/61266
253 528 : osWithoutSpace.find(
254 522 : "{\"geometry\":{\"type\":\"Point\",\"coordinates\":[") == 0 ||
255 522 : osWithoutSpace.find(
256 519 : "{\"geometry\":{\"type\":\"LineString\",\"coordinates\":[") == 0 ||
257 519 : osWithoutSpace.find(
258 516 : "{\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[") == 0 ||
259 516 : osWithoutSpace.find(
260 513 : "{\"geometry\":{\"type\":\"MultiPoint\",\"coordinates\":[") == 0 ||
261 513 : osWithoutSpace.find(
262 : "{\"geometry\":{\"type\":\"MultiLineString\",\"coordinates\":[") ==
263 510 : 0 ||
264 510 : osWithoutSpace.find(
265 : "{\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[") ==
266 1065 : 0 ||
267 507 : osWithoutSpace.find("{\"geometry\":{\"type\":\"GeometryCollection\","
268 : "\"geometries\":[") == 0)
269 : {
270 30 : return true;
271 : }
272 :
273 1140 : if (osTopLevelType == "Feature" || osTopLevelType == "Point" ||
274 825 : osTopLevelType == "LineString" || osTopLevelType == "Polygon" ||
275 465 : osTopLevelType == "MultiPoint" || osTopLevelType == "MultiLineString" ||
276 939 : osTopLevelType == "MultiPolygon" ||
277 60 : osTopLevelType == "GeometryCollection")
278 : {
279 450 : bMightBeSequence = true;
280 450 : return true;
281 : }
282 :
283 : // See https://github.com/OSGeo/gdal/issues/3280
284 54 : if (osWithoutSpace.find("{\"properties\":{") == 0)
285 : {
286 2 : bMightBeSequence = true;
287 2 : bReadMoreBytes = true;
288 2 : return false;
289 : }
290 :
291 52 : return false;
292 : }
293 :
294 26 : static bool IsGeoJSONLikeObject(const char *pszText, GDALOpenInfo *poOpenInfo,
295 : const char *pszExpectedDriverName)
296 : {
297 : bool bMightBeSequence;
298 : bool bReadMoreBytes;
299 26 : return IsGeoJSONLikeObject(pszText, bMightBeSequence, bReadMoreBytes,
300 52 : poOpenInfo, pszExpectedDriverName);
301 : }
302 :
303 : /************************************************************************/
304 : /* ESRIJSONIsObject() */
305 : /************************************************************************/
306 :
307 55664 : bool ESRIJSONIsObject(const char *pszText, GDALOpenInfo *poOpenInfo)
308 : {
309 55664 : if (!IsJSONObject(pszText))
310 55356 : return false;
311 :
312 310 : if (poOpenInfo->IsSingleAllowedDriver("ESRIJSON") &&
313 2 : GDALGetDriverByName("ESRIJSON"))
314 : {
315 2 : return true;
316 : }
317 :
318 306 : if ( // ESRI Json geometry
319 306 : (strstr(pszText, "\"geometryType\"") != nullptr &&
320 62 : strstr(pszText, "\"esriGeometry") != nullptr)
321 :
322 : // ESRI Json "FeatureCollection"
323 244 : || strstr(pszText, "\"fieldAliases\"") != nullptr
324 :
325 : // ESRI Json "FeatureCollection"
326 244 : || (strstr(pszText, "\"fields\"") != nullptr &&
327 0 : strstr(pszText, "\"esriFieldType") != nullptr))
328 : {
329 62 : return true;
330 : }
331 :
332 488 : const std::string osWithoutSpace = GetCompactJSon(pszText, strlen(pszText));
333 244 : if (osWithoutSpace.find(szESRIJSonFeaturesGeometryRings) !=
334 244 : std::string::npos ||
335 244 : osWithoutSpace.find(szESRIJSonFeaturesAttributes) !=
336 488 : std::string::npos ||
337 240 : osWithoutSpace.find("\"spatialReference\":{\"wkid\":") !=
338 : std::string::npos)
339 : {
340 4 : return true;
341 : }
342 :
343 240 : return false;
344 : }
345 :
346 : /************************************************************************/
347 : /* TopoJSONIsObject() */
348 : /************************************************************************/
349 :
350 55593 : bool TopoJSONIsObject(const char *pszText, GDALOpenInfo *poOpenInfo)
351 : {
352 55593 : if (!IsJSONObject(pszText))
353 55356 : return false;
354 :
355 239 : if (poOpenInfo->IsSingleAllowedDriver("TopoJSON") &&
356 2 : GDALGetDriverByName("TopoJSON"))
357 : {
358 2 : return true;
359 : }
360 :
361 235 : return GetTopLevelType(pszText) == "Topology";
362 : }
363 :
364 : /************************************************************************/
365 : /* IsLikelyNewlineSequenceGeoJSON() */
366 : /************************************************************************/
367 :
368 : static GDALIdentifyEnum
369 424 : IsLikelyNewlineSequenceGeoJSON(VSILFILE *fpL, const GByte *pabyHeader,
370 : const char *pszFileContent)
371 : {
372 424 : const size_t nBufferSize = 4096 * 10;
373 848 : std::vector<GByte> abyBuffer;
374 424 : abyBuffer.resize(nBufferSize + 1);
375 :
376 424 : int nCurlLevel = 0;
377 424 : bool bInString = false;
378 424 : bool bLastIsEscape = false;
379 424 : bool bFirstIter = true;
380 424 : bool bEOLFound = false;
381 424 : int nCountObject = 0;
382 : while (true)
383 : {
384 : size_t nRead;
385 556 : bool bEnd = false;
386 556 : if (bFirstIter)
387 : {
388 424 : const char *pszText =
389 424 : pszFileContent ? pszFileContent
390 : : reinterpret_cast<const char *>(pabyHeader);
391 424 : assert(pszText);
392 424 : nRead = std::min(strlen(pszText), nBufferSize);
393 424 : memcpy(abyBuffer.data(), pszText, nRead);
394 424 : bFirstIter = false;
395 424 : if (fpL)
396 : {
397 143 : VSIFSeekL(fpL, nRead, SEEK_SET);
398 : }
399 : }
400 : else
401 : {
402 132 : nRead = VSIFReadL(abyBuffer.data(), 1, nBufferSize, fpL);
403 132 : bEnd = nRead < nBufferSize;
404 : }
405 3256870 : for (size_t i = 0; i < nRead; i++)
406 : {
407 3256410 : if (nCurlLevel == 0)
408 : {
409 696 : if (abyBuffer[i] == '{')
410 : {
411 517 : nCountObject++;
412 517 : if (nCountObject == 2)
413 : {
414 93 : break;
415 : }
416 424 : nCurlLevel++;
417 : }
418 179 : else if (nCountObject == 1 && abyBuffer[i] == '\n')
419 : {
420 128 : bEOLFound = true;
421 : }
422 51 : else if (!isspace(static_cast<unsigned char>(abyBuffer[i])))
423 : {
424 3 : return GDAL_IDENTIFY_FALSE;
425 : }
426 : }
427 3255720 : else if (bInString)
428 : {
429 94065 : if (bLastIsEscape)
430 : {
431 798 : bLastIsEscape = false;
432 : }
433 93267 : else if (abyBuffer[i] == '\\')
434 : {
435 798 : bLastIsEscape = true;
436 : }
437 92469 : else if (abyBuffer[i] == '"')
438 : {
439 3080 : bInString = false;
440 : }
441 : }
442 3161650 : else if (abyBuffer[i] == '"')
443 : {
444 3080 : bInString = true;
445 : }
446 3158570 : else if (abyBuffer[i] == '{')
447 : {
448 307 : nCurlLevel++;
449 : }
450 3158260 : else if (abyBuffer[i] == '}')
451 : {
452 731 : nCurlLevel--;
453 : }
454 : }
455 553 : if (!fpL || bEnd || nCountObject == 2)
456 : break;
457 132 : }
458 421 : if (bEOLFound && nCountObject == 2)
459 93 : return GDAL_IDENTIFY_TRUE;
460 328 : return GDAL_IDENTIFY_UNKNOWN;
461 : }
462 :
463 : /************************************************************************/
464 : /* GeoJSONFileIsObject() */
465 : /************************************************************************/
466 :
467 56301 : static bool GeoJSONFileIsObject(GDALOpenInfo *poOpenInfo)
468 : {
469 : // By default read first 6000 bytes.
470 : // 6000 was chosen as enough bytes to
471 : // enable all current tests to pass.
472 :
473 56301 : if (poOpenInfo->fpL == nullptr || !poOpenInfo->TryToIngest(6000))
474 : {
475 51096 : return false;
476 : }
477 :
478 5205 : bool bMightBeSequence = false;
479 5205 : bool bReadMoreBytes = false;
480 5205 : if (!IsGeoJSONLikeObject(
481 5205 : reinterpret_cast<const char *>(poOpenInfo->pabyHeader),
482 : bMightBeSequence, bReadMoreBytes, poOpenInfo, "GeoJSON"))
483 : {
484 4534 : if (!(bReadMoreBytes && poOpenInfo->nHeaderBytes >= 6000 &&
485 2 : poOpenInfo->TryToIngest(1000 * 1000) &&
486 2 : !IsGeoJSONLikeObject(
487 2 : reinterpret_cast<const char *>(poOpenInfo->pabyHeader),
488 : bMightBeSequence, bReadMoreBytes, poOpenInfo, "GeoJSON")))
489 : {
490 4532 : return false;
491 : }
492 : }
493 :
494 758 : return !(bMightBeSequence && IsLikelyNewlineSequenceGeoJSON(
495 85 : poOpenInfo->fpL, poOpenInfo->pabyHeader,
496 673 : nullptr) == GDAL_IDENTIFY_TRUE);
497 : }
498 :
499 : /************************************************************************/
500 : /* GeoJSONIsObject() */
501 : /************************************************************************/
502 :
503 57183 : bool GeoJSONIsObject(const char *pszText, GDALOpenInfo *poOpenInfo)
504 : {
505 57183 : bool bMightBeSequence = false;
506 57183 : bool bReadMoreBytes = false;
507 57183 : if (!IsGeoJSONLikeObject(pszText, bMightBeSequence, bReadMoreBytes,
508 : poOpenInfo, "GeoJSON"))
509 : {
510 56301 : return false;
511 : }
512 :
513 1157 : return !(bMightBeSequence &&
514 275 : IsLikelyNewlineSequenceGeoJSON(nullptr, nullptr, pszText) ==
515 882 : GDAL_IDENTIFY_TRUE);
516 : }
517 :
518 : /************************************************************************/
519 : /* GeoJSONSeqFileIsObject() */
520 : /************************************************************************/
521 :
522 55686 : static bool GeoJSONSeqFileIsObject(GDALOpenInfo *poOpenInfo)
523 : {
524 : // By default read first 6000 bytes.
525 : // 6000 was chosen as enough bytes to
526 : // enable all current tests to pass.
527 :
528 55686 : if (poOpenInfo->fpL == nullptr || !poOpenInfo->TryToIngest(6000))
529 : {
530 51092 : return false;
531 : }
532 :
533 4594 : const char *pszText =
534 : reinterpret_cast<const char *>(poOpenInfo->pabyHeader);
535 4594 : if (pszText[0] == '\x1e')
536 26 : return IsGeoJSONLikeObject(pszText + 1, poOpenInfo, "GeoJSONSeq");
537 :
538 4568 : bool bMightBeSequence = false;
539 4568 : bool bReadMoreBytes = false;
540 4568 : if (!IsGeoJSONLikeObject(pszText, bMightBeSequence, bReadMoreBytes,
541 : poOpenInfo, "GeoJSONSeq"))
542 : {
543 4510 : if (!(bReadMoreBytes && poOpenInfo->nHeaderBytes >= 6000 &&
544 0 : poOpenInfo->TryToIngest(1000 * 1000) &&
545 0 : IsGeoJSONLikeObject(
546 0 : reinterpret_cast<const char *>(poOpenInfo->pabyHeader),
547 : bMightBeSequence, bReadMoreBytes, poOpenInfo, "GeoJSONSeq")))
548 : {
549 4510 : return false;
550 : }
551 : }
552 :
553 58 : if (poOpenInfo->IsSingleAllowedDriver("GeoJSONSeq") &&
554 2 : IsLikelyNewlineSequenceGeoJSON(poOpenInfo->fpL, poOpenInfo->pabyHeader,
555 60 : nullptr) != GDAL_IDENTIFY_FALSE &&
556 2 : GDALGetDriverByName("GeoJSONSeq"))
557 : {
558 2 : return true;
559 : }
560 :
561 112 : return bMightBeSequence && IsLikelyNewlineSequenceGeoJSON(
562 56 : poOpenInfo->fpL, poOpenInfo->pabyHeader,
563 56 : nullptr) == GDAL_IDENTIFY_TRUE;
564 : }
565 :
566 55695 : bool GeoJSONSeqIsObject(const char *pszText, GDALOpenInfo *poOpenInfo)
567 : {
568 55695 : if (pszText[0] == '\x1e')
569 0 : return IsGeoJSONLikeObject(pszText + 1, poOpenInfo, "GeoJSONSeq");
570 :
571 55695 : bool bMightBeSequence = false;
572 55695 : bool bReadMoreBytes = false;
573 55695 : if (!IsGeoJSONLikeObject(pszText, bMightBeSequence, bReadMoreBytes,
574 : poOpenInfo, "GeoJSONSeq"))
575 : {
576 55689 : return false;
577 : }
578 :
579 6 : if (poOpenInfo->IsSingleAllowedDriver("GeoJSONSeq") &&
580 0 : IsLikelyNewlineSequenceGeoJSON(nullptr, nullptr, pszText) !=
581 6 : GDAL_IDENTIFY_FALSE &&
582 0 : GDALGetDriverByName("GeoJSONSeq"))
583 : {
584 0 : return true;
585 : }
586 :
587 12 : return bMightBeSequence &&
588 6 : IsLikelyNewlineSequenceGeoJSON(nullptr, nullptr, pszText) ==
589 6 : GDAL_IDENTIFY_TRUE;
590 : }
591 :
592 : /************************************************************************/
593 : /* JSONFGFileIsObject() */
594 : /************************************************************************/
595 :
596 51026 : static bool JSONFGFileIsObject(GDALOpenInfo *poOpenInfo)
597 : {
598 : // 6000 somewhat arbitrary. Based on other JSON-like drivers
599 51026 : if (poOpenInfo->fpL == nullptr || !poOpenInfo->TryToIngest(6000))
600 : {
601 49470 : return false;
602 : }
603 :
604 1556 : const char *pszText =
605 : reinterpret_cast<const char *>(poOpenInfo->pabyHeader);
606 1556 : return JSONFGIsObject(pszText, poOpenInfo);
607 : }
608 :
609 54788 : bool JSONFGIsObject(const char *pszText, GDALOpenInfo *poOpenInfo)
610 : {
611 54788 : if (!IsJSONObject(pszText))
612 52255 : return false;
613 :
614 2535 : if (poOpenInfo->IsSingleAllowedDriver("JSONFG") &&
615 2 : GDALGetDriverByName("JSONFG"))
616 : {
617 2 : return true;
618 : }
619 :
620 5062 : const std::string osWithoutSpace = GetCompactJSon(pszText, strlen(pszText));
621 :
622 : // In theory, conformsTo should be required, but let be lax...
623 2531 : if (osWithoutSpace.find("conformsTo") != std::string::npos)
624 : {
625 1300 : if (osWithoutSpace.find("\"[ogc-json-fg-1-") != std::string::npos ||
626 492 : osWithoutSpace.find("\"http://www.opengis.net/spec/json-fg-1/") !=
627 1300 : std::string::npos ||
628 0 : osWithoutSpace.find(
629 : "\"http:\\/\\/www.opengis.net\\/spec\\/json-fg-1\\/") !=
630 : std::string::npos)
631 : {
632 808 : return true;
633 : }
634 : }
635 :
636 3446 : if (osWithoutSpace.find("\"place\":{\"type\":") != std::string::npos ||
637 1723 : osWithoutSpace.find("\"place\":{\"coordinates\":") !=
638 1723 : std::string::npos ||
639 3446 : osWithoutSpace.find("\"time\":{\"date\":") != std::string::npos ||
640 3446 : osWithoutSpace.find("\"time\":{\"timestamp\":") != std::string::npos ||
641 3446 : osWithoutSpace.find("\"time\":{\"interval\":") != std::string::npos ||
642 1723 : osWithoutSpace.find("\"type\":\"CircularString\"") !=
643 1723 : std::string::npos ||
644 1723 : osWithoutSpace.find("\"type\":\"CompoundCurve\"") !=
645 1723 : std::string::npos ||
646 3446 : osWithoutSpace.find("\"type\":\"CurvePolygon\"") != std::string::npos ||
647 5169 : osWithoutSpace.find("\"type\":\"MultiCurve\"") != std::string::npos ||
648 1723 : osWithoutSpace.find("\"type\":\"MultiSurface\"") != std::string::npos)
649 : {
650 0 : return true;
651 : }
652 :
653 3442 : if (osWithoutSpace.find("\"coordRefSys\":") != std::string::npos ||
654 1719 : osWithoutSpace.find("\"featureType\":") != std::string::npos)
655 : {
656 : // Check that coordRefSys and/or featureType are either at the
657 : // FeatureCollection or Feature level
658 : struct MyParser : public CPLJSonStreamingParser
659 : {
660 : bool m_bFoundJSONFGFeatureType = false;
661 : bool m_bFoundJSONFGCoordrefSys = false;
662 : std::string m_osLevel{};
663 :
664 92 : void StartObjectMember(std::string_view sKey) override
665 : {
666 92 : if (sKey == "featureType")
667 : {
668 6 : m_bFoundJSONFGFeatureType =
669 12 : (m_osLevel == "{" || // At FeatureCollection level
670 6 : m_osLevel == "{[{"); // At Feature level
671 6 : if (m_bFoundJSONFGFeatureType)
672 0 : StopParsing();
673 : }
674 86 : else if (sKey == "coordRefSys")
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(std::string_view(pszText), true);
709 10 : if (oParser.m_bFoundJSONFGFeatureType ||
710 10 : oParser.m_bFoundJSONFGCoordrefSys)
711 : {
712 4 : return true;
713 : }
714 : }
715 :
716 1719 : 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 56700 : GeoJSONSourceType GeoJSONGetSourceType(GDALOpenInfo *poOpenInfo)
738 : {
739 56700 : 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 56700 : if (STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSON:http://") ||
745 56700 : STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSON:https://") ||
746 56700 : STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSON:ftp://"))
747 : {
748 0 : srcType = eGeoJSONSourceService;
749 : }
750 56700 : else if (STARTS_WITH_CI(poOpenInfo->pszFilename, "http://") ||
751 56665 : STARTS_WITH_CI(poOpenInfo->pszFilename, "https://") ||
752 56663 : 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 56663 : 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 56663 : else if (GeoJSONIsObject(poOpenInfo->pszFilename, poOpenInfo))
784 : {
785 362 : srcType = eGeoJSONSourceText;
786 : }
787 56301 : else if (GeoJSONFileIsObject(poOpenInfo))
788 : {
789 645 : srcType = eGeoJSONSourceFile;
790 : }
791 :
792 56699 : return srcType;
793 : }
794 :
795 : /************************************************************************/
796 : /* ESRIJSONDriverGetSourceType() */
797 : /************************************************************************/
798 :
799 55640 : GeoJSONSourceType ESRIJSONDriverGetSourceType(GDALOpenInfo *poOpenInfo)
800 : {
801 55640 : if (STARTS_WITH_CI(poOpenInfo->pszFilename, "ESRIJSON:http://") ||
802 55640 : STARTS_WITH_CI(poOpenInfo->pszFilename, "ESRIJSON:https://") ||
803 55640 : STARTS_WITH_CI(poOpenInfo->pszFilename, "ESRIJSON:ftp://"))
804 : {
805 0 : return eGeoJSONSourceService;
806 : }
807 55640 : else if (STARTS_WITH(poOpenInfo->pszFilename, "http://") ||
808 55623 : STARTS_WITH(poOpenInfo->pszFilename, "https://") ||
809 55622 : 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 55622 : 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 55620 : if (poOpenInfo->fpL == nullptr)
837 : {
838 51093 : const char *pszText = poOpenInfo->pszFilename;
839 51093 : if (ESRIJSONIsObject(pszText, poOpenInfo))
840 4 : return eGeoJSONSourceText;
841 51089 : 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 4527 : if (!poOpenInfo->TryToIngest(6000))
848 : {
849 0 : return eGeoJSONSourceUnknown;
850 : }
851 :
852 9054 : if (poOpenInfo->pabyHeader != nullptr &&
853 4527 : ESRIJSONIsObject(reinterpret_cast<const char *>(poOpenInfo->pabyHeader),
854 : poOpenInfo))
855 : {
856 32 : return eGeoJSONSourceFile;
857 : }
858 4495 : return eGeoJSONSourceUnknown;
859 : }
860 :
861 : /************************************************************************/
862 : /* TopoJSONDriverGetSourceType() */
863 : /************************************************************************/
864 :
865 55617 : GeoJSONSourceType TopoJSONDriverGetSourceType(GDALOpenInfo *poOpenInfo)
866 : {
867 55617 : if (STARTS_WITH_CI(poOpenInfo->pszFilename, "TopoJSON:http://") ||
868 55617 : STARTS_WITH_CI(poOpenInfo->pszFilename, "TopoJSON:https://") ||
869 55617 : STARTS_WITH_CI(poOpenInfo->pszFilename, "TopoJSON:ftp://"))
870 : {
871 0 : return eGeoJSONSourceService;
872 : }
873 55617 : else if (STARTS_WITH(poOpenInfo->pszFilename, "http://") ||
874 55592 : STARTS_WITH(poOpenInfo->pszFilename, "https://") ||
875 55590 : 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 55590 : 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 55590 : if (poOpenInfo->fpL == nullptr)
903 : {
904 51089 : const char *pszText = poOpenInfo->pszFilename;
905 51089 : if (TopoJSONIsObject(pszText, poOpenInfo))
906 0 : return eGeoJSONSourceText;
907 51089 : 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 4501 : if (!poOpenInfo->TryToIngest(6000))
914 : {
915 0 : return eGeoJSONSourceUnknown;
916 : }
917 :
918 9002 : if (poOpenInfo->pabyHeader != nullptr &&
919 4501 : TopoJSONIsObject(reinterpret_cast<const char *>(poOpenInfo->pabyHeader),
920 : poOpenInfo))
921 : {
922 10 : return eGeoJSONSourceFile;
923 : }
924 4491 : return eGeoJSONSourceUnknown;
925 : }
926 :
927 : /************************************************************************/
928 : /* GeoJSONSeqGetSourceType() */
929 : /************************************************************************/
930 :
931 55721 : GeoJSONSourceType GeoJSONSeqGetSourceType(GDALOpenInfo *poOpenInfo)
932 : {
933 55721 : GeoJSONSourceType srcType = eGeoJSONSourceUnknown;
934 :
935 55721 : if (STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSONSeq:http://") ||
936 55721 : STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSONSeq:https://") ||
937 55721 : STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSONSeq:ftp://"))
938 : {
939 0 : srcType = eGeoJSONSourceService;
940 : }
941 55721 : else if (STARTS_WITH_CI(poOpenInfo->pszFilename, "http://") ||
942 55696 : STARTS_WITH_CI(poOpenInfo->pszFilename, "https://") ||
943 55694 : 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 55694 : 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 55692 : else if (GeoJSONSeqIsObject(poOpenInfo->pszFilename, poOpenInfo))
969 : {
970 6 : srcType = eGeoJSONSourceText;
971 : }
972 55686 : else if (GeoJSONSeqFileIsObject(poOpenInfo))
973 : {
974 84 : srcType = eGeoJSONSourceFile;
975 : }
976 :
977 55718 : return srcType;
978 : }
979 :
980 : /************************************************************************/
981 : /* JSONFGDriverGetSourceType() */
982 : /************************************************************************/
983 :
984 51147 : GeoJSONSourceType JSONFGDriverGetSourceType(GDALOpenInfo *poOpenInfo)
985 : {
986 51147 : GeoJSONSourceType srcType = eGeoJSONSourceUnknown;
987 :
988 51147 : if (STARTS_WITH_CI(poOpenInfo->pszFilename, "JSONFG:http://") ||
989 51147 : STARTS_WITH_CI(poOpenInfo->pszFilename, "JSONFG:https://") ||
990 51147 : STARTS_WITH_CI(poOpenInfo->pszFilename, "JSONFG:ftp://"))
991 : {
992 0 : srcType = eGeoJSONSourceService;
993 : }
994 51147 : else if (STARTS_WITH_CI(poOpenInfo->pszFilename, "http://") ||
995 51122 : STARTS_WITH_CI(poOpenInfo->pszFilename, "https://") ||
996 51120 : 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 51120 : 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 51120 : else if (JSONFGIsObject(poOpenInfo->pszFilename, poOpenInfo))
1022 : {
1023 94 : srcType = eGeoJSONSourceText;
1024 : }
1025 51026 : else if (JSONFGFileIsObject(poOpenInfo))
1026 : {
1027 314 : srcType = eGeoJSONSourceFile;
1028 : }
1029 :
1030 51146 : return srcType;
1031 : }
1032 :
1033 : /************************************************************************/
1034 : /* GeoJSONStringPropertyToFieldType() */
1035 : /************************************************************************/
1036 :
1037 846 : OGRFieldType GeoJSONStringPropertyToFieldType(json_object *poObject,
1038 : int &nTZFlag)
1039 : {
1040 846 : if (poObject == nullptr)
1041 : {
1042 14 : return OFTString;
1043 : }
1044 832 : const char *pszStr = json_object_get_string(poObject);
1045 :
1046 832 : nTZFlag = 0;
1047 : OGRField sWrkField;
1048 832 : CPLPushErrorHandler(CPLQuietErrorHandler);
1049 832 : const bool bSuccess = CPL_TO_BOOL(OGRParseDate(pszStr, &sWrkField, 0));
1050 832 : CPLPopErrorHandler();
1051 832 : CPLErrorReset();
1052 832 : 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 582 : 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 : }
|