// Copyright 2018 The Bazel Authors. All rights reserved.
//
// 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.

// Tests for the Windows implementation of the test wrapper.

#include <windows.h>

#include <algorithm>
#include <vector>

#include "gtest/gtest.h"
#include "src/main/cpp/util/path_platform.h"
#include "src/main/native/windows/file.h"
#include "src/main/native/windows/util.h"
#include "src/test/cpp/util/windows_test_util.h"
#include "third_party/ijar/common.h"
#include "third_party/ijar/zip.h"
#include "tools/test/windows/tw.h"

#if !defined(_WIN32) && !defined(__CYGWIN__)
#error("This test should only be run on Windows")
#endif  // !defined(_WIN32) && !defined(__CYGWIN__)

namespace {

using bazel::tools::test_wrapper::FileInfo;
using bazel::tools::test_wrapper::ZipEntryPaths;
using bazel::tools::test_wrapper::testing::TestOnly_AsMixedPath;
using bazel::tools::test_wrapper::testing::TestOnly_CdataEncodeBuffer;
using bazel::tools::test_wrapper::testing::TestOnly_CdataEscapeAndAppend;
using bazel::tools::test_wrapper::testing::TestOnly_CreateTee;
using bazel::tools::test_wrapper::testing::
    TestOnly_CreateUndeclaredOutputsAnnotations;
using bazel::tools::test_wrapper::testing::
    TestOnly_CreateUndeclaredOutputsManifest;
using bazel::tools::test_wrapper::testing::TestOnly_CreateZip;
using bazel::tools::test_wrapper::testing::TestOnly_GetEnv;
using bazel::tools::test_wrapper::testing::TestOnly_GetFileListRelativeTo;
using bazel::tools::test_wrapper::testing::TestOnly_GetMimeType;
using bazel::tools::test_wrapper::testing::TestOnly_ToZipEntryPaths;

class TestWrapperWindowsTest : public ::testing::Test {
 public:
  void TearDown() override {
    blaze_util::DeleteAllUnder(blaze_util::GetTestTmpDirW());
  }
};

void GetTestTmpdir(std::wstring* result, int line) {
  EXPECT_TRUE(TestOnly_GetEnv(L"TEST_TMPDIR", result))
      << __FILE__ << "(" << line << "): assertion failed here";
  ASSERT_GT(result->size(), 0)
      << __FILE__ << "(" << line << "): assertion failed here";
  std::replace(result->begin(), result->end(), L'/', L'\\');
  if (!bazel::windows::HasUncPrefix(result->c_str())) {
    *result = L"\\\\?\\" + *result;
  }
}

void CreateJunction(const std::wstring& name, const std::wstring& target,
                    int line) {
  std::wstring wname;
  std::wstring wtarget;
  EXPECT_TRUE(blaze_util::AsWindowsPath(name, &wname, nullptr))
      << __FILE__ << "(" << line << "): assertion failed here";
  EXPECT_TRUE(blaze_util::AsWindowsPath(target, &wtarget, nullptr))
      << __FILE__ << "(" << line << "): assertion failed here";
  EXPECT_EQ(bazel::windows::CreateJunction(wname, wtarget, nullptr),
            bazel::windows::CreateJunctionResult::kSuccess)
      << __FILE__ << "(" << line << "): assertion failed here";
}

void CompareFileInfos(std::vector<FileInfo> actual,
                      std::vector<FileInfo> expected, int line) {
  ASSERT_EQ(actual.size(), expected.size())
      << __FILE__ << "(" << line << "): assertion failed here";
  std::sort(actual.begin(), actual.end(),
            [](const FileInfo& a, const FileInfo& b) {
              return a.RelativePath() > b.RelativePath();
            });
  std::sort(expected.begin(), expected.end(),
            [](const FileInfo& a, const FileInfo& b) {
              return a.RelativePath() > b.RelativePath();
            });
  for (std::vector<FileInfo>::size_type i = 0; i < actual.size(); ++i) {
    ASSERT_EQ(actual[i].RelativePath(), expected[i].RelativePath())
        << __FILE__ << "(" << line << "): assertion failed here; index: " << i;
    ASSERT_EQ(actual[i].Size(), expected[i].Size())
        << __FILE__ << "(" << line << "): assertion failed here; index: " << i;
    ASSERT_EQ(actual[i].IsDirectory(), expected[i].IsDirectory())
        << __FILE__ << "(" << line << "): assertion failed here; index: " << i;
  }
}

// According to this StackOverflow post [1] `const` modifies what's on its
// *left*, and "const char" is equivalent to "char const".
// `ZipBuilder::EstimateSize`'s argument type is "char const* const*" meaning a
// mutable array (right *) of const pointers (left *) to const data (char).
//
// [1] https://stackoverflow.com/a/8091846/7778502
void CompareZipEntryPaths(char const* const* actual,
                          std::vector<const char*> expected, int line) {
  for (int i = 0; i < expected.size(); ++i) {
    EXPECT_NE(actual[i], nullptr)
        << __FILE__ << "(" << line << "): assertion failed here";
    EXPECT_EQ(std::string(actual[i]), std::string(expected[i]))
        << __FILE__ << "(" << line << "): assertion failed here";
  }
  EXPECT_EQ(actual[expected.size()], nullptr)
      << __FILE__ << "(" << line << "): assertion failed here; value was ("
      << actual[expected.size()] << ")";
}

#define GET_TEST_TMPDIR(result) GetTestTmpdir(result, __LINE__)
#define CREATE_JUNCTION(name, target) CreateJunction(name, target, __LINE__)
#define COMPARE_FILE_INFOS(actual, expected) \
  CompareFileInfos(actual, expected, __LINE__)
#define COMPARE_ZIP_ENTRY_PATHS(actual, expected) \
  CompareZipEntryPaths(actual, expected, __LINE__)

#define TOSTRING1(x) #x
#define TOSTRING(x) TOSTRING1(x)
#define TOWSTRING1(x) L##x
#define TOWSTRING(x) TOWSTRING1(x)
#define WLINE TOWSTRING(TOSTRING(__LINE__))

TEST_F(TestWrapperWindowsTest, TestGetFileListRelativeTo) {
  std::wstring tmpdir;
  GET_TEST_TMPDIR(&tmpdir);

  // Create a directory structure to parse.
  std::wstring root = tmpdir + L"\\tmp" + WLINE;
  EXPECT_TRUE(CreateDirectoryW(root.c_str(), NULL));
  EXPECT_TRUE(CreateDirectoryW((root + L"\\foo").c_str(), NULL));
  EXPECT_TRUE(CreateDirectoryW((root + L"\\foo\\sub").c_str(), NULL));
  EXPECT_TRUE(blaze_util::CreateDummyFile(root + L"\\foo\\sub\\file1", ""));
  EXPECT_TRUE(
      blaze_util::CreateDummyFile(root + L"\\foo\\sub\\file2", "hello"));
  EXPECT_TRUE(blaze_util::CreateDummyFile(root + L"\\foo\\file1", "foo"));
  EXPECT_TRUE(blaze_util::CreateDummyFile(root + L"\\foo\\file2", "foobar"));
  CREATE_JUNCTION(root + L"\\foo\\junc", root + L"\\foo\\sub");

  // Assert traversal of "root" -- should include all files, and also traverse
  // the junction.
  std::vector<FileInfo> actual;
  ASSERT_TRUE(TestOnly_GetFileListRelativeTo(root, &actual));

  std::vector<FileInfo> expected = {FileInfo(L"foo"),
                                    FileInfo(L"foo\\sub"),
                                    FileInfo(L"foo\\sub\\file1", 0),
                                    FileInfo(L"foo\\sub\\file2", 5),
                                    FileInfo(L"foo\\file1", 3),
                                    FileInfo(L"foo\\file2", 6),
                                    FileInfo(L"foo\\junc"),
                                    FileInfo(L"foo\\junc\\file1", 0),
                                    FileInfo(L"foo\\junc\\file2", 5)};
  COMPARE_FILE_INFOS(actual, expected);

  // Assert traversal of "foo" -- should include all files, but now with paths
  // relative to "foo".
  actual.clear();
  ASSERT_TRUE(
      TestOnly_GetFileListRelativeTo((root + L"\\foo").c_str(), &actual));

  expected = {FileInfo(L"sub"),
              FileInfo(L"sub\\file1", 0),
              FileInfo(L"sub\\file2", 5),
              FileInfo(L"file1", 3),
              FileInfo(L"file2", 6),
              FileInfo(L"junc"),
              FileInfo(L"junc\\file1", 0),
              FileInfo(L"junc\\file2", 5)};
  COMPARE_FILE_INFOS(actual, expected);

  // Assert traversal limited to the current directory (depth of 0).
  actual.clear();
  ASSERT_TRUE(TestOnly_GetFileListRelativeTo(root, &actual, 0));
  expected = {FileInfo(L"foo")};
  COMPARE_FILE_INFOS(actual, expected);

  // Assert traversal limited to depth of 1.
  actual.clear();
  ASSERT_TRUE(TestOnly_GetFileListRelativeTo(root, &actual, 1));
  expected = {FileInfo(L"foo"), FileInfo(L"foo\\sub"),
              FileInfo(L"foo\\file1", 3), FileInfo(L"foo\\file2", 6),
              FileInfo(L"foo\\junc")};
  COMPARE_FILE_INFOS(actual, expected);
}

TEST_F(TestWrapperWindowsTest, TestToZipEntryPaths) {
  // Pretend we already acquired a file list. The files don't have to exist.
  std::wstring root = L"c:\\nul\\root";
  std::vector<FileInfo> files = {FileInfo(L"foo"),
                                 FileInfo(L"foo\\sub"),
                                 FileInfo(L"foo\\sub\\file1", 0),
                                 FileInfo(L"foo\\sub\\file2", 5),
                                 FileInfo(L"foo\\file1", 3),
                                 FileInfo(L"foo\\file2", 6),
                                 FileInfo(L"foo\\junc"),
                                 FileInfo(L"foo\\junc\\file1", 0),
                                 FileInfo(L"foo\\junc\\file2", 5)};

  ZipEntryPaths actual;
  ASSERT_TRUE(TestOnly_ToZipEntryPaths(root, files, &actual));
  ASSERT_EQ(actual.Size(), 9);

  std::vector<const char*> expected_abs_paths = {
      "c:/nul/root/foo/",          "c:/nul/root/foo/sub/",
      "c:/nul/root/foo/sub/file1", "c:/nul/root/foo/sub/file2",
      "c:/nul/root/foo/file1",     "c:/nul/root/foo/file2",
      "c:/nul/root/foo/junc/",     "c:/nul/root/foo/junc/file1",
      "c:/nul/root/foo/junc/file2"};
  COMPARE_ZIP_ENTRY_PATHS(actual.AbsPathPtrs(), expected_abs_paths);

  std::vector<const char*> expected_entry_paths = {
      "foo/",      "foo/sub/",  "foo/sub/file1",  "foo/sub/file2", "foo/file1",
      "foo/file2", "foo/junc/", "foo/junc/file1", "foo/junc/file2"};
  COMPARE_ZIP_ENTRY_PATHS(actual.EntryPathPtrs(), expected_entry_paths);
}

TEST_F(TestWrapperWindowsTest, TestToZipEntryPathsLongPathRoot) {
  // Pretend we already acquired a file list. The files don't have to exist.
  // Assert that the root is allowed to have the `\\?\` prefix, but the zip
  // entry paths won't have it.
  std::wstring root = L"\\\\?\\c:\\nul\\unc";
  std::vector<FileInfo> files = {FileInfo(L"foo"),
                                 FileInfo(L"foo\\sub"),
                                 FileInfo(L"foo\\sub\\file1", 0),
                                 FileInfo(L"foo\\sub\\file2", 5),
                                 FileInfo(L"foo\\file1", 3),
                                 FileInfo(L"foo\\file2", 6),
                                 FileInfo(L"foo\\junc"),
                                 FileInfo(L"foo\\junc\\file1", 0),
                                 FileInfo(L"foo\\junc\\file2", 5)};

  ZipEntryPaths actual;
  ASSERT_TRUE(TestOnly_ToZipEntryPaths(root, files, &actual));
  ASSERT_EQ(actual.Size(), 9);

  std::vector<const char*> expected_abs_paths = {
      "c:/nul/unc/foo/",          "c:/nul/unc/foo/sub/",
      "c:/nul/unc/foo/sub/file1", "c:/nul/unc/foo/sub/file2",
      "c:/nul/unc/foo/file1",     "c:/nul/unc/foo/file2",
      "c:/nul/unc/foo/junc/",     "c:/nul/unc/foo/junc/file1",
      "c:/nul/unc/foo/junc/file2"};
  COMPARE_ZIP_ENTRY_PATHS(actual.AbsPathPtrs(), expected_abs_paths);

  std::vector<const char*> expected_entry_paths = {
      "foo/",      "foo/sub/",  "foo/sub/file1",  "foo/sub/file2", "foo/file1",
      "foo/file2", "foo/junc/", "foo/junc/file1", "foo/junc/file2"};
  COMPARE_ZIP_ENTRY_PATHS(actual.EntryPathPtrs(), expected_entry_paths);
}

class InMemoryExtractor : public devtools_ijar::ZipExtractorProcessor {
 public:
  struct ExtractedFile {
    std::string path;
    std::unique_ptr<devtools_ijar::u1[]> data;
    size_t size;
  };

  InMemoryExtractor(std::vector<ExtractedFile>* extracted)
      : extracted_(extracted) {}

  bool Accept(const char* filename, const devtools_ijar::u4 attr) override {
    return true;
  }

  void Process(const char* filename, const devtools_ijar::u4 attr,
               const devtools_ijar::u1* data, const size_t size) override {
    extracted_->push_back({});
    extracted_->back().path = filename;
    if (size > 0) {
      extracted_->back().data.reset(new devtools_ijar::u1[size]);
      memcpy(extracted_->back().data.get(), data, size);
    }
    extracted_->back().size = size;
  }

 private:
  std::vector<ExtractedFile>* extracted_;
};

TEST_F(TestWrapperWindowsTest, TestCreateZip) {
  std::wstring tmpdir;
  GET_TEST_TMPDIR(&tmpdir);

  // Create a directory structure to archive.
  std::wstring root = tmpdir + L"\\tmp" + WLINE;
  EXPECT_TRUE(CreateDirectoryW(root.c_str(), NULL));
  EXPECT_TRUE(CreateDirectoryW((root + L"\\foo").c_str(), NULL));
  EXPECT_TRUE(CreateDirectoryW((root + L"\\foo\\sub").c_str(), NULL));
  EXPECT_TRUE(blaze_util::CreateDummyFile(root + L"\\foo\\sub\\file1", ""));
  EXPECT_TRUE(
      blaze_util::CreateDummyFile(root + L"\\foo\\sub\\file2", "hello"));
  EXPECT_TRUE(blaze_util::CreateDummyFile(root + L"\\foo\\file1", "foo"));
  EXPECT_TRUE(blaze_util::CreateDummyFile(root + L"\\foo\\file2", "foobar"));
  CREATE_JUNCTION(root + L"\\foo\\junc", root + L"\\foo\\sub");

  std::vector<FileInfo> file_list = {FileInfo(L"foo"),
                                     FileInfo(L"foo\\sub"),
                                     FileInfo(L"foo\\sub\\file1", 0),
                                     FileInfo(L"foo\\sub\\file2", 5),
                                     FileInfo(L"foo\\file1", 3),
                                     FileInfo(L"foo\\file2", 6),
                                     FileInfo(L"foo\\junc"),
                                     FileInfo(L"foo\\junc\\file1", 0),
                                     FileInfo(L"foo\\junc\\file2", 5)};

  ASSERT_TRUE(TestOnly_CreateZip(root, file_list, root + L"\\x.zip"));

  std::string zip_path;
  EXPECT_TRUE(TestOnly_AsMixedPath(root + L"\\x.zip", &zip_path));

  // Extract the zip file into memory to verify its contents.
  std::vector<InMemoryExtractor::ExtractedFile> extracted;
  InMemoryExtractor extractor(&extracted);
  std::unique_ptr<devtools_ijar::ZipExtractor> zip(
      devtools_ijar::ZipExtractor::Create(zip_path.c_str(), &extractor));
  EXPECT_NE(zip.get(), nullptr);
  EXPECT_EQ(zip->ProcessAll(), 0);

  EXPECT_EQ(extracted.size(), 9);

  EXPECT_EQ(extracted[0].path, std::string("foo/"));
  EXPECT_EQ(extracted[1].path, std::string("foo/sub/"));
  EXPECT_EQ(extracted[2].path, std::string("foo/sub/file1"));
  EXPECT_EQ(extracted[3].path, std::string("foo/sub/file2"));
  EXPECT_EQ(extracted[4].path, std::string("foo/file1"));
  EXPECT_EQ(extracted[5].path, std::string("foo/file2"));
  EXPECT_EQ(extracted[6].path, std::string("foo/junc/"));
  EXPECT_EQ(extracted[7].path, std::string("foo/junc/file1"));
  EXPECT_EQ(extracted[8].path, std::string("foo/junc/file2"));

  EXPECT_EQ(extracted[0].size, 0);
  EXPECT_EQ(extracted[1].size, 0);
  EXPECT_EQ(extracted[2].size, 0);
  EXPECT_EQ(extracted[3].size, 5);
  EXPECT_EQ(extracted[4].size, 3);
  EXPECT_EQ(extracted[5].size, 6);
  EXPECT_EQ(extracted[6].size, 0);
  EXPECT_EQ(extracted[7].size, 0);
  EXPECT_EQ(extracted[8].size, 5);

  EXPECT_EQ(memcmp(extracted[3].data.get(), "hello", 5), 0);
  EXPECT_EQ(memcmp(extracted[4].data.get(), "foo", 3), 0);
  EXPECT_EQ(memcmp(extracted[5].data.get(), "foobar", 6), 0);
  EXPECT_EQ(memcmp(extracted[8].data.get(), "hello", 5), 0);
}

TEST_F(TestWrapperWindowsTest, TestGetMimeType) {
  // As of 2018-11-08, TestOnly_GetMimeType looks up the MIME type from the
  // registry under `HKCR\<extension>\Content Type`, e.g.
  // 'HKCR\.bmp\Content Type`.
  // Bazel's CI machines run Windows Server 2016 Core, whose registry contains
  // the Content Type for .ico and .bmp but not for common types such as .txt,
  // hence the file types we choose to test for.
  EXPECT_EQ(TestOnly_GetMimeType("foo.ico"), std::string("image/x-icon"));
  EXPECT_EQ(TestOnly_GetMimeType("foo.bmp"), std::string("image/bmp"));
  EXPECT_EQ(TestOnly_GetMimeType("foo"),
            std::string("application/octet-stream"));
}

TEST_F(TestWrapperWindowsTest, TestUndeclaredOutputsManifest) {
  // Pretend we already acquired a file list. The files don't have to exist.
  // Assert that the root is allowed to have the `\\?\` prefix, but the zip
  // entry paths won't have it.
  std::vector<FileInfo> files = {FileInfo(L"foo"),
                                 FileInfo(L"foo\\sub"),
                                 FileInfo(L"foo\\sub\\file1.ico", 0),
                                 FileInfo(L"foo\\sub\\file2.bmp", 5),
                                 FileInfo(L"foo\\file2", 6)};

  std::string content;
  ASSERT_TRUE(TestOnly_CreateUndeclaredOutputsManifest(files, &content));
  ASSERT_EQ(content, std::string("foo/sub/file1.ico\t0\timage/x-icon\n"
                                 "foo/sub/file2.bmp\t5\timage/bmp\n"
                                 "foo/file2\t6\tapplication/octet-stream\n"));
}

TEST_F(TestWrapperWindowsTest, TestCreateUndeclaredOutputsAnnotations) {
  std::wstring tmpdir;
  GET_TEST_TMPDIR(&tmpdir);

  // Create a directory structure to parse.
  std::wstring root = tmpdir + L"\\tmp" + WLINE;
  EXPECT_TRUE(CreateDirectoryW(root.c_str(), NULL));
  EXPECT_TRUE(CreateDirectoryW((root + L"\\foo").c_str(), NULL));
  EXPECT_TRUE(CreateDirectoryW((root + L"\\bar.part").c_str(), NULL));
  EXPECT_TRUE(blaze_util::CreateDummyFile(root + L"\\a.part", "Hello a"));
  EXPECT_TRUE(blaze_util::CreateDummyFile(root + L"\\b.txt", "Hello b"));
  EXPECT_TRUE(blaze_util::CreateDummyFile(root + L"\\c.part", "Hello c"));
  EXPECT_TRUE(blaze_util::CreateDummyFile(root + L"\\foo\\d.part", "Hello d"));
  EXPECT_TRUE(
      blaze_util::CreateDummyFile(root + L"\\bar.part\\e.part", "Hello e"));

  std::wstring annot = root + L"\\x.annot";
  ASSERT_TRUE(TestOnly_CreateUndeclaredOutputsAnnotations(root, annot));

  HANDLE h = CreateFileW(annot.c_str(), GENERIC_READ,
                         FILE_SHARE_READ | FILE_SHARE_DELETE, NULL,
                         OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  ASSERT_NE(h, INVALID_HANDLE_VALUE);
  char content[100];
  DWORD read;
  bool success = ReadFile(h, content, 100, &read, NULL) != FALSE;
  CloseHandle(h);
  EXPECT_TRUE(success);
  ASSERT_EQ(std::string(content, read), std::string("Hello aHello c"));
}

TEST_F(TestWrapperWindowsTest, TestTee) {
  HANDLE read1_h, write1_h;
  EXPECT_TRUE(CreatePipe(&read1_h, &write1_h, NULL, 0));
  bazel::windows::AutoHandle read1(read1_h), write1(write1_h);
  HANDLE read2_h, write2_h;
  EXPECT_TRUE(CreatePipe(&read2_h, &write2_h, NULL, 0));
  bazel::windows::AutoHandle read2(read2_h), write2(write2_h);
  HANDLE read3_h, write3_h;
  EXPECT_TRUE(CreatePipe(&read3_h, &write3_h, NULL, 0));
  bazel::windows::AutoHandle read3(read3_h), write3(write3_h);

  std::unique_ptr<bazel::tools::test_wrapper::Tee> tee;
  EXPECT_TRUE(TestOnly_CreateTee(&read1, &write2, &write3, &tee));

  DWORD written, read;
  char content[100];

  EXPECT_TRUE(WriteFile(write1, "hello", 5, &written, NULL));
  EXPECT_EQ(written, 5);
  EXPECT_TRUE(ReadFile(read2, content, 100, &read, NULL));
  EXPECT_EQ(read, 5);
  EXPECT_EQ(std::string(content, read), "hello");
  EXPECT_TRUE(ReadFile(read3, content, 100, &read, NULL));
  EXPECT_EQ(read, 5);
  EXPECT_EQ(std::string(content, read), "hello");

  EXPECT_TRUE(WriteFile(write1, "foo", 3, &written, NULL));
  EXPECT_EQ(written, 3);
  EXPECT_TRUE(ReadFile(read2, content, 100, &read, NULL));
  EXPECT_EQ(read, 3);
  EXPECT_EQ(std::string(content, read), "foo");
  EXPECT_TRUE(ReadFile(read3, content, 100, &read, NULL));
  EXPECT_EQ(read, 3);
  EXPECT_EQ(std::string(content, read), "foo");

  write1 = INVALID_HANDLE_VALUE;  // closes handle so the Tee thread can exit
}

void AssertCdataEncodeBuffer(
    int line, const std::string& input, const std::string& expected_output,
    const std::vector<DWORD>& expected_cdata_end_indices) {
  ASSERT_EQ(input.size(), expected_output.size());

  std::unique_ptr<uint8_t[]> mutable_buffer(new uint8_t[input.size()]);
  memcpy(mutable_buffer.get(), input.c_str(), input.size());

  std::vector<DWORD> cdata_ends;
  EXPECT_TRUE(TestOnly_CdataEncodeBuffer(mutable_buffer.get(), input.size(),
                                         &cdata_ends));
  for (int i = 0; i < input.size(); ++i) {
    EXPECT_EQ(mutable_buffer[i], static_cast<uint8_t>(expected_output[i]))
        << "FAILED(in line " << line << "): mismatch at index " << i;
  }

  EXPECT_EQ(cdata_ends, expected_cdata_end_indices);
}

TEST_F(TestWrapperWindowsTest, TestCdataEncodeBufferCdataEndings) {
  AssertCdataEncodeBuffer(
      __LINE__,
      // === Input ===
      // CDATA end sequence, followed by some arbitrary octet.
      "]]>x"
      // CDATA end sequence twice.
      "]]>]]>x"
      // CDATA end sequence at the end of the string.
      "]]>",

      // === Expected output ===
      // "]]>" sequences are left alone but their position is stored in
      "]]>x"
      "]]>]]>x"
      "]]>",

      // === Expected CDATA end positions ===
      {0, 4, 7, 11});
}

TEST_F(TestWrapperWindowsTest, TestCdataEncodeBufferSingleOctets) {
  AssertCdataEncodeBuffer(__LINE__,
                          // === Input ===
                          // Legal single-octets.
                          "AB\x9\xA\xD\x20\x7F"
                          // Illegal single-octets.
                          "\x8\xB\xC\x1F\x80\xFF"
                          "x",

                          // === Expected output ===
                          // Legal single-octets.
                          "AB\x9\xA\xD\x20\x7F"
                          // Illegal single-octets.
                          "??????"
                          "x",
                          {});
}

TEST_F(TestWrapperWindowsTest, TestCdataEncodeBufferDoubleOctets) {
  // Legal range: [\xc0-\xdf][\x80-\xbf]
  AssertCdataEncodeBuffer(
      __LINE__,
      "x"
      // Legal double-octet sequences.
      "\xC0\x80"
      "\xDE\xB0"
      "\xDF\xBF"
      // Illegal double-octet sequences, first octet is bad, second is good.
      "\xBF\x80"  // each are matched as single bad octets
      "\xE0\x80"
      // Illegal double-octet sequences, first octet is good, second is bad.
      "\xC0\x7F"  // 0x7F is legal as a single-octet, retained
      "\xDF\xC0"  // 0xC0 starts a legal two-octet sequence...
      // Illegal double-octet sequences, both octets bad.
      "\xBF\xFF"  // ...and 0xBF finishes that sequence
      "x",

      // === Expected output ===
      "x"
      // Legal double-octet sequences.
      "\xC0\x80"
      "\xDE\xB0"
      "\xDF\xBF"
      // Illegal double-octet sequences, first octet is bad, second is good.
      "??"
      "??"
      // Illegal double-octet sequences, first octet is good, second is bad.
      "?\x7F"  // 0x7F is legal as a single-octet, retained
      "?\xC0"  // 0xC0 starts a legal two-octet sequence...
      // Illegal double-octet sequences, both octets bad.
      "\xBF?"  // ...and 0xBF finishes that sequence
      "x",
      {});
}

TEST_F(TestWrapperWindowsTest, TestCdataEscapeAndAppend) {
  std::wstring tmpdir;
  GET_TEST_TMPDIR(&tmpdir);

  // Create a directory structure to parse.
  std::wstring root = tmpdir + L"\\tmp" + WLINE;
  EXPECT_TRUE(CreateDirectoryW(root.c_str(), NULL));
  EXPECT_TRUE(blaze_util::CreateDummyFile(root + L"\\a",
                                          "AB\xA\xC\xD"
                                          "]]>"
                                          "]]]>"
                                          "\xC0\x80"
                                          "a"
                                          "\xED\x9F\xBF"
                                          "b"
                                          "\xEF\xBF\xB0"
                                          "c"
                                          "\xF7\xB0\x80\x81"
                                          "d"
                                          "]]>"));

  ASSERT_TRUE(TestOnly_CdataEscapeAndAppend(root + L"\\a", root + L"\\b"));

  HANDLE h = CreateFileW((root + L"\\b").c_str(), GENERIC_READ,
                         FILE_SHARE_READ | FILE_SHARE_DELETE, NULL,
                         OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  ASSERT_NE(h, INVALID_HANDLE_VALUE);
  char content[200];
  DWORD read;
  bool success = ReadFile(h, content, 200, &read, NULL) != FALSE;
  CloseHandle(h);
  EXPECT_TRUE(success);

  ASSERT_EQ(std::string(content, read),
            "AB\xA?\xD"
            "]]>]]&gt;<![CDATA["
            "]]]>]]&gt;<![CDATA["
            "\xC0\x80"
            "a"
            "\xED\x9F\xBF"
            "b"
            "\xEF\xBF\xB0"
            "c"
            "\xF7\xB0\x80\x81"
            "d"
            "]]>]]&gt;<![CDATA[");
}

}  // namespace
