[vfs] prevent OOME on reading enormous files from archives (IDEA-167934)

This commit is contained in:
Roman Shevchenko
2017-02-10 18:22:54 +01:00
parent efbc83b2f6
commit d1b900d161
3 changed files with 114 additions and 36 deletions
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@ package com.intellij.openapi.vfs.impl;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.BufferExposingByteArrayInputStream;
import com.intellij.openapi.util.io.FileAttributes;
import com.intellij.openapi.util.io.FileSystemUtil;
import com.intellij.reference.SoftReference;
@@ -30,6 +31,7 @@ import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.Reference;
import java.util.Collections;
import java.util.List;
@@ -233,6 +235,11 @@ public abstract class ArchiveHandler {
@NotNull
public abstract byte[] contentsToByteArray(@NotNull String relativePath) throws IOException;
@NotNull
public InputStream getInputStream(@NotNull String relativePath) throws IOException {
return new BufferExposingByteArrayInputStream(contentsToByteArray(relativePath));
}
private static final AddonlyKeylessHash.KeyValueMapper<EntryInfo, Object> ourKeyValueMapper = new AddonlyKeylessHash.KeyValueMapper<EntryInfo, Object>() {
@Override
public int hash(EntryInfo info) {
@@ -17,9 +17,7 @@ package com.intellij.openapi.vfs.impl;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileAttributes;
import com.intellij.openapi.util.io.FileSystemUtil;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.io.*;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.io.FileAccessorCache;
import com.intellij.util.text.ByteArrayCharSequence;
@@ -33,6 +31,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
@@ -108,27 +107,34 @@ public class ZipHandler extends ArchiveHandler {
}
@Contract("true -> !null")
protected FileAccessorCache.Handle<ZipFile> getCachedZipFileHandle(boolean createHandleIfNeeded) throws IOException {
FileAccessorCache.Handle<ZipFile> handle = createHandleIfNeeded ? ourZipFileFileAccessorCache.get(this) : ourZipFileFileAccessorCache.getIfCached(this);
protected FileAccessorCache.Handle<ZipFile> getCachedZipFileHandle(boolean createIfNeeded) throws IOException {
try {
FileAccessorCache.Handle<ZipFile> handle = createIfNeeded ? ourZipFileFileAccessorCache.get(this) : ourZipFileFileAccessorCache.getIfCached(this);
// check handle is valid
if (handle != null && getFile() == getFileToUse()) { // files are canonicalized
// IDEA-148458, http://bugs.java.com/view_bug.do?bug_id=4425695, JVM crashes on use of opened ZipFile after it was updated
// Reopen file if the file has been changed
FileAttributes attributes = FileSystemUtil.getAttributes(getCanonicalPathToZip());
if (attributes == null) {
throw new FileNotFoundException(getCanonicalPathToZip());
// check handle is valid
if (handle != null && getFile() == getFileToUse()) { // files are canonicalized
// IDEA-148458, http://bugs.java.com/view_bug.do?bug_id=4425695, JVM crashes on use of opened ZipFile after it was updated
// Reopen file if the file has been changed
FileAttributes attributes = FileSystemUtil.getAttributes(getCanonicalPathToZip());
if (attributes == null) {
throw new FileNotFoundException(getCanonicalPathToZip());
}
if (attributes.lastModified == myFileStamp && attributes.length == myFileLength) return handle;
// Note that zip_util.c#ZIP_Get_From_Cache will allow us to have duplicated ZipFile instances without a problem
removeZipHandlerFromCache();
handle.release();
handle = ourZipFileFileAccessorCache.get(this);
}
if (attributes.lastModified == myFileStamp && attributes.length == myFileLength) return handle;
// Note that zip_util.c#ZIP_Get_From_Cache will allow us to have duplicated ZipFile instances without a problem
removeZipHandlerFromCache();
handle.release();
handle = ourZipFileFileAccessorCache.get(this);
return handle;
}
catch (RuntimeException e) {
Throwable cause = e.getCause();
if (cause instanceof IOException) throw (IOException)cause;
throw e;
}
return handle;
}
private void removeZipHandlerFromCache() {
@@ -207,26 +213,20 @@ public class ZipHandler extends ArchiveHandler {
@NotNull
@Override
public byte[] contentsToByteArray(@NotNull String relativePath) throws IOException {
FileAccessorCache.Handle<ZipFile> zipRef;
try {
zipRef = getCachedZipFileHandle(true);
}
catch (RuntimeException ex) {
Throwable cause = ex.getCause();
if (cause instanceof IOException) throw (IOException)cause;
throw ex;
}
FileAccessorCache.Handle<ZipFile> zipRef = getCachedZipFileHandle(true);
try {
ZipFile zip = zipRef.get();
ZipEntry entry = zip.getEntry(relativePath);
if (entry != null) {
long length = entry.getSize();
if (FileUtilRt.isTooLarge(length)) {
throw new FileTooBigException(getFile() + "!/" + relativePath);
}
InputStream stream = zip.getInputStream(entry);
if (stream != null) {
// ZipFile.c#Java_java_util_zip_ZipFile_read reads data in 8K (stack allocated) blocks - no sense to create BufferedInputStream
try {
return FileUtil.loadBytes(stream, (int)entry.getSize());
return FileUtil.loadBytes(stream, (int)length);
}
finally {
stream.close();
@@ -241,6 +241,78 @@ public class ZipHandler extends ArchiveHandler {
throw new FileNotFoundException(getFile() + "!/" + relativePath);
}
@NotNull
@Override
public InputStream getInputStream(@NotNull String relativePath) throws IOException {
boolean release = true;
final FileAccessorCache.Handle<ZipFile> zipRef = getCachedZipFileHandle(true);
try {
ZipFile zip = zipRef.get();
ZipEntry entry = zip.getEntry(relativePath);
if (entry != null) {
InputStream stream = zip.getInputStream(entry);
if (stream != null) {
long length = entry.getSize();
if (!FileUtilRt.isTooLarge(length)) {
try {
return new BufferExposingByteArrayInputStream(FileUtil.loadBytes(stream, (int)length));
}
finally {
stream.close();
}
}
else {
release = false;
return new InputStreamWrapper(stream, zipRef);
}
}
}
}
finally {
if (release) zipRef.release();
}
throw new FileNotFoundException(getFile() + "!/" + relativePath);
}
private static class InputStreamWrapper extends InputStream {
private final InputStream myStream;
private final FileAccessorCache.Handle<ZipFile> myZipRef;
private final AtomicBoolean closed = new AtomicBoolean(false);
public InputStreamWrapper(InputStream stream, FileAccessorCache.Handle<ZipFile> zipRef) {
myStream = stream;
myZipRef = zipRef;
}
@Override
public int read() throws IOException {
return myStream.read();
}
@Override
public int read(@NotNull byte[] b, int off, int len) throws IOException {
return myStream.read(b, off, len);
}
@Override
public int available() throws IOException {
return myStream.available();
}
@Override
public void close() throws IOException {
if (!closed.getAndSet(true)) {
try {
myStream.close();
}
finally {
myZipRef.release();
}
}
}
}
// also used in Kotlin
public static void clearFileAccessorCache() {
ourZipFileFileAccessorCache.clear();
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,7 +18,6 @@ package com.intellij.openapi.vfs.newvfs;
import com.intellij.openapi.fileTypes.FileTypeRegistry;
import com.intellij.openapi.fileTypes.FileTypes;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.io.BufferExposingByteArrayInputStream;
import com.intellij.openapi.util.io.FileAttributes;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.*;
@@ -215,7 +214,7 @@ public abstract class ArchiveFileSystem extends NewVirtualFileSystem {
@NotNull
@Override
public InputStream getInputStream(@NotNull VirtualFile file) throws IOException {
return new BufferExposingByteArrayInputStream(contentsToByteArray(file));
return getHandler(file).getInputStream(getRelativePath(file));
}
@Override