Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,19 @@
import java.util.Collections;
import java.util.Set;

import org.apache.commons.compress.MemoryLimitException;
import org.apache.commons.compress.PasswordRequiredException;
import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry;
import org.apache.commons.compress.archivers.sevenz.SevenZFile;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;

import org.apache.tika.config.ConfigDeserializer;
import org.apache.tika.config.JsonConfig;
import org.apache.tika.config.TikaComponent;
import org.apache.tika.exception.EncryptedDocumentException;
import org.apache.tika.exception.TikaException;
import org.apache.tika.exception.TikaMemoryLimitException;
import org.apache.tika.extractor.EmbeddedDocumentExtractor;
import org.apache.tika.extractor.EmbeddedDocumentUtil;
import org.apache.tika.io.TemporaryResources;
Expand Down Expand Up @@ -60,6 +64,37 @@ public class SevenZParser extends AbstractArchiveParser {

private static final Set<MediaType> SUPPORTED_TYPES = Collections.singleton(SEVENZ);

/**
* Configuration class for JSON deserialization.
*/
public static class Config {
// Cap the LZMA/LZMA2 dictionary allocation. A tiny 7z can declare a huge dictionary
// (a single header byte), forcing an eager multi-GiB allocation and OOMing the parser.
// Mirrors CompressorParser's default.
private int memoryLimitInKb = 100000;

public int getMemoryLimitInKb() {
return memoryLimitInKb;
}

public void setMemoryLimitInKb(int memoryLimitInKb) {
this.memoryLimitInKb = memoryLimitInKb;
}
}

private Config defaultConfig = new Config();

public SevenZParser() {
}

public SevenZParser(Config config) {
this.defaultConfig = config;
}

public SevenZParser(JsonConfig jsonConfig) {
this(ConfigDeserializer.buildConfig(jsonConfig, Config.class));
}

@Override
public Set<MediaType> getSupportedTypes(ParseContext context) {
return SUPPORTED_TYPES;
Expand All @@ -78,14 +113,21 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata

SevenZFile sevenZFile;
try {
SevenZFile.Builder builder = new SevenZFile.Builder().setFile(tis.getFile());
// Use setMaxMemoryLimitKiB (direct KiB); setMaxMemoryLimitKb divides the arg by 1024.
SevenZFile.Builder builder = new SevenZFile.Builder()
.setFile(tis.getFile())
.setMaxMemoryLimitKiB(defaultConfig.getMemoryLimitInKb());
if (password == null) {
sevenZFile = builder.get();
} else {
sevenZFile = builder.setPassword(password.toCharArray()).get();
}
} catch (PasswordRequiredException e) {
throw new EncryptedDocumentException(e);
} catch (MemoryLimitException e) {
// The limit can be exceeded at open time (assertValidity) as well as lazily on the
// first getNextEntry() when the LZMA/LZMA2 dictionary is allocated.
throw new TikaMemoryLimitException(e.getMessage());
}

metadata.set(Metadata.CONTENT_TYPE, SEVENZ.toString());
Expand All @@ -106,6 +148,8 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata
}
} catch (PasswordRequiredException e) {
throw new EncryptedDocumentException(e);
} catch (MemoryLimitException e) {
throw new TikaMemoryLimitException(e.getMessage());
} finally {
sevenZFile.close();
xhtml.endDocument();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;
import org.xml.sax.ContentHandler;

import org.apache.tika.exception.TikaMemoryLimitException;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.sax.BodyContentHandler;

/**
Expand Down Expand Up @@ -71,4 +74,23 @@ public void testEmbedded() throws Exception {
assertTrue(mod.startsWith("20"), "Modified at " + mod);
}
}

/**
* A 7z whose declared LZMA2 dictionary exceeds the configured limit must fail with a
* {@link TikaMemoryLimitException} rather than eagerly allocating the dictionary (which a
* tiny crafted 7z can drive to multiple GiB, OOMing the parser). The default-limit control
* is {@link #testEmbedded()} above, which would fail if the limit were applied in the wrong
* unit (e.g. via setMaxMemoryLimitKb, which divides the argument by 1024).
*/
@Test
public void testMemoryLimit() throws Exception {
SevenZParser.Config config = new SevenZParser.Config();
config.setMemoryLimitInKb(1); // 1 KiB, smaller than any real LZMA2 dictionary
SevenZParser parser = new SevenZParser(config);

try (TikaInputStream tis = getResourceAsStream("/test-documents/test-documents.7z")) {
assertThrows(TikaMemoryLimitException.class, () -> parser.parse(tis,
new BodyContentHandler(), new Metadata(), new ParseContext()));
}
}
}
Loading