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