diff --git a/CMakeLists.txt b/CMakeLists.txt index 59157f6..12d8d03 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -101,4 +101,14 @@ add_test(NAME AnnotationTests COMMAND test_annotations) add_executable(test_attributes tests/test_attributes.cpp) target_include_directories(test_attributes PRIVATE ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/tests) target_link_libraries(test_attributes dxfrw ${ICONV_LIBRARY}) -add_test(NAME AttributeTests COMMAND test_attributes) \ No newline at end of file +add_test(NAME AttributeTests COMMAND test_attributes) + +add_executable(test_class_underflow tests/test_class_underflow.cpp) +target_include_directories(test_class_underflow PRIVATE ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/tests) +target_link_libraries(test_class_underflow dxfrw ${ICONV_LIBRARY}) +add_test(NAME ClassUnderflowTests COMMAND test_class_underflow) + +add_executable(test_dwg_classes tests/test_dwg_classes.cpp) +target_include_directories(test_dwg_classes PRIVATE ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/tests) +target_link_libraries(test_dwg_classes dxfrw ${ICONV_LIBRARY}) +add_test(NAME DWGClassParsingTests COMMAND test_dwg_classes) diff --git a/src/intern/dwgreader18.cpp b/src/intern/dwgreader18.cpp index a27ff77..e32ccc5 100644 --- a/src/intern/dwgreader18.cpp +++ b/src/intern/dwgreader18.cpp @@ -503,7 +503,9 @@ bool dwgReader18::readDwgClasses(){ /*******************************/ - duint32 endDataPos = maxClassNum-499; + // Classes 0-499 are built-in; custom classes start at 500. + // If maxClassNum <= 499, there are no custom classes to parse. + duint32 endDataPos = (maxClassNum > 499) ? (maxClassNum - 499) : 0; DRW_DBG("\nbuff.getPosition: "); DRW_DBG(dataBuf.getPosition()); for (duint32 i= 0; i 499) ? (maxClassNum - 499) : 0; DRW_DBG("\nbuff.getPosition: "); DRW_DBG(buff.getPosition()); for (duint32 i= 0; i. ** +******************************************************************************/ + +/** + * Test cases for the maxClassNum underflow fix. + * + * Background: + * In DWG format, class numbers 0-499 are reserved for built-in classes. + * Custom classes start at number 500. The original code calculated: + * endDataPos = maxClassNum - 499 + * + * This caused an integer underflow when maxClassNum <= 499 (e.g., 236 in + * AC1032 files from DWG TrueView), resulting in: + * endDataPos = 236 - 499 = 4294967033 (unsigned wraparound) + * + * This caused massive memory allocation (80GB+) and infinite loop. + * + * The fix: + * endDataPos = (maxClassNum > 499) ? (maxClassNum - 499) : 0 + */ + +#include +#include +#include +#include + +// Type definitions matching libdxfrw +typedef uint32_t duint32; + +/** + * Original calculation (BUGGY - causes underflow) + */ +duint32 calculateEndDataPos_Original(duint32 maxClassNum) { + return maxClassNum - 499; +} + +/** + * Fixed calculation (safe) + */ +duint32 calculateEndDataPos_Fixed(duint32 maxClassNum) { + return (maxClassNum > 499) ? (maxClassNum - 499) : 0; +} + +/** + * Test Case 1: Normal case - maxClassNum > 499 + * Classes 500-516 means 17 custom classes (516 - 499 = 17) + */ +bool testNormalCase() { + std::cout << "\n=== Test: Normal Case (maxClassNum > 499) ===" << std::endl; + + // Test case from working AC1027 file: maxClassNum = 516 + duint32 maxClassNum = 516; + duint32 expected = 17; + + duint32 result = calculateEndDataPos_Fixed(maxClassNum); + + std::cout << " maxClassNum = " << maxClassNum << std::endl; + std::cout << " Expected endDataPos = " << expected << std::endl; + std::cout << " Actual endDataPos = " << result << std::endl; + + if (result != expected) { + std::cout << " FAIL: Result does not match expected" << std::endl; + return false; + } + + std::cout << " PASS" << std::endl; + return true; +} + +/** + * Test Case 2: Bug case - maxClassNum < 499 + * This is the case that caused the memory explosion bug + * AC1032 files from DWG TrueView have maxClassNum = 236 + */ +bool testBugCase_LessThan499() { + std::cout << "\n=== Test: Bug Case (maxClassNum < 499) ===" << std::endl; + + // Test case from problematic AC1032 file: maxClassNum = 236 + duint32 maxClassNum = 236; + duint32 expected = 0; // Should be 0, not 4294967033 + + // First, demonstrate the bug + duint32 buggyResult = calculateEndDataPos_Original(maxClassNum); + std::cout << " maxClassNum = " << maxClassNum << std::endl; + std::cout << " BUGGY calculation: " << buggyResult << " (would loop billions of times!)" << std::endl; + + // Now test the fix + duint32 fixedResult = calculateEndDataPos_Fixed(maxClassNum); + std::cout << " FIXED calculation: " << fixedResult << std::endl; + std::cout << " Expected: " << expected << std::endl; + + if (fixedResult != expected) { + std::cout << " FAIL: Fixed result does not match expected" << std::endl; + return false; + } + + // Verify buggy result is indeed a huge number (underflow) + if (buggyResult < 1000000000) { + std::cout << " FAIL: Buggy result should be > 1 billion (underflow)" << std::endl; + return false; + } + + std::cout << " PASS: Fix correctly prevents underflow" << std::endl; + return true; +} + +/** + * Test Case 3: Boundary case - maxClassNum == 499 + * Exactly at the threshold, no custom classes + */ +bool testBoundary_Equals499() { + std::cout << "\n=== Test: Boundary Case (maxClassNum == 499) ===" << std::endl; + + duint32 maxClassNum = 499; + duint32 expected = 0; // No custom classes + + // Buggy version would give 0 too (499 - 499 = 0) + duint32 buggyResult = calculateEndDataPos_Original(maxClassNum); + duint32 fixedResult = calculateEndDataPos_Fixed(maxClassNum); + + std::cout << " maxClassNum = " << maxClassNum << std::endl; + std::cout << " Original: " << buggyResult << std::endl; + std::cout << " Fixed: " << fixedResult << std::endl; + std::cout << " Expected: " << expected << std::endl; + + if (fixedResult != expected) { + std::cout << " FAIL: Result does not match expected" << std::endl; + return false; + } + + std::cout << " PASS" << std::endl; + return true; +} + +/** + * Test Case 4: Boundary case - maxClassNum == 500 + * Exactly one custom class + */ +bool testBoundary_Equals500() { + std::cout << "\n=== Test: Boundary Case (maxClassNum == 500) ===" << std::endl; + + duint32 maxClassNum = 500; + duint32 expected = 1; // One custom class (class 500) + + duint32 result = calculateEndDataPos_Fixed(maxClassNum); + + std::cout << " maxClassNum = " << maxClassNum << std::endl; + std::cout << " Expected endDataPos = " << expected << std::endl; + std::cout << " Actual endDataPos = " << result << std::endl; + + if (result != expected) { + std::cout << " FAIL: Result does not match expected" << std::endl; + return false; + } + + std::cout << " PASS" << std::endl; + return true; +} + +/** + * Test Case 5: Edge case - maxClassNum == 0 + * No classes at all + */ +bool testEdge_Zero() { + std::cout << "\n=== Test: Edge Case (maxClassNum == 0) ===" << std::endl; + + duint32 maxClassNum = 0; + duint32 expected = 0; + + // Buggy version would underflow massively + duint32 buggyResult = calculateEndDataPos_Original(maxClassNum); + duint32 fixedResult = calculateEndDataPos_Fixed(maxClassNum); + + std::cout << " maxClassNum = " << maxClassNum << std::endl; + std::cout << " BUGGY calculation: " << buggyResult << " (massive underflow!)" << std::endl; + std::cout << " FIXED calculation: " << fixedResult << std::endl; + std::cout << " Expected: " << expected << std::endl; + + if (fixedResult != expected) { + std::cout << " FAIL: Fixed result does not match expected" << std::endl; + return false; + } + + std::cout << " PASS" << std::endl; + return true; +} + +/** + * Test Case 6: Edge case - maxClassNum == 498 + * One less than threshold + */ +bool testEdge_JustBelow() { + std::cout << "\n=== Test: Edge Case (maxClassNum == 498) ===" << std::endl; + + duint32 maxClassNum = 498; + duint32 expected = 0; + + // Buggy version would underflow + duint32 buggyResult = calculateEndDataPos_Original(maxClassNum); + duint32 fixedResult = calculateEndDataPos_Fixed(maxClassNum); + + std::cout << " maxClassNum = " << maxClassNum << std::endl; + std::cout << " BUGGY calculation: " << buggyResult << " (underflow!)" << std::endl; + std::cout << " FIXED calculation: " << fixedResult << std::endl; + std::cout << " Expected: " << expected << std::endl; + + if (fixedResult != expected) { + std::cout << " FAIL: Fixed result does not match expected" << std::endl; + return false; + } + + // Verify buggy result is indeed a huge number (underflow) + if (buggyResult < 1000000000) { + std::cout << " FAIL: Buggy result should be > 1 billion (underflow)" << std::endl; + return false; + } + + std::cout << " PASS" << std::endl; + return true; +} + +/** + * Test Case 7: Large value - maxClassNum == 1000 + * Many custom classes + */ +bool testLargeValue() { + std::cout << "\n=== Test: Large Value (maxClassNum == 1000) ===" << std::endl; + + duint32 maxClassNum = 1000; + duint32 expected = 501; // 1000 - 499 = 501 + + duint32 result = calculateEndDataPos_Fixed(maxClassNum); + + std::cout << " maxClassNum = " << maxClassNum << std::endl; + std::cout << " Expected endDataPos = " << expected << std::endl; + std::cout << " Actual endDataPos = " << result << std::endl; + + if (result != expected) { + std::cout << " FAIL: Result does not match expected" << std::endl; + return false; + } + + std::cout << " PASS" << std::endl; + return true; +} + +/** + * Test Case 8: Various AC1032 test values + * Test with actual values seen in problematic files + */ +bool testAC1032Values() { + std::cout << "\n=== Test: AC1032 Actual Values ===" << std::endl; + + // Values observed in AC1032 files from DWG TrueView + duint32 testValues[] = {236, 240, 250, 300, 400, 450, 498}; + int numValues = sizeof(testValues) / sizeof(testValues[0]); + + bool allPassed = true; + + for (int i = 0; i < numValues; i++) { + duint32 maxClassNum = testValues[i]; + duint32 expected = 0; // All these are < 499 + + duint32 result = calculateEndDataPos_Fixed(maxClassNum); + + std::cout << " maxClassNum = " << maxClassNum + << " -> endDataPos = " << result + << " (expected " << expected << ")" << std::endl; + + if (result != expected) { + std::cout << " FAIL" << std::endl; + allPassed = false; + } + } + + if (allPassed) { + std::cout << " PASS: All AC1032 values handled correctly" << std::endl; + } + + return allPassed; +} + +/** + * Test Case 9: Consistency between original and fixed for valid values + * For maxClassNum > 499, both should give same result + */ +bool testConsistency() { + std::cout << "\n=== Test: Consistency (original == fixed for valid values) ===" << std::endl; + + bool allPassed = true; + + // Test various values where original code would work correctly + for (duint32 maxClassNum = 500; maxClassNum <= 1000; maxClassNum += 50) { + duint32 originalResult = calculateEndDataPos_Original(maxClassNum); + duint32 fixedResult = calculateEndDataPos_Fixed(maxClassNum); + + if (originalResult != fixedResult) { + std::cout << " FAIL at maxClassNum = " << maxClassNum + << ": original=" << originalResult + << ", fixed=" << fixedResult << std::endl; + allPassed = false; + } + } + + if (allPassed) { + std::cout << " PASS: Fixed code is consistent with original for valid inputs" << std::endl; + } + + return allPassed; +} + +int main(int argc, char* argv[]) { + std::cout << "libdxfrw Class Count Underflow Tests" << std::endl; + std::cout << "=====================================" << std::endl; + std::cout << "Testing fix for integer underflow in maxClassNum calculation" << std::endl; + std::cout << "Affected files: dwgreader18.cpp, dwgreader21.cpp" << std::endl; + + int failedTests = 0; + int totalTests = 0; + + totalTests++; + if (!testNormalCase()) failedTests++; + + totalTests++; + if (!testBugCase_LessThan499()) failedTests++; + + totalTests++; + if (!testBoundary_Equals499()) failedTests++; + + totalTests++; + if (!testBoundary_Equals500()) failedTests++; + + totalTests++; + if (!testEdge_Zero()) failedTests++; + + totalTests++; + if (!testEdge_JustBelow()) failedTests++; + + totalTests++; + if (!testLargeValue()) failedTests++; + + totalTests++; + if (!testAC1032Values()) failedTests++; + + totalTests++; + if (!testConsistency()) failedTests++; + + std::cout << "\n=====================================" << std::endl; + std::cout << "Tests: " << (totalTests - failedTests) << "/" << totalTests << " passed" << std::endl; + + if (failedTests > 0) { + std::cout << "FAIL: " << failedTests << " test(s) failed" << std::endl; + return 1; + } else { + std::cout << "SUCCESS: All underflow tests passed!" << std::endl; + return 0; + } +} diff --git a/tests/test_dwg_classes.cpp b/tests/test_dwg_classes.cpp new file mode 100644 index 0000000..746272b --- /dev/null +++ b/tests/test_dwg_classes.cpp @@ -0,0 +1,463 @@ +/****************************************************************************** +** libDXFrw - DWG Class Parsing Tests ** +** ** +** Copyright (C) 2025 libdxfrw contributors ** +** ** +** This library is free software, licensed under the terms of the GNU ** +** General Public License as published by the Free Software Foundation, ** +** either version 2 of the License, or (at your option) any later version. ** +** You should have received a copy of the GNU General Public License ** +** along with this program. If not, see . ** +******************************************************************************/ + +/** + * Integration tests for DWG class parsing. + * + * These tests verify that the class parsing logic in dwgreader18 and dwgreader21 + * correctly handles various maxClassNum values, especially edge cases that + * previously caused integer underflow bugs. + * + * Test coverage: + * - Regression test: AC1027 files still work correctly + * - Memory safety: AC1032 files don't cause memory explosion + * - Boundary conditions: Various class count thresholds + */ + +#include "libdwgr.h" +#include "test_interface.h" +#include +#include +#include +#include +#include + +// Platform-specific includes +#if defined(__unix__) || defined(__APPLE__) +#include +#include +#define HAS_GETRUSAGE 1 +#elif defined(_WIN32) +#include // For _getpid() +#define HAS_GETRUSAGE 0 +#else +#define HAS_GETRUSAGE 0 +#endif + +#if HAS_GETRUSAGE +/** + * Get current memory usage in KB (POSIX only) + * Note: ru_maxrss units differ by platform: + * - Linux: kilobytes + * - macOS/BSD: bytes + */ +long getMemoryUsageKB() { + struct rusage usage; + getrusage(RUSAGE_SELF, &usage); +#if defined(__APPLE__) + // macOS returns bytes, convert to KB + return usage.ru_maxrss / 1024; +#else + // Linux returns KB + return usage.ru_maxrss; +#endif +} +#endif + +/** + * Get platform-appropriate temp directory path + */ +std::string getTempDir() { + // Check environment variables in order of preference + const char* tmpdir = std::getenv("TMPDIR"); // Unix standard + if (tmpdir && tmpdir[0] != '\0') return tmpdir; + + tmpdir = std::getenv("TEMP"); // Windows + if (tmpdir && tmpdir[0] != '\0') return tmpdir; + + tmpdir = std::getenv("TMP"); // Windows fallback + if (tmpdir && tmpdir[0] != '\0') return tmpdir; + +#if defined(_WIN32) + return "C:\\Windows\\Temp"; +#else + return "/tmp"; +#endif +} + +/** + * Build a unique temp file path to avoid collisions + */ +std::string buildTempFilePath(const char* basename) { + std::string dir = getTempDir(); + // Ensure trailing separator + if (!dir.empty() && dir.back() != '/' && dir.back() != '\\') { +#if defined(_WIN32) + dir += '\\'; +#else + dir += '/'; +#endif + } + // Use PID for uniqueness +#if defined(_WIN32) + int pid = _getpid(); +#elif defined(__unix__) || defined(__APPLE__) + int pid = getpid(); +#else + // Fallback: use a fixed suffix if no PID available + int pid = 0; +#endif + return dir + basename + "_" + std::to_string(pid) + ".dwg"; +} + +/** + * Get the test data directory from environment variable + * Returns nullptr if not set + */ +const char* getTestDataDir() { + return std::getenv("LIBDXFRW_TEST_DATA_DIR"); +} + +/** + * Build full path to test file + */ +std::string buildTestPath(const char* filename) { + const char* testDataDir = getTestDataDir(); + if (!testDataDir) { + return ""; + } + std::string path = testDataDir; + if (!path.empty() && path.back() != '/') { + path += '/'; + } + path += filename; + return path; +} + +/** + * Test reading a DWG file with timeout and memory check + * Returns: true if file was read without memory explosion + */ +bool testDWGFileMemorySafe(const char* filepath, const char* description, + long maxMemoryKB = 100 * 1024) { // Default 100MB limit + std::cout << "\n=== Test: " << description << " ===" << std::endl; + std::cout << " File: " << filepath << std::endl; + + // Check if file exists + std::ifstream filecheck(filepath); + if (!filecheck.good()) { + std::cout << " SKIP: File not found" << std::endl; + return true; // Not a failure, just skip + } + filecheck.close(); + +#if HAS_GETRUSAGE + long initialMemory = getMemoryUsageKB(); + std::cout << " Initial memory: " << initialMemory << " KB" << std::endl; +#endif + + auto startTime = std::chrono::steady_clock::now(); + + // Try to read the file + dwgR dwg(filepath); + TestInterface reader; + bool readSuccess = dwg.read(&reader, false); + + auto endTime = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast(endTime - startTime); + +#if HAS_GETRUSAGE + long finalMemory = getMemoryUsageKB(); + long memoryUsed = finalMemory - initialMemory; +#endif + + std::cout << " Read result: " << (readSuccess ? "success" : "failed") << std::endl; + std::cout << " Time: " << duration.count() << " ms" << std::endl; + +#if HAS_GETRUSAGE + std::cout << " Memory used: " << memoryUsed << " KB" << std::endl; + std::cout << " Final memory: " << finalMemory << " KB" << std::endl; + + // Check for memory explosion (the original bug) + // Use memory increase rather than absolute value to avoid false positives + if (memoryUsed > maxMemoryKB) { + std::cout << " FAIL: Memory increase exceeded limit of " << maxMemoryKB << " KB" << std::endl; + return false; + } +#else + std::cout << " Memory check: SKIP (not available on this platform)" << std::endl; +#endif + + // Check for reasonable execution time (the original bug caused infinite loop) + if (duration.count() > 30000) { // 30 seconds + std::cout << " FAIL: Execution time exceeded 30 seconds (possible infinite loop)" << std::endl; + return false; + } + + std::cout << " PASS: Memory and time within acceptable limits" << std::endl; + return true; +} + +/** + * Test AC1027 (R2013) file - should work correctly (regression test) + */ +bool testAC1027Regression() { + std::string filepath = buildTestPath("autocad_2013.dwg"); + if (filepath.empty()) { + std::cout << "\n=== Test: AC1027 (R2013) Regression Test ===" << std::endl; + std::cout << " SKIP: LIBDXFRW_TEST_DATA_DIR not set" << std::endl; + return true; + } + return testDWGFileMemorySafe( + filepath.c_str(), + "AC1027 (R2013) Regression Test" + ); +} + +/** + * Test AC1032 (R2018) file - should not cause memory explosion + */ +bool testAC1032MemorySafety_2018() { + std::string filepath = buildTestPath("autocad_2018.dwg"); + if (filepath.empty()) { + std::cout << "\n=== Test: AC1032 (R2018) Memory Safety Test ===" << std::endl; + std::cout << " SKIP: LIBDXFRW_TEST_DATA_DIR not set" << std::endl; + return true; + } + return testDWGFileMemorySafe( + filepath.c_str(), + "AC1032 (R2018) Memory Safety Test" + ); +} + +/** + * Test AC1032 (R2021) file - should not cause memory explosion + */ +bool testAC1032MemorySafety_2021() { + std::string filepath = buildTestPath("autocad_2021.dwg"); + if (filepath.empty()) { + std::cout << "\n=== Test: AC1032 (R2021) Memory Safety Test ===" << std::endl; + std::cout << " SKIP: LIBDXFRW_TEST_DATA_DIR not set" << std::endl; + return true; + } + return testDWGFileMemorySafe( + filepath.c_str(), + "AC1032 (R2021) Memory Safety Test" + ); +} + +/** + * Test AC1032 (R2024) file - should not cause memory explosion + */ +bool testAC1032MemorySafety_2024() { + std::string filepath = buildTestPath("autocad_2024.dwg"); + if (filepath.empty()) { + std::cout << "\n=== Test: AC1032 (R2024) Memory Safety Test ===" << std::endl; + std::cout << " SKIP: LIBDXFRW_TEST_DATA_DIR not set" << std::endl; + return true; + } + return testDWGFileMemorySafe( + filepath.c_str(), + "AC1032 (R2024) Memory Safety Test" + ); +} + +/** + * Test AC1018 (R2004) file - should work correctly (regression test) + */ +bool testAC1018Regression() { + std::string filepath = buildTestPath("autocad_2004.dwg"); + if (filepath.empty()) { + std::cout << "\n=== Test: AC1018 (R2004) Regression Test ===" << std::endl; + std::cout << " SKIP: LIBDXFRW_TEST_DATA_DIR not set" << std::endl; + return true; + } + return testDWGFileMemorySafe( + filepath.c_str(), + "AC1018 (R2004) Regression Test" + ); +} + +/** + * Test that version detection works correctly + */ +bool testVersionDetection() { + std::cout << "\n=== Test: Version Detection ===" << std::endl; + + const char* testDataDir = getTestDataDir(); + if (!testDataDir) { + std::cout << " SKIP: LIBDXFRW_TEST_DATA_DIR not set" << std::endl; + return true; + } + + struct VersionTest { + const char* filename; + const char* expectedVersion; + }; + + VersionTest tests[] = { + {"autocad_2004.dwg", "AC1018"}, + {"autocad_2013.dwg", "AC1027"}, + {"autocad_2018.dwg", "AC1032"}, + {"autocad_2021.dwg", "AC1032"}, + {"autocad_2024.dwg", "AC1032"}, + }; + + int numTests = sizeof(tests) / sizeof(tests[0]); + bool allPassed = true; + + for (int i = 0; i < numTests; i++) { + std::string filepath = buildTestPath(tests[i].filename); + std::ifstream file(filepath, std::ios::binary); + if (!file.good()) { + std::cout << " SKIP: " << tests[i].filename << " not found" << std::endl; + continue; + } + + char version[7] = {0}; + file.read(version, 6); + file.close(); + + std::cout << " " << tests[i].filename << std::endl; + std::cout << " Expected: " << tests[i].expectedVersion << std::endl; + std::cout << " Actual: " << version << std::endl; + + if (strcmp(version, tests[i].expectedVersion) != 0) { + std::cout << " FAIL: Version mismatch" << std::endl; + allPassed = false; + } else { + std::cout << " PASS" << std::endl; + } + } + + return allPassed; +} + +/** + * Test that reading non-existent file fails gracefully + */ +bool testNonExistentFile() { + std::cout << "\n=== Test: Non-existent File Handling ===" << std::endl; + + const char* nonExistent = "/this/file/does/not/exist.dwg"; + + dwgR dwg(nonExistent); + TestInterface reader; + bool result = dwg.read(&reader, false); + + std::cout << " File: " << nonExistent << std::endl; + std::cout << " Read result: " << (result ? "success (unexpected)" : "failed (expected)") << std::endl; + + if (result) { + std::cout << " FAIL: Should have failed for non-existent file" << std::endl; + return false; + } + + std::cout << " PASS: Correctly failed for non-existent file" << std::endl; + return true; +} + +/** + * Test that corrupted/empty file fails gracefully + */ +bool testCorruptedFile() { + std::cout << "\n=== Test: Corrupted File Handling ===" << std::endl; + + // Create a temp file with invalid content (using portable path) + std::string tempFilePath = buildTempFilePath("test_corrupt"); + const char* tempFile = tempFilePath.c_str(); + std::ofstream out(tempFile, std::ios::binary); + out << "This is not a valid DWG file"; + out.close(); + + dwgR dwg(tempFile); + TestInterface reader; + +#if HAS_GETRUSAGE + long initialMemory = getMemoryUsageKB(); +#endif + bool result = dwg.read(&reader, false); +#if HAS_GETRUSAGE + long finalMemory = getMemoryUsageKB(); +#endif + + std::cout << " Read result: " << (result ? "success (unexpected)" : "failed (expected)") << std::endl; +#if HAS_GETRUSAGE + std::cout << " Memory before: " << initialMemory << " KB" << std::endl; + std::cout << " Memory after: " << finalMemory << " KB" << std::endl; +#endif + + std::remove(tempFile); + + if (result) { + std::cout << " FAIL: Should have failed for corrupted file" << std::endl; + return false; + } + +#if HAS_GETRUSAGE + // Check for memory leak + if (finalMemory > initialMemory + 10240) { // Allow 10MB tolerance + std::cout << " FAIL: Possible memory leak" << std::endl; + return false; + } +#endif + + std::cout << " PASS: Correctly failed for corrupted file without memory issues" << std::endl; + return true; +} + +int main(int argc, char* argv[]) { + std::cout << "libdxfrw DWG Class Parsing Tests" << std::endl; + std::cout << "=================================" << std::endl; + std::cout << "Testing DWG reading with focus on class count edge cases" << std::endl; + + const char* testDataDir = getTestDataDir(); + if (testDataDir) { + std::cout << "Test data directory: " << testDataDir << std::endl; + } else { + std::cout << "Note: LIBDXFRW_TEST_DATA_DIR not set, some tests will be skipped" << std::endl; + } + + int failedTests = 0; + int totalTests = 0; + + // Version detection test + totalTests++; + if (!testVersionDetection()) failedTests++; + + // Non-existent file test + totalTests++; + if (!testNonExistentFile()) failedTests++; + + // Corrupted file test + totalTests++; + if (!testCorruptedFile()) failedTests++; + + // AC1018 regression test + totalTests++; + if (!testAC1018Regression()) failedTests++; + + // AC1027 regression test + totalTests++; + if (!testAC1027Regression()) failedTests++; + + // AC1032 memory safety tests (the main bug fix verification) + totalTests++; + if (!testAC1032MemorySafety_2018()) failedTests++; + + totalTests++; + if (!testAC1032MemorySafety_2021()) failedTests++; + + totalTests++; + if (!testAC1032MemorySafety_2024()) failedTests++; + + std::cout << "\n=================================" << std::endl; + std::cout << "Tests: " << (totalTests - failedTests) << "/" << totalTests << " passed" << std::endl; + + if (failedTests > 0) { + std::cout << "FAIL: " << failedTests << " test(s) failed" << std::endl; + return 1; + } else { + std::cout << "SUCCESS: All DWG class parsing tests passed!" << std::endl; + return 0; + } +}