Skip to content

Commit cf7d87a

Browse files
authored
TIKA-4747 -- add axml detection (#2865)
1 parent 4ce5c70 commit cf7d87a

4 files changed

Lines changed: 142 additions & 0 deletions

File tree

tika-core/src/main/resources/org/apache/tika/mime/tika-mimetypes.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,21 @@
352352
<sub-class-of type="application/java-archive"/>
353353
<glob pattern="*.apk"/>
354354
</mime-type>
355+
<mime-type type="application/vnd.android.axml">
356+
<acronym>AXML</acronym>
357+
<_comment>Android Binary XML</_comment>
358+
<tika:link>https://developer.android.com/guide/topics/manifest/manifest-intro</tika:link>
359+
<!-- Compiled AndroidManifest.xml / res/*.xml inside an APK. The .xml extension would
360+
otherwise route it to the XML parser, which fails on the binary header. Signature:
361+
RES_XML_TYPE(0x0003)+headerSize(0x0008)=0x00080003 LE, plus RES_STRING_POOL_TYPE
362+
(0x0001) at offset 8 (the variable per-file size at offset 4 is skipped). Not a
363+
sub-class-of application/xml: must not reach an XML parser. -->
364+
<magic priority="50">
365+
<match value="0x03000800" type="string" offset="0">
366+
<match value="0x0001" type="little16" offset="8"/>
367+
</match>
368+
</magic>
369+
</mime-type>
355370
<mime-type type="application/x-tika-java-enterprise-archive">
356371
<sub-class-of type="application/java-archive"/>
357372
<glob pattern="*.ear"/>

tika-core/src/test/java/org/apache/tika/mime/MimeDetectionTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ public void testDetection() throws Exception {
8585

8686
// truncated xml should still be detected as xml, See TIKA-3596
8787
testFile("application/xml", "truncated-utf16-xml.xyz");
88+
89+
// Android Binary XML (compiled AndroidManifest.xml / res/*.xml inside an APK).
90+
// Carries a .xml extension, so magic must win over the *.xml glob and it must
91+
// NOT be routed to application/xml / the XML parser. See TIKA-4747.
92+
testFile("application/vnd.android.axml", "test-android-binary.xml");
8893
}
8994

9095
@Test
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.tika.parser;
18+
19+
import static org.junit.jupiter.api.Assertions.assertEquals;
20+
import static org.junit.jupiter.api.Assertions.assertNull;
21+
22+
import java.io.ByteArrayOutputStream;
23+
import java.nio.ByteBuffer;
24+
import java.nio.ByteOrder;
25+
import java.nio.charset.StandardCharsets;
26+
import java.util.List;
27+
import java.util.zip.ZipEntry;
28+
import java.util.zip.ZipOutputStream;
29+
30+
import org.junit.jupiter.api.Test;
31+
32+
import org.apache.tika.TikaTest;
33+
import org.apache.tika.io.TikaInputStream;
34+
import org.apache.tika.metadata.Metadata;
35+
import org.apache.tika.metadata.TikaCoreProperties;
36+
37+
/**
38+
* Android Binary XML (AXML) is the compiled binary form of AndroidManifest.xml and the
39+
* res/*.xml resources packed inside an APK. Those entries keep a .xml extension and live
40+
* inside the (zip) APK, so before TIKA-4747 the *.xml glob caused them to be detected as
41+
* application/xml and handed to the XML parser, which failed on the binary header with
42+
* "Invalid byte 1 of 1-byte UTF-8 sequence". This was a large source of exceptions in
43+
* regression runs over APK-heavy corpora.
44+
*
45+
* <p>Real corpus APKs can't be committed, so this builds an equivalent zip in memory:
46+
* two compiled (AXML) entries plus one genuine text-XML entry under assets/ as a control,
47+
* and asserts the AXML entries are detected as application/vnd.android.axml and produce no
48+
* exception, while the text-XML entry is still application/xml.
49+
*/
50+
public class AndroidBinaryXMLTest extends TikaTest {
51+
52+
private static final String AXML = "application/vnd.android.axml";
53+
54+
/**
55+
* Minimal compiled-AXML header: a RES_XML_TYPE ResChunk_header plus the ResStringPool
56+
* chunk real AXML always carries. The magic matches 0x00080003 (LE) at offset 0 and the
57+
* string-pool type 0x0001 at offset 8, so both must be present.
58+
*/
59+
private static byte[] axmlBytes() {
60+
ByteBuffer bb = ByteBuffer.allocate(64).order(ByteOrder.LITTLE_ENDIAN);
61+
bb.putShort((short) 0x0003); // RES_XML_TYPE
62+
bb.putShort((short) 0x0008); // headerSize
63+
bb.putInt(64); // total chunk size == file length (skipped by magic)
64+
bb.putShort((short) 0x0001); // RES_STRING_POOL_TYPE (checked at offset 8)
65+
bb.putShort((short) 0x001C); // string-pool headerSize
66+
bb.putInt(0x00000038); // string-pool chunk size == 64 - 8 (spans offset 8..EOF)
67+
// remaining bytes (string/style counts, flags, offsets) left zero
68+
return bb.array();
69+
}
70+
71+
private static byte[] zipWith(String[] names, byte[][] contents) throws Exception {
72+
ByteArrayOutputStream bos = new ByteArrayOutputStream();
73+
try (ZipOutputStream zos = new ZipOutputStream(bos)) {
74+
for (int i = 0; i < names.length; i++) {
75+
zos.putNextEntry(new ZipEntry(names[i]));
76+
zos.write(contents[i]);
77+
zos.closeEntry();
78+
}
79+
}
80+
return bos.toByteArray();
81+
}
82+
83+
@Test
84+
public void testAxmlInsideZipNotRoutedToXmlParser() throws Exception {
85+
byte[] textXml =
86+
"<?xml version=\"1.0\"?><root><city>example</city></root>".getBytes(StandardCharsets.UTF_8);
87+
byte[] zip = zipWith(
88+
new String[] {"AndroidManifest.xml", "res/anim/anim0to1.xml", "assets/province_data.xml"},
89+
new byte[][] {axmlBytes(), axmlBytes(), textXml});
90+
91+
List<Metadata> metadataList;
92+
try (TikaInputStream tis = TikaInputStream.get(zip)) {
93+
metadataList = getRecursiveMetadata(tis, true);
94+
}
95+
96+
Metadata manifest = byPathSuffix(metadataList, "AndroidManifest.xml");
97+
Metadata resAnim = byPathSuffix(metadataList, "anim0to1.xml");
98+
Metadata assetXml = byPathSuffix(metadataList, "province_data.xml");
99+
100+
// The two compiled AXML entries: detected as AXML, NOT routed to the XML parser.
101+
assertEquals(AXML, manifest.get(Metadata.CONTENT_TYPE));
102+
assertEquals(AXML, resAnim.get(Metadata.CONTENT_TYPE));
103+
assertNull(manifest.get(TikaCoreProperties.EMBEDDED_EXCEPTION),
104+
"AXML manifest must not throw a parse exception");
105+
assertNull(resAnim.get(TikaCoreProperties.EMBEDDED_EXCEPTION),
106+
"AXML resource must not throw a parse exception");
107+
108+
// Control: a genuine text XML under assets/ is still detected and parsed as XML.
109+
assertEquals("application/xml", assetXml.get(Metadata.CONTENT_TYPE));
110+
assertNull(assetXml.get(TikaCoreProperties.EMBEDDED_EXCEPTION));
111+
}
112+
113+
private static Metadata byPathSuffix(List<Metadata> metadataList, String suffix) {
114+
for (Metadata m : metadataList) {
115+
String path = m.get(TikaCoreProperties.EMBEDDED_RESOURCE_PATH);
116+
if (path != null && path.endsWith(suffix)) {
117+
return m;
118+
}
119+
}
120+
throw new AssertionError("No embedded entry found ending with: " + suffix);
121+
}
122+
}

0 commit comments

Comments
 (0)