Skip to content

Commit 81d75a8

Browse files
authored
1 parent 89de688 commit 81d75a8

29 files changed

Lines changed: 2699 additions & 141 deletions

File tree

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-apple-module/src/main/java/org/apache/tika/parser/iwork/KeynoteContentHandler.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,12 @@ public void endElement(String uri, String localName, String qName) throws SAXExc
142142
} else if (inMetadata && "key:authors".equals(qName)) {
143143
inMetaDataAuthors = false;
144144
} else if (inSlide && "sf:tabular-model".equals(qName)) {
145+
// If the final row has fewer cells than numberOfColumns,
146+
// parseTableData never reaches its </tr> emit. Close any open
147+
// row before </table> so the SAX stream stays balanced.
148+
if (currentColumn != null && currentColumn != 0) {
149+
xhtml.endElement("tr");
150+
}
145151
xhtml.endElement("table");
146152
tableId = null;
147153
numberOfColumns = null;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.tika.parser.iwork;
18+
19+
import static org.junit.jupiter.api.Assertions.assertTrue;
20+
21+
import org.junit.jupiter.api.Test;
22+
import org.xml.sax.helpers.AttributesImpl;
23+
24+
import org.apache.tika.metadata.Metadata;
25+
import org.apache.tika.parser.ParseContext;
26+
import org.apache.tika.sax.StrictXHTMLValidator;
27+
import org.apache.tika.sax.ToXMLContentHandler;
28+
import org.apache.tika.sax.XHTMLContentHandler;
29+
30+
public class KeynoteContentHandlerTest {
31+
32+
/**
33+
* TIKA-4744: Keynote tables whose final row contains fewer cells than
34+
* numberOfColumns left {@code <tr>} open when {@code </sf:tabular-model>}
35+
* fired {@code </table>}. Drive the handler with a synthetic 3-column
36+
* table whose final row has only 2 cells and assert the XHTML stays
37+
* balanced through endDocument.
38+
*/
39+
@Test
40+
public void testIncompleteFinalRowClosesTr() throws Exception {
41+
Metadata md = new Metadata();
42+
ToXMLContentHandler xml = new ToXMLContentHandler();
43+
XHTMLContentHandler xhtml = new XHTMLContentHandler(
44+
new StrictXHTMLValidator(xml), md, new ParseContext());
45+
KeynoteContentHandler h = new KeynoteContentHandler(xhtml, md);
46+
47+
xhtml.startDocument();
48+
49+
// <key:slide>
50+
h.startElement("", "slide", "key:slide", new AttributesImpl());
51+
// <sf:tabular-model sfa:ID="t1">
52+
AttributesImpl tabAttrs = new AttributesImpl();
53+
tabAttrs.addAttribute("", "sfa:ID", "sfa:ID", "CDATA", "t1");
54+
h.startElement("", "tabular-model", "sf:tabular-model", tabAttrs);
55+
// <sf:columns sf:count="3">
56+
AttributesImpl colAttrs = new AttributesImpl();
57+
colAttrs.addAttribute("", "sf:count", "sf:count", "CDATA", "3");
58+
h.startElement("", "columns", "sf:columns", colAttrs);
59+
h.endElement("", "columns", "sf:columns");
60+
// Two cells (less than 3) -> incomplete final row.
61+
AttributesImpl cell = new AttributesImpl();
62+
cell.addAttribute("", "sfa:s", "sfa:s", "CDATA", "A");
63+
h.startElement("", "ct", "sf:ct", cell);
64+
h.endElement("", "ct", "sf:ct");
65+
cell = new AttributesImpl();
66+
cell.addAttribute("", "sfa:s", "sfa:s", "CDATA", "B");
67+
h.startElement("", "ct", "sf:ct", cell);
68+
h.endElement("", "ct", "sf:ct");
69+
// </sf:tabular-model> -- without the fix this emits </table> while
70+
// <tr> is still topmost and StrictXHTMLValidator throws.
71+
h.endElement("", "tabular-model", "sf:tabular-model");
72+
// </key:slide>
73+
h.endElement("", "slide", "key:slide");
74+
75+
xhtml.endDocument();
76+
77+
String out = xml.toString();
78+
// Sanity: the cells emitted, the row closed, and the table closed.
79+
assertTrue(out.contains("<td>A</td>"), "expected td A; got: " + out);
80+
assertTrue(out.contains("<td>B</td>"), "expected td B; got: " + out);
81+
assertTrue(out.contains("</tr>"), "expected </tr>; got: " + out);
82+
assertTrue(out.contains("</table>"), "expected </table>; got: " + out);
83+
}
84+
}

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/FormattingTagManager.java

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -114,14 +114,16 @@ void applyFormatting(RunProperties runProperties) throws SAXException {
114114
}
115115

116116
if (runProperties.isBold() != isBold) {
117-
if (isStrikeThrough) {
118-
xhtml.endElement("s");
119-
isStrikeThrough = false;
120-
}
117+
// Close inner tags before flipping <b>. Nesting is <b><i><s><u>
118+
// (outermost to innermost), so close innermost first: u, s, i.
121119
if (isUnderline) {
122120
xhtml.endElement("u");
123121
isUnderline = false;
124122
}
123+
if (isStrikeThrough) {
124+
xhtml.endElement("s");
125+
isStrikeThrough = false;
126+
}
125127
if (isItalics) {
126128
xhtml.endElement("i");
127129
isItalics = false;
@@ -135,14 +137,15 @@ void applyFormatting(RunProperties runProperties) throws SAXException {
135137
}
136138

137139
if (runProperties.isItalics() != isItalics) {
138-
if (isStrikeThrough) {
139-
xhtml.endElement("s");
140-
isStrikeThrough = false;
141-
}
140+
// Close inner tags before flipping <i>: u then s (u is innermost).
142141
if (isUnderline) {
143142
xhtml.endElement("u");
144143
isUnderline = false;
145144
}
145+
if (isStrikeThrough) {
146+
xhtml.endElement("s");
147+
isStrikeThrough = false;
148+
}
146149
if (runProperties.isItalics()) {
147150
xhtml.startElement("i");
148151
} else {

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/OOXMLTikaBodyPartHandler.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,16 @@ private void popExpected(String tag) {
265265

266266
@Override
267267
public void startTable() throws SAXException {
268-
268+
// A <w:tbl> can appear nested inside an outer <w:p> -- corrupt-ish but
269+
// present in the corpus (e.g., <w:p><w:r>...<wps:txbx><w:txbxContent>
270+
// <w:tbl>...). At that point a run-level <b>/<i>/<u>/<s>/<a> may be on
271+
// the SAX stack just above where the <table> is about to land. When a
272+
// later paragraph inside a cell ends, formattingTags.closeAll() tries
273+
// to emit </b> for the outer-paragraph state, but <td> is topmost --
274+
// strict validator rejects the mismatch. Close pending formatting now
275+
// so the table opens at a clean layer and the outer style is forgotten.
276+
// Mirrors startSDT()'s same-shape guard.
277+
formattingTags.closeAll();
269278
xhtml.startElement("table");
270279
openStructuralTags.push("table");
271280
tableDepth++;
@@ -377,14 +386,21 @@ private void inlineNoteContent(byte[] xml, String cssClass) throws SAXException
377386
// from the footnote/endnote parts (needed for picture resolution)
378387
Map<String, String> noteRelationships = inlinePartMap.getLinkedRelationships();
379388
xhtml.startElement("div", "class", cssClass);
389+
// Track the inner handler so we can call its closeAnyPending() if
390+
// the inline-note parseSAX aborts mid-element. Without the drain
391+
// the surrounding </div> mismatches whatever the inner handler
392+
// left on the SAX stack (<p>/<td>/etc.) and StrictXHTMLValidator
393+
// propagates a misleading error.
394+
OOXMLTikaBodyPartHandler innerHandler = new OOXMLTikaBodyPartHandler(xhtml);
380395
try {
381396
XMLReaderUtils.parseSAX(new ByteArrayInputStream(xml),
382397
new EmbeddedContentHandler(
383398
new OOXMLWordAndPowerPointTextHandler(
384-
new OOXMLTikaBodyPartHandler(xhtml),
399+
innerHandler,
385400
noteRelationships)),
386401
parseContext);
387-
} catch (TikaException | IOException e) {
402+
} catch (TikaException | IOException | SAXException e) {
403+
innerHandler.closeAnyPending();
388404
xhtml.characters("[" + cssClass + " parse error]");
389405
}
390406
xhtml.endElement("div");

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/OOXMLWordAndPowerPointTextHandler.java

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,11 @@ public class OOXMLWordAndPowerPointTextHandler extends DefaultHandler {
146146
//have we signaled the start of a p?
147147
//pPr can happen multiple times within a p
148148
//<p><pPr/><r><t>text</t></r><pPr></p>
149-
private boolean pStarted = false;
149+
//
150+
//Stack rather than a single boolean: nested <w:p> (e.g., inside
151+
//<w:txbxContent>) must not clobber the outer paragraph's "started" marker,
152+
//or the outer </w:p> will skip its endParagraph and leave <p> open.
153+
private final java.util.Deque<Boolean> pStartedStack = new java.util.ArrayDeque<>();
150154
//alternate content can be embedded in itself.
151155
//need to track depth.
152156
//preferACChoice controls which branch is processed:
@@ -238,6 +242,7 @@ public void startElement(String uri, String localName, String qName, Attributes
238242
} else if (lastStartElementWasP) {
239243
// First child of <p> is not pPr — start paragraph immediately with defaults.
240244
bodyContentsHandler.startParagraph(currPProperties);
245+
markCurrentParagraphStarted();
241246
}
242247

243248
lastStartElementWasP = false;
@@ -271,16 +276,15 @@ public void startElement(String uri, String localName, String qName, Attributes
271276
runBuffer.append(TAB_CHAR);
272277
} else if (P.equals(localName)) {
273278
lastStartElementWasP = true;
274-
// Each <w:p> needs its own pStarted lifecycle. Without this,
275-
// a nested <w:p> (e.g., inside <wps:txbx>/<w:txbxContent>) would
276-
// inherit the outer paragraph's pStarted=true, suppress its own
277-
// startParagraph in the </w:pPr> branch, then fire its
278-
// endParagraph on </w:p> -- producing an unbalanced start/end
279-
// count that desyncs the XHTML <p>/<p> stream.
280-
pStarted = false;
279+
// Push a fresh frame for this <w:p>. A nested <w:p> (e.g., inside
280+
// <w:txbxContent>) must not share the outer paragraph's started-flag,
281+
// or the outer </w:p>'s endParagraph either fires twice (older bug)
282+
// or gets skipped (after the pStarted guard fix), and the XHTML
283+
// <p>/<p> stream desyncs either way.
284+
pStartedStack.push(Boolean.FALSE);
281285
} else if (B.equals(localName)) { //TODO: add bCs
282286
if (inR && inRPr) {
283-
currRunProperties.setBold(true);
287+
currRunProperties.setBold(getOnOff(atts, true));
284288
}
285289
} else if (TC.equals(localName)) {
286290
bodyContentsHandler.startTableCell();
@@ -290,11 +294,11 @@ public void startElement(String uri, String localName, String qName, Attributes
290294
} else if (I.equals(localName)) { //TODO: add iCs
291295
//rprs don't have to be inR; ignore those that aren't
292296
if (inR && inRPr) {
293-
currRunProperties.setItalics(true);
297+
currRunProperties.setItalics(getOnOff(atts, true));
294298
}
295299
} else if (STRIKE.equals(localName)) {
296300
if (inR && inRPr) {
297-
currRunProperties.setStrike(true);
301+
currRunProperties.setStrike(getOnOff(atts, true));
298302
}
299303
} else if (U.equals(localName)) {
300304
if (inR && inRPr) {
@@ -491,6 +495,33 @@ private String getStringVal(Attributes atts) {
491495
return "";
492496
}
493497

498+
/**
499+
* Reads a {@code ST_OnOff} {@code w:val} attribute: {@code "0"}/{@code "false"}/
500+
* {@code "off"} are off, anything else (including absent) follows the supplied
501+
* default. The toggle elements (&lt;w:b/&gt;, &lt;w:i/&gt;, &lt;w:strike/&gt;)
502+
* default to on when {@code w:val} is absent, but must respect an explicit
503+
* {@code w:val="0"} that turns the toggle off (overriding a style-inherited on).
504+
*/
505+
private boolean getOnOff(Attributes atts, boolean defaultValue) {
506+
String v = atts.getValue(W_NS, VAL);
507+
if (v == null) {
508+
return defaultValue;
509+
}
510+
return !("0".equals(v) || "false".equals(v) || "off".equals(v));
511+
}
512+
513+
private boolean isCurrentParagraphStarted() {
514+
Boolean top = pStartedStack.peek();
515+
return top != null && top;
516+
}
517+
518+
private void markCurrentParagraphStarted() {
519+
if (!pStartedStack.isEmpty()) {
520+
pStartedStack.pop();
521+
}
522+
pStartedStack.push(Boolean.TRUE);
523+
}
524+
494525
private int getIntVal(Attributes atts) {
495526
String valString = atts.getValue(W_NS, VAL);
496527
if (valString != null) {
@@ -531,9 +562,9 @@ public void endElement(String uri, String localName, String qName) throws SAXExc
531562
} else if (PPR.equals(localName) && inParagraphLevelPPr) {
532563
// Only process as paragraph properties if this pPr was a direct child of <p>.
533564
// pPr inside other elements (e.g., <a:fld> fields) must be ignored.
534-
if (!pStarted) {
565+
if (!isCurrentParagraphStarted()) {
535566
bodyContentsHandler.startParagraph(currPProperties);
536-
pStarted = true;
567+
markCurrentParagraphStarted();
537568
}
538569
currPProperties.reset();
539570
inParagraphLevelPPr = false;
@@ -544,8 +575,19 @@ public void endElement(String uri, String localName, String qName) throws SAXExc
544575
bodyContentsHandler.run(currRunProperties, runBuffer.toString());
545576
runBuffer.setLength(0);
546577
}
547-
pStarted = false;
548-
bodyContentsHandler.endParagraph();
578+
// Only fire endParagraph if startParagraph was actually called for this <w:p>.
579+
// A self-closing <w:p/> (e.g., inside <w:txbxContent>) has no children, so
580+
// neither the </pPr> branch nor the lastStartElementWasP branch fires
581+
// startParagraph -- but endElement(p) still runs. Without this guard the
582+
// body handler's pDepth counter desyncs and the outer paragraph's </p> gets
583+
// emitted prematurely, leaving the XHTML stack mismatched at endDocument.
584+
boolean started = pStartedStack.isEmpty() ? false : pStartedStack.pop();
585+
if (started) {
586+
bodyContentsHandler.endParagraph();
587+
}
588+
// Clear the "first child of p" trigger so the next outer-level startElement
589+
// doesn't spuriously fire startParagraph for this already-closed <w:p/>.
590+
lastStartElementWasP = false;
549591
} else if (TC.equals(localName)) {
550592
bodyContentsHandler.endTableCell();
551593
} else if (TR.equals(localName)) {

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFExcelExtractorDecorator.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,13 @@ protected void buildXHTML(XHTMLContentHandler xhtml)
239239
// the </tbody></table></div> emitted below land in the
240240
// right place.
241241
sheetExtractor.closeAnyPending();
242+
} catch (IOException e) {
243+
// Truncated stream — same risk: partial <tr>/<td> still
244+
// open. Close them so the surrounding </tbody></table>
245+
// stays balanced, record the failure, and keep going.
246+
metadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING,
247+
ExceptionUtils.getStackTrace(e));
248+
sheetExtractor.closeAnyPending();
242249
}
243250
try {
244251
getThreadedComments(container, sheetPart, xhtml);

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/rtf/RTFEmbObjHandler.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.apache.tika.metadata.TikaCoreProperties;
3636
import org.apache.tika.parser.ParseContext;
3737
import org.apache.tika.sax.EmbeddedContentHandler;
38+
import org.apache.tika.sax.XHTMLBalancingHandler;
3839

3940
/**
4041
* This class buffers data from embedded objects and pictures.
@@ -235,12 +236,24 @@ private void extractObj(byte[] bytes, ContentHandler handler, Metadata metadata)
235236
}
236237
metadata.set(TikaCoreProperties.RESOURCE_NAME_EXTENSION_INFERRED, true);
237238
}
239+
// Wrap the outer handler in a balancing handler so that if
240+
// the embedded parser aborts mid-element (leaving <p>/<b>/etc
241+
// open on the wire), we can drain those before propagating.
242+
// Without this, the outer RTF parser's subsequent
243+
// </b>/</p>/</body> sequence trips StrictXHTMLValidator on
244+
// unbalanced nesting and surfaces as a misleading error at
245+
// endDocument. Same shape as the SXWPF / Epub catch arms.
246+
XHTMLBalancingHandler balancer = new XHTMLBalancingHandler(handler);
238247
try {
239248
embeddedDocumentUtil
240-
.parseEmbedded(tis, new EmbeddedContentHandler(handler), metadata,
249+
.parseEmbedded(tis, new EmbeddedContentHandler(balancer), metadata,
241250
true);
242251
} catch (IOException e) {
252+
balancer.drainOpenElements();
243253
EmbeddedDocumentUtil.recordEmbeddedStreamException(e, metadata);
254+
} catch (SAXException e) {
255+
balancer.drainOpenElements();
256+
EmbeddedDocumentUtil.recordException(e, metadata);
244257
}
245258
}
246259
}

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/rtf/RTFParser.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.apache.tika.mime.MediaType;
3636
import org.apache.tika.parser.ParseContext;
3737
import org.apache.tika.parser.Parser;
38+
import org.apache.tika.sax.XHTMLBalancingHandler;
3839
import org.apache.tika.sax.XHTMLContentHandler;
3940

4041
/**
@@ -90,11 +91,26 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata
9091
TaggedInputStream tagged = new TaggedInputStream(tis);
9192
XHTMLContentHandler xhtml = new XHTMLContentHandler(handler, metadata, context);
9293
xhtml.startDocument();
94+
// Wrap xhtml in a balancing handler so the finally's endDocument
95+
// doesn't fire on an unbalanced stack if the RTF state machine
96+
// emits inconsistent SAX events (e.g., </b> while <p> is topmost
97+
// due to state-vs-stack drift on certain corpus files). Without
98+
// this, StrictXHTMLValidator's </body> vs <p>/<b> at endDocument
99+
// masks the real well-formedness error from inside extract().
100+
XHTMLBalancingHandler balancer = new XHTMLBalancingHandler(xhtml);
93101
try {
94-
parseInline(tis, xhtml, metadata, context);
102+
parseInline(tis, balancer, metadata, context);
95103
} catch (IOException e) {
96104
tagged.throwIfCauseOf(e);
105+
balancer.drainOpenElements();
97106
throw new TikaException("Error parsing an RTF document", e);
107+
} catch (SAXException | TikaException e) {
108+
// Drain on any exception escaping parseInline. parseInline can
109+
// throw TikaException too (memory limits, embedded extraction,
110+
// etc.); without the drain, the finally's xhtml.endDocument()
111+
// throws on the unbalanced stack and masks the real error.
112+
balancer.drainOpenElements();
113+
throw e;
98114
} finally {
99115
xhtml.endDocument();
100116
}

0 commit comments

Comments
 (0)