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 2c946ccabd [TIKA-4944] Extract GUIDs from onenote files (#3278)
2c946ccabd is described below
commit 2c946ccabd13df305560a260fd2d098409fd8ae5
Author: Henry Lindeman <[email protected]>
AuthorDate: Thu Oct 8 13:08:29 2026 -0700
[TIKA-4944] Extract GUIDs from onenote files (#3278)
* Extract GUIDs from onenote files
Generated-by: Glean Tau <various models>
Signed-off-by: Henry Lindeman <[email protected]>
* AI Review fixes and jacoco
Generated-by: Glean Tau <various models>
Signed-off-by: Henry Lindeman <[email protected]>
* Move section guid from bag to scalar metadata, add pages metadata key
Generated-by: Glean Tay <various models>
Signed-off-by: Henry Lindeman <[email protected]>
* More PR Comments (drop entityGuids entirely)
Generated-by: Glean Tau <various models>
Signed-off-by: Henry Lindeman <[email protected]>
* More AI reviews
Generated-by: Glean Tau <various models>
Signed-off-by: Henry Lindeman <[email protected]>
* copilot comments
Signed-off-by: Henry Lindeman <[email protected]>
---------
Signed-off-by: Henry Lindeman <[email protected]>
---
CHANGES.txt | 9 +
.../java/org/apache/tika/metadata/OneNote.java | 29 +-
.../apache/tika/metadata/metadata-key-fields.json | 5 +
.../org/apache/tika/metadata/metadata-keys.json | 5 +
.../apache/tika/parser/microsoft/onenote/GUID.java | 24 +
.../microsoft/onenote/OneNoteGuidCollector.java | 141 +++
.../tika/parser/microsoft/onenote/OneNoteJcid.java | 30 +
.../parser/microsoft/onenote/OneNoteParser.java | 8 +
.../tika/parser/microsoft/onenote/OneNotePtr.java | 33 +-
.../microsoft/onenote/OneNoteTreeWalker.java | 83 +-
.../onenote/fsshttpb/MSOneStorePackage.java | 374 ++++++--
.../tika/parser/microsoft/onenote/GUIDTest.java | 20 +
.../onenote/OneNoteGuidCollectorTest.java | 106 +++
.../microsoft/onenote/OneNoteParserTest.java | 94 ++
.../onenote/OneNoteTreeWalkerGuidTest.java | 218 +++++
.../onenote/fsshttpb/MSOneStorePackageTest.java | 998 ++++++++++++++++++++-
16 files changed, 2083 insertions(+), 94 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 2fcd701d48..7f7b5de69f 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -68,6 +68,15 @@ Release 4.2.0 - unreleased
Every resolver Tika installs answers with an empty stream, never null
(TIKA-4941).
* Deprecate XMLReaderUtils.getXMLInputFactory() and use SAX for XFA
(TIKA-4938).
+ * OneNote extracts a section GUID and page, page-series, page-node, and
conflict-page
+ GUID metadata from classic and FSSHTTPB files; the GUID bags are
deduplicated and
+ sorted, not page-ordered. Tree-collected GUID metadata is capped at
100,000 values
+ with a warning if the limit is reached. FSSHTTPB page divs now carry
+ id="{page GUID}" when a page GUID was recovered (fallback content can be
unwrapped).
+ FSSHTTPB root resolution is bounded at 10,000 roots per cell; when the
bound
+ truncates the declared roots before a content root, the walk falls back
to every
+ object in the cell so page content is not silently lost (TIKA-4944).
+
* Raise the default maxFilesProcessedPerProcess from 10,000 to 100,000;
forked pipes JVMs restart a tenth as often (TIKA-4950).
diff --git a/tika-core/src/main/java/org/apache/tika/metadata/OneNote.java
b/tika-core/src/main/java/org/apache/tika/metadata/OneNote.java
index 0077753c8b..e635825aca 100644
--- a/tika-core/src/main/java/org/apache/tika/metadata/OneNote.java
+++ b/tika-core/src/main/java/org/apache/tika/metadata/OneNote.java
@@ -18,10 +18,20 @@ package org.apache.tika.metadata;
/**
* OneNote file-header and revision metadata. Key suffixes are the
[MS-ONESTORE]/[MS-FSSHTTPB]
- * field names verbatim (external-standard spellings), under the {@code
onenote:} namespace.
+ * field names verbatim (external-standard spellings), under the {@code
onenote:} namespace,
+ * except the GUID keys below, which are projected from the source object that
carries the
+ * value (the section file identity, or a NotebookManagementEntityGuid keyed
by the carrying
+ * object's JCID).
* Previously these were minted per parse via the registering Property
factories
* (TIKA-4816 round-3 review: per-record global-lock interning); a bounded,
spec-defined
* vocabulary belongs in curated constants.
+ *
+ * <p>The GUID bags (pageGuids, pageSeriesGuids, pageNodeGuids,
conflictPageGuids) are
+ * unordered, deduplicated, lexically sorted inventories: do not join them
positionally
+ * to each other or to page content, and do not treat values as unique across
files.
+ * Page order and page-to-content association are available only through the
FSSHTTPB
+ * XHTML output, where each recognized page is wrapped in a div whose id is
the page
+ * GUID when one was recovered (fallback content can be unwrapped).
*/
public interface OneNote {
@@ -58,6 +68,23 @@ public interface OneNote {
Property MOST_RECENT_AUTHORS = Property.externalTextBag(PREFIX +
"mostRecentAuthors");
Property ORIGINAL_AUTHORS = Property.externalTextBag(PREFIX +
"originalAuthors");
+
+ /**
+ * GUID of the section file: guidFile from the classic header, or the first
+ * FileIdentityGuid property found in a newer-format package.
+ */
+ Property SECTION_GUID = Property.externalText(PREFIX + "sectionGuid");
+ /**
+ * GUIDs of jcidPageMetadata (0x30) objects. Classic parsing records only
pages referenced
+ * via MetaDataObjectsAboveGraphSpace; FSSHTTPB fallback walks may still
include older copies.
+ */
+ Property PAGE_GUIDS = Property.externalTextBag(PREFIX + "pageGuids");
+ /** GUIDs of jcidPageSeriesNode (0x08) objects. */
+ Property PAGE_SERIES_GUIDS = Property.externalTextBag(PREFIX +
"pageSeriesGuids");
+ /** GUIDs of jcidPageNode (0x0B) objects; source-object identifiers, not
cross-file page keys. */
+ Property PAGE_NODE_GUIDS = Property.externalTextBag(PREFIX +
"pageNodeGuids");
+ /** GUIDs of jcidConflictPageMetadata (0x38) objects. */
+ Property CONFLICT_PAGE_GUIDS = Property.externalTextBag(PREFIX +
"conflictPageGuids");
Property CREATION_TIMESTAMP = Property.externalText(PREFIX +
"creationTimestamp");
Property LAST_MODIFIED_TIMESTAMP = Property.externalText(PREFIX +
"lastModifiedTimestamp");
}
diff --git
a/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-key-fields.json
b/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-key-fields.json
index f884866b7c..2926590d46 100644
---
a/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-key-fields.json
+++
b/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-key-fields.json
@@ -377,6 +377,7 @@
{"class":"org.apache.tika.metadata.OneNote","field":"CB_FREE_SPACE_IN_FREE_CHUNK_LIST","key":"onenote:cbFreeSpaceInFreeChunkList"},
{"class":"org.apache.tika.metadata.OneNote","field":"CB_LEGACY_EXPECTED_FILE_LENGTH","key":"onenote:cbLegacyExpectedFileLength"},
{"class":"org.apache.tika.metadata.OneNote","field":"CB_LEGACY_FREE_SPACE_IN_FREE_CHUNK_LIST","key":"onenote:cbLegacyFreeSpaceInFreeChunkList"},
+
{"class":"org.apache.tika.metadata.OneNote","field":"CONFLICT_PAGE_GUIDS","key":"onenote:conflictPageGuids"},
{"class":"org.apache.tika.metadata.OneNote","field":"CRC_NAME","key":"onenote:crcName"},
{"class":"org.apache.tika.metadata.OneNote","field":"CREATION_TIMESTAMP","key":"onenote:creationTimestamp"},
{"class":"org.apache.tika.metadata.OneNote","field":"C_TRANSACTIONS_IN_LOG","key":"onenote:cTransactionsInLog"},
@@ -389,7 +390,11 @@
{"class":"org.apache.tika.metadata.OneNote","field":"MOST_RECENT_AUTHORS","key":"onenote:mostRecentAuthors"},
{"class":"org.apache.tika.metadata.OneNote","field":"N_FILE_VERSION_GENERATION","key":"onenote:nFileVersionGeneration"},
{"class":"org.apache.tika.metadata.OneNote","field":"ORIGINAL_AUTHORS","key":"onenote:originalAuthors"},
+
{"class":"org.apache.tika.metadata.OneNote","field":"PAGE_GUIDS","key":"onenote:pageGuids"},
+
{"class":"org.apache.tika.metadata.OneNote","field":"PAGE_NODE_GUIDS","key":"onenote:pageNodeGuids"},
+
{"class":"org.apache.tika.metadata.OneNote","field":"PAGE_SERIES_GUIDS","key":"onenote:pageSeriesGuids"},
{"class":"org.apache.tika.metadata.OneNote","field":"RGB_PLACEHOLDER","key":"onenote:rgbPlaceholder"},
+
{"class":"org.apache.tika.metadata.OneNote","field":"SECTION_GUID","key":"onenote:sectionGuid"},
{"class":"org.apache.tika.metadata.PDF","field":"ACTION_TRIGGER","key":"pdf:action-trigger"},
{"class":"org.apache.tika.metadata.PDF","field":"ACTION_TRIGGERS","key":"pdf:action-triggers"},
{"class":"org.apache.tika.metadata.PDF","field":"ACTION_TYPES","key":"pdf:action-types"},
diff --git
a/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-keys.json
b/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-keys.json
index 95d4221cb7..3f57a2f533 100644
---
a/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-keys.json
+++
b/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-keys.json
@@ -401,6 +401,7 @@
{"key":"onenote:cbFreeSpaceInFreeChunkList","namespace":"onenote","valueType":"TEXT","cardinality":"SIMPLE","module":"tika-core"},
{"key":"onenote:cbLegacyExpectedFileLength","namespace":"onenote","valueType":"TEXT","cardinality":"SIMPLE","module":"tika-core"},
{"key":"onenote:cbLegacyFreeSpaceInFreeChunkList","namespace":"onenote","valueType":"TEXT","cardinality":"SIMPLE","module":"tika-core"},
+
{"key":"onenote:conflictPageGuids","namespace":"onenote","valueType":"TEXT","cardinality":"BAG","module":"tika-core"},
{"key":"onenote:crcName","namespace":"onenote","valueType":"TEXT","cardinality":"SIMPLE","module":"tika-core"},
{"key":"onenote:creationTimestamp","namespace":"onenote","valueType":"TEXT","cardinality":"SIMPLE","module":"tika-core"},
{"key":"onenote:ffvLastCodeThatWroteToThisFile","namespace":"onenote","valueType":"TEXT","cardinality":"SIMPLE","module":"tika-core"},
@@ -412,7 +413,11 @@
{"key":"onenote:mostRecentAuthors","namespace":"onenote","valueType":"TEXT","cardinality":"BAG","module":"tika-core"},
{"key":"onenote:nFileVersionGeneration","namespace":"onenote","valueType":"TEXT","cardinality":"SIMPLE","module":"tika-core"},
{"key":"onenote:originalAuthors","namespace":"onenote","valueType":"TEXT","cardinality":"BAG","module":"tika-core"},
+
{"key":"onenote:pageGuids","namespace":"onenote","valueType":"TEXT","cardinality":"BAG","module":"tika-core"},
+
{"key":"onenote:pageNodeGuids","namespace":"onenote","valueType":"TEXT","cardinality":"BAG","module":"tika-core"},
+
{"key":"onenote:pageSeriesGuids","namespace":"onenote","valueType":"TEXT","cardinality":"BAG","module":"tika-core"},
{"key":"onenote:rgbPlaceholder","namespace":"onenote","valueType":"TEXT","cardinality":"SIMPLE","module":"tika-core"},
+
{"key":"onenote:sectionGuid","namespace":"onenote","valueType":"TEXT","cardinality":"SIMPLE","module":"tika-core"},
{"key":"pdf:action-trigger","namespace":"pdf","valueType":"TEXT","cardinality":"SIMPLE","module":"tika-core"},
{"key":"pdf:action-triggers","namespace":"pdf","valueType":"TEXT","cardinality":"BAG","module":"tika-core"},
{"key":"pdf:action-types","namespace":"pdf","valueType":"TEXT","cardinality":"BAG","module":"tika-core"},
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 55e788bb4a..04377e7e70 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
@@ -34,6 +34,30 @@ public class GUID implements Comparable<GUID> {
this.guid = guid;
}
+ /**
+ * Decodes a 16-byte GUID stored in the mixed-endian layout used by
MS-DTYP.
+ *
+ * @return the GUID, or {@code null} if {@code data} is not exactly 16
bytes
+ */
+ public static GUID fromMicrosoftBytes(byte[] data) {
+ if (data == null || data.length != 16) {
+ return null;
+ }
+ int[] guid = new int[16];
+ guid[0] = data[3] & 0xff;
+ guid[1] = data[2] & 0xff;
+ guid[2] = data[1] & 0xff;
+ guid[3] = data[0] & 0xff;
+ guid[4] = data[5] & 0xff;
+ guid[5] = data[4] & 0xff;
+ guid[6] = data[7] & 0xff;
+ guid[7] = data[6] & 0xff;
+ for (int i = 8; i < 16; i++) {
+ guid[i] = data[i] & 0xff;
+ }
+ return new GUID(guid);
+ }
+
/**
* Converts a GUID of format: {AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE} (in
bytes) to a GUID
* object.
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/OneNoteGuidCollector.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNoteGuidCollector.java
new file mode 100644
index 0000000000..e99bec5d3f
--- /dev/null
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNoteGuidCollector.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser.microsoft.onenote;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.OneNote;
+import org.apache.tika.metadata.Property;
+
+/**
+ * Collects the GUID metadata of one OneNote section for the classic and
FSSHTTPB parsers.
+ * Values are deduplicated per key and capped at {@link #MAX_GUID_COUNT} in
total.
+ */
+public final class OneNoteGuidCollector {
+
+ public static final int MAX_GUID_COUNT = 100_000;
+
+ public enum Category {
+ SECTION(OneNote.SECTION_GUID),
+ PAGE(OneNote.PAGE_GUIDS),
+ PAGE_SERIES(OneNote.PAGE_SERIES_GUIDS),
+ PAGE_NODE(OneNote.PAGE_NODE_GUIDS),
+ CONFLICT_PAGE(OneNote.CONFLICT_PAGE_GUIDS);
+
+ private final Property property;
+
+ Category(Property property) {
+ this.property = property;
+ }
+ }
+
+ private final Map<Category, LinkedHashSet<String>> values = new
EnumMap<>(Category.class);
+ private final Consumer<String> warningHandler;
+ private int count;
+ private boolean limitWarningReported;
+
+ public OneNoteGuidCollector(Consumer<String> warningHandler) {
+ this.warningHandler = warningHandler;
+ for (Category category : Category.values()) {
+ values.put(category, new LinkedHashSet<>());
+ }
+ }
+
+ public void add(Category category, String guid) {
+ if (guid == null) {
+ return;
+ }
+ Set<String> categoryValues = values.get(category);
+ if (categoryValues.contains(guid)
+ || (category == Category.SECTION &&
!categoryValues.isEmpty())) {
+ return;
+ }
+ if (count >= MAX_GUID_COUNT) {
+ reportLimitWarning();
+ return;
+ }
+ categoryValues.add(guid);
+ count++;
+ if (count == MAX_GUID_COUNT) {
+ reportLimitWarning();
+ }
+ }
+
+ /**
+ * Records a NotebookManagementEntityGuid under the key for the JCID of
the object that
+ * carries it. Section-node objects and unrecognized JCIDs are skipped.
+ */
+ public void addForObjectType(int jcid, String guid) {
+ Category category = categoryFor(jcid);
+ if (category != null) {
+ add(category, guid);
+ }
+ }
+
+ static Category categoryFor(int jcid) {
+ switch (jcid) {
+ case OneNoteJcid.PAGE_METADATA:
+ return Category.PAGE;
+ case OneNoteJcid.PAGE_SERIES_NODE:
+ return Category.PAGE_SERIES;
+ case OneNoteJcid.PAGE_NODE:
+ return Category.PAGE_NODE;
+ case OneNoteJcid.CONFLICT_PAGE_METADATA:
+ return Category.CONFLICT_PAGE;
+ default:
+ return null;
+ }
+ }
+
+ private void reportLimitWarning() {
+ if (!limitWarningReported) {
+ limitWarningReported = true;
+ warningHandler.accept("Capping OneNote GUID metadata at " +
MAX_GUID_COUNT +
+ " distinct values");
+ }
+ }
+
+ public Set<String> values(Category category) {
+ return Collections.unmodifiableSet(values.get(category));
+ }
+
+ public boolean isFull() {
+ return count >= MAX_GUID_COUNT;
+ }
+
+ /** Sets each non-empty key on {@code metadata}, with values sorted. */
+ public void publish(Metadata metadata) {
+ for (Map.Entry<Category, LinkedHashSet<String>> entry :
values.entrySet()) {
+ if (!entry.getValue().isEmpty()) {
+ if (entry.getKey() == Category.SECTION) {
+ metadata.set(entry.getKey().property,
entry.getValue().iterator().next());
+ } else {
+ String[] sorted = entry.getValue().toArray(new String[0]);
+ Arrays.sort(sorted);
+ metadata.set(entry.getKey().property, sorted);
+ }
+ }
+ }
+ }
+}
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/OneNoteJcid.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNoteJcid.java
new file mode 100644
index 0000000000..d4f87508e0
--- /dev/null
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNoteJcid.java
@@ -0,0 +1,30 @@
+/*
+ * 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;
+
+/** JCID index values from MS-ONE 2.1.13. */
+public final class OneNoteJcid {
+
+ public static final int SECTION_NODE = 0x07;
+ public static final int PAGE_SERIES_NODE = 0x08;
+ public static final int PAGE_NODE = 0x0B;
+ public static final int PAGE_METADATA = 0x30;
+ public static final int CONFLICT_PAGE_METADATA = 0x38;
+
+ private OneNoteJcid() {
+ }
+}
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 2e4935016b..4a4399c41b 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
@@ -129,6 +129,7 @@ public class OneNoteParser implements Parser {
"0x" +
Long.toHexString(oneNoteDocument.header.nFileVersionGeneration));
metadata.set(OneNote.RGB_PLACEHOLDER,
"0x" +
Long.toHexString(oneNoteDocument.header.rgbPlaceholder));
+ emitSectionFileGuid(header, metadata);
Exception structureFailure =
oneNoteDocument.structureParseException;
boolean walked = false;
@@ -211,6 +212,13 @@ public class OneNoteParser implements Parser {
}
+ static void emitSectionFileGuid(OneNoteHeader header, Metadata metadata) {
+ GUID guidFile = header.getGuidFile();
+ if (guidFile != null && !guidFile.equals(GUID.nil())) {
+ metadata.set(OneNote.SECTION_GUID, guidFile.toString());
+ }
+ }
+
private static String[] sortedValues(Set<String> values) {
String[] sorted = values.toArray(new String[0]);
Arrays.sort(sorted);
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNotePtr.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNotePtr.java
index 916b9bbb5a..e379f3564f 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNotePtr.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNotePtr.java
@@ -148,33 +148,18 @@ class OneNotePtr {
}
private GUID deserializeGUID() throws IOException {
- int[] guid = new int[16];
+ byte[] guidBytes = new byte[16];
for (int i = 0; i < 16; ++i) {
- guid[i] = dif.read();
+ int next = dif.read();
+ if (next < 0) {
+ // (byte) -1 would read as 0xFF and silently decode a wrong
GUID
+ throw new IOException("End of stream reached while reading a
GUID");
+ }
+ guidBytes[i] = (byte) next;
}
- int[] guid2 = new int[16];
- // re-order [0,1,2,3] to little endian
- guid2[0] = guid[3];
- guid2[1] = guid[2];
- guid2[2] = guid[1];
- guid2[3] = guid[0];
- // re-order [4,5,6,7] to little endian
- guid2[4] = guid[5];
- guid2[5] = guid[4];
- guid2[6] = guid[7];
- guid2[7] = guid[6];
- // the rest is already in right order.
- guid2[8] = guid[8];
- guid2[9] = guid[9];
- guid2[10] = guid[10];
- guid2[11] = guid[11];
- guid2[12] = guid[12];
- guid2[13] = guid[13];
- guid2[14] = guid[14];
- guid2[15] = guid[15];
-
+ GUID guid = GUID.fromMicrosoftBytes(guidBytes);
offset = dif.position();
- return new GUID(guid2);
+ return guid;
}
private byte[] deserializedReservedHeader() throws IOException {
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNoteTreeWalker.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNoteTreeWalker.java
index a4c3902c0c..116ed2ba49 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNoteTreeWalker.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/onenote/OneNoteTreeWalker.java
@@ -96,6 +96,7 @@ class OneNoteTreeWalker {
private final Set<String> authors = new HashSet<>();
private final Set<String> mostRecentAuthors = new HashSet<>();
private final Set<String> originalAuthors = new HashSet<>();
+ private final OneNoteGuidCollector guidCollector;
private final OneNoteTreeWalkerOptions options;
private final OneNoteDocument oneNoteDocument;
private final OneNoteDirectFileResource dif;
@@ -143,6 +144,7 @@ class OneNoteTreeWalker {
this.xhtml = xhtml;
this.parentMetadata = parentMetadata;
this.parseContext = parseContext;
+ this.guidCollector = new
OneNoteGuidCollector(this::recordGuidCapWarning);
this.embeddedDocumentExtractor =
EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(parseContext);
}
@@ -156,8 +158,12 @@ class OneNoteTreeWalker {
public Map<String, Object> walkTree() throws IOException, TikaException,
SAXException {
Map<String, Object> structure = new HashMap<>();
structure.put("header", oneNoteDocument.header);
- structure.put("rootFileNodes", walkRootFileNodes());
- return structure;
+ try {
+ structure.put("rootFileNodes", walkRootFileNodes());
+ return structure;
+ } finally {
+ publishGuidBags();
+ }
}
/**
@@ -384,7 +390,8 @@ class OneNoteTreeWalker {
structure.put("childFileNodeList",
walkFileNodeList(fileNode.childFileNodeList, parentPropertyId));
}
if (fileNode.propertySet != null) {
- List<Map<String, Object>> propSet =
processPropertySet(fileNode.propertySet, parentPropertyId);
+ List<Map<String, Object>> propSet =
+ processPropertySet(fileNode.propertySet, parentPropertyId,
jcidIndex(fileNode));
if (!propSet.isEmpty()) {
structure.put("propertySet", propSet);
}
@@ -461,12 +468,13 @@ class OneNoteTreeWalker {
* @throws IOException Can throw these when manipulating the seekable byte
channel.
*/
private List<Map<String, Object>> processPropertySet(PropertySet
propertySet,
- OneNotePropertyId
parentPropertyId)
+ OneNotePropertyId
parentPropertyId,
+ int objectType)
throws IOException, TikaException, SAXException {
List<Map<String, Object>> propValues = new ArrayList<>();
for (int i = 0; i < propertySet.rgPridsData.size(); ++i) {
PropertyValue propertyValue = propertySet.rgPridsData.get(i);
- propValues.add(processPropertyValue(propertyValue,
parentPropertyId));
+ propValues.add(processPropertyValue(propertyValue,
parentPropertyId, objectType));
}
return propValues;
}
@@ -495,8 +503,9 @@ class OneNoteTreeWalker {
* @return The map parsed by this property value.
* @throws IOException Can throw these when manipulating the seekable byte
channel.
*/
- private Map<String, Object> processPropertyValue(PropertyValue
propertyValue,
- OneNotePropertyId
parentPropertyId)
+ Map<String, Object> processPropertyValue(PropertyValue propertyValue,
+ OneNotePropertyId
parentPropertyId,
+ int objectType)
throws IOException, TikaException, SAXException {
Map<String, Object> propMap = new HashMap<>();
propMap.put("oneNoteType", "PropertyValue");
@@ -615,6 +624,20 @@ class OneNoteTreeWalker {
// have the onlyLatestRevision = false
handleRichEditTextUnicode(content.size());
}
+ } else if (propertyValue.propertyId.propertyEnum ==
+ OneNotePropertyEnum.NotebookManagementEntityGuid
+ && content.size() == 16) {
+ ByteBuffer guidBuffer = ByteBuffer.allocate(16);
+ dif.read(guidBuffer);
+ String guid =
pooledGuid(GUID.fromMicrosoftBytes(guidBuffer.array()).toString());
+ propMap.put("notebookManagementEntityGuid", guid);
+ // Version-history copies of a page are object-space
roots; only the
+ // current page's metadata is referenced via
MetaDataObjectsAboveGraphSpace.
+ if (objectType != OneNoteJcid.PAGE_METADATA ||
(parentPropertyId != null
+ && parentPropertyId.propertyEnum ==
+
OneNotePropertyEnum.MetaDataObjectsAboveGraphSpace)) {
+ addClassicEntityGuid(objectType, guid);
+ }
} else {
//TODO -- these seem to be somewhat broken font files and
other
//odds and ends...what are they and how should we process
them?
@@ -633,7 +656,8 @@ class OneNoteTreeWalker {
}
}
if (propertyValue.propertySet != null &&
propertyValue.propertySet.rgPridsData != null) {
- List<Map<String, Object>> propSet =
processPropertySet(propertyValue.propertySet, parentPropertyId);
+ List<Map<String, Object>> propSet =
+ processPropertySet(propertyValue.propertySet,
parentPropertyId, objectType);
if (!propSet.isEmpty()) {
propMap.put("propertySet", propSet);
}
@@ -703,6 +727,49 @@ class OneNoteTreeWalker {
}
}
+ void addClassicEntityGuid(int objectType, String guid) {
+ guidCollector.addForObjectType(objectType, guid);
+ }
+
+ private void publishGuidBags() {
+ if (parentMetadata != null) {
+ guidCollector.publish(parentMetadata);
+ }
+ }
+
+ /**
+ * Exempt from any warning budget: capped GUID metadata must always carry
its own
+ * incompleteness signal.
+ */
+ private void recordGuidCapWarning(String warning) {
+ LOG.warn(warning);
+ if (parentMetadata != null) {
+ parentMetadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING,
warning);
+ }
+ }
+
+ // revisit-heavy graphs decode the same GUID many times; pool the
canonical string so the
+ // retained per-node maps share one instance instead of one string per
visit
+ final Map<String, String> guidStringPool = new HashMap<>();
+
+ String pooledGuid(String guid) {
+ if (guidStringPool.size() >= OneNoteGuidCollector.MAX_GUID_COUNT) {
+ return guid;
+ }
+ String pooled = guidStringPool.putIfAbsent(guid, guid);
+ return pooled != null ? pooled : guid;
+ }
+
+ static int jcidIndex(FileNode fileNode) {
+ if (fileNode == null || fileNode.subType == null
+ || fileNode.subType.objectDeclarationWithRefCount == null
+ || fileNode.subType.objectDeclarationWithRefCount.body == null
+ || fileNode.subType.objectDeclarationWithRefCount.body.jcid ==
null) {
+ return -1;
+ }
+ return (int)
fileNode.subType.objectDeclarationWithRefCount.body.jcid.index;
+ }
+
public Set<String> getAuthors() {
return authors;
}
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 ca1a724ee1..a5cad9d413 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
@@ -33,6 +33,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -49,6 +50,9 @@ import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.OneNote;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.parser.ParseContext;
+import org.apache.tika.parser.microsoft.onenote.GUID;
+import org.apache.tika.parser.microsoft.onenote.OneNoteGuidCollector;
+import org.apache.tika.parser.microsoft.onenote.OneNoteJcid;
import org.apache.tika.parser.microsoft.onenote.OneNotePropertyEnum;
import org.apache.tika.parser.microsoft.onenote.OneNoteTreeWalkerOptions;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.property.ArrayNumber;
@@ -99,6 +103,10 @@ public class MSOneStorePackage {
private static final int MAX_REFERENCE_COUNT = 100000;
private static final int MAX_PARSE_WARNINGS = 100;
+ /** Defensive bound on root objects resolved per cell; real cells declare
a handful. */
+ static final int MAX_CELL_ROOTS = 10_000;
+ private static final int FILE_IDENTITY_GUID_PROPERTY_ID = 0x1C001D94;
+
static {
LocalDateTime time32Epoch1980 = LocalDateTime.of(1980, Month.JANUARY,
1, 0, 0);
Instant instant = time32Epoch1980.atZone(ZoneOffset.UTC).toInstant();
@@ -114,6 +122,7 @@ public class MSOneStorePackage {
private final Set<String> authors = new HashSet<>();
private final Set<String> mostRecentAuthors = new HashSet<>();
private final Set<String> originalAuthors = new HashSet<>();
+ private final OneNoteGuidCollector guids = new
OneNoteGuidCollector(this::recordGuidCapWarning);
/**
* The fully populated storage index. Set this before performing
storage-index lookups; its
* mapping lists are indexed once and must not be mutated afterward.
@@ -235,45 +244,71 @@ public class MSOneStorePackage {
metadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING,
warning);
}
parseWarnings.clear();
- if (!cells.isEmpty()) {
- // Walk each page cell (object space) as a tree, starting from the
root objects of
- // its current revision and following the object references in
property order. This
- // emits the text in document order. The pages are walked in the
order in which the
- // section object space references them; cells that hold older
versions of a page
- // (the same object space in a different revision context) are
skipped.
- List<RevisionStoreCell> pageCells = new ArrayList<>();
- List<RevisionStoreCell> otherCells = new ArrayList<>();
- splitCells(pageCells, otherCells);
- for (RevisionStoreCell cell : pageCells) {
- xhtml.startElement("div", "class", "page");
- try {
- walkCell(cell, options, metadata, xhtml);
- } finally {
- xhtml.endElement("div");
+ try {
+ collectSectionFileIdentityGuids();
+ if (!cells.isEmpty()) {
+ // Walk each page cell (object space) as a tree, starting from
the root objects of
+ // its current revision and following the object references in
property order. This
+ // emits the text in document order. The pages are walked in
the order in which the
+ // section object space references them; cells that hold older
versions of a page
+ // (the same object space in a different revision context) are
skipped.
+ List<RevisionStoreCell> pageCells = new ArrayList<>();
+ List<RevisionStoreCell> otherCells = new ArrayList<>();
+ splitCells(pageCells, otherCells);
+ for (RevisionStoreCell cell : pageCells) {
+ emitPage(cell, options, metadata, xhtml);
+ }
+ for (RevisionStoreCell cell : otherCells) {
+ walkCell(cell, indexObjectsById(cell.objectGroups),
options, metadata, xhtml);
+ }
+ } else {
+ // no cell information available - walk the object groups in
revision order
+ Map<ExGuid, RevisionStoreObject> objectsById =
indexObjectsById(OtherFileNodeList);
+ Set<ExGuid> visited = new HashSet<>();
+ for (RevisionStoreObjectGroup objectGroup : OtherFileNodeList)
{
+ if (objectGroup == null || objectGroup.objects == null) {
+ continue;
+ }
+ for (RevisionStoreObject object : objectGroup.objects) {
+ walkObject(object, objectsById, visited,
AuthorRole.NONE, options, metadata,
+ xhtml, 0);
+ }
}
}
- for (RevisionStoreCell cell : otherCells) {
- walkCell(cell, options, metadata, xhtml);
+ if (!authors.isEmpty()) {
+ metadata.set(TikaCoreProperties.CREATOR,
sortedValues(authors));
}
- } else {
- // no cell information available - walk the object groups in
revision order
- Map<ExGuid, RevisionStoreObject> objectsById =
indexObjectsById(OtherFileNodeList);
- Set<ExGuid> visited = new HashSet<>();
- for (RevisionStoreObjectGroup objectGroup : OtherFileNodeList) {
- for (RevisionStoreObject object : objectGroup.objects) {
- walkObject(object, objectsById, visited, AuthorRole.NONE,
options, metadata,
- xhtml, 0);
- }
+ if (!mostRecentAuthors.isEmpty()) {
+ metadata.set(OneNote.MOST_RECENT_AUTHORS,
sortedValues(mostRecentAuthors));
}
+ if (!originalAuthors.isEmpty()) {
+ metadata.set(OneNote.ORIGINAL_AUTHORS,
sortedValues(originalAuthors));
+ }
+ } finally {
+ guids.publish(metadata);
}
- if (!authors.isEmpty()) {
- metadata.set(TikaCoreProperties.CREATOR, sortedValues(authors));
- }
- if (!mostRecentAuthors.isEmpty()) {
- metadata.set(OneNote.MOST_RECENT_AUTHORS,
sortedValues(mostRecentAuthors));
+ }
+
+ OneNoteGuidCollector guidCollector() {
+ return guids;
+ }
+
+ void emitPage(RevisionStoreCell cell, OneNoteTreeWalkerOptions options,
+ Metadata metadata, XHTMLContentHandler xhtml)
+ throws SAXException, TikaException, IOException {
+ Map<ExGuid, RevisionStoreObject> objectsById =
indexObjectsById(cell.objectGroups);
+ String pageGuid = findPageGuid(cell, objectsById);
+ guids.add(OneNoteGuidCollector.Category.PAGE, pageGuid);
+ AttributesImpl attributes = new AttributesImpl();
+ attributes.addAttribute("", "class", "class", "CDATA", "page");
+ if (pageGuid != null) {
+ attributes.addAttribute("", "id", "id", "CDATA", pageGuid);
}
- if (!originalAuthors.isEmpty()) {
- metadata.set(OneNote.ORIGINAL_AUTHORS,
sortedValues(originalAuthors));
+ xhtml.startElement("div", attributes);
+ try {
+ walkCell(cell, objectsById, options, metadata, xhtml);
+ } finally {
+ xhtml.endElement("div");
}
}
@@ -355,6 +390,7 @@ public class MSOneStorePackage {
if (object.objectID != null && !visited.add(object.objectID)) {
return;
}
+ recordEntityGuid(object, jcidIndex(object));
List<PropertyAction> actions = collectObjectActions(object);
for (PropertyAction action : actions) {
if (action.spaceReference != null) {
@@ -370,36 +406,28 @@ public class MSOneStorePackage {
}
}
- private void walkCell(RevisionStoreCell cell, OneNoteTreeWalkerOptions
options,
- Metadata metadata, XHTMLContentHandler xhtml)
+ private void walkCell(RevisionStoreCell cell,
+ Map<ExGuid, RevisionStoreObject> objectsById,
+ OneNoteTreeWalkerOptions options, Metadata metadata,
+ XHTMLContentHandler xhtml)
throws SAXException, TikaException, IOException {
- Map<ExGuid, RevisionStoreObject> objectsById =
indexObjectsById(cell.objectGroups);
Set<ExGuid> visited = new HashSet<>();
// Only objects reachable from the root objects of the current
revision are part of
// the current content. The object groups may also contain older,
superseded versions
// of objects (under a different object ID); those are intentionally
not walked.
- // A blob-only root (no property set) cannot reach the page body, so
it does not
- // count as a resolved content root: if the content root declare
dangles next to it,
- // the walk-everything fallback must still fire or the page body is
silently lost.
- boolean resolvedContentRoot = false;
- boolean unresolvedRoot = false;
- for (RevisionManifestRootDeclare rootDeclare : cell.rootDeclares) {
- RevisionStoreObject rootObject =
objectsById.get(rootDeclare.objectExGuid);
- if (rootObject == null) {
- recordParseWarning("OneNote cell root object " +
rootDeclare.objectExGuid +
- " could not be resolved");
- unresolvedRoot = true;
- } else {
- if (rootObject.propertySet != null) {
- resolvedContentRoot = true;
- }
- walkObject(rootObject, objectsById, visited, AuthorRole.NONE,
options,
- metadata, xhtml, 0);
- }
+ CellRoots roots = cellRoots(cell, objectsById,
this::recordParseWarning);
+ for (RevisionStoreObject rootObject : roots.declared) {
+ walkObject(rootObject, objectsById, visited, AuthorRole.NONE,
options, metadata,
+ xhtml, 0);
}
- if (cell.rootDeclares.isEmpty() || (unresolvedRoot &&
!resolvedContentRoot)) {
- if (cell.rootDeclares.isEmpty()) {
+ if (roots.walkAll) {
+ if (cell.rootDeclares != null && cell.rootDeclares.isEmpty()) {
recordParseWarning("OneNote cell has no declared root objects;
walking all objects");
+ } else if (roots.truncated) {
+ // reachable only when no content root survived the cap, so
the plain
+ // check is enough here
+ recordParseWarning("OneNote cell has more than " +
MAX_CELL_ROOTS
+ + " root objects; walking all objects");
} else {
recordParseWarning("OneNote cell root objects could not be
resolved; walking all objects");
}
@@ -412,6 +440,57 @@ public class MSOneStorePackage {
}
}
+ /**
+ * Resolves the objects a cell walk starts from: the root objects declared
by the current
+ * revision, then, when the declares are empty or dangle without a
property-set root, every
+ * object in the groups. 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.
+ * The same holds when the root cap truncates the declares before reaching
a content root.
+ */
+ private CellRoots cellRoots(RevisionStoreCell cell,
+ Map<ExGuid, RevisionStoreObject> objectsById,
+ Consumer<String> unresolvedRootWarning) {
+ CellRoots roots = new CellRoots();
+ if (cell.rootDeclares != null) {
+ Set<RevisionStoreObject> seenRoots = new HashSet<>();
+ for (RevisionManifestRootDeclare rootDeclare : cell.rootDeclares) {
+ RevisionStoreObject rootObject =
objectsById.get(rootDeclare.objectExGuid);
+ if (rootObject == null) {
+ unresolvedRootWarning.accept("OneNote cell root object " +
+ rootDeclare.objectExGuid + " could not be
resolved");
+ roots.unresolvedRoot = true;
+ } else if (seenRoots.add(rootObject)) {
+ if (roots.declared.size() < MAX_CELL_ROOTS) {
+ if (rootObject.propertySet != null) {
+ roots.resolvedContentRoot = true;
+ }
+ roots.declared.add(rootObject);
+ } else if (!roots.truncated) {
+ roots.truncated = true;
+ unresolvedRootWarning.accept("OneNote cell has more
than " + MAX_CELL_ROOTS
+ + " root objects; ignoring the rest");
+ }
+ }
+ }
+ }
+ roots.walkAll = cell.rootDeclares == null ||
cell.rootDeclares.isEmpty()
+ || ((roots.unresolvedRoot || roots.truncated) &&
!roots.resolvedContentRoot);
+ return roots;
+ }
+
+ /**
+ * Root resolution shared by {@link #walkCell} and
+ * {@link #findPageGuid(RevisionStoreCell, Map)}.
+ */
+ private static final class CellRoots {
+ final List<RevisionStoreObject> declared = new ArrayList<>();
+ boolean resolvedContentRoot;
+ boolean unresolvedRoot;
+ boolean truncated;
+ boolean walkAll;
+ }
+
private String[] sortedValues(Set<String> values) {
String[] sorted = values.toArray(new String[0]);
Arrays.sort(sorted);
@@ -452,16 +531,27 @@ public class MSOneStorePackage {
}
}
+ /**
+ * Exempt from the parse-warning budget: capped GUID metadata must always
carry its own
+ * incompleteness signal, even when generic warnings were suppressed.
+ */
+ private void recordGuidCapWarning(String warning) {
+ emitParseWarning(warning);
+ }
+
/**
* Builds a map of object ID to object. The object groups are ordered from
the oldest
* revision to the newest, so a newer version of an object wins over an
older one.
*/
- private Map<ExGuid, RevisionStoreObject> indexObjectsById(
+ Map<ExGuid, RevisionStoreObject> indexObjectsById(
List<RevisionStoreObjectGroup> objectGroups) {
Map<ExGuid, RevisionStoreObject> objectsById = new HashMap<>();
for (RevisionStoreObjectGroup objectGroup : objectGroups) {
+ if (objectGroup == null || objectGroup.objects == null) {
+ continue;
+ }
for (RevisionStoreObject object : objectGroup.objects) {
- if (object.objectID != null) {
+ if (object != null && object.objectID != null) {
objectsById.put(object.objectID, object);
}
}
@@ -530,6 +620,8 @@ public class MSOneStorePackage {
break;
}
}
+ int objectType = jcidIndex(object);
+ recordEntityGuid(object, objectType);
// The title structure of a page (StructureElementChildNodes) appears
above the page
// body on screen, but is declared after the body child nodes. Emit it
first so the
// text comes out in visual order.
@@ -872,7 +964,8 @@ public class MSOneStorePackage {
lastModified = lastMod;
}
metadata.set(TikaCoreProperties.MODIFIED,
String.valueOf(lastModified));
- } else if (oneNotePropertyEnum == OneNotePropertyEnum.Author) {
+ } else if (oneNotePropertyEnum == OneNotePropertyEnum.Author
+ && property instanceof PrtFourBytesOfLengthFollowedByData) {
recordAuthor(decodeOneNoteText(
((PrtFourBytesOfLengthFollowedByData) property).data),
authorRole);
} else if (propertyType ==
PropertyType.FourBytesOfLengthFollowedByData) {
@@ -939,6 +1032,169 @@ public class MSOneStorePackage {
}
}
+ private void collectSectionFileIdentityGuids() {
+ if (headerCell != null && headerCell.objectData != null
+ && headerCell.objectData.body != null) {
+ collectFileIdentityGuids(headerCell.objectData.body, 0);
+ }
+ if (!sectionGuidCollected() && dataRootCell != null) {
+ collectFileIdentityGuidsFromGroups(dataRootCell.objectGroups);
+ }
+ if (!sectionGuidCollected()) {
+ collectFileIdentityGuidsFromGroups(OtherFileNodeList);
+ }
+ }
+
+ // sectionGuid is a scalar: past the first candidate, further scanning
cannot change
+ // the output, only burn work on values the collector will refuse
+ private boolean sectionGuidCollected() {
+ return !guids.values(OneNoteGuidCollector.Category.SECTION).isEmpty();
+ }
+
+ void collectFileIdentityGuidsFromGroups(List<RevisionStoreObjectGroup>
groups) {
+ if (groups == null || guids.isFull()) {
+ return;
+ }
+ for (RevisionStoreObjectGroup group : groups) {
+ if (guids.isFull() || sectionGuidCollected()) {
+ return;
+ }
+ if (group == null || group.objects == null) {
+ continue;
+ }
+ for (RevisionStoreObject object : group.objects) {
+ if (guids.isFull()) {
+ return;
+ }
+ if (object == null || object.propertySet == null
+ || object.propertySet.objectSpaceObjectPropSet ==
null) {
+ continue;
+ }
+
collectFileIdentityGuids(object.propertySet.objectSpaceObjectPropSet.body, 0);
+ }
+ }
+ }
+
+ void collectFileIdentityGuids(PropertySet propertySet, int depth) {
+ if (propertySet == null || propertySet.rgPrids == null ||
propertySet.rgData == null
+ || depth >= PropertySet.MAX_PROPERTY_NESTING ||
guids.isFull()) {
+ return;
+ }
+ int count = Math.min(propertySet.rgPrids.length,
propertySet.rgData.size());
+ for (int i = 0; i < count && !guids.isFull() &&
!sectionGuidCollected(); i++) {
+ PropertyID propertyID = propertySet.rgPrids[i];
+ IProperty property = propertySet.rgData.get(i);
+ long propertyIdValue = Unsigned.uint(propertyID.value).longValue();
+ PropertyType propertyType =
PropertyType.fromIntVal(propertyID.type);
+ if (propertyIdValue == FILE_IDENTITY_GUID_PROPERTY_ID
+ && property instanceof PrtFourBytesOfLengthFollowedByData)
{
+ GUID guid = GUID.fromMicrosoftBytes(
+ ((PrtFourBytesOfLengthFollowedByData) property).data);
+ if (guid != null) {
+ guids.add(OneNoteGuidCollector.Category.SECTION,
guid.toString());
+ }
+ } else if (propertyType == PropertyType.PropertySet && property
instanceof PropertySet) {
+ collectFileIdentityGuids((PropertySet) property, depth + 1);
+ } else if (propertyType == PropertyType.ArrayOfPropertyValues
+ && property instanceof PrtArrayOfPropertyValues
+ && ((PrtArrayOfPropertyValues) property).data != null) {
+ for (PropertySet nested : ((PrtArrayOfPropertyValues)
property).data) {
+ if (guids.isFull() || sectionGuidCollected()) {
+ return;
+ }
+ collectFileIdentityGuids(nested, depth + 1);
+ }
+ }
+ }
+ }
+
+ void recordEntityGuid(RevisionStoreObject object, int objectType) {
+ guids.addForObjectType(objectType,
notebookManagementEntityGuid(object));
+ }
+
+ String findPageGuid(RevisionStoreCell cell,
+ Map<ExGuid, RevisionStoreObject> objectsById) {
+ if (cell == null || cell.objectGroups == null) {
+ return null;
+ }
+ Set<ExGuid> visited = new HashSet<>();
+ CellRoots roots = cellRoots(cell, objectsById, warning -> { });
+ for (RevisionStoreObject rootObject : roots.declared) {
+ String guid = findPageGuid(rootObject, objectsById, visited, 0);
+ if (guid != null) {
+ return guid;
+ }
+ }
+ if (roots.walkAll) {
+ for (RevisionStoreObjectGroup group : cell.objectGroups) {
+ if (group == null || group.objects == null) {
+ continue;
+ }
+ for (RevisionStoreObject object : group.objects) {
+ String guid = findPageGuid(object, objectsById, visited,
0);
+ if (guid != null) {
+ return guid;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ String findPageGuid(RevisionStoreObject object,
+ Map<ExGuid, RevisionStoreObject> objectsById,
+ Set<ExGuid> visited, int depth) {
+ if (object == null || depth >= MAX_OBJECT_WALK_DEPTH
+ || object.propertySet == null
+ || object.propertySet.objectSpaceObjectPropSet == null) {
+ return null;
+ }
+ if (object.objectID != null && !visited.add(object.objectID)) {
+ return null;
+ }
+ if (jcidIndex(object) == OneNoteJcid.PAGE_METADATA) {
+ String guid = notebookManagementEntityGuid(object);
+ if (guid != null) {
+ return guid;
+ }
+ }
+ for (PropertyAction action : collectObjectActions(object)) {
+ if (action.isChildReference && action.childReference != null) {
+ String guid =
findPageGuid(objectsById.get(action.childReference), objectsById,
+ visited, depth + 1);
+ if (guid != null) {
+ return guid;
+ }
+ }
+ }
+ return null;
+ }
+
+ String notebookManagementEntityGuid(RevisionStoreObject object) {
+ if (object == null || object.propertySet == null
+ || object.propertySet.objectSpaceObjectPropSet == null) {
+ return null;
+ }
+ for (PropertyAction action : collectObjectActions(object)) {
+ if (action.oneNotePropertyEnum ==
OneNotePropertyEnum.NotebookManagementEntityGuid
+ && action.property instanceof
PrtFourBytesOfLengthFollowedByData) {
+ GUID guid = GUID.fromMicrosoftBytes(
+ ((PrtFourBytesOfLengthFollowedByData)
action.property).data);
+ if (guid != null) {
+ return guid.toString();
+ }
+ }
+ }
+ return null;
+ }
+
+ static int jcidIndex(RevisionStoreObject object) {
+ if (object == null || object.jcid == null || object.jcid.jcid == null)
{
+ return -1;
+ }
+ return object.jcid.jcid.index;
+ }
+
private EmbeddedResourceInfo embeddedResourceInfo(List<PropertyAction>
actions) {
for (PropertyAction action : actions) {
if (action.property instanceof PrtFourBytesOfLengthFollowedByData
&&
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 0f21f861db..7e3c965f4d 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
@@ -17,6 +17,7 @@
package org.apache.tika.parser.microsoft.onenote;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.nio.charset.StandardCharsets;
@@ -67,4 +68,23 @@ public class GUIDTest {
malformedGuid.getBytes(StandardCharsets.UTF_16LE)));
}
}
+
+ @Test
+ public void testFromMicrosoftBytes() {
+ byte[] data = new byte[] {
+ 0x33, 0x22, 0x11, 0x00, 0x55, 0x44, 0x77, 0x66,
+ (byte) 0x88, (byte) 0x99, (byte) 0xaa, (byte) 0xbb,
+ (byte) 0xcc, (byte) 0xdd, (byte) 0xee, (byte) 0xff
+ };
+ assertEquals("{00112233-4455-6677-8899-AABBCCDDEEFF}",
+ GUID.fromMicrosoftBytes(data).toString());
+ }
+
+ @Test
+ public void testFromMicrosoftBytesRejectsInvalidLengths() {
+ assertNull(GUID.fromMicrosoftBytes(null));
+ assertNull(GUID.fromMicrosoftBytes(new byte[15]));
+ assertNull(GUID.fromMicrosoftBytes(new byte[17]));
+ assertNull(GUID.fromMicrosoftBytes(new byte[32]));
+ }
}
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/OneNoteGuidCollectorTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/onenote/OneNoteGuidCollectorTest.java
new file mode 100644
index 0000000000..9dbcba4ec9
--- /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/OneNoteGuidCollectorTest.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser.microsoft.onenote;
+
+import static org.junit.jupiter.api.Assertions.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.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.OneNote;
+
+public class OneNoteGuidCollectorTest {
+
+ @Test
+ public void testCapsTotalGuidValuesAndWarnsOnce() {
+ List<String> warnings = new ArrayList<>();
+ OneNoteGuidCollector collector = new
OneNoteGuidCollector(warnings::add);
+ assertFalse(collector.isFull());
+ collector.add(OneNoteGuidCollector.Category.PAGE, null);
+ for (int i = 0; i < OneNoteGuidCollector.MAX_GUID_COUNT; i++) {
+ collector.add(OneNoteGuidCollector.Category.CONFLICT_PAGE, "guid-"
+ i);
+ }
+ collector.add(OneNoteGuidCollector.Category.CONFLICT_PAGE, "guid-0");
+ collector.add(OneNoteGuidCollector.Category.PAGE, "overflow-1");
+ collector.add(OneNoteGuidCollector.Category.PAGE, "overflow-2");
+
+ assertEquals(OneNoteGuidCollector.MAX_GUID_COUNT,
+
collector.values(OneNoteGuidCollector.Category.CONFLICT_PAGE).size());
+
assertTrue(collector.values(OneNoteGuidCollector.Category.PAGE).isEmpty());
+ assertEquals(1, warnings.size());
+ assertTrue(collector.isFull());
+ }
+
+ @Test
+ public void testDuplicateValuesDoNotConsumeBudgetBeforeSaturation() {
+ List<String> warnings = new ArrayList<>();
+ OneNoteGuidCollector collector = new
OneNoteGuidCollector(warnings::add);
+ collector.add(OneNoteGuidCollector.Category.PAGE, "dup");
+ collector.add(OneNoteGuidCollector.Category.PAGE, "dup");
+ for (int i = 0; i < OneNoteGuidCollector.MAX_GUID_COUNT - 1; i++) {
+ collector.add(OneNoteGuidCollector.Category.CONFLICT_PAGE, "e" +
i);
+ }
+ collector.add(OneNoteGuidCollector.Category.CONFLICT_PAGE,
"final-extra");
+
+ assertEquals(1,
collector.values(OneNoteGuidCollector.Category.PAGE).size());
+ assertEquals(OneNoteGuidCollector.MAX_GUID_COUNT - 1,
+
collector.values(OneNoteGuidCollector.Category.CONFLICT_PAGE).size());
+
assertFalse(collector.values(OneNoteGuidCollector.Category.CONFLICT_PAGE)
+ .contains("final-extra"));
+ assertEquals(1, warnings.size());
+ assertTrue(collector.isFull());
+ }
+
+ @Test
+ public void testObjectTypesMapToKeys() {
+ assertEquals(OneNoteGuidCollector.Category.PAGE,
+ OneNoteGuidCollector.categoryFor(OneNoteJcid.PAGE_METADATA));
+ assertEquals(OneNoteGuidCollector.Category.PAGE_SERIES,
+
OneNoteGuidCollector.categoryFor(OneNoteJcid.PAGE_SERIES_NODE));
+ assertEquals(OneNoteGuidCollector.Category.PAGE_NODE,
+ OneNoteGuidCollector.categoryFor(OneNoteJcid.PAGE_NODE));
+ assertEquals(OneNoteGuidCollector.Category.CONFLICT_PAGE,
+
OneNoteGuidCollector.categoryFor(OneNoteJcid.CONFLICT_PAGE_METADATA));
+ assertNull(OneNoteGuidCollector.categoryFor(OneNoteJcid.SECTION_NODE));
+ assertNull(OneNoteGuidCollector.categoryFor(0x7fff));
+ }
+
+ @Test
+ public void testPublishSortsValuesAndSkipsEmptyKeys() {
+ OneNoteGuidCollector collector = new OneNoteGuidCollector(w -> { });
+ collector.addForObjectType(OneNoteJcid.PAGE_NODE, "{B}");
+ collector.addForObjectType(OneNoteJcid.PAGE_NODE, "{A}");
+ collector.addForObjectType(OneNoteJcid.SECTION_NODE, "{section-node}");
+ collector.add(OneNoteGuidCollector.Category.SECTION, "{first}");
+ collector.add(OneNoteGuidCollector.Category.SECTION, "{second}");
+ Metadata metadata = new Metadata();
+
+ collector.publish(metadata);
+
+ assertArrayEquals(new String[] {"{A}", "{B}"},
+ metadata.getValues(OneNote.PAGE_NODE_GUIDS));
+ assertEquals("{first}", metadata.get(OneNote.SECTION_GUID));
+ assertEquals(2, metadata.names().length);
+ }
+}
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 d61aa5666e..f1dceec2cc 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
@@ -18,9 +18,11 @@ package org.apache.tika.parser.microsoft.onenote;
import static
org.apache.tika.parser.microsoft.onenote.OneNoteParser.ONE_NOTE_PREFIX;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -51,6 +53,7 @@ import org.apache.tika.exception.TikaMemoryLimitException;
import org.apache.tika.extractor.EmbeddedDocumentExtractor;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.OneNote;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.MSOneStorePackage;
@@ -79,6 +82,76 @@ public class OneNoteParserTest extends TikaTest {
return metadata;
}
+ @Test
+ public void testSectionGuidIsExtractedFromClassicHeader() throws Exception
{
+ Metadata metadata = new Metadata();
+ try (InputStream input =
getClass().getResourceAsStream("/test-documents/testOneNote1.one");
+ TikaInputStream tis = TikaInputStream.get(input)) {
+ new OneNoteParser().parse(tis, new ToTextContentHandler(),
metadata,
+ new ParseContext());
+ }
+ String guid = metadata.get(OneNote.SECTION_GUID);
+ assertNotNull(guid);
+ assertTrue(guid.matches("\\{[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-"
+ + "[0-9A-F]{4}-[0-9A-F]{12}\\}"));
+ }
+
+ @Test
+ public void testClassicGuidValues() throws Exception {
+ Metadata metadata = new Metadata();
+ getText("testOneNote2.one", metadata);
+ assertEquals("{D03D94F3-AFB4-484F-A5ED-B93EBA2806B9}",
+ metadata.get(OneNote.SECTION_GUID));
+ assertArrayEquals(new String[]
{"{0AD2F2F8-F7C0-4301-82DF-064544DD31E5}",
+ "{E1FDD004-97A6-49B8-A9C9-BB781D0C5423}"},
+ metadata.getValues(OneNote.PAGE_GUIDS));
+ assertArrayEquals(new String[]
{"{135E9FB6-B9E5-42B9-B185-2A72495D0A05}",
+ "{815B61E9-5C57-43CF-BCDD-9D57A0FEDADB}"},
+ metadata.getValues(OneNote.PAGE_SERIES_GUIDS));
+ assertArrayEquals(new String[]
{"{52ADC330-8420-4DDC-A381-C8C30ECD8739}",
+ "{EE2E0DFD-9B11-4E22-AB62-53C667EF58D1}"},
+ metadata.getValues(OneNote.PAGE_NODE_GUIDS));
+ }
+
+ @Test
+ public void testClassicGuidsSkipVersionHistoryPages() throws Exception {
+ Metadata metadata = new Metadata();
+ getText("test-tika-3970-dupetext.one", metadata);
+ assertArrayEquals(new String[]
{"{70D3B6E3-B023-4A5E-AC96-6FCE9D66F63C}"},
+ metadata.getValues(OneNote.PAGE_GUIDS));
+ assertArrayEquals(new String[]
{"{BB5DE1E3-25FB-4584-A37F-69D332F40BF7}"},
+ metadata.getValues(OneNote.PAGE_SERIES_GUIDS));
+ }
+
+ @Test
+ public void testFsshttpbGuidValuesAreSorted() throws Exception {
+ Metadata metadata = new Metadata();
+ getText("testOneNoteFromOffice365.one", metadata);
+ assertEquals("{54807CE9-568A-4883-B863-BF97F35C844F}",
+ metadata.get(OneNote.SECTION_GUID));
+ assertArrayEquals(new String[]
{"{9376CA69-2A0D-440E-BFC9-A728C597ED93}",
+ "{D1962227-01A7-4F73-A1F5-410DA0C519CC}"},
+ metadata.getValues(OneNote.PAGE_GUIDS));
+ assertArrayEquals(new String[]
{"{01B28DC5-49F4-4F05-8E10-131236FAAB82}",
+ "{429CB628-77C7-4600-BE0C-83166E392792}"},
+ metadata.getValues(OneNote.PAGE_SERIES_GUIDS));
+ }
+
+ @Test
+ public void testEmitSectionGuidSkipsNullAndNil() {
+ Metadata metadata = new Metadata();
+ OneNoteParser.emitSectionFileGuid(new
OneNoteHeader().setGuidFile(null), metadata);
+ OneNoteParser.emitSectionFileGuid(new
OneNoteHeader().setGuidFile(GUID.nil()), metadata);
+ assertNull(metadata.get(OneNote.SECTION_GUID));
+
+ GUID fileGuid = new GUID(new int[] {
+ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x08,
+ 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00
+ });
+ OneNoteParser.emitSectionFileGuid(new
OneNoteHeader().setGuidFile(fileGuid), metadata);
+ assertEquals(fileGuid.toString(), metadata.get(OneNote.SECTION_GUID));
+ }
+
@Test
public void testFuzzerRegressionInputsFallBackToLegacyDump() throws
Exception {
// a structural failure falls back to the legacy string dump; the
human warning names
@@ -296,6 +369,14 @@ public class OneNoteParserTest extends TikaTest {
assertNotContained("Microsoft\u0000", originalAuthors);
assertContains("ndipiazza\u0000", mostRecentAuthors);
+ String[] pageGuids = metadata.getValues(OneNote.PAGE_GUIDS);
+ assertNotNull(pageGuids);
+ assertTrue(pageGuids.length > 0);
+ assertEquals(pageGuids.length,
Arrays.stream(pageGuids).distinct().count());
+ String[] pageSeriesGuids =
metadata.getValues(OneNote.PAGE_SERIES_GUIDS);
+ assertNotNull(pageSeriesGuids);
+ assertTrue(pageSeriesGuids.length > 0);
+
assertEquals(Instant.ofEpochSecond(1574426385),
Instant.ofEpochSecond(Long.parseLong(metadata.get(ONE_NOTE_PREFIX +
"creationTimestamp"))));
assertEquals(Instant.ofEpochMilli(1574426548000L),
@@ -399,6 +480,19 @@ public class OneNoteParserTest extends TikaTest {
assertNotNull(embedded.get(TikaCoreProperties.EMBEDDED_RELATIONSHIP_ID));
}
+ @Test
+ public void testHeaderGuidDeserializationRejectsTruncatedFile(@TempDir
Path tempDir)
+ throws Exception {
+ // fewer bytes than one GUID: reading must fail, not silently decode
0xFF filler
+ Path shortFile = tempDir.resolve("short.one");
+ Files.write(shortFile, new byte[10]);
+ try (OneNoteDirectFileResource dif = new
OneNoteDirectFileResource(shortFile.toFile())) {
+ OneNotePtr ptr = new OneNotePtr(new OneNoteDocument(), dif);
+ IOException exception = assertThrows(IOException.class,
ptr::deserializeHeader);
+ assertTrue(exception.getMessage().contains("while reading a
GUID"));
+ }
+ }
+
@Test
public void testPropertyValueBudgetIsSharedAcrossCopiesAndResetsPerList(
@TempDir Path tempDir) throws Exception {
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/OneNoteTreeWalkerGuidTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/onenote/OneNoteTreeWalkerGuidTest.java
new file mode 100644
index 0000000000..9fff0a055d
--- /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/OneNoteTreeWalkerGuidTest.java
@@ -0,0 +1,218 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser.microsoft.onenote;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.xml.sax.SAXException;
+
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.OneNote;
+import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.sax.ToTextContentHandler;
+import org.apache.tika.sax.XHTMLContentHandler;
+
+public class OneNoteTreeWalkerGuidTest {
+
+ @Test
+ public void testGuidBagsPublishAfterWalkFailure() throws Exception {
+ Metadata metadata = new Metadata();
+ OneNoteTreeWalker walker = newWalker(metadata, true, null);
+ walker.addClassicEntityGuid(OneNoteJcid.PAGE_METADATA, "page-guid");
+ walker.addClassicEntityGuid(OneNoteJcid.PAGE_SERIES_NODE,
"series-guid");
+ walker.addClassicEntityGuid(OneNoteJcid.CONFLICT_PAGE_METADATA,
"conflict-guid");
+ walker.addClassicEntityGuid(OneNoteJcid.SECTION_NODE,
"section-node-guid");
+ walker.addClassicEntityGuid(OneNoteJcid.PAGE_NODE, "page-node-guid");
+ walker.addClassicEntityGuid(0x7fff, "entity-guid");
+ for (int i = 0; i < OneNoteGuidCollector.MAX_GUID_COUNT - 4; i++) {
+ walker.addClassicEntityGuid(OneNoteJcid.PAGE_SERIES_NODE,
"series-" + i);
+ }
+ walker.addClassicEntityGuid(OneNoteJcid.PAGE_SERIES_NODE,
"overflow-guid");
+
+ assertThrows(TikaException.class, walker::walkTree);
+ assertEquals(List.of("page-guid"),
+ Arrays.asList(metadata.getValues(OneNote.PAGE_GUIDS)));
+ assertEquals(List.of("conflict-guid"),
+
Arrays.asList(metadata.getValues(OneNote.CONFLICT_PAGE_GUIDS)));
+ assertEquals(List.of("page-node-guid"),
+ Arrays.asList(metadata.getValues(OneNote.PAGE_NODE_GUIDS)));
+ assertEquals(OneNoteGuidCollector.MAX_GUID_COUNT - 3,
+ metadata.getValues(OneNote.PAGE_SERIES_GUIDS).length);
+ assertTrue(Arrays.asList(metadata.getValues(OneNote.PAGE_SERIES_GUIDS))
+ .contains("series-guid"));
+
assertFalse(Arrays.asList(metadata.getValues(OneNote.PAGE_SERIES_GUIDS))
+ .contains("overflow-guid"));
+ assertEquals(1,
Arrays.stream(metadata.getValues(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING))
+ .filter(w -> w.contains("Capping OneNote GUID
metadata")).count());
+ }
+
+ @Test
+ public void testGuidLimitWithNoMetadataParent() throws Exception {
+ OneNoteTreeWalker walker = newWalker(null, false, null);
+ for (int i = 0; i < OneNoteGuidCollector.MAX_GUID_COUNT; i++) {
+ walker.addClassicEntityGuid(OneNoteJcid.PAGE_METADATA, "guid-" +
i);
+ }
+ walker.addClassicEntityGuid(OneNoteJcid.PAGE_METADATA,
"overflow-guid");
+ walker.walkTree();
+ }
+
+ @Test
+ public void testJcidIndexRequiresCompleteJcid() {
+ assertEquals(-1, OneNoteTreeWalker.jcidIndex(null));
+ FileNode fileNode = new FileNode();
+ assertEquals(0, OneNoteTreeWalker.jcidIndex(fileNode));
+ fileNode.subType = null;
+ assertEquals(-1, OneNoteTreeWalker.jcidIndex(fileNode));
+ fileNode.subType = new FileNodeUnion();
+ fileNode.subType.objectDeclarationWithRefCount = null;
+ assertEquals(-1, OneNoteTreeWalker.jcidIndex(fileNode));
+ fileNode.subType.objectDeclarationWithRefCount = new
ObjectDeclarationWithRefCount();
+ fileNode.subType.objectDeclarationWithRefCount.body = null;
+ assertEquals(-1, OneNoteTreeWalker.jcidIndex(fileNode));
+ fileNode.subType.objectDeclarationWithRefCount.body =
+ new ObjectDeclarationWithRefCountBody();
+ fileNode.subType.objectDeclarationWithRefCount.body.jcid = null;
+ assertEquals(-1, OneNoteTreeWalker.jcidIndex(fileNode));
+ fileNode.subType.objectDeclarationWithRefCount.body.jcid = new JCID();
+ fileNode.subType.objectDeclarationWithRefCount.body.jcid.index =
OneNoteJcid.PAGE_METADATA;
+ assertEquals(OneNoteJcid.PAGE_METADATA,
OneNoteTreeWalker.jcidIndex(fileNode));
+ }
+
+ @Test
+ public void testNotebookGuidRequiresSixteenBytes(@TempDir Path tempDir)
throws Exception {
+ byte[] guidBytes = new byte[] {
+ 0x33, 0x22, 0x11, 0x00, 0x55, 0x44, 0x77, 0x66,
+ (byte) 0x88, (byte) 0x99, (byte) 0xaa, (byte) 0xbb,
+ (byte) 0xcc, (byte) 0xdd, (byte) 0xee, (byte) 0xff
+ };
+ Path input = tempDir.resolve("guid-data.bin");
+ Files.write(input, guidBytes);
+ Metadata metadata = new Metadata();
+ try (OneNoteDirectFileResource dif = new
OneNoteDirectFileResource(input.toFile())) {
+ OneNoteTreeWalker walker = newWalker(metadata, false, dif);
+ PropertyValue valid = notebookGuidProperty(guidBytes.length);
+ Map<String, Object> parsed = walker.processPropertyValue(valid,
+ parent(OneNotePropertyEnum.MetaDataObjectsAboveGraphSpace),
+ OneNoteJcid.PAGE_METADATA);
+ assertEquals("{00112233-4455-6677-8899-AABBCCDDEEFF}",
+ parsed.get("notebookManagementEntityGuid"));
+
+ PropertyValue shortGuid = notebookGuidProperty(15);
+ Map<String, Object> malformed =
walker.processPropertyValue(shortGuid,
+ parent(OneNotePropertyEnum.MetaDataObjectsAboveGraphSpace),
+ OneNoteJcid.PAGE_METADATA);
+ assertFalse(malformed.containsKey("notebookManagementEntityGuid"));
+ walker.walkTree();
+ }
+ assertEquals(List.of("{00112233-4455-6677-8899-AABBCCDDEEFF}"),
+ Arrays.asList(metadata.getValues(OneNote.PAGE_GUIDS)));
+ }
+
+ @Test
+ public void testClassicPageGuidRequiresCurrentPageReference(@TempDir Path
tempDir)
+ throws Exception {
+ byte[] guidBytes = new byte[] {
+ 0x33, 0x22, 0x11, 0x00, 0x55, 0x44, 0x77, 0x66,
+ (byte) 0x88, (byte) 0x99, (byte) 0xaa, (byte) 0xbb,
+ (byte) 0xcc, (byte) 0xdd, (byte) 0xee, (byte) 0xff
+ };
+ Path input = tempDir.resolve("guid-data.bin");
+ Files.write(input, guidBytes);
+ Metadata metadata = new Metadata();
+ try (OneNoteDirectFileResource dif = new
OneNoteDirectFileResource(input.toFile())) {
+ OneNoteTreeWalker walker = newWalker(metadata, false, dif);
+ // version-history copies are reached as object-space roots or via
other properties
+ walker.processPropertyValue(notebookGuidProperty(16), null,
+ OneNoteJcid.PAGE_METADATA);
+ walker.processPropertyValue(notebookGuidProperty(16),
+
parent(OneNotePropertyEnum.ContentChildNodesOfPageManifest),
+ OneNoteJcid.PAGE_METADATA);
+ walker.processPropertyValue(notebookGuidProperty(16), null,
+ OneNoteJcid.PAGE_SERIES_NODE);
+ walker.walkTree();
+ }
+ assertEquals(0, metadata.getValues(OneNote.PAGE_GUIDS).length);
+ assertEquals(List.of("{00112233-4455-6677-8899-AABBCCDDEEFF}"),
+ Arrays.asList(metadata.getValues(OneNote.PAGE_SERIES_GUIDS)));
+ }
+
+ private static OneNotePropertyId parent(OneNotePropertyEnum propertyEnum) {
+ return new OneNotePropertyId().setPropertyEnum(propertyEnum);
+ }
+
+ private static PropertyValue notebookGuidProperty(int length) {
+ PropertyValue value = new PropertyValue();
+ value.propertyId.propertyEnum =
OneNotePropertyEnum.NotebookManagementEntityGuid;
+ value.propertyId.type = 7;
+ value.rawData.setStp(0).setCb(length);
+ return value;
+ }
+
+ @Test
+ public void testGuidStringPoolSharesAndBoundsInstances() {
+ OneNoteTreeWalker walker = newWalker(new Metadata(), false, null);
+ // equal-but-distinct instances: an identity implementation would fail
assertSame
+ String first = walker.pooledGuid("{A}");
+ String second = walker.pooledGuid(new String("{A}"));
+ assertSame(first, second);
+
+ walker.guidStringPool.clear();
+ for (int i = 0; i < OneNoteGuidCollector.MAX_GUID_COUNT; i++) {
+ walker.guidStringPool.put("g" + i, "g" + i);
+ }
+ String input = new String("{B}");
+ assertSame(input, walker.pooledGuid(input));
+ // a full pool must not grow: without the size guard this becomes MAX
+ 1
+ assertEquals(OneNoteGuidCollector.MAX_GUID_COUNT,
walker.guidStringPool.size());
+ }
+
+ private static OneNoteTreeWalker newWalker(Metadata metadata, boolean fail,
+ OneNoteDirectFileResource dif)
{
+ ParseContext context = new ParseContext();
+ Metadata handlerMetadata = metadata == null ? new Metadata() :
metadata;
+ XHTMLContentHandler xhtml = new XHTMLContentHandler(
+ new ToTextContentHandler(), handlerMetadata, context);
+ return new OneNoteTreeWalker(new OneNoteTreeWalkerOptions(), new
OneNoteDocument(), dif,
+ xhtml, metadata, context, Pair.of(1L, ExtendedGUID.nil())) {
+ @Override
+ public List<Map<String, Object>> walkRootFileNodes()
+ throws IOException, TikaException, SAXException {
+ if (fail) {
+ throw new TikaException("test walk failure");
+ }
+ return Collections.emptyList();
+ }
+ };
+ }
+}
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 bbeadea253..9ff8a0bc14 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
@@ -16,6 +16,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;
@@ -29,6 +30,8 @@ import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -42,6 +45,8 @@ import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.OneNote;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.parser.ParseContext;
+import org.apache.tika.parser.microsoft.onenote.OneNoteGuidCollector;
+import org.apache.tika.parser.microsoft.onenote.OneNoteJcid;
import org.apache.tika.parser.microsoft.onenote.OneNoteTreeWalkerOptions;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.property.ArrayNumber;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.property.IProperty;
@@ -50,6 +55,7 @@ import
org.apache.tika.parser.microsoft.onenote.fsshttpb.property.PrtArrayOfProp
import
org.apache.tika.parser.microsoft.onenote.fsshttpb.property.PrtFourBytesOfLengthFollowedByData;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.DataElement;
import
org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.FileDataObject;
+import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.JCIDObject;
import
org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.ObjectDataBLOB;
import
org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.ObjectDataBLOBDataElementData;
import
org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.ObjectGroupObjectData;
@@ -62,6 +68,7 @@ import
org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.RevisionStore
import
org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.CellID;
import
org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.DataElementType;
import
org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.ExGuid;
+import
org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.HeaderCell;
import
org.apache.tika.parser.microsoft.onenote.fsshttpb.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;
@@ -72,6 +79,814 @@ import org.apache.tika.sax.XHTMLContentHandler;
public class MSOneStorePackageTest {
+ @Test
+ public void testSectionAndPageGuidsAreExtracted() throws Exception {
+ String sectionGuid = "{00112233-4455-6677-8899-AABBCCDDEEFF}";
+ String pageGuid = "{10213243-5465-7687-98A9-BACBDCEDFE0F}";
+ String stalePageGuid = "{20314253-6475-8697-A8B9-CADBECFD0E1F}";
+ String pageSeriesGuid = "{30415263-7485-96A7-B8C9-DAEBFC0D1E2F}";
+ String conflictPageGuid = "{20314253-6475-8697-A8B9-CADBECFD0E1F}";
+ String entityGuid = "{40516273-8495-A6B7-C8D9-EAFB0C1D2E3F}";
+ String sectionNodeGuid = "{50617283-94A5-B6C7-D8E9-FA0B1C2D3E4F}";
+ String pageNodeGuid = "{60718293-A4B5-C6D7-E8F9-0A1B2C3D4E5F}";
+ byte[] pageNodeGuidBytes = new byte[] {
+ (byte) 0x93, (byte) 0x82, 0x71, 0x60, (byte) 0xb5, (byte)
0xa4, (byte) 0xd7,
+ (byte) 0xc6, (byte) 0xe8, (byte) 0xf9, 0x0a, 0x1b, 0x2c, 0x3d,
0x4e, 0x5f
+ };
+ byte[] guidBytes = sectionGuidBytes();
+ byte[] pageGuidBytes = new byte[] {
+ 0x43, 0x32, 0x21, 0x10, 0x65, 0x54, (byte) 0x87, 0x76,
+ (byte) 0x98, (byte) 0xa9, (byte) 0xba, (byte) 0xcb,
+ (byte) 0xdc, (byte) 0xed, (byte) 0xfe, 0x0f
+ };
+ byte[] stalePageGuidBytes = new byte[] {
+ 0x53, 0x42, 0x31, 0x20, 0x75, 0x64, (byte) 0x97, (byte) 0x86,
+ (byte) 0xa8, (byte) 0xb9, (byte) 0xca, (byte) 0xdb,
+ (byte) 0xec, (byte) 0xfd, 0x0e, 0x1f
+ };
+ byte[] pageSeriesGuidBytes = new byte[] {
+ 0x63, 0x52, 0x41, 0x30, (byte) 0x85, 0x74, (byte) 0xa7, (byte)
0x96,
+ (byte) 0xb8, (byte) 0xc9, (byte) 0xda, (byte) 0xeb,
+ (byte) 0xfc, 0x0d, 0x1e, 0x2f
+ };
+ byte[] conflictPageGuidBytes = new byte[] {
+ 0x53, 0x42, 0x31, 0x20, 0x75, 0x64, (byte) 0x97, (byte) 0x86,
+ (byte) 0xa8, (byte) 0xb9, (byte) 0xca, (byte) 0xdb,
+ (byte) 0xec, (byte) 0xfd, 0x0e, 0x1f
+ };
+ byte[] entityGuidBytes = new byte[] {
+ 0x73, 0x62, 0x51, 0x40, (byte) 0x95, (byte) 0x84, (byte) 0xb7,
(byte) 0xa6,
+ (byte) 0xc8, (byte) 0xd9, (byte) 0xea, (byte) 0xfb,
+ 0x0c, 0x1d, 0x2e, 0x3f
+ };
+ byte[] sectionNodeGuidBytes = new byte[] {
+ (byte) 0x83, 0x72, 0x61, 0x50, (byte) 0xa5, (byte) 0x94,
(byte) 0xc7, (byte) 0xb6,
+ (byte) 0xd8, (byte) 0xe9, (byte) 0xfa, 0x0b, 0x1c, 0x2d, 0x3e,
0x4f
+ };
+ RevisionStoreObject stalePageMetadata = object(id(1199),
+ propertySet(new
PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C001C30, bytes(stalePageGuidBytes))),
Collections.emptyList(),
+ Collections.emptyList());
+ RevisionStoreObject pageMetadata = object(id(1200),
+ propertySet(new
PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C001C30, bytes(pageGuidBytes))),
Collections.emptyList(),
+ Collections.emptyList());
+ setJcid(stalePageMetadata, OneNoteJcid.PAGE_METADATA);
+ setJcid(pageMetadata, OneNoteJcid.PAGE_METADATA);
+ RevisionStoreCell pageCell = new RevisionStoreCell();
+ pageCell.cellID = cell(12, 1200);
+ pageCell.objectGroups.add(group(stalePageMetadata, pageMetadata));
+ pageCell.rootDeclares.add(rootDeclare(pageMetadata.objectID));
+
+ ExGuid sectionRootId = id(1201);
+ ExGuid pageSeriesId = id(1203);
+ RevisionStoreObject pageSeries = object(pageSeriesId,
+ propertySet(new
PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C001C30, bytes(pageSeriesGuidBytes))),
Collections.emptyList(),
+ Collections.emptyList());
+ setJcid(pageSeries, OneNoteJcid.PAGE_SERIES_NODE);
+ ExGuid conflictPageId = id(1204);
+ RevisionStoreObject conflictPage = object(conflictPageId,
+ propertySet(new
PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C001C30, bytes(conflictPageGuidBytes))),
Collections.emptyList(),
+ Collections.emptyList());
+ setJcid(conflictPage, OneNoteJcid.CONFLICT_PAGE_METADATA);
+ ExGuid entityId = id(1205);
+ RevisionStoreObject entity = object(entityId,
+ propertySet(new
PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C001C30, bytes(entityGuidBytes))),
Collections.emptyList(),
+ Collections.emptyList());
+ setJcid(entity, 0x7fff);
+ ExGuid sectionNodeId = id(1206);
+ RevisionStoreObject sectionNode = object(sectionNodeId,
+ propertySet(new
PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C001C30, bytes(sectionNodeGuidBytes))),
Collections.emptyList(),
+ Collections.emptyList());
+ setJcid(sectionNode, OneNoteJcid.SECTION_NODE);
+ ExGuid pageNodeId = id(1207);
+ RevisionStoreObject pageNode = object(pageNodeId,
+ propertySet(new
PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C001C30, bytes(pageNodeGuidBytes))),
Collections.emptyList(),
+ Collections.emptyList());
+ setJcid(pageNode, OneNoteJcid.PAGE_NODE);
+ RevisionStoreObject sectionRoot = object(sectionRootId,
+ propertySet(new PropertySpec(PropertyType.ArrayOfObjectIDs,
0x24001C20,
+ arrayNumber(5)),
+ new PropertySpec(PropertyType.ObjectSpaceID,
0x20001D78, new NoData())),
+ Arrays.asList(pageSeriesId, conflictPageId, entityId,
sectionNodeId, pageNodeId),
+ Collections.singletonList(pageCell.cellID));
+ RevisionStoreObject sectionIdentity = object(id(1202),
+ propertySet(new
PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C001D94, bytes(guidBytes))),
Collections.emptyList(),
+ Collections.emptyList());
+ RevisionStoreCell sectionCell = new RevisionStoreCell();
+ sectionCell.objectGroups.add(group(sectionRoot, sectionIdentity,
pageSeries,
+ conflictPage, entity, sectionNode, pageNode));
+ sectionCell.rootDeclares.add(rootDeclare(sectionRootId));
+
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.dataRootCell = sectionCell;
+ pkg.cells.add(pageCell);
+ Metadata metadata = new Metadata();
+ walk(pkg, metadata);
+
+ assertArrayEquals(new String[] {sectionGuid},
metadata.getValues(OneNote.SECTION_GUID));
+ assertArrayEquals(new String[] {pageGuid},
metadata.getValues(OneNote.PAGE_GUIDS));
+ assertFalse(Arrays.asList(metadata.getValues(OneNote.SECTION_GUID))
+ .contains(sectionNodeGuid));
+ assertArrayEquals(new String[] {pageSeriesGuid},
+ metadata.getValues(OneNote.PAGE_SERIES_GUIDS));
+ assertArrayEquals(new String[] {conflictPageGuid},
+ metadata.getValues(OneNote.CONFLICT_PAGE_GUIDS));
+ assertArrayEquals(new String[] {pageNodeGuid},
+ metadata.getValues(OneNote.PAGE_NODE_GUIDS));
+ // a JCID with no GUID key (0x7fff here) contributes no GUID metadata
+ for (String name : metadata.names()) {
+
assertFalse(Arrays.asList(metadata.getValues(name)).contains(entityGuid), name);
+ }
+ String xml = walkXml(pkg);
+ assertTrue(xml.contains("id=\"" + pageGuid + "\""));
+ assertFalse(xml.contains("id=\"" + stalePageGuid + "\""));
+ }
+
+ @Test
+ public void testWalksOtherFileNodeListWhenCellsAreMissing() throws
Exception {
+ byte[] guidBytes = new byte[] {
+ 0x43, 0x32, 0x21, 0x10, 0x65, 0x54, (byte) 0x87, 0x76,
+ (byte) 0x98, (byte) 0xa9, (byte) 0xba, (byte) 0xcb,
+ (byte) 0xdc, (byte) 0xed, (byte) 0xfe, 0x0f
+ };
+ String pageGuid = "{10213243-5465-7687-98A9-BACBDCEDFE0F}";
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.OtherFileNodeList.add(group(notebookGuidObject(1400,
OneNoteJcid.PAGE_METADATA,
+ guidBytes)));
+ Metadata metadata = new Metadata();
+
+ walk(pkg, metadata);
+
+ assertArrayEquals(new String[] {pageGuid},
metadata.getValues(OneNote.PAGE_GUIDS));
+ }
+
+ @Test
+ public void testFallbackWalkSkipsMalformedGroups() throws Exception {
+ // a null group and a group without objects must be skipped, not crash
the walk
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.OtherFileNodeList.add(null);
+ RevisionStoreObjectGroup noObjects = group();
+ noObjects.objects = null;
+ pkg.OtherFileNodeList.add(noObjects);
+ RevisionStoreObjectGroup withNullObject = group();
+ withNullObject.objects.add(null);
+ RevisionStoreObject noId = object(id(2401),
+ propertySet(new
PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C003498, text("no-id text"))),
Collections.emptyList(),
+ Collections.emptyList());
+ noId.objectID = null;
+ withNullObject.objects.add(noId);
+ withNullObject.objects.add(object(id(2400),
+ propertySet(new
PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C003498, text("fallback text"))),
Collections.emptyList(),
+ Collections.emptyList()));
+ pkg.OtherFileNodeList.add(withNullObject);
+ Metadata metadata = new Metadata();
+
+ String text = walk(pkg, metadata);
+
+ assertTrue(text.contains("fallback text"));
+ assertTrue(text.contains("no-id text"));
+ }
+
+ @Test
+ public void
testFileIdentityScannerHandlesNestedSetsArraysAndMalformedValues() {
+ byte[] firstGuidBytes = sectionGuidBytes();
+ PropertySet noProperties = new PropertySet();
+ noProperties.rgData = new ArrayList<>();
+ PropertySet noData = new PropertySet();
+ noData.rgPrids = new PropertyID[0];
+ // guid-free nested structures first: once the winning GUID is found,
the scan stops,
+ // so anything placed later would never be scanned
+ PrtArrayOfPropertyValues guidFreeArray = new
PrtArrayOfPropertyValues();
+ guidFreeArray.data = new PropertySet[] {
+ propertySet(new
PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C003498, text("nested"))), null, noProperties,
noData
+ };
+ PropertySet deep = noProperties;
+ for (int i = 0; i <= PropertySet.MAX_PROPERTY_NESTING; i++) {
+ deep = propertySet(new PropertySpec(PropertyType.PropertySet,
0x24000001, deep));
+ }
+ PrtArrayOfPropertyValues winningArray = new PrtArrayOfPropertyValues();
+ winningArray.data = new PropertySet[] {
+ fileIdentitySet(firstGuidBytes), noProperties
+ };
+ PrtArrayOfPropertyValues emptyArray = new PrtArrayOfPropertyValues();
+ PropertySet top = propertySet(
+ new PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C001D94, bytes(new byte[17])),
+ new PropertySpec(PropertyType.PropertySet, 0x24000002,
+ propertySet(new
PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C003498, text("nested")))),
+ new PropertySpec(PropertyType.PropertySet, 0x1C001D94,
+ propertySet(new
PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C003498, text("nested")))),
+ new PropertySpec(PropertyType.PropertySet, 0x24000003, new
NoData()),
+ new PropertySpec(PropertyType.ArrayOfPropertyValues,
0x24000004, guidFreeArray),
+ new PropertySpec(PropertyType.ArrayOfPropertyValues,
0x24000005, emptyArray),
+ new PropertySpec(PropertyType.ArrayOfPropertyValues,
0x24000007, new NoData()),
+ new PropertySpec(PropertyType.PropertySet, 0x24000006, deep),
+ new PropertySpec(PropertyType.ArrayOfPropertyValues,
0x24000008, winningArray),
+ new PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C003498, text("after the guid")));
+ MSOneStorePackage pkg = new MSOneStorePackage();
+
+ pkg.collectFileIdentityGuids(null, 0);
+ pkg.collectFileIdentityGuids(top, 0);
+
+
assertEquals(Collections.singletonList("{00112233-4455-6677-8899-AABBCCDDEEFF}"),
+ new ArrayList<>(sectionGuidValues(pkg)));
+
+ MSOneStorePackage full = new MSOneStorePackage();
+ fillGuidBudget(full, OneNoteGuidCollector.MAX_GUID_COUNT);
+ full.collectFileIdentityGuids(fileIdentitySet(firstGuidBytes), 0);
+ assertTrue(sectionGuidValues(full).isEmpty());
+ }
+
+ @Test
+ public void testFileIdentityGroupScannerSkipsMalformedGroups() throws
Exception {
+ String sectionGuid = "{00112233-4455-6677-8899-AABBCCDDEEFF}";
+ byte[] guidBytes = sectionGuidBytes();
+ RevisionStoreObjectGroup noObjects = group();
+ noObjects.objects = null;
+ RevisionStoreObject noPropertySet = new RevisionStoreObject();
+ RevisionStoreObject noObjectSpace = object(id(1450),
fileIdentitySet(guidBytes),
+ Collections.emptyList(), Collections.emptyList());
+ noObjectSpace.propertySet.objectSpaceObjectPropSet = null;
+ RevisionStoreObject valid = object(id(1451),
fileIdentitySet(guidBytes),
+ Collections.emptyList(), Collections.emptyList());
+ List<RevisionStoreObjectGroup> groups = new ArrayList<>(Arrays.asList(
+ null, noObjects, group((RevisionStoreObject) null),
group(noPropertySet),
+ group(noObjectSpace), group(valid)));
+ MSOneStorePackage pkg = new MSOneStorePackage();
+
+ pkg.collectFileIdentityGuidsFromGroups(null);
+ pkg.collectFileIdentityGuidsFromGroups(groups);
+
+ assertEquals(Collections.singletonList(sectionGuid), new
ArrayList<>(sectionGuidValues(pkg)));
+ }
+
+ @Test
+ public void testHeaderCellGuidAndMissingHeaderDataFallbacks() throws
Exception {
+ String sectionGuid = "{00112233-4455-6677-8899-AABBCCDDEEFF}";
+ byte[] guidBytes = sectionGuidBytes();
+ HeaderCell noObjectData = new HeaderCell();
+ HeaderCell noBody = new HeaderCell();
+ noBody.objectData = new ObjectSpaceObjectPropSet();
+ for (HeaderCell header : Arrays.asList(null, noObjectData, noBody)) {
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.headerCell = header;
+ pkg.OtherFileNodeList.add(group(object(id(1460),
fileIdentitySet(guidBytes),
+ Collections.emptyList(), Collections.emptyList())));
+ pkg.cells.add(new RevisionStoreCell());
+ Metadata metadata = new Metadata();
+ walk(pkg, metadata);
+ assertArrayEquals(new String[] {sectionGuid},
+ metadata.getValues(OneNote.SECTION_GUID));
+ }
+
+ HeaderCell header = new HeaderCell();
+ header.objectData = new ObjectSpaceObjectPropSet();
+ header.objectData.body = fileIdentitySet(guidBytes);
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.headerCell = header;
+ pkg.cells.add(new RevisionStoreCell());
+ Metadata metadata = new Metadata();
+ walk(pkg, metadata);
+ assertArrayEquals(new String[] {sectionGuid},
metadata.getValues(OneNote.SECTION_GUID));
+ }
+
+ @Test
+ public void testSectionShortCircuitStopsWithBudgetToSpare() throws
Exception {
+ byte[] secondGuidBytes = new byte[] {
+ 0x43, 0x32, 0x21, 0x10, 0x65, 0x54, (byte) 0x87, 0x76,
+ (byte) 0x98, (byte) 0xa9, (byte) 0xba, (byte) 0xcb,
+ (byte) 0xdc, (byte) 0xed, (byte) 0xfe, 0x0f
+ };
+ // the winning identity is NOT the cap-filling value, so every stop
below is
+ // attributable to the sectionCollected short-circuit alone: removing
the property
+ // loop or nested-array operand sends the scan into the deep chain,
removing the
+ // group-loop operand enters the second group - either way the entry
count grows
+ PrtArrayOfPropertyValues array = new PrtArrayOfPropertyValues();
+ array.data = new PropertySet[] {
+ fileIdentitySet(sectionGuidBytes()), propertySet()
+ };
+ PropertySet deep = propertySet();
+ for (int i = 0; i <= PropertySet.MAX_PROPERTY_NESTING; i++) {
+ deep = propertySet(new PropertySpec(PropertyType.PropertySet,
0x24000006, deep));
+ }
+ RevisionStoreObject first = object(id(1474),
+ propertySet(new
PropertySpec(PropertyType.ArrayOfPropertyValues,
+ 0x24000007, array),
+ new PropertySpec(PropertyType.PropertySet, 0x24000006,
deep)),
+ Collections.emptyList(), Collections.emptyList());
+ RevisionStoreObject afterLimit = object(id(1475),
fileIdentitySet(secondGuidBytes),
+ Collections.emptyList(), Collections.emptyList());
+ CountingMSOneStorePackage pkg = new CountingMSOneStorePackage();
+ fillGuidBudget(pkg, OneNoteGuidCollector.MAX_GUID_COUNT - 5);
+ pkg.dataRootCell = new RevisionStoreCell();
+ pkg.dataRootCell.objectGroups.add(group(first));
+ pkg.dataRootCell.objectGroups.add(group(afterLimit));
+ Metadata metadata = new Metadata();
+
+ walk(pkg, metadata);
+
+ assertArrayEquals(new String[]
{"{00112233-4455-6677-8899-AABBCCDDEEFF}"},
+ metadata.getValues(OneNote.SECTION_GUID));
+ assertEquals(2, pkg.identityScanEntries);
+ assertEquals(0,
Arrays.stream(metadata.getValues(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING))
+ .filter(w -> w.contains("Capping OneNote GUID
metadata")).count());
+ }
+
+ @Test
+ public void testSectionIdentityScanStopsAtGuidLimit() throws Exception {
+ byte[] firstGuidBytes = sectionGuidBytes();
+ byte[] secondGuidBytes = new byte[] {
+ 0x43, 0x32, 0x21, 0x10, 0x65, 0x54, (byte) 0x87, 0x76,
+ (byte) 0x98, (byte) 0xa9, (byte) 0xba, (byte) 0xcb,
+ (byte) 0xdc, (byte) 0xed, (byte) 0xfe, 0x0f
+ };
+ PrtArrayOfPropertyValues array = new PrtArrayOfPropertyValues();
+ array.data = new PropertySet[] {
+ fileIdentitySet(firstGuidBytes),
fileIdentitySet(secondGuidBytes)
+ };
+ RevisionStoreObject withArray = object(id(1470),
+ propertySet(new
PropertySpec(PropertyType.ArrayOfPropertyValues,
+ 0x24000007, array)), Collections.emptyList(),
Collections.emptyList());
+ RevisionStoreObject afterLimit = object(id(1471),
fileIdentitySet(secondGuidBytes),
+ Collections.emptyList(), Collections.emptyList());
+ CountingMSOneStorePackage pkg = new CountingMSOneStorePackage();
+ fillGuidBudget(pkg, OneNoteGuidCollector.MAX_GUID_COUNT - 1);
+ pkg.dataRootCell = new RevisionStoreCell();
+ pkg.dataRootCell.objectGroups.add(group(withArray));
+ pkg.dataRootCell.objectGroups.add(group(afterLimit));
+ Metadata metadata = new Metadata();
+
+ walk(pkg, metadata);
+
+ assertArrayEquals(new String[]
{"{00112233-4455-6677-8899-AABBCCDDEEFF}"},
+ metadata.getValues(OneNote.SECTION_GUID));
+ // exact-work check: the nested array's first element wins, so the
scan must not
+ // enter the second element, the rest of the property list, or the
second group
+ assertEquals(2, pkg.identityScanEntries);
+ assertEquals(1,
Arrays.stream(metadata.getValues(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING))
+ .filter(w -> w.contains("Capping OneNote GUID
metadata")).count());
+ }
+
+ @Test
+ public void testSectionScanStopsAtLimitWithExactWork() throws Exception {
+ byte[] secondGuidBytes = new byte[] {
+ 0x43, 0x32, 0x21, 0x10, 0x65, 0x54, (byte) 0x87, 0x76,
+ (byte) 0x98, (byte) 0xa9, (byte) 0xba, (byte) 0xcb,
+ (byte) 0xdc, (byte) 0xed, (byte) 0xfe, 0x0f
+ };
+ // a nested property-set chain deeper than MAX_PROPERTY_NESTING:
isFull and
+ // sectionCollected both fire at the winning identity here, so the
chain is reached
+ // only when BOTH stop conditions are removed (removing one leaves the
other
+ // stopping the scan); single-operand attribution is covered by
+ // testSectionShortCircuitStopsWithBudgetToSpare
+ PropertySet deep = propertySet();
+ for (int i = 0; i <= PropertySet.MAX_PROPERTY_NESTING; i++) {
+ deep = propertySet(new PropertySpec(PropertyType.PropertySet,
0x24000006, deep));
+ }
+ RevisionStoreObject first = object(id(1472),
+ propertySet(new
PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C001D94, bytes(sectionGuidBytes())),
+ new PropertySpec(PropertyType.PropertySet, 0x24000006,
deep)),
+ Collections.emptyList(), Collections.emptyList());
+ RevisionStoreObject afterLimit = object(id(1473),
fileIdentitySet(secondGuidBytes),
+ Collections.emptyList(), Collections.emptyList());
+ CountingMSOneStorePackage pkg = new CountingMSOneStorePackage();
+ fillGuidBudget(pkg, OneNoteGuidCollector.MAX_GUID_COUNT - 1);
+ pkg.dataRootCell = new RevisionStoreCell();
+ pkg.dataRootCell.objectGroups.add(group(first, afterLimit));
+ Metadata metadata = new Metadata();
+
+ walk(pkg, metadata);
+
+ assertArrayEquals(new String[]
{"{00112233-4455-6677-8899-AABBCCDDEEFF}"},
+ metadata.getValues(OneNote.SECTION_GUID));
+ assertEquals(1, pkg.identityScanEntries);
+ assertEquals(1,
Arrays.stream(metadata.getValues(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING))
+ .filter(w -> w.contains("Capping OneNote GUID
metadata")).count());
+ }
+
+ @Test
+ public void testSectionGuidSourcePrecedence() throws Exception {
+ byte[] dataRootBytes = new byte[] {
+ 0x43, 0x32, 0x21, 0x10, 0x65, 0x54, (byte) 0x87, 0x76,
+ (byte) 0x98, (byte) 0xa9, (byte) 0xba, (byte) 0xcb,
+ (byte) 0xdc, (byte) 0xed, (byte) 0xfe, 0x0f
+ };
+ byte[] fallbackBytes = new byte[] {
+ 0x63, 0x52, 0x41, 0x30, (byte) 0x85, 0x74, (byte) 0xa7, (byte)
0x96,
+ (byte) 0xb8, (byte) 0xc9, (byte) 0xda, (byte) 0xeb,
+ (byte) 0xfc, 0x0d, 0x1e, 0x2f
+ };
+
+ // the header wins over both lower-priority sources
+ MSOneStorePackage withHeader = new MSOneStorePackage();
+ withHeader.headerCell = new HeaderCell();
+ withHeader.headerCell.objectData = new ObjectSpaceObjectPropSet();
+ withHeader.headerCell.objectData.body =
fileIdentitySet(sectionGuidBytes());
+ withHeader.dataRootCell = new RevisionStoreCell();
+ withHeader.dataRootCell.objectGroups.add(group(object(id(1500),
+ fileIdentitySet(dataRootBytes), Collections.emptyList(),
+ Collections.emptyList())));
+ withHeader.OtherFileNodeList.add(group(object(id(1501),
+ fileIdentitySet(fallbackBytes), Collections.emptyList(),
+ Collections.emptyList())));
+ Metadata headerMetadata = new Metadata();
+ walk(withHeader, headerMetadata);
+ assertEquals("{00112233-4455-6677-8899-AABBCCDDEEFF}",
+ headerMetadata.get(OneNote.SECTION_GUID));
+
+ // without a usable header, the data root wins over the
OtherFileNodeList fallback
+ MSOneStorePackage withoutHeader = new MSOneStorePackage();
+ withoutHeader.dataRootCell = new RevisionStoreCell();
+ withoutHeader.dataRootCell.objectGroups.add(group(object(id(1502),
+ fileIdentitySet(dataRootBytes), Collections.emptyList(),
+ Collections.emptyList())));
+ withoutHeader.OtherFileNodeList.add(group(object(id(1503),
+ fileIdentitySet(fallbackBytes), Collections.emptyList(),
+ Collections.emptyList())));
+ Metadata fallbackMetadata = new Metadata();
+ walk(withoutHeader, fallbackMetadata);
+ assertEquals("{10213243-5465-7687-98A9-BACBDCEDFE0F}",
+ fallbackMetadata.get(OneNote.SECTION_GUID));
+
+ // a scan entered after the section GUID was found must stop at the
group check
+ // (budget not full, so the short-circuit is the sectionCollected
operand): the
+ // assertion pins the collector at exactly one SECTION value, so
collecting the
+ // fallback GUID here would fail
+
withHeader.collectFileIdentityGuidsFromGroups(withHeader.OtherFileNodeList);
+
assertEquals(Collections.singleton("{00112233-4455-6677-8899-AABBCCDDEEFF}"),
+ sectionGuidValues(withHeader));
+ assertEquals("{00112233-4455-6677-8899-AABBCCDDEEFF}",
+ headerMetadata.get(OneNote.SECTION_GUID));
+ }
+
+ @Test
+ public void testCellRootsDeduplicatesAndCapsRoots() throws Exception {
+ RevisionStoreCell cell = new RevisionStoreCell();
+ RevisionStoreObject textRoot = object(id(2100), propertySet(
+ new PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C003498, text("root text"))),
Collections.emptyList(),
+ Collections.emptyList());
+ cell.rootDeclares.add(rootDeclare(textRoot.objectID));
+ cell.rootDeclares.add(rootDeclare(textRoot.objectID));
+ List<RevisionStoreObject> roots = new ArrayList<>();
+ roots.add(textRoot);
+ for (int i = 0; i <= MSOneStorePackage.MAX_CELL_ROOTS; i++) {
+ RevisionStoreObject root = object(id(2101 + i), propertySet(),
+ Collections.emptyList(), Collections.emptyList());
+ roots.add(root);
+ cell.rootDeclares.add(rootDeclare(root.objectID));
+ }
+ cell.objectGroups.add(group(roots.toArray(new
RevisionStoreObject[0])));
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.cells.add(cell);
+ Metadata metadata = new Metadata();
+
+ String xml = walk(pkg, metadata);
+
+ assertTrue(xml.contains("root text"));
+ assertEquals(1,
Arrays.stream(metadata.getValues(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING))
+ .filter(w -> w.contains("more than " +
MSOneStorePackage.MAX_CELL_ROOTS
+ + " root objects")).count());
+ }
+
+ @Test
+ public void testCellRootCapTruncationStillWalksContentRoot() throws
Exception {
+ // the cap must not silence the walk-everything fallback: 10,000
blob-only roots
+ // fill it, so the real content root is declare #10,001 and gets
truncated
+ RevisionStoreCell cell = new RevisionStoreCell();
+ List<RevisionStoreObject> roots = new ArrayList<>();
+ for (int i = 0; i < MSOneStorePackage.MAX_CELL_ROOTS; i++) {
+ RevisionStoreObject blob = new RevisionStoreObject();
+ blob.objectID = id(2200 + i);
+ roots.add(blob);
+ cell.rootDeclares.add(rootDeclare(blob.objectID));
+ }
+ RevisionStoreObject contentRoot = object(id(2199),
+ propertySet(new
PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C003498, text("root text"))),
Collections.emptyList(),
+ Collections.emptyList());
+ roots.add(contentRoot);
+ cell.rootDeclares.add(rootDeclare(contentRoot.objectID));
+ cell.objectGroups.add(group(roots.toArray(new
RevisionStoreObject[0])));
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.cells.add(cell);
+ Metadata metadata = new Metadata();
+
+ String xml = walk(pkg, metadata);
+
+ assertTrue(xml.contains("root text"));
+ assertEquals(1,
Arrays.stream(metadata.getValues(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING))
+ .filter(w -> w.contains("more than " +
MSOneStorePackage.MAX_CELL_ROOTS
+ + " root objects; ignoring the rest")).count());
+ assertEquals(1,
Arrays.stream(metadata.getValues(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING))
+ .filter(w -> w.contains("more than " +
MSOneStorePackage.MAX_CELL_ROOTS
+ + " root objects; walking all objects")).count());
+ }
+
+ @Test
+ public void testCellRootsDeduplicatesRootDeclares() throws Exception {
+ // two declares of the same object plus MAX_CELL_ROOTS - 1 distinct
roots fit the cap
+ // exactly when deduplicated; without dedupe the duplicate consumes a
slot and the
+ // last distinct root is truncated, so the absent-truncation assertion
fails
+ RevisionStoreCell cell = new RevisionStoreCell();
+ RevisionStoreObject textRoot = object(id(2300), propertySet(
+ new PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C003498, text("root text"))),
Collections.emptyList(),
+ Collections.emptyList());
+ cell.rootDeclares.add(rootDeclare(textRoot.objectID));
+ cell.rootDeclares.add(rootDeclare(textRoot.objectID));
+ List<RevisionStoreObject> roots = new ArrayList<>();
+ roots.add(textRoot);
+ for (int i = 0; i < MSOneStorePackage.MAX_CELL_ROOTS - 1; i++) {
+ RevisionStoreObject root = object(id(2301 + i), propertySet(),
+ Collections.emptyList(), Collections.emptyList());
+ roots.add(root);
+ cell.rootDeclares.add(rootDeclare(root.objectID));
+ }
+ cell.objectGroups.add(group(roots.toArray(new
RevisionStoreObject[0])));
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.cells.add(cell);
+ Metadata metadata = new Metadata();
+
+ String xml = walk(pkg, metadata);
+
+ assertTrue(xml.contains("root text"));
+ assertEquals(0,
Arrays.stream(metadata.getValues(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING))
+ .filter(w -> w.contains("more than " +
MSOneStorePackage.MAX_CELL_ROOTS
+ + " root objects")).count());
+ }
+
+ @Test
+ public void testPageGuidLookupFollowsReferencesAndFallbackRoots() throws
Exception {
+ String pageGuid = "{10213243-5465-7687-98A9-BACBDCEDFE0F}";
+ byte[] guidBytes = new byte[] {
+ 0x43, 0x32, 0x21, 0x10, 0x65, 0x54, (byte) 0x87, 0x76,
+ (byte) 0x98, (byte) 0xa9, (byte) 0xba, (byte) 0xcb,
+ (byte) 0xdc, (byte) 0xed, (byte) 0xfe, 0x0f
+ };
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ RevisionStoreObject page = notebookGuidObject(1480,
OneNoteJcid.PAGE_METADATA, guidBytes);
+ RevisionStoreObject parent = object(id(1481),
+ propertySet(new PropertySpec(PropertyType.ObjectID,
0x20001D78, new NoData()),
+ new
PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C003498, text("non-reference property"))),
+ Collections.singletonList(page.objectID),
Collections.emptyList());
+ Map<ExGuid, RevisionStoreObject> objectsById = new HashMap<>();
+ objectsById.put(parent.objectID, parent);
+ objectsById.put(page.objectID, page);
+ RevisionStoreCell cell = new RevisionStoreCell();
+ cell.objectGroups.add(group(parent, page));
+ cell.rootDeclares.add(rootDeclare(parent.objectID));
+
+ assertEquals(pageGuid, pkg.findPageGuid(cell, objectsById));
+
+ RevisionStoreCell fallbackCell = new RevisionStoreCell();
+ RevisionStoreObjectGroup noObjects = group();
+ noObjects.objects = null;
+ fallbackCell.objectGroups.add(null);
+ fallbackCell.objectGroups.add(noObjects);
+ fallbackCell.objectGroups.add(group(parent, page));
+ assertEquals(pageGuid, pkg.findPageGuid(fallbackCell, objectsById));
+ fallbackCell.rootDeclares.add(rootDeclare(id(1482)));
+ assertEquals(pageGuid, pkg.findPageGuid(fallbackCell, objectsById));
+ fallbackCell.rootDeclares = null;
+ assertEquals(pageGuid, pkg.findPageGuid(fallbackCell, objectsById));
+ assertNull(pkg.findPageGuid(null, objectsById));
+ RevisionStoreCell noGroups = new RevisionStoreCell();
+ noGroups.objectGroups = null;
+ assertNull(pkg.findPageGuid(noGroups, objectsById));
+
+ RevisionStoreObject resolvedRoot = object(id(1483),
+ propertySet(new
PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C003498, text("resolved root without page GUID"))),
+ Collections.emptyList(), Collections.emptyList());
+ Map<ExGuid, RevisionStoreObject> roots = new HashMap<>(objectsById);
+ roots.put(resolvedRoot.objectID, resolvedRoot);
+ RevisionStoreCell noUnreferencedFallback = new RevisionStoreCell();
+ noUnreferencedFallback.objectGroups.add(group(resolvedRoot, page));
+
noUnreferencedFallback.rootDeclares.add(rootDeclare(resolvedRoot.objectID));
+ assertNull(pkg.findPageGuid(noUnreferencedFallback, roots));
+ noUnreferencedFallback.rootDeclares.clear();
+ noUnreferencedFallback.rootDeclares.add(rootDeclare(id(1482)));
+
noUnreferencedFallback.rootDeclares.add(rootDeclare(resolvedRoot.objectID));
+ assertNull(pkg.findPageGuid(noUnreferencedFallback, roots));
+ RevisionStoreObject rootWithoutPropertySet = new RevisionStoreObject();
+ rootWithoutPropertySet.objectID = id(1486);
+ Map<ExGuid, RevisionStoreObject> noPropertyRoots = new
HashMap<>(objectsById);
+ noPropertyRoots.put(rootWithoutPropertySet.objectID,
rootWithoutPropertySet);
+ RevisionStoreCell noPropertyRoot = new RevisionStoreCell();
+ noPropertyRoot.objectGroups.add(group(rootWithoutPropertySet, page));
+
noPropertyRoot.rootDeclares.add(rootDeclare(rootWithoutPropertySet.objectID));
+ assertNull(pkg.findPageGuid(noPropertyRoot, noPropertyRoots));
+ RevisionStoreCell emptyFallback = new RevisionStoreCell();
+ assertNull(pkg.findPageGuid(emptyFallback, objectsById));
+ RevisionStoreCell failedFallback = new RevisionStoreCell();
+ failedFallback.objectGroups.add(group());
+ failedFallback.objectGroups.add(group(rootWithoutPropertySet));
+ assertNull(pkg.findPageGuid(failedFallback, objectsById));
+
+ assertNull(pkg.findPageGuid((RevisionStoreObject) null, objectsById,
+ new HashSet<>(), 0));
+ assertNull(pkg.findPageGuid(page, objectsById,
+ new HashSet<>(Collections.singleton(page.objectID)), 0));
+ assertNull(pkg.findPageGuid(page, objectsById, new HashSet<>(), 1000));
+ RevisionStoreObject malformedPage = notebookGuidObject(1487,
+ OneNoteJcid.PAGE_METADATA, new byte[17]);
+ assertNull(pkg.findPageGuid(malformedPage, objectsById, new
HashSet<>(), 0));
+ RevisionStoreObject noChildReference = object(id(1488),
+ propertySet(new PropertySpec(PropertyType.ObjectID,
0x20001D78, new NoData())),
+ Collections.emptyList(), Collections.emptyList());
+ assertNull(pkg.findPageGuid(noChildReference, objectsById, new
HashSet<>(), 0));
+ RevisionStoreObject noPropertySet = new RevisionStoreObject();
+ assertNull(pkg.findPageGuid(noPropertySet, objectsById, new
HashSet<>(), 0));
+ RevisionStoreObject noObjectSpace = object(id(1484),
+ propertySet(new PropertySpec(PropertyType.NoData, 0x20001D78,
new NoData())),
+ Collections.emptyList(), Collections.emptyList());
+ noObjectSpace.propertySet.objectSpaceObjectPropSet = null;
+ assertNull(pkg.findPageGuid(noObjectSpace, objectsById, new
HashSet<>(), 0));
+ RevisionStoreObject noObjectId = notebookGuidObject(1485,
+ OneNoteJcid.PAGE_METADATA, guidBytes);
+ noObjectId.objectID = null;
+ assertEquals(pageGuid, pkg.findPageGuid(noObjectId, objectsById, new
HashSet<>(), 0));
+ }
+
+ @Test
+ public void testPageEmissionBuildsOneObjectIndex() throws Exception {
+ String pageGuid = "{10213243-5465-7687-98A9-BACBDCEDFE0F}";
+ byte[] guidBytes = new byte[] {
+ 0x43, 0x32, 0x21, 0x10, 0x65, 0x54, (byte) 0x87, 0x76,
+ (byte) 0x98, (byte) 0xa9, (byte) 0xba, (byte) 0xcb,
+ (byte) 0xdc, (byte) 0xed, (byte) 0xfe, 0x0f
+ };
+ CountingMSOneStorePackage pkg = new CountingMSOneStorePackage();
+ RevisionStoreCell cell = new RevisionStoreCell();
+ cell.objectGroups.add(group(notebookGuidObject(1498,
OneNoteJcid.PAGE_METADATA,
+ guidBytes)));
+ Metadata metadata = new Metadata();
+ ParseContext context = new ParseContext();
+ ToXMLContentHandler xml = new ToXMLContentHandler();
+ XHTMLContentHandler xhtml = new XHTMLContentHandler(xml, metadata,
context);
+ xhtml.startDocument();
+
+ pkg.emitPage(cell, new OneNoteTreeWalkerOptions(), metadata, xhtml);
+
+ xhtml.endDocument();
+ assertEquals(1, pkg.objectIndexBuilds);
+ assertTrue(xml.toString().contains("id=\"" + pageGuid + "\""));
+ }
+
+ @Test
+ public void
testNotebookManagementGuidRejectsMissingAndMalformedProperties() throws
Exception {
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ assertNull(pkg.notebookManagementEntityGuid(null));
+ assertNull(pkg.notebookManagementEntityGuid(new
RevisionStoreObject()));
+
+ RevisionStoreObject noObjectSpace = object(id(1490),
+ propertySet(new PropertySpec(PropertyType.NoData, 0x20001D78,
new NoData())),
+ Collections.emptyList(), Collections.emptyList());
+ noObjectSpace.propertySet.objectSpaceObjectPropSet = null;
+ assertNull(pkg.notebookManagementEntityGuid(noObjectSpace));
+
+ byte[] validGuid = sectionGuidBytes();
+ RevisionStoreObject unrelated = object(id(1491),
+ propertySet(new
PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C003498, bytes(validGuid))),
Collections.emptyList(),
+ Collections.emptyList());
+ assertNull(pkg.notebookManagementEntityGuid(unrelated));
+ RevisionStoreObject wrongType = object(id(1492),
+ propertySet(new PropertySpec(PropertyType.NoData, 0x1C001C30,
new NoData())),
+ Collections.emptyList(), Collections.emptyList());
+ assertNull(pkg.notebookManagementEntityGuid(wrongType));
+ RevisionStoreObject malformed = object(id(1493),
+ propertySet(new
PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C001C30, bytes(new byte[17]))),
Collections.emptyList(),
+ Collections.emptyList());
+ assertNull(pkg.notebookManagementEntityGuid(malformed));
+ RevisionStoreObject valid = notebookGuidObject(1494,
OneNoteJcid.PAGE_METADATA,
+ validGuid);
+ assertEquals("{00112233-4455-6677-8899-AABBCCDDEEFF}",
+ pkg.notebookManagementEntityGuid(valid));
+ pkg.recordEntityGuid(null, OneNoteJcid.PAGE_METADATA);
+ }
+
+ @Test
+ public void testFssJcidIndexRequiresCompleteJcid() throws Exception {
+ assertEquals(-1, MSOneStorePackage.jcidIndex(null));
+ RevisionStoreObject object = new RevisionStoreObject();
+ assertEquals(-1, MSOneStorePackage.jcidIndex(object));
+ object.jcid = new JCIDObject(null, emptyObjectData());
+ object.jcid.jcid = null;
+ assertEquals(-1, MSOneStorePackage.jcidIndex(object));
+ object.jcid = new JCIDObject(null, emptyObjectData());
+ object.jcid.jcid.index = OneNoteJcid.PAGE_METADATA;
+ assertEquals(OneNoteJcid.PAGE_METADATA,
MSOneStorePackage.jcidIndex(object));
+ }
+
+ @Test
+ public void testGuidCapWarningIsPublishedAsParseWarning() throws Exception
{
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ fillGuidBudget(pkg, OneNoteGuidCollector.MAX_GUID_COUNT);
+ pkg.guidCollector().add(OneNoteGuidCollector.Category.PAGE,
"overflow-1");
+
+ Metadata metadata = new Metadata();
+ walk(pkg, metadata);
+ assertEquals(OneNoteGuidCollector.MAX_GUID_COUNT,
+ metadata.getValues(OneNote.PAGE_GUIDS).length);
+ String[] warnings =
metadata.getValues(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING);
+ assertEquals(1, Arrays.stream(warnings)
+ .filter(warning -> warning.contains("Capping OneNote GUID
metadata")).count());
+ }
+
+ @Test
+ public void testGuidCapWarningSurvivesSuppressedParseWarnings() throws
Exception {
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ for (int i = 0; i < 101; i++) {
+ pkg.recordParseWarning("warning " + i);
+ }
+ fillGuidBudget(pkg, OneNoteGuidCollector.MAX_GUID_COUNT - 1);
+ RevisionStoreObject pageMetadata = notebookGuidObject(1304,
OneNoteJcid.PAGE_METADATA,
+ new byte[] {
+ 0x43, 0x32, 0x21, 0x10, 0x65, 0x54, (byte) 0x87, 0x76,
+ (byte) 0x98, (byte) 0xa9, (byte) 0xba, (byte) 0xcb,
+ (byte) 0xdc, (byte) 0xed, (byte) 0xfe, 0x0f
+ });
+ RevisionStoreCell pageCell = new RevisionStoreCell();
+ pageCell.objectGroups.add(group(pageMetadata));
+ pageCell.rootDeclares.add(rootDeclare(pageMetadata.objectID));
+ pkg.cells.add(pageCell);
+ Metadata metadata = new Metadata();
+
+ walk(pkg, metadata);
+
+ String[] warnings =
metadata.getValues(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING);
+ assertTrue(Arrays.stream(warnings)
+ .anyMatch(warning -> warning.contains("Additional OneNote
parse warnings were suppressed")));
+ // the cap signal is exempt from the budget the generic notice consumed
+ assertEquals(1, Arrays.stream(warnings)
+ .filter(warning -> warning.contains("Capping OneNote GUID
metadata")).count());
+ assertEquals(OneNoteGuidCollector.MAX_GUID_COUNT,
+ metadata.getValues(OneNote.PAGE_GUIDS).length);
+ }
+
+ @Test
+ public void testGuidsCollectedBeforeWalkFailureArePublished() throws
Exception {
+ String sectionGuid = "{00112233-4455-6677-8899-AABBCCDDEEFF}";
+ String pageGuid = "{10213243-5465-7687-98A9-BACBDCEDFE0F}";
+ RevisionStoreObject sectionIdentity = object(id(1302),
+ propertySet(new
PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C001D94, bytes(sectionGuidBytes()))),
Collections.emptyList(),
+ Collections.emptyList());
+ RevisionStoreObject pageMetadata = object(id(1303),
+ propertySet(new
PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C001C30, bytes(new byte[] {
+ 0x43, 0x32, 0x21, 0x10, 0x65, 0x54, (byte)
0x87, 0x76,
+ (byte) 0x98, (byte) 0xa9, (byte) 0xba, (byte)
0xcb,
+ (byte) 0xdc, (byte) 0xed, (byte) 0xfe, 0x0f
+ }))), Collections.emptyList(),
+ Collections.emptyList());
+ setJcid(pageMetadata, OneNoteJcid.PAGE_METADATA);
+ RevisionStoreCell validCell = new RevisionStoreCell();
+ validCell.objectGroups.add(group(pageMetadata));
+ validCell.rootDeclares.add(rootDeclare(pageMetadata.objectID));
+ RevisionStoreCell damagedCell = new RevisionStoreCell();
+ damagedCell.objectGroups = null;
+
+ // deliberate failure seam: a future null guard inside
indexObjectsById must not
+ // turn this test green for the wrong reason
+ MSOneStorePackage pkg = new MSOneStorePackage() {
+ @Override
+ Map<ExGuid, RevisionStoreObject> indexObjectsById(
+ List<RevisionStoreObjectGroup> objectGroups) {
+ if (objectGroups == null) {
+ throw new IllegalStateException("forced index failure");
+ }
+ return super.indexObjectsById(objectGroups);
+ }
+ };
+ pkg.OtherFileNodeList.add(group(sectionIdentity));
+ pkg.cells.add(validCell);
+ pkg.cells.add(damagedCell);
+ Metadata metadata = new Metadata();
+
+ assertThrows(IllegalStateException.class, () -> walk(pkg, metadata));
+ assertArrayEquals(new String[] {sectionGuid},
metadata.getValues(OneNote.SECTION_GUID));
+ assertArrayEquals(new String[] {pageGuid},
metadata.getValues(OneNote.PAGE_GUIDS));
+ }
+
@Test
public void testParseWarningsAreBoundedAcrossParserAndWalkPhases() throws
Exception {
MSOneStorePackage pkg = new MSOneStorePackage();
@@ -135,6 +950,10 @@ public class MSOneStorePackageTest {
RevisionStoreCell oldPageOneCell = cellWithText(oldPageOne, "old page
one");
RevisionStoreCell pageOneCell = cellWithText(pageOne, "page one");
RevisionStoreCell unrelatedCell = cellWithText(cell(30, 300),
"unrelated");
+ // reverse-lexical page GUIDs: page one's GUID sorts after page two's,
so the sorted
+ // bag order cannot be mistaken for document order
+ attachPageGuid(pageOneCell, 3100, (byte) 0x60);
+ attachPageGuid(pageTwoCell, 3200, (byte) 0x50);
RevisionStoreObject sectionRoot = object(sectionRootId,
propertySet(new PropertySpec(PropertyType.ObjectSpaceID,
0x20001D78,
@@ -156,12 +975,25 @@ public class MSOneStorePackageTest {
pkg.cells.addAll(Arrays.asList(pageTwoCell, oldPageOneCell,
pageOneCell, unrelatedCell));
Metadata metadata = new Metadata();
- String text = walk(pkg, metadata);
+ // div ids are attributes, so the pairing assertions need the XML
output
+ String text = walkXml(pkg, metadata);
assertTrue(text.indexOf("page one") >= 0);
assertTrue(text.indexOf("page two") >= 0);
assertTrue(text.indexOf("page one") < text.indexOf("page two"));
assertFalse(text.contains("old page one"));
assertTrue(text.contains("unrelated"));
+ // each page div carries its own page GUID, in document order
+ int idPageOne =
text.indexOf("id=\"{60000000-0000-0000-0000-000000000000}\"");
+ int idPageTwo =
text.indexOf("id=\"{50000000-0000-0000-0000-000000000000}\"");
+ assertTrue(idPageOne >= 0);
+ assertTrue(idPageTwo >= 0);
+ assertTrue(idPageOne < text.indexOf("page one"));
+ assertTrue(text.indexOf("page one") < idPageTwo);
+ assertTrue(idPageTwo < text.indexOf("page two"));
+ // the published bag is sorted, not document-ordered
+ assertArrayEquals(new String[]
{"{50000000-0000-0000-0000-000000000000}",
+ "{60000000-0000-0000-0000-000000000000}"},
+ metadata.getValues(OneNote.PAGE_GUIDS));
assertTrue(Arrays.stream(metadata.getValues(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING))
.anyMatch(warning -> warning.contains("could not be
resolved")));
}
@@ -313,6 +1145,90 @@ public class MSOneStorePackageTest {
assertEquals("Single Author",
metadata.get(OneNote.MOST_RECENT_AUTHORS));
}
+ @Test
+ public void testDualRoleAuthorSkipsMissingPropertySetAndMalformedValue()
throws Exception {
+ RevisionStoreObject noPropertySet = new RevisionStoreObject();
+ noPropertySet.objectID = id(712);
+ RevisionStoreObject malformedAuthor = object(id(714), propertySet(
+ new PropertySpec(PropertyType.NoData, 0x1C001D75, new
NoData())),
+ Collections.emptyList(), Collections.emptyList());
+ RevisionStoreObject nullSetRoot = object(id(713), propertySet(
+ new PropertySpec(PropertyType.ObjectID, 0x20001D78,
new NoData()),
+ new PropertySpec(PropertyType.ObjectID, 0x20001D79,
new NoData())),
+ Arrays.asList(noPropertySet.objectID, noPropertySet.objectID),
+ Collections.emptyList());
+ RevisionStoreObject malformedRoot = object(id(715), propertySet(
+ new PropertySpec(PropertyType.ObjectID, 0x20001D78,
new NoData()),
+ new PropertySpec(PropertyType.ObjectID, 0x20001D79,
new NoData())),
+ Arrays.asList(malformedAuthor.objectID,
malformedAuthor.objectID),
+ Collections.emptyList());
+ RevisionStoreObject noObjectSpace = object(id(716), propertySet(
+ new PropertySpec(PropertyType.ObjectID, 0x20001D78,
new NoData())),
+ Collections.emptyList(), Collections.emptyList());
+ noObjectSpace.propertySet.objectSpaceObjectPropSet = null;
+ RevisionStoreObject emptySpaceRoot = object(id(717), propertySet(
+ new PropertySpec(PropertyType.ObjectID, 0x20001D78,
new NoData()),
+ new PropertySpec(PropertyType.ObjectID, 0x20001D79,
new NoData())),
+ Arrays.asList(noObjectSpace.objectID, noObjectSpace.objectID),
+ Collections.emptyList());
+ RevisionStoreCell cell = new RevisionStoreCell();
+ cell.objectGroups.add(group(nullSetRoot, noPropertySet, malformedRoot,
malformedAuthor,
+ emptySpaceRoot, noObjectSpace));
+ cell.rootDeclares.add(rootDeclare(nullSetRoot.objectID));
+ cell.rootDeclares.add(rootDeclare(malformedRoot.objectID));
+ cell.rootDeclares.add(rootDeclare(emptySpaceRoot.objectID));
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.cells.add(cell);
+ Metadata metadata = new Metadata();
+
+ walk(pkg, metadata);
+
+ assertEquals(0, metadata.getValues(TikaCoreProperties.CREATOR).length);
+ }
+
+ @Test
+ public void testHeaderGuidWinsOverDataRootScan() throws Exception {
+ byte[] dataRootGuidBytes = new byte[] {
+ 0x43, 0x32, 0x21, 0x10, 0x65, 0x54, (byte) 0x87, 0x76,
+ (byte) 0x98, (byte) 0xa9, (byte) 0xba, (byte) 0xcb,
+ (byte) 0xdc, (byte) 0xed, (byte) 0xfe, 0x0f
+ };
+ HeaderCell header = new HeaderCell();
+ header.objectData = new ObjectSpaceObjectPropSet();
+ header.objectData.body = fileIdentitySet(sectionGuidBytes());
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.headerCell = header;
+ pkg.dataRootCell = new RevisionStoreCell();
+ pkg.dataRootCell.objectGroups.add(group(object(id(1500),
+ fileIdentitySet(dataRootGuidBytes), Collections.emptyList(),
+ Collections.emptyList())));
+ Metadata metadata = new Metadata();
+
+ walk(pkg, metadata);
+
+ assertEquals("{00112233-4455-6677-8899-AABBCCDDEEFF}",
+ metadata.get(OneNote.SECTION_GUID));
+ }
+
+ @Test
+ public void testWalksCellWithMissingRootDeclares() throws Exception {
+ RevisionStoreCell cell = new RevisionStoreCell();
+ cell.rootDeclares = null;
+ cell.objectGroups.add(group(object(id(630), propertySet(
+ new
PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C003498, text("late body text"))),
+ Collections.emptyList(), Collections.emptyList())));
+ MSOneStorePackage pkg = new MSOneStorePackage();
+ pkg.cells.add(cell);
+ Metadata metadata = new Metadata();
+
+ String xml = walk(pkg, metadata);
+
+ assertTrue(xml.contains("late body text"));
+
assertTrue(Arrays.stream(metadata.getValues(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING))
+ .anyMatch(warning -> warning.contains("could not be resolved;
walking all objects")));
+ }
+
@Test
public void testBlobOnlyRootWithDanglingContentRootWalksAllObjects()
throws Exception {
RevisionStoreObject blobRoot = object(id(620), propertySet(),
@@ -690,6 +1606,56 @@ public class MSOneStorePackageTest {
assertTrue(text.contains("hi"));
}
+ private static class CountingMSOneStorePackage extends MSOneStorePackage {
+ private int objectIndexBuilds;
+ private int identityScanEntries;
+
+ @Override
+ Map<ExGuid, RevisionStoreObject> indexObjectsById(
+ List<RevisionStoreObjectGroup> objectGroups) {
+ objectIndexBuilds++;
+ return super.indexObjectsById(objectGroups);
+ }
+
+ @Override
+ void collectFileIdentityGuids(PropertySet propertySet, int depth) {
+ identityScanEntries++;
+ super.collectFileIdentityGuids(propertySet, depth);
+ }
+ }
+
+ private static byte[] sectionGuidBytes() {
+ return new byte[] {
+ 0x33, 0x22, 0x11, 0x00, 0x55, 0x44, 0x77, 0x66,
+ (byte) 0x88, (byte) 0x99, (byte) 0xaa, (byte) 0xbb,
+ (byte) 0xcc, (byte) 0xdd, (byte) 0xee, (byte) 0xff
+ };
+ }
+
+ private static PropertySet fileIdentitySet(byte[] value) {
+ return propertySet(new
PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C001D94, bytes(value)));
+ }
+
+ private static RevisionStoreObject notebookGuidObject(int objectId, int
jcid, byte[] value)
+ throws Exception {
+ RevisionStoreObject object = object(id(objectId),
+ propertySet(new
PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
+ 0x1C001C30, bytes(value))), Collections.emptyList(),
Collections.emptyList());
+ setJcid(object, jcid);
+ return object;
+ }
+
+ private static void fillGuidBudget(MSOneStorePackage pkg, int count) {
+ for (int i = 0; i < count; i++) {
+ pkg.guidCollector().add(OneNoteGuidCollector.Category.PAGE,
"budget-" + i);
+ }
+ }
+
+ private static Set<String> sectionGuidValues(MSOneStorePackage pkg) {
+ return
pkg.guidCollector().values(OneNoteGuidCollector.Category.SECTION);
+ }
+
private static String walk(MSOneStorePackage pkg) throws Exception {
Metadata metadata = new Metadata();
return walk(pkg, metadata);
@@ -707,7 +1673,10 @@ public class MSOneStorePackageTest {
}
private static String walkXml(MSOneStorePackage pkg) throws Exception {
- Metadata metadata = new Metadata();
+ return walkXml(pkg, new Metadata());
+ }
+
+ private static String walkXml(MSOneStorePackage pkg, Metadata metadata)
throws Exception {
ParseContext context = new ParseContext();
ToXMLContentHandler xml = new ToXMLContentHandler();
XHTMLContentHandler xhtml = new XHTMLContentHandler(xml, metadata,
context);
@@ -721,6 +1690,29 @@ public class MSOneStorePackageTest {
return value.split(java.util.regex.Pattern.quote(needle), -1).length -
1;
}
+ private static RevisionManifestRootDeclare rootDeclare(ExGuid objectID) {
+ RevisionManifestRootDeclare declare = new
RevisionManifestRootDeclare();
+ declare.objectExGuid = objectID;
+ return declare;
+ }
+
+ private static void setJcid(RevisionStoreObject object, int index) throws
Exception {
+ JCIDObject jcidObject = new JCIDObject(null, emptyObjectData());
+ jcidObject.jcid.index = index;
+ object.jcid = jcidObject;
+ }
+
+ /** Adds a PAGE_METADATA guid object as a second root so the page div
carries its id. */
+ private static void attachPageGuid(RevisionStoreCell cell, int objectId,
byte firstGroupByte)
+ throws Exception {
+ byte[] guidBytes = new byte[16];
+ guidBytes[3] = firstGroupByte;
+ RevisionStoreObject pageMetadata = notebookGuidObject(objectId,
+ OneNoteJcid.PAGE_METADATA, guidBytes);
+ cell.objectGroups.add(group(pageMetadata));
+ cell.rootDeclares.add(rootDeclare(pageMetadata.objectID));
+ }
+
private static RevisionStoreCell cellWithText(CellID cellID, String value)
throws Exception {
RevisionStoreObject object = object(id(cellID.extendGUID1.hashCode()),
propertySet(new
PropertySpec(PropertyType.FourBytesOfLengthFollowedByData,
@@ -786,6 +1778,8 @@ public class MSOneStorePackageTest {
return set;
}
+ // PropertyID.type and .value are set independently here; the real
deserializer derives
+ // the type from the value's high bits, so some fixtures are deliberately
impossible inputs
private static PropertyID propertyID(PropertyType type, int value) {
PropertyID id = new PropertyID();
id.type = type.getIntVal();