Add spirv-fuzz tool. (#2631)

The current tool can parse basic command-line argument, but generates
a binary identical to the input binary, since no transformations are
yet implemented.
diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt
index b1f741c..b3a4cc1 100644
--- a/tools/CMakeLists.txt
+++ b/tools/CMakeLists.txt
@@ -61,6 +61,11 @@
     set(SPIRV_INSTALL_TARGETS ${SPIRV_INSTALL_TARGETS} spirv-reduce)
   endif()
 
+  if(SPIRV_BUILD_FUZZER)
+    add_spvtools_tool(TARGET spirv-fuzz SRCS fuzz/fuzz.cpp util/cli_consumer.cpp LIBS SPIRV-Tools-fuzz ${SPIRV_TOOLS})
+    set(SPIRV_INSTALL_TARGETS ${SPIRV_INSTALL_TARGETS} spirv-fuzz)
+  endif(SPIRV_BUILD_FUZZER)
+
   if(ENABLE_SPIRV_TOOLS_INSTALL)
     install(TARGETS ${SPIRV_INSTALL_TARGETS}
       RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
diff --git a/tools/fuzz/fuzz.cpp b/tools/fuzz/fuzz.cpp
new file mode 100644
index 0000000..2a31a20
--- /dev/null
+++ b/tools/fuzz/fuzz.cpp
@@ -0,0 +1,255 @@
+// Copyright (c) 2019 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 <cassert>
+#include <cerrno>
+#include <cstring>
+#include <fstream>
+#include <functional>
+#include <string>
+
+#include "source/fuzz/fuzzer.h"
+#include "source/fuzz/protobufs/spirvfuzz_protobufs.h"
+#include "source/opt/build_module.h"
+#include "source/opt/ir_context.h"
+#include "source/opt/log.h"
+#include "source/spirv_fuzzer_options.h"
+#include "source/util/string_utils.h"
+#include "tools/io.h"
+#include "tools/util/cli_consumer.h"
+
+namespace {
+
+// Status and actions to perform after parsing command-line arguments.
+enum class FuzzActions { CONTINUE, STOP };
+
+struct FuzzStatus {
+  FuzzActions action;
+  int code;
+};
+
+void PrintUsage(const char* program) {
+  // NOTE: Please maintain flags in lexicographical order.
+  printf(
+      R"(%s - Fuzzes an equivalent SPIR-V binary based on a given binary.
+
+USAGE: %s [options] <input.spv> -o <output.spv>
+
+The SPIR-V binary is read from <input.spv>, which must have extension .spv.  If
+<input.json> is also present, facts about the SPIR-V binary are read from this
+file.
+
+The transformed SPIR-V binary is written to <output.spv>.  Human-readable and
+binary representations of the transformations that were applied to obtain this
+binary are written to <output.json> and <output.transformations>, respectively.
+
+NOTE: The fuzzer is a work in progress.
+
+Options (in lexicographical order):
+
+  -h, --help
+               Print this help.
+  --seed
+               Unsigned 32-bit integer seed to control random number
+               generation.
+  --version
+               Display fuzzer version information.
+
+)",
+      program, program);
+}
+
+// Message consumer for this tool.  Used to emit diagnostics during
+// initialization and setup. Note that |source| and |position| are irrelevant
+// here because we are still not processing a SPIR-V input file.
+void FuzzDiagnostic(spv_message_level_t level, const char* /*source*/,
+                    const spv_position_t& /*position*/, const char* message) {
+  if (level == SPV_MSG_ERROR) {
+    fprintf(stderr, "error: ");
+  }
+  fprintf(stderr, "%s\n", message);
+}
+
+bool EndsWithSpv(const std::string& filename) {
+  std::string dot_spv = ".spv";
+  return filename.length() >= dot_spv.length() &&
+         0 == filename.compare(filename.length() - dot_spv.length(),
+                               filename.length(), dot_spv);
+}
+
+FuzzStatus ParseFlags(int argc, const char** argv, std::string* in_binary_file,
+                      std::string* out_binary_file,
+                      spvtools::FuzzerOptions* fuzzer_options) {
+  uint32_t positional_arg_index = 0;
+
+  for (int argi = 1; argi < argc; ++argi) {
+    const char* cur_arg = argv[argi];
+    if ('-' == cur_arg[0]) {
+      if (0 == strcmp(cur_arg, "--version")) {
+        spvtools::Logf(FuzzDiagnostic, SPV_MSG_INFO, nullptr, {}, "%s\n",
+                       spvSoftwareVersionDetailsString());
+        return {FuzzActions::STOP, 0};
+      } else if (0 == strcmp(cur_arg, "--help") || 0 == strcmp(cur_arg, "-h")) {
+        PrintUsage(argv[0]);
+        return {FuzzActions::STOP, 0};
+      } else if (0 == strcmp(cur_arg, "-o")) {
+        if (out_binary_file->empty() && argi + 1 < argc) {
+          *out_binary_file = std::string(argv[++argi]);
+        } else {
+          PrintUsage(argv[0]);
+          return {FuzzActions::STOP, 1};
+        }
+      } else if (0 == strncmp(cur_arg, "--seed=", sizeof("--seed=") - 1)) {
+        const auto split_flag = spvtools::utils::SplitFlagArgs(cur_arg);
+        char* end = nullptr;
+        errno = 0;
+        const auto seed =
+            static_cast<uint32_t>(strtol(split_flag.second.c_str(), &end, 10));
+        assert(end != split_flag.second.c_str() && errno == 0);
+        fuzzer_options->set_random_seed(seed);
+      } else if ('\0' == cur_arg[1]) {
+        // We do not support fuzzing from standard input.  We could support
+        // this if there was a compelling use case.
+        PrintUsage(argv[0]);
+        return {FuzzActions::STOP, 0};
+      }
+    } else if (positional_arg_index == 0) {
+      // Binary input file name
+      assert(in_binary_file->empty());
+      *in_binary_file = std::string(cur_arg);
+      positional_arg_index++;
+    } else {
+      spvtools::Error(FuzzDiagnostic, nullptr, {},
+                      "Too many positional arguments specified");
+      return {FuzzActions::STOP, 1};
+    }
+  }
+
+  if (in_binary_file->empty()) {
+    spvtools::Error(FuzzDiagnostic, nullptr, {}, "No input file specified");
+    return {FuzzActions::STOP, 1};
+  }
+
+  if (!EndsWithSpv(*in_binary_file)) {
+    spvtools::Error(FuzzDiagnostic, nullptr, {},
+                    "Input filename must have extension .spv");
+    return {FuzzActions::STOP, 1};
+  }
+
+  if (out_binary_file->empty()) {
+    spvtools::Error(FuzzDiagnostic, nullptr, {}, "-o required");
+    return {FuzzActions::STOP, 1};
+  }
+
+  if (!EndsWithSpv(*out_binary_file)) {
+    spvtools::Error(FuzzDiagnostic, nullptr, {},
+                    "Output filename must have extension .spv");
+    return {FuzzActions::STOP, 1};
+  }
+
+  return {FuzzActions::CONTINUE, 0};
+}
+
+}  // namespace
+
+const auto kDefaultEnvironment = SPV_ENV_UNIVERSAL_1_3;
+
+int main(int argc, const char** argv) {
+  std::string in_binary_file;
+  std::string out_binary_file;
+
+  spv_target_env target_env = kDefaultEnvironment;
+  spvtools::FuzzerOptions fuzzer_options;
+
+  FuzzStatus status = ParseFlags(argc, argv, &in_binary_file, &out_binary_file,
+                                 &fuzzer_options);
+
+  if (status.action == FuzzActions::STOP) {
+    return status.code;
+  }
+
+  std::vector<uint32_t> binary_in;
+  if (!ReadFile<uint32_t>(in_binary_file.c_str(), "rb", &binary_in)) {
+    return 1;
+  }
+
+  spvtools::fuzz::protobufs::FactSequence initial_facts;
+  const std::string dot_spv(".spv");
+  std::string in_facts_file =
+      in_binary_file.substr(0, in_binary_file.length() - dot_spv.length()) +
+      ".json";
+  std::ifstream facts_input(in_facts_file);
+  if (facts_input) {
+    std::string facts_json_string((std::istreambuf_iterator<char>(facts_input)),
+                                  std::istreambuf_iterator<char>());
+    facts_input.close();
+    if (google::protobuf::util::Status::OK !=
+        google::protobuf::util::JsonStringToMessage(facts_json_string,
+                                                    &initial_facts)) {
+      spvtools::Error(FuzzDiagnostic, nullptr, {}, "Error reading facts data");
+      return 1;
+    }
+  }
+
+  std::vector<uint32_t> binary_out;
+  spvtools::fuzz::protobufs::TransformationSequence transformations_applied;
+
+  spvtools::fuzz::Fuzzer fuzzer(target_env);
+  fuzzer.SetMessageConsumer(spvtools::utils::CLIMessageConsumer);
+  auto fuzz_result_status =
+      fuzzer.Run(binary_in, initial_facts, &binary_out,
+                 &transformations_applied, fuzzer_options);
+  if (fuzz_result_status !=
+      spvtools::fuzz::Fuzzer::FuzzerResultStatus::kComplete) {
+    spvtools::Error(FuzzDiagnostic, nullptr, {}, "Error running fuzzer");
+    return 1;
+  }
+
+  if (!WriteFile<uint32_t>(out_binary_file.c_str(), "wb", binary_out.data(),
+                           binary_out.size())) {
+    spvtools::Error(FuzzDiagnostic, nullptr, {}, "Error writing out binary");
+    return 1;
+  }
+
+  std::string output_file_prefix =
+      out_binary_file.substr(0, out_binary_file.length() - dot_spv.length());
+  std::ofstream transformations_file;
+  transformations_file.open(output_file_prefix + ".transformations",
+                            std::ios::out | std::ios::binary);
+  bool success =
+      transformations_applied.SerializeToOstream(&transformations_file);
+  transformations_file.close();
+  if (!success) {
+    spvtools::Error(FuzzDiagnostic, nullptr, {},
+                    "Error writing out transformations binary");
+    return 1;
+  }
+
+  std::string json_string;
+  auto json_options = google::protobuf::util::JsonOptions();
+  json_options.add_whitespace = true;
+  auto json_generation_status = google::protobuf::util::MessageToJsonString(
+      transformations_applied, &json_string, json_options);
+  if (json_generation_status != google::protobuf::util::Status::OK) {
+    spvtools::Error(FuzzDiagnostic, nullptr, {},
+                    "Error writing out transformations in JSON format");
+    return 1;
+  }
+
+  std::ofstream transformations_json_file(output_file_prefix + ".json");
+  transformations_json_file << json_string;
+  transformations_json_file.close();
+
+  return 0;
+}