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