Coverage Report

Created: 2026-09-14 06:50

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gdal/apps/gdalalg_compare_common.cpp
Line
Count
Source
1
/******************************************************************************
2
 *
3
 * Project:  GDAL
4
 * Purpose:  Common code between raster compare and mdim compare
5
 * Author:   Even Rouault <even dot rouault at spatialys.com>
6
 *
7
 ******************************************************************************
8
 * Copyright (c) 2026, Even Rouault <even dot rouault at spatialys.com>
9
 *
10
 * SPDX-License-Identifier: MIT
11
 ****************************************************************************/
12
13
#include "gdalalg_compare_common.h"
14
#include "gdalalgorithm.h"
15
#include "gdal_dataset.h"
16
#include "gdal_driver.h"
17
18
#include "cpl_vsi_virtual.h"
19
20
#include <array>
21
#include <map>
22
#include <set>
23
24
//! @cond Doxygen_Suppress
25
26
0
GDALCompareCommon::GDALCompareCommon() = default;
27
28
0
GDALCompareCommon::~GDALCompareCommon() = default;
29
30
/************************************************************************/
31
/*                            CompareFile()                             */
32
/************************************************************************/
33
34
static bool CompareFile(std::vector<std::string> &aosReport,
35
                        const char *pszRefFilename,
36
                        const char *pszInputFilename)
37
0
{
38
0
    VSIStatBufL sStatRef;
39
0
    if (VSIStatL(pszRefFilename, &sStatRef) != 0)
40
0
    {
41
0
        aosReport.push_back(std::string("Reference file ")
42
0
                                .append(pszRefFilename)
43
0
                                .append(" does not exist"));
44
0
        return false;
45
0
    }
46
47
0
    VSIStatBufL sStatInput;
48
0
    if (VSIStatL(pszInputFilename, &sStatInput) != 0)
49
0
    {
50
0
        aosReport.push_back(std::string("Input file ")
51
0
                                .append(pszInputFilename)
52
0
                                .append(" does not exist"));
53
0
        return false;
54
0
    }
55
56
0
    if (VSI_ISDIR(sStatRef.st_mode) && !VSI_ISDIR(sStatInput.st_mode))
57
0
    {
58
0
        aosReport.push_back(std::string("Reference file ")
59
0
                                .append(pszRefFilename)
60
0
                                .append(" is a directory, but input file ")
61
0
                                .append(pszInputFilename)
62
0
                                .append(" is not"));
63
0
        return false;
64
0
    }
65
0
    else if (!VSI_ISDIR(sStatRef.st_mode) && VSI_ISDIR(sStatInput.st_mode))
66
0
    {
67
0
        aosReport.push_back(std::string("Reference file ")
68
0
                                .append(pszRefFilename)
69
0
                                .append(" is not a directory, but input file ")
70
0
                                .append(pszInputFilename)
71
0
                                .append(" is"));
72
0
        return false;
73
0
    }
74
75
0
    if (VSI_ISDIR(sStatRef.st_mode))
76
0
    {
77
0
        std::unique_ptr<VSIDIR, decltype(&VSICloseDir)> psDirRef(
78
0
            VSIOpenDir(pszRefFilename, -1, nullptr), VSICloseDir);
79
0
        if (!psDirRef)
80
0
        {
81
0
            aosReport.push_back(std::string("Reference directory ")
82
0
                                    .append(pszRefFilename)
83
0
                                    .append(" cannot be opened"));
84
0
            return false;
85
0
        }
86
87
0
        std::unique_ptr<VSIDIR, decltype(&VSICloseDir)> psDirInput(
88
0
            VSIOpenDir(pszInputFilename, -1, nullptr), VSICloseDir);
89
0
        if (!psDirInput)
90
0
        {
91
0
            aosReport.push_back(std::string("Input directory ")
92
0
                                    .append(pszInputFilename)
93
0
                                    .append(" cannot be opened"));
94
0
            return false;
95
0
        }
96
97
0
        std::set<std::string> oSetRefFilenames;
98
0
        while (auto psEntryRef = psDirRef->NextDirEntry())
99
0
        {
100
0
            oSetRefFilenames.insert(psEntryRef->pszName);
101
0
            if (!CompareFile(aosReport,
102
0
                             std::string(pszRefFilename)
103
0
                                 .append("/")
104
0
                                 .append(psEntryRef->pszName)
105
0
                                 .c_str(),
106
0
                             std::string(pszInputFilename)
107
0
                                 .append("/")
108
0
                                 .append(psEntryRef->pszName)
109
0
                                 .c_str()))
110
0
            {
111
0
                return false;
112
0
            }
113
0
        }
114
0
        while (auto psEntryInput = psDirInput->NextDirEntry())
115
0
        {
116
0
            if (!cpl::contains(oSetRefFilenames, psEntryInput->pszName))
117
0
            {
118
0
                aosReport.push_back(
119
0
                    std::string("Input file ")
120
0
                        .append(psEntryInput->pszName)
121
0
                        .append(" does not exist in reference directory"));
122
0
                return false;
123
0
            }
124
0
        }
125
0
    }
126
0
    else
127
0
    {
128
0
        VSIVirtualHandleUniquePtr fpRef(VSIFOpenL(pszRefFilename, "rb"));
129
0
        VSIVirtualHandleUniquePtr fpInput(VSIFOpenL(pszInputFilename, "rb"));
130
0
        if (!fpRef)
131
0
        {
132
0
            aosReport.push_back(std::string("Reference file '")
133
0
                                    .append(pszRefFilename)
134
0
                                    .append("' cannot be opened."));
135
0
            return false;
136
0
        }
137
138
0
        if (!fpInput)
139
0
        {
140
0
            aosReport.push_back(std::string("Input file '")
141
0
                                    .append(pszRefFilename)
142
0
                                    .append("' cannot be opened."));
143
0
            return false;
144
0
        }
145
146
0
        fpRef->Seek(0, SEEK_END);
147
0
        fpInput->Seek(0, SEEK_END);
148
0
        const auto nRefSize = fpRef->Tell();
149
0
        const auto nInputSize = fpInput->Tell();
150
0
        if (nRefSize != nInputSize)
151
0
        {
152
0
            aosReport.push_back(
153
0
                std::string("Reference file '")
154
0
                    .append(pszRefFilename)
155
0
                    .append("' has size ")
156
0
                    .append(std::to_string(nRefSize))
157
0
                    .append(" bytes, whereas input file has size ")
158
0
                    .append(std::to_string(nInputSize))
159
0
                    .append(" bytes."));
160
161
0
            return false;
162
0
        }
163
164
0
        constexpr size_t BUF_SIZE = 1024 * 1024;
165
0
        std::vector<GByte> abyRef(BUF_SIZE);
166
0
        std::vector<GByte> abyInput(BUF_SIZE);
167
168
0
        fpRef->Seek(0, SEEK_SET);
169
0
        fpInput->Seek(0, SEEK_SET);
170
171
0
        do
172
0
        {
173
0
            const size_t nRefRead = fpRef->Read(abyRef.data(), 1, BUF_SIZE);
174
0
            const size_t nInputRead =
175
0
                fpInput->Read(abyInput.data(), 1, BUF_SIZE);
176
177
0
            if (nRefRead != BUF_SIZE && fpRef->Tell() != nRefSize)
178
0
            {
179
0
                aosReport.push_back("Failed to fully read reference file");
180
0
                return false;
181
0
            }
182
183
0
            if (nInputRead != BUF_SIZE && fpInput->Tell() != nRefSize)
184
0
            {
185
0
                aosReport.push_back("Failed to fully read input file");
186
0
                return false;
187
0
            }
188
189
0
            if (abyRef != abyInput)
190
0
            {
191
0
                aosReport.push_back("Reference file and input file differ at "
192
0
                                    "the binary level.");
193
0
                return false;
194
0
            }
195
0
        } while (fpRef->Tell() < nRefSize);
196
0
    }
197
198
0
    return true;
199
0
}
200
201
/************************************************************************/
202
/*            GDALRasterCompareAlgorithm::BinaryComparison()            */
203
/************************************************************************/
204
205
/* static */
206
bool GDALCompareCommon::BinaryComparison(GDALAlgorithm *alg,
207
                                         std::vector<std::string> &aosReport,
208
                                         GDALDataset *poRefDS,
209
                                         GDALDataset *poInputDS)
210
0
{
211
0
    if (poRefDS->GetDescription()[0] == 0)
212
0
    {
213
0
        alg->ReportError(
214
0
            CE_Warning, CPLE_AppDefined,
215
0
            "Reference dataset has no name. Skipping binary file comparison");
216
0
        return false;
217
0
    }
218
219
0
    auto poRefDrv = poRefDS->GetDriver();
220
0
    if (poRefDrv && EQUAL(poRefDrv->GetDescription(), "MEM"))
221
0
    {
222
0
        alg->ReportError(
223
0
            CE_Warning, CPLE_AppDefined,
224
0
            "Reference dataset is a in-memory dataset. Skipping binary "
225
0
            "file comparison");
226
0
        return false;
227
0
    }
228
229
0
    if (poInputDS->GetDescription()[0] == 0)
230
0
    {
231
0
        alg->ReportError(
232
0
            CE_Warning, CPLE_AppDefined,
233
0
            "Input dataset has no name. Skipping binary file comparison");
234
0
        return false;
235
0
    }
236
237
0
    auto poInputDrv = poInputDS->GetDriver();
238
0
    if (poInputDrv && EQUAL(poInputDrv->GetDescription(), "MEM"))
239
0
    {
240
0
        alg->ReportError(
241
0
            CE_Warning, CPLE_AppDefined,
242
0
            "Input dataset is a in-memory dataset. Skipping binary "
243
0
            "file comparison");
244
0
        return false;
245
0
    }
246
247
0
    VSIStatBufL sStat;
248
0
    if (VSIStatL(poRefDS->GetDescription(), &sStat) != 0)
249
0
    {
250
0
        alg->ReportError(
251
0
            CE_Warning, CPLE_AppDefined,
252
0
            "Reference dataset '%s' is not a file. Skipping binary "
253
0
            "file comparison",
254
0
            poRefDS->GetDescription());
255
0
        return false;
256
0
    }
257
258
0
    if (VSIStatL(poInputDS->GetDescription(), &sStat) != 0)
259
0
    {
260
0
        alg->ReportError(
261
0
            CE_Warning, CPLE_AppDefined,
262
0
            "Input dataset '%s' is not a file. Skipping binary file comparison",
263
0
            poInputDS->GetDescription());
264
0
        return false;
265
0
    }
266
267
0
    return CompareFile(aosReport, poRefDS->GetDescription(),
268
0
                       poInputDS->GetDescription());
269
0
}
270
271
/************************************************************************/
272
/*               GDALCompareCommon::MetadataComparison()                */
273
/************************************************************************/
274
275
/* static */
276
void GDALCompareCommon::MetadataComparison(std::vector<std::string> &aosReport,
277
                                           const std::string &metadataDomain,
278
                                           CSLConstList aosRef,
279
                                           CSLConstList aosInput)
280
0
{
281
0
    std::map<std::string, std::string> oMapRef;
282
0
    std::map<std::string, std::string> oMapInput;
283
284
0
    std::array<const char *, 3> ignoredKeys = {
285
0
        "backend",   // from gdalcompare.py. Not sure why
286
0
        "ERR_BIAS",  // RPC optional key
287
0
        "ERR_RAND",  // RPC optional key
288
0
    };
289
290
0
    for (const auto &[key, value] : cpl::IterateNameValue(aosRef))
291
0
    {
292
0
        const char *pszKey = key;
293
0
        const auto eq = [pszKey](const char *s)
294
0
        { return strcmp(pszKey, s) == 0; };
295
0
        auto it = std::find_if(ignoredKeys.begin(), ignoredKeys.end(), eq);
296
0
        if (it == ignoredKeys.end())
297
0
        {
298
0
            oMapRef[key] = value;
299
0
        }
300
0
    }
301
302
0
    for (const auto &[key, value] : cpl::IterateNameValue(aosInput))
303
0
    {
304
0
        const char *pszKey = key;
305
0
        const auto eq = [pszKey](const char *s)
306
0
        { return strcmp(pszKey, s) == 0; };
307
0
        auto it = std::find_if(ignoredKeys.begin(), ignoredKeys.end(), eq);
308
0
        if (it == ignoredKeys.end())
309
0
        {
310
0
            oMapInput[key] = value;
311
0
        }
312
0
    }
313
314
0
    const auto strip = [](const std::string &s)
315
0
    {
316
0
        const auto posBegin = s.find_first_not_of(' ');
317
0
        if (posBegin == std::string::npos)
318
0
            return std::string();
319
0
        const auto posEnd = s.find_last_not_of(' ');
320
0
        return s.substr(posBegin, posEnd - posBegin + 1);
321
0
    };
322
323
0
    for (const auto &sKeyValuePair : oMapRef)
324
0
    {
325
0
        const auto oIter = oMapInput.find(sKeyValuePair.first);
326
0
        if (oIter == oMapInput.end())
327
0
        {
328
0
            aosReport.push_back("Reference metadata " + metadataDomain +
329
0
                                " contains key '" + sKeyValuePair.first +
330
0
                                "' but input metadata does not.");
331
0
        }
332
0
        else
333
0
        {
334
            // this will always have the current date set
335
0
            if (sKeyValuePair.first == "NITF_FDT")
336
0
                continue;
337
338
0
            std::string ref = oIter->second;
339
0
            std::string input = sKeyValuePair.second;
340
0
            if (metadataDomain == GDAL_MDD_RPC)
341
0
            {
342
                // _RPC.TXT files and in-file have a difference
343
                // in white space that is not otherwise meaningful.
344
0
                ref = strip(ref);
345
0
                input = strip(input);
346
0
            }
347
0
            if (ref != input)
348
0
            {
349
0
                aosReport.push_back(
350
0
                    "Reference metadata " + metadataDomain + " has value '" +
351
0
                    ref + "' for key '" + sKeyValuePair.first +
352
0
                    "' but input metadata has value '" + input + "'.");
353
0
            }
354
0
        }
355
0
    }
356
357
0
    for (const auto &sKeyValuePair : oMapInput)
358
0
    {
359
0
        if (!cpl::contains(oMapRef, sKeyValuePair.first))
360
0
        {
361
0
            aosReport.push_back("Input metadata " + metadataDomain +
362
0
                                " contains key '" + sKeyValuePair.first +
363
0
                                "' but reference metadata does not.");
364
0
        }
365
0
    }
366
0
}
367
368
//! @endcond