You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

EmbeddedExtractor.java 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  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. http://www.apache.org/licenses/LICENSE-2.0
  9. Unless required by applicable law or agreed to in writing, software
  10. distributed under the License is distributed on an "AS IS" BASIS,
  11. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. See the License for the specific language governing permissions and
  13. limitations under the License.
  14. ==================================================================== */
  15. package org.apache.poi.ss.extractor;
  16. import static org.apache.poi.util.StringUtil.endsWithIgnoreCase;
  17. import java.io.ByteArrayOutputStream;
  18. import java.io.IOException;
  19. import java.io.InputStream;
  20. import java.util.ArrayList;
  21. import java.util.Arrays;
  22. import java.util.Collections;
  23. import java.util.Iterator;
  24. import java.util.List;
  25. import org.apache.logging.log4j.LogManager;
  26. import org.apache.logging.log4j.Logger;
  27. import org.apache.poi.hpsf.ClassID;
  28. import org.apache.poi.hpsf.ClassIDPredefined;
  29. import org.apache.poi.poifs.filesystem.DirectoryNode;
  30. import org.apache.poi.poifs.filesystem.DocumentInputStream;
  31. import org.apache.poi.poifs.filesystem.Entry;
  32. import org.apache.poi.poifs.filesystem.Ole10Native;
  33. import org.apache.poi.poifs.filesystem.Ole10NativeException;
  34. import org.apache.poi.poifs.filesystem.POIFSFileSystem;
  35. import org.apache.poi.ss.usermodel.Drawing;
  36. import org.apache.poi.ss.usermodel.ObjectData;
  37. import org.apache.poi.ss.usermodel.Picture;
  38. import org.apache.poi.ss.usermodel.PictureData;
  39. import org.apache.poi.ss.usermodel.Shape;
  40. import org.apache.poi.ss.usermodel.ShapeContainer;
  41. import org.apache.poi.ss.usermodel.Sheet;
  42. import org.apache.poi.ss.usermodel.Workbook;
  43. import org.apache.poi.util.Beta;
  44. import org.apache.poi.util.IOUtils;
  45. import org.apache.poi.util.LocaleUtil;
  46. /**
  47. * This extractor class tries to identify various embedded documents within Excel files
  48. * and provide them via a common interface, i.e. the EmbeddedData instances
  49. */
  50. @Beta
  51. public class EmbeddedExtractor implements Iterable<EmbeddedExtractor> {
  52. private static final Logger LOG = LogManager.getLogger(EmbeddedExtractor.class);
  53. //arbitrarily selected; may need to increase
  54. private static final int MAX_RECORD_LENGTH = 1_000_000;
  55. // contentType
  56. private static final String CONTENT_TYPE_BYTES = "binary/octet-stream";
  57. private static final String CONTENT_TYPE_PDF = "application/pdf";
  58. private static final String CONTENT_TYPE_DOC = "application/msword";
  59. private static final String CONTENT_TYPE_XLS = "application/vnd.ms-excel";
  60. /**
  61. * @return the list of known extractors, if you provide custom extractors, override this method
  62. */
  63. @Override
  64. public Iterator<EmbeddedExtractor> iterator() {
  65. EmbeddedExtractor[] ee = {
  66. new Ole10Extractor(), new PdfExtractor(), new BiffExtractor(), new OOXMLExtractor(), new FsExtractor()
  67. };
  68. return Arrays.asList(ee).iterator();
  69. }
  70. public EmbeddedData extractOne(DirectoryNode src) throws IOException {
  71. for (EmbeddedExtractor ee : this) {
  72. if (ee.canExtract(src)) {
  73. return ee.extract(src);
  74. }
  75. }
  76. return null;
  77. }
  78. public EmbeddedData extractOne(Picture src) throws IOException {
  79. for (EmbeddedExtractor ee : this) {
  80. if (ee.canExtract(src)) {
  81. return ee.extract(src);
  82. }
  83. }
  84. return null;
  85. }
  86. public List<EmbeddedData> extractAll(Sheet sheet) throws IOException {
  87. Drawing<?> patriarch = sheet.getDrawingPatriarch();
  88. if (null == patriarch){
  89. return Collections.emptyList();
  90. }
  91. List<EmbeddedData> embeddings = new ArrayList<>();
  92. extractAll(patriarch, embeddings);
  93. return embeddings;
  94. }
  95. protected void extractAll(ShapeContainer<?> parent, List<EmbeddedData> embeddings) throws IOException {
  96. for (Shape shape : parent) {
  97. EmbeddedData data = null;
  98. if (shape instanceof ObjectData) {
  99. ObjectData od = (ObjectData)shape;
  100. try {
  101. if (od.hasDirectoryEntry()) {
  102. data = extractOne((DirectoryNode)od.getDirectory());
  103. } else {
  104. data = new EmbeddedData(od.getFileName(), od.getObjectData(), od.getContentType());
  105. }
  106. } catch (Exception e) {
  107. LOG.atWarn().withThrowable(e).log("Entry not found / readable - ignoring OLE embedding");
  108. }
  109. } else if (shape instanceof Picture) {
  110. data = extractOne((Picture)shape);
  111. } else if (shape instanceof ShapeContainer) {
  112. extractAll((ShapeContainer<?>)shape, embeddings);
  113. }
  114. if (data == null) {
  115. continue;
  116. }
  117. data.setShape(shape);
  118. String filename = data.getFilename();
  119. String extension = (filename == null || filename.lastIndexOf('.') == -1) ? ".bin" : filename.substring(filename.lastIndexOf('.'));
  120. // try to find an alternative name
  121. if (filename == null || filename.isEmpty() || filename.startsWith("MBD") || filename.startsWith("Root Entry")) {
  122. filename = shape.getShapeName();
  123. if (filename != null) {
  124. filename += extension;
  125. }
  126. }
  127. // default to dummy name
  128. if (filename == null || filename.isEmpty()) {
  129. filename = "picture_" + embeddings.size() + extension;
  130. }
  131. filename = filename.trim();
  132. data.setFilename(filename);
  133. embeddings.add(data);
  134. }
  135. }
  136. public boolean canExtract(DirectoryNode source) {
  137. return false;
  138. }
  139. public boolean canExtract(Picture source) {
  140. return false;
  141. }
  142. protected EmbeddedData extract(DirectoryNode dn) throws IOException {
  143. assert(canExtract(dn));
  144. ByteArrayOutputStream bos = new ByteArrayOutputStream(20000);
  145. try (POIFSFileSystem dest = new POIFSFileSystem()) {
  146. copyNodes(dn, dest.getRoot());
  147. // start with a reasonable big size
  148. dest.writeFilesystem(bos);
  149. }
  150. return new EmbeddedData(dn.getName(), bos.toByteArray(), CONTENT_TYPE_BYTES);
  151. }
  152. protected EmbeddedData extract(Picture source) throws IOException {
  153. return null;
  154. }
  155. public static class Ole10Extractor extends EmbeddedExtractor {
  156. @Override
  157. public boolean canExtract(DirectoryNode dn) {
  158. ClassID clsId = dn.getStorageClsid();
  159. return ClassIDPredefined.lookup(clsId) == ClassIDPredefined.OLE_V1_PACKAGE;
  160. }
  161. @Override
  162. public EmbeddedData extract(DirectoryNode dn) throws IOException {
  163. try {
  164. // TODO: inspect the CompObj record for more details, i.e. the content type
  165. Ole10Native ole10 = Ole10Native.createFromEmbeddedOleObject(dn);
  166. return new EmbeddedData(ole10.getFileName(), ole10.getDataBuffer(), CONTENT_TYPE_BYTES);
  167. } catch (Ole10NativeException e) {
  168. throw new IOException(e);
  169. }
  170. }
  171. }
  172. static class PdfExtractor extends EmbeddedExtractor {
  173. @Override
  174. public boolean canExtract(DirectoryNode dn) {
  175. ClassID clsId = dn.getStorageClsid();
  176. return (ClassIDPredefined.PDF.equals(clsId) || dn.hasEntry("CONTENTS"));
  177. }
  178. @Override
  179. public EmbeddedData extract(DirectoryNode dn) throws IOException {
  180. try(ByteArrayOutputStream bos = new ByteArrayOutputStream();
  181. InputStream is = dn.createDocumentInputStream("CONTENTS")) {
  182. IOUtils.copy(is, bos);
  183. return new EmbeddedData(dn.getName() + ".pdf", bos.toByteArray(), CONTENT_TYPE_PDF);
  184. }
  185. }
  186. @Override
  187. public boolean canExtract(Picture source) {
  188. PictureData pd = source.getPictureData();
  189. return (pd != null && pd.getPictureType() == Workbook.PICTURE_TYPE_EMF);
  190. }
  191. /**
  192. * Mac Office encodes embedded objects inside the picture, e.g. PDF is part of an EMF.
  193. * If an embedded stream is inside an EMF picture, this method extracts the payload.
  194. *
  195. * @return the embedded data in an EMF picture or null if none is found
  196. */
  197. @Override
  198. protected EmbeddedData extract(Picture source) throws IOException {
  199. // check for emf+ embedded pdf (poor mans style :( )
  200. // Mac Excel 2011 embeds pdf files with this method.
  201. PictureData pd = source.getPictureData();
  202. if (pd == null || pd.getPictureType() != Workbook.PICTURE_TYPE_EMF) {
  203. return null;
  204. }
  205. // TODO: investigate if this is just an EMF-hack or if other formats are also embedded in EMF
  206. byte[] pictureBytes = pd.getData();
  207. int idxStart = indexOf(pictureBytes, 0, "%PDF-".getBytes(LocaleUtil.CHARSET_1252));
  208. if (idxStart == -1) {
  209. return null;
  210. }
  211. int idxEnd = indexOf(pictureBytes, idxStart, "%%EOF".getBytes(LocaleUtil.CHARSET_1252));
  212. if (idxEnd == -1) {
  213. return null;
  214. }
  215. int pictureBytesLen = idxEnd-idxStart+6;
  216. byte[] pdfBytes = IOUtils.safelyClone(pictureBytes, idxStart, pictureBytesLen, MAX_RECORD_LENGTH);
  217. String filename = source.getShapeName().trim();
  218. if (!endsWithIgnoreCase(filename, ".pdf")) {
  219. filename += ".pdf";
  220. }
  221. return new EmbeddedData(filename, pdfBytes, CONTENT_TYPE_PDF);
  222. }
  223. }
  224. static class OOXMLExtractor extends EmbeddedExtractor {
  225. @Override
  226. public boolean canExtract(DirectoryNode dn) {
  227. return dn.hasEntry("package");
  228. }
  229. @Override
  230. public EmbeddedData extract(DirectoryNode dn) throws IOException {
  231. ClassIDPredefined clsId = ClassIDPredefined.lookup(dn.getStorageClsid());
  232. String contentType = null;
  233. String ext = null;
  234. if (clsId != null) {
  235. contentType = clsId.getContentType();
  236. ext = clsId.getFileExtension();
  237. }
  238. if (contentType == null || ext == null) {
  239. contentType = "application/zip";
  240. ext = ".zip";
  241. }
  242. DocumentInputStream dis = dn.createDocumentInputStream("package");
  243. byte[] data = IOUtils.toByteArray(dis);
  244. dis.close();
  245. return new EmbeddedData(dn.getName()+ext, data, contentType);
  246. }
  247. }
  248. static class BiffExtractor extends EmbeddedExtractor {
  249. @Override
  250. public boolean canExtract(DirectoryNode dn) {
  251. return canExtractExcel(dn) || canExtractWord(dn);
  252. }
  253. protected boolean canExtractExcel(DirectoryNode dn) {
  254. ClassIDPredefined clsId = ClassIDPredefined.lookup(dn.getStorageClsid());
  255. return (ClassIDPredefined.EXCEL_V7 == clsId
  256. || ClassIDPredefined.EXCEL_V8 == clsId
  257. || dn.hasEntry("Workbook") /*...*/);
  258. }
  259. protected boolean canExtractWord(DirectoryNode dn) {
  260. ClassIDPredefined clsId = ClassIDPredefined.lookup(dn.getStorageClsid());
  261. return (ClassIDPredefined.WORD_V7 == clsId
  262. || ClassIDPredefined.WORD_V8 == clsId
  263. || dn.hasEntry("WordDocument"));
  264. }
  265. @Override
  266. public EmbeddedData extract(DirectoryNode dn) throws IOException {
  267. EmbeddedData ed = super.extract(dn);
  268. if (canExtractExcel(dn)) {
  269. ed.setFilename(dn.getName() + ".xls");
  270. ed.setContentType(CONTENT_TYPE_XLS);
  271. } else if (canExtractWord(dn)) {
  272. ed.setFilename(dn.getName() + ".doc");
  273. ed.setContentType(CONTENT_TYPE_DOC);
  274. }
  275. return ed;
  276. }
  277. }
  278. static class FsExtractor extends EmbeddedExtractor {
  279. @Override
  280. public boolean canExtract(DirectoryNode dn) {
  281. return true;
  282. }
  283. @Override
  284. public EmbeddedData extract(DirectoryNode dn) throws IOException {
  285. EmbeddedData ed = super.extract(dn);
  286. ed.setFilename(dn.getName() + ".ole");
  287. // TODO: read the content type from CombObj stream
  288. return ed;
  289. }
  290. }
  291. protected static void copyNodes(DirectoryNode src, DirectoryNode dest) throws IOException {
  292. for (Entry e : src) {
  293. if (e instanceof DirectoryNode) {
  294. DirectoryNode srcDir = (DirectoryNode)e;
  295. DirectoryNode destDir = (DirectoryNode)dest.createDirectory(srcDir.getName());
  296. destDir.setStorageClsid(srcDir.getStorageClsid());
  297. copyNodes(srcDir, destDir);
  298. } else {
  299. try (InputStream is = src.createDocumentInputStream(e)) {
  300. dest.createDocument(e.getName(), is);
  301. }
  302. }
  303. }
  304. }
  305. /**
  306. * Knuth-Morris-Pratt Algorithm for Pattern Matching
  307. * Finds the first occurrence of the pattern in the text.
  308. */
  309. private static int indexOf(byte[] data, int offset, byte[] pattern) {
  310. int[] failure = computeFailure(pattern);
  311. int j = 0;
  312. if (data.length == 0) {
  313. return -1;
  314. }
  315. for (int i = offset; i < data.length; i++) {
  316. while (j > 0 && pattern[j] != data[i]) {
  317. j = failure[j - 1];
  318. }
  319. if (pattern[j] == data[i]) { j++; }
  320. if (j == pattern.length) {
  321. return i - pattern.length + 1;
  322. }
  323. }
  324. return -1;
  325. }
  326. /**
  327. * Computes the failure function using a boot-strapping process,
  328. * where the pattern is matched against itself.
  329. */
  330. private static int[] computeFailure(byte[] pattern) {
  331. int[] failure = new int[pattern.length];
  332. int j = 0;
  333. for (int i = 1; i < pattern.length; i++) {
  334. while (j > 0 && pattern[j] != pattern[i]) {
  335. j = failure[j - 1];
  336. }
  337. if (pattern[j] == pattern[i]) {
  338. j++;
  339. }
  340. failure[i] = j;
  341. }
  342. return failure;
  343. }
  344. }