// Copyright 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
//
//     https://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.

// Simple utility to wrap a binary file in a C++ source file.

#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <utility>
#include <vector>

#include "absl/strings/ascii.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/strings/str_replace.h"
#include "sandboxed_api/util/fileops.h"
#include "sandboxed_api/util/raw_logging.h"
#include "sandboxed_api/util/strerror.h"

// C-escapes a character and writes it to a file stream.
void FWriteCEscapedC(int c, FILE* out) {
  /* clang-format off */
  constexpr char kCEscapedLen[256] = {
      4, 4, 4, 4, 4, 4, 4, 4, 4, 2, 2, 4, 4, 2, 4, 4,  // \t, \n, \r
      4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
      1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,  // "
      1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2,  // '0'..'9'
      1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,  // 'A'..'O'
      1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1,  // 'P'..'Z', '\'
      1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,  // 'a'..'o'
      1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,  // 'p'..'z', DEL
      4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
      4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
      4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
      4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
      4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
      4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
      4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
      4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
  };
  /* clang-format on */

  int char_len = kCEscapedLen[c];
  if (char_len == 1) {
    fputc(c, out);
  } else if (char_len == 2) {
    fputc('\\', out);
    switch (c) {
      case '\0':
        fputc('0', out);
        break;
      case '\n':
        fputc('n', out);
        break;
      case '\r':
        fputc('r', out);
        break;
      case '\t':
        fputc('t', out);
        break;
      case '\"':
      case '\'':
      case '\\':
      case '?':
        fputc(c, out);
        break;
    }
  } else {
    fputc('\\', out);
    fputc('0' + c / 64, out);
    fputc('0' + (c % 64) / 8, out);
    fputc('0' + c % 8, out);
  }
}

// Small RAII class that wraps C-style FILE streams and sets up buffering.
class File {
 public:
  File(const char* name, const char* mode)
      : name_{name}, stream_{fopen(name, mode)}, buf_(4096, '\0') {
    SAPI_RAW_PCHECK(stream_ != nullptr, "Open %s", name_);
    std::setvbuf(stream_, &buf_[0], _IOFBF, buf_.size());
    Check();
  }
  ~File() { fclose(stream_); }

  void Check() {
    if (ferror(stream_)) {
      SAPI_RAW_PLOG(ERROR, "I/O on %s", name_);
      _Exit(EXIT_FAILURE);
    }
  }

  FILE* get() const { return stream_; }

 private:
  const char* name_;
  FILE* stream_;
  std::string buf_;
};

// Format literals for generating the .h file
constexpr const char kHFileHeaderFmt[] =
    R"(// Automatically generated by sapi_cc_embed_data() Bazel rule

#ifndef SANDBOXED_API_FILE_TOC_H_
#define SANDBOXED_API_FILE_TOC_H_

#include <cstddef>

struct FileToc {
  const char* name;
  const char* data;
  size_t size;
  // Not actually used/computed by sapi_cc_embed_data(), this is for
  // compatibility with legacy code.
  unsigned char md5digest[16];
};

#endif  // SANDBOXED_API_FILE_TOC_H_

#ifndef %1$s
#define %1$s

)";
constexpr const char kHNamespaceBeginFmt[] =
    R"(namespace %s {
)";
constexpr const char kHFileTocDefsFmt[] =
    R"(
const FileToc* %1$s_create();
size_t %1$s_size();
)";
constexpr const char kHNamespaceEndFmt[] =
    R"(
}  // namespace %s
)";
constexpr const char kHFileFooterFmt[] =
    R"(
#endif  // %s
)";

// Format literals for generating the .cc file out of the input files.
constexpr const char kCcFileHeaderFmt[] =
    R"(// Automatically generated by sapi_cc_embed_data() build rule

#include "%s.h"
#include "absl/base/macros.h"
#include "absl/strings/string_view.h"

)";
constexpr const char kCcNamespaceBeginFmt[] =
    R"(namespace %s {

)";
constexpr const char kCcDataBeginFmt[] =
    R"(constexpr absl::string_view %s = {")";
constexpr const char kCcDataEndFmt[] =
    R"(", %d};
)";
constexpr const char kCcFileTocDefsBegin[] =
    R"(
constexpr FileToc kToc[] = {
)";
constexpr const char kCcFileTocDefsEntryFmt[] =
    R"(    {"%1$s", %2$s.data(), %2$s.size(), {}},
)";
constexpr const char kCcFileTocDefsEndFmt[] =
    R"(
    // Terminate array
    {nullptr, nullptr, 0, {}},
};

const FileToc* %1$s_create() {
  return kToc;
}

size_t %1$s_size() {
  return ABSL_ARRAYSIZE(kToc) - 1;
}
)";
constexpr const char kCcNamespaceEndFmt[] =
    R"(
}  // namespace %s
)";

int main(int argc, char* argv[]) {
  if (argc < 7) {
    // We're not aiming for human usability here, as this tool is always run as
    // part of the build.
    absl::FPrintF(stderr,
                  "%s PACKAGE NAME NAMESPACE OUTPUT_H OUTPUT_CC INPUT...\n",
                  argv[0]);
    return EXIT_FAILURE;
  }
  char** arg = &argv[1];

  const char* package = *arg++;
  --argc;
  const char* name = *arg++;
  std::string toc_ident = absl::StrReplaceAll(name, {{"-", "_"}});
  --argc;

  const char* ns = *arg++;
  const bool have_ns = strlen(ns) > 0;
  --argc;

  {  // Write header file first.
    File out_h(*arg++, "wb");
    --argc;
    std::string header_guard = absl::StrFormat("%s_%s_H_", package, toc_ident);
    std::replace_if(
        header_guard.begin(), header_guard.end(),
        [](char c) { return !absl::ascii_isalnum(c); }, '_');
    absl::FPrintF(out_h.get(), kHFileHeaderFmt, header_guard);
    if (have_ns) {
      absl::FPrintF(out_h.get(), kHNamespaceBeginFmt, ns);
    }
    absl::FPrintF(out_h.get(), kHFileTocDefsFmt, toc_ident);
    if (have_ns) {
      absl::FPrintF(out_h.get(), kHNamespaceEndFmt, ns);
    }
    absl::FPrintF(out_h.get(), kHFileFooterFmt, header_guard);
    out_h.Check();
  }

  // Write actual translation unit with the data.
  File out_cc(*arg++, "wb");
  --argc;

  std::string package_name = package;
  if (!package_name.empty()) {
    absl::StrAppend(&package_name, "/");
  }
  absl::StrAppend(&package_name, name);
  absl::FPrintF(out_cc.get(), kCcFileHeaderFmt, package_name);
  if (have_ns) {
    absl::FPrintF(out_cc.get(), kCcNamespaceBeginFmt, ns);
  }

  std::vector<std::pair<std::string, std::string>> toc_entries;
  while (argc > 1) {
    const char* in_filename = *arg++;
    --argc;
    File in(in_filename, "rb");

    std::string basename = sapi::file_util::fileops::Basename(in_filename);
    std::string ident = absl::StrCat("k", basename);
    std::replace_if(
        ident.begin(), ident.end(),
        [](char c) { return !absl::ascii_isalnum(c); }, '_');
    absl::FPrintF(out_cc.get(), kCcDataBeginFmt, ident);
    // Remember identifiers, they are needed in the kToc array.
    toc_entries.emplace_back(std::move(basename), std::move(ident));

    int c;
    while ((c = fgetc(in.get())) != EOF) {
      FWriteCEscapedC(c, out_cc.get());
    }
    in.Check();

    absl::FPrintF(out_cc.get(), kCcDataEndFmt, ftell(in.get()));
  }
  absl::FPrintF(out_cc.get(), kCcFileTocDefsBegin);
  for (const auto& entry : toc_entries) {
    absl::FPrintF(out_cc.get(), kCcFileTocDefsEntryFmt, entry.first,
                  entry.second);
  }
  absl::FPrintF(out_cc.get(), kCcFileTocDefsEndFmt, toc_ident);

  if (have_ns) {
    absl::FPrintF(out_cc.get(), kCcNamespaceEndFmt, ns);
  }

  out_cc.Check();
  return EXIT_SUCCESS;
}
