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 b9ea18675c TIKA-5029 containers (#3556)
b9ea18675c is described below
commit b9ea18675c4fba35401a8f28760c8e02384d5d9e
Author: Tim Allison <[email protected]>
AuthorDate: Fri Nov 6 14:58:23 2026 -0500
TIKA-5029 containers (#3556)
* TIKA-5029: more container formats
TIKA-5029: detect Revit, 3ds Max, Ichitaro, Thumbs.db, newer SolidWorks
(OLE2), NuGet, FictionBook 3, HLK packages (OPC) and Mozilla add-ons by
container contents
* TIKA-5029: more containers and bug fixes
detect zip application packages (MuseScore, MusicXML, CorelDRAW zip, Power
BI, FreeCAD, SMART Notebook, Anki, Fritzing, Scratch, LEGO LXF, App Inventor,
USDZ); keep new zip types in ZipParser output; narrow Ichitaro
---
CHANGES.txt | 14 ++
.../org/apache/tika/mime/tika-mimetypes.xml | 114 ++++++++++
.../detect/microsoft/POIFSContainerDetector.java | 54 ++++-
.../detect/microsoft/ooxml/OPCPackageDetector.java | 41 ++++
.../microsoft/ContainerFormatDetectionTest.java | 123 +++++++++++
.../java/org/apache/tika/parser/pkg/ZipParser.java | 9 +-
.../apache/tika/detect/zip/AppPackageDetector.java | 230 +++++++++++++++++++++
.../org/apache/tika/detect/zip/XPIDetector.java | 63 ++++++
...org.apache.tika.detect.zip.ZipContainerDetector | 2 +
.../tika/detect/zip/AppPackageDetectionTest.java | 108 ++++++++++
.../apache/tika/detect/zip/XPIDetectionTest.java | 58 ++++++
11 files changed, 814 insertions(+), 2 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 1ac025f46f..d228daca63 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -89,6 +89,20 @@ Release 4.3.0 - ???
EXTRACTION AND FORMATS
+ * Container formats that were detected only as generic OLE2, OPC or zip are
now detected by
+ their streams, package relationships or entries: Autodesk Revit projects
and families,
+ 3ds Max scenes, Ichitaro documents, Windows Thumbs.db caches and newer
SolidWorks files
+ (OLE2); NuGet packages, FictionBook 3 e-books and Windows Hardware Lab
Kit packages (OPC);
+ and Mozilla add-ons with an install.rdf or a Mozilla signature, which are
now unpacked
+ as zip archives (TIKA-5029).
+
+ * Zip-based application packages are detected by their entries: MuseScore
scores,
+ compressed MusicXML (by its score), zip-format CorelDRAW drawings, Power
BI reports,
+ FreeCAD documents, SMART Notebook lessons, Anki decks, Fritzing sketches,
Scratch 2 and 3
+ projects (by their project.json), LEGO Digital Designer models, App
Inventor projects and
+ USDZ packages (model/vnd.usdz+zip). They are still unpacked as zip
archives, and keep their
+ detected type in the output (TIKA-5029).
+
* MP4 and QuickTime files with a box over 2 GB, typically an mdat in front
of
moov, keep their metadata: the walk skips such a box instead of stopping
(TIKA-5051).
diff --git
a/tika-core/src/main/resources/org/apache/tika/mime/tika-mimetypes.xml
b/tika-core/src/main/resources/org/apache/tika/mime/tika-mimetypes.xml
index 36badd6abc..4dafbda9ee 100644
--- a/tika-core/src/main/resources/org/apache/tika/mime/tika-mimetypes.xml
+++ b/tika-core/src/main/resources/org/apache/tika/mime/tika-mimetypes.xml
@@ -3055,7 +3055,9 @@
</mime-type>
<mime-type type="application/vnd.rapid"/>
<mime-type type="application/vnd.recordare.musicxml">
+ <_comment>Compressed MusicXML</_comment>
<glob pattern="*.mxl"/>
+ <sub-class-of type="application/zip"/>
</mime-type>
<mime-type type="application/vnd.recordare.musicxml+xml">
<glob pattern="*.musicxml"/>
@@ -3618,6 +3620,12 @@
<mime-type type="application/wspolicy+xml">
<glob pattern="*.wspolicy"/>
</mime-type>
+
+ <mime-type type="application/x-3ds-max">
+ <_comment>Autodesk 3ds Max scene</_comment>
+ <glob pattern="*.max"/>
+ <sub-class-of type="application/x-tika-msoffice"/>
+ </mime-type>
<mime-type type="image/x-svm">
<_comment>StarView Metafile</_comment>
@@ -3864,6 +3872,15 @@
<glob pattern="*.aas"/>
</mime-type>
+ <mime-type type="application/x-autodesk-revit">
+ <_comment>Autodesk Revit project or family</_comment>
+ <glob pattern="*.rvt"/>
+ <glob pattern="*.rfa"/>
+ <glob pattern="*.rte"/>
+ <glob pattern="*.rft"/>
+ <sub-class-of type="application/x-tika-msoffice"/>
+ </mime-type>
+
<mime-type type="application/x-bcpio">
<glob pattern="*.bcpio"/>
</mime-type>
@@ -4143,6 +4160,13 @@
<sub-class-of type="application/xml"/>
</mime-type>
+ <mime-type type="application/x-powerbi">
+ <_comment>Microsoft Power BI report or template</_comment>
+ <glob pattern="*.pbix"/>
+ <glob pattern="*.pbit"/>
+ <sub-class-of type="application/zip"/>
+ </mime-type>
+
<mime-type type="application/x-casio-fxi">
<_comment>Casio calculator data (Yellow Computing FX-Interface)</_comment>
<magic priority="50">
@@ -4283,6 +4307,19 @@
</magic>
</mime-type>
+ <mime-type type="application/x-anki-package">
+ <_comment>Anki flashcard package</_comment>
+ <glob pattern="*.apkg"/>
+ <glob pattern="*.colpkg"/>
+ <sub-class-of type="application/zip"/>
+ </mime-type>
+
+ <mime-type type="application/x-appinventor-project">
+ <_comment>MIT App Inventor project</_comment>
+ <glob pattern="*.aia"/>
+ <sub-class-of type="application/zip"/>
+ </mime-type>
+
<mime-type type="application/x-brotli">
<glob pattern="*.br" />
<glob pattern="*.brotli" />
@@ -4747,6 +4784,12 @@
</magic>
</mime-type>
+ <mime-type type="application/x-fritzing-bundle">
+ <_comment>Fritzing sketch bundle</_comment>
+ <glob pattern="*.fzz"/>
+ <sub-class-of type="application/zip"/>
+ </mime-type>
+
<mime-type type="application/x-futuresplash">
<_comment>Macromedia FutureSplash File</_comment>
<glob pattern="*.spl"/>
@@ -4871,6 +4914,14 @@
</magic>
<glob pattern="*.ibooks"/>
</mime-type>
+
+ <mime-type type="application/x-ichitaro">
+ <_comment>JustSystems Ichitaro document</_comment>
+ <glob pattern="*.jtd"/>
+ <glob pattern="*.jtt"/>
+ <glob pattern="*.jtdc"/>
+ <sub-class-of type="application/x-tika-msoffice"/>
+ </mime-type>
<mime-type type="application/x-idl-save-file">
<_comment>NV5 Geospatial Interactive Data Language Save File</_comment>
<tika:link>https://www.l3harrisgeospatial.com/docs/idl_savefile.html</tika:link>
@@ -4993,6 +5044,12 @@
<sub-class-of type="application/x-tex"/>
</mime-type>
+ <mime-type type="application/x-lego-lxf">
+ <_comment>LEGO Digital Designer model</_comment>
+ <glob pattern="*.lxf"/>
+ <sub-class-of type="application/zip"/>
+ </mime-type>
+
<mime-type type="application/x-lha">
<magic priority="50">
<match value="-lzs-" type="string" offset="2"/>
@@ -5301,6 +5358,13 @@
<match value="0x535A444488F0273341" offset="0"/>
</magic>
</mime-type>
+
+ <mime-type type="application/x-ms-hlk-package">
+ <_comment>Windows Hardware Lab Kit / Certification Kit package</_comment>
+ <glob pattern="*.hlkx"/>
+ <glob pattern="*.hckx"/>
+ <sub-class-of type="application/zip"/>
+ </mime-type>
<mime-type type="application/x-ms-owner">
<_comment>Temporary files created by MSOffice applications</_comment>
<_comment>PRONOM fmt-473</_comment>
@@ -5311,6 +5375,11 @@
<match value="(?s)^([\\x05-\\x0F])[\\x00\\x20-\\x7E]{10}.{43}\\1\x00"
type="regex" offset="0"/>
</magic>
</mime-type>
+
+ <mime-type type="application/x-ms-thumbs-db">
+ <_comment>Windows thumbnail cache (Thumbs.db)</_comment>
+ <sub-class-of type="application/x-tika-msoffice"/>
+ </mime-type>
<mime-type type="application/x-ms-nls">
<_comment>Microsoft National Language Support</_comment>
<_comment>Should take precedence over x-ms-owner</_comment>
@@ -5477,6 +5546,12 @@
<glob pattern="*.wri"/>
</mime-type>
+ <mime-type type="application/x-musescore">
+ <_comment>MuseScore score</_comment>
+ <glob pattern="*.mscz"/>
+ <sub-class-of type="application/zip"/>
+ </mime-type>
+
<mime-type type="application/x-mysql-db">
</mime-type>
<mime-type type="application/x-mysql-table-definition">
@@ -5567,6 +5642,12 @@
</magic>
<glob pattern="*.shk"/>
</mime-type>
+ <mime-type type="application/x-nupkg">
+ <_comment>NuGet package</_comment>
+ <glob pattern="*.nupkg"/>
+ <glob pattern="*.snupkg"/>
+ <sub-class-of type="application/zip"/>
+ </mime-type>
<mime-type type="application/x-nwc">
<_comment>NoteWorthy Composer song</_comment>
<magic priority="50">
@@ -5919,6 +6000,13 @@
</magic>
</mime-type>
+ <mime-type type="application/x-scratch-project">
+ <_comment>Scratch project</_comment>
+ <glob pattern="*.sb2"/>
+ <glob pattern="*.sb3"/>
+ <sub-class-of type="application/zip"/>
+ </mime-type>
+
<mime-type type="application/x-sh">
<_comment>UNIX/LINUX Shell Script</_comment>
<magic priority="50">
@@ -5939,6 +6027,12 @@
<glob pattern="*.sketch"/>
</mime-type>
+ <mime-type type="application/x-smarttech-notebook">
+ <_comment>SMART Notebook lesson</_comment>
+ <glob pattern="*.notebook"/>
+ <sub-class-of type="application/zip"/>
+ </mime-type>
+
<mime-type type="application/x-shar">
<glob pattern="*.shar"/>
</mime-type>
@@ -6422,6 +6516,11 @@
<glob pattern="*.vmdk"/>
</mime-type>
+ <mime-type type="application/x-vnd.corel.draw.document+zip">
+ <_comment>CorelDRAW drawing (zip container)</_comment>
+ <sub-class-of type="application/zip"/>
+ </mime-type>
+
<mime-type type="application/x-wais-source">
<glob pattern="*.src"/>
</mime-type>
@@ -6532,7 +6631,9 @@
<glob pattern="*.fig"/>
</mime-type>
<mime-type type="application/x-xpinstall">
+ <_comment>Mozilla add-on</_comment>
<glob pattern="*.xpi"/>
+ <sub-class-of type="application/zip"/>
</mime-type>
<mime-type type="application/x-xmind">
@@ -8726,6 +8827,13 @@
</mime-type>
<mime-type type="model/vnd.parasolid.transmit.binary"/>
<mime-type type="model/vnd.parasolid.transmit.text"/>
+
+ <mime-type type="model/vnd.usdz+zip">
+ <_comment>Universal Scene Description zip package</_comment>
+
<tika:link>https://www.iana.org/assignments/media-types/model/vnd.usdz+zip</tika:link>
+ <glob pattern="*.usdz"/>
+ <sub-class-of type="application/zip"/>
+ </mime-type>
<mime-type type="model/vnd.vtu">
<glob pattern="*.vtu"/>
</mime-type>
@@ -10231,6 +10339,12 @@
<glob pattern="*.fb2"/>
</mime-type>
+ <mime-type type="application/x-fictionbook3">
+ <_comment>FictionBook 3 e-book</_comment>
+ <glob pattern="*.fb3"/>
+ <sub-class-of type="application/zip"/>
+ </mime-type>
+
<mime-type type="text/x-asciidoc">
<_comment>Asciidoc source code</_comment>
<glob pattern="*.asciidoc"/>
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/detect/microsoft/POIFSContainerDetector.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/detect/microsoft/POIFSContainerDetector.java
index 7438e94f44..29fa3b9d9d 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/detect/microsoft/POIFSContainerDetector.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/detect/microsoft/POIFSContainerDetector.java
@@ -167,6 +167,14 @@ public class POIFSContainerDetector implements Detector {
public static final MediaType ESRI_LAYER = application("x-esri-layer");
public static final MediaType DGN_8 = image("vnd.dgn;version=8");
+
+ public static final MediaType REVIT = application("x-autodesk-revit");
+
+ public static final MediaType MAX_3DS = application("x-3ds-max");
+
+ public static final MediaType ICHITARO = application("x-ichitaro");
+
+ public static final MediaType THUMBS_DB = application("x-ms-thumbs-db");
/**
* Serial version UID
*/
@@ -183,6 +191,26 @@ public class POIFSContainerDetector implements Detector {
private static final String SW_DOC_MGR_TEMP_STORAGE =
"SwDocMgrTempStorage".toUpperCase(Locale.ROOT);
+ private static final String SW_INFORMATION =
"ISolidWorksInformation".toUpperCase(Locale.ROOT);
+
+ private static final String BASIC_FILE_INFO =
"BasicFileInfo".toUpperCase(Locale.ROOT);
+
+ private static final String PARTITIONS =
"Partitions".toUpperCase(Locale.ROOT);
+
+ private static final String CLASS_DIRECTORY_3 =
"ClassDirectory3".toUpperCase(Locale.ROOT);
+
+ private static final String SCENE = "Scene".toUpperCase(Locale.ROOT);
+
+ private static final String VIDEO_POST_QUEUE =
"VideoPostQueue".toUpperCase(Locale.ROOT);
+
+ private static final String JSRV_SUMMARY =
+ "\u0004JSRV_SummaryInformation".toUpperCase(Locale.ROOT);
+
+ private static final String JSRV_SEGMENT =
+ "\u0004JSRV_SegmentInformation".toUpperCase(Locale.ROOT);
+
+ private static final String CATALOG = "Catalog".toUpperCase(Locale.ROOT);
+
private static final String STAR_CALC_DOCUMENT =
"StarCalcDocument".toUpperCase(Locale.ROOT);
private static final String STAR_WRITER_DOCUMENT =
"StarWriterDocument".toUpperCase(Locale.ROOT);
@@ -306,8 +334,19 @@ public class POIFSContainerDetector implements Detector {
return XLS;
}
}
- if (ucNames.contains(SW_DOC_CONTENT_MGR) &&
ucNames.contains(SW_DOC_MGR_TEMP_STORAGE)) {
+ if ((ucNames.contains(SW_DOC_CONTENT_MGR) &&
ucNames.contains(SW_DOC_MGR_TEMP_STORAGE))
+ || ucNames.contains(SW_INFORMATION)) {
return SLDWORKS;
+ } else if (ucNames.contains(BASIC_FILE_INFO) &&
ucNames.contains(PARTITIONS)) {
+ // these also hold Contents, so they must come before the CONTENTS
checks
+ return REVIT;
+ } else if (ucNames.contains(CLASS_DIRECTORY_3) &&
ucNames.contains(SCENE)
+ && ucNames.contains(VIDEO_POST_QUEUE)) {
+ return MAX_3DS;
+ } else if (isThumbsDb(ucNames)) {
+ return THUMBS_DB;
+ } else if (ucNames.contains(JSRV_SUMMARY) ||
ucNames.contains(JSRV_SEGMENT)) {
+ return ICHITARO;
} else if (ucNames.contains(STAR_CALC_DOCUMENT)) {
// Star Office Calc
return SDC;
@@ -409,6 +448,19 @@ public class POIFSContainerDetector implements Detector {
return OLE;
}
+ // Catalog plus thumbnail streams named by reversed decimal ids
+ private static boolean isThumbsDb(Set<String> ucNames) {
+ if (!ucNames.contains(CATALOG) || ucNames.size() < 2) {
+ return false;
+ }
+ for (String name : ucNames) {
+ if (!name.equals(CATALOG) && !name.chars().allMatch(c -> c >= '0'
&& c <= '9')) {
+ return false;
+ }
+ }
+ return true;
+ }
+
private static MediaType checkEncrypted(Set<String> ucNames,
DirectoryEntry root) {
//figure out if encrypted/pw protected first
if (ucNames.contains(DATA_SPACES)) {
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/detect/microsoft/ooxml/OPCPackageDetector.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/detect/microsoft/ooxml/OPCPackageDetector.java
index dd335f7628..74fe94357d 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/detect/microsoft/ooxml/OPCPackageDetector.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/detect/microsoft/ooxml/OPCPackageDetector.java
@@ -109,6 +109,15 @@ public class OPCPackageDetector implements
ZipContainerDetector {
"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel";
static Map<String, MediaType> OOXML_CONTENT_TYPES = new
ConcurrentHashMap<>();
+ // package relationship types of non-Office OPC formats
+ static final Map<String, MediaType> PACKAGE_RELATIONSHIP_TYPES = Map.of(
+ "http://schemas.microsoft.com/packaging/2010/07/manifest",
+ MediaType.application("x-nupkg"),
+ "http://www.fictionbook.org/FictionBook3/relationships/Book",
+ MediaType.application("x-fictionbook3"),
+ "http://microsoft.com/schemas/windows/kits/hardware/2010/coredata",
+ MediaType.application("x-ms-hlk-package"));
+
static {
OOXML_CONTENT_TYPES.put(XWPFRelation.DOCUMENT.getContentType(), DOCX);
OOXML_CONTENT_TYPES.put(XWPFRelation.MACRO_DOCUMENT.getContentType(),
DOCM);
@@ -193,6 +202,13 @@ public class OPCPackageDetector implements
ZipContainerDetector {
return THREE_MF;
}
}
+ if (core.size() == 0) {
+ for (Map.Entry<String, MediaType> e :
PACKAGE_RELATIONSHIP_TYPES.entrySet()) {
+ if (pkg.getRelationshipsByType(e.getKey()).size() > 0) {
+ return e.getValue();
+ }
+ }
+ }
}
// If we didn't find a single core document of any type, skip detection
if (core.size() != 1) {
@@ -227,6 +243,25 @@ public class OPCPackageDetector implements
ZipContainerDetector {
return MediaType.parse(docType);
}
+ // POI refuses some non-Office packages (e.g. NuGet's "application/octet"
content type)
+ private static MediaType detectByPackageRelationships(ZipEntrySource
zipEntrySource) {
+ ZipArchiveEntry relsEntry = zipEntrySource.getEntry("_rels/.rels");
+ if (relsEntry == null) {
+ return null;
+ }
+ try (InputStream is = zipEntrySource.getInputStream(relsEntry)) {
+ for (String rel : parseOOXMLRels(is)) {
+ MediaType mt = PACKAGE_RELATIONSHIP_TYPES.get(rel);
+ if (mt != null) {
+ return mt;
+ }
+ }
+ } catch (IOException e) {
+ //swallow
+ }
+ return null;
+ }
+
public static Set<String> parseOOXMLRels(InputStream is) {
RelsHandler relsHandler = new RelsHandler();
try {
@@ -288,6 +323,12 @@ public class OPCPackageDetector implements
ZipContainerDetector {
// rels were malformed and POI silently produced an empty relationship
// collection. Either way, fall back to parsing [Content_Types].xml
// directly — same approach as the streaming detector.
+ if (type == null) {
+ type = detectByPackageRelationships(zipEntrySource);
+ if (type != null && pkg == null) {
+ return type;
+ }
+ }
if (type == null) {
ZipArchiveEntry ctEntry =
zipEntrySource.getEntry("[Content_Types].xml");
if (ctEntry != null) {
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/detect/microsoft/ContainerFormatDetectionTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/detect/microsoft/ContainerFormatDetectionTest.java
new file mode 100644
index 0000000000..8f7c8f9ae1
--- /dev/null
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/detect/microsoft/ContainerFormatDetectionTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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.detect.microsoft;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
+import org.junit.jupiter.api.Test;
+
+import org.apache.tika.detect.zip.DefaultZipContainerDetector;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.parser.ParseContext;
+
+/**
+ * OLE2 and OPC formats recognized by their stream names or package
relationships.
+ */
+public class ContainerFormatDetectionTest {
+
+ // a trailing slash makes a storage
+ private static String ole2(String... names) throws Exception {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ try (POIFSFileSystem fs = new POIFSFileSystem()) {
+ for (String name : names) {
+ if (name.endsWith("/")) {
+ fs.getRoot().createDirectory(name.substring(0,
name.length() - 1));
+ } else {
+ fs.createDocument(new ByteArrayInputStream(new byte[]{1,
2, 3}), name);
+ }
+ }
+ fs.writeFilesystem(bos);
+ }
+ try (TikaInputStream tis = TikaInputStream.get(bos.toByteArray())) {
+ return new POIFSContainerDetector().detect(tis, new Metadata(),
new ParseContext())
+ .toString();
+ }
+ }
+
+ private static String opc(String relType, String... extra) throws
Exception {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ try (ZipOutputStream zos = new ZipOutputStream(bos)) {
+ put(zos, "[Content_Types].xml", "<?xml version=\"1.0\"?><Types "
+ +
"xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">"
+ + "<Default Extension=\"rels\" "
+ +
"ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>"
+ + "<Default Extension=\"nuspec\"
ContentType=\"application/octet\"/></Types>");
+ put(zos, "_rels/.rels", "<?xml version=\"1.0\"?><Relationships "
+ +
"xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">"
+ + "<Relationship Type=\"" + relType + "\"
Target=\"/a.nuspec\" Id=\"R1\"/>"
+ + "</Relationships>");
+ put(zos, "a.nuspec", "<package/>");
+ for (String name : extra) {
+ put(zos, name, "x");
+ }
+ }
+ try (TikaInputStream tis = TikaInputStream.get(bos.toByteArray())) {
+ return new DefaultZipContainerDetector().detect(tis, new
Metadata(), new ParseContext())
+ .toString();
+ }
+ }
+
+ private static void put(ZipOutputStream zos, String name, String content)
throws Exception {
+ zos.putNextEntry(new ZipEntry(name));
+ zos.write(content.getBytes(StandardCharsets.UTF_8));
+ zos.closeEntry();
+ }
+
+ @Test
+ public void testOle2Formats() throws Exception {
+ assertEquals("application/x-autodesk-revit",
+ ole2("BasicFileInfo", "Contents", "PartAtom",
"TransmissionData", "Formats/",
+ "Global/", "Partitions/"));
+ assertEquals("application/x-3ds-max",
+ ole2("ClassData", "ClassDirectory3", "Config", "DllDirectory",
"Scene",
+ "VideoPostQueue"));
+ assertEquals("application/x-ichitaro",
+ ole2("\u0004JSRV_SegmentInformation",
"\u0004JSRV_SummaryInformation", "DocumentText"));
+ assertEquals("application/x-ms-thumbs-db", ole2("Catalog", "1", "01",
"11"));
+ assertEquals("application/sldworks",
+ ole2("ISolidWorksInformation", "Header2", "Preview",
"Contents/"));
+ }
+
+ @Test
+ public void testOle2LookalikesStayGeneric() throws Exception {
+ assertEquals("application/x-tika-msoffice", ole2("Catalog", "1",
"Data"));
+ assertEquals("application/x-tika-msoffice", ole2("Catalog"));
+ assertEquals("application/x-tika-msoffice", ole2("BasicFileInfo",
"Contents"));
+ assertEquals("application/x-tika-msoffice", ole2("Scene",
"ClassDirectory3"));
+ assertEquals("application/x-tika-msoffice", ole2("MyJSRV_Notes",
"Data"));
+ }
+
+ @Test
+ public void testOpcPackageRelationships() throws Exception {
+ assertEquals("application/x-nupkg",
+
opc("http://schemas.microsoft.com/packaging/2010/07/manifest"));
+ assertEquals("application/x-fictionbook3",
+
opc("http://www.fictionbook.org/FictionBook3/relationships/Book"));
+ assertEquals("application/x-ms-hlk-package",
+
opc("http://microsoft.com/schemas/windows/kits/hardware/2010/coredata"));
+ // the zip layer leaves an unknown package generic
+ assertEquals("application/zip", opc("http://example.com/unknown"));
+ }
+}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pkg-module/src/main/java/org/apache/tika/parser/pkg/ZipParser.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pkg-module/src/main/java/org/apache/tika/parser/pkg/ZipParser.java
index a893078332..ce974a697b 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pkg-module/src/main/java/org/apache/tika/parser/pkg/ZipParser.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pkg-module/src/main/java/org/apache/tika/parser/pkg/ZipParser.java
@@ -196,7 +196,14 @@ public class ZipParser extends AbstractArchiveParser {
"application/vnd.adobe.indesign-idml-package",
"application/x-wacz", "application/x-vnd.datapackage+zip",
"application/x-extension-fcstd", "application/x-krita",
- "application/x-procreate", "application/x-sketch",
"image/openraster"
+ "application/x-procreate", "application/x-sketch",
"image/openraster",
+ "application/x-nupkg", "application/x-fictionbook3",
"application/x-ms-hlk-package",
+ "application/x-xpinstall",
"application/vnd.recordare.musicxml",
+ "application/x-musescore",
"application/x-vnd.corel.draw.document+zip",
+ "application/x-powerbi", "application/x-smarttech-notebook",
+ "application/x-anki-package", "application/x-fritzing-bundle",
+ "application/x-scratch-project", "application/x-lego-lxf",
+ "application/x-appinventor-project", "model/vnd.usdz+zip"
}) {
zipSpecializations.add(MediaType.parse(mediaTypeString));
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/java/org/apache/tika/detect/zip/AppPackageDetector.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/java/org/apache/tika/detect/zip/AppPackageDetector.java
new file mode 100644
index 0000000000..9481664077
--- /dev/null
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/java/org/apache/tika/detect/zip/AppPackageDetector.java
@@ -0,0 +1,230 @@
+/*
+ * 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.detect.zip;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.regex.Pattern;
+
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
+import org.apache.commons.compress.archivers.zip.ZipFile;
+
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.mime.MediaType;
+
+/**
+ * Application packages recognized by their entry names alone: USDZ,
MuseScore, compressed
+ * MusicXML, CorelDRAW (zip), Power BI, FreeCAD, SMART Notebook, Anki,
Fritzing, Scratch,
+ * LEGO Digital Designer and App Inventor.
+ */
+public class AppPackageDetector implements ZipContainerDetector {
+
+ static final MediaType USDZ = MediaType.parse("model/vnd.usdz+zip");
+ static final MediaType MUSESCORE = MediaType.application("x-musescore");
+ static final MediaType MUSICXML =
MediaType.application("vnd.recordare.musicxml");
+ static final MediaType CORELDRAW_ZIP =
MediaType.application("x-vnd.corel.draw.document+zip");
+ static final MediaType POWER_BI = MediaType.application("x-powerbi");
+ static final MediaType FREECAD =
MediaType.application("x-extension-fcstd");
+ static final MediaType SMART_NOTEBOOK =
MediaType.application("x-smarttech-notebook");
+ static final MediaType ANKI = MediaType.application("x-anki-package");
+ static final MediaType FRITZING =
MediaType.application("x-fritzing-bundle");
+ static final MediaType SCRATCH =
MediaType.application("x-scratch-project");
+ static final MediaType LEGO_LXF = MediaType.application("x-lego-lxf");
+ static final MediaType APP_INVENTOR =
MediaType.application("x-appinventor-project");
+
+ private static final int MAX_NAMES = 10000;
+
+ private static final int SNIFF_BYTES = 4096;
+
+ private static final int MAX_SNIFFS = 16;
+
+ private static final Pattern MUSICXML_ROOT =
Pattern.compile("<score-(?:partwise|timewise)\\b");
+
+ // Scratch 2 numbers its assets; Scratch 3 names them by md5
+ private static final Pattern SCRATCH_PROJECT =
Pattern.compile("\"(?:targets|objName)\"");
+
+ private static final Pattern SCRATCH_ASSET =
+
Pattern.compile("(?:[0-9]+|[0-9a-f]{32})\\.(?:png|svg|jpg|jpeg|gif|bmp|wav|mp3)");
+
+ @Override
+ public MediaType detect(ZipFile zip, TikaInputStream tis) throws
IOException {
+ Names names = new Names();
+ Enumeration<ZipArchiveEntry> entries = zip.getEntriesInPhysicalOrder();
+ while (entries.hasMoreElements() && names.list.size() < MAX_NAMES) {
+ ZipArchiveEntry entry = entries.nextElement();
+ names.add(entry);
+ if (names.sniffed.size() < MAX_SNIFFS && sniffs(entry.getName())) {
+ try (InputStream is = zip.getInputStream(entry)) {
+ names.sniffed.put(entry.getName(), head(is));
+ } catch (IOException e) {
+ //swallow
+ }
+ }
+ }
+ return detect(names);
+ }
+
+ @Override
+ public MediaType streamingDetectUpdate(ZipArchiveEntry zae, InputStream
zis,
+ StreamingDetectContext
detectContext) {
+ Names names = detectContext.get(Names.class);
+ if (names == null) {
+ names = new Names();
+ detectContext.set(Names.class, names);
+ }
+ if (names.list.size() < MAX_NAMES) {
+ names.add(zae);
+ if (names.sniffed.size() < MAX_SNIFFS && sniffs(zae.getName())) {
+ try {
+ names.sniffed.put(zae.getName(), head(zis));
+ } catch (IOException e) {
+ //swallow
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public MediaType streamingDetectFinal(StreamingDetectContext
detectContext) {
+ Names names = detectContext.get(Names.class);
+ return names == null ? null : detect(names);
+ }
+
+ // root XML files (MusicXML score) and project.json (Scratch) are checked
by content
+ private static boolean sniffs(String name) {
+ if (name.indexOf('/') >= 0 || name.indexOf('\\') >= 0) {
+ return false;
+ }
+ String lc = name.toLowerCase(Locale.ROOT);
+ return lc.equals("project.json") || lc.endsWith(".xml") ||
lc.endsWith(".musicxml");
+ }
+
+ private static String head(InputStream is) throws IOException {
+ return new String(is.readNBytes(SNIFF_BYTES),
StandardCharsets.ISO_8859_1);
+ }
+
+ static MediaType detect(Names found) {
+ List<String> entryNames = found.list;
+ if (entryNames.isEmpty()) {
+ return null;
+ }
+ Set<String> names = new HashSet<>();
+ Set<String> rootExts = new HashSet<>();
+ boolean opf = false;
+ boolean annotationMetadata = false;
+ boolean scratchAssetsOnly = true;
+ for (String raw : entryNames) {
+ String name = raw.replace('\\', '/');
+ names.add(name);
+ String lc = name.toLowerCase(Locale.ROOT);
+ opf |= lc.endsWith(".opf");
+ annotationMetadata |= name.startsWith("annotationmetadata/");
+ if (name.indexOf('/') < 0) {
+ int dot = lc.lastIndexOf('.');
+ if (dot > 0) {
+ rootExts.add(lc.substring(dot));
+ }
+ if (!name.equals("project.json") &&
!SCRATCH_ASSET.matcher(lc).matches()) {
+ scratchAssetsOnly = false;
+ }
+ } else {
+ scratchAssetsOnly = false;
+ }
+ }
+ // the package's first file is its root USD layer, stored uncompressed
+ String first = entryNames.get(0).toLowerCase(Locale.ROOT);
+ if (found.firstStored && first.indexOf('/') < 0
+ && (first.endsWith(".usdc") || first.endsWith(".usda") ||
first.endsWith(".usd"))) {
+ return USDZ;
+ }
+ if (names.contains("META-INF/container.xml")) {
+ if (rootExts.contains(".mscx")) {
+ return MUSESCORE;
+ }
+ // EPUB and IDML share META-INF/container.xml; a MusicXML package
holds a score
+ if (!opf && found.anySniff(n -> !n.equals("project.json"),
MUSICXML_ROOT)) {
+ return MUSICXML;
+ }
+ }
+ if (names.contains("content/riffData.cdr")) {
+ return CORELDRAW_ZIP;
+ }
+ if (names.contains("Version") && names.contains("Report/Layout")
+ && names.contains("DiagramLayout")) {
+ return POWER_BI;
+ }
+ if (names.contains("Document.xml")
+ && (names.contains("GuiDocument.xml") ||
rootExts.contains(".brp"))) {
+ return FREECAD;
+ }
+ if (names.contains("imsmanifest.xml") && names.contains("settings.xml")
+ && names.contains("metadata.xml") && annotationMetadata) {
+ return SMART_NOTEBOOK;
+ }
+ if (names.contains("collection.anki2") ||
names.contains("collection.anki21")
+ || names.contains("collection.anki21b")) {
+ return ANKI;
+ }
+ if (rootExts.contains(".fz") && rootExts.contains(".fzp")) {
+ return FRITZING;
+ }
+ if (names.contains("project.json") && scratchAssetsOnly &&
names.size() > 1
+ && found.anySniff(n -> n.equals("project.json"),
SCRATCH_PROJECT)) {
+ return SCRATCH;
+ }
+ if (names.contains("IMAGE100.LXFML")) {
+ return LEGO_LXF;
+ }
+ if (names.contains("youngandroidproject/project.properties")) {
+ return APP_INVENTOR;
+ }
+ return null;
+ }
+
+ static class Names {
+ private final List<String> list = new ArrayList<>();
+ private final Map<String, String> sniffed = new HashMap<>();
+ private boolean firstStored;
+
+ void add(ZipArchiveEntry entry) {
+ if (list.isEmpty()) {
+ firstStored = entry.getMethod() == ZipArchiveEntry.STORED;
+ }
+ list.add(entry.getName());
+ }
+
+ boolean anySniff(Predicate<String> which, Pattern pattern) {
+ for (Map.Entry<String, String> e : sniffed.entrySet()) {
+ if (which.test(e.getKey()) &&
pattern.matcher(e.getValue()).find()) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/java/org/apache/tika/detect/zip/XPIDetector.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/java/org/apache/tika/detect/zip/XPIDetector.java
new file mode 100644
index 0000000000..cd41c8ba72
--- /dev/null
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/java/org/apache/tika/detect/zip/XPIDetector.java
@@ -0,0 +1,63 @@
+/*
+ * 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.detect.zip;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
+import org.apache.commons.compress.archivers.zip.ZipFile;
+
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.mime.MediaType;
+
+/**
+ * Mozilla add-ons: a legacy install manifest ({@code install.rdf}) or a
Mozilla signature
+ * ({@code META-INF/mozilla.rsa}). An unsigned WebExtension has only {@code
manifest.json},
+ * which Chromium extensions share, so it is left alone.
+ */
+public class XPIDetector implements ZipContainerDetector {
+
+ static final MediaType XPI = MediaType.application("x-xpinstall");
+
+ private static final String INSTALL_RDF = "install.rdf";
+
+ private static final String MOZILLA_RSA = "META-INF/mozilla.rsa";
+
+ @Override
+ public MediaType detect(ZipFile zip, TikaInputStream tis) throws
IOException {
+ if (zip.getEntry(INSTALL_RDF) != null || zip.getEntry(MOZILLA_RSA) !=
null) {
+ return XPI;
+ }
+ return null;
+ }
+
+ @Override
+ public MediaType streamingDetectUpdate(ZipArchiveEntry zae, InputStream
zis,
+ StreamingDetectContext
detectContext) {
+ String name = zae.getName();
+ if (name.equals(INSTALL_RDF) || name.equals(MOZILLA_RSA)) {
+ return XPI;
+ }
+ return null;
+ }
+
+ @Override
+ public MediaType streamingDetectFinal(StreamingDetectContext
detectContext) {
+ return null;
+ }
+}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/resources/META-INF/services/org.apache.tika.detect.zip.ZipContainerDetector
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/resources/META-INF/services/org.apache.tika.detect.zip.ZipContainerDetector
index 3d59fd80c0..8d44829757 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/resources/META-INF/services/org.apache.tika.detect.zip.ZipContainerDetector
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/resources/META-INF/services/org.apache.tika.detect.zip.ZipContainerDetector
@@ -22,3 +22,5 @@ org.apache.tika.detect.zip.StarOfficeDetector
org.apache.tika.detect.zip.FrictionlessPackageDetector
org.apache.tika.detect.zip.GeoGebraDetector
+org.apache.tika.detect.zip.XPIDetector
+org.apache.tika.detect.zip.AppPackageDetector
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/test/java/org/apache/tika/detect/zip/AppPackageDetectionTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/test/java/org/apache/tika/detect/zip/AppPackageDetectionTest.java
new file mode 100644
index 0000000000..4a2509200c
--- /dev/null
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/test/java/org/apache/tika/detect/zip/AppPackageDetectionTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.detect.zip;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.zip.CRC32;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.parser.ParseContext;
+
+public class AppPackageDetectionTest {
+
+ private static final String SCORE = "<?xml
version=\"1.0\"?><score-partwise version=\"4.0\">";
+
+ // name=content entries; a leading "!" stores the entry uncompressed
+ private static String detect(String... names) throws Exception {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ try (ZipOutputStream zos = new ZipOutputStream(bos)) {
+ for (String spec : names) {
+ boolean stored = spec.startsWith("!");
+ String[] kv = (stored ? spec.substring(1) : spec).split("=",
2);
+ byte[] content = (kv.length > 1 ? kv[1] :
"x").getBytes(StandardCharsets.UTF_8);
+ ZipEntry entry = new ZipEntry(kv[0]);
+ if (stored) {
+ CRC32 crc = new CRC32();
+ crc.update(content);
+ entry.setMethod(ZipEntry.STORED);
+ entry.setSize(content.length);
+ entry.setCrc(crc.getValue());
+ }
+ zos.putNextEntry(entry);
+ zos.write(content);
+ zos.closeEntry();
+ }
+ }
+ try (TikaInputStream tis = TikaInputStream.get(bos.toByteArray())) {
+ return new DefaultZipContainerDetector().detect(tis, new
Metadata(), new ParseContext())
+ .toString();
+ }
+ }
+
+ @Test
+ public void testAppPackages() throws Exception {
+ assertEquals("model/vnd.usdz+zip", detect("!model.usdc",
"0/baseColor.jpg"));
+ assertEquals("application/x-musescore",
+ detect("META-INF/container.xml", "Song.mscx",
"Thumbnails/thumbnail.png"));
+ assertEquals("application/vnd.recordare.musicxml",
+ detect("META-INF/container.xml", "score.xml=" + SCORE));
+ assertEquals("application/x-vnd.corel.draw.document+zip",
+ detect("content/riffData.cdr", "metadata/metadata.xml"));
+ assertEquals("application/x-powerbi",
+ detect("Version", "[Content_Types].xml", "DiagramLayout",
"Report/Layout"));
+ assertEquals("application/x-extension-fcstd",
+ detect("Document.xml", "PartShape.brp", "GuiDocument.xml"));
+ assertEquals("application/x-smarttech-notebook",
+ detect("annotationmetadata/metadata.xml", "settings.xml",
"imsmanifest.xml",
+ "metadata.xml", "page0.svg"));
+ assertEquals("application/x-anki-package", detect("collection.anki2",
"media"));
+ assertEquals("application/x-fritzing-bundle",
+ detect("part.x_1.fzp", "sketch.fz", "svg.icon.x_icon.svg"));
+ assertEquals("application/x-scratch-project",
detect("project.json={\"objName\": \"Stage\"}", "0.png", "1.wav"));
+ assertEquals("application/x-scratch-project",
+ detect("project.json={\"targets\": []}",
"83a9787d4cb6f3b7632b4ddfebf74367.wav"));
+ assertEquals("application/x-lego-lxf", detect("IMAGE100.PNG",
"IMAGE100.LXFML"));
+ assertEquals("application/x-appinventor-project",
+ detect("src/appinventor/ai_x/App/Screen1.scm",
"youngandroidproject/project.properties"));
+ }
+
+ @Test
+ public void testLookalikesStayGeneric() throws Exception {
+ // EPUB without a mimetype entry: container.xml points at an OPF
+ assertEquals("application/zip",
+ detect("META-INF/container.xml", "content.opf", "toc.xml"));
+ assertEquals("application/zip", detect("project.json", "README.md",
"src/index.js"));
+ assertEquals("application/zip", detect("project.json"));
+ assertEquals("application/zip", detect("Document.xml", "notes.txt"));
+ assertEquals("application/zip", detect("imsmanifest.xml",
"settings.xml", "metadata.xml"));
+ assertEquals("application/zip", detect("!docs/model.usdc"));
+ // USDZ forbids compression
+ assertEquals("application/zip", detect("model.usdc",
"0/baseColor.jpg"));
+ // InDesign IDML without its mimetype entry
+ assertEquals("application/zip",
+ detect("META-INF/container.xml", "designmap.xml=<?xml
version=\"1.0\"?><Document/>"));
+ assertEquals("application/zip", detect("project.json={\"name\":
\"x\"}", "0.png"));
+ }
+}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/test/java/org/apache/tika/detect/zip/XPIDetectionTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/test/java/org/apache/tika/detect/zip/XPIDetectionTest.java
new file mode 100644
index 0000000000..5bbd540012
--- /dev/null
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/test/java/org/apache/tika/detect/zip/XPIDetectionTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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.detect.zip;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.parser.ParseContext;
+
+public class XPIDetectionTest {
+
+ private static String detect(String... names) throws Exception {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ try (ZipOutputStream zos = new ZipOutputStream(bos)) {
+ for (String name : names) {
+ zos.putNextEntry(new ZipEntry(name));
+ zos.write("x".getBytes(StandardCharsets.US_ASCII));
+ zos.closeEntry();
+ }
+ }
+ try (TikaInputStream tis = TikaInputStream.get(bos.toByteArray())) {
+ return new DefaultZipContainerDetector().detect(tis, new
Metadata(), new ParseContext())
+ .toString();
+ }
+ }
+
+ @Test
+ public void testMozillaAddOns() throws Exception {
+ assertEquals("application/x-xpinstall", detect("install.rdf",
"chrome.manifest"));
+ assertEquals("application/x-xpinstall",
+ detect("manifest.json", "META-INF/manifest.mf",
"META-INF/mozilla.rsa"));
+ // unsigned WebExtensions are shared with Chromium
+ assertEquals("application/zip", detect("manifest.json",
"background.js"));
+ assertEquals("application/zip", detect("addon/install.rdf"));
+ }
+}