Coverage Report

Created: 2026-11-12 08:28

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gdal/frmts/esric/esric_dataset.cpp
Line
Count
Source
1
/******************************************************************************
2
 *
3
 * Purpose : gdal driver for reading Esri compact cache as raster
4
 *           based on public documentation available at
5
 *           https://github.com/Esri/raster-tiles-compactcache
6
 *
7
 * Author : Lucian Plesea
8
 *
9
 * Udate : 06 / 10 / 2020
10
 *
11
 *  Copyright 2020 Esri
12
 *
13
 * SPDX-License-Identifier: MIT
14
 *****************************************************************************/
15
16
#include "gdal_priv.h"
17
#include <cassert>
18
#include <vector>
19
#include <algorithm>
20
#include "cpl_json.h"
21
#include "gdal_proxy.h"
22
#include "gdal_utils.h"
23
#include "cpl_vsi_virtual.h"
24
25
using namespace std;
26
27
CPL_C_START
28
void CPL_DLL GDALRegister_ESRIC();
29
CPL_C_END
30
31
namespace ESRIC
32
{
33
34
#define ENDS_WITH_CI(a, b)                                                     \
35
    (strlen(a) >= strlen(b) && EQUAL(a + strlen(a) - strlen(b), b))
36
37
// ESRI tpkx files use root.json
38
static int IdentifyJSON(GDALOpenInfo *poOpenInfo)
39
554k
{
40
554k
    if (poOpenInfo->eAccess != GA_ReadOnly || poOpenInfo->nHeaderBytes < 512)
41
446k
        return false;
42
43
    // Recognize .tpkx file directly passed
44
108k
    if (!STARTS_WITH(poOpenInfo->pszFilename, "/vsizip/") &&
45
#if !defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION)
46
        ENDS_WITH_CI(poOpenInfo->pszFilename, ".tpkx") &&
47
#endif
48
108k
        memcmp(poOpenInfo->pabyHeader, "PK\x03\x04", 4) == 0)
49
964
    {
50
964
        return true;
51
964
    }
52
53
#if !defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION)
54
    if (!ENDS_WITH_CI(poOpenInfo->pszFilename, "root.json"))
55
        return false;
56
#endif
57
107k
    for (int i = 0; i < 2; ++i)
58
107k
    {
59
107k
        const std::string osHeader(
60
107k
            reinterpret_cast<char *>(poOpenInfo->pabyHeader),
61
107k
            poOpenInfo->nHeaderBytes);
62
107k
        if (std::string::npos != osHeader.find("tileBundlesPath"))
63
171
        {
64
171
            return true;
65
171
        }
66
        // If we didn't find tileBundlesPath i, the first bytes, but find
67
        // other elements typically of .tpkx, then ingest more bytes and
68
        // retry
69
107k
        constexpr int MORE_BYTES = 8192;
70
107k
        if (poOpenInfo->nHeaderBytes < MORE_BYTES &&
71
107k
            (std::string::npos != osHeader.find("tileInfo") ||
72
107k
             std::string::npos != osHeader.find("tileImageInfo")))
73
132
        {
74
132
            poOpenInfo->TryToIngest(MORE_BYTES);
75
132
        }
76
107k
        else
77
107k
            break;
78
107k
    }
79
107k
    return false;
80
107k
}
81
82
// Without full XML parsing, weak, might still fail
83
static int IdentifyXML(GDALOpenInfo *poOpenInfo)
84
554k
{
85
554k
    if (poOpenInfo->eAccess != GA_ReadOnly
86
#if !defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION)
87
        || !ENDS_WITH_CI(poOpenInfo->pszFilename, "conf.xml")
88
#endif
89
554k
        || poOpenInfo->nHeaderBytes < 512)
90
446k
        return false;
91
108k
    CPLString header(reinterpret_cast<char *>(poOpenInfo->pabyHeader),
92
108k
                     poOpenInfo->nHeaderBytes);
93
108k
    return (CPLString::npos != header.find("<CacheInfo"));
94
554k
}
95
96
static int Identify(GDALOpenInfo *poOpenInfo)
97
553k
{
98
553k
    return (IdentifyXML(poOpenInfo) || IdentifyJSON(poOpenInfo));
99
553k
}
100
101
// Stub default delete, don't delete a tile cache from GDAL
102
static CPLErr Delete(const char *)
103
0
{
104
0
    return CE_None;
105
0
}
106
107
// Read a 32bit unsigned integer stored in little endian
108
// Same as CPL_LSBUINT32PTR
109
static inline GUInt32 u32lat(void *data)
110
45
{
111
45
    GUInt32 val;
112
45
    memcpy(&val, data, 4);
113
45
    return CPL_LSBWORD32(val);
114
45
}
115
116
struct Bundle
117
{
118
    void Init(const char *filename)
119
10
    {
120
10
        name = filename;
121
10
        fh.reset(VSIFOpenL(name.c_str(), "rb"));
122
10
        if (nullptr == fh)
123
1
            return;
124
9
        GByte header[64] = {0};
125
        // Check a few header locations, then read the index
126
9
        fh->Read(header, 1, 64);
127
9
        index.resize(BSZ * BSZ);
128
9
        if (3 != u32lat(header) || 5 != u32lat(header + 12) ||
129
9
            40 != u32lat(header + 32) || 0 != u32lat(header + 36) ||
130
9
            BSZ * BSZ * 8 != u32lat(header + 60) ||
131
9
            index.size() != fh->Read(index.data(), 8, index.size()))
132
0
        {
133
0
            fh.reset();
134
0
        }
135
136
        if constexpr (!CPL_IS_LSB)
137
        {
138
            for (auto &v : index)
139
                CPL_LSBPTR64(&v);
140
        }
141
9
    }
142
143
    std::vector<GUInt64> index{};
144
    VSIVirtualHandleUniquePtr fh{};
145
    bool isV2 = false;
146
    CPLString name{};
147
    const size_t BSZ = 128;
148
};
149
150
class ECDataset final : public GDALDataset
151
{
152
    friend class ECBand;
153
154
  public:
155
    ECDataset();
156
157
    CPLErr GetGeoTransform(GDALGeoTransform &gt) const override
158
19
    {
159
19
        gt = m_gt;
160
19
        return CE_None;
161
19
    }
162
163
    const OGRSpatialReference *GetSpatialRef() const override;
164
165
    static GDALDataset *Open(GDALOpenInfo *poOpenInfo);
166
    static GDALDataset *Open(GDALOpenInfo *poOpenInfo,
167
                             const char *pszDescription);
168
169
  protected:
170
    GDALGeoTransform m_gt{};
171
    CPLString dname{};
172
    int isV2{};  // V2 bundle format
173
    int BSZ{};   // Bundle size in tiles
174
    int TSZ{};   // Tile size in pixels
175
    std::vector<Bundle> bundles{};
176
177
    Bundle &GetBundle(const char *fname);
178
179
  private:
180
    CPLErr Initialize(CPLXMLNode *CacheInfo, bool ignoreOversizedLods);
181
    CPLErr InitializeFromJSON(const CPLJSONObject &oRoot,
182
                              bool ignoreOversizedLods);
183
    CPLString compression{};
184
    std::vector<double> resolutions{};
185
    int m_nMinLOD = 0;
186
    OGRSpatialReference oSRS{};
187
    std::vector<GByte> tilebuffer{};  // Last read tile, decompressed
188
    std::vector<GByte> filebuffer{};  // raw tile buffer
189
190
    OGREnvelope m_sInitialExtent{};
191
    OGREnvelope m_sFullExtent{};
192
};
193
194
const OGRSpatialReference *ECDataset::GetSpatialRef() const
195
10
{
196
10
    return &oSRS;
197
10
}
198
199
class ECBand final : public GDALRasterBand
200
{
201
    friend class ECDataset;
202
203
  public:
204
    ECBand(ECDataset *parent, int b, int level = 0);
205
    ~ECBand() override;
206
207
    CPLErr IReadBlock(int xblk, int yblk, void *buffer) override;
208
209
    GDALColorInterp GetColorInterpretation() override
210
46
    {
211
46
        return ci;
212
46
    }
213
214
    int GetOverviewCount() override
215
31
    {
216
31
        return static_cast<int>(overviews.size());
217
31
    }
218
219
    GDALRasterBand *GetOverview(int n) override
220
3
    {
221
3
        return (n >= 0 && n < GetOverviewCount()) ? overviews[n] : nullptr;
222
3
    }
223
224
  protected:
225
  private:
226
    int lvl{};
227
    GDALColorInterp ci{};
228
229
    // Image image;
230
    void AddOverviews();
231
    std::vector<ECBand *> overviews{};
232
};
233
234
39
ECDataset::ECDataset() : isV2(true), BSZ(128), TSZ(256)
235
39
{
236
39
}
237
238
CPLErr ECDataset::Initialize(CPLXMLNode *CacheInfo, bool ignoreOversizedLods)
239
2
{
240
2
    CPLErr error = CE_None;
241
2
    try
242
2
    {
243
2
        CPLXMLNode *CSI = CPLGetXMLNode(CacheInfo, "CacheStorageInfo");
244
2
        CPLXMLNode *TCI = CPLGetXMLNode(CacheInfo, "TileCacheInfo");
245
2
        if (!CSI || !TCI)
246
0
            throw CPLString("Error parsing cache configuration");
247
2
        auto format = CPLGetXMLValue(CSI, "StorageFormat", "");
248
2
        isV2 = EQUAL(format, "esriMapCacheStorageModeCompactV2");
249
2
        if (!isV2)
250
0
            throw CPLString("Not recognized as esri V2 bundled cache");
251
2
        if (BSZ != CPLAtof(CPLGetXMLValue(CSI, "PacketSize", "128")))
252
0
            throw CPLString("Only PacketSize of 128 is supported");
253
2
        TSZ = static_cast<int>(CPLAtof(CPLGetXMLValue(TCI, "TileCols", "256")));
254
2
        if (TSZ != CPLAtof(CPLGetXMLValue(TCI, "TileRows", "256")))
255
0
            throw CPLString("Non-square tiles are not supported");
256
2
        if (TSZ < 0 || TSZ > 8192)
257
0
            throw CPLString("Unsupported TileCols value");
258
259
2
        double minx = CPLAtof(CPLGetXMLValue(TCI, "TileOrigin.X", "-180"));
260
2
        double maxy = CPLAtof(CPLGetXMLValue(TCI, "TileOrigin.Y", "90"));
261
        // Assume symmetric coverage, check custom end
262
2
        double maxx = -minx;
263
2
        double miny = -maxy;
264
2
        const char *pszmaxx = CPLGetXMLValue(TCI, "TileEnd.X", nullptr);
265
2
        const char *pszminy = CPLGetXMLValue(TCI, "TileEnd.Y", nullptr);
266
2
        if (pszmaxx && pszminy)
267
2
        {
268
2
            maxx = CPLAtof(pszmaxx);
269
2
            miny = CPLAtof(pszminy);
270
2
        }
271
272
2
        CPLXMLNode *LODInfo = CPLGetXMLNode(TCI, "LODInfos.LODInfo");
273
2
        double res = 0;
274
30
        while (LODInfo)
275
29
        {
276
29
            res = CPLAtof(CPLGetXMLValue(LODInfo, "Resolution", "0"));
277
29
            if (!(res > 0))
278
0
                throw CPLString("Can't parse resolution for LOD");
279
280
29
            double dxsz = (maxx - minx) / res;
281
29
            double dysz = (maxy - miny) / res;
282
            // Allow size just above INT32_MAX to handle FP rounding. Actual size is later clamped to INT32_MAX
283
29
            double maxRasterSize = static_cast<double>(INT32_MAX) + 2;
284
29
            if (dxsz < 1 || dxsz > maxRasterSize || dysz < 1 ||
285
28
                dysz > maxRasterSize)
286
1
            {
287
1
                if (ignoreOversizedLods)
288
0
                {
289
0
                    CPLDebug(
290
0
                        "ESRIC",
291
0
                        "Skipping resolution %.10f: raster size exceeds the "
292
0
                        "GDAL limit",
293
0
                        res);
294
0
                }
295
1
                else
296
1
                {
297
1
                    throw CPLString(
298
1
                        "Too many levels, resulting raster size exceeds "
299
1
                        "the GDAL limit. Open with IGNORE_OVERSIZED_LODS=YES "
300
1
                        "to ignore this");
301
1
                }
302
1
            }
303
28
            else
304
28
            {
305
28
                resolutions.push_back(res);
306
28
            }
307
308
28
            LODInfo = LODInfo->psNext;
309
28
        }
310
311
1
        sort(resolutions.begin(), resolutions.end());
312
1
        if (resolutions.empty())
313
0
            throw CPLString("Can't parse LODInfos");
314
315
1
        CPLString RawProj(
316
1
            CPLGetXMLValue(TCI, "SpatialReference.WKT", "EPSG:4326"));
317
1
        if (OGRERR_NONE != oSRS.SetFromUserInput(RawProj.c_str()))
318
0
            throw CPLString("Invalid Spatial Reference");
319
1
        oSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
320
321
        // resolution is the smallest figure
322
1
        res = resolutions[0];
323
1
        m_gt = GDALGeoTransform();
324
1
        m_gt.xorig = minx;
325
1
        m_gt.yorig = maxy;
326
1
        m_gt.xscale = res;
327
1
        m_gt.yscale = -res;
328
329
1
        double dxsz = (maxx - minx) / res;
330
1
        double dysz = (maxy - miny) / res;
331
332
1
        nRasterXSize = int(std::min(dxsz, double(INT32_MAX)));
333
1
        nRasterYSize = int(std::min(dysz, double(INT32_MAX)));
334
335
1
        SetMetadataItem(GDALMD_INTERLEAVE, "PIXEL", GDAL_MDD_IMAGE_STRUCTURE);
336
1
        compression =
337
1
            CPLGetXMLValue(CacheInfo, "TileImageInfo.CacheTileFormat", "JPEG");
338
1
        SetMetadataItem("COMPRESS", compression.c_str(),
339
1
                        GDAL_MDD_IMAGE_STRUCTURE);
340
341
1
        nBands = EQUAL(compression, "JPEG") ? 3 : 4;
342
5
        for (int i = 1; i <= nBands; i++)
343
4
        {
344
4
            ECBand *band = new ECBand(this, i);
345
4
            SetBand(i, band);
346
4
        }
347
        // Keep 4 bundle files open
348
1
        bundles.resize(4);
349
1
    }
350
2
    catch (CPLString &err)
351
2
    {
352
1
        error = CE_Failure;
353
1
        CPLError(error, CPLE_OpenFailed, "%s", err.c_str());
354
1
    }
355
2
    return error;
356
2
}
357
358
static std::unique_ptr<OGRSpatialReference>
359
CreateSRS(const CPLJSONObject &oSRSRoot)
360
27
{
361
27
    auto poSRS = std::make_unique<OGRSpatialReference>();
362
363
27
    bool bSuccess = false;
364
27
    const int nCode = oSRSRoot.GetInteger("wkid");
365
    // The concept of LatestWKID is explained in
366
    // https://support.esri.com/en/technical-article/000013950
367
27
    const int nLatestCode = oSRSRoot.GetInteger("latestWkid");
368
369
    // Try first with nLatestWKID as there is a higher chance it is a
370
    // EPSG code and not an ESRI one.
371
27
    if (nLatestCode > 0)
372
27
    {
373
27
        if (nLatestCode > 32767)
374
0
        {
375
0
            if (poSRS->SetFromUserInput(CPLSPrintf("ESRI:%d", nLatestCode)) ==
376
0
                OGRERR_NONE)
377
0
            {
378
0
                bSuccess = true;
379
0
            }
380
0
        }
381
27
        else if (poSRS->importFromEPSG(nLatestCode) == OGRERR_NONE)
382
27
        {
383
27
            bSuccess = true;
384
27
        }
385
27
    }
386
27
    if (!bSuccess && nCode > 0)
387
0
    {
388
0
        if (nCode > 32767)
389
0
        {
390
0
            if (poSRS->SetFromUserInput(CPLSPrintf("ESRI:%d", nCode)) ==
391
0
                OGRERR_NONE)
392
0
            {
393
0
                bSuccess = true;
394
0
            }
395
0
        }
396
0
        else if (poSRS->importFromEPSG(nCode) == OGRERR_NONE)
397
0
        {
398
0
            bSuccess = true;
399
0
        }
400
0
    }
401
27
    if (!bSuccess)
402
0
    {
403
0
        return nullptr;
404
0
    }
405
406
27
    poSRS->SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
407
27
    return poSRS;
408
27
}
409
410
CPLErr ECDataset::InitializeFromJSON(const CPLJSONObject &oRoot,
411
                                     bool ignoreOversizedLods)
412
37
{
413
37
    CPLErr error = CE_None;
414
37
    try
415
37
    {
416
37
        auto format = oRoot.GetString("storageInfo/storageFormat");
417
37
        isV2 = EQUAL(format.c_str(), "esriMapCacheStorageModeCompactV2");
418
37
        if (!isV2)
419
27
            throw CPLString("Not recognized as esri V2 bundled cache");
420
10
        if (BSZ != oRoot.GetInteger("storageInfo/packetSize"))
421
0
            throw CPLString("Only PacketSize of 128 is supported");
422
423
10
        TSZ = oRoot.GetInteger("tileInfo/rows");
424
10
        if (TSZ != oRoot.GetInteger("tileInfo/cols"))
425
0
            throw CPLString("Non-square tiles are not supported");
426
10
        if (TSZ < 0 || TSZ > 8192)
427
0
            throw CPLString("Unsupported tileInfo/rows value");
428
429
10
        double minx = oRoot.GetDouble("tileInfo/origin/x");
430
10
        double maxy = oRoot.GetDouble("tileInfo/origin/y");
431
        // Assume symmetric coverage
432
10
        double maxx = -minx;
433
10
        double miny = -maxy;
434
435
10
        const auto oLODs = oRoot.GetArray("tileInfo/lods");
436
10
        double res = 0;
437
        // we need to skip levels that don't have bundle files
438
10
        m_nMinLOD = oRoot.GetInteger("minLOD");
439
10
        if (m_nMinLOD < 0 || m_nMinLOD >= 31)
440
0
            throw CPLString("Invalid minLOD");
441
10
        const int maxLOD = std::min(oRoot.GetInteger("maxLOD"), 31);
442
10
        for (const auto &oLOD : oLODs)
443
241
        {
444
241
            const int level = oLOD.GetInteger("level");
445
241
            if (level < m_nMinLOD || level > maxLOD)
446
207
                continue;
447
448
34
            res = oLOD.GetDouble("resolution");
449
34
            if (!(res > 0))
450
0
                throw CPLString("Can't parse resolution for LOD");
451
452
34
            double dxsz = (maxx - minx) / res;
453
34
            double dysz = (maxy - miny) / res;
454
            // Allow size just above INT32_MAX to handle FP rounding. Actual size is later clamped to INT32_MAX
455
34
            double maxRasterSize = static_cast<double>(INT32_MAX) + 2;
456
34
            if (dxsz < 1 || dxsz > maxRasterSize || dysz < 1 ||
457
33
                dysz > maxRasterSize)
458
1
            {
459
1
                if (ignoreOversizedLods)
460
0
                {
461
0
                    CPLDebug("ESRIC",
462
0
                             "Skipping LOD with resolution %.10f: raster size "
463
0
                             "exceeds the GDAL limit",
464
0
                             res);
465
0
                    continue;
466
0
                }
467
1
                else
468
1
                {
469
1
                    throw CPLString(
470
1
                        "Too many levels, resulting raster size exceeds "
471
1
                        "the GDAL limit. Open with IGNORE_OVERSIZED_LODS=YES "
472
1
                        "to ignore this");
473
1
                }
474
1
            }
475
476
33
            resolutions.push_back(res);
477
33
        }
478
9
        sort(resolutions.begin(), resolutions.end());
479
9
        if (resolutions.empty())
480
0
            throw CPLString("Can't parse lods");
481
482
9
        {
483
9
            auto poSRS = CreateSRS(oRoot.GetObj("spatialReference"));
484
9
            if (!poSRS)
485
0
            {
486
0
                throw CPLString("Invalid Spatial Reference");
487
0
            }
488
9
            oSRS = std::move(*poSRS);
489
9
        }
490
491
        // resolution is the smallest figure
492
0
        res = resolutions[0];
493
9
        m_gt = GDALGeoTransform();
494
9
        m_gt.xorig = minx;
495
9
        m_gt.yorig = maxy;
496
9
        m_gt.xscale = res;
497
9
        m_gt.yscale = -res;
498
499
9
        double dxsz = (maxx - minx) / res;
500
9
        double dysz = (maxy - miny) / res;
501
502
9
        nRasterXSize = int(std::min(dxsz, double(INT32_MAX)));
503
9
        nRasterYSize = int(std::min(dysz, double(INT32_MAX)));
504
505
9
        SetMetadataItem(GDALMD_INTERLEAVE, "PIXEL", GDAL_MDD_IMAGE_STRUCTURE);
506
9
        compression = oRoot.GetString("tileImageInfo/format");
507
9
        SetMetadataItem("COMPRESS", compression.c_str(),
508
9
                        GDAL_MDD_IMAGE_STRUCTURE);
509
510
9
        auto oInitialExtent = oRoot.GetObj("initialExtent");
511
9
        if (oInitialExtent.IsValid() &&
512
9
            oInitialExtent.GetType() == CPLJSONObject::Type::Object)
513
9
        {
514
9
            m_sInitialExtent.MinX = oInitialExtent.GetDouble("xmin");
515
9
            m_sInitialExtent.MinY = oInitialExtent.GetDouble("ymin");
516
9
            m_sInitialExtent.MaxX = oInitialExtent.GetDouble("xmax");
517
9
            m_sInitialExtent.MaxY = oInitialExtent.GetDouble("ymax");
518
9
            auto oSRSRoot = oInitialExtent.GetObj("spatialReference");
519
9
            if (oSRSRoot.IsValid())
520
9
            {
521
9
                auto poSRS = CreateSRS(oSRSRoot);
522
9
                if (!poSRS)
523
0
                {
524
0
                    throw CPLString(
525
0
                        "Invalid Spatial Reference in initialExtent");
526
0
                }
527
9
                if (!poSRS->IsSame(&oSRS))
528
0
                {
529
0
                    CPLError(CE_Warning, CPLE_AppDefined,
530
0
                             "Ignoring initialExtent, because its SRS is "
531
0
                             "different from the main one");
532
0
                    m_sInitialExtent = OGREnvelope();
533
0
                }
534
9
            }
535
9
        }
536
537
9
        auto oFullExtent = oRoot.GetObj("fullExtent");
538
9
        if (oFullExtent.IsValid() &&
539
9
            oFullExtent.GetType() == CPLJSONObject::Type::Object)
540
9
        {
541
9
            m_sFullExtent.MinX = oFullExtent.GetDouble("xmin");
542
9
            m_sFullExtent.MinY = oFullExtent.GetDouble("ymin");
543
9
            m_sFullExtent.MaxX = oFullExtent.GetDouble("xmax");
544
9
            m_sFullExtent.MaxY = oFullExtent.GetDouble("ymax");
545
9
            auto oSRSRoot = oFullExtent.GetObj("spatialReference");
546
9
            if (oSRSRoot.IsValid())
547
9
            {
548
9
                auto poSRS = CreateSRS(oSRSRoot);
549
9
                if (!poSRS)
550
0
                {
551
0
                    throw CPLString("Invalid Spatial Reference in fullExtent");
552
0
                }
553
9
                if (!poSRS->IsSame(&oSRS))
554
0
                {
555
0
                    CPLError(CE_Warning, CPLE_AppDefined,
556
0
                             "Ignoring fullExtent, because its SRS is "
557
0
                             "different from the main one");
558
0
                    m_sFullExtent = OGREnvelope();
559
0
                }
560
9
            }
561
9
        }
562
563
9
        nBands = EQUAL(compression, "JPEG") ? 3 : 4;
564
45
        for (int i = 1; i <= nBands; i++)
565
36
        {
566
36
            ECBand *band = new ECBand(this, i);
567
36
            SetBand(i, band);
568
36
        }
569
        // Keep 4 bundle files open
570
9
        bundles.resize(4);
571
9
    }
572
37
    catch (CPLString &err)
573
37
    {
574
28
        error = CE_Failure;
575
28
        CPLError(error, CPLE_OpenFailed, "%s", err.c_str());
576
28
    }
577
37
    return error;
578
37
}
579
580
class ESRICProxyRasterBand final : public GDALProxyRasterBand
581
{
582
  private:
583
    GDALRasterBand *m_poUnderlyingBand = nullptr;
584
585
    CPL_DISALLOW_COPY_ASSIGN(ESRICProxyRasterBand)
586
587
  protected:
588
    GDALRasterBand *RefUnderlyingRasterBand(bool /*bForceOpen*/) const override;
589
590
  public:
591
    explicit ESRICProxyRasterBand(GDALRasterBand *poUnderlyingBand)
592
36
        : m_poUnderlyingBand(poUnderlyingBand)
593
36
    {
594
36
        nBand = poUnderlyingBand->GetBand();
595
36
        eDataType = poUnderlyingBand->GetRasterDataType();
596
36
        nRasterXSize = poUnderlyingBand->GetXSize();
597
36
        nRasterYSize = poUnderlyingBand->GetYSize();
598
36
        poUnderlyingBand->GetBlockSize(&nBlockXSize, &nBlockYSize);
599
36
    }
600
};
601
602
GDALRasterBand *
603
ESRICProxyRasterBand::RefUnderlyingRasterBand(bool /*bForceOpen*/) const
604
165
{
605
165
    return m_poUnderlyingBand;
606
165
}
607
608
class ESRICProxyDataset final : public GDALProxyDataset
609
{
610
  private:
611
    // m_poSrcDS must be placed before m_poUnderlyingDS for proper destruction
612
    // as m_poUnderlyingDS references m_poSrcDS
613
    std::unique_ptr<GDALDataset> m_poSrcDS{};
614
    std::unique_ptr<GDALDataset> m_poUnderlyingDS{};
615
    CPLStringList m_aosFileList{};
616
617
  protected:
618
    GDALDataset *RefUnderlyingDataset() const override;
619
620
  public:
621
    ESRICProxyDataset(GDALDataset *poSrcDS, GDALDataset *poUnderlyingDS,
622
                      const char *pszDescription)
623
9
        : m_poSrcDS(poSrcDS), m_poUnderlyingDS(poUnderlyingDS)
624
9
    {
625
9
        nRasterXSize = poUnderlyingDS->GetRasterXSize();
626
9
        nRasterYSize = poUnderlyingDS->GetRasterYSize();
627
45
        for (int i = 0; i < poUnderlyingDS->GetRasterCount(); ++i)
628
36
            SetBand(i + 1, new ESRICProxyRasterBand(
629
36
                               poUnderlyingDS->GetRasterBand(i + 1)));
630
9
        m_aosFileList.AddString(pszDescription);
631
9
    }
632
633
    GDALDriver *GetDriver() const override
634
9
    {
635
9
        return GDALDriver::FromHandle(GDALGetDriverByName("ESRIC"));
636
9
    }
637
638
    char **GetFileList() override
639
18
    {
640
18
        return CSLDuplicate(m_aosFileList.List());
641
18
    }
642
};
643
644
GDALDataset *ESRICProxyDataset::RefUnderlyingDataset() const
645
117
{
646
117
    return m_poUnderlyingDS.get();
647
117
}
648
649
GDALDataset *ECDataset::Open(GDALOpenInfo *poOpenInfo)
650
582
{
651
582
    return Open(poOpenInfo, poOpenInfo->pszFilename);
652
582
}
653
654
GDALDataset *ECDataset::Open(GDALOpenInfo *poOpenInfo,
655
                             const char *pszDescription)
656
1.06k
{
657
1.06k
    bool ignoreOversizedLods = CPL_TO_BOOL(CSLFetchBoolean(
658
1.06k
        poOpenInfo->papszOpenOptions, "IGNORE_OVERSIZED_LODS", FALSE));
659
1.06k
    if (IdentifyXML(poOpenInfo))
660
20
    {
661
20
        CPLXMLNode *config = CPLParseXMLFile(poOpenInfo->pszFilename);
662
20
        if (!config)  // Error was reported from parsing XML
663
11
            return nullptr;
664
9
        CPLXMLNode *CacheInfo = CPLGetXMLNode(config, "=CacheInfo");
665
9
        if (!CacheInfo)
666
7
        {
667
7
            CPLError(
668
7
                CE_Warning, CPLE_OpenFailed,
669
7
                "Error parsing configuration, can't find CacheInfo element");
670
7
            CPLDestroyXMLNode(config);
671
7
            return nullptr;
672
7
        }
673
2
        auto ds = new ECDataset();
674
2
        ds->dname = CPLGetDirnameSafe(poOpenInfo->pszFilename) + "/_alllayers";
675
2
        CPLErr error = ds->Initialize(CacheInfo, ignoreOversizedLods);
676
2
        CPLDestroyXMLNode(config);
677
2
        if (CE_None != error)
678
1
        {
679
1
            delete ds;
680
1
            ds = nullptr;
681
1
        }
682
2
        return ds;
683
9
    }
684
1.04k
    else if (IdentifyJSON(poOpenInfo))
685
573
    {
686
        // Recognize .tpkx file directly passed
687
573
        if (!STARTS_WITH(poOpenInfo->pszFilename, "/vsizip/") &&
688
#if !defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION)
689
            ENDS_WITH_CI(poOpenInfo->pszFilename, ".tpkx") &&
690
#endif
691
562
            memcmp(poOpenInfo->pabyHeader, "PK\x03\x04", 4) == 0)
692
482
        {
693
482
            GDALOpenInfo oOpenInfo((std::string("/vsizip/{") +
694
482
                                    poOpenInfo->pszFilename + "}/root.json")
695
482
                                       .c_str(),
696
482
                                   GA_ReadOnly);
697
482
            oOpenInfo.papszOpenOptions = poOpenInfo->papszOpenOptions;
698
482
            return Open(&oOpenInfo, pszDescription);
699
482
        }
700
701
91
        CPLJSONDocument oJSONDocument;
702
91
        if (!oJSONDocument.Load(poOpenInfo->pszFilename))
703
54
        {
704
54
            CPLError(CE_Warning, CPLE_OpenFailed,
705
54
                     "Error parsing configuration");
706
54
            return nullptr;
707
54
        }
708
709
37
        const CPLJSONObject &oRoot = oJSONDocument.GetRoot();
710
37
        if (!oRoot.IsValid())
711
0
        {
712
0
            CPLError(CE_Warning, CPLE_OpenFailed, "Invalid json document root");
713
0
            return nullptr;
714
0
        }
715
716
37
        auto ds = std::make_unique<ECDataset>();
717
37
        auto tileBundlesPath = oRoot.GetString("tileBundlesPath");
718
        // Strip leading relative path indicator (if present)
719
37
        if (tileBundlesPath.substr(0, 2) == "./")
720
10
        {
721
10
            tileBundlesPath.erase(0, 2);
722
10
        }
723
724
37
        ds->dname.Printf("%s/%s",
725
37
                         CPLGetDirnameSafe(poOpenInfo->pszFilename).c_str(),
726
37
                         tileBundlesPath.c_str());
727
37
        CPLErr error = ds->InitializeFromJSON(oRoot, ignoreOversizedLods);
728
37
        if (CE_None != error)
729
28
        {
730
28
            return nullptr;
731
28
        }
732
733
9
        const bool bIsFullExtentValid =
734
9
            (ds->m_sFullExtent.IsInit() &&
735
9
             ds->m_sFullExtent.MinX < ds->m_sFullExtent.MaxX &&
736
9
             ds->m_sFullExtent.MinY < ds->m_sFullExtent.MaxY);
737
9
        const char *pszExtentSource =
738
9
            CSLFetchNameValue(poOpenInfo->papszOpenOptions, "EXTENT_SOURCE");
739
740
9
        CPLStringList aosOptions;
741
9
        if ((!pszExtentSource && bIsFullExtentValid) ||
742
0
            (pszExtentSource && EQUAL(pszExtentSource, "FULL_EXTENT")))
743
9
        {
744
9
            if (!bIsFullExtentValid)
745
0
            {
746
0
                CPLError(CE_Failure, CPLE_AppDefined,
747
0
                         "fullExtent is not valid");
748
0
                return nullptr;
749
0
            }
750
9
            aosOptions.AddString("-projwin");
751
9
            aosOptions.AddString(CPLSPrintf("%.17g", ds->m_sFullExtent.MinX));
752
9
            aosOptions.AddString(CPLSPrintf("%.17g", ds->m_sFullExtent.MaxY));
753
9
            aosOptions.AddString(CPLSPrintf("%.17g", ds->m_sFullExtent.MaxX));
754
9
            aosOptions.AddString(CPLSPrintf("%.17g", ds->m_sFullExtent.MinY));
755
9
        }
756
0
        else if (pszExtentSource && EQUAL(pszExtentSource, "INITIAL_EXTENT"))
757
0
        {
758
0
            const bool bIsInitialExtentValid =
759
0
                (ds->m_sInitialExtent.IsInit() &&
760
0
                 ds->m_sInitialExtent.MinX < ds->m_sInitialExtent.MaxX &&
761
0
                 ds->m_sInitialExtent.MinY < ds->m_sInitialExtent.MaxY);
762
0
            if (!bIsInitialExtentValid)
763
0
            {
764
0
                CPLError(CE_Failure, CPLE_AppDefined,
765
0
                         "initialExtent is not valid");
766
0
                return nullptr;
767
0
            }
768
0
            aosOptions.AddString("-projwin");
769
0
            aosOptions.AddString(
770
0
                CPLSPrintf("%.17g", ds->m_sInitialExtent.MinX));
771
0
            aosOptions.AddString(
772
0
                CPLSPrintf("%.17g", ds->m_sInitialExtent.MaxY));
773
0
            aosOptions.AddString(
774
0
                CPLSPrintf("%.17g", ds->m_sInitialExtent.MaxX));
775
0
            aosOptions.AddString(
776
0
                CPLSPrintf("%.17g", ds->m_sInitialExtent.MinY));
777
0
        }
778
779
9
        if (!aosOptions.empty())
780
9
        {
781
9
            aosOptions.AddString("-of");
782
9
            aosOptions.AddString("VRT");
783
9
            aosOptions.AddString("-co");
784
9
            aosOptions.AddString(CPLSPrintf("BLOCKXSIZE=%d", ds->TSZ));
785
9
            aosOptions.AddString("-co");
786
9
            aosOptions.AddString(CPLSPrintf("BLOCKYSIZE=%d", ds->TSZ));
787
9
            auto psOptions =
788
9
                GDALTranslateOptionsNew(aosOptions.List(), nullptr);
789
9
            auto hDS = GDALTranslate("", GDALDataset::ToHandle(ds.get()),
790
9
                                     psOptions, nullptr);
791
9
            GDALTranslateOptionsFree(psOptions);
792
9
            if (!hDS)
793
0
            {
794
0
                return nullptr;
795
0
            }
796
9
            return new ESRICProxyDataset(
797
9
                ds.release(), GDALDataset::FromHandle(hDS), pszDescription);
798
9
        }
799
0
        return ds.release();
800
9
    }
801
471
    return nullptr;
802
1.06k
}
803
804
// Fetch a reference to an initialized bundle, based on file name
805
// The returned bundle could still have an invalid file handle, if the
806
// target bundle is not valid
807
Bundle &ECDataset::GetBundle(const char *fname)
808
205
{
809
205
    for (auto &bundle : bundles)
810
235
    {
811
        // If a bundle is missing, it still occupies a slot, with fh == nullptr
812
235
        if (EQUAL(bundle.name.c_str(), fname))
813
195
            return bundle;
814
235
    }
815
    // Not found, look for an empty // missing slot
816
10
    for (auto &bundle : bundles)
817
10
    {
818
10
        if (nullptr == bundle.fh)
819
10
        {
820
10
            bundle.Init(fname);
821
10
            return bundle;
822
10
        }
823
10
    }
824
    // No empties, eject one
825
0
    Bundle &bundle = bundles[
826
0
#ifndef __COVERITY__
827
0
        rand() % bundles.size()
828
#else
829
        0
830
#endif
831
0
    ];
832
0
    bundle.Init(fname);
833
0
    return bundle;
834
10
}
835
836
ECBand::~ECBand()
837
52
{
838
52
    for (auto ovr : overviews)
839
12
        if (ovr)
840
12
            delete ovr;
841
52
    overviews.clear();
842
52
}
843
844
ECBand::ECBand(ECDataset *parent, int b, int level)
845
52
    : lvl(level), ci(GCI_Undefined)
846
52
{
847
52
    static const GDALColorInterp rgba[4] = {GCI_RedBand, GCI_GreenBand,
848
52
                                            GCI_BlueBand, GCI_AlphaBand};
849
52
    static const GDALColorInterp la[2] = {GCI_GrayIndex, GCI_AlphaBand};
850
52
    poDS = parent;
851
52
    nBand = b;
852
853
52
    double factor = parent->resolutions[0] / parent->resolutions[lvl];
854
52
    nRasterXSize = static_cast<int>(parent->nRasterXSize * factor + 0.5);
855
52
    nRasterYSize = static_cast<int>(parent->nRasterYSize * factor + 0.5);
856
52
    nBlockXSize = nBlockYSize = parent->TSZ;
857
858
    // Default color interpretation
859
52
    assert(b - 1 >= 0);
860
52
    if (parent->nBands >= 3)
861
52
    {
862
52
        assert(b - 1 < static_cast<int>(CPL_ARRAYSIZE(rgba)));
863
52
        ci = rgba[b - 1];
864
52
    }
865
0
    else
866
0
    {
867
0
        assert(b - 1 < static_cast<int>(CPL_ARRAYSIZE(la)));
868
0
        ci = la[b - 1];
869
0
    }
870
52
    if (0 == lvl)
871
40
        AddOverviews();
872
52
}
873
874
void ECBand::AddOverviews()
875
40
{
876
40
    auto parent = cpl::down_cast<ECDataset *>(poDS);
877
52
    for (size_t i = 1; i < parent->resolutions.size(); i++)
878
12
    {
879
12
        ECBand *ovl = new ECBand(parent, nBand, int(i));
880
12
        if (!ovl)
881
0
            break;
882
12
        overviews.push_back(ovl);
883
12
    }
884
40
}
885
886
CPLErr ECBand::IReadBlock(int nBlockXOff, int nBlockYOff, void *pData)
887
205
{
888
205
    auto parent = cpl::down_cast<ECDataset *>(poDS);
889
205
    auto &buffer = parent->tilebuffer;
890
205
    auto TSZ = parent->TSZ;
891
205
    auto BSZ = parent->BSZ;
892
205
    size_t nBytes = size_t(TSZ) * TSZ;
893
894
205
    buffer.resize(nBytes * parent->nBands);
895
896
205
    const int lxx = parent->m_nMinLOD +
897
205
                    static_cast<int>(parent->resolutions.size() - lvl - 1);
898
205
    int bx, by;
899
205
    bx = (nBlockXOff / BSZ) * BSZ;
900
205
    by = (nBlockYOff / BSZ) * BSZ;
901
205
    CPLString fname;
902
205
    fname = CPLString().Printf("%s/L%02d/R%04xC%04x.bundle",
903
205
                               parent->dname.c_str(), lxx, by, bx);
904
205
    Bundle &bundle = parent->GetBundle(fname);
905
205
    if (nullptr == bundle.fh)
906
64
    {  // This is not an error in general, bundles can be missing
907
64
        CPLDebug("ESRIC", "Can't open bundle %s", fname.c_str());
908
64
        memset(pData, 0, nBytes);
909
64
        return CE_None;
910
64
    }
911
141
    int block = static_cast<int>((nBlockYOff % BSZ) * BSZ + (nBlockXOff % BSZ));
912
141
    GUInt64 offset = bundle.index[block] & 0xffffffffffull;
913
141
    GUInt64 size = bundle.index[block] >> 40;
914
141
    if (0 == size)
915
0
    {
916
0
        memset(pData, 0, nBytes);
917
0
        return CE_None;
918
0
    }
919
141
    auto &fbuffer = parent->filebuffer;
920
141
    fbuffer.resize(size_t(size));
921
141
    bundle.fh->Seek(offset, SEEK_SET);
922
141
    if (size != bundle.fh->Read(fbuffer.data(), size_t(1), size_t(size)))
923
4
    {
924
4
        CPLError(CE_Failure, CPLE_FileIO,
925
4
                 "Error reading tile, reading " CPL_FRMT_GUIB
926
4
                 " at " CPL_FRMT_GUIB,
927
4
                 GUInt64(size), GUInt64(offset));
928
4
        return CE_Failure;
929
4
    }
930
137
    const CPLString magic(VSIMemGenerateHiddenFilename("esric.tmp"));
931
137
    auto mfh = VSIFileFromMemBuffer(magic.c_str(), fbuffer.data(), size, false);
932
137
    VSIFCloseL(mfh);
933
    // Can't open a raster by handle?
934
137
    auto inds = std::unique_ptr<GDALDataset>(GDALDataset::Open(
935
137
        magic.c_str(), GDAL_OF_RASTER | GDAL_OF_VERBOSE_ERROR));
936
137
    if (!inds)
937
4
    {
938
4
        VSIUnlink(magic.c_str());
939
4
        CPLError(CE_Failure, CPLE_FileIO, "Error opening tile");
940
4
        return CE_Failure;
941
4
    }
942
    // Duplicate first band if not sufficient bands are provided
943
133
    auto inbands = inds->GetRasterCount();
944
133
    int ubands[4] = {1, 1, 1, 1};
945
133
    int *usebands = nullptr;
946
133
    int bandcount = parent->nBands;
947
133
    GDALColorTableH hCT = nullptr;
948
133
    if (inbands != bandcount)
949
43
    {
950
        // Opaque if output expects alpha channel
951
43
        if (0 == bandcount % 2)
952
43
        {
953
43
            fill(buffer.begin(), buffer.end(), GByte(255));
954
43
            bandcount--;
955
43
        }
956
43
        if (3 == inbands)
957
0
        {
958
            // Lacking opacity, copy the first three bands
959
0
            ubands[1] = 2;
960
0
            ubands[2] = 3;
961
0
            usebands = ubands;
962
0
        }
963
43
        else if (1 == inbands)
964
43
        {
965
            // Grayscale, expecting color
966
43
            usebands = ubands;
967
            // Check for the color table of 1 band rasters
968
43
            hCT = GDALGetRasterColorTable(
969
43
                GDALGetRasterBand(GDALDataset::ToHandle(inds.get()), 1));
970
43
        }
971
43
    }
972
973
133
    auto errcode = CE_None;
974
133
    if (nullptr != hCT)
975
43
    {
976
        // Expand color indexed to RGB(A)
977
43
        errcode = GDALDatasetRasterIO(GDALDataset::ToHandle(inds.get()),
978
43
                                      GF_Read, 0, 0, TSZ, TSZ, buffer.data(),
979
43
                                      TSZ, TSZ, GDT_UInt8, 1, usebands,
980
43
                                      parent->nBands, parent->nBands * TSZ, 1);
981
43
        if (CE_None == errcode)
982
43
        {
983
43
            GByte abyCT[4 * 256];
984
43
            GByte *pabyTileData = buffer.data();
985
43
            const int nEntries = std::min(256, GDALGetColorEntryCount(hCT));
986
142
            for (int i = 0; i < nEntries; i++)
987
99
            {
988
99
                const GDALColorEntry *psEntry = GDALGetColorEntry(hCT, i);
989
99
                abyCT[4 * i] = static_cast<GByte>(psEntry->c1);
990
99
                abyCT[4 * i + 1] = static_cast<GByte>(psEntry->c2);
991
99
                abyCT[4 * i + 2] = static_cast<GByte>(psEntry->c3);
992
99
                abyCT[4 * i + 3] = static_cast<GByte>(psEntry->c4);
993
99
            }
994
10.9k
            for (int i = nEntries; i < 256; i++)
995
10.9k
            {
996
10.9k
                abyCT[4 * i] = 0;
997
10.9k
                abyCT[4 * i + 1] = 0;
998
10.9k
                abyCT[4 * i + 2] = 0;
999
10.9k
                abyCT[4 * i + 3] = 0;
1000
10.9k
            }
1001
1002
43
            if (parent->nBands == 4)
1003
43
            {
1004
2.81M
                for (size_t i = 0; i < nBytes; i++)
1005
2.81M
                {
1006
2.81M
                    const GByte byVal = pabyTileData[4 * i];
1007
2.81M
                    pabyTileData[4 * i] = abyCT[4 * byVal];
1008
2.81M
                    pabyTileData[4 * i + 1] = abyCT[4 * byVal + 1];
1009
2.81M
                    pabyTileData[4 * i + 2] = abyCT[4 * byVal + 2];
1010
2.81M
                    pabyTileData[4 * i + 3] = abyCT[4 * byVal + 3];
1011
2.81M
                }
1012
43
            }
1013
0
            else if (parent->nBands == 3)
1014
0
            {
1015
0
                for (size_t i = 0; i < nBytes; i++)
1016
0
                {
1017
0
                    const GByte byVal = pabyTileData[3 * i];
1018
0
                    pabyTileData[3 * i] = abyCT[4 * byVal];
1019
0
                    pabyTileData[3 * i + 1] = abyCT[4 * byVal + 1];
1020
0
                    pabyTileData[3 * i + 2] = abyCT[4 * byVal + 2];
1021
0
                }
1022
0
            }
1023
0
            else
1024
0
            {
1025
                // Assuming grayscale output
1026
0
                for (size_t i = 0; i < nBytes; i++)
1027
0
                {
1028
0
                    const GByte byVal = pabyTileData[i];
1029
0
                    pabyTileData[i] = abyCT[4 * byVal];
1030
0
                }
1031
0
            }
1032
43
        }
1033
43
    }
1034
90
    else
1035
90
    {
1036
90
        errcode = GDALDatasetRasterIO(GDALDataset::ToHandle(inds.get()),
1037
90
                                      GF_Read, 0, 0, TSZ, TSZ, buffer.data(),
1038
90
                                      TSZ, TSZ, GDT_UInt8, bandcount, usebands,
1039
90
                                      parent->nBands, parent->nBands * TSZ, 1);
1040
90
    }
1041
133
    inds.reset();
1042
133
    VSIUnlink(magic.c_str());
1043
    // Error while unpacking tile
1044
133
    if (CE_None != errcode)
1045
20
        return errcode;
1046
1047
565
    for (int iBand = 1; iBand <= parent->nBands; iBand++)
1048
452
    {
1049
452
        auto band = parent->GetRasterBand(iBand);
1050
452
        if (lvl)
1051
0
            band = band->GetOverview(lvl - 1);
1052
452
        GDALRasterBlock *poBlock = nullptr;
1053
452
        if (band != this)
1054
339
        {
1055
339
            poBlock = band->GetLockedBlockRef(nBlockXOff, nBlockYOff, 1);
1056
339
            if (poBlock != nullptr)
1057
339
            {
1058
339
                GDALCopyWords(buffer.data() + iBand - 1, GDT_UInt8,
1059
339
                              parent->nBands, poBlock->GetDataRef(), GDT_UInt8,
1060
339
                              1, TSZ * TSZ);
1061
339
                poBlock->DropLock();
1062
339
            }
1063
339
        }
1064
113
        else
1065
113
        {
1066
113
            GDALCopyWords(buffer.data() + iBand - 1, GDT_UInt8, parent->nBands,
1067
113
                          pData, GDT_UInt8, 1, TSZ * TSZ);
1068
113
        }
1069
452
    }
1070
1071
113
    return CE_None;
1072
133
}  // IReadBlock
1073
1074
}  // namespace ESRIC
1075
1076
void CPL_DLL GDALRegister_ESRIC()
1077
24
{
1078
24
    if (GDALGetDriverByName("ESRIC") != nullptr)
1079
0
        return;
1080
1081
24
    auto poDriver = new GDALDriver;
1082
1083
24
    poDriver->SetDescription("ESRIC");
1084
24
    poDriver->SetMetadataItem(GDAL_DCAP_RASTER, "YES");
1085
24
    poDriver->SetMetadataItem(GDAL_DCAP_VIRTUALIO, "YES");
1086
24
    poDriver->SetMetadataItem(GDAL_DMD_LONGNAME, "Esri Compact Cache");
1087
1088
24
    poDriver->SetMetadataItem(GDAL_DMD_EXTENSIONS, "json tpkx");
1089
1090
24
    poDriver->SetMetadataItem(
1091
24
        GDAL_DMD_OPENOPTIONLIST,
1092
24
        "<OpenOptionList>"
1093
24
        "  <Option name='EXTENT_SOURCE' type='string-select' "
1094
24
        "description='Which source is used to determine the extent' "
1095
24
        "default='FULL_EXTENT'>"
1096
24
        "    <Value>FULL_EXTENT</Value>"
1097
24
        "    <Value>INITIAL_EXTENT</Value>"
1098
24
        "    <Value>TILING_SCHEME</Value>"
1099
24
        "  </Option>"
1100
24
        "  <Option name='IGNORE_OVERSIZED_LODS' type='boolean' "
1101
24
        "description='Whether to silently ignore LODs that exceed the "
1102
24
        "maximum size supported by GDAL (INT32_MAX)' "
1103
24
        "default='NO'>"
1104
24
        "  </Option>"
1105
24
        "</OpenOptionList>");
1106
24
    poDriver->pfnIdentify = ESRIC::Identify;
1107
24
    poDriver->pfnOpen = ESRIC::ECDataset::Open;
1108
24
    poDriver->pfnDelete = ESRIC::Delete;
1109
1110
24
    GetGDALDriverManager()->RegisterDriver(poDriver);
1111
24
}