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"));
+    }
+}

Reply via email to