[mapguide-commits] r10221 - branches/4.0/MgDev/Server/src/UnitTesting

svn_mapguide at osgeo.org svn_mapguide at osgeo.org
Wed Oct 21 15:24:20 PDT 2026


Author: jng
Date: 2026-10-21 15:24:19 -0700 (Wed, 21 Oct 2026)
New Revision: 10221

Modified:
   branches/4.0/MgDev/Server/src/UnitTesting/TestCoordinateSystem.cpp
Log:
#1835: Add regression test to verify this was indeed fixed. The fix landed in r7097

Modified: branches/4.0/MgDev/Server/src/UnitTesting/TestCoordinateSystem.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/TestCoordinateSystem.cpp	2026-10-21 08:23:18 UTC (rev 10220)
+++ branches/4.0/MgDev/Server/src/UnitTesting/TestCoordinateSystem.cpp	2026-10-21 22:24:19 UTC (rev 10221)
@@ -21,6 +21,11 @@
 #include "CatchHelperMacros.h"
 #include "catch.hpp"
 
+#include <cstring>
+#include <fstream>
+#include <iterator>
+#include <vector>
+
 #ifndef _WIN32
 #define stricmp strcasecmp
 
@@ -433,6 +438,208 @@
         &MgCoordinateSystemCatalog::GetCategoryDictionary>(L"Category", NULL, NULL);
 }
 
+///----------------------------------------------------------------------------
+/// Test Case Description:
+///
+/// Regression test for #1835: adding a coordinate system to a category that
+/// already contains duplicate entries must not truncate that category.
+///
+/// CCoordinateSystemCategory::CopyFrom() used to rebuild a category by calling
+/// AddCoordinateSystem() for every name it enumerated.  AddCoordinateSystem()
+/// throws on a duplicate, so the copy stopped at the first duplicate entry, and
+/// CCoordinateSystemCategoryDictionary::Modify() then wrote that truncated
+/// category back to the dictionary.
+///----------------------------------------------------------------------------
+
+//A category dictionary (.CSD) is a straight concatenation of fixed sized
+//records; see CSwrtCategory()/CSrdCategoryEx() in
+//Oem/CsMap/CsMapDev/Source/CS_category.c:
+//  UINT32 magic
+//  { char ctName[128]; UINT32 nameCnt; UINT32 allocCnt; char csName[24][allocCnt]; } *
+const size_t CategoryDictNameSize = 128;
+const size_t CategoryDictCsNameSize = 24;
+const size_t CategoryDictAllocCount = 12;
+const UINT32 CategoryDictMagic = 0x3372ED6C;
+
+static size_t CategoryDictStrLen(const unsigned char* buffer, size_t maxLength)
+{
+    size_t length = 0;
+    while (length < maxLength && buffer[length] != 0)
+        ++length;
+    return length;
+}
+
+static UINT32 ReadCategoryDictUInt32(const std::vector<unsigned char>& bytes, size_t offset)
+{
+    return (UINT32)bytes[offset]
+        | ((UINT32)bytes[offset + 1] << 8)
+        | ((UINT32)bytes[offset + 2] << 16)
+        | ((UINT32)bytes[offset + 3] << 24);
+}
+
+static void WriteCategoryDictUInt32(std::vector<unsigned char>& bytes, size_t offset, UINT32 value)
+{
+    bytes[offset] = (unsigned char)(value & 0xFF);
+    bytes[offset + 1] = (unsigned char)((value >> 8) & 0xFF);
+    bytes[offset + 2] = (unsigned char)((value >> 16) & 0xFF);
+    bytes[offset + 3] = (unsigned char)((value >> 24) & 0xFF);
+}
+
+static bool ReadCategoryDictFile(const STRING& file, std::vector<unsigned char>& bytes)
+{
+    std::string mbFile = MgUtil::WideCharToMultiByte(file);
+    std::ifstream stream(mbFile.c_str(), std::ios::in | std::ios::binary);
+    if (!stream.is_open())
+        return false;
+
+    bytes.assign(std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>());
+    return true;
+}
+
+static bool WriteCategoryDictFile(const STRING& file, const std::vector<unsigned char>& bytes)
+{
+    if (bytes.empty())
+        return false;
+
+    std::string mbFile = MgUtil::WideCharToMultiByte(file);
+    std::ofstream stream(mbFile.c_str(), std::ios::out | std::ios::binary | std::ios::trunc);
+    if (!stream.is_open())
+        return false;
+
+    stream.write((const char*)&bytes[0], (std::streamsize)bytes.size());
+    return stream.good();
+}
+
+//Appends a copy of its own first CS name to the first non-empty category in the
+//given category dictionary, i.e. makes that category contain a duplicate entry.
+//Reports which category and CS name were used, plus the category's entry count
+//before the duplicate was added.
+static bool DuplicateCategoryEntry(const STRING& csdFile, STRING& categoryName, STRING& csName, UINT32& originalCount)
+{
+    std::vector<unsigned char> bytes;
+    if (!ReadCategoryDictFile(csdFile, bytes))
+        return false;
+
+    if (bytes.size() < 4 || ReadCategoryDictUInt32(bytes, 0) != CategoryDictMagic)
+        return false;
+
+    size_t offset = 4;
+    while (offset + CategoryDictNameSize + 8 <= bytes.size())
+    {
+        const unsigned char* mbCategory = &bytes[offset];
+        std::string mbCategoryName((const char*)mbCategory, CategoryDictStrLen(mbCategory, CategoryDictNameSize));
+        size_t countOffset = offset + CategoryDictNameSize;
+        UINT32 nameCount = ReadCategoryDictUInt32(bytes, countOffset);
+        UINT32 allocCount = ReadCategoryDictUInt32(bytes, countOffset + 4);
+        size_t namesOffset = countOffset + 8;
+
+        if (nameCount > allocCount || namesOffset + allocCount * CategoryDictCsNameSize > bytes.size())
+            return false;
+
+        if (nameCount > 0)
+        {
+            const unsigned char* mbFirst = &bytes[namesOffset];
+            std::string mbDuplicate((const char*)mbFirst, CategoryDictStrLen(mbFirst, CategoryDictCsNameSize));
+
+            if (nameCount == allocCount)
+            {
+                //grow the entry block the same way CS-Map's AddCsName() does
+                bytes.insert(bytes.begin() + (namesOffset + allocCount * CategoryDictCsNameSize),
+                    CategoryDictAllocCount * CategoryDictCsNameSize, 0);
+                allocCount += (UINT32)CategoryDictAllocCount;
+                WriteCategoryDictUInt32(bytes, countOffset + 4, allocCount);
+            }
+
+            size_t newEntryOffset = namesOffset + nameCount * CategoryDictCsNameSize;
+            memset(&bytes[newEntryOffset], 0, CategoryDictCsNameSize);
+            memcpy(&bytes[newEntryOffset], mbDuplicate.c_str(), mbDuplicate.length());
+            WriteCategoryDictUInt32(bytes, countOffset, nameCount + 1);
+
+            categoryName = MgUtil::MultiByteToWideChar(mbCategoryName);
+            csName = MgUtil::MultiByteToWideChar(mbDuplicate);
+            originalCount = nameCount;
+
+            return WriteCategoryDictFile(csdFile, bytes);
+        }
+
+        offset = namesOffset + allocCount * CategoryDictCsNameSize;
+    }
+
+    return false;
+}
+
+TEST_CASE("AddCoordinateSystemToCategoryWithDuplicateEntries", "[CoordinateSystem]")
+{
+    try
+    {
+        MgCoordinateSystemFactory csFactory;
+        Ptr<MgCoordinateSystemCatalog> pCsCatalog = csFactory.GetCatalog();
+
+        //make sure the category dictionary is written into the system dictionary
+        if (!pCsCatalog->GetUserDictionaryDir().empty())
+            pCsCatalog->SetUserDictionaryDir(L"");
+
+        Ptr<MgCoordinateSystemCategoryDictionary> pCtDict = pCsCatalog->GetCategoryDictionary();
+        STRING csdFile = pCtDict->GetPath();
+        REQUIRE(MgFileUtil::IsFile(csdFile));
+
+        STRING categoryName;
+        STRING duplicateName;
+        UINT32 originalCount = 0;
+
+        {
+            //backup the current CSD file and automatically restore it after we are done
+            FileAutoBackup csdFileBackup(csdFile, L".UNIT_TEST_SAVE", true);
+
+            REQUIRE(DuplicateCategoryEntry(csdFile, categoryName, duplicateName, originalCount));
+            ACE_DEBUG((LM_INFO, ACE_TEXT("\nInjected duplicate %W into category %W\n"), duplicateName.c_str(), categoryName.c_str()));
+
+            pCtDict->SetFileName(pCtDict->GetFileName()); //force the Mg dictionary to re-read the file
+
+            Ptr<MgCoordinateSystemCategory> pCategory = pCtDict->GetCategory(categoryName);
+            REQUIRE(pCategory);
+
+            Ptr<MgStringCollection> pNamesBefore = pCategory->GetCoordinateSystems();
+            REQUIRE(pNamesBefore->GetCount() == (INT32)(originalCount + 1)); //the duplicate has been picked up
+            REQUIRE(pNamesBefore->Contains(duplicateName));
+
+            STRING newCsName = L"MG_UNIT_TEST_1835";
+            REQUIRE(!pNamesBefore->Contains(newCsName));
+
+            pCategory->AddCoordinateSystem(newCsName);
+            pCtDict->Modify(pCategory.p);
+
+            //now verify what's actually in the dictionary, not just the in-memory object
+            pCtDict->SetFileName(pCtDict->GetFileName());
+
+            pCategory = pCtDict->GetCategory(categoryName);
+            REQUIRE(pCategory);
+
+            Ptr<MgStringCollection> pNamesAfter = pCategory->GetCoordinateSystems();
+            REQUIRE(pNamesAfter->GetCount() == (INT32)(originalCount + 2));
+            REQUIRE(pNamesAfter->Contains(newCsName));
+
+            for (INT32 i = 0; i < pNamesBefore->GetCount(); i++)
+            {
+                REQUIRE(pNamesAfter->Contains(pNamesBefore->GetItem(i)));
+            }
+        }
+
+        //the CSD file has been restored - make sure the Mg dictionary sees that, too
+        pCtDict->SetFileName(pCtDict->GetFileName());
+    }
+    catch(MgException* e)
+    {
+        STRING message = e->GetDetails(TEST_LOCALE);
+        SAFE_RELEASE(e);
+        FAIL(MG_WCHAR_TO_CHAR(message.c_str()));
+    }
+    catch(...)
+    {
+        throw;
+    }
+}
+
 /// (5) Update a geodetic transformation
 TEST_CASE("UpdateGeodeticTransformations", "[CoordinateSystem]")
 {



More information about the mapguide-commits mailing list