blob: 799ad7b51f8732e00d60b3b0c4f5334abdb7bba3 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2018 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.debug.core.sourcelookup.containers;
import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.sourcelookup.ISourceContainerType;
import org.eclipse.debug.internal.core.sourcelookup.SourceLookupMessages;
import org.eclipse.debug.internal.core.sourcelookup.SourceLookupUtils;
/**
* An archive in the local file system. Returns instances
* of <code>ZipEntryStorage</code> as source elements.
* <p>
* Clients may instantiate this class.
* </p>
* @since 3.0
* @noextend This class is not intended to be subclassed by clients.
*/
public class ExternalArchiveSourceContainer extends AbstractSourceContainer {
private boolean fDisposed;
private boolean fDetectRoots;
private Set<String> fPotentialRoots;
private List<String> fRoots = new ArrayList<>();
private String fArchivePath;
/**
* Unique identifier for the external archive source container type
* (value <code>org.eclipse.debug.core.containerType.externalArchive</code>).
*/
public static final String TYPE_ID = DebugPlugin.getUniqueIdentifier() + ".containerType.externalArchive"; //$NON-NLS-1$
/**
* Creates an archive source container on the archive at the
* specified location in the local file system.
*
* @param archivePath path to the archive in the local file system
* @param detectRootPaths whether root container paths should be detected. When
* <code>true</code>, searching is performed relative to a root path
* within the archive based on fully qualified file names. A root
* path is automatically determined for when the first
* successful search is performed. For example, when searching for a file
* named <code>a/b/c.d</code>, and an entry in the archive named
* <code>r/a/b/c.d</code> exists, a root path is set to <code>r</code>.
* When searching for an unqualified file name, root containers are not
* considered.
* When <code>false</code>, searching is performed by
* matching file names as suffixes to the entries in the archive.
*/
public ExternalArchiveSourceContainer(String archivePath, boolean detectRootPaths) {
fArchivePath = archivePath;
fDetectRoots = detectRootPaths;
}
// Suppress resource leak warning. The ZipFile is provided from
// SourceLookupUtils which take care to close them at some point.
@SuppressWarnings("resource")
@Override
public Object[] findSourceElements(String name) throws CoreException {
String newname = name.replace('\\', '/');
ZipFile file = getArchive();
if (file == null) {
return EMPTY;
}
// NOTE: archive can be closed between get (above) and synchronized block (below)
synchronized (file) {
boolean isQualfied = newname.indexOf('/') > 0;
if (fDetectRoots && isQualfied) {
ZipEntry entry = searchRoots(file, newname);
if (entry != null) {
return new Object[]{new ZipEntryStorage(file, entry)};
}
} else {
// try exact match
ZipEntry entry = null;
try {
entry = file.getEntry(newname);
} catch (IllegalStateException e) {
// archive was closed between retrieving and locking
throw new CoreException(new Status(IStatus.ERROR, DebugPlugin.getUniqueIdentifier(),
e.getMessage(), e));
}
if (entry != null) {
// can't be any duplicates if there is an exact match
return new Object[]{new ZipEntryStorage(file, entry)};
}
// search
Enumeration<? extends ZipEntry> entries = file.entries();
List<ZipEntryStorage> matches = null;
try {
File zipFile = new File(fArchivePath);
String zipFileCanonical = zipFile.getCanonicalPath();
while (entries.hasMoreElements()) {
entry = entries.nextElement();
String entryName = entry.getName();
if (entryName.endsWith(newname)) {
String zipEntryCanonical = (new File(zipFile, entryName)).getCanonicalPath();
if (!zipEntryCanonical.startsWith(zipFileCanonical + File.separator)) {
throw new CoreException(new Status(IStatus.ERROR, DebugPlugin.getUniqueIdentifier(), "Invalid path: " + zipEntryCanonical)); //$NON-NLS-1$
}
if (isQualfied || entryName.length() == newname.length() || entryName.charAt(entryName.length() - newname.length() - 1) == '/') {
if (isFindDuplicates()) {
if (matches == null) {
matches = new ArrayList<>();
}
matches.add(new ZipEntryStorage(file, entry));
} else {
return new Object[] {
new ZipEntryStorage(file, entry) };
}
}
}
}
} catch (IOException e) {
throw new CoreException(new Status(IStatus.ERROR, DebugPlugin.getUniqueIdentifier(), "Invalid path: " + fArchivePath)); //$NON-NLS-1$
}
if (matches != null) {
return matches.toArray();
}
}
}
return EMPTY;
}
/**
* Returns the root path in this archive for the given file name, based
* on its type, or <code>null</code> if none. Detects a root if a root has
* not yet been detected for the given file type.
*
* @param file zip file to search in
* @param name file name
* @return the {@link ZipEntry} with the given name or <code>null</code>
* @exception CoreException if an exception occurs while detecting the root
*/
private synchronized ZipEntry searchRoots(ZipFile file, String name) throws CoreException {
if (fDisposed) {
return null;
}
if (fPotentialRoots == null) {
fPotentialRoots = new HashSet<>();
fPotentialRoots.add(""); //$NON-NLS-1$
// all potential roots are the directories
try {
Enumeration<? extends ZipEntry> entries = file.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if (entry.isDirectory()) {
fPotentialRoots.add(entry.getName());
} else {
String entryName = entry.getName();
int index = entryName.lastIndexOf('/');
while (index > 0) {
if (fPotentialRoots.add(entryName.substring(0, index + 1))) {
entryName = entryName.substring(0, index);
index = entryName.lastIndexOf('/');
} else {
break;
}
}
}
}
} catch (IllegalStateException e) {
// archive was closed between retrieving and locking
throw new CoreException(new Status(IStatus.ERROR, DebugPlugin.getUniqueIdentifier(),
e.getMessage(), e));
}
}
int i = 0;
while (i < fRoots.size()) {
String root = fRoots.get(i);
ZipEntry entry = file.getEntry(root+name);
if (entry != null) {
return entry;
}
i++;
}
if (!fPotentialRoots.isEmpty()) {
for (String root : fPotentialRoots) {
ZipEntry entry = file.getEntry(root + name);
if (entry != null) {
if (root != null) {
fRoots.add(root);
fPotentialRoots.remove(root);
// remove any roots that begin with the new root, as
// roots
// cannot be nested
Iterator<String> rs = fPotentialRoots.iterator();
while (rs.hasNext()) {
String r = rs.next();
if (r.startsWith(root)) {
rs.remove();
}
}
}
return entry;
}
}
}
return null;
}
/**
* Returns the archive to search in.
* @return the {@link ZipFile} to search in
*
* @throws CoreException if unable to access the archive
*/
private synchronized ZipFile getArchive() throws CoreException {
if (fDisposed) {
return null;
}
try {
return SourceLookupUtils.getZipFile(fArchivePath);
} catch (IOException e) {
File file = new File(fArchivePath);
if (file.exists()) {
abort(MessageFormat.format(SourceLookupMessages.ExternalArchiveSourceContainer_2, new Object[] { fArchivePath }), e);
} else {
warn(MessageFormat.format(SourceLookupMessages.ExternalArchiveSourceContainer_1, new Object[] { fArchivePath }), e);
}
}
return null;
}
@Override
public String getName() {
return fArchivePath;
}
@Override
public ISourceContainerType getType() {
return getSourceContainerType(TYPE_ID);
}
/**
* Returns whether root paths are automatically detected in this
* archive source container.
*
* @return whether root paths are automatically detected in this
* archive source container
*/
public boolean isDetectRoot() {
return fDetectRoots;
}
@Override
public boolean equals(Object obj) {
return obj instanceof ExternalArchiveSourceContainer &&
((ExternalArchiveSourceContainer)obj).getName().equals(getName());
}
@Override
public int hashCode() {
return getName().hashCode();
}
@Override
public synchronized void dispose() {
super.dispose();
if (fPotentialRoots != null) {
fPotentialRoots.clear();
}
fRoots.clear();
fDisposed = true;
}
}