You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tika.apache.org by ta...@apache.org on 2026/08/22 01:19:20 UTC
(tika) 01/01: TIKA-4814 -- OneNote dom-order extraction and follow-ups
This is an automated email from the ASF dual-hosted git repository.
tballison pushed a commit to branch TIKA-4814-for-3x
in repository https://gitbox.apache.org/repos/asf/tika.git
commit 9519878af1fc27a1137d165eed563869546bf5f3
Author: tallison <ta...@apache.org>
AuthorDate: Fri Aug 21 21:19:02 2026 -0400
TIKA-4814 -- OneNote dom-order extraction and follow-ups
Backport to 3.x of a9abca092c plus the TIKA-4814-followup-1 work,
adapted to the 3.x APIs: onenote: metadata key strings instead of the
4.x OneNote metadata class, the 1-arg/4-arg EmbeddedDocumentExtractor
signatures, the 2-arg XHTMLContentHandler, and no TikaComponent or
EmbeddedLimitReachedException.
Co-authored-by: Henry Lindeman <he...@glean.com>
Co-authored-by: nddipiazza <nd...@users.noreply.github.com>
---
CHANGES.txt | 11 +
.../tika/parser/microsoft/onenote/FileNodePtr.java | 15 +-
.../apache/tika/parser/microsoft/onenote/GUID.java | 40 +-
.../parser/microsoft/onenote/OneNoteDocument.java | 3 +
.../parser/microsoft/onenote/OneNoteParser.java | 150 +++-
.../tika/parser/microsoft/onenote/OneNotePtr.java | 183 +++-
.../microsoft/onenote/OneNoteTreeWalker.java | 135 ++-
.../onenote/fsshttpb/MSOneStorePackage.java | 999 ++++++++++++++++++---
.../onenote/fsshttpb/MSOneStoreParser.java | 326 +++++--
.../property/PrtArrayOfPropertyValues.java | 13 +-
.../PrtFourBytesOfLengthFollowedByData.java | 6 +
.../onenote/fsshttpb/streamobj/DataElement.java | 3 +-
.../onenote/fsshttpb/streamobj/FileDataObject.java | 18 +-
.../onenote/fsshttpb/streamobj/ObjectDataBLOB.java | 73 ++
.../streamobj/ObjectDataBLOBDataElementData.java | 48 +
.../onenote/fsshttpb/streamobj/PropertySet.java | 23 +-
...{FileDataObject.java => RevisionStoreCell.java} | 21 +-
.../streamobj/RevisionStoreObjectGroup.java | 45 +-
.../onenote/fsshttpb/streamobj/StreamObject.java | 3 +-
.../onenote/fsshttpb/streamobj/basic/CellID.java | 8 +
.../onenote/fsshttpb/streamobj/basic/ExGuid.java | 9 +
.../space/ObjectSpaceObjectStreamOfContextIDs.java | 8 +-
.../space/ObjectSpaceObjectStreamOfOIDs.java | 8 +-
.../space/ObjectSpaceObjectStreamOfOSIDs.java | 6 +
.../tika/parser/microsoft/onenote/GUIDTest.java | 70 ++
.../microsoft/onenote/OneNoteParserTest.java | 337 ++++++-
.../onenote/fsshttpb/MSOneStoreBlobTest.java | 343 +++++++
.../onenote/fsshttpb/MSOneStorePackageTest.java | 846 +++++++++++++++++
.../onenote/fsshttpb/MSOneStoreParserTest.java | 286 ++++++
.../PropertyDeserializationBoundsTest.java | 184 ++++
.../resources/test-documents/testOneNote-fuzz1.one | Bin 0 -> 6448 bytes
.../resources/test-documents/testOneNote-fuzz2.one | Bin 0 -> 295501 bytes
.../resources/test-documents/testOneNote-fuzz3.one | Bin 0 -> 120720 bytes
.../test-documents/testOneNoteEmbeddedImage.one | Bin 0 -> 52223 bytes
34 files changed, 3918 insertions(+), 302 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 4d46e86962..64baa29f35 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -3,6 +3,17 @@ Release 3.3.3 - ???
* MagicDetector now compiles its regular expression once, in the
constructor, instead of recompiling it on every match (TIKA-4796).
+ * OneNote extraction now follows document order, omits superseded page
+ revisions, sorts author metadata, extracts embedded object BLOBs, and
+ bounds malformed-input recursion and file-derived allocations. Parse
+ warnings and embedded relationship IDs are exposed in metadata. Malformed
+ or truncated files that cannot be fully parsed, and files whose walk
+ yields no content, now fall back to the legacy string dump instead of
+ failing or returning empty output. The legacy MS-ONESTORE walker bounds
+ its recursion (depth caps plus file-node-list and fragment-chain cycle
+ guards) and now honors shouldParseEmbedded for embedded file data
+ (TIKA-4814).
+
* PDFs whose %PDF- header is preceded by a print-composition job ticket are
no longer detected as text/x-matlab. Up to 50 %% comment or blank lines of
up to 150 characters, and nothing else, may now precede the header; the
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/FileNodePtr.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/FileNodePtr.java
index 2646def38b..e82706a7ad 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/FileNodePtr.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/FileNodePtr.java
@@ -56,12 +56,21 @@ class FileNodePtr {
if (nodeListPositions.isEmpty()) {
return null;
}
- if (nodeListPositions.get(0) >= document.root.children.size()) {
+ int rootPosition = nodeListPositions.get(0);
+ if (rootPosition < 0 || rootPosition >= document.root.children.size()) {
throw new TikaException("Exceeded root child size");
}
- FileNode cur = document.root.children.get(nodeListPositions.get(0));
+ FileNode cur = document.root.children.get(rootPosition);
for (int i = 1, ie = nodeListPositions.size(); i < ie; ++i) {
- cur = cur.childFileNodeList.children.get(nodeListPositions.get(i));
+ if (cur == null || cur.childFileNodeList == null ||
+ cur.childFileNodeList.children == null) {
+ throw new TikaException("Missing child file node list");
+ }
+ int childPosition = nodeListPositions.get(i);
+ if (childPosition < 0 || childPosition >= cur.childFileNodeList.children.size()) {
+ throw new TikaException("Exceeded child file node size");
+ }
+ cur = cur.childFileNodeList.children.get(childPosition);
}
return cur;
}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/GUID.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/GUID.java
index 670824eec7..55e788bb4a 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/GUID.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/GUID.java
@@ -23,6 +23,7 @@ import java.util.Arrays;
import java.util.List;
import java.util.Locale;
+import org.apache.tika.exception.TikaException;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.util.BitConverter;
import org.apache.tika.utils.StringUtils;
@@ -41,17 +42,44 @@ public class GUID implements Comparable<GUID> {
* -EEEEEEEEEEEE}
* @return GUID object parsed from guid bytes.
*/
- public static GUID fromCurlyBraceUTF16Bytes(byte[] guid) {
+ public static GUID fromCurlyBraceUTF16Bytes(byte[] guid) throws TikaException {
int[] intGuid = new int[16];
- String utf16Str = new String(guid, StandardCharsets.UTF_16LE).replaceAll("\\{", "")
- .replaceAll("-", "").replaceAll("}", "");
- for (int i = 0; i < utf16Str.length(); i += 2) {
- intGuid[i / 2] =
- Integer.parseUnsignedInt("" + utf16Str.charAt(i) + utf16Str.charAt(i + 1), 16);
+ String utf16Str = new String(guid, StandardCharsets.UTF_16LE);
+ if (utf16Str.length() != 38 || utf16Str.charAt(0) != '{' ||
+ utf16Str.charAt(9) != '-' || utf16Str.charAt(14) != '-' ||
+ utf16Str.charAt(19) != '-' || utf16Str.charAt(24) != '-' ||
+ utf16Str.charAt(37) != '}') {
+ throw new TikaException("Invalid GUID string");
+ }
+ String hex = utf16Str.substring(1, 37).replace("-", "");
+ if (hex.length() != 32) {
+ throw new TikaException("Invalid GUID string");
+ }
+ for (int i = 0; i < hex.length(); i += 2) {
+ int high = asciiHexDigit(hex.charAt(i));
+ int low = asciiHexDigit(hex.charAt(i + 1));
+ if (high < 0 || low < 0) {
+ throw new TikaException("Invalid GUID string");
+ }
+ intGuid[i / 2] = (high << 4) | low;
}
return new GUID(intGuid);
}
+ // Character.digit accepts non-ASCII Unicode digits; GUIDs are ASCII hex only
+ private static int asciiHexDigit(char c) {
+ if (c >= '0' && c <= '9') {
+ return c - '0';
+ }
+ if (c >= 'a' && c <= 'f') {
+ return c - 'a' + 10;
+ }
+ if (c >= 'A' && c <= 'F') {
+ return c - 'A' + 10;
+ }
+ return -1;
+ }
+
public static int memcmp(int[] b1, int[] b2, int sz) {
for (int i = 0; i < sz; i++) {
if (b1[i] != b2[i]) {
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNoteDocument.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNoteDocument.java
index 936228f25c..cd76a58a55 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNoteDocument.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNoteDocument.java
@@ -34,6 +34,9 @@ class OneNoteDocument {
Map<ExtendedGUID, Pair<Long, ExtendedGUID>> revisionRoleMap = new HashMap<>();
ExtendedGUID currentRevision = ExtendedGUID.nil();
FileNodeList root = new FileNodeList();
+ // set when the root file node list could not be fully parsed (e.g. a truncated file);
+ // the header and any structure parsed so far remain usable
+ Exception structureParseException;
public OneNoteDocument() {
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNoteParser.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNoteParser.java
index d4a265caee..f7e03378c0 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNoteParser.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNoteParser.java
@@ -19,6 +19,7 @@ package org.apache.tika.parser.microsoft.onenote;
import java.io.IOException;
import java.io.InputStream;
import java.time.Instant;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -27,10 +28,13 @@ import java.util.Set;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.tuple.Pair;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.apache.tika.exception.TikaException;
+import org.apache.tika.exception.WriteLimitReachedException;
import org.apache.tika.io.TemporaryResources;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
@@ -52,6 +56,7 @@ import org.apache.tika.sax.XHTMLContentHandler;
public class OneNoteParser implements Parser {
public static final String ONE_NOTE_PREFIX = "onenote:";
+ private static final Logger LOG = LoggerFactory.getLogger(OneNoteParser.class);
private static final Map<MediaType, List<String>> TYPES_MAP = new HashMap<>();
/**
* Serial version UID
@@ -126,53 +131,76 @@ public class OneNoteParser implements Parser {
metadata.set(ONE_NOTE_PREFIX + "rgbPlaceholder",
"0x" + Long.toHexString(oneNoteDocument.header.rgbPlaceholder));
- Pair<Long, ExtendedGUID> roleAndContext = Pair.of(1L, ExtendedGUID.nil());
- OneNoteTreeWalker oneNoteTreeWalker =
- new OneNoteTreeWalker(options, oneNoteDocument, oneNoteDirectFileResource,
- xhtml, metadata, context, roleAndContext);
+ Exception structureFailure = oneNoteDocument.structureParseException;
+ boolean walked = false;
+ if (structureFailure == null) {
+ try {
+ Pair<Long, ExtendedGUID> roleAndContext = Pair.of(1L, ExtendedGUID.nil());
+ OneNoteTreeWalker oneNoteTreeWalker =
+ new OneNoteTreeWalker(options, oneNoteDocument,
+ oneNoteDirectFileResource, xhtml, metadata, context,
+ roleAndContext);
- oneNoteTreeWalker.walkTree();
+ oneNoteTreeWalker.walkTree();
- if (!oneNoteTreeWalker.getAuthors().isEmpty()) {
- metadata.set(TikaCoreProperties.CREATOR,
- oneNoteTreeWalker.getAuthors().toArray(new String[]{}));
+ if (!oneNoteTreeWalker.getAuthors().isEmpty()) {
+ metadata.set(TikaCoreProperties.CREATOR,
+ sortedValues(oneNoteTreeWalker.getAuthors()));
+ }
+ if (!oneNoteTreeWalker.getMostRecentAuthors().isEmpty()) {
+ metadata.set(
+ Property.externalTextBag(ONE_NOTE_PREFIX + "mostRecentAuthors"),
+ sortedValues(oneNoteTreeWalker.getMostRecentAuthors()));
+ }
+ if (!oneNoteTreeWalker.getOriginalAuthors().isEmpty()) {
+ metadata.set(
+ Property.externalTextBag(ONE_NOTE_PREFIX + "originalAuthors"),
+ sortedValues(oneNoteTreeWalker.getOriginalAuthors()));
+ }
+ if (!Instant.MAX.equals(
+ Instant.ofEpochMilli(oneNoteTreeWalker.getCreationTimestamp()))) {
+ metadata.set(ONE_NOTE_PREFIX + "creationTimestamp",
+ String.valueOf(oneNoteTreeWalker.getCreationTimestamp()));
+ }
+ if (!Instant.MIN.equals(oneNoteTreeWalker.getLastModifiedTimestamp())) {
+ metadata.set(ONE_NOTE_PREFIX + "lastModifiedTimestamp", String.valueOf(
+ oneNoteTreeWalker.getLastModifiedTimestamp().toEpochMilli()));
+ }
+ if (oneNoteTreeWalker.getLastModified() > Long.MIN_VALUE) {
+ metadata.set(TikaCoreProperties.MODIFIED,
+ String.valueOf(oneNoteTreeWalker.getLastModified()));
+ }
+ walked = true;
+ } catch (Exception e) {
+ rethrowIfLimitReached(e);
+ structureFailure = e;
+ }
}
- if (!oneNoteTreeWalker.getMostRecentAuthors().isEmpty()) {
- metadata.set(Property.externalTextBag(ONE_NOTE_PREFIX + "mostRecentAuthors"),
- oneNoteTreeWalker.getMostRecentAuthors().toArray(new String[]{}));
- }
- if (!oneNoteTreeWalker.getOriginalAuthors().isEmpty()) {
- metadata.set(Property.externalTextBag(ONE_NOTE_PREFIX + "originalAuthors"),
- oneNoteTreeWalker.getOriginalAuthors().toArray(new String[]{}));
- }
- if (!Instant.MAX.equals(
- Instant.ofEpochMilli(oneNoteTreeWalker.getCreationTimestamp()))) {
- metadata.set(ONE_NOTE_PREFIX + "creationTimestamp",
- String.valueOf(oneNoteTreeWalker.getCreationTimestamp()));
- }
- if (!Instant.MIN.equals(oneNoteTreeWalker.getLastModifiedTimestamp())) {
- metadata.set(ONE_NOTE_PREFIX + "lastModifiedTimestamp", String.valueOf(
- oneNoteTreeWalker.getLastModifiedTimestamp().toEpochMilli()));
- }
- if (oneNoteTreeWalker.getLastModified() > Long.MIN_VALUE) {
- metadata.set(TikaCoreProperties.MODIFIED,
- String.valueOf(oneNoteTreeWalker.getLastModified()));
+ if (!walked) {
+ legacyFallbackDump("OneNote parse failed; falling back to legacy text dump: " +
+ failureMessage(structureFailure), structureFailure, metadata,
+ xhtml, oneNoteDirectFileResource);
}
} else if (header.isLegacyOrAlternativePackaging()) {
+ MSOneStorePackage pkg = null;
try {
AlternativePackaging alternatePackageOneStoreFile = new AlternativePackaging();
+ //enable streaming deserialization
alternatePackageOneStoreFile.doDeserializeFromByteArray(oneStoreFileBytes, 0);
MSOneStoreParser onenoteParser = new MSOneStoreParser();
- MSOneStorePackage pkg =
- onenoteParser.parse(alternatePackageOneStoreFile.dataElementPackage);
+ pkg = onenoteParser.parse(alternatePackageOneStoreFile.dataElementPackage);
- pkg.walkTree(options, metadata, xhtml);
+ pkg.walkTree(options, metadata, xhtml, context);
} catch (Exception e) {
- OneNoteLegacyDumpStrings dumpStrings =
- new OneNoteLegacyDumpStrings(oneNoteDirectFileResource, xhtml);
- dumpStrings.dump();
+ rethrowIfLimitReached(e);
+ legacyFallbackDump(
+ "OneNote FSSHTTPB parse failed; falling back to legacy text dump: " +
+ failureMessage(e), e, metadata, xhtml,
+ oneNoteDirectFileResource);
+ pkg = null;
}
+ legacyFallbackIfNoContent(pkg, metadata, xhtml, oneNoteDirectFileResource);
} else {
throw new TikaException("Invalid OneStore document - could not parse headers");
}
@@ -182,6 +210,45 @@ public class OneNoteParser implements Parser {
}
+ private static String[] sortedValues(Set<String> values) {
+ String[] sorted = values.toArray(new String[0]);
+ Arrays.sort(sorted);
+ return sorted;
+ }
+
+ private static void rethrowIfLimitReached(Exception e) throws TikaException, SAXException {
+ WriteLimitReachedException.throwIfWriteLimitReached(e);
+ }
+
+ private static String failureMessage(Exception e) {
+ return e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage();
+ }
+
+ // the walk completed but every page dangled - without this a degraded
+ // file would yield empty output where the dump still finds its text
+ static void legacyFallbackIfNoContent(MSOneStorePackage pkg, Metadata metadata,
+ XHTMLContentHandler xhtml,
+ OneNoteDirectFileResource oneNoteDirectFileResource)
+ throws TikaException, SAXException {
+ if (pkg != null && !pkg.hasEmittedContent()) {
+ legacyFallbackDump("OneNote FSSHTTPB parse produced no content; " +
+ "falling back to legacy text dump", null, metadata, xhtml,
+ oneNoteDirectFileResource);
+ }
+ }
+
+ private static void legacyFallbackDump(String warning, Exception cause, Metadata metadata,
+ XHTMLContentHandler xhtml,
+ OneNoteDirectFileResource oneNoteDirectFileResource)
+ throws TikaException, SAXException {
+ LOG.warn(warning);
+ if (cause != null) {
+ LOG.debug("OneNote parse failure", cause);
+ }
+ metadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING, warning);
+ new OneNoteLegacyDumpStrings(oneNoteDirectFileResource, xhtml).dump();
+ }
+
/**
* Create a OneNoteDocument object.
* <p>
@@ -215,7 +282,8 @@ public class OneNoteParser implements Parser {
* content.
* @return A parsed one note document. This document does not contain any of the binary data,
* rather it just contains
- * the data pointers and metadata.
+ * the data pointers and metadata. A failure while parsing the root file node list is not
+ * thrown; it is recorded in the returned document's {@code structureParseException}.
* @throws IOException Will throw IOException in typical IO issue situations.
*/
public OneNoteDocument createOneNoteDocumentFromDirectFileResource(
@@ -227,9 +295,15 @@ public class OneNoteParser implements Parser {
if (oneNoteDocument.header.isMsOneStoreFormat()) {
// Now that we parsed the header, the "root file node list"
- oneNotePtr.reposition(oneNoteDocument.header.fcrFileNodeListRoot);
- FileNodePtr curPath = new FileNodePtr();
- oneNotePtr.deserializeFileNodeList(oneNoteDocument.root, curPath);
+ try {
+ oneNotePtr.reposition(oneNoteDocument.header.fcrFileNodeListRoot);
+ FileNodePtr curPath = new FileNodePtr();
+ oneNotePtr.deserializeFileNodeList(oneNoteDocument.root, curPath);
+ } catch (TikaException | IOException | RuntimeException e) {
+ // a truncated or malformed root list is recorded, not thrown, so the
+ // caller can fall back to the legacy string dump
+ oneNoteDocument.structureParseException = e;
+ }
}
return oneNoteDocument;
}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNotePtr.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNotePtr.java
index 38a9f3d9f6..916b9bbb5a 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNotePtr.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNotePtr.java
@@ -19,11 +19,12 @@ package org.apache.tika.parser.microsoft.onenote;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
+import java.util.Set;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.io.EndianUtils;
@@ -54,8 +55,28 @@ class OneNotePtr {
new byte[]{60, 0, 105, 0, 102, 0, 110, 0, 100, 0, 102, 0, 62, 0};
private static final String PACKAGE_STORAGE_FILE_FORMAT_GUID =
"{638DE92F-A6D4-4BC1-9A36-B3FC2511A5B7}";
+ private static final int MAX_PROPERTY_VALUES = 100_000;
+ private static final int MAX_PROPERTY_SET_DEPTH = 1000;
+ // the format nests file node lists only a handful of levels deep; 100 leaves wide
+ // margin while keeping the recursion far from any stack limit
+ private static final int MAX_FILE_NODE_LIST_DEPTH = 100;
+
+ private static final class PropertyValueBudget {
+ private long remaining = MAX_PROPERTY_VALUES;
+ }
+
+ /**
+ * Recursion state for the baseType-2 file-node-list nesting, shared by every pointer
+ * copied while parsing one document.
+ */
+ private static final class FileNodeListRecursion {
+ private int depth;
+ private final Set<Long> activeListOffsets = new HashSet<>();
+ }
int indentLevel = 0;
+ private PropertyValueBudget propertyValueBudget = new PropertyValueBudget();
+ private FileNodeListRecursion fileNodeListRecursion = new FileNodeListRecursion();
long offset;
long end;
@@ -77,6 +98,8 @@ class OneNotePtr {
this.offset = oneNotePtr.offset;
this.end = oneNotePtr.end;
this.indentLevel = oneNotePtr.indentLevel;
+ this.propertyValueBudget = oneNotePtr.propertyValueBudget;
+ this.fileNodeListRecursion = oneNotePtr.fileNodeListRecursion;
}
public OneNoteHeader deserializeHeader() throws IOException, TikaException {
@@ -260,8 +283,11 @@ class OneNotePtr {
public OneNotePtr internalDeserializeFileNodeList(OneNotePtr ptr, FileNodeList fileNodeList,
FileNodePtr curPath)
throws IOException, TikaException {
- OneNotePtr localPtr = new OneNotePtr(document, dif);
+ OneNotePtr localPtr = new OneNotePtr(ptr);
FileNodePtrBackPush bp = new FileNodePtrBackPush(curPath);
+ // a next-fragment reference pointing at an already-seen fragment would loop forever
+ Set<Long> seenFragmentOffsets = new HashSet<>();
+ seenFragmentOffsets.add(ptr.offset);
try {
while (true) {
FileChunkReference next = FileChunkReference.nil();
@@ -269,6 +295,11 @@ class OneNotePtr {
if (FileChunkReference.nil().equals(next)) {
break;
}
+ if (!seenFragmentOffsets.add(next.stp)) {
+ throw new TikaException(
+ "OneNote file node list fragment cycle detected at offset " +
+ next.stp);
+ }
localPtr.reposition(next);
ptr = localPtr;
}
@@ -279,9 +310,30 @@ class OneNotePtr {
}
+ /**
+ * Deserializes one file-node list and its fragments. The property-value budget is reset for
+ * each list; pointers copied while parsing that list share the budget.
+ */
public OneNotePtr deserializeFileNodeList(FileNodeList fileNodeList, FileNodePtr curPath)
throws IOException, TikaException {
- return internalDeserializeFileNodeList(this, fileNodeList, curPath);
+ if (fileNodeListRecursion.depth >= MAX_FILE_NODE_LIST_DEPTH) {
+ throw new TikaMemoryLimitException(
+ "OneNote file node list nesting exceeds depth limit " +
+ MAX_FILE_NODE_LIST_DEPTH);
+ }
+ long listOffset = offset;
+ if (!fileNodeListRecursion.activeListOffsets.add(listOffset)) {
+ throw new TikaException(
+ "OneNote file node list cycle detected at offset " + listOffset);
+ }
+ fileNodeListRecursion.depth++;
+ try {
+ propertyValueBudget = new PropertyValueBudget();
+ return internalDeserializeFileNodeList(this, fileNodeList, curPath);
+ } finally {
+ fileNodeListRecursion.depth--;
+ fileNodeListRecursion.activeListOffsets.remove(listOffset);
+ }
}
/**
@@ -453,23 +505,22 @@ class OneNotePtr {
data.subType.globalIdTableEntryFNDX.guid = deserializeGUID();
- document.revisionMap.get(document.currentRevision).globalId.put(
- data.subType.globalIdTableEntryFNDX.index,
+ Revision currentRevision = getCurrentRevision();
+ currentRevision.globalId.put(data.subType.globalIdTableEntryFNDX.index,
data.subType.globalIdTableEntryFNDX.guid);
} else if (data.id == FndStructureConstants.GlobalIdTableEntry2FNDX) {
data.subType.globalIdTableEntry2FNDX.indexMapFrom = deserializeLittleEndianInt();
data.subType.globalIdTableEntry2FNDX.indexMapTo = deserializeLittleEndianInt();
- ExtendedGUID dependentRevision =
- document.revisionMap.get(document.currentRevision).dependent;
- // Get the compactId from the revisionMap's globalId map.
- GUID compactId = document.revisionMap.get(dependentRevision).globalId.get(
+ Revision currentRevision = getCurrentRevision();
+ Revision dependentRevision = getRevision(currentRevision.dependent);
+ GUID compactId = dependentRevision.globalId.get(
data.subType.globalIdTableEntry2FNDX.indexMapFrom);
if (compactId == null) {
throw new TikaException("COMPACT_ID_MISSING");
}
- document.revisionMap.get(document.currentRevision).globalId.put(
- data.subType.globalIdTableEntry2FNDX.indexMapTo, compactId);
+ currentRevision.globalId.put(data.subType.globalIdTableEntry2FNDX.indexMapTo,
+ compactId);
} else if (data.id == FndStructureConstants.GlobalIdTableEntry3FNDX) {
data.subType.globalIdTableEntry3FNDX.indexCopyFromStart = deserializeLittleEndianInt();
@@ -477,16 +528,17 @@ class OneNotePtr {
data.subType.globalIdTableEntry3FNDX.indexCopyToStart = deserializeLittleEndianInt();
- ExtendedGUID dependent_revision =
- document.revisionMap.get(document.currentRevision).dependent;
- for (int i = 0; i < data.subType.globalIdTableEntry3FNDX.entriesToCopy; ++i) {
- Map<Long, GUID> globalIdMap = document.revisionMap.get(dependent_revision).globalId;
- GUID compactId = globalIdMap.get(
+ Revision currentRevision = getCurrentRevision();
+ Revision dependentRevision = getRevision(currentRevision.dependent);
+ int entriesToCopy = checkedCount(
+ data.subType.globalIdTableEntry3FNDX.entriesToCopy, "global ID entries");
+ for (int i = 0; i < entriesToCopy; ++i) {
+ GUID compactId = dependentRevision.globalId.get(
data.subType.globalIdTableEntry3FNDX.indexCopyFromStart + i);
if (compactId == null) {
throw new TikaException("COMPACT_ID_MISSING");
}
- document.revisionMap.get(document.currentRevision).globalId.put(
+ currentRevision.globalId.put(
data.subType.globalIdTableEntry3FNDX.indexCopyToStart + i, compactId);
}
} else if (data.id == FndStructureConstants.CanRevise.ObjectRevisionWithRefCountFNDX ||
@@ -623,9 +675,9 @@ class OneNotePtr {
"Data out of bounds - cch " + cch + " is > room left = " + roomLeftLong);
}
- if (cch > dif.size()) {
+ if (cch > dif.size() || cch > Integer.MAX_VALUE / 2L) {
throw new TikaMemoryLimitException(
- "CCH=" + cch + " was found that was greater" + " than file size " +
+ "CCH=" + cch + " cannot be safely allocated for file size " +
dif.size());
}
ByteBuffer dataSpaceBuffer = ByteBuffer.allocate((int) cch * 2);
@@ -816,7 +868,7 @@ class OneNotePtr {
compactID.guid = ExtendedGUID.nil();
compactID.guid.n = compactID.n;
long index = compactID.guidIndex;
- Map<Long, GUID> globalIdMap = document.revisionMap.get(document.currentRevision).globalId;
+ Map<Long, GUID> globalIdMap = getCurrentRevision().globalId;
GUID guid = globalIdMap.get(index);
if (guid != null) {
compactID.guid.guid = guid;
@@ -976,22 +1028,69 @@ class OneNotePtr {
}
}
+ private Revision getCurrentRevision() throws TikaException {
+ Revision revision = document.revisionMap.get(document.currentRevision);
+ if (revision == null) {
+ throw new TikaException("Missing current revision");
+ }
+ return revision;
+ }
+
+ private Revision getRevision(ExtendedGUID revisionId) throws TikaException {
+ Revision revision = document.revisionMap.get(revisionId);
+ if (revision == null) {
+ throw new TikaException("Missing dependent revision");
+ }
+ return revision;
+ }
+
+ private int checkedCount(long count, String description) throws TikaException {
+ if (count < 0 || count > MAX_PROPERTY_VALUES) {
+ throw new TikaMemoryLimitException(
+ description + " count " + count + " exceeds limit " + MAX_PROPERTY_VALUES);
+ }
+ return (int) count;
+ }
+
+ private int reservePropertyCount(long count, String description) throws TikaException {
+ int checked = checkedCount(count, description);
+ if (count > propertyValueBudget.remaining) {
+ throw new TikaMemoryLimitException(description + " count " + count +
+ " exceeds remaining property value budget " + propertyValueBudget.remaining);
+ }
+ propertyValueBudget.remaining -= count;
+ return checked;
+ }
+
private PropertySet deserializePropertySet(ObjectStreamCounters counters,
ObjectSpaceObjectPropSet streams)
throws IOException, TikaException {
+ return deserializePropertySet(counters, streams, 0);
+ }
+
+ private PropertySet deserializePropertySet(ObjectStreamCounters counters,
+ ObjectSpaceObjectPropSet streams,
+ int depth)
+ throws IOException, TikaException {
+ if (depth >= MAX_PROPERTY_SET_DEPTH) {
+ throw new TikaException("Property set nesting exceeds limit " +
+ MAX_PROPERTY_SET_DEPTH);
+ }
PropertySet data = new PropertySet();
- long count = deserializeLittleEndianShort();
- data.rgPridsData =
- Stream.generate(PropertyValue::new).limit((int) count).collect(Collectors.toList());
+ // Reserve the count before allocating any PropertyValue instances.
+ int count = reservePropertyCount(deserializeLittleEndianShort(), "property");
+ data.rgPridsData = new ArrayList<>();
for (int i = 0; i < count; ++i) {
- data.rgPridsData.get(i).propertyId = deserializePropertyID();
- LOG.debug("{}Property {}", getIndent(), data.rgPridsData.get(i).propertyId);
+ PropertyValue propertyValue = new PropertyValue();
+ propertyValue.propertyId = deserializePropertyID();
+ data.rgPridsData.add(propertyValue);
+ LOG.debug("{}Property {}", getIndent(), propertyValue.propertyId);
}
LOG.debug("{}{} elements in property set:", getIndent(), count);
for (int i = 0; i < count; ++i) {
data.rgPridsData.set(i,
deserializePropertyValueFromPropertyID(data.rgPridsData.get(i).propertyId,
- streams, counters));
+ streams, counters, depth));
}
LOG.debug("");
return data;
@@ -1000,8 +1099,13 @@ class OneNotePtr {
private PropertyValue deserializePropertyValueFromPropertyID(OneNotePropertyId propertyID,
ObjectSpaceObjectPropSet streams,
- ObjectStreamCounters counters)
+ ObjectStreamCounters counters,
+ int propertySetDepth)
throws IOException, TikaException {
+ if (propertySetDepth >= MAX_PROPERTY_SET_DEPTH) {
+ throw new TikaException("Property value nesting exceeds limit " +
+ MAX_PROPERTY_SET_DEPTH);
+ }
PropertyValue data = new PropertyValue();
data.propertyId = propertyID;
char val8;
@@ -1119,22 +1223,18 @@ class OneNotePtr {
val32 = deserializeLittleEndianInt();
OneNotePropertyId propId = deserializePropertyID();
LOG.debug(" UnifiedSubPropertySet {} {}", val32, propId);
- data.propertySet.rgPridsData =
- Stream.generate(PropertyValue::new).limit((int) val32)
- .collect(Collectors.toList());
- for (int i = 0; i < val32; ++i) {
- try {
- data.propertySet.rgPridsData.set(i,
- deserializePropertyValueFromPropertyID(propId, streams,
- counters));
- } catch (IOException e) {
- return data;
- }
+ int propertyCount = reservePropertyCount(val32, "unified property");
+ data.propertySet.rgPridsData = new ArrayList<>();
+ for (int i = 0; i < propertyCount; ++i) {
+ data.propertySet.rgPridsData.add(
+ deserializePropertyValueFromPropertyID(propId, streams, counters,
+ propertySetDepth + 1));
}
break;
case 0x11:
LOG.debug(" SubPropertySet");
- data.propertySet = deserializePropertySet(counters, streams);
+ data.propertySet = deserializePropertySet(counters, streams,
+ propertySetDepth + 1);
break;
default:
throw new TikaException("Invalid type: " + type);
@@ -1185,7 +1285,8 @@ class OneNotePtr {
getIndent(), data.count, data.osidsStreamNotPresent,
data.extendedStreamsPresent);
}
- for (int i = 0; i < data.count; ++i) {
+ int count = checkedCount(data.count, "object stream");
+ for (int i = 0; i < count; ++i) {
CompactID cid;
cid = deserializeCompactID();
data.data.add(cid);
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNoteTreeWalker.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNoteTreeWalker.java
index 452b48089e..939d36bbc0 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNoteTreeWalker.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNoteTreeWalker.java
@@ -27,6 +27,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -35,6 +36,8 @@ import java.util.regex.Pattern;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.tuple.Pair;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
@@ -44,6 +47,7 @@ import org.apache.tika.extractor.EmbeddedDocumentExtractor;
import org.apache.tika.extractor.EmbeddedDocumentUtil;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.sax.EmbeddedContentHandler;
import org.apache.tika.sax.XHTMLContentHandler;
@@ -54,6 +58,10 @@ import org.apache.tika.sax.XHTMLContentHandler;
*/
class OneNoteTreeWalker {
+ private static final Logger LOG = LoggerFactory.getLogger(OneNoteTreeWalker.class);
+ private static final int MAX_CYCLE_WARNINGS = 100;
+ // low enough that the ~3 stack frames per level cannot overflow a default thread stack
+ private static final int MAX_WALK_DEPTH = 500;
private static final String P = "p";
/**
* See spec MS-ONE - 2.3.1 - TIME32 - epoch of jan 1 1980 UTC.
@@ -102,6 +110,14 @@ class OneNoteTreeWalker {
* Contains pairs of {Offset,Length} that we have added to the text stream already.
*/
private final Set<Pair<Long, Integer>> textAlreadyFetched = new HashSet<>();
+ private final Set<FileNode> activeFileNodes =
+ Collections.newSetFromMap(new IdentityHashMap<>());
+ private final Set<FileNode> reportedCycleNodes =
+ Collections.newSetFromMap(new IdentityHashMap<>());
+ private int cycleWarningCount;
+ private boolean cycleWarningsSuppressed;
+ private int walkDepth;
+ private boolean depthWarningRecorded;
/**
* Create a one tree walker.
@@ -290,6 +306,68 @@ class OneNoteTreeWalker {
public Map<String, Object> walkFileNode(FileNode fileNode,
OneNotePropertyId parentPropertyId)
throws IOException, TikaException, SAXException {
+ if (fileNode == null) {
+ return Collections.emptyMap();
+ }
+ if (walkDepth >= MAX_WALK_DEPTH) {
+ // the cycle guard cannot catch a long acyclic chain - cap the recursion depth
+ // so a crafted file cannot overflow the stack
+ recordDepthWarning();
+ return Collections.emptyMap();
+ }
+ if (!activeFileNodes.add(fileNode)) {
+ recordCycleWarning(fileNode);
+ return Collections.emptyMap();
+ }
+ walkDepth++;
+ try {
+ return walkFileNodeInternal(fileNode, parentPropertyId);
+ } finally {
+ walkDepth--;
+ activeFileNodes.remove(fileNode);
+ }
+ }
+
+ private void recordDepthWarning() {
+ if (depthWarningRecorded) {
+ return;
+ }
+ depthWarningRecorded = true;
+ String warning = "OneNote file-node traversal exceeded depth limit " + MAX_WALK_DEPTH;
+ LOG.warn(warning);
+ if (parentMetadata != null) {
+ parentMetadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING, warning);
+ }
+ }
+
+ private void recordCycleWarning(FileNode fileNode) {
+ if (reportedCycleNodes.contains(fileNode)) {
+ return;
+ }
+ if (cycleWarningCount >= MAX_CYCLE_WARNINGS) {
+ if (!cycleWarningsSuppressed) {
+ cycleWarningsSuppressed = true;
+ String warning = "Additional OneNote file-node cycle warnings were suppressed " +
+ "after " + MAX_CYCLE_WARNINGS + " distinct cycles";
+ LOG.warn(warning);
+ if (parentMetadata != null) {
+ parentMetadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING, warning);
+ }
+ }
+ return;
+ }
+ reportedCycleNodes.add(fileNode);
+ cycleWarningCount++;
+ String warning = "OneNote file-node cycle detected at " + fileNode.gosid;
+ LOG.warn(warning);
+ if (parentMetadata != null) {
+ parentMetadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING, warning);
+ }
+ }
+
+ private Map<String, Object> walkFileNodeInternal(FileNode fileNode,
+ OneNotePropertyId parentPropertyId)
+ throws IOException, TikaException, SAXException {
Map<String, Object> structure = new HashMap<>();
structure.put("oneNoteType", "FileNode");
structure.put("gosid", fileNode.gosid.toString());
@@ -312,7 +390,7 @@ class OneNoteTreeWalker {
if (fileNode.subType.fileDataStoreObjectReference.ref != null && !FileChunkReference.nil()
.equals(fileNode.subType.fileDataStoreObjectReference.ref.fileData)) {
structure.put("fileDataStoreObjectReference", walkFileDataStoreObjectReference(
- fileNode.subType.fileDataStoreObjectReference));
+ fileNode.subType.fileDataStoreObjectReference, fileNode.gosid));
}
return structure;
}
@@ -325,7 +403,7 @@ class OneNoteTreeWalker {
* @throws IOException Can throw these when manipulating the seekable byte channel.
*/
private Map<String, Object> walkFileDataStoreObjectReference(
- FileDataStoreObjectReference fileDataStoreObjectReference)
+ FileDataStoreObjectReference fileDataStoreObjectReference, ExtendedGUID gosid)
throws IOException, SAXException, TikaException {
Map<String, Object> structure = new HashMap<>();
OneNotePtr content = new OneNotePtr(oneNoteDocument, dif);
@@ -335,12 +413,13 @@ class OneNoteTreeWalker {
"File data store cb " + fileDataStoreObjectReference.ref.fileData.cb +
" exceeds document size: " + dif.size());
}
- handleEmbedded((int) fileDataStoreObjectReference.ref.fileData.cb);
+ handleEmbedded((int) fileDataStoreObjectReference.ref.fileData.cb, gosid);
structure.put("fileDataStoreObjectMetadata", fileDataStoreObjectReference);
return structure;
}
- private void handleEmbedded(int length) throws TikaException, IOException, SAXException {
+ private void handleEmbedded(int length, ExtendedGUID gosid)
+ throws TikaException, IOException, SAXException {
TikaInputStream stream = null;
ByteBuffer buf;
try {
@@ -352,14 +431,21 @@ class OneNoteTreeWalker {
return;
}
Metadata embeddedMetadata = new Metadata();
+ embeddedMetadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE,
+ TikaCoreProperties.EmbeddedResourceType.ATTACHMENT.toString());
+ if (gosid != null && !ExtendedGUID.nil().equals(gosid)) {
+ embeddedMetadata.set(TikaCoreProperties.EMBEDDED_RELATIONSHIP_ID, gosid.toString());
+ }
try {
AttributesImpl attributes = new AttributesImpl();
attributes.addAttribute("", "class", "class", "CDATA", "embedded");
xhtml.startElement("div", attributes);
xhtml.endElement("div");
- stream = TikaInputStream.get(buf.array());
- embeddedDocumentExtractor.parseEmbedded(stream, new EmbeddedContentHandler(xhtml),
- embeddedMetadata, false);
+ if (embeddedDocumentExtractor.shouldParseEmbedded(embeddedMetadata)) {
+ stream = TikaInputStream.get(buf.array());
+ embeddedDocumentExtractor.parseEmbedded(stream, new EmbeddedContentHandler(xhtml),
+ embeddedMetadata, false);
+ }
} finally {
IOUtils.closeQuietly(stream);
}
@@ -471,8 +557,11 @@ class OneNoteTreeWalker {
propMap.put("dataUnicode16LE", new String(buf.array(), StandardCharsets.UTF_16LE));
if (options.getUtf16PropertiesToPrint().contains(propertyValue.propertyId.propertyEnum)) {
xhtml.startElement(P);
- xhtml.characters((String) propMap.get("dataUnicode16LE"));
- xhtml.endElement(P);
+ try {
+ xhtml.characters((String) propMap.get("dataUnicode16LE"));
+ } finally {
+ xhtml.endElement(P);
+ }
}
} else if (propertyValue.propertyId.propertyEnum ==
OneNotePropertyEnum.TextExtendedAscii) {
@@ -485,8 +574,11 @@ class OneNoteTreeWalker {
dif.read(buf);
propMap.put("dataAscii", new String(buf.array(), StandardCharsets.US_ASCII));
xhtml.startElement(P);
- xhtml.characters((String) propMap.get("dataAscii"));
- xhtml.endElement(P);
+ try {
+ xhtml.characters((String) propMap.get("dataAscii"));
+ } finally {
+ xhtml.endElement(P);
+ }
} else if (!isBinary) {
if (content.size() > dif.size()) {
throw new TikaMemoryLimitException(
@@ -498,8 +590,11 @@ class OneNoteTreeWalker {
propMap.put("dataUnicode16LE", new String(buf.array(), StandardCharsets.UTF_16LE));
if (options.getUtf16PropertiesToPrint().contains(propertyValue.propertyId.propertyEnum)) {
xhtml.startElement(P);
- xhtml.characters((String) propMap.get("dataUnicode16LE"));
- xhtml.endElement(P);
+ try {
+ xhtml.characters((String) propMap.get("dataUnicode16LE"));
+ } finally {
+ xhtml.endElement(P);
+ }
}
} else {
if (content.size() > dif.size()) {
@@ -591,12 +686,18 @@ class OneNoteTreeWalker {
Matcher m = HYPERLINK_PATTERN.matcher(txt);
if (m.find()) {
xhtml.startElement("a", "href", m.group(1));
- xhtml.characters(m.group(2));
- xhtml.endElement("a");
+ try {
+ xhtml.characters(m.group(2));
+ } finally {
+ xhtml.endElement("a");
+ }
} else {
xhtml.startElement(P);
- xhtml.characters(txt);
- xhtml.endElement(P);
+ try {
+ xhtml.characters(txt);
+ } finally {
+ xhtml.endElement(P);
+ }
}
}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/MSOneStorePackage.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/MSOneStorePackage.java
index 3b8ad38227..f459522ba9 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/MSOneStorePackage.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/MSOneStorePackage.java
@@ -25,27 +25,44 @@ import java.time.LocalDateTime;
import java.time.Month;
import java.time.ZoneOffset;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;
+import org.xml.sax.helpers.AttributesImpl;
import org.apache.tika.exception.TikaException;
+import org.apache.tika.extractor.EmbeddedDocumentExtractor;
+import org.apache.tika.extractor.EmbeddedDocumentUtil;
+import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.Property;
import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.microsoft.onenote.OneNotePropertyEnum;
import org.apache.tika.parser.microsoft.onenote.OneNoteTreeWalkerOptions;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.property.ArrayNumber;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.property.EightBytesOfData;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.property.FourBytesOfData;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.property.IProperty;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.property.PrtArrayOfPropertyValues;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.property.PrtFourBytesOfLengthFollowedByData;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.CellManifestDataElementData;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.PropertySet;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.RevisionManifestDataElementData;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.RevisionManifestRootDeclare;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.RevisionStoreCell;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.RevisionStoreObject;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.RevisionStoreObjectGroup;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.StorageIndexCellMapping;
@@ -59,6 +76,7 @@ import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.Propert
import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.PropertyType;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.unsigned.Unsigned;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.util.BitConverter;
+import org.apache.tika.sax.EmbeddedContentHandler;
import org.apache.tika.sax.XHTMLContentHandler;
public class MSOneStorePackage {
@@ -76,7 +94,11 @@ public class MSOneStorePackage {
private static final long DATETIME_EPOCH_DIFF_1601;
private static final Pattern HYPERLINK_PATTERN =
Pattern.compile("\uFDDFHYPERLINK\\s+\"([^\"]+)\"([^\"]+)$");
+ private static final Logger LOG = LoggerFactory.getLogger(MSOneStorePackage.class);
private static final String P = "p";
+ private static final int MAX_OBJECT_WALK_DEPTH = 1000;
+ private static final int MAX_REFERENCE_COUNT = 100000;
+ private static final int MAX_PARSE_WARNINGS = 100;
static {
LocalDateTime time32Epoch1980 = LocalDateTime.of(1980, Month.JANUARY, 1, 0, 0);
@@ -93,6 +115,10 @@ public class MSOneStorePackage {
private final Set<String> authors = new HashSet<>();
private final Set<String> mostRecentAuthors = new HashSet<>();
private final Set<String> originalAuthors = new HashSet<>();
+ /**
+ * The fully populated storage index. Set this before performing storage-index lookups; its
+ * mapping lists are indexed once and must not be mutated afterward.
+ */
public StorageIndexDataElementData storageIndex;
public StorageManifestDataElementData storageManifest;
public CellManifestDataElementData headerCellCellManifest;
@@ -100,52 +126,75 @@ public class MSOneStorePackage {
public List<RevisionManifestDataElementData> revisionManifests;
public List<CellManifestDataElementData> cellManifests;
public HeaderCell headerCell;
- public List<RevisionStoreObjectGroup> dataRoot;
+ public RevisionStoreCell dataRootCell;
public List<RevisionStoreObjectGroup> OtherFileNodeList;
- private boolean mostRecentAuthorProp = false;
- private boolean originalAuthorProp = false;
+ /**
+ * The content cells (object spaces, e.g. pages), each with its object groups and the
+ * root object declarations of its current revision.
+ */
+ public List<RevisionStoreCell> cells;
private Instant lastModifiedTimestamp = Instant.MIN;
private long creationTimestamp = Long.MAX_VALUE;
private long lastModified = Long.MIN_VALUE;
+ private ParseContext parseContext;
+ private EmbeddedDocumentExtractor embeddedDocumentExtractor;
+ private Metadata parentMetadata;
+ private final List<String> parseWarnings = new ArrayList<>();
+ // This state intentionally spans parser construction and tree walking for one package.
+ private final Set<String> recordedParseWarningKeys = new HashSet<>();
+ private boolean parseWarningsSuppressed;
+ private boolean storageMappingsIndexed;
+ private boolean contentEmitted;
+ // flattened property actions per object; objects can be re-flattened many times during
+ // picture/resource-name resolution, which is quadratic without this cache
+ private final Map<RevisionStoreObject, List<PropertyAction>> objectActionsCache =
+ new IdentityHashMap<>();
+ private final Map<CellID, StorageIndexCellMapping> storageIndexCellMappingsById =
+ new HashMap<>();
+ private final Map<ExGuid, StorageIndexRevisionMapping> storageIndexRevisionMappingsById =
+ new HashMap<>();
public MSOneStorePackage() {
this.revisionManifests = new ArrayList<>();
this.cellManifests = new ArrayList<>();
this.OtherFileNodeList = new ArrayList<>();
+ this.cells = new ArrayList<>();
}
/**
* This method is used to find the Storage Index Cell Mapping matches the Cell ID.
*
* @param cellID Specify the Cell ID.
- * @return Return the specific Storage Index Cell Mapping.
+ * @return the specific Storage Index Cell Mapping, or {@code null} if it is absent.
*/
public StorageIndexCellMapping findStorageIndexCellMapping(CellID cellID) {
- StorageIndexCellMapping storageIndexCellMapping = null;
- if (this.storageIndex != null) {
- storageIndexCellMapping = this.storageIndex.storageIndexCellMappingList.stream()
- .filter(s -> s.cellID.equals(cellID)).findFirst()
- .orElse(new StorageIndexCellMapping());
- }
- return storageIndexCellMapping;
+ indexStorageMappings();
+ return storageIndexCellMappingsById.get(cellID);
}
/**
* This method is used to find the Storage Index Revision Mapping that matches the Revision Mapping Extended GUID.
*
* @param revisionExtendedGUID Specify the Revision Mapping Extended GUID.
- * @return Return the instance of Storage Index Revision Mapping.
+ * @return the instance of Storage Index Revision Mapping, or {@code null} if it is absent.
*/
public StorageIndexRevisionMapping findStorageIndexRevisionMapping(
ExGuid revisionExtendedGUID) {
- StorageIndexRevisionMapping instance = null;
- if (this.storageIndex != null) {
- instance = this.storageIndex.storageIndexRevisionMappingList.stream()
- .filter(r -> r.revisionExGuid.equals(revisionExtendedGUID)).findFirst()
- .orElse(new StorageIndexRevisionMapping());
- }
+ indexStorageMappings();
+ return storageIndexRevisionMappingsById.get(revisionExtendedGUID);
+ }
- return instance;
+ private void indexStorageMappings() {
+ if (storageMappingsIndexed || storageIndex == null) {
+ return;
+ }
+ storageMappingsIndexed = true;
+ for (StorageIndexCellMapping mapping : storageIndex.storageIndexCellMappingList) {
+ storageIndexCellMappingsById.putIfAbsent(mapping.cellID, mapping);
+ }
+ for (StorageIndexRevisionMapping mapping : storageIndex.storageIndexRevisionMappingList) {
+ storageIndexRevisionMappingsById.putIfAbsent(mapping.revisionExGuid, mapping);
+ }
}
/**
@@ -160,112 +209,810 @@ public class MSOneStorePackage {
property == OneNotePropertyEnum.RichEditTextUnicode;
}
+ /**
+ * The attribution of an Author property, determined by the property through which the
+ * author object was referenced.
+ */
+ private enum AuthorRole {
+ NONE, MOST_RECENT, ORIGINAL
+ }
+
+ private enum ParseWarningKind {
+ DEFAULT,
+ OBJECT_REFERENCE_ARRAY_UNAVAILABLE,
+ OBJECT_REFERENCE_ARRAY_CAPPED,
+ OBJECT_SPACE_REFERENCE_ARRAY_UNAVAILABLE,
+ OBJECT_SPACE_REFERENCE_ARRAY_CAPPED
+ }
+
public void walkTree(OneNoteTreeWalkerOptions options, Metadata metadata,
- XHTMLContentHandler xhtml)
+ XHTMLContentHandler xhtml, ParseContext parseContext)
throws SAXException, TikaException, IOException {
- for (RevisionStoreObjectGroup revisionStoreObjectGroup : OtherFileNodeList) {
- for (RevisionStoreObject revisionStoreObject : revisionStoreObjectGroup.objects) {
- PropertySet propertySet =
- revisionStoreObject.propertySet.objectSpaceObjectPropSet.body;
- for (int i = 0; i < propertySet.rgData.size(); ++i) {
- IProperty property = propertySet.rgData.get(i);
- PropertyID propertyID = propertySet.rgPrids[i];
- PropertyType propertyType = PropertyType.fromIntVal(propertyID.type);
- OneNotePropertyEnum oneNotePropertyEnum =
- OneNotePropertyEnum.of(Unsigned.uint(propertyID.value).longValue());
- if (oneNotePropertyEnum == OneNotePropertyEnum.LastModifiedTimeStamp) {
- long fullval = getScalar(property);
- Instant instant = Instant.ofEpochSecond(
- fullval / 10000000 + DATETIME_EPOCH_DIFF_1601);
- if (instant.isAfter(lastModifiedTimestamp)) {
- lastModifiedTimestamp = instant;
- }
- metadata.set(ONE_NOTE_PREFIX + "lastModifiedTimestamp",
- String.valueOf(lastModifiedTimestamp.toEpochMilli()));
- } else if (oneNotePropertyEnum == OneNotePropertyEnum.CreationTimeStamp) {
- // add the TIME32_EPOCH_DIFF_1980 because OneNote TIME32 epoch time is per 1980, not
- // 1970
- long scalar = getScalar(property);
- long creationTs = scalar + TIME32_EPOCH_DIFF_1980;
- if (creationTs < creationTimestamp) {
- creationTimestamp = creationTs;
- }
- metadata.set(ONE_NOTE_PREFIX + "creationTimestamp", String.valueOf(creationTimestamp));
- } else if (oneNotePropertyEnum == OneNotePropertyEnum.LastModifiedTime) {
- // add the TIME32_EPOCH_DIFF_1980 because OneNote TIME32 epoch time is per 1980, not
- // 1970
- long scalar = getScalar(property);
- long lastMod = scalar + TIME32_EPOCH_DIFF_1980;
- if (lastMod > lastModified) {
- lastModified = lastMod;
- }
- metadata.set(TikaCoreProperties.MODIFIED, String.valueOf(lastModified));
- } else if (oneNotePropertyEnum == OneNotePropertyEnum.Author) {
- String author =
- new String(((PrtFourBytesOfLengthFollowedByData) property).data,
- StandardCharsets.UTF_8);
- if (mostRecentAuthorProp) {
- mostRecentAuthors.add(author);
- } else if (originalAuthorProp) {
- originalAuthors.add(author);
- } else {
- authors.add(author);
- }
- } else if (oneNotePropertyEnum == OneNotePropertyEnum.AuthorMostRecent) {
- mostRecentAuthorProp = true;
- } else if (oneNotePropertyEnum == OneNotePropertyEnum.AuthorOriginal) {
- originalAuthorProp = true;
- } else if (propertyType == PropertyType.FourBytesOfLengthFollowedByData) {
- boolean isBinary = propertyIsBinary(oneNotePropertyEnum);
- PrtFourBytesOfLengthFollowedByData dataProperty =
- (PrtFourBytesOfLengthFollowedByData) property;
- if ((dataProperty.data.length & 1) == 0 &&
- oneNotePropertyEnum != OneNotePropertyEnum.TextExtendedAscii &&
- !isBinary) {
- if (options.getUtf16PropertiesToPrint().contains(oneNotePropertyEnum)) {
- xhtml.startElement(P);
- xhtml.characters(
- new String(dataProperty.data, StandardCharsets.UTF_16LE));
- xhtml.endElement(P);
- }
- } else if (oneNotePropertyEnum == OneNotePropertyEnum.TextExtendedAscii) {
- xhtml.startElement(P);
- xhtml.characters(
- new String(dataProperty.data, StandardCharsets.US_ASCII));
- xhtml.endElement(P);
- } else if (!isBinary) {
- if (options.getUtf16PropertiesToPrint().contains(oneNotePropertyEnum)) {
- xhtml.startElement(P);
- xhtml.characters(
- new String(dataProperty.data, StandardCharsets.UTF_16LE));
- xhtml.endElement(P);
- }
- } else {
- if (oneNotePropertyEnum == OneNotePropertyEnum.RichEditTextUnicode) {
- handleRichEditTextUnicode(dataProperty.data, xhtml);
- } else {
- //TODO -- these seem to be somewhat broken font files and other
- //odds and ends...what are they and how should we process them?
- //handleEmbedded(content.size());
- }
- }
- }
+ this.parseContext = parseContext;
+ this.parentMetadata = metadata;
+ this.embeddedDocumentExtractor =
+ EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(parseContext);
+ for (String warning : parseWarnings) {
+ metadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING, warning);
+ }
+ parseWarnings.clear();
+ if (!cells.isEmpty()) {
+ // Walk each page cell (object space) as a tree, starting from the root objects of
+ // its current revision and following the object references in property order. This
+ // emits the text in document order. The pages are walked in the order in which the
+ // section object space references them; cells that hold older versions of a page
+ // (the same object space in a different revision context) are skipped.
+ List<RevisionStoreCell> pageCells = new ArrayList<>();
+ List<RevisionStoreCell> otherCells = new ArrayList<>();
+ splitCells(pageCells, otherCells);
+ for (RevisionStoreCell cell : pageCells) {
+ xhtml.startElement("div", "class", "page");
+ try {
+ walkCell(cell, options, metadata, xhtml);
+ } finally {
+ xhtml.endElement("div");
+ }
+ }
+ for (RevisionStoreCell cell : otherCells) {
+ walkCell(cell, options, metadata, xhtml);
+ }
+ } else {
+ // no cell information available - walk the object groups in revision order
+ Map<ExGuid, RevisionStoreObject> objectsById = indexObjectsById(OtherFileNodeList);
+ Set<ExGuid> visited = new HashSet<>();
+ for (RevisionStoreObjectGroup objectGroup : OtherFileNodeList) {
+ for (RevisionStoreObject object : objectGroup.objects) {
+ walkObject(object, objectsById, visited, AuthorRole.NONE, options, metadata,
+ xhtml, 0);
}
}
}
if (!authors.isEmpty()) {
- metadata.set(TikaCoreProperties.CREATOR, authors.toArray(new String[]{}));
+ metadata.set(TikaCoreProperties.CREATOR, sortedValues(authors));
}
if (!mostRecentAuthors.isEmpty()) {
metadata.set(Property.externalTextBag(ONE_NOTE_PREFIX + "mostRecentAuthors"),
- mostRecentAuthors.toArray(new String[]{}));
+ sortedValues(mostRecentAuthors));
}
if (!originalAuthors.isEmpty()) {
metadata.set(Property.externalTextBag(ONE_NOTE_PREFIX + "originalAuthors"),
- originalAuthors.toArray(new String[]{}));
+ sortedValues(originalAuthors));
+ }
+ }
+
+ /**
+ * Splits the cells into page cells, ordered as the section object space references them,
+ * and the remaining cells. A cell that holds an older version of a page - the same object
+ * space referenced by a page cell, but in a different revision context - is dropped, so
+ * content is not emitted once per version snapshot.
+ */
+ private void splitCells(List<RevisionStoreCell> pageCells,
+ List<RevisionStoreCell> otherCells) {
+ if (dataRootCell == null) {
+ // Without a data root there is no reliable page ordering information.
+ otherCells.addAll(cells);
+ return;
+ }
+ List<CellID> orderedCellIds = collectSectionReferencedCells();
+ if (orderedCellIds.isEmpty()) {
+ // no page ordering information available - process the cells in storage order
+ otherCells.addAll(cells);
+ return;
+ }
+ Map<CellID, RevisionStoreCell> remainingCells = new LinkedHashMap<>();
+ for (RevisionStoreCell cell : cells) {
+ remainingCells.put(cell.cellID, cell);
+ }
+ Set<ExGuid> coveredObjectSpaces = new HashSet<>();
+ for (CellID cellId : orderedCellIds) {
+ RevisionStoreCell cell = remainingCells.remove(cellId);
+ if (cell != null) {
+ pageCells.add(cell);
+ if (cellId.extendGUID2 != null) {
+ coveredObjectSpaces.add(cellId.extendGUID2);
+ }
+ }
+ }
+ for (RevisionStoreCell cell : remainingCells.values()) {
+ if (!coveredObjectSpaces.contains(cell.cellID.extendGUID2)) {
+ // not an older version of one of the pages - keep it so no content is lost
+ otherCells.add(cell);
+ }
+ }
+ }
+
+ /**
+ * Walks the section object space (the data root cell) and collects the object space (cell)
+ * references in document order - this is the order of the pages in the section.
+ */
+ private List<CellID> collectSectionReferencedCells() {
+ List<CellID> orderedCellIds = new ArrayList<>();
+ if (dataRootCell == null) {
+ return orderedCellIds;
+ }
+ Map<ExGuid, RevisionStoreObject> objectsById = indexObjectsById(dataRootCell.objectGroups);
+ Set<ExGuid> visited = new HashSet<>();
+ for (RevisionManifestRootDeclare rootDeclare : dataRootCell.rootDeclares) {
+ RevisionStoreObject rootObject = objectsById.get(rootDeclare.objectExGuid);
+ if (rootObject == null) {
+ recordParseWarning("OneNote section root object " + rootDeclare.objectExGuid +
+ " could not be resolved");
+ }
+ collectReferencedCells(rootObject, objectsById, visited, orderedCellIds, 0);
+ }
+ return orderedCellIds;
+ }
+
+ private void collectReferencedCells(RevisionStoreObject object,
+ Map<ExGuid, RevisionStoreObject> objectsById,
+ Set<ExGuid> visited, List<CellID> out, int depth) {
+ if (object == null || object.propertySet == null ||
+ object.propertySet.objectSpaceObjectPropSet == null) {
+ return;
+ }
+ if (depth >= MAX_OBJECT_WALK_DEPTH) {
+ recordParseWarning("OneNote section reference traversal exceeded depth limit " +
+ MAX_OBJECT_WALK_DEPTH);
+ return;
+ }
+ if (object.objectID != null && !visited.add(object.objectID)) {
+ return;
+ }
+ List<PropertyAction> actions = collectObjectActions(object);
+ for (PropertyAction action : actions) {
+ if (action.spaceReference != null) {
+ out.add(action.spaceReference);
+ } else if (action.isChildReference && action.childReference != null) {
+ RevisionStoreObject child = objectsById.get(action.childReference);
+ if (child == null && !warningsSaturated()) {
+ recordParseWarning("OneNote section object " + action.childReference +
+ " could not be resolved");
+ }
+ collectReferencedCells(child, objectsById, visited, out, depth + 1);
+ }
+ }
+ }
+
+ private void walkCell(RevisionStoreCell cell, OneNoteTreeWalkerOptions options,
+ Metadata metadata, XHTMLContentHandler xhtml)
+ throws SAXException, TikaException, IOException {
+ Map<ExGuid, RevisionStoreObject> objectsById = indexObjectsById(cell.objectGroups);
+ Set<ExGuid> visited = new HashSet<>();
+ // Only objects reachable from the root objects of the current revision are part of
+ // the current content. The object groups may also contain older, superseded versions
+ // of objects (under a different object ID); those are intentionally not walked.
+ // A blob-only root (no property set) cannot reach the page body, so it does not
+ // count as a resolved content root: if the content root declare dangles next to it,
+ // the walk-everything fallback must still fire or the page body is silently lost.
+ boolean resolvedContentRoot = false;
+ boolean unresolvedRoot = false;
+ for (RevisionManifestRootDeclare rootDeclare : cell.rootDeclares) {
+ RevisionStoreObject rootObject = objectsById.get(rootDeclare.objectExGuid);
+ if (rootObject == null) {
+ recordParseWarning("OneNote cell root object " + rootDeclare.objectExGuid +
+ " could not be resolved");
+ unresolvedRoot = true;
+ } else {
+ if (rootObject.propertySet != null) {
+ resolvedContentRoot = true;
+ }
+ walkObject(rootObject, objectsById, visited, AuthorRole.NONE, options,
+ metadata, xhtml, 0);
+ }
+ }
+ if (cell.rootDeclares.isEmpty() || (unresolvedRoot && !resolvedContentRoot)) {
+ if (cell.rootDeclares.isEmpty()) {
+ recordParseWarning("OneNote cell has no declared root objects; walking all objects");
+ } else {
+ recordParseWarning("OneNote cell root objects could not be resolved; walking all objects");
+ }
+ for (RevisionStoreObjectGroup objectGroup : cell.objectGroups) {
+ for (RevisionStoreObject object : objectGroup.objects) {
+ walkObject(object, objectsById, visited, AuthorRole.NONE, options, metadata,
+ xhtml, 0);
+ }
+ }
+ }
+ }
+
+ private String[] sortedValues(Set<String> values) {
+ String[] sorted = values.toArray(new String[0]);
+ Arrays.sort(sorted);
+ return sorted;
+ }
+
+ private boolean warningsSaturated() {
+ return parseWarningsSuppressed;
+ }
+
+ void recordParseWarning(String warning) {
+ recordParseWarning(ParseWarningKind.DEFAULT, warning);
+ }
+
+ private void recordParseWarning(ParseWarningKind kind, String warning) {
+ String warningKey = kind == ParseWarningKind.DEFAULT ? warning : kind.name();
+ if (recordedParseWarningKeys.contains(warningKey)) {
+ return;
+ }
+ if (recordedParseWarningKeys.size() >= MAX_PARSE_WARNINGS) {
+ if (!parseWarningsSuppressed) {
+ parseWarningsSuppressed = true;
+ emitParseWarning("Additional OneNote parse warnings were suppressed after " +
+ MAX_PARSE_WARNINGS + " distinct warnings");
+ }
+ return;
+ }
+ recordedParseWarningKeys.add(warningKey);
+ emitParseWarning(warning);
+ }
+
+ private void emitParseWarning(String warning) {
+ LOG.warn(warning);
+ if (parentMetadata == null) {
+ parseWarnings.add(warning);
+ } else {
+ parentMetadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING, warning);
+ }
+ }
+
+ /**
+ * Builds a map of object ID to object. The object groups are ordered from the oldest
+ * revision to the newest, so a newer version of an object wins over an older one.
+ */
+ private Map<ExGuid, RevisionStoreObject> indexObjectsById(
+ List<RevisionStoreObjectGroup> objectGroups) {
+ Map<ExGuid, RevisionStoreObject> objectsById = new HashMap<>();
+ for (RevisionStoreObjectGroup objectGroup : objectGroups) {
+ for (RevisionStoreObject object : objectGroup.objects) {
+ if (object.objectID != null) {
+ objectsById.put(object.objectID, object);
+ }
+ }
+ }
+ return objectsById;
+ }
+
+ private void walkObject(RevisionStoreObject object,
+ Map<ExGuid, RevisionStoreObject> objectsById, Set<ExGuid> visited,
+ AuthorRole authorRole, OneNoteTreeWalkerOptions options,
+ Metadata metadata, XHTMLContentHandler xhtml, int depth)
+ throws SAXException, TikaException, IOException {
+ walkObject(object, objectsById, visited, authorRole, options, metadata, xhtml,
+ depth, null);
+ }
+
+ private void walkObject(RevisionStoreObject object,
+ Map<ExGuid, RevisionStoreObject> objectsById, Set<ExGuid> visited,
+ AuthorRole authorRole, OneNoteTreeWalkerOptions options,
+ Metadata metadata, XHTMLContentHandler xhtml, int depth,
+ EmbeddedResourceInfo inheritedResourceInfo)
+ throws SAXException, TikaException, IOException {
+ if (object == null) {
+ return;
+ }
+ if (depth >= MAX_OBJECT_WALK_DEPTH) {
+ recordParseWarning("OneNote object traversal exceeded depth limit " +
+ MAX_OBJECT_WALK_DEPTH);
+ return;
+ }
+ if (object.objectID != null && !visited.add(object.objectID)) {
+ // one object may be referenced under several author roles (e.g. the same author
+ // as both AuthorOriginal and AuthorMostRecent) - record the role even though
+ // the subtree is not walked again
+ recordAuthors(object, authorRole);
+ return;
+ }
+ List<PropertyAction> actions = object.propertySet != null &&
+ object.propertySet.objectSpaceObjectPropSet != null ?
+ collectObjectActions(object) : Collections.emptyList();
+ EmbeddedResourceInfo resourceInfo = embeddedResourceInfo(actions);
+ if (resourceInfo == null) {
+ resourceInfo = inheritedResourceInfo;
+ }
+ if (resourceInfo == null && object.fileDataObject != null && object.objectID != null) {
+ resourceInfo = resourceInfoFromReferencingObject(object, objectsById, depth);
+ }
+ if (object.fileDataObject != null) {
+ // the object carries opaque binary data, e.g. an embedded image or file
+ handleEmbedded(object.fileDataObject.getData(), xhtml, resourceInfo, object.objectID);
+ }
+ if (object.propertySet == null ||
+ object.propertySet.objectSpaceObjectPropSet == null) {
+ return;
+ }
+ // An image node can reference the same picture twice: PictureContainer holds the
+ // canonical image data and WebPictureContainer14 holds a rendition derived from it
+ // (e.g. re-rendered when the picture was resized). Only emit the derived rendition
+ // when the canonical container is missing, so the picture is not extracted twice.
+ boolean hasPrimaryPicture = false;
+ for (PropertyAction action : actions) {
+ if (action.isChildReference && action.childReference != null &&
+ action.oneNotePropertyEnum == OneNotePropertyEnum.PictureContainer &&
+ hasUsablePicture(action.childReference, objectsById, new HashSet<>(), depth)) {
+ hasPrimaryPicture = true;
+ break;
+ }
+ }
+ // The title structure of a page (StructureElementChildNodes) appears above the page
+ // body on screen, but is declared after the body child nodes. Emit it first so the
+ // text comes out in visual order.
+ for (PropertyAction action : actions) {
+ if (action.oneNotePropertyEnum == OneNotePropertyEnum.StructureElementChildNodes) {
+ processAction(action, objectsById, visited, authorRole, options, metadata, xhtml,
+ depth, resourceInfo);
+ }
+ }
+ for (PropertyAction action : actions) {
+ if (hasPrimaryPicture &&
+ action.oneNotePropertyEnum == OneNotePropertyEnum.WebPictureContainer14) {
+ continue;
+ }
+ if (action.oneNotePropertyEnum != OneNotePropertyEnum.StructureElementChildNodes) {
+ processAction(action, objectsById, visited, authorRole, options, metadata, xhtml,
+ depth, resourceInfo);
+ }
+ }
+ }
+
+ private boolean hasUsablePicture(ExGuid objectId,
+ Map<ExGuid, RevisionStoreObject> objectsById,
+ Set<ExGuid> visited, int depth) {
+ if (depth >= MAX_OBJECT_WALK_DEPTH || objectId == null || !visited.add(objectId)) {
+ return false;
+ }
+ RevisionStoreObject object = objectsById.get(objectId);
+ if (object == null) {
+ return false;
+ }
+ byte[] data = object.fileDataObject == null ? null : object.fileDataObject.getData();
+ if (data != null && data.length > 0) {
+ return true;
+ }
+ if (object.propertySet == null || object.propertySet.objectSpaceObjectPropSet == null) {
+ return false;
+ }
+ for (PropertyAction action : collectObjectActions(object)) {
+ if (action.isChildReference && action.childReference != null &&
+ hasUsablePicture(action.childReference, objectsById, visited, depth + 1)) {
+ return true;
+ }
}
+ return false;
}
+ private EmbeddedResourceInfo resourceInfoFromReferencingObject(
+ RevisionStoreObject target, Map<ExGuid, RevisionStoreObject> objectsById, int depth) {
+ if (depth >= MAX_OBJECT_WALK_DEPTH) {
+ return null;
+ }
+ EmbeddedResourceInfo typeOnlyFallback = null;
+ for (RevisionStoreObject candidate : objectsById.values()) {
+ if (candidate.propertySet == null ||
+ candidate.propertySet.objectSpaceObjectPropSet == null) {
+ continue;
+ }
+ List<PropertyAction> candidateActions = collectObjectActions(candidate);
+ for (PropertyAction action : candidateActions) {
+ if (action.isChildReference && target.objectID.equals(action.childReference) &&
+ (action.oneNotePropertyEnum == OneNotePropertyEnum.PictureContainer ||
+ action.oneNotePropertyEnum == OneNotePropertyEnum.EmbeddedFileContainer ||
+ action.oneNotePropertyEnum == OneNotePropertyEnum.WebPictureContainer14)) {
+ EmbeddedResourceInfo info = embeddedResourceInfo(candidateActions);
+ if (info != null) {
+ return info;
+ }
+ if (typeOnlyFallback == null) {
+ typeOnlyFallback = resourceInfoForChild(action, null);
+ }
+ }
+ }
+ }
+ return typeOnlyFallback;
+ }
+
+ private EmbeddedResourceInfo resourceInfoForChild(PropertyAction action,
+ EmbeddedResourceInfo parentInfo) {
+ if (action.oneNotePropertyEnum == OneNotePropertyEnum.PictureContainer ||
+ action.oneNotePropertyEnum == OneNotePropertyEnum.WebPictureContainer14) {
+ return parentInfo == null ? new EmbeddedResourceInfo(null,
+ TikaCoreProperties.EmbeddedResourceType.INLINE.toString()) : parentInfo;
+ }
+ if (action.oneNotePropertyEnum == OneNotePropertyEnum.EmbeddedFileContainer) {
+ return parentInfo == null ? new EmbeddedResourceInfo(null,
+ TikaCoreProperties.EmbeddedResourceType.ATTACHMENT.toString()) : parentInfo;
+ }
+ return parentInfo;
+ }
+
+ /**
+ * Metadata used when emitting an embedded resource.
+ */
+ private static final class EmbeddedResourceInfo {
+ private final String name;
+ private final String type;
+
+ private EmbeddedResourceInfo(String name, String type) {
+ this.name = name;
+ this.type = type;
+ }
+ }
+
+ /**
+ * A property of an object, together with the object reference assigned to it if it is
+ * an object reference property.
+ */
+ private static final class PropertyAction {
+ private final IProperty property;
+ private final PropertyType propertyType;
+ private final OneNotePropertyEnum oneNotePropertyEnum;
+ private final boolean isChildReference;
+ private final ExGuid childReference;
+ private final CellID spaceReference;
+
+ PropertyAction(IProperty property, PropertyType propertyType,
+ OneNotePropertyEnum oneNotePropertyEnum, boolean isChildReference,
+ ExGuid childReference, CellID spaceReference) {
+ this.property = property;
+ this.propertyType = propertyType;
+ this.oneNotePropertyEnum = oneNotePropertyEnum;
+ this.isChildReference = isChildReference;
+ this.childReference = childReference;
+ this.spaceReference = spaceReference;
+ }
+ }
+
+ /**
+ * Flattens the properties of an object, in order, into a list of actions. The result is
+ * cached per object; callers must not mutate the returned list.
+ */
+ private List<PropertyAction> collectObjectActions(RevisionStoreObject object) {
+ List<PropertyAction> actions = objectActionsCache.get(object);
+ if (actions != null) {
+ return actions;
+ }
+ List<ExGuid> referencedObjects =
+ object.referencedObjectID == null || object.referencedObjectID.content == null ?
+ Collections.emptyList() : object.referencedObjectID.content;
+ List<CellID> referencedSpaces = object.referencedObjectSpacesID == null ||
+ object.referencedObjectSpacesID.content == null ? Collections.emptyList() :
+ object.referencedObjectSpacesID.content;
+ actions = new ArrayList<>();
+ collectActions(object.propertySet.objectSpaceObjectPropSet.body, referencedObjects,
+ new int[]{0}, referencedSpaces, new int[]{0}, actions, 0);
+ objectActionsCache.put(object, actions);
+ return actions;
+ }
+
+ /**
+ * Flattens the properties of a property set, in order, into a list of actions.
+ * Properties of type ObjectID or ArrayOfObjectIDs consume, in property order, the object
+ * references of the containing object, and properties of type ObjectSpaceID or
+ * ArrayOfObjectSpaceIDs consume the object space (cell) references (see MS-ONESTORE
+ * section 2.7.8), so the references must be assigned here, in property order, no matter
+ * in which order the actions are processed later.
+ */
+ private void collectActions(PropertySet propertySet, List<ExGuid> referencedObjects,
+ int[] referenceCursor, List<CellID> referencedSpaces,
+ int[] spaceCursor, List<PropertyAction> actions, int depth) {
+ if (propertySet == null || propertySet.rgPrids == null || propertySet.rgData == null) {
+ return;
+ }
+ if (depth >= MAX_OBJECT_WALK_DEPTH) {
+ recordParseWarning("OneNote property traversal exceeded depth limit " +
+ MAX_OBJECT_WALK_DEPTH);
+ return;
+ }
+ for (int i = 0; i < propertySet.rgPrids.length && i < propertySet.rgData.size(); ++i) {
+ IProperty property = propertySet.rgData.get(i);
+ PropertyID propertyID = propertySet.rgPrids[i];
+ PropertyType propertyType = PropertyType.fromIntVal(propertyID.type);
+ OneNotePropertyEnum oneNotePropertyEnum =
+ OneNotePropertyEnum.of(Unsigned.uint(propertyID.value).longValue());
+ if (propertyType == PropertyType.ObjectID) {
+ ExGuid childReference = nextReference(referencedObjects, referenceCursor);
+ if (childReference == null) {
+ recordParseWarning("OneNote object reference slot was exhausted");
+ }
+ actions.add(new PropertyAction(property, propertyType, oneNotePropertyEnum, true,
+ childReference, null));
+ } else if (propertyType == PropertyType.ArrayOfObjectIDs) {
+ int available = referencedObjects.size() - referenceCursor[0];
+ int requestedCount = property instanceof ArrayNumber ?
+ Math.max(0, ((ArrayNumber) property).number) : 0;
+ int declaredCount = boundedAvailableReferenceCount(requestedCount, available);
+ int count = Math.min(declaredCount, MAX_REFERENCE_COUNT);
+ if (requestedCount > available && !warningsSaturated()) {
+ recordParseWarning(ParseWarningKind.OBJECT_REFERENCE_ARRAY_UNAVAILABLE,
+ "OneNote object reference array had unavailable entries " +
+ "(first occurrence: declared " + requestedCount +
+ " entries but only " + Math.max(available, 0) +
+ " were available)");
+ }
+ if (declaredCount > count && !warningsSaturated()) {
+ recordParseWarning(ParseWarningKind.OBJECT_REFERENCE_ARRAY_CAPPED,
+ "Capping OneNote object reference array at " +
+ MAX_REFERENCE_COUNT + " entries");
+ }
+ for (int j = 0; j < count; ++j) {
+ actions.add(new PropertyAction(property, propertyType, oneNotePropertyEnum,
+ true, nextReference(referencedObjects, referenceCursor), null));
+ }
+ referenceCursor[0] += declaredCount - count;
+ } else if (propertyType == PropertyType.ObjectSpaceID) {
+ CellID spaceReference = nextSpaceReference(referencedSpaces, spaceCursor);
+ if (spaceReference == null) {
+ recordParseWarning("OneNote object-space reference slot was exhausted");
+ }
+ actions.add(new PropertyAction(property, propertyType, oneNotePropertyEnum, false,
+ null, spaceReference));
+ } else if (propertyType == PropertyType.ArrayOfObjectSpaceIDs) {
+ int available = referencedSpaces.size() - spaceCursor[0];
+ int requestedCount = property instanceof ArrayNumber ?
+ Math.max(0, ((ArrayNumber) property).number) : 0;
+ int declaredCount = boundedAvailableReferenceCount(requestedCount, available);
+ int count = Math.min(declaredCount, MAX_REFERENCE_COUNT);
+ if (requestedCount > available && !warningsSaturated()) {
+ recordParseWarning(ParseWarningKind.OBJECT_SPACE_REFERENCE_ARRAY_UNAVAILABLE,
+ "OneNote object-space reference array had unavailable entries " +
+ "(first occurrence: declared " + requestedCount +
+ " entries but only " + Math.max(available, 0) +
+ " were available)");
+ }
+ if (declaredCount > count && !warningsSaturated()) {
+ recordParseWarning(ParseWarningKind.OBJECT_SPACE_REFERENCE_ARRAY_CAPPED,
+ "Capping OneNote object-space reference array at " +
+ MAX_REFERENCE_COUNT + " entries");
+ }
+ for (int j = 0; j < count; ++j) {
+ actions.add(new PropertyAction(property, propertyType, oneNotePropertyEnum,
+ false, null, nextSpaceReference(referencedSpaces, spaceCursor)));
+ }
+ spaceCursor[0] += declaredCount - count;
+ } else if (propertyType == PropertyType.PropertySet) {
+ if (property instanceof PropertySet) {
+ collectActions((PropertySet) property, referencedObjects, referenceCursor,
+ referencedSpaces, spaceCursor, actions, depth + 1);
+ }
+ } else if (propertyType == PropertyType.ArrayOfPropertyValues) {
+ if (property instanceof PrtArrayOfPropertyValues &&
+ ((PrtArrayOfPropertyValues) property).data != null) {
+ for (PropertySet nested : ((PrtArrayOfPropertyValues) property).data) {
+ collectActions(nested, referencedObjects, referenceCursor,
+ referencedSpaces, spaceCursor, actions, depth + 1);
+ }
+ }
+ } else {
+ actions.add(new PropertyAction(property, propertyType, oneNotePropertyEnum, false,
+ null, null));
+ }
+ }
+ }
+
+ private void processAction(PropertyAction action,
+ Map<ExGuid, RevisionStoreObject> objectsById, Set<ExGuid> visited,
+ AuthorRole authorRole, OneNoteTreeWalkerOptions options,
+ Metadata metadata, XHTMLContentHandler xhtml, int depth,
+ EmbeddedResourceInfo parentResourceInfo)
+ throws SAXException, TikaException, IOException {
+ if (action.spaceReference != null) {
+ // a reference to another object space (cell) - cells are walked separately
+ return;
+ }
+ if (action.isChildReference) {
+ AuthorRole childRole = AuthorRole.NONE;
+ if (action.oneNotePropertyEnum == OneNotePropertyEnum.AuthorMostRecent) {
+ childRole = AuthorRole.MOST_RECENT;
+ } else if (action.oneNotePropertyEnum == OneNotePropertyEnum.AuthorOriginal) {
+ childRole = AuthorRole.ORIGINAL;
+ }
+ EmbeddedResourceInfo childResourceInfo = resourceInfoForChild(action,
+ parentResourceInfo);
+ RevisionStoreObject child = action.childReference == null ? null :
+ objectsById.get(action.childReference);
+ if (child == null && action.childReference != null && !warningsSaturated()) {
+ recordParseWarning("OneNote object " + action.childReference +
+ " could not be resolved");
+ }
+ walkObject(child, objectsById, visited, childRole, options, metadata, xhtml,
+ depth + 1, childResourceInfo);
+ } else {
+ processPrimitiveProperty(action.property, action.propertyType,
+ action.oneNotePropertyEnum, authorRole, options, metadata, xhtml);
+ }
+ }
+
+ private int boundedAvailableReferenceCount(int declaredCount, int remainingReferences) {
+ if (declaredCount <= 0 || remainingReferences <= 0) {
+ return 0;
+ }
+ return Math.min(declaredCount, remainingReferences);
+ }
+
+ private ExGuid nextReference(List<ExGuid> referencedObjects, int[] referenceCursor) {
+ if (referenceCursor[0] < referencedObjects.size()) {
+ return referencedObjects.get(referenceCursor[0]++);
+ }
+ return null;
+ }
+
+ private CellID nextSpaceReference(List<CellID> referencedSpaces, int[] spaceCursor) {
+ if (spaceCursor[0] < referencedSpaces.size()) {
+ return referencedSpaces.get(spaceCursor[0]++);
+ }
+ return null;
+ }
+
+ private void processPrimitiveProperty(IProperty property, PropertyType propertyType,
+ OneNotePropertyEnum oneNotePropertyEnum,
+ AuthorRole authorRole,
+ OneNoteTreeWalkerOptions options, Metadata metadata,
+ XHTMLContentHandler xhtml)
+ throws SAXException, TikaException, IOException {
+ if (oneNotePropertyEnum == OneNotePropertyEnum.LastModifiedTimeStamp) {
+ long fullval = getScalar(property);
+ Instant instant = Instant.ofEpochSecond(
+ fullval / 10000000 + DATETIME_EPOCH_DIFF_1601);
+ if (instant.isAfter(lastModifiedTimestamp)) {
+ lastModifiedTimestamp = instant;
+ }
+ metadata.set(ONE_NOTE_PREFIX + "lastModifiedTimestamp",
+ String.valueOf(lastModifiedTimestamp.toEpochMilli()));
+ } else if (oneNotePropertyEnum == OneNotePropertyEnum.CreationTimeStamp) {
+ // add the TIME32_EPOCH_DIFF_1980 because OneNote TIME32 epoch time is per 1980, not
+ // 1970
+ long scalar = getScalar(property);
+ long creationTs = scalar + TIME32_EPOCH_DIFF_1980;
+ if (creationTs < creationTimestamp) {
+ creationTimestamp = creationTs;
+ }
+ metadata.set(ONE_NOTE_PREFIX + "creationTimestamp", String.valueOf(creationTimestamp));
+ } else if (oneNotePropertyEnum == OneNotePropertyEnum.LastModifiedTime) {
+ // add the TIME32_EPOCH_DIFF_1980 because OneNote TIME32 epoch time is per 1980, not
+ // 1970
+ long scalar = getScalar(property);
+ long lastMod = scalar + TIME32_EPOCH_DIFF_1980;
+ if (lastMod > lastModified) {
+ lastModified = lastMod;
+ }
+ metadata.set(TikaCoreProperties.MODIFIED, String.valueOf(lastModified));
+ } else if (oneNotePropertyEnum == OneNotePropertyEnum.Author) {
+ recordAuthor(decodeOneNoteText(
+ ((PrtFourBytesOfLengthFollowedByData) property).data), authorRole);
+ } else if (propertyType == PropertyType.FourBytesOfLengthFollowedByData) {
+ boolean isBinary = propertyIsBinary(oneNotePropertyEnum);
+ PrtFourBytesOfLengthFollowedByData dataProperty =
+ (PrtFourBytesOfLengthFollowedByData) property;
+ if ((dataProperty.data.length & 1) == 0 &&
+ oneNotePropertyEnum != OneNotePropertyEnum.TextExtendedAscii && !isBinary) {
+ if (options.getUtf16PropertiesToPrint().contains(oneNotePropertyEnum)) {
+ emitParagraph(xhtml, new String(dataProperty.data,
+ StandardCharsets.UTF_16LE));
+ }
+ } else if (oneNotePropertyEnum == OneNotePropertyEnum.TextExtendedAscii) {
+ emitParagraph(xhtml, new String(dataProperty.data, StandardCharsets.US_ASCII));
+ } else if (!isBinary) {
+ if (options.getUtf16PropertiesToPrint().contains(oneNotePropertyEnum)) {
+ emitParagraph(xhtml, new String(dataProperty.data,
+ StandardCharsets.UTF_16LE));
+ }
+ } else {
+ if (oneNotePropertyEnum == OneNotePropertyEnum.RichEditTextUnicode) {
+ handleRichEditTextUnicode(dataProperty.data, xhtml);
+ } else {
+ //TODO -- these seem to be somewhat broken font files and other
+ //odds and ends...what are they and how should we process them?
+ //handleEmbedded(content.size());
+ }
+ }
+ }
+ }
+
+
+ private String decodeOneNoteText(byte[] bytes) {
+ return new String(bytes, StandardCharsets.UTF_16LE).replace("\u0000", "");
+ }
+
+ private void recordAuthor(String author, AuthorRole role) {
+ if (role == AuthorRole.MOST_RECENT) {
+ mostRecentAuthors.add(author);
+ } else if (role == AuthorRole.ORIGINAL) {
+ originalAuthors.add(author);
+ // the original authors are the creators of the content
+ authors.add(author);
+ } else {
+ authors.add(author);
+ }
+ }
+
+ /**
+ * Records the Author properties of an already-visited object under the given role.
+ */
+ private void recordAuthors(RevisionStoreObject object, AuthorRole role) {
+ // NONE also records (into authors) so the result is visit-order independent
+ if (object.propertySet == null ||
+ object.propertySet.objectSpaceObjectPropSet == null) {
+ return;
+ }
+ for (PropertyAction action : collectObjectActions(object)) {
+ if (action.oneNotePropertyEnum == OneNotePropertyEnum.Author &&
+ action.property instanceof PrtFourBytesOfLengthFollowedByData) {
+ recordAuthor(decodeOneNoteText(
+ ((PrtFourBytesOfLengthFollowedByData) action.property).data), role);
+ }
+ }
+ }
+
+ private EmbeddedResourceInfo embeddedResourceInfo(List<PropertyAction> actions) {
+ for (PropertyAction action : actions) {
+ if (action.property instanceof PrtFourBytesOfLengthFollowedByData &&
+ (action.oneNotePropertyEnum == OneNotePropertyEnum.ImageFilename ||
+ action.oneNotePropertyEnum == OneNotePropertyEnum.EmbeddedFileName)) {
+ byte[] bytes = ((PrtFourBytesOfLengthFollowedByData) action.property).data;
+ String name = sanitizeResourceName(new String(bytes, StandardCharsets.UTF_16LE)
+ .replace("\u0000", ""));
+ if (!name.isEmpty()) {
+ String type = action.oneNotePropertyEnum == OneNotePropertyEnum.ImageFilename ?
+ TikaCoreProperties.EmbeddedResourceType.INLINE.toString() :
+ TikaCoreProperties.EmbeddedResourceType.ATTACHMENT.toString();
+ return new EmbeddedResourceInfo(name, type);
+ }
+ }
+ }
+ return null;
+ }
+
+ static String sanitizeResourceName(String name) {
+ name = name.replace('\\', '/');
+ // a drive-relative name like C:pic.png has no slash, so strip the prefix separately
+ if (name.length() > 1 && name.charAt(1) == ':') {
+ name = name.substring(2);
+ }
+ int slash = name.lastIndexOf('/');
+ name = slash >= 0 ? name.substring(slash + 1) : name;
+ return ".".equals(name) || "..".equals(name) ? "" : name;
+ }
+
+ /**
+ * Hands the binary data of an embedded object (e.g. an image or an attached file) to the
+ * embedded document extractor.
+ */
+ private void handleEmbedded(byte[] data, XHTMLContentHandler xhtml,
+ EmbeddedResourceInfo resourceInfo, ExGuid objectID)
+ throws SAXException, IOException {
+ if (data == null || data.length == 0 || embeddedDocumentExtractor == null) {
+ return;
+ }
+ Metadata embeddedMetadata = new Metadata();
+ embeddedMetadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE,
+ resourceInfo == null ? TikaCoreProperties.EmbeddedResourceType.ATTACHMENT.toString() :
+ resourceInfo.type);
+ if (resourceInfo != null && resourceInfo.name != null) {
+ embeddedMetadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, resourceInfo.name);
+ }
+ String relationshipID = objectID == null ? null : "_" + objectID.value + "_" +
+ objectID.guid;
+ if (relationshipID != null) {
+ embeddedMetadata.set(TikaCoreProperties.EMBEDDED_RELATIONSHIP_ID, relationshipID);
+ }
+ AttributesImpl attributes = new AttributesImpl();
+ attributes.addAttribute("", "class", "class", "CDATA", "embedded");
+ if (relationshipID != null) {
+ attributes.addAttribute("", "id", "id", "CDATA", relationshipID);
+ }
+ xhtml.startElement("div", attributes);
+ xhtml.endElement("div");
+ try (TikaInputStream tis = TikaInputStream.get(data)) {
+ if (embeddedDocumentExtractor.shouldParseEmbedded(embeddedMetadata)) {
+ // only counts as content when the extractor accepts it, so a declined
+ // embedded object in an otherwise-empty file still triggers the fallback
+ contentEmitted = true;
+ embeddedDocumentExtractor.parseEmbedded(tis, new EmbeddedContentHandler(xhtml),
+ embeddedMetadata, false);
+ }
+ } catch (IOException e) {
+ EmbeddedDocumentUtil.recordEmbeddedStreamException(e, parentMetadata);
+ }
+ }
private void handleRichEditTextUnicode(byte[] arr, XHTMLContentHandler xhtml)
throws SAXException, IOException, TikaException {
@@ -285,13 +1032,37 @@ public class MSOneStorePackage {
Matcher m = HYPERLINK_PATTERN.matcher(txt);
if (m.find()) {
xhtml.startElement("a", "href", m.group(1));
- xhtml.characters(m.group(2));
- xhtml.endElement("a");
+ try {
+ xhtml.characters(m.group(2));
+ } finally {
+ xhtml.endElement("a");
+ }
+ contentEmitted = true;
} else {
- xhtml.startElement(P);
- xhtml.characters(txt);
+ emitParagraph(xhtml, txt);
+ }
+ }
+
+ private void emitParagraph(XHTMLContentHandler xhtml, String text) throws SAXException {
+ xhtml.startElement(P);
+ try {
+ xhtml.characters(text);
+ } finally {
xhtml.endElement(P);
}
+ // blank text does not count as content, so it cannot suppress the legacy fallback
+ if (!text.isBlank()) {
+ contentEmitted = true;
+ }
+ }
+
+ /**
+ * Whether the tree walk emitted any content - text or an embedded object. When it did
+ * not, the caller can fall back to the legacy string dump so a degraded file still
+ * yields its text.
+ */
+ public boolean hasEmittedContent() {
+ return contentEmitted;
}
private long getScalar(IProperty property) throws TikaException, IOException {
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/MSOneStoreParser.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/MSOneStoreParser.java
index 25790ead8b..d4a41b43d0 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/MSOneStoreParser.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/MSOneStoreParser.java
@@ -17,9 +17,15 @@
package org.apache.tika.parser.microsoft.onenote.fsshttpb;
import java.io.IOException;
+import java.util.ArrayDeque;
import java.util.ArrayList;
+import java.util.Deque;
+import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@@ -31,6 +37,8 @@ import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.ObjectGroupDa
import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.RevisionManifestDataElementData;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.RevisionManifestObjectGroupReferences;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.RevisionManifestRootDeclare;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.RevisionStoreCell;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.RevisionStoreObject;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.RevisionStoreObjectGroup;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.StorageIndexCellMapping;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.StorageIndexDataElementData;
@@ -40,8 +48,14 @@ import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.CellID;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.DataElementType;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.ExGuid;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.HeaderCell;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.util.GuidUtil;
public class MSOneStoreParser {
+ /**
+ * The root role declaration used for the encryption key of encrypted sections.
+ */
+ private static final ExGuid ENCRYPTION_KEY_ROOT_EXGUID =
+ new ExGuid(3, UUID.fromString("4A3717F8-1C14-49E7-9526-81D942DE1741"));
private final Set<CellID> storageIndexHashTab = new HashSet<>();
// The DataElements of Storage Index
private List<DataElement> storageIndexDataElements;
@@ -49,15 +63,47 @@ public class MSOneStoreParser {
private List<DataElement> storageManifestDataElements;
// The DataElements of Cell Manifest
private List<DataElement> cellManifestDataElements;
+ private Map<ExGuid, DataElement> cellManifestDataElementsById = new HashMap<>();
// The DataElements of Revision Manifest
private List<DataElement> revisionManifestDataElements;
+ private Map<ExGuid, DataElement> revisionManifestDataElementsById = new HashMap<>();
// The DataElements of Object Group Data
private List<DataElement> objectGroupDataElements;
- // The DataElements of Object BLOB
- private List<DataElement> objectBlOBElements;
+ // The DataElements of Object Group Data, keyed by their data element extended GUID
+ private Map<ExGuid, DataElement> objectGroupDataElementsById;
+ private final Map<ObjectGroupCacheKey, RevisionStoreObjectGroup> objectGroupsByDataElementId =
+ new HashMap<>();
+ // The DataElements of Object BLOB, keyed by their data element extended GUID
+ private Map<ExGuid, DataElement> objectBlobElementsById;
+
+ private static final class ObjectGroupCacheKey {
+ private final ExGuid dataElementId;
+ private final boolean encryption;
+
+ private ObjectGroupCacheKey(ExGuid dataElementId, boolean encryption) {
+ this.dataElementId = dataElementId;
+ this.encryption = encryption;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof ObjectGroupCacheKey)) {
+ return false;
+ }
+ ObjectGroupCacheKey that = (ObjectGroupCacheKey) other;
+ return encryption == that.encryption && Objects.equals(dataElementId, that.dataElementId);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * Objects.hashCode(dataElementId) + Boolean.hashCode(encryption);
+ }
+ }
public MSOneStorePackage parse(DataElementPackage dataElementPackage) throws IOException {
+ storageIndexHashTab.clear();
MSOneStorePackage msOneStorePackage = new MSOneStorePackage();
+ objectGroupsByDataElementId.clear();
storageIndexDataElements = dataElementPackage.dataElements.stream()
.filter(d -> d.dataElementType == DataElementType.StorageIndexDataElementData)
@@ -68,20 +114,48 @@ public class MSOneStoreParser {
cellManifestDataElements = dataElementPackage.dataElements.stream()
.filter(d -> d.dataElementType == DataElementType.CellManifestDataElementData)
.collect(Collectors.toList());
+ cellManifestDataElementsById.clear();
+ for (DataElement element : cellManifestDataElements) {
+ cellManifestDataElementsById.putIfAbsent(element.dataElementExGuid, element);
+ }
revisionManifestDataElements = dataElementPackage.dataElements.stream()
.filter(d -> d.dataElementType == DataElementType.RevisionManifestDataElementData)
.collect(Collectors.toList());
+ revisionManifestDataElementsById.clear();
+ for (DataElement element : revisionManifestDataElements) {
+ revisionManifestDataElementsById.putIfAbsent(element.dataElementExGuid, element);
+ }
objectGroupDataElements = dataElementPackage.dataElements.stream()
.filter(d -> d.dataElementType == DataElementType.ObjectGroupDataElementData)
.collect(Collectors.toList());
- objectBlOBElements = dataElementPackage.dataElements.stream()
+ objectGroupDataElementsById = new HashMap<>();
+ for (DataElement objectGroupElement : objectGroupDataElements) {
+ objectGroupDataElementsById.putIfAbsent(objectGroupElement.dataElementExGuid,
+ objectGroupElement);
+ }
+ List<DataElement> objectBlobElements = dataElementPackage.dataElements.stream()
.filter(d -> d.dataElementType == DataElementType.ObjectDataBLOBDataElementData)
.collect(Collectors.toList());
+ objectBlobElementsById = new HashMap<>();
+ for (DataElement blobElement : objectBlobElements) {
+ objectBlobElementsById.putIfAbsent(blobElement.dataElementExGuid, blobElement);
+ }
+ if (storageIndexDataElements.isEmpty() || storageManifestDataElements.isEmpty()) {
+ throw new IOException("Missing OneNote storage index or storage manifest");
+ }
msOneStorePackage.storageIndex =
(StorageIndexDataElementData) storageIndexDataElements.get(0).data;
msOneStorePackage.storageManifest =
(StorageManifestDataElementData) storageManifestDataElements.get(0).data;
+ if (msOneStorePackage.storageIndex == null ||
+ msOneStorePackage.storageManifest == null) {
+ throw new IOException("Invalid OneNote storage index or storage manifest");
+ }
+ if (msOneStorePackage.storageManifest.storageManifestRootDeclareList == null ||
+ msOneStorePackage.storageManifest.storageManifestRootDeclareList.size() < 2) {
+ throw new IOException("Missing OneNote header or data-root declaration");
+ }
// Parse Header Cell
CellID headerCellID =
@@ -93,31 +167,50 @@ public class MSOneStoreParser {
if (headerCellStorageIndexCellMapping != null) {
msOneStorePackage.headerCellCellManifest =
this.findCellManifest(headerCellStorageIndexCellMapping.cellMappingExGuid);
- StorageIndexRevisionMapping headerCellRevisionManifestMapping =
- msOneStorePackage.findStorageIndexRevisionMapping(
- msOneStorePackage.headerCellCellManifest.cellManifestCurrentRevision
- .cellManifestCurrentRevisionExGuid);
- msOneStorePackage.headerCellRevisionManifest = this.findRevisionManifestDataElement(
- headerCellRevisionManifestMapping.revisionMappingExGuid);
- msOneStorePackage.headerCell =
- this.parseHeaderCell(msOneStorePackage.headerCellRevisionManifest);
-
- // Parse Data root
- CellID dataRootCellID =
- msOneStorePackage.storageManifest.storageManifestRootDeclareList.get(1).cellID;
- storageIndexHashTab.add(dataRootCellID);
- msOneStorePackage.dataRoot = this.parseObjectGroup(dataRootCellID, msOneStorePackage);
- // Parse other data
- for (StorageIndexCellMapping storageIndexCellMapping : msOneStorePackage.storageIndex
- .storageIndexCellMappingList) {
- if (!storageIndexHashTab.contains(storageIndexCellMapping.cellID)) {
- msOneStorePackage.OtherFileNodeList.addAll(
- this.parseObjectGroup(storageIndexCellMapping.cellID,
- msOneStorePackage));
- storageIndexHashTab.add(storageIndexCellMapping.cellID);
+ if (msOneStorePackage.headerCellCellManifest != null &&
+ msOneStorePackage.headerCellCellManifest.cellManifestCurrentRevision != null) {
+ StorageIndexRevisionMapping headerCellRevisionManifestMapping =
+ msOneStorePackage.findStorageIndexRevisionMapping(
+ msOneStorePackage.headerCellCellManifest.cellManifestCurrentRevision
+ .cellManifestCurrentRevisionExGuid);
+ if (headerCellRevisionManifestMapping != null) {
+ msOneStorePackage.headerCellRevisionManifest =
+ this.findRevisionManifestDataElement(
+ headerCellRevisionManifestMapping.revisionMappingExGuid);
+ if (msOneStorePackage.headerCellRevisionManifest != null) {
+ msOneStorePackage.headerCell =
+ this.parseHeaderCell(msOneStorePackage.headerCellRevisionManifest);
+ }
}
}
}
+
+ // Parse Data root independently of the header-cell metadata. A malformed header cell
+ // should not prevent valid section cells from being parsed.
+ CellID dataRootCellID =
+ msOneStorePackage.storageManifest.storageManifestRootDeclareList.get(1).cellID;
+ storageIndexHashTab.add(dataRootCellID);
+ RevisionStoreCell dataRootCell = this.parseCell(dataRootCellID, msOneStorePackage);
+ if (dataRootCell == null) {
+ throw new IOException("Could not parse the OneNote data root cell");
+ }
+ msOneStorePackage.dataRootCell = dataRootCell;
+ // Parse other data
+ for (StorageIndexCellMapping storageIndexCellMapping : msOneStorePackage.storageIndex
+ .storageIndexCellMappingList) {
+ if (!storageIndexHashTab.contains(storageIndexCellMapping.cellID)) {
+ RevisionStoreCell cell =
+ this.parseCell(storageIndexCellMapping.cellID, msOneStorePackage);
+ // The storage index can retain a mapping for a deleted version context.
+ // Such an entry has no CellManifestDataElementData (often its mapping GUID
+ // is all zero) and therefore cannot contain current document content.
+ if (cell != null) {
+ msOneStorePackage.OtherFileNodeList.addAll(cell.objectGroups);
+ msOneStorePackage.cells.add(cell);
+ }
+ storageIndexHashTab.add(storageIndexCellMapping.cellID);
+ }
+ }
return msOneStorePackage;
}
@@ -125,74 +218,187 @@ public class MSOneStoreParser {
* Find the CellManifestDataElementData
*
* @param cellMappingExtendedGUID The ExGuid of Cell Mapping Extended GUID.
- * @return The CellManifestDataElementData instance.
+ * @return The CellManifestDataElementData instance, or {@code null} if it is absent.
*/
private CellManifestDataElementData findCellManifest(ExGuid cellMappingExtendedGUID) {
- return (CellManifestDataElementData) this.cellManifestDataElements.stream()
- .filter(d -> d.dataElementExGuid.equals(cellMappingExtendedGUID)).findFirst()
- .orElse(new DataElement()).data;
+ DataElement element = cellManifestDataElementsById.get(cellMappingExtendedGUID);
+ return element == null ? null : (CellManifestDataElementData) element.data;
}
/**
* Find the Revision Manifest from Data Elements.
*
* @param revisionMappingExtendedGUID The Revision Mapping Extended GUID.
- * @return Returns the instance of RevisionManifestDataElementData
+ * @return The RevisionManifestDataElementData instance, or {@code null} if it is absent.
*/
private RevisionManifestDataElementData findRevisionManifestDataElement(
ExGuid revisionMappingExtendedGUID) {
- return (RevisionManifestDataElementData) this.revisionManifestDataElements.stream()
- .filter(d -> d.dataElementExGuid.equals(revisionMappingExtendedGUID)).findFirst()
- .orElse(new DataElement()).data;
+ DataElement element = revisionManifestDataElementsById.get(revisionMappingExtendedGUID);
+ return element == null ? null : (RevisionManifestDataElementData) element.data;
}
private HeaderCell parseHeaderCell(RevisionManifestDataElementData headerCellRevisionManifest)
throws IOException {
+ if (headerCellRevisionManifest == null ||
+ headerCellRevisionManifest.revisionManifestObjectGroupReferences == null ||
+ headerCellRevisionManifest.revisionManifestObjectGroupReferences.isEmpty()) {
+ return null;
+ }
ExGuid rootObjectId =
headerCellRevisionManifest.revisionManifestObjectGroupReferences.get(
0).objectGroupExtendedGUID;
- DataElement element = this.objectGroupDataElements.stream()
- .filter(d -> d.dataElementExGuid.equals(rootObjectId)).findFirst()
- .orElse(new DataElement());
-
+ DataElement element = objectGroupDataElementsById.get(rootObjectId);
+ if (element == null) {
+ return null;
+ }
return HeaderCell.createInstance((ObjectGroupDataElementData) element.data);
}
- private List<RevisionStoreObjectGroup> parseObjectGroup(CellID objectGroupCellID,
- MSOneStorePackage msOneStorePackage)
+ private RevisionStoreCell parseCell(CellID objectGroupCellID,
+ MSOneStorePackage msOneStorePackage)
throws IOException {
StorageIndexCellMapping storageIndexCellMapping =
msOneStorePackage.findStorageIndexCellMapping(objectGroupCellID);
+ if (storageIndexCellMapping == null) {
+ String warning = "Skipping OneNote cell " + objectGroupCellID +
+ ": no storage-index cell mapping";
+ msOneStorePackage.recordParseWarning(warning);
+ return null;
+ }
CellManifestDataElementData cellManifest =
this.findCellManifest(storageIndexCellMapping.cellMappingExGuid);
+ if (cellManifest == null || cellManifest.cellManifestCurrentRevision == null) {
+ String warning = "Skipping OneNote cell " + objectGroupCellID +
+ ": no current cell manifest";
+ msOneStorePackage.recordParseWarning(warning);
+ return null;
+ }
List<RevisionStoreObjectGroup> objectGroups = new ArrayList<>();
msOneStorePackage.cellManifests.add(cellManifest);
StorageIndexRevisionMapping revisionMapping =
msOneStorePackage.findStorageIndexRevisionMapping(
cellManifest.cellManifestCurrentRevision.cellManifestCurrentRevisionExGuid);
+ if (revisionMapping == null) {
+ String warning = "Skipping OneNote cell " + objectGroupCellID +
+ ": no revision mapping";
+ msOneStorePackage.recordParseWarning(warning);
+ return null;
+ }
RevisionManifestDataElementData revisionManifest =
findRevisionManifestDataElement(revisionMapping.revisionMappingExGuid);
- msOneStorePackage.revisionManifests.add(revisionManifest);
- RevisionManifestRootDeclare encryptionKeyRoot =
- revisionManifest.revisionManifestRootDeclareList.stream()
- .filter(r -> r.rootExGuid.equals(new ExGuid(3,
- UUID.fromString("4A3717F8-1C14-49E7-9526-81D942DE1741"))))
- .findFirst().orElse(null);
- boolean isEncryption = encryptionKeyRoot != null;
- for (RevisionManifestObjectGroupReferences objRef :
- revisionManifest.revisionManifestObjectGroupReferences) {
- ObjectGroupDataElementData dataObject =
- (ObjectGroupDataElementData) objectGroupDataElements.stream()
- .filter(d -> d.dataElementExGuid.equals(objRef.objectGroupExtendedGUID))
- .findFirst().get().data;
-
- RevisionStoreObjectGroup objectGroup =
- RevisionStoreObjectGroup.createInstance(objRef.objectGroupExtendedGUID,
- dataObject, isEncryption);
- objectGroups.add(objectGroup);
- }
-
- return objectGroups;
+ if (revisionManifest == null || revisionManifest.revisionManifest == null) {
+ String warning = "Skipping OneNote cell " + objectGroupCellID +
+ ": no revision manifest";
+ msOneStorePackage.recordParseWarning(warning);
+ return null;
+ }
+
+ // A revision manifest may only reference the object groups that were created or
+ // modified in that revision. The remaining object groups belong to the chain of
+ // base revisions (MS-FSSHTTPB "Base Revision ID"). Follow that chain and collect
+ // the object groups of every revision, oldest revision first, so no content is lost.
+ Deque<RevisionManifestDataElementData> revisionChain = new ArrayDeque<>();
+ Set<ExGuid> seenRevisionIds = new HashSet<>();
+ while (revisionManifest != null) {
+ ExGuid revisionId = revisionManifest.revisionManifest.revisionID;
+ if (revisionId != null && !seenRevisionIds.add(revisionId)) {
+ // cycle guard - stop if we have already visited this revision
+ break;
+ }
+ revisionChain.addFirst(revisionManifest);
+ ExGuid baseRevisionId = revisionManifest.revisionManifest.baseRevisionID;
+ if (baseRevisionId == null || baseRevisionId.guid == null ||
+ GuidUtil.emptyGuid().equals(baseRevisionId.guid)) {
+ break;
+ }
+ StorageIndexRevisionMapping baseRevisionMapping =
+ msOneStorePackage.findStorageIndexRevisionMapping(baseRevisionId);
+ revisionManifest = baseRevisionMapping == null ? null :
+ findRevisionManifestDataElement(baseRevisionMapping.revisionMappingExGuid);
+ }
+
+ Set<ExGuid> seenObjectGroupIds = new HashSet<>();
+ // for each root role, the declaration made by the most recent revision wins
+ Map<ExGuid, RevisionManifestRootDeclare> effectiveRootDeclares = new LinkedHashMap<>();
+ for (RevisionManifestDataElementData manifest : revisionChain) {
+ msOneStorePackage.revisionManifests.add(manifest);
+ RevisionManifestRootDeclare encryptionKeyRoot =
+ manifest.revisionManifestRootDeclareList.stream()
+ .filter(r -> r.rootExGuid.equals(ENCRYPTION_KEY_ROOT_EXGUID))
+ .findFirst().orElse(null);
+ boolean isEncryption = encryptionKeyRoot != null;
+ for (RevisionManifestRootDeclare rootDeclare :
+ manifest.revisionManifestRootDeclareList) {
+ if (!rootDeclare.rootExGuid.equals(ENCRYPTION_KEY_ROOT_EXGUID)) {
+ effectiveRootDeclares.put(rootDeclare.rootExGuid, rootDeclare);
+ }
+ }
+ for (RevisionManifestObjectGroupReferences objRef :
+ manifest.revisionManifestObjectGroupReferences) {
+ if (!seenObjectGroupIds.add(objRef.objectGroupExtendedGUID)) {
+ continue;
+ }
+ DataElement dataElement =
+ objectGroupDataElementsById.get(objRef.objectGroupExtendedGUID);
+ if (dataElement == null) {
+ msOneStorePackage.recordParseWarning(
+ "OneNote object group " + objRef.objectGroupExtendedGUID +
+ " could not be resolved");
+ continue;
+ }
+ ObjectGroupDataElementData dataObject =
+ (ObjectGroupDataElementData) dataElement.data;
+
+ ObjectGroupCacheKey cacheKey =
+ new ObjectGroupCacheKey(dataElement.dataElementExGuid, isEncryption);
+ RevisionStoreObjectGroup cachedObjectGroup = objectGroupsByDataElementId.get(cacheKey);
+ if (cachedObjectGroup == null) {
+ cachedObjectGroup = RevisionStoreObjectGroup.createInstance(
+ objRef.objectGroupExtendedGUID, dataObject, isEncryption,
+ objectBlobElementsById);
+ objectGroupsByDataElementId.put(cacheKey, cachedObjectGroup);
+ }
+ objectGroups.add(cachedObjectGroup.copy());
+ }
+ }
+
+ removeSupersededObjects(objectGroups);
+
+ RevisionStoreCell cell = new RevisionStoreCell();
+ cell.cellID = objectGroupCellID;
+ cell.objectGroups = objectGroups;
+ cell.rootDeclares = new ArrayList<>(effectiveRootDeclares.values());
+ return cell;
+ }
+
+ /**
+ * An object that is modified in a later revision appears again, with the same object ID,
+ * in that revision's object group. Keep only the newest version of each object, replacing
+ * the older version in place so the original object ordering is preserved.
+ *
+ * @param objectGroups The object groups ordered from the oldest revision to the newest.
+ */
+ private void removeSupersededObjects(List<RevisionStoreObjectGroup> objectGroups) {
+ Map<ExGuid, List<RevisionStoreObject>> containingList = new HashMap<>();
+ Map<ExGuid, Integer> indexInList = new HashMap<>();
+ for (RevisionStoreObjectGroup objectGroup : objectGroups) {
+ List<RevisionStoreObject> objects = objectGroup.objects;
+ for (int i = 0; i < objects.size(); ) {
+ RevisionStoreObject object = objects.get(i);
+ ExGuid objectId = object.objectID;
+ if (objectId != null && containingList.containsKey(objectId)) {
+ // newer version of an already seen object - replace the older one in place
+ containingList.get(objectId).set(indexInList.get(objectId), object);
+ objects.remove(i);
+ } else {
+ if (objectId != null) {
+ containingList.put(objectId, objects);
+ indexInList.put(objectId, i);
+ }
+ ++i;
+ }
+ }
+ }
}
}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/property/PrtArrayOfPropertyValues.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/property/PrtArrayOfPropertyValues.java
index 68b1dab169..7220f999bf 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/property/PrtArrayOfPropertyValues.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/property/PrtArrayOfPropertyValues.java
@@ -41,16 +41,27 @@ public class PrtArrayOfPropertyValues implements IProperty {
* @return
*/
public int doDeserializeFromByteArray(byte[] byteArray, int startIndex) throws IOException {
+ return doDeserializeFromByteArray(byteArray, startIndex, 0);
+ }
+
+ public int doDeserializeFromByteArray(byte[] byteArray, int startIndex, int depth)
+ throws IOException {
int index = startIndex;
this.cProperties = BitConverter.toInt32(byteArray, index);
index += 4;
this.propertyID = new PropertyID();
int len = this.propertyID.doDeserializeFromByteArray(byteArray, index);
index += len;
+ // each PropertySet consumes at least its 2-byte count, so a valid count cannot
+ // exceed the remaining bytes / 2; bounds the allocation on malformed counts
+ if (this.cProperties < 0 || this.cProperties > (byteArray.length - index) / 2) {
+ throw new IOException("prtArrayOfPropertyValues count " + this.cProperties +
+ " exceeds remaining data " + (byteArray.length - index));
+ }
this.data = new PropertySet[this.cProperties];
for (int i = 0; i < this.cProperties; i++) {
this.data[i] = new PropertySet();
- int length = this.data[i].doDeserializeFromByteArray(byteArray, index);
+ int length = this.data[i].doDeserializeFromByteArray(byteArray, index, depth + 1);
index += length;
}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/property/PrtFourBytesOfLengthFollowedByData.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/property/PrtFourBytesOfLengthFollowedByData.java
index 8518be09b5..4eb0ca7b83 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/property/PrtFourBytesOfLengthFollowedByData.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/property/PrtFourBytesOfLengthFollowedByData.java
@@ -43,6 +43,12 @@ public class PrtFourBytesOfLengthFollowedByData implements IProperty {
int index = startIndex;
this.cb = (int) BitConverter.toUInt32(byteArray, startIndex);
index += 4;
+ // copyOfRange zero-fills past the source end, so cb must be bounded by
+ // the remaining bytes or a malformed length allocates cb bytes of heap
+ if (this.cb < 0 || this.cb > byteArray.length - index) {
+ throw new IOException("prtFourBytesOfLengthFollowedByData length " + this.cb +
+ " exceeds remaining data " + (byteArray.length - index));
+ }
this.data = Arrays.copyOfRange(byteArray, index, index + this.cb);
index += this.cb;
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/DataElement.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/DataElement.java
index 2ae203fb2b..96e94f0e59 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/DataElement.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/DataElement.java
@@ -50,7 +50,8 @@ public class DataElement extends StreamObject {
String className = DataElement.class.getPackage().getName() + "." + value.name();
try {
- DATA_ELEMENT_DATA_TYPE_MAPPING.put(value, Class.forName(className));
+ DATA_ELEMENT_DATA_TYPE_MAPPING.put(value,
+ Class.forName(className, false, DataElement.class.getClassLoader()));
} catch (ClassNotFoundException e) {
// This is OK, we are not pulling over every single class
}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/FileDataObject.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/FileDataObject.java
index 1211d2bf9d..d3a70780a9 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/FileDataObject.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/FileDataObject.java
@@ -19,8 +19,24 @@ package org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj;
/**
* This class is used to represent the file data.
*/
-class FileDataObject {
+public class FileDataObject {
public ObjectGroupObjectBLOBDataDeclaration objectDataBLOBDeclaration;
public ObjectGroupObjectDataBLOBReference objectDataBLOBReference;
public DataElement objectDataBLOBDataElement;
+
+ /**
+ * @return the opaque binary data of this file data object, or null if it could not be
+ * resolved.
+ */
+ public byte[] getData() {
+ if (objectDataBLOBDataElement != null &&
+ objectDataBLOBDataElement.data instanceof ObjectDataBLOBDataElementData) {
+ ObjectDataBLOBDataElementData blobData =
+ (ObjectDataBLOBDataElementData) objectDataBLOBDataElement.data;
+ if (blobData.objectDataBLOB != null) {
+ return blobData.objectDataBLOB.getData();
+ }
+ }
+ return null;
+ }
}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/ObjectDataBLOB.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/ObjectDataBLOB.java
new file mode 100644
index 0000000000..2873e1d013
--- /dev/null
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/ObjectDataBLOB.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.BasicObject;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.BinaryItem;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.util.ByteUtil;
+
+/**
+ * Specifies an object data BLOB stream object - the opaque binary data of an object,
+ * e.g. an embedded image or file. See MS-FSSHTTPB section 2.2.1.12.8.
+ */
+public class ObjectDataBLOB extends StreamObject {
+ public BinaryItem data;
+
+ public ObjectDataBLOB() {
+ super(StreamObjectTypeHeaderStart.ObjectDataBLOB);
+ this.data = new BinaryItem();
+ }
+
+ /**
+ * @return the opaque binary data as a byte array, or null if not present.
+ */
+ public byte[] getData() {
+ if (this.data == null || this.data.content == null) {
+ return null;
+ }
+ return ByteUtil.toByteArray(this.data.content);
+ }
+
+ @Override
+ protected void deserializeItemsFromByteArray(byte[] byteArray, AtomicInteger currentIndex,
+ int lengthOfItems)
+ throws TikaException, IOException {
+ AtomicInteger index = new AtomicInteger(currentIndex.get());
+ this.data = BasicObject.parse(byteArray, index, BinaryItem.class);
+
+ if (index.get() - currentIndex.get() != lengthOfItems) {
+ throw new StreamObjectParseErrorException(currentIndex.get(), "ObjectDataBLOB",
+ "Stream object over-parse error", null);
+ }
+
+ currentIndex.set(index.get());
+ }
+
+ @Override
+ protected int serializeItemsToByteList(List<Byte> byteList) throws IOException {
+ int startPoint = byteList.size();
+ if (this.data != null) {
+ byteList.addAll(this.data.serializeToByteList());
+ }
+ return byteList.size() - startPoint;
+ }
+}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/ObjectDataBLOBDataElementData.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/ObjectDataBLOBDataElementData.java
new file mode 100644
index 0000000000..ff28e9d096
--- /dev/null
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/ObjectDataBLOBDataElementData.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.tika.exception.TikaException;
+
+/**
+ * Object data BLOB data element - carries the opaque binary data of an object, e.g. an
+ * embedded image or file. See MS-FSSHTTPB section 2.2.1.12.8.
+ */
+public class ObjectDataBLOBDataElementData extends DataElementData {
+ public ObjectDataBLOB objectDataBLOB;
+
+ public ObjectDataBLOBDataElementData() {
+ this.objectDataBLOB = new ObjectDataBLOB();
+ }
+
+ @Override
+ public int deserializeDataElementDataFromByteArray(byte[] byteArray, int startIndex)
+ throws TikaException, IOException {
+ AtomicInteger index = new AtomicInteger(startIndex);
+ this.objectDataBLOB = StreamObject.getCurrent(byteArray, index, ObjectDataBLOB.class);
+ return index.get() - startIndex;
+ }
+
+ @Override
+ public List<Byte> serializeToByteList() throws TikaException, IOException {
+ return this.objectDataBLOB.serializeToByteList();
+ }
+}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/PropertySet.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/PropertySet.java
index 45e1278f42..64276e5aa5 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/PropertySet.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/PropertySet.java
@@ -37,6 +37,10 @@ import org.apache.tika.parser.microsoft.onenote.fsshttpb.util.BitConverter;
* This class is used to represent a PropertySet.
*/
public class PropertySet implements IProperty {
+ // a nesting level costs ~14 file bytes, so unbounded depth lets a small
+ // crafted file drive a StackOverflowError that escapes catch(Exception)
+ public static final int MAX_PROPERTY_NESTING = 100;
+
public int cProperties;
public PropertyID[] rgPrids;
@@ -72,6 +76,14 @@ public class PropertySet implements IProperty {
* @return Return the length in byte of the PropertySet.
*/
public int doDeserializeFromByteArray(byte[] byteArray, int startIndex) throws IOException {
+ return doDeserializeFromByteArray(byteArray, startIndex, 0);
+ }
+
+ public int doDeserializeFromByteArray(byte[] byteArray, int startIndex, int depth)
+ throws IOException {
+ if (depth > MAX_PROPERTY_NESTING) {
+ throw new IOException("PropertySet nesting exceeds " + MAX_PROPERTY_NESTING);
+ }
int index = startIndex;
this.cProperties = BitConverter.toInt16(byteArray, startIndex);
@@ -124,7 +136,16 @@ public class PropertySet implements IProperty {
break;
}
if (property != null) {
- int len = property.doDeserializeFromByteArray(byteArray, index);
+ int len;
+ if (property instanceof PropertySet) {
+ len = ((PropertySet) property)
+ .doDeserializeFromByteArray(byteArray, index, depth + 1);
+ } else if (property instanceof PrtArrayOfPropertyValues) {
+ len = ((PrtArrayOfPropertyValues) property)
+ .doDeserializeFromByteArray(byteArray, index, depth + 1);
+ } else {
+ len = property.doDeserializeFromByteArray(byteArray, index);
+ }
this.rgData.add(property);
index += len;
}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/FileDataObject.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/RevisionStoreCell.java
similarity index 53%
copy from tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/FileDataObject.java
copy to tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/RevisionStoreCell.java
index 1211d2bf9d..188a2a45de 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/FileDataObject.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/RevisionStoreCell.java
@@ -16,11 +16,22 @@
*/
package org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.CellID;
+
/**
- * This class is used to represent the file data.
+ * The revision store content of a single cell (object space), together with the root object
+ * declarations of its current revision. The object groups are ordered from the oldest revision
+ * to the newest.
*/
-class FileDataObject {
- public ObjectGroupObjectBLOBDataDeclaration objectDataBLOBDeclaration;
- public ObjectGroupObjectDataBLOBReference objectDataBLOBReference;
- public DataElement objectDataBLOBDataElement;
+public class RevisionStoreCell {
+ public CellID cellID;
+ public List<RevisionStoreObjectGroup> objectGroups = new ArrayList<>();
+ /**
+ * The effective root object declarations of the cell's current revision, i.e. for each
+ * root role the declaration made by the most recent revision in the base revision chain.
+ */
+ public List<RevisionManifestRootDeclare> rootDeclares = new ArrayList<>();
}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/RevisionStoreObjectGroup.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/RevisionStoreObjectGroup.java
index afd2f6c067..fb16a8402a 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/RevisionStoreObjectGroup.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/RevisionStoreObjectGroup.java
@@ -18,7 +18,7 @@ package org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj;
import java.io.IOException;
import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -38,13 +38,20 @@ public class RevisionStoreObjectGroup {
public static RevisionStoreObjectGroup createInstance(ExGuid objectGroupId,
ObjectGroupDataElementData dataObject,
- boolean isEncryption) throws IOException {
+ boolean isEncryption,
+ Map<ExGuid, DataElement> blobElements)
+ throws IOException {
RevisionStoreObjectGroup objectGroup = new RevisionStoreObjectGroup(objectGroupId);
- Map<ExGuid, RevisionStoreObject> objectDict = new HashMap<>();
+ // insertion-ordered so the object list (and any degraded-file fallback walk) is
+ // deterministic
+ Map<ExGuid, RevisionStoreObject> objectDict = new LinkedHashMap<>();
if (!isEncryption) {
RevisionStoreObject revisionObject = null;
for (int i = 0; i < dataObject.objectGroupDeclarations.objectDeclarationList.size();
i++) {
+ if (i >= dataObject.objectGroupData.objectGroupObjectDataList.size()) {
+ throw new IOException("Missing object data for object declaration " + i);
+ }
ObjectGroupObjectDeclare objectDeclaration =
dataObject.objectGroupDeclarations.objectDeclarationList.get(i);
ObjectGroupObjectData objectData =
@@ -63,16 +70,20 @@ public class RevisionStoreObjectGroup {
} else if (objectDeclaration.objectPartitionID.getDecodedValue() == 1) {
revisionObject.propertySet =
new PropertySetObject(objectDeclaration, objectData);
- if (revisionObject.jcid.jcid.isFileData != 0) {
- revisionObject.referencedObjectID = objectData.objectExGUIDArray;
- revisionObject.referencedObjectSpacesID = objectData.cellIDArray;
- }
+ // the object extended GUID array lists the objects referenced by this
+ // object, in the same order as the CompactIDs in the OID stream of the
+ // ObjectSpaceObjectPropSet - see MS-ONESTORE section 2.7.8
+ revisionObject.referencedObjectID = objectData.objectExGUIDArray;
+ revisionObject.referencedObjectSpacesID = objectData.cellIDArray;
}
}
for (int i = 0; i <
dataObject.objectGroupDeclarations.objectGroupObjectBLOBDataDeclarationList.size();
i++) {
+ if (i >= dataObject.objectGroupData.objectGroupObjectDataBLOBReferenceList.size()) {
+ throw new IOException("Missing BLOB reference for object declaration " + i);
+ }
ObjectGroupObjectBLOBDataDeclaration objectGroupObjectBLOBDataDeclaration =
dataObject.objectGroupDeclarations.objectGroupObjectBLOBDataDeclarationList.get(
i);
@@ -87,17 +98,24 @@ public class RevisionStoreObjectGroup {
objectDict.get(objectGroupObjectBLOBDataDeclaration.objectExGUID);
}
if (objectGroupObjectBLOBDataDeclaration.objectPartitionID.getDecodedValue() == 2) {
+ revisionObject.objectID = objectGroupObjectBLOBDataDeclaration.objectExGUID;
+ revisionObject.objectGroupID = objectGroupId;
revisionObject.fileDataObject = new FileDataObject();
revisionObject.fileDataObject.objectDataBLOBDeclaration =
objectGroupObjectBLOBDataDeclaration;
revisionObject.fileDataObject.objectDataBLOBReference =
objectGroupObjectDataBLOBReference;
+ revisionObject.fileDataObject.objectDataBLOBDataElement = blobElements.get(
+ objectGroupObjectDataBLOBReference.blobExtendedGUID);
}
}
objectGroup.objects.addAll(objectDict.values());
} else {
for (int i = 0; i < dataObject.objectGroupDeclarations.objectDeclarationList.size();
i++) {
+ if (i >= dataObject.objectGroupData.objectGroupObjectDataList.size()) {
+ throw new IOException("Missing object data for object declaration " + i);
+ }
ObjectGroupObjectDeclare objectDeclaration =
dataObject.objectGroupDeclarations.objectDeclarationList.get(i);
ObjectGroupObjectData objectData =
@@ -114,4 +132,17 @@ public class RevisionStoreObjectGroup {
return objectGroup;
}
+
+ /**
+ * Creates a cell-local wrapper around this parsed group. The object lists are copied so
+ * revision supersession filtering cannot mutate a cached group used by another cell.
+ *
+ * @return a shallow copy with independent mutable lists
+ */
+ public RevisionStoreObjectGroup copy() {
+ RevisionStoreObjectGroup copy = new RevisionStoreObjectGroup(objectGroupID);
+ copy.objects.addAll(objects);
+ copy.encryptionObjects.addAll(encryptionObjects);
+ return copy;
+ }
}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/StreamObject.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/StreamObject.java
index 03bb1da92c..6a250ba124 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/StreamObject.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/StreamObject.java
@@ -73,7 +73,8 @@ public abstract class StreamObject implements IFSSHTTPBSerializable {
for (StreamObjectTypeHeaderStart value : StreamObjectTypeHeaderStart.values()) {
String className = StreamObject.class.getPackage().getName() + "." + value.name();
try {
- streamObjectTypeMapping.put(value, Class.forName(className));
+ streamObjectTypeMapping.put(value,
+ Class.forName(className, false, StreamObject.class.getClassLoader()));
} catch (ClassNotFoundException e) {
// This is OK, we are not pulling over every single class
}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/basic/CellID.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/basic/CellID.java
index a352dc6315..7b6dbcf886 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/basic/CellID.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/basic/CellID.java
@@ -105,6 +105,14 @@ public class CellID extends BasicObject {
return this.extendGUID1.hashCode() + this.extendGUID2.hashCode();
}
+ @Override
+ public String toString() {
+ return "CellID{" +
+ "extendGUID1=" + extendGUID1 +
+ ", extendGUID2=" + extendGUID2 +
+ '}';
+ }
+
/**
* This method is used to deserialize the CellID basic object from the specified byte array and start index.
*
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/basic/ExGuid.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/basic/ExGuid.java
index 4a7e149de2..42ff354b0c 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/basic/ExGuid.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/basic/ExGuid.java
@@ -144,6 +144,15 @@ public class ExGuid extends BasicObject {
return this.guid.hashCode() + Integer.valueOf(this.value).hashCode();
}
+ @Override
+ public String toString() {
+ return "ExGuid{" +
+ "type=" + type +
+ ", value=" + value +
+ ", guid=" + guid +
+ '}';
+ }
+
/**
* This method is used to deserialize the ExGuid basic object from the specified byte array and start index.
*
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/space/ObjectSpaceObjectStreamOfContextIDs.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/space/ObjectSpaceObjectStreamOfContextIDs.java
index fe6d288330..7e1e348e7d 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/space/ObjectSpaceObjectStreamOfContextIDs.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/space/ObjectSpaceObjectStreamOfContextIDs.java
@@ -57,10 +57,16 @@ public class ObjectSpaceObjectStreamOfContextIDs {
int headerCount = this.header.doDeserializeFromByteArray(byteArray, index);
index += headerCount;
+ // each CompactID consumes 4 bytes, so a valid count cannot exceed the remaining
+ // bytes / 4; bounds the allocation on malformed counts
+ if (this.header.count > (byteArray.length - index) / 4L) {
+ throw new IOException("ObjectSpaceObjectStreamOfContextIDs count " +
+ this.header.count + " exceeds remaining data " + (byteArray.length - index));
+ }
this.body = new CompactID[(int) this.header.count];
for (int i = 0; i < this.header.count; i++) {
CompactID compactID = new CompactID();
- int count = compactID.doDeserializeFromByteArray(byteArray, startIndex);
+ int count = compactID.doDeserializeFromByteArray(byteArray, index);
this.body[i] = compactID;
index += count;
}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/space/ObjectSpaceObjectStreamOfOIDs.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/space/ObjectSpaceObjectStreamOfOIDs.java
index 7901dd015e..53398663c3 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/space/ObjectSpaceObjectStreamOfOIDs.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/space/ObjectSpaceObjectStreamOfOIDs.java
@@ -58,10 +58,16 @@ public class ObjectSpaceObjectStreamOfOIDs {
int headerCount = this.header.doDeserializeFromByteArray(byteArray, index);
index += headerCount;
+ // each CompactID consumes 4 bytes, so a valid count cannot exceed the remaining
+ // bytes / 4; bounds the allocation on malformed counts
+ if (this.header.count > (byteArray.length - index) / 4L) {
+ throw new IOException("ObjectSpaceObjectStreamOfOIDs count " + this.header.count +
+ " exceeds remaining data " + (byteArray.length - index));
+ }
this.body = new CompactID[(int) this.header.count];
for (int i = 0; i < this.header.count; i++) {
CompactID compactID = new CompactID();
- int count = compactID.doDeserializeFromByteArray(byteArray, startIndex);
+ int count = compactID.doDeserializeFromByteArray(byteArray, index);
this.body[i] = compactID;
index += count;
}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/space/ObjectSpaceObjectStreamOfOSIDs.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/space/ObjectSpaceObjectStreamOfOSIDs.java
index 61f7deda90..f37de26f76 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/space/ObjectSpaceObjectStreamOfOSIDs.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/streamobj/space/ObjectSpaceObjectStreamOfOSIDs.java
@@ -57,6 +57,12 @@ public class ObjectSpaceObjectStreamOfOSIDs {
int headerCount = this.header.doDeserializeFromByteArray(byteArray, index);
index += headerCount;
+ // each CompactID consumes 4 bytes, so a valid count cannot exceed the remaining
+ // bytes / 4; bounds the allocation on malformed counts
+ if (this.header.count > (byteArray.length - index) / 4L) {
+ throw new IOException("ObjectSpaceObjectStreamOfOSIDs count " + this.header.count +
+ " exceeds remaining data " + (byteArray.length - index));
+ }
this.body = new CompactID[(int) this.header.count];
for (int i = 0; i < this.header.count; i++) {
CompactID compactID = new CompactID();
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/onenote/GUIDTest.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/onenote/GUIDTest.java
new file mode 100644
index 0000000000..0f21f861db
--- /dev/null
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/onenote/GUIDTest.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser.microsoft.onenote;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.tika.exception.TikaException;
+
+public class GUIDTest {
+
+ @Test
+ public void testParsesValidGuidRoundTrip() throws Exception {
+ byte[] valid = "{638DE92F-a6d4-4BC1-9A36-4AFC2511A5B7}"
+ .getBytes(StandardCharsets.UTF_16LE);
+
+ assertEquals("{638DE92F-A6D4-4BC1-9A36-4AFC2511A5B7}",
+ GUID.fromCurlyBraceUTF16Bytes(valid).toString());
+ }
+
+ @Test
+ public void testRejectsNonAsciiUnicodeDigits() {
+ // fullwidth '6' is a Unicode digit that Character.digit(c, 16) would accept
+ byte[] malformed = "{638DE92F-A6D4-4BC1-9A36-4AFC2511A5B7}"
+ .getBytes(StandardCharsets.UTF_16LE);
+
+ assertThrows(TikaException.class, () -> GUID.fromCurlyBraceUTF16Bytes(malformed));
+ }
+
+ @Test
+ public void testRejectsMalformedCurlyBraceGuid() {
+ byte[] malformed = "{638DE92F-A6D4-4BC1-9IJ36-4AFC2511A5B7}"
+ .getBytes(StandardCharsets.UTF_16LE);
+
+ assertThrows(TikaException.class, () -> GUID.fromCurlyBraceUTF16Bytes(malformed));
+ }
+
+ @Test
+ public void testRejectsInjectedDashes() {
+ String[] malformedGuids = {
+ "{-38DE92F-A6D4-4BC1-9A36-4AFC2511A5B7}",
+ "{--8DE92F-A6D4-4BC1-9A36-4AFC2511A5B7}",
+ "{---8DE92F-A6D4-4BC1-9A36-4AFC2511A5B7}",
+ "{----E92F-A6D4-4BC1-9A36-4AFC2511A5B7}"
+ };
+ for (String malformedGuid : malformedGuids) {
+ assertThrows(TikaException.class,
+ () -> GUID.fromCurlyBraceUTF16Bytes(
+ malformedGuid.getBytes(StandardCharsets.UTF_16LE)));
+ }
+ }
+}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/onenote/OneNoteParserTest.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/onenote/OneNoteParserTest.java
index bab0a6fbcb..e2c9e410dd 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/onenote/OneNoteParserTest.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/onenote/OneNoteParserTest.java
@@ -19,23 +19,128 @@ package org.apache.tika.parser.microsoft.onenote;
import static org.apache.tika.parser.microsoft.onenote.OneNoteParser.ONE_NOTE_PREFIX;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringWriter;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.time.Instant;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.xml.sax.ContentHandler;
import org.apache.tika.TikaTest;
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.exception.TikaMemoryLimitException;
+import org.apache.tika.extractor.EmbeddedDocumentExtractor;
+import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.MSOneStorePackage;
+import org.apache.tika.sax.ToTextContentHandler;
+import org.apache.tika.sax.XHTMLContentHandler;
public class OneNoteParserTest extends TikaTest {
//test recursive parser wrapper for image files
+ @Test
+ public void testFuzzerRegressionInputsFallBackToLegacyDump() throws Exception {
+ // a structural failure falls back to the legacy string dump; the failure is
+ // pinned in the parse-warning metadata
+ String[][] resources = {
+ {"testOneNote-fuzz1.one", "Missing dependent revision"},
+ {"testOneNote-fuzz2.one", "unified property count"},
+ {"testOneNote-fuzz3.one", "Invalid GUID string"}
+ };
+ for (String[] resource : resources) {
+ InputStream input = getClass().getResourceAsStream("/test-documents/" + resource[0]);
+ assertNotNull(input, resource[0]);
+ try (InputStream stream = input;
+ TikaInputStream tis = TikaInputStream.get(stream)) {
+ Metadata metadata = new Metadata();
+ new OneNoteParser().parse(tis, new ToTextContentHandler(), metadata,
+ new ParseContext());
+ assertTrue(Arrays.stream(
+ metadata.getValues(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING))
+ .anyMatch(warning -> warning.contains(resource[1])
+ && warning.contains("falling back to legacy text dump")),
+ () -> resource[0] + ": " + Arrays.toString(metadata.getValues(
+ TikaCoreProperties.TIKA_META_EXCEPTION_WARNING)));
+ }
+ }
+ }
+
+ @Test
+ public void testNoContentFsshttpbWalkFallsBackToLegacyDump(@TempDir Path tempDir)
+ throws Exception {
+ Path file = tempDir.resolve("empty-walk.one");
+ // trailing newline: the dump excludes the file's final byte from scanning
+ Files.write(file,
+ "recoverable legacy dump text\n".getBytes(StandardCharsets.US_ASCII));
+ Metadata metadata = new Metadata();
+ StringWriter writer = new StringWriter();
+ XHTMLContentHandler xhtml = new XHTMLContentHandler(new ToTextContentHandler(writer),
+ metadata);
+ xhtml.startDocument();
+ try (OneNoteDirectFileResource resource =
+ new OneNoteDirectFileResource(file.toFile())) {
+ // fresh package = the walk completed without emitting anything
+ OneNoteParser.legacyFallbackIfNoContent(new MSOneStorePackage(), metadata, xhtml,
+ resource);
+ assertTrue(writer.toString().contains("recoverable legacy dump text"));
+ assertTrue(metadata.get(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING)
+ .contains("produced no content"));
+
+ // pkg == null means the exception path already dumped - no second dump
+ StringWriter exceptionPath = new StringWriter();
+ XHTMLContentHandler xhtml2 = new XHTMLContentHandler(
+ new ToTextContentHandler(exceptionPath), metadata);
+ xhtml2.startDocument();
+ OneNoteParser.legacyFallbackIfNoContent(null, metadata, xhtml2, resource);
+ xhtml2.endDocument();
+ assertTrue(exceptionPath.toString().isBlank());
+ }
+ xhtml.endDocument();
+ }
+
+ @Test
+ public void testTruncatedFileFallsBackToLegacyDump(@TempDir Path tempDir) throws Exception {
+ byte[] full;
+ try (InputStream is = getClass()
+ .getResourceAsStream("/test-documents/testOneNote1.one")) {
+ full = is.readAllBytes();
+ }
+ // keep the 1024-byte header plus a sliver of content so the root file node list
+ // is unreachable
+ Path truncated = tempDir.resolve("truncated.one");
+ Files.write(truncated, Arrays.copyOf(full, 2048));
+
+ Metadata metadata = new Metadata();
+ try (TikaInputStream tis = TikaInputStream.get(truncated)) {
+ new OneNoteParser().parse(tis, new ToTextContentHandler(), metadata,
+ new ParseContext());
+ }
+ assertTrue(Arrays.stream(metadata.getValues(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING))
+ .anyMatch(warning -> warning.contains("falling back to legacy text dump")));
+ }
+
/**
* This is the sample document that is automatically created from onenote 2013.
*/
@@ -223,6 +328,217 @@ public class OneNoteParserTest extends TikaTest {
ml.get("Content-Type"))));
}
+ @Test
+ public void testOneNoteEmbeddedImage() throws Exception {
+ List<byte[]> embedded = new ArrayList<>();
+ List<String> embeddedTypes = new ArrayList<>();
+ ParseContext context = new ParseContext();
+ context.set(EmbeddedDocumentExtractor.class, new EmbeddedDocumentExtractor() {
+ @Override
+ public boolean shouldParseEmbedded(Metadata metadata) {
+ return true;
+ }
+
+ @Override
+ public void parseEmbedded(InputStream stream, ContentHandler handler,
+ Metadata metadata, boolean outputHtml) throws IOException {
+ embedded.add(stream.readAllBytes());
+ embeddedTypes.add(metadata.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE));
+ }
+ });
+ try (InputStream tis = getResourceAsStream("/test-documents/testOneNoteEmbeddedImage.one")) {
+ new OneNoteParser().parse(tis, new ToTextContentHandler(), new Metadata(), context);
+ }
+
+ assertEquals(1, embedded.size());
+ assertTrue(embedded.stream().anyMatch(bytes -> bytes.length > 1000),
+ () -> "embedded sizes: " + embedded.stream().map(bytes -> bytes.length).collect(Collectors.toList()));
+ assertTrue(embeddedTypes.contains("INLINE"));
+ }
+
+ @Test
+ public void testOneNoteEmbeddedImageRecursiveMetadata() throws Exception {
+ List<Metadata> metadataList = getRecursiveMetadata("testOneNoteEmbeddedImage.one");
+
+ assertEquals(2, metadataList.size());
+ Metadata embedded = metadataList.get(1);
+ assertEquals("INLINE", embedded.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE));
+ assertNotNull(embedded.get(TikaCoreProperties.EMBEDDED_RELATIONSHIP_ID));
+ }
+
+ @Test
+ public void testPropertyValueBudgetIsSharedAcrossCopiesAndResetsPerList(
+ @TempDir Path tempDir) throws Exception {
+ Path emptyFile = tempDir.resolve("empty");
+ Files.write(emptyFile, ByteBuffer.allocate(36).order(ByteOrder.LITTLE_ENDIAN)
+ .putLong(FileNodeListHeader.UNIT_MAGIC_CONSTANT).putInt(0x10).putInt(0)
+ .putLong(-1).putInt(0).putLong(OneNotePtr.FOOTER_CONST).array());
+ Method reservePropertyCount = OneNotePtr.class.getDeclaredMethod(
+ "reservePropertyCount", long.class, String.class);
+ reservePropertyCount.setAccessible(true);
+ try (OneNoteDirectFileResource dif = new OneNoteDirectFileResource(emptyFile.toFile())) {
+ OneNotePtr ptr = new OneNotePtr(new OneNoteDocument(), dif);
+ OneNotePtr copy = new OneNotePtr(ptr);
+ reservePropertyCount.invoke(ptr, 100_000L, "test");
+ InvocationTargetException exception = assertThrows(InvocationTargetException.class,
+ () -> reservePropertyCount.invoke(copy, 1L, "test"));
+ assertTrue(exception.getCause() instanceof TikaMemoryLimitException);
+
+ ptr.deserializeFileNodeList(new FileNodeList(), new FileNodePtr());
+ reservePropertyCount.invoke(ptr, 1L, "test");
+ }
+ }
+
+ @Test
+ public void testFileNodeCycleIsReportedAndStopsTraversal(@TempDir Path tempDir) throws Exception {
+ FileNode fileNode = new FileNode().setGosid(ExtendedGUID.nil());
+ fileNode.childFileNodeList.setFileNodeListHeader(new FileNodeListHeader(0,
+ FileNodeListHeader.UNIT_MAGIC_CONSTANT, 0x10, 0));
+ fileNode.childFileNodeList.children.add(fileNode);
+ Metadata metadata = new Metadata();
+ ParseContext parseContext = new ParseContext();
+ Path emptyFile = Files.createFile(tempDir.resolve("empty"));
+ try (OneNoteDirectFileResource dif = new OneNoteDirectFileResource(emptyFile.toFile())) {
+ OneNoteTreeWalker walker = new OneNoteTreeWalker(new OneNoteTreeWalkerOptions(),
+ new OneNoteDocument(), dif,
+ new XHTMLContentHandler(new ToTextContentHandler(), metadata),
+ metadata, parseContext, null);
+ walker.walkFileNode(fileNode, null);
+ }
+ assertEquals(1, Arrays.stream(
+ metadata.getValues(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING))
+ .filter(warning -> warning.contains("file-node cycle detected"))
+ .count());
+ }
+
+ @Test
+ public void testDeepFileNodeChainIsDepthCapped(@TempDir Path tempDir) throws Exception {
+ // a long acyclic chain slips past the cycle guard - the depth cap must stop it
+ // before the recursion can overflow the stack
+ FileNode root = new FileNode().setGosid(ExtendedGUID.nil());
+ FileNode current = root;
+ for (int i = 0; i < 600; i++) {
+ FileNode child = new FileNode().setGosid(ExtendedGUID.nil());
+ current.childFileNodeList.setFileNodeListHeader(new FileNodeListHeader(0,
+ FileNodeListHeader.UNIT_MAGIC_CONSTANT, 0x10, 0));
+ current.childFileNodeList.children.add(child);
+ current = child;
+ }
+ Metadata metadata = new Metadata();
+ ParseContext parseContext = new ParseContext();
+ Path emptyFile = Files.createFile(tempDir.resolve("empty"));
+ try (OneNoteDirectFileResource dif = new OneNoteDirectFileResource(emptyFile.toFile())) {
+ OneNoteTreeWalker walker = new OneNoteTreeWalker(new OneNoteTreeWalkerOptions(),
+ new OneNoteDocument(), dif,
+ new XHTMLContentHandler(new ToTextContentHandler(), metadata),
+ metadata, parseContext, null);
+ walker.walkFileNode(root, null);
+ }
+ assertEquals(1, Arrays.stream(
+ metadata.getValues(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING))
+ .filter(warning -> warning.contains("exceeded depth limit"))
+ .count());
+ }
+
+ /**
+ * Writes one minimal 56-byte file-node-list fragment: header, a single baseType-2 node
+ * whose child list is at {childStp, childCb}, a terminator, a nil next-fragment
+ * reference and the footer.
+ */
+ private static void writeFileNodeListBlock(ByteBuffer buf, long childStp, int childCb) {
+ // id=0x10, size=16 (node header + 8-byte stp + 4-byte cb), stpFormat=0, cbFormat=0,
+ // baseType=2, reserved=1
+ int fileNodeHeader = 0x10 | (16 << 10) | (2 << 27) | (1 << 31);
+ buf.putLong(FileNodeListHeader.UNIT_MAGIC_CONSTANT).putInt(0x10).putInt(0)
+ .putInt(fileNodeHeader).putLong(childStp).putInt(childCb)
+ .putInt(0)
+ .putLong(-1).putInt(0)
+ .putLong(OneNotePtr.FOOTER_CONST);
+ }
+
+ @Test
+ public void testFileNodeListCycleFailsCleanly(@TempDir Path tempDir) throws Exception {
+ // one fragment holding a baseType-2 node whose child list points back at itself
+ ByteBuffer buf = ByteBuffer.allocate(56).order(ByteOrder.LITTLE_ENDIAN);
+ writeFileNodeListBlock(buf, 0, 56);
+ Path cyclic = tempDir.resolve("cyclic");
+ Files.write(cyclic, buf.array());
+ try (OneNoteDirectFileResource dif = new OneNoteDirectFileResource(cyclic.toFile())) {
+ OneNotePtr ptr = new OneNotePtr(new OneNoteDocument(), dif);
+ TikaException e = assertThrows(TikaException.class,
+ () -> ptr.deserializeFileNodeList(new FileNodeList(), new FileNodePtr()));
+ assertTrue(e.getMessage().contains("cycle"), e.getMessage());
+ }
+ }
+
+ @Test
+ public void testFileNodeListNestingIsDepthCapped(@TempDir Path tempDir) throws Exception {
+ // 120 lists, each holding one baseType-2 node pointing at the next list - acyclic,
+ // so only the depth cap can stop the recursion
+ int lists = 120;
+ ByteBuffer buf = ByteBuffer.allocate(56 * lists).order(ByteOrder.LITTLE_ENDIAN);
+ for (int i = 0; i < lists; i++) {
+ long childStp = (i + 1 < lists ? i + 1 : i) * 56L;
+ writeFileNodeListBlock(buf, childStp, 56);
+ }
+ Path deep = tempDir.resolve("deep");
+ Files.write(deep, buf.array());
+ try (OneNoteDirectFileResource dif = new OneNoteDirectFileResource(deep.toFile())) {
+ OneNotePtr ptr = new OneNotePtr(new OneNoteDocument(), dif);
+ assertThrows(TikaMemoryLimitException.class,
+ () -> ptr.deserializeFileNodeList(new FileNodeList(), new FileNodePtr()));
+ }
+ }
+
+ @Test
+ public void testFragmentChainCycleFailsCleanly(@TempDir Path tempDir) throws Exception {
+ // an empty fragment whose next-fragment reference points back at itself would
+ // previously loop forever
+ ByteBuffer buf = ByteBuffer.allocate(36).order(ByteOrder.LITTLE_ENDIAN)
+ .putLong(FileNodeListHeader.UNIT_MAGIC_CONSTANT).putInt(0x10).putInt(0)
+ .putLong(0).putInt(36)
+ .putLong(OneNotePtr.FOOTER_CONST);
+ Path cyclic = tempDir.resolve("cyclic-fragment");
+ Files.write(cyclic, buf.array());
+ try (OneNoteDirectFileResource dif = new OneNoteDirectFileResource(cyclic.toFile())) {
+ OneNotePtr ptr = new OneNotePtr(new OneNoteDocument(), dif);
+ TikaException e = assertThrows(TikaException.class,
+ () -> ptr.deserializeFileNodeList(new FileNodeList(), new FileNodePtr()));
+ assertTrue(e.getMessage().contains("fragment cycle"), e.getMessage());
+ }
+ }
+
+ @Test
+ public void testLegacyEmbeddedExtractionHonorsShouldParseEmbedded() throws Exception {
+ List<Metadata> offered = new ArrayList<>();
+ ParseContext context = new ParseContext();
+ context.set(EmbeddedDocumentExtractor.class, new EmbeddedDocumentExtractor() {
+ @Override
+ public boolean shouldParseEmbedded(Metadata metadata) {
+ offered.add(metadata);
+ return false;
+ }
+
+ @Override
+ public void parseEmbedded(InputStream stream, ContentHandler handler,
+ Metadata metadata, boolean outputHtml) {
+ throw new AssertionError("must not parse embedded when shouldParseEmbedded" +
+ " returns false");
+ }
+ });
+ try (InputStream tis =
+ getResourceAsStream("/test-documents/testOneNoteEmbeddedWordDoc.one")) {
+ new OneNoteParser().parse(tis, new ToTextContentHandler(), new Metadata(), context);
+ }
+ assertTrue(offered.size() > 0);
+ // the legacy path offers every embedded object as an ATTACHMENT; the relationship
+ // id is set only when the object carries a non-nil gosid
+ for (Metadata offeredMetadata : offered) {
+ assertEquals(TikaCoreProperties.EmbeddedResourceType.ATTACHMENT.toString(),
+ offeredMetadata.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE));
+ }
+ }
+
/**
* Test a document pulled from Office 365 which stores the MS-ONESTORE document using the MS-FSSHTTPB
* protocol.
@@ -232,7 +548,14 @@ public class OneNoteParserTest extends TikaTest {
Metadata metadata = new Metadata();
String txt = getText("testOneNoteFromOffice365.one", metadata);
- assertEquals(1, metadata.getValues(ONE_NOTE_PREFIX + "mostRecentAuthors").length);
+ // only the authors of the current content count - authors that only appear in
+ // older page version snapshots are not reported
+ assertEquals(Arrays.asList("Chang Du", "Du Chang"),
+ Arrays.asList(metadata.getValues(TikaCoreProperties.CREATOR)));
+ // both authors are referenced as AuthorMostRecent by current content; one of them
+ // is first visited under another role, so its most-recent role must still register
+ assertEquals(Arrays.asList("Chang Du", "Du Chang"),
+ Arrays.asList(metadata.getValues(ONE_NOTE_PREFIX + "mostRecentAuthors")));
assertEquals(Instant.ofEpochSecond(1636621406),
Instant.ofEpochSecond(Long.parseLong(metadata.get(ONE_NOTE_PREFIX + "creationTimestamp"))));
@@ -241,6 +564,9 @@ public class OneNoteParserTest extends TikaTest {
assertEquals(Instant.ofEpochSecond(1636621448),
Instant.ofEpochSecond(Long.parseLong(metadata.get(TikaCoreProperties.MODIFIED))));
assertContains("Section1Page1Content", txt);
+ // content from revisions other than each cell's current revision manifest
+ assertContains("Section1Page2Content", txt);
+ assertTrue(txt.indexOf("Section1Page1Content") < txt.indexOf("Section1Page2Content"));
}
/**
@@ -252,20 +578,23 @@ public class OneNoteParserTest extends TikaTest {
Metadata metadata = new Metadata();
String txt = getText("testOneNoteFromOffice365-2.one", metadata);
+ assertEquals(List.of("Robert Lucarini"),
+ Arrays.asList(metadata.getValues(TikaCoreProperties.CREATOR)));
List<String> mostRecentAuthors = Arrays.asList(metadata.getValues(ONE_NOTE_PREFIX + "mostRecentAuthors"));
assertContains(
- "R\u0000o\u0000b\u0000e\u0000r\u0000t\u0000 \u0000L\u0000u\u0000c\u0000a" +
- "\u0000r\u0000i\u0000n\u0000i\u0000\u0000\u0000",
+ "Robert Lucarini",
mostRecentAuthors);
assertEquals(Instant.ofEpochSecond(1591712300),
Instant.ofEpochSecond(Long.parseLong(metadata.get(ONE_NOTE_PREFIX + "creationTimestamp"))));
- assertEquals(Instant.ofEpochMilli(1623252330000L),
+ assertEquals(Instant.ofEpochMilli(1623597638000L),
Instant.ofEpochMilli(Long.parseLong(metadata.get(ONE_NOTE_PREFIX + "lastModifiedTimestamp"))));
assertEquals(Instant.ofEpochSecond(1623597587),
Instant.ofEpochSecond(Long.parseLong(metadata.get(TikaCoreProperties.MODIFIED))));
assertContains("Section1Page1Content", txt);
+ // content from revisions other than each cell's current revision manifest
+ assertContains("Section1Page2Content", txt);
}
private void assertNoJunk(String txt) {
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/MSOneStoreBlobTest.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/MSOneStoreBlobTest.java
new file mode 100644
index 0000000000..ea5a4c986b
--- /dev/null
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/MSOneStoreBlobTest.java
@@ -0,0 +1,343 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser.microsoft.onenote.fsshttpb;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringWriter;
+import java.lang.reflect.InvocationTargetException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.jupiter.api.Test;
+import org.xml.sax.ContentHandler;
+
+import org.apache.tika.extractor.EmbeddedDocumentExtractor;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.parser.microsoft.onenote.OneNoteTreeWalkerOptions;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.property.NoData;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.property.PrtFourBytesOfLengthFollowedByData;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.DataElement;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.FileDataObject;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.ObjectDataBLOB;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.ObjectDataBLOBDataElementData;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.ObjectGroupDataElementData;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.ObjectGroupObjectBLOBDataDeclaration;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.ObjectGroupObjectData;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.ObjectGroupObjectDataBLOBReference;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.ObjectGroupObjectDeclare;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.PropertySet;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.PropertySetObject;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.RevisionStoreObject;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.RevisionStoreObjectGroup;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.StreamObject;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.StreamObjectParseErrorException;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.BinaryItem;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.DataElementType;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.ExGUIDArray;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.ExGuid;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.PropertyID;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.PropertyType;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.space.ObjectSpaceObjectPropSet;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.util.ByteUtil;
+import org.apache.tika.sax.ToTextContentHandler;
+import org.apache.tika.sax.XHTMLContentHandler;
+
+/**
+ * Tests for object data BLOBs (embedded images and files) in the MS-FSSHTTPB packaged
+ * revision store.
+ */
+public class MSOneStoreBlobTest {
+
+ private static final byte[] BLOB_BYTES =
+ "these are the bytes of an embedded image".getBytes(StandardCharsets.UTF_8);
+
+ /**
+ * An object data BLOB data element must survive a serialization round trip. Before
+ * ObjectDataBLOBDataElementData existed, deserializing a data element of this type threw,
+ * so any OneNote file with an embedded image or file lost its structure entirely.
+ */
+ @Test
+ public void testObjectDataBLOBDataElementRoundTrip() throws Exception {
+ DataElement reparsed = roundTripBlobElement();
+ assertEquals(DataElementType.ObjectDataBLOBDataElementData, reparsed.dataElementType);
+ assertArrayEquals(BLOB_BYTES,
+ ((ObjectDataBLOBDataElementData) reparsed.data).objectDataBLOB.getData());
+ }
+
+ /**
+ * A revision store object whose BLOB reference resolves to an object data BLOB element
+ * must expose the BLOB bytes and hand them to the embedded document extractor during the
+ * tree walk.
+ */
+ @Test
+ public void testBlobIsEmittedAsEmbeddedDocument() throws Exception {
+ DataElement blobElement = roundTripBlobElement();
+
+ ObjectGroupObjectBLOBDataDeclaration declaration =
+ new ObjectGroupObjectBLOBDataDeclaration();
+ declaration.objectExGUID = new ExGuid(1, UUID.randomUUID());
+ declaration.objectPartitionID.setDecodedValue(2);
+ ObjectGroupObjectDataBLOBReference blobReference =
+ new ObjectGroupObjectDataBLOBReference();
+ blobReference.blobExtendedGUID = blobElement.dataElementExGuid;
+
+ ObjectGroupDataElementData groupData = new ObjectGroupDataElementData();
+ groupData.objectGroupDeclarations.objectGroupObjectBLOBDataDeclarationList
+ .add(declaration);
+ groupData.objectGroupData.objectGroupObjectDataBLOBReferenceList.add(blobReference);
+
+ RevisionStoreObjectGroup objectGroup =
+ RevisionStoreObjectGroup.createInstance(new ExGuid(2, UUID.randomUUID()),
+ groupData, false,
+ Collections.singletonMap(blobElement.dataElementExGuid, blobElement));
+ assertEquals(1, objectGroup.objects.size());
+ assertArrayEquals(BLOB_BYTES, objectGroup.objects.get(0).fileDataObject.getData());
+ addImageContainer(objectGroup, objectGroup.objects.get(0), "picture.png");
+
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.OtherFileNodeList.add(objectGroup);
+
+ List<byte[]> embedded = new ArrayList<>();
+ List<String> embeddedNames = new ArrayList<>();
+ List<String> embeddedTypes = new ArrayList<>();
+ ParseContext context = new ParseContext();
+ context.set(EmbeddedDocumentExtractor.class, new EmbeddedDocumentExtractor() {
+ @Override
+ public boolean shouldParseEmbedded(Metadata metadata) {
+ return true;
+ }
+
+ @Override
+ public void parseEmbedded(InputStream stream, ContentHandler handler,
+ Metadata metadata, boolean outputHtml) throws IOException {
+ embedded.add(stream.readAllBytes());
+ embeddedNames.add(metadata.get(TikaCoreProperties.RESOURCE_NAME_KEY));
+ embeddedTypes.add(metadata.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE));
+ }
+ });
+
+ Metadata metadata = new Metadata();
+ XHTMLContentHandler xhtml =
+ new XHTMLContentHandler(new ToTextContentHandler(new StringWriter()), metadata);
+ xhtml.startDocument();
+ pkg.walkTree(new OneNoteTreeWalkerOptions(), metadata, xhtml, context);
+ xhtml.endDocument();
+
+ assertEquals(1, embedded.size());
+ assertArrayEquals(BLOB_BYTES, embedded.get(0));
+ assertEquals("picture.png", embeddedNames.get(0));
+ assertEquals("INLINE", embeddedTypes.get(0));
+ }
+
+ private static void addImageContainer(RevisionStoreObjectGroup objectGroup,
+ RevisionStoreObject blob, String name) throws IOException {
+ RevisionStoreObject parent = new RevisionStoreObject();
+ parent.objectID = new ExGuid(9, UUID.randomUUID());
+ parent.objectGroupID = objectGroup.objectGroupID;
+ parent.referencedObjectID = new ExGUIDArray();
+ parent.referencedObjectID.content = Collections.singletonList(blob.objectID);
+
+ PropertySet propertySet = new PropertySet();
+ propertySet.cProperties = 2;
+ PropertyID fileNameID = new PropertyID();
+ fileNameID.type = PropertyType.FourBytesOfLengthFollowedByData.getIntVal();
+ fileNameID.value = 0x1C001DD7;
+ PropertyID containerID = new PropertyID();
+ containerID.type = PropertyType.ObjectID.getIntVal();
+ containerID.value = 0x20001C3F;
+ propertySet.rgPrids = new PropertyID[]{fileNameID, containerID};
+ PrtFourBytesOfLengthFollowedByData fileName = new PrtFourBytesOfLengthFollowedByData();
+ fileName.data = (name + "\u0000").getBytes(StandardCharsets.UTF_16LE);
+ fileName.cb = fileName.data.length;
+ propertySet.rgData = Arrays.asList(fileName, new NoData());
+ ObjectSpaceObjectPropSet propSet = new ObjectSpaceObjectPropSet();
+ propSet.body = propertySet;
+ ObjectGroupObjectData objectData = new ObjectGroupObjectData();
+ objectData.data.content.addAll(ByteUtil.toListOfByte(new byte[]{0, 0, 0, (byte) 0x80,
+ 0, 0, 0, 0}));
+ PropertySetObject propertySetObject = new PropertySetObject(null, objectData);
+ propertySetObject.objectSpaceObjectPropSet = propSet;
+ parent.propertySet = propertySetObject;
+ objectGroup.objects.add(0, parent);
+ }
+
+ @Test
+ public void testMissingBlobDataAndMalformedLengthAreHandled() throws Exception {
+ FileDataObject fileData = new FileDataObject();
+ assertNull(fileData.getData());
+
+ DataElement wrongType = new DataElement();
+ fileData.objectDataBLOBDataElement = wrongType;
+ assertNull(fileData.getData());
+
+ ObjectDataBLOBDataElementData emptyBlobData = new ObjectDataBLOBDataElementData();
+ emptyBlobData.objectDataBLOB = null;
+ wrongType.data = emptyBlobData;
+ assertNull(fileData.getData());
+
+ ObjectDataBLOB blob = new ObjectDataBLOB();
+ blob.data = null;
+ assertNull(blob.getData());
+ blob.data = new BinaryItem();
+ blob.data.content = null;
+ assertNull(blob.getData());
+ blob.data = new BinaryItem();
+ blob.data.content.add((byte) 1);
+ assertFalse(blob.serializeToByteList().isEmpty());
+
+ ObjectDataBLOB validBlob = new ObjectDataBLOB();
+ validBlob.data.content.add((byte) 1);
+ byte[] itemBytes = ByteUtil.toByteArray(validBlob.data.serializeToByteList());
+ AtomicInteger index = new AtomicInteger(0);
+ java.lang.reflect.Method deserialize = ObjectDataBLOB.class.getDeclaredMethod(
+ "deserializeItemsFromByteArray", byte[].class, AtomicInteger.class, int.class);
+ deserialize.setAccessible(true);
+ InvocationTargetException exception = assertThrows(InvocationTargetException.class,
+ () -> deserialize.invoke(new ObjectDataBLOB(), itemBytes, index,
+ itemBytes.length + 1));
+ assertTrue(exception.getCause() instanceof StreamObjectParseErrorException);
+ }
+
+ @Test
+ public void testEmptyAndFailedEmbeddedDataAreIgnored() throws Exception {
+ MSOneStorePackage emptyPackage = packageWithFileData(new ObjectDataBLOBDataElementData());
+ walkWithExtractor(emptyPackage, new EmbeddedDocumentExtractor() {
+ @Override
+ public boolean shouldParseEmbedded(Metadata metadata) {
+ throw new AssertionError("empty data must not reach the extractor");
+ }
+
+ @Override
+ public void parseEmbedded(InputStream stream, ContentHandler handler,
+ Metadata metadata, boolean outputHtml) {
+ }
+ });
+
+ DataElement blobElement = roundTripBlobElement();
+ MSOneStorePackage failedPackage = packageWithFileData(
+ (ObjectDataBLOBDataElementData) blobElement.data);
+ walkWithExtractor(failedPackage, new EmbeddedDocumentExtractor() {
+ @Override
+ public boolean shouldParseEmbedded(Metadata metadata) {
+ return true;
+ }
+
+ @Override
+ public void parseEmbedded(InputStream stream, ContentHandler handler,
+ Metadata metadata, boolean outputHtml) throws IOException {
+ throw new IOException("synthetic embedded parse failure");
+ }
+ });
+ }
+
+ @Test
+ public void testDeclarationWithoutObjectDataFailsCleanly() {
+ ObjectGroupDataElementData mismatched = new ObjectGroupDataElementData();
+ ObjectGroupObjectDeclare declaration = new ObjectGroupObjectDeclare();
+ declaration.objectPartitionID.setDecodedValue(1);
+ mismatched.objectGroupDeclarations.objectDeclarationList.add(declaration);
+
+ IOException e = assertThrows(IOException.class,
+ () -> RevisionStoreObjectGroup.createInstance(new ExGuid(9, UUID.randomUUID()),
+ mismatched, false, Collections.emptyMap()));
+ assertTrue(e.getMessage().contains("Missing object data"));
+ }
+
+ @Test
+ public void testEncryptedObjectGroupAndMissingBlobReference() throws Exception {
+ ObjectGroupDataElementData encryptedData = new ObjectGroupDataElementData();
+ ObjectGroupObjectDeclare declaration = new ObjectGroupObjectDeclare();
+ declaration.objectPartitionID.setDecodedValue(1);
+ ObjectGroupObjectData objectData = new ObjectGroupObjectData();
+ objectData.data.content.add((byte) 42);
+ encryptedData.objectGroupDeclarations.objectDeclarationList.add(declaration);
+ encryptedData.objectGroupData.objectGroupObjectDataList.add(objectData);
+
+ RevisionStoreObjectGroup encrypted = RevisionStoreObjectGroup.createInstance(
+ new ExGuid(3, UUID.randomUUID()), encryptedData, true, Collections.emptyMap());
+ assertEquals(1, encrypted.encryptionObjects.size());
+
+ ObjectGroupDataElementData missingBlobData = new ObjectGroupDataElementData();
+ ObjectGroupObjectBLOBDataDeclaration blobDeclaration =
+ new ObjectGroupObjectBLOBDataDeclaration();
+ blobDeclaration.objectExGUID = new ExGuid(4, UUID.randomUUID());
+ blobDeclaration.objectPartitionID.setDecodedValue(2);
+ ObjectGroupObjectDataBLOBReference reference =
+ new ObjectGroupObjectDataBLOBReference();
+ reference.blobExtendedGUID = new ExGuid(5, UUID.randomUUID());
+ missingBlobData.objectGroupDeclarations.objectGroupObjectBLOBDataDeclarationList
+ .add(blobDeclaration);
+ missingBlobData.objectGroupData.objectGroupObjectDataBLOBReferenceList.add(reference);
+ RevisionStoreObjectGroup missing = RevisionStoreObjectGroup.createInstance(
+ new ExGuid(6, UUID.randomUUID()), missingBlobData, false, Collections.emptyMap());
+ assertEquals(1, missing.objects.size());
+ assertNull(missing.objects.get(0).fileDataObject.getData());
+ }
+
+ private MSOneStorePackage packageWithFileData(ObjectDataBLOBDataElementData blobData) {
+ DataElement element = new DataElement();
+ element.data = blobData;
+ FileDataObject fileData = new FileDataObject();
+ fileData.objectDataBLOBDataElement = element;
+ RevisionStoreObject object = new RevisionStoreObject();
+ object.objectID = new ExGuid(7, UUID.randomUUID());
+ object.fileDataObject = fileData;
+ RevisionStoreObjectGroup group = new RevisionStoreObjectGroup(
+ new ExGuid(8, UUID.randomUUID()));
+ group.objects.add(object);
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.OtherFileNodeList.add(group);
+ return pkg;
+ }
+
+ private void walkWithExtractor(MSOneStorePackage pkg, EmbeddedDocumentExtractor extractor)
+ throws Exception {
+ ParseContext context = new ParseContext();
+ context.set(EmbeddedDocumentExtractor.class, extractor);
+ Metadata metadata = new Metadata();
+ XHTMLContentHandler xhtml = new XHTMLContentHandler(new ToTextContentHandler(new StringWriter()), metadata);
+ xhtml.startDocument();
+ pkg.walkTree(new OneNoteTreeWalkerOptions(), metadata, xhtml, context);
+ xhtml.endDocument();
+ }
+
+ private DataElement roundTripBlobElement() throws Exception {
+ ObjectDataBLOBDataElementData blobData = new ObjectDataBLOBDataElementData();
+ for (byte b : BLOB_BYTES) {
+ blobData.objectDataBLOB.data.content.add(b);
+ }
+ DataElement blobElement =
+ new DataElement(DataElementType.ObjectDataBLOBDataElementData, blobData);
+ byte[] serialized = ByteUtil.toByteArray(blobElement.serializeToByteList());
+ return StreamObject.getCurrent(serialized, new AtomicInteger(0), DataElement.class);
+ }
+}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/MSOneStorePackageTest.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/MSOneStorePackageTest.java
new file mode 100644
index 0000000000..ab597063fc
--- /dev/null
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/MSOneStorePackageTest.java
@@ -0,0 +1,846 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser.microsoft.onenote.fsshttpb;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.StringWriter;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+
+import org.junit.jupiter.api.Test;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.parser.microsoft.onenote.OneNoteParser;
+import org.apache.tika.parser.microsoft.onenote.OneNoteTreeWalkerOptions;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.property.ArrayNumber;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.property.IProperty;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.property.NoData;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.property.PrtArrayOfPropertyValues;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.property.PrtFourBytesOfLengthFollowedByData;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.DataElement;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.FileDataObject;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.ObjectDataBLOB;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.ObjectDataBLOBDataElementData;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.ObjectGroupObjectData;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.PropertySet;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.PropertySetObject;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.RevisionManifestRootDeclare;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.RevisionStoreCell;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.RevisionStoreObject;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.RevisionStoreObjectGroup;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.CellID;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.DataElementType;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.ExGuid;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.PropertyID;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.PropertyType;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.space.ObjectSpaceObjectPropSet;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.util.ByteUtil;
+import org.apache.tika.sax.ToTextContentHandler;
+import org.apache.tika.sax.ToXMLContentHandler;
+import org.apache.tika.sax.XHTMLContentHandler;
+
+public class MSOneStorePackageTest {
+
+ @Test
+ public void testParseWarningsAreBoundedAcrossParserAndWalkPhases() throws Exception {
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ for (int i = 0; i < 99; i++) {
+ pkg.recordParseWarning("warning " + i);
+ }
+ pkg.recordParseWarning("duplicate warning");
+ pkg.recordParseWarning("duplicate warning");
+ RevisionStoreCell damagedCell = new RevisionStoreCell();
+ RevisionManifestRootDeclare missingRoot = new RevisionManifestRootDeclare();
+ missingRoot.objectExGuid = id(1006);
+ damagedCell.rootDeclares.add(missingRoot);
+ pkg.cells.add(damagedCell);
+
+ Metadata metadata = new Metadata();
+ pkg.walkTree(new OneNoteTreeWalkerOptions(), metadata,
+ new XHTMLContentHandler(new ToTextContentHandler(), metadata),
+ new ParseContext());
+
+ String[] warnings = metadata.getValues(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING);
+ assertEquals(101, warnings.length);
+ assertEquals(1, Arrays.stream(warnings)
+ .filter(warning -> warning.equals("duplicate warning")).count());
+ assertTrue(Arrays.stream(warnings).anyMatch(warning -> warning.contains("suppressed")));
+ }
+
+ @Test
+ public void testReferenceArrayReportsUnavailableAndCappedWarnings() throws Exception {
+ ExGuid rootId = id(2000);
+ List<ExGuid> references = new ArrayList<>(100_001);
+ for (int i = 0; i < 100_001; i++) {
+ references.add(rootId);
+ }
+ RevisionStoreObject root = object(rootId,
+ propertySet(new PropertySpec(PropertyType.ArrayOfObjectIDs, 0x24001D5F,
+ arrayNumber(100_002))), references, Collections.emptyList());
+ RevisionStoreCell cell = new RevisionStoreCell();
+ cell.objectGroups.add(group(root));
+ RevisionManifestRootDeclare rootDeclare = new RevisionManifestRootDeclare();
+ rootDeclare.objectExGuid = rootId;
+ cell.rootDeclares.add(rootDeclare);
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.cells.add(cell);
+
+ Metadata metadata = new Metadata();
+ walk(pkg, metadata);
+ String[] warnings = metadata.getValues(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING);
+ assertTrue(Arrays.stream(warnings)
+ .anyMatch(warning -> warning.contains("declared 100002 entries")));
+ assertTrue(Arrays.stream(warnings)
+ .anyMatch(warning -> warning.contains("Capping OneNote object reference array")));
+ }
+
+ @Test
+ public void testPagesFollowSectionOrderAndDropOlderCellVersions() throws Exception {
+ ExGuid sectionRootId = id(1);
+ CellID pageOne = cell(10, 100);
+ CellID pageTwo = cell(20, 200);
+ CellID oldPageOne = cell(11, 100);
+ RevisionStoreCell pageTwoCell = cellWithText(pageTwo, "page two");
+ RevisionStoreCell oldPageOneCell = cellWithText(oldPageOne, "old page one");
+ RevisionStoreCell pageOneCell = cellWithText(pageOne, "page one");
+ RevisionStoreCell unrelatedCell = cellWithText(cell(30, 300), "unrelated");
+
+ RevisionStoreObject sectionRoot = object(sectionRootId,
+ propertySet(new PropertySpec(PropertyType.ObjectSpaceID, 0x20001D78,
+ new NoData()),
+ new PropertySpec(PropertyType.ObjectSpaceID, 0x20001D79,
+ new NoData())),
+ Collections.emptyList(), Arrays.asList(pageOne, pageTwo));
+ RevisionStoreCell section = new RevisionStoreCell();
+ section.objectGroups.add(group(sectionRoot));
+ RevisionManifestRootDeclare rootDeclare = new RevisionManifestRootDeclare();
+ rootDeclare.objectExGuid = sectionRootId;
+ section.rootDeclares.add(rootDeclare);
+ RevisionManifestRootDeclare missingRoot = new RevisionManifestRootDeclare();
+ missingRoot.objectExGuid = id(1000);
+ section.rootDeclares.add(missingRoot);
+
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.dataRootCell = section;
+ pkg.cells.addAll(Arrays.asList(pageTwoCell, oldPageOneCell, pageOneCell, unrelatedCell));
+
+ Metadata metadata = new Metadata();
+ String text = walk(pkg, metadata);
+ assertTrue(text.indexOf("page one") >= 0);
+ assertTrue(text.indexOf("page two") >= 0);
+ assertTrue(text.indexOf("page one") < text.indexOf("page two"));
+ assertFalse(text.contains("old page one"));
+ assertTrue(text.contains("unrelated"));
+ assertTrue(Arrays.stream(metadata.getValues(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING))
+ .anyMatch(warning -> warning.contains("could not be resolved")));
+ }
+
+ @Test
+ public void testUnresolvedRootsFallBackToAllObjects() throws Exception {
+ RevisionStoreCell cell = cellWithText(cell(1, 1), "fallback content");
+ cell.rootDeclares.clear();
+ RevisionManifestRootDeclare missingRoot = new RevisionManifestRootDeclare();
+ missingRoot.objectExGuid = id(999);
+ cell.rootDeclares.add(missingRoot);
+
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.cells.add(cell);
+ Metadata metadata = new Metadata();
+
+ assertTrue(walk(pkg, metadata).contains("fallback content"));
+ assertTrue(Arrays.stream(metadata.getValues(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING))
+ .anyMatch(warning -> warning.contains(id(999).toString())));
+ }
+
+ @Test
+ public void testExhaustedObjectReferenceTriggersFallbackWarning() throws Exception {
+ RevisionStoreObject root = object(id(1004), propertySet(
+ new PropertySpec(PropertyType.ObjectID, 0x20001D78, new NoData())),
+ Collections.emptyList(), Collections.emptyList());
+ RevisionStoreObject fallback = object(id(1005), propertySet(
+ new PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C003498, text("fallback after exhausted reference"))),
+ Collections.emptyList(), Collections.emptyList());
+ RevisionStoreCell cell = new RevisionStoreCell();
+ cell.objectGroups.add(group(root, fallback));
+ RevisionManifestRootDeclare rootDeclare = new RevisionManifestRootDeclare();
+ rootDeclare.objectExGuid = root.objectID;
+ cell.rootDeclares.add(rootDeclare);
+
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.cells.add(cell);
+ Metadata metadata = new Metadata();
+ String text = walk(pkg, metadata);
+
+ assertFalse(text.contains("fallback after exhausted reference"));
+ assertTrue(Arrays.stream(metadata.getValues(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING))
+ .anyMatch(warning -> warning.contains("reference slot was exhausted")));
+ }
+
+ @Test
+ public void testExhaustedObjectSpaceReferenceWarns() throws Exception {
+ ExGuid sectionRootID = id(1007);
+ RevisionStoreCell section = new RevisionStoreCell();
+ section.objectGroups.add(group(object(sectionRootID,
+ propertySet(new PropertySpec(PropertyType.ObjectSpaceID, 0x20001D78,
+ new NoData())), Collections.emptyList(), Collections.emptyList())));
+ RevisionManifestRootDeclare sectionRoot = new RevisionManifestRootDeclare();
+ sectionRoot.objectExGuid = sectionRootID;
+ section.rootDeclares.add(sectionRoot);
+ RevisionStoreCell page = cellWithText(cell(1008, 1009), "page after missing space");
+
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.dataRootCell = section;
+ pkg.cells.add(page);
+ Metadata metadata = new Metadata();
+ String text = walk(pkg, metadata);
+
+ assertTrue(text.contains("page after missing space"));
+ assertTrue(Arrays.stream(metadata.getValues(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING))
+ .anyMatch(warning -> warning.contains("object-space reference slot was exhausted")));
+ }
+
+ @Test
+ public void testMixedRootsDoNotTriggerAllObjectFallback() throws Exception {
+ RevisionStoreObject root = object(id(1001), propertySet(
+ new PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C003498, text("resolved root"))),
+ Collections.emptyList(), Collections.emptyList());
+ RevisionStoreObject unrelated = object(id(1002), propertySet(
+ new PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C003498, text("unrelated object"))),
+ Collections.emptyList(), Collections.emptyList());
+ RevisionStoreCell cell = new RevisionStoreCell();
+ cell.objectGroups.add(group(root, unrelated));
+ RevisionManifestRootDeclare resolvedRoot = new RevisionManifestRootDeclare();
+ resolvedRoot.objectExGuid = root.objectID;
+ cell.rootDeclares.add(resolvedRoot);
+ RevisionManifestRootDeclare missingRoot = new RevisionManifestRootDeclare();
+ missingRoot.objectExGuid = id(1003);
+ cell.rootDeclares.add(missingRoot);
+
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.cells.add(cell);
+ Metadata metadata = new Metadata();
+ String text = walk(pkg, metadata);
+
+ assertTrue(text.contains("resolved root"));
+ assertFalse(text.contains("unrelated object"));
+ assertTrue(Arrays.stream(metadata.getValues(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING))
+ .anyMatch(warning -> warning.contains(id(1003).toString())));
+ }
+
+ @Test
+ public void testOriginalAuthorBecomesCreator() throws Exception {
+ ExGuid authorId = id(701);
+ RevisionStoreObject root = object(id(700), propertySet(
+ new PropertySpec(PropertyType.ObjectID, 0x20001D78, new NoData())),
+ Collections.singletonList(authorId), Collections.emptyList());
+ RevisionStoreObject author = object(authorId, propertySet(
+ new PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C001D75, utf16Text("Иван Петров"))),
+ Collections.emptyList(), Collections.emptyList());
+ RevisionStoreCell cell = new RevisionStoreCell();
+ cell.objectGroups.add(group(root, author));
+ RevisionManifestRootDeclare rootDeclare = new RevisionManifestRootDeclare();
+ rootDeclare.objectExGuid = root.objectID;
+ cell.rootDeclares.add(rootDeclare);
+
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.cells.add(cell);
+ Metadata metadata = new Metadata();
+ walk(pkg, metadata);
+ assertEquals("Иван Петров", metadata.get(TikaCoreProperties.CREATOR));
+ assertEquals("Иван Петров", metadata.get(OneNoteParser.ONE_NOTE_PREFIX + "originalAuthors"));
+ }
+
+ @Test
+ public void testDualRoleAuthorRecordedForBothRoles() throws Exception {
+ ExGuid authorId = id(710);
+ // the same author object referenced as both AuthorOriginal and AuthorMostRecent
+ RevisionStoreObject root = object(id(711), propertySet(
+ new PropertySpec(PropertyType.ObjectID, 0x20001D78, new NoData()),
+ new PropertySpec(PropertyType.ObjectID, 0x20001D79, new NoData())),
+ Arrays.asList(authorId, authorId), Collections.emptyList());
+ RevisionStoreObject author = object(authorId, propertySet(
+ new PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C001D75, utf16Text("Single Author"))),
+ Collections.emptyList(), Collections.emptyList());
+ RevisionStoreCell cell = new RevisionStoreCell();
+ cell.objectGroups.add(group(root, author));
+ RevisionManifestRootDeclare rootDeclare = new RevisionManifestRootDeclare();
+ rootDeclare.objectExGuid = root.objectID;
+ cell.rootDeclares.add(rootDeclare);
+
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.cells.add(cell);
+ Metadata metadata = new Metadata();
+ walk(pkg, metadata);
+
+ assertEquals("Single Author", metadata.get(TikaCoreProperties.CREATOR));
+ assertEquals("Single Author", metadata.get(OneNoteParser.ONE_NOTE_PREFIX + "originalAuthors"));
+ assertEquals("Single Author", metadata.get(OneNoteParser.ONE_NOTE_PREFIX + "mostRecentAuthors"));
+ }
+
+ @Test
+ public void testBlobOnlyRootWithDanglingContentRootWalksAllObjects() throws Exception {
+ RevisionStoreObject blobRoot = object(id(620), propertySet(),
+ Collections.emptyList(), Collections.emptyList());
+ blobRoot.propertySet = null;
+ blobRoot.fileDataObject = fileData("blob root");
+ RevisionStoreObject textObject = object(id(621), propertySet(
+ new PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C003498, text("page body text"))),
+ Collections.emptyList(), Collections.emptyList());
+ RevisionStoreCell cell = new RevisionStoreCell();
+ cell.objectGroups.add(group(blobRoot, textObject));
+ RevisionManifestRootDeclare blobDeclare = new RevisionManifestRootDeclare();
+ blobDeclare.objectExGuid = blobRoot.objectID;
+ cell.rootDeclares.add(blobDeclare);
+ RevisionManifestRootDeclare danglingContentRoot = new RevisionManifestRootDeclare();
+ danglingContentRoot.objectExGuid = id(622);
+ cell.rootDeclares.add(danglingContentRoot);
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.cells.add(cell);
+ Metadata metadata = new Metadata();
+
+ // the blob root alone cannot reach the page body; the dangling content root must
+ // trigger the walk-everything fallback so the body is not lost
+ assertTrue(walk(pkg, metadata).contains("page body text"));
+ assertTrue(Arrays.stream(metadata.getValues(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING))
+ .anyMatch(warning -> warning.contains("walking all objects")));
+ }
+
+ @Test
+ public void testHasEmittedContentTracksTextAndEmptyWalks() throws Exception {
+ MSOneStorePackage withText = new MSOneStorePackage();
+ withText.cells.add(cellWithText(cell(70, 71), "some text"));
+ walk(withText);
+ assertTrue(withText.hasEmittedContent());
+
+ MSOneStorePackage empty = new MSOneStorePackage();
+ RevisionStoreCell danglingCell = new RevisionStoreCell();
+ RevisionManifestRootDeclare missingRoot = new RevisionManifestRootDeclare();
+ missingRoot.objectExGuid = id(72);
+ danglingCell.rootDeclares.add(missingRoot);
+ empty.cells.add(danglingCell);
+ walk(empty);
+ assertFalse(empty.hasEmittedContent());
+ }
+
+ @Test
+ public void testSanitizeResourceNameKeepsBasenameOfPathShapedNames() {
+ assertEquals("pic 1.png",
+ MSOneStorePackage.sanitizeResourceName("D:\\images\\pic 1.png"));
+ assertEquals("pic.png", MSOneStorePackage.sanitizeResourceName("C:pic.png"));
+ assertEquals("a.png", MSOneStorePackage.sanitizeResourceName("/tmp/a.png"));
+ assertEquals("plain.png", MSOneStorePackage.sanitizeResourceName("plain.png"));
+ assertEquals("", MSOneStorePackage.sanitizeResourceName(".."));
+ assertEquals("", MSOneStorePackage.sanitizeResourceName("D:\\images\\.."));
+ }
+
+ @Test
+ public void testBlobOnlyRootDoesNotFallBackToOtherObjects() throws Exception {
+ RevisionStoreObject blobRoot = object(id(610), propertySet(),
+ Collections.emptyList(), Collections.emptyList());
+ blobRoot.propertySet = null;
+ blobRoot.fileDataObject = fileData("blob root");
+ RevisionStoreObject textObject = object(id(611), propertySet(
+ new PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C003498, text("fallback content"))),
+ Collections.emptyList(), Collections.emptyList());
+ RevisionStoreCell cell = new RevisionStoreCell();
+ cell.objectGroups.add(group(blobRoot, textObject));
+ RevisionManifestRootDeclare rootDeclare = new RevisionManifestRootDeclare();
+ rootDeclare.objectExGuid = blobRoot.objectID;
+ cell.rootDeclares.add(rootDeclare);
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.cells.add(cell);
+
+ assertFalse(walk(pkg).contains("fallback content"));
+ }
+
+ @Test
+ public void testPageMarkupIsBalancedAndPinned() throws Exception {
+ CellID pageID = cell(90, 91);
+ RevisionStoreCell page = cellWithText(pageID, "page text");
+ ExGuid sectionRootID = id(92);
+ RevisionStoreCell section = new RevisionStoreCell();
+ section.objectGroups.add(group(object(sectionRootID,
+ propertySet(new PropertySpec(PropertyType.ObjectSpaceID, 0x20001D78,
+ new NoData())), Collections.emptyList(),
+ Collections.singletonList(pageID))));
+ RevisionManifestRootDeclare sectionRoot = new RevisionManifestRootDeclare();
+ sectionRoot.objectExGuid = sectionRootID;
+ section.rootDeclares.add(sectionRoot);
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.dataRootCell = section;
+ pkg.cells.add(page);
+
+ String xml = walkXml(pkg);
+ assertEquals(1, count(xml, "<div class=\"page\">"));
+ assertEquals(1, count(xml, "</div>"));
+ assertTrue(xml.contains("page text"));
+ }
+
+ @Test
+ public void testPageMarkupClosesWhenWalkThrows() throws Exception {
+ CellID pageID = cell(95, 96);
+ RevisionStoreCell page = cellWithText(pageID, "page text");
+ ExGuid sectionRootID = id(97);
+ RevisionStoreCell section = new RevisionStoreCell();
+ section.objectGroups.add(group(object(sectionRootID,
+ propertySet(new PropertySpec(PropertyType.ObjectSpaceID, 0x20001D78,
+ new NoData())), Collections.emptyList(),
+ Collections.singletonList(pageID))));
+ RevisionManifestRootDeclare sectionRoot = new RevisionManifestRootDeclare();
+ sectionRoot.objectExGuid = sectionRootID;
+ section.rootDeclares.add(sectionRoot);
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.dataRootCell = section;
+ pkg.cells.add(page);
+ Metadata metadata = new Metadata();
+ List<String> elements = new ArrayList<>();
+ DefaultHandler recordingHandler = new DefaultHandler() {
+ private final StringBuilder text = new StringBuilder();
+
+ @Override
+ public void startElement(String uri, String localName, String qName,
+ org.xml.sax.Attributes atts) {
+ elements.add(qName);
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String qName) {
+ elements.add("/" + qName);
+ }
+
+ @Override
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ text.append(ch, start, length);
+ if (text.indexOf("page text") >= 0) {
+ throw new SAXException("intentional test failure");
+ }
+ }
+ };
+ XHTMLContentHandler xhtml = new XHTMLContentHandler(recordingHandler, metadata);
+ xhtml.startDocument();
+
+ assertThrows(SAXException.class, () -> pkg.walkTree(new OneNoteTreeWalkerOptions(),
+ metadata, xhtml, new ParseContext()));
+ assertTrue(elements.contains("p"));
+ assertTrue(elements.contains("/p"));
+ assertTrue(elements.contains("div"));
+ assertTrue(elements.contains("/div"));
+ assertTrue(elements.indexOf("p") < elements.indexOf("/p"));
+ assertTrue(elements.indexOf("div") < elements.indexOf("/div"));
+ assertTrue(elements.indexOf("/p") < elements.indexOf("/div"));
+ }
+
+ @Test
+ public void testRemoveSupersededObjectsKeepsNewestVersion() throws Exception {
+ ExGuid objectID = id(950);
+ RevisionStoreObject oldObject = object(objectID, propertySet(
+ new PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C003498, text("old"))), Collections.emptyList(),
+ Collections.emptyList());
+ RevisionStoreObject newObject = object(objectID, propertySet(
+ new PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C003498, text("new"))), Collections.emptyList(),
+ Collections.emptyList());
+ List<RevisionStoreObjectGroup> groups = new ArrayList<>(Arrays.asList(
+ group(oldObject), group(newObject)));
+ Method removeSupersededObjects = MSOneStoreParser.class.getDeclaredMethod(
+ "removeSupersededObjects", List.class);
+ removeSupersededObjects.setAccessible(true);
+
+ removeSupersededObjects.invoke(new MSOneStoreParser(), groups);
+
+ assertEquals(1, groups.get(0).objects.size());
+ assertSame(newObject, groups.get(0).objects.get(0));
+ assertTrue(groups.get(1).objects.isEmpty());
+ }
+
+ @Test
+ public void testArrayCountsAndRecursionDepthAreBounded() throws Exception {
+ RevisionStoreObject hugeArrayRoot = object(id(600), propertySet(
+ new PropertySpec(PropertyType.ArrayOfObjectIDs, 0x24001D5F,
+ arrayNumber(Integer.MAX_VALUE)),
+ new PropertySpec(PropertyType.ArrayOfObjectIDs, 0x24001D5F,
+ new NoData())),
+ Collections.emptyList(), Collections.emptyList());
+ Method collectActions = MSOneStorePackage.class.getDeclaredMethod("collectActions",
+ PropertySet.class, List.class, int[].class, List.class, int[].class, List.class,
+ int.class);
+ collectActions.setAccessible(true);
+ List<Object> actions = new ArrayList<>();
+ collectActions.invoke(new MSOneStorePackage(),
+ hugeArrayRoot.propertySet.objectSpaceObjectPropSet.body,
+ Collections.emptyList(), new int[]{0}, Collections.emptyList(), new int[]{0},
+ actions, 0);
+ assertTrue(actions.isEmpty());
+ actions.clear();
+ collectActions.invoke(new MSOneStorePackage(),
+ hugeArrayRoot.propertySet.objectSpaceObjectPropSet.body,
+ Collections.singletonList(id(601)), new int[]{0}, Collections.emptyList(),
+ new int[]{0}, actions, 1000);
+ assertTrue(actions.isEmpty());
+
+ Method collectReferencedCells = MSOneStorePackage.class.getDeclaredMethod(
+ "collectReferencedCells", RevisionStoreObject.class, Map.class, Set.class,
+ List.class, int.class);
+ collectReferencedCells.setAccessible(true);
+ RevisionStoreObject referencedRoot = object(id(602), propertySet(
+ new PropertySpec(PropertyType.ObjectSpaceID, 0x20001D78, new NoData())),
+ Collections.emptyList(), Collections.singletonList(cell(603, 604)));
+ List<CellID> orderedCellIds = new ArrayList<>();
+ collectReferencedCells.invoke(new MSOneStorePackage(), referencedRoot,
+ new java.util.HashMap<>(), new java.util.HashSet<>(), orderedCellIds, 1000);
+ assertTrue(orderedCellIds.isEmpty());
+
+ PropertySet alignedSet = propertySet(
+ new PropertySpec(PropertyType.ArrayOfObjectIDs, 0x24001D5F,
+ arrayNumber(100001)),
+ new PropertySpec(PropertyType.ObjectID, 0x24001D5F, new NoData()));
+ actions.clear();
+ collectActions.invoke(new MSOneStorePackage(), alignedSet,
+ Collections.nCopies(100001, id(601)), new int[]{0}, Collections.emptyList(),
+ new int[]{0}, actions, 0);
+ assertEquals(100001, actions.size());
+ java.lang.reflect.Field childReference = actions.get(actions.size() - 1).getClass()
+ .getDeclaredField("childReference");
+ childReference.setAccessible(true);
+ assertNull(childReference.get(actions.get(actions.size() - 1)));
+
+ Method walkObject = MSOneStorePackage.class.getDeclaredMethod("walkObject",
+ RevisionStoreObject.class, Map.class, Set.class,
+ Class.forName(MSOneStorePackage.class.getName() + "$AuthorRole"),
+ OneNoteTreeWalkerOptions.class, Metadata.class, XHTMLContentHandler.class,
+ int.class);
+ walkObject.setAccessible(true);
+ // an object that emits text when walked, so the depth-capped walk's blank
+ // output proves the cap fired rather than the setup having nothing to emit
+ RevisionStoreObject textRoot = object(id(605), propertySet(
+ new PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C003498, text("depth capped text"))),
+ Collections.emptyList(), Collections.emptyList());
+ Metadata metadata = new Metadata();
+ StringWriter writer = new StringWriter();
+ XHTMLContentHandler xhtml = new XHTMLContentHandler(new ToTextContentHandler(writer), metadata);
+ xhtml.startDocument();
+ walkObject.invoke(new MSOneStorePackage(), textRoot,
+ new java.util.HashMap<>(), new java.util.HashSet<>(), null,
+ new OneNoteTreeWalkerOptions(), metadata, xhtml, 1000);
+ assertTrue(writer.toString().isBlank());
+ walkObject.invoke(new MSOneStorePackage(), textRoot,
+ new java.util.HashMap<>(), new java.util.HashSet<>(), null,
+ new OneNoteTreeWalkerOptions(), metadata, xhtml, 0);
+ xhtml.endDocument();
+ assertTrue(writer.toString().contains("depth capped text"));
+ }
+
+ @Test
+ public void testDanglingPrimaryPictureDoesNotSuppressWebPicture() throws Exception {
+ ExGuid missingPictureID = id(50);
+ ExGuid webPictureID = id(51);
+ RevisionStoreObject root = object(id(52), propertySet(
+ new PropertySpec(PropertyType.ObjectID, 0x20001C3F, new NoData()),
+ new PropertySpec(PropertyType.ObjectID, 0x200034C8, new NoData())),
+ Arrays.asList(missingPictureID, webPictureID), Collections.emptyList());
+ RevisionStoreObject webPicture = object(webPictureID, propertySet(
+ new PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C003498, text("derived picture"))),
+ Collections.emptyList(), Collections.emptyList());
+ RevisionStoreCell cell = new RevisionStoreCell();
+ cell.objectGroups.add(group(root, webPicture));
+ RevisionManifestRootDeclare rootDeclare = new RevisionManifestRootDeclare();
+ rootDeclare.objectExGuid = root.objectID;
+ cell.rootDeclares.add(rootDeclare);
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.cells.add(cell);
+
+ String text = walk(pkg);
+ assertTrue(text.contains("derived picture"), text);
+ }
+
+ @Test
+ public void testContentlessPrimaryPictureDoesNotSuppressDerivedPicture() throws Exception {
+ ExGuid pictureID = id(40);
+ ExGuid webPictureID = id(41);
+ RevisionStoreObject root = object(id(42), propertySet(
+ new PropertySpec(PropertyType.ObjectID, 0x20001C3F, new NoData()),
+ new PropertySpec(PropertyType.ObjectID, 0x200034C8, new NoData())),
+ Arrays.asList(pictureID, webPictureID), Collections.emptyList());
+ RevisionStoreObject picture = object(pictureID, propertySet(),
+ Collections.emptyList(), Collections.emptyList());
+ picture.propertySet.objectSpaceObjectPropSet.body = null;
+ RevisionStoreObject webPicture = object(webPictureID, propertySet(
+ new PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C003498, text("derived picture"))),
+ Collections.emptyList(), Collections.emptyList());
+ RevisionStoreCell cell = new RevisionStoreCell();
+ cell.objectGroups.add(group(root, picture, webPicture));
+ RevisionManifestRootDeclare rootDeclare = new RevisionManifestRootDeclare();
+ rootDeclare.objectExGuid = root.objectID;
+ cell.rootDeclares.add(rootDeclare);
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.cells.add(cell);
+
+ assertTrue(walk(pkg).contains("derived picture"));
+ }
+
+ @Test
+ public void testUsablePrimaryPictureSuppressesDerivedPicture() throws Exception {
+ ExGuid pictureID = id(80);
+ ExGuid webPictureID = id(81);
+ RevisionStoreObject root = object(id(82), propertySet(
+ new PropertySpec(PropertyType.ObjectID, 0x20001C3F, new NoData()),
+ new PropertySpec(PropertyType.ObjectID, 0x200034C8, new NoData())),
+ Arrays.asList(pictureID, webPictureID), Collections.emptyList());
+ RevisionStoreObject picture = object(pictureID, propertySet(),
+ Collections.emptyList(), Collections.emptyList());
+ picture.fileDataObject = fileData("primary image");
+ RevisionStoreObject webPicture = object(webPictureID, propertySet(
+ new PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C003498, text("derived picture"))),
+ Collections.emptyList(), Collections.emptyList());
+ RevisionStoreCell cell = new RevisionStoreCell();
+ cell.objectGroups.add(group(root, picture, webPicture));
+ RevisionManifestRootDeclare rootDeclare = new RevisionManifestRootDeclare();
+ rootDeclare.objectExGuid = root.objectID;
+ cell.rootDeclares.add(rootDeclare);
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.cells.add(cell);
+
+ assertFalse(walk(pkg).contains("derived picture"));
+ }
+
+ @Test
+ public void testNestedPropertySetsAndMissingReferencesAreTraversedSafely() throws Exception {
+ ExGuid childId = id(2);
+ PropertySet nested = propertySet(new PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C003498, text("nested text")));
+ PrtArrayOfPropertyValues array = new PrtArrayOfPropertyValues();
+ array.data = new PropertySet[]{propertySet(new PropertySpec(
+ PropertyType.FourBytesOfLengthFollowedByData, 0x1C003498, text("array text")))};
+ RevisionStoreObject root = object(id(1), propertySet(
+ new PropertySpec(PropertyType.PropertySet, 0, nested),
+ new PropertySpec(PropertyType.ArrayOfPropertyValues, 0, array),
+ new PropertySpec(PropertyType.ArrayOfObjectIDs, 0x24001D5F,
+ arrayNumber(2)),
+ new PropertySpec(PropertyType.ObjectID, 0x24001D5F, new NoData()),
+ new PropertySpec(PropertyType.ObjectSpaceID, 0x20001D78, new NoData()),
+ new PropertySpec(PropertyType.ObjectSpaceID, 0x20001D79, new NoData()),
+ new PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C001DD7, bytes((byte) 'u', (byte) 0, (byte) 1)),
+ new PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C001C22, bytes((byte) 'h', (byte) 0, (byte) 'i', (byte) 0,
+ (byte) 0, (byte) 0))),
+ Collections.singletonList(childId), Collections.singletonList(cell(50, 51)));
+ RevisionStoreObject child = object(childId, propertySet(
+ new PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C003498, text("child text"))),
+ Collections.emptyList(), Collections.emptyList());
+ RevisionStoreCell cell = new RevisionStoreCell();
+ cell.objectGroups.add(group(root, child));
+ RevisionManifestRootDeclare rootDeclare = new RevisionManifestRootDeclare();
+ rootDeclare.objectExGuid = root.objectID;
+ cell.rootDeclares.add(rootDeclare);
+
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.cells.add(cell);
+ String text = walk(pkg);
+ assertTrue(text.contains("nested text"));
+ assertTrue(text.contains("array text"));
+ assertTrue(text.contains("child text"));
+ assertTrue(text.contains("u"));
+ assertTrue(text.contains("hi"));
+ }
+
+ private static String walk(MSOneStorePackage pkg) throws Exception {
+ Metadata metadata = new Metadata();
+ return walk(pkg, metadata);
+ }
+
+ private static String walk(MSOneStorePackage pkg, Metadata metadata) throws Exception {
+ ParseContext context = new ParseContext();
+ StringWriter writer = new StringWriter();
+ XHTMLContentHandler xhtml = new XHTMLContentHandler(new ToTextContentHandler(writer), metadata);
+ xhtml.startDocument();
+ pkg.walkTree(new OneNoteTreeWalkerOptions(), metadata, xhtml, context);
+ xhtml.endDocument();
+ return writer.toString();
+ }
+
+ private static String walkXml(MSOneStorePackage pkg) throws Exception {
+ Metadata metadata = new Metadata();
+ ParseContext context = new ParseContext();
+ ToXMLContentHandler xml = new ToXMLContentHandler();
+ XHTMLContentHandler xhtml = new XHTMLContentHandler(xml, metadata);
+ xhtml.startDocument();
+ pkg.walkTree(new OneNoteTreeWalkerOptions(), metadata, xhtml, context);
+ xhtml.endDocument();
+ return xml.toString();
+ }
+
+ private static int count(String value, String needle) {
+ return value.split(java.util.regex.Pattern.quote(needle), -1).length - 1;
+ }
+
+ private static RevisionStoreCell cellWithText(CellID cellID, String value) throws Exception {
+ RevisionStoreObject object = object(id(cellID.extendGUID1.hashCode()),
+ propertySet(new PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C003498, text(value))), Collections.emptyList(), Collections.emptyList());
+ RevisionStoreCell cell = new RevisionStoreCell();
+ cell.cellID = cellID;
+ cell.objectGroups.add(group(object));
+ RevisionManifestRootDeclare rootDeclare = new RevisionManifestRootDeclare();
+ rootDeclare.objectExGuid = object.objectID;
+ cell.rootDeclares.add(rootDeclare);
+ return cell;
+ }
+
+ private static RevisionStoreObject object(ExGuid objectID, PropertySet body,
+ List<ExGuid> references, List<CellID> spaces)
+ throws Exception {
+ RevisionStoreObject object = new RevisionStoreObject();
+ object.objectID = objectID;
+ PropertySetObject propertySetObject = new PropertySetObject(null, emptyObjectData());
+ ObjectSpaceObjectPropSet propSet = new ObjectSpaceObjectPropSet();
+ propSet.body = body;
+ propertySetObject.objectSpaceObjectPropSet = propSet;
+ object.propertySet = propertySetObject;
+ if (!references.isEmpty()) {
+ object.referencedObjectID = new org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.ExGUIDArray();
+ object.referencedObjectID.content = references;
+ }
+ if (!spaces.isEmpty()) {
+ object.referencedObjectSpacesID = new org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.CellIDArray();
+ object.referencedObjectSpacesID.content = spaces;
+ }
+ return object;
+ }
+
+ private static FileDataObject fileData(String value) {
+ ObjectDataBLOB blob = new ObjectDataBLOB();
+ blob.data.content.addAll(ByteUtil.toListOfByte(value.getBytes(StandardCharsets.UTF_8)));
+ ObjectDataBLOBDataElementData blobData = new ObjectDataBLOBDataElementData();
+ blobData.objectDataBLOB = blob;
+ DataElement element = new DataElement();
+ element.dataElementType = DataElementType.ObjectDataBLOBDataElementData;
+ element.data = blobData;
+ FileDataObject fileData = new FileDataObject();
+ fileData.objectDataBLOBDataElement = element;
+ return fileData;
+ }
+
+ private static RevisionStoreObjectGroup group(RevisionStoreObject... objects) {
+ RevisionStoreObjectGroup group = new RevisionStoreObjectGroup(id(500));
+ group.objects.addAll(Arrays.asList(objects));
+ return group;
+ }
+
+ private static PropertySet propertySet(PropertySpec... specs) {
+ PropertySet set = new PropertySet();
+ set.cProperties = specs.length;
+ set.rgPrids = new PropertyID[specs.length];
+ set.rgData = new ArrayList<>();
+ for (int i = 0; i < specs.length; i++) {
+ set.rgPrids[i] = propertyID(specs[i].type, specs[i].value);
+ set.rgData.add(specs[i].property);
+ }
+ return set;
+ }
+
+ private static PropertyID propertyID(PropertyType type, int value) {
+ PropertyID id = new PropertyID();
+ id.type = type.getIntVal();
+ id.value = value;
+ return id;
+ }
+
+ private static PrtFourBytesOfLengthFollowedByData bytes(byte... value) {
+ PrtFourBytesOfLengthFollowedByData data = new PrtFourBytesOfLengthFollowedByData();
+ data.data = value;
+ data.cb = data.data.length;
+ return data;
+ }
+
+ private static PrtFourBytesOfLengthFollowedByData text(String value) {
+ PrtFourBytesOfLengthFollowedByData data = new PrtFourBytesOfLengthFollowedByData();
+ data.data = value.getBytes(StandardCharsets.US_ASCII);
+ data.cb = data.data.length;
+ return data;
+ }
+
+ private static PrtFourBytesOfLengthFollowedByData utf16Text(String value) {
+ PrtFourBytesOfLengthFollowedByData data = new PrtFourBytesOfLengthFollowedByData();
+ data.data = (value + "\u0000").getBytes(StandardCharsets.UTF_16LE);
+ data.cb = data.data.length;
+ return data;
+ }
+
+ private static ArrayNumber arrayNumber(int number) {
+ ArrayNumber array = new ArrayNumber();
+ array.number = number;
+ return array;
+ }
+
+ private static ObjectGroupObjectData emptyObjectData() {
+ ObjectGroupObjectData data = new ObjectGroupObjectData();
+ data.data.content.addAll(ByteUtil.toListOfByte(new byte[]{0, 0, 0, (byte) 0x80,
+ 0, 0, 0, 0}));
+ return data;
+ }
+
+ private static CellID cell(int first, int second) {
+ return new CellID(id(first), id(second));
+ }
+
+ private static ExGuid id(int value) {
+ return new ExGuid(value, UUID.nameUUIDFromBytes(("id-" + value).getBytes(StandardCharsets.UTF_8)));
+ }
+
+ private static final class PropertySpec {
+ private final PropertyType type;
+ private final int value;
+ private final IProperty property;
+
+ private PropertySpec(PropertyType type, int value, IProperty property) {
+ this.type = type;
+ this.value = value;
+ this.property = property;
+ }
+ }
+}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/MSOneStoreParserTest.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/MSOneStoreParserTest.java
new file mode 100644
index 0000000000..bfbb34c8b3
--- /dev/null
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/MSOneStoreParserTest.java
@@ -0,0 +1,286 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser.microsoft.onenote.fsshttpb;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.parser.microsoft.onenote.OneNoteTreeWalkerOptions;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.CellManifestCurrentRevision;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.CellManifestDataElementData;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.DataElement;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.ObjectGroupDataElementData;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.RevisionManifestDataElementData;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.RevisionManifestObjectGroupReferences;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.RevisionStoreCell;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.StorageIndexCellMapping;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.StorageIndexDataElementData;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.StorageIndexRevisionMapping;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.CellID;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.ExGuid;
+import org.apache.tika.sax.ToTextContentHandler;
+import org.apache.tika.sax.XHTMLContentHandler;
+
+public class MSOneStoreParserTest {
+
+ @Test
+ public void testStorageMappingIndexes() {
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.storageIndex = new StorageIndexDataElementData();
+ CellID cellID = cell(700);
+ StorageIndexCellMapping cellMapping = new StorageIndexCellMapping();
+ cellMapping.cellID = cellID;
+ pkg.storageIndex.storageIndexCellMappingList.add(cellMapping);
+
+ ExGuid revisionID = id(701);
+ StorageIndexRevisionMapping revisionMapping = new StorageIndexRevisionMapping();
+ revisionMapping.revisionExGuid = revisionID;
+ pkg.storageIndex.storageIndexRevisionMappingList.add(revisionMapping);
+
+ assertSame(cellMapping, pkg.findStorageIndexCellMapping(cellID));
+ assertSame(revisionMapping, pkg.findStorageIndexRevisionMapping(revisionID));
+
+ StorageIndexCellMapping addedAfterIndexing = new StorageIndexCellMapping();
+ addedAfterIndexing.cellID = cell(702);
+ pkg.storageIndex.storageIndexCellMappingList.add(addedAfterIndexing);
+ assertNull(pkg.findStorageIndexCellMapping(addedAfterIndexing.cellID));
+ }
+
+ @Test
+ public void testMissingRootsAndRevisionMappingsReturnNoCell() throws Exception {
+ CellID cellID = cell(1);
+ ExGuid cellMappingID = id(2);
+ ExGuid missingRevisionID = id(3);
+ ExGuid revisionMappingID = id(4);
+
+ MSOneStoreParser noMappingParser = parserWithEmptyIndexes();
+ assertParseWarning(noMappingParser, new MSOneStorePackage(), cellID,
+ "no storage-index cell mapping");
+
+ MSOneStoreParser noManifestParser = parserWithEmptyIndexes();
+ MSOneStorePackage noManifestPackage = packageWithCellMapping(cellID, cellMappingID);
+ assertParseWarning(noManifestParser, noManifestPackage, cellID,
+ "no current cell manifest");
+
+ MSOneStoreParser noCurrentParser = parserWithEmptyIndexes();
+ DataElement currentlessManifest = cellManifest(cellMappingID, null);
+ set(noCurrentParser, "cellManifestDataElements", Arrays.asList(currentlessManifest));
+ set(noCurrentParser, "cellManifestDataElementsById",
+ Collections.singletonMap(cellMappingID, currentlessManifest));
+ assertParseWarning(noCurrentParser,
+ packageWithCellMapping(cellID, cellMappingID), cellID,
+ "no current cell manifest");
+
+ MSOneStoreParser noRevisionMappingParser = parserWithEmptyIndexes();
+ DataElement manifest = cellManifest(cellMappingID, missingRevisionID);
+ set(noRevisionMappingParser, "cellManifestDataElements", Arrays.asList(manifest));
+ set(noRevisionMappingParser, "cellManifestDataElementsById",
+ Collections.singletonMap(cellMappingID, manifest));
+ assertParseWarning(noRevisionMappingParser,
+ packageWithCellMapping(cellID, cellMappingID), cellID, "no revision mapping");
+
+ MSOneStoreParser noRevisionManifestParser = parserWithEmptyIndexes();
+ set(noRevisionManifestParser, "cellManifestDataElements", Arrays.asList(manifest));
+ set(noRevisionManifestParser, "cellManifestDataElementsById",
+ Collections.singletonMap(cellMappingID, manifest));
+ MSOneStorePackage noRevisionManifestPackage = packageWithCellMapping(cellID,
+ cellMappingID);
+ StorageIndexRevisionMapping revisionMapping = new StorageIndexRevisionMapping();
+ revisionMapping.revisionExGuid = missingRevisionID;
+ revisionMapping.revisionMappingExGuid = revisionMappingID;
+ noRevisionManifestPackage.storageIndex.storageIndexRevisionMappingList.add(revisionMapping);
+ assertParseWarning(noRevisionManifestParser, noRevisionManifestPackage, cellID,
+ "no revision manifest");
+ }
+
+ @Test
+ public void testRevisionChainStopsCyclesAndDeduplicatesObjectGroups() throws Exception {
+ MSOneStoreParser parser = new MSOneStoreParser();
+ set(parser, "cellManifestDataElements", new java.util.ArrayList<>());
+ set(parser, "revisionManifestDataElements", new java.util.ArrayList<>());
+ set(parser, "objectGroupDataElements", new java.util.ArrayList<>());
+ set(parser, "objectGroupDataElementsById", new java.util.HashMap<>());
+ set(parser, "objectBlobElementsById", new java.util.HashMap<>());
+
+ CellID cellID = cell(20);
+ ExGuid cellMappingID = id(21);
+ ExGuid currentMappingID = id(22);
+ ExGuid oldMappingID = id(23);
+ ExGuid currentRevisionID = id(24);
+ ExGuid oldRevisionID = id(25);
+ ExGuid currentGroupID = id(26);
+ ExGuid oldGroupID = id(27);
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.storageIndex = new StorageIndexDataElementData();
+ StorageIndexCellMapping cellMapping = new StorageIndexCellMapping();
+ cellMapping.cellID = cellID;
+ cellMapping.cellMappingExGuid = cellMappingID;
+ pkg.storageIndex.storageIndexCellMappingList.add(cellMapping);
+ StorageIndexRevisionMapping currentMapping = new StorageIndexRevisionMapping();
+ currentMapping.revisionMappingExGuid = currentMappingID;
+ currentMapping.revisionExGuid = currentRevisionID;
+ StorageIndexRevisionMapping oldMapping = new StorageIndexRevisionMapping();
+ oldMapping.revisionMappingExGuid = oldMappingID;
+ oldMapping.revisionExGuid = oldRevisionID;
+ pkg.storageIndex.storageIndexRevisionMappingList.add(currentMapping);
+ pkg.storageIndex.storageIndexRevisionMappingList.add(oldMapping);
+
+ DataElement cellManifestElement = new DataElement();
+ cellManifestElement.dataElementExGuid = cellMappingID;
+ CellManifestDataElementData cellManifest = new CellManifestDataElementData();
+ cellManifest.cellManifestCurrentRevision.cellManifestCurrentRevisionExGuid = currentRevisionID;
+ cellManifestElement.data = cellManifest;
+ set(parser, "cellManifestDataElements", Arrays.asList(cellManifestElement));
+ set(parser, "cellManifestDataElementsById",
+ Collections.singletonMap(cellManifestElement.dataElementExGuid,
+ cellManifestElement));
+
+ RevisionManifestDataElementData current = revision(currentRevisionID, oldRevisionID,
+ currentGroupID, currentGroupID, id(99));
+ RevisionManifestDataElementData old = revision(oldRevisionID, currentRevisionID,
+ oldGroupID);
+ DataElement currentElement = new DataElement();
+ currentElement.dataElementExGuid = currentMappingID;
+ currentElement.data = current;
+ DataElement oldElement = new DataElement();
+ oldElement.dataElementExGuid = oldMappingID;
+ oldElement.data = old;
+ set(parser, "revisionManifestDataElements", Arrays.asList(currentElement, oldElement));
+ Map<ExGuid, DataElement> revisionManifests = new HashMap<>();
+ revisionManifests.put(currentElement.dataElementExGuid, currentElement);
+ revisionManifests.put(oldElement.dataElementExGuid, oldElement);
+ set(parser, "revisionManifestDataElementsById", revisionManifests);
+
+ DataElement currentGroup = new DataElement();
+ currentGroup.dataElementExGuid = currentGroupID;
+ currentGroup.data = new ObjectGroupDataElementData();
+ DataElement oldGroup = new DataElement();
+ oldGroup.dataElementExGuid = oldGroupID;
+ oldGroup.data = new ObjectGroupDataElementData();
+ set(parser, "objectGroupDataElements", Arrays.asList(currentGroup, oldGroup));
+ java.util.Map<ExGuid, DataElement> objectGroups = new java.util.HashMap<>();
+ objectGroups.put(currentGroupID, currentGroup);
+ objectGroups.put(oldGroupID, oldGroup);
+ set(parser, "objectGroupDataElementsById", objectGroups);
+
+ RevisionStoreCell result = parseCell(parser, cellID, pkg);
+ assertEquals(2, result.objectGroups.size());
+
+ old.revisionManifest.baseRevisionID = id(1001);
+ RevisionStoreCell missingBaseResult = parseCell(parser, cellID, pkg);
+ assertEquals(2, missingBaseResult.objectGroups.size());
+ }
+
+ private static MSOneStoreParser parserWithEmptyIndexes() throws Exception {
+ MSOneStoreParser parser = new MSOneStoreParser();
+ set(parser, "cellManifestDataElements", new java.util.ArrayList<>());
+ set(parser, "revisionManifestDataElements", new java.util.ArrayList<>());
+ set(parser, "objectGroupDataElements", new java.util.ArrayList<>());
+ set(parser, "objectGroupDataElementsById", new java.util.HashMap<>());
+ set(parser, "objectBlobElementsById", new java.util.HashMap<>());
+ return parser;
+ }
+
+ private static MSOneStorePackage packageWithCellMapping(CellID cellID, ExGuid mappingID) {
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.storageIndex = new StorageIndexDataElementData();
+ StorageIndexCellMapping mapping = new StorageIndexCellMapping();
+ mapping.cellID = cellID;
+ mapping.cellMappingExGuid = mappingID;
+ pkg.storageIndex.storageIndexCellMappingList.add(mapping);
+ return pkg;
+ }
+
+ private static DataElement cellManifest(ExGuid mappingID, ExGuid revisionID) {
+ DataElement element = new DataElement();
+ element.dataElementExGuid = mappingID;
+ CellManifestDataElementData data = new CellManifestDataElementData();
+ data.cellManifestCurrentRevision = null;
+ if (revisionID != null) {
+ data.cellManifestCurrentRevision = new CellManifestCurrentRevision();
+ data.cellManifestCurrentRevision.cellManifestCurrentRevisionExGuid = revisionID;
+ }
+ element.data = data;
+ return element;
+ }
+
+ private static void assertParseWarning(MSOneStoreParser parser, MSOneStorePackage pkg,
+ CellID cellID, String expected) throws Exception {
+ assertNull(parseCell(parser, cellID, pkg));
+ Metadata metadata = new Metadata();
+ ParseContext context = new ParseContext();
+ XHTMLContentHandler xhtml = new XHTMLContentHandler(new ToTextContentHandler(), metadata);
+ xhtml.startDocument();
+ pkg.walkTree(new OneNoteTreeWalkerOptions(), metadata, xhtml, context);
+ xhtml.endDocument();
+ assertTrue(Arrays.stream(metadata.getValues(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING))
+ .anyMatch(warning -> warning.contains(expected)), expected);
+ }
+
+ private static RevisionManifestDataElementData revision(ExGuid revisionID,
+ ExGuid baseRevisionID,
+ ExGuid... groups) {
+ RevisionManifestDataElementData data = new RevisionManifestDataElementData();
+ data.revisionManifest.revisionID = revisionID;
+ data.revisionManifest.baseRevisionID = baseRevisionID;
+ for (ExGuid group : groups) {
+ RevisionManifestObjectGroupReferences reference =
+ new RevisionManifestObjectGroupReferences();
+ reference.objectGroupExtendedGUID = group;
+ data.revisionManifestObjectGroupReferences.add(reference);
+ }
+ return data;
+ }
+
+ private static RevisionStoreCell parseCell(MSOneStoreParser parser, CellID cellID,
+ MSOneStorePackage pkg) throws Exception {
+ Method method = MSOneStoreParser.class.getDeclaredMethod("parseCell", CellID.class,
+ MSOneStorePackage.class);
+ method.setAccessible(true);
+ return (RevisionStoreCell) method.invoke(parser, cellID, pkg);
+ }
+
+ private static void set(MSOneStoreParser parser, String field, Object value) throws Exception {
+ java.lang.reflect.Field declared = MSOneStoreParser.class.getDeclaredField(field);
+ declared.setAccessible(true);
+ declared.set(parser, value);
+ }
+
+ private static CellID cell(int value) {
+ return new CellID(id(value), id(value + 1));
+ }
+
+ private static ExGuid id(int value) {
+ return new ExGuid(value, UUID.nameUUIDFromBytes(
+ ("parser-" + value).getBytes(StandardCharsets.UTF_8)));
+ }
+}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/PropertyDeserializationBoundsTest.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/PropertyDeserializationBoundsTest.java
new file mode 100644
index 0000000000..73d5ecd395
--- /dev/null
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/onenote/fsshttpb/PropertyDeserializationBoundsTest.java
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser.microsoft.onenote.fsshttpb;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.property.PrtArrayOfPropertyValues;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.property.PrtFourBytesOfLengthFollowedByData;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.PropertySet;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.PropertyID;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.PropertyType;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.space.ObjectSpaceObjectStreamHeader;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.space.ObjectSpaceObjectStreamOfContextIDs;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.space.ObjectSpaceObjectStreamOfOIDs;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.space.ObjectSpaceObjectStreamOfOSIDs;
+
+/**
+ * File-derived counts and nesting in the fsshttpb property deserializers must be bounded -
+ * a malformed count would otherwise allocate up to gigabytes and a deeply nested property
+ * set would overflow the stack, both Errors that escape the parser's Exception fallback.
+ */
+public class PropertyDeserializationBoundsTest {
+
+ @Test
+ public void testFourBytesOfLengthRejectsLengthBeyondRemainingData() {
+ byte[] bytes = concat(int32(1000), new byte[2]);
+ IOException e = assertThrows(IOException.class,
+ () -> new PrtFourBytesOfLengthFollowedByData()
+ .doDeserializeFromByteArray(bytes, 0));
+ assertTrue(e.getMessage().contains("exceeds remaining data"), e.getMessage());
+ }
+
+ @Test
+ public void testFourBytesOfLengthRejectsNegativeLength() {
+ byte[] bytes = concat(int32(0xF0000000), new byte[8]);
+ assertThrows(IOException.class,
+ () -> new PrtFourBytesOfLengthFollowedByData()
+ .doDeserializeFromByteArray(bytes, 0));
+ }
+
+ @Test
+ public void testFourBytesOfLengthAcceptsExactFit() throws IOException {
+ byte[] bytes = concat(int32(3), new byte[]{1, 2, 3});
+ PrtFourBytesOfLengthFollowedByData property = new PrtFourBytesOfLengthFollowedByData();
+ assertEquals(7, property.doDeserializeFromByteArray(bytes, 0));
+ assertEquals(3, property.data.length);
+ }
+
+ @Test
+ public void testArrayOfPropertyValuesRejectsCountBeyondRemainingData() throws IOException {
+ byte[] bytes = concat(int32(Integer.MAX_VALUE),
+ propertyId(PropertyType.PropertySet.getIntVal()));
+ IOException e = assertThrows(IOException.class,
+ () -> new PrtArrayOfPropertyValues().doDeserializeFromByteArray(bytes, 0));
+ assertTrue(e.getMessage().contains("exceeds remaining data"), e.getMessage());
+ }
+
+ @Test
+ public void testArrayOfPropertyValuesAcceptsSmallCount() throws IOException {
+ // one element: an empty PropertySet (int16 count of 0)
+ byte[] bytes = concat(int32(1), propertyId(PropertyType.PropertySet.getIntVal()),
+ new byte[]{0, 0});
+ PrtArrayOfPropertyValues array = new PrtArrayOfPropertyValues();
+ array.doDeserializeFromByteArray(bytes, 0);
+ assertEquals(1, array.data.length);
+ }
+
+ @Test
+ public void testStreamOfIdsRejectCountBeyondRemainingData() throws IOException {
+ byte[] bytes = streamHeader(0xFFFFFF);
+ assertThrows(IOException.class,
+ () -> new ObjectSpaceObjectStreamOfOIDs().doDeserializeFromByteArray(bytes, 0));
+ assertThrows(IOException.class,
+ () -> new ObjectSpaceObjectStreamOfOSIDs().doDeserializeFromByteArray(bytes, 0));
+ assertThrows(IOException.class,
+ () -> new ObjectSpaceObjectStreamOfContextIDs()
+ .doDeserializeFromByteArray(bytes, 0));
+ }
+
+ @Test
+ public void testStreamOfOidsAcceptsSmallCount() throws IOException {
+ // one CompactID, 4 bytes
+ byte[] bytes = concat(streamHeader(1), new byte[4]);
+ ObjectSpaceObjectStreamOfOIDs stream = new ObjectSpaceObjectStreamOfOIDs();
+ stream.doDeserializeFromByteArray(bytes, 0);
+ assertEquals(1, stream.body.length);
+ }
+
+ @Test
+ public void testPropertySetNestingIsDepthCapped() throws IOException {
+ byte[] bytes = nestedPropertySet(PropertySet.MAX_PROPERTY_NESTING + 50);
+ IOException e = assertThrows(IOException.class,
+ () -> new PropertySet().doDeserializeFromByteArray(bytes, 0));
+ assertTrue(e.getMessage().contains("nesting exceeds"), e.getMessage());
+ }
+
+ @Test
+ public void testPropertySetShallowNestingParses() throws IOException {
+ PropertySet propertySet = new PropertySet();
+ propertySet.doDeserializeFromByteArray(nestedPropertySet(3), 0);
+ assertEquals(1, propertySet.rgData.size());
+ }
+
+ @Test
+ public void testNestingThroughArrayOfPropertyValuesIsDepthCapped() throws IOException {
+ // alternate PropertySet -> ArrayOfPropertyValues -> PropertySet -> ...
+ byte[] bytes = new byte[]{0, 0};
+ for (int i = 0; i < PropertySet.MAX_PROPERTY_NESTING + 50; i++) {
+ bytes = concat(new byte[]{1, 0},
+ propertyId(PropertyType.ArrayOfPropertyValues.getIntVal()), int32(1),
+ propertyId(PropertyType.PropertySet.getIntVal()), bytes);
+ }
+ byte[] finalBytes = bytes;
+ IOException e = assertThrows(IOException.class,
+ () -> new PropertySet().doDeserializeFromByteArray(finalBytes, 0));
+ assertTrue(e.getMessage().contains("nesting exceeds"), e.getMessage());
+ }
+
+ private static byte[] nestedPropertySet(int depth) throws IOException {
+ // innermost: empty PropertySet; each wrapper declares one PropertySet-typed property
+ byte[] bytes = new byte[]{0, 0};
+ for (int i = 0; i < depth; i++) {
+ bytes = concat(new byte[]{1, 0},
+ propertyId(PropertyType.PropertySet.getIntVal()), bytes);
+ }
+ return bytes;
+ }
+
+ private static byte[] propertyId(int type) throws IOException {
+ PropertyID propertyID = new PropertyID();
+ propertyID.id = 1;
+ propertyID.type = type;
+ return toArray(propertyID.serializeToByteList());
+ }
+
+ private static byte[] streamHeader(int count) throws IOException {
+ ObjectSpaceObjectStreamHeader header = new ObjectSpaceObjectStreamHeader();
+ header.count = count;
+ return toArray(header.serializeToByteList());
+ }
+
+ private static byte[] int32(int value) {
+ return new byte[]{(byte) value, (byte) (value >> 8), (byte) (value >> 16),
+ (byte) (value >> 24)};
+ }
+
+ private static byte[] toArray(List<Byte> bytes) {
+ byte[] result = new byte[bytes.size()];
+ for (int i = 0; i < result.length; i++) {
+ result[i] = bytes.get(i);
+ }
+ return result;
+ }
+
+ private static byte[] concat(byte[]... parts) {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ for (byte[] part : parts) {
+ bos.writeBytes(part);
+ }
+ return bos.toByteArray();
+ }
+}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/resources/test-documents/testOneNote-fuzz1.one b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/resources/test-documents/testOneNote-fuzz1.one
new file mode 100644
index 0000000000..857ac4f7d3
Binary files /dev/null and b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/resources/test-documents/testOneNote-fuzz1.one differ
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/resources/test-documents/testOneNote-fuzz2.one b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/resources/test-documents/testOneNote-fuzz2.one
new file mode 100644
index 0000000000..a038f43984
Binary files /dev/null and b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/resources/test-documents/testOneNote-fuzz2.one differ
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/resources/test-documents/testOneNote-fuzz3.one b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/resources/test-documents/testOneNote-fuzz3.one
new file mode 100644
index 0000000000..35539efef8
Binary files /dev/null and b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/resources/test-documents/testOneNote-fuzz3.one differ
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/resources/test-documents/testOneNoteEmbeddedImage.one b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/resources/test-documents/testOneNoteEmbeddedImage.one
new file mode 100644
index 0000000000..2e2b122952
Binary files /dev/null and b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/resources/test-documents/testOneNoteEmbeddedImage.one differ