Create NsswitchReader

Reads /etc/nsswitch.conf files and parses tokens. Designed to be fairly
lenient and attempts to turn most unrecognized/unparsable input into
"unknown" output tokens.

Bug: 117655
Change-Id: Icab4e2aef501ab1e7d195747cc8c4d4d7a56a7b1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2803074
Reviewed-by: Dan McArdle <[email protected]>
Commit-Queue: Eric Orth <[email protected]>
Cr-Commit-Position: refs/heads/master@{#869626}
diff --git a/net/dns/BUILD.gn b/net/dns/BUILD.gn
index c872eba..a5a19491 100644
--- a/net/dns/BUILD.gn
+++ b/net/dns/BUILD.gn
@@ -73,6 +73,8 @@
       "httpssvc_metrics.cc",
       "httpssvc_metrics.h",
       "mapped_host_resolver.cc",
+      "nsswitch_reader.cc",
+      "nsswitch_reader.h",
       "record_parsed.cc",
       "record_rdata.cc",
       "resolve_context.cc",
@@ -417,6 +419,7 @@
     "https_record_rdata_unittest.cc",
     "httpssvc_metrics_unittest.cc",
     "mapped_host_resolver_unittest.cc",
+    "nsswitch_reader_unittest.cc",
     "record_parsed_unittest.cc",
     "record_rdata_unittest.cc",
     "resolve_context_unittest.cc",
diff --git a/net/dns/nsswitch_reader.cc b/net/dns/nsswitch_reader.cc
new file mode 100644
index 0000000..a02ea60
--- /dev/null
+++ b/net/dns/nsswitch_reader.cc
@@ -0,0 +1,225 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/dns/nsswitch_reader.h"
+
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/strings/string_piece.h"
+#include "base/strings/string_split.h"
+#include "base/strings/string_util.h"
+#include "build/build_config.h"
+
+#if defined(OS_POSIX)
+#include <netdb.h>
+#endif  // defined (OS_POSIX)
+
+namespace net {
+
+namespace {
+
+#ifdef _PATH_NSSWITCH_CONF
+constexpr base::FilePath::CharType kNsswitchPath[] =
+    FILE_PATH_LITERAL(_PATH_NSSWITCH_CONF);
+#else
+constexpr base::FilePath::CharType kNsswitchPath[] =
+    FILE_PATH_LITERAL("/etc/nsswitch.conf");
+#endif
+
+// Choose 1 MiB as the largest handled filesize. Arbitrarily chosen as seeming
+// large enough to handle any reasonable file contents and similar to the size
+// limit for HOSTS files (32 MiB).
+constexpr size_t kMaxFileSize = 1024 * 1024;
+
+std::string ReadNsswitch() {
+  std::string file;
+  if (!base::ReadFileToStringWithMaxSize(base::FilePath(kNsswitchPath), &file,
+                                         kMaxFileSize))
+    return "";
+
+  return file;
+}
+
+base::StringPiece SkipRestOfLine(base::StringPiece text) {
+  base::StringPiece::size_type line_end = text.find('\n');
+  if (line_end == base::StringPiece::npos)
+    return "";
+  return text.substr(line_end);
+}
+
+// In case of multiple entries for `database_name`, finds only the first.
+base::StringPiece FindDatabase(base::StringPiece text,
+                               base::StringPiece database_name) {
+  DCHECK(!text.empty());
+  DCHECK(!database_name.empty());
+  DCHECK(!base::StartsWith(database_name, "#"));
+  DCHECK(!base::IsAsciiWhitespace(database_name.front()));
+  DCHECK(base::EndsWith(database_name, ":"));
+
+  while (!text.empty()) {
+    text = base::TrimWhitespaceASCII(text, base::TrimPositions::TRIM_LEADING);
+
+    if (base::StartsWith(text, database_name,
+                         base::CompareCase::INSENSITIVE_ASCII)) {
+      DCHECK(!base::StartsWith(text, "#"));
+
+      text = text.substr(database_name.size());
+      base::StringPiece::size_type line_end = text.find('\n');
+      if (line_end != base::StringPiece::npos)
+        text = text.substr(0, line_end);
+
+      return base::TrimWhitespaceASCII(text, base::TrimPositions::TRIM_ALL);
+    }
+
+    text = SkipRestOfLine(text);
+  }
+
+  return "";
+}
+
+NsswitchReader::ServiceAction TokenizeAction(base::StringPiece column) {
+  NsswitchReader::ServiceAction result = {/*negated=*/false,
+                                          NsswitchReader::Status::kUnknown,
+                                          NsswitchReader::Action::kUnknown};
+
+  if (column.front() != '[' || column.back() != ']')
+    return result;
+  column = column.substr(1, column.size() - 2);
+
+  std::vector<base::StringPiece> split = base::SplitStringPiece(
+      column, "=", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+  if (split.size() != 2)
+    return result;
+
+  if (split[0].size() >= 2 && split[0].front() == '!') {
+    result.negated = true;
+    split[0] = split[0].substr(1);
+  }
+
+  if (base::EqualsCaseInsensitiveASCII(split[0], "SUCCESS")) {
+    result.status = NsswitchReader::Status::kSuccess;
+  } else if (base::EqualsCaseInsensitiveASCII(split[0], "NOTFOUND")) {
+    result.status = NsswitchReader::Status::kNotFound;
+  } else if (base::EqualsCaseInsensitiveASCII(split[0], "UNAVAIL")) {
+    result.status = NsswitchReader::Status::kUnavailable;
+  } else if (base::EqualsCaseInsensitiveASCII(split[0], "TRYAGAIN")) {
+    result.status = NsswitchReader::Status::kTryAgain;
+  }
+
+  if (base::EqualsCaseInsensitiveASCII(split[1], "RETURN")) {
+    result.action = NsswitchReader::Action::kReturn;
+  } else if (base::EqualsCaseInsensitiveASCII(split[1], "CONTINUE")) {
+    result.action = NsswitchReader::Action::kContinue;
+  } else if (base::EqualsCaseInsensitiveASCII(split[1], "MERGE")) {
+    result.action = NsswitchReader::Action::kMerge;
+  }
+
+  return result;
+}
+
+std::vector<NsswitchReader::ServiceSpecification> TokenizeDatabase(
+    base::StringPiece database) {
+  std::vector<NsswitchReader::ServiceSpecification> tokenized;
+
+  for (const auto& column : base::SplitStringPiece(
+           database, base::kWhitespaceASCII, base::KEEP_WHITESPACE,
+           base::SPLIT_WANT_NONEMPTY)) {
+    DCHECK(!column.empty());
+
+    // Note: Assuming comments can only be started at the start of a column.
+    if (base::StartsWith(column, "#")) {
+      // Once a comment is hit, the rest of the database is comment.
+      return tokenized;
+    }
+
+    if (column.front() == '[') {
+      // Actions are expected to come after a service.
+      if (tokenized.empty()) {
+        tokenized.emplace_back(NsswitchReader::Service::kUnknown);
+      }
+
+      tokenized.back().actions.push_back(TokenizeAction(column));
+    } else if (base::EqualsCaseInsensitiveASCII(column, "files")) {
+      tokenized.emplace_back(NsswitchReader::Service::kFiles);
+    } else if (base::EqualsCaseInsensitiveASCII(column, "dns")) {
+      tokenized.emplace_back(NsswitchReader::Service::kDns);
+    } else if (base::EqualsCaseInsensitiveASCII(column, "mdns")) {
+      tokenized.emplace_back(NsswitchReader::Service::kMdns);
+    } else if (base::EqualsCaseInsensitiveASCII(column, "mdns4")) {
+      tokenized.emplace_back(NsswitchReader::Service::kMdns4);
+    } else if (base::EqualsCaseInsensitiveASCII(column, "mdns6")) {
+      tokenized.emplace_back(NsswitchReader::Service::kMdns6);
+    } else if (base::EqualsCaseInsensitiveASCII(column, "mdns_minimal")) {
+      tokenized.emplace_back(NsswitchReader::Service::kMdnsMinimal);
+    } else if (base::EqualsCaseInsensitiveASCII(column, "mdns4_minimal")) {
+      tokenized.emplace_back(NsswitchReader::Service::kMdns4Minimal);
+    } else if (base::EqualsCaseInsensitiveASCII(column, "mdns6_minimal")) {
+      tokenized.emplace_back(NsswitchReader::Service::kMdns6Minimal);
+    } else if (base::EqualsCaseInsensitiveASCII(column, "myhostname")) {
+      tokenized.emplace_back(NsswitchReader::Service::kMyHostname);
+    } else if (base::EqualsCaseInsensitiveASCII(column, "resolve")) {
+      tokenized.emplace_back(NsswitchReader::Service::kResolve);
+    } else if (base::EqualsCaseInsensitiveASCII(column, "nis")) {
+      tokenized.emplace_back(NsswitchReader::Service::kNis);
+    } else {
+      tokenized.emplace_back(NsswitchReader::Service::kUnknown);
+    }
+  }
+
+  return tokenized;
+}
+
+std::vector<NsswitchReader::ServiceSpecification> GetDefaultHosts() {
+  return {NsswitchReader::ServiceSpecification(NsswitchReader::Service::kFiles),
+          NsswitchReader::ServiceSpecification(NsswitchReader::Service::kDns)};
+}
+
+}  // namespace
+
+NsswitchReader::ServiceSpecification::ServiceSpecification(
+    Service service,
+    std::vector<ServiceAction> actions)
+    : service(service), actions(std::move(actions)) {}
+
+NsswitchReader::ServiceSpecification::~ServiceSpecification() = default;
+
+NsswitchReader::ServiceSpecification::ServiceSpecification(
+    const ServiceSpecification&) = default;
+
+NsswitchReader::ServiceSpecification&
+NsswitchReader::ServiceSpecification::operator=(const ServiceSpecification&) =
+    default;
+
+NsswitchReader::ServiceSpecification::ServiceSpecification(
+    ServiceSpecification&&) = default;
+
+NsswitchReader::ServiceSpecification&
+NsswitchReader::ServiceSpecification::operator=(ServiceSpecification&&) =
+    default;
+
+NsswitchReader::NsswitchReader()
+    : file_read_call_(base::BindRepeating(&ReadNsswitch)) {}
+
+NsswitchReader::~NsswitchReader() = default;
+
+std::vector<NsswitchReader::ServiceSpecification>
+NsswitchReader::ReadAndParseHosts() {
+  std::string file = file_read_call_.Run();
+  if (file.empty())
+    return GetDefaultHosts();
+
+  base::StringPiece hosts = FindDatabase(file, "hosts:");
+  if (hosts.empty())
+    return GetDefaultHosts();
+
+  return TokenizeDatabase(hosts);
+}
+
+}  // namespace net
diff --git a/net/dns/nsswitch_reader.h b/net/dns/nsswitch_reader.h
new file mode 100644
index 0000000..187d9c3c
--- /dev/null
+++ b/net/dns/nsswitch_reader.h
@@ -0,0 +1,107 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_DNS_NSSWITCH_READER_H_
+#define NET_DNS_NSSWITCH_READER_H_
+
+#include <string>
+#include <tuple>
+#include <vector>
+
+#include "base/callback.h"
+#include "net/base/net_export.h"
+
+namespace net {
+
+// Reader to read and parse Posix nsswitch.conf files, particularly the "hosts:"
+// database entry.
+class NET_EXPORT_PRIVATE NsswitchReader {
+ public:
+  enum class Service {
+    kUnknown,
+    kFiles,
+    kDns,
+    kMdns,
+    kMdns4,
+    kMdns6,
+    kMdnsMinimal,
+    kMdns4Minimal,
+    kMdns6Minimal,
+    kMyHostname,
+    kResolve,
+    kNis,
+  };
+
+  enum class Status {
+    kUnknown,
+    kSuccess,
+    kNotFound,
+    kUnavailable,
+    kTryAgain,
+  };
+
+  enum class Action {
+    kUnknown,
+    kReturn,
+    kContinue,
+    kMerge,
+  };
+
+  struct ServiceAction {
+    bool operator==(const ServiceAction& other) const {
+      return std::tie(negated, status, action) ==
+             std::tie(other.negated, other.status, other.action);
+    }
+
+    bool negated;
+    Status status;
+    Action action;
+  };
+
+  struct NET_EXPORT_PRIVATE ServiceSpecification {
+    explicit ServiceSpecification(Service service,
+                                  std::vector<ServiceAction> actions = {});
+    ~ServiceSpecification();
+    ServiceSpecification(const ServiceSpecification&);
+    ServiceSpecification& operator=(const ServiceSpecification&);
+    ServiceSpecification(ServiceSpecification&&);
+    ServiceSpecification& operator=(ServiceSpecification&&);
+
+    bool operator==(const ServiceSpecification& other) const {
+      return std::tie(service, actions) ==
+             std::tie(other.service, other.actions);
+    }
+
+    Service service;
+    std::vector<ServiceAction> actions;
+  };
+
+  // Test-replacable call for the actual file read. Default implementation does
+  // a fresh read of the nsswitch.conf file every time it is called. Returns
+  // empty string on error reading the file.
+  using FileReadCall = base::RepeatingCallback<std::string()>;
+
+  NsswitchReader();
+  ~NsswitchReader();
+
+  NsswitchReader(const NsswitchReader&) = delete;
+  NsswitchReader& operator=(const NsswitchReader&) = delete;
+
+  // Reads nsswitch.conf and parses the "hosts:" database. In case of multiple
+  // matching databases, only parses the first. Assumes a basic default
+  // configuration if the file cannot be read or a "hosts:" database cannot be
+  // found.
+  std::vector<ServiceSpecification> ReadAndParseHosts();
+
+  void set_file_read_call_for_testing(FileReadCall file_read_call) {
+    file_read_call_ = std::move(file_read_call);
+  }
+
+ private:
+  FileReadCall file_read_call_;
+};
+
+}  // namespace net
+
+#endif  // NET_DNS_NSSWITCH_READER_H_
diff --git a/net/dns/nsswitch_reader_unittest.cc b/net/dns/nsswitch_reader_unittest.cc
new file mode 100644
index 0000000..2077132
--- /dev/null
+++ b/net/dns/nsswitch_reader_unittest.cc
@@ -0,0 +1,409 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/dns/nsswitch_reader.h"
+
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "base/bind.h"
+#include "base/check.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace net {
+namespace {
+
+class TestFileReader {
+ public:
+  explicit TestFileReader(std::string text) : text_(std::move(text)) {}
+  TestFileReader(const TestFileReader&) = delete;
+  TestFileReader& operator=(const TestFileReader&) = delete;
+
+  NsswitchReader::FileReadCall GetFileReadCall() {
+    return base::BindRepeating(&TestFileReader::ReadFile,
+                               base::Unretained(this));
+  }
+
+  std::string ReadFile() {
+    CHECK(!already_read_);
+
+    already_read_ = true;
+    return text_;
+  }
+
+ private:
+  std::string text_;
+  bool already_read_ = false;
+};
+
+class NsswitchReaderTest : public testing::Test {
+ public:
+  NsswitchReaderTest() = default;
+  NsswitchReaderTest(const NsswitchReaderTest&) = delete;
+  NsswitchReaderTest& operator=(const NsswitchReaderTest&) = delete;
+
+ protected:
+  NsswitchReader reader_;
+};
+
+// Attempt to load the actual nsswitch.conf for the test machine and run
+// rationality checks for the result.
+TEST_F(NsswitchReaderTest, ActualReadAndParseHosts) {
+  std::vector<NsswitchReader::ServiceSpecification> services =
+      reader_.ReadAndParseHosts();
+
+  // Assume nobody will ever run this on a machine with more than 1000
+  // configured services.
+  EXPECT_THAT(services, testing::SizeIs(testing::Le(1000u)));
+
+  // Assume no service will ever have more than 10 configured actions per
+  // service.
+  for (const NsswitchReader::ServiceSpecification& service : services) {
+    EXPECT_THAT(service.actions, testing::SizeIs(testing::Le(10u)));
+  }
+}
+
+TEST_F(NsswitchReaderTest, FileReadErrorResultsInDefault) {
+  TestFileReader file_reader("");
+  reader_.set_file_read_call_for_testing(file_reader.GetFileReadCall());
+
+  std::vector<NsswitchReader::ServiceSpecification> services =
+      reader_.ReadAndParseHosts();
+
+  // Expect "files dns".
+  EXPECT_THAT(
+      services,
+      testing::ElementsAre(
+          NsswitchReader::ServiceSpecification(NsswitchReader::Service::kFiles),
+          NsswitchReader::ServiceSpecification(NsswitchReader::Service::kDns)));
+}
+
+TEST_F(NsswitchReaderTest, MissingHostsResultsInDefault) {
+  const std::string kFile =
+      "passwd: files ldap\nshadow: files\ngroup: files ldap\n";
+  TestFileReader file_reader(kFile);
+  reader_.set_file_read_call_for_testing(file_reader.GetFileReadCall());
+
+  std::vector<NsswitchReader::ServiceSpecification> services =
+      reader_.ReadAndParseHosts();
+
+  // Expect "files dns".
+  EXPECT_THAT(
+      services,
+      testing::ElementsAre(
+          NsswitchReader::ServiceSpecification(NsswitchReader::Service::kFiles),
+          NsswitchReader::ServiceSpecification(NsswitchReader::Service::kDns)));
+}
+
+TEST_F(NsswitchReaderTest, ParsesAllKnownServices) {
+  const std::string kFile =
+      "hosts: files dns mdns mdns4 mdns6 mdns_minimal mdns4_minimal "
+      "mdns6_minimal myhostname resolve nis";
+  TestFileReader file_reader(kFile);
+  reader_.set_file_read_call_for_testing(file_reader.GetFileReadCall());
+
+  std::vector<NsswitchReader::ServiceSpecification> services =
+      reader_.ReadAndParseHosts();
+
+  EXPECT_THAT(
+      services,
+      testing::ElementsAre(
+          NsswitchReader::ServiceSpecification(NsswitchReader::Service::kFiles),
+          NsswitchReader::ServiceSpecification(NsswitchReader::Service::kDns),
+          NsswitchReader::ServiceSpecification(NsswitchReader::Service::kMdns),
+          NsswitchReader::ServiceSpecification(NsswitchReader::Service::kMdns4),
+          NsswitchReader::ServiceSpecification(NsswitchReader::Service::kMdns6),
+          NsswitchReader::ServiceSpecification(
+              NsswitchReader::Service::kMdnsMinimal),
+          NsswitchReader::ServiceSpecification(
+              NsswitchReader::Service::kMdns4Minimal),
+          NsswitchReader::ServiceSpecification(
+              NsswitchReader::Service::kMdns6Minimal),
+          NsswitchReader::ServiceSpecification(
+              NsswitchReader::Service::kMyHostname),
+          NsswitchReader::ServiceSpecification(
+              NsswitchReader::Service::kResolve),
+          NsswitchReader::ServiceSpecification(NsswitchReader::Service::kNis)));
+}
+
+TEST_F(NsswitchReaderTest, ParsesRepeatedServices) {
+  const std::string kFile = "hosts: mdns4 mdns6 mdns6 myhostname";
+  TestFileReader file_reader(kFile);
+  reader_.set_file_read_call_for_testing(file_reader.GetFileReadCall());
+
+  std::vector<NsswitchReader::ServiceSpecification> services =
+      reader_.ReadAndParseHosts();
+
+  EXPECT_THAT(
+      services,
+      testing::ElementsAre(
+          NsswitchReader::ServiceSpecification(NsswitchReader::Service::kMdns4),
+          NsswitchReader::ServiceSpecification(NsswitchReader::Service::kMdns6),
+          NsswitchReader::ServiceSpecification(NsswitchReader::Service::kMdns6),
+          NsswitchReader::ServiceSpecification(
+              NsswitchReader::Service::kMyHostname)));
+}
+
+TEST_F(NsswitchReaderTest, ParsesAllKnownActions) {
+  const std::string kFile =
+      "hosts: files [UNAVAIL=RETURN] [UNAVAIL=CONTINUE] [UNAVAIL=MERGE]";
+  TestFileReader file_reader(kFile);
+  reader_.set_file_read_call_for_testing(file_reader.GetFileReadCall());
+
+  std::vector<NsswitchReader::ServiceSpecification> services =
+      reader_.ReadAndParseHosts();
+
+  EXPECT_THAT(services,
+              testing::ElementsAre(NsswitchReader::ServiceSpecification(
+                  NsswitchReader::Service::kFiles,
+                  {{/*negated=*/false, NsswitchReader::Status::kUnavailable,
+                    NsswitchReader::Action::kReturn},
+                   {/*negated=*/false, NsswitchReader::Status::kUnavailable,
+                    NsswitchReader::Action::kContinue},
+                   {/*negated=*/false, NsswitchReader::Status::kUnavailable,
+                    NsswitchReader::Action::kMerge}})));
+}
+
+TEST_F(NsswitchReaderTest, ParsesAllKnownStatuses) {
+  const std::string kFile =
+      "hosts: dns [SUCCESS=RETURN] [NOTFOUND=RETURN] [UNAVAIL=RETURN] "
+      "[TRYAGAIN=RETURN]";
+  TestFileReader file_reader(kFile);
+  reader_.set_file_read_call_for_testing(file_reader.GetFileReadCall());
+
+  std::vector<NsswitchReader::ServiceSpecification> services =
+      reader_.ReadAndParseHosts();
+
+  EXPECT_THAT(services,
+              testing::ElementsAre(NsswitchReader::ServiceSpecification(
+                  NsswitchReader::Service::kDns,
+                  {{/*negated=*/false, NsswitchReader::Status::kSuccess,
+                    NsswitchReader::Action::kReturn},
+                   {/*negated=*/false, NsswitchReader::Status::kNotFound,
+                    NsswitchReader::Action::kReturn},
+                   {/*negated=*/false, NsswitchReader::Status::kUnavailable,
+                    NsswitchReader::Action::kReturn},
+                   {/*negated=*/false, NsswitchReader::Status::kTryAgain,
+                    NsswitchReader::Action::kReturn}})));
+}
+
+TEST_F(NsswitchReaderTest, ParsesRepeatedActions) {
+  const std::string kFile =
+      "hosts: nis [!SUCCESS=RETURN] [NOTFOUND=RETURN] [NOTFOUND=RETURN] "
+      "[!UNAVAIL=RETURN]";
+  TestFileReader file_reader(kFile);
+  reader_.set_file_read_call_for_testing(file_reader.GetFileReadCall());
+
+  std::vector<NsswitchReader::ServiceSpecification> services =
+      reader_.ReadAndParseHosts();
+
+  EXPECT_THAT(services,
+              testing::ElementsAre(NsswitchReader::ServiceSpecification(
+                  NsswitchReader::Service::kNis,
+                  {{/*negated=*/true, NsswitchReader::Status::kSuccess,
+                    NsswitchReader::Action::kReturn},
+                   {/*negated=*/false, NsswitchReader::Status::kNotFound,
+                    NsswitchReader::Action::kReturn},
+                   {/*negated=*/false, NsswitchReader::Status::kNotFound,
+                    NsswitchReader::Action::kReturn},
+                   {/*negated=*/true, NsswitchReader::Status::kUnavailable,
+                    NsswitchReader::Action::kReturn}})));
+}
+
+TEST_F(NsswitchReaderTest, HandlesAtypicalWhitespace) {
+  const std::string kFile =
+      " database:  service   \n\n   hosts: files\tdns   mdns4 \t mdns6    \t  "
+      "\t\n\t\n";
+  TestFileReader file_reader(kFile);
+  reader_.set_file_read_call_for_testing(file_reader.GetFileReadCall());
+
+  std::vector<NsswitchReader::ServiceSpecification> services =
+      reader_.ReadAndParseHosts();
+
+  EXPECT_THAT(
+      services,
+      testing::ElementsAre(
+          NsswitchReader::ServiceSpecification(NsswitchReader::Service::kFiles),
+          NsswitchReader::ServiceSpecification(NsswitchReader::Service::kDns),
+          NsswitchReader::ServiceSpecification(NsswitchReader::Service::kMdns4),
+          NsswitchReader::ServiceSpecification(
+              NsswitchReader::Service::kMdns6)));
+}
+
+TEST_F(NsswitchReaderTest, ParsesActionsWithoutService) {
+  const std::string kFile = "hosts: [SUCCESS=RETURN]";
+  TestFileReader file_reader(kFile);
+  reader_.set_file_read_call_for_testing(file_reader.GetFileReadCall());
+
+  std::vector<NsswitchReader::ServiceSpecification> services =
+      reader_.ReadAndParseHosts();
+
+  EXPECT_THAT(services,
+              testing::ElementsAre(NsswitchReader::ServiceSpecification(
+                  NsswitchReader::Service::kUnknown,
+                  {{/*negated=*/false, NsswitchReader::Status::kSuccess,
+                    NsswitchReader::Action::kReturn}})));
+}
+
+TEST_F(NsswitchReaderTest, ParsesNegatedActions) {
+  const std::string kFile =
+      "hosts: mdns_minimal [!UNAVAIL=RETURN] [NOTFOUND=CONTINUE] "
+      "[!TRYAGAIN=CONTINUE]";
+  TestFileReader file_reader(kFile);
+  reader_.set_file_read_call_for_testing(file_reader.GetFileReadCall());
+
+  std::vector<NsswitchReader::ServiceSpecification> services =
+      reader_.ReadAndParseHosts();
+
+  EXPECT_THAT(services,
+              testing::ElementsAre(NsswitchReader::ServiceSpecification(
+                  NsswitchReader::Service::kMdnsMinimal,
+                  {{/*negated=*/true, NsswitchReader::Status::kUnavailable,
+                    NsswitchReader::Action::kReturn},
+                   {/*negated=*/false, NsswitchReader::Status::kNotFound,
+                    NsswitchReader::Action::kContinue},
+                   {/*negated=*/true, NsswitchReader::Status::kTryAgain,
+                    NsswitchReader::Action::kContinue}})));
+}
+
+TEST_F(NsswitchReaderTest, ParsesUnrecognizedServiceAsUnknown) {
+  const std::string kFile =
+      "passwd: files\nhosts: files super_awesome_service myhostname";
+  TestFileReader file_reader(kFile);
+  reader_.set_file_read_call_for_testing(file_reader.GetFileReadCall());
+
+  std::vector<NsswitchReader::ServiceSpecification> services =
+      reader_.ReadAndParseHosts();
+
+  EXPECT_THAT(services,
+              testing::ElementsAre(NsswitchReader::ServiceSpecification(
+                                       NsswitchReader::Service::kFiles),
+                                   NsswitchReader::ServiceSpecification(
+                                       NsswitchReader::Service::kUnknown),
+                                   NsswitchReader::ServiceSpecification(
+                                       NsswitchReader::Service::kMyHostname)));
+}
+
+TEST_F(NsswitchReaderTest, ParsesUnrecognizedStatusAsUnknown) {
+  const std::string kFile =
+      "hosts: nis [HELLO=CONTINUE]\nshadow: service\ndatabase: cheese";
+  TestFileReader file_reader(kFile);
+  reader_.set_file_read_call_for_testing(file_reader.GetFileReadCall());
+
+  std::vector<NsswitchReader::ServiceSpecification> services =
+      reader_.ReadAndParseHosts();
+
+  EXPECT_THAT(services,
+              testing::ElementsAre(NsswitchReader::ServiceSpecification(
+                  NsswitchReader::Service::kNis,
+                  {{/*negated=*/false, NsswitchReader::Status::kUnknown,
+                    NsswitchReader::Action::kContinue}})));
+}
+
+TEST_F(NsswitchReaderTest, ParsesUnrecognizedActionAsUnknown) {
+  const std::string kFile =
+      "more: service\nhosts: mdns6 [!UNAVAIL=HI]\nshadow: service";
+  TestFileReader file_reader(kFile);
+  reader_.set_file_read_call_for_testing(file_reader.GetFileReadCall());
+
+  std::vector<NsswitchReader::ServiceSpecification> services =
+      reader_.ReadAndParseHosts();
+
+  EXPECT_THAT(services,
+              testing::ElementsAre(NsswitchReader::ServiceSpecification(
+                  NsswitchReader::Service::kMdns6,
+                  {{/*negated=*/true, NsswitchReader::Status::kUnavailable,
+                    NsswitchReader::Action::kUnknown}})));
+}
+
+TEST_F(NsswitchReaderTest, ParsesInvalidActionsAsUnknown) {
+  const std::string kFile = "hosts: mdns_minimal [a=b=c] nis";
+  TestFileReader file_reader(kFile);
+  reader_.set_file_read_call_for_testing(file_reader.GetFileReadCall());
+
+  std::vector<NsswitchReader::ServiceSpecification> services =
+      reader_.ReadAndParseHosts();
+
+  EXPECT_THAT(
+      services,
+      testing::ElementsAre(
+          NsswitchReader::ServiceSpecification(
+              NsswitchReader::Service::kMdnsMinimal,
+              {{/*negated=*/false, NsswitchReader::Status::kUnknown,
+                NsswitchReader::Action::kUnknown}}),
+          NsswitchReader::ServiceSpecification(NsswitchReader::Service::kNis)));
+}
+
+TEST_F(NsswitchReaderTest, ParsesInvalidlyClosedActionsAsUnknown) {
+  const std::string kFile = "hosts: resolve [SUCCESS=CONTINUE dns";
+  TestFileReader file_reader(kFile);
+  reader_.set_file_read_call_for_testing(file_reader.GetFileReadCall());
+
+  std::vector<NsswitchReader::ServiceSpecification> services =
+      reader_.ReadAndParseHosts();
+
+  EXPECT_THAT(
+      services,
+      testing::ElementsAre(
+          NsswitchReader::ServiceSpecification(
+              NsswitchReader::Service::kResolve,
+              {{/*negated=*/false, NsswitchReader::Status::kUnknown,
+                NsswitchReader::Action::kUnknown}}),
+          NsswitchReader::ServiceSpecification(NsswitchReader::Service::kDns)));
+}
+
+TEST_F(NsswitchReaderTest, IgnoresComments) {
+  const std::string kFile =
+      "#hosts: files super_awesome_service myhostname\nnetmask: service";
+  TestFileReader file_reader(kFile);
+  reader_.set_file_read_call_for_testing(file_reader.GetFileReadCall());
+
+  std::vector<NsswitchReader::ServiceSpecification> services =
+      reader_.ReadAndParseHosts();
+
+  // Expect "files dns" due to not finding an uncommented "hosts:" row.
+  EXPECT_THAT(
+      services,
+      testing::ElementsAre(
+          NsswitchReader::ServiceSpecification(NsswitchReader::Service::kFiles),
+          NsswitchReader::ServiceSpecification(NsswitchReader::Service::kDns)));
+}
+
+TEST_F(NsswitchReaderTest, IgnoresEndOfLineComments) {
+  const std::string kFile =
+      "hosts: files super_awesome_service myhostname # dns";
+  TestFileReader file_reader(kFile);
+  reader_.set_file_read_call_for_testing(file_reader.GetFileReadCall());
+
+  std::vector<NsswitchReader::ServiceSpecification> services =
+      reader_.ReadAndParseHosts();
+
+  EXPECT_THAT(services,
+              testing::ElementsAre(NsswitchReader::ServiceSpecification(
+                                       NsswitchReader::Service::kFiles),
+                                   NsswitchReader::ServiceSpecification(
+                                       NsswitchReader::Service::kUnknown),
+                                   NsswitchReader::ServiceSpecification(
+                                       NsswitchReader::Service::kMyHostname)));
+}
+
+TEST_F(NsswitchReaderTest, IgnoresCapitalization) {
+  const std::string kFile = "HoStS: mDNS6 [!uNaVaIl=MeRgE]";
+  TestFileReader file_reader(kFile);
+  reader_.set_file_read_call_for_testing(file_reader.GetFileReadCall());
+
+  std::vector<NsswitchReader::ServiceSpecification> services =
+      reader_.ReadAndParseHosts();
+
+  EXPECT_THAT(services,
+              testing::ElementsAre(NsswitchReader::ServiceSpecification(
+                  NsswitchReader::Service::kMdns6,
+                  {{/*negated=*/true, NsswitchReader::Status::kUnavailable,
+                    NsswitchReader::Action::kMerge}})));
+}
+
+}  // namespace
+}  // namespace net