blob: 852adb9a2b332cc033da40f043ff4c1cfb5f9afa [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2015 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
* Martin Oberhuber (Wind River) - [105554] handle cyclic symbolic links
* Martin Oberhuber (Wind River) - [232426] shared prefix histories for symlinks
* Serge Beauchamp (Freescale Semiconductor) - [252996] add resource filtering
* Martin Oberhuber (Wind River) - [292267] OutOfMemoryError due to leak in UnifiedTree
* James Blackburn (Broadcom Corp.) - ongoing development
* Lars Vogel <Lars.Vogel@vogella.com> - Bug 473427
* Sergey Prigogin (Google) - ongoing development
*******************************************************************************/
package org.eclipse.core.internal.localstore;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.regex.Pattern;
import org.eclipse.core.filesystem.*;
import org.eclipse.core.internal.refresh.RefreshJob;
import org.eclipse.core.internal.resources.*;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.jobs.Job;
/**
* Represents the workspace's tree merged with the file system's tree.
*/
public class UnifiedTree {
/** Skip advanced link checking, see bug 537449 */
private static boolean disable_advanced_recursive_link_checks = System.getProperty("org.eclipse.core.resources.disable_advanced_recursive_link_checks") != null; //$NON-NLS-1$
/** special node to mark the separation of a node's children */
protected static final UnifiedTreeNode childrenMarker = new UnifiedTreeNode(null, null, null, null, false);
private static final Iterator<UnifiedTreeNode> EMPTY_ITERATOR = Collections.EMPTY_LIST.iterator();
/** special node to mark the beginning of a level in the tree */
protected static final UnifiedTreeNode levelMarker = new UnifiedTreeNode(null, null, null, null, false);
private static final IFileInfo[] NO_CHILDREN = {};
/** Singleton to indicate no local children */
private static final IResource[] NO_RESOURCES = {};
/**
* True if the level of the children of the current node are valid according
* to the requested refresh depth, false otherwise
*/
protected boolean childLevelValid;
/** an IFileTree which can be used to build a unified tree*/
protected IFileTree fileTree;
/** Spare node objects available for reuse */
protected ArrayList<UnifiedTreeNode> freeNodes = new ArrayList<>();
/** tree's actual level */
protected int level;
/** our queue */
protected LinkedList<UnifiedTreeNode> queue;
/** path prefixes for checking symbolic link cycles */
protected PrefixPool pathPrefixHistory, rootPathHistory;
/** tree's root */
protected IResource root;
/**
* The root must only be a file or a folder.
*/
public UnifiedTree(IResource root) {
setRoot(root);
}
/**
* Pass in a a root for the tree, a file tree containing all of the entries for this
* tree and a flag indicating whether the UnifiedTree should consult the fileTree where
* possible for entries
* @param root
* @param fileTree
*/
public UnifiedTree(IResource root, IFileTree fileTree) {
this(root);
this.fileTree = fileTree;
}
public void accept(IUnifiedTreeVisitor visitor) throws CoreException {
accept(visitor, IResource.DEPTH_INFINITE);
}
/**
* Performs a breadth-first traversal of the unified tree, passing each
* node to the provided visitor.
*/
public void accept(IUnifiedTreeVisitor visitor, int depth) throws CoreException {
Assert.isNotNull(root);
initializeQueue();
setLevel(0, depth);
while (!queue.isEmpty()) {
UnifiedTreeNode node = queue.remove();
if (isChildrenMarker(node))
continue;
if (isLevelMarker(node)) {
if (!setLevel(getLevel() + 1, depth))
break;
continue;
}
if (visitor.visit(node))
addNodeChildrenToQueue(node);
else
removeNodeChildrenFromQueue(node);
//allow reuse of the node, but don't let the freeNodes list grow infinitely
if (freeNodes.size() < 32767) {
//free memory-consuming elements of the node for garbage collection
node.releaseForGc();
freeNodes.add(node);
}
//else, the whole node will be garbage collected since there is no
//reference to it any more.
}
}
protected void addChildren(UnifiedTreeNode node) {
Resource parent = (Resource) node.getResource();
// is there a possibility to have children?
int parentType = parent.getType();
if (parentType == IResource.FILE && !node.isFolder())
return;
//don't refresh resources in closed or non-existent projects
if (!parent.getProject().isAccessible())
return;
// get the list of resources in the file system
// don't ask for local children if we know it doesn't exist locally
IFileInfo[] list = node.existsInFileSystem() ? getLocalList(node) : NO_CHILDREN;
int localIndex = 0;
// See if the children of this resource have been computed before
ResourceInfo resourceInfo = parent.getResourceInfo(false, false);
int flags = parent.getFlags(resourceInfo);
boolean unknown = ResourceInfo.isSet(flags, ICoreConstants.M_CHILDREN_UNKNOWN);
// get the list of resources in the workspace
if (!unknown && (parentType == IResource.FOLDER || parentType == IResource.PROJECT) && parent.exists(flags, true)) {
IResource target = null;
UnifiedTreeNode child = null;
IResource[] members;
try {
members = ((IContainer) parent).members(IContainer.INCLUDE_TEAM_PRIVATE_MEMBERS | IContainer.INCLUDE_HIDDEN);
} catch (CoreException e) {
members = NO_RESOURCES;
}
int workspaceIndex = 0;
//iterate simultaneously over file system and workspace members
while (workspaceIndex < members.length) {
target = members[workspaceIndex];
String name = target.getName();
IFileInfo localInfo = localIndex < list.length ? list[localIndex] : null;
int comp = localInfo != null ? name.compareTo(localInfo.getName()) : -1;
//special handling for linked resources
if (target.isLinked()) {
//child will be null if location is undefined
child = createChildForLinkedResource(target);
workspaceIndex++;
//if there is a matching local file, skip it - it will be blocked by the linked resource
if (comp == 0)
localIndex++;
} else if (comp == 0) {
// resource exists in workspace and file system --> localInfo is non-null
//create workspace-only node for symbolic link that creates a cycle
if (localInfo.getAttribute(EFS.ATTRIBUTE_SYMLINK) && localInfo.isDirectory() && isRecursiveLink(node.getStore(), localInfo))
child = createNode(target, null, null, true);
else
child = createNode(target, null, localInfo, true);
localIndex++;
workspaceIndex++;
} else if (comp > 0) {
// resource exists only in file system
//don't create a node for symbolic links that create a cycle
if (localInfo.getAttribute(EFS.ATTRIBUTE_SYMLINK) && localInfo.isDirectory() && isRecursiveLink(node.getStore(), localInfo))
child = null;
else
child = createChildNodeFromFileSystem(node, localInfo);
localIndex++;
} else {
// resource exists only in the workspace
child = createNode(target, null, null, true);
workspaceIndex++;
}
if (child != null)
addChildToTree(node, child);
}
}
/* process any remaining resource from the file system */
addChildrenFromFileSystem(node, list, localIndex);
/* Mark the children as now known */
if (unknown) {
// Don't open the info - we might not be inside a workspace-modifying operation
resourceInfo = parent.getResourceInfo(false, false);
if (resourceInfo != null)
resourceInfo.clear(ICoreConstants.M_CHILDREN_UNKNOWN);
}
/* if we added children, add the childMarker separator */
if (node.getFirstChild() != null)
addChildrenMarker();
}
protected void addChildrenFromFileSystem(UnifiedTreeNode node, IFileInfo[] childInfos, int index) {
if (childInfos == null)
return;
for (int i = index; i < childInfos.length; i++) {
IFileInfo info = childInfos[i];
//don't create a node for symbolic links that create a cycle
if (!info.getAttribute(EFS.ATTRIBUTE_SYMLINK) || !info.isDirectory() || !isRecursiveLink(node.getStore(), info))
addChildToTree(node, createChildNodeFromFileSystem(node, info));
}
}
protected void addChildrenMarker() {
addElementToQueue(childrenMarker);
}
protected void addChildToTree(UnifiedTreeNode node, UnifiedTreeNode child) {
if (node.getFirstChild() == null)
node.setFirstChild(child);
addElementToQueue(child);
}
protected void addElementToQueue(UnifiedTreeNode target) {
queue.add(target);
}
protected void addNodeChildrenToQueue(UnifiedTreeNode node) {
/* if the first child is not null we already added the children */
/* If the children won't be at a valid level for the refresh depth, don't bother adding them */
if (!childLevelValid || node.getFirstChild() != null)
return;
addChildren(node);
if (queue.isEmpty())
return;
//if we're about to change levels, then the children just added
//are the last nodes for their level, so add a level marker to the queue
UnifiedTreeNode nextNode = queue.peek();
if (isChildrenMarker(nextNode))
queue.remove();
nextNode = queue.peek();
if (isLevelMarker(nextNode))
addElementToQueue(levelMarker);
}
protected void addRootToQueue() {
//don't refresh in closed projects
if (!root.getProject().isAccessible())
return;
IFileStore store = ((Resource) root).getStore();
IFileInfo fileInfo = fileTree != null ? fileTree.getFileInfo(store) : store.fetchInfo();
UnifiedTreeNode node = createNode(root, store, fileInfo, root.exists());
if (node.existsInFileSystem() || node.existsInWorkspace())
addElementToQueue(node);
}
/**
* Creates a tree node for a resource that is linked in a different file system location.
*/
protected UnifiedTreeNode createChildForLinkedResource(IResource target) {
IFileStore store = ((Resource) target).getStore();
return createNode(target, store, store.fetchInfo(), true);
}
/**
* Creates a child node for a location in the file system. Does nothing and returns null if the location does not correspond to a valid file/folder.
*/
protected UnifiedTreeNode createChildNodeFromFileSystem(UnifiedTreeNode parent, IFileInfo info) {
IPath childPath = parent.getResource().getFullPath().append(info.getName());
int type = info.isDirectory() ? IResource.FOLDER : IResource.FILE;
IResource target = getWorkspace().newResource(childPath, type);
return createNode(target, null, info, target.exists());
}
/**
* Factory method for creating a node for this tree. If the file exists on
* disk, either the parent store or child store can be provided. Providing
* only the parent store avoids creation of the child store in cases where
* it is not needed. The store object is only needed for directories for
* simple file system traversals, so this avoids creating store objects
* for all files.
*/
protected UnifiedTreeNode createNode(IResource resource, IFileStore store, IFileInfo info, boolean existsWorkspace) {
//first check for reusable objects
UnifiedTreeNode node = null;
int size = freeNodes.size();
if (size > 0) {
node = freeNodes.remove(size - 1);
node.reuse(this, resource, store, info, existsWorkspace);
return node;
}
//none available, so create a new one
return new UnifiedTreeNode(this, resource, store, info, existsWorkspace);
}
protected Iterator<UnifiedTreeNode> getChildren(UnifiedTreeNode node) {
/* if first child is null we need to add node's children to queue */
if (node.getFirstChild() == null)
addNodeChildrenToQueue(node);
/* if the first child is still null, the node does not have any children */
if (node.getFirstChild() == null)
return EMPTY_ITERATOR;
/* get the index of the first child */
int index = queue.indexOf(node.getFirstChild());
/* if we do not have children, just return an empty enumeration */
if (index == -1)
return EMPTY_ITERATOR;
/* create an enumeration with node's children */
List<UnifiedTreeNode> result = new ArrayList<>(10);
while (true) {
UnifiedTreeNode child = queue.get(index);
if (isChildrenMarker(child))
break;
result.add(child);
if (index == queue.size() - 1) {
index = 0;
} else {
index++;
}
}
return result.iterator();
}
protected int getLevel() {
return level;
}
protected IFileInfo[] getLocalList(UnifiedTreeNode node) {
try {
final IFileStore store = node.getStore();
IFileInfo[] list;
if (fileTree != null && (fileTree.getTreeRoot().equals(store) || fileTree.getTreeRoot().isParentOf(store)))
list = fileTree.getChildInfos(store);
else
list = store.childInfos(EFS.NONE, null);
if (list == null || list.length == 0)
return NO_CHILDREN;
list = ((Resource) node.getResource()).filterChildren(list, false);
int size = list.length;
if (size > 1)
quickSort(list, 0, size - 1);
return list;
} catch (CoreException e) {
//treat failure to access the directory as a non-existent directory
return NO_CHILDREN;
}
}
protected Workspace getWorkspace() {
return (Workspace) root.getWorkspace();
}
protected void initializeQueue() {
//initialize the queue
if (queue == null)
queue = new LinkedList<>();
else
queue.clear();
//initialize the free nodes list
if (freeNodes == null)
freeNodes = new ArrayList<>(100);
else
freeNodes.clear();
addRootToQueue();
addElementToQueue(levelMarker);
}
protected boolean isChildrenMarker(UnifiedTreeNode node) {
return node == childrenMarker;
}
protected boolean isLevelMarker(UnifiedTreeNode node) {
return node == levelMarker;
}
private static class PatternHolder {
//Initialize-on-demand Holder class to avoid compiling Pattern if never needed
//Pattern: A UNIX or Windows relative path that just points backward
private static final String REGEX = Platform.getOS().equals(Platform.OS_WIN32) ? "\\.[.\\\\]*" : "\\.[./]*"; //$NON-NLS-1$ //$NON-NLS-2$
public static final Pattern TRIVIAL_SYMLINK_PATTERN = Pattern.compile(REGEX);
private static final String REGEX_BACK_REPEATING = Platform.getOS().equals(Platform.OS_WIN32) ? "(\\.\\.\\\\)+.*" : "(\\.\\./)+.*"; //$NON-NLS-1$ //$NON-NLS-2$
public static final Pattern REPEATING_BACKWARDS_PATTERN = Pattern.compile(REGEX_BACK_REPEATING);
}
/**
* Initialize history stores for symbolic links.
* This may be done when starting a visitor, or later on demand.
*/
protected void initLinkHistoriesIfNeeded() {
if (pathPrefixHistory == null) {
//Bug 232426: Check what life cycle we need for the histories
Job job = Job.getJobManager().currentJob();
if (job instanceof RefreshJob) {
//we are running from the RefreshJob: use the path history of the job
RefreshJob refreshJob = (RefreshJob) job;
pathPrefixHistory = refreshJob.getPathPrefixHistory();
rootPathHistory = refreshJob.getRootPathHistory();
} else {
//Local Histories
pathPrefixHistory = new PrefixPool(20);
rootPathHistory = new PrefixPool(20);
}
}
if (rootPathHistory.size() == 0) {
//add current root to history
IFileStore rootStore = ((Resource) root).getStore();
try {
java.io.File rootFile = rootStore.toLocalFile(EFS.NONE, null);
if (rootFile != null) {
IPath rootProjPath = root.getProject().getLocation();
if (rootProjPath != null) {
try {
java.io.File rootProjFile = new java.io.File(rootProjPath.toOSString());
rootPathHistory.insertShorter(rootProjFile.getCanonicalPath() + '/');
} catch (IOException ioe) {
/*ignore here*/
}
}
rootPathHistory.insertShorter(rootFile.getCanonicalPath() + '/');
}
} catch (CoreException e) {
/*ignore*/
} catch (IOException e) {
/*ignore*/
}
}
}
/**
* Check if the given child represents a recursive symbolic link.
* <p>
* On remote EFS stores, this check is not exhaustive and just
* finds trivial recursive symbolic links pointing up in the tree.
* </p><p>
* On local stores, where {@link java.io.File#getCanonicalPath()}
* is available, the test is exhaustive but may also find some
* false positives with transitive symbolic links. This may lead
* to suppressing duplicates of already known resources in the
* tree, but it will never lead to not finding a resource at
* all. See bug 105554 for details.
* </p>
* @param parentStore EFS IFileStore representing the parent folder
* @param localInfo child representing a symbolic link
* @return <code>true</code> if the given child represents a
* recursive symbolic link.
*/
private boolean isRecursiveLink(IFileStore parentStore, IFileInfo localInfo) {
//Try trivial pattern first - works also on remote EFS stores
String linkTarget = localInfo.getStringAttribute(EFS.ATTRIBUTE_LINK_TARGET);
if (linkTarget != null && PatternHolder.TRIVIAL_SYMLINK_PATTERN.matcher(linkTarget).matches()) {
return true;
}
//Need canonical paths to check all other possibilities
try {
java.io.File parentFile = parentStore.toLocalFile(EFS.NONE, null);
//If this store cannot be represented as a local file, there is nothing we can do
//In the future, we could try to resolve the link target
//against the remote file system to do more checks.
if (parentFile == null) {
return false;
}
Path parent = parentFile.toPath();
Path realParentPath = parent.toRealPath();
if (disable_advanced_recursive_link_checks) {
// Multiple ../ backwards links can go outside the project tree
if (linkTarget != null && PatternHolder.REPEATING_BACKWARDS_PATTERN.matcher(linkTarget).matches()) {
Path targetPath = parent.resolve(linkTarget).normalize();
// Recursive if literal target points to the literal parent of this tree
if (parent.normalize().startsWith(targetPath)) {
return true;
}
// Recursive if resolved target points to the resolved parent of this tree
Path realTargetPath = targetPath.toRealPath();
if (realParentPath.startsWith(realTargetPath)) {
return true;
}
// If link is outside the project tree, consider as non recursive
// The link still can create recursion in the tree, but we can't detect it here.
}
// Skip advanced link checking, it may hide valid directories, see bug 537449
return false;
}
//get canonical path for both child and parent
java.io.File childFile = new java.io.File(parentFile, localInfo.getName());
String parentPath = realParentPath.toString() + java.io.File.separatorChar;
String childPath = childFile.toPath().toRealPath().toString() + java.io.File.separatorChar;
//get or instantiate the prefix and root path histories.
//Might be done earlier - for now, do it on demand.
initLinkHistoriesIfNeeded();
//insert the parent for checking loops
pathPrefixHistory.insertLonger(parentPath);
if (pathPrefixHistory.containsAsPrefix(childPath)) {
//found a potential loop: is it spanning up a new tree?
if (!rootPathHistory.insertShorter(childPath)) {
//not spanning up a new tree, so it is a real loop.
return true;
}
} else if (rootPathHistory.hasPrefixOf(childPath)) {
//child points into a different portion of the tree that we visited already before, or will certainly visit.
//This does not introduce a loop yet, but introduces duplicate resources.
//TODO Ideally, such duplicates should be modelled as linked resources. See bug 105534
return false;
} else {
//child neither introduces a loop nor points to a known tree.
//It probably spans up a new tree of potential prefixes.
rootPathHistory.insertShorter(childPath);
}
} catch (IOException e) {
//ignore
} catch (CoreException e) {
//ignore
}
return false;
}
protected boolean isValidLevel(int currentLevel, int depth) {
switch (depth) {
case IResource.DEPTH_INFINITE :
return true;
case IResource.DEPTH_ONE :
return currentLevel <= 1;
case IResource.DEPTH_ZERO :
return currentLevel == 0;
default :
return currentLevel + 1000 <= depth;
}
}
/**
* Sorts the given array of strings in place. This is
* not using the sorting framework to avoid casting overhead.
*/
protected void quickSort(IFileInfo[] infos, int left, int right) {
int originalLeft = left;
int originalRight = right;
IFileInfo mid = infos[(left + right) / 2];
do {
while (mid.compareTo(infos[left]) > 0)
left++;
while (infos[right].compareTo(mid) > 0)
right--;
if (left <= right) {
IFileInfo tmp = infos[left];
infos[left] = infos[right];
infos[right] = tmp;
left++;
right--;
}
} while (left <= right);
if (originalLeft < right)
quickSort(infos, originalLeft, right);
if (left < originalRight)
quickSort(infos, left, originalRight);
return;
}
/**
* Remove from the last element of the queue to the first child of the
* given node.
*/
protected void removeNodeChildrenFromQueue(UnifiedTreeNode node) {
UnifiedTreeNode first = node.getFirstChild();
if (first == null)
return;
while (true) {
if (first.equals(queue.pollLast()))
break;
}
node.setFirstChild(null);
}
/**
* Increases the current tree level by one. Returns true if the new
* level is still valid for the given depth
*/
protected boolean setLevel(int newLevel, int depth) {
level = newLevel;
childLevelValid = isValidLevel(level + 1, depth);
return isValidLevel(level, depth);
}
private void setRoot(IResource root) {
this.root = root;
}
}