3030import org .apache .commons .compress .archivers .zip .ZipArchiveInputStream ;
3131import org .apache .commons .compress .archivers .zip .ZipFile ;
3232import org .apache .commons .io .IOUtils ;
33+ import org .apache .commons .io .input .BoundedInputStream ;
3334import org .apache .commons .io .input .CloseShieldInputStream ;
3435import org .apache .commons .io .input .UnsynchronizedByteArrayInputStream ;
3536import org .xml .sax .ContentHandler ;
3839import org .apache .tika .annotation .TikaComponent ;
3940import org .apache .tika .detect .XmlRootExtractor ;
4041import org .apache .tika .exception .TikaException ;
42+ import org .apache .tika .extractor .EmbeddedDocumentExtractor ;
43+ import org .apache .tika .extractor .EmbeddedDocumentUtil ;
4144import org .apache .tika .io .TikaInputStream ;
4245import org .apache .tika .metadata .HttpHeaders ;
4346import org .apache .tika .metadata .Metadata ;
47+ import org .apache .tika .metadata .TikaCoreProperties ;
4448import org .apache .tika .mime .MediaType ;
4549import org .apache .tika .parser .ParseContext ;
4650import org .apache .tika .parser .Parser ;
@@ -87,12 +91,48 @@ public Set<MediaType> getSupportedTypes(ParseContext context) {
8791 return supportedTypes ;
8892 }
8993
94+ /**
95+ * The document preview of an iWork '09 package.
96+ */
97+ public final static String IWORK_THUMBNAIL_ENTRY = "QuickLook/Thumbnail.jpg" ;
98+
99+ /**
100+ * Bound on the preview held in memory until the content has been
101+ * parsed; a real one is well under a megabyte.
102+ */
103+ private static final long MAX_THUMBNAIL_BYTES = 20 * 1024 * 1024 ;
104+
90105 public void parse (TikaInputStream tis , ContentHandler handler , Metadata metadata ,
91106 ParseContext context ) throws IOException , SAXException , TikaException {
92107 ZipArchiveInputStream zip = new ZipArchiveInputStream (tis );
93108 ZipArchiveEntry entry = zip .getNextEntry ();
109+ //the package is read as a stream, so the preview may come before the
110+ //content: hold it back and emit it once the content is written, and
111+ //only when the embedded document extractor wants it at all
112+ EmbeddedDocumentExtractor extractor =
113+ EmbeddedDocumentUtil .getEmbeddedDocumentExtractor (context );
114+ Metadata thumbnailMetadata = null ;
115+ byte [] thumbnail = null ;
116+ XHTMLContentHandler xhtml = null ;
94117
95118 while (entry != null ) {
119+ if (IWORK_THUMBNAIL_ENTRY .equals (entry .getName ()) && zip .canReadEntryData (entry )) {
120+ thumbnailMetadata = thumbnailMetadata (context );
121+ if (extractor .shouldParseEmbedded (thumbnailMetadata , context )) {
122+ //read one byte past the limit so an oversized entry is
123+ //recognized and skipped instead of emitted truncated
124+ thumbnail = BoundedInputStream .builder ().setInputStream (zip )
125+ .setMaxCount (MAX_THUMBNAIL_BYTES + 1 ).get ().readAllBytes ();
126+ if (thumbnail .length > MAX_THUMBNAIL_BYTES ) {
127+ thumbnail = null ;
128+ metadata .add (TikaCoreProperties .TIKA_META_EXCEPTION_WARNING ,
129+ IWORK_THUMBNAIL_ENTRY + " exceeds " + MAX_THUMBNAIL_BYTES
130+ + " bytes and was skipped" );
131+ }
132+ }
133+ entry = zip .getNextEntry ();
134+ continue ;
135+ }
96136 if (!IWORK_CONTENT_ENTRIES .contains (entry .getName ())) {
97137 entry = zip .getNextEntry ();
98138 continue ;
@@ -104,7 +144,12 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata
104144 entryStream .reset (); // 4096 fails on github
105145
106146 if (type != null ) {
107- XHTMLContentHandler xhtml = new XHTMLContentHandler (handler , metadata , context );
147+ if (xhtml == null ) {
148+ //a package carries one content entry; guard against a
149+ //crafted one with several so the document is started once
150+ xhtml = new XHTMLContentHandler (handler , metadata , context );
151+ xhtml .startDocument ();
152+ }
108153 ContentHandler contentHandler ;
109154
110155 switch (type ) {
@@ -126,19 +171,40 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata
126171 }
127172
128173 metadata .set (HttpHeaders .CONTENT_TYPE , type .getType ().toString ());
129- xhtml .startDocument ();
130174 if (contentHandler != null ) {
131175 XMLReaderUtils .parseSAX (CloseShieldInputStream .wrap (entryStream ),
132176 contentHandler , context );
133177 }
134- xhtml .endDocument ();
135178 }
136179
137180 entry = zip .getNextEntry ();
138181 }
182+ if (xhtml != null ) {
183+ if (thumbnail != null ) {
184+ try (TikaInputStream thumbnailStream = TikaInputStream .get (thumbnail )) {
185+ extractor .parseEmbedded (thumbnailStream , xhtml , thumbnailMetadata , context ,
186+ true );
187+ }
188+ }
189+ xhtml .endDocument ();
190+ }
139191 // Don't close the zip InputStream (TIKA-1117).
140192 }
141193
194+ /**
195+ * The metadata of the document preview, a
196+ * {@link TikaCoreProperties.EmbeddedResourceType#THUMBNAIL} embedded
197+ * document.
198+ */
199+ private static Metadata thumbnailMetadata (ParseContext context ) {
200+ Metadata embeddedMetadata = Metadata .newInstance (context );
201+ embeddedMetadata .set (TikaCoreProperties .EMBEDDED_RESOURCE_TYPE ,
202+ TikaCoreProperties .EmbeddedResourceType .THUMBNAIL .toString ());
203+ embeddedMetadata .set (TikaCoreProperties .RESOURCE_NAME_KEY , IWORK_THUMBNAIL_ENTRY );
204+ embeddedMetadata .set (HttpHeaders .CONTENT_TYPE , "image/jpeg" );
205+ return embeddedMetadata ;
206+ }
207+
142208 private IWORKDocumentType detectType (InputStream entryStream , int markLimit ) throws IOException {
143209 byte [] bytes = new byte [markLimit ];
144210 try {
0 commit comments