/*
* Copyright 2013-present Facebook, 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.facebook.buck.io;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.util.environment.Platform;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.ByteSource;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import javax.annotation.Nullable;
/**
* Common functions that are done with a {@link Path}. If a function is going to take a {@link
* ProjectFilesystem}, then it should be in {@link MoreProjectFilesystems} instead.
*/
public class MorePaths {
/** Utility class: do not instantiate. */
private MorePaths() {}
public static final Path EMPTY_PATH = Paths.get("");
public static String pathWithUnixSeparators(String path) {
return pathWithUnixSeparators(Paths.get(path));
}
public static String pathWithUnixSeparators(Path path) {
return path.toString().replace('\\', '/');
}
public static String pathWithWindowsSeparators(Path path) {
return path.toString().replace('/', '\\');
}
public static String pathWithPlatformSeparators(String path) {
return pathWithPlatformSeparators(Paths.get(path));
}
public static String pathWithPlatformSeparators(Path path) {
if (Platform.detect() == Platform.WINDOWS) {
return pathWithWindowsSeparators(path);
} else {
return pathWithUnixSeparators(path);
}
}
public static String pathWithUnixSeparatorsAndTrailingSlash(Path path) {
return pathWithUnixSeparators(path) + "/";
}
public static Path getParentOrEmpty(Path path) {
Path parent = path.getParent();
if (parent == null) {
parent = EMPTY_PATH;
}
return parent;
}
/**
* Get the path of a file relative to a base directory.
*
* @param path must reference a file, not a directory.
* @param baseDir must reference a directory that is relative to a common directory with the path.
* may be null if referencing the same directory as the path.
* @return the relative path of path from the directory baseDir.
*/
public static Path getRelativePath(Path path, @Nullable Path baseDir) {
if (baseDir == null) {
// This allows callers to use this method with "file.parent()" for files from the project
// root dir.
baseDir = EMPTY_PATH;
}
Preconditions.checkArgument(!path.isAbsolute(), "Path must be relative: %s.", path);
Preconditions.checkArgument(!baseDir.isAbsolute(), "Path must be relative: %s.", baseDir);
return relativize(baseDir, path);
}
/**
* Get a relative path from path1 to path2, first normalizing each path.
*
* <p>This method is a workaround for JDK-6925169 (Path.relativize returns incorrect result if
* path contains "." or "..").
*/
public static Path relativize(Path path1, Path path2) {
Preconditions.checkArgument(
path1.isAbsolute() == path2.isAbsolute(),
"Both paths must be absolute or both paths must be relative. (%s is %s, %s is %s)",
path1,
path1.isAbsolute() ? "absolute" : "relative",
path2,
path2.isAbsolute() ? "absolute" : "relative");
path1 = normalize(path1);
path2 = normalize(path2);
// On Windows, if path1 is "" then Path.relativize returns ../path2 instead of path2 or ./path2
if (EMPTY_PATH.equals(path1)) {
return path2;
}
return path1.relativize(path2);
}
/**
* Get a path without unnecessary path parts.
*
* <p>This method is a workaround for JDK-8037945 (Paths.get("").normalize() throws
* ArrayIndexOutOfBoundsException).
*/
public static Path normalize(Path path) {
if (!EMPTY_PATH.equals(path)) {
path = path.normalize();
}
return path;
}
/**
* Creates a symlink at {@code pathToProjectRoot.resolve(pathToDesiredLinkUnderProjectRoot)} that
* points to {@code pathToProjectRoot.resolve(pathToExistingFileUnderProjectRoot)} using a
* relative symlink. Both params must be relative to the project root.
*
* @param pathToDesiredLinkUnderProjectRoot must reference a file, not a directory.
* @param pathToExistingFileUnderProjectRoot must reference a file, not a directory.
* @return the relative path from the new symlink that was created to the existing file.
*/
public static Path createRelativeSymlink(
Path pathToDesiredLinkUnderProjectRoot,
Path pathToExistingFileUnderProjectRoot,
Path pathToProjectRoot)
throws IOException {
Path target =
getRelativePath(
pathToExistingFileUnderProjectRoot, pathToDesiredLinkUnderProjectRoot.getParent());
Files.createSymbolicLink(pathToProjectRoot.resolve(pathToDesiredLinkUnderProjectRoot), target);
return target;
}
/**
* Filters out {@link Path} objects from {@code paths} that aren't a subpath of {@code root} and
* returns a set of paths relative to {@code root}.
*/
public static ImmutableSet<Path> filterForSubpaths(Iterable<Path> paths, final Path root) {
final Path normalizedRoot = root.toAbsolutePath().normalize();
return FluentIterable.from(paths)
.filter(
input -> {
if (input.isAbsolute()) {
return input.normalize().startsWith(normalizedRoot);
} else {
return true;
}
})
.transform(
input -> {
if (input.isAbsolute()) {
return relativize(normalizedRoot, input);
} else {
return input;
}
})
.toSet();
}
/** Expands "~/foo" into "/home/zuck/foo". Returns regular paths unmodified. */
public static Path expandHomeDir(Path path) {
if (!path.startsWith("~")) {
return path;
}
Path homePath = path.getFileSystem().getPath(System.getProperty("user.home"));
if (path.equals(path.getFileSystem().getPath("~"))) {
return homePath;
}
return homePath.resolve(path.subpath(1, path.getNameCount()));
}
public static ByteSource asByteSource(final Path path) {
return new ByteSource() {
@Override
public InputStream openStream() throws IOException {
return Files.newInputStream(path);
}
};
}
public static String getFileExtension(Path path) {
String name = path.getFileName().toString();
int index = name.lastIndexOf('.');
return index == -1 ? "" : name.substring(index + 1);
}
public static String getNameWithoutExtension(Path file) {
String name = file.getFileName().toString();
int index = name.lastIndexOf('.');
return index == -1 ? name : name.substring(0, index);
}
public static String stripPathPrefixAndExtension(Path fileName, String prefix) {
String nameWithoutExtension = getNameWithoutExtension(fileName);
if (!nameWithoutExtension.startsWith(prefix)
|| nameWithoutExtension.length() < prefix.length()) {
throw new HumanReadableException(
"Invalid prefix on filename in path %s (file %s) - expecting %s",
fileName, nameWithoutExtension, prefix);
}
return nameWithoutExtension.substring(prefix.length(), nameWithoutExtension.length());
}
public static Optional<Path> stripPrefix(Path p, Path prefix) {
if (prefix.getNameCount() > p.getNameCount()) {
return Optional.empty();
}
for (int i = 0; i < prefix.getNameCount(); ++i) {
if (!prefix.getName(i).equals(p.getName(i))) {
return Optional.empty();
}
}
return Optional.of(p.subpath(prefix.getNameCount(), p.getNameCount()));
}
public static Function<String, Path> toPathFn(final FileSystem fileSystem) {
return input -> fileSystem.getPath(input);
}
private static Path dropPathPart(Path p, int i) {
if (i == 0) {
return p.subpath(1, p.getNameCount());
} else if (i == p.getNameCount() - 1) {
return p.subpath(0, p.getNameCount() - 1);
} else {
return p.subpath(0, i).resolve(p.subpath(i + 1, p.getNameCount()));
}
}
/**
* Drop any "." parts (useless). Do keep ".." parts; don't normalize them away.
*
* <p>Note that while Path objects provide a {@link Path#normalize()} method for eliminating
* redundant parts of paths like in {@code foo/a/../b/c}, changing its internal parts (and
* actually using the filesystem), we don't use those methods to clean up the incoming paths; we
* only strip empty parts, and those consisting only of {@code .} because doing so maps
* exactly-same paths together, and can't influence where it may point to, whereas {@code ..} and
* symbolic links might.
*/
public static Path fixPath(Path p) {
int i = 0;
while (i < p.getNameCount()) {
if (p.getName(i).toString().equals(".")) {
p = dropPathPart(p, i);
} else {
i++;
}
}
return p;
}
/**
* Drop the cache in Path object.
*
* <p>Path's implementation class {@code UnixPath}, will lazily initialize a String representation
* and store it in the object when {@code #toString()} is called for the first time. This doubles
* the memory requirement for the Path object.
*
* <p>This hack constructs a new path, dropping the cached toString value.
*
* <p>Due to the nature of what this function does, it's very sensitive to the implementation. Any
* calls to {@code #toString()} on the returned object would also recreate the cached string
* value.
*/
public static Path dropInternalCaches(Path p) {
return p.getFileSystem().getPath(p.toString());
}
}