crashtesting: buffer a shape's end only if its start is buffered

seen on converting interop-test-files/doc/forum-mso-en4-218597.doc to
rtf and reloading the export, which aborts with:

 soffice.bin: sw/source/writerfilter/dmapper/DomainMapper_Impl.cxx:10493:
 void writerfilter::dmapper::DomainMapper_Impl::substream(Id, const
 writerfilter::Reference<writerfilter::Stream>::Pointer_t&):
 Assertion `m_aContextStack.size() == contextSize' failed.

 #12 writerfilter::dmapper::DomainMapper_Impl::substream at engine/sw/source/writerfilter/dmapper/DomainMapper_Impl.cxx:10494
 #13 writerfilter::dmapper::DomainMapper::lcl_substream at engine/sw/source/writerfilter/dmapper/DomainMapper.cxx:5039
 #14 writerfilter::LoggedStream::substream at engine/sw/source/writerfilter/dmapper/LoggedResources.cxx:275
 #15 writerfilter::rtftok::RTFDocumentImpl::resolveSubstream at engine/sw/source/writerfilter/rtftok/rtfdocumentimpl.cxx:407
 #16 writerfilter::rtftok::RTFDocumentImpl::resolveSubstream at engine/sw/source/writerfilter/rtftok/rtfdocumentimpl.cxx:379
 #17 writerfilter::rtftok::RTFDocumentImpl::sectBreak at engine/sw/source/writerfilter/rtftok/rtfdocumentimpl.cxx:778
 #18 writerfilter::rtftok::RTFDocumentImpl::dispatchSymbol at engine/sw/source/writerfilter/rtftok/rtfdispatchsymbol.cxx:217

In the header \intbl makes the table buffer current, so \shptxt buffers
the shape start instead of sending it to the dmapper. A paragraph mark
inside the shape text finds no table properties in that buffer and
decides it is not in a table. Replaying the buffer then sends the start
to the dmapper, and the start pushes a paragraph context.

When the shape text group ends the current buffer is still set, so the
end is appended to the buffer that was just drained. Nothing replays
that buffer again, so the paragraph context stays on the stack and the
header substream ends deeper than it began.

The start and the end have tested the current buffer separately since:

commit 818d80eb0c
Date:   Fri Jun 28 16:47:49 2013 +0200

    RTF import: support shapes inside tables

so remember whether the shape is open in the dmapper, and send its end
there directly when it is

Signed-off-by: Caolán McNamara <caolan.mcnamara@collabora.com>
Change-Id: Ifb94f77e3db4d3fcc6041a7927e88c62d9d970a4
Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/12757
Reviewed-by: Miklos Vajna <vmiklos@collabora.com>
Tested-by: Jenkins CPCI <releng@collaboraoffice.com>
(cherry picked from commit 306c1f840c413b352940513c83d1498d599b8359)
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/211313
Reviewed-by: Xisco Fauli <xiscofauli@libreoffice.org>
Tested-by: Jenkins
This commit is contained in:
Caolán McNamara
2026-09-22 18:09:52 +02:00
committed by Xisco Fauli
parent 7934165914
commit 2bdc2aef5c
5 changed files with 38 additions and 5 deletions
@@ -0,0 +1 @@
{\rtf1{\header\pard\intbl{\shp{\*\shpinst{\shptxt A{\par}}}}\cell\row}\par}
@@ -134,6 +134,20 @@ CPPUNIT_TEST_FIXTURE(Test, testImageSizeInShapeText)
// i.e. the size of the icon was inherited from the containing shape, losing the own size.
CPPUNIT_ASSERT_EQUAL(nExpected, rImageSize.GetWidth());
}
CPPUNIT_TEST_FIXTURE(Test, testShapeInHeaderTableRow)
{
// Given a header with a shape in a table row, and a paragraph mark inside the shape text:
// When loading this document:
createSwDoc("shape-in-header-table-row.rtf");
// Then make sure the document loads and the shape is there:
// Without the accompanying fix in place, this test would have failed with an assertion
// failure in DomainMapper_Impl::substream(), because the header left the context stack one
// level deeper than it found it.
SwDoc* pDoc = getSwDoc();
CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(1), pDoc->GetSpzFrameFormats()->size());
}
}
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
@@ -2013,7 +2013,7 @@ void RTFDocumentImpl::replayBuffer(RTFBuffer_t& rBuffer, RTFSprms* const pSprms,
uno::Reference<drawing::XShape> xShape;
std::get<1>(aTuple)->getAny() >>= xShape;
if (xShape.is())
Mapper().startShape(xShape);
m_pSdrImport->open(xShape);
}
else if (std::get<0>(aTuple) == RTFBufferTypes::ResolveSubstream)
{
@@ -3813,7 +3813,9 @@ void RTFDocumentImpl::afterPopState(RTFParserState& rState)
&& !m_aStates.top().getDrawingObject().getHadShapeText())
{
m_aStates.top().setHadShapeText(true);
if (!m_aStates.top().getCurrentBuffer())
// A replay can send a buffered start before the group ends, and the end
// has to follow it.
if (m_pSdrImport->isOpenInMapper() || !m_aStates.top().getCurrentBuffer())
m_pSdrImport->close();
else
m_aStates.top().getCurrentBuffer()->emplace_back(RTFBufferTypes::EndShape,
+14 -3
View File
@@ -65,6 +65,7 @@ RTFSdrImport::RTFSdrImport(RTFDocumentImpl& rDocument,
, m_bTextFrame(false)
, m_bTextGraphicObject(false)
, m_bFakePict(false)
, m_bOpenInMapper(false)
{
if (xDstDoc)
m_aParents.push(xDstDoc->getDrawPage());
@@ -1165,10 +1166,10 @@ void RTFSdrImport::resolve(RTFShape& rShape, bool bClose, ShapeOrPict const shap
// cells exist - resolvePict() defers its own output the same way.
if (xShape.is() && !m_rImport.bufferShapeInsertion(xShape, bClose))
{
m_rImport.Mapper().startShape(xShape);
open(xShape);
if (bClose)
{
m_rImport.Mapper().endShape();
close();
}
}
@@ -1186,7 +1187,17 @@ void RTFSdrImport::resolve(RTFShape& rShape, bool bClose, ShapeOrPict const shap
m_xShape = std::move(xShape);
}
void RTFSdrImport::close() { m_rImport.Mapper().endShape(); }
void RTFSdrImport::open(uno::Reference<drawing::XShape> const& xShape)
{
m_rImport.Mapper().startShape(xShape);
m_bOpenInMapper = true;
}
void RTFSdrImport::close()
{
m_rImport.Mapper().endShape();
m_bOpenInMapper = false;
}
void RTFSdrImport::append(std::u16string_view aKey, std::u16string_view aValue)
{
@@ -53,6 +53,8 @@ public:
PICT
};
void resolve(RTFShape& rShape, bool bClose, ShapeOrPict shapeOrPict);
/// Send the start of a shape to the dmapper. It stays open there until its end is sent.
void open(css::uno::Reference<css::drawing::XShape> const& xShape);
void close();
void append(std::u16string_view aKey, std::u16string_view aValue);
/// Append property on the current parent.
@@ -78,6 +80,7 @@ public:
void popParent();
css::uno::Reference<css::drawing::XShape> const& getCurrentShape() const { return m_xShape; }
bool isFakePict() const { return m_bFakePict; }
bool isOpenInMapper() const { return m_bOpenInMapper; }
bool isTextGraphicObject() const { return m_bTextGraphicObject; }
private:
@@ -98,6 +101,8 @@ private:
bool m_bTextGraphicObject;
/// if inside \pict, but actually it's a shape (not a picture)
bool m_bFakePict;
/// If a shape start has gone to the dmapper and its end has not.
bool m_bOpenInMapper;
std::stack<writerfilter::dmapper::GraphicZOrderHelper> m_aGraphicZOrderHelpers;
};
} // namespace writerfilter::rtftok