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 250128 : static void SkipUTF8BOM(const char *&pszText)
36 : {
37 : /* Skip UTF-8 BOM (#5630) */
38 250128 : const GByte *pabyData = reinterpret_cast<const GByte *>(pszText);
39 250128 : if (pabyData[0] == 0xEF && pabyData[1] == 0xBB && pabyData[2] == 0xBF)
40 9 : pszText += 3;
41 250128 : }
42 :
43 : /************************************************************************/
44 : /* IsJSONObject() */
45 : /************************************************************************/
46 :
47 248216 : static bool IsJSONObject(const char *pszText)
48 : {
49 248216 : if (nullptr == pszText)
50 0 : return false;
51 :
52 248216 : SkipUTF8BOM(pszText);
53 :
54 : /* -------------------------------------------------------------------- */
55 : /* This is a primitive test, but we need to perform it fast. */
56 : /* -------------------------------------------------------------------- */
57 249988 : while (*pszText != '\0' && isspace(static_cast<unsigned char>(*pszText)))
58 1772 : pszText++;
59 :
60 248216 : const char *const apszPrefix[] = {"loadGeoJSON(", "jsonp("};
61 744618 : for (size_t iP = 0; iP < sizeof(apszPrefix) / sizeof(apszPrefix[0]); iP++)
62 : {
63 496417 : if (strncmp(pszText, apszPrefix[iP], strlen(apszPrefix[iP])) == 0)
64 : {
65 15 : pszText += strlen(apszPrefix[iP]);
66 15 : break;
67 : }
68 : }
69 :
70 248216 : if (*pszText != '{')
71 243842 : return false;
72 :
73 4374 : return true;
74 : }
75 :
76 : /************************************************************************/
77 : /* GetTopLevelType() */
78 : /************************************************************************/
79 :
80 2018 : static std::string GetTopLevelType(const char *pszText)
81 : {
82 2018 : if (!strstr(pszText, "\"type\""))
83 106 : return std::string();
84 :
85 1912 : 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 2424 : void StartObjectMember(const char *pszKey, size_t nLength) override
94 : {
95 2424 : m_bInTopLevelType = false;
96 4398 : if (nLength == strlen("type") && strcmp(pszKey, "type") == 0 &&
97 1974 : m_osLevel == "{")
98 : {
99 1890 : m_bInTopLevelType = true;
100 : }
101 2424 : }
102 :
103 2189 : void String(const char *pszValue, size_t nLength) override
104 : {
105 2189 : if (m_bInTopLevelType)
106 : {
107 1890 : m_osTopLevelTypeValue.assign(pszValue, nLength);
108 1890 : StopParsing();
109 : }
110 2189 : }
111 :
112 2060 : void StartObject() override
113 : {
114 2060 : m_osLevel += '{';
115 2060 : m_bInTopLevelType = false;
116 2060 : }
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 3824 : MyParser oParser;
140 1912 : oParser.Parse(pszText, strlen(pszText), true);
141 1912 : return oParser.m_osTopLevelTypeValue;
142 : }
143 :
144 : /************************************************************************/
145 : /* GetCompactJSon() */
146 : /************************************************************************/
147 :
148 2732 : static CPLString GetCompactJSon(const char *pszText, size_t nMaxSize)
149 : {
150 : /* Skip UTF-8 BOM (#5630) */
151 2732 : const GByte *pabyData = reinterpret_cast<const GByte *>(pszText);
152 2732 : if (pabyData[0] == 0xEF && pabyData[1] == 0xBB && pabyData[2] == 0xBF)
153 6 : pszText += 3;
154 :
155 2732 : CPLString osWithoutSpace;
156 2732 : bool bInString = false;
157 2258240 : for (int i = 0; pszText[i] != '\0' && osWithoutSpace.size() < nMaxSize; i++)
158 : {
159 2255510 : if (bInString)
160 : {
161 826856 : if (pszText[i] == '\\')
162 : {
163 2060 : osWithoutSpace += pszText[i];
164 2060 : if (pszText[i + 1] == '\0')
165 0 : break;
166 2060 : osWithoutSpace += pszText[i + 1];
167 2060 : i++;
168 : }
169 824796 : else if (pszText[i] == '"')
170 : {
171 59363 : bInString = false;
172 59363 : osWithoutSpace += '"';
173 : }
174 : else
175 : {
176 765433 : osWithoutSpace += pszText[i];
177 : }
178 : }
179 1428660 : else if (pszText[i] == '"')
180 : {
181 59370 : bInString = true;
182 59370 : osWithoutSpace += '"';
183 : }
184 1369280 : else if (!isspace(static_cast<unsigned char>(pszText[i])))
185 : {
186 717825 : osWithoutSpace += pszText[i];
187 : }
188 : }
189 2732 : return osWithoutSpace;
190 : }
191 :
192 : /************************************************************************/
193 : /* IsGeoJSONLikeObject() */
194 : /************************************************************************/
195 :
196 105833 : static bool IsGeoJSONLikeObject(const char *pszText, bool &bMightBeSequence,
197 : bool &bReadMoreBytes, GDALOpenInfo *poOpenInfo,
198 : const char *pszExpectedDriverName)
199 : {
200 105833 : bMightBeSequence = false;
201 105833 : bReadMoreBytes = false;
202 :
203 105833 : if (!IsJSONObject(pszText))
204 103986 : return false;
205 :
206 3694 : const std::string osTopLevelType = GetTopLevelType(pszText);
207 1847 : if (osTopLevelType == "Topology")
208 6 : return false;
209 :
210 1880 : if (poOpenInfo->IsSingleAllowedDriver(pszExpectedDriverName) &&
211 39 : GDALGetDriverByName(pszExpectedDriverName))
212 : {
213 39 : return true;
214 : }
215 :
216 3606 : if ((!poOpenInfo->papszAllowedDrivers ||
217 2 : CSLFindString(poOpenInfo->papszAllowedDrivers, "JSONFG") >= 0) &&
218 1804 : GDALGetDriverByName("JSONFG") && JSONFGIsObject(pszText, poOpenInfo))
219 : {
220 284 : return false;
221 : }
222 :
223 1518 : if (osTopLevelType == "FeatureCollection")
224 : {
225 1074 : return true;
226 : }
227 :
228 888 : const std::string osWithoutSpace = GetCompactJSon(pszText, strlen(pszText));
229 449 : if (osWithoutSpace.find("{\"features\":[") == 0 &&
230 449 : 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 441 : 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 803 : if (osWithoutSpace.find("{\"coordinates\":[") == 0 ||
249 : // and https://github.com/OSGeo/gdal/issues/2787
250 400 : osWithoutSpace.find("{\"geometry\":{\"coordinates\":[") == 0)
251 : {
252 6 : return true;
253 : }
254 :
255 834 : if (osTopLevelType == "Feature" || osTopLevelType == "Point" ||
256 606 : osTopLevelType == "LineString" || osTopLevelType == "Polygon" ||
257 345 : osTopLevelType == "MultiPoint" || osTopLevelType == "MultiLineString" ||
258 699 : osTopLevelType == "MultiPolygon" ||
259 58 : osTopLevelType == "GeometryCollection")
260 : {
261 345 : bMightBeSequence = true;
262 345 : 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 47970 : bool ESRIJSONIsObject(const char *pszText, GDALOpenInfo *poOpenInfo)
290 : {
291 47970 : if (!IsJSONObject(pszText))
292 47728 : 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 47901 : bool TopoJSONIsObject(const char *pszText, GDALOpenInfo *poOpenInfo)
331 : {
332 47901 : if (!IsJSONObject(pszText))
333 47728 : 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 319 : IsLikelyNewlineSequenceGeoJSON(VSILFILE *fpL, const GByte *pabyHeader,
350 : const char *pszFileContent)
351 : {
352 319 : const size_t nBufferSize = 4096 * 10;
353 638 : std::vector<GByte> abyBuffer;
354 319 : abyBuffer.resize(nBufferSize + 1);
355 :
356 319 : int nCurlLevel = 0;
357 319 : bool bInString = false;
358 319 : bool bLastIsEscape = false;
359 319 : bool bFirstIter = true;
360 319 : bool bEOLFound = false;
361 319 : int nCountObject = 0;
362 : while (true)
363 : {
364 : size_t nRead;
365 451 : bool bEnd = false;
366 451 : if (bFirstIter)
367 : {
368 319 : const char *pszText =
369 319 : pszFileContent ? pszFileContent
370 : : reinterpret_cast<const char *>(pabyHeader);
371 319 : assert(pszText);
372 319 : nRead = std::min(strlen(pszText), nBufferSize);
373 319 : memcpy(abyBuffer.data(), pszText, nRead);
374 319 : bFirstIter = false;
375 319 : if (fpL)
376 : {
377 145 : VSIFSeekL(fpL, nRead, SEEK_SET);
378 : }
379 : }
380 : else
381 : {
382 132 : nRead = VSIFReadL(abyBuffer.data(), 1, nBufferSize, fpL);
383 132 : bEnd = nRead < nBufferSize;
384 : }
385 3251980 : for (size_t i = 0; i < nRead; i++)
386 : {
387 3251630 : if (nCurlLevel == 0)
388 : {
389 591 : if (abyBuffer[i] == '{')
390 : {
391 409 : nCountObject++;
392 409 : if (nCountObject == 2)
393 : {
394 93 : break;
395 : }
396 316 : nCurlLevel++;
397 : }
398 182 : else if (nCountObject == 1 && abyBuffer[i] == '\n')
399 : {
400 128 : bEOLFound = true;
401 : }
402 54 : else if (!isspace(static_cast<unsigned char>(abyBuffer[i])))
403 : {
404 6 : return GDAL_IDENTIFY_FALSE;
405 : }
406 : }
407 3251040 : else if (bInString)
408 : {
409 90780 : if (bLastIsEscape)
410 : {
411 798 : bLastIsEscape = false;
412 : }
413 89982 : else if (abyBuffer[i] == '\\')
414 : {
415 798 : bLastIsEscape = true;
416 : }
417 89184 : else if (abyBuffer[i] == '"')
418 : {
419 2711 : bInString = false;
420 : }
421 : }
422 3160260 : else if (abyBuffer[i] == '"')
423 : {
424 2711 : bInString = true;
425 : }
426 3157550 : else if (abyBuffer[i] == '{')
427 : {
428 307 : nCurlLevel++;
429 : }
430 3157240 : else if (abyBuffer[i] == '}')
431 : {
432 623 : nCurlLevel--;
433 : }
434 : }
435 445 : if (!fpL || bEnd || nCountObject == 2)
436 : break;
437 132 : }
438 313 : if (bEOLFound && nCountObject == 2)
439 93 : return GDAL_IDENTIFY_TRUE;
440 220 : return GDAL_IDENTIFY_UNKNOWN;
441 : }
442 :
443 : /************************************************************************/
444 : /* GeoJSONFileIsObject() */
445 : /************************************************************************/
446 :
447 48582 : 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 48582 : if (poOpenInfo->fpL == nullptr || !poOpenInfo->TryToIngest(6000))
454 : {
455 44036 : return false;
456 : }
457 :
458 4546 : bool bMightBeSequence = false;
459 4546 : bool bReadMoreBytes = false;
460 4546 : if (!IsGeoJSONLikeObject(
461 4546 : reinterpret_cast<const char *>(poOpenInfo->pabyHeader),
462 : bMightBeSequence, bReadMoreBytes, poOpenInfo, "GeoJSON"))
463 : {
464 3903 : 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 3901 : return false;
471 : }
472 : }
473 :
474 732 : return !(bMightBeSequence && IsLikelyNewlineSequenceGeoJSON(
475 87 : poOpenInfo->fpL, poOpenInfo->pabyHeader,
476 645 : nullptr) == GDAL_IDENTIFY_TRUE);
477 : }
478 :
479 : /************************************************************************/
480 : /* GeoJSONIsObject() */
481 : /************************************************************************/
482 :
483 49318 : bool GeoJSONIsObject(const char *pszText, GDALOpenInfo *poOpenInfo)
484 : {
485 49318 : bool bMightBeSequence = false;
486 49318 : bool bReadMoreBytes = false;
487 49318 : if (!IsGeoJSONLikeObject(pszText, bMightBeSequence, bReadMoreBytes,
488 : poOpenInfo, "GeoJSON"))
489 : {
490 48582 : return false;
491 : }
492 :
493 904 : return !(bMightBeSequence &&
494 168 : IsLikelyNewlineSequenceGeoJSON(nullptr, nullptr, pszText) ==
495 736 : GDAL_IDENTIFY_TRUE);
496 : }
497 :
498 : /************************************************************************/
499 : /* GeoJSONSeqFileIsObject() */
500 : /************************************************************************/
501 :
502 47995 : 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 47995 : if (poOpenInfo->fpL == nullptr || !poOpenInfo->TryToIngest(6000))
509 : {
510 44032 : return false;
511 : }
512 :
513 3963 : const char *pszText =
514 : reinterpret_cast<const char *>(poOpenInfo->pabyHeader);
515 3963 : if (pszText[0] == '\x1e')
516 26 : return IsGeoJSONLikeObject(pszText + 1, poOpenInfo, "GeoJSONSeq");
517 :
518 3937 : bool bMightBeSequence = false;
519 3937 : bool bReadMoreBytes = false;
520 3937 : if (!IsGeoJSONLikeObject(pszText, bMightBeSequence, bReadMoreBytes,
521 : poOpenInfo, "GeoJSONSeq"))
522 : {
523 3879 : 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 3879 : 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 48004 : bool GeoJSONSeqIsObject(const char *pszText, GDALOpenInfo *poOpenInfo)
547 : {
548 48004 : if (pszText[0] == '\x1e')
549 0 : return IsGeoJSONLikeObject(pszText + 1, poOpenInfo, "GeoJSONSeq");
550 :
551 48004 : bool bMightBeSequence = false;
552 48004 : bool bReadMoreBytes = false;
553 48004 : if (!IsGeoJSONLikeObject(pszText, bMightBeSequence, bReadMoreBytes,
554 : poOpenInfo, "GeoJSONSeq"))
555 : {
556 47998 : 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 43524 : static bool JSONFGFileIsObject(GDALOpenInfo *poOpenInfo)
577 : {
578 : // 6000 somewhat arbitrary. Based on other JSON-like drivers
579 43524 : if (poOpenInfo->fpL == nullptr || !poOpenInfo->TryToIngest(6000))
580 : {
581 42440 : return false;
582 : }
583 :
584 1084 : const char *pszText =
585 : reinterpret_cast<const char *>(poOpenInfo->pabyHeader);
586 1084 : return JSONFGIsObject(pszText, poOpenInfo);
587 : }
588 :
589 46512 : bool JSONFGIsObject(const char *pszText, GDALOpenInfo *poOpenInfo)
590 : {
591 46512 : if (!IsJSONObject(pszText))
592 44400 : return false;
593 :
594 2114 : if (poOpenInfo->IsSingleAllowedDriver("JSONFG") &&
595 2 : GDALGetDriverByName("JSONFG"))
596 : {
597 2 : return true;
598 : }
599 :
600 4220 : const std::string osWithoutSpace = GetCompactJSon(pszText, strlen(pszText));
601 :
602 : // In theory, conformsTo should be required, but let be lax...
603 : {
604 2110 : const auto nPos = osWithoutSpace.find("\"conformsTo\":[");
605 2110 : 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 3092 : if (osWithoutSpace.find("\"place\":{\"type\":") != std::string::npos ||
625 1546 : osWithoutSpace.find("\"place\":{\"coordinates\":") !=
626 1546 : std::string::npos ||
627 3092 : osWithoutSpace.find("\"time\":{\"date\":") != std::string::npos ||
628 4638 : osWithoutSpace.find("\"time\":{\"timestamp\":") != std::string::npos ||
629 1546 : osWithoutSpace.find("\"time\":{\"interval\":") != std::string::npos)
630 : {
631 0 : return true;
632 : }
633 :
634 3088 : if (osWithoutSpace.find("\"coordRefSys\":") != std::string::npos ||
635 1542 : 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 1542 : return false;
700 : }
701 :
702 : /************************************************************************/
703 : /* IsLikelyESRIJSONURL() */
704 : /************************************************************************/
705 :
706 131 : 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 262 : return (strstr(pszURL, "f=json") != nullptr ||
711 131 : strstr(pszURL, "f=pjson") != nullptr ||
712 262 : strstr(pszURL, "resultRecordCount=") != nullptr) &&
713 131 : strstr(pszURL, "/items?") == nullptr;
714 : }
715 :
716 : /************************************************************************/
717 : /* GeoJSONGetSourceType() */
718 : /************************************************************************/
719 :
720 48893 : GeoJSONSourceType GeoJSONGetSourceType(GDALOpenInfo *poOpenInfo)
721 : {
722 48893 : 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 48893 : if (STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSON:http://") ||
728 48893 : STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSON:https://") ||
729 48893 : STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSON:ftp://"))
730 : {
731 0 : srcType = eGeoJSONSourceService;
732 : }
733 48893 : else if (STARTS_WITH_CI(poOpenInfo->pszFilename, "http://") ||
734 48858 : STARTS_WITH_CI(poOpenInfo->pszFilename, "https://") ||
735 48856 : STARTS_WITH_CI(poOpenInfo->pszFilename, "ftp://"))
736 : {
737 37 : if (poOpenInfo->IsSingleAllowedDriver("GeoJSON"))
738 : {
739 1 : return eGeoJSONSourceService;
740 : }
741 36 : if ((strstr(poOpenInfo->pszFilename, "SERVICE=WFS") ||
742 36 : strstr(poOpenInfo->pszFilename, "service=WFS") ||
743 36 : strstr(poOpenInfo->pszFilename, "service=wfs")) &&
744 0 : !strstr(poOpenInfo->pszFilename, "json"))
745 : {
746 0 : return eGeoJSONSourceUnknown;
747 : }
748 36 : if (IsLikelyESRIJSONURL(poOpenInfo->pszFilename))
749 : {
750 0 : return eGeoJSONSourceUnknown;
751 : }
752 36 : srcType = eGeoJSONSourceService;
753 : }
754 48856 : 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 48856 : else if (GeoJSONIsObject(poOpenInfo->pszFilename, poOpenInfo))
767 : {
768 274 : srcType = eGeoJSONSourceText;
769 : }
770 48582 : else if (GeoJSONFileIsObject(poOpenInfo))
771 : {
772 617 : srcType = eGeoJSONSourceFile;
773 : }
774 :
775 48892 : return srcType;
776 : }
777 :
778 : /************************************************************************/
779 : /* ESRIJSONDriverGetSourceType() */
780 : /************************************************************************/
781 :
782 47949 : GeoJSONSourceType ESRIJSONDriverGetSourceType(GDALOpenInfo *poOpenInfo)
783 : {
784 47949 : if (STARTS_WITH_CI(poOpenInfo->pszFilename, "ESRIJSON:http://") ||
785 47949 : STARTS_WITH_CI(poOpenInfo->pszFilename, "ESRIJSON:https://") ||
786 47949 : STARTS_WITH_CI(poOpenInfo->pszFilename, "ESRIJSON:ftp://"))
787 : {
788 0 : return eGeoJSONSourceService;
789 : }
790 47949 : else if (STARTS_WITH(poOpenInfo->pszFilename, "http://") ||
791 47932 : STARTS_WITH(poOpenInfo->pszFilename, "https://") ||
792 47931 : 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 47931 : 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 47929 : if (poOpenInfo->fpL == nullptr)
820 : {
821 44033 : const char *pszText = poOpenInfo->pszFilename;
822 44033 : if (ESRIJSONIsObject(pszText, poOpenInfo))
823 4 : return eGeoJSONSourceText;
824 44029 : 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 3896 : if (!poOpenInfo->TryToIngest(6000))
831 : {
832 0 : return eGeoJSONSourceUnknown;
833 : }
834 :
835 7792 : if (poOpenInfo->pabyHeader != nullptr &&
836 3896 : ESRIJSONIsObject(reinterpret_cast<const char *>(poOpenInfo->pabyHeader),
837 : poOpenInfo))
838 : {
839 32 : return eGeoJSONSourceFile;
840 : }
841 3864 : return eGeoJSONSourceUnknown;
842 : }
843 :
844 : /************************************************************************/
845 : /* TopoJSONDriverGetSourceType() */
846 : /************************************************************************/
847 :
848 47925 : GeoJSONSourceType TopoJSONDriverGetSourceType(GDALOpenInfo *poOpenInfo)
849 : {
850 47925 : if (STARTS_WITH_CI(poOpenInfo->pszFilename, "TopoJSON:http://") ||
851 47925 : STARTS_WITH_CI(poOpenInfo->pszFilename, "TopoJSON:https://") ||
852 47925 : STARTS_WITH_CI(poOpenInfo->pszFilename, "TopoJSON:ftp://"))
853 : {
854 0 : return eGeoJSONSourceService;
855 : }
856 47925 : else if (STARTS_WITH(poOpenInfo->pszFilename, "http://") ||
857 47900 : STARTS_WITH(poOpenInfo->pszFilename, "https://") ||
858 47898 : 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 47898 : 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 47898 : if (poOpenInfo->fpL == nullptr)
886 : {
887 44029 : const char *pszText = poOpenInfo->pszFilename;
888 44029 : if (TopoJSONIsObject(pszText, poOpenInfo))
889 0 : return eGeoJSONSourceText;
890 44029 : 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 3869 : if (!poOpenInfo->TryToIngest(6000))
897 : {
898 0 : return eGeoJSONSourceUnknown;
899 : }
900 :
901 7738 : if (poOpenInfo->pabyHeader != nullptr &&
902 3869 : TopoJSONIsObject(reinterpret_cast<const char *>(poOpenInfo->pabyHeader),
903 : poOpenInfo))
904 : {
905 8 : return eGeoJSONSourceFile;
906 : }
907 3861 : return eGeoJSONSourceUnknown;
908 : }
909 :
910 : /************************************************************************/
911 : /* GeoJSONSeqGetSourceType() */
912 : /************************************************************************/
913 :
914 48030 : GeoJSONSourceType GeoJSONSeqGetSourceType(GDALOpenInfo *poOpenInfo)
915 : {
916 48030 : GeoJSONSourceType srcType = eGeoJSONSourceUnknown;
917 :
918 48030 : if (STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSONSeq:http://") ||
919 48030 : STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSONSeq:https://") ||
920 48030 : STARTS_WITH_CI(poOpenInfo->pszFilename, "GEOJSONSeq:ftp://"))
921 : {
922 0 : srcType = eGeoJSONSourceService;
923 : }
924 48030 : else if (STARTS_WITH_CI(poOpenInfo->pszFilename, "http://") ||
925 48005 : STARTS_WITH_CI(poOpenInfo->pszFilename, "https://") ||
926 48003 : 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 48003 : 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 48001 : else if (GeoJSONSeqIsObject(poOpenInfo->pszFilename, poOpenInfo))
952 : {
953 6 : srcType = eGeoJSONSourceText;
954 : }
955 47995 : else if (GeoJSONSeqFileIsObject(poOpenInfo))
956 : {
957 84 : srcType = eGeoJSONSourceFile;
958 : }
959 :
960 48027 : return srcType;
961 : }
962 :
963 : /************************************************************************/
964 : /* JSONFGDriverGetSourceType() */
965 : /************************************************************************/
966 :
967 43641 : GeoJSONSourceType JSONFGDriverGetSourceType(GDALOpenInfo *poOpenInfo)
968 : {
969 43641 : GeoJSONSourceType srcType = eGeoJSONSourceUnknown;
970 :
971 43641 : if (STARTS_WITH_CI(poOpenInfo->pszFilename, "JSONFG:http://") ||
972 43641 : STARTS_WITH_CI(poOpenInfo->pszFilename, "JSONFG:https://") ||
973 43641 : STARTS_WITH_CI(poOpenInfo->pszFilename, "JSONFG:ftp://"))
974 : {
975 0 : srcType = eGeoJSONSourceService;
976 : }
977 43641 : else if (STARTS_WITH_CI(poOpenInfo->pszFilename, "http://") ||
978 43616 : STARTS_WITH_CI(poOpenInfo->pszFilename, "https://") ||
979 43614 : 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 43614 : 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 43614 : else if (JSONFGIsObject(poOpenInfo->pszFilename, poOpenInfo))
1005 : {
1006 90 : srcType = eGeoJSONSourceText;
1007 : }
1008 43524 : else if (JSONFGFileIsObject(poOpenInfo))
1009 : {
1010 196 : srcType = eGeoJSONSourceFile;
1011 : }
1012 :
1013 43640 : return srcType;
1014 : }
1015 :
1016 : /************************************************************************/
1017 : /* GeoJSONStringPropertyToFieldType() */
1018 : /************************************************************************/
1019 :
1020 834 : OGRFieldType GeoJSONStringPropertyToFieldType(json_object *poObject,
1021 : int &nTZFlag)
1022 : {
1023 834 : if (poObject == nullptr)
1024 : {
1025 13 : return OFTString;
1026 : }
1027 821 : const char *pszStr = json_object_get_string(poObject);
1028 :
1029 821 : nTZFlag = 0;
1030 : OGRField sWrkField;
1031 821 : CPLPushErrorHandler(CPLQuietErrorHandler);
1032 821 : const bool bSuccess = CPL_TO_BOOL(OGRParseDate(pszStr, &sWrkField, 0));
1033 821 : CPLPopErrorHandler();
1034 821 : CPLErrorReset();
1035 821 : if (bSuccess)
1036 : {
1037 250 : const bool bHasDate =
1038 250 : strchr(pszStr, '/') != nullptr || strchr(pszStr, '-') != nullptr;
1039 250 : const bool bHasTime = strchr(pszStr, ':') != nullptr;
1040 250 : nTZFlag = sWrkField.Date.TZFlag;
1041 250 : if (bHasDate && bHasTime)
1042 148 : 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 571 : return OFTString;
1050 : }
1051 :
1052 : /************************************************************************/
1053 : /* GeoJSONHTTPFetchWithContentTypeHeader() */
1054 : /************************************************************************/
1055 :
1056 7 : CPLHTTPResult *GeoJSONHTTPFetchWithContentTypeHeader(const char *pszURL)
1057 : {
1058 14 : std::string osHeaders;
1059 : const char *pszGDAL_HTTP_HEADERS =
1060 7 : CPLGetConfigOption("GDAL_HTTP_HEADERS", nullptr);
1061 7 : bool bFoundAcceptHeader = false;
1062 7 : if (pszGDAL_HTTP_HEADERS)
1063 : {
1064 3 : bool bHeadersDone = false;
1065 : // Compatibility hack for "HEADERS=Accept: text/plain, application/json"
1066 3 : if (strstr(pszGDAL_HTTP_HEADERS, "\r\n") == nullptr)
1067 : {
1068 2 : const char *pszComma = strchr(pszGDAL_HTTP_HEADERS, ',');
1069 2 : if (pszComma != nullptr && strchr(pszComma, ':') == nullptr)
1070 : {
1071 1 : osHeaders = pszGDAL_HTTP_HEADERS;
1072 1 : bFoundAcceptHeader =
1073 1 : STARTS_WITH_CI(pszGDAL_HTTP_HEADERS, "Accept:");
1074 1 : bHeadersDone = true;
1075 : }
1076 : }
1077 3 : if (!bHeadersDone)
1078 : {
1079 : // We accept both raw headers with \r\n as a separator, or as
1080 : // a comma separated list of foo: bar values.
1081 : const CPLStringList aosTokens(
1082 2 : strstr(pszGDAL_HTTP_HEADERS, "\r\n")
1083 1 : ? CSLTokenizeString2(pszGDAL_HTTP_HEADERS, "\r\n", 0)
1084 1 : : CSLTokenizeString2(pszGDAL_HTTP_HEADERS, ",",
1085 6 : CSLT_HONOURSTRINGS));
1086 7 : for (int i = 0; i < aosTokens.size(); ++i)
1087 : {
1088 5 : if (!osHeaders.empty())
1089 3 : osHeaders += "\r\n";
1090 5 : if (!bFoundAcceptHeader)
1091 5 : bFoundAcceptHeader =
1092 5 : STARTS_WITH_CI(aosTokens[i], "Accept:");
1093 5 : osHeaders += aosTokens[i];
1094 : }
1095 : }
1096 : }
1097 7 : if (!bFoundAcceptHeader)
1098 : {
1099 5 : if (!osHeaders.empty())
1100 1 : osHeaders += "\r\n";
1101 5 : osHeaders += "Accept: text/plain, application/json";
1102 : }
1103 :
1104 14 : CPLStringList aosOptions;
1105 7 : aosOptions.SetNameValue("HEADERS", osHeaders.c_str());
1106 7 : CPLHTTPResult *pResult = CPLHTTPFetch(pszURL, aosOptions.List());
1107 :
1108 14 : if (nullptr == pResult || 0 == pResult->nDataLen ||
1109 7 : 0 != CPLGetLastErrorNo())
1110 : {
1111 0 : CPLHTTPDestroyResult(pResult);
1112 0 : return nullptr;
1113 : }
1114 :
1115 7 : if (0 != pResult->nStatus)
1116 : {
1117 0 : CPLError(CE_Failure, CPLE_AppDefined, "Curl reports error: %d: %s",
1118 : pResult->nStatus, pResult->pszErrBuf);
1119 0 : CPLHTTPDestroyResult(pResult);
1120 0 : return nullptr;
1121 : }
1122 :
1123 7 : return pResult;
1124 : }
|