Coverage Report

Created: 2025-08-11 09:23

/src/gdal/ogr/ogrsf_frmts/pmtiles/ogrpmtilesfrommbtiles.cpp
Line
Count
Source (jump to first uncovered line)
1
/******************************************************************************
2
 *
3
 * Project:  OpenGIS Simple Features Reference Implementation
4
 * Purpose:  Implementation of PMTiles
5
 * Author:   Even Rouault <even.rouault at spatialys.com>
6
 *
7
 ******************************************************************************
8
 * Copyright (c) 2023, Planet Labs
9
 *
10
 * SPDX-License-Identifier: MIT
11
 ****************************************************************************/
12
13
#include "cpl_json.h"
14
15
#include "ogrsf_frmts.h"
16
#include "ogrpmtilesfrommbtiles.h"
17
18
#include "include_pmtiles.h"
19
20
#include "cpl_compressor.h"
21
#include "cpl_md5.h"
22
#include "cpl_string.h"
23
#include "cpl_vsi_virtual.h"
24
25
#include <algorithm>
26
#include <array>
27
#include <cassert>
28
#include <unordered_map>
29
#include <utility>
30
31
/************************************************************************/
32
/*                         ProcessMetadata()                            */
33
/************************************************************************/
34
35
static bool ProcessMetadata(GDALDataset *poSQLiteDS, pmtiles::headerv3 &sHeader,
36
                            std::string &osMetadata)
37
0
{
38
39
0
    auto poMetadata = poSQLiteDS->GetLayerByName("metadata");
40
0
    if (!poMetadata)
41
0
    {
42
0
        CPLError(CE_Failure, CPLE_AppDefined, "metadata table not found");
43
0
        return false;
44
0
    }
45
46
0
    const int iName = poMetadata->GetLayerDefn()->GetFieldIndex("name");
47
0
    const int iValue = poMetadata->GetLayerDefn()->GetFieldIndex("value");
48
0
    if (iName < 0 || iValue < 0)
49
0
    {
50
0
        CPLError(CE_Failure, CPLE_AppDefined,
51
0
                 "Bad structure for metadata table");
52
0
        return false;
53
0
    }
54
55
0
    CPLJSONObject oObj;
56
0
    CPLJSONDocument oJsonDoc;
57
0
    for (auto &&poFeature : poMetadata)
58
0
    {
59
0
        const char *pszName = poFeature->GetFieldAsString(iName);
60
0
        const char *pszValue = poFeature->GetFieldAsString(iValue);
61
0
        if (EQUAL(pszName, "json"))
62
0
        {
63
0
            if (!oJsonDoc.LoadMemory(pszValue))
64
0
            {
65
0
                CPLError(CE_Failure, CPLE_AppDefined,
66
0
                         "Cannot parse 'json' metadata item");
67
0
                return false;
68
0
            }
69
0
            for (const auto &oChild : oJsonDoc.GetRoot().GetChildren())
70
0
            {
71
0
                oObj.Add(oChild.GetName(), oChild);
72
0
            }
73
0
        }
74
0
        else
75
0
        {
76
0
            oObj.Add(pszName, pszValue);
77
0
        }
78
0
    }
79
80
    // MBTiles advertises scheme=tms. Override this
81
0
    oObj.Set("scheme", "xyz");
82
83
0
    const auto osFormat = oObj.GetString("format", "{missing}");
84
0
    if (osFormat != "pbf")
85
0
    {
86
0
        CPLError(CE_Failure, CPLE_AppDefined, "format=%s unhandled",
87
0
                 osFormat.c_str());
88
0
        return false;
89
0
    }
90
91
0
    int nMinZoom = atoi(oObj.GetString("minzoom", "-1").c_str());
92
0
    if (nMinZoom < 0 || nMinZoom > 255)
93
0
    {
94
0
        CPLError(CE_Failure, CPLE_AppDefined, "Missing or invalid minzoom");
95
0
        return false;
96
0
    }
97
98
0
    int nMaxZoom = atoi(oObj.GetString("maxzoom", "-1").c_str());
99
0
    if (nMaxZoom < 0 || nMaxZoom > 255)
100
0
    {
101
0
        CPLError(CE_Failure, CPLE_AppDefined, "Missing or invalid maxzoom");
102
0
        return false;
103
0
    }
104
105
0
    const CPLStringList aosCenter(
106
0
        CSLTokenizeString2(oObj.GetString("center").c_str(), ",", 0));
107
0
    if (aosCenter.size() != 3)
108
0
    {
109
0
        CPLError(CE_Failure, CPLE_AppDefined, "Expected 3 values for center");
110
0
        return false;
111
0
    }
112
0
    const double dfCenterLong = CPLAtof(aosCenter[0]);
113
0
    const double dfCenterLat = CPLAtof(aosCenter[1]);
114
0
    if (std::fabs(dfCenterLong) > 180 || std::fabs(dfCenterLat) > 90)
115
0
    {
116
0
        CPLError(CE_Failure, CPLE_AppDefined, "Invalid center");
117
0
        return false;
118
0
    }
119
0
    const int nCenterZoom = atoi(aosCenter[2]);
120
0
    if (nCenterZoom < 0 || nCenterZoom > 255)
121
0
    {
122
0
        CPLError(CE_Failure, CPLE_AppDefined, "Missing or invalid center zoom");
123
0
        return false;
124
0
    }
125
126
0
    const CPLStringList aosBounds(
127
0
        CSLTokenizeString2(oObj.GetString("bounds").c_str(), ",", 0));
128
0
    if (aosBounds.size() != 4)
129
0
    {
130
0
        CPLError(CE_Failure, CPLE_AppDefined, "Expected 4 values for bounds");
131
0
        return false;
132
0
    }
133
0
    const double dfMinX = CPLAtof(aosBounds[0]);
134
0
    const double dfMinY = CPLAtof(aosBounds[1]);
135
0
    const double dfMaxX = CPLAtof(aosBounds[2]);
136
0
    const double dfMaxY = CPLAtof(aosBounds[3]);
137
0
    if (std::fabs(dfMinX) > 180 || std::fabs(dfMinY) > 90 ||
138
0
        std::fabs(dfMaxX) > 180 || std::fabs(dfMaxY) > 90)
139
0
    {
140
0
        CPLError(CE_Failure, CPLE_AppDefined, "Invalid bounds");
141
0
        return false;
142
0
    }
143
144
0
    CPLJSONDocument oMetadataDoc;
145
0
    oMetadataDoc.SetRoot(oObj);
146
0
    osMetadata = oMetadataDoc.SaveAsString();
147
    // CPLDebugOnly("PMTiles", "Metadata = %s", osMetadata.c_str());
148
149
0
    sHeader.root_dir_offset = 127;
150
0
    sHeader.root_dir_bytes = 0;
151
0
    sHeader.json_metadata_offset = 0;
152
0
    sHeader.json_metadata_bytes = 0;
153
0
    sHeader.leaf_dirs_offset = 0;
154
0
    sHeader.leaf_dirs_bytes = 0;
155
0
    sHeader.tile_data_offset = 0;
156
0
    sHeader.tile_data_bytes = 0;
157
0
    sHeader.addressed_tiles_count = 0;
158
0
    sHeader.tile_entries_count = 0;
159
0
    sHeader.tile_contents_count = 0;
160
0
    sHeader.clustered = true;
161
0
    sHeader.internal_compression = pmtiles::COMPRESSION_GZIP;
162
0
    sHeader.tile_compression = pmtiles::COMPRESSION_GZIP;
163
0
    sHeader.tile_type = pmtiles::TILETYPE_MVT;
164
0
    sHeader.min_zoom = static_cast<uint8_t>(nMinZoom);
165
0
    sHeader.max_zoom = static_cast<uint8_t>(nMaxZoom);
166
0
    sHeader.min_lon_e7 = static_cast<int32_t>(dfMinX * 10e6);
167
0
    sHeader.min_lat_e7 = static_cast<int32_t>(dfMinY * 10e6);
168
0
    sHeader.max_lon_e7 = static_cast<int32_t>(dfMaxX * 10e6);
169
0
    sHeader.max_lat_e7 = static_cast<int32_t>(dfMaxY * 10e6);
170
0
    sHeader.center_zoom = static_cast<uint8_t>(nCenterZoom);
171
0
    sHeader.center_lon_e7 = static_cast<int32_t>(dfCenterLong * 10e6);
172
0
    sHeader.center_lat_e7 = static_cast<int32_t>(dfCenterLat * 10e6);
173
174
0
    return true;
175
0
}
176
177
/************************************************************************/
178
/*                               HashArray()                            */
179
/************************************************************************/
180
181
// From https://codereview.stackexchange.com/questions/171999/specializing-stdhash-for-stdarray
182
// We do not use std::hash<std::array<T, N>> as the name of the struct
183
// because with gcc 5.4 we get the following error:
184
// https://stackoverflow.com/questions/25594644/warning-specialization-of-template-in-different-namespace
185
template <class T, size_t N> struct HashArray
186
{
187
    CPL_NOSANITIZE_UNSIGNED_INT_OVERFLOW
188
    size_t operator()(const std::array<T, N> &key) const
189
0
    {
190
0
        std::hash<T> hasher;
191
0
        size_t result = 0;
192
0
        for (size_t i = 0; i < N; ++i)
193
0
        {
194
0
            result = result * 31 + hasher(key[i]);
195
0
        }
196
0
        return result;
197
0
    }
198
};
199
200
/************************************************************************/
201
/*                    OGRPMTilesConvertFromMBTiles()                    */
202
/************************************************************************/
203
204
bool OGRPMTilesConvertFromMBTiles(const char *pszDestName,
205
                                  const char *pszSrcName)
206
0
{
207
0
    const char *const apszAllowedDrivers[] = {"SQLite", nullptr};
208
0
    auto poSQLiteDS = std::unique_ptr<GDALDataset>(
209
0
        GDALDataset::Open(pszSrcName, GDAL_OF_VECTOR, apszAllowedDrivers));
210
0
    if (!poSQLiteDS)
211
0
    {
212
0
        CPLError(CE_Failure, CPLE_AppDefined,
213
0
                 "Cannot open %s with SQLite driver", pszSrcName);
214
0
        return false;
215
0
    }
216
217
0
    pmtiles::headerv3 sHeader;
218
0
    std::string osMetadata;
219
0
    if (!ProcessMetadata(poSQLiteDS.get(), sHeader, osMetadata))
220
0
        return false;
221
222
0
    auto poTilesLayer = poSQLiteDS->GetLayerByName("tiles");
223
0
    if (!poTilesLayer)
224
0
    {
225
0
        CPLError(CE_Failure, CPLE_AppDefined, "tiles table not found");
226
0
        return false;
227
0
    }
228
229
0
    const int iZoomLevel =
230
0
        poTilesLayer->GetLayerDefn()->GetFieldIndex("zoom_level");
231
0
    const int iTileColumn =
232
0
        poTilesLayer->GetLayerDefn()->GetFieldIndex("tile_column");
233
0
    const int iTileRow =
234
0
        poTilesLayer->GetLayerDefn()->GetFieldIndex("tile_row");
235
0
    const int iTileData =
236
0
        poTilesLayer->GetLayerDefn()->GetFieldIndex("tile_data");
237
0
    if (iZoomLevel < 0 || iTileColumn < 0 || iTileRow < 0 || iTileData < 0)
238
0
    {
239
0
        CPLError(CE_Failure, CPLE_AppDefined, "Bad structure for tiles table");
240
0
        return false;
241
0
    }
242
243
0
    struct TileEntry
244
0
    {
245
0
        uint64_t nTileId;
246
0
        std::array<unsigned char, 16> abyMD5;
247
0
    };
248
249
    // In a first step browse through the tiles table to compute the PMTiles
250
    // tile_id of each tile, and compute a hash of the tile data for
251
    // deduplication
252
0
    std::vector<TileEntry> asTileEntries;
253
0
    for (auto &&poFeature : poTilesLayer)
254
0
    {
255
0
        const int nZoomLevel = poFeature->GetFieldAsInteger(iZoomLevel);
256
0
        if (nZoomLevel < 0 || nZoomLevel > 30)
257
0
        {
258
0
            CPLError(CE_Warning, CPLE_AppDefined,
259
0
                     "Skipping tile with missing or invalid zoom_level");
260
0
            continue;
261
0
        }
262
0
        const int nColumn = poFeature->GetFieldAsInteger(iTileColumn);
263
0
        if (nColumn < 0 || nColumn >= (1 << nZoomLevel))
264
0
        {
265
0
            CPLError(CE_Warning, CPLE_AppDefined,
266
0
                     "Skipping tile with missing or invalid tile_column");
267
0
            continue;
268
0
        }
269
0
        const int nRow = poFeature->GetFieldAsInteger(iTileRow);
270
0
        if (nRow < 0 || nRow >= (1 << nZoomLevel))
271
0
        {
272
0
            CPLError(CE_Warning, CPLE_AppDefined,
273
0
                     "Skipping tile with missing or invalid tile_row");
274
0
            continue;
275
0
        }
276
        // MBTiles uses a 0=bottom-most row, whereas PMTiles uses
277
        // 0=top-most row
278
0
        const int nY = (1 << nZoomLevel) - 1 - nRow;
279
0
        uint64_t nTileId;
280
0
        try
281
0
        {
282
0
            nTileId = pmtiles::zxy_to_tileid(static_cast<uint8_t>(nZoomLevel),
283
0
                                             nColumn, nY);
284
0
        }
285
0
        catch (const std::exception &e)
286
0
        {
287
            // shouldn't happen given previous checks
288
0
            CPLError(CE_Failure, CPLE_AppDefined, "Cannot compute tile id: %s",
289
0
                     e.what());
290
0
            return false;
291
0
        }
292
0
        int nTileDataLength = 0;
293
0
        const GByte *pabyData =
294
0
            poFeature->GetFieldAsBinary(iTileData, &nTileDataLength);
295
0
        if (!pabyData)
296
0
        {
297
0
            CPLError(CE_Failure, CPLE_AppDefined, "Missing tile_data");
298
0
            return false;
299
0
        }
300
301
0
        TileEntry sEntry;
302
0
        sEntry.nTileId = nTileId;
303
304
0
        CPLMD5Context md5context;
305
0
        CPLMD5Init(&md5context);
306
0
        CPLMD5Update(&md5context, pabyData, nTileDataLength);
307
0
        CPLMD5Final(&sEntry.abyMD5[0], &md5context);
308
0
        try
309
0
        {
310
0
            asTileEntries.push_back(sEntry);
311
0
        }
312
0
        catch (const std::exception &e)
313
0
        {
314
0
            CPLError(CE_Failure, CPLE_AppDefined,
315
0
                     "Out of memory browsing through tiles: %s", e.what());
316
0
            return false;
317
0
        }
318
0
    }
319
320
    // Sort the tiles by ascending tile_id. This is a requirement to build
321
    // the PMTiles directories.
322
0
    std::sort(asTileEntries.begin(), asTileEntries.end(),
323
0
              [](const TileEntry &a, const TileEntry &b)
324
0
              { return a.nTileId < b.nTileId; });
325
326
    // Let's build a temporary file that contains the tile data in
327
    // a way that corresponds to the "clustered" mode, that is
328
    // "offsets are either contiguous with the previous offset+length, or
329
    // refer to a lesser offset, when writing with deduplication."
330
0
    std::string osTmpFile(std::string(pszDestName) + ".tmp");
331
0
    if (!VSIIsLocal(pszDestName))
332
0
    {
333
0
        osTmpFile = CPLGenerateTempFilenameSafe(CPLGetFilename(pszDestName));
334
0
    }
335
336
0
    auto poTmpFile =
337
0
        VSIVirtualHandleUniquePtr(VSIFOpenL(osTmpFile.c_str(), "wb+"));
338
0
    VSIUnlink(osTmpFile.c_str());
339
0
    if (!poTmpFile)
340
0
    {
341
0
        CPLError(CE_Failure, CPLE_FileIO, "Cannot open %s for write",
342
0
                 osTmpFile.c_str());
343
0
        return false;
344
0
    }
345
346
0
    struct ResetAndUnlinkTmpFile
347
0
    {
348
0
        VSIVirtualHandleUniquePtr &m_poFile;
349
0
        std::string m_osFilename;
350
351
0
        ResetAndUnlinkTmpFile(VSIVirtualHandleUniquePtr &poFile,
352
0
                              const std::string &osFilename)
353
0
            : m_poFile(poFile), m_osFilename(osFilename)
354
0
        {
355
0
        }
356
357
0
        ~ResetAndUnlinkTmpFile()
358
0
        {
359
0
            m_poFile.reset();
360
0
            VSIUnlink(m_osFilename.c_str());
361
0
        }
362
0
    };
363
364
0
    ResetAndUnlinkTmpFile oReseer(poTmpFile, osTmpFile);
365
366
0
    std::vector<pmtiles::entryv3> asPMTilesEntries;
367
0
    uint64_t nLastTileId = 0;
368
0
    uint64_t nFileOffset = 0;
369
0
    std::array<unsigned char, 16> abyLastMD5;
370
0
    std::unordered_map<std::array<unsigned char, 16>,
371
0
                       std::pair<uint64_t, uint32_t>,
372
0
                       HashArray<unsigned char, 16>>
373
0
        oMapMD5ToOffsetLen;
374
0
    for (const auto &sEntry : asTileEntries)
375
0
    {
376
0
        if (sEntry.nTileId == nLastTileId + 1 && sEntry.abyMD5 == abyLastMD5)
377
0
        {
378
            // If the tile id immediately follows the previous one and
379
            // has the same tile data, increase the run_length
380
0
            asPMTilesEntries.back().run_length++;
381
0
        }
382
0
        else
383
0
        {
384
0
            pmtiles::entryv3 sPMTilesEntry;
385
0
            sPMTilesEntry.tile_id = sEntry.nTileId;
386
0
            sPMTilesEntry.run_length = 1;
387
388
0
            auto oIter = oMapMD5ToOffsetLen.find(sEntry.abyMD5);
389
0
            if (oIter != oMapMD5ToOffsetLen.end())
390
0
            {
391
                // Point to previously written tile data if this content
392
                // has already been written
393
0
                sPMTilesEntry.offset = oIter->second.first;
394
0
                sPMTilesEntry.length = oIter->second.second;
395
0
            }
396
0
            else
397
0
            {
398
0
                try
399
0
                {
400
0
                    const auto sXYZ = pmtiles::tileid_to_zxy(sEntry.nTileId);
401
0
                    poTilesLayer->SetAttributeFilter(CPLSPrintf(
402
0
                        "zoom_level = %d AND tile_column = %u AND tile_row = "
403
0
                        "%u",
404
0
                        sXYZ.z, sXYZ.x, (1U << sXYZ.z) - 1U - sXYZ.y));
405
0
                }
406
0
                catch (const std::exception &e)
407
0
                {
408
                    // shouldn't happen given previous checks
409
0
                    CPLError(CE_Failure, CPLE_AppDefined,
410
0
                             "Cannot compute xyz: %s", e.what());
411
0
                    return false;
412
0
                }
413
0
                poTilesLayer->ResetReading();
414
0
                auto poFeature =
415
0
                    std::unique_ptr<OGRFeature>(poTilesLayer->GetNextFeature());
416
0
                if (!poFeature)
417
0
                {
418
0
                    CPLError(CE_Failure, CPLE_AppDefined, "Cannot find tile");
419
0
                    return false;
420
0
                }
421
0
                int nTileDataLength = 0;
422
0
                const GByte *pabyData =
423
0
                    poFeature->GetFieldAsBinary(iTileData, &nTileDataLength);
424
0
                if (!pabyData)
425
0
                {
426
0
                    CPLError(CE_Failure, CPLE_AppDefined, "Missing tile_data");
427
0
                    return false;
428
0
                }
429
430
0
                sPMTilesEntry.offset = nFileOffset;
431
0
                sPMTilesEntry.length = nTileDataLength;
432
433
0
                oMapMD5ToOffsetLen[sEntry.abyMD5] =
434
0
                    std::pair<uint64_t, uint32_t>(nFileOffset, nTileDataLength);
435
436
0
                nFileOffset += nTileDataLength;
437
438
0
                if (poTmpFile->Write(pabyData, nTileDataLength, 1) != 1)
439
0
                {
440
0
                    CPLError(CE_Failure, CPLE_FileIO, "Failed writing");
441
0
                    return false;
442
0
                }
443
0
            }
444
445
0
            asPMTilesEntries.push_back(sPMTilesEntry);
446
447
0
            nLastTileId = sEntry.nTileId;
448
0
            abyLastMD5 = sEntry.abyMD5;
449
0
        }
450
0
    }
451
452
0
    const CPLCompressor *psCompressor = CPLGetCompressor("gzip");
453
0
    assert(psCompressor);
454
0
    std::string osCompressed;
455
456
0
    struct compression_exception : std::exception
457
0
    {
458
0
        const char *what() const noexcept override
459
0
        {
460
0
            return "Compression failed";
461
0
        }
462
0
    };
463
464
0
    const auto oCompressFunc = [psCompressor,
465
0
                                &osCompressed](const std::string &osBytes,
466
0
                                               uint8_t) -> std::string
467
0
    {
468
0
        osCompressed.resize(32 + osBytes.size() * 2);
469
0
        size_t nOutputSize = osCompressed.size();
470
0
        void *pOutputData = &osCompressed[0];
471
0
        if (!psCompressor->pfnFunc(osBytes.data(), osBytes.size(), &pOutputData,
472
0
                                   &nOutputSize, nullptr,
473
0
                                   psCompressor->user_data))
474
0
        {
475
0
            throw compression_exception();
476
0
        }
477
0
        osCompressed.resize(nOutputSize);
478
0
        return osCompressed;
479
0
    };
480
481
0
    std::string osCompressedMetadata;
482
483
0
    std::string osRootBytes;
484
0
    std::string osLeaveBytes;
485
0
    int nNumLeaves;
486
0
    try
487
0
    {
488
0
        osCompressedMetadata =
489
0
            oCompressFunc(osMetadata, pmtiles::COMPRESSION_GZIP);
490
491
        // Build the root and leave directories (one depth max)
492
0
        std::tie(osRootBytes, osLeaveBytes, nNumLeaves) =
493
0
            pmtiles::make_root_leaves(oCompressFunc, pmtiles::COMPRESSION_GZIP,
494
0
                                      asPMTilesEntries);
495
0
    }
496
0
    catch (const std::exception &e)
497
0
    {
498
0
        CPLError(CE_Failure, CPLE_AppDefined, "Cannot build directories: %s",
499
0
                 e.what());
500
0
        return false;
501
0
    }
502
503
    // Finalize the header fields related to offsets and size of the
504
    // different parts of the file
505
0
    sHeader.root_dir_bytes = osRootBytes.size();
506
0
    sHeader.json_metadata_offset =
507
0
        sHeader.root_dir_offset + sHeader.root_dir_bytes;
508
0
    sHeader.json_metadata_bytes = osCompressedMetadata.size();
509
0
    sHeader.leaf_dirs_offset =
510
0
        sHeader.json_metadata_offset + sHeader.json_metadata_bytes;
511
0
    sHeader.leaf_dirs_bytes = osLeaveBytes.size();
512
0
    sHeader.tile_data_offset =
513
0
        sHeader.leaf_dirs_offset + sHeader.leaf_dirs_bytes;
514
0
    sHeader.tile_data_bytes = nFileOffset;
515
516
    // Nomber of tiles that are addressable in the PMTiles archive, that is
517
    // the number of tiles we would have if not deduplicating them
518
0
    sHeader.addressed_tiles_count = asTileEntries.size();
519
520
    // Number of tile entries in root and leave directories
521
    // ie entries whose run_length >= 1
522
0
    sHeader.tile_entries_count = asPMTilesEntries.size();
523
524
    // Number of distinct tile blobs
525
0
    sHeader.tile_contents_count = oMapMD5ToOffsetLen.size();
526
527
    // Now build the final file!
528
0
    auto poFile = VSIVirtualHandleUniquePtr(VSIFOpenL(pszDestName, "wb"));
529
0
    if (!poFile)
530
0
    {
531
0
        CPLError(CE_Failure, CPLE_FileIO, "Cannot open %s for write",
532
0
                 pszDestName);
533
0
        return false;
534
0
    }
535
0
    const auto osHeader = sHeader.serialize();
536
537
0
    if (poTmpFile->Seek(0, SEEK_SET) != 0 ||
538
0
        poFile->Write(osHeader.data(), osHeader.size(), 1) != 1 ||
539
0
        poFile->Write(osRootBytes.data(), osRootBytes.size(), 1) != 1 ||
540
0
        poFile->Write(osCompressedMetadata.data(), osCompressedMetadata.size(),
541
0
                      1) != 1 ||
542
0
        (!osLeaveBytes.empty() &&
543
0
         poFile->Write(osLeaveBytes.data(), osLeaveBytes.size(), 1) != 1))
544
0
    {
545
0
        CPLError(CE_Failure, CPLE_FileIO, "Failed writing");
546
0
        return false;
547
0
    }
548
549
    // Copy content of the temporary file at end of the output file.
550
0
    std::string oCopyBuffer;
551
0
    oCopyBuffer.resize(1024 * 1024);
552
0
    const uint64_t nTotalSize = nFileOffset;
553
0
    nFileOffset = 0;
554
0
    while (nFileOffset < nTotalSize)
555
0
    {
556
0
        const size_t nToRead = static_cast<size_t>(
557
0
            std::min<uint64_t>(nTotalSize - nFileOffset, oCopyBuffer.size()));
558
0
        if (poTmpFile->Read(&oCopyBuffer[0], nToRead, 1) != 1 ||
559
0
            poFile->Write(&oCopyBuffer[0], nToRead, 1) != 1)
560
0
        {
561
0
            CPLError(CE_Failure, CPLE_FileIO, "Failed writing");
562
0
            return false;
563
0
        }
564
0
        nFileOffset += nToRead;
565
0
    }
566
567
0
    if (poFile->Close() != 0)
568
0
        return false;
569
570
0
    return true;
571
0
}