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 290450 : static void SkipUTF8BOM(const char *&pszText)
37 : {
38 : /* Skip UTF-8 BOM (#5630) */
39 290450 : const GByte *pabyData = reinterpret_cast<const GByte *>(pszText);
40 290450 : if (pabyData[0] == 0xEF && pabyData[1] == 0xBB && pabyData[2] == 0xBF)
41 9 : pszText += 3;
42 290450 : }
43 :
44 : /************************************************************************/
45 : /* IsJSONObject() */
46 : /************************************************************************/
47 :
48 288183 : static bool IsJSONObject(const char *pszText)
49 : {
50 288183 : if (nullptr == pszText)
51 0 : return false;
52 :
53 288183 : SkipUTF8BOM(pszText);
54 :
55 : /* -------------------------------------------------------------------- */
56 : /* This is a primitive test, but we need to perform it fast. */
57 : /* -------------------------------------------------------------------- */
58 290107 : while (*pszText != '\0' && isspace(static_cast<unsigned char>(*pszText)))
59 1924 : pszText++;
60 :
61 288183 : const char *const apszPrefix[] = {"loadGeoJSON(", "jsonp("};
62 864519 : for (size_t iP = 0; iP < sizeof(apszPrefix) / sizeof(apszPrefix[0]); iP++)
63 : {
64 576351 : if (strncmp(pszText, apszPrefix[iP], strlen(apszPrefix[iP])) == 0)
65 : {
66 15 : pszText += strlen(apszPrefix[iP]);
67 15 : break;
68 : }
69 : }
70 :
71 288183 : if (*pszText != '{')
72 282972 : return false;
73 :
74 5211 : return true;
75 : }
76 :
77 : /************************************************************************/
78 : /* GetTopLevelType() */
79 : /************************************************************************/
80 :
81 2376 : static std::string GetTopLevelType(const char *pszText)
82 : {
83 2376 : if (!strstr(pszText, "\"type\""))
84 109 : return std::string();
85 :
86 2267 : 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 3011 : void StartObjectMember(std::string_view sKey) override
95 : {
96 3011 : m_bInTopLevelType = false;
97 3011 : if (sKey == "type" && m_osLevel == "{")
98 : {
99 2224 : m_bInTopLevelType = true;
100 : }
101 3011 : }
102 :
103 2840 : void String(std::string_view sValue) override
104 : {
105 2840 : if (m_bInTopLevelType)
106 : {
107 2224 : m_osTopLevelTypeValue = sValue;
108 2224 : StopParsing();
109 : }
110 2840 : }
111 :
112 2446 : void StartObject() override
113 : {
114 2446 : m_osLevel += '{';
115 2446 : m_bInTopLevelType = false;
116 2446 : }
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 4534 : MyParser oParser;
140 2267 : oParser.Parse(std::string_view(pszText), true);
141 2267 : return oParser.m_osTopLevelTypeValue;
142 : }
143 :
144 : /************************************************************************/
145 : /* GetCompactJSon() */
146 : /************************************************************************/
147 :
148 3345 : static CPLString GetCompactJSon(const char *pszText, size_t nMaxSize)
149 : {
150 : /* Skip UTF-8 BOM (#5630) */
151 3345 : const GByte *pabyData = reinterpret_cast<const GByte *>(pszText);
152 3345 : if (pabyData[0] == 0xEF && pabyData[1] == 0xBB && pabyData[2] == 0xBF)
153 6 : pszText += 3;
154 :
155 3345 : CPLString osWithoutSpace;
156 3345 : bool bInString = false;
157 3148830 : for (int i = 0; pszText[i] != '\0' && osWithoutSpace.size() < nMaxSize; i++)
158 : {
159 3145490 : if (bInString)
160 : {
161 1001460 : 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 999460 : else if (pszText[i] == '"')
170 : {
171 69924 : bInString = false;
172 69924 : osWithoutSpace += '"';
173 : }
174 : else
175 : {
176 929536 : osWithoutSpace += pszText[i];
177 : }
178 : }
179 2144030 : else if (pszText[i] == '"')
180 : {
181 69941 : bInString = true;
182 69941 : osWithoutSpace += '"';
183 : }
184 2074090 : else if (!isspace(static_cast<unsigned char>(pszText[i])))
185 : {
186 985080 : osWithoutSpace += pszText[i];
187 : }
188 : }
189 3345 : return osWithoutSpace;
190 : }
191 :
192 : /************************************************************************/
193 : /* IsGeoJSONLikeObject() */
194 : /************************************************************************/
195 :
196 122438 : static bool IsGeoJSONLikeObject(const char *pszText, bool &bMightBeSequence,
197 : bool &bReadMoreBytes, GDALOpenInfo *poOpenInfo,
198 : const char *pszExpectedDriverName)
199 : {
200 122438 : bMightBeSequence = false;
201 122438 : bReadMoreBytes = false;
202 :
203 122438 : if (!IsJSONObject(pszText))
204 120296 : return false;
205 :
206 4284 : const std::string osTopLevelType = GetTopLevelType(pszText);
207 2142 : if (osTopLevelType == "Topology")
208 8 : return false;
209 :
210 2173 : if (poOpenInfo->IsSingleAllowedDriver(pszExpectedDriverName) &&
211 39 : GDALGetDriverByName(pszExpectedDriverName))
212 : {
213 39 : return true;
214 : }
215 :
216 4192 : if ((!poOpenInfo->papszAllowedDrivers ||
217 2 : CSLFindString(poOpenInfo->papszAllowedDrivers, "JSONFG") >= 0) &&
218 2097 : GDALGetDriverByName("JSONFG") && JSONFGIsObject(pszText, poOpenInfo))
219 : {
220 404 : return false;
221 : }
222 :
223 1691 : if (osTopLevelType == "FeatureCollection")
224 : {
225 1113 : 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 55556 : bool ESRIJSONIsObject(const char *pszText, GDALOpenInfo *poOpenInfo)
308 : {
309 55556 : if (!IsJSONObject(pszText))
310 55249 : return false;
311 :
312 309 : if (poOpenInfo->IsSingleAllowedDriver("ESRIJSON") &&
313 2 : GDALGetDriverByName("ESRIJSON"))
314 : {
315 2 : return true;
316 : }
317 :
318 305 : if ( // ESRI Json geometry
319 305 : (strstr(pszText, "\"geometryType\"") != nullptr &&
320 62 : strstr(pszText, "\"esriGeometry") != nullptr)
321 :
322 : // ESRI Json "FeatureCollection"
323 243 : || strstr(pszText, "\"fieldAliases\"") != nullptr
324 :
325 : // ESRI Json "FeatureCollection"
326 243 : || (strstr(pszText, "\"fields\"") != nullptr &&
327 0 : strstr(pszText, "\"esriFieldType") != nullptr))
328 : {
329 62 : return true;
330 : }
331 :
332 486 : const std::string osWithoutSpace = GetCompactJSon(pszText, strlen(pszText));
333 243 : if (osWithoutSpace.find(szESRIJSonFeaturesGeometryRings) !=
334 243 : std::string::npos ||
335 243 : osWithoutSpace.find(szESRIJSonFeaturesAttributes) !=
336 486 : std::string::npos ||
337 239 : osWithoutSpace.find("\"spatialReference\":{\"wkid\":") !=
338 : std::string::npos)
339 : {
340 4 : return true;
341 : }
342 :
343 239 : return false;
344 : }
345 :
346 : /************************************************************************/
347 : /* TopoJSONIsObject() */
348 : /************************************************************************/
349 :
350 55485 : bool TopoJSONIsObject(const char *pszText, GDALOpenInfo *poOpenInfo)
351 : {
352 55485 : if (!IsJSONObject(pszText))
353 55249 : return false;
354 :
355 238 : if (poOpenInfo->IsSingleAllowedDriver("TopoJSON") &&
356 2 : GDALGetDriverByName("TopoJSON"))
357 : {
358 2 : return true;
359 : }
360 :
361 234 : 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 56191 : 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 56191 : if (poOpenInfo->fpL == nullptr || !poOpenInfo->TryToIngest(6000))
474 : {
475 50998 : return false;
476 : }
477 :
478 5193 : bool bMightBeSequence = false;
479 5193 : bool bReadMoreBytes = false;
480 5193 : if (!IsGeoJSONLikeObject(
481 5193 : reinterpret_cast<const char *>(poOpenInfo->pabyHeader),
482 : bMightBeSequence, bReadMoreBytes, poOpenInfo, "GeoJSON"))
483 : {
484 4524 : 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 4522 : return false;
491 : }
492 : }
493 :
494 756 : return !(bMightBeSequence && IsLikelyNewlineSequenceGeoJSON(
495 85 : poOpenInfo->fpL, poOpenInfo->pabyHeader,
496 671 : nullptr) == GDAL_IDENTIFY_TRUE);
497 : }
498 :
499 : /************************************************************************/
500 : /* GeoJSONIsObject() */
501 : /************************************************************************/
502 :
503 57072 : bool GeoJSONIsObject(const char *pszText, GDALOpenInfo *poOpenInfo)
504 : {
505 57072 : bool bMightBeSequence = false;
506 57072 : bool bReadMoreBytes = false;
507 57072 : if (!IsGeoJSONLikeObject(pszText, bMightBeSequence, bReadMoreBytes,
508 : poOpenInfo, "GeoJSON"))
509 : {
510 56191 : return false;
511 : }
512 :
513 1156 : return !(bMightBeSequence &&
514 275 : IsLikelyNewlineSequenceGeoJSON(nullptr, nullptr, pszText) ==
515 881 : GDAL_IDENTIFY_TRUE);
516 : }
517 :
518 : /************************************************************************/
519 : /* GeoJSONSeqFileIsObject() */
520 : /************************************************************************/
521 :
522 55578 : 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 55578 : if (poOpenInfo->fpL == nullptr || !poOpenInfo->TryToIngest(6000))
529 : {
530 50994 : return false;
531 : }
532 :
533 4584 : const char *pszText =
534 : reinterpret_cast<const char *>(poOpenInfo->pabyHeader);
535 4584 : if (pszText[0] == '\x1e')
536 26 : return IsGeoJSONLikeObject(pszText + 1, poOpenInfo, "GeoJSONSeq");
537 :
538 4558 : bool bMightBeSequence = false;
539 4558 : bool bReadMoreBytes = false;
540 4558 : if (!IsGeoJSONLikeObject(pszText, bMightBeSequence, bReadMoreBytes,
541 : poOpenInfo, "GeoJSONSeq"))
542 : {
543 4500 : 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 4500 : 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 55587 : bool GeoJSONSeqIsObject(const char *pszText, GDALOpenInfo *poOpenInfo)
567 : {
568 55587 : if (pszText[0] == '\x1e')
569 0 : return IsGeoJSONLikeObject(pszText + 1, poOpenInfo, "GeoJSONSeq");
570 :
571 55587 : bool bMightBeSequence = false;
572 55587 : bool bReadMoreBytes = false;
573 55587 : if (!IsGeoJSONLikeObject(pszText, bMightBeSequence, bReadMoreBytes,
574 : poOpenInfo, "GeoJSONSeq"))
575 : {
576 55581 : 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 50950 : static bool JSONFGFileIsObject(GDALOpenInfo *poOpenInfo)
597 : {
598 : // 6000 somewhat arbitrary. Based on other JSON-like drivers
599 50950 : if (poOpenInfo->fpL == nullptr || !poOpenInfo->TryToIngest(6000))
600 : {
601 49395 : return false;
602 : }
603 :
604 1555 : const char *pszText =
605 : reinterpret_cast<const char *>(poOpenInfo->pabyHeader);
606 1555 : return JSONFGIsObject(pszText, poOpenInfo);
607 : }
608 :
609 54704 : bool JSONFGIsObject(const char *pszText, GDALOpenInfo *poOpenInfo)
610 : {
611 54704 : if (!IsJSONObject(pszText))
612 52178 : return false;
613 :
614 2528 : if (poOpenInfo->IsSingleAllowedDriver("JSONFG") &&
615 2 : GDALGetDriverByName("JSONFG"))
616 : {
617 2 : return true;
618 : }
619 :
620 5048 : const std::string osWithoutSpace = GetCompactJSon(pszText, strlen(pszText));
621 :
622 : // In theory, conformsTo should be required, but let be lax...
623 2524 : if (osWithoutSpace.find("conformsTo") != std::string::npos)
624 : {
625 1296 : if (osWithoutSpace.find("\"[ogc-json-fg-1-") != std::string::npos ||
626 492 : osWithoutSpace.find("\"http://www.opengis.net/spec/json-fg-1/") !=
627 1296 : std::string::npos ||
628 0 : osWithoutSpace.find(
629 : "\"http:\\/\\/www.opengis.net\\/spec\\/json-fg-1\\/") !=
630 : std::string::npos)
631 : {
632 804 : return true;
633 : }
634 : }
635 :
636 3440 : if (osWithoutSpace.find("\"place\":{\"type\":") != std::string::npos ||
637 1720 : osWithoutSpace.find("\"place\":{\"coordinates\":") !=
638 1720 : std::string::npos ||
639 3440 : osWithoutSpace.find("\"time\":{\"date\":") != std::string::npos ||
640 3440 : osWithoutSpace.find("\"time\":{\"timestamp\":") != std::string::npos ||
641 3440 : osWithoutSpace.find("\"time\":{\"interval\":") != std::string::npos ||
642 1720 : osWithoutSpace.find("\"type\":\"CircularString\"") !=
643 1720 : std::string::npos ||
644 1720 : osWithoutSpace.find("\"type\":\"CompoundCurve\"") !=
645 1720 : std::string::npos ||
646 3440 : osWithoutSpace.find("\"type\":\"CurvePolygon\"") != std::string::npos ||
647 5160 : osWithoutSpace.find("\"type\":\"MultiCurve\"") != std::string::npos ||
648 1720 : osWithoutSpace.find("\"type\":\"MultiSurface\"") != std::string::npos)
649 : {
650 0 : return true;
651 : }
652 :
653 3436 : if (osWithoutSpace.find("\"coordRefSys\":") != std::string::npos ||
654 1716 : 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 1716 : 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 56590 : GeoJSONSourceType GeoJSONGetSourceType(GDALOpenInfo *poOpenInfo)
738 : {
739 56590 : 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 56590 : if (STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSON:http://") ||
745 56590 : STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSON:https://") ||
746 56590 : STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSON:ftp://"))
747 : {
748 0 : srcType = eGeoJSONSourceService;
749 : }
750 56590 : else if (STARTS_WITH_CI(poOpenInfo->pszFilename, "http://") ||
751 56555 : STARTS_WITH_CI(poOpenInfo->pszFilename, "https://") ||
752 56553 : 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 56553 : 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 56553 : else if (GeoJSONIsObject(poOpenInfo->pszFilename, poOpenInfo))
784 : {
785 362 : srcType = eGeoJSONSourceText;
786 : }
787 56191 : else if (GeoJSONFileIsObject(poOpenInfo))
788 : {
789 643 : srcType = eGeoJSONSourceFile;
790 : }
791 :
792 56589 : return srcType;
793 : }
794 :
795 : /************************************************************************/
796 : /* ESRIJSONDriverGetSourceType() */
797 : /************************************************************************/
798 :
799 55532 : GeoJSONSourceType ESRIJSONDriverGetSourceType(GDALOpenInfo *poOpenInfo)
800 : {
801 55532 : if (STARTS_WITH_CI(poOpenInfo->pszFilename, "ESRIJSON:http://") ||
802 55532 : STARTS_WITH_CI(poOpenInfo->pszFilename, "ESRIJSON:https://") ||
803 55532 : STARTS_WITH_CI(poOpenInfo->pszFilename, "ESRIJSON:ftp://"))
804 : {
805 0 : return eGeoJSONSourceService;
806 : }
807 55532 : else if (STARTS_WITH(poOpenInfo->pszFilename, "http://") ||
808 55515 : STARTS_WITH(poOpenInfo->pszFilename, "https://") ||
809 55514 : 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 55514 : 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 55512 : if (poOpenInfo->fpL == nullptr)
837 : {
838 50995 : const char *pszText = poOpenInfo->pszFilename;
839 50995 : if (ESRIJSONIsObject(pszText, poOpenInfo))
840 4 : return eGeoJSONSourceText;
841 50991 : 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 4517 : if (!poOpenInfo->TryToIngest(6000))
848 : {
849 0 : return eGeoJSONSourceUnknown;
850 : }
851 :
852 9034 : if (poOpenInfo->pabyHeader != nullptr &&
853 4517 : ESRIJSONIsObject(reinterpret_cast<const char *>(poOpenInfo->pabyHeader),
854 : poOpenInfo))
855 : {
856 32 : return eGeoJSONSourceFile;
857 : }
858 4485 : return eGeoJSONSourceUnknown;
859 : }
860 :
861 : /************************************************************************/
862 : /* TopoJSONDriverGetSourceType() */
863 : /************************************************************************/
864 :
865 55509 : GeoJSONSourceType TopoJSONDriverGetSourceType(GDALOpenInfo *poOpenInfo)
866 : {
867 55509 : if (STARTS_WITH_CI(poOpenInfo->pszFilename, "TopoJSON:http://") ||
868 55509 : STARTS_WITH_CI(poOpenInfo->pszFilename, "TopoJSON:https://") ||
869 55509 : STARTS_WITH_CI(poOpenInfo->pszFilename, "TopoJSON:ftp://"))
870 : {
871 0 : return eGeoJSONSourceService;
872 : }
873 55509 : else if (STARTS_WITH(poOpenInfo->pszFilename, "http://") ||
874 55484 : STARTS_WITH(poOpenInfo->pszFilename, "https://") ||
875 55482 : 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 55482 : 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 55482 : if (poOpenInfo->fpL == nullptr)
903 : {
904 50991 : const char *pszText = poOpenInfo->pszFilename;
905 50991 : if (TopoJSONIsObject(pszText, poOpenInfo))
906 0 : return eGeoJSONSourceText;
907 50991 : 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 4491 : if (!poOpenInfo->TryToIngest(6000))
914 : {
915 0 : return eGeoJSONSourceUnknown;
916 : }
917 :
918 8982 : if (poOpenInfo->pabyHeader != nullptr &&
919 4491 : TopoJSONIsObject(reinterpret_cast<const char *>(poOpenInfo->pabyHeader),
920 : poOpenInfo))
921 : {
922 10 : return eGeoJSONSourceFile;
923 : }
924 4481 : return eGeoJSONSourceUnknown;
925 : }
926 :
927 : /************************************************************************/
928 : /* GeoJSONSeqGetSourceType() */
929 : /************************************************************************/
930 :
931 55613 : GeoJSONSourceType GeoJSONSeqGetSourceType(GDALOpenInfo *poOpenInfo)
932 : {
933 55613 : GeoJSONSourceType srcType = eGeoJSONSourceUnknown;
934 :
935 55613 : if (STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSONSeq:http://") ||
936 55613 : STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSONSeq:https://") ||
937 55613 : STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSONSeq:ftp://"))
938 : {
939 0 : srcType = eGeoJSONSourceService;
940 : }
941 55613 : else if (STARTS_WITH_CI(poOpenInfo->pszFilename, "http://") ||
942 55588 : STARTS_WITH_CI(poOpenInfo->pszFilename, "https://") ||
943 55586 : 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 55586 : 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 55584 : else if (GeoJSONSeqIsObject(poOpenInfo->pszFilename, poOpenInfo))
969 : {
970 6 : srcType = eGeoJSONSourceText;
971 : }
972 55578 : else if (GeoJSONSeqFileIsObject(poOpenInfo))
973 : {
974 84 : srcType = eGeoJSONSourceFile;
975 : }
976 :
977 55610 : return srcType;
978 : }
979 :
980 : /************************************************************************/
981 : /* JSONFGDriverGetSourceType() */
982 : /************************************************************************/
983 :
984 51069 : GeoJSONSourceType JSONFGDriverGetSourceType(GDALOpenInfo *poOpenInfo)
985 : {
986 51069 : GeoJSONSourceType srcType = eGeoJSONSourceUnknown;
987 :
988 51069 : if (STARTS_WITH_CI(poOpenInfo->pszFilename, "JSONFG:http://") ||
989 51069 : STARTS_WITH_CI(poOpenInfo->pszFilename, "JSONFG:https://") ||
990 51069 : STARTS_WITH_CI(poOpenInfo->pszFilename, "JSONFG:ftp://"))
991 : {
992 0 : srcType = eGeoJSONSourceService;
993 : }
994 51069 : else if (STARTS_WITH_CI(poOpenInfo->pszFilename, "http://") ||
995 51044 : STARTS_WITH_CI(poOpenInfo->pszFilename, "https://") ||
996 51042 : 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 51042 : 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 51042 : else if (JSONFGIsObject(poOpenInfo->pszFilename, poOpenInfo))
1022 : {
1023 92 : srcType = eGeoJSONSourceText;
1024 : }
1025 50950 : else if (JSONFGFileIsObject(poOpenInfo))
1026 : {
1027 314 : srcType = eGeoJSONSourceFile;
1028 : }
1029 :
1030 51068 : 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 : }
|