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 204ddced4e TIKA-5043: Let RarParser read RAR5 archives (#3536)
204ddced4e is described below

commit 204ddced4eab54a02d6c401c03e441276c8ff085
Author: Dominik Schmidt <[email protected]>
AuthorDate: Thu Nov 5 21:38:18 2026 +0100

    TIKA-5043: Let RarParser read RAR5 archives (#3536)
    
    * TIKA-5043: let RarParser read RAR5 archives
    
    * TIKA-5043: RAR5 times as instants, cap the dictionary size
    
    RAR5 stores times as Unix time or Windows FILETIME, instants, where RAR4 
stores DOS local time; they are now set as UTC dates instead of the JVM zone's 
wall clock. junrar allows a 4 GB dictionary by default and a RAR5 window grows 
with what is unpacked; archives are opened with a 1 GB cap, what WinRAR 5 and 6 
write. A test writes an mtime into the synthetic RAR5 and parses it under three 
time zones.
---
 CHANGES.txt                                        |  4 +
 .../java/org/apache/tika/parser/pkg/RarParser.java | 33 +++++---
 .../org/apache/tika/parser/pkg/RarParserTest.java  | 94 ++++++++++++++++++++++
 3 files changed, 119 insertions(+), 12 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index ccb3c9f882..3e090af93b 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -81,6 +81,10 @@ Release 4.3.0 - ???
 
   EXTRACTION AND FORMATS
 
+   * RAR5 archives are parsed: RarParser no longer refuses them, junrar has 
read RAR5
+     since 8.0.0 (TIKA-5043).
+
+
    * Jupyter notebooks (.ipynb, new type application/x-ipynb+json) are parsed: 
cell
      sources and plain-text outputs as text, kernel, language and format 
version as
      jupyter:kernel, jupyter:language and jupyter:nbformat, and the image 
outputs as
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pkg-module/src/main/java/org/apache/tika/parser/pkg/RarParser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pkg-module/src/main/java/org/apache/tika/parser/pkg/RarParser.java
index 377667da03..c121232047 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pkg-module/src/main/java/org/apache/tika/parser/pkg/RarParser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pkg-module/src/main/java/org/apache/tika/parser/pkg/RarParser.java
@@ -22,6 +22,8 @@ import java.util.Collections;
 import java.util.Set;
 
 import com.github.junrar.Archive;
+import com.github.junrar.ArchiveOptions;
+import com.github.junrar.RarFormat;
 import com.github.junrar.exception.RarException;
 import com.github.junrar.rarfile.FileHeader;
 import org.apache.commons.io.input.ProxyInputStream;
@@ -31,13 +33,11 @@ import org.xml.sax.SAXException;
 import org.apache.tika.annotation.TikaComponent;
 import org.apache.tika.exception.EncryptedDocumentException;
 import org.apache.tika.exception.TikaException;
-import org.apache.tika.exception.UnsupportedFormatException;
 import org.apache.tika.extractor.EmbeddedDocumentExtractor;
 import org.apache.tika.extractor.EmbeddedDocumentUtil;
 import org.apache.tika.io.TemporaryResources;
 import org.apache.tika.io.TikaInputStream;
 import org.apache.tika.metadata.FileSystem;
-import org.apache.tika.metadata.HttpHeaders;
 import org.apache.tika.metadata.Metadata;
 import org.apache.tika.mime.MediaType;
 import org.apache.tika.parser.ParseContext;
@@ -51,6 +51,12 @@ import org.apache.tika.sax.XHTMLContentHandler;
 public class RarParser implements Parser {
     private static final long serialVersionUID = 6157727985054451501L;
 
+    /**
+     * The largest dictionary an archive may declare. WinRAR 5 and 6 write up 
to 1 GB; junrar's
+     * default is 4 GB, and a RAR5 window grows with the data unpacked into it.
+     */
+    static final long MAX_DICTIONARY_SIZE = 1024L * 1024 * 1024;
+
     private static final Set<MediaType> SUPPORTED_TYPES =
             Collections.singleton(MediaType.application("x-rar-compressed"));
 
@@ -68,14 +74,10 @@ public class RarParser implements Parser {
 
         EmbeddedDocumentExtractor extractor =
                 EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context);
-        String mediaType = metadata.get(HttpHeaders.CONTENT_TYPE);
-
-        if (mediaType != null && mediaType.contains("version=5")) {
-            throw new UnsupportedFormatException("Tika does not yet support 
rar version 5.");
-        }
         Archive rar = null;
         try (TemporaryResources tmp = new TemporaryResources()) {
-            rar = new Archive(tis.getFile());
+            rar = new Archive(tis.getFile(),
+                    
ArchiveOptions.builder().maxDictionarySize(MAX_DICTIONARY_SIZE).build());
 
             if (rar.isEncrypted()) {
                 throw new EncryptedDocumentException();
@@ -89,10 +91,17 @@ public class RarParser implements Parser {
                 if (!header.isDirectory()) {
                     Metadata entrydata = 
AbstractArchiveParser.handleEntryMetadata(
                             header.getFileName(), null, null, 
header.getFullUnpackSize(), xhtml, context);
-                    // RAR4 times are DOS local time; junrar resolves them in 
the JVM default zone
-                    AbstractArchiveParser.setLocalTime(entrydata, 
FileSystem.CREATED, header.getCTime());
-                    AbstractArchiveParser.setLocalTime(entrydata, 
FileSystem.MODIFIED, header.getMTime());
-                    AbstractArchiveParser.setLocalTime(entrydata, 
FileSystem.ACCESSED, header.getATime());
+                    if (rar.getFormat() == RarFormat.RAR50) {
+                        // RAR5 stores instants, as Unix time or Windows 
FILETIME
+                        AbstractArchiveParser.setInstant(entrydata, 
FileSystem.CREATED, header.getCTime());
+                        AbstractArchiveParser.setInstant(entrydata, 
FileSystem.MODIFIED, header.getMTime());
+                        AbstractArchiveParser.setInstant(entrydata, 
FileSystem.ACCESSED, header.getATime());
+                    } else {
+                        // RAR4 times are DOS local time; junrar resolves them 
in the JVM default zone
+                        AbstractArchiveParser.setLocalTime(entrydata, 
FileSystem.CREATED, header.getCTime());
+                        AbstractArchiveParser.setLocalTime(entrydata, 
FileSystem.MODIFIED, header.getMTime());
+                        AbstractArchiveParser.setLocalTime(entrydata, 
FileSystem.ACCESSED, header.getATime());
+                    }
                     EntryStream entryStream = new 
EntryStream(rar.getInputStream(header));
                     try (TikaInputStream rarTis = 
TikaInputStream.get(entryStream)) {
                         if (extractor.shouldParseEmbedded(entrydata, 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/RarParserTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pkg-module/src/test/java/org/apache/tika/parser/pkg/RarParserTest.java
index fc0e601e52..cae3f69a57 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pkg-module/src/test/java/org/apache/tika/parser/pkg/RarParserTest.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pkg-module/src/test/java/org/apache/tika/parser/pkg/RarParserTest.java
@@ -22,9 +22,13 @@ import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.List;
+import java.util.TimeZone;
+import java.util.zip.CRC32;
 
 import org.apache.commons.io.IOUtils;
 import org.junit.jupiter.api.Test;
@@ -33,6 +37,8 @@ import org.xml.sax.ContentHandler;
 
 import org.apache.tika.exception.EncryptedDocumentException;
 import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.FileSystem;
+import org.apache.tika.metadata.HttpHeaders;
 import org.apache.tika.metadata.Metadata;
 import org.apache.tika.metadata.TikaCoreProperties;
 import org.apache.tika.parser.ParseContext;
@@ -119,6 +125,94 @@ public class RarParserTest extends AbstractPkgTest {
         assertTrue(hasEmbeddedPath(metadataList, "/testXML.xml"));
     }
 
+    @Test
+    public void testRar5(@TempDir Path tmp) throws Exception {
+        Path rar5 = Files.write(tmp.resolve("stored.rar"), storedRar5(-1,
+                "page-002.txt", "second page", "page-001.txt", "first page"));
+        List<Metadata> metadataList = getRecursiveMetadata(rar5, new 
ParseContext(), false);
+        assertContains("version=5", 
metadataList.get(0).get(HttpHeaders.CONTENT_TYPE));
+        
assertNull(metadataList.get(0).get(TikaCoreProperties.CONTAINER_EXCEPTION));
+        assertEquals(3, metadataList.size());
+        assertTrue(hasEmbeddedPath(metadataList, "/page-001.txt"));
+        assertTrue(hasEmbeddedPath(metadataList, "/page-002.txt"));
+        assertContains("page-001.txt", 
metadataList.get(0).get(TikaCoreProperties.TIKA_CONTENT));
+    }
+
+    /** RAR5 stores a Unix time, an instant: it does not move with the JVM's 
time zone. */
+    @Test
+    public void testRar5TimesAreInstants(@TempDir Path tmp) throws Exception {
+        Path rar5 = Files.write(tmp.resolve("timed.rar"),
+                storedRar5(1_700_000_000L, "a.txt", "a"));
+        TimeZone zone = TimeZone.getDefault();
+        try {
+            for (String id : new String[]{"UTC", "America/New_York", 
"Asia/Tokyo"}) {
+                TimeZone.setDefault(TimeZone.getTimeZone(id));
+                List<Metadata> metadataList = getRecursiveMetadata(rar5, new 
ParseContext(), false);
+                assertEquals("2023-11-14T22:13:20Z",
+                        metadataList.get(1).get(FileSystem.MODIFIED), id);
+            }
+        } finally {
+            TimeZone.setDefault(zone);
+        }
+    }
+
+    /**
+     * A RAR5 archive of stored (uncompressed) files, written after the RAR 
5.0 archive format
+     * description (https://www.rarlab.com/technote.htm): signature, main 
archive header, one
+     * file header with its data per file, end of archive header. A 
modification time other
+     * than -1 is written as Unix time.
+     */
+    private static byte[] storedRar5(long mtime, String... namesAndContents) {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        out.writeBytes(new byte[]{'R', 'a', 'r', '!', 0x1a, 0x07, 0x01, 0x00});
+        out.writeBytes(rar5Block(concat(vint(1), vint(0), vint(0))));
+        for (int i = 0; i < namesAndContents.length; i += 2) {
+            byte[] name = namesAndContents[i].getBytes(StandardCharsets.UTF_8);
+            byte[] data = namesAndContents[i + 
1].getBytes(StandardCharsets.UTF_8);
+            CRC32 crc = new CRC32();
+            crc.update(data);
+            byte[] time = mtime < 0 ? new byte[0] : littleEndian((int) mtime);
+            out.writeBytes(rar5Block(concat(vint(2), vint(0x0002), 
vint(data.length),
+                    vint(mtime < 0 ? 0x0004 : 0x0006), vint(data.length), 
vint(0x20), time,
+                    littleEndian((int) crc.getValue()), vint(0), vint(0), 
vint(name.length),
+                    name)));
+            out.writeBytes(data);
+        }
+        out.writeBytes(rar5Block(concat(vint(5), vint(0), vint(0))));
+        return out.toByteArray();
+    }
+
+    /** A header block: CRC32 of the size and the header, the size as a vint, 
the header. */
+    private static byte[] rar5Block(byte[] header) {
+        byte[] sized = concat(vint(header.length), header);
+        CRC32 crc = new CRC32();
+        crc.update(sized);
+        return concat(littleEndian((int) crc.getValue()), sized);
+    }
+
+    private static byte[] vint(long value) {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        do {
+            int b = (int) (value & 0x7f);
+            value >>>= 7;
+            out.write(value != 0 ? b | 0x80 : b);
+        } while (value != 0);
+        return out.toByteArray();
+    }
+
+    private static byte[] littleEndian(int value) {
+        return new byte[]{(byte) value, (byte) (value >>> 8), (byte) (value 
>>> 16),
+                (byte) (value >>> 24)};
+    }
+
+    private static byte[] concat(byte[]... parts) {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        for (byte[] part : parts) {
+            out.writeBytes(part);
+        }
+        return out.toByteArray();
+    }
+
     private static boolean hasEmbeddedPath(List<Metadata> metadataList, String 
path) {
         return metadataList.stream()
                 .anyMatch(m -> 
path.equals(m.get(TikaCoreProperties.EMBEDDED_RESOURCE_PATH)));

Reply via email to