Copybara Merge: https://github.com/bazelbuild/rules_cc/pull/263

BEGIN_PUBLIC
Copybara import of the project:

--
82e44f3c472981bd13e86074c9ebd82974280030 by Fabian Meumertzheim <fabian@meumertzhe.im>:

Copy over runfiles library from Bazel

The runfiles library can be maintained independently of Bazel releases and bazel_tools can refer to it via an alias.

--
885d333d5565b210c1abc120eeee8e84c14e186f by Fabian Meumertzheim <fabian@meumertzhe.im>:

Switch to new include path and namespace

--
b8cd815963ecb00c826eed56461fd01d771306a1 by Fabian Meumertzheim <fabian@meumertzhe.im>:

Fix CI

--
41b7d48ed80d396c0e98b039a6a078366f21f9dc by Fabian Meumertzheim <fabian@meumertzhe.im>:

Silence warnings

--
b97a30e8bbb4eed94f024171289f28f9d78c4862 by Fabian Meumertzheim <fabian@meumertzhe.im>:

Remove per_file_copt

END_PUBLIC

PiperOrigin-RevId: 697942902
Change-Id: I10a6c9baf0464722fca026db99cf572acfd159f1
diff --git a/.bazelrc b/.bazelrc
new file mode 100644
index 0000000..a244d62
--- /dev/null
+++ b/.bazelrc
@@ -0,0 +1,2 @@
+# Required by abseil-cpp.
+common --cxxopt=-std=c++14
diff --git a/.gitignore b/.gitignore
index 0d4fed2..5a364af 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
 bazel-*
 MODULE.bazel.lock
+/.ijwb/
+/.clwb/
diff --git a/MODULE.bazel b/MODULE.bazel
index 123e24d..01b6d8e 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -15,5 +15,6 @@
 register_toolchains("@local_config_cc_toolchains//:all")
 
 bazel_dep(name = "rules_shell", version = "0.2.0", dev_dependency = True)
+bazel_dep(name = "googletest", version = "1.15.2", dev_dependency = True)
 bazel_dep(name = "rules_testing", version = "0.6.0", dev_dependency = True)
 bazel_dep(name = "stardoc", version = "0.7.0", dev_dependency = True)
diff --git a/WORKSPACE b/WORKSPACE
index ce5112f..ce6d15d 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -50,3 +50,10 @@
     strip_prefix = "protobuf-27.0",
     url = "https://github.com/protocolbuffers/protobuf/releases/download/v27.0/protobuf-27.0.tar.gz",
 )
+
+http_archive(
+    name = "googletest",
+    integrity = "sha256-e0K01u1IgQxTYsJloX+uvpDcI3PIheUhZDnTeSfwKSY=",
+    strip_prefix = "googletest-1.15.2",
+    url = "https://github.com/google/googletest/releases/download/v1.15.2/googletest-1.15.2.tar.gz",
+)
diff --git a/cc/runfiles/BUILD b/cc/runfiles/BUILD
index 887e1f2..6a81a7c 100644
--- a/cc/runfiles/BUILD
+++ b/cc/runfiles/BUILD
@@ -1,7 +1,12 @@
+load("//cc:cc_library.bzl", "cc_library")
+
 licenses(["notice"])
 
-alias(
+cc_library(
     name = "runfiles",
-    actual = "@bazel_tools//tools/cpp/runfiles",
+    srcs = ["runfiles.cc"],
+    hdrs = ["runfiles.h"],
+    include_prefix = "rules_cc/cc/runfiles",
+    strip_include_prefix = ".",
     visibility = ["//visibility:public"],
 )
diff --git a/cc/runfiles/runfiles.cc b/cc/runfiles/runfiles.cc
new file mode 100644
index 0000000..f580163
--- /dev/null
+++ b/cc/runfiles/runfiles.cc
@@ -0,0 +1,482 @@
+// Copyright 2018 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.
+
+#include "rules_cc/cc/runfiles/runfiles.h"
+
+#ifdef _WIN32
+#include <windows.h>
+#else  // not _WIN32
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+#endif  // _WIN32
+
+#include <fstream>
+#include <functional>
+#include <map>
+#include <sstream>
+#include <vector>
+#include <utility>
+
+#ifdef _WIN32
+#include <memory>
+#endif  // _WIN32
+
+namespace rules_cc {
+namespace cc {
+namespace runfiles {
+
+using std::function;
+using std::map;
+using std::pair;
+using std::string;
+using std::vector;
+
+namespace {
+
+bool starts_with(const string& s, const char* prefix) {
+  if (!prefix || !*prefix) {
+    return true;
+  }
+  if (s.empty()) {
+    return false;
+  }
+  return s.find(prefix) == 0;
+}
+
+bool contains(const string& s, const char* substr) {
+  if (!substr || !*substr) {
+    return true;
+  }
+  if (s.empty()) {
+    return false;
+  }
+  return s.find(substr) != string::npos;
+}
+
+bool ends_with(const string& s, const string& suffix) {
+  if (suffix.empty()) {
+    return true;
+  }
+  if (s.empty()) {
+    return false;
+  }
+  return s.rfind(suffix) == s.size() - suffix.size();
+}
+
+bool IsReadableFile(const string& path) {
+  return std::ifstream(path).is_open();
+}
+
+bool IsDirectory(const string& path) {
+#ifdef _WIN32
+  DWORD attrs = GetFileAttributesA(path.c_str());
+  return (attrs != INVALID_FILE_ATTRIBUTES) &&
+         (attrs & FILE_ATTRIBUTE_DIRECTORY);
+#else
+  struct stat buf;
+  return stat(path.c_str(), &buf) == 0 && S_ISDIR(buf.st_mode);
+#endif
+}
+
+bool PathsFrom(const std::string& argv0, std::string runfiles_manifest_file,
+               std::string runfiles_dir, std::string* out_manifest,
+               std::string* out_directory);
+
+bool PathsFrom(const std::string& argv0, std::string runfiles_manifest_file,
+               std::string runfiles_dir,
+               std::function<bool(const std::string&)> is_runfiles_manifest,
+               std::function<bool(const std::string&)> is_runfiles_directory,
+               std::string* out_manifest, std::string* out_directory);
+
+bool ParseManifest(const string& path, map<string, string>* result,
+                   string* error);
+bool ParseRepoMapping(const string& path,
+                      map<pair<string, string>, string>* result, string* error);
+
+}  // namespace
+
+Runfiles* Runfiles::Create(const string& argv0,
+                           const string& runfiles_manifest_file,
+                           const string& runfiles_dir,
+                           const string& source_repository, string* error) {
+  string manifest, directory;
+  if (!PathsFrom(argv0, runfiles_manifest_file, runfiles_dir, &manifest,
+                 &directory)) {
+    if (error) {
+      std::ostringstream err;
+      err << "ERROR: " << __FILE__ << "(" << __LINE__
+          << "): cannot find runfiles (argv0=\"" << argv0 << "\")";
+      *error = err.str();
+    }
+    return nullptr;
+  }
+
+  vector<pair<string, string> > envvars = {
+      {"RUNFILES_MANIFEST_FILE", manifest},
+      {"RUNFILES_DIR", directory},
+      // TODO(laszlocsomor): remove JAVA_RUNFILES once the Java launcher can
+      // pick up RUNFILES_DIR.
+      {"JAVA_RUNFILES", directory}};
+
+  map<string, string> runfiles;
+  if (!manifest.empty()) {
+    if (!ParseManifest(manifest, &runfiles, error)) {
+      return nullptr;
+    }
+  }
+
+  map<pair<string, string>, string> mapping;
+  if (!ParseRepoMapping(
+          RlocationUnchecked("_repo_mapping", runfiles, directory), &mapping,
+          error)) {
+    return nullptr;
+  }
+
+  return new Runfiles(std::move(runfiles), std::move(directory),
+                      std::move(mapping), std::move(envvars),
+                      string(source_repository));
+}
+
+bool IsAbsolute(const string& path) {
+  if (path.empty()) {
+    return false;
+  }
+  char c = path.front();
+  return (c == '/' && (path.size() < 2 || path[1] != '/')) ||
+         (path.size() >= 3 &&
+          ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) &&
+          path[1] == ':' && (path[2] == '\\' || path[2] == '/'));
+}
+
+string GetEnv(const string& key) {
+#ifdef _WIN32
+  DWORD size = ::GetEnvironmentVariableA(key.c_str(), nullptr, 0);
+  if (size == 0) {
+    return string();  // unset or empty envvar
+  }
+  std::unique_ptr<char[]> value(new char[size]);
+  ::GetEnvironmentVariableA(key.c_str(), value.get(), size);
+  return value.get();
+#else
+  char* result = getenv(key.c_str());
+  return (result == nullptr) ? string() : string(result);
+#endif
+}
+
+// Replaces \s, \n, and \b with their respective characters.
+string Unescape(const string& path) {
+  string result;
+  result.reserve(path.size());
+  for (size_t i = 0; i < path.size(); ++i) {
+    if (path[i] == '\\' && i + 1 < path.size()) {
+      switch (path[i + 1]) {
+        case 's': {
+          result.push_back(' ');
+          break;
+        }
+        case 'n': {
+          result.push_back('\n');
+          break;
+        }
+        case 'b': {
+          result.push_back('\\');
+          break;
+        }
+        default: {
+          result.push_back(path[i]);
+          result.push_back(path[i + 1]);
+          break;
+        }
+      }
+      ++i;
+    } else {
+      result.push_back(path[i]);
+    }
+  }
+  return result;
+}
+
+string Runfiles::Rlocation(const string& path) const {
+  return Rlocation(path, source_repository_);
+}
+
+string Runfiles::Rlocation(const string& path,
+                           const string& source_repo) const {
+  if (path.empty() || starts_with(path, "../") || contains(path, "/..") ||
+      starts_with(path, "./") || contains(path, "/./") ||
+      ends_with(path, "/.") || contains(path, "//")) {
+    return string();
+  }
+  if (IsAbsolute(path)) {
+    return path;
+  }
+
+  string::size_type first_slash = path.find_first_of('/');
+  if (first_slash == string::npos) {
+    return RlocationUnchecked(path, runfiles_map_, directory_);
+  }
+  string target_apparent = path.substr(0, first_slash);
+  auto target =
+      repo_mapping_.find(std::make_pair(source_repo, target_apparent));
+  if (target == repo_mapping_.cend()) {
+    return RlocationUnchecked(path, runfiles_map_, directory_);
+  }
+  return RlocationUnchecked(target->second + path.substr(first_slash),
+                            runfiles_map_, directory_);
+}
+
+string Runfiles::RlocationUnchecked(const string& path,
+                                    const map<string, string>& runfiles_map,
+                                    const string& directory) {
+  const auto exact_match = runfiles_map.find(path);
+  if (exact_match != runfiles_map.end()) {
+    return exact_match->second;
+  }
+  if (!runfiles_map.empty()) {
+    // If path references a runfile that lies under a directory that itself is a
+    // runfile, then only the directory is listed in the manifest. Look up all
+    // prefixes of path in the manifest and append the relative path from the
+    // prefix to the looked up path.
+    std::size_t prefix_end = path.size();
+    while ((prefix_end = path.find_last_of('/', prefix_end - 1)) !=
+           string::npos) {
+      const string prefix = path.substr(0, prefix_end);
+      const auto prefix_match = runfiles_map.find(prefix);
+      if (prefix_match != runfiles_map.end()) {
+        return prefix_match->second + "/" + path.substr(prefix_end + 1);
+      }
+    }
+  }
+  if (!directory.empty()) {
+    return directory + "/" + path;
+  }
+  return "";
+}
+
+namespace {
+
+bool ParseManifest(const string& path, map<string, string>* result,
+                   string* error) {
+  std::ifstream stm(path);
+  if (!stm.is_open()) {
+    if (error) {
+      std::ostringstream err;
+      err << "ERROR: " << __FILE__ << "(" << __LINE__
+          << "): cannot open runfiles manifest \"" << path << "\"";
+      *error = err.str();
+    }
+    return false;
+  }
+  string line;
+  std::getline(stm, line);
+  size_t line_count = 1;
+  while (!line.empty()) {
+    std::string source;
+    std::string target;
+    if (line[0] == ' ') {
+      // The link path contains escape sequences for spaces and backslashes.
+      string::size_type idx = line.find(' ', 1);
+      if (idx == string::npos) {
+        if (error) {
+          std::ostringstream err;
+          err << "ERROR: " << __FILE__ << "(" << __LINE__
+              << "): bad runfiles manifest entry in \"" << path << "\" line #"
+              << line_count << ": \"" << line << "\"";
+          *error = err.str();
+        }
+        return false;
+      }
+      source = Unescape(line.substr(1, idx - 1));
+      target = Unescape(line.substr(idx + 1));
+    } else {
+      string::size_type idx = line.find(' ');
+      if (idx == string::npos) {
+        if (error) {
+          std::ostringstream err;
+          err << "ERROR: " << __FILE__ << "(" << __LINE__
+              << "): bad runfiles manifest entry in \"" << path << "\" line #"
+              << line_count << ": \"" << line << "\"";
+          *error = err.str();
+        }
+        return false;
+      }
+      source = line.substr(0, idx);
+      target = line.substr(idx + 1);
+    }
+    (*result)[source] = target;
+    std::getline(stm, line);
+    ++line_count;
+  }
+  return true;
+}
+
+bool ParseRepoMapping(const string& path,
+                      map<pair<string, string>, string>* result,
+                      string* error) {
+  std::ifstream stm(path);
+  if (!stm.is_open()) {
+    return true;
+  }
+  string line;
+  std::getline(stm, line);
+  size_t line_count = 1;
+  while (!line.empty()) {
+    string::size_type first_comma = line.find_first_of(',');
+    if (first_comma == string::npos) {
+      if (error) {
+        std::ostringstream err;
+        err << "ERROR: " << __FILE__ << "(" << __LINE__
+            << "): bad repository mapping entry in \"" << path << "\" line #"
+            << line_count << ": \"" << line << "\"";
+        *error = err.str();
+      }
+      return false;
+    }
+    string::size_type second_comma = line.find_first_of(',', first_comma + 1);
+    if (second_comma == string::npos) {
+      if (error) {
+        std::ostringstream err;
+        err << "ERROR: " << __FILE__ << "(" << __LINE__
+            << "): bad repository mapping entry in \"" << path << "\" line #"
+            << line_count << ": \"" << line << "\"";
+        *error = err.str();
+      }
+      return false;
+    }
+
+    string source = line.substr(0, first_comma);
+    string target_apparent =
+        line.substr(first_comma + 1, second_comma - (first_comma + 1));
+    string target = line.substr(second_comma + 1);
+
+    (*result)[std::make_pair(source, target_apparent)] = target;
+    std::getline(stm, line);
+    ++line_count;
+  }
+  return true;
+}
+
+}  // namespace
+
+namespace testing {
+
+bool TestOnly_PathsFrom(const string& argv0, string mf, string dir,
+                        function<bool(const string&)> is_runfiles_manifest,
+                        function<bool(const string&)> is_runfiles_directory,
+                        string* out_manifest, string* out_directory) {
+  return PathsFrom(argv0, mf, dir, is_runfiles_manifest, is_runfiles_directory,
+                   out_manifest, out_directory);
+}
+
+bool TestOnly_IsAbsolute(const string& path) { return IsAbsolute(path); }
+
+}  // namespace testing
+
+Runfiles* Runfiles::Create(const std::string& argv0,
+                           const std::string& runfiles_manifest_file,
+                           const std::string& runfiles_dir,
+                           std::string* error) {
+  return Runfiles::Create(argv0, runfiles_manifest_file, runfiles_dir, "",
+                          error);
+}
+
+Runfiles* Runfiles::Create(const string& argv0, const string& source_repository,
+                           string* error) {
+  return Runfiles::Create(argv0, GetEnv("RUNFILES_MANIFEST_FILE"),
+                          GetEnv("RUNFILES_DIR"), source_repository, error);
+}
+
+Runfiles* Runfiles::Create(const string& argv0, string* error) {
+  return Runfiles::Create(argv0, "", error);
+}
+
+Runfiles* Runfiles::CreateForTest(const string& source_repository,
+                                  std::string* error) {
+  return Runfiles::Create(std::string(), GetEnv("RUNFILES_MANIFEST_FILE"),
+                          GetEnv("TEST_SRCDIR"), source_repository, error);
+}
+
+Runfiles* Runfiles::CreateForTest(std::string* error) {
+  return Runfiles::CreateForTest("", error);
+}
+
+namespace {
+
+bool PathsFrom(const string& argv0, string mf, string dir, string* out_manifest,
+               string* out_directory) {
+  return PathsFrom(
+      argv0, mf, dir, [](const string& path) { return IsReadableFile(path); },
+      [](const string& path) { return IsDirectory(path); }, out_manifest,
+      out_directory);
+}
+
+bool PathsFrom(const string& argv0, string mf, string dir,
+               function<bool(const string&)> is_runfiles_manifest,
+               function<bool(const string&)> is_runfiles_directory,
+               string* out_manifest, string* out_directory) {
+  out_manifest->clear();
+  out_directory->clear();
+
+  bool mfValid = is_runfiles_manifest(mf);
+  bool dirValid = is_runfiles_directory(dir);
+
+  if (!argv0.empty() && !mfValid && !dirValid) {
+    mf = argv0 + ".runfiles/MANIFEST";
+    dir = argv0 + ".runfiles";
+    mfValid = is_runfiles_manifest(mf);
+    dirValid = is_runfiles_directory(dir);
+    if (!mfValid) {
+      mf = argv0 + ".runfiles_manifest";
+      mfValid = is_runfiles_manifest(mf);
+    }
+  }
+
+  if (!mfValid && !dirValid) {
+    return false;
+  }
+
+  if (!mfValid) {
+    mf = dir + "/MANIFEST";
+    mfValid = is_runfiles_manifest(mf);
+    if (!mfValid) {
+      mf = dir + "_manifest";
+      mfValid = is_runfiles_manifest(mf);
+    }
+  }
+
+  if (!dirValid &&
+      (ends_with(mf, ".runfiles_manifest") || ends_with(mf, "/MANIFEST"))) {
+    static const size_t kSubstrLen = 9;  // "_manifest" or "/MANIFEST"
+    dir = mf.substr(0, mf.size() - kSubstrLen);
+    dirValid = is_runfiles_directory(dir);
+  }
+
+  if (mfValid) {
+    *out_manifest = mf;
+  }
+
+  if (dirValid) {
+    *out_directory = dir;
+  }
+
+  return true;
+}
+
+}  // namespace
+
+}  // namespace runfiles
+}  // namespace cc
+}  // namespace rules_cc
diff --git a/cc/runfiles/runfiles.h b/cc/runfiles/runfiles.h
new file mode 100644
index 0000000..d80a602
--- /dev/null
+++ b/cc/runfiles/runfiles.h
@@ -0,0 +1,264 @@
+// Copyright 2018 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.
+
+// Runfiles lookup library for Bazel-built C++ binaries and tests.
+//
+// USAGE:
+// 1.  Depend on this runfiles library from your build rule:
+//
+//       cc_binary(
+//           name = "my_binary",
+//           ...
+//           deps = ["@rules_cc//cc/runfiles"],
+//       )
+//
+// 2.  Include the runfiles library.
+//
+//       #include "rules_cc/cc/runfiles/runfiles.h"
+//
+//       using rules_cc::cc::runfiles::Runfiles;
+//
+// 3.  Create a Runfiles object and use rlocation to look up runfile paths:
+//
+//       int main(int argc, char** argv) {
+//         std::string error;
+//         std::unique_ptr<Runfiles> runfiles(
+//             Runfiles::Create(argv[0], BAZEL_CURRENT_REPOSITORY, &error));
+//
+//         // Important:
+//         //   If this is a test, use
+//         //   Runfiles::CreateForTest(BAZEL_CURRENT_REPOSITORY, &error).
+//
+//         if (runfiles == nullptr) {
+//           ...  // error handling
+//         }
+//         std::string path =
+//             runfiles->Rlocation("my_workspace/path/to/my/data.txt");
+//         ...
+//
+//      The code above creates a Runfiles object and retrieves a runfile path.
+//      The BAZEL_CURRENT_REPOSITORY macro is available in every target that
+//      depends on the runfiles library.
+//
+//      The Runfiles::Create function uses the runfiles manifest and the
+//      runfiles directory from the RUNFILES_MANIFEST_FILE and RUNFILES_DIR
+//      environment variables. If not present, the function looks for the
+//      manifest and directory near argv[0], the path of the main program.
+//
+// To start child processes that also need runfiles, you need to set the right
+// environment variables for them:
+//
+//   std::unique_ptr<Runfiles> runfiles(Runfiles::Create(
+//     argv[0], BAZEL_CURRENT_REPOSITORY, &error));
+//
+//   std::string path = runfiles->Rlocation("path/to/binary"));
+//   if (!path.empty()) {
+//     ... // create "args" argument vector for execv
+//     const auto envvars = runfiles->EnvVars();
+//     pid_t child = fork();
+//     if (child) {
+//       int status;
+//       waitpid(child, &status, 0);
+//     } else {
+//       for (const auto i : envvars) {
+//         setenv(i.first.c_str(), i.second.c_str(), 1);
+//       }
+//       execv(args[0], args);
+//     }
+
+#ifndef RULES_CC_CC_RUNFILES_RUNFILES_H_
+#define RULES_CC_CC_RUNFILES_RUNFILES_H_ 1
+
+#include <functional>
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace rules_cc {
+namespace cc {
+namespace runfiles {
+
+class Runfiles {
+ public:
+  virtual ~Runfiles() {}
+
+  // Returns a new `Runfiles` instance.
+  //
+  // Use this from within `cc_test` rules.
+  //
+  // Returns nullptr on error. If `error` is provided, the method prints an
+  // error message into it.
+  //
+  // This method looks at the RUNFILES_MANIFEST_FILE and TEST_SRCDIR
+  // environment variables.
+  //
+  // If source_repository is not provided, it defaults to the main repository
+  // (also known as the workspace).
+  static Runfiles* CreateForTest(std::string* error = nullptr);
+  static Runfiles* CreateForTest(const std::string& source_repository,
+                                 std::string* error = nullptr);
+
+  // Returns a new `Runfiles` instance.
+  //
+  // Use this from `cc_binary` or `cc_library` rules. You may pass an empty
+  // `argv0` if `argv[0]` from the `main` method is unknown.
+  //
+  // Returns nullptr on error. If `error` is provided, the method prints an
+  // error message into it.
+  //
+  // This method looks at the RUNFILES_MANIFEST_FILE and RUNFILES_DIR
+  // environment variables. If either is empty, the method looks for the
+  // manifest or directory using the other environment variable, or using argv0
+  // (unless it's empty).
+  //
+  // If source_repository is not provided, it defaults to the main repository
+  // (also known as the workspace).
+  static Runfiles* Create(const std::string& argv0,
+                          std::string* error = nullptr);
+  static Runfiles* Create(const std::string& argv0,
+                          const std::string& source_repository,
+                          std::string* error = nullptr);
+
+  // Returns a new `Runfiles` instance.
+  //
+  // Use this from any `cc_*` rule if you want to manually specify the paths to
+  // the runfiles manifest and/or runfiles directory. You may pass an empty
+  // `argv0` if `argv[0]` from the `main` method is unknown.
+  //
+  // This method is the same as `Create(argv0, error)`, except it uses
+  // `runfiles_manifest_file` and `runfiles_dir` as the corresponding
+  // environment variable values, instead of looking up the actual environment
+  // variables.
+  static Runfiles* Create(const std::string& argv0,
+                          const std::string& runfiles_manifest_file,
+                          const std::string& runfiles_dir,
+                          std::string* error = nullptr);
+  static Runfiles* Create(const std::string& argv0,
+                          const std::string& runfiles_manifest_file,
+                          const std::string& runfiles_dir,
+                          const std::string& source_repository,
+                          std::string* error = nullptr);
+
+  // Returns the runtime path of a runfile.
+  //
+  // Runfiles are data-dependencies of Bazel-built binaries and tests.
+  //
+  // The returned path may not exist. The caller should verify the path's
+  // existence.
+  //
+  // The function may return an empty string if it cannot find a runfile.
+  //
+  // Args:
+  //   path: runfiles-root-relative path of the runfile; must not be empty and
+  //     must not contain uplevel references.
+  //   source_repository: if provided, overrides the source repository set when
+  //     this Runfiles instance was created.
+  // Returns:
+  //   the path to the runfile, which the caller should check for existence, or
+  //   an empty string if the method doesn't know about this runfile
+  std::string Rlocation(const std::string& path) const;
+  std::string Rlocation(const std::string& path,
+                        const std::string& source_repository) const;
+
+  // Returns environment variables for subprocesses.
+  //
+  // The caller should set the returned key-value pairs in the environment of
+  // subprocesses, so that those subprocesses can also access runfiles (in case
+  // they are also Bazel-built binaries).
+  const std::vector<std::pair<std::string, std::string> >& EnvVars() const {
+    return envvars_;
+  }
+
+  // Returns a new Runfiles instance that by default uses the provided source
+  // repository as a default for all calls to Rlocation.
+  //
+  // The current instance remains valid.
+  std::unique_ptr<Runfiles> WithSourceRepository(
+      const std::string& source_repository) const {
+    return std::unique_ptr<Runfiles>(new Runfiles(
+        runfiles_map_, directory_, repo_mapping_, envvars_, source_repository));
+  }
+
+ private:
+  Runfiles(
+      std::map<std::string, std::string> runfiles_map, std::string directory,
+      std::map<std::pair<std::string, std::string>, std::string> repo_mapping,
+      std::vector<std::pair<std::string, std::string> > envvars,
+      std::string source_repository)
+      : runfiles_map_(std::move(runfiles_map)),
+        directory_(std::move(directory)),
+        repo_mapping_(std::move(repo_mapping)),
+        envvars_(std::move(envvars)),
+        source_repository_(std::move(source_repository)) {}
+  Runfiles(const Runfiles&) = delete;
+  Runfiles(Runfiles&&) = delete;
+  Runfiles& operator=(const Runfiles&) = delete;
+  Runfiles& operator=(Runfiles&&) = delete;
+
+  static std::string RlocationUnchecked(
+      const std::string& path,
+      const std::map<std::string, std::string>& runfiles_map,
+      const std::string& directory);
+
+  const std::map<std::string, std::string> runfiles_map_;
+  const std::string directory_;
+  const std::map<std::pair<std::string, std::string>, std::string>
+      repo_mapping_;
+  const std::vector<std::pair<std::string, std::string> > envvars_;
+  const std::string source_repository_;
+};
+
+// The "testing" namespace contains functions that allow unit testing the code.
+// Do not use these outside of runfiles_test.cc, they are only part of the
+// public API for the benefit of the tests.
+// These functions and their interface may change without notice.
+namespace testing {
+
+// For testing only.
+//
+// Computes the path of the runfiles manifest and the runfiles directory.
+//
+// If the method finds both a valid manifest and valid directory according to
+// `is_runfiles_manifest` and `is_runfiles_directory`, then the method sets
+// the corresponding values to `out_manifest` and `out_directory` and returns
+// true.
+//
+// If the method only finds a valid manifest or a valid directory, but not
+// both, then it sets the corresponding output variable (`out_manifest` or
+// `out_directory`) to the value while clearing the other output variable. The
+// method still returns true in this case.
+//
+// If the method cannot find either a valid manifest or valid directory, it
+// clears both output variables and returns false.
+bool TestOnly_PathsFrom(
+    const std::string& argv0, std::string runfiles_manifest_file,
+    std::string runfiles_dir,
+    std::function<bool(const std::string&)> is_runfiles_manifest,
+    std::function<bool(const std::string&)> is_runfiles_directory,
+    std::string* out_manifest, std::string* out_directory);
+
+// For testing only.
+// Returns true if `path` is an absolute Unix or Windows path.
+// For Windows paths, this function does not regard drive-less absolute paths
+// (i.e. absolute-on-current-drive, e.g. "\foo\bar") as absolute and returns
+// false for these.
+bool TestOnly_IsAbsolute(const std::string& path);
+
+}  // namespace testing
+}  // namespace runfiles
+}  // namespace cc
+}  // namespace rules_cc
+
+#endif  // RULES_CC_CC_RUNFILES_RUNFILES_H_
diff --git a/tests/runfiles/BUILD b/tests/runfiles/BUILD
new file mode 100644
index 0000000..4d906a7
--- /dev/null
+++ b/tests/runfiles/BUILD
@@ -0,0 +1,12 @@
+load("//cc:cc_test.bzl", "cc_test")
+
+licenses(["notice"])
+
+cc_test(
+    name = "runfiles_test",
+    srcs = ["runfiles_test.cc"],
+    deps = [
+        "//cc/runfiles",
+        "@googletest//:gtest_main",
+    ],
+)
diff --git a/tests/runfiles/runfiles_test.cc b/tests/runfiles/runfiles_test.cc
new file mode 100644
index 0000000..0f7e894
--- /dev/null
+++ b/tests/runfiles/runfiles_test.cc
@@ -0,0 +1,880 @@
+// Copyright 2018 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.
+
+#include "rules_cc/cc/runfiles/runfiles.h"
+
+#ifdef _WIN32
+#include <windows.h>
+#endif  // _WIN32
+
+#include <fstream>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "gtest/gtest.h"
+
+#define RUNFILES_TEST_TOSTRING_HELPER(x) #x
+#define RUNFILES_TEST_TOSTRING(x) RUNFILES_TEST_TOSTRING_HELPER(x)
+#define LINE_AS_STRING() RUNFILES_TEST_TOSTRING(__LINE__)
+
+namespace rules_cc {
+namespace cc {
+namespace runfiles {
+namespace {
+
+using rules_cc::cc::runfiles::testing::TestOnly_IsAbsolute;
+using rules_cc::cc::runfiles::testing::TestOnly_PathsFrom;
+using std::cerr;
+using std::endl;
+using std::function;
+using std::pair;
+using std::string;
+using std::unique_ptr;
+using std::vector;
+
+class RunfilesTest : public ::testing::Test {
+ protected:
+  // Create a temporary file that is deleted with the destructor.
+  class MockFile {
+   public:
+    // Create an empty file with the given name under $TEST_TMPDIR.
+    static MockFile* Create(const string& name);
+
+    // Create a file with the given name and contents under $TEST_TMPDIR.
+    // The method ensures to create all parent directories, so `name` is allowed
+    // to contain directory components.
+    static MockFile* Create(const string& name, const vector<string>& lines);
+
+    ~MockFile();
+    const string& Path() const { return path_; }
+
+    string DirName() const {
+      string::size_type pos = path_.find_last_of('/');
+      return pos == string::npos ? "" : path_.substr(0, pos);
+    }
+
+   private:
+    MockFile(const string& path) : path_(path) {}
+    MockFile(const MockFile&) = delete;
+    MockFile(MockFile&&) = delete;
+    MockFile& operator=(const MockFile&) = delete;
+    MockFile& operator=(MockFile&&) = delete;
+
+    const string path_;
+  };
+
+  void AssertEnvvars(const Runfiles& runfiles,
+                     const string& expected_manifest_file,
+                     const string& expected_directory);
+
+  static string GetTemp();
+};
+
+void RunfilesTest::AssertEnvvars(const Runfiles& runfiles,
+                                 const string& expected_manifest_file,
+                                 const string& expected_directory) {
+  vector<pair<string, string> > expected = {
+      {"RUNFILES_MANIFEST_FILE", expected_manifest_file},
+      {"RUNFILES_DIR", expected_directory},
+      {"JAVA_RUNFILES", expected_directory}};
+  ASSERT_EQ(runfiles.EnvVars(), expected);
+}
+
+string RunfilesTest::GetTemp() {
+#ifdef _WIN32
+  DWORD size = ::GetEnvironmentVariableA("TEST_TMPDIR", nullptr, 0);
+  if (size == 0) {
+    return string();  // unset or empty envvar
+  }
+  unique_ptr<char[]> value(new char[size]);
+  ::GetEnvironmentVariableA("TEST_TMPDIR", value.get(), size);
+  return value.get();
+#else
+  char* result = getenv("TEST_TMPDIR");
+  return result != nullptr ? string(result) : string();
+#endif
+}
+
+RunfilesTest::MockFile* RunfilesTest::MockFile::Create(const string& name) {
+  return Create(name, vector<string>());
+}
+
+RunfilesTest::MockFile* RunfilesTest::MockFile::Create(
+    const string& name, const vector<string>& lines) {
+  if (name.find("..") != string::npos || TestOnly_IsAbsolute(name)) {
+    cerr << "WARNING: " << __FILE__ << "(" << __LINE__ << "): bad name: \""
+         << name << "\"" << endl;
+    return nullptr;
+  }
+
+  string tmp(RunfilesTest::GetTemp());
+  if (tmp.empty()) {
+    cerr << "WARNING: " << __FILE__ << "(" << __LINE__
+         << "): $TEST_TMPDIR is empty" << endl;
+    return nullptr;
+  }
+  string path(tmp + "/" + name);
+
+  string::size_type i = 0;
+#ifdef _WIN32
+  while ((i = name.find_first_of("/\\", i + 1)) != string::npos) {
+    string d = tmp + "\\" + name.substr(0, i);
+    if (!CreateDirectoryA(d.c_str(), nullptr)) {
+      cerr << "ERROR: " << __FILE__ << "(" << __LINE__
+           << "): failed to create directory \"" << d << "\"" << endl;
+      return nullptr;
+    }
+  }
+#else
+  while ((i = name.find_first_of('/', i + 1)) != string::npos) {
+    string d = tmp + "/" + name.substr(0, i);
+    if (mkdir(d.c_str(), 0777)) {
+      cerr << "ERROR: " << __FILE__ << "(" << __LINE__
+           << "): failed to create directory \"" << d << "\"" << endl;
+      return nullptr;
+    }
+  }
+#endif
+
+  auto stm = std::ofstream(path);
+  for (auto i : lines) {
+    stm << i << std::endl;
+  }
+  return new MockFile(path);
+}
+
+RunfilesTest::MockFile::~MockFile() { std::remove(path_.c_str()); }
+
+TEST_F(RunfilesTest, CreatesManifestBasedRunfilesFromManifestNextToBinary) {
+  unique_ptr<MockFile> mf(MockFile::Create(
+      "foo" LINE_AS_STRING() ".runfiles_manifest", {"a/b c/d"}));
+  ASSERT_TRUE(mf != nullptr);
+  string argv0(mf->Path().substr(
+      0, mf->Path().size() - string(".runfiles_manifest").size()));
+
+  string error;
+  unique_ptr<Runfiles> r(Runfiles::Create(argv0, /*runfiles_manifest_file=*/"",
+                                          /*runfiles_dir=*/"", &error));
+  ASSERT_TRUE(r != nullptr);
+  EXPECT_TRUE(error.empty());
+  EXPECT_EQ(r->Rlocation("a/b"), "c/d");
+  // We know it's manifest-based because it returns empty string for unknown
+  // paths.
+  EXPECT_EQ(r->Rlocation("unknown"), "");
+  AssertEnvvars(*r, mf->Path(), "");
+}
+
+TEST_F(RunfilesTest,
+       CreatesManifestBasedRunfilesFromManifestInRunfilesDirectory) {
+  unique_ptr<MockFile> mf(MockFile::Create(
+      "foo" LINE_AS_STRING() ".runfiles/MANIFEST", {"a/b c/d"}));
+  ASSERT_TRUE(mf != nullptr);
+  string argv0(mf->Path().substr(
+      0, mf->Path().size() - string(".runfiles/MANIFEST").size()));
+
+  string error;
+  unique_ptr<Runfiles> r(Runfiles::Create(argv0, /*runfiles_manifest_file=*/"",
+                                          /*runfiles_dir=*/"", &error));
+  ASSERT_TRUE(r != nullptr);
+  EXPECT_TRUE(error.empty());
+  EXPECT_EQ(r->Rlocation("a/b"), "c/d");
+  EXPECT_EQ(r->Rlocation("foo"), argv0 + ".runfiles/foo");
+  AssertEnvvars(*r, mf->Path(), argv0 + ".runfiles");
+}
+
+TEST_F(RunfilesTest, CreatesManifestBasedRunfilesFromEnvvar) {
+  unique_ptr<MockFile> mf(MockFile::Create(
+      "foo" LINE_AS_STRING() ".runfiles_manifest", {"a/b c/d"}));
+  ASSERT_TRUE(mf != nullptr);
+
+  string error;
+  unique_ptr<Runfiles> r(Runfiles::Create("ignore-argv0", mf->Path(),
+                                          "non-existent-runfiles_dir", &error));
+  ASSERT_TRUE(r != nullptr);
+  EXPECT_TRUE(error.empty());
+  EXPECT_EQ(r->Rlocation("a/b"), "c/d");
+  // We know it's manifest-based because it returns empty string for unknown
+  // paths.
+  EXPECT_EQ(r->Rlocation("unknown"), "");
+  AssertEnvvars(*r, mf->Path(), "");
+}
+
+TEST_F(RunfilesTest, CannotCreateManifestBasedRunfilesDueToBadManifest) {
+  unique_ptr<MockFile> mf(MockFile::Create(
+      "foo" LINE_AS_STRING() ".runfiles_manifest", {"a b", "nospace"}));
+  ASSERT_TRUE(mf != nullptr);
+
+  string error;
+  unique_ptr<Runfiles> r(
+      Runfiles::Create("ignore-argv0", mf->Path(), "", &error));
+  EXPECT_EQ(r, nullptr);
+  EXPECT_NE(error.find("bad runfiles manifest entry"), string::npos);
+  EXPECT_NE(error.find("line #2: \"nospace\""), string::npos);
+}
+
+TEST_F(RunfilesTest, ManifestBasedRunfilesRlocationAndEnvVars) {
+  unique_ptr<MockFile> mf(
+      MockFile::Create("foo" LINE_AS_STRING() ".runfiles_manifest",
+                       {
+                           "a/b c/d",
+                           "e/f target path with spaces",
+                           " h/\\si j k",
+                           " dir\\swith\\sspaces l/m",
+                           " h/\\n\\s\\bi j k \\n\\b",
+                           "not_escaped with\\backslash and spaces",
+                       }));
+  ASSERT_TRUE(mf != nullptr);
+
+  string error;
+  unique_ptr<Runfiles> r(
+      Runfiles::Create("ignore-argv0", mf->Path(), "", &error));
+
+  ASSERT_TRUE(r != nullptr);
+  EXPECT_TRUE(error.empty());
+  EXPECT_EQ(r->Rlocation("a/b"), "c/d");
+  EXPECT_EQ(r->Rlocation("c/d"), "");
+  EXPECT_EQ(r->Rlocation(""), "");
+  EXPECT_EQ(r->Rlocation("foo"), "");
+  EXPECT_EQ(r->Rlocation("foo/"), "");
+  EXPECT_EQ(r->Rlocation("foo/bar"), "");
+  EXPECT_EQ(r->Rlocation("../foo"), "");
+  EXPECT_EQ(r->Rlocation("foo/.."), "");
+  EXPECT_EQ(r->Rlocation("foo/../bar"), "");
+  EXPECT_EQ(r->Rlocation("./foo"), "");
+  EXPECT_EQ(r->Rlocation("foo/."), "");
+  EXPECT_EQ(r->Rlocation("foo/./bar"), "");
+  EXPECT_EQ(r->Rlocation("//foo"), "");
+  EXPECT_EQ(r->Rlocation("foo//"), "");
+  EXPECT_EQ(r->Rlocation("foo//bar"), "");
+  EXPECT_EQ(r->Rlocation("/Foo"), "/Foo");
+  EXPECT_EQ(r->Rlocation("c:/Foo"), "c:/Foo");
+  EXPECT_EQ(r->Rlocation("c:\\Foo"), "c:\\Foo");
+  EXPECT_EQ(r->Rlocation("a/b/file"), "c/d/file");
+  EXPECT_EQ(r->Rlocation("a/b/deeply/nested/file"), "c/d/deeply/nested/file");
+  EXPECT_EQ(r->Rlocation("a/b/deeply/nested/file with spaces"),
+            "c/d/deeply/nested/file with spaces");
+  EXPECT_EQ(r->Rlocation("e/f"), "target path with spaces");
+  EXPECT_EQ(r->Rlocation("e/f/file"), "target path with spaces/file");
+  EXPECT_EQ(r->Rlocation("h/ i"), "j k");
+  EXPECT_EQ(r->Rlocation("h/\n \\i"), "j k \n\\");
+  EXPECT_EQ(r->Rlocation("dir with spaces"), "l/m");
+  EXPECT_EQ(r->Rlocation("dir with spaces/file"), "l/m/file");
+  EXPECT_EQ(r->Rlocation("not_escaped"), "with\\backslash and spaces");
+}
+
+TEST_F(RunfilesTest, DirectoryBasedRunfilesRlocationAndEnvVars) {
+  unique_ptr<MockFile> dummy(
+      MockFile::Create("foo" LINE_AS_STRING() ".runfiles/dummy", {"a/b c/d"}));
+  ASSERT_TRUE(dummy != nullptr);
+  string dir = dummy->DirName();
+
+  string error;
+  unique_ptr<Runfiles> r(Runfiles::Create("ignore-argv0", "", dir, &error));
+  ASSERT_TRUE(r != nullptr);
+  EXPECT_TRUE(error.empty());
+
+  EXPECT_EQ(r->Rlocation("a/b"), dir + "/a/b");
+  EXPECT_EQ(r->Rlocation("c/d"), dir + "/c/d");
+  EXPECT_EQ(r->Rlocation(""), "");
+  EXPECT_EQ(r->Rlocation("foo"), dir + "/foo");
+  EXPECT_EQ(r->Rlocation("foo/"), dir + "/foo/");
+  EXPECT_EQ(r->Rlocation("foo/bar"), dir + "/foo/bar");
+  EXPECT_EQ(r->Rlocation("../foo"), "");
+  EXPECT_EQ(r->Rlocation("foo/.."), "");
+  EXPECT_EQ(r->Rlocation("foo/../bar"), "");
+  EXPECT_EQ(r->Rlocation("./foo"), "");
+  EXPECT_EQ(r->Rlocation("foo/."), "");
+  EXPECT_EQ(r->Rlocation("foo/./bar"), "");
+  EXPECT_EQ(r->Rlocation("//foo"), "");
+  EXPECT_EQ(r->Rlocation("foo//"), "");
+  EXPECT_EQ(r->Rlocation("foo//bar"), "");
+  EXPECT_EQ(r->Rlocation("/Foo"), "/Foo");
+  EXPECT_EQ(r->Rlocation("c:/Foo"), "c:/Foo");
+  EXPECT_EQ(r->Rlocation("c:\\Foo"), "c:\\Foo");
+  AssertEnvvars(*r, "", dir);
+}
+
+TEST_F(RunfilesTest, ManifestAndDirectoryBasedRunfilesRlocationAndEnvVars) {
+  unique_ptr<MockFile> mf(MockFile::Create(
+      "foo" LINE_AS_STRING() ".runfiles/MANIFEST", {"a/b c/d"}));
+  ASSERT_TRUE(mf != nullptr);
+  string dir = mf->DirName();
+
+  string error;
+  unique_ptr<Runfiles> r(
+      Runfiles::Create("ignore-argv0", mf->Path(), "", &error));
+
+  ASSERT_TRUE(r != nullptr);
+  EXPECT_TRUE(error.empty());
+  EXPECT_EQ(r->Rlocation("a/b"), "c/d");
+  EXPECT_EQ(r->Rlocation("c/d"), dir + "/c/d");
+  EXPECT_EQ(r->Rlocation(""), "");
+  EXPECT_EQ(r->Rlocation("foo"), dir + "/foo");
+  EXPECT_EQ(r->Rlocation("foo/"), dir + "/foo/");
+  EXPECT_EQ(r->Rlocation("foo/bar"), dir + "/foo/bar");
+  EXPECT_EQ(r->Rlocation("../foo"), "");
+  EXPECT_EQ(r->Rlocation("foo/.."), "");
+  EXPECT_EQ(r->Rlocation("foo/../bar"), "");
+  EXPECT_EQ(r->Rlocation("./foo"), "");
+  EXPECT_EQ(r->Rlocation("foo/."), "");
+  EXPECT_EQ(r->Rlocation("foo/./bar"), "");
+  EXPECT_EQ(r->Rlocation("//foo"), "");
+  EXPECT_EQ(r->Rlocation("foo//"), "");
+  EXPECT_EQ(r->Rlocation("foo//bar"), "");
+  EXPECT_EQ(r->Rlocation("/Foo"), "/Foo");
+  EXPECT_EQ(r->Rlocation("c:/Foo"), "c:/Foo");
+  EXPECT_EQ(r->Rlocation("c:\\Foo"), "c:\\Foo");
+  EXPECT_EQ(r->Rlocation("a/b/file"), "c/d/file");
+  EXPECT_EQ(r->Rlocation("a/b/deeply/nested/file"), "c/d/deeply/nested/file");
+  AssertEnvvars(*r, mf->Path(), dir);
+}
+
+TEST_F(RunfilesTest, ManifestBasedRunfilesEnvVars) {
+  unique_ptr<MockFile> mf(
+      MockFile::Create(string("foo" LINE_AS_STRING() ".runfiles_manifest")));
+  ASSERT_TRUE(mf != nullptr);
+
+  string error;
+  unique_ptr<Runfiles> r(
+      Runfiles::Create("ignore-argv0", mf->Path(), "", &error));
+  ASSERT_TRUE(r != nullptr);
+  EXPECT_TRUE(error.empty());
+
+  AssertEnvvars(*r, mf->Path(), "");
+}
+
+TEST_F(RunfilesTest, CreatesDirectoryBasedRunfilesFromDirectoryNextToBinary) {
+  // We create a directory as a side-effect of creating a mock file.
+  unique_ptr<MockFile> mf(
+      MockFile::Create(string("foo" LINE_AS_STRING() ".runfiles/dummy")));
+  string argv0(mf->Path().substr(
+      0, mf->Path().size() - string(".runfiles/dummy").size()));
+
+  string error;
+  unique_ptr<Runfiles> r(Runfiles::Create(argv0, /*runfiles_manifest_file=*/"",
+                                          /*runfiles_dir=*/"", &error));
+  ASSERT_TRUE(r != nullptr);
+  EXPECT_TRUE(error.empty());
+
+  EXPECT_EQ(r->Rlocation("a/b"), argv0 + ".runfiles/a/b");
+  // We know it's directory-based because it returns some result for unknown
+  // paths.
+  EXPECT_EQ(r->Rlocation("unknown"), argv0 + ".runfiles/unknown");
+  AssertEnvvars(*r, "", argv0 + ".runfiles");
+}
+
+TEST_F(RunfilesTest, CreatesDirectoryBasedRunfilesFromEnvvar) {
+  // We create a directory as a side-effect of creating a mock file.
+  unique_ptr<MockFile> mf(
+      MockFile::Create(string("foo" LINE_AS_STRING() ".runfiles/dummy")));
+  string dir = mf->DirName();
+
+  string error;
+  unique_ptr<Runfiles> r(Runfiles::Create("ignore-argv0", "", dir, &error));
+  ASSERT_TRUE(r != nullptr);
+  EXPECT_TRUE(error.empty());
+
+  EXPECT_EQ(r->Rlocation("a/b"), dir + "/a/b");
+  EXPECT_EQ(r->Rlocation("foo"), dir + "/foo");
+  EXPECT_EQ(r->Rlocation("/Foo"), "/Foo");
+  EXPECT_EQ(r->Rlocation("c:/Foo"), "c:/Foo");
+  EXPECT_EQ(r->Rlocation("c:\\Foo"), "c:\\Foo");
+  AssertEnvvars(*r, "", dir);
+}
+
+TEST_F(RunfilesTest, FailsToCreateAnyRunfilesBecauseEnvvarsAreNotDefined) {
+  unique_ptr<MockFile> mf(
+      MockFile::Create(string("foo" LINE_AS_STRING() ".runfiles/MANIFEST")));
+  ASSERT_TRUE(mf != nullptr);
+
+  string error;
+  unique_ptr<Runfiles> r(
+      Runfiles::Create("ignore-argv0", mf->Path(), "whatever", &error));
+  ASSERT_TRUE(r != nullptr);
+  EXPECT_TRUE(error.empty());
+
+  // We create a directory as a side-effect of creating a mock file.
+  mf.reset(MockFile::Create(string("foo" LINE_AS_STRING() ".runfiles/dummy")));
+  r.reset(Runfiles::Create("ignore-argv0", "", mf->DirName(), &error));
+  ASSERT_TRUE(r != nullptr);
+  EXPECT_TRUE(error.empty());
+
+  r.reset(Runfiles::Create("ignore-argv0", /*runfiles_manifest_file=*/"",
+                           /*runfiles_dir=*/"", &error));
+  EXPECT_EQ(r, nullptr);
+  EXPECT_NE(error.find("cannot find runfiles"), string::npos);
+}
+
+TEST_F(RunfilesTest, MockFileTest) {
+  {
+    unique_ptr<MockFile> mf(
+        MockFile::Create(string("foo" LINE_AS_STRING() "/..")));
+    EXPECT_TRUE(mf == nullptr);
+  }
+
+  {
+    unique_ptr<MockFile> mf(MockFile::Create(string("/Foo" LINE_AS_STRING())));
+    EXPECT_TRUE(mf == nullptr);
+  }
+
+  {
+    unique_ptr<MockFile> mf(
+        MockFile::Create(string("C:/Foo" LINE_AS_STRING())));
+    EXPECT_TRUE(mf == nullptr);
+  }
+
+  string path;
+  {
+    unique_ptr<MockFile> mf(
+        MockFile::Create(string("foo" LINE_AS_STRING() "/bar1/qux")));
+    ASSERT_TRUE(mf != nullptr);
+    path = mf->Path();
+
+    std::ifstream stm(path);
+    EXPECT_TRUE(stm.good());
+    string actual;
+    stm >> actual;
+    EXPECT_TRUE(actual.empty());
+  }
+  {
+    std::ifstream stm(path);
+    EXPECT_FALSE(stm.good());
+  }
+
+  {
+    unique_ptr<MockFile> mf(MockFile::Create(
+        string("foo" LINE_AS_STRING() "/bar2/qux"), vector<string>()));
+    ASSERT_TRUE(mf != nullptr);
+    path = mf->Path();
+
+    std::ifstream stm(path);
+    EXPECT_TRUE(stm.good());
+    string actual;
+    stm >> actual;
+    EXPECT_TRUE(actual.empty());
+  }
+  {
+    std::ifstream stm(path);
+    EXPECT_FALSE(stm.good());
+  }
+
+  {
+    unique_ptr<MockFile> mf(
+        MockFile::Create(string("foo" LINE_AS_STRING() "/bar3/qux"),
+                         {"hello world", "you are beautiful"}));
+    ASSERT_TRUE(mf != nullptr);
+    path = mf->Path();
+
+    std::ifstream stm(path);
+    EXPECT_TRUE(stm.good());
+    string actual;
+    std::getline(stm, actual);
+    EXPECT_EQ("hello world", actual);
+    std::getline(stm, actual);
+    EXPECT_EQ("you are beautiful", actual);
+    std::getline(stm, actual);
+    EXPECT_EQ("", actual);
+  }
+  {
+    std::ifstream stm(path);
+    EXPECT_FALSE(stm.good());
+  }
+}
+
+TEST_F(RunfilesTest, IsAbsolute) {
+  EXPECT_FALSE(TestOnly_IsAbsolute("foo"));
+  EXPECT_FALSE(TestOnly_IsAbsolute("foo/bar"));
+  EXPECT_FALSE(TestOnly_IsAbsolute("\\foo"));
+  EXPECT_TRUE(TestOnly_IsAbsolute("c:\\foo"));
+  EXPECT_TRUE(TestOnly_IsAbsolute("c:/foo"));
+  EXPECT_TRUE(TestOnly_IsAbsolute("/foo"));
+  EXPECT_TRUE(TestOnly_IsAbsolute("x:\\foo"));
+  EXPECT_FALSE(TestOnly_IsAbsolute("::\\foo"));
+  EXPECT_FALSE(TestOnly_IsAbsolute("x\\foo"));
+  EXPECT_FALSE(TestOnly_IsAbsolute("x:"));
+  EXPECT_TRUE(TestOnly_IsAbsolute("x:\\"));
+}
+
+TEST_F(RunfilesTest, PathsFromEnvVars) {
+  string mf, dir, rm;
+
+  // Both envvars have a valid value.
+  EXPECT_TRUE(TestOnly_PathsFrom(
+      "argv0", "mock1.runfiles/MANIFEST", "mock2.runfiles",
+      [](const string& path) { return path == "mock1.runfiles/MANIFEST"; },
+      [](const string& path) { return path == "mock2.runfiles"; }, &mf, &dir));
+  EXPECT_EQ(mf, "mock1.runfiles/MANIFEST");
+  EXPECT_EQ(dir, "mock2.runfiles");
+
+  // RUNFILES_MANIFEST_FILE is invalid but RUNFILES_DIR is good and there's a
+  // runfiles manifest in the runfiles directory.
+  EXPECT_TRUE(TestOnly_PathsFrom(
+      "argv0", "mock1.runfiles/MANIFEST", "mock2.runfiles",
+      [](const string& path) { return path == "mock2.runfiles/MANIFEST"; },
+      [](const string& path) { return path == "mock2.runfiles"; }, &mf, &dir));
+  EXPECT_EQ(mf, "mock2.runfiles/MANIFEST");
+  EXPECT_EQ(dir, "mock2.runfiles");
+
+  // RUNFILES_MANIFEST_FILE is invalid but RUNFILES_DIR is good, but there's no
+  // runfiles manifest in the runfiles directory.
+  EXPECT_TRUE(TestOnly_PathsFrom(
+      "argv0", "mock1.runfiles/MANIFEST", "mock2.runfiles",
+      [](const string& path) { return false; },
+      [](const string& path) { return path == "mock2.runfiles"; }, &mf, &dir));
+  EXPECT_EQ(mf, "");
+  EXPECT_EQ(dir, "mock2.runfiles");
+
+  // RUNFILES_DIR is invalid but RUNFILES_MANIFEST_FILE is good, and it is in
+  // a valid-looking runfiles directory.
+  EXPECT_TRUE(TestOnly_PathsFrom(
+      "argv0", "mock1.runfiles/MANIFEST", "mock2",
+      [](const string& path) { return path == "mock1.runfiles/MANIFEST"; },
+      [](const string& path) { return path == "mock1.runfiles"; }, &mf, &dir));
+  EXPECT_EQ(mf, "mock1.runfiles/MANIFEST");
+  EXPECT_EQ(dir, "mock1.runfiles");
+
+  // RUNFILES_DIR is invalid but RUNFILES_MANIFEST_FILE is good, but it is not
+  // in any valid-looking runfiles directory.
+  EXPECT_TRUE(TestOnly_PathsFrom(
+      "argv0", "mock1/MANIFEST", "mock2",
+      [](const string& path) { return path == "mock1/MANIFEST"; },
+      [](const string& path) { return false; }, &mf, &dir));
+  EXPECT_EQ(mf, "mock1/MANIFEST");
+  EXPECT_EQ(dir, "");
+
+  // Both envvars are invalid, but there's a manifest in a runfiles directory
+  // next to argv0, however there's no other content in the runfiles directory.
+  EXPECT_TRUE(TestOnly_PathsFrom(
+      "argv0", "mock1/MANIFEST", "mock2",
+      [](const string& path) { return path == "argv0.runfiles/MANIFEST"; },
+      [](const string& path) { return false; }, &mf, &dir));
+  EXPECT_EQ(mf, "argv0.runfiles/MANIFEST");
+  EXPECT_EQ(dir, "");
+
+  // Both envvars are invalid, but there's a manifest next to argv0. There's
+  // no runfiles tree anywhere.
+  EXPECT_TRUE(TestOnly_PathsFrom(
+      "argv0", "mock1/MANIFEST", "mock2",
+      [](const string& path) { return path == "argv0.runfiles_manifest"; },
+      [](const string& path) { return false; }, &mf, &dir));
+  EXPECT_EQ(mf, "argv0.runfiles_manifest");
+  EXPECT_EQ(dir, "");
+
+  // Both envvars are invalid, but there's a valid manifest next to argv0, and a
+  // valid runfiles directory (without a manifest in it).
+  EXPECT_TRUE(TestOnly_PathsFrom(
+      "argv0", "mock1/MANIFEST", "mock2",
+      [](const string& path) { return path == "argv0.runfiles_manifest"; },
+      [](const string& path) { return path == "argv0.runfiles"; }, &mf, &dir));
+  EXPECT_EQ(mf, "argv0.runfiles_manifest");
+  EXPECT_EQ(dir, "argv0.runfiles");
+
+  // Both envvars are invalid, but there's a valid runfiles directory next to
+  // argv0, though no manifest in it.
+  EXPECT_TRUE(TestOnly_PathsFrom(
+      "argv0", "mock1/MANIFEST", "mock2",
+      [](const string& path) { return false; },
+      [](const string& path) { return path == "argv0.runfiles"; }, &mf, &dir));
+  EXPECT_EQ(mf, "");
+  EXPECT_EQ(dir, "argv0.runfiles");
+
+  // Both envvars are invalid, but there's a valid runfiles directory next to
+  // argv0 with a valid manifest in it.
+  EXPECT_TRUE(TestOnly_PathsFrom(
+      "argv0", "mock1/MANIFEST", "mock2",
+      [](const string& path) { return path == "argv0.runfiles/MANIFEST"; },
+      [](const string& path) { return path == "argv0.runfiles"; }, &mf, &dir));
+  EXPECT_EQ(mf, "argv0.runfiles/MANIFEST");
+  EXPECT_EQ(dir, "argv0.runfiles");
+}
+
+TEST_F(RunfilesTest, ManifestBasedRlocationWithRepoMapping_fromMain) {
+  string uid = LINE_AS_STRING();
+  unique_ptr<MockFile> rm(
+      MockFile::Create("foo" + uid + ".repo_mapping",
+                       {",config.json,config.json+1.2.3", ",my_module,_main",
+                        ",my_protobuf,protobuf+3.19.2", ",my_workspace,_main",
+                        "protobuf+3.19.2,config.json,config.json+1.2.3",
+                        "protobuf+3.19.2,protobuf,protobuf+3.19.2"}));
+  ASSERT_TRUE(rm != nullptr);
+  unique_ptr<MockFile> mf(MockFile::Create(
+      "foo" + uid + ".runfiles_manifest",
+      {"_repo_mapping " + rm->Path(), "config.json /etc/config.json",
+       "protobuf+3.19.2/foo/runfile C:/Actual Path\\protobuf\\runfile",
+       "_main/bar/runfile /the/path/./to/other//other runfile.txt",
+       "protobuf+3.19.2/bar/dir E:\\Actual Path\\Directory"}));
+  ASSERT_TRUE(mf != nullptr);
+  string argv0(mf->Path().substr(
+      0, mf->Path().size() - string(".runfiles_manifest").size()));
+
+  string error;
+  unique_ptr<Runfiles> r(Runfiles::Create(argv0, /*runfiles_manifest_file=*/"",
+                                          /*runfiles_dir=*/"",
+                                          /*source_repository=*/"", &error));
+  ASSERT_TRUE(r != nullptr);
+  EXPECT_TRUE(error.empty());
+
+  EXPECT_EQ(r->Rlocation("my_module/bar/runfile"),
+            "/the/path/./to/other//other runfile.txt");
+  EXPECT_EQ(r->Rlocation("my_workspace/bar/runfile"),
+            "/the/path/./to/other//other runfile.txt");
+  EXPECT_EQ(r->Rlocation("my_protobuf/foo/runfile"),
+            "C:/Actual Path\\protobuf\\runfile");
+  EXPECT_EQ(r->Rlocation("my_protobuf/bar/dir"), "E:\\Actual Path\\Directory");
+  EXPECT_EQ(r->Rlocation("my_protobuf/bar/dir/file"),
+            "E:\\Actual Path\\Directory/file");
+  EXPECT_EQ(r->Rlocation("my_protobuf/bar/dir/de eply/nes ted/fi+le"),
+            "E:\\Actual Path\\Directory/de eply/nes ted/fi+le");
+
+  EXPECT_EQ(r->Rlocation("protobuf/foo/runfile"), "");
+  EXPECT_EQ(r->Rlocation("protobuf/bar/dir"), "");
+  EXPECT_EQ(r->Rlocation("protobuf/bar/dir/file"), "");
+  EXPECT_EQ(r->Rlocation("protobuf/bar/dir/dir/de eply/nes ted/fi+le"), "");
+
+  EXPECT_EQ(r->Rlocation("_main/bar/runfile"),
+            "/the/path/./to/other//other runfile.txt");
+  EXPECT_EQ(r->Rlocation("protobuf+3.19.2/foo/runfile"),
+            "C:/Actual Path\\protobuf\\runfile");
+  EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir"),
+            "E:\\Actual Path\\Directory");
+  EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir/file"),
+            "E:\\Actual Path\\Directory/file");
+  EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir/de eply/nes  ted/fi+le"),
+            "E:\\Actual Path\\Directory/de eply/nes  ted/fi+le");
+
+  EXPECT_EQ(r->Rlocation("config.json"), "/etc/config.json");
+  EXPECT_EQ(r->Rlocation("_main"), "");
+  EXPECT_EQ(r->Rlocation("my_module"), "");
+  EXPECT_EQ(r->Rlocation("protobuf"), "");
+}
+
+TEST_F(RunfilesTest, ManifestBasedRlocationWithRepoMapping_fromOtherRepo) {
+  string uid = LINE_AS_STRING();
+  unique_ptr<MockFile> rm(
+      MockFile::Create("foo" + uid + ".repo_mapping",
+                       {",config.json,config.json+1.2.3", ",my_module,_main",
+                        ",my_protobuf,protobuf+3.19.2", ",my_workspace,_main",
+                        "protobuf+3.19.2,config.json,config.json+1.2.3",
+                        "protobuf+3.19.2,protobuf,protobuf+3.19.2"}));
+  ASSERT_TRUE(rm != nullptr);
+  unique_ptr<MockFile> mf(MockFile::Create(
+      "foo" + uid + ".runfiles_manifest",
+      {"_repo_mapping " + rm->Path(), "config.json /etc/config.json",
+       "protobuf+3.19.2/foo/runfile C:/Actual Path\\protobuf\\runfile",
+       "_main/bar/runfile /the/path/./to/other//other runfile.txt",
+       "protobuf+3.19.2/bar/dir E:\\Actual Path\\Directory"}));
+  ASSERT_TRUE(mf != nullptr);
+  string argv0(mf->Path().substr(
+      0, mf->Path().size() - string(".runfiles_manifest").size()));
+
+  string error;
+  unique_ptr<Runfiles> r(Runfiles::Create(argv0, /*runfiles_manifest_file=*/"",
+                                          /*runfiles_dir=*/"",
+                                          "protobuf+3.19.2", &error));
+  ASSERT_TRUE(r != nullptr);
+  EXPECT_TRUE(error.empty());
+
+  EXPECT_EQ(r->Rlocation("protobuf/foo/runfile"),
+            "C:/Actual Path\\protobuf\\runfile");
+  EXPECT_EQ(r->Rlocation("protobuf/bar/dir"), "E:\\Actual Path\\Directory");
+  EXPECT_EQ(r->Rlocation("protobuf/bar/dir/file"),
+            "E:\\Actual Path\\Directory/file");
+  EXPECT_EQ(r->Rlocation("protobuf/bar/dir/de eply/nes  ted/fi+le"),
+            "E:\\Actual Path\\Directory/de eply/nes  ted/fi+le");
+
+  EXPECT_EQ(r->Rlocation("my_module/bar/runfile"), "");
+  EXPECT_EQ(r->Rlocation("my_protobuf/foo/runfile"), "");
+  EXPECT_EQ(r->Rlocation("my_protobuf/bar/dir"), "");
+  EXPECT_EQ(r->Rlocation("my_protobuf/bar/dir/file"), "");
+  EXPECT_EQ(r->Rlocation("my_protobuf/bar/dir/de eply/nes  ted/fi+le"), "");
+
+  EXPECT_EQ(r->Rlocation("_main/bar/runfile"),
+            "/the/path/./to/other//other runfile.txt");
+  EXPECT_EQ(r->Rlocation("protobuf+3.19.2/foo/runfile"),
+            "C:/Actual Path\\protobuf\\runfile");
+  EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir"),
+            "E:\\Actual Path\\Directory");
+  EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir/file"),
+            "E:\\Actual Path\\Directory/file");
+  EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir/de eply/nes  ted/fi+le"),
+            "E:\\Actual Path\\Directory/de eply/nes  ted/fi+le");
+
+  EXPECT_EQ(r->Rlocation("config.json"), "/etc/config.json");
+  EXPECT_EQ(r->Rlocation("_main"), "");
+  EXPECT_EQ(r->Rlocation("my_module"), "");
+  EXPECT_EQ(r->Rlocation("protobuf"), "");
+}
+
+TEST_F(RunfilesTest, DirectoryBasedRlocationWithRepoMapping_fromMain) {
+  string uid = LINE_AS_STRING();
+  unique_ptr<MockFile> rm(
+      MockFile::Create("foo" + uid + ".runfiles/_repo_mapping",
+                       {",config.json,config.json+1.2.3", ",my_module,_main",
+                        ",my_protobuf,protobuf+3.19.2", ",my_workspace,_main",
+                        "protobuf+3.19.2,config.json,config.json+1.2.3",
+                        "protobuf+3.19.2,protobuf,protobuf+3.19.2"}));
+  ASSERT_TRUE(rm != nullptr);
+  string dir = rm->DirName();
+  string argv0(dir.substr(0, dir.size() - string(".runfiles").size()));
+
+  string error;
+  unique_ptr<Runfiles> r(Runfiles::Create(argv0, /*runfiles_manifest_file=*/"",
+                                          /*runfiles_dir=*/"",
+                                          /*source_repository=*/"", &error));
+  ASSERT_TRUE(r != nullptr);
+  EXPECT_TRUE(error.empty());
+
+  EXPECT_EQ(r->Rlocation("my_module/bar/runfile"), dir + "/_main/bar/runfile");
+  EXPECT_EQ(r->Rlocation("my_workspace/bar/runfile"),
+            dir + "/_main/bar/runfile");
+  EXPECT_EQ(r->Rlocation("my_protobuf/foo/runfile"),
+            dir + "/protobuf+3.19.2/foo/runfile");
+  EXPECT_EQ(r->Rlocation("my_protobuf/bar/dir"),
+            dir + "/protobuf+3.19.2/bar/dir");
+  EXPECT_EQ(r->Rlocation("my_protobuf/bar/dir/file"),
+            dir + "/protobuf+3.19.2/bar/dir/file");
+  EXPECT_EQ(r->Rlocation("my_protobuf/bar/dir/de eply/nes ted/fi+le"),
+            dir + "/protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le");
+
+  EXPECT_EQ(r->Rlocation("protobuf/foo/runfile"),
+            dir + "/protobuf/foo/runfile");
+  EXPECT_EQ(r->Rlocation("protobuf/bar/dir/dir/de eply/nes ted/fi+le"),
+            dir + "/protobuf/bar/dir/dir/de eply/nes ted/fi+le");
+
+  EXPECT_EQ(r->Rlocation("_main/bar/runfile"), dir + "/_main/bar/runfile");
+  EXPECT_EQ(r->Rlocation("protobuf+3.19.2/foo/runfile"),
+            dir + "/protobuf+3.19.2/foo/runfile");
+  EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir"),
+            dir + "/protobuf+3.19.2/bar/dir");
+  EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir/file"),
+            dir + "/protobuf+3.19.2/bar/dir/file");
+  EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir/de eply/nes  ted/fi+le"),
+            dir + "/protobuf+3.19.2/bar/dir/de eply/nes  ted/fi+le");
+
+  EXPECT_EQ(r->Rlocation("config.json"), dir + "/config.json");
+}
+
+TEST_F(RunfilesTest, DirectoryBasedRlocationWithRepoMapping_fromOtherRepo) {
+  string uid = LINE_AS_STRING();
+  unique_ptr<MockFile> rm(
+      MockFile::Create("foo" + uid + ".runfiles/_repo_mapping",
+                       {",config.json,config.json+1.2.3", ",my_module,_main",
+                        ",my_protobuf,protobuf+3.19.2", ",my_workspace,_main",
+                        "protobuf+3.19.2,config.json,config.json+1.2.3",
+                        "protobuf+3.19.2,protobuf,protobuf+3.19.2"}));
+  ASSERT_TRUE(rm != nullptr);
+  string dir = rm->DirName();
+  string argv0(dir.substr(0, dir.size() - string(".runfiles").size()));
+
+  string error;
+  unique_ptr<Runfiles> r(Runfiles::Create(argv0, /*runfiles_manifest_file=*/"",
+                                          /*runfiles_dir=*/"",
+                                          "protobuf+3.19.2", &error));
+  ASSERT_TRUE(r != nullptr);
+  EXPECT_TRUE(error.empty());
+
+  EXPECT_EQ(r->Rlocation("protobuf/foo/runfile"),
+            dir + "/protobuf+3.19.2/foo/runfile");
+  EXPECT_EQ(r->Rlocation("protobuf/bar/dir"), dir + "/protobuf+3.19.2/bar/dir");
+  EXPECT_EQ(r->Rlocation("protobuf/bar/dir/file"),
+            dir + "/protobuf+3.19.2/bar/dir/file");
+  EXPECT_EQ(r->Rlocation("protobuf/bar/dir/de eply/nes  ted/fi+le"),
+            dir + "/protobuf+3.19.2/bar/dir/de eply/nes  ted/fi+le");
+
+  EXPECT_EQ(r->Rlocation("my_module/bar/runfile"),
+            dir + "/my_module/bar/runfile");
+  EXPECT_EQ(r->Rlocation("my_protobuf/bar/dir/de eply/nes  ted/fi+le"),
+            dir + "/my_protobuf/bar/dir/de eply/nes  ted/fi+le");
+
+  EXPECT_EQ(r->Rlocation("_main/bar/runfile"), dir + "/_main/bar/runfile");
+  EXPECT_EQ(r->Rlocation("protobuf+3.19.2/foo/runfile"),
+            dir + "/protobuf+3.19.2/foo/runfile");
+  EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir"),
+            dir + "/protobuf+3.19.2/bar/dir");
+  EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir/file"),
+            dir + "/protobuf+3.19.2/bar/dir/file");
+  EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir/de eply/nes  ted/fi+le"),
+            dir + "/protobuf+3.19.2/bar/dir/de eply/nes  ted/fi+le");
+
+  EXPECT_EQ(r->Rlocation("config.json"), dir + "/config.json");
+}
+
+TEST_F(RunfilesTest,
+       DirectoryBasedRlocationWithRepoMapping_fromOtherRepo_withSourceRepo) {
+  string uid = LINE_AS_STRING();
+  unique_ptr<MockFile> rm(
+      MockFile::Create("foo" + uid + ".runfiles/_repo_mapping",
+                       {",config.json,config.json+1.2.3", ",my_module,_main",
+                        ",my_protobuf,protobuf+3.19.2", ",my_workspace,_main",
+                        "protobuf+3.19.2,config.json,config.json+1.2.3",
+                        "protobuf+3.19.2,protobuf,protobuf+3.19.2"}));
+  ASSERT_TRUE(rm != nullptr);
+  string dir = rm->DirName();
+  string argv0(dir.substr(0, dir.size() - string(".runfiles").size()));
+
+  string error;
+  unique_ptr<Runfiles> r(Runfiles::Create(argv0, /*runfiles_manifest_file=*/"",
+                                          /*runfiles_dir=*/"",
+                                          /*source_repository=*/"", &error));
+  r = r->WithSourceRepository("protobuf+3.19.2");
+  ASSERT_TRUE(r != nullptr);
+  EXPECT_TRUE(error.empty());
+
+  EXPECT_EQ(r->Rlocation("protobuf/foo/runfile"),
+            dir + "/protobuf+3.19.2/foo/runfile");
+  EXPECT_EQ(r->Rlocation("protobuf/bar/dir"), dir + "/protobuf+3.19.2/bar/dir");
+  EXPECT_EQ(r->Rlocation("protobuf/bar/dir/file"),
+            dir + "/protobuf+3.19.2/bar/dir/file");
+  EXPECT_EQ(r->Rlocation("protobuf/bar/dir/de eply/nes  ted/fi+le"),
+            dir + "/protobuf+3.19.2/bar/dir/de eply/nes  ted/fi+le");
+
+  EXPECT_EQ(r->Rlocation("my_module/bar/runfile"),
+            dir + "/my_module/bar/runfile");
+  EXPECT_EQ(r->Rlocation("my_protobuf/bar/dir/de eply/nes  ted/fi+le"),
+            dir + "/my_protobuf/bar/dir/de eply/nes  ted/fi+le");
+
+  EXPECT_EQ(r->Rlocation("_main/bar/runfile"), dir + "/_main/bar/runfile");
+  EXPECT_EQ(r->Rlocation("protobuf+3.19.2/foo/runfile"),
+            dir + "/protobuf+3.19.2/foo/runfile");
+  EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir"),
+            dir + "/protobuf+3.19.2/bar/dir");
+  EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir/file"),
+            dir + "/protobuf+3.19.2/bar/dir/file");
+  EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir/de eply/nes  ted/fi+le"),
+            dir + "/protobuf+3.19.2/bar/dir/de eply/nes  ted/fi+le");
+
+  EXPECT_EQ(r->Rlocation("config.json"), dir + "/config.json");
+}
+
+TEST_F(RunfilesTest, InvalidRepoMapping) {
+  string uid = LINE_AS_STRING();
+  unique_ptr<MockFile> rm(
+      MockFile::Create("foo" + uid + ".runfiles/_repo_mapping", {"a,b"}));
+  ASSERT_TRUE(rm != nullptr);
+  string dir = rm->DirName();
+  string argv0(dir.substr(0, dir.size() - string(".runfiles").size()));
+
+  string error;
+  unique_ptr<Runfiles> r(Runfiles::Create(argv0, /*runfiles_manifest_file=*/"",
+                                          /*runfiles_dir=*/"",
+                                          /*source_repository=*/"", &error));
+  EXPECT_EQ(r, nullptr);
+  EXPECT_TRUE(error.find("bad repository mapping") != string::npos);
+}
+
+}  // namespace
+}  // namespace runfiles
+}  // namespace cc
+}  // namespace rules_cc