LinkTracker.java

/**
 * Copyright 2016-2018 Dell Inc. or its subsidiaries. All rights reserved.
 *
 * <p>Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file
 * except in compliance with the License. A copy of the License is located at
 *
 * <p>http://www.apache.org/licenses/LICENSE-2.0.txt
 *
 * <p>or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package com.emc.ecs.nfsclient.nfs.io;

import com.emc.ecs.nfsclient.nfs.Nfs;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * This class tracks the process of resolving a symbolic link all the way to a concrete file. It
 * ensures that exceptions are thrown if the links form a loop, or if there are too many links in
 * the chain. Link trackers must be passed so that monitoring is continued until the link resolves
 * to a file that is not a symbolic link.
 *
 * @author seibed
 */
public class LinkTracker<N extends Nfs<?>, F extends NfsFile<N, F>> {

  /** Maximum size of a symlink chain to follow. */
  private static final int MAXSYMLINKS = 40;

  private Set<String> _unresolvedPaths = new HashSet<String>();

  private Map<String, F> _resolvedPaths = new HashMap<String, F>();

  private int linksTraversed = 0;

  /** The only constructor. */
  public LinkTracker() {}

  /**
   * Checks for problems. If the link has already been resolved, it returns the final file. If not,
   * it adds the link path to the list of unresolved paths that have been seen while evaluating this
   * chain.
   *
   * @param path The path to the link that is currently being resolved.
   * @return The file, if that link has already been resolved.
   * @throws IOException If there are too many links in the chain (more than MAXSYMLINKS)
   */
  final synchronized F addLink(String path) throws IOException {
    if (++linksTraversed > MAXSYMLINKS) {
      throw new IllegalArgumentException(
          "Too many symbolic links to follow: path="
              + path
              + ", traversed="
              + linksTraversed
              + ", limit="
              + MAXSYMLINKS);
    }

    F resolvedPath = _resolvedPaths.get(path);

    if (resolvedPath == null) {
      for (String unresolvedPath : _unresolvedPaths) {
        if (path.equals(unresolvedPath) || path.startsWith(unresolvedPath + "/")) {
          throw new IOException(
              "Symbolic link loop: path=" + path + ", previouslyVisited=" + unresolvedPath);
        }
      }
      _unresolvedPaths.add(path);
    }

    return resolvedPath;
  }

  /**
   * After each link is completely resolved, the linkTracker caller should call this method to store
   * that resolved path so that it can be resolved directly the next time is is seen.
   *
   * @param path The path to the original symbolic link.
   * @param file The file to which that link was finally resolved.
   */
  synchronized void addResolvedPath(String path, F file) {
    _resolvedPaths.put(path, file);
    _unresolvedPaths.remove(path);
  }
}