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