LCOV - code coverage report
Current view: top level - apps - gdalalg_compare_common.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 148 174 85.1 %
Date: 2026-09-11 05:09:32 Functions: 6 6 100.0 %

          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

Generated by: LCOV version 1.14