blob: 6f95f66cf91d3baf1121fa0727836267350a2ef1 [file] [log] [blame] [edit]
// Copyright 2022 Google LLC
//
// 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 "./fuzztest/internal/io.h"
#include <cerrno>
#include <cstring>
#include <filesystem> // NOLINT
#include <fstream>
#include <memory>
#include <optional>
#include <sstream>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include <vector>
#include "absl/functional/function_ref.h"
#include "absl/hash/hash.h"
#include "absl/status/status.h"
#include "absl/strings/str_format.h"
#include "absl/strings/string_view.h"
#include "absl/time/clock.h"
#include "absl/time/time.h"
#include "absl/types/span.h"
#include "./common/blob_file.h"
#include "./common/defs.h"
#include "./common/remote_file.h"
#include "./fuzztest/internal/logging.h"
namespace fuzztest::internal {
#if defined(FUZZTEST_STUB_STD_FILESYSTEM)
// TODO(lszekeres): Return absl::Status instead of bool for these.
bool WriteFile(absl::string_view path, absl::string_view contents) {
FUZZTEST_INTERNAL_CHECK(false, "Filesystem API not supported in iOS/MacOS");
}
std::optional<std::string> ReadFile(absl::string_view path) {
FUZZTEST_INTERNAL_CHECK(false, "Filesystem API not supported in iOS/MacOS");
}
bool IsDirectory(absl::string_view path) {
FUZZTEST_INTERNAL_CHECK(false, "Filesystem API not supported in iOS/MacOS");
}
bool CreateDirectory(absl::string_view path) {
FUZZTEST_INTERNAL_CHECK(false, "Filesystem API not supported in iOS/MacOS");
}
std::vector<std::string> ListDirectory(absl::string_view path) {
FUZZTEST_INTERNAL_CHECK(false, "Filesystem API not supported in iOS/MacOS");
}
std::vector<std::string> ListDirectoryRecursively(absl::string_view path) {
FUZZTEST_INTERNAL_CHECK(false, "Filesystem API not supported in iOS/MacOS");
}
#else
bool WriteFile(absl::string_view path, absl::string_view contents) {
const std::filesystem::path fs_path{
std::string_view{path.data(), path.size()}};
// Just in case the directory does not currently exist.
// If it does, this is a noop.
CreateDirectory(fs_path.parent_path().string());
std::ofstream file(fs_path);
file << contents;
file.close();
if (!file.good()) {
absl::FPrintF(GetStderr(), "[!] %s:%d: Error writing %s: (%d) %s\n",
__FILE__, __LINE__, path, errno, strerror(errno));
}
return !file.fail();
}
std::optional<std::string> ReadFile(absl::string_view path) {
const std::filesystem::path fs_path{
std::string_view{path.data(), path.size()}};
std::ifstream stream(fs_path);
if (!stream.good()) {
// Using stderr instead of GetStderr() to avoid initialization-order-fiasco
// when reading files at static init time with
// `.WithSeeds(fuzztest::ReadFilesFromDirectory(...))`.
absl::FPrintF(stderr, "[!] %s:%d: Error reading %s: (%d) %s\n", __FILE__,
__LINE__, path, errno, strerror(errno));
return std::nullopt;
}
std::stringstream buffer;
buffer << stream.rdbuf();
return buffer.str();
}
bool IsDirectory(absl::string_view path) {
const std::filesystem::path fs_path{
std::string_view{path.data(), path.size()}};
return std::filesystem::is_directory(fs_path);
}
bool CreateDirectory(absl::string_view path) {
const std::filesystem::path fs_path{
std::string_view{path.data(), path.size()}};
return std::filesystem::create_directories(fs_path);
}
std::vector<std::string> ListDirectory(absl::string_view path) {
std::vector<std::string> output_paths;
const std::filesystem::path fs_path{
std::string_view{path.data(), path.size()}};
if (!std::filesystem::is_directory(fs_path)) return output_paths;
for (const auto& entry : std::filesystem::directory_iterator(fs_path)) {
output_paths.push_back(entry.path().string());
}
return output_paths;
}
std::vector<std::string> ListDirectoryRecursively(absl::string_view path) {
std::vector<std::string> output_paths;
const std::filesystem::path fs_path{
std::string_view{path.data(), path.size()}};
for (const auto& entry : std::filesystem::recursive_directory_iterator(
fs_path,
std::filesystem::directory_options::follow_directory_symlink)) {
output_paths.push_back(entry.path().string());
}
return output_paths;
}
#endif // FUZZTEST_STUB_STD_FILESYSTEM
std::string WriteDataToDir(absl::string_view data, absl::string_view outdir) {
std::string filename(outdir);
if (filename.back() != '/') filename += '/';
absl::StrAppendFormat(&filename, "%016x",
absl::Hash<absl::string_view>{}(data));
if (!WriteFile(filename, data)) return "";
return filename;
}
std::vector<FilePathAndData> ReadFileOrDirectory(
absl::string_view file_or_dir) {
std::vector<FilePathAndData> out;
const auto try_append_file = [&](std::string path) {
std::optional<std::string> contents = ReadFile(path);
if (contents.has_value()) {
out.push_back(FilePathAndData{std::move(path), *std::move(contents)});
}
};
if (IsDirectory(file_or_dir)) {
for (const auto& path : ListDirectoryRecursively(file_or_dir)) {
if (!IsDirectory(path)) {
try_append_file(path);
}
}
} else {
try_append_file(std::string(file_or_dir));
}
return out;
}
absl::string_view Basename(absl::string_view filename) {
auto last_slash_pos = filename.find_last_of("/\\");
return last_slash_pos == absl::string_view::npos
? filename
: filename.substr(last_slash_pos + 1);
}
std::vector<std::tuple<std::string>> ReadFilesFromDirectory(
absl::string_view dir) {
std::vector<FilePathAndData> files =
ReadFileOrDirectory({dir.data(), dir.size()});
std::vector<std::tuple<std::string>> out;
out.reserve(files.size());
for (const FilePathAndData& file : files) {
out.push_back(std::make_tuple(file.data));
}
return out;
}
// TODO(b/348702296): Consider merging with `centipede::ReadShard()`.
void ForEachSerializedInput(absl::Span<const std::string> file_paths,
absl::FunctionRef<absl::Status(
absl::string_view file_path,
std::optional<int> blob_idx, std::string input)>
consume,
absl::Duration timeout) {
int total_loaded_inputs = 0;
int total_invalid_inputs = 0;
const absl::Time start_time = absl::Now();
for (const std::string& file_path : file_paths) {
FUZZTEST_INTERNAL_CHECK_PRECONDITION(centipede::RemotePathExists(file_path),
"File path ", file_path,
" does not exist.");
FUZZTEST_INTERNAL_CHECK_PRECONDITION(
!centipede::RemotePathIsDirectory(file_path), "File path ", file_path,
" is a directory.");
int loaded_inputs_from_file = 0;
int invalid_inputs_from_file = 0;
// The reader cannot be reused for multiple files because of the way it
// handles its internal state. So we instantiate a new reader for each file.
std::unique_ptr<centipede::BlobFileReader> reader =
centipede::DefaultBlobFileReaderFactory();
if (reader->Open(file_path).ok()) {
centipede::ByteSpan blob;
for (int blob_idx = 0; reader->Read(blob).ok(); ++blob_idx) {
if (absl::Now() - start_time > timeout) {
absl::FPrintF(GetStderr(),
"[!] Timeout reached while processing input at index "
"%d in file %s.\n",
blob_idx, file_path);
break;
}
absl::Status result = consume(
file_path, blob_idx, std::string(centipede::AsStringView(blob)));
if (result.ok()) {
++loaded_inputs_from_file;
} else {
++invalid_inputs_from_file;
absl::FPrintF(GetStderr(),
"[!] Invalid input at index %d in file %s: %s\n",
blob_idx, file_path, result.message());
}
}
}
if (absl::Now() - start_time > timeout) {
absl::FPrintF(GetStderr(),
"[!] Timeout reached while processing input %s.\n",
file_path);
break;
}
if (loaded_inputs_from_file + invalid_inputs_from_file > 0) {
// The file was a blob file and we read some inputs from it.
absl::FPrintF(
GetStderr(),
"[*] Loaded %d inputs and ignored %d invalid inputs from %s.\n",
loaded_inputs_from_file, invalid_inputs_from_file, file_path);
total_loaded_inputs += loaded_inputs_from_file;
total_invalid_inputs += invalid_inputs_from_file;
continue;
}
// The file was not a blob file (or, unlikely, it was an empty blob file);
// read its contents directly.
// TODO(b/349115475): Currently, we cannot distinguish between an empty blob
// file and a file that is not a blob file. Once we can, we should not fall
// back to reading the file directly if it is an empty blob file.
std::string contents;
const absl::Status get_contents_status =
centipede::RemoteFileGetContents(file_path, contents);
FUZZTEST_INTERNAL_CHECK_PRECONDITION(
get_contents_status.ok(), "RemoteFileGetContents failed on ", file_path,
", status: ", get_contents_status.message());
absl::Status result = consume(file_path, std::nullopt, std::move(contents));
if (result.ok()) {
++total_loaded_inputs;
} else {
++total_invalid_inputs;
absl::FPrintF(GetStderr(), "[!] Invalid input file %s: %s\n", file_path,
result.message());
}
}
// Print stats if we attempted to load something.
if (total_loaded_inputs != 0 || total_invalid_inputs != 0) {
absl::FPrintF(
GetStderr(),
"[*] In total, loaded %d inputs and ignored %d invalid inputs.\n",
total_loaded_inputs, total_invalid_inputs);
}
}
} // namespace fuzztest::internal