This is an automated email from the ASF dual-hosted git repository.

tballison pushed a commit to branch TIKA-4814-followup-1
in repository https://gitbox.apache.org/repos/asf/tika.git

commit 34b881ed37e1c2a99a7fffcc519370895586d19d
Author: tallison <[email protected]>
AuthorDate: Fri Aug 21 16:25:42 2026 -0400

    TIKA-4814 -- OneNote follow-ups: fall back to legacy string dump when 
parsing
      fails or yields no content, record dual-role authors, cap file-derived
      allocations in fsshttpb deserializers, cache flattened property actions
---
 CHANGES.txt                                        |  21 +--
 .../apache/tika/parser/microsoft/onenote/GUID.java |  18 ++-
 .../parser/microsoft/onenote/OneNoteDocument.java  |   3 +
 .../parser/microsoft/onenote/OneNoteParser.java    | 137 ++++++++++++-------
 .../onenote/fsshttpb/MSOneStorePackage.java        | 150 +++++++++++++--------
 .../property/PrtArrayOfPropertyValues.java         |   6 +
 .../onenote/fsshttpb/streamobj/ObjectDataBLOB.java |  19 ---
 .../streamobj/ObjectDataBLOBDataElementData.java   |  15 ---
 .../streamobj/RevisionStoreObjectGroup.java        |  12 +-
 .../space/ObjectSpaceObjectStreamOfContextIDs.java |   8 +-
 .../space/ObjectSpaceObjectStreamOfOIDs.java       |   8 +-
 .../space/ObjectSpaceObjectStreamOfOSIDs.java      |   6 +
 .../tika/parser/microsoft/onenote/GUIDTest.java    |  19 +++
 .../microsoft/onenote/OneNoteParserTest.java       |  55 ++++++--
 .../onenote/fsshttpb/MSOneStoreBlobTest.java       |  17 ++-
 .../onenote/fsshttpb/MSOneStorePackageTest.java    |  87 ++++++++++++
 16 files changed, 422 insertions(+), 159 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 5db77dc8be..390f3ff3c9 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,13 @@
 Release 4.1.0 - ???
 
+   * 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 (TIKA-4814).
+
    * RawTiffParser extracts the camera-generated JPEG previews embedded in
      TIFF-based raw images (Nikon NEF/NRW, Sony ARW/SRF/SR2, Pentax PEF/PTX,
      Adobe DNG and Canon CR2, including BigTIFF DNG containers) as thumbnail
@@ -481,15 +489,6 @@ Release 4.0.0 - 8/18/2026
 
   OTHER CHANGES
 
-   * OneNote extraction now follows document order, omits superseded page
-     revisions, sorts author metadata, extracts embedded object BLOBs, and
-     bounds malformed-input recursion and per-file-node-list property 
allocation.
-     Malformed truncated property arrays now fail rather than return partial 
data;
-     parse warnings and embedded relationship IDs are exposed in metadata 
(TIKA-4814).
-
-   * Dependency upgrades since 4.0.0-beta-1, including Jetty 12.1.12, CXF
-     4.2.3 and SolrJ 10.0.0 (TIKA-4327).
-
    * Release artifacts are now channel-specific. Maven Central gets slim
      per-module jars (plus pom, sources and javadoc); the Apache dist area
      gets runnable zip distributions (tika-app, tika-server-standard,
@@ -559,6 +558,10 @@ Release 4.0.0 - 8/18/2026
 
    * Fix concurrency bug in TikaToXMP (TIKA-4393).
 
+   * Dependency upgrades throughout the 4.0.0 line, including Jetty 11 ->
+     12.1.12, CXF 4.0 -> 4.2.3 and SolrJ 8.11.4 -> 10.0.0, plus routine
+     library updates (TIKA-4327).
+
 
 Release 4.0.0-beta-1 - 6/29/2026
 
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 fe63cc9bf4..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
@@ -56,8 +56,8 @@ public class GUID implements Comparable<GUID> {
             throw new TikaException("Invalid GUID string");
         }
         for (int i = 0; i < hex.length(); i += 2) {
-            int high = Character.digit(hex.charAt(i), 16);
-            int low = Character.digit(hex.charAt(i + 1), 16);
+            int high = asciiHexDigit(hex.charAt(i));
+            int low = asciiHexDigit(hex.charAt(i + 1));
             if (high < 0 || low < 0) {
                 throw new TikaException("Invalid GUID string");
             }
@@ -66,6 +66,20 @@ public class GUID implements Comparable<GUID> {
         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 d3c4ec2515..85dc2f2e78 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
@@ -128,39 +128,56 @@ public class OneNoteParser implements Parser {
                 metadata.set(OneNote.RGB_PLACEHOLDER,
                         "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,
-                            sortedValues(oneNoteTreeWalker.getAuthors()));
-                }
-                if (!oneNoteTreeWalker.getMostRecentAuthors().isEmpty()) {
-                    metadata.set(OneNote.MOST_RECENT_AUTHORS,
-                            
sortedValues(oneNoteTreeWalker.getMostRecentAuthors()));
-                }
-                if (!oneNoteTreeWalker.getOriginalAuthors().isEmpty()) {
-                    metadata.set(OneNote.ORIGINAL_AUTHORS,
-                            
sortedValues(oneNoteTreeWalker.getOriginalAuthors()));
-                }
-                if (!Instant.MAX.equals(
-                        
Instant.ofEpochMilli(oneNoteTreeWalker.getCreationTimestamp()))) {
-                    metadata.set(OneNote.CREATION_TIMESTAMP,
-                            
String.valueOf(oneNoteTreeWalker.getCreationTimestamp()));
-                }
-                if 
(!Instant.MIN.equals(oneNoteTreeWalker.getLastModifiedTimestamp())) {
-                    metadata.set(OneNote.LAST_MODIFIED_TIMESTAMP, 
String.valueOf(
-                            
oneNoteTreeWalker.getLastModifiedTimestamp().toEpochMilli()));
+                        if (!oneNoteTreeWalker.getAuthors().isEmpty()) {
+                            metadata.set(TikaCoreProperties.CREATOR,
+                                    
sortedValues(oneNoteTreeWalker.getAuthors()));
+                        }
+                        if 
(!oneNoteTreeWalker.getMostRecentAuthors().isEmpty()) {
+                            metadata.set(OneNote.MOST_RECENT_AUTHORS,
+                                    
sortedValues(oneNoteTreeWalker.getMostRecentAuthors()));
+                        }
+                        if (!oneNoteTreeWalker.getOriginalAuthors().isEmpty()) 
{
+                            metadata.set(OneNote.ORIGINAL_AUTHORS,
+                                    
sortedValues(oneNoteTreeWalker.getOriginalAuthors()));
+                        }
+                        if (!Instant.MAX.equals(
+                                
Instant.ofEpochMilli(oneNoteTreeWalker.getCreationTimestamp()))) {
+                            metadata.set(OneNote.CREATION_TIMESTAMP,
+                                    
String.valueOf(oneNoteTreeWalker.getCreationTimestamp()));
+                        }
+                        if 
(!Instant.MIN.equals(oneNoteTreeWalker.getLastModifiedTimestamp())) {
+                            metadata.set(OneNote.LAST_MODIFIED_TIMESTAMP, 
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.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();
                     byte[] bytes = Files.readAllBytes(tis.getPath());
@@ -168,26 +185,23 @@ public class OneNoteParser implements Parser {
                     
alternatePackageOneStoreFile.doDeserializeFromByteArray(bytes, 0);
 
                     MSOneStoreParser onenoteParser = new MSOneStoreParser();
-                    MSOneStorePackage pkg =
-                            
onenoteParser.parse(alternatePackageOneStoreFile.dataElementPackage);
+                    pkg = 
onenoteParser.parse(alternatePackageOneStoreFile.dataElementPackage);
 
                     pkg.walkTree(options, metadata, xhtml, context);
                 } catch (Exception e) {
-                    WriteLimitReachedException.throwIfWriteLimitReached(e);
-                    if (e instanceof EmbeddedLimitReachedException) {
-                        throw (EmbeddedLimitReachedException) e;
-                    }
-                    String failure = e.getMessage() == null ? 
e.getClass().getSimpleName() :
-                            e.getMessage();
-                    LOG.warn("OneNote FSSHTTPB parse failed; falling back to 
legacy text dump: {}",
-                            failure);
-                    LOG.debug("OneNote FSSHTTPB parse failure", e);
-                    
metadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING,
+                    rethrowIfLimitReached(e);
+                    legacyFallbackDump(
                             "OneNote FSSHTTPB parse failed; falling back to 
legacy text dump: " +
-                                    failure);
-                    OneNoteLegacyDumpStrings dumpStrings =
-                            new 
OneNoteLegacyDumpStrings(oneNoteDirectFileResource, xhtml);
-                    dumpStrings.dump();
+                                    failureMessage(e), e, metadata, xhtml,
+                            oneNoteDirectFileResource);
+                    pkg = null;
+                }
+                if (pkg != null && !pkg.hasEmittedContent()) {
+                    // the walk completed but every page dangled - without 
this a degraded
+                    // file would yield empty output where the dump still 
finds its text
+                    legacyFallbackDump("OneNote FSSHTTPB parse produced no 
content; " +
+                                    "falling back to legacy text dump", null, 
metadata, xhtml,
+                            oneNoteDirectFileResource);
                 }
             } else {
                 throw new TikaException("Invalid OneStore document - could not 
parse headers");
@@ -204,6 +218,29 @@ public class OneNoteParser implements Parser {
         return sorted;
     }
 
+    private static void rethrowIfLimitReached(Exception e) throws 
TikaException, SAXException {
+        WriteLimitReachedException.throwIfWriteLimitReached(e);
+        if (e instanceof EmbeddedLimitReachedException) {
+            throw (EmbeddedLimitReachedException) e;
+        }
+    }
+
+    private static String failureMessage(Exception e) {
+        return e.getMessage() == null ? e.getClass().getSimpleName() : 
e.getMessage();
+    }
+
+    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>
@@ -249,9 +286,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/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 b8c9bd0b31..cc015d5239 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
@@ -28,6 +28,7 @@ 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;
@@ -142,6 +143,11 @@ public class MSOneStorePackage {
     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 =
@@ -305,8 +311,7 @@ public class MSOneStorePackage {
             }
         }
         for (RevisionStoreCell cell : remainingCells.values()) {
-            if (cell.cellID == null || cell.cellID.extendGUID2 == null ||
-                    !coveredObjectSpaces.contains(cell.cellID.extendGUID2)) {
+            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);
             }
@@ -350,7 +355,7 @@ public class MSOneStorePackage {
         if (object.objectID != null && !visited.add(object.objectID)) {
             return;
         }
-        List<PropertyAction> actions = collectObjectActions(object, depth);
+        List<PropertyAction> actions = collectObjectActions(object);
         for (PropertyAction action : actions) {
             if (action.spaceReference != null) {
                 out.add(action.spaceReference);
@@ -373,19 +378,26 @@ public class MSOneStorePackage {
         // 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.
-        boolean resolvedRoot = false;
+        // 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 {
-                resolvedRoot = true;
+                if (rootObject.propertySet != null) {
+                    resolvedContentRoot = true;
+                }
                 walkObject(rootObject, objectsById, visited, AuthorRole.NONE, 
options,
                         metadata, xhtml, 0);
             }
         }
-        if (!resolvedRoot) {
+        if (cell.rootDeclares.isEmpty() || (unresolvedRoot && 
!resolvedContentRoot)) {
             if (cell.rootDeclares.isEmpty()) {
                 recordParseWarning("OneNote cell has no declared root objects; 
walking all objects");
             } else {
@@ -481,11 +493,15 @@ public class MSOneStorePackage {
             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, depth) : Collections.emptyList();
+                collectObjectActions(object) : Collections.emptyList();
         EmbeddedResourceInfo resourceInfo = embeddedResourceInfo(actions);
         if (resourceInfo == null) {
             resourceInfo = inheritedResourceInfo;
@@ -552,7 +568,7 @@ public class MSOneStorePackage {
         if (object.propertySet == null || 
object.propertySet.objectSpaceObjectPropSet == null) {
             return false;
         }
-        for (PropertyAction action : collectObjectActions(object, depth)) {
+        for (PropertyAction action : collectObjectActions(object)) {
             if (action.isChildReference && action.childReference != null &&
                     hasUsablePicture(action.childReference, objectsById, 
visited, depth + 1)) {
                 return true;
@@ -572,7 +588,7 @@ public class MSOneStorePackage {
                     candidate.propertySet.objectSpaceObjectPropSet == null) {
                 continue;
             }
-            List<PropertyAction> candidateActions = 
collectObjectActions(candidate, depth);
+            List<PropertyAction> candidateActions = 
collectObjectActions(candidate);
             for (PropertyAction action : candidateActions) {
                 if (action.isChildReference && 
target.objectID.equals(action.childReference) &&
                         (action.oneNotePropertyEnum == 
OneNotePropertyEnum.PictureContainer ||
@@ -643,22 +659,24 @@ public class MSOneStorePackage {
     }
 
     /**
-     * Flattens the properties of an object, in order, into a list of actions.
+     * 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) {
-        return collectObjectActions(object, 0);
-    }
-
-    private List<PropertyAction> collectObjectActions(RevisionStoreObject 
object, int depth) {
+        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;
-        List<PropertyAction> actions = new ArrayList<>();
+        actions = new ArrayList<>();
         collectActions(object.propertySet.objectSpaceObjectPropSet.body, 
referencedObjects,
-                new int[]{0}, referencedSpaces, new int[]{0}, actions, depth);
+                new int[]{0}, referencedSpaces, new int[]{0}, actions, 0);
+        objectActionsCache.put(object, actions);
         return actions;
     }
 
@@ -862,17 +880,8 @@ public class MSOneStorePackage {
             }
             metadata.set(TikaCoreProperties.MODIFIED, 
String.valueOf(lastModified));
         } else if (oneNotePropertyEnum == OneNotePropertyEnum.Author) {
-            String author = decodeOneNoteText(
-                    ((PrtFourBytesOfLengthFollowedByData) property).data);
-            if (authorRole == AuthorRole.MOST_RECENT) {
-                mostRecentAuthors.add(author);
-            } else if (authorRole == AuthorRole.ORIGINAL) {
-                originalAuthors.add(author);
-                // the original authors are the creators of the content
-                authors.add(author);
-            } else {
-                authors.add(author);
-            }
+            recordAuthor(decodeOneNoteText(
+                    ((PrtFourBytesOfLengthFollowedByData) property).data), 
authorRole);
         } else if (propertyType == 
PropertyType.FourBytesOfLengthFollowedByData) {
             boolean isBinary = propertyIsBinary(oneNotePropertyEnum);
             PrtFourBytesOfLengthFollowedByData dataProperty =
@@ -880,28 +889,15 @@ public class MSOneStorePackage {
             if ((dataProperty.data.length & 1) == 0 &&
                     oneNotePropertyEnum != 
OneNotePropertyEnum.TextExtendedAscii && !isBinary) {
                 if 
(options.getUtf16PropertiesToPrint().contains(oneNotePropertyEnum)) {
-                    xhtml.startElement(P);
-                    try {
-                        xhtml.characters(new String(dataProperty.data, 
StandardCharsets.UTF_16LE));
-                    } finally {
-                        xhtml.endElement(P);
-                    }
+                    emitParagraph(xhtml, new String(dataProperty.data,
+                            StandardCharsets.UTF_16LE));
                 }
             } else if (oneNotePropertyEnum == 
OneNotePropertyEnum.TextExtendedAscii) {
-                xhtml.startElement(P);
-                try {
-                    xhtml.characters(new String(dataProperty.data, 
StandardCharsets.US_ASCII));
-                } finally {
-                    xhtml.endElement(P);
-                }
+                emitParagraph(xhtml, new String(dataProperty.data, 
StandardCharsets.US_ASCII));
             } else if (!isBinary) {
                 if 
(options.getUtf16PropertiesToPrint().contains(oneNotePropertyEnum)) {
-                    xhtml.startElement(P);
-                    try {
-                        xhtml.characters(new String(dataProperty.data, 
StandardCharsets.UTF_16LE));
-                    } finally {
-                        xhtml.endElement(P);
-                    }
+                    emitParagraph(xhtml, new String(dataProperty.data,
+                            StandardCharsets.UTF_16LE));
                 }
             } else {
                 if (oneNotePropertyEnum == 
OneNotePropertyEnum.RichEditTextUnicode) {
@@ -920,6 +916,35 @@ public class MSOneStorePackage {
         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) {
+        if (role == AuthorRole.NONE || 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 
&&
@@ -939,10 +964,11 @@ public class MSOneStorePackage {
         return null;
     }
 
-    private String sanitizeResourceName(String name) {
+    static String sanitizeResourceName(String name) {
         name = name.replace('\\', '/');
+        // drop a drive-letter prefix (C:...) so the basename below survives
         if (name.length() > 1 && name.charAt(1) == ':') {
-            return "";
+            name = name.substring(2);
         }
         int slash = name.lastIndexOf('/');
         name = slash >= 0 ? name.substring(slash + 1) : name;
@@ -959,6 +985,7 @@ public class MSOneStorePackage {
         if (data == null || data.length == 0 || embeddedDocumentExtractor == 
null) {
             return;
         }
+        contentEmitted = true;
         Metadata embeddedMetadata = Metadata.newInstance(this.parseContext);
         embeddedMetadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE,
                 resourceInfo == null ? 
TikaCoreProperties.EmbeddedResourceType.ATTACHMENT.toString() :
@@ -1011,16 +1038,33 @@ public class MSOneStorePackage {
             } finally {
                 xhtml.endElement("a");
             }
+            contentEmitted = true;
         } else {
-            xhtml.startElement(P);
-            try {
-                xhtml.characters(txt);
-            } finally {
-                xhtml.endElement(P);
-            }
+            emitParagraph(xhtml, txt);
         }
     }
 
+    private void emitParagraph(XHTMLContentHandler xhtml, String text) throws 
SAXException {
+        xhtml.startElement(P);
+        try {
+            xhtml.characters(text);
+        } finally {
+            xhtml.endElement(P);
+        }
+        if (!text.isEmpty()) {
+            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 {
         if (property instanceof FourBytesOfData) {
             FourBytesOfData fourBytesOfDataProp = (FourBytesOfData) property;
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..d39506b553 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
@@ -47,6 +47,12 @@ public class PrtArrayOfPropertyValues implements IProperty {
         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();
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
index e92d04f6e9..2873e1d013 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/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
@@ -30,14 +30,8 @@ import 
org.apache.tika.parser.microsoft.onenote.fsshttpb.util.ByteUtil;
  * e.g. an embedded image or file. See MS-FSSHTTPB section 2.2.1.12.8.
  */
 public class ObjectDataBLOB extends StreamObject {
-    /**
-     * A binary item that holds the opaque binary data.
-     */
     public BinaryItem data;
 
-    /**
-     * Initializes a new instance of the ObjectDataBLOB class.
-     */
     public ObjectDataBLOB() {
         super(StreamObjectTypeHeaderStart.ObjectDataBLOB);
         this.data = new BinaryItem();
@@ -53,13 +47,6 @@ public class ObjectDataBLOB extends StreamObject {
         return ByteUtil.toByteArray(this.data.content);
     }
 
-    /**
-     * Used to de-serialize the element.
-     *
-     * @param byteArray     A Byte array
-     * @param currentIndex  Start position
-     * @param lengthOfItems The length of the items
-     */
     @Override
     protected void deserializeItemsFromByteArray(byte[] byteArray, 
AtomicInteger currentIndex,
                                                  int lengthOfItems)
@@ -75,12 +62,6 @@ public class ObjectDataBLOB extends StreamObject {
         currentIndex.set(index.get());
     }
 
-    /**
-     * Used to convert the element into a byte List
-     *
-     * @param byteList A Byte list
-     * @return The number of elements actually contained in the list
-     */
     @Override
     protected int serializeItemsToByteList(List<Byte> byteList) throws 
IOException {
         int startPoint = byteList.size();
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
index 17828cf0d7..ff28e9d096 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/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
@@ -29,20 +29,10 @@ import org.apache.tika.exception.TikaException;
 public class ObjectDataBLOBDataElementData extends DataElementData {
     public ObjectDataBLOB objectDataBLOB;
 
-    /**
-     * Initializes a new instance of the ObjectDataBLOBDataElementData class.
-     */
     public ObjectDataBLOBDataElementData() {
         this.objectDataBLOB = new ObjectDataBLOB();
     }
 
-    /**
-     * Deserializes this element from the supplied byte array.
-     *
-     * @param byteArray  A byte array
-     * @param startIndex Start position
-     * @return The number of bytes consumed
-     */
     @Override
     public int deserializeDataElementDataFromByteArray(byte[] byteArray, int 
startIndex)
             throws TikaException, IOException {
@@ -51,11 +41,6 @@ public class ObjectDataBLOBDataElementData extends 
DataElementData {
         return index.get() - startIndex;
     }
 
-    /**
-     * Used to convert the element into a byte List.
-     *
-     * @return The Byte list
-     */
     @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/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 1381fdbdbe..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;
 
@@ -42,11 +42,16 @@ public class RevisionStoreObjectGroup {
                                                           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 =
@@ -108,6 +113,9 @@ public class RevisionStoreObjectGroup {
         } 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 =
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
index 6e7f3b70d1..0f21f861db 100644
--- 
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
@@ -16,6 +16,7 @@
  */
 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;
@@ -26,6 +27,24 @@ 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}"
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 90e1ada59e..891757d9f0 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
@@ -42,7 +42,6 @@ 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;
@@ -57,7 +56,9 @@ public class OneNoteParserTest extends TikaTest {
     //test recursive parser wrapper for image files
 
     @Test
-    public void testFuzzerRegressionInputsFailWithTikaExceptions() throws 
Exception {
+    public void testFuzzerRegressionInputsFallBackToLegacyDump() throws 
Exception {
+        // structural failures no longer abort the parse - the legacy string 
dump runs and
+        // the original failure is pinned in the parse-warning metadata
         String[][] resources = {
                 {"testOneNote-fuzz1.one", "Missing dependent revision"},
                 {"testOneNote-fuzz2.one", "unified property count"},
@@ -68,15 +69,40 @@ public class OneNoteParserTest extends TikaTest {
             assertNotNull(input, resource[0]);
             try (InputStream stream = input;
                  TikaInputStream tis = TikaInputStream.get(stream)) {
-                TikaException exception = assertThrows(TikaException.class,
-                        () -> new OneNoteParser().parse(tis, new 
ToTextContentHandler(),
-                                new Metadata(), new ParseContext()), 
resource[0]);
-                assertTrue(exception.getMessage().contains(resource[1]),
-                        exception.getMessage());
+                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 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.
      */
@@ -293,6 +319,16 @@ public class OneNoteParserTest extends TikaTest {
         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 {
@@ -351,7 +387,10 @@ public class OneNoteParserTest extends TikaTest {
         // older page version snapshots are not reported
         assertEquals(Arrays.asList("Chang Du", "Du Chang"),
                 Arrays.asList(metadata.getValues(TikaCoreProperties.CREATOR)));
-        assertEquals(1, metadata.getValues(ONE_NOTE_PREFIX + 
"mostRecentAuthors").length);
+        // 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"))));
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
index 38b5fc9b1b..b7d7508f8b 100644
--- 
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
@@ -18,6 +18,7 @@ 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;
@@ -213,8 +214,7 @@ public class MSOneStoreBlobTest {
         assertNull(blob.getData());
         blob.data = new BinaryItem();
         blob.data.content.add((byte) 1);
-        // Exercise serialization of a non-null BLOB; the round-trip assertion 
is above.
-        blob.serializeToByteList();
+        assertFalse(blob.serializeToByteList().isEmpty());
 
         ObjectDataBLOB validBlob = new ObjectDataBLOB();
         validBlob.data.content.add((byte) 1);
@@ -263,6 +263,19 @@ public class MSOneStoreBlobTest {
         });
     }
 
+    @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();
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
index 4de6e92c12..720fed1ca4 100644
--- 
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
@@ -285,6 +285,91 @@ public class MSOneStorePackageTest {
         assertEquals("Иван Петров", metadata.get(OneNote.ORIGINAL_AUTHORS));
     }
 
+    @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(OneNote.ORIGINAL_AUTHORS));
+        assertEquals("Single Author", 
metadata.get(OneNote.MOST_RECENT_AUTHORS));
+    }
+
+    @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(),
@@ -476,6 +561,8 @@ public class MSOneStorePackageTest {
                 new java.util.HashMap<>(), new java.util.HashSet<>(), null,
                 new OneNoteTreeWalkerOptions(), metadata, xhtml, 1000);
         xhtml.endDocument();
+        // at the depth limit the walk must stop before emitting anything
+        assertTrue(writer.toString().isBlank());
     }
 
     @Test

Reply via email to