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

tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git


The following commit(s) were added to refs/heads/main by this push:
     new 9c2a853f94 [TIKA-5055] Improve onenote performance (#3565)
9c2a853f94 is described below

commit 9c2a853f94ba3e439c1eb4b11e473fa65d1f5dc1
Author: Henry Lindeman <[email protected]>
AuthorDate: Fri Nov 6 16:42:41 2026 -0800

    [TIKA-5055] Improve onenote performance (#3565)
    
    * Improve onenote parsing performance
    
    Optimization      | Section-1 Time Saved | Section-2 Time Saved
    ---|---|---
    BitReader.getBytes shifts instead of BitSet | 0.78 ms | 1.17 ms
    BinaryItem stores byte[], not List<Byte>    | 0.09 ms | 5.33 ms
    GUID→UUID cache                             | 0.08 ms | 0.12 ms
    
    Generated-by: Glean Tau <various models>
    Signed-off-by: Henry Lindeman <[email protected]>
    
    * AI Review comments
    
    Generated-by: Glean Tau <various models>
    Signed-off-by: Henry Lindeman <[email protected]>
    
    ---------
    
    Signed-off-by: Henry Lindeman <[email protected]>
---
 CHANGES.txt                                        |   6 +
 .../onenote/fsshttpb/MSOneStorePackage.java        |  14 ++-
 .../onenote/fsshttpb/streamobj/FileDataObject.java |  30 +++--
 .../onenote/fsshttpb/streamobj/JCIDObject.java     |   3 +-
 .../onenote/fsshttpb/streamobj/LeafNodeObject.java |   6 +-
 .../onenote/fsshttpb/streamobj/ObjectDataBLOB.java |  15 ++-
 .../fsshttpb/streamobj/PropertySetObject.java      |   4 +-
 .../streamobj/RevisionStoreObjectGroup.java        |   3 +-
 .../fsshttpb/streamobj/basic/BinaryItem.java       | 105 ++++++++++++++---
 .../fsshttpb/streamobj/basic/HeaderCell.java       |   4 +-
 .../microsoft/onenote/fsshttpb/util/BitReader.java |  85 +++++++++-----
 .../onenote/fsshttpb/MSOneStoreBlobTest.java       |  16 +--
 .../onenote/fsshttpb/MSOneStorePackageTest.java    |   6 +-
 .../fsshttpb/streamobj/basic/BinaryItemTest.java   | 129 +++++++++++++++++++++
 .../onenote/fsshttpb/util/BitReaderTest.java       | 118 +++++++++++++++++++
 15 files changed, 453 insertions(+), 91 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 6b61b28444..def32a6c9c 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -611,6 +611,12 @@ Release 4.2.0 - 10/25/2026
    * OneNote: table and list structure in FSSHTTPB files (TIKA-4943); section 
and page
      GUID metadata (TIKA-4944).
 
+   * OneNote FSSHTTPB parsing is ~6% faster with ~20% fewer GCs: binary items 
are read
+     in place instead of copied, bit reads assemble whole bytes, and GUIDs are 
cached.
+     Compat: BinaryItem.content (a List<Byte>) is replaced by 
getContent()/setContent()
+     on byte[]; a binary item whose length decodes as 2^63 or more now fails 
the
+     FSSHTTPB parse (TIKA-5055).
+
    * Detection: *.rs is text/x-rust (TIKA-4977); a MIME email nesting
      multipart/related is message/rfc822, not MHTML (TIKA-4973); XML with an 
element
      like <htmlPath> is no longer HTML (TIKA-4962); XUL is detected by content 
and
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 0a02f61e93..c77156dd3a 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
@@ -64,6 +64,7 @@ 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.FileDataObject;
 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;
@@ -614,7 +615,7 @@ public class MSOneStorePackage {
         }
         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);
+            handleEmbedded(object.fileDataObject, xhtml, resourceInfo, 
object.objectID);
         }
         if (object.propertySet == null ||
                 object.propertySet.objectSpaceObjectPropSet == null) {
@@ -1115,8 +1116,7 @@ public class MSOneStorePackage {
         if (object == null) {
             return false;
         }
-        byte[] data = object.fileDataObject == null ? null : 
object.fileDataObject.getData();
-        if (data != null && data.length > 0) {
+        if (object.fileDataObject != null && object.fileDataObject.hasData()) {
             return true;
         }
         if (object.propertySet == null || 
object.propertySet.objectSpaceObjectPropSet == null) {
@@ -1691,10 +1691,10 @@ public class MSOneStorePackage {
      * 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,
+    private void handleEmbedded(FileDataObject fileData, XHTMLContentHandler 
xhtml,
                                 EmbeddedResourceInfo resourceInfo, ExGuid 
objectID)
             throws SAXException, IOException {
-        if (data == null || data.length == 0 || embeddedDocumentExtractor == 
null) {
+        if (!fileData.hasData() || embeddedDocumentExtractor == null) {
             return;
         }
         Metadata embeddedMetadata = Metadata.newInstance(this.parseContext);
@@ -1716,7 +1716,9 @@ public class MSOneStorePackage {
         }
         xhtml.startElement("div", attributes);
         xhtml.endElement("div");
-        try (TikaInputStream tis = TikaInputStream.get(data)) {
+        // A byte-array stream, not one re-opened over the parsed buffer: 
parsers that need a
+        // seekable channel (zip containers, PDF) get the array zero-copy 
instead of a temp file.
+        try (TikaInputStream tis = TikaInputStream.get(fileData.getData())) {
             if 
(embeddedDocumentExtractor.shouldParseEmbedded(embeddedMetadata, parseContext)) 
{
                 // only counts as content when the extractor accepts it, so a 
declined
                 // embedded object in an otherwise-empty file still triggers 
the fallback
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 d3a70780a9..e7a33c20b8 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
@@ -25,17 +25,33 @@ public class FileDataObject {
     public DataElement objectDataBLOBDataElement;
 
     /**
-     * @return the opaque binary data of this file data object, or null if it 
could not be
-     * resolved.
+     * @return a copy of the opaque binary data of this file data object, or 
null if it could
+     * not be resolved.
      */
     public byte[] getData() {
+        ObjectDataBLOB blob = getBlob();
+        return blob == null ? null : blob.getData();
+    }
+
+    /**
+     * @return the length of the binary data, 0 if it could not be resolved
+     */
+    public int getDataLength() {
+        ObjectDataBLOB blob = getBlob();
+        return blob == null ? 0 : blob.getDataLength();
+    }
+
+    /**
+     * @return whether the file data resolves to non-empty binary data, 
without copying it.
+     */
+    public boolean hasData() {
+        return getDataLength() > 0;
+    }
+
+    private ObjectDataBLOB getBlob() {
         if (objectDataBLOBDataElement != null &&
                 objectDataBLOBDataElement.data instanceof 
ObjectDataBLOBDataElementData) {
-            ObjectDataBLOBDataElementData blobData =
-                    (ObjectDataBLOBDataElementData) 
objectDataBLOBDataElement.data;
-            if (blobData.objectDataBLOB != null) {
-                return blobData.objectDataBLOB.getData();
-            }
+            return ((ObjectDataBLOBDataElementData) 
objectDataBLOBDataElement.data).objectDataBLOB;
         }
         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/JCIDObject.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/JCIDObject.java
index 465a264205..28b5b605a4 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/JCIDObject.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/JCIDObject.java
@@ -19,7 +19,6 @@ package 
org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj;
 import java.io.IOException;
 
 import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.JCID;
-import org.apache.tika.parser.microsoft.onenote.fsshttpb.util.ByteUtil;
 
 
 /**
@@ -39,6 +38,6 @@ public class JCIDObject {
                       ObjectGroupObjectData objectData) throws IOException {
         this.objectDeclaration = objectDeclaration;
         this.jcid = new JCID();
-        
this.jcid.doDeserializeFromByteArray(ByteUtil.toByteArray(objectData.data.content),
 0);
+        this.jcid.doDeserializeFromByteArray(objectData.data.getContent(), 0);
     }
 }
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/LeafNodeObject.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/LeafNodeObject.java
index 3630dfc741..98fe156c5b 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/LeafNodeObject.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/LeafNodeObject.java
@@ -128,7 +128,7 @@ public class LeafNodeObject extends NodeObject {
             AtomicReference<IntermediateNodeObject> rootNode = new 
AtomicReference<>();
 
             AtomicInteger index = new AtomicInteger(0);
-            if 
(StreamObject.tryGetCurrent(ByteUtil.toByteArray(dataObj.data.content), index, 
node,
+            if (StreamObject.tryGetCurrent(dataObj.data.getContent(), index, 
node,
                     LeafNodeObject.class)) {
                 if (dataObj.objectExGUIDArray == null) {
                     throw new TikaException(
@@ -146,7 +146,7 @@ public class LeafNodeObject extends NodeObject {
                     BinaryItem data = dataNodeData.data;
 
                     node.get().dataNodeObjectData =
-                            new 
DataNodeObjectData(ByteUtil.toByteArray(data.content), 0,
+                            new DataNodeObjectData(data.getContent(), 0,
                                     (int) data.length.getDecodedValue());
                     node.get().dataNodeObjectData.exGuid = 
dataObj.objectExGUIDArray.content.get(0);
                     node.get().intermediateNodeObjectList = null;
@@ -164,7 +164,7 @@ public class LeafNodeObject extends NodeObject {
                                         intermediateData, extGuid));
                     }
                 }
-            } else if 
(StreamObject.tryGetCurrent(ByteUtil.toByteArray(dataObj.data.content), index,
+            } else if (StreamObject.tryGetCurrent(dataObj.data.getContent(), 
index,
                     rootNode, IntermediateNodeObject.class)) {
                 // In Sub chunking for larger than 1MB zip file, MOSS2010 
could return IntermediateNodeObject.
                 // For easy further process, the rootNode will be replaced by 
intermediate node instead.
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 2873e1d013..2b5e359492 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
@@ -23,7 +23,6 @@ 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,
@@ -38,13 +37,17 @@ public class ObjectDataBLOB extends StreamObject {
     }
 
     /**
-     * @return the opaque binary data as a byte array, or null if not present.
+     * @return a copy of the opaque binary data, or null if not present.
      */
     public byte[] getData() {
-        if (this.data == null || this.data.content == null) {
-            return null;
-        }
-        return ByteUtil.toByteArray(this.data.content);
+        return this.data == null ? null : this.data.copyContent();
+    }
+
+    /**
+     * @return the length of the binary data, 0 if not present
+     */
+    public int getDataLength() {
+        return this.data == null ? 0 : this.data.getContentLength();
     }
 
     @Override
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/PropertySetObject.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/PropertySetObject.java
index 89843a903c..85e883548a 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/PropertySetObject.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/PropertySetObject.java
@@ -19,7 +19,6 @@ package 
org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj;
 import java.io.IOException;
 
 import 
org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.space.ObjectSpaceObjectPropSet;
-import org.apache.tika.parser.microsoft.onenote.fsshttpb.util.ByteUtil;
 
 
 /**
@@ -39,7 +38,6 @@ public class PropertySetObject {
                              ObjectGroupObjectData objectData) throws 
IOException {
         this.objectDeclaration = objectDeclaration;
         this.objectSpaceObjectPropSet = new ObjectSpaceObjectPropSet();
-        this.objectSpaceObjectPropSet.doDeserializeFromByteArray(
-                ByteUtil.toByteArray(objectData.data.content), 0);
+        
this.objectSpaceObjectPropSet.doDeserializeFromByteArray(objectData.data.getContent(),
 0);
     }
 }
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 fb16a8402a..4f1e34b617 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
@@ -23,7 +23,6 @@ import java.util.List;
 import java.util.Map;
 
 import 
org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.ExGuid;
-import org.apache.tika.parser.microsoft.onenote.fsshttpb.util.ByteUtil;
 
 public class RevisionStoreObjectGroup {
     public ExGuid objectGroupID;
@@ -124,7 +123,7 @@ public class RevisionStoreObjectGroup {
                 if (objectDeclaration.objectPartitionID.getDecodedValue() == 
1) {
                     EncryptionObject encrypObject = new EncryptionObject();
                     encrypObject.objectDeclaration = objectDeclaration;
-                    encrypObject.objectData = 
ByteUtil.toByteArray(objectData.data.content);
+                    encrypObject.objectData = objectData.data.getContent();
                     objectGroup.encryptionObjects.add(encrypObject);
                 }
             }
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/BinaryItem.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/BinaryItem.java
index 2db0be6b42..b69b518eb4 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/BinaryItem.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/BinaryItem.java
@@ -18,22 +18,46 @@ package 
org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.tika.exception.TikaException;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.util.ByteUtil;
 
+/**
+ * A length-prefixed run of bytes.
+ *
+ * <p>The content is read in place, never copied on the way in: a deserialized 
item is a range
+ * of the buffer it was parsed from, and an item built from an array keeps 
that array. Neither
+ * may be modified while the item is in use. {@link #getContent()} swaps a 
range for an array of
+ * its own on the first call; {@link #copyContent()} always returns a fresh 
array.
+ *
+ * <p>Construction, {@link #setContent} and serialization keep {@link #length} 
equal to the
+ * content size, which is 0 for content set to null.
+ */
 public class BinaryItem extends BasicObject {
+    private static final byte[] EMPTY = new byte[0];
+
     public Compact64bitInt length;
-    public List<Byte> content;
+
+    /**
+     * The payload is {@code source[offset, offset + count)}. Deserializing 
keeps a view of the
+     * parsed buffer, so items that are never read, or only measured, cost no 
copy; object data
+     * BLOBs hold whole embedded files. {@code source} is null when the 
content was explicitly
+     * set to null.
+     */
+    private byte[] source;
+    private int offset;
+    private int count;
 
     /**
      * Initializes a new instance of the BinaryItem class.
      */
     public BinaryItem() {
         this.length = new Compact64bitInt();
-        this.content = new ArrayList<>();
+        this.source = EMPTY;
     }
 
     /**
@@ -42,10 +66,56 @@ public class BinaryItem extends BasicObject {
      * @param content Specify the binary content.
      */
     public BinaryItem(Collection<Byte> content) {
+        this(ByteUtil.toByteArray(content instanceof List<Byte> list ? list : 
new ArrayList<>(content)));
+    }
+
+    /**
+     * Initializes a new instance of the BinaryItem class with the specified 
content. The
+     * array is used as-is, not copied.
+     *
+     * @param content Specify the binary content, or null for none.
+     */
+    public BinaryItem(byte[] content) {
         this.length = new Compact64bitInt();
-        this.content = new ArrayList<>();
-        this.content.addAll(content);
-        this.length.setDecodedValue(this.content.size());
+        setContent(content);
+    }
+
+    /**
+     * @return the content as an array of exactly its length, or null if the 
content was set
+     * to null. A deserialized item copies its range out of the parsed buffer 
on the first
+     * call and returns that same array afterwards.
+     */
+    public byte[] getContent() {
+        if (source != null && (offset != 0 || count != source.length)) {
+            source = Arrays.copyOfRange(source, offset, offset + count);
+            offset = 0;
+        }
+        return source;
+    }
+
+    /**
+     * @return a new copy of the content, or null if the content was set to 
null. Unlike
+     * {@link #getContent()}, the item does not keep the copy.
+     */
+    public byte[] copyContent() {
+        return source == null ? null : Arrays.copyOfRange(source, offset, 
offset + count);
+    }
+
+    /**
+     * @return the content length in bytes, 0 if the content was set to null
+     */
+    public int getContentLength() {
+        return count;
+    }
+
+    /**
+     * Replaces the content. The array is used as-is, not copied.
+     */
+    public void setContent(byte[] content) {
+        this.source = content;
+        this.offset = 0;
+        this.count = content == null ? 0 : content.length;
+        this.length.setDecodedValue(this.count);
     }
 
     /**
@@ -55,12 +125,12 @@ public class BinaryItem extends BasicObject {
      */
     @Override
     public List<Byte> serializeToByteList() throws IOException {
-        this.length.setDecodedValue(this.content.size());
-
-        List<Byte> result = new ArrayList<>();
-        result.addAll(this.length.serializeToByteList());
-        result.addAll(this.content);
-
+        List<Byte> length = 
this.length.setDecodedValue(count).serializeToByteList();
+        List<Byte> result = new ArrayList<>(length.size() + count);
+        result.addAll(length);
+        for (int i = offset; i < offset + count; i++) {
+            result.add(source[i]);
+        }
         return result;
     }
 
@@ -78,10 +148,17 @@ public class BinaryItem extends BasicObject {
 
         this.length = BasicObject.parse(byteArray, index, 
Compact64bitInt.class);
 
-        this.content.clear();
-        for (long i = 0; i < this.length.getDecodedValue(); i++) {
-            this.content.add(byteArray[index.getAndIncrement()]);
+        long contentLength = this.length.getDecodedValue();
+        int contentStart = index.get();
+        if (contentLength < 0 || contentLength > byteArray.length - 
contentStart) {
+            throw new TikaException("BinaryItem length " + contentLength + " 
at offset " +
+                    contentStart + " exceeds the " + (byteArray.length - 
contentStart) +
+                    " bytes remaining");
         }
+        this.source = byteArray;
+        this.offset = contentStart;
+        this.count = (int) contentLength;
+        index.addAndGet((int) contentLength);
 
         return index.get() - startIndex;
     }
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/HeaderCell.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/HeaderCell.java
index 24a8e664b3..e2db132dc1 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/HeaderCell.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/HeaderCell.java
@@ -22,7 +22,6 @@ import 
org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.ObjectGroupDa
 import 
org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.ObjectGroupObjectData;
 import 
org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.ObjectGroupObjectDeclare;
 import 
org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.space.ObjectSpaceObjectPropSet;
-import org.apache.tika.parser.microsoft.onenote.fsshttpb.util.ByteUtil;
 
 public class HeaderCell {
     public ObjectGroupObjectDeclare objectDeclaration;
@@ -49,8 +48,7 @@ public class HeaderCell {
                 ObjectGroupObjectData objectData =
                         
objectElement.objectGroupData.objectGroupObjectDataList.get(i);
                 instance.objectData = new ObjectSpaceObjectPropSet();
-                instance.objectData.doDeserializeFromByteArray(
-                        ByteUtil.toByteArray(objectData.data.content), 0);
+                
instance.objectData.doDeserializeFromByteArray(objectData.data.getContent(), 0);
                 break;
             }
         }
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/util/BitReader.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/util/BitReader.java
index 73b3f4a61c..d36b34a050 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/util/BitReader.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/util/BitReader.java
@@ -17,14 +17,24 @@
 package org.apache.tika.parser.microsoft.onenote.fsshttpb.util;
 
 import java.io.IOException;
-import java.util.Arrays;
-import java.util.BitSet;
 import java.util.UUID;
 
 /**
  * A class is used to extract values across byte boundaries with arbitrary bit 
positions.
  */
 public class BitReader {
+    /**
+     * Direct-mapped cache from raw GUID bytes to the derived UUID. A file 
references the same
+     * few GUIDs from every ExGuid and cell ID, so most reads would otherwise 
recompute the same
+     * MD5. Entries are immutable and the mapping is a pure function of the 
key, so racing
+     * writers can only replace one correct entry with another.
+     */
+    private static final int GUID_CACHE_MASK = 1023;
+    private static final GuidCacheEntry[] GUID_CACHE = new 
GuidCacheEntry[GUID_CACHE_MASK + 1];
+
+    private record GuidCacheEntry(long high, long low, UUID uuid) {
+    }
+
     /**
      * A start position which will be not changed in the process of reading.
      * This value will be used for recording the start position and will be 
used by the function reset.
@@ -56,18 +66,6 @@ public class BitReader {
         this.byteArray = array;
     }
 
-    private static String toBinaryString(BitSet bs, int nbits) {
-        final StringBuilder buffer = new StringBuilder(bs.size());
-        for (int i = nbits - 1; i >= 0; --i) {
-            if (i < bs.size()) {
-                buffer.append(bs.get(i) ? "1" : "0");
-            } else {
-                buffer.append("0");
-            }
-        }
-        return buffer.toString();
-    }
-
     public boolean getCurrent() {
         return (byteArray[(int) offset >> 3] & (1 << ((int) offset & 7))) != 0;
     }
@@ -137,7 +135,22 @@ public class BitReader {
      * @return Return the GUID value.
      */
     public UUID readGuid() throws IOException {
-        return UUID.nameUUIDFromBytes(this.getBytes(128, 16));
+        byte[] raw = this.getBytes(128, 16);
+        long high = 0;
+        long low = 0;
+        for (int i = 0; i < 8; i++) {
+            high = (high << 8) | (raw[i] & 0xff);
+            low = (low << 8) | (raw[i + 8] & 0xff);
+        }
+        long mixed = (high ^ low) * 0x9E3779B97F4A7C15L;
+        int slot = (int) (mixed >>> 54) & GUID_CACHE_MASK;
+        GuidCacheEntry entry = GUID_CACHE[slot];
+        if (entry != null && entry.high == high && entry.low == low) {
+            return entry.uuid;
+        }
+        UUID uuid = UUID.nameUUIDFromBytes(raw);
+        GUID_CACHE[slot] = new GuidCacheEntry(high, low, uuid);
+        return uuid;
     }
 
     /**
@@ -172,25 +185,35 @@ public class BitReader {
      * @return Returns the constructed byte array.
      */
     private byte[] getBytes(int needReadlength, int size) throws IOException {
-        BitSet retSet = new BitSet(size);
-        int i = 0;
-        while (i < needReadlength) {
-            if (!this.moveNext()) {
-                throw new IOException("Unexpected to meet the byte array 
end.");
-            }
-            if (getCurrent()) {
-                retSet.set(i);
-            } else {
-                retSet.clear(i);
-            }
-            i++;
+        // Bit i of the result is bit i after the current offset, least 
significant bit first.
+        // The bounds check below guarantees every source byte read holds a 
requested bit.
+        long start = this.offset + 1;
+        if (needReadlength > this.length - start) {
+            this.offset = this.length;
+            throw new IOException("Unexpected to meet the byte array end.");
         }
         byte[] result = new byte[size];
-        Arrays.fill(result, (byte) 0);
-        byte[] retSetBa = retSet.toByteArray();
-        for (i = 0; i < retSetBa.length; ++i) {
-            result[i] = retSetBa[i];
+        int shift = (int) (start & 7);
+        int source = (int) (start >> 3);
+        int fullBytes = needReadlength >> 3;
+        int remainingBits = needReadlength & 7;
+        if (shift == 0) {
+            System.arraycopy(byteArray, source, result, 0, fullBytes);
+            source += fullBytes;
+        } else {
+            for (int i = 0; i < fullBytes; i++, source++) {
+                result[i] = (byte) (((byteArray[source] & 0xFF) >>> shift) |
+                        (byteArray[source + 1] << (8 - shift)));
+            }
+        }
+        if (remainingBits > 0) {
+            int bits = (byteArray[source] & 0xFF) >>> shift;
+            if (shift + remainingBits > 8) {
+                bits |= byteArray[source + 1] << (8 - shift);
+            }
+            result[fullBytes] = (byte) (bits & ((1 << remainingBits) - 1));
         }
+        this.offset += needReadlength;
         return result;
     }
 }
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 b7d7508f8b..13368f8291 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
@@ -184,8 +184,7 @@ public class MSOneStoreBlobTest {
         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}));
+        objectData.data.setContent(new byte[]{0, 0, 0, (byte) 0x80, 0, 0, 0, 
0});
         PropertySetObject propertySetObject = new PropertySetObject(null, 
objectData);
         propertySetObject.objectSpaceObjectPropSet = propSet;
         parent.propertySet = propertySetObject;
@@ -210,14 +209,13 @@ public class MSOneStoreBlobTest {
         blob.data = null;
         assertNull(blob.getData());
         blob.data = new BinaryItem();
-        blob.data.content = null;
+        blob.data.setContent(null);
         assertNull(blob.getData());
-        blob.data = new BinaryItem();
-        blob.data.content.add((byte) 1);
+        blob.data = new BinaryItem(new byte[]{1});
         assertFalse(blob.serializeToByteList().isEmpty());
 
         ObjectDataBLOB validBlob = new ObjectDataBLOB();
-        validBlob.data.content.add((byte) 1);
+        validBlob.data.setContent(new byte[]{1});
         byte[] itemBytes = 
ByteUtil.toByteArray(validBlob.data.serializeToByteList());
         AtomicInteger index = new AtomicInteger(0);
         java.lang.reflect.Method deserialize = 
ObjectDataBLOB.class.getDeclaredMethod(
@@ -282,7 +280,7 @@ public class MSOneStoreBlobTest {
         ObjectGroupObjectDeclare declaration = new ObjectGroupObjectDeclare();
         declaration.objectPartitionID.setDecodedValue(1);
         ObjectGroupObjectData objectData = new ObjectGroupObjectData();
-        objectData.data.content.add((byte) 42);
+        objectData.data.setContent(new byte[]{42});
         
encryptedData.objectGroupDeclarations.objectDeclarationList.add(declaration);
         
encryptedData.objectGroupData.objectGroupObjectDataList.add(objectData);
 
@@ -337,9 +335,7 @@ public class MSOneStoreBlobTest {
 
     private DataElement roundTripBlobElement() throws Exception {
         ObjectDataBLOBDataElementData blobData = new 
ObjectDataBLOBDataElementData();
-        for (byte b : BLOB_BYTES) {
-            blobData.objectDataBLOB.data.content.add(b);
-        }
+        blobData.objectDataBLOB.data.setContent(BLOB_BYTES.clone());
         DataElement blobElement =
                 new DataElement(DataElementType.ObjectDataBLOBDataElementData, 
blobData);
         byte[] serialized = 
ByteUtil.toByteArray(blobElement.serializeToByteList());
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 b8095abd45..bff0021a4e 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
@@ -76,7 +76,6 @@ import 
org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.HeaderC
 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;
@@ -2678,7 +2677,7 @@ public class MSOneStorePackageTest {
 
     private static FileDataObject fileData(String value) {
         ObjectDataBLOB blob = new ObjectDataBLOB();
-        
blob.data.content.addAll(ByteUtil.toListOfByte(value.getBytes(StandardCharsets.UTF_8)));
+        blob.data.setContent(value.getBytes(StandardCharsets.UTF_8));
         ObjectDataBLOBDataElementData blobData = new 
ObjectDataBLOBDataElementData();
         blobData.objectDataBLOB = blob;
         DataElement element = new DataElement();
@@ -2745,8 +2744,7 @@ public class MSOneStorePackageTest {
 
     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}));
+        data.data.setContent(new byte[]{0, 0, 0, (byte) 0x80, 0, 0, 0, 0});
         return data;
     }
 
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/streamobj/basic/BinaryItemTest.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/streamobj/basic/BinaryItemTest.java
new file mode 100644
index 0000000000..d8cde111c8
--- /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/streamobj/basic/BinaryItemTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.basic;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+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 java.util.Arrays;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.util.ByteUtil;
+
+public class BinaryItemTest {
+
+    /**
+     * Deserializing keeps only a view of the parsed buffer, so the accessors 
must still
+     * return exactly the item's bytes, surrounded by unrelated data, and hand 
out arrays
+     * that cannot reach back into that buffer.
+     */
+    @Test
+    public void testDeserializedContentIsExactAndDetached() throws Exception {
+        byte[] payload = {10, 20, 30, 40, 50};
+        byte[] item = serialize(new BinaryItem(payload.clone()));
+        byte[] buffer = new byte[item.length + 6];
+        Arrays.fill(buffer, (byte) 99);
+        System.arraycopy(item, 0, buffer, 3, item.length);
+
+        AtomicInteger index = new AtomicInteger(3);
+        BinaryItem parsed = BasicObject.parse(buffer, index, BinaryItem.class);
+        assertEquals(3 + item.length, index.get());
+        assertEquals(payload.length, parsed.getContentLength());
+
+        byte[] copy = parsed.copyContent();
+        assertArrayEquals(payload, copy);
+        copy[0] = 0;
+        assertArrayEquals(payload, parsed.copyContent());
+
+        byte[] content = parsed.getContent();
+        assertArrayEquals(payload, content);
+        assertSame(content, parsed.getContent());
+        assertNotSame(buffer, content);
+        content[1] = 0;
+        assertEquals(20, buffer[3 + item.length - payload.length + 1]);
+    }
+
+    /**
+     * Replacing the content of a deserialized item must drop the view of the 
parsed buffer,
+     * keep {@code length} in step, and serialize the new bytes; a 
deserialized item must also
+     * reserialize to exactly the bytes it was parsed from.
+     */
+    @Test
+    public void testSetContentAfterDeserializeAndRoundTrip() throws Exception {
+        byte[] item = serialize(new BinaryItem(new byte[]{1, 2, 3, 4}));
+        byte[] buffer = new byte[item.length + 2];
+        System.arraycopy(item, 0, buffer, 1, item.length);
+        BinaryItem parsed = BasicObject.parse(buffer, new AtomicInteger(1), 
BinaryItem.class);
+        assertArrayEquals(item, serialize(parsed));
+
+        byte[] replacement = {7, 8};
+        parsed.setContent(replacement);
+        assertSame(replacement, parsed.getContent());
+        assertEquals(2, parsed.getContentLength());
+        assertEquals(2, parsed.length.getDecodedValue());
+
+        byte[] reserialized = serialize(parsed);
+        BinaryItem reparsed =
+                BasicObject.parse(reserialized, new AtomicInteger(0), 
BinaryItem.class);
+        assertArrayEquals(replacement, reparsed.getContent());
+    }
+
+    @Test
+    public void testLengthPastBufferEndFails() throws Exception {
+        byte[] item = serialize(new BinaryItem(new byte[]{1, 2, 3, 4}));
+        byte[] truncated = Arrays.copyOf(item, item.length - 1);
+        assertThrows(TikaException.class,
+                () -> BasicObject.parse(truncated, new AtomicInteger(0), 
BinaryItem.class));
+    }
+
+    /**
+     * A 64-bit compact length of 2^63 or more decodes as a negative long; it 
must fail like any
+     * other impossible length rather than produce an empty item.
+     */
+    @Test
+    public void testNegativeLengthFails() {
+        byte[] item = {(byte) 0x80, 0, 0, 0, 0, 0, 0, 0, (byte) 0x80};
+        assertThrows(TikaException.class,
+                () -> BasicObject.parse(item, new AtomicInteger(0), 
BinaryItem.class));
+    }
+
+    @Test
+    public void testNullContent() throws Exception {
+        BinaryItem item = new BinaryItem(new byte[]{1});
+        item.setContent(null);
+        assertNull(item.getContent());
+        assertNull(item.copyContent());
+        assertEquals(0, item.getContentLength());
+        assertEquals(0, item.length.getDecodedValue());
+
+        BinaryItem constructed = new BinaryItem((byte[]) null);
+        assertNull(constructed.getContent());
+        assertEquals(0, constructed.length.getDecodedValue());
+        assertArrayEquals(serialize(new BinaryItem()), serialize(constructed));
+    }
+
+    private static byte[] serialize(BinaryItem item) throws Exception {
+        return ByteUtil.toByteArray(item.serializeToByteList());
+    }
+}
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/util/BitReaderTest.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/util/BitReaderTest.java
new file mode 100644
index 0000000000..0c1b3ee809
--- /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/util/BitReaderTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.util;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.IOException;
+import java.util.Random;
+import java.util.UUID;
+
+import org.junit.jupiter.api.Test;
+
+public class BitReaderTest {
+
+    /**
+     * Every read must match a reference reader stepping 
moveNext()/getCurrent() one bit at a
+     * time over the same bytes.
+     */
+    @Test
+    public void testReadsMatchBitByBitReference() throws IOException {
+        Random random = new Random(20261104L);
+        for (int trial = 0; trial < 200; trial++) {
+            byte[] data = new byte[1 + random.nextInt(64)];
+            random.nextBytes(data);
+            int startByte = random.nextInt(data.length);
+            BitReader reader = new BitReader(data, startByte);
+            BitReader reference = new BitReader(data, startByte);
+            long bitsLeft = (long) (data.length - startByte) * 8;
+            while (bitsLeft > 0) {
+                int kind = random.nextInt(3);
+                if (kind == 0 && bitsLeft >= 128) {
+                    
assertEquals(UUID.nameUUIDFromBytes(referenceBytes(reference, 128, 16)),
+                            reader.readGuid());
+                    bitsLeft -= 128;
+                } else if (kind == 1 && bitsLeft >= 8) {
+                    int bytes = 1 + random.nextInt((int) Math.min(bitsLeft / 
8, 20));
+                    assertArrayEquals(referenceBytes(reference, bytes * 8, 
bytes),
+                            reader.readBytes(bytes));
+                    bitsLeft -= bytes * 8L;
+                } else {
+                    int bits = 1 + random.nextInt((int) Math.min(bitsLeft, 
64));
+                    assertEquals(LittleEndianBitConverter.toUInt64(
+                                    referenceBytes(reference, bits, 8), 0),
+                            reader.readUInt64(bits), "trial " + trial + ", 
width " + bits);
+                    bitsLeft -= bits;
+                }
+            }
+            assertThrows(IOException.class, () -> reader.readUInt32(1));
+        }
+    }
+
+    /**
+     * The GUID cache slot depends on high ^ low, so XORing both 8-byte halves 
of a GUID with the
+     * same mask gives a different GUID in the same slot. Reads alternating 
between the two must
+     * each return their own MD5-derived UUID; a repeat read of the resident 
one is a cache hit.
+     */
+    @Test
+    public void testGuidCacheCollision() throws IOException {
+        byte[] first = new byte[16];
+        new Random(20261106L).nextBytes(first);
+        byte[] second = first.clone();
+        second[0] ^= (byte) 0x5A;
+        second[8] ^= (byte) 0x5A;
+        UUID firstUuid = UUID.nameUUIDFromBytes(first);
+        UUID secondUuid = UUID.nameUUIDFromBytes(second);
+
+        byte[] data = new byte[16 * 5];
+        for (int i = 0; i < 5; i++) {
+            System.arraycopy(i % 2 == 0 ? first : second, 0, data, i * 16, 16);
+        }
+        BitReader reader = new BitReader(data, 0);
+        assertEquals(firstUuid, reader.readGuid());
+        assertEquals(secondUuid, reader.readGuid());
+        assertEquals(firstUuid, reader.readGuid());
+        assertEquals(secondUuid, reader.readGuid());
+
+        BitReader repeat = new BitReader(data, 64);
+        UUID resident = repeat.readGuid();
+        assertEquals(firstUuid, resident);
+        assertSame(resident, new BitReader(data, 64).readGuid());
+    }
+
+    @Test
+    public void testReadPastEndThrows() {
+        BitReader reader = new BitReader(new byte[]{1, 2, 3}, 1);
+        assertThrows(IOException.class, () -> reader.readUInt32(17));
+    }
+
+    private static byte[] referenceBytes(BitReader reference, int bits, int 
size) {
+        byte[] result = new byte[size];
+        for (int i = 0; i < bits; i++) {
+            if (!reference.moveNext()) {
+                throw new IllegalStateException("reference reader ran out of 
bits");
+            }
+            if (reference.getCurrent()) {
+                result[i >> 3] |= (byte) (1 << (i & 7));
+            }
+        }
+        return result;
+    }
+}

Reply via email to