// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/services/unzip/public/cpp/unzip.h"

#include <cstdint>
#include <string_view>
#include <utility>

#include "base/base_paths.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/callback_helpers.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "components/services/unzip/unzipper_impl.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace unzip {
namespace {

base::FilePath GetArchivePath(std::string_view archive_name) {
  base::FilePath path;
  EXPECT_TRUE(base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &path));
  return path.AppendASCII("components")
      .AppendASCII("test")
      .AppendASCII("data")
      .AppendASCII("unzip_service")
      .AppendASCII(archive_name);
}

// Counts the number of files under |dir|. If |some_files_empty| is not null,
// sets it to true if at least one of the files is empty.
int CountFiles(const base::FilePath& dir, bool* some_files_empty = nullptr) {
  int file_count = 0;
  base::FileEnumerator file_enumerator(dir, /*recursive=*/true,
                                       base::FileEnumerator::FILES);
  for (base::FilePath path = file_enumerator.Next(); !path.empty();
       path = file_enumerator.Next()) {
    std::optional<int64_t> file_size = base::GetFileSize(path);

    if (some_files_empty != nullptr && file_size.has_value() &&
        file_size.value() == 0) {
      *some_files_empty = true;
      some_files_empty = nullptr;  // So we don't check files again.
    }

    file_count++;
  }

  return file_count;
}

class UnzipTest : public testing::Test {
 public:
  UnzipTest() = default;
  ~UnzipTest() override = default;

  // Unzips |zip_file| into |output_dir| and returns true if the unzip was
  // successful. Only extract files for which |filter_callback| returns true, if
  // it is provided.
  bool DoUnzip(const base::FilePath& zip_file,
               const base::FilePath& output_dir,
               UnzipFilterCallback filter_callback = unzip::AllContents()) {
    mojo::PendingRemote<mojom::Unzipper> unzipper;
    receivers_.Add(&unzipper_, unzipper.InitWithNewPipeAndPassReceiver());

    base::RunLoop run_loop;
    bool result = false;

    Unzip(std::move(unzipper), zip_file, output_dir,
          unzip::mojom::UnzipOptions::New(), std::move(filter_callback),
          base::DoNothing(), base::BindLambdaForTesting([&](bool success) {
            result = success;
            run_loop.QuitClosure().Run();
          }));

    run_loop.Run();
    return result;
  }

  // Unzips |zip_file| into |output_dir| and returns true if the unzip was
  // successful. |options| hosts parameters for the unpack.
  bool DoUnzipWithOptions(const base::FilePath& zip_file,
                          const base::FilePath& output_dir,
                          mojom::UnzipOptionsPtr options) {
    mojo::PendingRemote<mojom::Unzipper> unzipper;
    receivers_.Add(&unzipper_, unzipper.InitWithNewPipeAndPassReceiver());

    base::RunLoop run_loop;
    bool result = false;

    Unzip(std::move(unzipper), zip_file, output_dir, std::move(options),
          AllContents(), base::BindLambdaForTesting([&](uint64_t) {}),
          base::BindLambdaForTesting([&](bool success) {
            result = success;
            run_loop.QuitClosure().Run();
          }));

    run_loop.Run();
    return result;
  }

  Encoding DoDetectEncoding(const base::FilePath& zip_file) {
    mojo::PendingRemote<mojom::Unzipper> unzipper;
    receivers_.Add(&unzipper_, unzipper.InitWithNewPipeAndPassReceiver());

    base::RunLoop run_loop;
    Encoding result = UNKNOWN_ENCODING;

    DetectEncodingCallback result_callback =
        base::BindLambdaForTesting([&](const Encoding encoding) {
          result = encoding;
          run_loop.QuitClosure().Run();
        });

    DetectEncoding(std::move(unzipper), zip_file, std::move(result_callback));
    run_loop.Run();
    return result;
  }

  mojom::Info DoGetExtractedInfo(const base::FilePath& zip_file) {
    mojo::PendingRemote<mojom::Unzipper> unzipper;
    receivers_.Add(&unzipper_, unzipper.InitWithNewPipeAndPassReceiver());

    base::RunLoop run_loop;
    mojom::Info result;

    GetExtractedInfoCallback result_callback =
        base::BindLambdaForTesting([&](mojom::InfoPtr info) {
          result = *info;
          run_loop.QuitClosure().Run();
        });

    GetExtractedInfo(std::move(unzipper), zip_file, std::move(result_callback));
    run_loop.Run();
    return result;
  }

  uint64_t DoGetProgressSize(const base::FilePath& zip_file,
                             const base::FilePath& output_dir) {
    mojo::PendingRemote<mojom::Unzipper> unzipper;
    receivers_.Add(&unzipper_, unzipper.InitWithNewPipeAndPassReceiver());

    base::RunLoop run_loop;
    uint64_t bytes = 0;

    Unzip(std::move(unzipper), zip_file, output_dir,
          unzip::mojom::UnzipOptions::New("auto", ""), AllContents(),
          base::BindLambdaForTesting([&](uint64_t written_bytes) {
            bytes = written_bytes;
            run_loop.QuitClosure().Run();
          }),
          base::BindLambdaForTesting(
              [&](bool) { run_loop.QuitClosure().Run(); }));

    run_loop.Run();
    return bytes;
  }

  bool DoDecodeXz(const base::FilePath& in_file,
                  const base::FilePath& out_file) {
    mojo::PendingRemote<mojom::Unzipper> unzipper;
    receivers_.Add(&unzipper_, unzipper.InitWithNewPipeAndPassReceiver());
    bool result = false;
    base::RunLoop run_loop;
    DecodeXz(std::move(unzipper), in_file, out_file,
             base::BindLambdaForTesting([&](bool success) {
               result = success;
               run_loop.Quit();
             }));
    run_loop.Run();
    return result;
  }

 protected:
  void SetUp() override {
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
    unzip_dir_ = temp_dir_.GetPath();
  }

  base::test::TaskEnvironment task_environment_;

  base::ScopedTempDir temp_dir_;
  base::FilePath unzip_dir_;

  unzip::UnzipperImpl unzipper_;
  mojo::ReceiverSet<mojom::Unzipper> receivers_;
};

TEST_F(UnzipTest, UnzipAbsentArchive) {
  EXPECT_FALSE(DoUnzip(GetArchivePath("absent_archive.zip"), unzip_dir_));

  // No files should have been extracted.
  EXPECT_EQ(0, CountFiles(unzip_dir_));
}

TEST_F(UnzipTest, UnzipBadArchive) {
  EXPECT_FALSE(DoUnzip(GetArchivePath("bad_archive.zip"), unzip_dir_));

  // No files should have been extracted.
  EXPECT_EQ(0, CountFiles(unzip_dir_));
}

TEST_F(UnzipTest, UnzipWrongCrc) {
  EXPECT_FALSE(DoUnzip(GetArchivePath("Wrong CRC.zip"), unzip_dir_));

  // No files should have been extracted.
  EXPECT_EQ(0, CountFiles(unzip_dir_));
}

TEST_F(UnzipTest, UnzipGoodArchive) {
  EXPECT_TRUE(DoUnzip(GetArchivePath("good_archive.zip"), unzip_dir_));

  // Sanity check that the right number of files have been extracted and that
  // they are not empty.
  bool some_files_empty = false;
  EXPECT_EQ(8, CountFiles(unzip_dir_, &some_files_empty));
  EXPECT_FALSE(some_files_empty);
}

TEST_F(UnzipTest, UnzipGoodArchiveWithExtraBytes) {
  EXPECT_TRUE(DoUnzip(GetArchivePath("good_archive_prefixed.zip"), unzip_dir_));
  bool some_files_empty = false;
  EXPECT_EQ(8, CountFiles(unzip_dir_, &some_files_empty));
  EXPECT_FALSE(some_files_empty);
}

TEST_F(UnzipTest, UnzipBadArchiveHang) {
  // Don't hang trying to open this bad archive.
  EXPECT_FALSE(DoUnzip(GetArchivePath("bad_archive_hang.zip"), unzip_dir_));
  EXPECT_EQ(0, CountFiles(unzip_dir_));
}

TEST_F(UnzipTest, UnzipZip64) {
  EXPECT_TRUE(DoUnzip(GetArchivePath("good_zip64.zip"), unzip_dir_));
  bool some_files_empty = false;
  EXPECT_EQ(1, CountFiles(unzip_dir_, &some_files_empty));
  EXPECT_FALSE(some_files_empty);
}

TEST_F(UnzipTest, UnzipZip64WithExtraBytes) {
  EXPECT_TRUE(DoUnzip(GetArchivePath("good_zip64_prefixed.zip"), unzip_dir_));
  bool some_files_empty = false;
  EXPECT_EQ(1, CountFiles(unzip_dir_, &some_files_empty));
  EXPECT_FALSE(some_files_empty);
}

TEST_F(UnzipTest, UnzipWithFilter) {
  EXPECT_TRUE(DoUnzip(GetArchivePath("good_archive.zip"), unzip_dir_,
                      base::BindRepeating([](const base::FilePath& path) {
                        return path.MatchesExtension(FILE_PATH_LITERAL(".txt"));
                      })));

  // It should only have kept the 2 text files from the archive.
  bool some_files_empty = false;
  EXPECT_EQ(2, CountFiles(unzip_dir_, &some_files_empty));
  EXPECT_FALSE(some_files_empty);
}

// Checks that the Unzipper service does not overwrite an existing file.
TEST_F(UnzipTest, DuplicatedNames) {
  EXPECT_FALSE(DoUnzip(GetArchivePath("Duplicate Filenames.zip"), unzip_dir_));

  // Check that the first file was correctly extracted.
  std::string content;
  EXPECT_TRUE(
      base::ReadFileToString(unzip_dir_.AppendASCII("Simple.txt"), &content));
  EXPECT_EQ("Simple 1\n", content);

  // Check that no other file was extracted.
  EXPECT_EQ(1, CountFiles(unzip_dir_));
}

TEST_F(UnzipTest, DetectEncodingAbsentArchive) {
  EXPECT_EQ(UNKNOWN_ENCODING,
            DoDetectEncoding(GetArchivePath("absent_archive.zip")));
}

TEST_F(UnzipTest, DetectEncodingBadArchive) {
  EXPECT_EQ(UNKNOWN_ENCODING,
            DoDetectEncoding(GetArchivePath("bad_archive.zip")));
}

TEST_F(UnzipTest, DetectEncodingAscii) {
  EXPECT_EQ(Encoding::ASCII_7BIT,
            DoDetectEncoding(GetArchivePath("good_archive.zip")));
}

// See https://crbug.com/903664
TEST_F(UnzipTest, DetectEncodingUtf8) {
  EXPECT_EQ(Encoding::UTF8,
            DoDetectEncoding(GetArchivePath("UTF8 (Bug 903664).zip")));
}

// See https://crbug.com/1287893
TEST_F(UnzipTest, DetectEncodingSjis) {
  for (const std::string_view name : {
           "SJIS 00.zip",
           "SJIS 01.zip",
           "SJIS 02.zip",
           "SJIS 03.zip",
           "SJIS 04.zip",
           "SJIS 05.zip",
           "SJIS 06.zip",
           "SJIS 07.zip",
           "SJIS 08.zip",
           "SJIS 09.zip",
           "SJIS 10.zip",
           "SJIS 11.zip",
           "SJIS 12.zip",
           "SJIS 13.zip",
       }) {
    EXPECT_EQ(Encoding::JAPANESE_SHIFT_JIS,
              DoDetectEncoding(GetArchivePath(name)));
  }
}

TEST_F(UnzipTest, GetExtractedSize) {
  mojom::Info result = DoGetExtractedInfo(GetArchivePath("good_archive.zip"));
  EXPECT_TRUE(result.size_is_valid);
  EXPECT_EQ(137, static_cast<int64_t>(result.size));
}

TEST_F(UnzipTest, GetExtractedSizeBrokenArchive) {
  mojom::Info result = DoGetExtractedInfo(GetArchivePath("bad_archive.zip"));
  EXPECT_FALSE(result.size_is_valid);
}

TEST_F(UnzipTest, UnzipWithOptions) {
  unzip::mojom::UnzipOptionsPtr options =
      unzip::mojom::UnzipOptions::New("auto", "");
  EXPECT_TRUE(DoUnzipWithOptions(GetArchivePath("good_archive.zip"), unzip_dir_,
                                 std::move(options)));

  // 8 files should have been extracted.
  bool some_files_empty = false;
  EXPECT_EQ(8, CountFiles(unzip_dir_, &some_files_empty));
}

TEST_F(UnzipTest, GetExtractedProgressSize) {
  uint64_t result =
      DoGetProgressSize(GetArchivePath("good_archive.zip"), unzip_dir_);
  // Check: first file extracted is 23 bytes long.
  EXPECT_EQ(23ul, result);
}

TEST_F(UnzipTest, ExtractEncrypted) {
  mojom::Info result =
      DoGetExtractedInfo(GetArchivePath("encrypted_archive.zip"));
  EXPECT_TRUE(result.is_encrypted);

  unzip::mojom::UnzipOptionsPtr options =
      unzip::mojom::UnzipOptions::New("auto", "fake_password");
  EXPECT_TRUE(DoUnzipWithOptions(GetArchivePath("encrypted_archive.zip"),
                                 unzip_dir_, std::move(options)));

  // Check: 5 files should have been extracted.
  bool some_files_empty = false;
  EXPECT_EQ(5, CountFiles(unzip_dir_, &some_files_empty));
}

TEST_F(UnzipTest, DetectAESArchive) {
  mojom::Info result =
      DoGetExtractedInfo(GetArchivePath("DifferentEncryptions.zip"));
  EXPECT_TRUE(result.uses_aes_encryption);
}

TEST_F(UnzipTest, DecodeXz_Success) {
  base::FilePath out = unzip_dir_.AppendASCII("out");
  ASSERT_TRUE(DoDecodeXz(GetArchivePath("file1.xz"), out));
  EXPECT_EQ(ReadFileToBytes(out), ReadFileToBytes(GetArchivePath("file1")));
  ASSERT_TRUE(base::DeleteFile(out));
  ASSERT_TRUE(DoDecodeXz(GetArchivePath("bd646.xz"), out));
  EXPECT_EQ(ReadFileToBytes(out), ReadFileToBytes(GetArchivePath("bd646")));
}

TEST_F(UnzipTest, DecodeXz_DontReplaceExistingOutfile) {
  base::FilePath out = unzip_dir_.AppendASCII("out");
  ASSERT_TRUE(base::WriteFile(out, "data"));
  ASSERT_FALSE(DoDecodeXz(GetArchivePath("file1.xz"), out));
  std::string out_contents;
  ASSERT_TRUE(base::ReadFileToString(out, &out_contents));
  EXPECT_EQ(out_contents, "data");
}

TEST_F(UnzipTest, DecodeXz_MissingInput) {
  base::FilePath out = unzip_dir_.AppendASCII("out");
  EXPECT_FALSE(DoDecodeXz(GetArchivePath("doesnotexist"), out));
  EXPECT_FALSE(base::PathExists(out));
}

TEST_F(UnzipTest, DecodeXz_BadFormat) {
  base::FilePath out = unzip_dir_.AppendASCII("out");
  EXPECT_FALSE(DoDecodeXz(GetArchivePath("file1"), out));
  EXPECT_FALSE(base::PathExists(out));
}

TEST_F(UnzipTest, DecodeXz_Cancel) {
  base::FilePath out = unzip_dir_.AppendASCII("out");
  base::FilePath in = GetArchivePath("file1.xz");
  mojo::PendingRemote<mojom::Unzipper> unzipper;
  receivers_.Add(&unzipper_, unzipper.InitWithNewPipeAndPassReceiver());
  base::RunLoop run_loop;
  DecodeXz(std::move(unzipper), in, out,
           base::BindLambdaForTesting([&](bool success) {
             EXPECT_FALSE(success);
             run_loop.Quit();
           }))
      .Run();
  run_loop.Run();
  EXPECT_FALSE(base::PathExists(out));
}

TEST_F(UnzipTest, DecodeXz_CancelAfterReturn) {
  base::FilePath out = unzip_dir_.AppendASCII("out");
  base::FilePath in = GetArchivePath("file1.xz");
  base::OnceClosure cancellation;
  int call_count = 0;
  {
    mojo::PendingRemote<mojom::Unzipper> unzipper;
    receivers_.Add(&unzipper_, unzipper.InitWithNewPipeAndPassReceiver());
    base::RunLoop run_loop;
    cancellation = DecodeXz(std::move(unzipper), in, out,
                            base::BindLambdaForTesting([&](bool success) {
                              EXPECT_TRUE(success);
                              ++call_count;
                              run_loop.Quit();
                            }));
    run_loop.Run();
    EXPECT_EQ(call_count, 1);
  }
  std::move(cancellation).Run();
  base::RunLoop run_loop;
  run_loop.RunUntilIdle();
  EXPECT_EQ(call_count, 1);
}

TEST_F(UnzipTest, UnzipZip64InZip64) {
  // Unzip outer ZIP.
  EXPECT_TRUE(DoUnzip(GetArchivePath("zip64_in_zip64.zip"), unzip_dir_));
  base::FilePath extracted_file = unzip_dir_.Append(FILE_PATH_LITERAL("-"));

  // Confirm the extracted file is a ZIP. If the inner Zip64 is mistakenly
  // unzipped, this will fail.
  std::optional<std::vector<uint8_t>> contents =
      base::ReadFileToBytes(extracted_file);
  ASSERT_TRUE(contents);
  ASSERT_TRUE(contents->size() >= 4);
  ASSERT_EQ((*contents)[0], 0x50);
  ASSERT_EQ((*contents)[1], 0x4b);
  ASSERT_EQ((*contents)[2], 0x03);
  ASSERT_EQ((*contents)[3], 0x04);

  // Extract the inner ZIP.
  base::FilePath inner_unzip_dir =
      unzip_dir_.Append(FILE_PATH_LITERAL("inner_out"));
  ASSERT_TRUE(base::CreateDirectory(inner_unzip_dir));
  EXPECT_TRUE(DoUnzip(extracted_file, inner_unzip_dir));

  // Confirm the inner file contents.
  std::string inner_contents;
  ASSERT_TRUE(base::ReadFileToString(
      inner_unzip_dir.Append(FILE_PATH_LITERAL("-")), &inner_contents));
  EXPECT_EQ(inner_contents, "innermost\n");
}

TEST_F(UnzipTest, UnzipZip64InZip) {
  // Unzip outer ZIP.
  EXPECT_TRUE(DoUnzip(GetArchivePath("zip64_in_zip.zip"), unzip_dir_));
  base::FilePath extracted_file =
      unzip_dir_.Append(FILE_PATH_LITERAL("zip64.zip"));

  // Confirm the extracted file.
  std::optional<std::vector<uint8_t>> contents =
      base::ReadFileToBytes(extracted_file);
  // If "-" exists but not "zip64.zip", the inner zip64 was unzipped.
  ASSERT_TRUE(contents) << "Failed to read `zip64.zip`; does `-` exist? "
                        << base::PathExists(
                               unzip_dir_.Append(FILE_PATH_LITERAL("-")));
  ASSERT_TRUE(contents->size() >= 4);

  // Note: If this is instead 'i', the inner ZIP was unzipped.
  ASSERT_EQ((*contents)[0], 0x50);
  ASSERT_EQ((*contents)[1], 0x4b);
  ASSERT_EQ((*contents)[2], 0x03);
  ASSERT_EQ((*contents)[3], 0x04);

  // Extract the inner ZIP.
  base::FilePath inner_unzip_dir =
      unzip_dir_.Append(FILE_PATH_LITERAL("inner_out"));
  ASSERT_TRUE(base::CreateDirectory(inner_unzip_dir));
  EXPECT_TRUE(DoUnzip(extracted_file, inner_unzip_dir));

  // Confirm the inner file contents.
  std::string inner_contents;
  ASSERT_TRUE(base::ReadFileToString(
      inner_unzip_dir.Append(FILE_PATH_LITERAL("-")), &inner_contents));
  EXPECT_EQ(inner_contents, "innermost\n");
}

// zip_with_two_interesting_zip64s.zip is a non-zip64 zip that contains two
// zip64s. The second zip64 has the interesting property that the "relative
// offset" in its zip64 EoCDL, if interpreted as a relative offset in the
// outer zip, happens to point at the (well-formed) zip64 EoCDR of the first
// zip64. A parser that simply scans for zip64 EoCDLs is likely to get
// confused by this.
TEST_F(UnzipTest, UnzipZipWithInterestingZip64s) {
  // Unzip outer ZIP.
  EXPECT_TRUE(DoUnzip(GetArchivePath("zip_with_two_interesting_zip64s.zip"),
                      unzip_dir_));
  // Two inner ZIPs. If there's only one, it's probably the contents of the
  // inner ZIP.
  EXPECT_EQ(2, CountFiles(unzip_dir_));
}

}  // namespace
}  // namespace unzip
