diff --git a/UnitTest/UnitTest.vcxproj b/UnitTest/UnitTest.vcxproj
index e0db92731..db3b2fc5b 100644
--- a/UnitTest/UnitTest.vcxproj
+++ b/UnitTest/UnitTest.vcxproj
@@ -795,6 +795,7 @@
+
diff --git a/UnitTest/UnitTest.vcxproj.filters b/UnitTest/UnitTest.vcxproj.filters
index 5a2b4b128..a81f9d748 100644
--- a/UnitTest/UnitTest.vcxproj.filters
+++ b/UnitTest/UnitTest.vcxproj.filters
@@ -32,6 +32,9 @@
Source Files
+
+ Source Files
+
Source Files
diff --git a/UnitTest/tests/mapioutputtest.cpp b/UnitTest/tests/mapioutputtest.cpp
new file mode 100644
index 000000000..31dbc13b3
--- /dev/null
+++ b/UnitTest/tests/mapioutputtest.cpp
@@ -0,0 +1,517 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// Resolve only the exporter's memory APIs through this DLL, not an installed MAPI provider.
+#ifdef _M_IX86
+#pragma comment(linker, "/export:MAPIAllocateBuffer@8=_TestMAPIAllocateBuffer@8")
+#pragma comment(linker, "/export:MAPIFreeBuffer@4=_TestMAPIFreeBuffer@4")
+#pragma comment(linker, "/export:FreeProws@4=_TestFreeProws@4")
+#else
+#pragma comment(linker, "/export:MAPIAllocateBuffer=TestMAPIAllocateBuffer")
+#pragma comment(linker, "/export:MAPIFreeBuffer=TestMAPIFreeBuffer")
+#pragma comment(linker, "/export:FreeProws=TestFreeProws")
+#endif
+
+namespace
+{
+ union Buffer {
+ Buffer* next;
+ std::max_align_t alignment;
+ };
+
+ Buffer* buffers{};
+ ULONG invalidFrees{};
+ ULONG freedRowSets{};
+ std::vector freedPropertyTags;
+} // namespace
+
+extern "C" SCODE STDAPICALLTYPE TestMAPIAllocateBuffer(ULONG size, LPVOID* result)
+{
+ if (!result) return MAPI_E_INVALID_PARAMETER;
+ *result = nullptr;
+ if (static_cast(size) > (std::numeric_limits::max)() - sizeof(Buffer))
+ return MAPI_E_NOT_ENOUGH_MEMORY;
+ const auto buffer = static_cast(HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(Buffer) + size));
+ if (!buffer) return MAPI_E_NOT_ENOUGH_MEMORY;
+ buffer->next = buffers;
+ buffers = buffer;
+ *result = buffer + 1;
+ return S_OK;
+}
+
+extern "C" ULONG STDAPICALLTYPE TestMAPIFreeBuffer(LPVOID buffer)
+{
+ if (!buffer) return 0;
+ auto link = &buffers;
+ while (*link && *link + 1 != buffer)
+ link = &(*link)->next;
+ if (!*link)
+ {
+ ++invalidFrees;
+ return static_cast(MAPI_E_INVALID_PARAMETER);
+ }
+
+ const auto allocation = *link;
+ *link = allocation->next;
+ HeapFree(GetProcessHeap(), 0, allocation);
+ return 0;
+}
+
+extern "C" void STDAPICALLTYPE TestFreeProws(LPSRowSet rows)
+{
+ if (!rows) return;
+ ++freedRowSets;
+ for (ULONG row = 0; row < rows->cRows; ++row)
+ {
+ for (ULONG prop = 0; prop < rows->aRow[row].cValues; ++prop)
+ freedPropertyTags.push_back(rows->aRow[row].lpProps[prop].ulPropTag);
+ TestMAPIFreeBuffer(rows->aRow[row].lpProps);
+ }
+
+ TestMAPIFreeBuffer(rows);
+}
+
+namespace mapioutputtest
+{
+ constexpr ULONG numberTag = 0x60010003;
+ constexpr ULONG stringTag = 0x6002001F;
+
+ class TestTable final : public IMAPITable
+ {
+ public:
+ TestTable(ULONG rowCount, ULONG batchSize, bool withProperties = false)
+ : m_rowCount(rowCount), m_batchSize(batchSize), m_withProperties(withProperties)
+ {
+ }
+
+ HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, void** object) override
+ {
+ *object = nullptr;
+ return E_NOINTERFACE;
+ }
+ ULONG STDMETHODCALLTYPE AddRef() override { return ++m_refs; }
+ ULONG STDMETHODCALLTYPE Release() override { return --m_refs; }
+ HRESULT STDMETHODCALLTYPE GetLastError(HRESULT, ULONG, LPMAPIERROR*) override { return MAPI_E_NO_SUPPORT; }
+ HRESULT STDMETHODCALLTYPE Advise(ULONG, LPMAPIADVISESINK, ULONG_PTR*) override { return MAPI_E_NO_SUPPORT; }
+ HRESULT STDMETHODCALLTYPE Unadvise(ULONG_PTR) override { return MAPI_E_NO_SUPPORT; }
+ HRESULT STDMETHODCALLTYPE GetStatus(ULONG*, ULONG*) override { return MAPI_E_NO_SUPPORT; }
+ HRESULT STDMETHODCALLTYPE SetColumns(LPSPropTagArray, ULONG) override { return MAPI_E_NO_SUPPORT; }
+ HRESULT STDMETHODCALLTYPE QueryColumns(ULONG, LPSPropTagArray*) override { return MAPI_E_NO_SUPPORT; }
+ HRESULT STDMETHODCALLTYPE GetRowCount(ULONG, ULONG*) override { return MAPI_E_NO_SUPPORT; }
+ HRESULT STDMETHODCALLTYPE SeekRow(BOOKMARK origin, LONG count, LONG* sought) override
+ {
+ Assert::AreEqual(static_cast(BOOKMARK_BEGINNING), origin);
+ Assert::AreEqual(0L, count);
+ ++seekCount;
+ if (FAILED(seekResult)) return seekResult;
+ position = 0;
+ if (sought) *sought = 0;
+ return S_OK;
+ }
+ HRESULT STDMETHODCALLTYPE SeekRowApprox(ULONG, ULONG) override { return MAPI_E_NO_SUPPORT; }
+ HRESULT STDMETHODCALLTYPE QueryPosition(ULONG*, ULONG*, ULONG*) override { return MAPI_E_NO_SUPPORT; }
+ HRESULT STDMETHODCALLTYPE FindRow(LPSRestriction, BOOKMARK, ULONG) override { return MAPI_E_NO_SUPPORT; }
+ HRESULT STDMETHODCALLTYPE Restrict(LPSRestriction, ULONG) override { return MAPI_E_NO_SUPPORT; }
+ HRESULT STDMETHODCALLTYPE CreateBookmark(BOOKMARK*) override { return MAPI_E_NO_SUPPORT; }
+ HRESULT STDMETHODCALLTYPE FreeBookmark(BOOKMARK) override { return MAPI_E_NO_SUPPORT; }
+ HRESULT STDMETHODCALLTYPE SortTable(LPSSortOrderSet, ULONG) override { return MAPI_E_NO_SUPPORT; }
+ HRESULT STDMETHODCALLTYPE QuerySortOrder(LPSSortOrderSet*) override { return MAPI_E_NO_SUPPORT; }
+ HRESULT STDMETHODCALLTYPE QueryRows(LONG requested, ULONG flags, LPSRowSet* rows) override
+ {
+ Assert::AreEqual(20L, requested);
+ Assert::AreEqual(0UL, flags);
+ ++queryCount;
+ *rows = nullptr;
+ if (queryCount == failOnQuery && !returnRowsOnFailure) return MAPI_E_CALL_FAILED;
+ if (position == m_rowCount && nullAtEnd) return S_OK;
+ const auto count = (std::min)({static_cast(requested), m_batchSize, m_rowCount - position});
+ *rows = mapi::allocate(CbNewSRowSet(count));
+ if (!*rows) return MAPI_E_NOT_ENOUGH_MEMORY;
+ (*rows)->cRows = count;
+ if (m_withProperties)
+ {
+ for (ULONG row = 0; row < count; ++row)
+ {
+ auto& outputRow = (*rows)->aRow[row];
+ outputRow.lpProps = mapi::allocate(2 * sizeof(SPropValue));
+ if (!outputRow.lpProps) return MAPI_E_NOT_ENOUGH_MEMORY;
+ outputRow.cValues = 2;
+ outputRow.lpProps[0].ulPropTag = stringTag;
+ outputRow.lpProps[0].Value.lpszW = m_text.data();
+ outputRow.lpProps[1].ulPropTag = numberTag;
+ outputRow.lpProps[1].Value.l = static_cast(position + row);
+ }
+ }
+
+ position += count;
+ return queryCount == failOnQuery ? MAPI_E_CALL_FAILED : S_OK;
+ }
+ HRESULT STDMETHODCALLTYPE Abort() override { return MAPI_E_NO_SUPPORT; }
+ HRESULT STDMETHODCALLTYPE ExpandRow(ULONG, LPBYTE, ULONG, ULONG, LPSRowSet*, ULONG*) override
+ {
+ return MAPI_E_NO_SUPPORT;
+ }
+ HRESULT STDMETHODCALLTYPE CollapseRow(ULONG, LPBYTE, ULONG, ULONG*) override { return MAPI_E_NO_SUPPORT; }
+ HRESULT STDMETHODCALLTYPE WaitForCompletion(ULONG, ULONG, ULONG*) override { return MAPI_E_NO_SUPPORT; }
+ HRESULT STDMETHODCALLTYPE GetCollapseState(ULONG, ULONG, LPBYTE, ULONG*, LPBYTE*) override
+ {
+ return MAPI_E_NO_SUPPORT;
+ }
+ HRESULT STDMETHODCALLTYPE SetCollapseState(ULONG, ULONG, LPBYTE, BOOKMARK*) override
+ {
+ return MAPI_E_NO_SUPPORT;
+ }
+
+ ULONG queryCount{};
+ ULONG seekCount{};
+ ULONG position{};
+ ULONG failOnQuery{};
+ HRESULT seekResult{S_OK};
+ bool returnRowsOnFailure{};
+ bool nullAtEnd{};
+
+ private:
+ ULONG m_refs{1};
+ ULONG m_rowCount{};
+ ULONG m_batchSize{};
+ bool m_withProperties{};
+ std::wstring m_text{L"\u03A9<&"};
+ };
+
+ class OutputFile
+ {
+ public:
+ OutputFile()
+ {
+ FILE* file{};
+ Assert::AreEqual(0, tmpfile_s(&file));
+ m_file.reset(file);
+ Assert::IsTrue(_setmode(_fileno(file), _O_U16TEXT) != -1);
+ }
+
+ FILE* get() const noexcept { return m_file.get(); }
+
+ std::wstring read() const
+ {
+ Assert::AreEqual(0, fflush(get()));
+ Assert::AreEqual(0, fseek(get(), 0, SEEK_SET));
+ auto actual = std::wstring{};
+ for (auto ch = fgetwc(get()); ch != WEOF; ch = fgetwc(get()))
+ actual.push_back(static_cast(ch));
+ Assert::AreEqual(0, ferror(get()));
+ return actual;
+ }
+
+ private:
+ std::unique_ptr m_file{nullptr, fclose};
+ };
+
+ std::wstring ExpectedNumber(ULONG tag, ULONG value)
+ {
+ const auto names =
+ tag == numberTag ? L"\t\tPR_DOTSTUFF_STATE\n"
+ L"\t\tPR_NICK_NAME, PR_NICK_NAME_A, PR_NICK_NAME_W\n"
+ : tag == 0x60030003
+ ? L"\t\tPR_DROPDOWN_DISPLAY_NAME\n"
+ L"\t\tPR_DROPDOWN_DISPLAY_NAME_A, PR_DROPDOWN_DISPLAY_NAME_W\n"
+ : L"";
+ return strings::format(
+ L"\t\n"
+ L"%ws"
+ L"\t\t%u\n"
+ L"\t\t0x%X\n"
+ L"\t\n",
+ tag,
+ names,
+ value,
+ value);
+ }
+
+ std::wstring ExpectedProperties(ULONG value)
+ {
+ return ExpectedNumber(numberTag, value) + L"\t\n"
+ L"\t\t\n"
+ L"\t\tA9033C002600\n"
+ L"\t\n";
+ }
+
+ std::wstring ExpectedTable(ULONG rowCount, bool withProperties = false, ULONG firstValue = 0)
+ {
+ auto expected = std::wstring(L"\n\n");
+ for (ULONG row = 0; row < rowCount; ++row)
+ {
+ expected += strings::format(L"\n", row);
+ if (withProperties) expected += ExpectedProperties(firstValue + row);
+
+ expected += L"
\n";
+ }
+
+ return expected + L"
\n";
+ }
+
+ TEST_CLASS(mapioutputtest)
+ {
+ public:
+ TEST_CLASS_INITIALIZE(initialize) { unittest::init(); }
+
+ TEST_METHOD_INITIALIZE(initializeMemory)
+ {
+ Assert::IsTrue(buffers == nullptr);
+ invalidFrees = 0;
+ freedRowSets = 0;
+ freedPropertyTags.clear();
+ m_previousFreedRowSets = 0;
+ m_errors.clear();
+ HMODULE testModule{};
+ Assert::IsTrue(GetModuleHandleExW(0, L"UnitTest.dll", &testModule) != FALSE);
+ const auto original = mapistub::GetMAPIHandle();
+ if (original)
+ {
+ if (!GetModuleHandleExW(
+ GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, reinterpret_cast(original), &m_originalMAPI))
+ {
+ FreeLibrary(testModule);
+ Assert::Fail(L"Could not retain the original MAPI module.");
+ }
+ }
+
+ m_debugTag = registry::debugTag;
+ registry::debugTag = 0;
+ mapistub::SetMAPIHandle(nullptr);
+ mapistub::SetMAPIHandle(testModule);
+ m_installed = true;
+ m_originalDisplayError = std::move(error::displayError);
+ error::displayError = [this](const std::wstring& text) { m_errors.push_back(text); };
+ }
+
+ TEST_METHOD_CLEANUP(uninitializeMemory)
+ {
+ ULONG leakedBuffers{};
+ while (buffers)
+ {
+ ++leakedBuffers;
+ TestMAPIFreeBuffer(buffers + 1);
+ }
+
+ if (m_installed)
+ {
+ error::displayError = std::move(m_originalDisplayError);
+ mapistub::SetMAPIHandle(nullptr);
+ if (m_originalMAPI) mapistub::SetMAPIHandle(m_originalMAPI);
+ registry::debugTag = m_debugTag;
+ m_originalMAPI = nullptr;
+ m_installed = false;
+ }
+
+ Assert::AreEqual(0UL, leakedBuffers, L"The exporter leaked MAPI allocations.");
+ Assert::AreEqual(0UL, invalidFrees, L"The exporter freed an unowned MAPI allocation.");
+ }
+
+ TEST_METHOD(Test_TableRowIndicesAcrossBatches) { CheckTable(41, 20); }
+ TEST_METHOD(Test_TableRowIndicesShortBatches) { CheckTable(41, 7); }
+ TEST_METHOD(Test_TableRowIndicesSingleRowBatches) { CheckTable(21, 1); }
+
+ TEST_METHOD(Test_TableRowIndicesBoundaries)
+ {
+ for (const auto count : {0UL, 1UL, 19UL, 20UL, 21UL, 40UL})
+ CheckTable(count, 20);
+ }
+
+ TEST_METHOD(Test_TablePropertiesStayWithRowsAcrossBatches)
+ {
+ CheckTable(41, 7, true);
+ Assert::AreEqual(size_t{82}, freedPropertyTags.size());
+ for (size_t prop = 0; prop < freedPropertyTags.size(); prop += 2)
+ {
+ Assert::AreEqual(stringTag, freedPropertyTags[prop]);
+ Assert::AreEqual(numberTag, freedPropertyTags[prop + 1]);
+ }
+ }
+
+ TEST_METHOD(Test_TableSeeksToBeginning)
+ {
+ auto table = TestTable(3, 2, true);
+ table.position = 2;
+ auto file = OutputFile{};
+ output::outputTable(output::dbgLevel::NoDebug, file.get(), &table);
+ unittest::AreEqualEx(ExpectedTable(3, true), file.read());
+ Assert::AreEqual(1UL, table.seekCount);
+ Assert::IsTrue(m_errors.empty());
+ }
+
+ TEST_METHOD(Test_TableAcceptsNullTerminalRowSet)
+ {
+ auto table = TestTable(21, 20, true);
+ table.nullAtEnd = true;
+ auto file = OutputFile{};
+ output::outputTable(output::dbgLevel::NoDebug, file.get(), &table);
+ unittest::AreEqualEx(ExpectedTable(21, true), file.read());
+ Assert::AreEqual(3UL, table.queryCount);
+ Assert::AreEqual(2UL, freedRowSets);
+ Assert::IsTrue(m_errors.empty());
+ }
+
+ TEST_METHOD(Test_TableQueryFailureBeforeFirstRow) { CheckQueryFailure(1, false); }
+ TEST_METHOD(Test_TableQueryFailureAfterBatch) { CheckQueryFailure(2, false); }
+ TEST_METHOD(Test_TableQueryFailureFreesReturnedRows) { CheckQueryFailure(2, true); }
+
+ TEST_METHOD(Test_TableSeekFailureIsReportedAndExportContinues)
+ {
+ auto table = TestTable(3, 20, true);
+ table.position = 1;
+ table.seekResult = MAPI_E_CALL_FAILED;
+ auto file = OutputFile{};
+ output::outputTable(output::dbgLevel::NoDebug, file.get(), &table);
+ unittest::AreEqualEx(ExpectedTable(2, true, 1), file.read());
+ CheckError(L"SeekRow");
+ }
+
+ TEST_METHOD(Test_NullTableProducesNoOutput)
+ {
+ auto file = OutputFile{};
+ output::outputTable(output::dbgLevel::NoDebug, file.get(), nullptr);
+ Assert::IsTrue(file.read().empty());
+ Assert::IsTrue(m_errors.empty());
+ }
+
+ TEST_METHOD(Test_DisabledDebugDoesNotReadTable)
+ {
+ auto table = TestTable(3, 20);
+ output::outputTable(output::dbgLevel::Generic, nullptr, &table);
+ Assert::AreEqual(0UL, table.queryCount);
+ Assert::AreEqual(0UL, table.seekCount);
+ Assert::IsTrue(m_errors.empty());
+ }
+
+ TEST_METHOD(Test_PropertiesAreSortedWithoutChangingInput)
+ {
+ SPropValue props[3]{};
+ props[0].ulPropTag = 0x60030003;
+ props[0].Value.l = 3;
+ props[1].ulPropTag = numberTag;
+ props[1].Value.l = 1;
+ props[2].ulPropTag = 0x60020003;
+ props[2].Value.l = 2;
+ const auto original = std::vector(std::begin(props), std::end(props));
+ auto file = OutputFile{};
+ output::outputProperties(output::dbgLevel::NoDebug, file.get(), 3, props, nullptr, false);
+ unittest::AreEqualEx(
+ ExpectedNumber(numberTag, 1) + ExpectedNumber(0x60020003, 2) + ExpectedNumber(0x60030003, 3),
+ file.read());
+ Assert::IsTrue(memcmp(original.data(), props, sizeof(props)) == 0);
+ Assert::IsTrue(m_errors.empty());
+ }
+
+ TEST_METHOD(Test_RowSetOutputsEveryRowsProperties)
+ {
+ auto table = TestTable(2, 20, true);
+ LPSRowSet rows{};
+ Assert::AreEqual(S_OK, table.QueryRows(20, 0, &rows));
+ auto file = OutputFile{};
+ output::outputSRowSet(output::dbgLevel::NoDebug, file.get(), rows, nullptr);
+ FreeProws(rows);
+ unittest::AreEqualEx(ExpectedProperties(0) + ExpectedProperties(1), file.read());
+ Assert::IsTrue(m_errors.empty());
+ }
+
+ TEST_METHOD(Test_EmptyRowSetProducesNoOutput)
+ {
+ SRowSet rows{};
+ auto file = OutputFile{};
+ output::outputSRowSet(output::dbgLevel::NoDebug, file.get(), &rows, nullptr);
+ Assert::IsTrue(file.read().empty());
+ Assert::IsTrue(m_errors.empty());
+ }
+
+ TEST_METHOD(Test_InvalidRowInputsProduceDiagnostics)
+ {
+ auto file = OutputFile{};
+ output::outputSRowSet(output::dbgLevel::NoDebug, file.get(), nullptr, nullptr);
+ output::outputSRow(output::dbgLevel::NoDebug, file.get(), nullptr, nullptr);
+ SRow row{};
+ row.cValues = 1;
+ output::outputSRow(output::dbgLevel::NoDebug, file.get(), &row, nullptr);
+ output::outputProperties(output::dbgLevel::NoDebug, file.get(), 1, nullptr, nullptr, false);
+ const auto actual = file.read();
+ for (const auto message :
+ {L"OutputSRowSet called with NULL lpRowSet!",
+ L"OutputSRow called with NULL lpSRow!",
+ L"OutputSRow called with NULL lpSRow->lpProps!",
+ L"OutputProperties called with NULL lpProps!"})
+ Assert::IsTrue(actual.find(message) != std::wstring::npos);
+ Assert::IsTrue(m_errors.empty());
+ }
+
+ TEST_METHOD(Test_DisabledDebugSkipsRowAndPropertyOutput)
+ {
+ output::outputSRowSet(output::dbgLevel::Generic, nullptr, nullptr, nullptr);
+ output::outputSRow(output::dbgLevel::Generic, nullptr, nullptr, nullptr);
+ output::outputProperties(output::dbgLevel::Generic, nullptr, 1, nullptr, nullptr, false);
+ output::outputProperty(output::dbgLevel::Generic, nullptr, nullptr, nullptr, false);
+ Assert::IsTrue(m_errors.empty());
+ }
+
+ TEST_METHOD(Test_NullPropertyProducesNoOutput)
+ {
+ auto file = OutputFile{};
+ output::outputProperty(output::dbgLevel::NoDebug, file.get(), nullptr, nullptr, false);
+ Assert::IsTrue(file.read().empty());
+ Assert::IsTrue(m_errors.empty());
+ }
+
+ private:
+ void CheckTable(ULONG rowCount, ULONG batchSize, bool withProperties = false)
+ {
+ auto table = TestTable(rowCount, batchSize, withProperties);
+ auto file = OutputFile{};
+ output::outputTable(output::dbgLevel::NoDebug, file.get(), &table);
+ unittest::AreEqualEx(
+ ExpectedTable(rowCount, withProperties),
+ file.read(),
+ strings::format(L"Rows: %u, batch size: %u", rowCount, batchSize).c_str());
+ Assert::AreEqual((rowCount + batchSize - 1) / batchSize + 1, table.queryCount);
+ Assert::AreEqual(table.queryCount, freedRowSets - m_previousFreedRowSets);
+ m_previousFreedRowSets = freedRowSets;
+ Assert::AreEqual(1UL, table.seekCount);
+ Assert::IsTrue(m_errors.empty());
+ }
+
+ void CheckQueryFailure(ULONG failOnQuery, bool returnRows)
+ {
+ auto table = TestTable(25, 20, true);
+ table.failOnQuery = failOnQuery;
+ table.returnRowsOnFailure = returnRows;
+ auto file = OutputFile{};
+ output::outputTable(output::dbgLevel::NoDebug, file.get(), &table);
+ unittest::AreEqualEx(ExpectedTable((failOnQuery - 1) * 20, true), file.read());
+ Assert::AreEqual(failOnQuery, table.queryCount);
+ Assert::AreEqual(failOnQuery - (returnRows ? 0UL : 1UL), freedRowSets);
+ CheckError(L"QueryRows");
+ }
+
+ void CheckError(const std::wstring& function)
+ {
+ Assert::AreEqual(size_t{1}, m_errors.size());
+ Assert::IsTrue(m_errors[0].find(function) != std::wstring::npos);
+ Assert::IsTrue(m_errors[0].find(L"0x80004005") != std::wstring::npos);
+ }
+
+ HMODULE m_originalMAPI{};
+ bool m_installed{};
+ ULONG m_debugTag{};
+ ULONG m_previousFreedRowSets{};
+ std::function m_originalDisplayError;
+ std::vector m_errors;
+ };
+} // namespace mapioutputtest
diff --git a/core/mapi/mapiOutput.cpp b/core/mapi/mapiOutput.cpp
index 256bb9698..2b79e4053 100644
--- a/core/mapi/mapiOutput.cpp
+++ b/core/mapi/mapiOutput.cpp
@@ -252,6 +252,7 @@ namespace output
Output(ulDbgLvl, fFile, false, g_szXMLHeader);
Output(ulDbgLvl, fFile, false, L"\n");
+ ULONG iOutputRow = 0;
for (;;)
{
FreeProws(lpRows);
@@ -261,7 +262,7 @@ namespace output
for (ULONG iCurRow = 0; iCurRow < lpRows->cRows; iCurRow++)
{
- Outputf(ulDbgLvl, fFile, false, L"\n", iCurRow);
+ Outputf(ulDbgLvl, fFile, false, L"\n", iOutputRow++);
outputSRow(ulDbgLvl, fFile, &lpRows->aRow[iCurRow], nullptr);
Output(ulDbgLvl, fFile, false, L"
\n");
}