Provide a default implementation of quiche_file_utils.
This also adds support for building on Windows, at least far enough to get the file_utils test to build.
PiperOrigin-RevId: 382741424
diff --git a/common/platform/api/quiche_file_utils.cc b/common/platform/api/quiche_file_utils.cc
new file mode 100644
index 0000000..fc0c189
--- /dev/null
+++ b/common/platform/api/quiche_file_utils.cc
@@ -0,0 +1,51 @@
+#include "common/platform/api/quiche_file_utils.h"
+
+#include "quiche_platform_impl/quiche_file_utils_impl.h"
+
+namespace quiche {
+
+std::string JoinPath(absl::string_view a, absl::string_view b) {
+ return JoinPathImpl(a, b);
+}
+
+absl::optional<std::string> ReadFileContents(absl::string_view file) {
+ return ReadFileContentsImpl(file);
+}
+
+bool EnumerateDirectory(absl::string_view path,
+ std::vector<std::string>& directories,
+ std::vector<std::string>& files) {
+ return EnumerateDirectoryImpl(path, directories, files);
+}
+
+bool EnumerateDirectoryRecursivelyInner(absl::string_view path,
+ int recursion_limit,
+ std::vector<std::string>& files) {
+ if (recursion_limit < 0) {
+ return false;
+ }
+
+ std::vector<std::string> local_files;
+ std::vector<std::string> directories;
+ if (!EnumerateDirectory(path, directories, local_files)) {
+ return false;
+ }
+ for (const std::string& directory : directories) {
+ if (!EnumerateDirectoryRecursivelyInner(JoinPath(path, directory),
+ recursion_limit - 1, files)) {
+ return false;
+ }
+ }
+ for (const std::string& file : local_files) {
+ files.push_back(JoinPath(path, file));
+ }
+ return true;
+}
+
+bool EnumerateDirectoryRecursively(absl::string_view path,
+ std::vector<std::string>& files) {
+ constexpr int kRecursionLimit = 20;
+ return EnumerateDirectoryRecursivelyInner(path, kRecursionLimit, files);
+}
+
+} // namespace quiche
diff --git a/common/platform/api/quiche_file_utils.h b/common/platform/api/quiche_file_utils.h
new file mode 100644
index 0000000..47723d1
--- /dev/null
+++ b/common/platform/api/quiche_file_utils.h
@@ -0,0 +1,40 @@
+// 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.
+
+// This header contains basic filesystem functions for use in unit tests and CLI
+// tools. Note that those are not 100% suitable for production use, as in, they
+// might be prone to race conditions and not always handle non-ASCII filenames
+// correctly.
+#ifndef QUICHE_COMMON_PLATFORM_API_QUICHE_FILE_UTILS_H_
+#define QUICHE_COMMON_PLATFORM_API_QUICHE_FILE_UTILS_H_
+
+#include <string>
+#include <vector>
+
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+
+namespace quiche {
+
+// Join two paths in a platform-specific way. Returns |a| if |b| is empty, and
+// vice versa.
+std::string JoinPath(absl::string_view a, absl::string_view b);
+
+// Reads the entire file into the memory.
+absl::optional<std::string> ReadFileContents(absl::string_view file);
+
+// Lists all files and directories in the directory specified by |path|. Returns
+// true on success, false on failure.
+bool EnumerateDirectory(absl::string_view path,
+ std::vector<std::string>& directories,
+ std::vector<std::string>& files);
+
+// Recursively enumerates all of the files in the directory and all of the
+// internal subdirectories. Has a fairly small recursion limit.
+bool EnumerateDirectoryRecursively(absl::string_view path,
+ std::vector<std::string>& files);
+
+} // namespace quiche
+
+#endif // QUICHE_COMMON_PLATFORM_API_QUICHE_FILE_UTILS_H_
diff --git a/common/platform/api/quiche_file_utils_test.cc b/common/platform/api/quiche_file_utils_test.cc
new file mode 100644
index 0000000..ddf1a40
--- /dev/null
+++ b/common/platform/api/quiche_file_utils_test.cc
@@ -0,0 +1,86 @@
+#include "common/platform/api/quiche_file_utils.h"
+
+#include <vector>
+
+#include "absl/algorithm/container.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/strip.h"
+#include "absl/types/optional.h"
+#include "common/platform/api/quiche_test.h"
+
+namespace quiche {
+namespace test {
+namespace {
+
+using testing::UnorderedElementsAre;
+using testing::UnorderedElementsAreArray;
+
+TEST(QuicheFileUtilsTest, ReadFileContents) {
+ std::string path = absl::StrCat(QuicheGetCommonSourcePath(),
+ "/platform/api/testdir/testfile");
+ absl::optional<std::string> contents = ReadFileContents(path);
+ ASSERT_TRUE(contents.has_value());
+ EXPECT_EQ(*contents, "This is a test file.");
+}
+
+TEST(QuicheFileUtilsTest, ReadFileContentsFileNotFound) {
+ std::string path =
+ absl::StrCat(QuicheGetCommonSourcePath(),
+ "/platform/api/testdir/file-that-does-not-exist");
+ absl::optional<std::string> contents = ReadFileContents(path);
+ EXPECT_FALSE(contents.has_value());
+}
+
+TEST(QuicheFileUtilsTest, EnumerateDirectory) {
+ std::string path =
+ absl::StrCat(QuicheGetCommonSourcePath(), "/platform/api/testdir");
+ std::vector<std::string> dirs;
+ std::vector<std::string> files;
+ bool success = EnumerateDirectory(path, dirs, files);
+ EXPECT_TRUE(success);
+ EXPECT_THAT(files, UnorderedElementsAre("testfile", "README.md"));
+ EXPECT_THAT(dirs, UnorderedElementsAre("a"));
+}
+
+TEST(QuicheFileUtilsTest, EnumerateDirectoryNoSuchDirectory) {
+ std::string path = absl::StrCat(QuicheGetCommonSourcePath(),
+ "/platform/api/testdir/no-such-directory");
+ std::vector<std::string> dirs;
+ std::vector<std::string> files;
+ bool success = EnumerateDirectory(path, dirs, files);
+ EXPECT_FALSE(success);
+}
+
+TEST(QuicheFileUtilsTest, EnumerateDirectoryNotADirectory) {
+ std::string path = absl::StrCat(QuicheGetCommonSourcePath(),
+ "/platform/api/testdir/testfile");
+ std::vector<std::string> dirs;
+ std::vector<std::string> files;
+ bool success = EnumerateDirectory(path, dirs, files);
+ EXPECT_FALSE(success);
+}
+
+TEST(QuicheFileUtilsTest, EnumerateDirectoryRecursively) {
+ std::vector<std::string> expected_paths = {"a/b/c/d/e", "a/subdir/testfile",
+ "a/z", "testfile", "README.md"};
+
+ std::string root_path =
+ absl::StrCat(QuicheGetCommonSourcePath(), "/platform/api/testdir");
+ for (std::string& path : expected_paths) {
+ // For Windows, use Windows path separators.
+ if (JoinPath("a", "b") == "a\\b") {
+ absl::c_replace(path, '/', '\\');
+ }
+
+ path = JoinPath(root_path, path);
+ }
+
+ std::vector<std::string> files;
+ bool success = EnumerateDirectoryRecursively(root_path, files);
+ EXPECT_TRUE(success);
+ EXPECT_THAT(files, UnorderedElementsAreArray(expected_paths));
+}
+
+} // namespace
+} // namespace test
+} // namespace quiche
diff --git a/common/platform/api/quiche_test.h b/common/platform/api/quiche_test.h
index 1af25df..efec638 100644
--- a/common/platform/api/quiche_test.h
+++ b/common/platform/api/quiche_test.h
@@ -12,6 +12,18 @@
template <class T>
using QuicheTestWithParam = quiche::test::QuicheTestWithParamImpl<T>;
+namespace quiche {
+namespace test {
+
+// Returns the path to quiche/common directory where the test data could be
+// located.
+inline std::string QuicheGetCommonSourcePath() {
+ return QuicheGetCommonSourcePathImpl();
+}
+
+} // namespace test
+} // namespace quiche
+
#define EXPECT_QUICHE_DEBUG_DEATH(condition, message) \
EXPECT_QUICHE_DEBUG_DEATH_IMPL(condition, message)
diff --git a/common/platform/default/quiche_platform_impl/quiche_file_utils_impl.cc b/common/platform/default/quiche_platform_impl/quiche_file_utils_impl.cc
new file mode 100644
index 0000000..65965b2
--- /dev/null
+++ b/common/platform/default/quiche_platform_impl/quiche_file_utils_impl.cc
@@ -0,0 +1,182 @@
+// 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 "quiche_platform_impl/quiche_file_utils_impl.h"
+
+#if defined(_WIN32)
+#include <windows.h>
+#else
+#include <dirent.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+#endif // defined(_WIN32)
+
+#include <fstream>
+#include <ios>
+#include <iostream>
+
+#include "absl/strings/str_cat.h"
+#include "absl/strings/string_view.h"
+#include "absl/strings/strip.h"
+#include "absl/types/optional.h"
+
+namespace quiche {
+
+#if defined(_WIN32)
+std::string JoinPathImpl(absl::string_view a, absl::string_view b) {
+ if (a.empty()) {
+ return std::string(b);
+ }
+ if (b.empty()) {
+ return std::string(a);
+ }
+ // Win32 actually provides two different APIs for combining paths; one of them
+ // has issues that could potentially lead to buffer overflow, and another is
+ // not supported in Windows 7, which is why we're doing it manually.
+ a = absl::StripSuffix(a, "/");
+ a = absl::StripSuffix(a, "\\");
+ return absl::StrCat(a, "\\", b);
+}
+#else
+std::string JoinPathImpl(absl::string_view a, absl::string_view b) {
+ if (a.empty()) {
+ return std::string(b);
+ }
+ if (b.empty()) {
+ return std::string(a);
+ }
+ return absl::StrCat(absl::StripSuffix(a, "/"), "/", b);
+}
+#endif // defined(_WIN32)
+
+absl::optional<std::string> ReadFileContentsImpl(absl::string_view file) {
+ std::ifstream input_file(std::string{file}, std::ios::binary);
+ if (!input_file || !input_file.is_open()) {
+ return absl::nullopt;
+ }
+
+ input_file.seekg(0, std::ios_base::end);
+ auto file_size = input_file.tellg();
+ if (!input_file) {
+ return absl::nullopt;
+ }
+ input_file.seekg(0, std::ios_base::beg);
+
+ std::string output;
+ output.resize(file_size);
+ input_file.read(&output[0], file_size);
+ if (!input_file) {
+ return absl::nullopt;
+ }
+
+ return output;
+}
+
+#if defined(_WIN32)
+
+class ScopedDir {
+ public:
+ ScopedDir(HANDLE dir) : dir_(dir) {}
+ ~ScopedDir() {
+ if (dir_ != INVALID_HANDLE_VALUE) {
+ // The API documentation explicitly says that CloseHandle() should not be
+ // used on directory search handles.
+ FindClose(dir_);
+ dir_ = INVALID_HANDLE_VALUE;
+ }
+ }
+
+ HANDLE get() { return dir_; }
+
+ private:
+ HANDLE dir_;
+};
+
+bool EnumerateDirectoryImpl(absl::string_view path,
+ std::vector<std::string>& directories,
+ std::vector<std::string>& files) {
+ std::string path_owned(path);
+
+ // Explicitly check that the directory we are trying to search is in fact a
+ // directory.
+ DWORD attributes = GetFileAttributesA(path_owned.c_str());
+ if (attributes == INVALID_FILE_ATTRIBUTES) {
+ return false;
+ }
+ if ((attributes & FILE_ATTRIBUTE_DIRECTORY) == 0) {
+ return false;
+ }
+
+ std::string search_path = JoinPathImpl(path, "*");
+ WIN32_FIND_DATAA file_data;
+ ScopedDir dir(FindFirstFileA(search_path.c_str(), &file_data));
+ if (dir.get() == INVALID_HANDLE_VALUE) {
+ return GetLastError() == ERROR_FILE_NOT_FOUND;
+ }
+ do {
+ std::string filename(file_data.cFileName);
+ if (filename == "." || filename == "..") {
+ continue;
+ }
+ if ((file_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
+ directories.push_back(std::move(filename));
+ } else {
+ files.push_back(std::move(filename));
+ }
+ } while (FindNextFileA(dir.get(), &file_data));
+ return GetLastError() == ERROR_NO_MORE_FILES;
+}
+
+#else // defined(_WIN32)
+
+class ScopedDir {
+ public:
+ ScopedDir(DIR* dir) : dir_(dir) {}
+ ~ScopedDir() {
+ if (dir_ != nullptr) {
+ closedir(dir_);
+ dir_ = nullptr;
+ }
+ }
+
+ DIR* get() { return dir_; }
+
+ private:
+ DIR* dir_;
+};
+
+bool EnumerateDirectoryImpl(absl::string_view path,
+ std::vector<std::string>& directories,
+ std::vector<std::string>& files) {
+ std::string path_owned(path);
+ ScopedDir dir(opendir(path_owned.c_str()));
+ if (dir.get() == nullptr) {
+ return false;
+ }
+
+ dirent* entry;
+ while ((entry = readdir(dir.get()))) {
+ const std::string filename(entry->d_name);
+ if (filename == "." || filename == "..") {
+ continue;
+ }
+
+ const std::string entry_path = JoinPathImpl(path, filename);
+ struct stat stat_entry;
+ if (stat(entry_path.c_str(), &stat_entry) != 0) {
+ return false;
+ }
+ if (S_ISREG(stat_entry.st_mode)) {
+ files.push_back(std::move(filename));
+ } else if (S_ISDIR(stat_entry.st_mode)) {
+ directories.push_back(std::move(filename));
+ }
+ }
+ return true;
+}
+
+#endif // defined(_WIN32)
+
+} // namespace quiche
diff --git a/common/platform/default/quiche_platform_impl/quiche_file_utils_impl.h b/common/platform/default/quiche_platform_impl/quiche_file_utils_impl.h
new file mode 100644
index 0000000..ad5ff1a
--- /dev/null
+++ b/common/platform/default/quiche_platform_impl/quiche_file_utils_impl.h
@@ -0,0 +1,26 @@
+// 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 QUICHE_COMMON_PLATFORM_DEFAULT_QUICHE_PLATFORM_IMPL_QUICHE_FILE_UTILS_IMPL_H_
+#define QUICHE_COMMON_PLATFORM_DEFAULT_QUICHE_PLATFORM_IMPL_QUICHE_FILE_UTILS_IMPL_H_
+
+#include <string>
+#include <vector>
+
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+
+namespace quiche {
+
+std::string JoinPathImpl(absl::string_view a, absl::string_view b);
+
+absl::optional<std::string> ReadFileContentsImpl(absl::string_view file);
+
+bool EnumerateDirectoryImpl(absl::string_view path,
+ std::vector<std::string>& directories,
+ std::vector<std::string>& files);
+
+} // namespace quiche
+
+#endif // QUICHE_COMMON_PLATFORM_DEFAULT_QUICHE_PLATFORM_IMPL_QUICHE_FILE_UTILS_IMPL_H_