Skip to content

Commit

Permalink
Add case-checking PackageIdentifier
Browse files Browse the repository at this point in the history
If the "bazel.check_label_casing" JVM property is
"1", then PackageIdentifier objects will store not
only the RepositoryName and PathFragment, but also
the toString() values of those.

These augmented PackageIdentifiers (actually:
CaseCheckingPackageIdentifiers) allow telling
apart PackageIdentifiers whose RepositoryName and
PathFragment values are equal but are cased
differently. This scenario is common on
case-insensitive filesystems, where the paths
"foo/x" and "FoO/X" are equal but differently
cased.

Motivation is to allow case-checking the package
names in PackageLookupFunction.

The SkyKey of PackageLookupFunction contains a
PackageIdentifier, which holds a reference to the
RepositoryName and PathFragment (package
fragment). On case-insensitive filesystems, both
of these objects check equality without regard to
case, and so do PackageIdentifiers as a
consequence.

But PackageIdentifier objects are interned for
better memory usage, which means that on a
case-insensitive filesystem it's impossible to
create two PackageIdentifiers whose RepositoryName
and PathFragment are equal but differently cased.
And that means it's also impossible to check in
the PackageLookupFunction whether the requested
package path (and thus the PackageIdentifier) has
correct casing.

With this PR, PackageIdentifier.create() will
return CaseCheckingPackageIdentifier when case
checking is enabled, so PackageLookupFunction will
know exactly what casing the user requested, and
can therefore validate that casing and deny
loading the package if the casing is wrong. And
that is what would fix bazelbuild#8799

Change-Id: I46106f83c0a5b16fa347dd8a07e3c5342248c605
  • Loading branch information
laszlocsomor committed Dec 5, 2019
1 parent 7d68029 commit f76615b
Show file tree
Hide file tree
Showing 6 changed files with 361 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,18 @@ public class LabelConstants {
public static final PathFragment WORKSPACE_DOT_BAZEL_FILE_NAME =
PathFragment.create("WORKSPACE.bazel");
public static final String DEFAULT_REPOSITORY_DIRECTORY = "__main__";

/**
* Whether Bazel should check (true) or trust (false) the casing of Labels.
*
* <p>A Package "//foo" exists if its BUILD file "foo/BUILD" exists, i.e. if stat("foo/BUILD")
* succeeds. On a case-sensitive filesystem (e.g. Ext4), this stat() call succeeds for "foo/BUILD"
* and fails for "Foo/BUILD" and "FOO/build". But on a case-ignoring filesystem (e.g. APFS and
* NTFS), all of these stat calls succeed, so apparently "//Foo" and "//FOO" also exist.
*
* <p>When CHECK_CASING is true, Bazel validates the label casing against the actual directory
* entry on the filesystem, so "//Foo" and "//FOO" won't feign existence.
*/
public static final boolean CHECK_CASING =
"1".equals(System.getProperty("bazel.check_label_casing"));
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
*/
@AutoCodec
@Immutable
public final class PackageIdentifier implements Comparable<PackageIdentifier>, Serializable {
public class PackageIdentifier implements Comparable<PackageIdentifier>, Serializable {
private static final Interner<PackageIdentifier> INTERNER = BlazeInterners.newWeakInterner();

public static PackageIdentifier create(String repository, PathFragment pkgName)
Expand All @@ -44,8 +44,22 @@ public static PackageIdentifier create(String repository, PathFragment pkgName)

@AutoCodec.Instantiator
public static PackageIdentifier create(RepositoryName repository, PathFragment pkgName) {
// Note: We rely on these being interned to fast-path Label#equals.
return INTERNER.intern(new PackageIdentifier(repository, pkgName));
if (LabelConstants.CHECK_CASING) {
// Note: We rely on these being interned to fast-path Label#equals.
String repositoryCasing = repository.toString();
String pkgNameCasing = pkgName.toString();
return INTERNER.intern(
new CaseCheckingPackageIdentifier(
repository,
pkgName,
repositoryCasing,
pkgNameCasing,
Objects.hash(repositoryCasing, pkgNameCasing)));
} else {
// Note: We rely on these being interned to fast-path Label#equals.
return INTERNER.intern(
new PackageIdentifier(repository, pkgName, Objects.hash(repository, pkgName)));
}
}

public static final PackageIdentifier EMPTY_PACKAGE_ID = createInMainRepo(
Expand Down Expand Up @@ -108,12 +122,12 @@ public static PackageIdentifier discoverFromExecPath(PathFragment execPath, bool
* Precomputed hash code. Hash/equality is based on repository and pkgName. Note that due to
* interning, x.equals(y) <=> x==y.
**/
private final int hashCode;
protected final int hashCode;

private PackageIdentifier(RepositoryName repository, PathFragment pkgName) {
private PackageIdentifier(RepositoryName repository, PathFragment pkgName, int hashCode) {
this.repository = Preconditions.checkNotNull(repository);
this.pkgName = Preconditions.checkNotNull(pkgName);
this.hashCode = Objects.hash(repository, pkgName);
this.hashCode = hashCode;
}

public static PackageIdentifier parse(String input) throws LabelSyntaxException {
Expand Down Expand Up @@ -232,4 +246,48 @@ public int compareTo(PackageIdentifier that) {
.compare(pkgName, that.pkgName)
.result();
}

private static final class CaseCheckingPackageIdentifier extends PackageIdentifier {
private final String repositoryCasing;
private final String pkgNameCasing;

private CaseCheckingPackageIdentifier(
RepositoryName repository,
PathFragment pkgName,
String repositoryCasing,
String pkgNameCasing,
int hashCode) {
super(repository, pkgName, hashCode);
this.repositoryCasing = repositoryCasing;
this.pkgNameCasing = pkgNameCasing;
}

@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof CaseCheckingPackageIdentifier)) {
return false;
}
CaseCheckingPackageIdentifier that = (CaseCheckingPackageIdentifier) object;
return this.hashCode == that.hashCode && pkgNameCasing.equals(that.pkgNameCasing)
&& repositoryCasing.equals(that.repositoryCasing);
}

@Override
public int compareTo(PackageIdentifier that) {
if (that instanceof CaseCheckingPackageIdentifier) {
return ComparisonChain.start()
.compare(repositoryCasing, ((CaseCheckingPackageIdentifier) that).repositoryCasing)
.compare(pkgNameCasing, ((CaseCheckingPackageIdentifier) that).pkgNameCasing)
.result();
} else {
return ComparisonChain.start()
.compare(repositoryCasing, that.repository.toString())
.compare(pkgNameCasing, that.pkgName.toString())
.result();
}
}
}
}
84 changes: 76 additions & 8 deletions src/test/java/com/google/devtools/build/lib/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ package(
)

# Tests for Windows-specific functionality that can run cross-platform.
# These don't need to run on Windows, they merely use Windows- and case-insensitive path semantics.
CROSS_PLATFORM_WINDOWS_TESTS = [
"util/DependencySetWindowsTest.java",
"vfs/PathFragmentWindowsTest.java",
Expand Down Expand Up @@ -184,18 +183,66 @@ java_library(
java_test(
name = "cmdline_test",
size = "small",
srcs = glob(["cmdline/*.java"]),
srcs = glob(
["cmdline/*.java"],
exclude = [
"cmdline/CaseSensitiveFsPackageIdentifierTest.java",
"cmdline/CaseInsensitiveFsPackageIdentifierTest.java",
"cmdline/CaseInsensitiveFsLabelCaseCheckingPackageIdentifierTest.java",
],
),
tags = [
"foundations",
],
test_class = "com.google.devtools.build.lib.AllTests",
deps = [
":guava_junit_truth",
":test_runner",
":testutil",
"//src/main/java/com/google/devtools/build/lib/cmdline",
"//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
deps = [":cmdline_test_deps"],
)

java_test(
name = "case_sensitive_fs_cmdline_test",
size = "small",
srcs = ["cmdline/CaseSensitiveFsPackageIdentifierTest.java"],
jvm_flags = [
# Force case-sensitive filesystem semantics, by pretending we run on Linux.
"-Dblaze.os=Linux",
# Disable checking the label casing. It's unnecessary on case-sensitive filesystems.
"-Dbazel.check_label_casing=0",
],
tags = ["foundations"],
test_class = "com.google.devtools.build.lib.AllTests",
deps = [":cmdline_test_deps"],
)

java_test(
name = "case_insensitive_fs_cmdline_test",
size = "small",
srcs = ["cmdline/CaseInsensitiveFsPackageIdentifierTest.java"],
jvm_flags = [
# Force case-insensitive filesystem semantics, by pretending we run on Windows.
"-Dblaze.os=Windows",
"-Dbazel.windows_unix_root=C:/fake/msys",
# Disable checking the label casing.
"-Dbazel.check_label_casing=0",
],
tags = ["foundations"],
test_class = "com.google.devtools.build.lib.AllTests",
deps = [":cmdline_test_deps"],
)

java_test(
name = "case_insensitive_fs_with_label_case_checking_cmdline_test",
size = "small",
srcs = ["cmdline/CaseInsensitiveFsLabelCaseCheckingPackageIdentifierTest.java"],
jvm_flags = [
# Force case-insensitive filesystem semantics, by pretending we run on Windows.
"-Dblaze.os=Windows",
"-Dbazel.windows_unix_root=C:/fake/msys",
# Enable checking the label casing.
"-Dbazel.check_label_casing=1",
],
tags = ["foundations"],
test_class = "com.google.devtools.build.lib.AllTests",
deps = [":cmdline_test_deps"],
)

java_test(
Expand All @@ -215,6 +262,27 @@ java_test(
],
)

java_library(
name = "cmdline_test_deps",
testonly = 1,
exports = [
":guava_junit_truth",
":test_runner",
":testutil",
"//src/main/java/com/google/devtools/build/lib/cmdline",
"//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
],
)

test_suite(
name = "case_sensitivity_cmdline_tests",
tests = [
":case_insensitive_fs_cmdline_test",
":case_insensitive_fs_with_label_case_checking_cmdline_test",
":case_sensitive_fs_cmdline_test",
],
)

java_test(
name = "collect_test",
size = "small",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright 2019 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.cmdline;

import static com.google.common.truth.Truth.assertThat;

import com.google.common.testing.EqualsTester;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
* Unit tests for {@link PackageIdentifier}.
*/
@RunWith(JUnit4.class)
public final class CaseInsensitiveFsLabelCaseCheckingPackageIdentifierTest {

@Test
public void testSemantics() throws Exception {
PathFragment pf1 = PathFragment.create("foo/bar");
PathFragment pf2 = PathFragment.create("FoO/BaR");
assertThat(pf1).isEqualTo(pf2);
// Fortunately, PathFragments are not interned so we get different instances. The ability to
// create equal, but not-same instances allows storing them in wrapper objects with their
// toString() value, thus distinguish differently cased versions of otherwise equal paths.
assertThat(pf1).isNotSameInstanceAs(pf2);
assertThat(pf1.toString()).isNotEqualTo(pf2.toString());

RepositoryName rn1 = RepositoryName.create("@foo");
RepositoryName rn2 = RepositoryName.create("@FoO");
// Fortunately, RepositoryNames are not interned so we get different instances. The ability to
// create equal, but not-same instances allows storing them in wrapper objects with their
// toString() value, thus distinguish differently cased versions of otherwise equal paths.
assertThat(rn1).isEqualTo(rn2);
assertThat(rn1).isNotSameInstanceAs(rn2);
assertThat(rn1.toString()).isNotEqualTo(rn2.toString());

PackageIdentifier id1 = PackageIdentifier.parse("@foo//bar/baz");
PackageIdentifier id2 = PackageIdentifier.parse("@foo//BAR/baz");
PackageIdentifier id3 = PackageIdentifier.parse("@FOO//bar/baz");
PackageIdentifier id4 = PackageIdentifier.parse("@FOO//BAR/baz");
// On a case-insensitive filesystem, RepositoryName ("foo" and "FOO") and PathFragment
// ("bar/baz" and "BAR/baz") are compared case-insensitively. But we also enabled label case
// checkiung, so the PackageIdentifiers are actually CaseCheckingPackageIdentifiers, so they are
// all unequal.
new EqualsTester()
.addEqualityGroup(id1)
.addEqualityGroup(id2)
.addEqualityGroup(id3)
.addEqualityGroup(id4)
.testEquals();
assertThat(id1).isNotSameInstanceAs(id2);
assertThat(id1).isNotSameInstanceAs(id3);
assertThat(id1).isNotSameInstanceAs(id4);
assertThat(id2).isNotSameInstanceAs(id3);
assertThat(id2).isNotSameInstanceAs(id4);
assertThat(id3).isNotSameInstanceAs(id4);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright 2019 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.cmdline;

import static com.google.common.truth.Truth.assertThat;

import com.google.common.testing.EqualsTester;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
* Unit tests for {@link PackageIdentifier}.
*/
@RunWith(JUnit4.class)
public final class CaseInsensitiveFsPackageIdentifierTest {

@Test
public void testSemantics() throws Exception {
PathFragment pf1 = PathFragment.create("foo/bar");
PathFragment pf2 = PathFragment.create("FoO/BaR");
assertThat(pf1).isEqualTo(pf2);
// Fortunately, PathFragments are not interned so we get different instances. The ability to
// create equal, but not-same instances allows storing them in wrapper objects with their
// toString() value, thus distinguish differently cased versions of otherwise equal paths.
assertThat(pf1).isNotSameInstanceAs(pf2);
assertThat(pf1.toString()).isNotEqualTo(pf2.toString());

RepositoryName rn1 = RepositoryName.create("@foo");
RepositoryName rn2 = RepositoryName.create("@FoO");
// Fortunately, RepositoryNames are not interned so we get different instances. The ability to
// create equal, but not-same instances allows storing them in wrapper objects with their
// toString() value, thus distinguish differently cased versions of otherwise equal paths.
assertThat(rn1).isEqualTo(rn2);
assertThat(rn1).isNotSameInstanceAs(rn2);
assertThat(rn1.toString()).isNotEqualTo(rn2.toString());

PackageIdentifier id1 = PackageIdentifier.parse("@foo//bar/baz");
PackageIdentifier id2 = PackageIdentifier.parse("@foo//BAR/baz");
PackageIdentifier id3 = PackageIdentifier.parse("@FOO//bar/baz");
PackageIdentifier id4 = PackageIdentifier.parse("@FOO//BAR/baz");
// On a case-insensitive filesystem, RepositoryName ("foo" and "FOO") and PathFragment
// ("bar/baz" and "BAR/baz") are compared case-insensitively, so all PackageIdentifiers are
// equal.
new EqualsTester().addEqualityGroup(id1, id2, id3, id4).testEquals();
// PackageIdentifier.create() interns objects, and it returns CaseTrustingPackageIdentifier
// because label case checking is disnabled, so the interned objects are all the same instance.
assertThat(id1).isSameInstanceAs(id2);
assertThat(id1).isSameInstanceAs(id3);
assertThat(id1).isSameInstanceAs(id4);
}
}
Loading

0 comments on commit f76615b

Please sign in to comment.