| /* |
| * Copyright 2013 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License 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.google.common.jimfs; |
| |
| import static com.google.common.base.Preconditions.checkNotNull; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSortedMap; |
| import com.google.common.collect.ImmutableSortedSet; |
| import java.io.IOException; |
| import java.nio.file.LinkOption; |
| import java.nio.file.NoSuchFileException; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.Set; |
| import org.checkerframework.checker.nullness.compatqual.NullableDecl; |
| |
| /** |
| * The tree of directories and files for the file system. Contains the file system root directories |
| * and provides the ability to look up files by path. One piece of the file store implementation. |
| * |
| * @author Colin Decker |
| */ |
| final class FileTree { |
| |
| /** |
| * Doesn't much matter, but this number comes from MIN_ELOOP_THRESHOLD <a |
| * href="https://sourceware.org/git/gitweb.cgi?p=glibc.git;a=blob_plain;f=sysdeps/generic/eloop-threshold.h;hb=HEAD"> |
| * here</a> |
| */ |
| private static final int MAX_SYMBOLIC_LINK_DEPTH = 40; |
| |
| private static final ImmutableList<Name> EMPTY_PATH_NAMES = ImmutableList.of(Name.SELF); |
| |
| /** Map of root names to root directories. */ |
| private final ImmutableSortedMap<Name, Directory> roots; |
| |
| /** Creates a new file tree with the given root directories. */ |
| FileTree(Map<Name, Directory> roots) { |
| this.roots = ImmutableSortedMap.copyOf(roots, Name.canonicalOrdering()); |
| } |
| |
| /** Returns the names of the root directories in this tree. */ |
| public ImmutableSortedSet<Name> getRootDirectoryNames() { |
| return roots.keySet(); |
| } |
| |
| /** |
| * Gets the directory entry for the root with the given name or {@code null} if no such root |
| * exists. |
| */ |
| @NullableDecl |
| public DirectoryEntry getRoot(Name name) { |
| Directory dir = roots.get(name); |
| return dir == null ? null : dir.entryInParent(); |
| } |
| |
| /** Returns the result of the file lookup for the given path. */ |
| public DirectoryEntry lookUp( |
| File workingDirectory, JimfsPath path, Set<? super LinkOption> options) throws IOException { |
| checkNotNull(path); |
| checkNotNull(options); |
| |
| DirectoryEntry result = lookUp(workingDirectory, path, options, 0); |
| if (result == null) { |
| // an intermediate file in the path did not exist or was not a directory |
| throw new NoSuchFileException(path.toString()); |
| } |
| return result; |
| } |
| |
| @NullableDecl |
| private DirectoryEntry lookUp( |
| File dir, JimfsPath path, Set<? super LinkOption> options, int linkDepth) throws IOException { |
| ImmutableList<Name> names = path.names(); |
| |
| if (path.isAbsolute()) { |
| // look up the root directory |
| DirectoryEntry entry = getRoot(path.root()); |
| if (entry == null) { |
| // root not found; always return null as no real parent directory exists |
| // this prevents new roots from being created in file systems supporting multiple roots |
| return null; |
| } else if (names.isEmpty()) { |
| // root found, no more names to look up |
| return entry; |
| } else { |
| // root found, more names to look up; set dir to the root directory for the path |
| dir = entry.file(); |
| } |
| } else if (isEmpty(names)) { |
| // set names to the canonical list of names for an empty path (singleton list of ".") |
| names = EMPTY_PATH_NAMES; |
| } |
| |
| return lookUp(dir, names, options, linkDepth); |
| } |
| |
| /** |
| * Looks up the given names against the given base file. If the file is not a directory, the |
| * lookup fails. |
| */ |
| @NullableDecl |
| private DirectoryEntry lookUp( |
| File dir, Iterable<Name> names, Set<? super LinkOption> options, int linkDepth) |
| throws IOException { |
| Iterator<Name> nameIterator = names.iterator(); |
| Name name = nameIterator.next(); |
| while (nameIterator.hasNext()) { |
| Directory directory = toDirectory(dir); |
| if (directory == null) { |
| return null; |
| } |
| |
| DirectoryEntry entry = directory.get(name); |
| if (entry == null) { |
| return null; |
| } |
| |
| File file = entry.file(); |
| if (file.isSymbolicLink()) { |
| DirectoryEntry linkResult = followSymbolicLink(dir, (SymbolicLink) file, linkDepth); |
| |
| if (linkResult == null) { |
| return null; |
| } |
| |
| dir = linkResult.fileOrNull(); |
| } else { |
| dir = file; |
| } |
| |
| name = nameIterator.next(); |
| } |
| |
| return lookUpLast(dir, name, options, linkDepth); |
| } |
| |
| /** Looks up the last element of a path. */ |
| @NullableDecl |
| private DirectoryEntry lookUpLast( |
| @NullableDecl File dir, Name name, Set<? super LinkOption> options, int linkDepth) |
| throws IOException { |
| Directory directory = toDirectory(dir); |
| if (directory == null) { |
| return null; |
| } |
| |
| DirectoryEntry entry = directory.get(name); |
| if (entry == null) { |
| return new DirectoryEntry(directory, name, null); |
| } |
| |
| File file = entry.file(); |
| if (!options.contains(LinkOption.NOFOLLOW_LINKS) && file.isSymbolicLink()) { |
| return followSymbolicLink(dir, (SymbolicLink) file, linkDepth); |
| } |
| |
| return getRealEntry(entry); |
| } |
| |
| /** |
| * Returns the directory entry located by the target path of the given symbolic link, resolved |
| * relative to the given directory. |
| */ |
| @NullableDecl |
| private DirectoryEntry followSymbolicLink(File dir, SymbolicLink link, int linkDepth) |
| throws IOException { |
| if (linkDepth >= MAX_SYMBOLIC_LINK_DEPTH) { |
| throw new IOException("too many levels of symbolic links"); |
| } |
| |
| return lookUp(dir, link.target(), Options.FOLLOW_LINKS, linkDepth + 1); |
| } |
| |
| /** |
| * Returns the entry for the file in its parent directory. This will be the given entry unless the |
| * name for the entry is "." or "..", in which the directory linking to the file is not the file's |
| * parent directory. In that case, we know the file must be a directory ("." and ".." can only |
| * link to directories), so we can just get the entry in the directory's parent directory that |
| * links to it. So, for example, if we have a directory "foo" that contains a directory "bar" and |
| * we find an entry [bar -> "." -> bar], we instead return the entry for bar in its parent, [foo |
| * -> "bar" -> bar]. |
| */ |
| @NullableDecl |
| private DirectoryEntry getRealEntry(DirectoryEntry entry) { |
| Name name = entry.name(); |
| |
| if (name.equals(Name.SELF) || name.equals(Name.PARENT)) { |
| Directory dir = toDirectory(entry.file()); |
| assert dir != null; |
| return dir.entryInParent(); |
| } else { |
| return entry; |
| } |
| } |
| |
| @NullableDecl |
| private Directory toDirectory(@NullableDecl File file) { |
| return file == null || !file.isDirectory() ? null : (Directory) file; |
| } |
| |
| private static boolean isEmpty(ImmutableList<Name> names) { |
| // the empty path (created by FileSystem.getPath("")), has no root and a single name, "" |
| return names.isEmpty() || (names.size() == 1 && names.get(0).toString().isEmpty()); |
| } |
| } |