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 c7f68988f6 TIKA-4908: shield the archive stream when closing an
entry's TikaInputStream (#3206)
c7f68988f6 is described below
commit c7f68988f6b9901cc93ba523731ac0bbc3d3980c
Author: Tim Allison <[email protected]>
AuthorDate: Sat Sep 19 07:29:17 2026 -0400
TIKA-4908: shield the archive stream when closing an entry's
TikaInputStream (#3206)
---
.../org/apache/tika/parser/pkg/PackageParser.java | 8 +-
.../java/org/apache/tika/parser/pkg/ZipParser.java | 6 +-
.../tika/parser/pkg/PackageMultiEntryTest.java | 150 +++++++++++++++++++++
3 files changed, 160 insertions(+), 4 deletions(-)
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pkg-module/src/main/java/org/apache/tika/parser/pkg/PackageParser.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pkg-module/src/main/java/org/apache/tika/parser/pkg/PackageParser.java
index 1bc5b317b7..f0441e5e4b 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pkg-module/src/main/java/org/apache/tika/parser/pkg/PackageParser.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pkg-module/src/main/java/org/apache/tika/parser/pkg/PackageParser.java
@@ -35,6 +35,7 @@ import
org.apache.commons.compress.archivers.arj.ArjArchiveInputStream;
import org.apache.commons.compress.archivers.cpio.CpioArchiveInputStream;
import org.apache.commons.compress.archivers.dump.DumpArchiveInputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+import org.apache.commons.io.input.CloseShieldInputStream;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
@@ -176,9 +177,10 @@ public class PackageParser extends AbstractArchiveParser {
if (extractor.shouldParseEmbedded(entrydata, context)) {
TemporaryResources tmp = new TemporaryResources();
- // close the stream, not only the temp dir: an in-memory cache
is not owned by
- // tmp, and its budget reservation is released only on close
- try (TikaInputStream tis = TikaInputStream.get(archive, tmp,
entrydata)) {
+ // Close the entry stream so its in-memory cache is released,
but shield the
+ // archive: cpio's stream refuses further reads once closed.
+ try (TikaInputStream tis = TikaInputStream.get(
+ CloseShieldInputStream.wrap(archive), tmp, entrydata))
{
extractor.parseEmbedded(tis, xhtml, entrydata, context,
true);
} finally {
tmp.dispose();
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 c21ece862b..eb3ed69419 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
@@ -38,6 +38,7 @@ import
org.apache.commons.compress.archivers.zip.UnsupportedZipFeatureException.
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.apache.commons.compress.archivers.zip.ZipFile;
+import org.apache.commons.io.input.CloseShieldInputStream;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
@@ -548,7 +549,10 @@ public class ZipParser extends AbstractArchiveParser {
if (extractor.shouldParseEmbedded(entryMetadata, context)) {
TemporaryResources tmp = new TemporaryResources();
- try (TikaInputStream tis = TikaInputStream.get(zis, tmp,
entryMetadata)) {
+ // shield the archive stream: a closed ZipArchiveInputStream
silently reports no
+ // more entries
+ try (TikaInputStream tis = TikaInputStream.get(
+ CloseShieldInputStream.wrap(zis), tmp, entryMetadata)) {
extractor.parseEmbedded(tis, xhtml, entryMetadata, context,
true);
} catch (UnsupportedZipFeatureException e) {
EmbeddedDocumentUtil.recordEmbeddedStreamException(e,
parentMetadata, context);
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pkg-module/src/test/java/org/apache/tika/parser/pkg/PackageMultiEntryTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pkg-module/src/test/java/org/apache/tika/parser/pkg/PackageMultiEntryTest.java
new file mode 100644
index 0000000000..707a0b03a9
--- /dev/null
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pkg-module/src/test/java/org/apache/tika/parser/pkg/PackageMultiEntryTest.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser.pkg;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.apache.commons.compress.archivers.ArchiveOutputStream;
+import org.apache.commons.compress.archivers.ar.ArArchiveEntry;
+import org.apache.commons.compress.archivers.ar.ArArchiveOutputStream;
+import org.apache.commons.compress.archivers.cpio.CpioArchiveEntry;
+import org.apache.commons.compress.archivers.cpio.CpioArchiveOutputStream;
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
+import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import org.apache.tika.metadata.HttpHeaders;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
+
+/**
+ * Every entry of a multi-entry archive must be parsed. The per-entry
TikaInputStream wraps the
+ * archive's own stream; closing it must not close the archive (cpio's stream
refuses further
+ * reads once closed, zip's silently returns no more entries).
+ */
+public class PackageMultiEntryTest extends AbstractPkgTest {
+
+ @TempDir
+ Path tmp;
+
+ private static final String[] NAMES = {"a.txt", "b.txt", "c.txt"};
+
+ private static byte[] body(String name) {
+ return ("entry " + name + "
body\n").repeat(64).getBytes(StandardCharsets.UTF_8);
+ }
+
+ private static byte[] cpio() throws IOException {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ try (CpioArchiveOutputStream out = new CpioArchiveOutputStream(bos)) {
+ for (String n : NAMES) {
+ byte[] b = body(n);
+ CpioArchiveEntry e = new CpioArchiveEntry(n);
+ e.setSize(b.length);
+ put(out, e, b);
+ }
+ }
+ return bos.toByteArray();
+ }
+
+ private static byte[] ar() throws IOException {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ try (ArArchiveOutputStream out = new ArArchiveOutputStream(bos)) {
+ for (String n : NAMES) {
+ byte[] b = body(n);
+ put(out, new ArArchiveEntry(n, b.length), b);
+ }
+ }
+ return bos.toByteArray();
+ }
+
+ private static byte[] tar() throws IOException {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ try (TarArchiveOutputStream out = new TarArchiveOutputStream(bos)) {
+ for (String n : NAMES) {
+ byte[] b = body(n);
+ TarArchiveEntry e = new TarArchiveEntry(n);
+ e.setSize(b.length);
+ put(out, e, b);
+ }
+ }
+ return bos.toByteArray();
+ }
+
+ private static byte[] zip() throws IOException {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ try (ZipArchiveOutputStream out = new ZipArchiveOutputStream(bos)) {
+ for (String n : NAMES) {
+ put(out, new ZipArchiveEntry(n), body(n));
+ }
+ }
+ return bos.toByteArray();
+ }
+
+ private static <E extends
org.apache.commons.compress.archivers.ArchiveEntry> void put(
+ ArchiveOutputStream<E> out, E entry, byte[] b) throws IOException {
+ out.putArchiveEntry(entry);
+ out.write(b);
+ out.closeArchiveEntry();
+ }
+
+ /** The module's test classpath has no text parser, so entries are checked
by name and
+ * declared size rather than by content. */
+ private void assertAllEntries(String ext, byte[] archive) throws Exception
{
+ Path p = tmp.resolve("multi." + ext);
+ Files.write(p, archive);
+ List<Metadata> list = getRecursiveMetadata(p, false);
+ assertEquals(NAMES.length + 1, list.size(), ext + ": container + one
per entry");
+ assertNull(list.get(0).get(TikaCoreProperties.EMBEDDED_EXCEPTION),
ext);
+ for (int i = 0; i < NAMES.length; i++) {
+ Metadata m = list.get(i + 1);
+ assertEquals(NAMES[i],
m.get(TikaCoreProperties.RESOURCE_NAME_KEY), ext);
+ assertEquals(Integer.toString(body(NAMES[i]).length),
m.get(HttpHeaders.CONTENT_LENGTH),
+ ext + ": declared size of " + NAMES[i]);
+ }
+ }
+
+ @Test
+ public void testCpio() throws Exception {
+ assertAllEntries("cpio", cpio());
+ }
+
+ @Test
+ public void testAr() throws Exception {
+ assertAllEntries("ar", ar());
+ }
+
+ @Test
+ public void testTar() throws Exception {
+ assertAllEntries("tar", tar());
+ }
+
+ @Test
+ public void testZip() throws Exception {
+ assertAllEntries("zip", zip());
+ }
+}