diff options
Diffstat (limited to 'src')
96 files changed, 2326 insertions, 4698 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/actions/Actions.java b/src/main/java/com/google/devtools/build/lib/actions/Actions.java index 91438261c7..9b49d5cc36 100644 --- a/src/main/java/com/google/devtools/build/lib/actions/Actions.java +++ b/src/main/java/com/google/devtools/build/lib/actions/Actions.java @@ -23,7 +23,9 @@ import com.google.common.escape.Escapers; import com.google.devtools.build.lib.actions.MutableActionGraph.ActionConflictException; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe; +import com.google.devtools.build.lib.vfs.OsPathPolicy; import com.google.devtools.build.lib.vfs.PathFragment; +import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; @@ -149,7 +151,7 @@ public final class Actions { */ public static Map<ActionAnalysisMetadata, ArtifactPrefixConflictException> findArtifactPrefixConflicts(Map<Artifact, ActionAnalysisMetadata> generatingActions) { - TreeMap<PathFragment, Artifact> artifactPathMap = new TreeMap(); + TreeMap<PathFragment, Artifact> artifactPathMap = new TreeMap<>(comparatorForPrefixConflicts()); for (Artifact artifact : generatingActions.keySet()) { artifactPathMap.put(artifact.getExecPath(), artifact); } @@ -159,18 +161,63 @@ public final class Actions { } /** + * Returns a comparator for use with {@link #findArtifactPrefixConflicts(ActionGraph, SortedMap)}. + */ + public static Comparator<PathFragment> comparatorForPrefixConflicts() { + return PathFragmentPrefixComparator.INSTANCE; + } + + private static class PathFragmentPrefixComparator implements Comparator<PathFragment> { + private static final PathFragmentPrefixComparator INSTANCE = new PathFragmentPrefixComparator(); + + @Override + public int compare(PathFragment lhs, PathFragment rhs) { + // We need to use the OS path policy in case the OS is case insensitive. + OsPathPolicy os = OsPathPolicy.getFilePathOs(); + String str1 = lhs.getPathString(); + String str2 = rhs.getPathString(); + int len1 = str1.length(); + int len2 = str2.length(); + int n = Math.min(len1, len2); + for (int i = 0; i < n; ++i) { + char c1 = str1.charAt(i); + char c2 = str2.charAt(i); + int res = os.compare(c1, c2); + if (res != 0) { + if (c1 == PathFragment.SEPARATOR_CHAR) { + return -1; + } else if (c2 == PathFragment.SEPARATOR_CHAR) { + return 1; + } + return res; + } + } + return len1 - len2; + } + } + + /** * Finds Artifact prefix conflicts between generated artifacts. An artifact prefix conflict * happens if one action generates an artifact whose path is a prefix of another artifact's path. * Those two artifacts cannot exist simultaneously in the output tree. * * @param actionGraph the {@link ActionGraph} to query for artifact conflicts - * @param artifactPathMap a map mapping generated artifacts to their exec paths + * @param artifactPathMap a map mapping generated artifacts to their exec paths. The map must be + * sorted using the comparator from {@link #comparatorForPrefixConflicts()}. * @return A map between actions that generated the conflicting artifacts and their associated * {@link ArtifactPrefixConflictException}. */ public static Map<ActionAnalysisMetadata, ArtifactPrefixConflictException> - findArtifactPrefixConflicts(ActionGraph actionGraph, - SortedMap<PathFragment, Artifact> artifactPathMap) { + findArtifactPrefixConflicts( + ActionGraph actionGraph, SortedMap<PathFragment, Artifact> artifactPathMap) { + // You must construct the sorted map using this comparator for the algorithm to work. + // The algorithm requires subdirectories to immediately follow parent directories, + // before any files in that directory. + // Example: "foo", "foo.obj", foo/bar" must be sorted + // "foo", "foo/bar", foo.obj" + Preconditions.checkArgument( + artifactPathMap.comparator() instanceof PathFragmentPrefixComparator, + "artifactPathMap must be sorted with PathFragmentPrefixComparator"); // No actions in graph -- currently happens only in tests. Special-cased because .next() call // below is unconditional. if (artifactPathMap.isEmpty()) { diff --git a/src/main/java/com/google/devtools/build/lib/actions/Artifact.java b/src/main/java/com/google/devtools/build/lib/actions/Artifact.java index 395f5dfde8..964b05d5a6 100644 --- a/src/main/java/com/google/devtools/build/lib/actions/Artifact.java +++ b/src/main/java/com/google/devtools/build/lib/actions/Artifact.java @@ -176,7 +176,6 @@ public class Artifact public static final Predicate<Artifact> MIDDLEMAN_FILTER = input -> !input.isMiddlemanArtifact(); private final int hashCode; - private final Path path; private final ArtifactRoot root; private final PathFragment execPath; private final PathFragment rootRelativePath; @@ -203,35 +202,26 @@ public class Artifact */ @VisibleForTesting @AutoCodec.Instantiator - public Artifact(Path path, ArtifactRoot root, PathFragment execPath, ArtifactOwner owner) { - if (root == null || !root.getRoot().contains(path)) { - throw new IllegalArgumentException(root + ": illegal root for " + path - + " (execPath: " + execPath + ")"); - } - if (execPath == null - || execPath.isAbsolute() != root.getRoot().isAbsolute() - || !path.asFragment().endsWith(execPath)) { - throw new IllegalArgumentException(execPath + ": illegal execPath for " + path - + " (root: " + root + ")"); + public Artifact(ArtifactRoot root, PathFragment execPath, ArtifactOwner owner) { + Preconditions.checkNotNull(root); + if (execPath == null || execPath.isAbsolute() != root.getRoot().isAbsolute()) { + throw new IllegalArgumentException( + execPath + ": illegal execPath for " + execPath + " (root: " + root + ")"); } if (execPath.isEmpty()) { throw new IllegalArgumentException( "it is illegal to create an artifact with an empty execPath"); } - this.hashCode = path.hashCode(); - this.path = path; + this.hashCode = execPath.hashCode(); this.root = root; this.execPath = execPath; - // These two lines establish the invariant that - // execPath == rootRelativePath <=> execPath.equals(rootRelativePath) - // This is important for isSourceArtifact. - PathFragment rootRel = root.getRoot().relativize(path); - if (!execPath.endsWith(rootRel)) { - throw new IllegalArgumentException(execPath + ": illegal execPath doesn't end with " - + rootRel + " at " + path + " with root " + root); + PathFragment rootExecPath = root.getExecPath(); + if (rootExecPath.isEmpty()) { + this.rootRelativePath = execPath; + } else { + this.rootRelativePath = execPath.relativeTo(root.getExecPath()); } - this.rootRelativePath = rootRel.equals(execPath) ? execPath : rootRel; - this.owner = Preconditions.checkNotNull(owner, path); + this.owner = Preconditions.checkNotNull(owner); } /** @@ -251,8 +241,8 @@ public class Artifact * <pre> */ @VisibleForTesting - public Artifact(Path path, ArtifactRoot root, PathFragment execPath) { - this(path, root, execPath, ArtifactOwner.NullArtifactOwner.INSTANCE); + public Artifact(ArtifactRoot root, PathFragment execPath) { + this(root, execPath, ArtifactOwner.NullArtifactOwner.INSTANCE); } /** @@ -262,7 +252,6 @@ public class Artifact @VisibleForTesting // Only exists for testing. public Artifact(Path path, ArtifactRoot root) { this( - path, root, root.getExecPath().getRelative(root.getRoot().relativize(path)), ArtifactOwner.NullArtifactOwner.INSTANCE); @@ -272,14 +261,13 @@ public class Artifact @VisibleForTesting // Only exists for testing. public Artifact(PathFragment rootRelativePath, ArtifactRoot root) { this( - root.getRoot().getRelative(rootRelativePath), root, root.getExecPath().getRelative(rootRelativePath), ArtifactOwner.NullArtifactOwner.INSTANCE); } public final Path getPath() { - return path; + return root.getRoot().getRelative(rootRelativePath); } public boolean hasParent() { @@ -412,9 +400,8 @@ public class Artifact structField = true, doc = "Returns true if this is a source file, i.e. it is not generated." ) - @SuppressWarnings("ReferenceEquality") // == is an optimization public final boolean isSourceArtifact() { - return execPath == rootRelativePath; + return root.isSourceRoot(); } /** @@ -479,12 +466,8 @@ public class Artifact @VisibleForSerialization public SpecialArtifact( - Path path, - ArtifactRoot root, - PathFragment execPath, - ArtifactOwner owner, - SpecialArtifactType type) { - super(path, root, execPath, owner); + ArtifactRoot root, PathFragment execPath, ArtifactOwner owner, SpecialArtifactType type) { + super(root, execPath, owner); this.type = type; } @@ -559,17 +542,16 @@ public class Artifact TreeFileArtifact( SpecialArtifact parentTreeArtifact, PathFragment parentRelativePath, ArtifactOwner owner) { super( - parentTreeArtifact.getPath().getRelative(parentRelativePath), parentTreeArtifact.getRoot(), parentTreeArtifact.getExecPath().getRelative(parentRelativePath), owner); - Preconditions.checkState( + Preconditions.checkArgument( parentTreeArtifact.isTreeArtifact(), "The parent of TreeFileArtifact (parent-relative path: %s) is not a TreeArtifact: %s", parentRelativePath, parentTreeArtifact); - Preconditions.checkState( - parentRelativePath.isNormalized() && !parentRelativePath.isAbsolute(), + Preconditions.checkArgument( + !parentRelativePath.containsUplevelReferences() && !parentRelativePath.isAbsolute(), "%s is not a proper normalized relative path", parentRelativePath); this.parentTreeArtifact = parentTreeArtifact; @@ -668,7 +650,7 @@ public class Artifact // We don't bother to check root in the equivalence relation, because we // assume that no root is an ancestor of another one. Artifact that = (Artifact) other; - return Objects.equals(this.path, that.path); + return Objects.equals(this.execPath, that.execPath) && Objects.equals(this.root, that.root); } @Override @@ -702,7 +684,7 @@ public class Artifact return "[" + root + "]" + rootRelativePath; } else { // Derived Artifact: path and root are under execRoot - PathFragment execRoot = trimTail(path.asFragment(), execPath); + PathFragment execRoot = trimTail(getPath().asFragment(), execPath); return "[[" + execRoot + "]" diff --git a/src/main/java/com/google/devtools/build/lib/actions/ArtifactFactory.java b/src/main/java/com/google/devtools/build/lib/actions/ArtifactFactory.java index 7a0f88b7b4..d23637a0fb 100644 --- a/src/main/java/com/google/devtools/build/lib/actions/ArtifactFactory.java +++ b/src/main/java/com/google/devtools/build/lib/actions/ArtifactFactory.java @@ -149,8 +149,7 @@ public class ArtifactFactory implements ArtifactResolver { Preconditions.checkArgument( execPath.isAbsolute() == root.getRoot().isAbsolute(), "%s %s %s", execPath, root, owner); Preconditions.checkNotNull(owner, "%s %s", execPath, root); - execPath = execPath.normalize(); - return getArtifact(root.getRoot().getRelative(execPath), root, execPath, owner, null); + return getArtifact(root, execPath, owner, null); } @Override @@ -162,7 +161,7 @@ public class ArtifactFactory implements ArtifactResolver { Preconditions.checkArgument(!root.isSourceRoot()); Preconditions.checkArgument( rootRelativePath.isAbsolute() == root.getRoot().isAbsolute(), rootRelativePath); - Preconditions.checkArgument(rootRelativePath.isNormalized(), rootRelativePath); + Preconditions.checkArgument(!rootRelativePath.containsUplevelReferences(), rootRelativePath); Preconditions.checkArgument( root.getRoot().asPath().startsWith(execRootParent), "%s %s", root, execRootParent); Preconditions.checkArgument( @@ -182,8 +181,7 @@ public class ArtifactFactory implements ArtifactResolver { public Artifact getDerivedArtifact( PathFragment rootRelativePath, ArtifactRoot root, ArtifactOwner owner) { validatePath(rootRelativePath, root); - Path path = root.getRoot().getRelative(rootRelativePath); - return getArtifact(path, root, root.getExecPath().getRelative(rootRelativePath), owner, null); + return getArtifact(root, root.getExecPath().getRelative(rootRelativePath), owner, null); } /** @@ -197,13 +195,8 @@ public class ArtifactFactory implements ArtifactResolver { public Artifact getFilesetArtifact( PathFragment rootRelativePath, ArtifactRoot root, ArtifactOwner owner) { validatePath(rootRelativePath, root); - Path path = root.getRoot().getRelative(rootRelativePath); return getArtifact( - path, - root, - root.getExecPath().getRelative(rootRelativePath), - owner, - SpecialArtifactType.FILESET); + root, root.getExecPath().getRelative(rootRelativePath), owner, SpecialArtifactType.FILESET); } /** @@ -216,21 +209,14 @@ public class ArtifactFactory implements ArtifactResolver { public Artifact getTreeArtifact( PathFragment rootRelativePath, ArtifactRoot root, ArtifactOwner owner) { validatePath(rootRelativePath, root); - Path path = root.getRoot().getRelative(rootRelativePath); return getArtifact( - path, - root, - root.getExecPath().getRelative(rootRelativePath), - owner, - SpecialArtifactType.TREE); + root, root.getExecPath().getRelative(rootRelativePath), owner, SpecialArtifactType.TREE); } public Artifact getConstantMetadataArtifact( PathFragment rootRelativePath, ArtifactRoot root, ArtifactOwner owner) { validatePath(rootRelativePath, root); - Path path = root.getRoot().getRelative(rootRelativePath); return getArtifact( - path, root, root.getExecPath().getRelative(rootRelativePath), owner, @@ -242,7 +228,6 @@ public class ArtifactFactory implements ArtifactResolver { * root</code> and <code>execPath</code> to the specified values. */ private synchronized Artifact getArtifact( - Path path, ArtifactRoot root, PathFragment execPath, ArtifactOwner owner, @@ -251,7 +236,7 @@ public class ArtifactFactory implements ArtifactResolver { Preconditions.checkNotNull(execPath); if (!root.isSourceRoot()) { - return createArtifact(path, root, execPath, owner, type); + return createArtifact(root, execPath, owner, type); } Artifact artifact = sourceArtifactCache.getArtifact(execPath); @@ -260,23 +245,22 @@ public class ArtifactFactory implements ArtifactResolver { // There really should be a safety net that makes it impossible to create two Artifacts // with the same exec path but a different Owner, but we also need to reuse Artifacts from // previous builds. - artifact = createArtifact(path, root, execPath, owner, type); + artifact = createArtifact(root, execPath, owner, type); sourceArtifactCache.putArtifact(execPath, artifact); } return artifact; } private Artifact createArtifact( - Path path, ArtifactRoot root, PathFragment execPath, ArtifactOwner owner, @Nullable SpecialArtifactType type) { - Preconditions.checkNotNull(owner, path); + Preconditions.checkNotNull(owner); if (type == null) { - return new Artifact(path, root, execPath, owner); + return new Artifact(root, execPath, owner); } else { - return new Artifact.SpecialArtifact(path, root, execPath, owner, type); + return new Artifact.SpecialArtifact(root, execPath, owner, type); } } @@ -301,7 +285,6 @@ public class ArtifactFactory implements ArtifactResolver { !relativePath.isEmpty(), "%s %s %s", relativePath, baseExecPath, baseRoot); PathFragment execPath = baseExecPath == null ? relativePath : baseExecPath.getRelative(relativePath); - execPath = execPath.normalize(); if (execPath.containsUplevelReferences()) { // Source exec paths cannot escape the source root. return null; @@ -371,17 +354,16 @@ public class ArtifactFactory implements ArtifactResolver { ArrayList<PathFragment> unresolvedPaths = new ArrayList<>(); for (PathFragment execPath : execPaths) { - PathFragment execPathNormalized = execPath.normalize(); - if (execPathNormalized.containsUplevelReferences()) { + if (execPath.containsUplevelReferences()) { // Source exec paths cannot escape the source root. result.put(execPath, null); continue; } // First try a quick map lookup to see if the artifact already exists. - Artifact a = sourceArtifactCache.getArtifactIfValid(execPathNormalized); + Artifact a = sourceArtifactCache.getArtifactIfValid(execPath); if (a != null) { result.put(execPath, a); - } else if (isDerivedArtifact(execPathNormalized)) { + } else if (isDerivedArtifact(execPath)) { // Don't create an artifact if it's derived. result.put(execPath, null); } else { diff --git a/src/main/java/com/google/devtools/build/lib/analysis/actions/PopulateTreeArtifactAction.java b/src/main/java/com/google/devtools/build/lib/analysis/actions/PopulateTreeArtifactAction.java index fb5eafda48..fb9dc28823 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/actions/PopulateTreeArtifactAction.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/actions/PopulateTreeArtifactAction.java @@ -273,7 +273,7 @@ public final class PopulateTreeArtifactAction extends AbstractAction { if (!line.isEmpty()) { PathFragment path = PathFragment.create(line); - if (!path.isNormalized() || path.isAbsolute()) { + if (!PathFragment.isNormalized(line) || path.isAbsolute()) { throw new IllegalManifestFileException( path + " is not a proper relative path"); } diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java index d5aa190596..8652028507 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java @@ -1307,6 +1307,16 @@ public class BuildConfiguration implements BuildEvent { return Objects.hash(isActionsEnabled(), fragments, buildOptions.getOptions()); } + public void describe(StringBuilder sb) { + sb.append(isActionsEnabled()).append('\n'); + for (Fragment fragment : fragments.values()) { + sb.append(fragment.getClass().getName()).append('\n'); + } + for (String s : buildOptions.toString().split(" ")) { + sb.append(s).append('\n'); + } + } + @Override public int hashCode() { return hashCode; diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/StripPrefixedPath.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/StripPrefixedPath.java index a957a00d02..ea3f887c21 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/repository/StripPrefixedPath.java +++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/StripPrefixedPath.java @@ -64,10 +64,9 @@ public final class StripPrefixedPath { * Normalize the path and, if it is absolute, make it relative (e.g., /foo/bar becomes foo/bar). */ private static PathFragment relativize(String path) { - PathFragment entryPath = PathFragment.create(path).normalize(); + PathFragment entryPath = PathFragment.create(path); if (entryPath.isAbsolute()) { - entryPath = PathFragment.create(entryPath.getSafePathString().substring( - entryPath.windowsVolume().length() + 1)); + entryPath = entryPath.toRelative(); } return entryPath; } diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/ZipDecompressor.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/ZipDecompressor.java index 71d26fcc55..0f32e752b0 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/repository/ZipDecompressor.java +++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/ZipDecompressor.java @@ -124,19 +124,17 @@ public class ZipDecompressor implements Decompressor { // For symlinks, the "compressed data" is actually the target name. int read = reader.getInputStream(entry).read(buffer); Preconditions.checkState(read == buffer.length); - PathFragment target = PathFragment.create(new String(buffer, Charset.defaultCharset())) - .normalize(); - if (!target.isNormalized()) { - PathFragment pointsTo = - PathFragment.create(strippedRelativePath.getParentDirectory(), target).normalize(); - if (!pointsTo.isNormalized()) { + PathFragment target = PathFragment.create(new String(buffer, Charset.defaultCharset())); + if (target.containsUplevelReferences()) { + PathFragment pointsTo = strippedRelativePath.getParentDirectory().getRelative(target); + if (pointsTo.containsUplevelReferences()) { throw new IOException("Zip entries cannot refer to files outside of their directory: " + reader.getFilename() + " has a symlink " + strippedRelativePath + " pointing to " + target); } } if (target.isAbsolute()) { - target = target.relativeTo(PathFragment.ROOT_DIR); + target = target.relativeTo("/"); target = destinationDirectory.getRelative(target).asFragment(); } outputPath.createSymbolicLink(target); diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaSemantics.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaSemantics.java index 50c65c1298..21f5ae5c55 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaSemantics.java +++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaSemantics.java @@ -236,8 +236,8 @@ public class BazelJavaSemantics implements JavaSemantics { if (!isRunfilesEnabled) { buffer.append("$(rlocation "); PathFragment runfilePath = - PathFragment.create(PathFragment.create(workspacePrefix), artifact.getRunfilesPath()); - buffer.append(runfilePath.normalize().getPathString()); + PathFragment.create(workspacePrefix).getRelative(artifact.getRunfilesPath()); + buffer.append(runfilePath.getPathString()); buffer.append(")"); } else { buffer.append("${RUNPATH}"); diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPythonSemantics.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPythonSemantics.java index 0227f75ab2..8f9ede020b 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPythonSemantics.java +++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPythonSemantics.java @@ -107,8 +107,8 @@ public class BazelPythonSemantics implements PythonSemantics { "ignoring invalid absolute path '" + importsAttr + "'"); continue; } - PathFragment importsPath = packageFragment.getRelative(importsAttr).normalize(); - if (!importsPath.isNormalized()) { + PathFragment importsPath = packageFragment.getRelative(importsAttr); + if (importsPath.containsUplevelReferences()) { ruleContext.attributeError("imports", "Path " + importsAttr + " references a path above the execution root"); } @@ -239,10 +239,10 @@ public class BazelPythonSemantics implements PythonSemantics { String zipRunfilesPath; if (isUnderWorkspace(path)) { // If the file is under workspace, add workspace name as prefix - zipRunfilesPath = workspaceName.getRelative(path).normalize().toString(); + zipRunfilesPath = workspaceName.getRelative(path).toString(); } else { // If the file is in external package, strip "external" - zipRunfilesPath = path.relativeTo(Label.EXTERNAL_PATH_PREFIX).normalize().toString(); + zipRunfilesPath = path.relativeTo(Label.EXTERNAL_PATH_PREFIX).toString(); } // We put the whole runfiles tree under the ZIP_RUNFILES_DIRECTORY_NAME directory, by doing this // , we avoid the conflict between default workspace name "__main__" and __main__.py file. diff --git a/src/main/java/com/google/devtools/build/lib/buildeventstream/transports/BuildEventTransportFactory.java b/src/main/java/com/google/devtools/build/lib/buildeventstream/transports/BuildEventTransportFactory.java index d07f6a59ea..8c3921521e 100644 --- a/src/main/java/com/google/devtools/build/lib/buildeventstream/transports/BuildEventTransportFactory.java +++ b/src/main/java/com/google/devtools/build/lib/buildeventstream/transports/BuildEventTransportFactory.java @@ -22,6 +22,8 @@ import com.google.devtools.build.lib.buildeventstream.BuildEventTransport; import com.google.devtools.build.lib.buildeventstream.PathConverter; import com.google.devtools.build.lib.vfs.Path; import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; /** Factory used to create a Set of BuildEventTransports from BuildEventStreamOptions. */ public enum BuildEventTransportFactory { @@ -101,7 +103,41 @@ public enum BuildEventTransportFactory { private static class NullPathConverter implements PathConverter { @Override public String apply(Path path) { - return path.toURI().toString(); + return pathToUriString(path.getPathString()); + } + } + + /** + * Returns the path encoded as an {@link URI}. + * + * <p>This concrete implementation returns URIs with "file" as the scheme. For Example: - On Unix + * the path "/tmp/foo bar.txt" will be encoded as "file:///tmp/foo%20bar.txt". - On Windows the + * path "C:\Temp\Foo Bar.txt" will be encoded as "file:///C:/Temp/Foo%20Bar.txt" + * + * <p>Implementors extending this class for special filesystems will likely need to override this + * method. + * + * @throws URISyntaxException if the URI cannot be constructed. + */ + static String pathToUriString(String path) { + if (!path.startsWith("/")) { + // On Windows URI's need to start with a '/'. i.e. C:\Foo\Bar would be file:///C:/Foo/Bar + path = "/" + path; + } + try { + return new URI( + "file", + // Needs to be "" instead of null, so that toString() will append "//" after the + // scheme. + // We need this for backwards compatibility reasons as some consumers of the BEP are + // broken. + "", + path, + null, + null) + .toString(); + } catch (URISyntaxException e) { + throw new IllegalStateException(e); } } } diff --git a/src/main/java/com/google/devtools/build/lib/cmdline/Label.java b/src/main/java/com/google/devtools/build/lib/cmdline/Label.java index 2fbb2a4217..054648dd18 100644 --- a/src/main/java/com/google/devtools/build/lib/cmdline/Label.java +++ b/src/main/java/com/google/devtools/build/lib/cmdline/Label.java @@ -345,8 +345,17 @@ public final class Label return packageIdentifier.getPackageFragment(); } - /** Returns the label as a path fragment, using the package and the label name. */ + /** + * Returns the label as a path fragment, using the package and the label name. + * + * <p>Make sure that the label refers to a file. Non-file labels do not necessarily have + * PathFragment representations. + */ public PathFragment toPathFragment() { + // PathFragments are normalized, so if we do this on a non-file target named '.' + // then the package would be returned. Detect this and throw. + // A target named '.' can never refer to a file. + Preconditions.checkArgument(!name.equals(".")); return packageIdentifier.getPackageFragment().getRelative(name); } diff --git a/src/main/java/com/google/devtools/build/lib/cmdline/PackageIdentifier.java b/src/main/java/com/google/devtools/build/lib/cmdline/PackageIdentifier.java index ac62a3ae96..2067b641f0 100644 --- a/src/main/java/com/google/devtools/build/lib/cmdline/PackageIdentifier.java +++ b/src/main/java/com/google/devtools/build/lib/cmdline/PackageIdentifier.java @@ -87,7 +87,7 @@ public final class PackageIdentifier // TODO(ulfjack): Remove this when kchodorow@'s exec root rearrangement has been rolled out. RepositoryName repository = RepositoryName.create("@" + tofind.getSegment(1)); return PackageIdentifier.create(repository, tofind.subFragment(2)); - } else if (!tofind.normalize().isNormalized()) { + } else if (tofind.containsUplevelReferences()) { RepositoryName repository = RepositoryName.create("@" + tofind.getSegment(1)); return PackageIdentifier.create(repository, tofind.subFragment(2)); } else { @@ -112,9 +112,6 @@ public final class PackageIdentifier private PackageIdentifier(RepositoryName repository, PathFragment pkgName) { this.repository = Preconditions.checkNotNull(repository); - if (!pkgName.isNormalized()) { - pkgName = pkgName.normalize(); - } this.pkgName = Canonicalizer.fragments().intern(Preconditions.checkNotNull(pkgName)); this.hashCode = Objects.hash(repository, pkgName); } diff --git a/src/main/java/com/google/devtools/build/lib/packages/Package.java b/src/main/java/com/google/devtools/build/lib/packages/Package.java index 4bbd3bfe7c..8a1f02a63d 100644 --- a/src/main/java/com/google/devtools/build/lib/packages/Package.java +++ b/src/main/java/com/google/devtools/build/lib/packages/Package.java @@ -536,7 +536,7 @@ public class Package { // stat(2) is executed. Path filename = getPackageDirectory().getRelative(targetName); String suffix; - if (!PathFragment.create(targetName).isNormalized()) { + if (!PathFragment.isNormalized(targetName)) { // Don't check for file existence in this case because the error message // would be confusing and wrong. If the targetName is "foo/bar/.", and // there is a directory "foo/bar", it doesn't mean that "//pkg:foo/bar/." diff --git a/src/main/java/com/google/devtools/build/lib/packages/RelativePackageNameResolver.java b/src/main/java/com/google/devtools/build/lib/packages/RelativePackageNameResolver.java index 871080702b..bc314a4f1a 100644 --- a/src/main/java/com/google/devtools/build/lib/packages/RelativePackageNameResolver.java +++ b/src/main/java/com/google/devtools/build/lib/packages/RelativePackageNameResolver.java @@ -72,7 +72,6 @@ public class RelativePackageNameResolver { } PathFragment result = isAbsolute ? relative : offset.getRelative(relative); - result = result.normalize(); if (result.containsUplevelReferences()) { throw new InvalidPackageNameException( PackageIdentifier.createInMainRepo(pkg), diff --git a/src/main/java/com/google/devtools/build/lib/remote/TreeNodeRepository.java b/src/main/java/com/google/devtools/build/lib/remote/TreeNodeRepository.java index 0e57b17e44..8a1dfe9fa4 100644 --- a/src/main/java/com/google/devtools/build/lib/remote/TreeNodeRepository.java +++ b/src/main/java/com/google/devtools/build/lib/remote/TreeNodeRepository.java @@ -110,8 +110,8 @@ public final class TreeNodeRepository extends TreeTraverser<TreeNodeRepository.T return false; } ChildEntry other = (ChildEntry) o; - // Pointer comparisons only, because both the Path segments and the TreeNodes are interned. - return other.segment == segment && other.child == child; + // Pointer comparison for the TreeNode as it is interned + return other.segment.equals(segment) && other.child == child; } @Override @@ -322,7 +322,7 @@ public final class TreeNodeRepository extends TreeTraverser<TreeNodeRepository.T String segment = segments.get(inputsStart).get(segmentIndex); for (int inputIndex = inputsStart; inputIndex < inputsEnd; ++inputIndex) { if (inputIndex + 1 == inputsEnd - || segment != segments.get(inputIndex + 1).get(segmentIndex)) { + || !segment.equals(segments.get(inputIndex + 1).get(segmentIndex))) { entries.add( new TreeNode.ChildEntry( segment, diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCommon.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCommon.java index fa2c96e789..e9ae312128 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCommon.java +++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCommon.java @@ -545,8 +545,8 @@ public final class CcCommon { "ignoring invalid absolute path '" + includesAttr + "'"); continue; } - PathFragment includesPath = packageFragment.getRelative(includesAttr).normalize(); - if (!includesPath.isNormalized()) { + PathFragment includesPath = packageFragment.getRelative(includesAttr); + if (includesPath.containsUplevelReferences()) { ruleContext.attributeError("includes", "Path references a path above the execution root."); } diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCompilationHelper.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCompilationHelper.java index d6464d1340..a7c1615d92 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCompilationHelper.java +++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCompilationHelper.java @@ -757,15 +757,25 @@ public final class CcCompilationHelper { null); } - PathFragment prefix = - ruleContext.attributes().isAttributeValueExplicitlySpecified("include_prefix") - ? PathFragment.create(ruleContext.attributes().get("include_prefix", Type.STRING)) - : null; + PathFragment prefix = null; + if (ruleContext.attributes().isAttributeValueExplicitlySpecified("include_prefix")) { + String prefixAttr = ruleContext.attributes().get("include_prefix", Type.STRING); + prefix = PathFragment.create(prefixAttr); + if (PathFragment.containsUplevelReferences(prefixAttr)) { + ruleContext.attributeError("include_prefix", "should not contain uplevel references"); + } + if (prefix.isAbsolute()) { + ruleContext.attributeError("include_prefix", "should be a relative path"); + } + } PathFragment stripPrefix; if (ruleContext.attributes().isAttributeValueExplicitlySpecified("strip_include_prefix")) { - stripPrefix = - PathFragment.create(ruleContext.attributes().get("strip_include_prefix", Type.STRING)); + String stripPrefixAttr = ruleContext.attributes().get("strip_include_prefix", Type.STRING); + if (PathFragment.containsUplevelReferences(stripPrefixAttr)) { + ruleContext.attributeError("strip_include_prefix", "should not contain uplevel references"); + } + stripPrefix = PathFragment.create(stripPrefixAttr); if (stripPrefix.isAbsolute()) { stripPrefix = ruleContext @@ -791,18 +801,6 @@ public final class CcCompilationHelper { null); } - if (stripPrefix.containsUplevelReferences()) { - ruleContext.attributeError("strip_include_prefix", "should not contain uplevel references"); - } - - if (prefix != null && prefix.containsUplevelReferences()) { - ruleContext.attributeError("include_prefix", "should not contain uplevel references"); - } - - if (prefix != null && prefix.isAbsolute()) { - ruleContext.attributeError("include_prefix", "should be a relative path"); - } - if (ruleContext.hasErrors()) { return new PublicHeaders(ImmutableList.<Artifact>of(), ImmutableList.<Artifact>of(), null); } diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcIncLibrary.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcIncLibrary.java index a844ef6375..9101e9b768 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcIncLibrary.java +++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcIncLibrary.java @@ -98,13 +98,12 @@ public abstract class CcIncLibrary implements RuleConfiguredTargetFactory { // For every source artifact, we compute a virtual artifact that is below the include directory. // These are used for include checking. - PathFragment prefixFragment = packageFragment.getRelative(expandedIncSymlinkAttr); - if (!prefixFragment.isNormalized()) { + if (!PathFragment.isNormalized(expandedIncSymlinkAttr)) { ruleContext.attributeWarning("prefix", "should not contain '.' or '..' elements"); } + PathFragment prefixFragment = packageFragment.getRelative(expandedIncSymlinkAttr); ImmutableSortedMap.Builder<Artifact, Artifact> virtualArtifactMapBuilder = ImmutableSortedMap.orderedBy(Artifact.EXEC_PATH_COMPARATOR); - prefixFragment = prefixFragment.normalize(); ImmutableList<Artifact> hdrs = ruleContext.getPrerequisiteArtifacts("hdrs", Mode.TARGET).list(); for (Artifact src : hdrs) { // All declared header files must start with package/targetPrefix. diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcToolchain.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcToolchain.java index 122fc14914..5f848b4a29 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcToolchain.java +++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcToolchain.java @@ -169,10 +169,10 @@ public class CcToolchain implements RuleConfiguredTargetFactory { } } - PathFragment path = PathFragment.create(pathString); - if (!path.isNormalized()) { + if (!PathFragment.isNormalized(pathString)) { throw new InvalidConfigurationException("The include path '" + s + "' is not normalized."); } + PathFragment path = PathFragment.create(pathString); return pathPrefix.getRelative(path); } diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompilationContext.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompilationContext.java index da8827d6ac..ea12fadece 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompilationContext.java +++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompilationContext.java @@ -512,7 +512,7 @@ public final class CppCompilationContext implements TransitiveInfoProvider { * absolute. Before it is stored, the include directory is normalized. */ public Builder addIncludeDir(PathFragment includeDir) { - includeDirs.add(includeDir.normalize()); + includeDirs.add(includeDir); return this; } @@ -536,7 +536,7 @@ public final class CppCompilationContext implements TransitiveInfoProvider { * is stored, the include directory is normalized. */ public Builder addQuoteIncludeDir(PathFragment quoteIncludeDir) { - quoteIncludeDirs.add(quoteIncludeDir.normalize()); + quoteIncludeDirs.add(quoteIncludeDir); return this; } @@ -547,7 +547,7 @@ public final class CppCompilationContext implements TransitiveInfoProvider { * is stored, the include directory is normalized. */ public Builder addSystemIncludeDir(PathFragment systemIncludeDir) { - systemIncludeDirs.add(systemIncludeDir.normalize()); + systemIncludeDirs.add(systemIncludeDir); return this; } diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileActionBuilder.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileActionBuilder.java index 247d3ef91f..e462a03822 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileActionBuilder.java +++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileActionBuilder.java @@ -488,8 +488,7 @@ public class CppCompileActionBuilder { if (includePath.startsWith(Label.EXTERNAL_PATH_PREFIX)) { includePath = includePath.relativeTo(Label.EXTERNAL_PATH_PREFIX); } - if (includePath.isAbsolute() - || !PathFragment.EMPTY_FRAGMENT.getRelative(includePath).normalize().isNormalized()) { + if (includePath.isAbsolute() || includePath.containsUplevelReferences()) { errorReporter.accept( String.format( "The include path '%s' references a path outside of the execution root.", diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppConfiguration.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppConfiguration.java index 058f34db51..107a4cce77 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppConfiguration.java +++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppConfiguration.java @@ -1381,15 +1381,15 @@ public final class CppConfiguration extends BuildConfiguration.Fragment { } public static PathFragment computeDefaultSysroot(CToolchain toolchain) { - PathFragment defaultSysroot = - toolchain.getBuiltinSysroot().length() == 0 - ? null - : PathFragment.create(toolchain.getBuiltinSysroot()); - if ((defaultSysroot != null) && !defaultSysroot.isNormalized()) { + String builtInSysroot = toolchain.getBuiltinSysroot(); + if (builtInSysroot.isEmpty()) { + return null; + } + if (!PathFragment.isNormalized(builtInSysroot)) { throw new IllegalArgumentException( - "The built-in sysroot '" + defaultSysroot + "' is not normalized."); + "The built-in sysroot '" + builtInSysroot + "' is not normalized."); } - return defaultSysroot; + return PathFragment.create(builtInSysroot); } @Override diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppToolchainInfo.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppToolchainInfo.java index 135b70be3b..91f95020b8 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppToolchainInfo.java +++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppToolchainInfo.java @@ -794,11 +794,11 @@ public final class CppToolchainInfo { CToolchain toolchain, PathFragment crosstoolTopPathFragment) { Map<String, PathFragment> toolPathsCollector = Maps.newHashMap(); for (CrosstoolConfig.ToolPath tool : toolchain.getToolPathList()) { - PathFragment path = PathFragment.create(tool.getPath()); - if (!path.isNormalized()) { - throw new IllegalArgumentException( - "The include path '" + tool.getPath() + "' is not normalized."); + String pathStr = tool.getPath(); + if (!PathFragment.isNormalized(pathStr)) { + throw new IllegalArgumentException("The include path '" + pathStr + "' is not normalized."); } + PathFragment path = PathFragment.create(pathStr); toolPathsCollector.put(tool.getName(), crosstoolTopPathFragment.getRelative(path)); } diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCommon.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCommon.java index 458ac578d1..b2394ca51d 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCommon.java +++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCommon.java @@ -505,9 +505,9 @@ public class JavaCommon { if (!javaExecutable.isAbsolute()) { javaExecutable = - PathFragment.create(PathFragment.create(ruleContext.getWorkspaceName()), javaExecutable); + PathFragment.create(ruleContext.getWorkspaceName()).getRelative(javaExecutable); } - return javaExecutable.normalize().getPathString(); + return javaExecutable.getPathString(); } /** diff --git a/src/main/java/com/google/devtools/build/lib/rules/nativedeps/NativeDepsHelper.java b/src/main/java/com/google/devtools/build/lib/rules/nativedeps/NativeDepsHelper.java index 7f6437ed24..e15c7485a3 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/nativedeps/NativeDepsHelper.java +++ b/src/main/java/com/google/devtools/build/lib/rules/nativedeps/NativeDepsHelper.java @@ -312,7 +312,7 @@ public abstract class NativeDepsHelper { PathFragment libParentDir = relativePath.replaceName(lib.getExecPath().getParentDirectory().getBaseName()); String libName = lib.getExecPath().getBaseName(); - return PathFragment.create(libParentDir, PathFragment.create(libName)); + return libParentDir.getRelative(libName); } /** diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/AppleStubBinary.java b/src/main/java/com/google/devtools/build/lib/rules/objc/AppleStubBinary.java index 2e84f6f491..e73c66572a 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/objc/AppleStubBinary.java +++ b/src/main/java/com/google/devtools/build/lib/rules/objc/AppleStubBinary.java @@ -188,8 +188,7 @@ public class AppleStubBinary implements RuleConfiguredTargetFactory { makeVariableContext.validatePathRoot(pathString); - PathFragment pathFragment = PathFragment.create(pathString); - if (!pathFragment.isNormalized()) { + if (!PathFragment.isNormalized(pathString)) { throw ruleContext.throwWithAttributeError( AppleStubBinaryRule.XCENV_BASED_PATH_ATTR, PATH_NOT_NORMALIZED_ERROR); } diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/CompilationAttributes.java b/src/main/java/com/google/devtools/build/lib/rules/objc/CompilationAttributes.java index 39c28b750a..f3c79b270b 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/objc/CompilationAttributes.java +++ b/src/main/java/com/google/devtools/build/lib/rules/objc/CompilationAttributes.java @@ -403,7 +403,7 @@ final class CompilationAttributes { Iterables.filter(includes(), Predicates.not(PathFragment::isAbsolute)); for (PathFragment include : relativeIncludes) { for (PathFragment rootFragment : rootFragments) { - paths.add(rootFragment.getRelative(include).normalize()); + paths.add(rootFragment.getRelative(include)); } } } diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/J2ObjcLibrary.java b/src/main/java/com/google/devtools/build/lib/rules/objc/J2ObjcLibrary.java index 27c1f67b5e..61bbb37431 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/objc/J2ObjcLibrary.java +++ b/src/main/java/com/google/devtools/build/lib/rules/objc/J2ObjcLibrary.java @@ -130,7 +130,7 @@ public class J2ObjcLibrary implements RuleConfiguredTargetFactory { // We add another header search path with gen root if we have generated sources to translate. for (Artifact sourceToTranslate : sourcesToTranslate) { if (!sourceToTranslate.isSourceArtifact()) { - headerSearchPaths.add(PathFragment.create(objcFileRootExecPath, genRoot)); + headerSearchPaths.add(objcFileRootExecPath.getRelative(genRoot)); return headerSearchPaths.build(); } } diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ProtobufSupport.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ProtobufSupport.java index 1a7e1e28f6..b3c6d7e93c 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/objc/ProtobufSupport.java +++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ProtobufSupport.java @@ -515,8 +515,7 @@ final class ProtobufSupport { // of dependers. PathFragment rootRelativeOutputDir = ruleContext.getUniqueDirectory(UNIQUE_DIRECTORY_NAME); - return PathFragment.create( - buildConfiguration.getBinDirectory().getExecPath(), rootRelativeOutputDir); + return buildConfiguration.getBinDirectory().getExecPath().getRelative(rootRelativeOutputDir); } private Iterable<Artifact> getGeneratedProtoOutputs( @@ -526,9 +525,8 @@ final class ProtobufSupport { String protoFileName = FileSystemUtils.removeExtension(protoFile.getFilename()); String generatedOutputName = attributes.getGeneratedProtoFilename(protoFileName, true); - PathFragment generatedFilePath = PathFragment.create( - protoFile.getRootRelativePath().getParentDirectory(), - PathFragment.create(generatedOutputName)); + PathFragment generatedFilePath = + protoFile.getRootRelativePath().getParentDirectory().getRelative(generatedOutputName); PathFragment outputFile = FileSystemUtils.appendExtension(generatedFilePath, extension); diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/DumpCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/DumpCommand.java index c4b1e5f301..87cddd7d3b 100644 --- a/src/main/java/com/google/devtools/build/lib/runtime/commands/DumpCommand.java +++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/DumpCommand.java @@ -33,7 +33,6 @@ import com.google.devtools.build.lib.runtime.CommandEnvironment; import com.google.devtools.build.lib.skyframe.SkyframeExecutor; import com.google.devtools.build.lib.skyframe.SkyframeExecutor.RuleStat; import com.google.devtools.build.lib.util.ExitCode; -import com.google.devtools.build.lib.vfs.FileSystemUtils; import com.google.devtools.common.options.EnumConverter; import com.google.devtools.common.options.Option; import com.google.devtools.common.options.OptionDocumentationCategory; @@ -207,8 +206,9 @@ public class DumpCommand implements BlazeCommand { } if (dumpOptions.dumpVfs) { + // TODO(b/72498697): Remove this flag out.println("Filesystem cache"); - FileSystemUtils.dump(env.getOutputBase().getFileSystem(), out); + out.println("dump --vfs is no longer meaningful"); out.println(); } diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/LocalRepositoryLookupFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/LocalRepositoryLookupFunction.java index 6d6d2eb3f7..4c699ab635 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/LocalRepositoryLookupFunction.java +++ b/src/main/java/com/google/devtools/build/lib/skyframe/LocalRepositoryLookupFunction.java @@ -217,8 +217,7 @@ public class LocalRepositoryLookupFunction implements SkyFunction { String path = (String) rule.getAttributeContainer().getAttr("path"); return Optional.of( LocalRepositoryLookupValue.success( - RepositoryName.create("@" + rule.getName()), - PathFragment.create(path).normalize())); + RepositoryName.create("@" + rule.getName()), PathFragment.create(path))); } catch (LabelSyntaxException e) { // This shouldn't be possible if the rule name is valid, and it should already have been // validated. diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java index 6da94fd90b..6114f1d933 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java +++ b/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java @@ -814,7 +814,13 @@ public class PackageFunction implements SkyFunction { Set<SkyKey> containingPkgLookupKeys = Sets.newHashSet(); Map<Target, SkyKey> targetToKey = new HashMap<>(); for (Target target : pkgBuilder.getTargets()) { - PathFragment dir = target.getLabel().toPathFragment().getParentDirectory(); + PathFragment dir = getContainingDirectory(target.getLabel()); + if (dir == null) { + throw new IllegalStateException( + String.format( + "Null pkg for label %s as path fragment %s in pkg %s", + target.getLabel(), target.getLabel().getPackageFragment(), pkgId)); + } PackageIdentifier dirId = PackageIdentifier.create(pkgId.getRepository(), dir); if (dir.equals(pkgId.getPackageFragment())) { continue; @@ -825,7 +831,7 @@ public class PackageFunction implements SkyFunction { } Map<Label, SkyKey> subincludeToKey = new HashMap<>(); for (Label subincludeLabel : pkgBuilder.getSubincludeLabels()) { - PathFragment dir = subincludeLabel.toPathFragment().getParentDirectory(); + PathFragment dir = getContainingDirectory(subincludeLabel); PackageIdentifier dirId = PackageIdentifier.create(pkgId.getRepository(), dir); if (dir.equals(pkgId.getPackageFragment())) { continue; @@ -870,6 +876,12 @@ public class PackageFunction implements SkyFunction { } } + private static PathFragment getContainingDirectory(Label label) { + PathFragment pkg = label.getPackageFragment(); + String name = label.getName(); + return name.equals(".") ? pkg : pkg.getRelative(name).getParentDirectory(); + } + @Nullable private static ContainingPackageLookupValue getContainingPkgLookupValueAndPropagateInconsistentFilesystemExceptions( diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java index 97d257c84a..ff12e07091 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java +++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java @@ -266,7 +266,8 @@ public final class SkyframeActionExecutor implements ActionExecutionContextFacto ConcurrentMap<ActionAnalysisMetadata, ConflictException> badActionMap) throws InterruptedException { MutableActionGraph actionGraph = new MapBasedActionGraph(actionKeyContext); - ConcurrentNavigableMap<PathFragment, Artifact> artifactPathMap = new ConcurrentSkipListMap<>(); + ConcurrentNavigableMap<PathFragment, Artifact> artifactPathMap = + new ConcurrentSkipListMap<>(Actions.comparatorForPrefixConflicts()); // Action graph construction is CPU-bound. int numJobs = Runtime.getRuntime().availableProcessors(); // No great reason for expecting 5000 action lookup values, but not worth counting size of diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TargetMarkerFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/TargetMarkerFunction.java index 2aeba41d09..1758fb3f1f 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/TargetMarkerFunction.java +++ b/src/main/java/com/google/devtools/build/lib/skyframe/TargetMarkerFunction.java @@ -57,7 +57,7 @@ public final class TargetMarkerFunction implements SkyFunction { if (label.getName().contains("/")) { // This target is in a subdirectory, therefore it could potentially be invalidated by // a new BUILD file appearing in the hierarchy. - PathFragment containingDirectory = label.toPathFragment().getParentDirectory(); + PathFragment containingDirectory = getContainingDirectory(label); ContainingPackageLookupValue containingPackageLookupValue; try { PackageIdentifier newPkgId = PackageIdentifier.create( @@ -109,6 +109,12 @@ public final class TargetMarkerFunction implements SkyFunction { return TargetMarkerValue.TARGET_MARKER_INSTANCE; } + private static PathFragment getContainingDirectory(Label label) { + PathFragment pkg = label.getPackageFragment(); + String name = label.getName(); + return name.equals(".") ? pkg : pkg.getRelative(name).getParentDirectory(); + } + @Override public String extractTag(SkyKey skyKey) { return Label.print((Label) skyKey.argument()); diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TreeArtifactValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/TreeArtifactValue.java index 783899ab45..f54522f8f1 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/TreeArtifactValue.java +++ b/src/main/java/com/google/devtools/build/lib/skyframe/TreeArtifactValue.java @@ -182,8 +182,7 @@ class TreeArtifactValue implements SkyValue { PathFragment pathToExplode, ImmutableSet.Builder<PathFragment> valuesBuilder) throws IOException { for (Path subpath : treeArtifact.getPath().getRelative(pathToExplode).getDirectoryEntries()) { - PathFragment canonicalSubpathFragment = - pathToExplode.getChild(subpath.getBaseName()).normalize(); + PathFragment canonicalSubpathFragment = pathToExplode.getChild(subpath.getBaseName()); if (subpath.isDirectory()) { explodeDirectory(treeArtifact, pathToExplode.getChild(subpath.getBaseName()), valuesBuilder); @@ -202,7 +201,7 @@ class TreeArtifactValue implements SkyValue { // TreeArtifact into a/b/outside_dir. PathFragment intermediatePath = canonicalSubpathFragment.getParentDirectory(); for (String pathSegment : linkTarget.getSegments()) { - intermediatePath = intermediatePath.getRelative(pathSegment).normalize(); + intermediatePath = intermediatePath.getRelative(pathSegment); if (intermediatePath.containsUplevelReferences()) { String errorMessage = String.format( "A TreeArtifact may not contain relative symlinks whose target paths traverse " diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkImports.java b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkImports.java index e8c9ad2b54..26fc565930 100644 --- a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkImports.java +++ b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkImports.java @@ -173,7 +173,7 @@ public class SkylarkImports { @Override public PathFragment asPathFragment() { - return PathFragment.create(PathFragment.ROOT_DIR).getRelative(importLabel.toPathFragment()); + return PathFragment.create("/").getRelative(importLabel.toPathFragment()); } @Override diff --git a/src/main/java/com/google/devtools/build/lib/unix/UnixFileSystem.java b/src/main/java/com/google/devtools/build/lib/unix/UnixFileSystem.java index 033a6a692d..d5a74ebaf6 100644 --- a/src/main/java/com/google/devtools/build/lib/unix/UnixFileSystem.java +++ b/src/main/java/com/google/devtools/build/lib/unix/UnixFileSystem.java @@ -326,7 +326,7 @@ public class UnixFileSystem extends AbstractFileSystemWithCustomStat { @Override protected void createSymbolicLink(Path linkPath, PathFragment targetFragment) throws IOException { - NativePosixFiles.symlink(targetFragment.toString(), linkPath.toString()); + NativePosixFiles.symlink(targetFragment.getSafePathString(), linkPath.toString()); } @Override diff --git a/src/main/java/com/google/devtools/build/lib/vfs/BUILD b/src/main/java/com/google/devtools/build/lib/vfs/BUILD index f7a57bee03..b829858847 100644 --- a/src/main/java/com/google/devtools/build/lib/vfs/BUILD +++ b/src/main/java/com/google/devtools/build/lib/vfs/BUILD @@ -10,8 +10,9 @@ PATH_FRAGMENT_SOURCES = [ "Canonicalizer.java", "PathFragment.java", "PathFragmentSerializationProxy.java", - "UnixPathFragment.java", - "WindowsPathFragment.java", + "OsPathPolicy.java", + "UnixOsPathPolicy.java", + "WindowsOsPathPolicy.java", ] java_library( @@ -25,6 +26,8 @@ java_library( "//src/main/java/com/google/devtools/build/lib:skylarkinterface", "//src/main/java/com/google/devtools/build/lib/concurrent", "//src/main/java/com/google/devtools/build/lib/skyframe/serialization", + "//src/main/java/com/google/devtools/build/lib/windows:windows_short_path", + "//src/main/java/com/google/devtools/build/lib/windows/jni", "//third_party:guava", "//third_party/protobuf:protobuf_java", ], @@ -47,7 +50,6 @@ java_library( ":pathfragment", "//src/main/java/com/google/devtools/build/lib:base-util", "//src/main/java/com/google/devtools/build/lib:filetype", - "//src/main/java/com/google/devtools/build/lib:os_util", "//src/main/java/com/google/devtools/build/lib:skylarkinterface", "//src/main/java/com/google/devtools/build/lib/clock", "//src/main/java/com/google/devtools/build/lib/concurrent", @@ -55,8 +57,6 @@ java_library( "//src/main/java/com/google/devtools/build/lib/shell", "//src/main/java/com/google/devtools/build/lib/skyframe/serialization", "//src/main/java/com/google/devtools/build/lib/skyframe/serialization/autocodec", - "//src/main/java/com/google/devtools/build/lib/windows:windows_short_path", - "//src/main/java/com/google/devtools/build/lib/windows/jni", "//src/main/java/com/google/devtools/common/options", "//third_party:guava", "//third_party:jsr305", diff --git a/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java index aaaacd93d2..38da81c6d9 100644 --- a/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java +++ b/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java @@ -23,7 +23,6 @@ import com.google.common.io.ByteSource; import com.google.common.io.CharStreams; import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe; import com.google.devtools.build.lib.vfs.Dirent.Type; -import com.google.devtools.build.lib.vfs.Path.PathFactory; import com.google.devtools.common.options.EnumConverter; import java.io.FileNotFoundException; import java.io.IOException; @@ -84,25 +83,6 @@ public abstract class FileSystem { return digestFunction; } - private enum UnixPathFactory implements PathFactory { - INSTANCE { - @Override - public Path createRootPath(FileSystem filesystem) { - return new Path(filesystem, PathFragment.ROOT_DIR, null); - } - - @Override - public Path createChildPath(Path parent, String childName) { - return new Path(parent.getFileSystem(), childName, parent); - } - - @Override - public Path getCachedChildPathInternal(Path path, String childName) { - return Path.getCachedChildPathInternal(path, childName, /*cacheable=*/ true); - } - }; - } - /** * An exception thrown when attempting to resolve an ordinary file as a symlink. */ @@ -112,53 +92,23 @@ public abstract class FileSystem { } } - /** Lazy-initialized on first access, always use {@link FileSystem#getRootDirectory} */ - private volatile Path rootPath; - private final Root absoluteRoot = new Root.AbsoluteRoot(this); - /** Returns filesystem-specific path factory. */ - protected PathFactory getPathFactory() { - return UnixPathFactory.INSTANCE; - } - - /** - * Returns an absolute path instance, given an absolute path name, without - * double slashes, .., or . segments. While this method will normalize the - * path representation by creating a structured/parsed representation, it will - * not cause any IO. (e.g., it will not resolve symbolic links if it's a Unix - * file system. - */ - public Path getPath(String pathName) { - return getPath(PathFragment.create(pathName)); - } - /** - * Returns an absolute path instance, given an absolute path name, without - * double slashes, .., or . segments. While this method will normalize the - * path representation by creating a structured/parsed representation, it will - * not cause any IO. (e.g., it will not resolve symbolic links if it's a Unix - * file system. + * Returns an absolute path instance, given an absolute path name, without double slashes, .., or + * . segments. While this method will normalize the path representation by creating a + * structured/parsed representation, it will not cause any IO. (e.g., it will not resolve symbolic + * links if it's a Unix file system. */ - public Path getPath(PathFragment pathName) { - if (!pathName.isAbsolute()) { - throw new IllegalArgumentException(pathName.getPathString() + " (not an absolute path)"); - } - return getRootDirectory().getRelative(pathName); + public Path getPath(String path) { + return Path.create(path, this); } - /** - * Returns a path representing the root directory of the current file system. - */ - public final Path getRootDirectory() { - if (rootPath == null) { - synchronized (this) { - if (rootPath == null) { - rootPath = getPathFactory().createRootPath(this); - } - } - } - return rootPath; + /** Returns an absolute path instance, given an absolute path fragment. */ + public Path getPath(PathFragment pathFragment) { + Preconditions.checkArgument(pathFragment.isAbsolute()); + return Path.createAlreadyNormalized( + pathFragment.getPathString(), pathFragment.getDriveStrLength(), this); } final Root getAbsoluteRoot() { @@ -401,7 +351,7 @@ public abstract class FileSystem { throw new IOException(naive + " (Too many levels of symbolic links)"); } if (linkTarget.isAbsolute()) { - dir = getRootDirectory(); + dir = getPath(linkTarget.getDriveStr()); } for (String name : linkTarget.segments()) { if (name.equals(".") || name.isEmpty()) { diff --git a/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java b/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java index adeb9c847c..b7fd8d2a43 100644 --- a/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java +++ b/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java @@ -25,7 +25,6 @@ import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.PrintStream; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; @@ -90,25 +89,13 @@ public class FileSystemUtils { return a; } - /** - * Returns a path fragment from a given from-dir to a given to-path. May be - * either a short relative path "foo/bar", an up'n'over relative path - * "../../foo/bar" or an absolute path. - */ - public static PathFragment relativePath(Path fromDir, Path to) { - if (to.getFileSystem() != fromDir.getFileSystem()) { - throw new IllegalArgumentException("fromDir and to must be on the same FileSystem"); - } - - return relativePath(fromDir.asFragment(), to.asFragment()); - } /** * Returns a path fragment from a given from-dir to a given to-path. */ public static PathFragment relativePath(PathFragment fromDir, PathFragment to) { if (to.equals(fromDir)) { - return PathFragment.create("."); // same dir, just return '.' + return PathFragment.EMPTY_FRAGMENT; } if (to.startsWith(fromDir)) { return to.relativeTo(fromDir); // easy case--it's a descendant @@ -883,30 +870,6 @@ public class FileSystemUtils { } /** - * Dumps diagnostic information about the specified filesystem to {@code out}. - * This is the implementation of the filesystem part of the 'blaze dump' - * command. It lives here, rather than in DumpCommand, because it requires - * privileged access to members of this package. - * - * <p>Its results are unspecified and MUST NOT be interpreted programmatically. - */ - public static void dump(FileSystem fs, final PrintStream out) { - // Unfortunately there's no "letrec" for anonymous functions so we have to - // (a) name the function, (b) put it in a box and (c) use List not array - // because of the generic type. *sigh*. - final List<Predicate<Path>> dumpFunction = new ArrayList<>(); - dumpFunction.add( - child -> { - Path path = child; - out.println(" " + path + " (" + path.toDebugString() + ")"); - path.applyToChildren(dumpFunction.get(0)); - return false; - }); - - fs.getRootDirectory().applyToChildren(dumpFunction.get(0)); - } - - /** * Returns the type of the file system path belongs to. */ public static String getFileSystem(Path path) { diff --git a/src/main/java/com/google/devtools/build/lib/vfs/JavaIoFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/JavaIoFileSystem.java index 5bba53533f..f5b0220e32 100644 --- a/src/main/java/com/google/devtools/build/lib/vfs/JavaIoFileSystem.java +++ b/src/main/java/com/google/devtools/build/lib/vfs/JavaIoFileSystem.java @@ -281,7 +281,7 @@ public class JavaIoFileSystem extends AbstractFileSystemWithCustomStat { throws IOException { java.nio.file.Path nioPath = getNioPath(linkPath); try { - Files.createSymbolicLink(nioPath, Paths.get(targetFragment.getPathString())); + 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) { diff --git a/src/main/java/com/google/devtools/build/lib/vfs/LocalPath.java b/src/main/java/com/google/devtools/build/lib/vfs/LocalPath.java deleted file mode 100644 index a32a4e356c..0000000000 --- a/src/main/java/com/google/devtools/build/lib/vfs/LocalPath.java +++ /dev/null @@ -1,715 +0,0 @@ -// Copyright 2017 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.common.base.Preconditions; -import com.google.devtools.build.lib.util.OS; -import com.google.devtools.build.lib.windows.WindowsShortPath; -import com.google.devtools.build.lib.windows.jni.WindowsFileOperations; -import java.io.IOException; -import java.util.Arrays; -import java.util.concurrent.atomic.AtomicReference; -import javax.annotation.Nullable; - -/** - * A local file path representing a file on the host machine. You should use this when you want to - * access local files via the file system. - * - * <p>Paths are either absolute or relative. - * - * <p>Strings are normalized with '.' and '..' removed and resolved (if possible), any multiple - * slashes ('/') removed, and any trailing slash also removed. The current implementation does not - * touch the incoming path string unless the string actually needs to be normalized. - * - * <p>There is some limited support for Windows-style paths. Most importantly, drive identifiers in - * front of a path (c:/abc) are supported and such paths are correctly recognized as absolute, as - * are paths with backslash separators (C:\\foo\\bar). However, advanced Windows-style features like - * \\\\network\\paths and \\\\?\\unc\\paths are not supported. We are currently using forward - * slashes ('/') even on Windows, so backslashes '\' get converted to forward slashes during - * normalization. - * - * <p>Mac and Windows file paths are case insensitive. Case is preserved. - * - * <p>This class is replaces {@link Path} as the way to access the host machine's file system. - * Developers should use this class instead of {@link Path}. - */ -public final class LocalPath implements Comparable<LocalPath> { - private static final OsPathPolicy DEFAULT_OS = createFilePathOs(); - - public static final LocalPath EMPTY = create(""); - - private final String path; - private final int driveStrLength; // 0 for relative paths, 1 on Unix, 3 on Windows - private final OsPathPolicy os; - - /** Creates a local path that is specific to the host OS. */ - public static LocalPath create(String path) { - return createWithOs(path, DEFAULT_OS); - } - - @VisibleForTesting - static LocalPath createWithOs(String path, OsPathPolicy os) { - Preconditions.checkNotNull(path); - int normalizationLevel = os.needsToNormalize(path); - String normalizedPath = os.normalize(path, normalizationLevel); - int driveStrLength = os.getDriveStrLength(normalizedPath); - return new LocalPath(normalizedPath, driveStrLength, os); - } - - /** This method expects path to already be normalized. */ - private LocalPath(String path, int driveStrLength, OsPathPolicy os) { - this.path = Preconditions.checkNotNull(path); - this.driveStrLength = driveStrLength; - this.os = Preconditions.checkNotNull(os); - } - - public String getPathString() { - return path; - } - - /** - * If called on a {@link LocalPath} instance for a mount name (eg. '/' or 'C:/'), the empty string - * is returned. - */ - public String getBaseName() { - int lastSeparator = path.lastIndexOf(os.getSeparator()); - return lastSeparator < driveStrLength - ? path.substring(driveStrLength) - : path.substring(lastSeparator + 1); - } - - /** - * Returns a {@link LocalPath} instance representing the relative path between this {@link - * LocalPath} and the given {@link LocalPath}. - * - * <pre> - * Example: - * - * LocalPath.create("/foo").getRelative(LocalPath.create("bar/baz")) - * -> "/foo/bar/baz" - * </pre> - * - * <p>If the passed path is absolute it is returned untouched. This can be useful to resolve - * symlinks. - */ - public LocalPath getRelative(LocalPath other) { - Preconditions.checkNotNull(other); - Preconditions.checkArgument(os == other.os); - return getRelative(other.getPathString(), other.driveStrLength); - } - - /** - * Returns a {@link LocalPath} instance representing the relative path between this {@link - * LocalPath} and the given path. - * - * <p>See {@link #getRelative(LocalPath)} for details. - */ - public LocalPath getRelative(String other) { - Preconditions.checkNotNull(other); - return getRelative(other, os.getDriveStrLength(other)); - } - - private LocalPath getRelative(String other, int otherDriveStrLength) { - if (path.isEmpty()) { - return create(other); - } - if (other.isEmpty()) { - return this; - } - // Note that even if other came from a LocalPath instance we still might - // need to normalize the result if (for instance) other is a path that - // starts with '..' - int normalizationLevel = os.needsToNormalize(other); - // This is an absolute path, simply return it - if (otherDriveStrLength > 0) { - String normalizedPath = os.normalize(other, normalizationLevel); - return new LocalPath(normalizedPath, otherDriveStrLength, os); - } - String newPath; - if (path.length() == driveStrLength) { - newPath = path + other; - } else { - newPath = path + '/' + other; - } - newPath = os.normalize(newPath, normalizationLevel); - return new LocalPath(newPath, driveStrLength, os); - } - - /** - * Returns the parent directory of this {@link LocalPath}. - * - * <p>If this is called on an single directory for a relative path, this returns an empty relative - * path. If it's called on a root (like '/') or the empty string, it returns null. - */ - @Nullable - public LocalPath getParentDirectory() { - int lastSeparator = path.lastIndexOf(os.getSeparator()); - - // For absolute paths we need to specially handle when we hit root - // Relative paths can't hit this path as driveStrLength == 0 - if (driveStrLength > 0) { - if (lastSeparator < driveStrLength) { - if (path.length() > driveStrLength) { - String newPath = path.substring(0, driveStrLength); - return new LocalPath(newPath, driveStrLength, os); - } else { - return null; - } - } - } else { - if (lastSeparator == -1) { - if (!path.isEmpty()) { - return EMPTY; - } else { - return null; - } - } - } - String newPath = path.substring(0, lastSeparator); - return new LocalPath(newPath, driveStrLength, os); - } - - /** - * Returns the {@link LocalPath} relative to the base {@link LocalPath}. - * - * <p>For example, <code>LocalPath.create("foo/bar/wiz").relativeTo(LocalPath.create("foo")) - * </code> returns <code>LocalPath.create("bar/wiz")</code>. - * - * <p>If the {@link LocalPath} is not a child of the passed {@link LocalPath} an {@link - * IllegalArgumentException} is thrown. In particular, this will happen whenever the two {@link - * LocalPath} instances aren't both absolute or both relative. - */ - public LocalPath relativeTo(LocalPath base) { - Preconditions.checkNotNull(base); - Preconditions.checkArgument(os == base.os); - if (isAbsolute() != base.isAbsolute()) { - throw new IllegalArgumentException( - "Cannot relativize an absolute and a non-absolute path pair"); - } - String basePath = base.path; - if (!os.startsWith(path, basePath)) { - throw new IllegalArgumentException( - String.format("Path '%s' is not under '%s', cannot relativize", this, base)); - } - int bn = basePath.length(); - if (bn == 0) { - return this; - } - if (path.length() == bn) { - return EMPTY; - } - final int lastSlashIndex; - if (basePath.charAt(bn - 1) == '/') { - lastSlashIndex = bn - 1; - } else { - lastSlashIndex = bn; - } - if (path.charAt(lastSlashIndex) != '/') { - throw new IllegalArgumentException( - String.format("Path '%s' is not under '%s', cannot relativize", this, base)); - } - String newPath = path.substring(lastSlashIndex + 1); - return new LocalPath(newPath, 0 /* Always a relative path */, os); - } - - /** - * Splits a path into its constituent parts. The root is not included. This is an inefficient - * operation and should be avoided. - */ - public String[] split() { - String[] segments = path.split("/"); - if (driveStrLength > 0) { - // String#split("/") for some reason returns a zero-length array - // String#split("/hello") returns a 2-length array, so this makes little sense - if (segments.length == 0) { - return segments; - } - return Arrays.copyOfRange(segments, 1, segments.length); - } - return segments; - } - - /** - * Returns whether this path is an ancestor of another path. - * - * <p>A path is considered an ancestor of itself. - * - * <p>An absolute path can never be an ancestor of a relative path, and vice versa. - */ - public boolean startsWith(LocalPath other) { - Preconditions.checkNotNull(other); - Preconditions.checkArgument(os == other.os); - if (other.path.length() > path.length()) { - return false; - } - if (driveStrLength != other.driveStrLength) { - return false; - } - if (!os.startsWith(path, other.path)) { - return false; - } - return path.length() == other.path.length() - || other.path.length() == driveStrLength - || path.charAt(other.path.length()) == os.getSeparator(); - } - - public boolean isAbsolute() { - return driveStrLength > 0; - } - - @Override - public String toString() { - return path; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - return os.compare(this.path, ((LocalPath) o).path) == 0; - } - - @Override - public int hashCode() { - return os.hashPath(this.path); - } - - @Override - public int compareTo(LocalPath o) { - return os.compare(this.path, o.path); - } - - /** - * An interface class representing the differences in path style between different OSs. - * - * <p>Eg. case sensitivity, '/' mounts vs. 'C:/', etc. - */ - @VisibleForTesting - interface OsPathPolicy { - int NORMALIZED = 0; // Path is normalized - int NEEDS_NORMALIZE = 1; // Path requires normalization - - /** Returns required normalization level, passed to {@link #normalize}. */ - int needsToNormalize(String path); - - /** - * Normalizes the passed string according to the passed normalization level. - * - * @param normalizationLevel The normalizationLevel from {@link #needsToNormalize} - */ - String normalize(String path, int normalizationLevel); - - /** - * Returns the length of the mount, eg. 1 for unix '/', 3 for Windows 'C:/'. - * - * <p>If the path is relative, 0 is returned - */ - int getDriveStrLength(String path); - - /** Compares two path strings, using the given OS case sensitivity. */ - int compare(String s1, String s2); - - /** Computes the hash code for a path string. */ - int hashPath(String s); - - /** - * Returns whether the passed string starts with the given prefix, given the OS case - * sensitivity. - * - * <p>This is a pure string operation and doesn't need to worry about matching path segments. - */ - boolean startsWith(String path, String prefix); - - char getSeparator(); - - boolean isCaseSensitive(); - } - - @VisibleForTesting - static class UnixOsPathPolicy implements OsPathPolicy { - - @Override - public int needsToNormalize(String path) { - int n = path.length(); - int dotCount = 0; - char prevChar = 0; - for (int i = 0; i < n; i++) { - char c = path.charAt(i); - if (c == '/') { - if (prevChar == '/') { - return NEEDS_NORMALIZE; - } - if (dotCount == 1 || dotCount == 2) { - return NEEDS_NORMALIZE; - } - } - dotCount = c == '.' ? dotCount + 1 : 0; - prevChar = c; - } - if (prevChar == '/' || dotCount == 1 || dotCount == 2) { - return NEEDS_NORMALIZE; - } - return NORMALIZED; - } - - @Override - public String normalize(String path, int normalizationLevel) { - if (normalizationLevel == NORMALIZED) { - return path; - } - if (path.isEmpty()) { - return path; - } - boolean isAbsolute = path.charAt(0) == '/'; - String[] segments = path.split("/+"); - int segmentCount = removeRelativePaths(segments, isAbsolute ? 1 : 0); - StringBuilder sb = new StringBuilder(path.length()); - if (isAbsolute) { - sb.append('/'); - } - for (int i = 0; i < segmentCount; ++i) { - sb.append(segments[i]); - sb.append('/'); - } - if (segmentCount > 0) { - sb.deleteCharAt(sb.length() - 1); - } - return sb.toString(); - } - - @Override - public int getDriveStrLength(String path) { - if (path.length() == 0) { - return 0; - } - return (path.charAt(0) == '/') ? 1 : 0; - } - - @Override - public int compare(String s1, String s2) { - return s1.compareTo(s2); - } - - @Override - public int hashPath(String s) { - return s.hashCode(); - } - - @Override - public boolean startsWith(String path, String prefix) { - return path.startsWith(prefix); - } - - @Override - public char getSeparator() { - return '/'; - } - - @Override - public boolean isCaseSensitive() { - return true; - } - } - - /** Mac is a unix file system that is case insensitive. */ - @VisibleForTesting - static class MacOsPathPolicy extends UnixOsPathPolicy { - @Override - public int compare(String s1, String s2) { - return s1.compareToIgnoreCase(s2); - } - - @Override - public int hashPath(String s) { - return s.toLowerCase().hashCode(); - } - - @Override - public boolean isCaseSensitive() { - return false; - } - } - - @VisibleForTesting - static class WindowsOsPathPolicy implements OsPathPolicy { - - private static final int NEEDS_SHORT_PATH_NORMALIZATION = NEEDS_NORMALIZE + 1; - - // msys root, used to resolve paths from msys starting with "/" - private static final AtomicReference<String> UNIX_ROOT = new AtomicReference<>(null); - private final ShortPathResolver shortPathResolver; - - interface ShortPathResolver { - String resolveShortPath(String path); - } - - static class DefaultShortPathResolver implements ShortPathResolver { - @Override - public String resolveShortPath(String path) { - try { - return WindowsFileOperations.getLongPath(path); - } catch (IOException e) { - return path; - } - } - } - - WindowsOsPathPolicy() { - this(new DefaultShortPathResolver()); - } - - WindowsOsPathPolicy(ShortPathResolver shortPathResolver) { - this.shortPathResolver = shortPathResolver; - } - - @Override - public int needsToNormalize(String path) { - int n = path.length(); - int normalizationLevel = 0; - // Check for unix path - if (n > 0 && path.charAt(0) == '/') { - normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE); - } - int dotCount = 0; - char prevChar = 0; - int segmentBeginIndex = 0; // The start index of the current path index - boolean segmentHasShortPathChar = false; // Triggers more expensive short path regex test - for (int i = 0; i < n; i++) { - char c = path.charAt(i); - if (c == '/' || c == '\\') { - if (c == '\\') { - normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE); - } - // No need to check for '\' here because that already causes normalization - if (prevChar == '/') { - normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE); - } - if (dotCount == 1 || dotCount == 2) { - normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE); - } - if (segmentHasShortPathChar) { - if (WindowsShortPath.isShortPath(path.substring(segmentBeginIndex, i))) { - normalizationLevel = Math.max(normalizationLevel, NEEDS_SHORT_PATH_NORMALIZATION); - } - } - segmentBeginIndex = i + 1; - segmentHasShortPathChar = false; - } else if (c == '~') { - // This path segment might be a Windows short path segment - segmentHasShortPathChar = true; - } - dotCount = c == '.' ? dotCount + 1 : 0; - prevChar = c; - } - if (prevChar == '/' || dotCount == 1 || dotCount == 2) { - normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE); - } - return normalizationLevel; - } - - @Override - public String normalize(String path, int normalizationLevel) { - if (normalizationLevel == NORMALIZED) { - return path; - } - if (normalizationLevel == NEEDS_SHORT_PATH_NORMALIZATION) { - String resolvedPath = shortPathResolver.resolveShortPath(path); - if (resolvedPath != null) { - path = resolvedPath; - } - } - String[] segments = path.split("[\\\\/]+"); - int driveStrLength = getDriveStrLength(path); - boolean isAbsolute = driveStrLength > 0; - int segmentSkipCount = isAbsolute ? 1 : 0; - - StringBuilder sb = new StringBuilder(path.length()); - if (isAbsolute) { - char driveLetter = path.charAt(0); - sb.append(Character.toUpperCase(driveLetter)); - sb.append(":/"); - } - // unix path support - if (!path.isEmpty() && path.charAt(0) == '/') { - if (path.length() == 2 || (path.length() > 2 && path.charAt(2) == '/')) { - sb.append(Character.toUpperCase(path.charAt(1))); - sb.append(":/"); - segmentSkipCount = 2; - } else { - String unixRoot = getUnixRoot(); - sb.append(unixRoot); - } - } - int segmentCount = removeRelativePaths(segments, segmentSkipCount); - for (int i = 0; i < segmentCount; ++i) { - sb.append(segments[i]); - sb.append('/'); - } - if (segmentCount > 0) { - sb.deleteCharAt(sb.length() - 1); - } - return sb.toString(); - } - - @Override - public int getDriveStrLength(String path) { - int n = path.length(); - if (n < 3) { - return 0; - } - if (isDriveLetter(path.charAt(0)) - && path.charAt(1) == ':' - && (path.charAt(2) == '/' || path.charAt(2) == '\\')) { - return 3; - } - return 0; - } - - private static boolean isDriveLetter(char c) { - return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z')); - } - - @Override - public int compare(String s1, String s2) { - // Windows is case-insensitive - return s1.compareToIgnoreCase(s2); - } - - @Override - public int hashPath(String s) { - // Windows is case-insensitive - return s.toLowerCase().hashCode(); - } - - @Override - public boolean startsWith(String path, String prefix) { - int pathn = path.length(); - int prefixn = prefix.length(); - if (pathn < prefixn) { - return false; - } - for (int i = 0; i < prefixn; ++i) { - if (Character.toLowerCase(path.charAt(i)) != Character.toLowerCase(prefix.charAt(i))) { - return false; - } - } - return true; - } - - @Override - public char getSeparator() { - return '/'; - } - - @Override - public boolean isCaseSensitive() { - return false; - } - - private String getUnixRoot() { - String value = UNIX_ROOT.get(); - if (value == null) { - String jvmFlag = "bazel.windows_unix_root"; - value = determineUnixRoot(jvmFlag); - if (value == null) { - throw new IllegalStateException( - String.format( - "\"%1$s\" JVM flag is not set. Use the --host_jvm_args flag or export the " - + "BAZEL_SH environment variable. For example " - + "\"--host_jvm_args=-D%1$s=c:/tools/msys64\" or " - + "\"set BAZEL_SH=c:/tools/msys64/usr/bin/bash.exe\".", - jvmFlag)); - } - if (getDriveStrLength(value) != 3) { - throw new IllegalStateException( - String.format("\"%s\" must be an absolute path, got: \"%s\"", jvmFlag, value)); - } - value = value.replace('\\', '/'); - if (value.length() > 3 && value.endsWith("/")) { - value = value.substring(0, value.length() - 1); - } - UNIX_ROOT.set(value); - } - return value; - } - - private String determineUnixRoot(String jvmArgName) { - // Get the path from a JVM flag, if specified. - String path = System.getProperty(jvmArgName); - if (path == null) { - return null; - } - path = path.trim(); - if (path.isEmpty()) { - return null; - } - return path; - } - } - - private static OsPathPolicy createFilePathOs() { - switch (OS.getCurrent()) { - case LINUX: - case FREEBSD: - case UNKNOWN: - return new UnixOsPathPolicy(); - case DARWIN: - return new MacOsPathPolicy(); - case WINDOWS: - return new WindowsOsPathPolicy(); - default: - throw new AssertionError("Not covering all OSs"); - } - } - - /** - * Normalizes any '.' and '..' in-place in the segment array by shifting other segments to the - * front. Returns the remaining number of items. - */ - private static int removeRelativePaths(String[] segments, int starti) { - int segmentCount = 0; - int shift = starti; - for (int i = starti; i < segments.length; ++i) { - String segment = segments[i]; - switch (segment) { - case ".": - // Just discard it - ++shift; - break; - case "..": - if (segmentCount > 0 && !segments[segmentCount - 1].equals("..")) { - // Remove the last segment, if there is one and it is not "..". This - // means that the resulting path can still contain ".." - // segments at the beginning. - segmentCount--; - shift += 2; - break; - } - // Fall through - default: - ++segmentCount; - if (shift > 0) { - segments[i - shift] = segments[i]; - } - break; - } - } - return segmentCount; - } -} diff --git a/src/main/java/com/google/devtools/build/lib/vfs/OsPathPolicy.java b/src/main/java/com/google/devtools/build/lib/vfs/OsPathPolicy.java new file mode 100644 index 0000000000..6b0380f8c8 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/vfs/OsPathPolicy.java @@ -0,0 +1,144 @@ +// Copyright 2017 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.devtools.build.lib.util.OS; + +/** + * An interface class representing the differences in path style between different OSs. + * + * <p>Eg. case sensitivity, '/' mounts vs. 'C:/', etc. + */ +public interface OsPathPolicy { + int NORMALIZED = 0; // Path is normalized + int NEEDS_NORMALIZE = 1; // Path requires normalization + + /** Returns required normalization level, passed to {@link #normalize}. */ + int needsToNormalize(String path); + + /** + * Returns the required normalization level if an already normalized string is concatenated with + * another normalized path fragment. + * + * <p>This method may be faster than {@link #needsToNormalize(String)}. + */ + int needsToNormalizeSuffix(String normalizedSuffix); + + /** + * Normalizes the passed string according to the passed normalization level. + * + * @param normalizationLevel The normalizationLevel from {@link #needsToNormalize} + */ + String normalize(String path, int normalizationLevel); + + /** + * Returns the length of the mount, eg. 1 for unix '/', 3 for Windows 'C:/'. + * + * <p>If the path is relative, 0 is returned + */ + int getDriveStrLength(String path); + + /** Compares two path strings, using the given OS case sensitivity. */ + int compare(String s1, String s2); + + /** Compares two characters, using the given OS case sensitivity. */ + int compare(char c1, char c2); + + /** Tests two path strings for equality, using the given OS case sensitivity. */ + boolean equals(String s1, String s2); + + /** Computes the hash code for a path string. */ + int hash(String s); + + /** + * Returns whether the passed string starts with the given prefix, given the OS case sensitivity. + * + * <p>This is a pure string operation and doesn't need to worry about matching path segments. + */ + boolean startsWith(String path, String prefix); + + /** + * Returns whether the passed string ends with the given prefix, given the OS case sensitivity. + * + * <p>This is a pure string operation and doesn't need to worry about matching path segments. + */ + boolean endsWith(String path, String suffix); + + /** Returns the separator used for normalized paths. */ + char getSeparator(); + + /** Returns whether the unnormalized character c is a separator. */ + boolean isSeparator(char c); + + boolean isCaseSensitive(); + + static OsPathPolicy getFilePathOs() { + switch (OS.getCurrent()) { + case LINUX: + case FREEBSD: + case UNKNOWN: + return UnixOsPathPolicy.INSTANCE; + case DARWIN: + // NOTE: We *should* return a path policy that is case insensitive, + // but we currently don't handle this + return UnixOsPathPolicy.INSTANCE; + case WINDOWS: + return WindowsOsPathPolicy.INSTANCE; + default: + throw new AssertionError("Not covering all OSs"); + } + } + + /** Utilities for implementations of {@link OsPathPolicy}. */ + class Utils { + /** + * Normalizes any '.' and '..' in-place in the segment array by shifting other segments to the + * front. Returns the remaining number of items. + */ + static int removeRelativePaths(String[] segments, int starti, boolean isAbsolute) { + int segmentCount = 0; + int shift = starti; + int n = segments.length; + for (int i = starti; i < n; ++i) { + String segment = segments[i]; + switch (segment) { + case ".": + ++shift; + break; + case "..": + if (segmentCount > 0 && !segments[segmentCount - 1].equals("..")) { + // Remove the last segment, if there is one and it is not "..". This + // means that the resulting path can still contain ".." + // segments at the beginning. + segmentCount--; + shift += 2; + break; + } else if (isAbsolute) { + // If this is absolute, then just pop it the ".." off and remain at root + ++shift; + break; + } + // Fall through + default: + ++segmentCount; + if (shift > 0) { + segments[i - shift] = segments[i]; + } + break; + } + } + return segmentCount; + } + } +} diff --git a/src/main/java/com/google/devtools/build/lib/vfs/Path.java b/src/main/java/com/google/devtools/build/lib/vfs/Path.java index 9549d25bab..075c47bd07 100644 --- a/src/main/java/com/google/devtools/build/lib/vfs/Path.java +++ b/src/main/java/com/google/devtools/build/lib/vfs/Path.java @@ -14,14 +14,14 @@ package com.google.devtools.build.lib.vfs; import com.google.common.base.Preconditions; -import com.google.common.base.Predicate; import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe; import com.google.devtools.build.lib.skyframe.serialization.InjectingObjectCodec; +import com.google.devtools.build.lib.skyframe.serialization.ObjectCodec; import com.google.devtools.build.lib.skyframe.serialization.SerializationException; +import com.google.devtools.build.lib.skyframe.serialization.strings.StringCodecs; import com.google.devtools.build.lib.skylarkinterface.SkylarkPrintable; import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter; import com.google.devtools.build.lib.util.FileType; -import com.google.devtools.build.lib.util.StringCanonicalizer; import com.google.devtools.build.lib.vfs.FileSystem.HashFunction; import com.google.protobuf.CodedInputStream; import com.google.protobuf.CodedOutputStream; @@ -33,28 +33,29 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.Serializable; -import java.lang.ref.Reference; -import java.lang.ref.ReferenceQueue; -import java.lang.ref.WeakReference; -import java.net.URI; -import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; -import java.util.IdentityHashMap; -import java.util.Objects; +import javax.annotation.Nullable; /** - * Instances of this class represent pathnames, forming a tree structure to implement sharing of - * common prefixes (parent directory names). A node in these trees is something like foo, bar, .., - * ., or /. If the instance is not a root path, it will have a parent path. A path can also have - * children, which are indexed by name in a map. + * A local file path representing a file on the host machine. You should use this when you want to + * access local files via the file system. + * + * <p>Paths are always absolute. + * + * <p>Strings are normalized with '.' and '..' removed and resolved (if possible), any multiple + * slashes ('/') removed, and any trailing slash also removed. Windows drive letters are uppercased. + * The current implementation does not touch the incoming path string unless the string actually + * needs to be normalized. * * <p>There is some limited support for Windows-style paths. Most importantly, drive identifiers in - * front of a path (c:/abc) are supported. However, Windows-style backslash separators - * (C:\\foo\\bar) and drive-relative paths ("C:foo") are explicitly not supported, same with - * advanced features like \\\\network\\paths and \\\\?\\unc\\paths. + * front of a path (c:/abc) are supported and such paths are correctly recognized as absolute, as + * are paths with backslash separators (C:\\foo\\bar). However, advanced Windows-style features like + * \\\\network\\paths and \\\\?\\unc\\paths are not supported. We are currently using forward + * slashes ('/') even on Windows, so backslashes '\' get converted to forward slashes during + * normalization. * - * <p>{@link FileSystem} implementations maintain pointers into this graph. + * <p>Mac and Windows file paths are case insensitive. Case is preserved. */ @ThreadSafe public class Path @@ -62,34 +63,6 @@ public class Path public static final InjectingObjectCodec<Path, FileSystemProvider> CODEC = new PathCodecWithInjectedFileSystem(); - /** Filesystem-specific factory for {@link Path} objects. */ - public static interface PathFactory { - /** - * Creates the root of all paths used by a filesystem. - * - * <p>All other paths are instantiated via {@link Path#createChildPath(String)} which calls - * {@link #createChildPath(Path, String)}. - * - * <p>Beware: this is called during the FileSystem constructor which may occur before subclasses - * are completely initialized. - */ - Path createRootPath(FileSystem filesystem); - - /** - * Create a child path of the given parent. - * - * <p>All {@link Path} objects are instantiated via this method, with the sole exception of the - * filesystem root, which is created by {@link #createRootPath(FileSystem)}. - */ - Path createChildPath(Path parent, String childName); - - /** - * Makes the proper invocation of {@link FileSystem#getCachedChildPathInternal}, doing - * filesystem-specific logic if necessary. - */ - Path getCachedChildPathInternal(Path path, String childName); - } - private static FileSystem fileSystemForSerialization; /** @@ -106,398 +79,279 @@ public class Path return fileSystemForSerialization; } - // These are basically final, but can't be marked as such in order to support serialization. - private FileSystem fileSystem; - private String name; - private Path parent; - private int depth; - private int hashCode; - - private static final ReferenceQueue<Path> REFERENCE_QUEUE = new ReferenceQueue<>(); + private static final OsPathPolicy OS = OsPathPolicy.getFilePathOs(); + private static final char SEPARATOR = OS.getSeparator(); - private static class PathWeakReferenceForCleanup extends WeakReference<Path> { - final Path parent; - final String baseName; + private String path; + private int driveStrLength; // 1 on Unix, 3 on Windows + private FileSystem fileSystem; - PathWeakReferenceForCleanup(Path referent, ReferenceQueue<Path> referenceQueue) { - super(referent, referenceQueue); - parent = referent.getParentDirectory(); - baseName = referent.getBaseName(); - } + /** Creates a local path that is specific to the host OS. */ + static Path create(String path, FileSystem fileSystem) { + Preconditions.checkNotNull(path); + int normalizationLevel = OS.needsToNormalize(path); + String normalizedPath = OS.normalize(path, normalizationLevel); + return createAlreadyNormalized(normalizedPath, fileSystem); } - private static final Thread PATH_CHILD_CACHE_CLEANUP_THREAD = new Thread("Path cache cleanup") { - @Override - public void run() { - while (true) { - try { - PathWeakReferenceForCleanup ref = (PathWeakReferenceForCleanup) REFERENCE_QUEUE.remove(); - Path parent = ref.parent; - synchronized (parent) { - // It's possible that since this reference was enqueued for deletion, the Path was - // recreated with a new entry in the map. We definitely shouldn't delete that entry, so - // check to make sure they're the same. - Reference<Path> currentRef = ref.parent.children.get(ref.baseName); - if (currentRef == ref) { - ref.parent.children.remove(ref.baseName); - } - } - } catch (InterruptedException e) { - // Ignored. - } - } - } - }; - - static { - PATH_CHILD_CACHE_CLEANUP_THREAD.setDaemon(true); - PATH_CHILD_CACHE_CLEANUP_THREAD.start(); + static Path createAlreadyNormalized(String path, FileSystem fileSystem) { + int driveStrLength = OS.getDriveStrLength(path); + return createAlreadyNormalized(path, driveStrLength, fileSystem); } - /** - * A mapping from a child file name to the {@link Path} representing it. - * - * <p>File names must be a single path segment. The strings must be - * canonical. We use IdentityHashMap instead of HashMap for reasons of space - * efficiency: instances are smaller by a single word. Also, since all path - * segments are interned, the universe of Paths holds a minimal number of - * references to strings. (It's doubtful that there's any time gain from use - * of an IdentityHashMap, since the time saved by avoiding string equality - * tests during hash lookups is probably equal to the time spent eagerly - * interning strings, unless the collision rate is high.) - * - * <p>The Paths are stored as weak references to ensure that a live - * Path for a directory does not hold a strong reference to all of its - * descendants, which would prevent collection of paths we never intend to - * use again. Stale references in the map must be treated as absent. - * - * <p>A Path may be recycled once there is no Path that refers to it or - * to one of its descendants. This means that any data stored in the - * Path instance by one of its subclasses must be recoverable: it's ok to - * store data in Paths as an optimization, but there must be another - * source for that data in case the Path is recycled. - * - * <p>We intentionally avoid using the existing library classes for reasons of - * space efficiency: while ConcurrentHashMap would reduce our locking - * overhead, and ReferenceMap would simplify the code a little, both of those - * classes have much higher per-instance overheads than IdentityHashMap. - * - * <p>The Path object must be synchronized while children is being - * accessed. - */ - private volatile IdentityHashMap<String, Reference<Path>> children; - - /** - * Create a path instance. - * - * <p>Should only be called by {@link PathFactory#createChildPath(Path, String)}. - * - * @param name the name of this path; it must be canonicalized with {@link - * StringCanonicalizer#intern} - * @param parent this path's parent - */ - protected Path(FileSystem fileSystem, String name, Path parent) { - this.fileSystem = fileSystem; - this.name = name; - this.parent = parent; - this.depth = parent == null ? 0 : parent.depth + 1; - - // No need to include the drive letter in the hash code, because it's derived from the parent - // and/or the name. - if (fileSystem == null || fileSystem.isFilePathCaseSensitive()) { - this.hashCode = Objects.hash(parent, name); - } else { - this.hashCode = Objects.hash(parent, name.toLowerCase()); - } + static Path createAlreadyNormalized(String path, int driveStrLength, FileSystem fileSystem) { + return new Path(path, driveStrLength, fileSystem); } - /** - * Create the root path. - * - * <p>Should only be called by {@link PathFactory#createRootPath(FileSystem)}. - */ - protected Path(FileSystem fileSystem) { - this(fileSystem, StringCanonicalizer.intern("/"), null); + /** This method expects path to already be normalized. */ + private Path(String path, int driveStrLength, FileSystem fileSystem) { + Preconditions.checkArgument(driveStrLength > 0, "Paths must be absolute: '%s'", path); + this.path = Preconditions.checkNotNull(path); + this.driveStrLength = driveStrLength; + this.fileSystem = fileSystem; } - private void writeObject(ObjectOutputStream out) throws IOException { - Preconditions.checkState( - fileSystem == fileSystemForSerialization, "%s %s", fileSystem, fileSystemForSerialization); - out.writeUTF(getPathString()); + public String getPathString() { + return path; } - private void readObject(ObjectInputStream in) throws IOException { - fileSystem = fileSystemForSerialization; - String p = in.readUTF(); - PathFragment pf = PathFragment.create(p); - PathFragment parentDir = pf.getParentDirectory(); - if (parentDir == null) { - this.name = "/"; - this.parent = null; - this.depth = 0; - } else { - this.name = pf.getBaseName(); - this.parent = fileSystem.getPath(parentDir); - this.depth = this.parent.depth + 1; - } - this.hashCode = Objects.hash(parent, name); - reinitializeAfterDeserialization(); + @Override + public String filePathForFileTypeMatcher() { + return path; } /** - * Returns the filesystem instance to which this path belongs. + * Returns the name of the leaf file or directory. + * + * <p>If called on a {@link Path} instance for a mount name (eg. '/' or 'C:/'), the empty string + * is returned. */ - public FileSystem getFileSystem() { - return fileSystem; - } - - public boolean isRootDirectory() { - return parent == null; + public String getBaseName() { + int lastSeparator = path.lastIndexOf(SEPARATOR); + return lastSeparator < driveStrLength + ? path.substring(driveStrLength) + : path.substring(lastSeparator + 1); } - protected Path createChildPath(String childName) { - return fileSystem.getPathFactory().createChildPath(this, childName); + /** Synonymous with {@link Path#getRelative(String)}. */ + public Path getChild(String child) { + FileSystemUtils.checkBaseName(child); + return getRelative(child); } /** - * Reinitializes this object after deserialization. - * - * <p>Derived classes should use this hook to initialize additional state. + * Returns a {@link Path} instance representing the relative path between this {@link Path} and + * the given path. */ - protected void reinitializeAfterDeserialization() {} - - /** - * Returns true if {@code ancestorPath} may be an ancestor of {@code path}. - * - * <p>The return value may be a false positive, but it cannot be a false negative. This means that - * a true return value doesn't mean the ancestor candidate is really an ancestor, however a false - * return value means it's guaranteed that {@code ancestorCandidate} is not an ancestor of this - * path. - * - * <p>Subclasses may override this method with filesystem-specific logic, e.g. a Windows - * filesystem may return false if the ancestor path is on a different drive than this one, because - * it is then guaranteed that the ancestor candidate cannot be an ancestor of this path. - * - * @param ancestorCandidate the path that may or may not be an ancestor of this one - */ - protected boolean isMaybeRelativeTo(Path ancestorCandidate) { - return true; + public Path getRelative(PathFragment other) { + Preconditions.checkNotNull(other); + String otherStr = other.getPathString(); + // Fast-path: The path fragment is already normal, use cheaper normalization check + return getRelative(otherStr, other.getDriveStrLength(), OS.needsToNormalizeSuffix(otherStr)); } /** - * Returns true if this directory is top-level, i.e. it is its own parent. - * - * <p>When canonicalizing paths the ".." segment of a top-level directory always resolves to the - * directory itself. - * - * <p>On Unix, a top-level directory would be just the filesystem root ("/), on Windows it would - * be the filesystem root and the volume roots. + * Returns a {@link Path} instance representing the relative path between this {@link Path} and + * the given path. */ - protected boolean isTopLevelDirectory() { - return isRootDirectory(); + public Path getRelative(String other) { + Preconditions.checkNotNull(other); + return getRelative(other, OS.getDriveStrLength(other), OS.needsToNormalize(other)); } - /** - * Returns the child path named name, or creates such a path (and caches it) - * if it doesn't already exist. - */ - private Path getCachedChildPath(String childName) { - return fileSystem.getPathFactory().getCachedChildPathInternal(this, childName); + private Path getRelative(String other, int otherDriveStrLength, int normalizationLevel) { + if (other.isEmpty()) { + return this; + } + // This is an absolute path, simply return it + if (otherDriveStrLength > 0) { + String normalizedPath = OS.normalize(other, normalizationLevel); + return new Path(normalizedPath, otherDriveStrLength, fileSystem); + } + String newPath; + if (path.length() == driveStrLength) { + newPath = path + other; + } else { + newPath = path + '/' + other; + } + // Note that even if other came from a PathFragment instance we still might + // need to normalize the result if (for instance) other is a path that + // starts with '..' + newPath = OS.normalize(newPath, normalizationLevel); + return new Path(newPath, driveStrLength, fileSystem); } /** - * Internal method only intended to be called by {@link PathFactory#getCachedChildPathInternal}. + * Returns the parent directory of this {@link Path}. + * + * <p>If called on a root (like '/'), it returns null. */ - public static Path getCachedChildPathInternal(Path parent, String childName, boolean cacheable) { - // We get a canonical instance since 'children' is an IdentityHashMap. - childName = StringCanonicalizer.intern(childName); - if (!cacheable) { - // Non-cacheable children won't show up in `children` so applyToChildren won't run for these. - return parent.createChildPath(childName); - } - - synchronized (parent) { - if (parent.children == null) { - // 66% of Paths have size == 1, 80% <= 2 - parent.children = new IdentityHashMap<>(1); - } - Reference<Path> childRef = parent.children.get(childName); - Path child; - if (childRef == null || (child = childRef.get()) == null) { - child = parent.createChildPath(childName); - parent.children.put(childName, new PathWeakReferenceForCleanup(child, REFERENCE_QUEUE)); + @Nullable + public Path getParentDirectory() { + int lastSeparator = path.lastIndexOf(SEPARATOR); + if (lastSeparator < driveStrLength) { + if (path.length() > driveStrLength) { + String newPath = path.substring(0, driveStrLength); + return new Path(newPath, driveStrLength, fileSystem); + } else { + return null; } - return child; } + String newPath = path.substring(0, lastSeparator); + return new Path(newPath, driveStrLength, fileSystem); } /** - * Applies the specified function to each {@link Path} that is an existing direct - * descendant of this one. The Predicate is evaluated only for its - * side-effects. + * Returns the drive. * - * <p>This function exists to hide the "children" field, whose complex - * synchronization and identity requirements are too unsafe to be exposed to - * subclasses. For example, the "children" field must be synchronized for - * the duration of any iteration over it; it may be null; and references - * within it may be stale, and must be ignored. + * <p>On unix, this will return "/". On Windows it will return the drive letter, like "C:/". */ - protected synchronized void applyToChildren(Predicate<Path> function) { - if (children != null) { - for (Reference<Path> childRef : children.values()) { - Path child = childRef.get(); - if (child != null) { - function.apply(child); - } - } - } + public String getDriveStr() { + return path.substring(0, driveStrLength); } /** - * Returns whether this path is recursively "under" {@code prefix} - that is, - * whether {@code path} is a prefix of this path. + * Returns the {@link Path} relative to the base {@link Path}. * - * <p>This method returns {@code true} when called with this path itself. This - * method acts independently of the existence of files or folders. + * <p>For example, <code>Path.create("foo/bar/wiz").relativeTo(Path.create("foo")) + * </code> returns <code>Path.create("bar/wiz")</code>. * - * @param prefix a path which may or may not be a prefix of this path + * <p>If the {@link Path} is not a child of the passed {@link Path} an {@link + * IllegalArgumentException} is thrown. In particular, this will happen whenever the two {@link + * Path} instances aren't both absolute or both relative. */ - public boolean startsWith(Path prefix) { - Path n = this; - for (int i = 0, len = depth - prefix.depth; i < len; i++) { - n = n.getParentDirectory(); + public PathFragment relativeTo(Path base) { + Preconditions.checkNotNull(base); + checkSameFileSystem(base); + String basePath = base.path; + if (!OS.startsWith(path, basePath)) { + throw new IllegalArgumentException( + String.format("Path '%s' is not under '%s', cannot relativize", this, base)); + } + int bn = basePath.length(); + if (bn == 0) { + return PathFragment.createAlreadyNormalized(path, driveStrLength); + } + if (path.length() == bn) { + return PathFragment.EMPTY_FRAGMENT; + } + final int lastSlashIndex; + if (basePath.charAt(bn - 1) == '/') { + lastSlashIndex = bn - 1; + } else { + lastSlashIndex = bn; + } + if (path.charAt(lastSlashIndex) != '/') { + throw new IllegalArgumentException( + String.format("Path '%s' is not under '%s', cannot relativize", this, base)); } - return prefix.equals(n); + String newPath = path.substring(lastSlashIndex + 1); + return PathFragment.createAlreadyNormalized(newPath, 0); } /** - * Computes a string representation of this path, and writes it to the given string builder. Only - * called locally with a new instance. + * Returns whether this path is an ancestor of another path. + * + * <p>A path is considered an ancestor of itself. */ - protected void buildPathString(StringBuilder result) { - if (isRootDirectory()) { - result.append(PathFragment.ROOT_DIR); - } else { - parent.buildPathString(result); - if (!parent.isRootDirectory()) { - result.append(PathFragment.SEPARATOR_CHAR); - } - result.append(name); + public boolean startsWith(Path other) { + if (fileSystem != other.fileSystem) { + return false; } + return startsWith(other.path, other.driveStrLength); } /** - * Returns the path encoded as an {@link URI}. + * Returns whether this path is an ancestor of another path. * - * <p>This concrete implementation returns URIs with "file" as the scheme. - * For Example: - * - On Unix the path "/tmp/foo bar.txt" will be encoded as - * "file:///tmp/foo%20bar.txt". - * - On Windows the path "C:\Temp\Foo Bar.txt" will be encoded as - * "file:///C:/Temp/Foo%20Bar.txt" + * <p>A path is considered an ancestor of itself. * - * <p>Implementors extending this class for special filesystems will likely need to override - * this method. - * - * @throws URISyntaxException if the URI cannot be constructed. + * <p>An absolute path can never be an ancestor of a relative path fragment. */ - public URI toURI() { - String ps = getPathString(); - if (!ps.startsWith("/")) { - // On Windows URI's need to start with a '/'. i.e. C:\Foo\Bar would be file:///C:/Foo/Bar - ps = "/" + ps; - } - try { - return new URI("file", - // Needs to be "" instead of null, so that toString() will append "//" after the scheme. - // We need this for backwards compatibility reasons as some consumers of the BEP are - // broken. - "", - ps, null, null); - } catch (URISyntaxException e) { - throw new IllegalStateException(e); + public boolean startsWith(PathFragment other) { + if (!other.isAbsolute()) { + return false; } + String otherPath = other.getPathString(); + return startsWith(otherPath, OS.getDriveStrLength(otherPath)); } - /** - * Returns the path as a string. - */ - public String getPathString() { - // Profile driven optimization: - // Preallocate a size determined by the depth, so that - // we do not have to expand the capacity of the StringBuilder - StringBuilder builder = new StringBuilder(depth * 20); - buildPathString(builder); - return builder.toString(); + private boolean startsWith(String otherPath, int otherDriveStrLength) { + Preconditions.checkNotNull(otherPath); + if (otherPath.length() > path.length()) { + return false; + } + if (driveStrLength != otherDriveStrLength) { + return false; + } + if (!OS.startsWith(path, otherPath)) { + return false; + } + return path.length() == otherPath.length() // Handle equal paths + || otherPath.length() == driveStrLength // Handle (eg.) 'C:/foo' starts with 'C:/' + // Handle 'true' ancestors, eg. "foo/bar" starts with "foo", but does not start with "fo" + || path.charAt(otherPath.length()) == SEPARATOR; } - @Override - public void repr(SkylarkPrinter printer) { - printer.append(getPathString()); + public FileSystem getFileSystem() { + return fileSystem; } - @Override - public String filePathForFileTypeMatcher() { - return name; + public PathFragment asFragment() { + return PathFragment.createAlreadyNormalized(path, driveStrLength); } - /** - * Returns the path as a string. - */ @Override public String toString() { - return getPathString(); + return path; } @Override - public int hashCode() { - return hashCode; - } - - @Override - public boolean equals(Object other) { - if (this == other) { + public boolean equals(Object o) { + if (this == o) { return true; } - if (!(other instanceof Path)) { + if (o == null || getClass() != o.getClass()) { return false; } - - Path otherPath = (Path) other; - if (hashCode != otherPath.hashCode) { + Path other = (Path) o; + if (fileSystem != other.fileSystem) { return false; } + return OS.equals(this.path, other.path); + } - if (!fileSystem.equals(otherPath.fileSystem)) { - return false; - } + @Override + public int hashCode() { + // Do not include file system for efficiency. + // In practice we never construct paths from different file systems. + return OS.hash(this.path); + } - if (fileSystem.isFilePathCaseSensitive()) { - return name.equals(otherPath.name) - && Objects.equals(parent, otherPath.parent); - } else { - return name.toLowerCase().equals(otherPath.name.toLowerCase()) - && Objects.equals(parent, otherPath.parent); + @Override + public int compareTo(Path o) { + // If they are on different file systems, the file system decides the ordering. + FileSystem otherFs = o.getFileSystem(); + if (!fileSystem.equals(otherFs)) { + int thisFileSystemHash = System.identityHashCode(fileSystem); + int otherFileSystemHash = System.identityHashCode(otherFs); + if (thisFileSystemHash < otherFileSystemHash) { + return -1; + } else if (thisFileSystemHash > otherFileSystemHash) { + return 1; + } } + return OS.compare(this.path, o.path); } - /** - * Returns a string of debugging information associated with this path. - * The results are unspecified and MUST NOT be interpreted programmatically. - */ - protected String toDebugString() { - return ""; + @Override + public void repr(SkylarkPrinter printer) { + printer.append(path); } - /** - * Returns a path representing the parent directory of this path, - * or null iff this Path represents the root of the filesystem. - * - * <p>Note: This method normalises ".." and "." path segments by string - * processing, not by directory lookups. - */ - public Path getParentDirectory() { - return parent; + @Override + public void str(SkylarkPrinter printer) { + repr(printer); } /** Returns true iff this path denotes an existing file of any kind. Follows symbolic links. */ @@ -662,157 +516,6 @@ public class Path } /** - * Returns the last segment of this path, or "/" for the root directory. - */ - public String getBaseName() { - return name; - } - - /** - * Interprets the name of a path segment relative to the current path and - * returns the result. - * - * <p>This is a purely syntactic operation, i.e. it does no I/O, it does not - * validate the existence of any path, nor resolve symbolic links. If 'prefix' - * is not canonical, then a 'name' of '..' will be interpreted incorrectly. - * - * @precondition segment contains no slashes. - */ - private Path getCanonicalPath(String segment) { - if (segment.equals(".") || segment.isEmpty()) { - return this; // that's a noop - } else if (segment.equals("..")) { - // top-level directory's parent is root, when canonicalising: - return isTopLevelDirectory() ? this : parent; - } else { - return getCachedChildPath(segment); - } - } - - /** - * Returns the path formed by appending the single non-special segment - * "baseName" to this path. - * - * <p>You should almost always use {@link #getRelative} instead, which has - * the same performance characteristics if the given name is a valid base - * name, and which also works for '.', '..', and strings containing '/'. - * - * @throws IllegalArgumentException if {@code baseName} is not a valid base - * name according to {@link FileSystemUtils#checkBaseName} - */ - public Path getChild(String baseName) { - FileSystemUtils.checkBaseName(baseName); - return getCachedChildPath(baseName); - } - - protected Path getRootForRelativePathComputation(PathFragment suffix) { - return suffix.isAbsolute() ? fileSystem.getRootDirectory() : this; - } - - /** - * Returns the path formed by appending the relative or absolute path fragment - * {@code suffix} to this path. - * - * <p>If suffix is absolute, the current path will be ignored; otherwise, they - * will be combined. Up-level references ("..") cause the preceding path - * segment to be elided; this interpretation is only correct if the base path - * is canonical. - */ - public Path getRelative(PathFragment suffix) { - Path result = getRootForRelativePathComputation(suffix); - for (String segment : suffix.segments()) { - result = result.getCanonicalPath(segment); - } - return result; - } - - /** - * Returns the path formed by appending the relative or absolute string - * {@code path} to this path. - * - * <p>If the given path string is absolute, the current path will be ignored; - * otherwise, they will be combined. Up-level references ("..") cause the - * preceding path segment to be elided. - * - * <p>This is a purely syntactic operation, i.e. it does no I/O, it does not - * validate the existence of any path, nor resolve symbolic links. - */ - public Path getRelative(String path) { - // Fast path for valid base names. - if ((path.length() == 0) || (path.equals("."))) { - return this; - } else if (path.equals("..")) { - return isTopLevelDirectory() ? this : parent; - } else if (PathFragment.containsSeparator(path)) { - return getRelative(PathFragment.create(path)); - } else { - return getCachedChildPath(path); - } - } - - protected final String[] getSegments() { - String[] resultSegments = new String[depth]; - Path currentPath = this; - for (int pos = depth - 1; pos >= 0; pos--) { - resultSegments[pos] = currentPath.getBaseName(); - currentPath = currentPath.getParentDirectory(); - } - return resultSegments; - } - - /** Returns an absolute PathFragment representing this path. */ - public PathFragment asFragment() { - return PathFragment.createAlreadyInterned('\0', true, getSegments()); - } - - /** - * Returns a relative path fragment to this path, relative to {@code - * ancestorDirectory}. {@code ancestorDirectory} must be on the same - * filesystem as this path. (Currently, both this path and "ancestorDirectory" - * must be absolute, though this restriction could be loosened.) - * <p> - * <code>x.relativeTo(z) == y</code> implies - * <code>z.getRelative(y.getPathString()) == x</code>. - * <p> - * For example, <code>"/foo/bar/wiz".relativeTo("/foo")</code> returns - * <code>"bar/wiz"</code>. - * - * @throws IllegalArgumentException if this path is not beneath {@code - * ancestorDirectory} or if they are not part of the same filesystem - */ - public PathFragment relativeTo(Path ancestorPath) { - checkSameFilesystem(ancestorPath); - - if (isMaybeRelativeTo(ancestorPath)) { - // Fast path: when otherPath is the ancestor of this path - int resultSegmentCount = depth - ancestorPath.depth; - if (resultSegmentCount >= 0) { - String[] resultSegments = new String[resultSegmentCount]; - Path currentPath = this; - for (int pos = resultSegmentCount - 1; pos >= 0; pos--) { - resultSegments[pos] = currentPath.getBaseName(); - currentPath = currentPath.getParentDirectory(); - } - if (ancestorPath.equals(currentPath)) { - return PathFragment.createAlreadyInterned('\0', false, resultSegments); - } - } - } - - throw new IllegalArgumentException("Path " + this + " is not beneath " + ancestorPath); - } - - /** - * Checks that "this" and "that" are paths on the same filesystem. - */ - protected void checkSameFilesystem(Path that) { - if (this.fileSystem != that.fileSystem) { - throw new IllegalArgumentException("Files are on different filesystems: " - + this + ", " + that); - } - } - - /** * Returns an output stream to the file denoted by the current path, creating it and truncating it * if necessary. The stream is opened for writing. * @@ -868,7 +571,7 @@ public class Path * @throws IOException if the creation of the symbolic link was unsuccessful for any reason */ public void createSymbolicLink(Path target) throws IOException { - checkSameFilesystem(target); + checkSameFileSystem(target); fileSystem.createSymbolicLink(this, target.asFragment()); } @@ -943,7 +646,7 @@ public class Path * @throws IOException if the rename failed for any reason */ public void renameTo(Path target) throws IOException { - checkSameFilesystem(target); + checkSameFileSystem(target); fileSystem.renameTo(this, target); } @@ -1183,69 +886,28 @@ public class Path fileSystem.prefetchPackageAsync(this, maxDirs); } - /** - * Compare Paths of the same file system using their PathFragments. - * - * <p>Paths from different filesystems will be compared using the identity - * hash code of their respective filesystems. - */ - @Override - public int compareTo(Path o) { - // Fast-path. - if (equals(o)) { - return 0; + private void checkSameFileSystem(Path that) { + if (this.fileSystem != that.fileSystem) { + throw new IllegalArgumentException( + "Files are on different filesystems: " + this + ", " + that); } + } - // If they are on different file systems, the file system decides the ordering. - FileSystem otherFs = o.getFileSystem(); - if (!fileSystem.equals(otherFs)) { - int thisFileSystemHash = System.identityHashCode(fileSystem); - int otherFileSystemHash = System.identityHashCode(otherFs); - if (thisFileSystemHash < otherFileSystemHash) { - return -1; - } else if (thisFileSystemHash > otherFileSystemHash) { - return 1; - } else { - // TODO(bazel-team): Add a name to every file system to be used here. - return 0; - } - } + private void writeObject(ObjectOutputStream out) throws IOException { + Preconditions.checkState( + fileSystem == fileSystemForSerialization, "%s %s", fileSystem, fileSystemForSerialization); + out.writeUTF(path); + } - // Equal file system, but different paths, because of the canonicalization. - // We expect to often compare Paths that are very similar, for example for files in the same - // directory. This can be done efficiently by going up segment by segment until we get the - // identical path (canonicalization again), and then just compare the immediate child segments. - // Overall this is much faster than creating PathFragment instances, and comparing those, which - // requires us to always go up to the top-level directory and copy all segments into a new - // string array. - // This was previously showing up as a hotspot in a profile of globbing a large directory. - Path a = this; - Path b = o; - int maxDepth = Math.min(a.depth, b.depth); - while (a.depth > maxDepth) { - a = a.getParentDirectory(); - } - while (b.depth > maxDepth) { - b = b.getParentDirectory(); - } - // One is the child of the other. - if (a.equals(b)) { - // If a is the same as this, this.depth must be less than o.depth. - return equals(a) ? -1 : 1; - } - Path previousa; - Path previousb; - do { - previousa = a; - previousb = b; - a = a.getParentDirectory(); - b = b.getParentDirectory(); - } while (!a.equals(b)); // This has to happen eventually. - return previousa.name.compareTo(previousb.name); + private void readObject(ObjectInputStream in) throws IOException { + path = in.readUTF(); + fileSystem = fileSystemForSerialization; + driveStrLength = OS.getDriveStrLength(path); } private static class PathCodecWithInjectedFileSystem implements InjectingObjectCodec<Path, FileSystemProvider> { + private final ObjectCodec<String> stringCodec = StringCodecs.asciiOptimized(); @Override public Class<Path> getEncodedClass() { @@ -1256,14 +918,14 @@ public class Path public void serialize(FileSystemProvider fsProvider, Path path, CodedOutputStream codedOut) throws IOException, SerializationException { Preconditions.checkArgument(path.getFileSystem() == fsProvider.getFileSystem()); - PathFragment.CODEC.serialize(path.asFragment(), codedOut); + stringCodec.serialize(path.getPathString(), codedOut); } @Override public Path deserialize(FileSystemProvider fsProvider, CodedInputStream codedIn) throws IOException, SerializationException { - PathFragment pathFragment = PathFragment.CODEC.deserialize(codedIn); - return fsProvider.getFileSystem().getPath(pathFragment); + return Path.createAlreadyNormalized( + stringCodec.deserialize(codedIn), fsProvider.getFileSystem()); } } } diff --git a/src/main/java/com/google/devtools/build/lib/vfs/PathCodec.java b/src/main/java/com/google/devtools/build/lib/vfs/PathCodec.java index ee0ef364e3..c7644ed960 100644 --- a/src/main/java/com/google/devtools/build/lib/vfs/PathCodec.java +++ b/src/main/java/com/google/devtools/build/lib/vfs/PathCodec.java @@ -17,6 +17,7 @@ package com.google.devtools.build.lib.vfs; import com.google.common.base.Preconditions; import com.google.devtools.build.lib.skyframe.serialization.ObjectCodec; import com.google.devtools.build.lib.skyframe.serialization.SerializationException; +import com.google.devtools.build.lib.skyframe.serialization.strings.StringCodecs; import com.google.protobuf.CodedInputStream; import com.google.protobuf.CodedOutputStream; import java.io.IOException; @@ -24,6 +25,7 @@ import java.io.IOException; /** Custom serialization for {@link Path}s. */ public class PathCodec implements ObjectCodec<Path> { + private final ObjectCodec<String> stringCodec = StringCodecs.asciiOptimized(); private final FileSystem fileSystem; /** Create an instance for serializing and deserializing {@link Path}s on {@code fileSystem}. */ @@ -41,15 +43,15 @@ public class PathCodec implements ObjectCodec<Path> { throws IOException, SerializationException { Preconditions.checkState( path.getFileSystem() == fileSystem, - "Path's FileSystem (%s) did not match the configured FileSystem (%s)", + "Path's FileSystem (%s) did not match the configured FileSystem (%s) for path (%s)", path.getFileSystem(), - fileSystem); - PathFragment.CODEC.serialize(path.asFragment(), codedOut); + fileSystem, + path); + stringCodec.serialize(path.getPathString(), codedOut); } @Override public Path deserialize(CodedInputStream codedIn) throws IOException, SerializationException { - PathFragment pathFragment = PathFragment.CODEC.deserialize(codedIn); - return fileSystem.getPath(pathFragment); + return fileSystem.getPath(stringCodec.deserialize(codedIn)); } } diff --git a/src/main/java/com/google/devtools/build/lib/vfs/PathFragment.java b/src/main/java/com/google/devtools/build/lib/vfs/PathFragment.java index 572042a7d7..88ae732590 100644 --- a/src/main/java/com/google/devtools/build/lib/vfs/PathFragment.java +++ b/src/main/java/com/google/devtools/build/lib/vfs/PathFragment.java @@ -1,4 +1,4 @@ -// Copyright 2014 The Bazel Authors. All rights reserved. +// Copyright 2017 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. @@ -16,630 +16,470 @@ package com.google.devtools.build.lib.vfs; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.devtools.build.lib.analysis.actions.CommandLineItem; -import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable; -import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe; import com.google.devtools.build.lib.skyframe.serialization.ObjectCodec; import com.google.devtools.build.lib.skyframe.serialization.SerializationException; import com.google.devtools.build.lib.skyframe.serialization.strings.StringCodecs; import com.google.devtools.build.lib.skylarkinterface.SkylarkPrintable; import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter; import com.google.devtools.build.lib.util.FileType; -import com.google.devtools.build.lib.util.OS; -import com.google.devtools.build.lib.util.StringCanonicalizer; import com.google.protobuf.CodedInputStream; import com.google.protobuf.CodedOutputStream; -import java.io.File; import java.io.IOException; -import java.io.InvalidObjectException; -import java.io.ObjectInputStream; import java.io.Serializable; -import java.util.Arrays; -import java.util.List; import java.util.Set; +import javax.annotation.Nullable; /** - * This class represents an immutable filesystem path, which may be absolute or relative. The path - * is maintained as a simple ordered list of path segment strings. + * A path segment representing a path fragment using the host machine's path style. That is; If you + * are running on a Unix machine, the path style will be unix, on Windows it is the windows path + * style. * - * <p>This class is independent from other VFS classes, especially anything requiring native code. - * It is safe to use in places that need simple segmented string path functionality. + * <p>Path fragments are either absolute or relative. + * + * <p>Strings are normalized with '.' and '..' removed and resolved (if possible), any multiple + * slashes ('/') removed, and any trailing slash also removed. Windows drive letters are uppercased. + * The current implementation does not touch the incoming path string unless the string actually + * needs to be normalized. * * <p>There is some limited support for Windows-style paths. Most importantly, drive identifiers in * front of a path (c:/abc) are supported and such paths are correctly recognized as absolute, as * are paths with backslash separators (C:\\foo\\bar). However, advanced Windows-style features like - * \\\\network\\paths and \\\\?\\unc\\paths are not supported. + * \\\\network\\paths and \\\\?\\unc\\paths are not supported. We are currently using forward + * slashes ('/') even on Windows. + * + * <p>Mac and Windows path fragments are case insensitive. */ -@Immutable -@javax.annotation.concurrent.Immutable -@ThreadSafe -public abstract class PathFragment +public final class PathFragment implements Comparable<PathFragment>, Serializable, - SkylarkPrintable, FileType.HasFileType, + SkylarkPrintable, CommandLineItem { - private static final Helper HELPER = - OS.getCurrent() == OS.WINDOWS ? WindowsPathFragment.HELPER : UnixPathFragment.HELPER; - - public static final char SEPARATOR_CHAR = HELPER.getPrimarySeparatorChar(); + private static final OsPathPolicy OS = OsPathPolicy.getFilePathOs(); - public static final int INVALID_SEGMENT = -1; - - public static final String ROOT_DIR = "/"; - - /** An empty path fragment. */ public static final PathFragment EMPTY_FRAGMENT = create(""); + public static final char SEPARATOR_CHAR = OS.getSeparator(); + public static final int INVALID_SEGMENT = -1; - /** The path fragment representing the root directory. */ - public static final PathFragment ROOT_FRAGMENT = create(ROOT_DIR); + private final String path; + private final int driveStrLength; // 0 for relative paths, 1 on Unix, 3 on Windows public static final ObjectCodec<PathFragment> CODEC = new PathFragmentCodec(); - /** - * A helper object for manipulating the various internal {@link PathFragment} implementations. - * - * <p>There will be exactly one {@link Helper} instance used to manipulate all the {@link - * PathFragment} instances (see {@link PathFragment#HELPER}). All of the various {@link Helper} - * and {@link PathFragment} implementations may assume this property. - */ - protected abstract static class Helper { - /** - * Returns whether the two given arrays of segments have the same length and should be - * considered have logically equal contents. - */ - protected final boolean segmentsEqual(String[] segments1, String[] segments2) { - return segments1.length == segments2.length - && segmentsEqual(segments1.length, segments1, 0, segments2); - } - - /** - * Returns whether the {@code length} segments in {@code segments1}, starting at {@code offset1} - * should be considered to be logically equal to the first {@code length} segments in {@code - * segments2}. - */ - abstract boolean segmentsEqual(int length, String[] segments1, int offset1, String[] segments2); - - /** Returns the comparison result of two {@link PathFragment} instances. */ - protected abstract int compare(PathFragment pathFragment1, PathFragment pathFragment2); - - /** Returns a fresh {@link PathFragment} instance from the given path string. */ - abstract PathFragment create(String path); - /** - * Returns a fresh {@link PathFragment} instance from the given information, taking ownership of - * {@code segments} and assuming the {@link String}s within have already been interned. - */ - abstract PathFragment createAlreadyInterned( - char driveLetter, boolean isAbsolute, String[] segments); - - /** Returns whether {@code c} is a path separator. */ - abstract boolean isSeparator(char c); - /** Returns the primary path separator. */ - abstract char getPrimarySeparatorChar(); - /** Return whether the given {@code path} contains a path separator. */ - abstract boolean containsSeparatorChar(String path); - - /** - * Splits the given {@code toSegment} into path segments, starting at the given {@code offset}. - */ - protected final String[] segment(String toSegment, int offset) { - int length = toSegment.length(); - - // We make two passes through the array of characters: count & alloc, - // because simply using ArrayList was a bottleneck showing up during profiling. - int seg = 0; - int start = offset; - for (int i = offset; i < length; i++) { - if (isSeparator(toSegment.charAt(i))) { - if (i > start) { // to skip repeated separators - seg++; - } - start = i + 1; - } - } - if (start < length) { - seg++; - } - String[] result = new String[seg]; - seg = 0; - start = offset; - for (int i = offset; i < length; i++) { - if (isSeparator(toSegment.charAt(i))) { - if (i > start) { // to skip repeated separators - result[seg] = StringCanonicalizer.intern(toSegment.substring(start, i)); - seg++; - } - start = i + 1; - } - } - if (start < length) { - result[seg] = StringCanonicalizer.intern(toSegment.substring(start, length)); - } - return result; - } - } - - /** Lower-level API. Create a PathFragment, interning segments. */ - public static PathFragment create(char driveLetter, boolean isAbsolute, String[] segments) { - String[] internedSegments = new String[segments.length]; - for (int i = 0; i < segments.length; i++) { - internedSegments[i] = StringCanonicalizer.intern(segments[i]); - } - return createAlreadyInterned(driveLetter, isAbsolute, internedSegments); - } - - /** Same as {@link #create(char, boolean, String[])}, except for {@link List}s of segments. */ - public static PathFragment create(char driveLetter, boolean isAbsolute, List<String> segments) { - String[] internedSegments = new String[segments.size()]; - for (int i = 0; i < segments.size(); i++) { - internedSegments[i] = StringCanonicalizer.intern(segments.get(i)); - } - return createAlreadyInterned(driveLetter, isAbsolute, internedSegments); - } - - /** - * Construct a PathFragment from a java.io.File, which is an absolute or - * relative UNIX path. Does not support Windows-style Files. - */ - public static PathFragment create(File path) { - return HELPER.create(path.getPath()); - } - - /** - * Construct a PathFragment from a string, which is an absolute or relative UNIX or Windows path. - */ + /** Creates a new normalized path fragment. */ public static PathFragment create(String path) { - return HELPER.create(path); + int normalizationLevel = OS.needsToNormalize(path); + String normalizedPath = + normalizationLevel != OsPathPolicy.NORMALIZED + ? OS.normalize(path, normalizationLevel) + : path; + int driveStrLength = OS.getDriveStrLength(normalizedPath); + return new PathFragment(normalizedPath, driveStrLength); } /** - * Constructs a PathFragment, taking ownership of {@code segments} and assuming the {@link - * String}s within have already been interned. + * Creates a new path fragment, where the caller promises that the path is normalized. * - * <p>Package-private because it does not perform a defensive copy of the segments array. Used - * here in PathFragment, and by Path.asFragment() and Path.relativeTo(). + * <p>WARNING! Make sure the path fragment is in fact already normalized. The rest of the code + * assumes this is the case. */ - static PathFragment createAlreadyInterned( - char driveLetter, boolean isAbsolute, String[] segments) { - return HELPER.createAlreadyInterned(driveLetter, isAbsolute, segments); - } - - /** Returns whether the current {@code path} contains a path separator. */ - static boolean containsSeparator(String path) { - return HELPER.containsSeparatorChar(path); + public static PathFragment createAlreadyNormalized(String normalizedPath) { + int driveStrLength = OS.getDriveStrLength(normalizedPath); + return createAlreadyNormalized(normalizedPath, driveStrLength); } /** - * Construct a PathFragment from a sequence of other PathFragments. The new fragment will be - * absolute iff the first fragment was absolute. + * Creates a new path fragment, where the caller promises that the path is normalized. + * + * <p>Should only be used internally. */ - // TODO(bazel-team): Most usages of this method are wasteful from a garbage perspective. Refactor - // to something better. - public static PathFragment create(PathFragment first, PathFragment second, PathFragment... more) { - String[] segments = new String[sumLengths(first, second, more)]; - int offset = 0; - offset += addSegmentsTo(segments, offset, first); - offset += addSegmentsTo(segments, offset, second); - for (PathFragment fragment : more) { - offset += addSegmentsTo(segments, offset, fragment); - } - boolean isAbsolute = first.isAbsolute(); - char driveLetter = first.getDriveLetter(); - return HELPER.createAlreadyInterned(driveLetter, isAbsolute, segments); + static PathFragment createAlreadyNormalized(String normalizedPath, int driveStrLength) { + return new PathFragment(normalizedPath, driveStrLength); } - // Medium sized builds can easily hold millions of live PathFragments, so the per-instance size of - // PathFragment is a concern. - // - // We have two oop-sized fields (segments, path), and one 4-byte-sized one (hashCode). - // - // If Blaze is run on a jvm with -XX:+UseCompressedOops, each PathFragment instance is 24 bytes - // and so adding any additional field will increase the per-instance size to at least 32 bytes. - // - // If Blaze is run on a jvm with -XX:-UseCompressedOops, each PathFragment instance is 32 bytes - // and so adding any additional field will increase the per-instance size to at least 40 bytes. - // - // Therefore, do not add any additional fields unless you have considered the memory implications. - - // The individual path components. - // Does *not* include the Windows drive letter. - protected final String[] segments; - - // hashCode and path are lazily initialized but semantically immutable. - private int hashCode; - private String path; - - protected PathFragment(String[] segments) { - this.segments = segments; - } - - private static int addSegmentsTo(String[] segments, int offset, PathFragment fragment) { - int count = fragment.segmentCount(); - System.arraycopy(fragment.segments, 0, segments, offset, count); - return count; + /** This method expects path to already be normalized. */ + private PathFragment(String path, int driveStrLength) { + this.path = Preconditions.checkNotNull(path); + this.driveStrLength = driveStrLength; } - private static int sumLengths(PathFragment first, PathFragment second, PathFragment[] more) { - int total = first.segmentCount() + second.segmentCount(); - for (PathFragment fragment : more) { - total += fragment.segmentCount(); - } - return total; + public String getPathString() { + return path; } - protected Object writeReplace() { - return new PathFragmentSerializationProxy(toString()); + public boolean isEmpty() { + return path.isEmpty(); } - protected void readObject(ObjectInputStream stream) throws InvalidObjectException { - throw new InvalidObjectException("Serialization is allowed only by proxy"); + int getDriveStrLength() { + return driveStrLength; } /** - * Returns the path string using '/' as the name-separator character. Returns "" if the path - * is both relative and empty. + * If called on a {@link PathFragment} instance for a mount name (eg. '/' or 'C:/'), the empty + * string is returned. + * + * <p>This operation allocates a new string. */ - public String getPathString() { - // Double-checked locking works, even without volatile, because path is a String, according to: - // http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html - if (path == null) { - synchronized (this) { - if (path == null) { - path = StringCanonicalizer.intern(joinSegments(HELPER.getPrimarySeparatorChar())); - } - } - } - return path; + public String getBaseName() { + int lastSeparator = path.lastIndexOf(SEPARATOR_CHAR); + return lastSeparator < driveStrLength + ? path.substring(driveStrLength) + : path.substring(lastSeparator + 1); } /** - * Returns "." if the path fragment is both relative and empty, or {@link - * #getPathString} otherwise. + * Returns a {@link PathFragment} instance representing the relative path between this {@link + * PathFragment} and the given {@link PathFragment}. + * + * <p>If the passed path is absolute it is returned untouched. This can be useful to resolve + * symlinks. */ - // TODO(bazel-team): Change getPathString to do this - this behavior makes more sense. - public String getSafePathString() { - return (!isAbsolute() && (segmentCount() == 0)) ? "." : getPathString(); + public PathFragment getRelative(PathFragment other) { + Preconditions.checkNotNull(other); + // Fast-path: The path fragment is already normal, use cheaper normalization check + String otherStr = other.path; + return getRelative(otherStr, other.getDriveStrLength(), OS.needsToNormalizeSuffix(otherStr)); } /** - * Returns the path string using '/' as the name-separator character, but do so in a way - * unambiguously recognizable as path. In other words, return "." for relative and empty paths, - * and prefix relative paths with one segment by "./". + * Returns a {@link PathFragment} instance representing the relative path between this {@link + * PathFragment} and the given path. * - * <p>In this way, a shell will always interpret such a string as path (absolute or relative to - * the working directory) and not as command to be searched for in the search path. + * <p>See {@link #getRelative(PathFragment)} for details. */ - public String getCallablePathString() { - if (isAbsolute()) { - return getPathString(); - } else if (segmentCount() == 0) { - return "."; - } else if (segmentCount() == 1) { - return "." + HELPER.getPrimarySeparatorChar() + getPathString(); - } else { - return getPathString(); - } + public PathFragment getRelative(String other) { + Preconditions.checkNotNull(other); + return getRelative(other, OS.getDriveStrLength(other), OS.needsToNormalize(other)); } - /** - * Throws {@link IllegalArgumentException} if {@code paths} contains any paths that - * are equal to {@code startingWithPath} or that are not beneath {@code startingWithPath}. - */ - public static void checkAllPathsAreUnder(Iterable<PathFragment> paths, - PathFragment startingWithPath) { - for (PathFragment path : paths) { - Preconditions.checkArgument( - !path.equals(startingWithPath) && path.startsWith(startingWithPath), - "%s is not beneath %s", path, startingWithPath); + private PathFragment getRelative(String other, int otherDriveStrLength, int normalizationLevel) { + if (path.isEmpty()) { + return create(other); } - } - - private String joinSegments(char separatorChar) { - if (segments.length == 0 && isAbsolute()) { - return windowsVolume() + ROOT_DIR; + if (other.isEmpty()) { + return this; } - - // Profile driven optimization: - // Preallocate a size determined by the number of segments, so that - // we do not have to expand the capacity of the StringBuilder. - // Heuristically, this estimate is right for about 99% of the time. - int estimateSize = - ((getDriveLetter() != '\0') ? 2 : 0) - + ((segments.length == 0) ? 0 : (segments.length + 1) * 20); - StringBuilder result = new StringBuilder(estimateSize); - if (isAbsolute()) { - // Only print the Windows volume label if the PathFragment is absolute. Do not print relative - // Windows paths like "C:foo/bar", it would break all kinds of things, e.g. glob(). - result.append(windowsVolume()); - } - boolean initialSegment = true; - for (String segment : segments) { - if (!initialSegment || isAbsolute()) { - result.append(separatorChar); - } - initialSegment = false; - result.append(segment); + // This is an absolute path, simply return it + if (otherDriveStrLength > 0) { + String normalizedPath = + normalizationLevel != OsPathPolicy.NORMALIZED + ? OS.normalize(other, normalizationLevel) + : other; + return new PathFragment(normalizedPath, otherDriveStrLength); + } + String newPath; + if (path.length() == driveStrLength) { + newPath = path + other; + } else { + newPath = path + '/' + other; } - return result.toString(); + newPath = + normalizationLevel != OsPathPolicy.NORMALIZED + ? OS.normalize(newPath, normalizationLevel) + : newPath; + return new PathFragment(newPath, driveStrLength); } - /** - * Return true iff none of the segments are either "." or "..". - */ - public boolean isNormalized() { - for (String segment : segments) { - if (segment.equals(".") || segment.equals("..")) { - return false; - } + public PathFragment getChild(String baseName) { + checkBaseName(baseName); + String newPath; + if (path.length() == driveStrLength) { + newPath = path + baseName; + } else { + newPath = path + '/' + baseName; } - return true; - } - - public static boolean isNormalized(String path) { - return PathFragment.create(path).isNormalized(); + return new PathFragment(newPath, driveStrLength); } /** - * Normalizes the path fragment: removes "." and ".." segments if possible - * (if there are too many ".." segments, the resulting PathFragment will still - * start with ".."). + * Returns the parent directory of this {@link PathFragment}. + * + * <p>If this is called on an single directory for a relative path, this returns an empty relative + * path. If it's called on a root (like '/') or the empty string, it returns null. */ - public PathFragment normalize() { - String[] scratchSegments = new String[segments.length]; - int segmentCount = 0; - - for (String segment : segments) { - switch (segment) { - case ".": - // Just discard it - break; - case "..": - if (segmentCount > 0 && !scratchSegments[segmentCount - 1].equals("..")) { - // Remove the last segment, if there is one and it is not "..". This - // means that the resulting PathFragment can still contain ".." - // segments at the beginning. - segmentCount--; - } else { - scratchSegments[segmentCount++] = segment; - } - break; - default: - scratchSegments[segmentCount++] = segment; + @Nullable + public PathFragment getParentDirectory() { + int lastSeparator = path.lastIndexOf(SEPARATOR_CHAR); + + // For absolute paths we need to specially handle when we hit root + // Relative paths can't hit this path as driveStrLength == 0 + if (driveStrLength > 0) { + if (lastSeparator < driveStrLength) { + if (path.length() > driveStrLength) { + String newPath = path.substring(0, driveStrLength); + return new PathFragment(newPath, driveStrLength); + } else { + return null; + } + } + } else { + if (lastSeparator == -1) { + if (!path.isEmpty()) { + return EMPTY_FRAGMENT; + } else { + return null; + } } } - - if (segmentCount == segments.length) { - // Optimization, no new PathFragment needs to be created. - return this; - } - - return HELPER.createAlreadyInterned( - getDriveLetter(), isAbsolute(), subarray(scratchSegments, 0, segmentCount)); + String newPath = path.substring(0, lastSeparator); + return new PathFragment(newPath, driveStrLength); } /** - * Returns the path formed by appending the relative or absolute path fragment - * {@code otherFragment} to this path. + * Returns the {@link PathFragment} relative to the base {@link PathFragment}. * - * <p>If {@code otherFragment} is absolute, the current path will be ignored; - * otherwise, they will be concatenated. This is a purely syntactic operation, - * with no path normalization or I/O performed. + * <p>For example, <code>FilePath.create("foo/bar/wiz").relativeTo(FilePath.create("foo"))</code> + * returns <code>"bar/wiz"</code>. + * + * <p>If the {@link PathFragment} is not a child of the passed {@link PathFragment} an {@link + * IllegalArgumentException} is thrown. In particular, this will happen whenever the two {@link + * PathFragment} instances aren't both absolute or both relative. */ - public PathFragment getRelative(PathFragment otherFragment) { - if (otherFragment == EMPTY_FRAGMENT) { + public PathFragment relativeTo(PathFragment base) { + Preconditions.checkNotNull(base); + if (isAbsolute() != base.isAbsolute()) { + throw new IllegalArgumentException( + "Cannot relativize an absolute and a non-absolute path pair"); + } + String basePath = base.path; + if (!OS.startsWith(path, basePath)) { + throw new IllegalArgumentException( + String.format("Path '%s' is not under '%s', cannot relativize", this, base)); + } + int bn = basePath.length(); + if (bn == 0) { return this; } - - if (otherFragment.isAbsolute()) { - char driveLetter = getDriveLetter(); - return driveLetter == '\0' || otherFragment.getDriveLetter() != '\0' - ? otherFragment - : createAlreadyInterned(driveLetter, true, otherFragment.segments); + if (path.length() == bn) { + return EMPTY_FRAGMENT; + } + final int lastSlashIndex; + if (basePath.charAt(bn - 1) == '/') { + lastSlashIndex = bn - 1; } else { - return create(this, otherFragment); + lastSlashIndex = bn; + } + if (path.charAt(lastSlashIndex) != '/') { + throw new IllegalArgumentException( + String.format("Path '%s' is not under '%s', cannot relativize", this, base)); } + String newPath = path.substring(lastSlashIndex + 1); + return new PathFragment(newPath, 0 /* Always a relative path */); } - /** - * Returns the path formed by appending the relative or absolute string - * {@code path} to this path. - * - * <p>If the given path string is absolute, the current path will be ignored; - * otherwise, they will be concatenated. This is a purely syntactic operation, - * with no path normalization or I/O performed. - */ - public PathFragment getRelative(String path) { - return getRelative(create(path)); + public PathFragment relativeTo(String base) { + return relativeTo(PathFragment.create(base)); } /** - * Returns the path formed by appending the single non-special segment "baseName" to this path. + * Returns whether this path is an ancestor of another path. * - * <p>You should almost always use {@link #getRelative} instead, which has the same performance - * characteristics if the given name is a valid base name, and which also works for '.', '..', and - * strings containing '/'. + * <p>If this == other, true is returned. * - * @throws IllegalArgumentException if {@code baseName} is not a valid base name according to - * {@link #checkBaseName} + * <p>An absolute path can never be an ancestor of a relative path, and vice versa. */ - public PathFragment getChild(String baseName) { - checkBaseName(baseName); - baseName = StringCanonicalizer.intern(baseName); - String[] newSegments = Arrays.copyOf(segments, segments.length + 1); - newSegments[newSegments.length - 1] = baseName; - return createAlreadyInterned(getDriveLetter(), isAbsolute(), newSegments); + public boolean startsWith(PathFragment other) { + Preconditions.checkNotNull(other); + if (other.path.length() > path.length()) { + return false; + } + if (driveStrLength != other.driveStrLength) { + return false; + } + if (!OS.startsWith(path, other.path)) { + return false; + } + return path.length() == other.path.length() + || other.path.length() == driveStrLength + || path.charAt(other.path.length()) == SEPARATOR_CHAR; } /** - * Returns the last segment of this path, or "" for the empty fragment. + * Returns true iff {@code suffix}, considered as a list of path segments, is relative and a + * suffix of {@code this}, or both are absolute and equal. + * + * <p>This is a reflexive, transitive, anti-symmetric relation (i.e. a partial order) */ - public String getBaseName() { - return (segments.length == 0) ? "" : segments[segments.length - 1]; + public boolean endsWith(PathFragment other) { + Preconditions.checkNotNull(other); + if (other.path.length() > path.length()) { + return false; + } + if (other.isAbsolute()) { + return this.equals(other); + } + if (!OS.endsWith(path, other.path)) { + return false; + } + return path.length() == other.path.length() + || other.path.length() == 0 + || path.charAt(path.length() - other.path.length() - 1) == SEPARATOR_CHAR; } - /** - * Returns the file extension of this path, excluding the period, or "" if there is no extension. - */ - public String getFileExtension() { - String baseName = getBaseName(); + public boolean isAbsolute() { + return driveStrLength > 0; + } - int lastIndex = baseName.lastIndexOf('.'); - if (lastIndex != -1) { - return baseName.substring(lastIndex + 1); - } + public static boolean isAbsolute(String path) { + return OS.getDriveStrLength(path) > 0; + } - return ""; + @Override + public String toString() { + return path; } - /** - * Returns a relative path fragment to this path, relative to - * {@code ancestorDirectory}. - * <p> - * <code>x.relativeTo(z) == y</code> implies - * <code>z.getRelative(y) == x</code>. - * <p> - * For example, <code>"foo/bar/wiz".relativeTo("foo")</code> - * returns <code>"bar/wiz"</code>. - */ - public PathFragment relativeTo(PathFragment ancestorDirectory) { - String[] ancestorSegments = ancestorDirectory.segments(); - int ancestorLength = ancestorSegments.length; + @Override + public void repr(SkylarkPrinter printer) { + printer.append(path); + } - if (isAbsolute() != ancestorDirectory.isAbsolute() || segments.length < ancestorLength) { - throw new IllegalArgumentException("PathFragment " + this - + " is not beneath " + ancestorDirectory); + @Override + public boolean equals(Object o) { + if (this == o) { + return true; } - - if (!HELPER.segmentsEqual(ancestorLength, segments, 0, ancestorSegments)) { - throw new IllegalArgumentException( - "PathFragment " + this + " is not beneath " + ancestorDirectory); + if (o == null || getClass() != o.getClass()) { + return false; } - - int length = segments.length - ancestorLength; - String[] resultSegments = subarray(segments, ancestorLength, length); - return createAlreadyInterned('\0', false, resultSegments); + return OS.equals(this.path, ((PathFragment) o).path); } - /** - * Returns a relative path fragment to this path, relative to {@code path}. - */ - public PathFragment relativeTo(String path) { - return relativeTo(create(path)); + @Override + public int hashCode() { + return OS.hash(this.path); } - /** - * Returns a new PathFragment formed by appending {@code newName} to the - * parent directory. Null is returned iff this method is called on a - * PathFragment with zero segments. If {@code newName} designates an absolute path, - * the value of {@code this} will be ignored and a PathFragment corresponding to - * {@code newName} will be returned. This behavior is consistent with the behavior of - * {@link #getRelative(String)}. - */ - public PathFragment replaceName(String newName) { - return segments.length == 0 ? null : getParentDirectory().getRelative(newName); + @Override + public int compareTo(PathFragment o) { + return OS.compare(this.path, o.path); } - /** - * Returns a path representing the parent directory of this path, - * or null iff this Path represents the root of the filesystem. - * - * <p>Note: This method DOES NOT normalize ".." and "." path segments. - */ - public PathFragment getParentDirectory() { - return segments.length == 0 ? null : subFragment(0, segments.length - 1); - } + //////////////////////////////////////////////////////////////////////// /** - * Returns true iff {@code prefix}, considered as a list of path segments, is - * a prefix of {@code this}, and that they are both relative or both - * absolute. + * Returns the number of segments in this path. * - * <p>This is a reflexive, transitive, anti-symmetric relation (i.e. a partial - * order) + * <p>This operation is O(N) on the length of the string. */ - public boolean startsWith(PathFragment prefix) { - if (isAbsolute() != prefix.isAbsolute() - || this.segments.length < prefix.segments.length - || (isAbsolute() && getDriveLetter() != prefix.getDriveLetter())) { - return false; + public int segmentCount() { + int n = path.length(); + int segmentCount = 0; + int i; + for (i = driveStrLength; i < n; ++i) { + if (path.charAt(i) == SEPARATOR_CHAR) { + ++segmentCount; + } } - return HELPER.segmentsEqual(prefix.segments.length, segments, 0, prefix.segments); - } - - /** - * Returns true iff {@code suffix}, considered as a list of path segments, is - * relative and a suffix of {@code this}, or both are absolute and equal. - * - * <p>This is a reflexive, transitive, anti-symmetric relation (i.e. a partial - * order) - */ - public boolean endsWith(PathFragment suffix) { - if ((suffix.isAbsolute() && !suffix.equals(this)) - || this.segments.length < suffix.segments.length) { - return false; + // Add last segment if one exists. + if (i > driveStrLength) { + ++segmentCount; } - int offset = this.segments.length - suffix.segments.length; - return HELPER.segmentsEqual(suffix.segments.length, segments, offset, suffix.segments); - } - - private static String[] subarray(String[] array, int start, int length) { - String[] subarray = new String[length]; - System.arraycopy(array, start, subarray, 0, length); - return subarray; + return segmentCount; } /** - * Returns a new path fragment that is a sub fragment of this one. - * The sub fragment begins at the specified <code>beginIndex</code> segment - * and ends at the segment at index <code>endIndex - 1</code>. Thus the number - * of segments in the new PathFragment is <code>endIndex - beginIndex</code>. + * Returns the specified segment of this path; index must be positive and less than numSegments(). * - * @param beginIndex the beginning index, inclusive. - * @param endIndex the ending index, exclusive. - * @return the specified sub fragment, never null. - * @exception IndexOutOfBoundsException if the - * <code>beginIndex</code> is negative, or - * <code>endIndex</code> is larger than the length of - * this <code>String</code> object, or - * <code>beginIndex</code> is larger than - * <code>endIndex</code>. + * <p>This operation is O(N) on the length of the string. */ - public PathFragment subFragment(int beginIndex, int endIndex) { - int count = segments.length; - if ((beginIndex < 0) || (beginIndex > endIndex) || (endIndex > count)) { - throw new IndexOutOfBoundsException(String.format("path: %s, beginIndex: %d endIndex: %d", - toString(), beginIndex, endIndex)); + public String getSegment(int index) { + int n = path.length(); + int segmentCount = 0; + int i; + for (i = driveStrLength; i < n && segmentCount < index; ++i) { + if (path.charAt(i) == SEPARATOR_CHAR) { + ++segmentCount; + } + } + int starti = i; + for (; i < n; ++i) { + if (path.charAt(i) == SEPARATOR_CHAR) { + break; + } } - boolean isAbsolute = (beginIndex == 0) && isAbsolute(); - return ((beginIndex == 0) && (endIndex == count)) - ? this - : createAlreadyInterned( - getDriveLetter(), isAbsolute, subarray(segments, beginIndex, endIndex - beginIndex)); + // Add last segment if one exists. + if (i > driveStrLength) { + ++segmentCount; + } + int endi = i; + if (index < 0 || index >= segmentCount) { + throw new IllegalArgumentException("Illegal segment index: " + index); + } + return path.substring(starti, endi); } /** * Returns a new path fragment that is a sub fragment of this one. The sub fragment begins at the - * specified <code>beginIndex</code> segment and contains the rest of the original path fragment. + * specified <code>beginIndex</code> segment and ends at the segment at index <code>endIndex - 1 + * </code>. Thus the number of segments in the new PathFragment is <code>endIndex - beginIndex + * </code>. + * + * <p>This operation is O(N) on the length of the string. * * @param beginIndex the beginning index, inclusive. + * @param endIndex the ending index, exclusive. * @return the specified sub fragment, never null. * @exception IndexOutOfBoundsException if the <code>beginIndex</code> is negative, or <code> * endIndex</code> is larger than the length of this <code>String</code> object, or <code> * beginIndex</code> is larger than <code>endIndex</code>. */ - public PathFragment subFragment(int beginIndex) { - return subFragment(beginIndex, segments.length); + public PathFragment subFragment(int beginIndex, int endIndex) { + if (beginIndex < 0 || beginIndex > endIndex) { + throw new IndexOutOfBoundsException( + String.format("path: %s, beginIndex: %d endIndex: %d", toString(), beginIndex, endIndex)); + } + return subFragmentImpl(beginIndex, endIndex); } - /** - * Returns true iff the path represented by this object is absolute. - * - * <p>True both for UNIX-style absolute paths ("/foo") and Windows-style ("C:/foo"). False for a - * Windows-style volume label ("C:") which is actually a relative path. - */ - public abstract boolean isAbsolute(); + public PathFragment subFragment(int beginIndex) { + if (beginIndex < 0) { + throw new IndexOutOfBoundsException( + String.format("path: %s, beginIndex: %d", toString(), beginIndex)); + } + return subFragmentImpl(beginIndex, -1); + } - public static boolean isAbsolute(String path) { - return PathFragment.create(path).isAbsolute(); + private PathFragment subFragmentImpl(int beginIndex, int endIndex) { + int n = path.length(); + int segmentIndex = 0; + int i; + for (i = driveStrLength; i < n && segmentIndex < beginIndex; ++i) { + if (path.charAt(i) == SEPARATOR_CHAR) { + ++segmentIndex; + } + } + int starti = i; + if (segmentIndex < endIndex) { + for (; i < n; ++i) { + if (path.charAt(i) == SEPARATOR_CHAR) { + ++segmentIndex; + if (segmentIndex == endIndex) { + break; + } + } + } + } else if (endIndex == -1) { + i = path.length(); + } + int endi = i; + // Add last segment if one exists for verification + if (i == n && i > driveStrLength) { + ++segmentIndex; + } + if (beginIndex > segmentIndex || endIndex > segmentIndex) { + throw new IndexOutOfBoundsException( + String.format("path: %s, beginIndex: %d endIndex: %d", toString(), beginIndex, endIndex)); + } + // If beginIndex is 0 we include the drive. Very odd semantics. + int driveStrLength = 0; + if (beginIndex == 0) { + starti = 0; + driveStrLength = this.driveStrLength; + endi = Math.max(endi, driveStrLength); + } + return new PathFragment(path.substring(starti, endi), driveStrLength); } /** @@ -647,62 +487,104 @@ public abstract class PathFragment * modified. */ String[] segments() { + int segmentCount = segmentCount(); + String[] segments = new String[segmentCount]; + int segmentIndex = 0; + int nexti = driveStrLength; + int n = path.length(); + for (int i = driveStrLength; i < n; ++i) { + if (path.charAt(i) == SEPARATOR_CHAR) { + segments[segmentIndex++] = path.substring(nexti, i); + nexti = i + 1; + } + } + // Add last segment if one exists. + if (nexti < n) { + segments[segmentIndex] = path.substring(nexti); + } return segments; } + /** + * Returns a list of the segments. + * + * <p>This operation is O(N) on the length of the string. + */ public ImmutableList<String> getSegments() { - return ImmutableList.copyOf(segments); + return ImmutableList.copyOf(segments()); } - public abstract String windowsVolume(); - - /** Return the drive letter or '\0' if not applicable. */ - // TODO(bazel-team): This doesn't need to pollute the PathFragment interface (ditto for - // windowsVolume). - public abstract char getDriveLetter(); + public int getFirstSegment(Set<String> values) { + String[] segments = segments(); + for (int i = 0; i < segments.length; ++i) { + if (values.contains(segments[i])) { + return i; + } + } + return INVALID_SEGMENT; + } - public boolean isEmpty() { - return segments.length == 0; + /** Returns the path string, or '.' if the path is empty. */ + public String getSafePathString() { + return !path.isEmpty() ? path : "."; } /** - * Returns the number of segments in this path. + * Returns the path string using '/' as the name-separator character, but do so in a way + * unambiguously recognizable as path. In other words, return "." for relative and empty paths, + * and prefix relative paths with one segment by "./". + * + * <p>In this way, a shell will always interpret such a string as path (absolute or relative to + * the working directory) and not as command to be searched for in the search path. */ - public int segmentCount() { - return segments.length; + public String getCallablePathString() { + if (isAbsolute()) { + return path; + } else if (path.isEmpty()) { + return "."; + } else if (path.indexOf(SEPARATOR_CHAR) == -1) { + return "." + SEPARATOR_CHAR + path; + } else { + return path; + } } /** - * Returns the specified segment of this path; index must be positive and - * less than numSegments(). + * Returns the file extension of this path, excluding the period, or "" if there is no extension. */ - public String getSegment(int index) { - return segments[index]; + public String getFileExtension() { + int n = path.length(); + for (int i = n - 1; i > driveStrLength; --i) { + char c = path.charAt(i); + if (c == '.') { + return path.substring(i + 1, n); + } else if (c == SEPARATOR_CHAR) { + break; + } + } + return ""; } /** - * Returns the index of the first segment which equals one of the input values - * or {@link PathFragment#INVALID_SEGMENT} if none of the segments match. + * Returns a new PathFragment formed by appending {@code newName} to the parent directory. Null is + * returned iff this method is called on a PathFragment with zero segments. If {@code newName} + * designates an absolute path, the value of {@code this} will be ignored and a PathFragment + * corresponding to {@code newName} will be returned. This behavior is consistent with the + * behavior of {@link #getRelative(String)}. */ - public int getFirstSegment(Set<String> values) { - for (int i = 0; i < segments.length; i++) { - if (values.contains(segments[i])) { - return i; - } - } - return INVALID_SEGMENT; + public PathFragment replaceName(String newName) { + PathFragment parent = getParentDirectory(); + return parent != null ? parent.getRelative(newName) : null; } /** - * Returns true iff this path contains uplevel references "..". + * Returns the drive for an absolute path fragment. + * + * <p>On unix, this will return "/". On Windows it will return the drive letter, like "C:/". */ - public boolean containsUplevelReferences() { - for (String segment : segments) { - if (segment.equals("..")) { - return true; - } - } - return false; + public String getDriveStr() { + Preconditions.checkArgument(isAbsolute()); + return path.substring(0, driveStrLength); } /** @@ -711,60 +593,129 @@ public abstract class PathFragment */ public PathFragment toRelative() { Preconditions.checkArgument(isAbsolute()); - return HELPER.createAlreadyInterned(getDriveLetter(), false, segments); + return new PathFragment(path.substring(driveStrLength), 0); } - @Override - public final int hashCode() { - // We use the hash code caching strategy employed by java.lang.String. There are three subtle - // things going on here: - // - // (1) We use a value of 0 to indicate that the hash code hasn't been computed and cached yet. - // Yes, this means that if the hash code is really 0 then we will "recompute" it each time. But - // this isn't a problem in practice since a hash code of 0 is rare. - // - // (2) Since we have no synchronization, multiple threads can race here thinking they are the - // first one to compute and cache the hash code. - // - // (3) Moreover, since 'hashCode' is non-volatile, the cached hash code value written from one - // thread may not be visible by another. Note that we don't need to worry about multiple - // inefficient reads of 'hashCode' on the same thread since it's non-volatile. - // - // All three of these issues are benign from a correctness perspective; in the end we have no - // overhead from synchronization, at the cost of potentially computing the hash code more than - // once. - if (hashCode == 0) { - hashCode = computeHashCode(); - } - return hashCode; - } - - protected abstract int computeHashCode(); - - @Override - public abstract boolean equals(Object other); + /** + * Returns true if this path contains uplevel references "..". + * + * <p>Since path fragments are normalized, this implies that the uplevel reference is at the start + * of the path fragment. + */ + public boolean containsUplevelReferences() { + // Path is normalized, so any ".." would have to be at the very start + return path.startsWith(".."); + } /** - * Compares two PathFragments using the lexicographical order. + * Returns true if the passed path contains uplevel references ".." or single-dot references "." + * + * <p>This is useful to check a string for normalization before constructing a PathFragment, since + * these are always normalized and will throw uplevel references away. */ - @Override - public int compareTo(PathFragment p2) { - return HELPER.compare(this, p2); + public static boolean isNormalized(String path) { + return isNormalizedImpl(path, true /* lookForSameLevelReferences */); } - @Override - public String toString() { - return getPathString(); + /** + * Returns true if the passed path contains uplevel references "..". + * + * <p>This is useful to check a string for '..' segments before constructing a PathFragment, since + * these are always normalized and will throw uplevel references away. + */ + public static boolean containsUplevelReferences(String path) { + return !isNormalizedImpl(path, false /* lookForSameLevelReferences */); + } + + private enum NormalizedImplState { + Base, /* No particular state, eg. an 'a' or 'L' character */ + Separator, /* We just saw a separator */ + Dot, /* We just saw a dot after a separator */ + DotDot, /* We just saw two dots after a separator */ + } + + private static boolean isNormalizedImpl(String path, boolean lookForSameLevelReferences) { + // Starting state is equivalent to having just seen a separator + NormalizedImplState state = NormalizedImplState.Separator; + int n = path.length(); + for (int i = 0; i < n; ++i) { + char c = path.charAt(i); + boolean isSeparator = OS.isSeparator(c); + switch (state) { + case Base: + if (isSeparator) { + state = NormalizedImplState.Separator; + } else { + state = NormalizedImplState.Base; + } + break; + case Separator: + if (isSeparator) { + state = NormalizedImplState.Separator; + } else if (c == '.') { + state = NormalizedImplState.Dot; + } else { + state = NormalizedImplState.Base; + } + break; + case Dot: + if (isSeparator) { + if (lookForSameLevelReferences) { + // "." segment found + return false; + } + state = NormalizedImplState.Separator; + } else if (c == '.') { + state = NormalizedImplState.DotDot; + } else { + state = NormalizedImplState.Base; + } + break; + case DotDot: + if (isSeparator) { + // ".." segment found + return false; + } else { + state = NormalizedImplState.Base; + } + break; + default: + throw new IllegalStateException("Unhandled state: " + state); + } + } + // The character just after the string is equivalent to a separator + switch (state) { + case Dot: + if (lookForSameLevelReferences) { + // "." segment found + return false; + } + break; + case DotDot: + return false; + default: + } + return true; } - @Override - public void repr(SkylarkPrinter printer) { - printer.append(getPathString()); + /** + * Throws {@link IllegalArgumentException} if {@code paths} contains any paths that are equal to + * {@code startingWithPath} or that are not beneath {@code startingWithPath}. + */ + public static void checkAllPathsAreUnder( + Iterable<PathFragment> paths, PathFragment startingWithPath) { + for (PathFragment path : paths) { + Preconditions.checkArgument( + !path.equals(startingWithPath) && path.startsWith(startingWithPath), + "%s is not beneath %s", + path, + startingWithPath); + } } @Override public String filePathForFileTypeMatcher() { - return getBaseName(); + return path; } @Override @@ -783,25 +734,13 @@ public abstract class PathFragment @Override public void serialize(PathFragment pathFragment, CodedOutputStream codedOut) throws IOException, SerializationException { - codedOut.writeInt32NoTag(pathFragment.getDriveLetter()); - codedOut.writeBoolNoTag(pathFragment.isAbsolute()); - codedOut.writeInt32NoTag(pathFragment.segmentCount()); - for (int i = 0; i < pathFragment.segmentCount(); i++) { - stringCodec.serialize(pathFragment.getSegment(i), codedOut); - } + stringCodec.serialize(pathFragment.getPathString(), codedOut); } @Override public PathFragment deserialize(CodedInputStream codedIn) throws IOException, SerializationException { - char driveLetter = (char) codedIn.readInt32(); - boolean isAbsolute = codedIn.readBool(); - int segmentCount = codedIn.readInt32(); - String[] segments = new String[segmentCount]; - for (int i = 0; i < segmentCount; i++) { - segments[i] = stringCodec.deserialize(codedIn); - } - return PathFragment.create(driveLetter, isAbsolute, segments); + return PathFragment.createAlreadyNormalized(stringCodec.deserialize(codedIn)); } } @@ -816,4 +755,8 @@ public abstract class PathFragment throw new IllegalArgumentException("baseName must not contain a slash: '" + baseName + "'"); } } + + private Object writeReplace() { + return new PathFragmentSerializationProxy(path); + } } diff --git a/src/main/java/com/google/devtools/build/lib/vfs/PathFragmentSerializationProxy.java b/src/main/java/com/google/devtools/build/lib/vfs/PathFragmentSerializationProxy.java index 39aefd1318..4ba66be7fc 100644 --- a/src/main/java/com/google/devtools/build/lib/vfs/PathFragmentSerializationProxy.java +++ b/src/main/java/com/google/devtools/build/lib/vfs/PathFragmentSerializationProxy.java @@ -17,7 +17,6 @@ import java.io.Externalizable; import java.io.IOException; import java.io.ObjectOutput; - /** * A helper proxy for serializing immutable {@link PathFragment} objects. */ @@ -44,7 +43,7 @@ public final class PathFragmentSerializationProxy implements Externalizable { } private Object readResolve() { - return PathFragment.create(pathFragmentString); + return PathFragment.createAlreadyNormalized(pathFragmentString); } } diff --git a/src/main/java/com/google/devtools/build/lib/vfs/PathTrie.java b/src/main/java/com/google/devtools/build/lib/vfs/PathTrie.java deleted file mode 100644 index fd783c5929..0000000000 --- a/src/main/java/com/google/devtools/build/lib/vfs/PathTrie.java +++ /dev/null @@ -1,82 +0,0 @@ -// 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.base.Preconditions; -import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible; -import java.util.HashMap; -import java.util.Map; - -/** - * A trie that operates on path segments. - * - * @param <T> the type of the values. - */ -@ThreadCompatible -public class PathTrie<T> { - @SuppressWarnings("unchecked") - private static class Node<T> { - private Node() { - children = new HashMap<>(); - } - - private T value; - private Map<String, Node<T>> children; - } - - private final Node<T> root; - - public PathTrie() { - root = new Node<T>(); - } - - /** - * Puts a value in the trie. - * - * @param key must be an absolute path. - */ - public void put(PathFragment key, T value) { - Preconditions.checkArgument(key.isAbsolute(), "PathTrie only accepts absolute paths as keys."); - Node<T> current = root; - for (String segment : key.getSegments()) { - current.children.putIfAbsent(segment, new Node<T>()); - current = current.children.get(segment); - } - current.value = value; - } - - /** - * Gets a value from the trie. If there is an entry with the same key, that will be returned, - * otherwise, the value corresponding to the key that matches the longest prefix of the input. - */ - public T get(PathFragment key) { - Node<T> current = root; - T lastValue = current.value; - - for (String segment : key.getSegments()) { - if (current.children.containsKey(segment)) { - current = current.children.get(segment); - // Track the values of increasing matching prefixes. - if (current.value != null) { - lastValue = current.value; - } - } else { - // We've reached the longest prefix, no further to go. - break; - } - } - - return lastValue; - } -} diff --git a/src/main/java/com/google/devtools/build/lib/vfs/RootedPath.java b/src/main/java/com/google/devtools/build/lib/vfs/RootedPath.java index 9182f10c0f..09ab3139a4 100644 --- a/src/main/java/com/google/devtools/build/lib/vfs/RootedPath.java +++ b/src/main/java/com/google/devtools/build/lib/vfs/RootedPath.java @@ -30,9 +30,8 @@ import java.util.Objects; * <p>Two {@link RootedPath}s are considered equal iff they have equal roots and equal relative * paths. * - * <p>TODO(bazel-team): refactor Artifact to use this instead of Root. TODO(bazel-team): use an - * opaque root representation so as to not expose the absolute path to clients via #asPath or - * #getRoot. + * <p>TODO(bazel-team): use an opaque root representation so as to not expose the absolute path to + * clients via #asPath or #getRoot. */ public class RootedPath implements Serializable { @@ -48,7 +47,7 @@ public class RootedPath implements Serializable { rootRelativePath, root); this.root = root; - this.rootRelativePath = rootRelativePath.normalize(); + this.rootRelativePath = rootRelativePath; this.path = root.getRelative(this.rootRelativePath); } diff --git a/src/main/java/com/google/devtools/build/lib/vfs/UnionFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/UnionFileSystem.java index d924e19d45..a929f1435d 100644 --- a/src/main/java/com/google/devtools/build/lib/vfs/UnionFileSystem.java +++ b/src/main/java/com/google/devtools/build/lib/vfs/UnionFileSystem.java @@ -15,12 +15,14 @@ package com.google.devtools.build.lib.vfs; import com.google.common.base.Preconditions; -import com.google.common.collect.Lists; import com.google.devtools.build.lib.concurrent.ThreadSafety; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; +import java.util.List; import java.util.Map; import javax.annotation.Nullable; @@ -42,12 +44,21 @@ import javax.annotation.Nullable; * are not currently supported. */ @ThreadSafety.ThreadSafe -public class UnionFileSystem extends FileSystem { +public final class UnionFileSystem extends FileSystem { - // Prefix trie index, allowing children to easily inherit prefix mappings - // of their parents. - // This does not currently handle unicode filenames. - private final PathTrie<FileSystem> pathDelegate; + private static class FileSystemAndPrefix { + final PathFragment prefix; + final FileSystem fileSystem; + + public FileSystemAndPrefix(PathFragment prefix, FileSystem fileSystem) { + this.prefix = prefix; + this.fileSystem = fileSystem; + } + } + + // List of file systems and their mappings, sorted by prefix length descending. + private final List<FileSystemAndPrefix> fileSystems; + private final FileSystem rootFileSystem; // True if the file path is case-sensitive on all the FileSystem // or False if they are all case-insensitive, otherwise error. @@ -65,11 +76,12 @@ public class UnionFileSystem extends FileSystem { Preconditions.checkNotNull(rootFileSystem); Preconditions.checkArgument(rootFileSystem != this, "Circular root filesystem."); Preconditions.checkArgument( - !prefixMapping.containsKey(PathFragment.EMPTY_FRAGMENT), + prefixMapping.keySet().stream().noneMatch(p -> p.getPathString().equals("/")), "Attempted to specify an explicit root prefix mapping; " + "please use the rootFileSystem argument instead."); - this.pathDelegate = new PathTrie<>(); + this.fileSystems = new ArrayList<>(); + this.rootFileSystem = rootFileSystem; this.isCaseSensitive = rootFileSystem.isFilePathCaseSensitive(); for (Map.Entry<PathFragment, FileSystem> prefix : prefixMapping.entrySet()) { @@ -80,9 +92,13 @@ public class UnionFileSystem extends FileSystem { PathFragment prefixPath = prefix.getKey(); // Extra slash prevents within-directory mappings, which Path can't handle. - pathDelegate.put(prefixPath, delegate); + fileSystems.add(new FileSystemAndPrefix(prefixPath, delegate)); } - pathDelegate.put(PathFragment.ROOT_FRAGMENT, rootFileSystem); + // Order by length descending. This ensures that more specific mapping takes precedence + // when we try to find the file system of a given path. + Comparator<FileSystemAndPrefix> comparator = + Comparator.comparing(f -> f.prefix.getPathString().length()); + fileSystems.sort(comparator.reversed()); } /** @@ -92,19 +108,24 @@ public class UnionFileSystem extends FileSystem { * @param path the {@link Path} to map to a filesystem * @throws IllegalArgumentException if no delegate exists for the path */ - protected FileSystem getDelegate(Path path) { + FileSystem getDelegate(Path path) { Preconditions.checkNotNull(path); - FileSystem immediateDelegate = pathDelegate.get(path.asFragment()); - - // Should never actually happen if the root delegate is present. - Preconditions.checkNotNull(immediateDelegate, "No delegate filesystem exists for %s", path); - return immediateDelegate; + FileSystem delegate = null; + // Linearly iterate over each mapped file system and find the one that handles this path. + // For small number of mappings, this will be more efficient than using a trie + for (FileSystemAndPrefix fileSystemAndPrefix : this.fileSystems) { + if (path.startsWith(fileSystemAndPrefix.prefix)) { + delegate = fileSystemAndPrefix.fileSystem; + break; + } + } + return delegate != null ? delegate : rootFileSystem; } // Associates the path with the root of the given delegate filesystem. // Necessary to avoid null pointer problems inside of the delegates. - protected Path adjustPath(Path path, FileSystem delegate) { - return delegate.getPath(path.asFragment()); + Path adjustPath(Path path, FileSystem delegate) { + return delegate.getPath(path.getPathString()); } /** @@ -344,12 +365,7 @@ public class UnionFileSystem extends FileSystem { path = internalResolveSymlink(path); FileSystem delegate = getDelegate(path); Path resolvedPath = adjustPath(path, delegate); - Collection<Path> entries = resolvedPath.getDirectoryEntries(); - Collection<String> result = Lists.newArrayListWithCapacity(entries.size()); - for (Path entry : entries) { - result.add(entry.getBaseName()); - } - return result; + return delegate.getDirectoryEntries(resolvedPath); } // No need for the more complex logic of getDirectoryEntries; it calls it implicitly. diff --git a/src/main/java/com/google/devtools/build/lib/vfs/UnixOsPathPolicy.java b/src/main/java/com/google/devtools/build/lib/vfs/UnixOsPathPolicy.java new file mode 100644 index 0000000000..8576643267 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/vfs/UnixOsPathPolicy.java @@ -0,0 +1,138 @@ +// Copyright 2017 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.common.base.Splitter; +import com.google.common.collect.Iterables; + +@VisibleForTesting +class UnixOsPathPolicy implements OsPathPolicy { + + static final UnixOsPathPolicy INSTANCE = new UnixOsPathPolicy(); + private static final Splitter PATH_SPLITTER = Splitter.onPattern("/+").omitEmptyStrings(); + + @Override + public int needsToNormalize(String path) { + int n = path.length(); + int dotCount = 0; + char prevChar = 0; + for (int i = 0; i < n; i++) { + char c = path.charAt(i); + if (c == '\\') { + return NEEDS_NORMALIZE; + } + if (c == '/') { + if (prevChar == '/') { + return NEEDS_NORMALIZE; + } + if (dotCount == 1 || dotCount == 2) { + return NEEDS_NORMALIZE; + } + } + dotCount = c == '.' ? dotCount + 1 : 0; + prevChar = c; + } + if (prevChar == '/' || dotCount == 1 || dotCount == 2) { + return NEEDS_NORMALIZE; + } + return NORMALIZED; + } + + @Override + public int needsToNormalizeSuffix(String normalizedSuffix) { + // We know that the string is normalized + // In this case only suffixes starting with ".." may cause + // normalization once concatenated with other strings + return normalizedSuffix.startsWith("..") ? NEEDS_NORMALIZE : NORMALIZED; + } + + @Override + public String normalize(String path, int normalizationLevel) { + if (normalizationLevel == NORMALIZED) { + return path; + } + if (path.isEmpty()) { + return path; + } + boolean isAbsolute = path.charAt(0) == '/'; + StringBuilder sb = new StringBuilder(path.length()); + if (isAbsolute) { + sb.append('/'); + } + String[] segments = Iterables.toArray(PATH_SPLITTER.splitToList(path), String.class); + int segmentCount = Utils.removeRelativePaths(segments, 0, isAbsolute); + for (int i = 0; i < segmentCount; ++i) { + sb.append(segments[i]); + sb.append('/'); + } + if (segmentCount > 0) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + + @Override + public int getDriveStrLength(String path) { + if (path.length() == 0) { + return 0; + } + return (path.charAt(0) == '/') ? 1 : 0; + } + + @Override + public int compare(String s1, String s2) { + return s1.compareTo(s2); + } + + @Override + public int compare(char c1, char c2) { + return Character.compare(c1, c2); + } + + @Override + public boolean equals(String s1, String s2) { + return s1.equals(s2); + } + + @Override + public int hash(String s) { + return s.hashCode(); + } + + @Override + public boolean startsWith(String path, String prefix) { + return path.startsWith(prefix); + } + + @Override + public boolean endsWith(String path, String suffix) { + return path.endsWith(suffix); + } + + @Override + public char getSeparator() { + return '/'; + } + + @Override + public boolean isSeparator(char c) { + return c == '/'; + } + + @Override + public boolean isCaseSensitive() { + return true; + } +} diff --git a/src/main/java/com/google/devtools/build/lib/vfs/UnixPathFragment.java b/src/main/java/com/google/devtools/build/lib/vfs/UnixPathFragment.java deleted file mode 100644 index b581d4375a..0000000000 --- a/src/main/java/com/google/devtools/build/lib/vfs/UnixPathFragment.java +++ /dev/null @@ -1,220 +0,0 @@ -// Copyright 2017 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.base.Preconditions; -import java.io.InvalidObjectException; -import java.io.ObjectInputStream; - -/** - * Abstract base class for {@link PathFragment} instances that will be allocated when Blaze is run - * on a non-Windows platform. - */ -abstract class UnixPathFragment extends PathFragment { - static final Helper HELPER = new Helper(); - /** - * We have two concrete subclasses with zero per-instance additional memory overhead. Do not add - * any fields. See the comment on memory use in PathFragment for details on the current - * per-instance memory usage. - */ - - protected UnixPathFragment(String[] segments) { - super(segments); - } - - @Override - protected int computeHashCode() { - int h = 0; - for (String segment : segments) { - int segmentHash = segment.hashCode(); - h = h * 31 + segmentHash; - } - return h; - } - - @Override - public String windowsVolume() { - return ""; - } - - @Override - public char getDriveLetter() { - return '\0'; - } - - private static class Helper extends PathFragment.Helper { - private static final char SEPARATOR_CHAR = '/'; - - @Override - PathFragment create(String path) { - boolean isAbsolute = path.length() > 0 && isSeparator(path.charAt(0)); - return isAbsolute - ? new AbsoluteUnixPathFragment(segment(path, 1)) - : new RelativeUnixPathFragment(segment(path, 0)); - } - - @Override - PathFragment createAlreadyInterned(char driveLetter, boolean isAbsolute, String[] segments) { - Preconditions.checkState(driveLetter == '\0', driveLetter); - return isAbsolute - ? new AbsoluteUnixPathFragment(segments) - : new RelativeUnixPathFragment(segments); - } - - @Override - char getPrimarySeparatorChar() { - return SEPARATOR_CHAR; - } - - @Override - boolean isSeparator(char c) { - return c == SEPARATOR_CHAR; - } - - @Override - boolean containsSeparatorChar(String path) { - return path.indexOf(SEPARATOR_CHAR) != -1; - } - - @Override - boolean segmentsEqual(int length, String[] segments1, int offset1, String[] segments2) { - if ((segments1.length - offset1) < length || segments2.length < length) { - return false; - } - for (int i = 0; i < length; ++i) { - String seg1 = segments1[i + offset1]; - String seg2 = segments2[i]; - if ((seg1 == null) != (seg2 == null)) { - return false; - } - if (seg1 == null) { - continue; - } - if (!seg1.equals(seg2)) { - return false; - } - } - return true; - } - - @Override - protected int compare(PathFragment pathFragment1, PathFragment pathFragment2) { - if (pathFragment1.isAbsolute() != pathFragment2.isAbsolute()) { - return pathFragment1.isAbsolute() ? -1 : 1; - } - String[] segments1 = pathFragment1.segments(); - String[] segments2 = pathFragment2.segments(); - int len1 = segments1.length; - int len2 = segments2.length; - int n = Math.min(len1, len2); - for (int i = 0; i < n; i++) { - String seg1 = segments1[i]; - String seg2 = segments2[i]; - int cmp = seg1.compareTo(seg2); - if (cmp != 0) { - return cmp; - } - } - return len1 - len2; - } - } - - private static final class AbsoluteUnixPathFragment extends UnixPathFragment { - private AbsoluteUnixPathFragment(String[] segments) { - super(segments); - } - - @Override - public boolean isAbsolute() { - return true; - } - - @Override - protected int computeHashCode() { - int h = Boolean.TRUE.hashCode(); - h = h * 31 + super.computeHashCode(); - return h; - } - - @Override - public boolean equals(Object other) { - if (!(other instanceof AbsoluteUnixPathFragment)) { - return false; - } - if (this == other) { - return true; - } - AbsoluteUnixPathFragment otherAbsoluteUnixPathFragment = (AbsoluteUnixPathFragment) other; - return HELPER.segmentsEqual(this.segments, otherAbsoluteUnixPathFragment.segments); - } - - // Java serialization looks for the presence of this method in the concrete class. It is not - // inherited from the parent class. - @Override - protected Object writeReplace() { - return super.writeReplace(); - } - - // Java serialization looks for the presence of this method in the concrete class. It is not - // inherited from the parent class. - @Override - protected void readObject(ObjectInputStream stream) throws InvalidObjectException { - super.readObject(stream); - } - } - - private static final class RelativeUnixPathFragment extends UnixPathFragment { - private RelativeUnixPathFragment(String[] segments) { - super(segments); - } - - @Override - public boolean isAbsolute() { - return false; - } - - @Override - protected int computeHashCode() { - int h = Boolean.FALSE.hashCode(); - h = h * 31 + super.computeHashCode(); - return h; - } - - @Override - public boolean equals(Object other) { - if (!(other instanceof RelativeUnixPathFragment)) { - return false; - } - if (this == other) { - return true; - } - RelativeUnixPathFragment otherRelativeUnixPathFragment = (RelativeUnixPathFragment) other; - return HELPER.segmentsEqual(this.segments, otherRelativeUnixPathFragment.segments); - } - - // Java serialization looks for the presence of this method in the concrete class. It is not - // inherited from the parent class. - @Override - protected Object writeReplace() { - return super.writeReplace(); - } - - // Java serialization looks for the presence of this method in the concrete class. It is not - // inherited from the parent class. - @Override - protected void readObject(ObjectInputStream stream) throws InvalidObjectException { - super.readObject(stream); - } - } -} diff --git a/src/main/java/com/google/devtools/build/lib/vfs/WindowsOsPathPolicy.java b/src/main/java/com/google/devtools/build/lib/vfs/WindowsOsPathPolicy.java new file mode 100644 index 0000000000..f420683b75 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/vfs/WindowsOsPathPolicy.java @@ -0,0 +1,240 @@ +// Copyright 2017 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.common.base.Splitter; +import com.google.common.collect.Iterables; +import com.google.devtools.build.lib.windows.WindowsShortPath; +import com.google.devtools.build.lib.windows.jni.WindowsFileOperations; +import java.io.IOException; + +@VisibleForTesting +class WindowsOsPathPolicy implements OsPathPolicy { + + static final WindowsOsPathPolicy INSTANCE = new WindowsOsPathPolicy(); + + static final int NEEDS_SHORT_PATH_NORMALIZATION = NEEDS_NORMALIZE + 1; + + private static final Splitter WINDOWS_PATH_SPLITTER = + Splitter.onPattern("[\\\\/]+").omitEmptyStrings(); + + private final ShortPathResolver shortPathResolver; + + interface ShortPathResolver { + String resolveShortPath(String path); + } + + static class DefaultShortPathResolver implements ShortPathResolver { + @Override + public String resolveShortPath(String path) { + try { + return WindowsFileOperations.getLongPath(path); + } catch (IOException e) { + return path; + } + } + } + + WindowsOsPathPolicy() { + this(new DefaultShortPathResolver()); + } + + WindowsOsPathPolicy(ShortPathResolver shortPathResolver) { + this.shortPathResolver = shortPathResolver; + } + + @Override + public int needsToNormalize(String path) { + int n = path.length(); + int normalizationLevel = NORMALIZED; + int dotCount = 0; + char prevChar = 0; + int segmentBeginIndex = 0; // The start index of the current path index + boolean segmentHasShortPathChar = false; // Triggers more expensive short path regex test + for (int i = 0; i < n; i++) { + char c = path.charAt(i); + if (isSeparator(c)) { + if (c == '\\') { + normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE); + } + // No need to check for '\\' here because that already causes normalization + if (prevChar == '/') { + normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE); + } + if (dotCount == 1 || dotCount == 2) { + normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE); + } + if (segmentHasShortPathChar) { + if (WindowsShortPath.isShortPath(path.substring(segmentBeginIndex, i))) { + normalizationLevel = Math.max(normalizationLevel, NEEDS_SHORT_PATH_NORMALIZATION); + } + } + segmentBeginIndex = i + 1; + segmentHasShortPathChar = false; + } else if (c == '~') { + // This path segment might be a Windows short path segment + segmentHasShortPathChar = true; + } + dotCount = c == '.' ? dotCount + 1 : 0; + prevChar = c; + } + if (segmentHasShortPathChar) { + if (WindowsShortPath.isShortPath(path.substring(segmentBeginIndex))) { + normalizationLevel = Math.max(normalizationLevel, NEEDS_SHORT_PATH_NORMALIZATION); + } + } + if ((n > 1 && isSeparator(prevChar)) || dotCount == 1 || dotCount == 2) { + normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE); + } + return normalizationLevel; + } + + @Override + public int needsToNormalizeSuffix(String normalizedSuffix) { + // On Windows, all bets are off because of short paths, so we have to check the entire string + return needsToNormalize(normalizedSuffix); + } + + @Override + public String normalize(String path, int normalizationLevel) { + if (normalizationLevel == NORMALIZED) { + return path; + } + if (normalizationLevel == NEEDS_SHORT_PATH_NORMALIZATION) { + String resolvedPath = shortPathResolver.resolveShortPath(path); + if (resolvedPath != null) { + path = resolvedPath; + } + } + String[] segments = Iterables.toArray(WINDOWS_PATH_SPLITTER.splitToList(path), String.class); + int driveStrLength = getDriveStrLength(path); + boolean isAbsolute = driveStrLength > 0; + int segmentSkipCount = isAbsolute && driveStrLength > 1 ? 1 : 0; + + StringBuilder sb = new StringBuilder(path.length()); + if (isAbsolute) { + char c = path.charAt(0); + if (isSeparator(c)) { + sb.append('/'); + } else { + sb.append(Character.toUpperCase(c)); + sb.append(":/"); + } + } + int segmentCount = Utils.removeRelativePaths(segments, segmentSkipCount, isAbsolute); + for (int i = 0; i < segmentCount; ++i) { + sb.append(segments[i]); + sb.append('/'); + } + if (segmentCount > 0) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + + @Override + public int getDriveStrLength(String path) { + int n = path.length(); + if (n == 0) { + return 0; + } + char c0 = path.charAt(0); + if (isSeparator(c0)) { + return 1; + } + if (n < 3) { + return 0; + } + char c1 = path.charAt(1); + char c2 = path.charAt(2); + if (isDriveLetter(c0) && c1 == ':' && isSeparator(c2)) { + return 3; + } + return 0; + } + + private static boolean isDriveLetter(char c) { + return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z')); + } + + @Override + public int compare(String s1, String s2) { + // Windows is case-insensitive + return s1.compareToIgnoreCase(s2); + } + + @Override + public int compare(char c1, char c2) { + return Character.compare(Character.toLowerCase(c1), Character.toLowerCase(c2)); + } + + @Override + public boolean equals(String s1, String s2) { + return s1.equalsIgnoreCase(s2); + } + + @Override + public int hash(String s) { + // Windows is case-insensitive + return s.toLowerCase().hashCode(); + } + + @Override + public boolean startsWith(String path, String prefix) { + int pathn = path.length(); + int prefixn = prefix.length(); + if (pathn < prefixn) { + return false; + } + for (int i = 0; i < prefixn; ++i) { + if (Character.toLowerCase(path.charAt(i)) != Character.toLowerCase(prefix.charAt(i))) { + return false; + } + } + return true; + } + + @Override + public boolean endsWith(String path, String suffix) { + int pathn = path.length(); + int suffixLength = suffix.length(); + if (pathn < suffixLength) { + return false; + } + int offset = pathn - suffixLength; + for (int i = 0; i < suffixLength; ++i) { + if (Character.toLowerCase(path.charAt(i + offset)) + != Character.toLowerCase(suffix.charAt(i))) { + return false; + } + } + return true; + } + + @Override + public char getSeparator() { + return '/'; + } + + @Override + public boolean isSeparator(char c) { + return c == '/' || c == '\\'; + } + + @Override + public boolean isCaseSensitive() { + return false; + } +} diff --git a/src/main/java/com/google/devtools/build/lib/vfs/WindowsPathFragment.java b/src/main/java/com/google/devtools/build/lib/vfs/WindowsPathFragment.java deleted file mode 100644 index ac3be0f524..0000000000 --- a/src/main/java/com/google/devtools/build/lib/vfs/WindowsPathFragment.java +++ /dev/null @@ -1,254 +0,0 @@ -// Copyright 2017 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 java.io.InvalidObjectException; -import java.io.ObjectInputStream; - -/** - * Abstract base class for {@link PathFragment} instances that will be allocated when Blaze is run - * on a Windows platform. - */ -abstract class WindowsPathFragment extends PathFragment { - static final Helper HELPER = new Helper(); - - // The drive letter of an absolute path, eg. 'C' for 'C:/foo'. - // We deliberately ignore "C:foo" style paths and treat them like a literal "C:foo" path segment. - protected final char driveLetter; - - protected WindowsPathFragment(char driveLetter, String[] segments) { - super(segments); - this.driveLetter = driveLetter; - } - - @Override - public String windowsVolume() { - return (driveLetter != '\0') ? driveLetter + ":" : ""; - } - - @Override - public char getDriveLetter() { - return driveLetter; - } - - @Override - protected int computeHashCode() { - int h = 0; - for (String segment : segments) { - int segmentHash = segment.toLowerCase().hashCode(); - h = h * 31 + segmentHash; - } - return h; - } - - private static class Helper extends PathFragment.Helper { - private static final char SEPARATOR_CHAR = '/'; - // TODO(laszlocsomor): Lots of internal PathFragment operations, e.g. getPathString, use the - // primary separator char and do not use this. - private static final char EXTRA_SEPARATOR_CHAR = '\\'; - - private static boolean isDriveLetter(char c) { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); - } - - @Override - PathFragment create(String path) { - char driveLetter = - path.length() >= 3 - && path.charAt(1) == ':' - && isSeparator(path.charAt(2)) - && isDriveLetter(path.charAt(0)) - ? Character.toUpperCase(path.charAt(0)) - : '\0'; - if (driveLetter != '\0') { - path = path.substring(2); - } - boolean isAbsolute = path.length() > 0 && isSeparator(path.charAt(0)); - return isAbsolute - ? new AbsoluteWindowsPathFragment(driveLetter, segment(path, 1)) - : new RelativeWindowsPathFragment(driveLetter, segment(path, 0)); - } - - @Override - PathFragment createAlreadyInterned(char driveLetter, boolean isAbsolute, String[] segments) { - return isAbsolute - ? new AbsoluteWindowsPathFragment(driveLetter, segments) - : new RelativeWindowsPathFragment(driveLetter, segments); - } - - @Override - char getPrimarySeparatorChar() { - return SEPARATOR_CHAR; - } - - @Override - boolean isSeparator(char c) { - return c == SEPARATOR_CHAR || c == EXTRA_SEPARATOR_CHAR; - } - - @Override - boolean containsSeparatorChar(String path) { - // TODO(laszlocsomor): This is inefficient. - return path.indexOf(SEPARATOR_CHAR) != -1 || path.indexOf(EXTRA_SEPARATOR_CHAR) != -1; - } - - @Override - boolean segmentsEqual(int length, String[] segments1, int offset1, String[] segments2) { - if ((segments1.length - offset1) < length || segments2.length < length) { - return false; - } - for (int i = 0; i < length; ++i) { - String seg1 = segments1[i + offset1]; - String seg2 = segments2[i]; - if ((seg1 == null) != (seg2 == null)) { - return false; - } - if (seg1 == null) { - continue; - } - // TODO(laszlocsomor): The calls to String#toLowerCase are inefficient and potentially - // repeated too. Also, why not use String#equalsIgnoreCase. - seg1 = seg1.toLowerCase(); - seg2 = seg2.toLowerCase(); - if (!seg1.equals(seg2)) { - return false; - } - } - return true; - } - - @Override - protected int compare(PathFragment pathFragment1, PathFragment pathFragment2) { - if (pathFragment1.isAbsolute() != pathFragment2.isAbsolute()) { - return pathFragment1.isAbsolute() ? -1 : 1; - } - int cmp = Character.compare(pathFragment1.getDriveLetter(), pathFragment2.getDriveLetter()); - if (cmp != 0) { - return cmp; - } - String[] segments1 = pathFragment1.segments(); - String[] segments2 = pathFragment2.segments(); - int len1 = segments1.length; - int len2 = segments2.length; - int n = Math.min(len1, len2); - for (int i = 0; i < n; i++) { - String seg1 = segments1[i].toLowerCase(); - String seg2 = segments2[i].toLowerCase(); - cmp = seg1.compareTo(seg2); - if (cmp != 0) { - return cmp; - } - } - return len1 - len2; - } - } - - private static final class AbsoluteWindowsPathFragment extends WindowsPathFragment { - private AbsoluteWindowsPathFragment(char driveLetter, String[] segments) { - super(driveLetter, segments); - } - - @Override - public boolean isAbsolute() { - return true; - } - - @Override - protected int computeHashCode() { - int h = Boolean.TRUE.hashCode(); - h = h * 31 + super.computeHashCode(); - h = h * 31 + Character.valueOf(getDriveLetter()).hashCode(); - return h; - } - - @Override - public boolean equals(Object other) { - if (!(other instanceof AbsoluteWindowsPathFragment)) { - return false; - } - if (this == other) { - return true; - } - AbsoluteWindowsPathFragment otherAbsoluteWindowsPathFragment = - (AbsoluteWindowsPathFragment) other; - return this.driveLetter == otherAbsoluteWindowsPathFragment.driveLetter - && HELPER.segmentsEqual(this.segments, otherAbsoluteWindowsPathFragment.segments); - } - - // Java serialization looks for the presence of this method in the concrete class. It is not - // inherited from the parent class. - @Override - protected Object writeReplace() { - return super.writeReplace(); - } - - // Java serialization looks for the presence of this method in the concrete class. It is not - // inherited from the parent class. - @Override - protected void readObject(ObjectInputStream stream) throws InvalidObjectException { - super.readObject(stream); - } - } - - private static final class RelativeWindowsPathFragment extends WindowsPathFragment { - private RelativeWindowsPathFragment(char driveLetter, String[] segments) { - super(driveLetter, segments); - } - - @Override - public boolean isAbsolute() { - return false; - } - - @Override - protected int computeHashCode() { - int h = Boolean.FALSE.hashCode(); - h = h * 31 + super.computeHashCode(); - if (!isEmpty()) { - h = h * 31 + Character.valueOf(getDriveLetter()).hashCode(); - } - return h; - } - - @Override - public boolean equals(Object other) { - if (!(other instanceof RelativeWindowsPathFragment)) { - return false; - } - if (this == other) { - return true; - } - RelativeWindowsPathFragment otherRelativeWindowsPathFragment = - (RelativeWindowsPathFragment) other; - return isEmpty() && otherRelativeWindowsPathFragment.isEmpty() - ? true - : this.driveLetter == otherRelativeWindowsPathFragment.driveLetter - && HELPER.segmentsEqual(this.segments, otherRelativeWindowsPathFragment.segments); - } - - // Java serialization looks for the presence of this method in the concrete class. It is not - // inherited from the parent class. - @Override - protected Object writeReplace() { - return super.writeReplace(); - } - - // Java serialization looks for the presence of this method in the concrete class. It is not - // inherited from the parent class. - @Override - protected void readObject(ObjectInputStream stream) throws InvalidObjectException { - super.readObject(stream); - } - } -} diff --git a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java index 8fd21180b4..c2a52a5b3d 100644 --- a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java +++ b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java @@ -350,7 +350,8 @@ public class InMemoryFileSystem extends FileSystem { Stack<String> stack = new Stack<>(); for (Path p = path; !isRootDirectory(p); p = p.getParentDirectory()) { - stack.push(p.getBaseName()); + String name = baseNameOrWindowsDrive(p); + stack.push(name); } InMemoryContentInfo inode = rootInode; @@ -381,6 +382,13 @@ public class InMemoryFileSystem extends FileSystem { for (int ii = segments.size() - 1; ii >= 0; --ii) { stack.push(segments.get(ii)); // Note this may include ".." segments. } + // Push Windows drive if there is one + if (linkTarget.isAbsolute()) { + String driveStr = linkTarget.getDriveStr(); + if (driveStr.length() > 1) { + stack.push(driveStr); + } + } } else { inode = child; } @@ -412,7 +420,7 @@ public class InMemoryFileSystem extends FileSystem { private synchronized InMemoryContentInfo getNoFollowStatOrOutOfScopeParent(Path path) throws IOException { InMemoryDirectoryInfo dirInfo = getDirectory(path.getParentDirectory()); - return directoryLookup(dirInfo, path.getBaseName(), /*create=*/ false, path); + return directoryLookup(dirInfo, baseNameOrWindowsDrive(path), /*create=*/ false, path); } /** @@ -606,7 +614,7 @@ public class InMemoryFileSystem extends FileSystem { InMemoryDirectoryInfo parent; synchronized (this) { parent = getDirectory(path.getParentDirectory()); - InMemoryContentInfo child = parent.getChild(path.getBaseName()); + InMemoryContentInfo child = parent.getChild(baseNameOrWindowsDrive(path)); if (child != null) { // already exists if (child.isDirectory()) { return false; @@ -618,7 +626,7 @@ public class InMemoryFileSystem extends FileSystem { InMemoryDirectoryInfo newDir = new InMemoryDirectoryInfo(clock); newDir.addChild(".", newDir); newDir.addChild("..", parent); - insert(parent, path.getBaseName(), newDir, path); + insert(parent, baseNameOrWindowsDrive(path), newDir, path); return true; } @@ -649,10 +657,11 @@ public class InMemoryFileSystem extends FileSystem { synchronized (this) { InMemoryDirectoryInfo parent = getDirectory(path.getParentDirectory()); - if (parent.getChild(path.getBaseName()) != null) { + if (parent.getChild(baseNameOrWindowsDrive(path)) != null) { throw Error.EEXIST.exception(path); } - insert(parent, path.getBaseName(), new InMemoryLinkInfo(clock, targetFragment), path); + insert( + parent, baseNameOrWindowsDrive(path), new InMemoryLinkInfo(clock, targetFragment), path); } } @@ -703,11 +712,11 @@ public class InMemoryFileSystem extends FileSystem { synchronized (this) { InMemoryDirectoryInfo parent = getDirectory(path.getParentDirectory()); - InMemoryContentInfo child = parent.getChild(path.getBaseName()); + InMemoryContentInfo child = parent.getChild(baseNameOrWindowsDrive(path)); if (child.isDirectory() && child.getSize() > 2) { throw Error.ENOTEMPTY.exception(path); } - unlink(parent, path.getBaseName(), path); + unlink(parent, baseNameOrWindowsDrive(path), path); return true; } } @@ -780,13 +789,13 @@ public class InMemoryFileSystem extends FileSystem { InMemoryDirectoryInfo sourceParent = getDirectory(sourcePath.getParentDirectory()); InMemoryDirectoryInfo targetParent = getDirectory(targetPath.getParentDirectory()); - InMemoryContentInfo sourceInode = sourceParent.getChild(sourcePath.getBaseName()); + InMemoryContentInfo sourceInode = sourceParent.getChild(baseNameOrWindowsDrive(sourcePath)); if (sourceInode == null) { throw Error.ENOENT.exception(sourcePath); } - InMemoryContentInfo targetInode = targetParent.getChild(targetPath.getBaseName()); + InMemoryContentInfo targetInode = targetParent.getChild(baseNameOrWindowsDrive(targetPath)); - unlink(sourceParent, sourcePath.getBaseName(), sourcePath); + unlink(sourceParent, baseNameOrWindowsDrive(sourcePath), sourcePath); try { // TODO(bazel-team): (2009) test with symbolic links. @@ -802,15 +811,19 @@ public class InMemoryFileSystem extends FileSystem { } else if (sourceInode.isDirectory()) { throw new IOException(sourcePath + " -> " + targetPath + " (" + Error.ENOTDIR + ")"); } - unlink(targetParent, targetPath.getBaseName(), targetPath); + unlink(targetParent, baseNameOrWindowsDrive(targetPath), targetPath); } sourceInode.movedTo(targetPath); - insert(targetParent, targetPath.getBaseName(), sourceInode, targetPath); + insert(targetParent, baseNameOrWindowsDrive(targetPath), sourceInode, targetPath); return; } catch (IOException e) { sourceInode.movedTo(sourcePath); - insert(sourceParent, sourcePath.getBaseName(), sourceInode, sourcePath); // restore source + insert( + sourceParent, + baseNameOrWindowsDrive(sourcePath), + sourceInode, + sourcePath); // restore source throw e; } } @@ -828,18 +841,33 @@ public class InMemoryFileSystem extends FileSystem { synchronized (this) { InMemoryDirectoryInfo linkParent = getDirectory(linkPath.getParentDirectory()); // Same check used when creating a symbolic link - if (linkParent.getChild(linkPath.getBaseName()) != null) { + if (linkParent.getChild(baseNameOrWindowsDrive(linkPath)) != null) { throw Error.EEXIST.exception(linkPath); } insert( linkParent, - linkPath.getBaseName(), - getDirectory(originalPath.getParentDirectory()).getChild(originalPath.getBaseName()), + baseNameOrWindowsDrive(linkPath), + getDirectory(originalPath.getParentDirectory()) + .getChild(baseNameOrWindowsDrive(originalPath)), linkPath); } } - private boolean isRootDirectory(Path path) { - return path.isRootDirectory(); + /** + * On Unix the root directory is "/". On Windows there isn't one, so we reach null from + * getParentDirectory. + */ + private boolean isRootDirectory(@Nullable Path path) { + return path == null || path.getPathString().equals("/"); + } + + /** + * Returns either the base name of the path, or the drive (if referring to a Windows drive). + * + * <p>This allows the file system to treat windows drives much like directories. + */ + private static String baseNameOrWindowsDrive(Path path) { + String name = path.getBaseName(); + return !name.isEmpty() ? name : path.getDriveStr(); } } diff --git a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryLinkInfo.java b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryLinkInfo.java index 107f319115..dabbbcefeb 100644 --- a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryLinkInfo.java +++ b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryLinkInfo.java @@ -31,7 +31,7 @@ class InMemoryLinkInfo extends InMemoryContentInfo { InMemoryLinkInfo(Clock clock, PathFragment linkContent) { super(clock); this.linkContent = linkContent; - this.normalizedLinkContent = linkContent.normalize(); + this.normalizedLinkContent = linkContent; } @Override @@ -56,7 +56,7 @@ class InMemoryLinkInfo extends InMemoryContentInfo { @Override public long getSize() { - return linkContent.toString().length(); + return linkContent.getSafePathString().length(); } /** diff --git a/src/main/java/com/google/devtools/build/lib/windows/WindowsFileSystem.java b/src/main/java/com/google/devtools/build/lib/windows/WindowsFileSystem.java index af97e27d3d..961465ec6d 100644 --- a/src/main/java/com/google/devtools/build/lib/windows/WindowsFileSystem.java +++ b/src/main/java/com/google/devtools/build/lib/windows/WindowsFileSystem.java @@ -14,15 +14,10 @@ package com.google.devtools.build.lib.windows; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Function; -import com.google.common.base.Preconditions; -import com.google.common.base.Predicate; import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe; import com.google.devtools.build.lib.vfs.FileStatus; -import com.google.devtools.build.lib.vfs.FileSystem; import com.google.devtools.build.lib.vfs.JavaIoFileSystem; import com.google.devtools.build.lib.vfs.Path; -import com.google.devtools.build.lib.vfs.Path.PathFactory; import com.google.devtools.build.lib.vfs.PathFragment; import com.google.devtools.build.lib.windows.jni.WindowsFileOperations; import java.io.File; @@ -31,246 +26,11 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.attribute.DosFileAttributes; -import java.util.Arrays; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import javax.annotation.Nullable; /** File system implementation for Windows. */ @ThreadSafe public class WindowsFileSystem extends JavaIoFileSystem { - // Properties of 8dot3 (DOS-style) short file names: - // - they are at most 11 characters long - // - they have a prefix (before "~") that is {1..6} characters long, may contain numbers, letters, - // "_", even "~", and maybe even more - // - they have a "~" after the prefix - // - have {1..6} numbers after "~" (according to [1] this is only one digit, but MSDN doesn't - // clarify this), the combined length up till this point is at most 8 - // - they have an optional "." afterwards, and another {0..3} more characters - // - just because a path looks like a short name it isn't necessarily one; the user may create - // such names and they'd resolve to themselves - // [1] https://en.wikipedia.org/wiki/8.3_filename#VFAT_and_Computer-generated_8.3_filenames - // bullet point (3) (on 2016-12-05) - @VisibleForTesting - static final Predicate<String> SHORT_NAME_MATCHER = - new Predicate<String>() { - private final Pattern pattern = Pattern.compile("^(.{1,6})~([0-9]{1,6})(\\..{0,3}){0,1}"); - - @Override - public boolean apply(@Nullable String input) { - Matcher m = pattern.matcher(input); - return input.length() <= 12 - && m.matches() - && m.groupCount() >= 2 - && (m.group(1).length() + m.group(2).length()) < 8; // the "~" makes it at most 8 - } - }; - - /** Resolves DOS-style, shortened path names, returning the last segment's long form. */ - private static final Function<String, String> WINDOWS_SHORT_PATH_RESOLVER = - path -> { - try { - // Since Path objects are created hierarchically, we know for sure that every segment of - // the path, except the last one, is already canonicalized, so we can return just that. - // Plus the returned value is passed to Path.getChild so we must not return a full - // path here. - return PathFragment.create(WindowsFileOperations.getLongPath(path)).getBaseName(); - } catch (IOException e) { - return null; - } - }; - - @VisibleForTesting - private enum WindowsPathFactory implements PathFactory { - INSTANCE { - @Override - public Path createRootPath(FileSystem filesystem) { - return new WindowsPath(filesystem, PathFragment.ROOT_DIR, null); - } - - @Override - public Path createChildPath(Path parent, String childName) { - Preconditions.checkState(parent instanceof WindowsPath); - return new WindowsPath(parent.getFileSystem(), childName, (WindowsPath) parent); - } - - @Override - public Path getCachedChildPathInternal(Path path, String childName) { - return WindowsPathFactory.getCachedChildPathInternalImpl( - path, childName, WINDOWS_SHORT_PATH_RESOLVER); - } - }; - - private static Path getCachedChildPathInternalImpl( - Path parent, String child, Function<String, String> resolver) { - if (parent != null && parent.isRootDirectory()) { - // This is a top-level directory. It must be a drive name ("C:" or "c"). - if (WindowsPath.isWindowsVolumeName(child)) { - child = WindowsPath.getDriveLetter((WindowsPath) parent, child) + ":"; - } else { - throw new IllegalArgumentException("Cannot create Unix-style paths on Windows."); - } - } - - String resolvedChild = child; - if (parent != null && !parent.isRootDirectory() && SHORT_NAME_MATCHER.apply(child)) { - String pathString = parent.getPathString(); - if (!pathString.endsWith("/")) { - pathString += "/"; - } - pathString += child; - resolvedChild = resolver.apply(pathString); - } - return Path.getCachedChildPathInternal( - parent, - // If resolution succeeded, or we didn't attempt to resolve, then `resolvedChild` has the - // child name. If it's null, then resolution failed; use the unresolved child name in that - // case. - resolvedChild != null ? resolvedChild : child, - // If resolution failed, likely because the path doesn't exist, then do not cache the - // child. If we did, then in case the path later came into existence, we'd have a stale - // cache entry. - /* cacheable */ resolvedChild != null); - } - - /** - * Creates a {@link PathFactory} with a mock shortname resolver. - * - * <p>The factory works exactly like the actual one ({@link WindowsPathFactory#INSTANCE}) except - * it's using the mock resolver. - */ - public static PathFactory createForTesting(final Function<String, String> mockResolver) { - return new PathFactory() { - @Override - public Path createRootPath(FileSystem filesystem) { - return INSTANCE.createRootPath(filesystem); - } - - @Override - public Path createChildPath(Path parent, String childName) { - return INSTANCE.createChildPath(parent, childName); - } - - @Override - public Path getCachedChildPathInternal(Path path, String childName) { - return WindowsPathFactory.getCachedChildPathInternalImpl(path, childName, mockResolver); - } - }; - } - } - - /** A windows-specific subclass of Path. */ - @VisibleForTesting - protected static final class WindowsPath extends Path { - - // The drive letter is '\0' if and only if this Path is the filesystem root "/". - private char driveLetter; - - private WindowsPath(FileSystem fileSystem) { - super(fileSystem); - this.driveLetter = '\0'; - } - - private WindowsPath(FileSystem fileSystem, String name, WindowsPath parent) { - super(fileSystem, name, parent); - this.driveLetter = getDriveLetter(parent, name); - } - - @Override - protected void buildPathString(StringBuilder result) { - if (isRootDirectory()) { - result.append(PathFragment.ROOT_DIR); - } else { - if (isTopLevelDirectory()) { - result.append(driveLetter).append(':').append(PathFragment.SEPARATOR_CHAR); - } else { - WindowsPath parent = (WindowsPath) getParentDirectory(); - parent.buildPathString(result); - if (!parent.isTopLevelDirectory()) { - result.append(PathFragment.SEPARATOR_CHAR); - } - result.append(getBaseName()); - } - } - } - - @Override - public void reinitializeAfterDeserialization() { - Preconditions.checkState( - getParentDirectory().isRootDirectory() || getParentDirectory() instanceof WindowsPath); - this.driveLetter = - (getParentDirectory() != null) ? ((WindowsPath) getParentDirectory()).driveLetter : '\0'; - } - - @Override - public boolean isMaybeRelativeTo(Path ancestorCandidate) { - Preconditions.checkState(ancestorCandidate instanceof WindowsPath); - return ancestorCandidate.isRootDirectory() - || driveLetter == ((WindowsPath) ancestorCandidate).driveLetter; - } - - @Override - public boolean isTopLevelDirectory() { - return isRootDirectory() || getParentDirectory().isRootDirectory(); - } - - @Override - public PathFragment asFragment() { - String[] segments = getSegments(); - if (segments.length > 0) { - // Strip off the first segment that contains the volume name. - segments = Arrays.copyOfRange(segments, 1, segments.length); - } - - return PathFragment.create(driveLetter, true, segments); - } - - @Override - protected Path getRootForRelativePathComputation(PathFragment relative) { - Path result = this; - if (relative.isAbsolute()) { - result = getFileSystem().getRootDirectory(); - if (!relative.windowsVolume().isEmpty()) { - result = result.getRelative(relative.windowsVolume()); - } - } - return result; - } - - private static boolean isWindowsVolumeName(String name) { - return (name.length() == 1 || (name.length() == 2 && name.charAt(1) == ':')) - && Character.isLetter(name.charAt(0)); - } - - private static char getDriveLetter(WindowsPath parent, String name) { - if (parent == null) { - return '\0'; - } else { - if (parent.isRootDirectory()) { - Preconditions.checkState( - isWindowsVolumeName(name), - "top-level directory on Windows must be a drive (name = '%s')", - name); - return Character.toUpperCase(name.charAt(0)); - } else { - return parent.driveLetter; - } - } - } - - @VisibleForTesting - @Override - protected synchronized void applyToChildren(Predicate<Path> function) { - super.applyToChildren(function); - } - } - - @VisibleForTesting - static PathFactory getPathFactoryForTesting(Function<String, String> mockResolver) { - return WindowsPathFactory.createForTesting(mockResolver); - } - public static final LinkOption[] NO_OPTIONS = new LinkOption[0]; public static final LinkOption[] NO_FOLLOW = new LinkOption[] {LinkOption.NOFOLLOW_LINKS}; @@ -281,11 +41,6 @@ public class WindowsFileSystem extends JavaIoFileSystem { } @Override - protected PathFactory getPathFactory() { - return WindowsPathFactory.INSTANCE; - } - - @Override public String getFileSystemType(Path path) { // TODO(laszlocsomor): implement this properly, i.e. actually query this information from // somewhere (java.nio.Filesystem? System.getProperty? implement JNI method and use WinAPI?). diff --git a/src/main/java/com/google/devtools/build/lib/windows/WindowsSubprocessFactory.java b/src/main/java/com/google/devtools/build/lib/windows/WindowsSubprocessFactory.java index a69884066c..af591cc7e8 100644 --- a/src/main/java/com/google/devtools/build/lib/windows/WindowsSubprocessFactory.java +++ b/src/main/java/com/google/devtools/build/lib/windows/WindowsSubprocessFactory.java @@ -83,9 +83,7 @@ public class WindowsSubprocessFactory implements SubprocessFactory { // If it's not absolute, then it cannot be longer than MAX_PATH, since MAX_PATH also limits the // length of file names. PathFragment argv0fragment = PathFragment.create(argv0); - return (argv0fragment.isAbsolute()) - ? argv0fragment.normalize().getPathString().replace('/', '\\') - : argv0; + return (argv0fragment.isAbsolute()) ? argv0fragment.getPathString().replace('/', '\\') : argv0; } private String getRedirectPath(StreamAction action, File file) { diff --git a/src/test/java/com/google/devtools/build/lib/BUILD b/src/test/java/com/google/devtools/build/lib/BUILD index 7cbf304f16..538cb8fd2c 100644 --- a/src/test/java/com/google/devtools/build/lib/BUILD +++ b/src/test/java/com/google/devtools/build/lib/BUILD @@ -9,8 +9,7 @@ package( CROSS_PLATFORM_WINDOWS_TESTS = [ "util/DependencySetWindowsTest.java", "vfs/PathFragmentWindowsTest.java", - "vfs/WindowsLocalPathTest.java", - "windows/PathWindowsTest.java", + "vfs/WindowsPathTest.java", ] # Tests for Windows-specific functionality that run on Windows. @@ -18,7 +17,6 @@ WINDOWS_ON_WINDOWS_TESTS = glob( ["windows/*.java"], exclude = [ "windows/MockSubprocess.java", - "windows/PathWindowsTest.java", ], ) @@ -374,6 +372,7 @@ java_library( deps = [ ":guava_junit_truth", ":vfs_filesystem_test", + "//src/main/java/com/google/devtools/build/lib:os_util", "//src/main/java/com/google/devtools/build/lib/vfs", ], ) @@ -401,7 +400,7 @@ java_test( # systems java_test( name = "windows_test", - srcs = CROSS_PLATFORM_WINDOWS_TESTS + ["vfs/LocalPathAbstractTest.java"], + srcs = CROSS_PLATFORM_WINDOWS_TESTS + ["vfs/PathAbstractTest.java"], jvm_flags = [ "-Dblaze.os=Windows", "-Dbazel.windows_unix_root=C:/fake/msys", diff --git a/src/test/java/com/google/devtools/build/lib/actions/ArtifactTest.java b/src/test/java/com/google/devtools/build/lib/actions/ArtifactTest.java index 363bae9df3..f26d00eaa5 100644 --- a/src/test/java/com/google/devtools/build/lib/actions/ArtifactTest.java +++ b/src/test/java/com/google/devtools/build/lib/actions/ArtifactTest.java @@ -27,6 +27,7 @@ import com.google.devtools.build.lib.rules.cpp.CppFileTypes; import com.google.devtools.build.lib.rules.java.JavaSemantics; import com.google.devtools.build.lib.skyframe.serialization.InjectingObjectCodecAdapter; import com.google.devtools.build.lib.skyframe.serialization.testutils.ObjectCodecTester; +import com.google.devtools.build.lib.testutil.MoreAsserts; import com.google.devtools.build.lib.testutil.Scratch; import com.google.devtools.build.lib.vfs.Path; import com.google.devtools.build.lib.vfs.PathFragment; @@ -58,7 +59,7 @@ public class ArtifactTest { Path f1 = scratch.file("/exec/dir/file.ext"); Path bogusDir = scratch.file("/exec/dir/bogus"); try { - new Artifact(f1, ArtifactRoot.asDerivedRoot(execDir, bogusDir), f1.relativeTo(execDir)); + new Artifact(ArtifactRoot.asDerivedRoot(execDir, bogusDir), f1.relativeTo(execDir)); fail("Expected IllegalArgumentException constructing artifact with a bad root dir"); } catch (IllegalArgumentException expected) {} } @@ -93,7 +94,7 @@ public class ArtifactTest { @Test public void testRootPrefixedExecPath_normal() throws IOException { Path f1 = scratch.file("/exec/root/dir/file.ext"); - Artifact a1 = new Artifact(f1, rootDir, f1.relativeTo(execDir)); + Artifact a1 = new Artifact(rootDir, f1.relativeTo(execDir)); assertThat(Artifact.asRootPrefixedExecPath(a1)).isEqualTo("root:dir/file.ext"); } @@ -109,9 +110,10 @@ public class ArtifactTest { public void testRootPrefixedExecPath_nullRootDir() throws IOException { Path f1 = scratch.file("/exec/dir/file.ext"); try { - new Artifact(f1, null, f1.relativeTo(execDir)); - fail("Expected IllegalArgumentException creating artifact with null root"); - } catch (IllegalArgumentException expected) {} + new Artifact(null, f1.relativeTo(execDir)); + fail("Expected NullPointerException creating artifact with null root"); + } catch (NullPointerException expected) { + } } @Test @@ -119,9 +121,9 @@ public class ArtifactTest { Path f1 = scratch.file("/exec/root/dir/file1.ext"); Path f2 = scratch.file("/exec/root/dir/dir/file2.ext"); Path f3 = scratch.file("/exec/root/dir/dir/dir/file3.ext"); - Artifact a1 = new Artifact(f1, rootDir, f1.relativeTo(execDir)); - Artifact a2 = new Artifact(f2, rootDir, f2.relativeTo(execDir)); - Artifact a3 = new Artifact(f3, rootDir, f3.relativeTo(execDir)); + Artifact a1 = new Artifact(rootDir, f1.relativeTo(execDir)); + Artifact a2 = new Artifact(rootDir, f2.relativeTo(execDir)); + Artifact a3 = new Artifact(rootDir, f3.relativeTo(execDir)); List<String> strings = new ArrayList<>(); Artifact.addRootPrefixedExecPaths(Lists.newArrayList(a1, a2, a3), strings); assertThat(strings).containsExactly( @@ -283,10 +285,9 @@ public class ArtifactTest { @Test public void testToDetailString() throws Exception { - Path execRoot = scratch.getFileSystem().getPath("/"); + Path execRoot = scratch.getFileSystem().getPath("/a"); Artifact a = new Artifact( - scratch.file("/a/b/c"), ArtifactRoot.asDerivedRoot(execRoot, scratch.dir("/a/b")), PathFragment.create("b/c")); assertThat(a.toDetailString()).isEqualTo("[[/a]b]c"); @@ -294,17 +295,12 @@ public class ArtifactTest { @Test public void testWeirdArtifact() throws Exception { - try { - Path execRoot = scratch.getFileSystem().getPath("/"); - new Artifact( - scratch.file("/a/b/c"), - ArtifactRoot.asDerivedRoot(execRoot, scratch.dir("/a")), - PathFragment.create("c")); - fail(); - } catch (IllegalArgumentException e) { - assertThat(e).hasMessage( - "c: illegal execPath doesn't end with b/c at /a/b/c with root /a[derived]"); - } + Path execRoot = scratch.getFileSystem().getPath("/"); + MoreAsserts.expectThrows( + IllegalArgumentException.class, + () -> + new Artifact( + ArtifactRoot.asDerivedRoot(execRoot, scratch.dir("/a")), PathFragment.create("c"))); } @Test @@ -319,25 +315,23 @@ public class ArtifactTest { @Test public void testSerializeToStringWithExecPath() throws Exception { - Path execRoot = scratch.getFileSystem().getPath("/"); - Path path = scratch.file("/aaa/bbb/ccc"); + Path execRoot = scratch.getFileSystem().getPath("/aaa"); ArtifactRoot root = ArtifactRoot.asDerivedRoot(execRoot, scratch.dir("/aaa/bbb")); PathFragment execPath = PathFragment.create("bbb/ccc"); - assertThat(new Artifact(path, root, execPath).serializeToString()).isEqualTo("bbb/ccc /3"); + assertThat(new Artifact(root, execPath).serializeToString()).isEqualTo("bbb/ccc /3"); } @Test public void testSerializeToStringWithOwner() throws Exception { - Path execRoot = scratch.getFileSystem().getPath("/"); + Path execRoot = scratch.getFileSystem().getPath("/aa"); assertThat( new Artifact( - scratch.file("/aa/b/c"), - ArtifactRoot.asDerivedRoot(execRoot, scratch.dir("/aa")), + ArtifactRoot.asDerivedRoot(execRoot, scratch.dir("/aa/b")), PathFragment.create("b/c"), new LabelArtifactOwner(Label.parseAbsoluteUnchecked("//foo:bar"))) .serializeToString()) - .isEqualTo("b/c /3 //foo:bar"); + .isEqualTo("b/c /1 //foo:bar"); } @Test @@ -349,10 +343,9 @@ public class ArtifactTest { new Artifact( PathFragment.create("src/b"), ArtifactRoot.asSourceRoot(Root.fromPath(execDir))), new Artifact( - scratch.file("/src/c"), ArtifactRoot.asDerivedRoot( scratch.getFileSystem().getPath("/"), scratch.dir("/src")), - PathFragment.create("c"), + PathFragment.create("src/c"), new LabelArtifactOwner(Label.parseAbsoluteUnchecked("//foo:bar")))) .buildAndRunTests(); } @@ -387,7 +380,6 @@ public class ArtifactTest { public void testIsSourceArtifact() throws Exception { assertThat( new Artifact( - scratch.file("/src/foo.cc"), ArtifactRoot.asSourceRoot(Root.fromPath(scratch.dir("/"))), PathFragment.create("src/foo.cc")) .isSourceArtifact()) diff --git a/src/test/java/com/google/devtools/build/lib/actions/CustomCommandLineTest.java b/src/test/java/com/google/devtools/build/lib/actions/CustomCommandLineTest.java index 085415cf34..b9f341a588 100644 --- a/src/test/java/com/google/devtools/build/lib/actions/CustomCommandLineTest.java +++ b/src/test/java/com/google/devtools/build/lib/actions/CustomCommandLineTest.java @@ -984,7 +984,6 @@ public class CustomCommandLineTest { private SpecialArtifact createTreeArtifact(String rootRelativePath) { PathFragment relpath = PathFragment.create(rootRelativePath); return new SpecialArtifact( - rootDir.getRoot().getRelative(relpath), rootDir, rootDir.getExecPath().getRelative(relpath), ArtifactOwner.NullArtifactOwner.INSTANCE, diff --git a/src/test/java/com/google/devtools/build/lib/analysis/actions/ParamFileWriteActionTest.java b/src/test/java/com/google/devtools/build/lib/analysis/actions/ParamFileWriteActionTest.java index 4c9d2dda5c..8678d50a30 100644 --- a/src/test/java/com/google/devtools/build/lib/analysis/actions/ParamFileWriteActionTest.java +++ b/src/test/java/com/google/devtools/build/lib/analysis/actions/ParamFileWriteActionTest.java @@ -111,7 +111,6 @@ public class ParamFileWriteActionTest extends BuildViewTestCase { private SpecialArtifact createTreeArtifact(String rootRelativePath) { PathFragment relpath = PathFragment.create(rootRelativePath); return new SpecialArtifact( - rootDir.getRoot().getRelative(relpath), rootDir, rootDir.getExecPath().getRelative(relpath), ArtifactOwner.NullArtifactOwner.INSTANCE, diff --git a/src/test/java/com/google/devtools/build/lib/analysis/actions/PopulateTreeArtifactActionTest.java b/src/test/java/com/google/devtools/build/lib/analysis/actions/PopulateTreeArtifactActionTest.java index 345aa97ab8..1ce4fd5b43 100644 --- a/src/test/java/com/google/devtools/build/lib/analysis/actions/PopulateTreeArtifactActionTest.java +++ b/src/test/java/com/google/devtools/build/lib/analysis/actions/PopulateTreeArtifactActionTest.java @@ -346,7 +346,6 @@ public class PopulateTreeArtifactActionTest extends BuildViewTestCase { private SpecialArtifact createTreeArtifact(String rootRelativePath) { PathFragment relpath = PathFragment.create(rootRelativePath); return new SpecialArtifact( - root.getRoot().getRelative(relpath), root, root.getExecPath().getRelative(relpath), ArtifactOwner.NullArtifactOwner.INSTANCE, diff --git a/src/test/java/com/google/devtools/build/lib/analysis/actions/SpawnActionTemplateTest.java b/src/test/java/com/google/devtools/build/lib/analysis/actions/SpawnActionTemplateTest.java index ad03fe7ef9..17588a6504 100644 --- a/src/test/java/com/google/devtools/build/lib/analysis/actions/SpawnActionTemplateTest.java +++ b/src/test/java/com/google/devtools/build/lib/analysis/actions/SpawnActionTemplateTest.java @@ -325,7 +325,6 @@ public class SpawnActionTemplateTest { private SpecialArtifact createTreeArtifact(String rootRelativePath) { PathFragment relpath = PathFragment.create(rootRelativePath); return new SpecialArtifact( - root.getRoot().getRelative(relpath), root, root.getExecPath().getRelative(relpath), ArtifactOwner.NullArtifactOwner.INSTANCE, diff --git a/src/test/java/com/google/devtools/build/lib/buildeventstream/transports/BuildEventTransportFactoryTest.java b/src/test/java/com/google/devtools/build/lib/buildeventstream/transports/BuildEventTransportFactoryTest.java index ca8bef91a2..5e22269292 100644 --- a/src/test/java/com/google/devtools/build/lib/buildeventstream/transports/BuildEventTransportFactoryTest.java +++ b/src/test/java/com/google/devtools/build/lib/buildeventstream/transports/BuildEventTransportFactoryTest.java @@ -133,6 +133,13 @@ public class BuildEventTransportFactoryTest { assertThat(transports).isEmpty(); } + @Test + public void testPathToUriString() { + // See https://blogs.msdn.microsoft.com/ie/2006/12/06/file-uris-in-windows/ + assertThat(BuildEventTransportFactory.pathToUriString("C:/Temp/Foo Bar.txt")) + .isEqualTo("file:///C:/Temp/Foo%20Bar.txt"); + } + private void sendEventsAndClose(BuildEvent event, Iterable<BuildEventTransport> transports) throws IOException{ for (BuildEventTransport transport : transports) { diff --git a/src/test/java/com/google/devtools/build/lib/pkgcache/PathPackageLocatorTest.java b/src/test/java/com/google/devtools/build/lib/pkgcache/PathPackageLocatorTest.java index 1cac35349c..a9b224f026 100644 --- a/src/test/java/com/google/devtools/build/lib/pkgcache/PathPackageLocatorTest.java +++ b/src/test/java/com/google/devtools/build/lib/pkgcache/PathPackageLocatorTest.java @@ -338,7 +338,7 @@ public class PathPackageLocatorTest extends FoundationTestCase { workspace.getRelative("foo"), BazelSkyframeExecutorConstants.BUILD_FILES_BY_PRIORITY); assertThat(eventCollector.count()).isSameAs(1); - assertContainsEvent("The package path element './foo' will be taken relative"); + assertContainsEvent("The package path element 'foo' will be taken relative"); } /** Regression test for bug: "IllegalArgumentException in PathPackageLocator.create()" */ diff --git a/src/test/java/com/google/devtools/build/lib/rules/android/ResourceTestBase.java b/src/test/java/com/google/devtools/build/lib/rules/android/ResourceTestBase.java index 1bd5bed2d3..9ea174a8ad 100644 --- a/src/test/java/com/google/devtools/build/lib/rules/android/ResourceTestBase.java +++ b/src/test/java/com/google/devtools/build/lib/rules/android/ResourceTestBase.java @@ -183,6 +183,6 @@ public abstract class ResourceTestBase { public Artifact getResource(String pathString) { Path path = fileSystem.getPath("/" + RESOURCE_ROOT + "/" + pathString); return new Artifact( - path, root, root.getExecPath().getRelative(root.getRoot().relativize(path)), OWNER); + root, root.getExecPath().getRelative(root.getRoot().relativize(path)), OWNER); } } diff --git a/src/test/java/com/google/devtools/build/lib/rules/cpp/CcLibraryConfiguredTargetTest.java b/src/test/java/com/google/devtools/build/lib/rules/cpp/CcLibraryConfiguredTargetTest.java index 4d7c3fc697..36fcadf028 100644 --- a/src/test/java/com/google/devtools/build/lib/rules/cpp/CcLibraryConfiguredTargetTest.java +++ b/src/test/java/com/google/devtools/build/lib/rules/cpp/CcLibraryConfiguredTargetTest.java @@ -1066,7 +1066,7 @@ public class CcLibraryConfiguredTargetTest extends BuildViewTestCase { checkError( "root", "a", - "The include path 'd/../../somewhere' references a path outside of the execution root.", + "The include path '../somewhere' references a path outside of the execution root.", "cc_library(name='a', srcs=['a.cc'], copts=['-Id/../../somewhere'])"); } diff --git a/src/test/java/com/google/devtools/build/lib/rules/cpp/CppLinkActionTest.java b/src/test/java/com/google/devtools/build/lib/rules/cpp/CppLinkActionTest.java index db56bd53d6..f7bf8ebfbd 100644 --- a/src/test/java/com/google/devtools/build/lib/rules/cpp/CppLinkActionTest.java +++ b/src/test/java/com/google/devtools/build/lib/rules/cpp/CppLinkActionTest.java @@ -524,10 +524,6 @@ public class CppLinkActionTest extends BuildViewTestCase { public Artifact getOutputArtifact(String relpath) { return new Artifact( - getTargetConfiguration() - .getBinDirectory(RepositoryName.MAIN) - .getRoot() - .getRelative(relpath), getTargetConfiguration().getBinDirectory(RepositoryName.MAIN), getTargetConfiguration().getBinFragment().getRelative(relpath)); } @@ -672,9 +668,7 @@ public class CppLinkActionTest extends BuildViewTestCase { FileSystem fs = scratch.getFileSystem(); Path execRoot = fs.getPath(TestUtils.tmpDir()); PathFragment execPath = PathFragment.create("out").getRelative(name); - Path path = execRoot.getRelative(execPath); return new SpecialArtifact( - path, ArtifactRoot.asDerivedRoot(execRoot, execRoot.getRelative("out")), execPath, ArtifactOwner.NullArtifactOwner.INSTANCE, diff --git a/src/test/java/com/google/devtools/build/lib/rules/cpp/CrosstoolConfigurationLoaderTest.java b/src/test/java/com/google/devtools/build/lib/rules/cpp/CrosstoolConfigurationLoaderTest.java index 4a3714b890..e45e32fe56 100644 --- a/src/test/java/com/google/devtools/build/lib/rules/cpp/CrosstoolConfigurationLoaderTest.java +++ b/src/test/java/com/google/devtools/build/lib/rules/cpp/CrosstoolConfigurationLoaderTest.java @@ -186,7 +186,7 @@ public class CrosstoolConfigurationLoaderTest extends AnalysisTestCase { assertThat(ccProvider.getTargetCpu()).isEqualTo("piii"); assertThat(ccProvider.getTargetGnuSystemName()).isEqualTo("target-system-name"); - assertThat(toolchain.getToolPathFragment(Tool.AR)).isEqualTo(getToolPath("/path-to-ar")); + assertThat(toolchain.getToolPathFragment(Tool.AR)).isEqualTo(getToolPath("path-to-ar")); assertThat(ccProvider.getAbi()).isEqualTo("abi-version"); assertThat(ccProvider.getAbiGlibcVersion()).isEqualTo("abi-libc-version"); @@ -199,7 +199,7 @@ public class CrosstoolConfigurationLoaderTest extends AnalysisTestCase { assertThat(ccProvider.supportsFission()).isTrue(); assertThat(ccProvider.getBuiltInIncludeDirectories()) - .containsExactly(getToolPath("/system-include-dir")); + .containsExactly(getToolPath("system-include-dir")); assertThat(ccProvider.getSysroot()).isNull(); assertThat(CppHelper.getCompilerOptions(toolchain, ccProvider, NO_FEATURES)) @@ -238,8 +238,8 @@ public class CrosstoolConfigurationLoaderTest extends AnalysisTestCase { "CC_FLAGS", "") .entrySet()); - assertThat(toolchain.getToolPathFragment(Tool.LD)).isEqualTo(getToolPath("/path-to-ld")); - assertThat(toolchain.getToolPathFragment(Tool.DWP)).isEqualTo(getToolPath("/path-to-dwp")); + assertThat(toolchain.getToolPathFragment(Tool.LD)).isEqualTo(getToolPath("path-to-ld")); + assertThat(toolchain.getToolPathFragment(Tool.DWP)).isEqualTo(getToolPath("path-to-dwp")); } /** @@ -605,7 +605,7 @@ public class CrosstoolConfigurationLoaderTest extends AnalysisTestCase { .entrySet()); assertThat(ccProviderA.getBuiltInIncludeDirectories()) .containsExactly( - getToolPath("/system-include-dir-A-1"), getToolPath("/system-include-dir-A-2")) + getToolPath("system-include-dir-A-1"), getToolPath("system-include-dir-A-2")) .inOrder(); assertThat(ccProviderA.getSysroot()).isEqualTo(PathFragment.create("some")); @@ -682,9 +682,7 @@ public class CrosstoolConfigurationLoaderTest extends AnalysisTestCase { PackageIdentifier packageIdentifier = PackageIdentifier.create( TestConstants.TOOLS_REPOSITORY, - PathFragment.create( - PathFragment.create(TestConstants.MOCK_CC_CROSSTOOL_PATH), - PathFragment.create(path))); + PathFragment.create(TestConstants.MOCK_CC_CROSSTOOL_PATH).getRelative(path)); return packageIdentifier.getPathUnderExecRoot(); } diff --git a/src/test/java/com/google/devtools/build/lib/rules/objc/HeaderThinningTest.java b/src/test/java/com/google/devtools/build/lib/rules/objc/HeaderThinningTest.java index cdd9d72764..ee9b9fb6d8 100644 --- a/src/test/java/com/google/devtools/build/lib/rules/objc/HeaderThinningTest.java +++ b/src/test/java/com/google/devtools/build/lib/rules/objc/HeaderThinningTest.java @@ -189,7 +189,6 @@ public class HeaderThinningTest extends ObjcRuleTestCase { private Artifact getTreeArtifact(String name) { Artifact treeArtifactBase = getSourceArtifact(name); return new SpecialArtifact( - treeArtifactBase.getPath(), treeArtifactBase.getRoot(), treeArtifactBase.getExecPath(), treeArtifactBase.getArtifactOwner(), diff --git a/src/test/java/com/google/devtools/build/lib/rules/proto/ProtoCompileActionBuilderTest.java b/src/test/java/com/google/devtools/build/lib/rules/proto/ProtoCompileActionBuilderTest.java index d21acf1839..bc0ebaabe7 100644 --- a/src/test/java/com/google/devtools/build/lib/rules/proto/ProtoCompileActionBuilderTest.java +++ b/src/test/java/com/google/devtools/build/lib/rules/proto/ProtoCompileActionBuilderTest.java @@ -347,7 +347,6 @@ public class ProtoCompileActionBuilderTest { private Artifact artifact(String ownerLabel, String path) { return new Artifact( - root.getRoot().getRelative(path), root, root.getExecPath().getRelative(path), new LabelArtifactOwner(Label.parseAbsoluteUnchecked(ownerLabel))); @@ -356,7 +355,6 @@ public class ProtoCompileActionBuilderTest { /** Creates a dummy artifact with the given path, that actually resides in /out/<path>. */ private Artifact derivedArtifact(String ownerLabel, String path) { return new Artifact( - derivedRoot.getRoot().getRelative(path), derivedRoot, derivedRoot.getExecPath().getRelative(path), new LabelArtifactOwner(Label.parseAbsoluteUnchecked(ownerLabel))); diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/ActionTemplateExpansionFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/ActionTemplateExpansionFunctionTest.java index 7c864789ec..f8b6a9db3d 100644 --- a/src/test/java/com/google/devtools/build/lib/skyframe/ActionTemplateExpansionFunctionTest.java +++ b/src/test/java/com/google/devtools/build/lib/skyframe/ActionTemplateExpansionFunctionTest.java @@ -39,7 +39,6 @@ import com.google.devtools.build.lib.analysis.actions.SpawnActionTemplate.Output import com.google.devtools.build.lib.events.NullEventHandler; import com.google.devtools.build.lib.pkgcache.PathPackageLocator; import com.google.devtools.build.lib.testutil.FoundationTestCase; -import com.google.devtools.build.lib.vfs.Path; import com.google.devtools.build.lib.vfs.PathFragment; import com.google.devtools.build.lib.vfs.Root; import com.google.devtools.build.skyframe.EvaluationResult; @@ -205,9 +204,7 @@ public final class ActionTemplateExpansionFunctionTest extends FoundationTestCas private SpecialArtifact createTreeArtifact(String path) { PathFragment execPath = PathFragment.create("out").getRelative(path); - Path fullPath = rootDirectory.getRelative(execPath); return new SpecialArtifact( - fullPath, ArtifactRoot.asDerivedRoot(rootDirectory, rootDirectory.getRelative("out")), execPath, ArtifactOwner.NullArtifactOwner.INSTANCE, diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/ArtifactFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/ArtifactFunctionTest.java index 1699fc25fc..21b0a10274 100644 --- a/src/test/java/com/google/devtools/build/lib/skyframe/ArtifactFunctionTest.java +++ b/src/test/java/com/google/devtools/build/lib/skyframe/ArtifactFunctionTest.java @@ -250,10 +250,8 @@ public class ArtifactFunctionTest extends ArtifactFunctionTestCase { private Artifact createDerivedArtifact(String path) { PathFragment execPath = PathFragment.create("out").getRelative(path); - Path fullPath = root.getRelative(execPath); Artifact output = new Artifact( - fullPath, ArtifactRoot.asDerivedRoot(root, root.getRelative("out")), execPath, ALL_OWNER); @@ -264,9 +262,7 @@ public class ArtifactFunctionTest extends ArtifactFunctionTestCase { private Artifact createMiddlemanArtifact(String path) { ArtifactRoot middlemanRoot = ArtifactRoot.middlemanRoot(middlemanPath, middlemanPath.getRelative("out")); - Path fullPath = middlemanRoot.getRoot().getRelative(path); - return new Artifact( - fullPath, middlemanRoot, middlemanRoot.getExecPath().getRelative(path), ALL_OWNER); + return new Artifact(middlemanRoot, middlemanRoot.getExecPath().getRelative(path), ALL_OWNER); } private SpecialArtifact createDerivedTreeArtifactWithAction(String path) { @@ -277,9 +273,7 @@ public class ArtifactFunctionTest extends ArtifactFunctionTestCase { private SpecialArtifact createDerivedTreeArtifactOnly(String path) { PathFragment execPath = PathFragment.create("out").getRelative(path); - Path fullPath = root.getRelative(execPath); return new SpecialArtifact( - fullPath, ArtifactRoot.asDerivedRoot(root, root.getRelative("out")), execPath, ALL_OWNER, diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/FilesystemValueCheckerTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/FilesystemValueCheckerTest.java index f7a989a4ae..48ccd7e341 100644 --- a/src/test/java/com/google/devtools/build/lib/skyframe/FilesystemValueCheckerTest.java +++ b/src/test/java/com/google/devtools/build/lib/skyframe/FilesystemValueCheckerTest.java @@ -620,7 +620,6 @@ public class FilesystemValueCheckerTest { outputDir.createDirectory(); ArtifactRoot derivedRoot = ArtifactRoot.asDerivedRoot(fs.getPath("/"), outputDir); return new SpecialArtifact( - outputPath, derivedRoot, derivedRoot.getExecPath().getRelative(derivedRoot.getRoot().relativize(outputPath)), ArtifactOwner.NullArtifactOwner.INSTANCE, diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunctionTest.java index 872de47ad0..25b510e18e 100644 --- a/src/test/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunctionTest.java +++ b/src/test/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunctionTest.java @@ -171,10 +171,8 @@ public final class RecursiveFilesystemTraversalFunctionTest extends FoundationTe private Artifact derivedArtifact(String path) { PathFragment execPath = PathFragment.create("out").getRelative(path); - Path fullPath = rootDirectory.getRelative(execPath); Artifact output = new Artifact( - fullPath, ArtifactRoot.asDerivedRoot(rootDirectory, rootDirectory.getRelative("out")), execPath); return output; diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTestCase.java b/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTestCase.java index 3b39d03441..e63d1ea06e 100644 --- a/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTestCase.java +++ b/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTestCase.java @@ -336,9 +336,7 @@ public abstract class TimestampBuilderTestCase extends FoundationTestCase { Artifact createDerivedArtifact(FileSystem fs, String name) { Path execRoot = fs.getPath(TestUtils.tmpDir()); PathFragment execPath = PathFragment.create("out").getRelative(name); - Path path = execRoot.getRelative(execPath); return new Artifact( - path, ArtifactRoot.asDerivedRoot(execRoot, execRoot.getRelative("out")), execPath, ACTION_LOOKUP_KEY); diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactBuildTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactBuildTest.java index 30b6c4caf8..5d24b9ef28 100644 --- a/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactBuildTest.java +++ b/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactBuildTest.java @@ -1192,9 +1192,7 @@ public class TreeArtifactBuildTest extends TimestampBuilderTestCase { FileSystem fs = scratch.getFileSystem(); Path execRoot = fs.getPath(TestUtils.tmpDir()); PathFragment execPath = PathFragment.create("out").getRelative(name); - Path path = execRoot.getRelative(execPath); return new SpecialArtifact( - path, ArtifactRoot.asDerivedRoot(execRoot, execRoot.getRelative("out")), execPath, ACTION_LOOKUP_KEY, diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactMetadataTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactMetadataTest.java index 96bc4bed65..9e3fd39ddd 100644 --- a/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactMetadataTest.java +++ b/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactMetadataTest.java @@ -204,7 +204,6 @@ public class TreeArtifactMetadataTest extends ArtifactFunctionTestCase { Path fullPath = root.getRelative(execPath); SpecialArtifact output = new SpecialArtifact( - fullPath, ArtifactRoot.asDerivedRoot(root, root.getRelative("out")), execPath, ALL_OWNER, diff --git a/src/test/java/com/google/devtools/build/lib/unix/UnixPathEqualityTest.java b/src/test/java/com/google/devtools/build/lib/unix/UnixPathEqualityTest.java index d77b10fd86..2d81e13965 100644 --- a/src/test/java/com/google/devtools/build/lib/unix/UnixPathEqualityTest.java +++ b/src/test/java/com/google/devtools/build/lib/unix/UnixPathEqualityTest.java @@ -14,9 +14,9 @@ package com.google.devtools.build.lib.unix; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; import com.google.common.testing.EqualsTester; +import com.google.devtools.build.lib.testutil.MoreAsserts; import com.google.devtools.build.lib.vfs.FileSystem; import com.google.devtools.build.lib.vfs.Path; import org.junit.Before; @@ -93,25 +93,8 @@ public class UnixPathEqualityTest { Path a = unixFs.getPath("/a"); Path b = otherUnixFs.getPath("/b"); - try { - a.renameTo(b); - fail(); - } catch (IllegalArgumentException e) { - assertThat(e).hasMessageThat().contains("different filesystems"); - } - - try { - a.relativeTo(b); - fail(); - } catch (IllegalArgumentException e) { - assertThat(e).hasMessageThat().contains("different filesystems"); - } - - try { - a.createSymbolicLink(b); - fail(); - } catch (IllegalArgumentException e) { - assertThat(e).hasMessageThat().contains("different filesystems"); - } + MoreAsserts.expectThrows(IllegalArgumentException.class, () -> a.renameTo(b)); + MoreAsserts.expectThrows(IllegalArgumentException.class, () -> a.relativeTo(b)); + MoreAsserts.expectThrows(IllegalArgumentException.class, () -> a.createSymbolicLink(b)); } } diff --git a/src/test/java/com/google/devtools/build/lib/vfs/FileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemTest.java index e8896bbc0d..47ae76aa8b 100644 --- a/src/test/java/com/google/devtools/build/lib/vfs/FileSystemTest.java +++ b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemTest.java @@ -14,7 +14,6 @@ package com.google.devtools.build.lib.vfs; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.fail; @@ -789,23 +788,6 @@ public abstract class FileSystemTest { assertThat(xNonEmptyDirectoryFoo.isFile()).isTrue(); } - @Test - public void testCannotRemoveRoot() { - Path rootDirectory = testFS.getRootDirectory(); - try { - rootDirectory.delete(); - fail(); - } catch (IOException e) { - String msg = e.getMessage(); - assertWithMessage(String.format("got %s want EBUSY or ENOTEMPTY", msg)) - .that( - msg.endsWith(" (Directory not empty)") - || msg.endsWith(" (Device or resource busy)") - || msg.endsWith(" (Is a directory)")) - .isTrue(); // Happens on OS X. - } - } - // Test the date functions @Test public void testCreateFileChangesTimeOfDirectory() throws Exception { @@ -1105,22 +1087,13 @@ public abstract class FileSystemTest { // Test the Paths @Test public void testGetPathOnlyAcceptsAbsolutePath() { - try { - testFS.getPath("not-absolute"); - fail("The expected Exception was not thrown."); - } catch (IllegalArgumentException ex) { - assertThat(ex).hasMessage("not-absolute (not an absolute path)"); - } + MoreAsserts.expectThrows(IllegalArgumentException.class, () -> testFS.getPath("not-absolute")); } @Test public void testGetPathOnlyAcceptsAbsolutePathFragment() { - try { - testFS.getPath(PathFragment.create("not-absolute")); - fail("The expected Exception was not thrown."); - } catch (IllegalArgumentException ex) { - assertThat(ex).hasMessage("not-absolute (not an absolute path)"); - } + MoreAsserts.expectThrows( + IllegalArgumentException.class, () -> testFS.getPath(PathFragment.create("not-absolute"))); } // Test the access permissions diff --git a/src/test/java/com/google/devtools/build/lib/vfs/FileSystemUtilsTest.java b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemUtilsTest.java index 656363333e..f9cc1a9031 100644 --- a/src/test/java/com/google/devtools/build/lib/vfs/FileSystemUtilsTest.java +++ b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemUtilsTest.java @@ -175,11 +175,29 @@ public class FileSystemUtilsTest { @Test public void testRelativePath() throws IOException { createTestDirectoryTree(); - assertThat(relativePath(topDir, file1).getPathString()).isEqualTo("file-1"); - assertThat(relativePath(topDir, topDir).getPathString()).isEqualTo("."); - assertThat(relativePath(topDir, dirLink).getPathString()).isEqualTo("a-dir/inner-dir/dir-link"); - assertThat(relativePath(topDir, file4).getPathString()).isEqualTo("../file-4"); - assertThat(relativePath(innerDir, file4).getPathString()).isEqualTo("../../../file-4"); + assertThat( + relativePath(PathFragment.create("/top-dir"), PathFragment.create("/top-dir/file-1")) + .getPathString()) + .isEqualTo("file-1"); + assertThat( + relativePath(PathFragment.create("/top-dir"), PathFragment.create("/top-dir")) + .getPathString()) + .isEqualTo(""); + assertThat( + relativePath( + PathFragment.create("/top-dir"), + PathFragment.create("/top-dir/a-dir/inner-dir/dir-link")) + .getPathString()) + .isEqualTo("a-dir/inner-dir/dir-link"); + assertThat( + relativePath(PathFragment.create("/top-dir"), PathFragment.create("/file-4")) + .getPathString()) + .isEqualTo("../file-4"); + assertThat( + relativePath( + PathFragment.create("/top-dir/a-dir/inner-dir"), PathFragment.create("/file-4")) + .getPathString()) + .isEqualTo("../../../file-4"); } @Test diff --git a/src/test/java/com/google/devtools/build/lib/vfs/LocalPathAbstractTest.java b/src/test/java/com/google/devtools/build/lib/vfs/LocalPathAbstractTest.java deleted file mode 100644 index 93869521ad..0000000000 --- a/src/test/java/com/google/devtools/build/lib/vfs/LocalPathAbstractTest.java +++ /dev/null @@ -1,180 +0,0 @@ -// Copyright 2017 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 static com.google.common.truth.Truth.assertThat; -import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows; -import static java.util.stream.Collectors.toList; - -import com.google.common.collect.Lists; -import com.google.common.testing.EqualsTester; -import com.google.devtools.build.lib.vfs.LocalPath.OsPathPolicy; -import java.util.Collections; -import java.util.List; -import org.junit.Before; -import org.junit.Test; - -/** Tests for {@link LocalPath}. */ -public abstract class LocalPathAbstractTest { - - private OsPathPolicy os; - - @Before - public void setup() { - os = getFilePathOs(); - } - - @Test - public void testEqualsAndHashCode() { - new EqualsTester() - .addEqualityGroup( - create("../relative/path"), create("..").getRelative("relative").getRelative("path")) - .addEqualityGroup(create("something/else")) - .addEqualityGroup(create(""), LocalPath.EMPTY) - .testEquals(); - } - - @Test - public void testRelativeTo() { - assertThat(create("").relativeTo(create("")).getPathString()).isEmpty(); - assertThat(create("foo").relativeTo(create("foo")).getPathString()).isEmpty(); - assertThat(create("foo/bar/baz").relativeTo(create("foo")).getPathString()) - .isEqualTo("bar/baz"); - assertThat(create("foo/bar/baz").relativeTo(create("foo/bar")).getPathString()) - .isEqualTo("baz"); - assertThat(create("foo").relativeTo(create("")).getPathString()).isEqualTo("foo"); - - // Cannot relativize non-ancestors - assertThrows(IllegalArgumentException.class, () -> create("foo/bar").relativeTo(create("fo"))); - - // Make sure partial directory matches aren't reported - assertThrows( - IllegalArgumentException.class, () -> create("foo/bar").relativeTo(create("foo/ba"))); - } - - @Test - public void testGetRelative() { - assertThat(create("a").getRelative("b").getPathString()).isEqualTo("a/b"); - assertThat(create("a/b").getRelative("c/d").getPathString()).isEqualTo("a/b/c/d"); - assertThat(create("a").getRelative("").getPathString()).isEqualTo("a"); - assertThat(create("a/b").getRelative("../c").getPathString()).isEqualTo("a/c"); - assertThat(create("a/b").getRelative("..").getPathString()).isEqualTo("a"); - } - - @Test - public void testEmptyPathToEmptyPath() { - // compare string forms - assertThat(create("").getPathString()).isEmpty(); - // compare fragment forms - assertThat(create("")).isEqualTo(create("")); - } - - @Test - public void testSimpleNameToSimpleName() { - // compare string forms - assertThat(create("foo").getPathString()).isEqualTo("foo"); - // compare fragment forms - assertThat(create("foo")).isEqualTo(create("foo")); - } - - @Test - public void testSimplePathToSimplePath() { - // compare string forms - assertThat(create("foo/bar").getPathString()).isEqualTo("foo/bar"); - // compare fragment forms - assertThat(create("foo/bar")).isEqualTo(create("foo/bar")); - } - - @Test - public void testStripsTrailingSlash() { - // compare string forms - assertThat(create("foo/bar/").getPathString()).isEqualTo("foo/bar"); - // compare fragment forms - assertThat(create("foo/bar/")).isEqualTo(create("foo/bar")); - } - - @Test - public void testGetParentDirectory() { - LocalPath fooBarWiz = create("foo/bar/wiz"); - LocalPath fooBar = create("foo/bar"); - LocalPath foo = create("foo"); - LocalPath empty = create(""); - assertThat(fooBarWiz.getParentDirectory()).isEqualTo(fooBar); - assertThat(fooBar.getParentDirectory()).isEqualTo(foo); - assertThat(foo.getParentDirectory()).isEqualTo(empty); - assertThat(empty.getParentDirectory()).isNull(); - } - - @Test - public void testBasename() throws Exception { - assertThat(create("foo/bar").getBaseName()).isEqualTo("bar"); - assertThat(create("foo/").getBaseName()).isEqualTo("foo"); - assertThat(create("foo").getBaseName()).isEqualTo("foo"); - assertThat(create("").getBaseName()).isEmpty(); - } - - @Test - public void testStartsWith() { - // (relative path, relative prefix) => true - assertThat(create("foo/bar").startsWith(create("foo/bar"))).isTrue(); - assertThat(create("foo/bar").startsWith(create("foo"))).isTrue(); - assertThat(create("foot/bar").startsWith(create("foo"))).isFalse(); - } - - @Test - public void testNormalize() { - assertThat(create("a/b")).isEqualTo(create("a/b")); - assertThat(create("a/../../b")).isEqualTo(create("../b")); - assertThat(create("a/../..")).isEqualTo(create("..")); - assertThat(create("a/../b")).isEqualTo(create("b")); - assertThat(create("a/b/../b")).isEqualTo(create("a/b")); - } - - @Test - public void testNormalStringsDoNotAllocate() { - String normal1 = "a/b/hello.txt"; - assertThat(create(normal1).getPathString()).isSameAs(normal1); - - // Sanity check our testing strategy - String notNormal = "a/../b"; - assertThat(create(notNormal).getPathString()).isNotSameAs(notNormal); - } - - @Test - public void testComparableSortOrder() { - List<LocalPath> list = - Lists.newArrayList( - create("zzz"), - create("ZZZ"), - create("ABC"), - create("aBc"), - create("AbC"), - create("abc")); - Collections.sort(list); - List<String> result = list.stream().map(LocalPath::getPathString).collect(toList()); - - if (os.isCaseSensitive()) { - assertThat(result).containsExactly("ABC", "AbC", "ZZZ", "aBc", "abc", "zzz").inOrder(); - } else { - // Partial ordering among case-insensitive items guaranteed by Collections.sort stability - assertThat(result).containsExactly("ABC", "aBc", "AbC", "abc", "zzz", "ZZZ").inOrder(); - } - } - - protected abstract OsPathPolicy getFilePathOs(); - - protected LocalPath create(String path) { - return LocalPath.createWithOs(path, os); - } -} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/MacOsLocalPathTest.java b/src/test/java/com/google/devtools/build/lib/vfs/MacOsLocalPathTest.java deleted file mode 100644 index c99ad487ee..0000000000 --- a/src/test/java/com/google/devtools/build/lib/vfs/MacOsLocalPathTest.java +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2017 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 static com.google.common.truth.Truth.assertThat; - -import com.google.common.testing.EqualsTester; -import com.google.devtools.build.lib.vfs.LocalPath.MacOsPathPolicy; -import com.google.devtools.build.lib.vfs.LocalPath.OsPathPolicy; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** Tests Mac-specific parts of {@link LocalPath}. */ -@RunWith(JUnit4.class) -public class MacOsLocalPathTest extends UnixLocalPathTest { - - @Override - protected OsPathPolicy getFilePathOs() { - return new MacOsPathPolicy(); - } - - @Test - public void testMacEqualsAndHashCode() { - new EqualsTester() - .addEqualityGroup(create("a/b"), create("A/B")) - .addEqualityGroup(create("/a/b"), create("/A/B")) - .addEqualityGroup(create("something/else")) - .addEqualityGroup(create("/something/else")) - .testEquals(); - } - - @Test - public void testCaseIsPreserved() { - assertThat(create("a/B").getPathString()).isEqualTo("a/B"); - } -} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/NativePathTest.java b/src/test/java/com/google/devtools/build/lib/vfs/NativePathTest.java index 6d112853fd..5dc43a2567 100644 --- a/src/test/java/com/google/devtools/build/lib/vfs/NativePathTest.java +++ b/src/test/java/com/google/devtools/build/lib/vfs/NativePathTest.java @@ -106,13 +106,6 @@ public class NativePathTest { } @Test - public void testParentOfRootIsRoot() { - assertThat(fs.getPath("/..")).isEqualTo(fs.getPath("/")); - assertThat(fs.getPath("/../../../../../..")).isEqualTo(fs.getPath("/")); - assertThat(fs.getPath("/../../../foo")).isEqualTo(fs.getPath("/foo")); - } - - @Test public void testIsDirectory() { assertThat(fs.getPath(aDirectory.getPath()).isDirectory()).isTrue(); assertThat(fs.getPath(aFile.getPath()).isDirectory()).isFalse(); @@ -241,14 +234,4 @@ public class NativePathTest { assertThat(in.read()).isEqualTo(-1); in.close(); } - - @Test - public void testDerivedSegmentEquality() { - Path absoluteSegment = fs.getRootDirectory(); - - Path derivedNode = absoluteSegment.getChild("derivedSegment"); - Path otherDerivedNode = absoluteSegment.getChild("derivedSegment"); - - assertThat(otherDerivedNode).isSameAs(derivedNode); - } } diff --git a/src/test/java/com/google/devtools/build/lib/vfs/PathAbstractTest.java b/src/test/java/com/google/devtools/build/lib/vfs/PathAbstractTest.java new file mode 100644 index 0000000000..7494683391 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/PathAbstractTest.java @@ -0,0 +1,141 @@ +// Copyright 2017 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 static com.google.common.truth.Truth.assertThat; +import static java.util.stream.Collectors.toList; + +import com.google.common.collect.Lists; +import com.google.common.testing.EqualsTester; +import com.google.devtools.build.lib.clock.BlazeClock; +import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.Collections; +import java.util.List; +import org.junit.Before; +import org.junit.Test; + +/** Tests for {@link Path}. */ +public abstract class PathAbstractTest { + + private FileSystem fileSystem; + private boolean isCaseSensitive; + + @Before + public void setup() { + fileSystem = new InMemoryFileSystem(BlazeClock.instance()); + isCaseSensitive = OsPathPolicy.getFilePathOs().isCaseSensitive(); + } + + @Test + public void testStripsTrailingSlash() { + // compare string forms + assertThat(create("/foo/bar/").getPathString()).isEqualTo("/foo/bar"); + // compare fragment forms + assertThat(create("/foo/bar/")).isEqualTo(create("/foo/bar")); + } + + @Test + public void testBasename() throws Exception { + assertThat(create("/foo/bar").getBaseName()).isEqualTo("bar"); + assertThat(create("/foo/").getBaseName()).isEqualTo("foo"); + assertThat(create("/foo").getBaseName()).isEqualTo("foo"); + assertThat(create("/").getBaseName()).isEmpty(); + } + + @Test + public void testNormalStringsDoNotAllocate() { + String normal1 = "/a/b/hello.txt"; + assertThat(create(normal1).getPathString()).isSameAs(normal1); + + // Sanity check our testing strategy + String notNormal = "/a/../b"; + assertThat(create(notNormal).getPathString()).isNotSameAs(notNormal); + } + + @Test + public void testComparableSortOrder() { + List<Path> list = + Lists.newArrayList( + create("/zzz"), + create("/ZZZ"), + create("/ABC"), + create("/aBc"), + create("/AbC"), + create("/abc")); + Collections.sort(list); + List<String> result = list.stream().map(Path::getPathString).collect(toList()); + + if (isCaseSensitive) { + assertThat(result).containsExactly("/ABC", "/AbC", "/ZZZ", "/aBc", "/abc", "/zzz").inOrder(); + } else { + // Partial ordering among case-insensitive items guaranteed by Collections.sort stability + assertThat(result).containsExactly("/ABC", "/aBc", "/AbC", "/abc", "/zzz", "/ZZZ").inOrder(); + } + } + + @Test + public void testSerialization() throws Exception { + FileSystem oldFileSystem = Path.getFileSystemForSerialization(); + try { + Path.setFileSystemForSerialization(fileSystem); + Path root = fileSystem.getPath("/"); + Path p1 = fileSystem.getPath("/foo"); + Path p2 = fileSystem.getPath("/foo/bar"); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(bos); + + oos.writeObject(root); + oos.writeObject(p1); + oos.writeObject(p2); + + ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bis); + + Path dsRoot = (Path) ois.readObject(); + Path dsP1 = (Path) ois.readObject(); + Path dsP2 = (Path) ois.readObject(); + + new EqualsTester() + .addEqualityGroup(root, dsRoot) + .addEqualityGroup(p1, dsP1) + .addEqualityGroup(p2, dsP2) + .testEquals(); + + assertThat(p2.startsWith(p1)).isTrue(); + assertThat(p2.startsWith(dsP1)).isTrue(); + assertThat(dsP2.startsWith(p1)).isTrue(); + assertThat(dsP2.startsWith(dsP1)).isTrue(); + + // Regression test for a very specific bug in compareTo involving our incorrect usage of + // reference equality rather than logical equality. + String relativePathStringA = "child/grandchildA"; + String relativePathStringB = "child/grandchildB"; + assertThat( + p1.getRelative(relativePathStringA).compareTo(dsP1.getRelative(relativePathStringB))) + .isEqualTo( + p1.getRelative(relativePathStringA).compareTo(p1.getRelative(relativePathStringB))); + } finally { + Path.setFileSystemForSerialization(oldFileSystem); + } + } + + protected Path create(String path) { + return Path.create(path, fileSystem); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentTest.java b/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentTest.java index 7fa3c73d89..02446c0337 100644 --- a/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentTest.java +++ b/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentTest.java @@ -15,13 +15,14 @@ package com.google.devtools.build.lib.vfs; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; +import static com.google.devtools.build.lib.vfs.PathFragment.create; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.testing.EqualsTester; import com.google.devtools.build.lib.skyframe.serialization.testutils.ObjectCodecTester; +import com.google.devtools.build.lib.testutil.MoreAsserts; import com.google.devtools.build.lib.testutil.TestUtils; import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem; import java.io.File; @@ -36,32 +37,6 @@ import org.junit.runners.JUnit4; */ @RunWith(JUnit4.class) public class PathFragmentTest { - @Test - public void testMergeFourPathsWithAbsolute() { - assertThat( - PathFragment.create( - PathFragment.create("x/y"), - PathFragment.create("z/a"), - PathFragment.create("/b/c"), - PathFragment.create("d/e"))) - .isEqualTo(PathFragment.create("x/y/z/a/b/c/d/e")); - } - - @Test - public void testCreateInternsPathFragments() { - String[] firstSegments = new String[] {"hello", "world"}; - PathFragment first = PathFragment.create( - /*driveLetter=*/ '\0', /*isAbsolute=*/ false, firstSegments); - - String[] secondSegments = new String[] {new String("hello"), new String("world")}; - PathFragment second = PathFragment.create( - /*driveLetter=*/ '\0', /*isAbsolute=*/ false, secondSegments); - - assertThat(first.segmentCount()).isEqualTo(second.segmentCount()); - for (int i = 0; i < first.segmentCount(); i++) { - assertThat(first.getSegment(i)).isSameAs(second.getSegment(i)); - } - } @Test public void testEqualsAndHashCode() { @@ -69,23 +44,21 @@ public class PathFragmentTest { new EqualsTester() .addEqualityGroup( - PathFragment.create("../relative/path"), - PathFragment.create("..").getRelative("relative").getRelative("path"), - PathFragment.createAlreadyInterned( - '\0', false, new String[] {"..", "relative", "path"}), - PathFragment.create(new File("../relative/path"))) - .addEqualityGroup(PathFragment.create("something/else")) - .addEqualityGroup(PathFragment.create("/something/else")) - .addEqualityGroup(PathFragment.create("/"), PathFragment.create("//////")) - .addEqualityGroup(PathFragment.create(""), PathFragment.EMPTY_FRAGMENT) + create("../relative/path"), + create("..").getRelative("relative").getRelative("path"), + create(new File("../relative/path").getPath())) + .addEqualityGroup(create("something/else")) + .addEqualityGroup(create("/something/else")) + .addEqualityGroup(create("/"), create("//////")) + .addEqualityGroup(create(""), PathFragment.EMPTY_FRAGMENT) .addEqualityGroup(filesystem.getPath("/")) // A Path object. .testEquals(); } @Test public void testHashCodeCache() { - PathFragment relativePath = PathFragment.create("../relative/path"); - PathFragment rootPath = PathFragment.create("/"); + PathFragment relativePath = create("../relative/path"); + PathFragment rootPath = create("/"); int oldResult = relativePath.hashCode(); int rootResult = rootPath.hashCode(); @@ -93,279 +66,272 @@ public class PathFragmentTest { assertThat(rootPath.hashCode()).isEqualTo(rootResult); } - private void checkRelativeTo(String path, String base) { - PathFragment relative = PathFragment.create(path).relativeTo(base); - assertThat(PathFragment.create(base).getRelative(relative).normalize()) - .isEqualTo(PathFragment.create(path)); - } - @Test public void testRelativeTo() { - assertPath("bar/baz", PathFragment.create("foo/bar/baz").relativeTo("foo")); - assertPath("bar/baz", PathFragment.create("/foo/bar/baz").relativeTo("/foo")); - assertPath("baz", PathFragment.create("foo/bar/baz").relativeTo("foo/bar")); - assertPath("baz", PathFragment.create("/foo/bar/baz").relativeTo("/foo/bar")); - assertPath("foo", PathFragment.create("/foo").relativeTo("/")); - assertPath("foo", PathFragment.create("foo").relativeTo("")); - assertPath("foo/bar", PathFragment.create("foo/bar").relativeTo("")); - - checkRelativeTo("foo/bar/baz", "foo"); - checkRelativeTo("/foo/bar/baz", "/foo"); - checkRelativeTo("foo/bar/baz", "foo/bar"); - checkRelativeTo("/foo/bar/baz", "/foo/bar"); - checkRelativeTo("/foo", "/"); - checkRelativeTo("foo", ""); - checkRelativeTo("foo/bar", ""); + assertThat(create("foo/bar/baz").relativeTo("foo").getPathString()).isEqualTo("bar/baz"); + assertThat(create("/foo/bar/baz").relativeTo("/foo").getPathString()).isEqualTo("bar/baz"); + assertThat(create("foo/bar/baz").relativeTo("foo/bar").getPathString()).isEqualTo("baz"); + assertThat(create("/foo/bar/baz").relativeTo("/foo/bar").getPathString()).isEqualTo("baz"); + assertThat(create("/foo").relativeTo("/").getPathString()).isEqualTo("foo"); + assertThat(create("foo").relativeTo("").getPathString()).isEqualTo("foo"); + assertThat(create("foo/bar").relativeTo("").getPathString()).isEqualTo("foo/bar"); } @Test public void testIsAbsolute() { - assertThat(PathFragment.create("/absolute/test").isAbsolute()).isTrue(); - assertThat(PathFragment.create("relative/test").isAbsolute()).isFalse(); - assertThat(PathFragment.create(new File("/absolute/test")).isAbsolute()).isTrue(); - assertThat(PathFragment.create(new File("relative/test")).isAbsolute()).isFalse(); + assertThat(create("/absolute/test").isAbsolute()).isTrue(); + assertThat(create("relative/test").isAbsolute()).isFalse(); + assertThat(create(new File("/absolute/test").getPath()).isAbsolute()).isTrue(); + assertThat(create(new File("relative/test").getPath()).isAbsolute()).isFalse(); } @Test public void testIsNormalized() { - assertThat(PathFragment.create("/absolute/path").isNormalized()).isTrue(); - assertThat(PathFragment.create("some//path").isNormalized()).isTrue(); - assertThat(PathFragment.create("some/./path").isNormalized()).isFalse(); - assertThat(PathFragment.create("../some/path").isNormalized()).isFalse(); - assertThat(PathFragment.create("some/other/../path").isNormalized()).isFalse(); - assertThat(PathFragment.create("some/other//tricky..path..").isNormalized()).isTrue(); - assertThat(PathFragment.create("/some/other//tricky..path..").isNormalized()).isTrue(); + assertThat(PathFragment.isNormalized("/absolute/path")).isTrue(); + assertThat(PathFragment.isNormalized("some//path")).isTrue(); + assertThat(PathFragment.isNormalized("some/./path")).isFalse(); + assertThat(PathFragment.isNormalized("../some/path")).isFalse(); + assertThat(PathFragment.isNormalized("./some/path")).isFalse(); + assertThat(PathFragment.isNormalized("some/path/..")).isFalse(); + assertThat(PathFragment.isNormalized("some/path/.")).isFalse(); + assertThat(PathFragment.isNormalized("some/other/../path")).isFalse(); + assertThat(PathFragment.isNormalized("some/other//tricky..path..")).isTrue(); + assertThat(PathFragment.isNormalized("/some/other//tricky..path..")).isTrue(); + } + + @Test + public void testContainsUpLevelReferences() { + assertThat(PathFragment.containsUplevelReferences("/absolute/path")).isFalse(); + assertThat(PathFragment.containsUplevelReferences("some//path")).isFalse(); + assertThat(PathFragment.containsUplevelReferences("some/./path")).isFalse(); + assertThat(PathFragment.containsUplevelReferences("../some/path")).isTrue(); + assertThat(PathFragment.containsUplevelReferences("./some/path")).isFalse(); + assertThat(PathFragment.containsUplevelReferences("some/path/..")).isTrue(); + assertThat(PathFragment.containsUplevelReferences("some/path/.")).isFalse(); + assertThat(PathFragment.containsUplevelReferences("some/other/../path")).isTrue(); + assertThat(PathFragment.containsUplevelReferences("some/other//tricky..path..")).isFalse(); + assertThat(PathFragment.containsUplevelReferences("/some/other//tricky..path..")).isFalse(); + + // Normalization cannot remove leading uplevel references, so this will be true + assertThat(create("../some/path").containsUplevelReferences()).isTrue(); + // Normalization will remove these, so no uplevel references left + assertThat(create("some/path/..").containsUplevelReferences()).isFalse(); } @Test public void testRootNodeReturnsRootString() { - PathFragment rootFragment = PathFragment.create("/"); + PathFragment rootFragment = create("/"); assertThat(rootFragment.getPathString()).isEqualTo("/"); } @Test - public void testGetPathFragmentDoesNotNormalize() { - String nonCanonicalPath = "/a/weird/noncanonical/../path/."; - assertThat(PathFragment.create(nonCanonicalPath).getPathString()).isEqualTo(nonCanonicalPath); - } - - @Test public void testGetRelative() { - assertThat(PathFragment.create("a").getRelative("b").getPathString()).isEqualTo("a/b"); - assertThat(PathFragment.create("a/b").getRelative("c/d").getPathString()).isEqualTo("a/b/c/d"); - assertThat(PathFragment.create("c/d").getRelative("/a/b").getPathString()).isEqualTo("/a/b"); - assertThat(PathFragment.create("a").getRelative("").getPathString()).isEqualTo("a"); - assertThat(PathFragment.create("/").getRelative("").getPathString()).isEqualTo("/"); + assertThat(create("a").getRelative("b").getPathString()).isEqualTo("a/b"); + assertThat(create("a/b").getRelative("c/d").getPathString()).isEqualTo("a/b/c/d"); + assertThat(create("c/d").getRelative("/a/b").getPathString()).isEqualTo("/a/b"); + assertThat(create("a").getRelative("").getPathString()).isEqualTo("a"); + assertThat(create("/").getRelative("").getPathString()).isEqualTo("/"); + assertThat(create("a/b").getRelative("../foo").getPathString()).isEqualTo("a/foo"); + assertThat(create("/a/b").getRelative("../foo").getPathString()).isEqualTo("/a/foo"); + + // Make sure any fast path of PathFragment#getRelative(PathFragment) works + assertThat(create("a/b").getRelative(create("../foo")).getPathString()).isEqualTo("a/foo"); + assertThat(create("/a/b").getRelative(create("../foo")).getPathString()).isEqualTo("/a/foo"); + + // Make sure any fast path of PathFragment#getRelative(PathFragment) works + assertThat(create("c/d").getRelative(create("/a/b")).getPathString()).isEqualTo("/a/b"); + + // Test normalization + assertThat(create("a").getRelative(".").getPathString()).isEqualTo("a"); } @Test public void testGetChildWorks() { - PathFragment pf = PathFragment.create("../some/path"); - assertThat(pf.getChild("hi")).isEqualTo(PathFragment.create("../some/path/hi")); + PathFragment pf = create("../some/path"); + assertThat(pf.getChild("hi")).isEqualTo(create("../some/path/hi")); } @Test public void testGetChildRejectsInvalidBaseNames() { - PathFragment pf = PathFragment.create("../some/path"); - assertGetChildFails(pf, "."); - assertGetChildFails(pf, ".."); - assertGetChildFails(pf, "x/y"); - assertGetChildFails(pf, "/y"); - assertGetChildFails(pf, "y/"); - assertGetChildFails(pf, ""); - } - - private void assertGetChildFails(PathFragment pf, String baseName) { - try { - pf.getChild(baseName); - fail(); - } catch (Exception e) { /* Expected. */ } - } - - // Tests after here test the canonicalization - private void assertRegular(String expected, String actual) { - // compare string forms - assertThat(PathFragment.create(actual).getPathString()).isEqualTo(expected); - // compare fragment forms - assertThat(PathFragment.create(actual)).isEqualTo(PathFragment.create(expected)); + PathFragment pf = create("../some/path"); + MoreAsserts.expectThrows(IllegalArgumentException.class, () -> pf.getChild(".")); + MoreAsserts.expectThrows(IllegalArgumentException.class, () -> pf.getChild("..")); + MoreAsserts.expectThrows(IllegalArgumentException.class, () -> pf.getChild("x/y")); + MoreAsserts.expectThrows(IllegalArgumentException.class, () -> pf.getChild("/y")); + MoreAsserts.expectThrows(IllegalArgumentException.class, () -> pf.getChild("y/")); + MoreAsserts.expectThrows(IllegalArgumentException.class, () -> pf.getChild("")); } @Test public void testEmptyPathToEmptyPath() { - assertRegular("/", "/"); - assertRegular("", ""); + assertThat(create("/").getPathString()).isEqualTo("/"); + assertThat(create("").getPathString()).isEqualTo(""); } @Test public void testRedundantSlashes() { - assertRegular("/", "///"); - assertRegular("/foo/bar", "/foo///bar"); - assertRegular("/foo/bar", "////foo//bar"); + assertThat(create("///").getPathString()).isEqualTo("/"); + assertThat(create("/foo///bar").getPathString()).isEqualTo("/foo/bar"); + assertThat(create("////foo//bar").getPathString()).isEqualTo("/foo/bar"); } @Test public void testSimpleNameToSimpleName() { - assertRegular("/foo", "/foo"); - assertRegular("foo", "foo"); + assertThat(create("/foo").getPathString()).isEqualTo("/foo"); + assertThat(create("foo").getPathString()).isEqualTo("foo"); } @Test public void testSimplePathToSimplePath() { - assertRegular("/foo/bar", "/foo/bar"); - assertRegular("foo/bar", "foo/bar"); + assertThat(create("/foo/bar").getPathString()).isEqualTo("/foo/bar"); + assertThat(create("foo/bar").getPathString()).isEqualTo("foo/bar"); } @Test public void testStripsTrailingSlash() { - assertRegular("/foo/bar", "/foo/bar/"); + assertThat(create("/foo/bar/").getPathString()).isEqualTo("/foo/bar"); } @Test public void testGetParentDirectory() { - PathFragment fooBarWiz = PathFragment.create("foo/bar/wiz"); - PathFragment fooBar = PathFragment.create("foo/bar"); - PathFragment foo = PathFragment.create("foo"); - PathFragment empty = PathFragment.create(""); + PathFragment fooBarWiz = create("foo/bar/wiz"); + PathFragment fooBar = create("foo/bar"); + PathFragment foo = create("foo"); + PathFragment empty = create(""); assertThat(fooBarWiz.getParentDirectory()).isEqualTo(fooBar); assertThat(fooBar.getParentDirectory()).isEqualTo(foo); assertThat(foo.getParentDirectory()).isEqualTo(empty); assertThat(empty.getParentDirectory()).isNull(); - PathFragment fooBarWizAbs = PathFragment.create("/foo/bar/wiz"); - PathFragment fooBarAbs = PathFragment.create("/foo/bar"); - PathFragment fooAbs = PathFragment.create("/foo"); - PathFragment rootAbs = PathFragment.create("/"); + PathFragment fooBarWizAbs = create("/foo/bar/wiz"); + PathFragment fooBarAbs = create("/foo/bar"); + PathFragment fooAbs = create("/foo"); + PathFragment rootAbs = create("/"); assertThat(fooBarWizAbs.getParentDirectory()).isEqualTo(fooBarAbs); assertThat(fooBarAbs.getParentDirectory()).isEqualTo(fooAbs); assertThat(fooAbs.getParentDirectory()).isEqualTo(rootAbs); assertThat(rootAbs.getParentDirectory()).isNull(); - - // Note, this is surprising but correct behavior: - assertThat(PathFragment.create("/foo/bar/..").getParentDirectory()).isEqualTo(fooBarAbs); } @Test public void testSegmentsCount() { - assertThat(PathFragment.create("foo/bar").segmentCount()).isEqualTo(2); - assertThat(PathFragment.create("/foo/bar").segmentCount()).isEqualTo(2); - assertThat(PathFragment.create("foo//bar").segmentCount()).isEqualTo(2); - assertThat(PathFragment.create("/foo//bar").segmentCount()).isEqualTo(2); - assertThat(PathFragment.create("foo/").segmentCount()).isEqualTo(1); - assertThat(PathFragment.create("/foo/").segmentCount()).isEqualTo(1); - assertThat(PathFragment.create("foo").segmentCount()).isEqualTo(1); - assertThat(PathFragment.create("/foo").segmentCount()).isEqualTo(1); - assertThat(PathFragment.create("/").segmentCount()).isEqualTo(0); - assertThat(PathFragment.create("").segmentCount()).isEqualTo(0); + assertThat(create("foo/bar").segmentCount()).isEqualTo(2); + assertThat(create("/foo/bar").segmentCount()).isEqualTo(2); + assertThat(create("foo//bar").segmentCount()).isEqualTo(2); + assertThat(create("/foo//bar").segmentCount()).isEqualTo(2); + assertThat(create("foo/").segmentCount()).isEqualTo(1); + assertThat(create("/foo/").segmentCount()).isEqualTo(1); + assertThat(create("foo").segmentCount()).isEqualTo(1); + assertThat(create("/foo").segmentCount()).isEqualTo(1); + assertThat(create("/").segmentCount()).isEqualTo(0); + assertThat(create("").segmentCount()).isEqualTo(0); } @Test public void testGetSegment() { - assertThat(PathFragment.create("foo/bar").getSegment(0)).isEqualTo("foo"); - assertThat(PathFragment.create("foo/bar").getSegment(1)).isEqualTo("bar"); - assertThat(PathFragment.create("/foo/bar").getSegment(0)).isEqualTo("foo"); - assertThat(PathFragment.create("/foo/bar").getSegment(1)).isEqualTo("bar"); - assertThat(PathFragment.create("foo/").getSegment(0)).isEqualTo("foo"); - assertThat(PathFragment.create("/foo/").getSegment(0)).isEqualTo("foo"); - assertThat(PathFragment.create("foo").getSegment(0)).isEqualTo("foo"); - assertThat(PathFragment.create("/foo").getSegment(0)).isEqualTo("foo"); + assertThat(create("foo/bar").getSegment(0)).isEqualTo("foo"); + assertThat(create("foo/bar").getSegment(1)).isEqualTo("bar"); + assertThat(create("/foo/bar").getSegment(0)).isEqualTo("foo"); + assertThat(create("/foo/bar").getSegment(1)).isEqualTo("bar"); + assertThat(create("foo/").getSegment(0)).isEqualTo("foo"); + assertThat(create("/foo/").getSegment(0)).isEqualTo("foo"); + assertThat(create("foo").getSegment(0)).isEqualTo("foo"); + assertThat(create("/foo").getSegment(0)).isEqualTo("foo"); } @Test public void testBasename() throws Exception { - assertThat(PathFragment.create("foo/bar").getBaseName()).isEqualTo("bar"); - assertThat(PathFragment.create("/foo/bar").getBaseName()).isEqualTo("bar"); - assertThat(PathFragment.create("foo/").getBaseName()).isEqualTo("foo"); - assertThat(PathFragment.create("/foo/").getBaseName()).isEqualTo("foo"); - assertThat(PathFragment.create("foo").getBaseName()).isEqualTo("foo"); - assertThat(PathFragment.create("/foo").getBaseName()).isEqualTo("foo"); - assertThat(PathFragment.create("/").getBaseName()).isEmpty(); - assertThat(PathFragment.create("").getBaseName()).isEmpty(); + assertThat(create("foo/bar").getBaseName()).isEqualTo("bar"); + assertThat(create("/foo/bar").getBaseName()).isEqualTo("bar"); + assertThat(create("foo/").getBaseName()).isEqualTo("foo"); + assertThat(create("/foo/").getBaseName()).isEqualTo("foo"); + assertThat(create("foo").getBaseName()).isEqualTo("foo"); + assertThat(create("/foo").getBaseName()).isEqualTo("foo"); + assertThat(create("/").getBaseName()).isEmpty(); + assertThat(create("").getBaseName()).isEmpty(); } @Test public void testFileExtension() throws Exception { - assertThat(PathFragment.create("foo.bar").getFileExtension()).isEqualTo("bar"); - assertThat(PathFragment.create("foo.barr").getFileExtension()).isEqualTo("barr"); - assertThat(PathFragment.create("foo.b").getFileExtension()).isEqualTo("b"); - assertThat(PathFragment.create("foo.").getFileExtension()).isEmpty(); - assertThat(PathFragment.create("foo").getFileExtension()).isEmpty(); - assertThat(PathFragment.create(".").getFileExtension()).isEmpty(); - assertThat(PathFragment.create("").getFileExtension()).isEmpty(); - assertThat(PathFragment.create("foo/bar.baz").getFileExtension()).isEqualTo("baz"); - assertThat(PathFragment.create("foo.bar.baz").getFileExtension()).isEqualTo("baz"); - assertThat(PathFragment.create("foo.bar/baz").getFileExtension()).isEmpty(); - } - - private static void assertPath(String expected, PathFragment actual) { - assertThat(actual.getPathString()).isEqualTo(expected); + assertThat(create("foo.bar").getFileExtension()).isEqualTo("bar"); + assertThat(create("foo.barr").getFileExtension()).isEqualTo("barr"); + assertThat(create("foo.b").getFileExtension()).isEqualTo("b"); + assertThat(create("foo.").getFileExtension()).isEmpty(); + assertThat(create("foo").getFileExtension()).isEmpty(); + assertThat(create(".").getFileExtension()).isEmpty(); + assertThat(create("").getFileExtension()).isEmpty(); + assertThat(create("foo/bar.baz").getFileExtension()).isEqualTo("baz"); + assertThat(create("foo.bar.baz").getFileExtension()).isEqualTo("baz"); + assertThat(create("foo.bar/baz").getFileExtension()).isEmpty(); } @Test public void testReplaceName() throws Exception { - assertPath("foo/baz", PathFragment.create("foo/bar").replaceName("baz")); - assertPath("/foo/baz", PathFragment.create("/foo/bar").replaceName("baz")); - assertPath("foo", PathFragment.create("foo/bar").replaceName("")); - assertPath("baz", PathFragment.create("foo/").replaceName("baz")); - assertPath("/baz", PathFragment.create("/foo/").replaceName("baz")); - assertPath("baz", PathFragment.create("foo").replaceName("baz")); - assertPath("/baz", PathFragment.create("/foo").replaceName("baz")); - assertThat(PathFragment.create("/").replaceName("baz")).isNull(); - assertThat(PathFragment.create("/").replaceName("")).isNull(); - assertThat(PathFragment.create("").replaceName("baz")).isNull(); - assertThat(PathFragment.create("").replaceName("")).isNull(); - - assertPath("foo/bar/baz", PathFragment.create("foo/bar").replaceName("bar/baz")); - assertPath("foo/bar/baz", PathFragment.create("foo/bar").replaceName("bar/baz/")); + assertThat(create("foo/bar").replaceName("baz").getPathString()).isEqualTo("foo/baz"); + assertThat(create("/foo/bar").replaceName("baz").getPathString()).isEqualTo("/foo/baz"); + assertThat(create("foo/bar").replaceName("").getPathString()).isEqualTo("foo"); + assertThat(create("foo/").replaceName("baz").getPathString()).isEqualTo("baz"); + assertThat(create("/foo/").replaceName("baz").getPathString()).isEqualTo("/baz"); + assertThat(create("foo").replaceName("baz").getPathString()).isEqualTo("baz"); + assertThat(create("/foo").replaceName("baz").getPathString()).isEqualTo("/baz"); + assertThat(create("/").replaceName("baz")).isNull(); + assertThat(create("/").replaceName("")).isNull(); + assertThat(create("").replaceName("baz")).isNull(); + assertThat(create("").replaceName("")).isNull(); + + assertThat(create("foo/bar").replaceName("bar/baz").getPathString()).isEqualTo("foo/bar/baz"); + assertThat(create("foo/bar").replaceName("bar/baz/").getPathString()).isEqualTo("foo/bar/baz"); // Absolute path arguments will clobber the original path. - assertPath("/absolute", PathFragment.create("foo/bar").replaceName("/absolute")); - assertPath("/", PathFragment.create("foo/bar").replaceName("/")); + assertThat(create("foo/bar").replaceName("/absolute").getPathString()).isEqualTo("/absolute"); + assertThat(create("foo/bar").replaceName("/").getPathString()).isEqualTo("/"); } @Test public void testSubFragment() throws Exception { - assertPath("/foo/bar/baz", - PathFragment.create("/foo/bar/baz").subFragment(0, 3)); - assertPath("foo/bar/baz", - PathFragment.create("foo/bar/baz").subFragment(0, 3)); - assertPath("/foo/bar", - PathFragment.create("/foo/bar/baz").subFragment(0, 2)); - assertPath("bar/baz", - PathFragment.create("/foo/bar/baz").subFragment(1, 3)); - assertPath("/foo", - PathFragment.create("/foo/bar/baz").subFragment(0, 1)); - assertPath("bar", - PathFragment.create("/foo/bar/baz").subFragment(1, 2)); - assertPath("baz", PathFragment.create("/foo/bar/baz").subFragment(2, 3)); - assertPath("/", PathFragment.create("/foo/bar/baz").subFragment(0, 0)); - assertPath("", PathFragment.create("foo/bar/baz").subFragment(0, 0)); - assertPath("", PathFragment.create("foo/bar/baz").subFragment(1, 1)); - assertPath("/foo/bar/baz", PathFragment.create("/foo/bar/baz").subFragment(0)); - assertPath("bar/baz", PathFragment.create("/foo/bar/baz").subFragment(1)); - try { - fail("unexpectedly succeeded: " + PathFragment.create("foo/bar/baz").subFragment(3, 2)); - } catch (IndexOutOfBoundsException e) { /* Expected. */ } - try { - fail("unexpectedly succeeded: " + PathFragment.create("foo/bar/baz").subFragment(4, 4)); - } catch (IndexOutOfBoundsException e) { /* Expected. */ } + assertThat(create("/foo/bar/baz").subFragment(0, 3).getPathString()).isEqualTo("/foo/bar/baz"); + assertThat(create("foo/bar/baz").subFragment(0, 3).getPathString()).isEqualTo("foo/bar/baz"); + assertThat(create("/foo/bar/baz").subFragment(0, 2).getPathString()).isEqualTo("/foo/bar"); + assertThat(create("/foo/bar/baz").subFragment(1, 3).getPathString()).isEqualTo("bar/baz"); + assertThat(create("/foo/bar/baz").subFragment(0, 1).getPathString()).isEqualTo("/foo"); + assertThat(create("/foo/bar/baz").subFragment(1, 2).getPathString()).isEqualTo("bar"); + assertThat(create("/foo/bar/baz").subFragment(2, 3).getPathString()).isEqualTo("baz"); + assertThat(create("/foo/bar/baz").subFragment(0, 0).getPathString()).isEqualTo("/"); + assertThat(create("foo/bar/baz").subFragment(0, 0).getPathString()).isEqualTo(""); + assertThat(create("foo/bar/baz").subFragment(1, 1).getPathString()).isEqualTo(""); + + assertThat(create("/foo/bar/baz").subFragment(0).getPathString()).isEqualTo("/foo/bar/baz"); + assertThat(create("foo/bar/baz").subFragment(0).getPathString()).isEqualTo("foo/bar/baz"); + assertThat(create("/foo/bar/baz").subFragment(1).getPathString()).isEqualTo("bar/baz"); + assertThat(create("foo/bar/baz").subFragment(1).getPathString()).isEqualTo("bar/baz"); + assertThat(create("foo/bar/baz").subFragment(2).getPathString()).isEqualTo("baz"); + assertThat(create("foo/bar/baz").subFragment(3).getPathString()).isEqualTo(""); + + MoreAsserts.expectThrows( + IndexOutOfBoundsException.class, () -> create("foo/bar/baz").subFragment(3, 2)); + MoreAsserts.expectThrows( + IndexOutOfBoundsException.class, () -> create("foo/bar/baz").subFragment(4, 4)); + MoreAsserts.expectThrows( + IndexOutOfBoundsException.class, () -> create("foo/bar/baz").subFragment(3, 2)); + MoreAsserts.expectThrows( + IndexOutOfBoundsException.class, () -> create("foo/bar/baz").subFragment(4)); } @Test public void testStartsWith() { - PathFragment foobar = PathFragment.create("/foo/bar"); - PathFragment foobarRelative = PathFragment.create("foo/bar"); + PathFragment foobar = create("/foo/bar"); + PathFragment foobarRelative = create("foo/bar"); // (path, prefix) => true assertThat(foobar.startsWith(foobar)).isTrue(); - assertThat(foobar.startsWith(PathFragment.create("/"))).isTrue(); - assertThat(foobar.startsWith(PathFragment.create("/foo"))).isTrue(); - assertThat(foobar.startsWith(PathFragment.create("/foo/"))).isTrue(); - assertThat(foobar.startsWith(PathFragment.create("/foo/bar/"))) - .isTrue(); // Includes trailing slash. + assertThat(foobar.startsWith(create("/"))).isTrue(); + assertThat(foobar.startsWith(create("/foo"))).isTrue(); + assertThat(foobar.startsWith(create("/foo/"))).isTrue(); + assertThat(foobar.startsWith(create("/foo/bar/"))).isTrue(); // Includes trailing slash. // (prefix, path) => false - assertThat(PathFragment.create("/foo").startsWith(foobar)).isFalse(); - assertThat(PathFragment.create("/").startsWith(foobar)).isFalse(); + assertThat(create("/foo").startsWith(foobar)).isFalse(); + assertThat(create("/").startsWith(foobar)).isFalse(); // (absolute, relative) => false assertThat(foobar.startsWith(foobarRelative)).isFalse(); @@ -373,69 +339,53 @@ public class PathFragmentTest { // (relative path, relative prefix) => true assertThat(foobarRelative.startsWith(foobarRelative)).isTrue(); - assertThat(foobarRelative.startsWith(PathFragment.create("foo"))).isTrue(); - assertThat(foobarRelative.startsWith(PathFragment.create(""))).isTrue(); + assertThat(foobarRelative.startsWith(create("foo"))).isTrue(); + assertThat(foobarRelative.startsWith(create(""))).isTrue(); // (path, sibling) => false - assertThat(PathFragment.create("/foo/wiz").startsWith(foobar)).isFalse(); - assertThat(foobar.startsWith(PathFragment.create("/foo/wiz"))).isFalse(); - - // Does not normalize. - PathFragment foodotbar = PathFragment.create("foo/./bar"); - assertThat(foodotbar.startsWith(foodotbar)).isTrue(); - assertThat(foodotbar.startsWith(PathFragment.create("foo/."))).isTrue(); - assertThat(foodotbar.startsWith(PathFragment.create("foo/./"))).isTrue(); - assertThat(foodotbar.startsWith(PathFragment.create("foo/./bar"))).isTrue(); - assertThat(foodotbar.startsWith(PathFragment.create("foo/bar"))).isFalse(); + assertThat(create("/foo/wiz").startsWith(foobar)).isFalse(); + assertThat(foobar.startsWith(create("/foo/wiz"))).isFalse(); } @Test public void testCheckAllPathsStartWithButAreNotEqualTo() { // Check passes: - PathFragment.checkAllPathsAreUnder(toPathsSet("a/b", "a/c"), - PathFragment.create("a")); + PathFragment.checkAllPathsAreUnder(toPathsSet("a/b", "a/c"), create("a")); // Check trivially passes: - PathFragment.checkAllPathsAreUnder(ImmutableList.<PathFragment>of(), - PathFragment.create("a")); + PathFragment.checkAllPathsAreUnder(ImmutableList.<PathFragment>of(), create("a")); // Check fails when some path does not start with startingWithPath: - try { - PathFragment.checkAllPathsAreUnder(toPathsSet("a/b", "b/c"), - PathFragment.create("a")); - fail(); - } catch (IllegalArgumentException expected) { - } + MoreAsserts.expectThrows( + IllegalArgumentException.class, + () -> PathFragment.checkAllPathsAreUnder(toPathsSet("a/b", "b/c"), create("a"))); // Check fails when some path is equal to startingWithPath: - try { - PathFragment.checkAllPathsAreUnder(toPathsSet("a/b", "a"), - PathFragment.create("a")); - fail(); - } catch (IllegalArgumentException expected) { - } + MoreAsserts.expectThrows( + IllegalArgumentException.class, + () -> PathFragment.checkAllPathsAreUnder(toPathsSet("a/b", "a"), create("a"))); } @Test public void testEndsWith() { - PathFragment foobar = PathFragment.create("/foo/bar"); - PathFragment foobarRelative = PathFragment.create("foo/bar"); + PathFragment foobar = create("/foo/bar"); + PathFragment foobarRelative = create("foo/bar"); // (path, suffix) => true assertThat(foobar.endsWith(foobar)).isTrue(); - assertThat(foobar.endsWith(PathFragment.create("bar"))).isTrue(); - assertThat(foobar.endsWith(PathFragment.create("foo/bar"))).isTrue(); - assertThat(foobar.endsWith(PathFragment.create("/foo/bar"))).isTrue(); - assertThat(foobar.endsWith(PathFragment.create("/bar"))).isFalse(); + assertThat(foobar.endsWith(create("bar"))).isTrue(); + assertThat(foobar.endsWith(create("foo/bar"))).isTrue(); + assertThat(foobar.endsWith(create("/foo/bar"))).isTrue(); + assertThat(foobar.endsWith(create("/bar"))).isFalse(); // (prefix, path) => false - assertThat(PathFragment.create("/foo").endsWith(foobar)).isFalse(); - assertThat(PathFragment.create("/").endsWith(foobar)).isFalse(); + assertThat(create("/foo").endsWith(foobar)).isFalse(); + assertThat(create("/").endsWith(foobar)).isFalse(); // (suffix, path) => false - assertThat(PathFragment.create("/bar").endsWith(foobar)).isFalse(); - assertThat(PathFragment.create("bar").endsWith(foobar)).isFalse(); - assertThat(PathFragment.create("").endsWith(foobar)).isFalse(); + assertThat(create("/bar").endsWith(foobar)).isFalse(); + assertThat(create("bar").endsWith(foobar)).isFalse(); + assertThat(create("").endsWith(foobar)).isFalse(); // (absolute, relative) => true assertThat(foobar.endsWith(foobarRelative)).isTrue(); @@ -445,18 +395,32 @@ public class PathFragmentTest { // (relative path, relative prefix) => true assertThat(foobarRelative.endsWith(foobarRelative)).isTrue(); - assertThat(foobarRelative.endsWith(PathFragment.create("bar"))).isTrue(); - assertThat(foobarRelative.endsWith(PathFragment.create(""))).isTrue(); + assertThat(foobarRelative.endsWith(create("bar"))).isTrue(); + assertThat(foobarRelative.endsWith(create(""))).isTrue(); // (path, sibling) => false - assertThat(PathFragment.create("/foo/wiz").endsWith(foobar)).isFalse(); - assertThat(foobar.endsWith(PathFragment.create("/foo/wiz"))).isFalse(); + assertThat(create("/foo/wiz").endsWith(foobar)).isFalse(); + assertThat(foobar.endsWith(create("/foo/wiz"))).isFalse(); + } + + @Test + public void testToRelative() { + assertThat(create("/foo/bar").toRelative()).isEqualTo(create("foo/bar")); + assertThat(create("/").toRelative()).isEqualTo(create("")); + MoreAsserts.expectThrows(IllegalArgumentException.class, () -> create("foo").toRelative()); + } + + @Test + public void testGetDriveStr() { + assertThat(create("/foo/bar").getDriveStr()).isEqualTo("/"); + assertThat(create("/").getDriveStr()).isEqualTo("/"); + MoreAsserts.expectThrows(IllegalArgumentException.class, () -> create("foo").getDriveStr()); } static List<PathFragment> toPaths(List<String> strs) { List<PathFragment> paths = Lists.newArrayList(); for (String s : strs) { - paths.add(PathFragment.create(s)); + paths.add(create(s)); } return paths; } @@ -464,15 +428,26 @@ public class PathFragmentTest { static ImmutableSet<PathFragment> toPathsSet(String... strs) { ImmutableSet.Builder<PathFragment> builder = ImmutableSet.builder(); for (String str : strs) { - builder.add(PathFragment.create(str)); + builder.add(create(str)); } return builder.build(); } @Test public void testCompareTo() throws Exception { - List<String> pathStrs = ImmutableList.of( - "", "/", "//", ".", "/./", "foo/.//bar", "foo", "/foo", "foo/bar", "foo/Bar", "Foo/bar"); + List<String> pathStrs = + ImmutableList.of( + "", + "/", + "foo", + "/foo", + "foo/bar", + "foo.bar", + "foo/bar.baz", + "foo/bar/baz", + "foo/barfile", + "foo/Bar", + "Foo/bar"); List<PathFragment> paths = toPaths(pathStrs); // First test that compareTo is self-consistent. for (PathFragment x : paths) { @@ -493,36 +468,80 @@ public class PathFragmentTest { } } } - // Now test that compareTo does what we expect. The exact ordering here doesn't matter much, - // but there are three things to notice: 1. absolute < relative, 2. comparison is lexicographic - // 3. repeated slashes are ignored. (PathFragment("//") prints as "/"). + // Now test that compareTo does what we expect. The exact ordering here doesn't matter much. Collections.shuffle(paths); Collections.sort(paths); - List<PathFragment> expectedOrder = toPaths(ImmutableList.of( - "/", "//", "/./", "/foo", "", ".", "Foo/bar", "foo", "foo/.//bar", "foo/Bar", "foo/bar")); + List<PathFragment> expectedOrder = + toPaths( + ImmutableList.of( + "", + "/", + "/foo", + "Foo/bar", + "foo", + "foo.bar", + "foo/Bar", + "foo/bar", + "foo/bar.baz", + "foo/bar/baz", + "foo/barfile")); assertThat(paths).isEqualTo(expectedOrder); } @Test public void testGetSafePathString() { - assertThat(PathFragment.create("/").getSafePathString()).isEqualTo("/"); - assertThat(PathFragment.create("/abc").getSafePathString()).isEqualTo("/abc"); - assertThat(PathFragment.create("").getSafePathString()).isEqualTo("."); + assertThat(create("/").getSafePathString()).isEqualTo("/"); + assertThat(create("/abc").getSafePathString()).isEqualTo("/abc"); + assertThat(create("").getSafePathString()).isEqualTo("."); assertThat(PathFragment.EMPTY_FRAGMENT.getSafePathString()).isEqualTo("."); - assertThat(PathFragment.create("abc/def").getSafePathString()).isEqualTo("abc/def"); + assertThat(create("abc/def").getSafePathString()).isEqualTo("abc/def"); } @Test public void testNormalize() { - assertThat(PathFragment.create("/a/b").normalize()).isEqualTo(PathFragment.create("/a/b")); - assertThat(PathFragment.create("/a/./b").normalize()).isEqualTo(PathFragment.create("/a/b")); - assertThat(PathFragment.create("/a/../b").normalize()).isEqualTo(PathFragment.create("/b")); - assertThat(PathFragment.create("a/b").normalize()).isEqualTo(PathFragment.create("a/b")); - assertThat(PathFragment.create("a/../../b").normalize()).isEqualTo(PathFragment.create("../b")); - assertThat(PathFragment.create("a/../..").normalize()).isEqualTo(PathFragment.create("..")); - assertThat(PathFragment.create("a/../b").normalize()).isEqualTo(PathFragment.create("b")); - assertThat(PathFragment.create("a/b/../b").normalize()).isEqualTo(PathFragment.create("a/b")); - assertThat(PathFragment.create("/..").normalize()).isEqualTo(PathFragment.create("/..")); + assertThat(create("/a/b")).isEqualTo(create("/a/b")); + assertThat(create("/a/./b")).isEqualTo(create("/a/b")); + assertThat(create("/a/../b")).isEqualTo(create("/b")); + assertThat(create("a/b")).isEqualTo(create("a/b")); + assertThat(create("a/../../b")).isEqualTo(create("../b")); + assertThat(create("a/../..")).isEqualTo(create("..")); + assertThat(create("a/../b")).isEqualTo(create("b")); + assertThat(create("a/b/../b")).isEqualTo(create("a/b")); + assertThat(create("/..")).isEqualTo(create("/..")); + assertThat(create("..")).isEqualTo(create("..")); + } + + @Test + public void testSegments() { + assertThat(create("").segmentCount()).isEqualTo(0); + assertThat(create("a").segmentCount()).isEqualTo(1); + assertThat(create("a/b").segmentCount()).isEqualTo(2); + assertThat(create("a/b/c").segmentCount()).isEqualTo(3); + assertThat(create("/").segmentCount()).isEqualTo(0); + assertThat(create("/a").segmentCount()).isEqualTo(1); + assertThat(create("/a/b").segmentCount()).isEqualTo(2); + assertThat(create("/a/b/c").segmentCount()).isEqualTo(3); + + assertThat(create("").getSegments()).isEmpty(); + assertThat(create("a").getSegments()).containsExactly("a").inOrder(); + assertThat(create("a/b").getSegments()).containsExactly("a", "b").inOrder(); + assertThat(create("a/b/c").getSegments()).containsExactly("a", "b", "c").inOrder(); + assertThat(create("/").getSegments()).isEmpty(); + assertThat(create("/a").getSegments()).containsExactly("a").inOrder(); + assertThat(create("/a/b").getSegments()).containsExactly("a", "b").inOrder(); + assertThat(create("/a/b/c").getSegments()).containsExactly("a", "b", "c").inOrder(); + + assertThat(create("a").getSegment(0)).isEqualTo("a"); + assertThat(create("a/b").getSegment(0)).isEqualTo("a"); + assertThat(create("a/b").getSegment(1)).isEqualTo("b"); + assertThat(create("a/b/c").getSegment(2)).isEqualTo("c"); + assertThat(create("/a").getSegment(0)).isEqualTo("a"); + assertThat(create("/a/b").getSegment(0)).isEqualTo("a"); + assertThat(create("/a/b").getSegment(1)).isEqualTo("b"); + assertThat(create("/a/b/c").getSegment(2)).isEqualTo("c"); + + MoreAsserts.expectThrows(IllegalArgumentException.class, () -> create("").getSegment(0)); + MoreAsserts.expectThrows(IllegalArgumentException.class, () -> create("a/b").getSegment(2)); } @Test @@ -552,7 +571,7 @@ public class PathFragmentTest { } private void checkSerialization(String pathFragmentString, int expectedSize) throws Exception { - PathFragment a = PathFragment.create(pathFragmentString); + PathFragment a = create(pathFragmentString); byte[] sa = TestUtils.serializeObject(a); assertThat(sa).hasLength(expectedSize); diff --git a/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentWindowsTest.java b/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentWindowsTest.java index 1f18d07475..b2a8a52977 100644 --- a/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentWindowsTest.java +++ b/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentWindowsTest.java @@ -14,9 +14,9 @@ package com.google.devtools.build.lib.vfs; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; -import static org.junit.Assert.fail; +import static com.google.devtools.build.lib.vfs.PathFragment.create; +import com.google.devtools.build.lib.testutil.MoreAsserts; import java.io.File; import org.junit.Test; import org.junit.runner.RunWith; @@ -30,303 +30,199 @@ public class PathFragmentWindowsTest { @Test public void testWindowsSeparator() { - assertThat(PathFragment.create("bar\\baz").toString()).isEqualTo("bar/baz"); - assertThat(PathFragment.create("c:\\bar\\baz").toString()).isEqualTo("C:/bar/baz"); + assertThat(create("bar\\baz").toString()).isEqualTo("bar/baz"); + assertThat(create("c:\\bar\\baz").toString()).isEqualTo("C:/bar/baz"); } @Test public void testIsAbsoluteWindows() { - assertThat(PathFragment.create("C:/").isAbsolute()).isTrue(); - assertThat(PathFragment.create("C:/").isAbsolute()).isTrue(); - assertThat(PathFragment.create("C:/foo").isAbsolute()).isTrue(); - assertThat(PathFragment.create("d:/foo/bar").isAbsolute()).isTrue(); + assertThat(create("C:/").isAbsolute()).isTrue(); + assertThat(create("C:/").isAbsolute()).isTrue(); + assertThat(create("C:/foo").isAbsolute()).isTrue(); + assertThat(create("d:/foo/bar").isAbsolute()).isTrue(); - assertThat(PathFragment.create("*:/").isAbsolute()).isFalse(); + assertThat(create("*:/").isAbsolute()).isFalse(); } @Test public void testAbsoluteAndAbsoluteLookingPaths() { - PathFragment p1 = PathFragment.create("/c"); - assertThat(p1.isAbsolute()).isTrue(); - assertThat(p1.getDriveLetter()).isEqualTo('\0'); - assertThat(p1.getSegments()).containsExactly("c"); - - PathFragment p2 = PathFragment.create("/c/"); - assertThat(p2.isAbsolute()).isTrue(); - assertThat(p2.getDriveLetter()).isEqualTo('\0'); - assertThat(p2.getSegments()).containsExactly("c"); - - PathFragment p3 = PathFragment.create("C:/"); - assertThat(p3.isAbsolute()).isTrue(); - assertThat(p3.getDriveLetter()).isEqualTo('C'); - assertThat(p3.getSegments()).isEmpty(); - - PathFragment p5 = PathFragment.create("/c:"); + assertThat(create("/c").isAbsolute()).isTrue(); + assertThat(create("/c").getSegments()).containsExactly("c"); + + assertThat(create("/c/").isAbsolute()).isTrue(); + assertThat(create("/c/").getSegments()).containsExactly("c"); + + assertThat(create("C:/").isAbsolute()).isTrue(); + assertThat(create("C:/").getSegments()).isEmpty(); + + PathFragment p5 = create("/c:"); assertThat(p5.isAbsolute()).isTrue(); - assertThat(p5.getDriveLetter()).isEqualTo('\0'); assertThat(p5.getSegments()).containsExactly("c:"); + assertThat(create("C:").isAbsolute()).isFalse(); - assertThat(p1).isEqualTo(p2); - assertThat(p1).isNotEqualTo(p3); - assertThat(p1).isNotEqualTo(p5); - assertThat(p3).isNotEqualTo(p5); - } + assertThat(create("/c:").isAbsolute()).isTrue(); + assertThat(create("/c:").getSegments()).containsExactly("c:"); - @Test - public void testIsAbsoluteWindowsBackslash() { - assertThat(PathFragment.create(new File("C:\\blah")).isAbsolute()).isTrue(); - assertThat(PathFragment.create(new File("C:\\")).isAbsolute()).isTrue(); - assertThat(PathFragment.create(new File("\\blah")).isAbsolute()).isTrue(); - assertThat(PathFragment.create(new File("\\")).isAbsolute()).isTrue(); + assertThat(create("/c")).isEqualTo(create("/c/")); + assertThat(create("/c")).isNotEqualTo(create("C:/")); + assertThat(create("/c")).isNotEqualTo(create("C:")); + assertThat(create("/c")).isNotEqualTo(create("/c:")); + assertThat(create("C:/")).isNotEqualTo(create("C:")); + assertThat(create("C:/")).isNotEqualTo(create("/c:")); } @Test - public void testIsNormalizedWindows() { - assertThat(PathFragment.create("C:/").isNormalized()).isTrue(); - assertThat(PathFragment.create("C:/absolute/path").isNormalized()).isTrue(); - assertThat(PathFragment.create("C:/absolute/./path").isNormalized()).isFalse(); - assertThat(PathFragment.create("C:/absolute/../path").isNormalized()).isFalse(); + public void testIsAbsoluteWindowsBackslash() { + assertThat(create(new File("C:\\blah").getPath()).isAbsolute()).isTrue(); + assertThat(create(new File("C:\\").getPath()).isAbsolute()).isTrue(); + assertThat(create(new File("\\blah").getPath()).isAbsolute()).isTrue(); + assertThat(create(new File("\\").getPath()).isAbsolute()).isTrue(); } @Test public void testRootNodeReturnsRootStringWindows() { - PathFragment rootFragment = PathFragment.create("C:/"); - assertThat(rootFragment.getPathString()).isEqualTo("C:/"); + assertThat(create("C:/").getPathString()).isEqualTo("C:/"); } @Test public void testGetRelativeWindows() { - assertThat(PathFragment.create("C:/a").getRelative("b").getPathString()).isEqualTo("C:/a/b"); - assertThat(PathFragment.create("C:/a/b").getRelative("c/d").getPathString()) - .isEqualTo("C:/a/b/c/d"); - assertThat(PathFragment.create("C:/a").getRelative("C:/b").getPathString()).isEqualTo("C:/b"); - assertThat(PathFragment.create("C:/a/b").getRelative("C:/c/d").getPathString()) - .isEqualTo("C:/c/d"); - assertThat(PathFragment.create("a").getRelative("C:/b").getPathString()).isEqualTo("C:/b"); - assertThat(PathFragment.create("a/b").getRelative("C:/c/d").getPathString()) - .isEqualTo("C:/c/d"); - } - - private void assertGetRelative(String path, String relative, PathFragment expected) - throws Exception { - PathFragment actual = PathFragment.create(path).getRelative(relative); - assertThat(actual.getPathString()).isEqualTo(expected.getPathString()); - assertThat(actual).isEqualTo(expected); - assertThat(actual.getDriveLetter()).isEqualTo(expected.getDriveLetter()); - assertThat(actual.hashCode()).isEqualTo(expected.hashCode()); - } - - private void assertRelativeTo(String path, String relativeTo, String... expectedPathSegments) - throws Exception { - PathFragment expected = PathFragment.createAlreadyInterned('\0', false, expectedPathSegments); - PathFragment actual = PathFragment.create(path).relativeTo(relativeTo); - assertThat(actual.getPathString()).isEqualTo(expected.getPathString()); - assertThat(actual).isEqualTo(expected); - assertThat(actual.getDriveLetter()).isEqualTo(expected.getDriveLetter()); - assertThat(actual.hashCode()).isEqualTo(expected.hashCode()); - } - - private void assertCantComputeRelativeTo(String path, String relativeTo) throws Exception { - try { - PathFragment.create(path).relativeTo(relativeTo); - fail("expected failure"); - } catch (Exception e) { - assertThat(e).hasMessageThat().contains("is not beneath"); - } - } - - private static PathFragment makePath(char drive, boolean absolute, String... segments) { - return PathFragment.createAlreadyInterned(drive, absolute, segments); + assertThat(create("C:/a").getRelative("b").getPathString()).isEqualTo("C:/a/b"); + assertThat(create("C:/a/b").getRelative("c/d").getPathString()).isEqualTo("C:/a/b/c/d"); + assertThat(create("C:/a").getRelative("C:/b").getPathString()).isEqualTo("C:/b"); + assertThat(create("C:/a/b").getRelative("C:/c/d").getPathString()).isEqualTo("C:/c/d"); + assertThat(create("a").getRelative("C:/b").getPathString()).isEqualTo("C:/b"); + assertThat(create("a/b").getRelative("C:/c/d").getPathString()).isEqualTo("C:/c/d"); } @Test public void testGetRelativeMixed() throws Exception { - assertGetRelative("a", "b", makePath('\0', false, "a", "b")); - assertGetRelative("a", "/b", makePath('\0', true, "b")); - assertGetRelative("a", "E:/b", makePath('E', true, "b")); + assertThat(create("a").getRelative("b")).isEqualTo(create("a/b")); + assertThat(create("a").getRelative("/b")).isEqualTo(create("/b")); + assertThat(create("a").getRelative("E:/b")).isEqualTo(create("E:/b")); - assertGetRelative("/a", "b", makePath('\0', true, "a", "b")); - assertGetRelative("/a", "/b", makePath('\0', true, "b")); - assertGetRelative("/a", "E:/b", makePath('E', true, "b")); + assertThat(create("/a").getRelative("b")).isEqualTo(create("/a/b")); + assertThat(create("/a").getRelative("/b")).isEqualTo(create("/b")); + assertThat(create("/a").getRelative("E:/b")).isEqualTo(create("E:/b")); - assertGetRelative("D:/a", "b", makePath('D', true, "a", "b")); - assertGetRelative("D:/a", "/b", makePath('D', true, "b")); - assertGetRelative("D:/a", "E:/b", makePath('E', true, "b")); + assertThat(create("D:/a").getRelative("b")).isEqualTo(create("D:/a/b")); + assertThat(create("D:/a").getRelative("/b")).isEqualTo(create("/b")); + assertThat(create("D:/a").getRelative("E:/b")).isEqualTo(create("E:/b")); } @Test public void testRelativeTo() throws Exception { - assertRelativeTo("", ""); - assertCantComputeRelativeTo("", "a"); + assertThat(create("").relativeTo("").getPathString()).isEqualTo(""); + MoreAsserts.expectThrows(IllegalArgumentException.class, () -> create("").relativeTo("a")); - assertRelativeTo("a", "", "a"); - assertRelativeTo("a", "a"); - assertCantComputeRelativeTo("a", "b"); - assertRelativeTo("a/b", "a", "b"); + assertThat(create("a").relativeTo("")).isEqualTo(create("a")); + assertThat(create("a").relativeTo("a").getPathString()).isEqualTo(""); + MoreAsserts.expectThrows(IllegalArgumentException.class, () -> create("a").relativeTo("b")); + assertThat(create("a/b").relativeTo("a")).isEqualTo(create("b")); - assertCantComputeRelativeTo("C:/", ""); - assertRelativeTo("C:/", "C:/"); + MoreAsserts.expectThrows(IllegalArgumentException.class, () -> create("C:/").relativeTo("")); + assertThat(create("C:/").relativeTo("C:/").getPathString()).isEqualTo(""); } @Test public void testGetChildWorks() { - PathFragment pf = PathFragment.create("../some/path"); - assertThat(pf.getChild("hi")).isEqualTo(PathFragment.create("../some/path/hi")); - } - - // Tests after here test the canonicalization - private void assertRegular(String expected, String actual) { - PathFragment exp = PathFragment.create(expected); - PathFragment act = PathFragment.create(actual); - assertThat(exp.getPathString()).isEqualTo(expected); - assertThat(act.getPathString()).isEqualTo(expected); - assertThat(act).isEqualTo(exp); - assertThat(act.hashCode()).isEqualTo(exp.hashCode()); + assertThat(create("../some/path").getChild("hi")).isEqualTo(create("../some/path/hi")); } @Test public void testEmptyPathToEmptyPathWindows() { - assertRegular("C:/", "C:/"); - } - - private void assertAllEqual(PathFragment... ps) { - assertThat(ps.length).isGreaterThan(1); - for (int i = 1; i < ps.length; i++) { - String msg = "comparing items 0 and " + i; - assertWithMessage(msg + " for getPathString") - .that(ps[i].getPathString()) - .isEqualTo(ps[0].getPathString()); - assertWithMessage(msg + " for equals").that(ps[0]).isEqualTo(ps[i]); - assertWithMessage(msg + " for hashCode").that(ps[0].hashCode()).isEqualTo(ps[i].hashCode()); - } - } - - @Test - public void testEmptyRelativePathToEmptyPathWindows() { - // Surprising but correct behavior: a PathFragment made of just a drive identifier (and not the - // absolute path "C:/") is equal not only to the empty fragment, but (therefore) also to other - // drive identifiers. - // This makes sense if you consider that these are still empty paths, the drive letter adds no - // information to the path itself. - assertAllEqual( - PathFragment.EMPTY_FRAGMENT, - PathFragment.createAlreadyInterned('\0', false, new String[0]), - PathFragment.createAlreadyInterned('C', false, new String[0]), - PathFragment.createAlreadyInterned('D', false, new String[0])); - assertAllEqual(PathFragment.create("/c"), PathFragment.create("/c/")); - assertThat(PathFragment.create("C:/")).isNotEqualTo(PathFragment.create("/c")); - assertThat(PathFragment.create("C:/foo")).isNotEqualTo(PathFragment.create("/c/foo")); - - assertThat(PathFragment.create("C:/")).isNotEqualTo(PathFragment.create("C:")); - assertThat(PathFragment.create("C:/").getPathString()) - .isNotEqualTo(PathFragment.create("C:").getPathString()); + assertThat(create("C:/")).isEqualTo(create("C:/")); } @Test public void testWindowsVolumeUppercase() { - assertRegular("C:/", "c:/"); + assertThat(create("C:/")).isEqualTo(create("c:/")); } @Test public void testRedundantSlashesWindows() { - assertRegular("C:/", "C:///"); - assertRegular("C:/foo/bar", "C:/foo///bar"); - assertRegular("C:/foo/bar", "C:////foo//bar"); + assertThat(create("C:/")).isEqualTo(create("C:///")); + assertThat(create("C:/foo/bar")).isEqualTo(create("C:/foo///bar")); + assertThat(create("C:/foo/bar")).isEqualTo(create("C:////foo//bar")); } @Test public void testSimpleNameToSimpleNameWindows() { - assertRegular("C:/foo", "C:/foo"); + assertThat(create("C:/foo")).isEqualTo(create("C:/foo")); } @Test public void testStripsTrailingSlashWindows() { - assertRegular("C:/foo/bar", "C:/foo/bar/"); + assertThat(create("C:/foo/bar")).isEqualTo(create("C:/foo/bar/")); } @Test public void testGetParentDirectoryWindows() { - PathFragment fooBarWizAbs = PathFragment.create("C:/foo/bar/wiz"); - PathFragment fooBarAbs = PathFragment.create("C:/foo/bar"); - PathFragment fooAbs = PathFragment.create("C:/foo"); - PathFragment rootAbs = PathFragment.create("C:/"); - assertThat(fooBarWizAbs.getParentDirectory()).isEqualTo(fooBarAbs); - assertThat(fooBarAbs.getParentDirectory()).isEqualTo(fooAbs); - assertThat(fooAbs.getParentDirectory()).isEqualTo(rootAbs); - assertThat(rootAbs.getParentDirectory()).isNull(); - - // Note, this is suprising but correct behaviour: - assertThat(PathFragment.create("C:/foo/bar/..").getParentDirectory()).isEqualTo(fooBarAbs); + assertThat(create("C:/foo/bar/wiz").getParentDirectory()).isEqualTo(create("C:/foo/bar")); + assertThat(create("C:/foo/bar").getParentDirectory()).isEqualTo(create("C:/foo")); + assertThat(create("C:/foo").getParentDirectory()).isEqualTo(create("C:/")); + assertThat(create("C:/").getParentDirectory()).isNull(); } @Test public void testSegmentsCountWindows() { - assertThat(PathFragment.create("C:/foo").segmentCount()).isEqualTo(1); - assertThat(PathFragment.create("C:/").segmentCount()).isEqualTo(0); + assertThat(create("C:/foo").segmentCount()).isEqualTo(1); + assertThat(create("C:/").segmentCount()).isEqualTo(0); } @Test public void testGetSegmentWindows() { - assertThat(PathFragment.create("C:/foo/bar").getSegment(0)).isEqualTo("foo"); - assertThat(PathFragment.create("C:/foo/bar").getSegment(1)).isEqualTo("bar"); - assertThat(PathFragment.create("C:/foo/").getSegment(0)).isEqualTo("foo"); - assertThat(PathFragment.create("C:/foo").getSegment(0)).isEqualTo("foo"); + assertThat(create("C:/foo/bar").getSegment(0)).isEqualTo("foo"); + assertThat(create("C:/foo/bar").getSegment(1)).isEqualTo("bar"); + assertThat(create("C:/foo/").getSegment(0)).isEqualTo("foo"); + assertThat(create("C:/foo").getSegment(0)).isEqualTo("foo"); } @Test public void testBasenameWindows() throws Exception { - assertThat(PathFragment.create("C:/foo/bar").getBaseName()).isEqualTo("bar"); - assertThat(PathFragment.create("C:/foo").getBaseName()).isEqualTo("foo"); + assertThat(create("C:/foo/bar").getBaseName()).isEqualTo("bar"); + assertThat(create("C:/foo").getBaseName()).isEqualTo("foo"); // Never return the drive name as a basename. - assertThat(PathFragment.create("C:/").getBaseName()).isEmpty(); - } - - private static void assertPath(String expected, PathFragment actual) { - assertThat(actual.getPathString()).isEqualTo(expected); + assertThat(create("C:/").getBaseName()).isEmpty(); } @Test public void testReplaceNameWindows() throws Exception { - assertPath("C:/foo/baz", PathFragment.create("C:/foo/bar").replaceName("baz")); - assertThat(PathFragment.create("C:/").replaceName("baz")).isNull(); + assertThat(create("C:/foo/bar").replaceName("baz").getPathString()).isEqualTo("C:/foo/baz"); + assertThat(create("C:/").replaceName("baz")).isNull(); } @Test public void testStartsWithWindows() { - assertThat(PathFragment.create("C:/foo/bar").startsWith(PathFragment.create("C:/foo"))) - .isTrue(); - assertThat(PathFragment.create("C:/foo/bar").startsWith(PathFragment.create("C:/"))).isTrue(); - assertThat(PathFragment.create("C:/").startsWith(PathFragment.create("C:/"))).isTrue(); + assertThat(create("C:/foo/bar").startsWith(create("C:/foo"))).isTrue(); + assertThat(create("C:/foo/bar").startsWith(create("C:/"))).isTrue(); + assertThat(create("C:/").startsWith(create("C:/"))).isTrue(); // The first path is absolute, the second is not. - assertThat(PathFragment.create("C:/foo/bar").startsWith(PathFragment.create("C:"))).isFalse(); - assertThat(PathFragment.create("C:/").startsWith(PathFragment.create("C:"))).isFalse(); + assertThat(create("C:/foo/bar").startsWith(create("C:"))).isFalse(); + assertThat(create("C:/").startsWith(create("C:"))).isFalse(); } @Test public void testEndsWithWindows() { - assertThat(PathFragment.create("C:/foo/bar").endsWith(PathFragment.create("bar"))).isTrue(); - assertThat(PathFragment.create("C:/foo/bar").endsWith(PathFragment.create("foo/bar"))).isTrue(); - assertThat(PathFragment.create("C:/foo/bar").endsWith(PathFragment.create("C:/foo/bar"))) - .isTrue(); - assertThat(PathFragment.create("C:/").endsWith(PathFragment.create("C:/"))).isTrue(); + assertThat(create("C:/foo/bar").endsWith(create("bar"))).isTrue(); + assertThat(create("C:/foo/bar").endsWith(create("foo/bar"))).isTrue(); + assertThat(create("C:/foo/bar").endsWith(create("C:/foo/bar"))).isTrue(); + assertThat(create("C:/").endsWith(create("C:/"))).isTrue(); } @Test public void testGetSafePathStringWindows() { - assertThat(PathFragment.create("C:/").getSafePathString()).isEqualTo("C:/"); - assertThat(PathFragment.create("C:/abc").getSafePathString()).isEqualTo("C:/abc"); - assertThat(PathFragment.create("C:/abc/def").getSafePathString()).isEqualTo("C:/abc/def"); + assertThat(create("C:/").getSafePathString()).isEqualTo("C:/"); + assertThat(create("C:/abc").getSafePathString()).isEqualTo("C:/abc"); + assertThat(create("C:/abc/def").getSafePathString()).isEqualTo("C:/abc/def"); } @Test public void testNormalizeWindows() { - assertThat(PathFragment.create("C:/a/b").normalize()).isEqualTo(PathFragment.create("C:/a/b")); - assertThat(PathFragment.create("C:/a/./b").normalize()) - .isEqualTo(PathFragment.create("C:/a/b")); - assertThat(PathFragment.create("C:/a/../b").normalize()).isEqualTo(PathFragment.create("C:/b")); - assertThat(PathFragment.create("C:/../b").normalize()) - .isEqualTo(PathFragment.create("C:/../b")); + assertThat(create("C:/a/b")).isEqualTo(create("C:/a/b")); + assertThat(create("C:/a/./b")).isEqualTo(create("C:/a/b")); + assertThat(create("C:/a/../b")).isEqualTo(create("C:/b")); + assertThat(create("C:/../b")).isEqualTo(create("C:/../b")); } @Test @@ -335,8 +231,21 @@ public class PathFragmentWindowsTest { // of drive C:\". // Bazel doesn't resolve such paths, and just takes them literally like normal path segments. // If the user attempts to open files under such paths, the file system API will give an error. - assertThat(PathFragment.create("C:").isAbsolute()).isFalse(); - assertThat(PathFragment.create("C:").getDriveLetter()).isEqualTo('\0'); - assertThat(PathFragment.create("C:").getSegments()).containsExactly("C:"); + assertThat(create("C:").isAbsolute()).isFalse(); + assertThat(create("C:").getSegments()).containsExactly("C:"); + } + + @Test + public void testToRelative() { + assertThat(create("C:/foo/bar").toRelative()).isEqualTo(create("foo/bar")); + assertThat(create("C:/").toRelative()).isEqualTo(create("")); + MoreAsserts.expectThrows(IllegalArgumentException.class, () -> create("foo").toRelative()); + } + + @Test + public void testGetDriveStr() { + assertThat(create("C:/foo/bar").getDriveStr()).isEqualTo("C:/"); + assertThat(create("C:/").getDriveStr()).isEqualTo("C:/"); + MoreAsserts.expectThrows(IllegalArgumentException.class, () -> create("foo").getDriveStr()); } } diff --git a/src/test/java/com/google/devtools/build/lib/vfs/PathTest.java b/src/test/java/com/google/devtools/build/lib/vfs/PathTest.java deleted file mode 100644 index eaad458438..0000000000 --- a/src/test/java/com/google/devtools/build/lib/vfs/PathTest.java +++ /dev/null @@ -1,341 +0,0 @@ -// 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 static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Lists; -import com.google.common.testing.EqualsTester; -import com.google.common.testing.GcFinalization; -import com.google.devtools.build.lib.clock.BlazeClock; -import com.google.devtools.build.lib.skyframe.serialization.InjectingObjectCodecAdapter; -import com.google.devtools.build.lib.skyframe.serialization.testutils.FsUtils; -import com.google.devtools.build.lib.skyframe.serialization.testutils.ObjectCodecTester; -import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.lang.ref.WeakReference; -import java.net.URI; -import java.util.Collections; -import java.util.List; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** - * A test for {@link Path}. - */ -@RunWith(JUnit4.class) -public class PathTest { - private FileSystem filesystem; - private Path root; - - @Before - public final void initializeFileSystem() throws Exception { - filesystem = new InMemoryFileSystem(BlazeClock.instance()); - root = filesystem.getRootDirectory(); - Path first = root.getChild("first"); - first.createDirectory(); - } - - @Test - public void testStartsWithWorksForSelf() { - assertStartsWithReturns(true, "/first/child", "/first/child"); - } - - @Test - public void testStartsWithWorksForChild() { - assertStartsWithReturns(true, - "/first/child", "/first/child/grandchild"); - } - - @Test - public void testStartsWithWorksForDeepDescendant() { - assertStartsWithReturns(true, - "/first/child", "/first/child/grandchild/x/y/z"); - } - - @Test - public void testStartsWithFailsForParent() { - assertStartsWithReturns(false, "/first/child", "/first"); - } - - @Test - public void testStartsWithFailsForSibling() { - assertStartsWithReturns(false, "/first/child", "/first/child2"); - } - - @Test - public void testStartsWithFailsForLinkToDescendant() - throws Exception { - Path linkTarget = filesystem.getPath("/first/linked_to"); - FileSystemUtils.createEmptyFile(linkTarget); - Path second = filesystem.getPath("/second/"); - second.createDirectory(); - second.getChild("child_link").createSymbolicLink(linkTarget); - assertStartsWithReturns(false, "/first", "/second/child_link"); - } - - @Test - public void testStartsWithFailsForNullPrefix() { - try { - filesystem.getPath("/first").startsWith(null); - fail(); - } catch (Exception e) { - } - } - - private void assertStartsWithReturns(boolean expected, - String ancestor, - String descendant) { - Path parent = filesystem.getPath(ancestor); - Path child = filesystem.getPath(descendant); - assertThat(child.startsWith(parent)).isEqualTo(expected); - } - - @Test - public void testGetChildWorks() { - assertGetChildWorks("second"); - assertGetChildWorks("..."); - assertGetChildWorks("...."); - } - - private void assertGetChildWorks(String childName) { - assertThat(filesystem.getPath("/first").getChild(childName)) - .isEqualTo(filesystem.getPath("/first/" + childName)); - } - - @Test - public void testGetChildFailsForChildWithSlashes() { - assertGetChildFails("second/third"); - assertGetChildFails("./third"); - assertGetChildFails("../third"); - assertGetChildFails("second/.."); - assertGetChildFails("second/."); - assertGetChildFails("/third"); - assertGetChildFails("third/"); - } - - private void assertGetChildFails(String childName) { - try { - filesystem.getPath("/first").getChild(childName); - fail(); - } catch (IllegalArgumentException e) { - // Expected. - } - } - - @Test - public void testGetChildFailsForDotAndDotDot() { - assertGetChildFails("."); - assertGetChildFails(".."); - } - - @Test - public void testGetChildFailsForEmptyString() { - assertGetChildFails(""); - } - - @Test - public void testRelativeToWorks() { - assertRelativeToWorks("apple", "/fruit/apple", "/fruit"); - assertRelativeToWorks("apple/jonagold", "/fruit/apple/jonagold", "/fruit"); - } - - @Test - public void testGetRelativeWithStringWorks() { - assertGetRelativeWorks("/first/x/y", "y"); - assertGetRelativeWorks("/y", "/y"); - assertGetRelativeWorks("/first/x/x", "./x"); - assertGetRelativeWorks("/first/y", "../y"); - assertGetRelativeWorks("/", "../../../../.."); - } - - @Test - public void testAsFragmentWorks() { - assertAsFragmentWorks("/"); - assertAsFragmentWorks("//"); - assertAsFragmentWorks("/first"); - assertAsFragmentWorks("/first/x/y"); - assertAsFragmentWorks("/first/x/y.foo"); - } - - @Test - public void testGetRelativeWithFragmentWorks() { - Path dir = filesystem.getPath("/first/x"); - assertThat(dir.getRelative(PathFragment.create("y")).toString()).isEqualTo("/first/x/y"); - assertThat(dir.getRelative(PathFragment.create("./x")).toString()).isEqualTo("/first/x/x"); - assertThat(dir.getRelative(PathFragment.create("../y")).toString()).isEqualTo("/first/y"); - } - - @Test - public void testGetRelativeWithAbsoluteFragmentWorks() { - Path root = filesystem.getPath("/first/x"); - assertThat(root.getRelative(PathFragment.create("/x/y")).toString()).isEqualTo("/x/y"); - } - - @Test - public void testGetRelativeWithAbsoluteStringWorks() { - Path root = filesystem.getPath("/first/x"); - assertThat(root.getRelative("/x/y").toString()).isEqualTo("/x/y"); - } - - @Test - public void testComparableSortOrder() { - Path zzz = filesystem.getPath("/zzz"); - Path ZZZ = filesystem.getPath("/ZZZ"); - Path abc = filesystem.getPath("/abc"); - Path aBc = filesystem.getPath("/aBc"); - Path AbC = filesystem.getPath("/AbC"); - Path ABC = filesystem.getPath("/ABC"); - List<Path> list = Lists.newArrayList(zzz, ZZZ, ABC, aBc, AbC, abc); - Collections.sort(list); - assertThat(list).containsExactly(ABC, AbC, ZZZ, aBc, abc, zzz).inOrder(); - } - - @Test - public void testParentOfRootIsRoot() { - assertThat(root.getRelative("..")).isSameAs(root); - - assertThat(root.getRelative("broken/../../dots")).isSameAs(root.getRelative("dots")); - } - - @Test - public void testSingleSegmentEquivalence() { - assertThat(root.getRelative("aSingleSegment")).isSameAs(root.getRelative("aSingleSegment")); - } - - @Test - public void testSiblingNonEquivalenceString() { - assertThat(root.getRelative("aDifferentSegment")) - .isNotSameAs(root.getRelative("aSingleSegment")); - } - - @Test - public void testSiblingNonEquivalenceFragment() { - assertThat(root.getRelative(PathFragment.create("aDifferentSegment"))) - .isNotSameAs(root.getRelative(PathFragment.create("aSingleSegment"))); - } - - @Test - public void testHashCodeStableAcrossGarbageCollections() { - Path parent = filesystem.getPath("/a"); - PathFragment childFragment = PathFragment.create("b"); - Path child = parent.getRelative(childFragment); - WeakReference<Path> childRef = new WeakReference<>(child); - int childHashCode1 = childRef.get().hashCode(); - assertThat(parent.getRelative(childFragment).hashCode()).isEqualTo(childHashCode1); - child = null; - GcFinalization.awaitClear(childRef); - int childHashCode2 = parent.getRelative(childFragment).hashCode(); - assertThat(childHashCode2).isEqualTo(childHashCode1); - } - - @Test - public void testSerialization() throws Exception { - FileSystem oldFileSystem = Path.getFileSystemForSerialization(); - try { - Path.setFileSystemForSerialization(filesystem); - Path root = filesystem.getPath("/"); - Path p1 = filesystem.getPath("/foo"); - Path p2 = filesystem.getPath("/foo/bar"); - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - ObjectOutputStream oos = new ObjectOutputStream(bos); - - oos.writeObject(root); - oos.writeObject(p1); - oos.writeObject(p2); - - ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); - ObjectInputStream ois = new ObjectInputStream(bis); - - Path dsRoot = (Path) ois.readObject(); - Path dsP1 = (Path) ois.readObject(); - Path dsP2 = (Path) ois.readObject(); - - new EqualsTester() - .addEqualityGroup(root, dsRoot) - .addEqualityGroup(p1, dsP1) - .addEqualityGroup(p2, dsP2) - .testEquals(); - - assertThat(p2.startsWith(p1)).isTrue(); - assertThat(p2.startsWith(dsP1)).isTrue(); - assertThat(dsP2.startsWith(p1)).isTrue(); - assertThat(dsP2.startsWith(dsP1)).isTrue(); - - // Regression test for a very specific bug in compareTo involving our incorrect usage of - // reference equality rather than logical equality. - String relativePathStringA = "child/grandchildA"; - String relativePathStringB = "child/grandchildB"; - assertThat( - p1.getRelative(relativePathStringA).compareTo(dsP1.getRelative(relativePathStringB))) - .isEqualTo( - p1.getRelative(relativePathStringA).compareTo(p1.getRelative(relativePathStringB))); - } finally { - Path.setFileSystemForSerialization(oldFileSystem); - } - } - - @Test - public void testAbsolutePathRoot() { - assertThat(new Path(null).toString()).isEqualTo("/"); - } - - @Test - public void testAbsolutePath() { - Path segment = new Path(null, "bar.txt", - new Path(null, "foo", new Path(null))); - assertThat(segment.toString()).isEqualTo("/foo/bar.txt"); - } - - @Test - public void testToURI() throws Exception { - Path p = root.getRelative("/tmp/foo bar.txt"); - URI uri = p.toURI(); - assertThat(uri.toString()).isEqualTo("file:///tmp/foo%20bar.txt"); - } - - @Test - public void testCodec() throws Exception { - ObjectCodecTester.newBuilder( - new InjectingObjectCodecAdapter<>(Path.CODEC, FsUtils.TEST_FILESYSTEM_PROVIDER)) - .addSubjects( - ImmutableList.of( - FsUtils.TEST_FILESYSTEM.getPath("/"), - FsUtils.TEST_FILESYSTEM.getPath("/some/path"), - FsUtils.TEST_FILESYSTEM.getPath("/some/other/path/with/empty/last/fragment/"))) - .buildAndRunTests(); - } - - private void assertAsFragmentWorks(String expected) { - assertThat(filesystem.getPath(expected).asFragment()).isEqualTo(PathFragment.create(expected)); - } - - private void assertGetRelativeWorks(String expected, String relative) { - assertThat(filesystem.getPath("/first/x").getRelative(relative)) - .isEqualTo(filesystem.getPath(expected)); - } - - private void assertRelativeToWorks(String expected, String relative, String original) { - assertThat(filesystem.getPath(relative).relativeTo(filesystem.getPath(original))) - .isEqualTo(PathFragment.create(expected)); - } -} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/PathTrieTest.java b/src/test/java/com/google/devtools/build/lib/vfs/PathTrieTest.java deleted file mode 100644 index 0807b4aa5d..0000000000 --- a/src/test/java/com/google/devtools/build/lib/vfs/PathTrieTest.java +++ /dev/null @@ -1,78 +0,0 @@ -// 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 static com.google.common.truth.Truth.assertThat; -import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** Unit tests for {@link PathTrie}. */ -@RunWith(JUnit4.class) -public class PathTrieTest { - @Test - public void empty() { - PathTrie<Integer> pathTrie = new PathTrie<>(); - assertThat(pathTrie.get(PathFragment.EMPTY_FRAGMENT)).isNull(); - assertThat(pathTrie.get(PathFragment.create("/x"))).isNull(); - assertThat(pathTrie.get(PathFragment.create("/x/y"))).isNull(); - } - - @Test - public void simpleBranches() { - PathTrie<Integer> pathTrie = new PathTrie<>(); - pathTrie.put(PathFragment.create("/a"), 1); - pathTrie.put(PathFragment.create("/b"), 2); - - assertThat(pathTrie.get(PathFragment.EMPTY_FRAGMENT)).isNull(); - assertThat(pathTrie.get(PathFragment.create("/a"))).isEqualTo(1); - assertThat(pathTrie.get(PathFragment.create("/a/b"))).isEqualTo(1); - assertThat(pathTrie.get(PathFragment.create("/a/b/c"))).isEqualTo(1); - assertThat(pathTrie.get(PathFragment.create("/b"))).isEqualTo(2); - assertThat(pathTrie.get(PathFragment.create("/b/c"))).isEqualTo(2); - } - - @Test - public void nestedDirectories() { - PathTrie<Integer> pathTrie = new PathTrie<>(); - pathTrie.put(PathFragment.create("/a/b/c"), 3); - assertThat(pathTrie.get(PathFragment.EMPTY_FRAGMENT)).isNull(); - assertThat(pathTrie.get(PathFragment.create("/a"))).isNull(); - assertThat(pathTrie.get(PathFragment.create("/a/b"))).isNull(); - assertThat(pathTrie.get(PathFragment.create("/a/b/c"))).isEqualTo(3); - assertThat(pathTrie.get(PathFragment.create("/a/b/c/d"))).isEqualTo(3); - - pathTrie.put(PathFragment.create("/a"), 1); - assertThat(pathTrie.get(PathFragment.EMPTY_FRAGMENT)).isNull(); - assertThat(pathTrie.get(PathFragment.create("/b"))).isNull(); - assertThat(pathTrie.get(PathFragment.create("/a"))).isEqualTo(1); - assertThat(pathTrie.get(PathFragment.create("/a/b"))).isEqualTo(1); - assertThat(pathTrie.get(PathFragment.create("/a/b/c"))).isEqualTo(3); - assertThat(pathTrie.get(PathFragment.create("/a/b/c/d"))).isEqualTo(3); - - pathTrie.put(PathFragment.ROOT_FRAGMENT, 0); - assertThat(pathTrie.get(PathFragment.EMPTY_FRAGMENT)).isEqualTo(0); - assertThat(pathTrie.get(PathFragment.create("/b"))).isEqualTo(0); - assertThat(pathTrie.get(PathFragment.create("/a"))).isEqualTo(1); - assertThat(pathTrie.get(PathFragment.create("/a/b"))).isEqualTo(1); - } - - @Test - public void unrootedDirectoriesError() { - PathTrie<Integer> pathTrie = new PathTrie<>(); - assertThrows(IllegalArgumentException.class, () -> pathTrie.put(PathFragment.create("a"), 1)); - } -} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/SymlinkAwareFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/SymlinkAwareFileSystemTest.java index 4720a6e773..ba7029daf2 100644 --- a/src/test/java/com/google/devtools/build/lib/vfs/SymlinkAwareFileSystemTest.java +++ b/src/test/java/com/google/devtools/build/lib/vfs/SymlinkAwareFileSystemTest.java @@ -16,6 +16,7 @@ package com.google.devtools.build.lib.vfs; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import com.google.devtools.build.lib.util.OS; import com.google.devtools.build.lib.vfs.FileSystem.NotASymlinkException; import java.io.FileNotFoundException; import java.io.IOException; @@ -197,7 +198,8 @@ public abstract class SymlinkAwareFileSystemTest extends FileSystemTest { linkPath.delete(); createSymbolicLink(linkPath, relative); if (testFS.supportsSymbolicLinksNatively(linkPath)) { - assertThat(linkPath.getFileSize(Symlinks.NOFOLLOW)).isEqualTo(linkTarget.length()); + assertThat(linkPath.getFileSize(Symlinks.NOFOLLOW)) + .isEqualTo(relative.getSafePathString().length()); assertThat(linkPath.readSymbolicLink()).isEqualTo(relative); } } @@ -205,6 +207,10 @@ public abstract class SymlinkAwareFileSystemTest extends FileSystemTest { @Test public void testLinkToRootResolvesCorrectly() throws IOException { + if (OS.getCurrent() == OS.WINDOWS) { + // This test cannot be run on Windows, it mixes "/" paths with "C:/" paths + return; + } Path rootPath = testFS.getPath("/"); try { diff --git a/src/test/java/com/google/devtools/build/lib/vfs/UnionFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/UnionFileSystemTest.java index 1e877a5255..3799cfdd46 100644 --- a/src/test/java/com/google/devtools/build/lib/vfs/UnionFileSystemTest.java +++ b/src/test/java/com/google/devtools/build/lib/vfs/UnionFileSystemTest.java @@ -163,7 +163,7 @@ public class UnionFileSystemTest extends SymlinkAwareFileSystemTest { // FileSystemTest.setUp() silently creates the test root on the filesystem... Path testDirUnderRoot = unionfs.getPath(workingDir.asFragment().subFragment(0, 1)); - assertThat(unionfs.getDirectoryEntries(unionfs.getRootDirectory())) + assertThat(unionfs.getDirectoryEntries(unionfs.getPath("/"))) .containsExactly( foo.getBaseName(), bar.getBaseName(), diff --git a/src/test/java/com/google/devtools/build/lib/vfs/UnixLocalPathTest.java b/src/test/java/com/google/devtools/build/lib/vfs/UnixPathTest.java index 2cdb4014ae..306ca9bc08 100644 --- a/src/test/java/com/google/devtools/build/lib/vfs/UnixLocalPathTest.java +++ b/src/test/java/com/google/devtools/build/lib/vfs/UnixPathTest.java @@ -17,15 +17,13 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows; import com.google.common.testing.EqualsTester; -import com.google.devtools.build.lib.vfs.LocalPath.OsPathPolicy; -import com.google.devtools.build.lib.vfs.LocalPath.UnixOsPathPolicy; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -/** Tests the unix implementation of {@link LocalPath}. */ +/** Tests the unix implementation of {@link Path}. */ @RunWith(JUnit4.class) -public class UnixLocalPathTest extends LocalPathAbstractTest { +public class UnixPathTest extends PathAbstractTest { @Test public void testEqualsAndHashCodeUnix() { @@ -37,8 +35,6 @@ public class UnixLocalPathTest extends LocalPathAbstractTest { @Test public void testRelativeToUnix() { - // Cannot relativize absolute and non-absolute - assertThat(create("c/d").getRelative("/a/b").getPathString()).isEqualTo("/a/b"); assertThat(create("/").relativeTo(create("/")).getPathString()).isEmpty(); assertThat(create("/foo").relativeTo(create("/foo")).getPathString()).isEmpty(); assertThat(create("/foo/bar/baz").relativeTo(create("/foo")).getPathString()) @@ -53,16 +49,24 @@ public class UnixLocalPathTest extends LocalPathAbstractTest { } @Test - public void testIsAbsoluteUnix() { - assertThat(create("/absolute/test").isAbsolute()).isTrue(); - assertThat(create("relative/test").isAbsolute()).isFalse(); - } - - @Test public void testGetRelativeUnix() { assertThat(create("/a").getRelative("b").getPathString()).isEqualTo("/a/b"); + assertThat(create("/a/b").getRelative("c/d").getPathString()).isEqualTo("/a/b/c/d"); + assertThat(create("/c/d").getRelative("/a/b").getPathString()).isEqualTo("/a/b"); + assertThat(create("/a").getRelative("").getPathString()).isEqualTo("/a"); assertThat(create("/").getRelative("").getPathString()).isEqualTo("/"); - assertThat(create("c/d").getRelative("/a/b").getPathString()).isEqualTo("/a/b"); + assertThat(create("/a/b").getRelative("../foo").getPathString()).isEqualTo("/a/foo"); + + // Make sure any fast path of Path#getRelative(PathFragment) works + assertThat(create("/a/b").getRelative(PathFragment.create("../foo")).getPathString()) + .isEqualTo("/a/foo"); + + // Make sure any fast path of Path#getRelative(PathFragment) works + assertThat(create("/c/d").getRelative(PathFragment.create("/a/b")).getPathString()) + .isEqualTo("/a/b"); + + // Test normalization + assertThat(create("/a").getRelative(".").getPathString()).isEqualTo("/a"); } @Test @@ -107,14 +111,10 @@ public class UnixLocalPathTest extends LocalPathAbstractTest { @Test public void testGetParentDirectoryUnix() { - LocalPath fooBarWizAbs = create("/foo/bar/wiz"); - LocalPath fooBarAbs = create("/foo/bar"); - LocalPath fooAbs = create("/foo"); - LocalPath rootAbs = create("/"); - assertThat(fooBarWizAbs.getParentDirectory()).isEqualTo(fooBarAbs); - assertThat(fooBarAbs.getParentDirectory()).isEqualTo(fooAbs); - assertThat(fooAbs.getParentDirectory()).isEqualTo(rootAbs); - assertThat(rootAbs.getParentDirectory()).isNull(); + assertThat(create("/foo/bar/wiz").getParentDirectory()).isEqualTo(create("/foo/bar")); + assertThat(create("/foo/bar").getParentDirectory()).isEqualTo(create("/foo")); + assertThat(create("/foo").getParentDirectory()).isEqualTo(create("/")); + assertThat(create("/").getParentDirectory()).isNull(); } @Test @@ -127,8 +127,7 @@ public class UnixLocalPathTest extends LocalPathAbstractTest { @Test public void testStartsWithUnix() { - LocalPath foobar = create("/foo/bar"); - LocalPath foobarRelative = create("foo/bar"); + Path foobar = create("/foo/bar"); // (path, prefix) => true assertThat(foobar.startsWith(foobar)).isTrue(); @@ -141,13 +140,6 @@ public class UnixLocalPathTest extends LocalPathAbstractTest { assertThat(create("/foo").startsWith(foobar)).isFalse(); assertThat(create("/").startsWith(foobar)).isFalse(); - // (absolute, relative) => false - assertThat(foobar.startsWith(foobarRelative)).isFalse(); - assertThat(foobarRelative.startsWith(foobar)).isFalse(); - - // relative paths start with nothing, absolute paths do not - assertThat(foobar.startsWith(create(""))).isFalse(); - // (path, sibling) => false assertThat(create("/foo/wiz").startsWith(foobar)).isFalse(); assertThat(foobar.startsWith(create("/foo/wiz"))).isFalse(); @@ -162,8 +154,10 @@ public class UnixLocalPathTest extends LocalPathAbstractTest { assertThat(create("/..")).isEqualTo(create("/..")); } - @Override - protected OsPathPolicy getFilePathOs() { - return new UnixOsPathPolicy(); + @Test + public void testParentOfRootIsRootUnix() { + assertThat(create("/..")).isEqualTo(create("/")); + assertThat(create("/../../../../../..")).isEqualTo(create("/")); + assertThat(create("/../../../foo")).isEqualTo(create("/foo")); } } diff --git a/src/test/java/com/google/devtools/build/lib/vfs/WindowsLocalPathTest.java b/src/test/java/com/google/devtools/build/lib/vfs/WindowsPathTest.java index ac5acef1f4..c7592197e1 100644 --- a/src/test/java/com/google/devtools/build/lib/vfs/WindowsLocalPathTest.java +++ b/src/test/java/com/google/devtools/build/lib/vfs/WindowsPathTest.java @@ -17,18 +17,16 @@ import static com.google.common.truth.Truth.assertThat; import com.google.common.testing.EqualsTester; import com.google.devtools.build.lib.testutil.MoreAsserts; -import com.google.devtools.build.lib.vfs.LocalPath.OsPathPolicy; -import com.google.devtools.build.lib.vfs.LocalPath.WindowsOsPathPolicy; -import com.google.devtools.build.lib.vfs.LocalPath.WindowsOsPathPolicy.ShortPathResolver; +import com.google.devtools.build.lib.vfs.WindowsOsPathPolicy.ShortPathResolver; import java.util.HashMap; import java.util.Map; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -/** Tests windows-specific parts of {@link LocalPath} */ +/** Tests windows-specific parts of {@link Path} */ @RunWith(JUnit4.class) -public class WindowsLocalPathTest extends LocalPathAbstractTest { +public class WindowsPathTest extends PathAbstractTest { private static final class MockShortPathResolver implements ShortPathResolver { // Full path to resolved child mapping. @@ -52,26 +50,18 @@ public class WindowsLocalPathTest extends LocalPathAbstractTest { } } - private final MockShortPathResolver shortPathResolver = new MockShortPathResolver(); - - @Override - protected OsPathPolicy getFilePathOs() { - return new WindowsOsPathPolicy(shortPathResolver); - } - @Test public void testEqualsAndHashcodeWindows() { new EqualsTester() - .addEqualityGroup(create("a/b"), create("A/B")) .addEqualityGroup(create("/a/b"), create("/A/B")) .addEqualityGroup(create("c:/a/b"), create("C:\\A\\B")) - .addEqualityGroup(create("something/else")) + .addEqualityGroup(create("C:/something/else")) .testEquals(); } @Test public void testCaseIsPreserved() { - assertThat(create("a/B").getPathString()).isEqualTo("a/B"); + assertThat(create("C:/a/B").getPathString()).isEqualTo("C:/a/B"); } @Test @@ -96,69 +86,62 @@ public class WindowsLocalPathTest extends LocalPathAbstractTest { public void testGetParentDirectoryWindows() { assertThat(create("C:/foo").getParentDirectory()).isEqualTo(create("C:/")); assertThat(create("C:/").getParentDirectory()).isNull(); + assertThat(create("/").getParentDirectory()).isNull(); } @Test - public void testisAbsoluteWindows() { - assertThat(create("C:/").isAbsolute()).isTrue(); - // test that msys paths turn into absolute paths - assertThat(create("/").isAbsolute()).isTrue(); + public void testParentOfRootIsRootWindows() { + assertThat(create("C:/..")).isEqualTo(create("C:/")); + assertThat(create("C:/../../../../../..")).isEqualTo(create("C:/")); + assertThat(create("C:/../../../foo")).isEqualTo(create("C:/foo")); } @Test public void testRelativeToWindows() { - assertThat(create("C:/foo").relativeTo(create("C:/"))).isEqualTo(create("foo")); + assertThat(create("C:/foo").relativeTo(create("C:/")).getPathString()).isEqualTo("foo"); // Case insensitivity test - assertThat(create("C:/foo/bar").relativeTo(create("C:/FOO"))).isEqualTo(create("bar")); + assertThat(create("C:/foo/bar").relativeTo(create("C:/FOO")).getPathString()).isEqualTo("bar"); MoreAsserts.assertThrows( IllegalArgumentException.class, () -> create("D:/foo").relativeTo(create("C:/"))); } @Test - public void testAbsoluteUnixPathIsRelativeToWindowsUnixRoot() { - assertThat(create("/").getPathString()).isEqualTo("C:/fake/msys"); - assertThat(create("/foo/bar").getPathString()).isEqualTo("C:/fake/msys/foo/bar"); - assertThat(create("/foo/bar").getPathString()).isEqualTo("C:/fake/msys/foo/bar"); - } - - @Test - public void testAbsoluteUnixPathReferringToDriveIsRecognized() { - assertThat(create("/c/foo").getPathString()).isEqualTo("C:/foo"); - assertThat(create("/c/foo").getPathString()).isEqualTo("C:/foo"); - assertThat(create("/c:").getPathString()).isNotEqualTo("C:/foo"); - } - - @Test public void testResolvesShortenedPaths() { + MockShortPathResolver shortPathResolver = new MockShortPathResolver(); + WindowsOsPathPolicy osPathPolicy = new WindowsOsPathPolicy(shortPathResolver); shortPathResolver.resolutions.put("d:/progra~1", "program files"); shortPathResolver.resolutions.put("d:/program files/micros~1", "microsoft something"); shortPathResolver.resolutions.put( "d:/program files/microsoft something/foo/~bar~1", "~bar_hello"); // Assert normal shortpath resolution. - LocalPath normal = create("d:/progra~1/micros~1/foo/~bar~1/baz"); - // The path string has an upper-case drive letter because that's how path printing works. - assertThat(normal.getPathString()) + assertThat(normalize(osPathPolicy, "d:/progra~1/micros~1/foo/~bar~1/baz")) .isEqualTo("D:/program files/microsoft something/foo/~bar_hello/baz"); - LocalPath notYetExistent = create("d:/progra~1/micros~1/foo/will~1.exi/bar"); - // The path string has an upper-case drive letter because that's how path printing works. - assertThat(notYetExistent.getPathString()) + assertThat(normalize(osPathPolicy, "d:/progra~1/micros~1/foo/will~1.exi/bar")) .isEqualTo("D:/program files/microsoft something/foo/will~1.exi/bar"); - LocalPath msRoot = create("d:/progra~1/micros~1"); - assertThat(msRoot.getPathString()).isEqualTo("D:/program files/microsoft something"); + assertThat(normalize(osPathPolicy, "d:/progra~1/micros~1")) + .isEqualTo("D:/program files/microsoft something"); // Pretend that a path we already failed to resolve once came into existence. shortPathResolver.resolutions.put( "d:/program files/microsoft something/foo/will~1.exi", "will.exist"); // Assert that this time we can resolve the previously non-existent path. - LocalPath nowExists = create("d:/progra~1/micros~1/foo/will~1.exi/bar"); // The path string has an upper-case drive letter because that's how path printing works. - assertThat(nowExists.getPathString()) + assertThat(normalize(osPathPolicy, "d:/progra~1/micros~1/foo/will~1.exi/bar")) .isEqualTo("D:/program files/microsoft something/foo/will.exist/bar"); - // Assert relative paths that look like short paths are untouched - assertThat(create("progra~1").getPathString()).isEqualTo("progra~1"); + // Check needsToNormalized + assertThat(osPathPolicy.needsToNormalize("d:/progra~1/micros~1/foo/will~1.exi/bar")) + .isEqualTo(WindowsOsPathPolicy.NEEDS_SHORT_PATH_NORMALIZATION); + assertThat(osPathPolicy.needsToNormalize("will~1.exi")) + .isEqualTo(WindowsOsPathPolicy.NEEDS_SHORT_PATH_NORMALIZATION); + assertThat(osPathPolicy.needsToNormalize("d:/no-normalization")) + .isEqualTo(WindowsOsPathPolicy.NORMALIZED); // Sanity check + } + + private static String normalize(OsPathPolicy osPathPolicy, String str) { + return osPathPolicy.normalize(str, osPathPolicy.needsToNormalize(str)); } } diff --git a/src/test/java/com/google/devtools/build/lib/windows/PathWindowsTest.java b/src/test/java/com/google/devtools/build/lib/windows/PathWindowsTest.java deleted file mode 100644 index e77de52d83..0000000000 --- a/src/test/java/com/google/devtools/build/lib/windows/PathWindowsTest.java +++ /dev/null @@ -1,298 +0,0 @@ -// 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.windows; - -import static com.google.common.truth.Truth.assertThat; - -import com.google.common.base.Function; -import com.google.common.base.Predicate; -import com.google.devtools.build.lib.clock.BlazeClock; -import com.google.devtools.build.lib.vfs.FileSystem; -import com.google.devtools.build.lib.vfs.Path; -import com.google.devtools.build.lib.vfs.Path.PathFactory; -import com.google.devtools.build.lib.vfs.PathFragment; -import com.google.devtools.build.lib.vfs.Root; -import com.google.devtools.build.lib.vfs.RootedPath; -import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem; -import com.google.devtools.build.lib.windows.WindowsFileSystem.WindowsPath; -import java.net.URI; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** A test for windows aspects of {@link Path}. */ -@RunWith(JUnit4.class) -public class PathWindowsTest { - - private static final class MockShortPathResolver implements Function<String, String> { - public List<String> resolutionQueries = new ArrayList<>(); - - // Full path to resolved child mapping. - public Map<String, String> resolutions = new HashMap<>(); - - @Override - public String apply(String path) { - path = path.toLowerCase(); - resolutionQueries.add(path); - return resolutions.get(path); - } - } - - private FileSystem filesystem; - private WindowsPath root; - private final MockShortPathResolver shortPathResolver = new MockShortPathResolver(); - - @Before - public final void initializeFileSystem() throws Exception { - filesystem = - new InMemoryFileSystem(BlazeClock.instance()) { - @Override - protected PathFactory getPathFactory() { - return WindowsFileSystem.getPathFactoryForTesting(shortPathResolver); - } - - @Override - public boolean isFilePathCaseSensitive() { - return false; - } - }; - root = (WindowsPath) filesystem.getRootDirectory().getRelative("C:/"); - root.createDirectory(); - } - - private void assertAsFragmentWorks(String expected) { - assertThat(filesystem.getPath(expected).asFragment()).isEqualTo(PathFragment.create(expected)); - } - - @Test - public void testWindowsPath() { - Path p = filesystem.getPath("C:/foo/bar"); - assertThat(p.getPathString()).isEqualTo("C:/foo/bar"); - assertThat(p.toString()).isEqualTo("C:/foo/bar"); - } - - @Test - public void testAsFragmentWindows() { - assertAsFragmentWorks("C:/"); - assertAsFragmentWorks("C://"); - assertAsFragmentWorks("C:/first"); - assertAsFragmentWorks("C:/first/x/y"); - assertAsFragmentWorks("C:/first/x/y.foo"); - } - - @Test - public void testGetRelativeWithFragmentWindows() { - Path dir = filesystem.getPath("C:/first/x"); - assertThat(dir.getRelative(PathFragment.create("y")).toString()).isEqualTo("C:/first/x/y"); - assertThat(dir.getRelative(PathFragment.create("./x")).toString()).isEqualTo("C:/first/x/x"); - assertThat(dir.getRelative(PathFragment.create("../y")).toString()).isEqualTo("C:/first/y"); - assertThat(dir.getRelative(PathFragment.create("../y")).toString()).isEqualTo("C:/first/y"); - assertThat(dir.getRelative(PathFragment.create("../../../y")).toString()).isEqualTo("C:/y"); - } - - @Test - public void testGetRelativeWithAbsoluteFragmentWindows() { - Path x = filesystem.getPath("C:/first/x"); - assertThat(x.getRelative(PathFragment.create("C:/x/y")).toString()).isEqualTo("C:/x/y"); - } - - @Test - public void testGetRelativeWithAbsoluteStringWorksWindows() { - Path x = filesystem.getPath("C:/first/x"); - assertThat(x.getRelative("C:/x/y").toString()).isEqualTo("C:/x/y"); - } - - @Test - public void testParentOfRootIsRootWindows() { - assertThat(root).isSameAs(root.getRelative("..")); - assertThat(root.getRelative("dots")).isSameAs(root.getRelative("broken/../../dots")); - } - - @Test - public void testStartsWithWorksOnWindows() { - assertStartsWithReturnsOnWindows(true, "C:/first/x", "C:/first/x/y"); - assertStartsWithReturnsOnWindows(true, "c:/first/x", "C:/FIRST/X/Y"); - assertStartsWithReturnsOnWindows(true, "C:/FIRST/X", "c:/first/x/y"); - assertStartsWithReturnsOnWindows(false, "C:/", "/"); - assertStartsWithReturnsOnWindows(false, "C:/", "D:/"); - assertStartsWithReturnsOnWindows(false, "C:/", "D:/foo"); - } - - @Test - public void testGetRelative() { - Path x = filesystem.getPath("C:\\first\\x"); - Path other = x.getRelative("a\\b\\c"); - assertThat(other.asFragment().getPathString()).isEqualTo("C:/first/x/a/b/c"); - } - - private static void assertStartsWithReturnsOnWindows( - boolean expected, String ancestor, String descendant) { - FileSystem windowsFileSystem = new WindowsFileSystem(); - Path parent = windowsFileSystem.getPath(ancestor); - Path child = windowsFileSystem.getPath(descendant); - assertThat(child.startsWith(parent)).isEqualTo(expected); - } - - @Test - public void testResolvesShortenedPaths() { - shortPathResolver.resolutions.put("d:/progra~1", "program files"); - shortPathResolver.resolutions.put("d:/program files/micros~1", "microsoft something"); - shortPathResolver.resolutions.put( - "d:/program files/microsoft something/foo/~bar~1", "~bar_hello"); - - // Assert normal shortpath resolution. - Path normal = root.getRelative("d:/progra~1/micros~1/foo/~bar~1/baz"); - // The path string has an upper-case drive letter because that's how path printing works. - assertThat(normal.getPathString()) - .isEqualTo("D:/program files/microsoft something/foo/~bar_hello/baz"); - // Assert that we only try to resolve the path segments that look like they may be shortened. - assertThat(shortPathResolver.resolutionQueries) - .containsExactly( - "d:/progra~1", - "d:/program files/micros~1", - "d:/program files/microsoft something/foo/~bar~1"); - - // Assert resolving a path that has a segment which doesn't exist but later will. - shortPathResolver.resolutionQueries.clear(); - Path notYetExistent = root.getRelative("d:/progra~1/micros~1/foo/will~1.exi/bar"); - // The path string has an upper-case drive letter because that's how path printing works. - assertThat(notYetExistent.getPathString()) - .isEqualTo("D:/program files/microsoft something/foo/will~1.exi/bar"); - // Assert that we only try to resolve the path segments that look like they may be shortened. - assertThat(shortPathResolver.resolutionQueries) - .containsExactly( - "d:/progra~1", - "d:/program files/micros~1", - "d:/program files/microsoft something/foo/will~1.exi"); - - // Assert that the paths we failed to resolve don't get cached. - final List<String> children = new ArrayList<>(2); - Predicate<Path> collector = - new Predicate<Path>() { - @Override - public boolean apply(Path child) { - children.add(child.getPathString()); - return true; - } - }; - - WindowsPath msRoot = (WindowsPath) root.getRelative("d:/progra~1/micros~1"); - assertThat(msRoot.getPathString()).isEqualTo("D:/program files/microsoft something"); - msRoot.applyToChildren(collector); - // The path string has an upper-case drive letter because that's how path printing works. - assertThat(children).containsExactly("D:/program files/microsoft something/foo"); - - // Assert that the non-resolvable path was not cached. - children.clear(); - WindowsPath foo = (WindowsPath) msRoot.getRelative("foo"); - foo.applyToChildren(collector); - assertThat(children).containsExactly("D:/program files/microsoft something/foo/~bar_hello"); - - // Pretend that a path we already failed to resolve once came into existence. - shortPathResolver.resolutions.put( - "d:/program files/microsoft something/foo/will~1.exi", "will.exist"); - - // Assert that this time we can resolve the previously non-existent path. - shortPathResolver.resolutionQueries.clear(); - Path nowExists = root.getRelative("d:/progra~1/micros~1/foo/will~1.exi/bar"); - // The path string has an upper-case drive letter because that's how path printing works. - assertThat(nowExists.getPathString()) - .isEqualTo("D:/program files/microsoft something/foo/will.exist/bar"); - // Assert that we only try to resolve the path segments that look like they may be shortened. - assertThat(shortPathResolver.resolutionQueries) - .containsExactly( - "d:/progra~1", - "d:/program files/micros~1", - "d:/program files/microsoft something/foo/will~1.exi"); - - // Assert that this time we cached the previously non-existent path. - children.clear(); - foo.applyToChildren(collector); - // The path strings have upper-case drive letters because that's how path printing works. - children.clear(); - foo.applyToChildren(collector); - assertThat(children) - .containsExactly( - "D:/program files/microsoft something/foo/~bar_hello", - "D:/program files/microsoft something/foo/will.exist"); - } - - @Test - public void testCaseInsensitivePathFragment() { - // equals - assertThat(PathFragment.create("c:/FOO/BAR")).isEqualTo(PathFragment.create("c:\\foo\\bar")); - assertThat(PathFragment.create("c:/FOO/BAR")).isNotEqualTo(PathFragment.create("d:\\foo\\bar")); - assertThat(PathFragment.create("c:/FOO/BAR")).isNotEqualTo(PathFragment.create("/foo/bar")); - // equals for the string representation - assertThat(PathFragment.create("c:/FOO/BAR").toString()) - .isNotEqualTo(PathFragment.create("c:/foo/bar").toString()); - // hashCode - assertThat(PathFragment.create("c:/FOO/BAR").hashCode()) - .isEqualTo(PathFragment.create("c:\\foo\\bar").hashCode()); - assertThat(PathFragment.create("c:/FOO/BAR").hashCode()) - .isNotEqualTo(PathFragment.create("d:\\foo\\bar").hashCode()); - assertThat(PathFragment.create("c:/FOO/BAR").hashCode()) - .isNotEqualTo(PathFragment.create("/foo/bar").hashCode()); - // compareTo - assertThat(PathFragment.create("c:/FOO/BAR").compareTo(PathFragment.create("c:\\foo\\bar"))) - .isEqualTo(0); - assertThat(PathFragment.create("c:/FOO/BAR").compareTo(PathFragment.create("d:\\foo\\bar"))) - .isLessThan(0); - assertThat(PathFragment.create("c:/FOO/BAR").compareTo(PathFragment.create("/foo/bar"))) - .isGreaterThan(0); - // startsWith - assertThat(PathFragment.create("c:/FOO/BAR").startsWith(PathFragment.create("c:\\foo"))) - .isTrue(); - assertThat(PathFragment.create("c:/FOO/BAR").startsWith(PathFragment.create("d:\\foo"))) - .isFalse(); - // endsWith - assertThat(PathFragment.create("c:/FOO/BAR/BAZ").endsWith(PathFragment.create("bar\\baz"))) - .isTrue(); - assertThat(PathFragment.create("c:/FOO/BAR/BAZ").endsWith(PathFragment.create("/bar/baz"))) - .isFalse(); - assertThat(PathFragment.create("c:/FOO/BAR/BAZ").endsWith(PathFragment.create("d:\\bar\\baz"))) - .isFalse(); - // relativeTo - assertThat( - PathFragment.create("c:/FOO/BAR/BAZ/QUX") - .relativeTo(PathFragment.create("c:\\foo\\bar"))) - .isEqualTo(PathFragment.create("Baz/Qux")); - } - - @Test - public void testCaseInsensitiveRootedPath() { - Path ancestor = filesystem.getPath("C:\\foo\\bar"); - assertThat(ancestor).isInstanceOf(WindowsPath.class); - Path child = filesystem.getPath("C:\\FOO\\Bar\\baz"); - assertThat(child).isInstanceOf(WindowsPath.class); - assertThat(child.startsWith(ancestor)).isTrue(); - assertThat(child.relativeTo(ancestor)).isEqualTo(PathFragment.create("baz")); - RootedPath actual = RootedPath.toRootedPath(Root.fromPath(ancestor), child); - assertThat(actual.getRoot()).isEqualTo(Root.fromPath(ancestor)); - assertThat(actual.getRootRelativePath()).isEqualTo(PathFragment.create("baz")); - } - - @Test - public void testToURI() { - // See https://blogs.msdn.microsoft.com/ie/2006/12/06/file-uris-in-windows/ - Path p = root.getRelative("Temp\\Foo Bar.txt"); - URI uri = p.toURI(); - assertThat(uri.toString()).isEqualTo("file:///C:/Temp/Foo%20Bar.txt"); - } -} diff --git a/src/test/java/com/google/devtools/build/lib/windows/WindowsFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/windows/WindowsFileSystemTest.java index 8937c453a7..3e9cd27c93 100644 --- a/src/test/java/com/google/devtools/build/lib/windows/WindowsFileSystemTest.java +++ b/src/test/java/com/google/devtools/build/lib/windows/WindowsFileSystemTest.java @@ -18,7 +18,6 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import com.google.common.base.Function; -import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; @@ -27,16 +26,13 @@ import com.google.devtools.build.lib.util.OS; import com.google.devtools.build.lib.vfs.Path; import com.google.devtools.build.lib.vfs.PathFragment; import com.google.devtools.build.lib.vfs.Symlinks; -import com.google.devtools.build.lib.windows.WindowsFileSystem.WindowsPath; import com.google.devtools.build.lib.windows.jni.WindowsFileOperations; import com.google.devtools.build.lib.windows.util.WindowsTestUtil; import java.io.File; import java.io.IOException; import java.nio.file.Files; -import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; -import java.util.List; import java.util.Map; import org.junit.After; import org.junit.Before; @@ -246,42 +242,22 @@ public class WindowsFileSystemTest { assertThat(p.getPathString()).endsWith(longPath); assertThat(p).isEqualTo(fs.getPath(scratchRoot).getRelative(shortPath)); assertThat(p).isEqualTo(fs.getPath(scratchRoot).getRelative(longPath)); - assertThat(fs.getPath(scratchRoot).getRelative(shortPath)).isSameAs(p); - assertThat(fs.getPath(scratchRoot).getRelative(longPath)).isSameAs(p); + assertThat(fs.getPath(scratchRoot).getRelative(shortPath)).isEqualTo(p); + assertThat(fs.getPath(scratchRoot).getRelative(longPath)).isEqualTo(p); } @Test public void testUnresolvableShortPathWhichIsThenCreated() throws Exception { String shortPath = "unreso~1.sho/foo/will~1.exi/bar/hello.txt"; - String longPrefix = "unresolvable.shortpath/foo/"; - String longPath = longPrefix + "will.exist/bar/hello.txt"; - testUtil.scratchDir(longPrefix); - final WindowsPath foo = (WindowsPath) fs.getPath(scratchRoot).getRelative(longPrefix); - + String longPath = "unresolvable.shortpath/foo/will.exist/bar/hello.txt"; // Assert that we can create an unresolvable path. Path p = fs.getPath(scratchRoot).getRelative(shortPath); - assertThat(p.getPathString()).endsWith(longPrefix + "will~1.exi/bar/hello.txt"); - // Assert that said path is not cached in its parent's `children` cache. - final List<String> children = new ArrayList<>(); - Predicate<Path> collector = - new Predicate<Path>() { - @Override - public boolean apply(Path child) { - children.add(child.relativeTo(foo).getPathString()); - return true; - } - }; - foo.applyToChildren(collector); - assertThat(children).isEmpty(); + assertThat(p.getPathString()).endsWith(shortPath); // Assert that we can then create the whole path, and can now resolve the short form. testUtil.scratchFile(longPath, "hello"); Path q = fs.getPath(scratchRoot).getRelative(shortPath); assertThat(q.getPathString()).endsWith(longPath); - // Assert however that the unresolved and resolved Path objects are different, and only the - // resolved one is cached. assertThat(p).isNotEqualTo(q); - foo.applyToChildren(collector); - assertThat(children).containsExactly("will.exist"); } /** @@ -297,18 +273,11 @@ public class WindowsFileSystemTest { Path p2 = fs.getPath(scratchRoot).getRelative("longpa~1"); assertThat(p1.exists()).isFalse(); assertThat(p1).isEqualTo(p2); - assertThat(p1).isNotSameAs(p2); testUtil.scratchDir("longpathnow"); Path q1 = fs.getPath(scratchRoot).getRelative("longpa~1"); - Path q2 = fs.getPath(scratchRoot).getRelative("longpa~1"); assertThat(q1.exists()).isTrue(); - assertThat(q1).isEqualTo(q2); - // Assert q1 == q2, because we could successfully resolve the short path to a long name and we - // cache them by the long name, so it's irrelevant they were created from a 8dot3 name, or what - // that name resolves to later in time. - assertThat(q1).isSameAs(q2); - assertThat(q1).isSameAs(fs.getPath(scratchRoot).getRelative("longpathnow")); + assertThat(q1).isEqualTo(fs.getPath(scratchRoot).getRelative("longpathnow")); // Delete the original resolution of "longpa~1" ("longpathnow"). assertThat(q1.delete()).isTrue(); @@ -317,14 +286,8 @@ public class WindowsFileSystemTest { // Create a directory whose 8dot3 name is also "longpa~1" but its long name is different. testUtil.scratchDir("longpaththen"); Path r1 = fs.getPath(scratchRoot).getRelative("longpa~1"); - Path r2 = fs.getPath(scratchRoot).getRelative("longpa~1"); assertThat(r1.exists()).isTrue(); - assertThat(r1).isEqualTo(r2); - assertThat(r1).isSameAs(r2); - assertThat(r1).isSameAs(fs.getPath(scratchRoot).getRelative("longpaththen")); - // r1 == r2 and q1 == q2, but r1 != q1, because the resolution of "longpa~1" changed over time. - assertThat(r1).isNotEqualTo(q1); - assertThat(r1).isNotSameAs(q1); + assertThat(r1).isEqualTo(fs.getPath(scratchRoot).getRelative("longpaththen")); } @Test |