// Copyright 2014 The Bazel Authors. All rights reserved. // // 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.devtools.build.lib.vfs; import com.google.common.annotations.VisibleForTesting; import com.google.devtools.build.lib.clock.Clock; import com.google.devtools.build.lib.clock.JavaClock; import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe; import com.google.devtools.build.lib.profiler.Profiler; import com.google.devtools.build.lib.profiler.ProfilerTask; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Collection; /** * A FileSystem that does not use any JNI and hence, does not require a shared library be present at * execution. * *

Note: Blaze profiler tasks are defined on the system call level - thus we do not distinguish * (from profiling perspective) between different methods on this class that end up doing stat() * system call - they all are associated with the VFS_STAT task. */ @ThreadSafe public class JavaIoFileSystem extends AbstractFileSystemWithCustomStat { private static final LinkOption[] NO_LINK_OPTION = new LinkOption[0]; // This isn't generally safe; we rely on the file system APIs not modifying the array. private static final LinkOption[] NOFOLLOW_LINKS_OPTION = new LinkOption[] { LinkOption.NOFOLLOW_LINKS }; private final Clock clock; protected static final String ERR_IS_DIRECTORY = " (Is a directory)"; protected static final String ERR_DIRECTORY_NOT_EMPTY = " (Directory not empty)"; protected static final String ERR_FILE_EXISTS = " (File exists)"; protected static final String ERR_NO_SUCH_FILE_OR_DIR = " (No such file or directory)"; protected static final String ERR_NOT_A_DIRECTORY = " (Not a directory)"; public JavaIoFileSystem() { this(new JavaClock()); } public JavaIoFileSystem(DigestHashFunction hashFunction) { super(hashFunction); this.clock = new JavaClock(); } @VisibleForTesting JavaIoFileSystem(Clock clock) { this.clock = clock; } protected File getIoFile(Path path) { return new File(path.toString()); } /** * Returns a {@link java.nio.file.Path} representing the same path as provided {@code path}. * *

Note: while it's possible to use {@link #getIoFile(Path)} in combination with {@link * File#toPath()} to achieve essentially the same, using this method is preferable because it * avoids extra allocations and does not lose track of the underlying Java filesystem, which is * useful for some in-memory filesystem implementations like JimFS. */ protected java.nio.file.Path getNioPath(Path path) { return Paths.get(path.toString()); } private LinkOption[] linkOpts(boolean followSymlinks) { return followSymlinks ? NO_LINK_OPTION : NOFOLLOW_LINKS_OPTION; } @Override protected Collection getDirectoryEntries(Path path) throws IOException { File file = getIoFile(path); String[] entries = null; long startTime = Profiler.nanoTimeMaybe(); try { entries = file.list(); if (entries == null) { if (file.exists()) { throw new IOException(path + ERR_NOT_A_DIRECTORY); } else { throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR); } } } finally { profiler.logSimpleTask(startTime, ProfilerTask.VFS_DIR, file.getPath()); } Collection result = new ArrayList<>(entries.length); for (String entry : entries) { if (!entry.equals(".") && !entry.equals("..")) { result.add(entry); } } return result; } @Override protected boolean exists(Path path, boolean followSymlinks) { java.nio.file.Path nioPath = getNioPath(path); long startTime = Profiler.nanoTimeMaybe(); try { return Files.exists(nioPath, linkOpts(followSymlinks)); } finally { profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, path.toString()); } } @Override protected boolean isReadable(Path path) throws IOException { File file = getIoFile(path); long startTime = Profiler.nanoTimeMaybe(); try { if (!file.exists()) { throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR); } return file.canRead(); } finally { profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, file.getPath()); } } @Override protected boolean isWritable(Path path) throws IOException { File file = getIoFile(path); long startTime = Profiler.nanoTimeMaybe(); try { if (!file.exists()) { if (linkExists(file)) { throw new IOException(path + ERR_PERMISSION_DENIED); } else { throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR); } } return file.canWrite(); } finally { profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, file.getPath()); } } @Override protected boolean isExecutable(Path path) throws IOException { File file = getIoFile(path); long startTime = Profiler.nanoTimeMaybe(); try { if (!file.exists()) { throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR); } return file.canExecute(); } finally { profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, file.getPath()); } } @Override protected void setReadable(Path path, boolean readable) throws IOException { File file = getIoFile(path); if (!file.exists()) { throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR); } file.setReadable(readable); } @Override public void setWritable(Path path, boolean writable) throws IOException { File file = getIoFile(path); if (!file.exists()) { throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR); } file.setWritable(writable); } @Override protected void setExecutable(Path path, boolean executable) throws IOException { File file = getIoFile(path); if (!file.exists()) { throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR); } file.setExecutable(executable); } @Override public boolean supportsModifications(Path path) { return true; } @Override public boolean supportsSymbolicLinksNatively(Path path) { return true; } @Override public boolean supportsHardLinksNatively(Path path) { return true; } @Override public boolean isFilePathCaseSensitive() { return true; } @Override public boolean createDirectory(Path path) throws IOException { File file = getIoFile(path); if (file.mkdir()) { return true; } if (fileIsSymbolicLink(file)) { throw new IOException(path + ERR_FILE_EXISTS); } if (file.isDirectory()) { return false; // directory already existed } else if (file.exists()) { throw new IOException(path + ERR_FILE_EXISTS); } else if (!file.getParentFile().exists()) { throw new FileNotFoundException(path.getParentDirectory() + ERR_NO_SUCH_FILE_OR_DIR); } // Parent directory apparently exists - try to create our directory again. if (file.mkdir()) { return true; // Everything is fine finally. } else if (!file.getParentFile().canWrite()) { throw new FileAccessException(path + ERR_PERMISSION_DENIED); } else { // Parent exists, is writable, yet we can't create our directory. throw new FileNotFoundException(path.getParentDirectory() + ERR_NOT_A_DIRECTORY); } } @Override public void createDirectoryAndParents(Path path) throws IOException { java.nio.file.Path nioPath = getNioPath(path); try { Files.createDirectories(nioPath); } catch (java.nio.file.FileAlreadyExistsException e) { // Files.createDirectories will handle this case normally, but if the existing // file is a symlink to a directory then it still throws. Swallow this. if (!path.isDirectory()) { throw e; } } } private boolean linkExists(File file) { String shortName = file.getName(); File parentFile = file.getParentFile(); if (parentFile == null) { return false; } String[] filenames = parentFile.list(); if (filenames == null) { return false; } for (String name : filenames) { if (name.equals(shortName)) { return true; } } return false; } @Override protected void createSymbolicLink(Path linkPath, PathFragment targetFragment) throws IOException { java.nio.file.Path nioPath = getNioPath(linkPath); try { Files.createSymbolicLink(nioPath, Paths.get(targetFragment.getSafePathString())); } catch (java.nio.file.FileAlreadyExistsException e) { throw new IOException(linkPath + ERR_FILE_EXISTS); } catch (java.nio.file.AccessDeniedException e) { throw new IOException(linkPath + ERR_PERMISSION_DENIED); } catch (java.nio.file.NoSuchFileException e) { throw new FileNotFoundException(linkPath + ERR_NO_SUCH_FILE_OR_DIR); } } @Override protected PathFragment readSymbolicLink(Path path) throws IOException { java.nio.file.Path nioPath = getNioPath(path); long startTime = Profiler.nanoTimeMaybe(); try { String link = Files.readSymbolicLink(nioPath).toString(); return PathFragment.create(link); } catch (java.nio.file.NotLinkException e) { throw new NotASymlinkException(path); } catch (java.nio.file.NoSuchFileException e) { throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR); } finally { profiler.logSimpleTask(startTime, ProfilerTask.VFS_READLINK, path.getPathString()); } } @Override public void renameTo(Path sourcePath, Path targetPath) throws IOException { File sourceFile = getIoFile(sourcePath); File targetFile = getIoFile(targetPath); if (!sourceFile.renameTo(targetFile)) { if (!sourceFile.exists()) { throw new FileNotFoundException(sourcePath + ERR_NO_SUCH_FILE_OR_DIR); } if (targetFile.exists()) { if (targetFile.isDirectory() && targetFile.list().length > 0) { throw new IOException(targetPath + ERR_DIRECTORY_NOT_EMPTY); } else if (sourceFile.isDirectory() && targetFile.isFile()) { throw new IOException(sourcePath + " -> " + targetPath + ERR_NOT_A_DIRECTORY); } else if (sourceFile.isFile() && targetFile.isDirectory()) { throw new IOException(sourcePath + " -> " + targetPath + ERR_IS_DIRECTORY); } else { throw new IOException(sourcePath + " -> " + targetPath + ERR_PERMISSION_DENIED); } } else { throw new FileAccessException(sourcePath + " -> " + targetPath + ERR_PERMISSION_DENIED); } } } @Override protected long getFileSize(Path path, boolean followSymlinks) throws IOException { long startTime = Profiler.nanoTimeMaybe(); try { return stat(path, followSymlinks).getSize(); } finally { profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, path.getPathString()); } } @Override public boolean delete(Path path) throws IOException { java.nio.file.Path nioPath = getNioPath(path); long startTime = Profiler.nanoTimeMaybe(); try { return Files.deleteIfExists(nioPath); } catch (java.nio.file.DirectoryNotEmptyException e) { throw new IOException(path.getPathString() + ERR_DIRECTORY_NOT_EMPTY); } catch (java.nio.file.AccessDeniedException e) { throw new IOException(path.getPathString() + ERR_PERMISSION_DENIED); } catch (java.nio.file.AtomicMoveNotSupportedException | java.nio.file.FileAlreadyExistsException | java.nio.file.FileSystemLoopException | java.nio.file.NoSuchFileException | java.nio.file.NotDirectoryException | java.nio.file.NotLinkException e) { // All known but unexpected subclasses of FileSystemException. throw new IOException(path.getPathString() + ": unexpected FileSystemException", e); } catch (java.nio.file.FileSystemException e) { // Files.deleteIfExists() throws FileSystemException on Linux if a path component is a file. // We caught all known subclasses of FileSystemException so `e` is either an unknown // subclass or it is indeed a "Not a directory" error. Non-English JDKs may use a different // error message than "Not a directory", so we should not look for that text. Checking the // parent directory if it's indeed a directory is unrealiable, because another process may // modify it concurrently... but we have no better choice. if (e.getClass().equals(java.nio.file.FileSystemException.class) && !nioPath.getParent().toFile().isDirectory()) { // Hopefully the try-block failed because a parent directory was in fact not a directory. // Theoretically it's possible that the try-block failed for some other reason and all // parent directories were indeed directories, but another process changed a parent // directory into a file after the try-block failed but before this catch-block started, and // we return false here losing the real exception in `e`, but we cannot know. return false; } else { throw new IOException(path.getPathString() + ": unexpected FileSystemException", e); } } finally { profiler.logSimpleTask(startTime, ProfilerTask.VFS_DELETE, path.getPathString()); } } @Override protected long getLastModifiedTime(Path path, boolean followSymlinks) throws IOException { File file = getIoFile(path); long startTime = Profiler.nanoTimeMaybe(); try { return stat(path, followSymlinks).getLastModifiedTime(); } finally { profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, file.getPath()); } } protected boolean fileIsSymbolicLink(File file) { return Files.isSymbolicLink(file.toPath()); } @Override public void setLastModifiedTime(Path path, long newTime) throws IOException { File file = getIoFile(path); if (!file.setLastModified(newTime == -1L ? clock.currentTimeMillis() : newTime)) { if (!file.exists()) { throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR); } else if (!file.getParentFile().canWrite()) { throw new FileAccessException(path.getParentDirectory() + ERR_PERMISSION_DENIED); } else { throw new FileAccessException(path + ERR_PERMISSION_DENIED); } } } @Override protected byte[] getDigest(Path path) throws IOException { String name = path.toString(); long startTime = Profiler.nanoTimeMaybe(); try { return super.getDigest(path); } finally { profiler.logSimpleTask(startTime, ProfilerTask.VFS_MD5, name); } } /** * Returns the status of a file. See {@link Path#stat(Symlinks)} for * specification. * *

The default implementation of this method is a "lazy" one, based on * other accessor methods such as {@link #isFile}, etc. Subclasses may provide * more efficient specializations. However, we still try to follow Unix-like * semantics of failing fast in case of non-existent files (or in case of * permission issues). */ @Override protected FileStatus stat(final Path path, final boolean followSymlinks) throws IOException { java.nio.file.Path nioPath = getNioPath(path); final BasicFileAttributes attributes; try { attributes = Files.readAttributes(nioPath, BasicFileAttributes.class, linkOpts(followSymlinks)); } catch (java.nio.file.FileSystemException e) { throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR); } FileStatus status = new FileStatus() { @Override public boolean isFile() { return attributes.isRegularFile() || isSpecialFile(); } @Override public boolean isSpecialFile() { return attributes.isOther(); } @Override public boolean isDirectory() { return attributes.isDirectory(); } @Override public boolean isSymbolicLink() { return attributes.isSymbolicLink(); } @Override public long getSize() throws IOException { return attributes.size(); } @Override public long getLastModifiedTime() throws IOException { return attributes.lastModifiedTime().toMillis(); } @Override public long getLastChangeTime() { // This is the best we can do with Java NIO... return attributes.lastModifiedTime().toMillis(); } @Override public long getNodeId() { // TODO(bazel-team): Consider making use of attributes.fileKey(). return -1; } }; return status; } @Override protected FileStatus statIfFound(Path path, boolean followSymlinks) { try { return stat(path, followSymlinks); } catch (FileNotFoundException e) { // JavaIoFileSystem#stat (incorrectly) only throws FileNotFoundException (because it calls // #getLastModifiedTime, which can only throw a FileNotFoundException), so we always hit this // codepath. Thus, this method will incorrectly not throw an exception for some filesystem // errors. return null; } catch (IOException e) { // If this codepath is ever hit, then this method should be rewritten to properly distinguish // between not-found exceptions and others. throw new IllegalStateException(e); } } @Override protected void createFSDependentHardLink(Path linkPath, Path originalPath) throws IOException { Files.createLink( java.nio.file.Paths.get(linkPath.toString()), java.nio.file.Paths.get(originalPath.toString())); } }