blob: ca11706c0739e508514e3d3da55593215e042734 [file] [log] [blame]
#include "./fuzztest/fuzztest_macros.h"
#include <cerrno>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "absl/strings/escaping.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_split.h"
#include "absl/strings/string_view.h"
namespace fuzztest {
namespace {
absl::StatusOr<std::string> ParseDictionaryEntry(absl::string_view entry) {
// We can't use absl::CUnescape directly on the string, because it assumes hex
// codes can have more than 2 digits, which is not the case here. "\x41BC" is
// a valid entry that should be unescaped to "ABC", but absl::CUnescape will
// fail as it will interpret it as a 4-digit hex code, which does not fit
// into a single byte.
// We unescape "\\", "\"", as well as each 2-digit hex codes (e.g. "\xab").
std::string parsed_entry;
int i = 0;
while (i < entry.size()) {
if (entry[i] != '\\') { // Handle unescaped character
parsed_entry.push_back(entry[i]);
++i;
} else if (i + 1 < entry.size() &&
(entry[i + 1] == '\\' ||
entry[i + 1] == '"')) { // Handle \\ and \"
parsed_entry.push_back(entry[i + 1]);
i += 2;
} else if (i + 3 < entry.size() &&
entry[i + 1] == 'x') { // Handle \xHH escape sequence
std::string unescaped_hex;
std::string error;
if (!absl::CUnescape(entry.substr(i, 4), &unescaped_hex, &error)) {
return absl::InvalidArgumentError(absl::StrCat(
"Could not unescape ", entry.substr(i, 4), ": ", error));
}
if (unescaped_hex.size() != 1) {
return absl::InvalidArgumentError(
absl::StrCat("Could not unescape ", entry.substr(i, 4)));
}
parsed_entry.append(unescaped_hex);
i += 4;
} else { // No other escape sequences are allowed.
return absl::InvalidArgumentError(absl::StrCat(
"Invalid escape sequence in dictionary entry: ", entry.substr(i, 2)));
}
}
return parsed_entry;
}
} // namespace
std::vector<std::tuple<std::string>> ReadFilesFromDirectory(
std::string_view dir) {
std::vector<std::tuple<std::string>> out;
const std::filesystem::path fs_dir(dir);
if (!std::filesystem::is_directory(fs_dir)) return out;
for (const auto& entry :
std::filesystem::recursive_directory_iterator(fs_dir)) {
if (std::filesystem::is_directory(entry)) continue;
std::ifstream stream(entry.path().string());
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__, entry.path().string(), errno, strerror(errno));
continue;
}
std::stringstream buffer;
buffer << stream.rdbuf();
out.push_back({buffer.str()});
}
return out;
}
absl::StatusOr<std::vector<std::string>> ParseDictionary(
absl::string_view text) {
std::vector<std::string> parsed_entries;
int line_number = 0;
for (absl::string_view line : absl::StrSplit(text, '\n')) {
++line_number;
if (line.empty() || line[0] == '#') continue;
auto first_index = line.find_first_of('"');
auto last_index = line.find_last_of('"');
if (last_index == std::string::npos) {
return absl::InvalidArgumentError(
absl::StrCat("Unparseable dictionary entry at line ", line_number,
": missing quotes"));
}
if (last_index <= first_index) {
return absl::InvalidArgumentError(
absl::StrCat("Unparseable dictionary entry at line ", line_number,
": entry must be enclosed in quotes"));
}
// Skip characters outside quotations.
const absl::string_view entry =
line.substr(first_index + 1, last_index - first_index - 1);
absl::StatusOr<std::string> parsed_entry = ParseDictionaryEntry(entry);
if (!parsed_entry.ok()) {
return absl::Status(
parsed_entry.status().code(),
absl::StrCat("Unparseable dictionary entry at line ", line_number,
": ", parsed_entry.status().message()));
}
parsed_entries.emplace_back(std::move(*parsed_entry));
}
return parsed_entries;
}
std::vector<std::string> ReadDictionaryFromFile(
std::string_view dictionary_file) {
std::vector<fuzztest::internal::FilePathAndData> files =
fuzztest::internal::ReadFileOrDirectory(
{dictionary_file.data(), dictionary_file.size()});
std::vector<std::string> out;
// Dictionary must be in the format specified at
// https://llvm.org/docs/LibFuzzer.html#dictionaries
for (const fuzztest::internal::FilePathAndData& file : files) {
absl::StatusOr<std::vector<std::string>> parsed_entries =
ParseDictionary(file.data);
CHECK(parsed_entries.status().ok())
<< "Could not parse dictionary file " << file.path << ": "
<< parsed_entries.status();
out.insert(out.end(), parsed_entries->begin(), parsed_entries->end());
}
return out;
}
} // namespace fuzztest